从seccomp沙箱逃逸到实战:CTF Pwn中的受限环境攻击艺术 1. 项目概述从沙箱到逃逸一次完整的攻防视角转换在二进制安全尤其是CTF Pwnable的赛场上我们常常会遇到一种特殊的挑战程序本身存在漏洞但攻击者却被限制在一个“沙箱”里。这个沙箱就是seccompSecure Computing Mode它像一个严格的保安只允许程序执行白名单里的系统调用。你的目标是在这个保安的眼皮底下利用有限的“合法动作”完成一次漂亮的“越狱”最终拿到系统权限。这就是沙箱逃逸。我最初接触这个概念时也觉得它高深莫测。但当你亲手用seccomp-tools这把“手术刀”剖开一个程序看清它所有被允许和禁止的系统调用后整个攻击面会瞬间清晰起来。今天我就以CTFshow的pwn69到pwn72这四道经典的沙箱逃逸题为例手把手带你走一遍完整的分析、思考和利用流程。这不仅仅是解题更是理解现代安全防护机制与突破思路的绝佳实践。无论你是刚入门Pwn的新手还是想深化二进制利用技巧的进阶者这篇文章都将为你提供一个从工具使用到实战利用的完整视角。2. 核心工具与原理深入理解seccomp与seccomp-tools2.1 seccomp机制Linux内核的“系统调用防火墙”要逃逸先得了解关住你的“笼子”。seccomp是Linux内核提供的一种安全机制它允许进程进入一种“严格模式”。在此模式下进程只能调用exit()、sigreturn()、read()和write()这四个最基本的系统调用这是最初的seccomp mode 1。后来发展出的seccomp-bpfBerkeley Packet Filter模式则强大得多它允许程序通过BPF伯克利包过滤器规则来定义一份复杂的系统调用白名单或黑名单。在CTF和实际漏洞利用中我们遇到的几乎都是seccomp-bpf。程序在初始化时会通过prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, ...)或seccomp(SECCOMP_SET_MODE_FILTER, ...)系统调用加载一段BPF字节码。这段字节码就是“保安”手中的规则手册它决定了哪些系统调用通过其调用号syscall_nr可以被执行哪些会被内核直接拒绝返回-EPERM错误甚至杀死进程。注意seccomp规则一旦设置通常对该进程及其所有子进程都生效并且规则只能追加、不能删除或修改这体现了其“只增不减”的安全设计哲学。这意味着攻击者必须在现有规则框架下寻找出路。2.2 seccomp-tools你的规则“反编译器”手动分析BPF字节码无疑是痛苦的。seccomp-tools这个Ruby工具集的出现极大地简化了这一过程。它主要提供两个核心功能Dump转储从正在运行的进程、核心转储文件core dump或二进制文件本身中提取并反编译出人类可读的seccomp-bpf规则。Disasm反汇编将原始的BPF字节码转换成易于理解的指令序列和规则描述。它的命令行界面非常直观。最常用的命令就是seccomp-tools dump。你可以直接附加到一个运行中的进程需要权限或者分析一个已经设置了沙箱的二进制文件。对于CTF题目我们通常直接分析题目提供的二进制文件。# 分析一个可执行文件中的seccomp规则 seccomp-tools dump ./pwn69 # 附加到运行中的进程进行动态分析需要sudo或相应权限 sudo seccomp-tools dump -p 1234执行后它会输出类似下面的规则列表这是所有分析的起点line CODE JT JF K 0000: 0x20 0x00 0x00 0x00000004 ld $arch 0001: 0x15 0x00 0x08 0xc000003e jeq AUDIT_ARCH_X86_64 goto 0010 0002: 0x20 0x00 0x00 0x00000000 ld $nr 0003: 0x15 0x06 0x00 0x00000000 jeq read goto 0010 0004: 0x15 0x05 0x00 0x00000001 jeq write goto 0010 0005: 0x15 0x04 0x00 0x00000005 jeq fstat goto 0010 ... 0010: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0011: 0x06 0x00 0x00 0x00000000 return KILL左边是BPF指令右边return ALLOW或return KILL就是该路径下的最终裁决。我们的任务就是读懂这份“规则手册”找到所有被ALLOW的系统调用并思考如何用它们组合出强大的攻击链。2.3 实操心得分析时的关键点在实际使用seccomp-tools时有几点经验非常重要结合静态分析不要只看seccomp-tools的输出。一定要用IDA Pro、Ghidra或Binary Ninja等反编译器打开程序找到初始化并调用prctl或seccomp的函数。这能帮你理解规则是在什么条件下、以什么方式加载的有时还能发现规则加载前存在的短暂时间窗口漏洞。关注架构规则的第一条通常是检查系统调用架构如AUDIT_ARCH_X86_64。这很重要因为系统调用号因架构而异。CTF题目绝大多数是x86-64但偶尔也会有ARM或MIPS的题目架构不对一切分析都是徒劳。理解规则逻辑BPF规则本质上是一个决策树。它从检查系统调用号$nr开始可能还会检查参数通过ld $arg[N]指令。你需要顺着jeq等于跳转、jgt大于跳转等指令画出允许和禁止的路径。seccomp-tools输出的goto语句就是跳转目标行号。系统调用号查询你需要一个系统调用号表。在Linux下/usr/include/asm/unistd_64.h对于x86-64是权威来源。也可以在线查询。记住常用系统调用的号码比如execve是59openat是257open是2read是0write是1socket是41connect是42等能极大提升分析效率。3. 沙箱逃逸的通用攻击思路与战术库在看清了沙箱的规则后我们的大脑就需要从“程序员思维”切换到“攻击者思维”。我们的武器库仅限于白名单上的那些系统调用。以下是一些经过实战检验的经典逃逸思路你可以把它们当作战术卡片来组合使用。3.1 思路一文件操作与信息泄露即使不能直接执行命令如果能读写文件我们也能做很多事。读取敏感文件如果open/openat和read被允许我们可以尝试读取/flag、/proc/self/maps获取内存布局、/proc/self/environ获取环境变量可能包含有用信息。写文件实现持久化或触发如果write被允许且我们能以某种方式打开文件比如程序本身提供了文件描述符我们可以写入恶意内容。例如写入/proc/self/mem来修改自身内存需要ptrace等配合通常被禁或者写入一个计划任务文件如/var/spool/cron/crontabs/root但这需要root权限。目录遍历getdents或getdents64系统调用允许读取目录条目。如果被允许可以用来探索文件系统寻找可写的目录或配置文件。实操心得/proc和/sys文件系统是宝库。/proc/self/fd/目录下列出了进程打开的所有文件描述符。有时程序会意外泄露一个关键文件的描述符在这里你可以直接通过/proc/self/fd/[num]路径去读写它绕过了对open系统调用的限制。3.2 思路二网络通信与外部联动这是非常强大的一类手法核心思想是“我不能做的让我的小伙伴来做”。Socket通信如果socket、connect、sendto、recvfrom等网络相关系统调用被允许攻击者可以让受限制的进程与一个由攻击者控制的远程服务器通信。受限制进程可以将内存数据如/flag的内容通过网络发送出来或者接收外部服务器发送的恶意指令或shellcode如果存在可执行内存区域且能跳转过去。管道Pipe或Unix Domain Socket类似网络通信但用于本地进程间通信。如果程序本身是某个服务的一部分可能有机会与其他权限更高的进程通信诱导其执行操作。3.3 思路三利用现有代码与内存破坏这是最考验二进制功底的一类。沙箱限制了系统调用但没限制进程内的代码执行。ROP面向返回编程链的复用如果程序本身存在栈溢出等漏洞我们可以劫持控制流。在普通Pwn题中我们通常用ROP调用execve(“/bin/sh”, 0, 0)。在沙箱下这条路被堵死。但我们可以构造一个更复杂的ROP链这个链只使用被允许的系统调用。例如用open打开flag文件用read读到内存某个已知位置再用write写到标准输出。ORWOpen-Read-Write链这是沙箱逃逸中最著名、最基础的ROP链组合。顾名思义就是按顺序调用open、read、write这三个系统调用。只要这三个调用被允许理论上就能读取任何有权限读取的文件。构造时需要注意系统调用的参数传递约定x86-64下依次是rdi,rsi,rdx,r10,r8,r9以及每个系统调用后需要平衡栈并跳转到下一个gadget。SROPSigreturn Oriented Programmingsigreturn是一个特殊的系统调用它可以从栈上恢复所有寄存器状态。如果它被允许在严格的seccomp下常被禁攻击者可以伪造一个sigcontext结构体放在栈上然后触发sigreturn一次性设置所有寄存器实现极其灵活的利用甚至可以构造出任意系统调用序列。3.4 思路四规则本身的弱点与侧信道有时规则制定者可能百密一疏。参数检查不严规则可能只检查了系统调用号但没有检查参数。例如允许write但没有检查文件描述符参数。那么我们可以尝试向一个已经打开的关键文件描述符比如一个指向敏感文件的fd写入数据或者向标准错误fd2写入数据。时间窗口规则可能在程序初始化后的一段时间才被加载。如果存在漏洞的代码在规则加载前就被执行那么就可以进行无限制的攻击。规则绕过某些系统调用功能强大且灵活可能被用来间接实现被禁止的功能。例如sendfile系统调用可以在两个文件描述符间直接传输数据如果它被允许且我们有一个可读的源fd和一个可写的目标fd比如socket就能实现数据外泄。4. CTFshow pwn69~72 四连击实战详解掌握了理论和工具我们进入实战。CTFshow这四道题难度循序渐进是绝佳的练手材料。假设你已经具备了基础的Pwn技能如栈溢出、ROP链构造、使用pwntools等。4.1 pwn69经典的ORW入门第一步信息收集与分析用checksec查看保护用seccomp-tools查看规则。seccomp-tools dump ./pwn69输出会显示一个非常简洁的规则只允许open、read、write、exit、exit_group这几个系统调用。这几乎就是明牌告诉你本题的逃逸路径就是ORW。第二步漏洞定位用IDA分析发现一个明显的栈溢出漏洞可以覆盖返回地址。并且程序提供了puts等函数可以泄露libc地址进而计算出gadget和字符串的地址。第三步利用链构造泄露地址利用溢出和puts泄露libc基址。布置ROP链我们的目标是执行open(“/flag”, 0, 0)-read(fd, buf, 0x100)-write(1, buf, 0x100)。找字符串需要在内存中有一个”/flag”字符串。可以在libc里找如__libc_start_main附近的字符串或者利用溢出写到bss段等可写区域。找gadget需要pop rdi; ret;,pop rsi; ret;,pop rdx; ret;或能控制rdx的gadget来控制三个参数。还需要syscall; ret;gadget来触发系统调用。链式构造pop rdi; ret; - 指向”/flag”字符串的地址 pop rsi; ret; - 0 (O_RDONLY) pop rdx; ret; - 0 (modeopen的第三个参数打开文件时通常为0) syscall; ret; - 触发 open返回值文件描述符会保存在rax ... (需要将rax中的fd转移到rdi供后续read使用) ... pop rdi; ret; - rax (fd) // 通常需要一个 mov rdi, rax; ret 或 xchg 的gadget pop rsi; ret; - 目标缓冲区地址 (如.bss段) pop rdx; ret; - 要读取的长度 syscall; ret; - 触发 read pop rdi; ret; - 1 (标准输出文件描述符) pop rsi; ret; - 同上缓冲区地址 pop rdx; ret; - 实际读取的长度可以从read的返回值rax得到但通常简化处理用预期长度 syscall; ret; - 触发 write踩坑记录最大的坑在于open系统调用在x86-64下实际是openat(AT_FDCWD, pathname, flags, mode)。但内核提供的系统调用号__NR_open2依然存在并会被正确处理。所以使用syscall指令设置rax2rdi”/flag”rsi0rdx0是可以正确打开文件的。另一个常见错误是忽略了open的第三个参数mode当创建文件时需要对于只读打开现有文件设为0即可。第四步编写Exploit使用pwntools编写脚本按上述链构造payload发送接收flag。4.2 pwn70受限的ORW与参数传递挑战pwn70在pwn69的基础上增加了难度。seccomp-tools分析显示允许的系统调用更少可能去掉了open或者对open的参数进行了检查。关键分析假设open被完全禁止但openat被允许。openat的第一个参数是一个目录文件描述符dirfd通常使用AT_FDCWD值为-100表示当前工作目录。那么我们的ROP链就需要构造openat(AT_FDCWD, “/flag”, O_RDONLY, 0)。这需要我们能构造出-100这个数放到rdi寄存器。在ROP中构造负数需要一点技巧可以通过pop rdi; ret接一个很大的数因为是无符号数或者找一个能进行算术运算的gadget。另一种可能是open被允许但规则检查了flags参数要求必须是O_RDONLY只读。如果我们构造的flags包含了O_RDWR等位就会被拦截。这时需要确保传入的rsi寄存器值是准确的0O_RDONLY。解决方案仔细检查seccomp-tools的输出确认到底哪个调用被允许以及参数检查的细节。然后调整ROP链中相应寄存器的值。如果open被禁而openat允许就改用openat。这要求你对系统调用及其参数有更精确的理解。4.3 pwn71引入信息泄露与更复杂的布局pwn71的沙箱规则可能进一步收紧或者漏洞形式发生了变化。例如栈溢出长度可能很短不足以放下完整的ORW链。战术演变栈迁移Stack Pivot当溢出空间不足时这是一个关键技术。利用leave; ret指令其操作是mov rsp, rbp; pop rbp我们可以将栈指针rsp劫持到我们可控的另一个区域如.bss段。首先通过溢出覆盖rbp为我们可控区域的地址fake_stack_addr然后覆盖返回地址为leave; retgadget。执行后rsp就指向了fake_stack_addr后续的ROP链就可以布置在fake_stack_addr开始的大片可控内存中。利用程序自身功能程序可能提供了诸如read、write、printf等功能。我们可以利用这些合法函数来辅助利用。例如用write来泄露内存布局用read向.bss段读入更长的ROP链或字符串。链式读取如果一次read无法读入完整的flag比如flag很长就需要循环读取。这需要更复杂的ROP链包含条件判断和循环在沙箱限制下实现循环是高级技巧通常需要利用内存中已有的复杂gadget或部分代码片段。4.4 pwn72综合挑战与高阶技巧pwn72通常是这个系列的终极挑战它可能融合了前面所有的难点严格的沙箱、微小的溢出窗口、需要精确的信息泄露、以及可能需要组合多种逃逸思路。可能的场景沙箱极度严格可能只允许read和write甚至只允许write。这时ORW路径被完全切断。漏洞难以触发可能不是简单的栈溢出而是堆漏洞、格式化字符串漏洞或者需要特定竞争条件才能触发。需要组合利用例如先利用一个漏洞泄露内存信息计算出某个关键地址再利用另一个漏洞修改该地址处的数据或指针最后触发一个合法系统调用但其参数指向被我们篡改的内容从而实现逃逸。应对策略彻底审计规则用seccomp-tools逐行分析BPF规则不放过任何一个被允许的系统调用即使它看起来很冷门。深入静态分析用反编译器仔细梳理程序的所有逻辑寻找任何可能的信息泄露点、指针修改点、或未初始化的数据使用点。考虑非典型路径如果open/read/write被禁是否可以使用sendfile如果允许在文件描述符间直接传输使用socketconnectsendmsg将数据发送到网络利用ptrace如果允许但通常被禁控制其他进程利用execveat如果允许以某种受限方式执行程序利用/proc文件系统如前所述/proc/self/mem、/proc/self/fd/、/proc/self/maps都是潜在的突破口。如果程序意外打开了某个文件其文件描述符可能会出现在/proc/self/fd/下我们可以尝试用read/write去操作这个数字fd。5. 漏洞挖掘与利用中的常见问题排查在实际操作中即使思路正确也常常会遇到各种问题。这里记录一些典型的排查点。5.1 系统调用失败返回-1原因一参数错误。这是最常见的原因。使用strace动态跟踪你的攻击进程可以看到系统调用具体是如何被发起、以及内核返回了什么错误码errno。例如EACCES是权限不足ENOENT是文件不存在EBADF是坏的文件描述符。原因二架构不匹配。确保你的ROP链是在正确的架构下运行。在64位程序中调用32位系统调用如通过int 0x80会导致不可预知的行为。原因三地址错误。传递给系统调用的指针如文件名路径、缓冲区地址必须是当前进程地址空间内有效的、可访问的地址。如果指向未映射的内存内核会返回-EFAULT。排查技巧在ROP链中可以在关键的系统调用前后插入一些用于调试的write调用向标准错误输出一些寄存器的值或内存内容。例如在open调用后将rax返回值即文件描述符或错误码打印出来能极大帮助定位问题。5.2 ROP链执行流断裂原因一栈对齐问题。在x86-64 Linux上某些函数或syscall指令可能要求rsp寄存器16字节对齐。如果你的ROP链导致rsp在进入syscall时不是16字节对齐的可能会引发段错误。解决方法是在syscallgadget前添加一个简单的ret指令它只做pop rip不改变rsp值或者使用pop rsp; ret之类的gadget来调整栈指针。原因二gadget副作用。某些gadget在完成主要操作如pop rdi后可能会修改其他寄存器如rsp,rbp或内存。你需要用反编译器或调试器如GDB配合pwntools单步跟踪ROP链的执行观察每个gadget执行前后寄存器和栈的变化。原因三内存破坏。你的ROP链或写入的字符串可能覆盖了程序正常运行所需的关键数据导致在ROP链完全执行前程序就崩溃了。5.3 沙箱规则动态变化有些题目不会在程序一开始就加载所有规则而是分阶段加载。或者程序可能根据某些条件如输入动态调整规则。应对方法一定要用调试器动态跟踪。在prctl或seccomp系统调用处设置断点观察规则是在什么时候、以什么条件加载的。有时在第一次规则加载后、第二次规则加载前存在一个短暂的无限制窗口这就是攻击的黄金时间。5.4 利用脚本本地成功但远程失败网络延迟与交互远程环境可能有网络延迟你的脚本中send和recv的时序可能需要调整。使用pwntools的tube对象的交互方法如sendlineafter,recvuntil比简单的sleep更可靠。环境差异远程服务器的libc版本、系统内核版本可能与你本地不同导致偏移量计算错误。题目通常会提供libc.so.6文件务必使用它来计算偏移。使用pwntools的ELF和LibcSearcher如果未给定libc可以简化这一过程。地址随机化ASLR确保你的泄露步骤正确获取了基址。远程的ASLR是开启的每次运行的基址都不同你的脚本必须能动态计算出正确的地址。6. 从CTF到实战沙箱逃逸的深远意义通过这四道题的实战我们不仅学会了使用seccomp-tools更重要的是建立起一种“受限环境下的攻击思维”。这种思维在真实世界的安全评估中极具价值。现代的安全应用如Docker容器、Chrome浏览器的沙箱、智能手机的应用沙箱其核心思想都是seccomp的延伸——限制进程的能力。安全研究员的工作就是不断挑战这些边界发现规则配置的疏漏、内核实现的缺陷、或者合法系统调用的危险组合方式。例如一个只允许write的沙箱如果未能检查文件描述符参数攻击者或许可以向一个早已打开的网络套接字描述符写入数据实现数据外泄。再比如openat与procfs的结合可能绕过某些路径检查。因此掌握沙箱逃逸不仅仅是解几道CTF题。它训练的是你在一个“最小权限”模型下如何最大化利用有限资源达成目标的能力。这是高级漏洞利用和高级威胁对抗中的核心技能之一。下次当你看到seccomp时希望你的第一反应不是畏惧而是兴奋——因为你知道一场有趣的“猫鼠游戏”即将开始。拿起seccomp-tools仔细阅读规则然后开始构思你的“越狱”计划吧。