绕过防御之击败PIE保护
PIE的介绍
Position-independent executable 地址无关可执行文件,该技术就是对于代码段、text 段、data 段、.bss 段等固定地址的一个防护手段,应用了 PIE 的程序在每次加载时都会变换加载基址,从而使 ropper 等工具无法得到准确的地址的一种防护。
但注意:PIE 只是 “整体偏移随机”,程序内部的相对地址不变!
tip) 在计算机中,内存地址本质是一串二进制数(比如 64 位系统中是 64 位二进制),“最后 12 位” 就是这串二进制数的最低 12 位(最右侧的 12 位)。 也可以认为成是十六进制的最后 1.5 位
虚拟内存的 “页对齐” 规则
操作系统分配内存时,是以 页(Page) 为最小单位的,x86-64 系统默认页大小是 4KB = 0x1000 字节
- 程序的基地址必须是 0x1000 的整数倍(即基地址的最后 12 位全是 0,比如 0x55f8a7600000、0x551234500000);
- 程序内部的函数 / 变量偏移,是编译时确定的(比如 main 偏移 0x5d6、system 偏移 0x2d290),且偏移值一定小于一个页(或跨页但相对偏移固定);
因此:实际地址 = 基地址(最后 12 位为 0) + 内部偏移(≤ 0xFFF 或固定) → 实际地址的最后 12 位,就是内部偏移的最后 12 位(完全不变)。
IDA 的 “虚拟地址” 显示逻辑
IDA 反编译时,会给 PIE 程序分配一个 默认基地址(通常是 0x100000,可在 IDA 中修改,但不影响偏移),然后显示 “默认基地址 + 内部偏移” 作为 “虚拟地址”。比如:
- IDA 中 main 的虚拟地址是 0x1005d6 → 实际是 “默认基址 0x100000 + 偏移 0x5d6”;
- 程序运行时,实际基址是 0x55f8a7600000 → 实际地址 0x55f8a76005d6;
对比两者的最后两位:0x05d6(IDA 虚拟地址)和 0x05d6(运行时实际地址)完全一致!这就是你说的 “PIE 隐藏了基地址,但最后的 12 位仍然会显示在 IDA 中”——IDA 显示的最后 12 位,本质是程序的 “内部相对偏移”,和运行时实际地址的最后 12 位完全相同。比如:
- 程序编译后,main 相对于基地址的偏移是 0x5d6(假设);
- 第一次启动,基地址随机为 0x55f8a7600000,则 main 实际地址 = 基地址 + 偏移 = 0x55f8a76005d6;
- 第二次启动,基地址随机为 0x551234500000,则 main 实际地址 = 0x5512345005d6
PIE的绕过方式(部分覆盖&直接泄露)
1)爆破 PIE Partial Write [BUU] linkctf_2018.7_babypie



PIE隐藏了基地址,但是最后的12位仍然会显示在IDA中,我们可以通过对最后一个半字节的改变修改地址,达到进攻目的。因为我们只能一个字节一个字节改动,最后半个字节就需要爆破处理,1/16的概率。
1 | __int64 sub_960() |
找到后门函数 system(“/bin/sh”);地址为 0x0000000000000A42(由于开启 PIE,只有 A42 是固定不变的)
1 | int sub_A3E() |
基本函数分析: memset(buf, 0, 32);对buf的32个字节进行清零。
Canary:__readfsqword(0x28u) 是 x86_64 Linux 下读取全局 Canary 值的核心操作,对应读取 FS 段 0x28 偏移处的 8 字节;
分析 IDA 发现有两个 read 函数,第一个可以用来泄露 canary,第二个用来后早 payload 获取 shell。
由于 Canary 是八个字节且最后一个字节为 \x00,所以我们截取小端排序的前七个字节来获取 Canary。
1 | from pwn import * |
我们已知 PIE 的最后 1.5 个字节是不变的,因此我们只需要爆破倒数第二个字节的十六进制的第一位 16 次即可。
1 | addr = b'\x42' |
爆破exp
1 | from pwn import * |

当然赌狗要是觉得自己 exp 准确无误,也可以尝试不用爆破尝试暴力碰撞那 0.5 个字节,不过是 1/16 的概率。
1 | from pwn import * # 导入 pwntools 库(远程连接、payload 构造、调试都依赖它) |
2)直接泄露 NSSCTF[深育杯 2021]find_flag

61)[深育杯 2021]find_flag(栈全开)



发现格式化字符串漏洞,先用该漏洞寻找输入的字符串的偏移:发现是 6

之后思考我们需要干什么,1. 泄露 Canary 2. 利用泄露的返回地址和 PIE 去寻找基地址。我们可以先找偏移
由于开启了 PIE,我们不可以在 main 函数下断点,我们把断点下到 printf 处:

printf 能直接断是因为它是 libc 的符号,gdb 从系统就能提前知道并 pending 等待加载;
而 main 函数是你 PIE 二进制里的,gdb 在加载前不知道基址,所以函数名无效。我们不能在 main 函数下断点
我们在 printf 处下断点之后一直单步运行直到可以输入为止,输入%p%p%p 之后查看栈情况:

此时 rax 处偏移为 6,之后递增,可知 Canary 处偏移为 17,返回地址的偏移为 19。当我们泄露了返回地址之后,就可以去 IDA 中寻找返回地址的偏移值:

我们要寻找 call sub_132F 后的返回地址,他的偏移为 0x146F。我们可以利用这个来计算基地址。
有了基地址之后我们就可以去寻找后门函数:发现偏移为 0x122E,还要预留 system 的位置

当我们获取完需要的信息之后,就可以利用第二个 gets 函数进行栈溢出获取 shell 了。
1 | from pwn import* |

用 aaa 把两个地址隔开,之后自己去截取 canary 和返回地址就可以了。

计算出 base_addr:利用 ret_addr - base_addr 得到偏移,那么就可以确定随机地址
随机地址+固定偏移就是准确地址
1 | from pwn import * |


更新: 2026-05-22 21:00:21
原文: https://www.yuque.com/idcm/wnemg9/owph8w5gdwfergf9