上一周在整理CTFshow pwn系列的前置基础,做到pwn 032这道题时,发现它把FORTIFY_SOURCE单独拎出来讲,还标了个Level 0。初看觉得很简单,可越往深挖越有意思——这玩意不是单纯的“防溢出开关”,它更像编译器在标准库函数调用上做的一道“尺寸校验关卡”。很多刚接触pwn的选手,checksec跑完只盯NX和PIE,完全忽略FORTIFY这一行,直到调试时发现strcpy被编译器偷偷换成了__strcpy_chk,程序一到溢出点就异常终止,这才知道还有这么个东西。
这篇文章适合两类人:一是刚入坑CTF pwn、还没系统性梳理过Linux用户态安全机制的萌新;二是已经有漏洞利用经验,但对编译器如何介入“安全检查”的底层逻辑不清楚的老人。我会从FORTIFY_SOURCE的原理讲起,拆开Level 0、1、2这三档差异,再给出一套能直接用的检测流程,最后聊一聊“开了FORTIFY之后,我们的利用思路应该怎么调整”。
1. FORTIFY_SOURCE 是来堵哪个窟窿的
1.1 所有问题的起点:C标准库不检查边界
C语言的设计哲学是信任程序员。strcpy、memcpy、sprintf、gets这些函数从诞生起就不负责校验目标缓冲区够不够大,写入越界属于“未定义行为”。这类函数在CTF里出现频率极高,栈溢出题的经典套路就是往一个固定大小的局部数组里塞超长输入,然后覆盖返回地址或栈上变量。
在早期Linux程序中,这种问题几乎裸奔。后来内核和用户态都引入了各种缓解机制,比如栈Canary、NX、ASLR、PIE,但这些都是“内存区域级别的防御”,在函数调用层并没有自动拦截。于是GCC和glibc联手搞出了FORTIFY_SOURCE,目标很直接:在编译器层面,对标准库函数里那些“写入长度可能超过对象大小”的调用做额外检查。
需要澄清一点,FORTIFY_SOURCE不是像NX那样让栈不可执行,也不是像Canary那样在函数序言里插入随机值。它的工作方式是:编译器在编译阶段尽量推导每个缓冲区对象的实际大小,然后决定是直接编译报警,还是把一个函数调用替换成带_chk后缀的安全版本,在运行时再去验证一次。
1.2 编译期检查与运行期检查,各自管什么
FORTIFY_SOURCE的检查分成两个阶段。
第一个阶段是编译期。如果编译器能够静态确定目标对象的大小,并且发现源数据长度明显超过了那个大小,它会在编译时输出一个warning,甚至在-Werror下直接编译失败。举例来说:
c复制#include <stdio.h>
#include <string.h>
int main(void) {
char dst[8];
strcpy(dst, "AAAAAAAAAAAAAAAAAAAA");
puts(dst);
return 0;
}
用下面的命令编译:
bash复制gcc -O2 -D_FORTIFY_SOURCE=2 -o demo demo.c
GCC会直接给出类似这样的警告:
text复制warning: '__builtin_memcpy' writing 20 bytes into a region of size 8 overflows the destination [-Wstringop-overflow=]
这个阶段很有价值:很多明显的栈溢出在编译阶段就暴露了,不需要等到运行时崩溃。
第二个阶段是运行期。当编译器无法静态确定长度,比如长度来自函数参数或外部输入时,它不会傻傻地放行,而是把调用替换成对应的_chk版本。以memcpy为例,__memcpy_chk的签名大致是:
c复制void *__memcpy_chk(void *dest, const void *src, size_t len, size_t destlen);
第四个参数destlen就是编译器推导出来的目标对象大小。运行时如果len > destlen,glibc直接调用__chk_fail终止进程,并把崩溃信息打出来。
c复制#include <stdio.h>
#include <string.h>
void write_buf(char *src, size_t n) {
char dst[8];
memcpy(dst, src, n);
puts(dst);
}
int main(void) {
char *payload = "AAAAAAAAAAAAAAAAAAAA";
write_buf(payload, 20);
return 0;
}
编译运行:
bash复制gcc -O2 -D_FORTIFY_SOURCE=2 -o demo2 demo2.c
./demo2
你会看到:
text复制*** buffer overflow detected ***: terminated
Aborted (core dumped)
程序直接消亡,你的payload根本没机会继续执行。
1.3 把它想成快递发货前的称重,而不是巡逻保安
很多人把FORTIFY_SOURCE和那几种缓解机制搞混,我习惯用一个类比:NX、ASLR、Canary是在程序运行期间布置的“巡逻保安”,时刻盯着内存区域
