54、内存逆向(三):内存保护机制(ASLR、DEP、SafeSEH、CFG)
兄弟们,咱们继续聊内存逆向。前两章我们把内存布局和堆栈机制摸了个透,今天要聊的,是操作系统为了防我们这些逆向工程师,搞出来的几道「铁栅栏」——ASLR、DEP、SafeSEH、CFG。
说实话,我刚入行那会儿,这些机制还没这么普及。那时候写个溢出 exploit,直接往栈上怼 shellcode 就行,简单粗暴。现在?你试试看。系统会把你安排得明明白白。嗯,这就是进步,也是我们必须要面对的挑战。
一、ASLR——让地址「随机化」
ASLR,全称 Address Space Layout Randomization。说白了,就是每次程序启动,系统把代码段、数据段、堆、栈、DLL 的基址都随机打乱。
为什么要这么做?
你想啊,以前写 exploit,最爽的就是知道 kernel32.dll 一定加载在 0x7C800000。我直接硬编码地址跳过去就行。ASLR 一开,每次启动地址都不一样,你硬编码的地址就废了。
核心要点:ASLR 让攻击者无法预测目标地址,从而大幅提升利用难度。
我在项目中遇到过一件事。有一次分析一个老旧的第三方组件,它内部硬编码了某个 DLL 的地址。结果在 Win10 上跑,ASLR 一开,直接崩。这就是典型的「没考虑 ASLR」的坑。
ASLR 的强度分级
| 级别 | 说明 | 常见场景 |
|---|---|---|
| 0 (关闭) | 不随机化 | 调试阶段、兼容性模式 |
| 1 (部分) | 仅随机化 DLL 和 EXE | 旧系统、部分嵌入式 |
| 2 (完全) | 堆、栈、PEB、TEB 全部随机 | Win8+ 默认 |
我的习惯:逆向时先用 !process 0 0 或 lm 命令查看模块基址,确认 ASLR 是否生效。如果基址每次调试都变,那就是开了。
二、DEP——不让数据段「执行」
DEP,Data Execution Prevention。名字很直白——数据段不允许执行代码。
以前我们写 exploit,最喜欢把 shellcode 塞到栈上或者堆上,然后跳过去执行。DEP 一开,你往栈上写再多机器码也没用,CPU 直接给你个异常:访问违例。
为什么会这样?
因为 DEP 利用了 CPU 的 NX(No-Execute)位。每个内存页都有一个标志位,标记它是否可执行。栈和堆默认是「可读写但不可执行」的。你强行跳过去,系统直接翻脸。
注意:DEP 不是万能的。它防的是「直接执行数据段代码」,但防不住 ROP(Return-Oriented Programming)。ROP 利用的是已有的代码片段,不是新写的 shellcode。这个我们后面会细讲。
我记得有一次逆向一个恶意软件,它为了绕过 DEP,用了 VirtualProtect 把栈改成可执行。嗯,这招很老套,但确实有效。不过现在很多杀软会监控 VirtualProtect 的调用,你一动它就报警。
三、SafeSEH——保护异常处理链
SafeSEH 是专门针对 SEH(Structured Exception Handling)攻击的防护机制。
SEH 是什么?简单说,就是程序出异常时,系统会沿着一个链表找异常处理函数。攻击者可以覆盖这个链表里的某个节点,把地址指向自己的 shellcode。程序一崩,shellcode 就执行了。
SafeSEH 的做法是:在编译时生成一个合法的异常处理函数列表。运行时,系统只允许跳转到列表里的地址。你覆盖的地址不在列表里?对不起,直接终止。
关键点:SafeSEH 只在编译时启用 /SAFESEH 选项的模块上生效。如果某个 DLL 没开 SafeSEH,它仍然是攻击目标。
我建议你在逆向时,用 !load 和 !seh 命令查看当前进程的 SEH 链。如果发现某个模块的异常处理函数地址不在合法列表里,那基本可以断定——有人在搞事情。
四、CFG——控制流防护
CFG,Control Flow Guard,是微软在 Win8.1 和 Win10 上推的更狠的机制。
它的思路很简单:程序里所有间接调用(比如通过函数指针、虚函数表调用)都必须经过一个「校验点」。系统维护一个白名单,只有白名单里的地址才能被调用。你篡改了函数指针?系统在校验点就拦住了。
说白了,CFG 是在编译器和操作系统层面,给控制流上了一把锁。
实战技巧:逆向时如果遇到 __guard_check_icall 或 ntdll!LdrpValidateUserCallTarget,那就是 CFG 在干活。你可以尝试绕过它,但说实话,难度很大。我一般会先看看目标程序是不是全开 CFG,如果是,我会换个思路——比如找未受保护的模块。
五、四道防线的关系与实战思路
这四道防线不是孤立的。它们层层叠加,构成了现代 Windows 的内存防护体系。
我画了一张图,帮你理清它们的关系:
你看,从 ASLR 到 CFG,一层比一层狠。ASLR 让你找不到地址,DEP 不让你执行代码,SafeSEH 堵死了异常处理这条路,CFG 连间接调用都给你管死了。
那逆向工程师怎么办?
我的经验是:不要硬刚。找薄弱环节。比如:
- 有些旧模块没开 ASLR,基址固定,可以作为跳板。
- 有些程序没开 CFG,间接调用随便改。
- 有些第三方 DLL 没开 SafeSEH,SEH 攻击仍然有效。
核心思路:逆向不是要突破所有防线,而是找到那条「没锁上的门」。
六、如何检测这些保护机制
实战中,我一般用以下几个方法快速判断目标程序开了哪些保护:
- PE 头检查:用
dumpbin /headers或PE-bear查看DLL Characteristics字段。如果IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE置位,说明开了 ASLR;IMAGE_DLLCHARACTERISTICS_NX_COMPAT置位,说明开了 DEP。 - SafeSEH 检查:看 PE 头里有没有
LOAD_CONFIG目录,以及SEHandlerTable和SEHandlerCount是否有效。 - CFG 检查:看导入表里有没有
__guard_check_icall,或者用!process 0 0查看ControlFlowGuard标志。
一个小技巧:用 Process Hacker 或 x64dbg 的插件,可以直接看到每个模块的保护状态。省得自己手动翻 PE 头。
七、避坑指南
我曾经踩过一个坑。有一次逆向一个老程序,它没开 ASLR,但开了 DEP。我心想:好办,ROP 走起。结果折腾了半天,发现它连 SafeSEH 都没开,但异常处理函数里有个 call [ebp+arg0] 的指令。我直接覆盖 SEH 链,跳过去就执行了。嗯,有时候最简单的办法反而最有效。
所以我的建议是:
- 不要一上来就搞 ROP,先看看有没有更简单的路。
- 不要只看主模块,第三方 DLL 往往是突破口。
- 不要迷信「全开保护」——总有没开的地方。
最后提醒一句:这些保护机制是动态的。同一个程序,在 Win7 和 Win10 上表现可能完全不同。调试前,先确认目标系统的版本和补丁级别。
好了,这一章的内容就到这儿。内存保护机制这块,说白了就是「道高一尺,魔高一丈」。系统在加固,我们也在进步。理解这些机制,不是为了绕过而绕过,而是为了更深入地理解系统的工作原理。
下次见。