乐于分享
好东西不私藏

二进制安全-源码防护Fortify_Source

二进制安全-源码防护Fortify_Source

源码防护-fortify_source

Fortify Source 是什么

本质:GCC 的编译时/运行时缓冲区溢出检测机制,属于源码级保护(不是二进制层面的 NX/Canary 那种)

保护对象:标准库中的高风险函数(strcpy、memcpy、sprintf、gets 等),一般主要是和字符串相关的函数。

FORTIFY_SOURCE 是 GCC/glibc 提供的一种编译时 + 运行时联合防护机制,通过将危险的 C 库函数替换为带边界检查的安全版本(__*_chk 后缀函数),在编译期和运行期检测缓冲区溢出。

核心思想: 编译器在编译时尽可能推断出缓冲区大小,如果能在编译期判定溢出就直接报错;如果编译期无法确定,就插入运行时检查代码,在溢出发生的瞬间终止程序。

Fortify Source干了什么

要想知道Fortify Source干了什么,那就要先看看如果没有Fortify Source会发生什么

char buf[32];strcpy(buf, user_input);  //①memcpy(buf, src, len);    //②sprintf(buf, "%s", str);  //③read(fd, buf, 1024);      //④

先来看这段源码,毫无疑问的,①②③④这四条语句都有安全漏洞:

①完全没有长度检查,溢出

②len作为变量可能会>32

③str也可能长度>32

④1024>32毫无疑问的溢出

而编译器对这些代码没有任何反应,也就是说,对生成的代码不会做任何的边界检查

Fortify Source的工作机制

工作流程:

源代码:strcpy(buf, src)          │          ▼  ┌───────────────────────────┐  │ 预处理阶段(宏替换)         │  │ #include <string.h>       │  │ FORTIFY 宏将 strcpy 替换为  │  │ __builtin___strcpy_chk()  │  └───────────┬───────────────┘              │              ▼  ┌───────────────────────────────────┐  │ 编译阶段(GCC 内置函数处理)          │  │                                  │  │ GCC 尝试用 __builtin_object_size() │  │ 推断 buf 的大小                    │  │                                  │  ├──── 能确定大小 ────────────────────┤  │  │                               │  │  ├─ 编译期可判定溢出                │  │  │  → 编译错误/警告                │  │  │                               │  │  ├─ 编译期可判定安全                │  │  │  → 优化为普通 strcpy(零开销)   │  │  │                               │  │  └─ 编译期无法确定                 │  │     → 插入运行时检查               │  │     → 调用 __strcpy_chk()         │  │                                  │  ├──── 不能确定大小 ──────────────────┤  │  → 退化为普通 strcpy(无法保护)     │  └──────────────────────────────────┘

而工作流程中的三个关键组件:

┌───────────────────────────────────────────────────┐│                                                   ││  1. __builtin_object_size(ptr, type)              ││     GCC 内置函数,编译时推断对象的剩余大小              ││                                                   ││  2. 头文件中的 FORTIFY 宏(<string.h> 等)           ││     将标准函数替换为 __builtin___xxx_chk() 调用      ││                                                   ││  3. glibc 中的 __xxx_chk() 函数                    ││     运行时执行实际的边界检查                          ││                                                   │└───────────────────────────────────────────────────┘

原理详解

主要是对 __builtin_object_size()这个函数进行分析

__builtin_object_size(ptr, type)// ptr:  指向某个对象的指针// type: 推断模式(0-3)// 返回值: ptr 指向位置到对象末尾的剩余字节数//         如果无法确定,返回 (size_t)-1

type参数的含义:

structS {char a[10]; char b[20]; };structSs;char *p = &s.a[5];__builtin_object_size(p, 0);// type=0: 整个对象的剩余大小 = sizeof(S) - 5 = 25// 从 p 到整个结构体 s 的末尾__builtin_object_size(p, 1);// type=1: 最内层子对象的剩余大小 = sizeof(a) - 5 = 5// 从 p 到成员 a[] 的末尾// type 0,1: 不确定时返回 (size_t)-1(最大值,相当于"不限制")// type 2,3: 不确定时返回 0(最小值,相当于"零空间")

实际的情况:

// 情况1:栈上局部数组 → 编译器知道大小char buf[64];__builtin_object_size(buf, 0);   // → 64 ✓ 确定// 情况2:已知大小的 mallocchar *p = malloc(100);__builtin_object_size(p, 0);     // → 100 ✓ 有时能推断// 情况3:动态大小char *p = malloc(n);             // n 是变量__builtin_object_size(p, 0);     // → (size_t)-1 ✗ 无法确定// 情况4:数组偏移char buf[64];char *p = buf + 10;__builtin_object_size(p, 0);     // → 54 ✓ 编译器能算// 情况5:条件分支char a[10], b[20];char *p = (condition) ? a : b;__builtin_object_size(p, 0);     // → 10 (取最小值,保守估计)

编译时:替换危险函数

// 源码char buf[10];strcpy(buf, "hello");  // 编译器能判断是否安全// 实际编译后strcpy(buf, "hello");  // 直接使用普通的strcpy

解析:在这里编译器在编译期就能够静态判断出 “hello”字符串的长度为6字节,小于buf的长度,故直接使用原版的strcpy减少开销

// 源码char buf[10];strcpy(buf, "hello world");  // 编译器判断溢出// 实际编译// error: call to __builtin___strcpy_chk will always overflow

解析:编译器在编译时期就能够确定溢出,所以会直接报错误

// 源码char buf[10];strcpy(buf, user_input);  // 编译器看到固定大小缓冲区// 实际编译后(伪代码)__strcpy_chk(buf, user_input, 10);  // 多了长度检查参数

解析:因为在编译时期 未能判断 是否安全,但是已知缓冲区的大小,GCC 插入了 __*_chk 版本函数,多传入目标缓冲区大小参数,从而在 运行时进行检查

运行时:长度检查

// __strcpy_chk 内部逻辑char *__strcpy_chk(char *dest, constchar *src, size_t destlen) {if (strlen(src) >= destlen)  // 检查:源字符串长度 >= 目标大小?        __chk_fail();            // 溢出检测!调用 __stack_chk_fail 类似机制returnstrcpy(dest, src);    // 安全时才执行}

触发条件:当 GCC 能静态推断出缓冲区大小也就是destlen时才会插入检查

Pwn 中的实际影响

场景1:完全阻止经典栈溢出

voidvulnerable(){char buf[64];    gets(buf);  // 被替换成 __gets_chk,直接检测溢出strcpy(buf, attacker_controlled);  // 同上}

结果:溢出瞬间触发 buffer overflow detected,程序 abort

场景2:绕过点——动态大小无法检查

voidtricky(char *input, size_t len){char *buf = malloc(len);  // 动态分配,编译时不知道大小strcpy(buf, input);       // 无法替换为 __strcpy_chk!}

关键malloc 返回值的大小编译器无法静态确定,不插入检查

场景3:结构体/指针混淆

structpacket {int type;char data[0];  // 柔性数组};voidparse(void *raw){structpacket *p = raw;strcpy(p->data, source);  // 编译器不知道data实际大小,可能不检查}

绕过/利用技术

方法1:目标选择

找没保护的函数

  • 自定义的循环复制(不用 strcpy/memcpy)
  • 动态分配内存后的操作
  • 第三方库(未用 Fortify 编译)
// 这个有保护strcpy(buf, input);// 这个无保护(GCC看不到大小)memcpy(ptr, input, len);  // 如果ptr是malloc返回的// 这个也无保护for (i = 0; i < len; i++)  // 自定义循环    buf[i] = input[i];

方法2:整数溢出绕过长度检查

// __memcpy_chk 检查:if (n > destlen) abort();// 但如果 n 是计算出来的,先让整数下溢size_t n = attacker_controlled;n = n - 100;  // 如果n很小,下溢变成极大值// 但检查时用原始的n?不,这里要看具体场景

更实际的例子

char buf[64];int len = read_int();  // 读入负数如 -1memcpy(buf, data, len);  // 转size_t后变成极大值,但检查可能用有符号比较?

方法3:竞争条件/TOCTOU(极少见)

// 检查和使用之间的时间差__strcpy_chk(buf, src, 64);  // 检查 strlen(src) < 64 通过后// 但 src 在另一个线程被修改?(几乎不可能利用)

方法4:直接攻击 __chk_fail 本身

// 如果能覆盖 GOT 表中的 __stack_chk_fail 或 __chk_fail// 但 Full RELRO 下 GOT 只读,且这属于"用溢出修溢出检测",难度极高

CTF/实战中的真实案例

案例:绕过思路总结

# 检查二进制是否开启 Fortify$ checksec ./target...FORTIFY:  YES  # 说明有 __*_chk 函数# 查看具体哪些函数被保护了$ readelf -s ./target | grep _chk0000000000401230  __strcpy_chk0000000000401280  __memcpy_chk    ...

实战策略

  1. 优先找堆漏洞:Fortify 主要针对栈缓冲区,堆分配通常无保护
  2. 找动态缓冲区allocamalloc 后的操作
  3. 找自定义实现:程序自己写的 my_strcpy 类函数
  4. 格式化字符串%n 系列不受 Fortify 影响

典型题目特征

// 这种题目 Fortify 防不住(CTF常见套路)voidvuln(){char *buf = malloc(64);int idx = read_int();    buf[idx] = read_char();  // 任意写,无函数调用// 或者    read(0, buf, 0x1000);  // read 系统调用,无 _chk 版本}

与其他保护的关系

保护机制
防护层面
Fortify 能否替代
Stack Canary
二进制运行时
❌ 不同机制,互补
NX
内存权限
❌ 完全不相关
ASLR
地址随机化
❌ 不相关
RELRO
GOT 保护
❌ 不相关
Fortify 源码级函数替换

关键认知:Fortify 是编译时最佳努力保护,不是强制安全边界。它:

  • 只保护用 <string.h> 等标准头文件的代码
  • 只保护编译器能推断大小的缓冲区
  • 不阻止逻辑漏洞(整数溢出、UAF、类型混淆等)

总结:Pwn 中的应对

遇到 Fortify 开启的目标

  1. 不要慌:它不是 Canary/NX 那种硬保护,只是”让危险函数变安全”
  2. 找替代路径:动态内存、自定义循环、系统调用
  3. 堆利用优先:Fortify 对堆几乎无影响
  4. 信息泄露__chk_fail 报错可能泄露内存布局(虽然程序会退出)

如果想要更详细地了解Fortify Source请前往 [FORTIFY_SOURCE(编译时安全检查) – GKLBB – 博客园]

https://www.cnblogs.com/GKLBB/p/19603331

看这位师傅的文章。

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » 二进制安全-源码防护Fortify_Source

评论 抢沙发

3 + 4 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮