写在前面在前三篇文章中我们掌握了 ret2libc 泄露、基础 ROP 拼接以及大杀器 ret2csu。但 CTF 的世界总是千奇百怪如果题目没有给你 libc或者程序里根本找不到syscall指令怎么办如果溢出的空间极其狭小连 ROP 链都放不下怎么办本文作为 Week4 的收官将带你学习三种高阶栈溢出技巧彻底补全你的基础攻击面。 目录越过 libcret2syscall 直接调 execve空间魔术栈劫持与 fake stack frame 构造破除禁锢ret2mprotect 修改内存权限Week4 总结与进阶展望1. 越过 libcret2syscall 直接调 execve场景痛点有时候题目是静态编译的或者没有输出函数让我们泄露 libc 基址。此时 ret2libc 走不通。破局思路既然程序静态编译了那它内部大概率包含了syscall指令。我们不需要调用system函数而是直接通过 ROP 链布置寄存器最后执行syscall指令直接触发操作系统的execve(/bin/sh, NULL, NULL)系统调用。64 位 execve 系统调用约定rax 0x3b (59execve 的系统调用号)rdi “/bin/sh” 字符串地址rsi 0 (argv)rdx 0 (envp)最后执行syscall指令假设性说明模拟 Gadget 查找使用 ROPgadget 查找静态编译的程序ROPgadget --binary vuln --only pop|ret | grep rax # 模拟输出: 0x0000000000401b8f : pop rax ; ret ROPgadget --binary vuln --only pop|ret | grep rdi # 模拟输出: 0x0000000000401b93 : pop rdi ; ret ROPgadget --binary vuln --string /bin/sh # 模拟输出: 0x00000000006c1000 : /bin/sh ROPgadget --binary vuln --only syscall|ret # 模拟输出: 0x0000000000401b95 : syscall ; ret假设我们通过 ret2csu 或其他方式已经将rdx和rsi置零了ROP 链拼接推演payload bA * offset # 假设已经处理了 rsi 和 rdx 为 0 payload p64(pop_rdi) p64(bin_sh_addr) payload p64(pop_rax) p64(0x3b) payload p64(syscall_addr)通过这种方式我们完全绕过了 libc直接让 CPU 替我们执行系统调用。2. 空间魔术栈劫持与 fake stack frame 构造场景痛点有时候程序的溢出点非常小比如read(0, buf, 0x20)除去 16 字节的填充和 8 字节的返回地址我们只剩下 8 字节的空间根本塞不下完整的 ROP 链。破局思路既然当前的栈空间不够那我们就把 ROP 链写到其他地方如 BSS 段然后把栈指针RSP劫持过去。核心指令leave; ret回顾汇编知识leave指令等价于mov rsp, rbp ; 把 rbp 的值赋给 rsp pop rbp ; 把此时栈顶的值弹入 rbp如果我们能控制rbp的值然后执行leave; ret就能瞬间改变rsp的指向这就是“栈迁移”或“伪造栈帧”的核心。假设性实战推演假设溢出偏移为 16即 16 字节后覆盖到 rbp我们只能控制rbp和返回地址。我们的目标是把栈迁移到bss_addr。第一次溢出时我们在bss_addr提前写好完整的 ROP 链。构造第一次的 Payloadrbp被覆盖为bss_addr - 8因为leave中的pop rbp会弹出一个 8 字节我们需要让执行完pop rbp后rsp正好指向bss_addr。返回地址覆盖为程序中某个leave; ret指令的地址。栈结构设计低地址 | 16字节填充 | | bss_addr - 8 (覆盖 rbp) | - 此时原 rbp 被篡改 | leave_ret_addr (返回地址)| - ret 跳到 leave; ret 执行 高地址当程序执行原本的leave; ret或者跳到我们自己构造的leave; ret时mov rsp, rbp-rsp变成了bss_addr - 8pop rbp-rsp变成了bss_addr此时栈顶就是我们提前写好的完整 ROP 链ret- 开始执行我们在 BSS 段布置的 ROP 链。这种技术在空间极其狭小的栈溢出中是起死回生的神技。3. 破除禁锢ret2mprotect 修改内存权限场景痛点程序开启了 NX 保护栈和 BSS 段都不可执行。我们想用 Shellcode但系统不让执行。破局思路Linux 提供了mprotect函数可以修改内存页的权限。如果我们通过 ROP 调用mprotect(bss_addr, 0x1000, 7)就能把 BSS 段改成可读可写可执行rwx7 421然后跳过去执行 Shellcode。函数原型int mprotect(void *addr, size_t len, int prot);addr必须是内存页的整数倍通常是 0x1000 对齐如0x404000len是长度prot是权限7 代表PROT_READ | PROT_WRITE | PROT_EXEC攻击步骤规划使用 ret2csu 或基础 ROP控制rdi 0x404000rsi 0x1000rdx 7。调用mprotect。接着调用read(0, 0x404000, 0x100)将我们的 Shellcode 读入 BSS 段。最后ret跳转到0x404000执行 Shellcode。假设性说明模拟 ROP 链结构payload bA * offset # 1. 调用 mprotect(0x404000, 0x1000, 7) payload p64(pop_rdi) p64(0x404000) payload p64(pop_rsi) p64(0x1000) # 假设 rdx 已经是 7或者用 csu 控制 payload p64(mprotect_addr) # 2. 调用 read(0, 0x404000, 0x100) 把 shellcode 读进去 payload p64(pop_rdi) p64(0) payload p64(pop_rsi) p64(0x404000) payload p64(pop_rdx) p64(0x100) payload p64(read_addr) # 3. 跳转到 0x404000 执行 payload p64(0x404000)当read读取完我们发送的 Shellcode 后ret指令会精准跳到 BSS 段此时该区域已是rwx权限Shellcode 顺利执行。4. Week4 总结与进阶展望至此Week4 的四大模块全部完成从最基础的ret2libc 泄露与劫持到拼接任意函数调用的基础 ROP从解决 64 位传参痛点的ret2csu到直接越权系统调用的ret2syscall再到空间魔术般的栈劫持和改变内存属性的ret2mprotect。如果说 Week1-Week3 是让你认识了 PWN 的武器库那么 Week4 就是教你如何组合这些武器形成一套完整的攻击体系。掌握了这些常规的栈溢出题已经很难挡住你的脚步。下周预告 (Week5)栈上的基础利用我们已基本讲完。但在实际环境中栈上的保护Canary越来越严密。下周我们将正式踏入堆的世界从malloc和free的底层实现讲起揭开Use-After-Free (UAF)、Double Free和Tcache机制的神秘面纱。堆利用才是现代 PWN 的主战场如果 Week4 的系列文章对你的学习有帮助请点赞收藏支持你的鼓励是我持续更新的最大动力。我们 Week5 见