17、运行时防御机制:地址空间布局随机化、数据执行保护、安全异常处理

各位同学,今天我们来聊聊运行时防御机制。说白了,就是程序在跑起来之后,操作系统怎么帮我们挡子弹。

我刚开始做嵌入式开发那会儿,对这类机制其实不太上心。总觉得「我代码写得稳,用不着这些花架子」。直到有一次,一个缓冲区溢出漏洞让攻击者直接拿到了 shell 控制权……嗯,从那以后,我再也不敢轻视运行时防御了。

这一章,我们重点讲三个东西:ASLR(地址空间布局随机化)DEP/NX(数据执行保护)、以及安全异常处理(SEH)。它们三个配合起来,能挡住绝大多数内存破坏类攻击。

17.1 地址空间布局随机化(ASLR)

ASLR 的核心思想很简单:每次程序启动时,把代码段、堆、栈、动态库的基地址随机化。攻击者想利用缓冲区溢出跳转到某个固定地址?对不起,地址每次都不一样。

你想想看,如果没有 ASLR,攻击者只要分析出 libc 的基地址是 0xb7e00000,他就能精确构造 ROP 链。有了 ASLR,这个地址每次启动都变,攻击难度直接上了一个数量级。

ASLR 的随机化粒度:

  • 库/可执行文件基址:每次启动随机偏移
  • 栈基址:每次启动随机偏移
  • 堆基址:每次启动随机偏移
  • mmap 区域:每次启动随机偏移

我个人习惯在 Linux 上检查 ASLR 是否开启:

# 查看 ASLR 级别
cat /proc/sys/kernel/randomize_va_space

# 返回值含义:
# 0 = 关闭
# 1 = 部分随机化(栈、mmap、共享库)
# 2 = 完全随机化(包括堆)

我在项目中遇到过一个问题:某个嵌入式 Linux 系统为了「性能优化」,把 ASLR 关了。结果一个原本无害的栈溢出漏洞,直接变成了远程代码执行。嗯,从那以后,我每次做安全审计,第一件事就是检查 randomize_va_space 的值。

注意:ASLR 不是万能的。如果攻击者能通过信息泄露(比如格式化字符串漏洞)拿到当前进程的内存布局,ASLR 就被绕过了。所以 ASLR 必须配合信息泄露防护一起用。

17.2 数据执行保护(DEP/NX)

DEP 的全称是 Data Execution Prevention,在 x86 上叫 NX(No-Execute)位,在 ARM 上叫 XN(Execute Never)。它的逻辑更直接:数据区不允许执行代码

你想想看,传统的栈溢出攻击,攻击者把 shellcode 塞到栈上,然后跳转到栈上执行。DEP 一开,栈页面的 NX 位被置位,CPU 直接触发异常,shellcode 根本跑不起来。

在 Linux 上,我们可以用 readelf 检查可执行文件是否启用了 NX:

readelf -l your_program | grep GNU_STACK

# 输出示例:
# GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x10
#                                                          ^^^
# RWE = 可读可写可执行(NX 关闭)
# RW  = 可读可写(NX 开启)

如果看到 RWE,说明栈是可执行的——这基本等于给攻击者开了绿灯。我建议所有生产环境的程序,都必须确保栈是 RW 而不是 RWE

避坑指南:我曾经在交叉编译时,链接器脚本里忘了加 -z noexecstack 选项,导致 ARM 板子上的程序栈是可执行的。安全扫描工具一跑就报警。后来我在 Makefile 里统一加了:

LDFLAGS += -Wl,-z,noexecstack

从此再没出过这个问题。

17.3 安全异常处理(SEH)

SEH(Structured Exception Handling)是 Windows 上的异常处理机制。攻击者经常利用 SEH 覆盖来劫持控制流——把异常处理函数的指针改成攻击者控制的地址,然后故意触发异常,就能跳转到 shellcode。

Windows 从 XP SP2 开始引入了 SEHOP(SEH Overwrite Protection),在 Vista 之后又加入了 SEH 验证机制。核心思路是:在异常处理链的末尾加一个「哨兵」记录,如果链被破坏(指针被覆盖),系统就拒绝执行异常处理。

在 Linux 上,类似的机制是 sigaction 的信号处理函数。如果攻击者能覆盖信号处理函数的指针,也能劫持控制流。不过 Linux 内核在 2.6 之后加入了 SA_SIGINFO 的验证,加上 ASLR 和 NX 的配合,这类攻击已经很难成功了。

安全异常处理的最佳实践:

  • Windows:确保编译时启用 /SAFESEH/DYNAMICBASE
  • Linux:使用 sigaction 而不是 signal,并检查返回值
  • 通用:不要在异常处理函数中执行复杂逻辑,只做最小化的清理和日志

17.4 三种机制的协同工作

这三种机制不是孤立的。它们配合起来,能形成纵深防御:

  • ASLR 让攻击者猜不到地址
  • DEP/NX 让攻击者无法在数据区执行代码
  • 安全异常处理 防止攻击者劫持异常处理流程

举个例子:攻击者想用 ROP(Return-Oriented Programming)绕过 DEP。他需要知道 libc 中 gadget 的地址。ASLR 让这些地址每次启动都变。如果他还想通过 SEH 覆盖来触发异常,安全异常处理又会拦住他。三层防护叠在一起,攻击成本就高得多了。

下面这张图展示了这三种机制在内存布局中的位置和作用:

运行时防御机制协同工作示意图 栈(Stack) 局部变量、返回地址、SEH 链 DEP/NX:禁止执行 堆(Heap) 动态分配的内存 ASLR:基址随机化 数据段(.data/.bss) 全局变量、静态变量 代码段(.text) 防御机制说明 🔀 ASLR(地址空间布局随机化) 栈、堆、共享库基址每次启动随机化 防止攻击者预测内存地址 🚫 DEP/NX(数据执行保护) 栈、堆、数据段标记为不可执行 阻止 shellcode 在数据区执行 🛡️ 安全异常处理(SEH) 验证异常处理链的完整性 防止 SEH 覆盖攻击 三层防护叠加 → 纵深防御 攻击者需要同时绕过三层才能成功

17.5 实际项目中的配置建议

说了这么多理论,咱们来点实际的。我在项目中总结了一套配置清单,分享给大家:

防御机制 Linux 配置 Windows 配置 检查方法
ASLR randomize_va_space = 2 /DYNAMICBASE 链接选项 cat /proc/sys/kernel/randomize_va_space
DEP/NX -z noexecstack 链接选项 /NXCOMPAT 链接选项 readelf -l 检查 GNU_STACK
安全异常处理 使用 sigaction 替代 signal /SAFESEH 链接选项 检查异常处理链完整性

重要提醒:这些机制只是增加了攻击难度,不是绝对安全。如果代码本身有逻辑漏洞(比如格式化字符串、整数溢出),攻击者仍然可能绕过这些防御。所以,写好安全的代码才是根本。

好了,这一章的内容就到这里。运行时防御机制是操作系统层面的最后一道防线,但千万别觉得有了它们就可以随便写代码。我见过太多人因为「反正有 ASLR」就放松了警惕,结果被信息泄露漏洞一锅端。记住:纵深防御,每一层都要做好


公众号:蓝海资料掘金营,微信 deep3321