讲讲 Linux 平台的漏洞缓解机制
1. 堆栈保护(Stack Smashing Protection, SSP)
这是最基础,也是最重要的堆栈溢出缓解机制
- 原理: 在函数调用时,编译器会在栈上的局部变量和返回地址之间插入一个随机的“金丝雀值”(Canary Value)
- 工作方式:
- 函数进入时,金丝雀值被推入栈中
- 函数返回前,程序会检查这个金丝雀值是否被改变
- 如果金丝雀值被修改,说明发生了缓冲区溢出,程序会立即终止(通常会调用
__stack_chk_fail
函数),而不是让攻击者控制程序流
- 局限性: 攻击者可以通过覆盖低地址的变量或利用其他漏洞(如格式化字符串漏洞)来泄露金丝雀值,从而绕过此保护
2. 地址空间布局随机化(Address Space Layout Randomization, ASLR)
ASLR 是一个非常有效的漏洞缓解机制,它让攻击者难以预测内存中关键数据的位置
- 原理: 每次程序启动时,ASLR 会将程序的主要内存区域(如可执行文件基址、堆、栈和共享库(DLL/SO))加载到随机的地址上
- 工作方式: 攻击者在利用漏洞时,通常需要知道某个函数(例如
system
函数)或某个数据(例如返回地址)的精确内存地址。ASLR 打破了这种确定性,使得攻击者无法在不知道这些地址的情况下构造 ROP 链(Return-Oriented Programming) - 局限性:
- 熵不足: 早期版本的 ASLR 随机化范围有限,攻击者可以通过暴力破解或多次尝试来绕过
- 信息泄露: 如果程序存在信息泄露漏洞(如格式化字符串漏洞),攻击者可以泄露出某个模块的基址,从而推算出其他所有函数的地址,绕过 ASLR
3. 不可执行内存(Non-Executable Memory)/ NX 位(No-eXecute)
这是为了防止攻击者将恶意代码注入数据段(如堆或栈)并执行而设计的
- 原理: CPU 的 MMU(内存管理单元)会根据内存页的权限来决定是否允许执行该页中的代码。NX 位被设置在页表项中,如果该位为 1,则该页不可执行
- 工作方式: 操作系统会将堆、栈等数据段标记为不可执行。当攻击者利用缓冲区溢出将 Shellcode(恶意代码)写入栈上并尝试执行时,CPU 会抛出异常,阻止代码的执行
- 局限性:
- ROP 攻击: ROP(Return-Oriented Programming)是一种绕过 NX 位的方法。攻击者不注入代码,而是通过利用程序本身已有的代码片段(称为“gadgets”),并精心构造返回地址链来执行恶意逻辑
- JIT(即时编译)代码: 对于一些需要动态生成和执行代码的应用程序(如 Java 或 JavaScript 引擎),它们需要创建可执行的内存区域,这可能会被攻击者利用
4. 只读重定位(Read-Only Relocations, RELRO)
RELRO 旨在保护程序在加载后不被修改,特别是全局偏移表(GOT)和过程链接表(PLT)
- 原理:
- GOT(Global Offset Table): 存储了外部共享库函数的实际地址
- PLT(Procedure Linkage Table): 负责将函数调用重定向到 GOT
- 工作方式:
- 部分 RELRO: 在程序加载时,
.got
段是可写的,因为加载器需要填充外部函数的地址。加载后,.got
段变为只读 - 完全 RELRO: 将 GOT 表完全设置为只读,所有重定位都在程序加载前完成
- 部分 RELRO: 在程序加载时,
- 局限性: 攻击者无法再利用 GOT 覆写漏洞来劫持程序流。然而,它并不能防御所有类型的攻击
5. 控制流完整性(Control Flow Integrity, CFI)
CFI 是一种更高级的保护机制,它旨在确保程序执行的控制流不会被攻击者劫持
- 原理: CFI 在编译和链接阶段为每个间接调用(如函数指针调用)和返回指令创建元数据,并在运行时检查这些元数据,确保控制流的跳转是合法的、预期的
- 工作方式:
- 向前边沿 CFI(Forward-Edge): 保护间接函数调用,确保函数指针只能跳转到其类型兼容的函数
- 向后边沿 CFI(Backward-Edge): 保护函数返回,确保返回地址不会被篡改
- 局限性: CFI 的实现较为复杂,并且可能引入性能开销。虽然能防御 ROP 攻击,但并不能防御所有类型的攻击,且可以被绕过