资讯动态

从seccomp到ORW:CTF沙盒pwn题利用链全解析

发布时间:2026/10/20 17:45:32 来源:尧图企业网站定制
做CTF pwn题这两年最怕遇到的不是堆漏洞也不是内核题而是那种明明已经拿到shell结果执行不了system(/bin/sh)的沙盒题。沙盒机制简单理解就是程序启动后挂一层系统调用过滤把execve、fork这些危险操作直接掐掉逼着你只能按它允许的open、read、write白名单走。这篇内容就围绕pwn里最常见的seccomp沙盒展开把底层原理、分析工具、利用链设计和个人踩过的坑一次性讲明白。适合正在刷沙盒题的CTF选手、做Linux安全开发的朋友也适合刚接触pwn、想搞懂“为什么拿到shell却执行不了命令”的入门读者。说到底沙盒题比普通pwn题多出来的不是难度而是约束条件。它强迫你在给定规则内找出路这个过程其实特别训练对系统调用、ROP链和shellcode的理解。下面我从出题思路讲起一直聊到工具实操和排错。1. 沙盒机制在pwn题中的角色定位1.1 为什么pwn题要套沙盒普通pwn题的最终目标通常很简单拿到任意代码执行然后直接getshell。一旦能执行shellflag基本就到手了。但这会带来一个问题——漏洞利用的路径过于同质化出一道题最后全在打shellcode和ROP打system。出题人为了增加题目的层次感往往会引入沙盒机制最典型的就是seccomp-bpf系统调用过滤。沙盒的本质是限制进程能做的系统调用集合。常见模式有白名单和黑名单两种白名单只放行open、read、write、exit这类“安全”调用其余全部拒绝黑名单则只屏蔽execve、fork、ptrace等高危调用。无论是哪种最终效果都是让你丢掉“执行命令”这条路只能通过文件读写把flag带出来。这是pwn题目沙盒最常见的用法也叫“flag直接当文件程序不允许拉起shell”的限定型关卡。对学习者来说沙盒机制强迫你换一种思路看漏洞利用不再只盯着system地址和/bin/sh字符串而是把注意力转移到syscall级别重新组织输入输出。这也是为什么我觉得沙盒题值得专门花时间拆解——它逼你理解程序在操作系统层面到底能干什么、不能干什么。1.2 常见沙盒形式与快速识别pwn题里的沙盒不全是seccomp常见的还有这么几类seccomp / seccomp-bpf最常见通过内核BPF规则过滤系统调用规则加载后对进程及其后代生效。CTF里九成沙盒题都是这个。chroot 文件系统隔离限定进程能看到的文件系统让你找不到flag文件但不会直接限制系统调用。这类通常要结合目录逃逸或文件描述符传递来打。容器/namespace 隔离CTF里相对少见一般是题目运行在Docker等容器内靠权限隔离而不是规则过滤。静态编译自定义syscall wrapper有些题不用内核沙盒而是自己封装系统调用入口在用户态做过滤这属于模拟沙盒。拿到一个题快速判断有没有沙盒、挂的是什么沙盒是解题的第一步。我通常先做三件事用checksec看保护确认NX、PIE、Canary这些基础状态。用seccomp-tools dump ./程序名直接导出seccomp规则这是最直接的判断方式。把程序丢进gdb用catch syscall或者直接看启动流程检查是否调用了prctl(PR_SET_SECCOMP, ...)或seccomp()。如果dump结果里没有execve或者只有open/read/write这类白名单那基本可以确定这是一道标准沙盒题。接下来要做的就是根据规则设计利用链而不是继续找shell。2. 核心技术点seccomp与系统调用过滤原理2.1 seccomp的底层工作方式seccomp全称是Secure Computing Mode它工作在系统调用这一层。进程发起系统调用时内核在处理之前会先经过seccomp过滤器根据过滤器返回的决定来决定放行还是拒绝。严格模式下进程只能使用read、write、_exit和sigreturn这四个系统调用任何其他调用都会导致进程被SIGKILL杀死。过滤模式下内核允许用户挂载一段BPF程序BPF程序对每个系统调用做检查返回ALLOW或ERRNO/KILL。这段BPF程序的挂载方式很关键。常见写法是通过prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, prog)也有的程序用seccomp()系统调用带SECCOMP_SET_MODE_FILTER参数。从流程上看程序会在进入正式逻辑前、或者在fork之后给子进程挂规则不同挂载时机对利用路径影响很大。一个容易忽略但又很关键的细节seccomp过滤器具有继承性。如果程序在fork之后才给子进程挂规则那父进程可能完全不受限制整个程序的利用切入点就不一样了。另外seccomp在execve之后依然保留所以你想通过“先打开文件再执行程序”这种方式绕过是行不通的内核会带着过滤器执行新程序规则原封不动保留。2.2 用seccomp-tools解读过滤规则seccomp-tools是目前最顺手的分析工具它可以把BPF规则还原成人能读懂的syscall列表。命令行用法很简单seccomp-tools dump ./pwn_sandbox输出类似这样line CODE JT JF K 0000: 0x20 0x00 0x00 0x00000004 A arch 0001: 0x15 0x00 0x06 0xc000003e if (A ! ARCH_X86_64) goto 0008 0002: 0x20 0x00 0x00 0x00000000 A sys_number 0003: 0x15 0x00 0x04 0x00000000 if (A ! read) goto 0008 ...人话版就是只要系统调用号不是白名单里的就直接返回KILL。看到这种规则基本等于出题人告诉你别想execve了。这里有个实操建议拿到规则后先在纸上写清哪些syscall可用。我就吃过亏只看了白名单里有open没注意open在64位下被glibc调成了openat结果ROP链里用的是open系统调用号2而规则里只放行了openat也就是257导致远程一直打不通。后面细说。2.3 规则中的经典设计逻辑出题人写seccomp规则时通常遵循一套固定套路。最常见的白名单是read、write、open/openat、close、exit、exit_group、brk、mmap、munmap、mprotect。这套集合够程序正常运行也够选手把flag读出来发走但不够它起shell。另一个常见设计是“前后分离”程序先让你完成一次利用比如给你一个栈溢出点但在溢出触发之前先挂seccomp。这类题目考的是单次溢出里把规则、栈、寄存器一次安排明白。还有一种把规则挂在子进程上父进程负责与选手交互子进程执行核心逻辑。这时如果溢出点发生在子进程你子进程的所有系统调用都受限但父进程可能没限制需要考虑父子进程间的通信通道比如管道或共享内存。理解出题人的意图特别重要。看到规则里保留了open/read/write基本可以断定期待解法是ORW利用链看到规则给了read和write但没给open那flag可能早就被程序自己打开、文件描述符保存在某个地方或者需要借助触发程序的既有逻辑来读文件。沙盒规则本身就是题面不要把它当成单纯的限制它在很多情况下直接泄露了解题路径。3. 受限环境下的典型利用思路3.1 最经典的ORW利用链ORW就是open-read-write即通过ROP或shellcode依次调用open、read、write三个系统调用把flag文件内容读出来发到标准输出。这是沙盒pwn题的基操常规到很多题目直接把名字写在提示里。64位下的系统调用规则是rax决定系统调用号rdi、rsi、rdx分别传递第一、第二、第三个参数。open需要rdi指向文件名字符串、rsi传打开标志0表示O_RDONLYread需要rdi传文件描述符、rsi传缓冲区地址、rdx传长度write需要rdi传fd、rsi传缓冲区、rdx传长度。把这三段串起来就是完整的ORW链。拿shellcode语言来说用pwntools的shellcraft可以这样生成from pwn import * context.arch amd64 sc shellcraft.open(flag.txt) sc shellcraft.read(rax, rsp, 0x100) sc shellcraft.write(1, rsp, 0x100) print(asm(sc))这段shellcode的思路是open返回的文件描述符会存在rax里直接把这个rax作为read的第一个参数把读到的内容放到栈上再write给标准输出。面纱一揭其实就这么简单。但shellcode可用的情况比较少。大多数沙盒题开了NX栈不可执行只能走ROP这时就需要手动构造三个系统调用的调用序列。核心工作就变成了找gadget、理寄存器和拼栈。3.2 64位ROP链的参数怎么凑齐ROP构造ORW的关键是找到能控制rdi、rsi、rdx的gadget。最常见的形态是pop rdi; ret、pop rsi; pop r15; ret、pop rdx; ret这种。用ROPgadget搜索即可ROPgadget --binary ./pwn_sandbox --only pop|ret我一般关注这几个pop rdi; ret控制open和read的第一个参数最常用。pop rsi; pop r15; ret控制read和write的第二个参数顺便填掉一个无用寄存器。pop rdx; ret控制长度参数。syscall; ret终极gadget如果找不到单独的syscall指令可以用libc里的syscall地址。一个典型的ORW ROP链长这样rop flat( pop_rdi, addr_flag, # rdi flag字符串地址 pop_rsi_r15, 0, 0, # rsi 0 (O_RDONLY) pop_rax, 2, # rax 2 (x64的open号) syscall, # open(flag.txt, 0) pop_rdi, 3, # rdi 返回值fd假设是3 pop_rsi_r15, bss_addr, 0, # rsi bss缓冲区 pop_rdx, 0x100, # rdx 长度 pop_rax, 0, # rax 0 (read号) syscall, # read(fd, bss, 0x100) pop_rdi, 1, # rdi stdout pop_rsi_r15, bss_addr, 0, # rsi bss pop_rdx, 0x100, # rdx 长度 pop_rax, 1, # rax 1 (write号) syscall, # write(1, bss, 0x100) )这里面最容易被忽略的一步是open的返回值。系统调用后rax就是fd正常顺序下文件中第一个可用的fd是3因为0、1、2被stdin、stdout、stderr占着。如果题目程序自己开了额外文件这个fd会往后顺延所以在写exp时不要硬编码3最好先确认程序运行时实际打开了哪些fd或者直接在gdb里看open返回什么。3.3 进阶变体SROP与奇诡限制的应对有的题目规则非常苛刻只放行read、write、sigreturn连open都禁了。这种时候需要开动一下逆向思路flag很可能在程序启动时就被偷偷打开并读到了内存里只是你没有直接拿到那个fd。遇到这种规则我的建议是优先回看程序逻辑找全局变量和已经打开的fd而不是硬着头皮构造open链。还有一类题走SROP也就是sigreturn oriented programming。思路是向内核发送一个sigreturn系统调用64位下系统调用号15伪造一个signal frame内核在处理信号返回时会把frame里的内容写入各个寄存器相当于你只需要控制一次rax15并触发syscall就能一次性把所有寄存器设置到位。这种手法在缺少pop rdi这类gadget的二进制里特别好用甚至在只有栈溢出和syscall指令时也能完成全套ORW。SROP的构造在pwntools里有现成结构但实际用起来有几个坑一是signal frame里的rip、rsp必须填对否则恢复完寄存器直接段错误二是有些程序对sigreturn有额外校验比如检查内存里的magic。这类题写exp时最好先在本地一步步调试确认内核确实接受了伪造的frame再往远程打。4. 完整实操复盘一道沙盒pwn题的解题全流程4.1 赛题信息收集与沙盒分析以某次模拟竞赛里一道栈溢出加seccomp的题为例演示一下完整流程。题目是一个64位静态编译的ELF运行时输出一个栈地址存在明显的栈溢出点。第一步先checksec结果NX开启、Canary关闭、PIE关闭这意味栈地址固定可以直接用固定地址拼ROP。接着用seccomp-tools拿到过滤规则关键行如下if (A ! read) goto ERRNO if (A ! write) goto ERRNO if (A ! openat) goto ERRNO if (A ! exit) goto ERRNO if (A ! exit_group) goto ERRNO if (A ! brk) goto ERRNO if (A ! mmap) goto ERRNO这题目连open都没放行只给了openat。因为64位glibc里open最终会调用openat所以这种情况其实挺常见。看到规则后我的解题思路直接锁定ORW系统调用选择用openat(257)、read(0)、write(1)。这里最坑的就是系统调用号很多新手按老教程写open2结果触发非法调用程序直接SIGKILL这就是没把规则读透。4.2 构造ORW ROP链并调参程序自带了一个输出栈地址的函数也就是说我们不需要再单独泄露栈flag文件名可以直接写到栈顶。但直接用栈地址有点风险因为溢出会改变栈内容稳妥做法是把flag字符串放到返回地址后面的固定偏移处ROP链里引用那个精确地址。gadget搜索后拿到一组可用的0x401a81: pop rdi ; ret 0x401a8d: pop rsi ; pop r15 ; ret 0x401a95: pop rdx ; ret 0x401aa9: syscall写exp时就按三系统的顺序依次填充。openat的第一个参数是flag路径地址第二个参数是flags传0就行第三个参数是mode也传0。read的第一个参数是fd这里有个细节静态编译的程序可能已经开了某些fd所以我在本地先用gdb在openat处打断点看返回值确认fd确实是3然后才把read里硬编码成3。write就简单了fd1输出读取到的内容。整个exp用pwntools组织重点代码段是payload bA * offset payload p64(pop_rdi) p64(path_addr) # openat的path payload p64(pop_rsi_r15) p64(0) p64(0) payload p64(pop_rdx) p64(0) payload p64(pop_rax) p64(257) # openat payload p64(syscall) ...调试时发现第一次openat返回的fd不是3而是5因为程序在运行中额外打开了几个文件。这个坑不本地调根本看不出来。4.3 本地通了但远程不行的排查思路这道题本地完美打印flag换到远程却一连断了几次要么超时要么输出全是乱码。最后定位到两个原因首先是路径问题。本地环境当前目录下有flag.txt远程环境不一定在同一个工作目录flag文件名也可能带一段随机后缀。遇到这种情况不能硬编码文件名要么先通过readdir等操作探测目录要么注意题目提示的固定路径。其次是栈里的flag路径字符串被覆盖。本地调试时payload长度可能刚好避开覆盖字符串的位置但不同libc版本下题目程序在栈上预留的字长可能不一样。我最后改成把字符串放在bss段先通过read从输入里把文件名读进bss再让openat读取bss里的路径。这样既规避了栈布局的不确定性也让路径不受溢出payload长度影响。这种从栈上存字符串改成bss存字符串的经验后来我用在很多沙盒题上。别把关键数据放在会被自己污染的位置这是写ROP链的基本原则。5. 常见问题与排查技巧实录5.1 高频问题速查表我把做沙盒题时反复踩到的坑整理成了一张表每次卡壳先对着查一遍。现象常见原因处理方向seccomp-tools dump出来规则为空程序未启用seccomp或过滤规则挂在子进程上在gdb里跟踪fork和prctl调用使用open系统调用号却报KILL规则只放行openat(257)glibc实际调用的是openat改用openat注意路径参数远程打了没有回显本地flag文件名与远程不同或fd号不是3动态探测文件或从程序里提取fdROP链开头能执行但中途崩溃栈对齐问题或某个gadget破坏了rsp在gadget间补ret调整16字节对齐程序直接SIGKILL而非SEGV触发了seccomp的KILL动作用strace确认是哪一步系统调用触发shellcode明明没问题却不输出缺少retshellcode执行完落到无效地址在shellcode末尾加ret或跳到下一个gadget这里特别强调第一行。seccomp-tools dump不到东西不代表题目没有沙盒很多程序是先fork再在子进程里挂规则父进程负责交互。这种题目如果你只盯着父进程的输出位置分析会被坑很久。正确做法是在gdb里设断点观察fork之后的执行路径确认过滤规则到底挂在哪个进程。5.2 几条压箱底的实测经验第一拿到题目先确定glibc版本再看系统调用号。同一个open老glibc可能直接用2新glibc则走openat(257)。这个看起来是小事却能让你在exp调试上少浪费两个小时。建议在自己常用环境的libc.so文件里动态验证一下不用凭记忆猜。第二凡是涉及fd号的地方一律动态确认。不管是程序自己打开的fd还是open返回的fd都别凭常识认为它是3。程序加载动态链接库、打开配置文件、甚至题目框架自带的日志fd都会影响fd分配顺序。自己在本地gdb跟一遍比什么都强。第三注意栈对齐。64位ROP里如果不对齐遇到movaps这类指令时会直接触发段错误。解决方案是链子前面加一个ret。这条经验虽然不独属于沙盒题但在ORW这种超长ROP链里特别容易触发因为链越长栈指针偏移的可能性越高。第四shellcode优先级高于ROP。如果题目没开NX优先写shellcode代码逻辑更直观调试成本也更低。只有在NX强制开启时我才去拼ROP否则时间成本不划算。第五分阶段测试。我把沙盒题的exp写好后习惯在本地分别测试“是否成功open”“read是否拿到内容”“write是否正常打印”每个阶段用一个独立的调用序列确保每步都正确后再串成一条完整链。串成大链后一旦出错很难定位是哪一段的问题。个人实操体会刷沙盒题最大的收获不是学会了ORW而是真正理解了一个进程“能做哪些事”取决于系统调用层面的许可而不是取决于我们觉得自己能执行哪段代码。seccomp这种机制方向感很强规则给什么答案就在哪里。个人经验是新手拿到沙盒题第一件事真的别去翻exp模板先花十分钟把规则读懂再问自己在这套白名单下flag只能用哪条路出来。很多时候规则本身就是提示出题人把路径画好了我们只需要把缺失的拼图补完。最后再分享一个我自己一直在用的小技巧写完exp后用strace跟踪一下题目进程看它在seccomp生效之后收到的系统调用返回码。凡是显示EPERM或者直接被SIGSYS杀掉的就是你的利用链撞上规则了。这一招比反复盲调exp靠谱得多也能让你快速定位是哪一条系统调用出了问题。多练几道题你会发现沙盒题其实是pwn方向里思路最清晰的一种越做越有意思。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑