绕过防御之击败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
strcat
strcat(format, "!\n"); 是 C 语言中 字符串拼接函数 的调用,作用是将字符串 "!\n" 追加到字符数组 format 的末尾,拼接后 format 会包含原内容 + 感叹号 + 换行符
典型canary保护
v3 = __readfsqword(0x28u); return __readfsqword(0x28u) ^ v3;
这两行代码是 GCC 编译器为 x86-64 架构生成的 Canary(栈保护)校验逻辑,核心作用是 检测栈是否被非法篡改(如栈溢出),是 CTF Pwn 中绕不过的 “栈保护屏障”。
不知道为什么main下不了断点,我们就在printf处下断点吧
直接找到canary,然后计算偏移
在 GDB(尤其是搭配 pwndbg 插件)中,fm 是 fmtarg 命令的缩写,核心作用是 快速定位格式化字符串漏洞的参数偏移(即确定 %n$p 中的 n 是多少),是 CTF 中分析格式化字符串漏洞的高频工具。
确定gets的偏移
直接泄露exp
1 | from pwn import * |
exp详解
1 | p.sendlineafter("Hi! What's your name? ", '%17$llx, %19$llx') # 发送格式化字符串: |
更新: 2026-01-26 15:47:24
原文: https://www.yuque.com/idcm/wnemg9/owph8w5gdwfergf9