☰
Linux二进制安全防护机制与PWN绕过实战:NX、ASLR、PIE、Canary、RELRO全解析
2026/10/19 10:46:16 网站建设 项目流程

最近在整理某实验室的靶机环境时,遇到一个很有意思的现象:同一个栈溢出漏洞,放在不同版本的编译器环境下,利用难度差着好几个量级。有的题目一眼就能看出改返回地址就能拿到权限,有的题目却连栈地址都摸不到。这背后的差异,就是Linux安全防护机制在起作用。

PWN方向玩到一定阶段,很多人会卡在“知道有防护,但不知道怎么绕”这个坎上。网上文章大多零散地讲某个单一机制的绕过思路,很少有人把Linux平台下的整条防护链路讲透。本文打算把这块拼图补齐:从NX、ASLR、PIE、Canary、RELRO这些基础机制的原理出发,结合CTF里常见的利用手法,把攻防双方是怎么一层层博弈的完整拆开。适合刚接触PWN不久、已经会写简单栈溢出exp但面对全防护题目无从下手的读者,也适合想系统梳理Linux二进制防护知识体系的人。

1. 为什么搞PWN必须先搞懂防护机制

1.1 防护机制不是“开关”,而是一套分层防线

很多新手有个误区:以为安全防护就是某个编译选项,开了就完事。实际上,Linux平台下的防护体系是从CPU硬件、内核态、动态链接器到编译器层层叠加的。每一层解决一类具体问题。

CPU层面,页表项里有NX位(No-eXecute),标记某块内存能不能执行代码,这是硬件级的NX支持。内核层面,ASLR负责对栈、堆、共享库、程序基址做地址随机化;动态链接器层面,RELRO决定GOT表能不能被改写;编译器层面,Canary在函数栈帧里插入哨兵值,FORTIFY则对危险的库函数调用做编译期拦截。

这一套层级设计很有意思:它不是为了把漏洞彻底消灭(这也不现实),而是提高利用成本。一次完整的漏洞利用,往往需要同时突破好几层防护,哪一层没绕过去,整个攻击链就断了。从PWN选手的角度看,防护机制本身就是题目设计的一部分——你看懂了几层,就知道这道题该用什么思路打。

1.2 从checksec输出说起

PWN里最常用的第一步操作就是跑checksec。某个CTF比赛的经典题,刚拿到checksec输出的时候,大概是这样的:

Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled

四行输出,每行都代表一个机制的状态。很多新手跑完checksec就懵了:全开着,栈溢出还有得打吗?其实恰恰相反,每一条防护机制都只是提高了门槛,并不代表不可利用。比如“Canary found”不代表你不能溢出,而是溢出时不能弄脏金丝雀;“PIE enabled”不代表你找不到地址,而是需要先想办法泄露基址。

理解这些机制,本质上就是在理解“漏洞利用的约束条件”。一个成熟的PWN选手看到checksec输出,心里基本就有了攻击方向的草稿。下面把每一层防护单独拎出来拆解。

2. NX与栈不可执行:最基础的拦路虎

2.1 NX的原理:CPU在帮你检查“这是数据还是代码”

NX,全称No-eXecute,在x86体系里也叫XD bit(Execute Disable)。它的本质是页表项中的一个标志位:如果一个内存页的NX位被置位,CPU取指令时就不会从这块页里取,一旦尝试执行就会触发分段错误。

用生活化的方式理解:程序运行时,内存里的数据分两类——代码段是“可以念的经文”,栈和堆是“随手记的草稿纸”。NX机制就是在草稿纸上盖了个“禁止印刷”的章,就算你把攻击代码抄在草稿纸上,CPU也拒绝拿它当指令执行。

在PWN出现之前,经典的利用方式是直接把shellcode塞进栈上溢出的区域,然后跳到栈地址执行。NX开启后这条路被彻底堵死。这是最基础的一道防线,也是目前几乎所有Linux二进制默认开启的防护。

那么问题来了:栈不能执行代码了,程序自身代码(text段)里的gadget还能执行吧?这就引出了ret2libc和ROP体系——用程序自己的代码块拼出攻击逻辑,而不是注入新代码。

2.2 怎么判断NX开没开

checksec是最直观的方式。如果要手动确认,可以用readelf查看ELF的段信息:

readelf -l ./target

重点看GNU_STACK段的Flags。正常开启NX时,Flags那行是RW——注意,只有R和W,没有E。如果显示RWE,说明栈段同时具备读、写、执行权限,那就是关闭了NX。

有个细节值得注意:有些老旧教程会告诉你“x86架构下NX不可靠,因为CPU不支持”,这已经是过时信息了。主流的x86_64平台都支持硬件NX,再加上Linux内核从4.x之后都默认开启(依赖CONFIG_X86_SMAP、CONFIG_X86_UMIP等配置的综合效果),现在的实战环境基本默认NX是开着的。

2.3 NX下的利用思路转变:从注入代码到复用代码

NX强制要求攻击者改变思路。最基础的替代方案是ret2libc:通过覆盖函数返回地址,把程序的执行流劫持到libc库里的system函数,配合参数构造,直接执行system("/bin/sh")。

按这个思路走,利用要素变成三个:

  • 一个能控制返回地址的溢出点
  • libc里system函数、/bin/sh字符串的地址
  • 一个gadget或pop链来正确布置参数

这个转变意义重大——它把PWN从“写shellcode”变成了“找gadget、拼ROP链”。后面的PIE、RELRO这些防护,基本都是围绕“你找不到地址”“你改不了函数指针”这些角度继续给攻击者制造麻烦。理解了NX,就理解了PWN利用的主线思维:所有复杂的攻击链,本质都是在NX这条主线的基础之上做变种。

2.4 实操一个最小示例

模拟某个简单程序,栈溢出点在read函数读入0x100字节、缓冲区只有0x20字节,关闭PIE但开启NX。用gdb调试时,先确认溢出偏移量(这个场景通常是0x28,因为0x20缓冲区加0x8的保存的rbp),然后确定system和bin_sh在libc里的offset。

构造的骨架payload大致是这个逻辑:

payload = b'A' * 0x28 payload += p64(pop_rdi_ret) payload += p64(bin_sh_addr) payload += p64(system_addr)

这里有第一个实战心得:不要上来就找one_gadget。one_gadget虽然方便,但对寄存器约束敏感,很多场景下需要先调整栈布局。先把ret2libc的基础链路跑通,再谈优化,配合本地gdb的vmmap命令确认libc基址,是调试这类payload最有效的路径。

提示:每道题目用的libc版本不一样,system的偏移也不同。在本地调试时直接看vmmap里的libc加载基址,再算偏移;在远程环境里,如果服务器不给你libc文件,就需要先通过各种泄露手段把libc版本确认了,再匹配偏移。

3. ASLR与PIE:地址随机化到底在随机什么

3.1 先区分两个容易混淆的概念

ASLR和PIE经常被混为一谈,其实它俩的职责不同。

ASLR是全系统层面的地址随机化机制,由内核实现,控制映射到进程空间的各个段是否随机、随机到什么程度。Linux下通过/proc/sys/kernel/randomize_va_space控制,取值0到2:

值效果场景
0完全不随机调试或某些嵌入式环境
1随机化栈、共享库、mmap区域,但不随机程序基址早期默认配置
2在上面基础上,连程序自身的代码段基址也随机现代主流配置

PIE则是编译链接层面的特性,决定程序自身代码段、数据段能不能被随机装载。一个ELF如果以PIE方式编译,它的代码段会有一个随机的基址base,所有内部地址都基于这个base加上固定偏移。如果没有PIE,程序代码段从固定地址加载,通常是0x400000(x86_64)或0x8000(ARM)。

两者的关系可以这样理解:ASLR是“内核发牌”,PIE是“程序愿意被打乱”。只有两者同时生效,程序自身代码段的地址才会随机。很多CTF题目故意不开PIE,正是因为利用时需要固定的程序内地址,给选手降低难度。

3.2 为什么PIE让利用变难这么多

不开PIE时,程序内部的gadget、PLT表项、GOT表项的地址都是固定且已知的。攻击者拿到一个漏洞点,不需要额外泄露任何信息,直接就能用固定地址拼ROP链。

开了PIE后,代码段基址每次加载都不同。攻击者要使用程序内部的gadget,就必须先泄露一个“基于代码段基址寻址”的地址,然后算出基址,再算出目标地址。这一下就多了一步信息泄露的前置步骤。

有个很常见的操作:如果checksec显示PIE没有开,你就可以直接使用程序里plt地址来调用函数,而不需要通过libc来泄露。如果PIE开了,常见的信息泄露手法有几种:

  • 利用格式化字符串漏洞,读出某个栈上残留的函数返回地址
  • 利用溢出写一个地址到某个可以被打印的区域
  • 利用未初始化变量残留的指针值
  • 通过partial overwrite只改地址的低16位,从而跳转到邻近的gadget

其中partial overwrite是一个很经典的技巧。PIE虽然随机了基址,但页内偏移是稳定的。比如某个目标地址和已知地址处于同一个页内,攻击者只需要覆盖低12位甚至低8位,就能在保持高地址不变的情况下改写到目标位置。这个技巧在栈溢出中特别有用,缺点是调试时比较麻烦,容易覆盖错字节。

3.3 ASLR对栈和堆的影响

ASLR随机化的不只是代码段,栈和堆的起始地址同样在随机。这对利用的影响很深远:

  • 栈变量取地址后,每次运行都不一样,栈上的shellcode(即使NX关了)也不容易精准定位
  • 堆对象地址不固定,格式化字符串、堆溢出利用要做地址计算时会多一步泄露

CTF里做堆题时,地址泄露几乎成了标配步骤——先想办法把某个堆块的地址打印出来,再根据固定偏移推算出其他对象的地址。ASLR的本质,是把“已知地址”变成“需泄露地址”,这就解释了为什么PWN题里的信息泄露点(格式化字符串、越界读、UAF读)往往比写更值钱。

3.4 手动关闭ASLR做调试的方法

本地调试时,反复重启进程导致地址漂移会很烦。最常用的方式:

setarch $(uname -m) -R ./target

-R参数的意思是关掉ASLR的随机化,让每次运行的地址保持稳定。注意这个只对当前命令生效,不会影响全局配置。调试完记得回到正常环境,否则你开发的exp在随机化环境下可能跑不通。

另外一个细节:gdb里默认会关闭ASLR(这是为了调试方便),所以你在gdb里跑通的exp,不保证在正常shell下一定能跑通。解决办法是在gdb里执行set disable-randomization off来开启随机化,模拟真实环境。很多新手在本地验证exp时,因为gdb的保护反而忽略了ASLR的差异,结果一到远程就崩,这是很经典的坑。

提示:PIE和ASLR结合时,泄露地址的前提是控制程序输出某个已知点上的值。如果程序没有任何输出点,那就要考虑“盲打”路线,比如用已知偏移逐步探测、爆破低字节等办法,这类题目属于高难度PWN,实战里比较少见。

4. Canary(栈金丝雀):拦截栈溢出的哨兵

4.1 Canary机制的工作原理

Canary的名字来源于煤矿里的金丝雀——矿工带金丝雀下井,瓦斯泄漏时鸟先死,人就有时间逃。栈保护机制借鉴这个思路:在函数的栈帧里,缓冲区和高危数据(保存的rbp、返回地址)之间插入一个随机哨兵值,函数返回前检查哨兵值有没有被改动,一旦发现不对就立刻终止进程。

具体的存储位置,x86_64下是通过fs段寄存器的偏移访问的,通常是fs:0x28;i386下是gs:0x14。这个值在进程启动时从内核提供的随机数生成,存放在线程本地存储(TLS)的stack_guard字段里。

Canary的低字节恒为0x00,这是刻意设计的:字符串拷贝类操作(strcpy、gets等)遇到0x00会停止,所以攻击者用字符串溢出的方式无法直接改写整个Canary。这个细节直接影响了后面的绕过思路。

4.2 编译选项与识别方法

gcc的栈保护相关选项有三个级别:

选项行为
无选项 /-fno-stack-protector不插入Canary
-fstack-protector只对包含大缓冲区或特定危险函数的函数插入Canary
-fstack-protector-all对所有函数插入Canary
-fstack-protector-strong在-fstack-protector基础上扩大覆盖范围,对任何取地址的局部变量等也插入

CTF题目里,用-fstack-protector的是常态,用-fstack-protector-all的可以理解为全副武装。checksec的“Canary found”只能告诉你存在Canary,但具体是哪个级别,还是得靠逆向分析。

分析方法:把ELF丢进反汇编工具里,搜索__stack_chk_fail的调用点,就能定位哪些函数有防护。或者直接看函数开头有没有mov rax, qword ptr fs:[0x28]这样的指令。再看一遍所有函数,就能判断是all还是部分保护。

4.3 绕过思路一:精确泄露

最常规的绕过方案是把Canary的值先泄露出来,再在构造payload时原样填回。泄露的前提是存在越界读或格式化字符串漏洞。

比如利用格式化字符串时,Canary通常作为栈上某个参数被打印出来。你需要在调试时确认Canary在格式化串参数里的第几个位置,然后在payload里加上%n$p这样的格式符把它读出来。调试时可以用gdb的canary命令直接查看当前函数的canary值,但注意这只是帮你确认偏移位置,真实攻击时canary每次运行都不同。

一个实战中的细节:格式化字符串泄露出来的值,打印格式可能是16进制带前导0。而栈上Canary的值包含一个0x00字节,在打印时这个字节会被当成字符串终止符,所以泄露出来的地址往往丢失了低字节。处理办法是先把它按高字节补齐,再手工在内存里补上空字节。这个细节很多人第一次写exp时会卡住半天。

4.4 绕过思路二:爆破

Canary是一个随机值,但不同线程、不同进程之间,如果使用fork创建子进程——经典的CTF场景是服务端程序每accept一个连接就fork一次——Canary值在子进程间是共享的。因为fork继承了父进程的整个内存,包括TLS里的stack_guard。

这意味着什么?攻击者可以多次连接,每次都去爆破Canary的低字节。x86_64下Canary是8字节,低字节恒为0,剩下的7字节理论上要爆破7轮×256次。但实际题目里,很多时候只需要爆破1-2个字节(因为高字节往往和libc地址相关或者被其他途径泄露),配合fork场景就能在可接受的时间范围内拿到正确值。

32位程序的Canary只有4字节,爆破难度更低,有的32位题目甚至拿4次机会就能爆破成功。所以见到“每个连接fork一个进程还开Canary”的题目,先别慌,爆破可能是最稳妥的路。

4.5 绕过思路三:劫持TLS

既然Canary存放在TLS里,攻击者只要能改写TLS中stack_guard的值,就可以让检查永远通过。这在有任意地址写能力时是可行的。具体做法是先算出fs段对应地址(在x86_64下就是固定偏移的位置),往里面写一个可控值,然后正常溢出触发栈溢出时,检测会和你写入的值匹配,从而绕过。

这个思路的实战价值在于:当你面对的是一个强Canary保护的题目,同时存在堆写或格式化字符串写能力,可以优先考虑劫持TLS而不是费劲泄露Canary。这条路在部分堆利用里非常有用。

4.6 一个实操经验:返回值也在被保护

栈溢出的目标通常不只是覆盖返回地址,还可能是覆盖函数指针、局部变量。Canary只保护返回地址和帧指针,不会保护局部变量的越界写。所以如果在函数栈帧里有一个函数指针变量,攻击者不需要触碰Canary就能直接改写它。这在某些题目里是绕开Canary的捷径——绕过保护机制,而不是绕过检查逻辑。

用gdb调试时,可以用search指令在栈上搜索类似函数指针的地址,确认哪些变量可控。这种“不正面突破Canary,而是寻找未保护攻击面”的思路,在实战中比硬刚Canary更常见。

5. RELRO与延迟绑定:全局表保护的攻防

5.1 GOT与PLT的工作方式

要理解RELRO,先得搞明白ELF里GOT(Global Offset Table)和PLT(Procedure Linkage Table)的分工。

程序调用外部函数(比如puts、system)时,并不是直接用绝对地址去跳,而是通过PLT桩代码跳转到GOT表项,再由GOT表项里的地址完成跳转。这是因为动态链接器在程序启动时未必把所有函数地址都填好,采用延迟绑定(lazy binding)机制后,第一次调用外部函数时才去解析地址并写入GOT表。

这个机制本来是为了性能优化,但它引入了一个致命问题:GOT表在程序运行期间是可写的。攻击者如果能控制某个函数的GOT表项内容,就能把对puts的调用劫持成对system的调用——经典的GOT覆写攻击,在没有开RELRO的题目里几乎是最主流的利用手段。

5.2 RELRO的三种状态

RELRO(RELocation Read-Only)的作用就是把重定位相关数据段设为只读。它有三种状态:

状态GOT表权限特点
No RELRO完全可写最脆弱,GOT覆写随便用
Partial RELRO只读部分段,GOT保留可写仅将.dynamic等段设为只读,GOT仍可写,但部分高地址表项已只读
Full RELROGOT全部只读最安全,GOT不可改写;代价是启动时必须一次完成所有符号解析,性能略低

checksec里显示的“Partial RELRO”意味着GOT仍然有利用空间。很多比赛题刻意不开Full RELRO,就是想保留GOT覆写这条路。Full RELRO开启时,GOT覆写就彻底不可行了,攻击者需要寻找堆上的__free_hook、__malloc_hook(在新版本glibc中已经移除)、vtable指针等其他劫持点。

注意一个细节:Full RELRO和延迟绑定是互斥的。开启Full RELRO后,所有动态符号在启动时就被解析完毕,GOT表项全部填好,之后GOT段被置为只读。这也是为什么Full RELRO的程序启动稍慢一点的原因,但在安全上的收益是值的。

5.3 Partial RELRO下的GOT覆写

Partial RELRO的保护范围有限,GOT表前半部分(通常是程序自身的GOT项)仍然可写。经典的攻击流程是这样的:

  1. 确认目标GOT表项在可写范围内
  2. 利用漏洞(通常是格式化字符串写或栈溢出配合任意地址写)把GOT表项覆盖为目标函数地址
  3. 等程序下一次调用该函数时,跳转即被劫持

使用格式化字符串写GOT时,payload一般是构造%n写入需要改写的地址。这里有个常见错误:直接把system的绝对地址塞进格式化串前面,导致payload里有0x00截断。正确做法是利用fmtstr_payload工具自动生成带分隔的格式化令牌,或者手工分两次写入(先写双字节,再写高双字节)。

5.4 Full RELRO下的其他劫持点

Full RELRO意味着GOT这条路完全断了。这时候PWN题的攻击目标会转向以下位置:

  • __free_hook/__malloc_hook(低版本glibc):堆利用时通过UAF或双free覆盖hook指针,再调用malloc/free触发
  • libc里的_IO_2_1_stdout_结构:通过FSOP(File Stream Oriented Programming)伪造IO结构体链,控制程序输出路径
  • 函数指针(在全局或堆上):比如signal handler钩子、atexit函数列表
  • 寄存器状态劫持:完全走ROP链

新版本glibc(2.34往后)把__malloc_hook这些给移除了,堆题难度整体上升。替代方向是劫持tcache的key实现任意分配、劫持setcontext做栈迁移、利用_IO_list_all做FSOP。这些内容比基础GOT覆写复杂得多,但核心思路一致:GOT不能动了,就找其他“可写且会被程序执行流经过”的内存位置。

提示:做题时先确认glibc版本。版本决定了可用的hook点。很多PWN选手在Full RELRO的堆题里卡住,常常是因为用了不适用于当前glibc版本的攻击点。

6. FORTIFY与其他容易被忽略的防护细节

6.1 FORTIFY_SOURCE到底做了什么

FORTIFY_SOURCE是gcc的一个安全特性,在编译期结合-O优化级别,识别那些存在溢出风险的函数调用(strcpy、memcpy、sprintf等),替换为带边界检查的版本。例如strcpy会被替换为__strcpy_chk,printf可能被替换为__printf_chk。这些检查版本会在运行时校验目标缓冲区大小,如果发现操作超出边界就终止程序。

CTF里看到程序里出现了__printf_chk、__memcpy_chk这样的符号,就说明开启了FORTIFY。它的存在微妙地影响了漏洞利用:

  • 格式化字符串类的payload如果太长或格式符使用不当,可能被__printf_chk拒绝(检查格式串里是否包含%n等)
  • 栈溢出的缓冲区如果被拷贝操作触碰,可能在溢出前就被检查出来

绕过FORTIFY没有通用方法,需要结合具体函数分析检查逻辑。一个技巧是寻找那些没有被替换调用的函数——FORTIFY的检查是很“看脸”的,不是所有危险调用都能被编译器识别并替换。比如用sprintf但还是原版符号的情况,往往说明缓冲区大小参数没被编译器推断出来,意味着有可能绕过。

6.2 其他值得留意的防护机制

除了前面几个主流机制,Linux PWN还会遇到一些不那么显眼但同样致命的防护:

  • seccomp:通过系统调用过滤限制程序能做的事情。开了seccomp后不能用execve("/bin/sh"),得改用ORW(open/read/write)方案读取flag。
  • SELinux / AppArmor:强制访问控制,在CTF本地题里一般影响不大,但在真实环境利用中会限制文件读写路径。做题时如果发现flag读不到,可以考虑权限问题。
  • ASan / UBSan等运行时Sanitizer:不是加固机制,但比赛中偶尔遇到。ASan自带检测,会导致利用受到额外限制。
  • SMAP / SMEP:内核防御机制,虽然PWN用户态题目用不上,但研究内核提权时是核心绕不过去的防护。

seccomp在PWN题里越来越常见,需要掌握的工具有seccomp-tools检查过滤规则、mprotect系统调用配合ROP来动态调整内存权限。这类题目里,shellcode的写法要变成针对特定文件描述符的read/write循环,不再是无脑/bin/sh。

6.3 从防护组合反推利用策略

一道PWN题,checksec全部跑完能得出四行信息,但这四行共同决定了攻击方向。这里列一个常见组合的策略对照表:

防护组合(排除FORTIFY和seccomp)通用攻击策略
NX / 无Canary / 无PIE / Partial RELROGOT覆写或者ret2libc,怎么方便怎么来
NX / 有Canary / 无PIE / Partial RELRO先格式化字符串泄露Canary+libc,再ROP打GOT
NX / 有Canary / PIE / Full RELRO优先找信息泄露和任意读写,配合hook或FSOP
无NX / 无其他直接栈上shellcode,最复古的解法
NX + seccomp无法execve,只能做ORW,通常配合SROP

这个表是粗粒度的,真实题目还会有堆版本、溢出点位置、漏洞类型等变量。但方向上足够启发思路:每个防护机制都筛选掉了某些“直路”,逼迫攻击者切换到更复杂的路径。PWN题的本质,就是在这些筛选条件下寻找依然可行的那条路。

7. 真实靶机场景推演:四行输出背后的完整攻防

7.1 虚构一个典型题目环境

假设某模拟项目X提供了一个易受攻击的Web服务后端,同一个HTTP接口存在缓冲区溢出漏洞。checksec输出为:

Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled FORTIFY: Enabled

很典型的“全绿”配置,在真实CTF里属于中高难度。拿到这个输出后,攻防推演按如下顺序展开:

第一步,Canary存在,栈溢出不能直接覆盖返回地址,必须先找信息泄露。网上已知该服务的日志模块存在格式化字符串漏洞,可以把Canary和某个PLT地址读出来——这是后续所有exp的基础。

第二步,通过格式化字符串泄露栈上的Canary值和程序的某个GOT表项地址。GOT表项地址能推导出程序基址,Canary值用来在payload里填充回原来的位置。

第三步,程序是Full RELRO,GOT不可写,不能用GOT覆写。攻击目标定为libc里的__free_hook(假设glibc版本在2.34以下)。需要一个任意地址写原语,这里通过服务端另一个UAF漏洞配合double free拿到堆块分配,实现写hook。

第四步,把__free_hook覆盖为system,触发某个调用free的逻辑——服务端在解析完请求后会free掉某个结构体,把这个结构内容设为/bin/sh字符串,就能拿到shell。

每一步都对应一个防护机制的绕过,而且前面的泄露结果直接为后面的利用做支撑。这就是一道经典的全防护题攻击链。如果哪一步没做,整条链就断了,所以这类题对基本功的要求特别全面。

7.2 完成利用后的复盘要点

复盘时最容易忽略的是防护组合的“联动分析”。Full RELRO加上PIE会让泄露地址的需求量增加——你既需要libc地址又需要程序基址,这时寻找泄露点就尤其重要。Canary本身不难绕,难的是在泄露Canary的同时不弄丢其他关键信息,很多payload因为格式化串的长度限制会冲突,需要反复调整。

以这个虚构场景为例,最终的exp里其实用到了本文提到的几乎每一条机制:

  • NX开启 => 不使用栈上shellcode,改用ROP和libc函数
  • PIE开启 => 首先用格式化字符串泄露基址,再计算偏移
  • Canary开启 => 用泄露的canary值填充payload对应位置
  • Full RELRO => 放弃GOT覆写,转向堆hook
  • FORTIFY => 确认格式化字符串写入时没有被检查版本拦截(这里要注意%n的使用限制)

这种“checksec输出 -> 推导攻击流程”的思考模式,是PWN学习路上最核心的能力。它不是背教程,而是理解每个机制背后的攻防逻辑,然后在题目环境里灵活组合。当你拿到一个新靶机时,在脑海中能自动形成这么一张攻防推导图,就说明防护机制这一关真正过了。

7.3 最后分享几个实战中踩过的坑

第一个坑是忽略gdb和真实环境的ASLR差异。你在一台机器上调试好的地址,拿到另一台机器或者换一次libc版本就完全失效。PWN题换libc环境太常见了,用ldd确认远程libc版本,用调试器里的starti记录初始加载地址,这些步骤不能偷懒。

第二个坑是Canary泄露不全。前面提到格式化字符串泄露canary时会丢掉0x00字节,很多人拿到缺字节的值就直接塞回payload,结果程序在栈检查时直接挂掉。正确处理是在泄露值的高字节方向上手动补齐一个0x00到正确偏移位置。

第三个坑是以为Full RELRO是绝对的。RELRO保护的是GOT表本身,但如果你找到了一个还在可写段里的函数指针(比如某些全局结构体里的函数地址),它依然有利用价值。做题久了你会发现,安全机制从来不是单点防御,它只是让攻击者换一条更绕的路而已。

第四个坑是FORTIFY环境下的格式化字符串长度限制。__printf_chk对%n的使用有严格限制,payload构造时如果触发检查,程序会直接abort,连利用的机会都没有。遇到这种情况,可以考虑避开%n改用其他写入方式,或者寻找没被FORTIFY替换的旧调用点。

这些经验初看都是些小细节,但每一个都曾经让exp在关键时刻崩掉。安全研究就是这样:大部分时间在绕路,小部分时间在聚焦细节,而细节往往决定了整条攻击链的成败。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询