写在前面:欢迎来到 Week14 的收官篇!在过去的几篇中,我们攻克了信息泄露、沙箱 ORW、无 Libc 环境等硬核技术。然而,在实际 CTF 综合题中,出题人常通过多进程和复杂栈管理设置最后障碍。今天,我们将探讨fork 环境下的联动利用与栈残留数据挖掘,这是区分熟练选手与顶尖选手的关键技术细节。掌握它们,你就能在别人止步的地方继续向前。
📑 目录
- fork 环境下的利用策略:内存共享与漏洞叠加
- 栈残留数据:被遗忘的“信息金矿”
- 综合实战Checklist:Week14 技术全景图
- Week14 总结与下期预告
1. fork 环境下的利用策略:内存共享与漏洞叠加
在服务器类 PWN 题中,常用fork创建子进程处理请求。这为我们提供了独特的利用机会。
1.1 fork 的内存模型:共享与差异
当父进程调用fork后,子进程是父进程的完整副本,但采用写时复制技术。这意味着:
- 共享阶段:
fork后,子进程的内存内容与父进程完全相同,包括堆、栈、libc 数据段。地址空间布局也一致。 - 写时复制:一旦子进程(或父进程)修改了某页内存,内核会为该页创建副本,此后两者不再共享该页。
1.2 利用策略:利用共享内存泄露信息
在 fork 模型中,我们可以利用“共享”特性进行信息泄露。
- 泄露堆地址:父进程释放一个堆块进入 Unsorted Bin,其
fd/bk指向main_arena+88。子进程继承此堆状态,可通过 UAF 读取该指针,从而泄露 Libc 基址。 - 泄露栈地址:父进程的栈帧(如 RBP、返回地址)被子进程继承。子进程通过栈溢出或格式化字符串,可读取父进程栈上的残留数据,甚至包括父进程的 Libc 返回地址,从而精确计算栈基址。
1.3 经典模型:父子进程漏洞叠加
更复杂的题目会设计父子进程漏洞叠加。
- 父进程漏洞:存在栈溢出,但只能覆盖返回地址,无法构造完整 ROP 链(如缺少 gadgets)。
- 子进程漏洞:存在 UAF 或格式化字符串,可泄露地址。
- 联动利用:
- 子进程通过漏洞泄露父进程栈地址或 Libc 地址。
- 子进程将泄露信息通过管道或共享内存传递给父进程。
- 礪进程利用泄露的地址,在子进程返回时精准覆盖父进程的返回地址,构造完整 ROP 链执行
system("/bin/sh")或 ORW。
父进程: 请求处理
子进程: 存在格式化字符串漏洞
子进程泄露父进程栈地址
子进程将栈地址写入共享内存/管道
父进程读取共享信息
父进程利用栈溢出+泄露地址
构造完整 ROP 链
父进程 Getshell
子进程: 存在 UAF 漏洞
子进程泄露 Libc 地址
子进程将 Libc 地址写入共享内存
2. 栈残留数据:被遗忘的“信息金矿”
函数调用结束后,栈帧虽然被“弹出”,但栈上的数据并未清零。这些残留数据往往是极有价值的信息泄露源。
2.1 残留数据来源
- 旧函数调用残留:函数A调用函数B,B返回后,B的栈帧数据仍残留在原位置。当函数C被调用且复用该栈区时,C的局部变量会初始化这些残留数据。
- 初始化未清零的缓冲区:
gets或read读取输入时,若输入长度小于缓冲区,多余部分会保留旧数据。 - 系统调用残留:某些系统调用(如
stat)返回后,参数缓冲区可能残留内核数据。 - GDB 调试残留:在某些题目中,GDB 调试过程中可能改变内存状态,导致本地能利用但远程失败(需注意调试器污染)。
- 父进程残留:在 fork 模型中,父进程的栈数据会残留给子进程。
- libc 残留:libc 函数调用后,其内部临时栈帧可能残留指针。
- 环境变量/参数残留:
main函数的argv和envp指针通常位于栈底,如果程序误读这些指针,可能泄露栈基址。 - 初始化为零的内存:虽然不完全是“残留”,但初始化为零的内存(如
bss段)在首次使用时,其值为零,这可用于构造特定条件(如_lock指针需指向零值内存)。
2.2 实战利用案例
案例1:利用残留的 Libc 指针泄露基址
假设程序流程为:
init()函数调用setvbuf设置缓冲区,期间在栈上分配了临时缓冲区并存放了 libc 指针。init()返回,栈帧弹出,但指针数据残留。vuln()函数被调用,复用同一栈区,其局部变量char buf[0x20]初始化了残留数据。- 程序执行
printf(buf),由于buf未以 null 结尾,printf会持续打印直到遇到 null 字节,从而泄露残留的 libc 指针。
案例2:利用残留栈指针实现栈迁移
- 函数A存在栈溢出,但只能覆盖返回地址,无法覆盖足够长度进行 ROP。
- 函数A返回后,栈上残留了旧 RBP 指针。
- 函数B被调用,复用该栈区。函数B存在格式化字符串漏洞,可泄露栈上残留的旧 RBP,从而推算出栈基址。
- 利用泄露的栈地址,我们可以在堆上伪造一个栈帧,并通过栈迁移技术将执行流转移到伪造栈帧上执行 ROP 链。
2.3 棘手情况:栈上无可用残留数据
有时栈上确实没有可用的残留数据。此时需考虑以下策略:
- 主动制造残留:在
vuln之前,通过其他函数调用在栈上留下有价值的指针(如通过调用read读取数据到栈上,然后返回)。 - 利用堆残留:将残留数据从堆上迁移到栈上。例如,通过 UAF 读取堆上的 libc 毞针,然后通过栈溢出将该指针覆盖到栈上,再利用格式化字符串泄露该栈地址。
- 利用环境变量:在
main函数之前的初始化阶段,环境变量和程序参数存储在栈底。如果程序误读这些指针,可能泄露栈基址。 - 接受“无泄露”现实:在某些极端题目中,可能需要放弃泄露,直接采用爆破或
ret2dlresolve等无需泄露的技术。 - 利用 libc 内部残留:某些 libc 函数调用后,其内部临时栈帧可能残留指针。例如,
malloc调用后,其分配的 chunk 头部可能残留堆地址。通过 UAF 读取该 chunk,可泄露堆地址。
3. 树立综合实战思维:Week14 技术全景图
将 Week14 学到的所有技术整合成一张实战流程图,是打综合题的核心思维。
栈溢出/UAF/格式化字符串
是
是
否
否
有
无
是
否
是
漏洞点触发
漏洞类型判断
信息泄露
泄露 Libc/堆/栈地址
是否有沙箱?
沙箱分析
seccomp-tools dump
open 被禁?
openat 替代
或 /proc/self/mem
基础 ORW ROP
Get Flag
是否有 Libc?
ret2libc: system/execve
Partial RELRO?
got2plt 半盲劫持
ret2dlresolve 或 ret2mprotect
Getshell
多进程环境?
父子进程联动利用
利用残留数据或共享内存
构造完整 ROP 链
4. Week14 总结与下期预告
4.1 核心知识点总结
- fork 联动利用:利用子进程继承父进程内存的特性,通过共享内存泄露地址,或通过子进程漏洞为父进程利用铺路。
- 栈残留数据:函数返回后栈帧未清零,旧数据可能泄露 Libc/堆/栈地址,或用于构造栈迁移条件。
- 综合实战思维:将信息泄露、沙箱 ORW、无 Libc 破局、多进程联动等技术系统化整合,根据题目环境灵活组合。
- 技术选型决策树:根据漏洞类型、沙箱规则、Libc 可得性、RELRO 状态、进程模型等条件,选择最优利用路径。
- “无泄露”应对策略:在无可用残留数据时,主动制造残留、利用堆残留迁移、或采用无需泄露的技术(
ret2dlresolve、ret2mprotect)。 - Checklist 应用:将信息泄露、沙箱分析、ORW 构造、GOT 劫持、
ret2mprotect等步骤系统化成 Checklist,打题时按图索骥,极大提升效率。 - 工具链整合:熟练使用
seccomp-tools、pwntools的Ret2dlresolvePayload、ROPgadget琁工具,是现代 CTF PWN 的必备技能。 - 环境适配思维:根据远程环境(glibc 版本、内核版本、保护机制)动态调整利用策略,是成熟 PWN 选手的标志。
- “最后一公里”思维:在技术方案确定后,如何精准控制寄存器、处理栈对齐、利用残留数据,往往是决定成败的关键细节。
4.2 下期预告
在 Week14 的综合实战体系中,我们经常需要根据远程环境动态调整利用策略。但这一切的基础,是拥有一个稳定且可复现的调试环境。在 Week15 中,我们将深入工具链整合与环境适配。
patchelf+glibc-all-in-one:一键切换 Libc 版本,模拟远程环境。pwninit+ GDB 脚本自动化:秒级初始化 PWN 题调试环境。angr符号执行辅助:解决复杂的路径约束问题。- Docker 复现环境:搭建与远程完全一致的调试环境。
- 内核 PWN 入门:理解用户态与内核态的区别,搭建 Kernel PWN 调试环境。
结语:从信息泄露到 ORW,从无 Libc 破局到多进程联动,Week14 为你构筑了现代 CTF PWN 的完整技术体系。但记住,技术只是工具,真正的核心是系统化的利用思维。当你能将所有技术点融会贯通,根据题目环境灵活组合时,你将无往不利。