26、地址空间布局随机化:ASLR原理,对编译链接的影响,PIE与PIC
各位好,我是老刘。今天咱们聊一个跟系统安全紧密相关的话题——ASLR。说白了,就是操作系统怎么让程序的内存地址“捉摸不定”。
我记得刚入行那会儿,写了个简单的缓冲区溢出demo,在实验室的Linux机器上跑得挺欢。结果拿到同事的机器上,同样的攻击代码就失效了。我当时还纳闷:难道我代码写错了?后来才知道,是ASLR在“捣鬼”。
嗯,咱们今天就把它彻底讲透。
ASLR是什么?为什么需要它?
先问一个问题:如果没有ASLR,会发生什么?
你想想看,每次运行一个程序,它的代码段、数据段、栈、堆都加载到固定的地址。攻击者只要摸清一次布局,就能写出通用的攻击代码。比如经典的return-to-libc攻击,攻击者知道system()函数在libc里的固定偏移,再知道libc加载的基址,就能算出system()的实际地址。
ASLR就是来解决这个问题的。它让每次程序启动时,关键内存区域的基地址都随机化。攻击者就算知道偏移,也猜不到基址。
核心思想:让攻击者无法预测目标地址,从而大幅提升利用难度。
ASLR随机化了哪些区域?
我习惯把ASLR的随机化范围分成三层:
- 栈基址随机化——每次程序启动,栈的起始地址都不同
- 堆基址随机化——
malloc分配的内存区域,基址随机 - 共享库/可执行文件基址随机化——这是最关键的,也是PIE和PIC的用武之地
在Linux上,你可以通过/proc/sys/kernel/randomize_va_space查看当前ASLR等级:
# 0 = 关闭
# 1 = 部分随机化(栈、mmap基址)
# 2 = 完全随机化(包括堆)
$ cat /proc/sys/kernel/randomize_va_space
2
我个人建议,生产环境一定要开成2。曾经有个项目,测试环境忘了开ASLR,结果安全扫描直接报高危漏洞。
ASLR对编译链接的影响
这里有个关键问题:如果可执行文件本身加载地址是固定的,那ASLR只随机化库和栈,攻击者仍然可以攻击程序自身的代码段。所以,我们需要让可执行文件本身也支持地址随机化。
这就引出了两个重要概念:PIE 和 PIC。
PIE:位置无关可执行文件
PIE(Position Independent Executable)让可执行文件像共享库一样,可以在任意地址加载。编译时加-fpie -pie选项即可。
# 传统方式:固定地址
$ gcc -o hello hello.c
$ readelf -h hello | grep Type
Type: EXEC (Executable file)
# 可以看到Type是EXEC,加载地址固定
# PIE方式:位置无关
$ gcc -fpie -pie -o hello_pie hello.c
$ readelf -h hello_pie | grep Type
Type: DYN (Shared object file)
# Type变成了DYN,跟共享库一样
你看,加了PIE之后,可执行文件的类型从EXEC变成了DYN。这意味着它可以在任意地址加载,ASLR就能随机化它的基址了。
小提示:现代Linux发行版(如Ubuntu 18.04+)默认编译时已经加了-fpie -pie。你可以用checksec --file=hello来验证。
PIC:位置无关代码
PIC(Position Independent Code)通常用于共享库。它比PIE更彻底——不仅代码段可以重定位,数据段中的全局变量、函数指针等也能通过GOT(全局偏移表)和PLT(过程链接表)实现间接寻址。
编译共享库时,我们通常加-fPIC:
$ gcc -fPIC -shared -o libfoo.so foo.c
这里有个细节:-fPIC和-fpic的区别。前者生成更通用的代码(支持更多符号),后者生成更小更快的代码(但有符号数量限制)。我一般直接用-fPIC,省得踩坑。
PIE vs PIC:到底有什么区别?
很多初学者容易搞混。我画个表格帮你理清:
| 特性 | PIE | PIC |
|---|---|---|
| 适用对象 | 可执行文件 | 共享库 |
| 编译选项 | -fpie -pie |
-fPIC -shared |
| ELF类型 | DYN | DYN |
| 地址随机化 | 支持ASLR | 支持ASLR |
| 性能开销 | 较小(少一次间接跳转) | 稍大(GOT/PLT间接访问) |
| 典型场景 | 现代Linux可执行文件 | .so动态库 |
说白了,PIE是让整个程序“搬家”,PIC是让库代码“随遇而安”。
ASLR与编译链接的实战避坑
我曾经在一个嵌入式Linux项目上栽过跟头。当时为了性能,把ASLR关了(设置randomize_va_space=0),结果产品上线后安全扫描没过。后来我学乖了:
- 不要为了性能牺牲安全——ASLR的开销其实很小,现代CPU几乎感觉不到
- 检查编译选项——确保你的Makefile里加了
-fpie -pie(对于可执行文件) - 验证ASLR是否生效——写个小程序打印
main函数地址,多跑几次看看是否变化
#include <stdio.h>
int main() {
printf("main is at %p\n", main);
return 0;
}
// 多跑几次,观察地址是否变化
$ ./a.out
main is at 0x55a1e2c00149
$ ./a.out
main is at 0x55f8b3c00149 // 地址变了,ASLR生效
警告:如果你在调试时发现地址不变,先检查ASLR等级。可以用setarch `uname -m` -R ./a.out临时关闭ASLR来调试,但千万别在生产环境这么干。
知识体系总览
下面这张图帮你把本章的核心逻辑串起来:
从图上可以看得很清楚:ASLR随机化三个区域,其中代码/库的随机化依赖PIE和PIC技术。没有PIE,可执行文件本身就成了ASLR的“漏网之鱼”。
总结一下
ASLR不是什么高深莫测的东西,说白了就是让内存地址“打一枪换一个地方”。PIE和PIC是实现这个目标的两把利器。你写代码时可能感觉不到它们的存在,但它们在默默守护着系统的安全。
嗯,今天就聊到这儿。下次你编译程序时,不妨用readelf看看它的类型,再跑几次看看地址变不变——你会对ASLR有更直观的感受。
一句话记住:没有PIE的可执行文件,ASLR就是瘸腿的。
公众号:蓝海资料掘金营,微信deep3321