【学习记录】Week5(四):精细化控制——Partial Overwrite、off-by-null 与 GOT 表复用
写在前面在前三篇文章中我们为了拿到完整的地址往往需要构造复杂的泄露链。但在有些 CTF 题目中溢出空间可能只有寥寥几个字节连一个完整的 8 字节地址都写不进去或者程序开启了极其严格的 PIE导致我们连代码段的一个字节都泄露不出来。这时候“大力出奇迹”的思路行不通了我们需要“微创手术”。本文将带你掌握 Partial Overwrite部分覆盖、off-by-null 精细化控制以及 GOT 表地址推导技巧。 目录核心思想盲人摸象的 Partial Overwrite实战推演低字节覆盖绕过 PIE极限控制off-by-null 覆盖返回地址低字节魔法数学GOT 表复用与地址推导Week5 总结与进阶展望1. 核心思想盲人摸象的 Partial OverwritePartial Overwrite部分覆盖是指在栈溢出时我们不覆盖完整的返回地址比如 64 位下 8 字节的地址而是只覆盖其中的 1 到 2 个低位字节。为什么这能奏效低位固定虽然 PIE 和 ASLR 让基址随机化但内存是以页4KB 0x1000为单位加载的。这意味着地址的最低 12 位即末尾 3 个十六进制数1.5 字节是永远固定的。不破坏高位如果我们只覆盖最低的 1 或 2 个字节高位原有的随机地址不会被破坏。CPU 执行ret时依然会跳到当前页内被我们修改后的新地址。2. 实战推演低字节覆盖绕过 PIE假设性场景程序开启了 PIE存在溢出但只能覆盖返回地址的最低 2 个字节。原本的返回地址是main0x2a其静态偏移为0x1156。程序中有一个后门函数get_shell其静态偏移为0x1200。推演逻辑因为最低 12 位固定所以0x1156和0x1200都在同一个0x1000页内。我们只需要把返回地址的最低 2 字节从\x56\x11覆盖为\x00\x12。此时高 6 字节的随机值保持不变CPU 就会精准跳到get_shellPayload 构造假设偏移为 40# 假设原本返回地址在栈上占据 8 字节 # 我们只覆盖最低的 2 字节 payload bA * 40 # 注意这里不能直接 p64()因为 p64 会补 0 破坏高位 payload b\x00\x12 p.sendline(payload)注意避坑如果使用sendline末尾会加上\n即\x0a这会覆盖掉第 3 个字节所以在部分覆盖时一定要用send并且精确计算覆盖的字节数。3. 极限控制off-by-null 覆盖返回地址低字节在 Week5二中我们提到off-by-null可以用来覆盖 Canary 的\x00。但如果溢出点在返回地址之后呢场景推演假设栈结构如下并且存在off-by-null漏洞可以多写一个\x00高地址 | 返回地址 (如 0x0000555555555128) | 低地址如果我们的溢出刚好能覆盖到返回地址的最低 1 字节并且我们写入的是\x00。那么返回地址就从0x0000555555555128变成了0x0000555555555100。这有什么用重置执行流如果0x5100刚好是main函数的开头程序就会重新开始执行这在无法泄露地址但需要多次触发漏洞时非常有用。跳过检查逻辑如果0x5128是某个安全检查函数的中间部分退回到0x5100可能刚好绕过了这个检查直接执行后续的核心逻辑。这种利用方式完全不需要知道高位地址只需要在 IDA 里算好静态偏移的低 2 字节用\x00强行抹零即可。4. 魔法数学GOT 表复用与地址推导如果题目没有puts也没有printf我们无法直接打印 libc 地址但如果我们有任意地址写漏洞能篡改 GOT 表该怎么推导system的地址核心原理相对偏移固定。在同一个 libc 版本中system和puts之间的距离是永恒不变的。system_addr - puts_addr 常数 (Offset)推导流程假设程序调用过puts此时putsgot里存放了puts的真实地址。我们通过任意地址写读取putsgot的值假设为0x7ffff7a649c0加上偏移差算出system的地址再写回putsgot。假设性模拟结合任意写原语from pwn import * p process(./vuln) elf ELF(./vuln) libc ELF(./libc.so.6) # 假设我们有一个任意写原语 write_anywhere(addr, data) # 第一步读取 putsgot 的真实地址假设通过某种侧信道或程序回显得知 # 模拟读取到的 puts 真实地址 puts_real 0x7ffff7a649c0 # 第二步计算偏移差 # libc.symbols[system] - libc.symbols[puts] 就是固定的相对距离 offset libc.symbols[system] - libc.symbols[puts] system_real puts_real offset log.success(fSystem addr: {hex(system_real)}) # 第三步将 system 真实地址写回 putsgot (GOT 表复用) # 下次程序调用 puts(sh) 时实际上会去执行 system(sh) write_anywhere(elf.got[puts], p64(system_real)) # 触发程序调用 puts等同于调用 system p.sendline(bsh) p.interactive()模拟终端输出[] System addr: 0x7ffff7a85390 [*] Switching to interactive mode $ id uid1000(user) gid1000(user) groups1000(user)不需要算 libc 基址不需要完整的泄露链直接利用“地址平移”就能完成劫持这就是 GOT 表复用的精髓。5. Week5 总结与进阶展望至此Week5 的全部内容结束本周我们从破防 Canary 开始经历了无输出环境下的盲爆破与 off-by-null 绕过在 PIE 的迷雾中寻找栈上残留的基址线索最后在极端条件下利用 Partial Overwrite 和 GOT 表地址推导完成了精细化控制。如果说 Week4 是教你如何“排兵布阵”构造完整的 ROP 链那么 Week5 就是教你如何“暗度陈仓”在重重保护下偷取和篡改信息。栈溢出的基础到进阶内容到这里已经形成了一个完整的闭环。下周预告 (Week6)栈上的厮杀暂告一段落。从下周起我们将正式踏入现代 PWN 的主战场——堆。我们将从glibc的内存管理机制讲起揭开malloc和free的底层面纱学习Use-After-Free (UAF)、Double Free以及最经典的Fastbin Attack。堆的世界更加复杂但也更加精彩如果 Week5 的系列文章对你的学习有帮助请点赞收藏支持你的鼓励是我持续更新的最大动力。我们 Week6 见