:无 Libc 环境破局与 ret2mprotect / GOT 劫持进阶)
写在前面在上一篇中我们攻克了沙箱与 ORW 的技术壁垒。但设想一个极端场景题目去除了libc.so.6的附件远程环境未知且one_gadget因为环境约束全部失效。此时传统的“泄露 Libc 基址 - 查 system - 覆写返回地址”流水线彻底瘫痪。今天我们将探讨在无 Libc 环境下的破局之道深入理解ret2mprotect进阶应用、Partial RELRO 下的got2plt劫持以及one_gadget失效时的手动 Getshell 策略。 目录痛点分析无 Libc 环境与one_gadget失效内存控制权夺取进阶ret2mprotect与 ShellcodePartial RELRO 的盛宴got2plt半盲劫持终极伪造ret2dlresolve简述总结与下篇预告1. 痛点分析无 Libc 环境与one_gadget失效在 CTF 中我们常遇到以下两种绝望情况无 Libc 环境题目不提供libc.so.6且远程环境非主流发行版。由于不同版本 Libc 中system、/bin/sh的偏移天差地别盲打极难成功。one_gadget失效即使有了 Libcone_gadget往往附带严苛的约束条件如[rsp0x70] NULL或栈对齐要求。在现代 glibc 的do_system函数中由于频繁使用movaps等 SSE 指令如果栈指针没有 16 字节对齐程序会直接在system内部段错误。破局思维转换既然“借用” Libc 现成的system行不通我们就必须回归漏洞利用的本质——夺取内存的执行权。要么让一块内存变得可执行mprotect要么篡改程序自身的动态解析过程got劫持 /dl_resolve。2. 内存控制权夺取进阶ret2mprotect与 Shellcodemprotect系统调用的作用是修改内存页的权限读/写/执行。如果我们能把一段 bss 段或堆内存改为可执行rwx然后通过read把 Shellcode 写进去并跳转执行就能彻底无视 Libc 版本和one_gadget约束。2.1 核心难点凑齐三大寄存器mprotect的函数原型为int mprotect(void *addr, size_t len, int prot);对应 x64 调用约定我们需要控制rdi 目标地址必须是页对齐即末尾为0x000rsi 长度通常随便给一个覆盖一页的值如0x1000rdx 权限7表示可读可写可执行痛点程序中往往有pop rdi; ret和pop rsi; ret但极少有直接的pop rdx; ret。2.2 万能桥接利用__libc_csu_init(ret2csu)在没有pop rdx时我们通常会利用程序自带的__libc_csu_init函数尾部的万能 Gadgets。其核心逻辑如下mov rdx, r14 ; 控制第三个参数 rdx mov rsi, r13 ; 控制第二个参数 rsi mov edi, r12d ; 控制第一个参数 rdi (注意是32位高32位清零) call [r15] ; 调用 r15 指向的函数利用步骤通过栈溢出填充跳转到上述代码块。设置r14 7(权限),r13 0x1000(长度),r12 bss_addr(页对齐地址),r15 mprotect_got。执行call [r15]即调用mprotect修改 bss 段权限。利用ret接着布置 ROP 链调用read(0, bss_addr, len)写入 Shellcode。最后jmp bss_addr执行 Shellcode。栈溢出触发ret2csu 控制寄存器调用 mprotect 修改 bss 为 RWX调用 read 将 Shellcode 读入 bss跳转执行 Shellcode无视 Libc 版本 Getshell / ORW3. Partial RELRO 的盛宴got2plt半盲劫持如果程序编译时开启了 Partial RELRO部分重定位只读这意味着.got.plt表是可写的。这为我们提供了一条不依赖mprotect的捷径。3.1 原理偷梁换柱在延迟绑定机制下当程序第一次调用puts时会去.got.plt表中读取地址。如果我们能在程序解析前将.got.plt表中puts对应的项篡改为system的地址那么下次程序调用puts(/bin/sh)时实际执行的将是system(/bin/sh)。3.2 无 Libc 的半盲劫持策略在无 Libc 环境下我们不知道system的绝对地址但我们可以利用偏移差进行半盲打。泄露puts的真实地址通过格式化字符串或任意读泄露一次puts的真实运行时地址。计算system的相对偏移在绝大多数同一架构的 Libc 版本中puts和system之间的相对偏移是固定的或者说变化不大。addr_system addr_puts - (puts_offset - system_offset)。覆盖 GOT 表利用任意写漏洞将putsgot覆盖为计算出的addr_system。触发程序下次执行puts(/bin/sh)时成功 Getshell。*注此方法在现代 glibc 频繁更新中偏移固定的概率在降低但在 Partial RELRO 且无 Libc 的题目中依然是首选尝试方案。*3.3 更彻底的 GOT 劫持控制执行流如果题目本身没有调用system但调用了puts。我们甚至不需要/bin/sh字符串。我们可以将putsgot覆盖为程序中现有的main函数地址或某个pop rdi; ret的 gadget 地址从而将控制流重新拉回我们手中实现循环利用。4. 终极伪造ret2dlresolve简述如果说got2plt是半盲打那么ret2dlresolve就是无 Libc 环境下的终极魔法。4.1 核心思想当程序调用一个未解析的库函数时会调用_dl_runtime_resolve函数。这个函数会根据传入的参数 reloc_offset 去.rel.plt、.dynsym、.dynstr表中查找对应的函数名字符串然后从 Libc 中加载该函数。如果我们能伪造这三大表的结构并控制reloc_offset指向我们的伪造数据我们就能欺骗动态链接器让它去加载system函数哪怕我们根本不知道 Libc 在哪4.2 实战现状x86 (32位)ret2dlresolve极其成熟且容易构造是 32 位无 Libc 题目的标准解法。x64 (64位)由于 64 位下Elf64_Rel和Elf64_Sym结构体对齐要求严格且引入了ndx版本检查手工构造难度极大。通常借助pwntools的ROP(dlresolve)模块自动生成伪造结构。# pwntools 64位 ret2dlresolve 模板伪代码 dlresolve Ret2dlresolvePayload(elf, symbolsystem, args[/bin/sh]) rop ROP(elf) rop.read(0, dlresolve.data_addr) # 将伪造的结构读到 bss 段 rop.ret2dlresolve(dlresolve) # 触发伪造解析 io.sendline(rop.chain()) io.sendline(dlresolve.payload) # 发送伪造数据5. 总结与下篇预告5.1 核心知识点总结回归本质当 Libc 不可用或one_gadget失效时放弃寻找system转而夺取内存执行权mprotect Shellcode或篡改程序自身的解析机制。ret2mprotect进阶熟练掌握__libc_csu_init(ret2csu) 万能 Gadgets解决缺少pop rdx的痛点。GOT 劫持Partial RELRO 下.got.plt可写通过相对偏移半盲打或重定向控制流是无 Libc 环境的高效破局点。ret2dlresolve伪造动态链接器三大表是脱离 Libc 依赖的终极武器64 位强烈依赖pwntools自动化构造。5.2 下篇预告在解决了沙箱和无 Libc 环境后下一篇我们将转向多进程与栈环境中的“阴暗面”。父子进程漏洞利用fork进程时的内存共享与FILE结构体联动利用。栈残留数据利用函数返回后栈帧未清零如何利用残留的旧数据绕过随机化或构造 ROP。系统化梳理 Week14 的综合实战 Checklist。结语无 Libc 环境不是绝境而是逼你褪去“查表做题”的舒适区直视操作系统底层运行机制的试炼场。当你能用ret2csu优雅地控制三个寄存器用伪造的结构欺骗动态链接器时你才真正掌握了内存的控制权。