【学习记录】Week1:深入理解二进制安全防护机制 (NX/Canary/PIE/RELRO/ASLR) 写在前面在上一篇博客中我们学会了使用checksec工具来查看程序开启了哪些保护机制。但知其然还要知其所以然如果不理解这些保护机制的底层原理在实战中就根本不知道该如何绕过它们。本文将详细记录我对 Linux 下五大核心安全机制NX、Canary、PIE、RELRO、ASLR原理的学习与总结。 目录为什么需要安全机制NX (No-eXecute) - 不可执行保护Canary - 栈溢出金丝雀保护ASLR - 地址空间布局随机化PIE - 程序基址随机化RELRO - 重定位只读保护总结与对抗思路1. 为什么需要安全机制在早期的操作系统和编译器中内存区域没有执行权限限制栈和堆的地址也是固定的。这导致黑客可以通过栈溢出将恶意代码直接注入到栈上并跳过去执行。为了遏制日益猖獗的漏洞利用操作系统和编译器厂商引入了一系列的防御机制。可以说PWN的发展史就是一部攻击者与防御机制对抗的史诗。2. NX (No-eXecute) - 不可执行保护️ 原理NXWindows 下称为 DEPData Execution Prevention将内存区域严格划分为了“可读可写”和“可读可执行”两类。数据区如栈、堆、.bss段可读可写但不可执行。代码区如.text段可读可执行但通常不可写。当程序开启 NX 后如果你把 Shellcode 写到栈上并尝试跳转过去执行CPU 会触发段错误并崩溃。 检测在checksec中显示为NX enabled。⚔️ 对抗思路不能直接执行栈上的代码了怎么办攻击者发明了ROPReturn-Oriented Programming面向返回编程。既然代码段本身是可执行的我们在代码段中寻找以ret结尾的微小代码片段称为 Gadget通过在栈上布置一系列 Gadget 的地址将它们拼接起来实现复杂的操作。3. Canary - 栈溢出金丝雀保护️ 原理“金丝雀”这个名字来源于早期煤矿工人下井时带上的金丝雀用于检测瓦斯泄漏。在函数调用发生时栈的结构通常为局部变量 - 保存的EBP - 返回地址。如果发生栈溢出由于是从低地址向高地址覆盖要覆盖返回地址必然会先覆盖前面的区域。Canary 机制会在函数序言阶段从内存的某个特殊区域如fs:0x28取出一个随机的 Cookie 值插入到局部变量和 EBP/返回地址之间。在函数返回前检查这个 Cookie 是否被改变。如果改变了说明发生了栈溢出程序会立刻调用__stack_chk_fail终止运行。小细节Canary 的最低位字节通常是\x00NULL字节这样可以阻断strcpy等字符串拷贝函数的提前截断迫使攻击者必须完整覆盖。 检测在checksec中显示为Canary found或Stack Canary found。⚔️ 对抗思路信息泄露如果能通过格式化字符串漏洞或读越界漏洞把 Canary 的值打印出来我们就可以在 Payload 中原封不动地填入正确的 Canary从而绕过检查继续覆盖返回地址。逐字节爆破在fork进程中子进程的 Canary 与父进程相同可以通过暴力枚举爆破出来。4. ASLR - 地址空间布局随机化️ 原理ASLR 是操作系统级别的保护机制。在程序每次运行时操作系统会将程序的堆、栈、共享库如 libc.so的加载基址进行随机化偏移。如果没有 ASLR黑客知道 libc 中system函数的地址就可以直接在 Payload 中硬编码这个地址。开启 ASLR 后每次运行时 libc 的基址都在变硬编码的地址就失效了。 检测ASLR 是系统层面的checksec默认不显示。可以通过以下命令在 Linux 中查看 ASLR 状态cat /proc/sys/kernel/randomize_va_space0关闭1部分随机化共享库、栈等随机化2完全随机化增加堆的随机化默认状态⚔️ 对抗思路ASLR 的致命弱点是只有程序重新运行基址才会变在一次运行中基址是不变的。所以对抗的核心是泄露地址。只要能在同一次运行中泄露出一个位于 libc 中的函数地址就可以通过偏移量计算出 libc 的基址进而算出system和/bin/sh的地址。5. PIE - 程序基址随机化️ 原理ASLR 负责随机化系统层面的库、栈、堆。而 PIE 是编译器级别的保护用于随机化程序自身的代码段和数据段.text, .data, .bss 等。如果程序没开 PIENo PIE程序自身的函数地址在编译后就固定了例如 64 位下默认从0x400000开始。开启了 PIE 后程序自身的基址也会在每次运行时随机化。 检测在checksec中显示为PIE enabled。注意如果显示No PIE (0x400000)说明程序基址固定。⚔️ 对抗思路和 ASLR 一样需要泄露。只要泄露程序自身的一个代码地址就能算出程序的运行基址从而定位程序内部的后门函数或全局变量。6. RELRO - 重定位只读保护️ 原理在动态链接的程序中为了延迟绑定提高运行效率程序在第一次调用外部函数如printf时才会去 libc 中找到真实地址并填入 GOT 表中。GOT 表是可写的这就导致了一个致命漏洞如果黑客能通过任意地址写把 GOT 表中printf的地址改成system的地址那么下次程序调用printf时实际调用的就是system。RELRO 就是为了保护 GOT 表Partial RELRO部分开启将包含解析器的一些内部指针如.init_array,.fini_array等设为只读。但 GOT 表依然可写。安全性较低。Full RELRO完全开启在程序启动时就强制解析所有动态链接函数的地址填入 GOT 表后将 GOT 表所在内存页设置为只读。这样就无法进行 GOT 表覆写了。 检测在checksec中显示为Full RELRO或Partial RELRO。⚔️ 对抗思路如果开启了 Full RELROGOT 表覆写这条路就断了。我们通常转向使用__malloc_hook、__free_hook高版本 glibc 已移除或者伪造 IO_FILE 结构以及利用栈上的 ROP 链来进行攻击。7. 总结与对抗思路备忘录至此Week1 的所有内容结束。掌握这五大保护机制是迈向实战 PWN 的必经之路。我整理了一个简单的表格方便以后复习保护机制保护对象核心原理常见绕过手法NX/DEP栈/堆数据数据区不可执行ROP 链构造Canary返回地址在栈底插入随机校验值泄露 Canary 值 / 爆破ASLRlibc/栈/堆每次运行系统库地址随机泄露 libc 基址算偏移PIE程序自身代码/数据每次运行程序自身基址随机泄露程序运行时代码地址Full RELROGOT 表启动时解析并设 GOT 表只读改写 hooks 或伪造 IO_FILE如果本篇文章对您有帮助请点赞收藏支持一下感谢阅读