【学习记录】Week4(四):进阶栈溢出——ret2syscall、栈劫持与 ret2mprotect 实战
写在前面:在前三篇文章中,我们掌握了 ret2libc 泄露、基础 ROP 拼接以及大杀器 ret2csu。但 CTF 的世界总是千奇百怪:如果题目没有给你 libc,或者程序里根本找不到
syscall指令怎么办?如果溢出的空间极其狭小,连 ROP 链都放不下怎么办?本文作为 Week4 的收官,将带你学习三种高阶栈溢出技巧,彻底补全你的基础攻击面。
📑 目录
- 越过 libc:ret2syscall 直接调 execve
- 空间魔术:栈劫持与 fake stack frame 构造
- 破除禁锢:ret2mprotect 修改内存权限
- Week4 总结与进阶展望
1. 越过 libc:ret2syscall 直接调 execve
场景痛点:有时候题目是静态编译的,或者没有输出函数让我们泄露 libc 基址。此时 ret2libc 走不通。
破局思路:既然程序静态编译了,那它内部大概率包含了syscall指令。我们不需要调用system函数,而是直接通过 ROP 链布置寄存器,最后执行syscall指令,直接触发操作系统的execve("/bin/sh", NULL, NULL)系统调用。
64 位 execve 系统调用约定:
rax= 0x3b (59,execve 的系统调用号)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 = b'A' * 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 链。 - 构造第一次的 Payload:
rbp被覆盖为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 段改成可读可写可执行(rwx,7 = 4+2+1),然后跳过去执行 Shellcode。
函数原型:int mprotect(void *addr, size_t len, int prot);
addr必须是内存页的整数倍(通常是 0x1000 对齐,如0x404000)len是长度prot是权限,7 代表PROT_READ | PROT_WRITE | PROT_EXEC
攻击步骤规划:
- 使用 ret2csu 或基础 ROP,控制
rdi = 0x404000,rsi = 0x1000,rdx = 7。 - 调用
mprotect。 - 接着调用
read(0, 0x404000, 0x100),将我们的 Shellcode 读入 BSS 段。 - 最后
ret跳转到0x404000执行 Shellcode。
假设性说明(模拟 ROP 链结构):
payload = b'A' * 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 见!🙏
