深入解析BUUCTF-pwn中的orw_seccomp绕过技术
1. 理解orw_seccomp的核心限制第一次接触BUUCTF的pwnable_orw题目时很多人会被这个奇怪的缩写搞懵。orw其实是open/read/write三个系统调用的首字母组合而seccomp则是Linux内核的安全沙箱机制。这道题的精妙之处在于它通过seccomp严格限制了你能使用的系统调用就像把你关在一个只有三扇门的房间里——每扇门分别贴着打开、读取和写入的标签。seccomp的工作原理有点像严格的安检系统。当程序执行orw_seccomp函数时会通过两次prctl调用来设置安全策略第一次禁止了所有提权操作第二次更是狠到只允许open、read、write和exit这四个最基本的系统调用。这意味着你那些花哨的execve、fork等系统调用全都成了废招就像带着瑞士军刀过机场安检被没收了所有工具只剩下指甲钳还能用。在实际做题时我遇到过最典型的错误就是试图执行/bin/sh的shellcode。记得有次熬夜调试时shellcode明明能正常执行却拿不到shell后来才意识到seccomp已经把这些危险调用全都过滤了。这时候正确的思路应该是既然只能使用open/read/write那就用这三板斧把flag文件的内容直接掏出来。2. 手工打造orw shellcode的艺术2.1 文件打开阶段的汇编魔法手工编写orw shellcode就像在用汇编语言玩解谜游戏。以打开flag文件为例我们需要在栈上构造文件路径字符串。32位环境下的小端序存储是个容易踩坑的地方——字符串flag的十六进制表示是0x67616c66但压栈时需要倒序处理push 0x0 ; 字符串终止符 push 0x67616c66 ; g,a,l,f的ASCII码 mov ebx, esp ; EBX现在指向栈上的flag\0 xor ecx, ecx ; 打开标志位设为0 xor edx, edx ; 模式参数设为0 mov eax, 0x5 ; sys_open的系统调用号 int 0x80 ; 触发系统调用这里有个实用技巧用python的hex(ord(f))hex(ord(l))...可以快速获取字符串的十六进制表示。我在实际调试时发现如果忘记加字符串终止符会导致后续读取时出现内存错误。2.2 文件读取的缓冲区玄学成功打开文件后读取操作需要特别注意文件描述符的传递。很多新手会在这里犯迷糊——为什么用3作为文件描述符这是因为在Linux中0/1/2已经被标准输入、输出和错误占用新打开的文件会从3开始分配mov eax, 0x3 ; sys_read的系统调用号 mov ebx, 0x3 ; 使用刚才打开的文件描述符 mov ecx, esp ; 将栈顶作为缓冲区地址 mov edx, 0x100 ; 读取256字节 int 0x80缓冲区选择很有讲究。我建议使用栈顶地址(esp)作为缓冲区这样不需要额外分配内存空间。曾经有次我把缓冲区设在了.bss段结果因为地址空间随机化(ASLR)导致exp不稳定调试到凌晨三点才找到问题所在。2.3 输出阶段的注意事项最后的写入操作看似简单实则暗藏杀机。输出到标准输出(fd1)时必须确保读取到的内容长度正确mov eax, 0x4 ; sys_write的系统调用号 mov ebx, 0x1 ; 标准输出文件描述符 mov ecx, esp ; 要输出的缓冲区地址 mov edx, eax ; 聪明的做法是用read的返回值 int 0x80这里有个优化技巧read系统调用会把实际读取的字节数存放在eax中我们可以直接复用这个值作为write的长度参数避免硬编码长度值。我在某次比赛中就因为这个优化比对手快了10分钟拿到flag。3. 使用pwntools快速构造shellcode3.1 shellcraft的便捷之道对于不想手写汇编的朋友pwntools的shellcraft模块简直是福音。下面这个例子展示了如何用三行Python代码完成全套orw操作from pwn import * context.arch i386 shellcode shellcraft.open(/flag) shellcode shellcraft.read(eax, esp, 100) shellcode shellcraft.write(1, esp, 100)不过要注意的是shellcraft生成的代码可能不是最精简的。有次我对比发现手工汇编只有24字节而shellcraft生成的却有40多字节在缓冲区有限的情况下这就很致命了。3.2 上下文环境的正确设置使用pwntools时最容易忽略的就是context设置。特别是在32位/64位混合环境中一定要明确指定架构context(oslinux, archi386, log_leveldebug)log_level设置为debug后所有发送接收的数据都会完整显示这对调试shellcode非常有用。我曾经因为忘记设置arch导致生成的shellcode完全不对白白浪费了两小时。4. 实战调试技巧与常见坑点4.1 本地测试环境的搭建在真正攻击远程服务器前强烈建议在本地复现环境。可以用以下命令模拟seccomp限制gcc -m32 -z execstack -o tester tester.c记得加上-z execstack参数允许栈执行否则shellcode会触发段错误。我见过最搞笑的错误就是有人shellcode写得完美却忘了这个编译参数。4.2 使用strace追踪系统调用当shellcode表现异常时strace是最好的诊断工具strace -f -i ./pwnable_orw input.bin-f参数会跟踪子进程-i能显示指令指针这对定位哪条系统调用被拦截特别有用。有次我发现exit_group调用也被拦截了这才知道题目比想象的限制更严格。4.3 处理canary保护的小技巧题目开启了canary保护但有趣的是我们的shellcode是通过合法输入点注入的不需要绕过canary。这与常规的栈溢出有很大区别——你只需要确保不触发栈破坏检测即可。在实际操作中保持shellcode紧凑不越界是最安全的做法。5. 进阶理解seccomp的底层机制seccomp最初是作为Linux内核的简易沙盒出现的其发展经历了两个重要阶段。最初的seccomp模式只允许read/write/exit/_exit四个系统调用被称为严格模式。而seccomp-bpf则引入了更灵活的伯克利包过滤器(BPF)规则允许细粒度地控制系统调用。在本题中通过逆向分析可以看到两次prctl调用prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT);第一条禁止了子进程提权第二条则启用了严格模式。有趣的是题目实际允许的系统调用比文档记载的严格模式多了一个open这可能是出题人特意修改过的。这种细节在真实比赛中往往就是突破的关键点。6. 其他可能的解题路径除了标准的orw方法这道题还有几个有趣的变种解法。比如可以通过write系统调用泄漏内存信息或者利用有限的系统调用实现更复杂的文件遍历。在某个变种题目中我甚至见过通过精心构造的open调用实现目录穿越的案例。对于想深入研究的同学建议看看Linux内核中seccomp的源码实现特别是arch/x86/kernel/seccomp.c文件。理解内核如何拦截和过滤系统调用会让你对这类题目有降维打击的能力。