当前位置: 首页 > news >正文

CppCon 2015 学习:Beyond Sanitizers

Sanitizers,一类基于编译时插桩(instrumentation)的动态测试工具,用来检测程序运行时的各种错误。

Sanitizers 简介

  • 基于编译时插桩:编译器在编译代码时自动插入检测代码。
  • 动态运行时检测:程序运行时实时检查错误。
  • 常见类型
    • ASan(AddressSanitizer):检测内存相关错误,如越界访问、使用后释放(Use-After-Free)、内存泄漏等。
    • UBSan(UndefinedBehaviorSanitizer):检测未定义行为。
    • TSan(ThreadSanitizer):检测数据竞争。
    • MSan(MemorySanitizer):检测未初始化内存读取。

ASan(AddressSanitizer)

  • 主要发现 内存访问错误
    • 堆、栈和全局变量的缓冲区溢出
    • Use-After-Free
    • 双重释放
    • 内存泄漏(需要配合 LeakSanitizer)

ASan (AddressSanitizer) 报告示例,展示了一个全局数组越界访问的错误检测。

代码示例

int global_array[100] = {-1};
int main(int argc, char **argv) {return global_array[argc + 100];  // 越界访问,导致错误
}

编译与运行命令

clang++ -O1 -fsanitize=address a.cc
./a.out

ASan 报告解析

10538 ERROR: AddressSanitizer global-buffer-overflow
READ of size 4 at 0x000000415354 thread T0
#0 0x402481 in main a.cc:3
#1 0x7f0a1c295c4d in __libc_start_main ??:0
#2 0x402379 in _start ??:0
0x000000415354 is located 4 bytes to the right of global
variable ‘global_array’ (0x4151c0) of size 400

  • 错误类型:global-buffer-overflow(全局缓冲区越界)
  • 错误位置main 函数,源代码第3行
  • 读取大小:4字节(int)
  • 内存地址:访问的地址比 global_array 末尾多了4字节,越界访问了

总结

  • ASan 能精准检测内存越界,定位代码行和具体访问的地址。
  • 帮助快速发现并修复这类难调试的内存错误。

这是一个典型的 AddressSanitizer (ASan) 检测到的 use-after-free 错误案例。

解释逐行含义:

int *array = new int[100];  // 分配了 100 个 int 的堆内存
delete [] array;            // 正确地释放了这块内存
return array[argc];         //  错误!使用了已经释放的内存:Use-After-Free

ASan 报告详解:

==30226== ERROR: AddressSanitizer heap-use-after-free
READ of size 4 at 0x7faa07fce084 thread T0
  • 程序试图读取已释放内存中的 4 个字节(即一个 int),地址是 0x7faa07fce084
#0 0x40433c in main a.cc:4
  • 错误发生在 main() 函数的第 4 行,也就是 return array[argc];
freed by thread T0 here:
#0 0x4058fd in operator delete[](void*) _asan_rtl_
#1 0x404303 in main a.cc:3
  • 指出堆内存是在第 3 行被释放的:delete [] array;
previously allocated by thread T0 here:
#0 0x405579 in operator new[](unsigned long) _asan_rtl_
#1 0x4042f3 in main a.cc:2
  • 内存是在第 2 行分配的:new int[100]

总结

  • 错误:释放内存后继续访问(Use-After-Free)
  • ASan(-fsanitize=address)很好地捕捉了这个问题
  • 解决:不要在 delete[] 之后再使用该指针

改正代码

int main(int argc, char **argv) {int *array = new int[100];int result = array[argc];  // 在 delete 之前使用delete [] array;return result;
}

这是一个 AddressSanitizer (ASan) 报告的 stack-use-after-return 错误。

错误解释:函数返回后访问其局部变量

原始代码

int *g;
void LeakLocal() {int local;g = &local;  //  把局部变量的地址赋值给全局指针
}
int main() {LeakLocal(); return *g;   //  函数返回后访问已经无效的栈内存
}

出错原因

  • localLeakLocal() 的局部变量,分配在栈上
  • LeakLocal() 返回时,栈帧被销毁
  • g 仍然指向已销毁的栈空间 → 未定义行为

ASan 报告解析

==19177==ERROR: AddressSanitizer: stack-use-after-return
  • 错误类型:函数返回后访问栈上已释放的变量
READ of size 4 at 0x7f473d0000a0 thread T0
#0 0x461ccf in main a.cc:8
  • 第 8 行读取了已无效的栈地址,大小为 4 字节(即一个 int
Address is located in stack of thread T0 at offset 32 in frame
#0 0x461a5f in LeakLocal() a.cc:2
  • 说明地址属于函数 LeakLocal() 中的局部变量 local
This frame has 1 object(s):[32, 36) 'local' <== Memory access at offset 32
  • local 占用的是偏移 [32, 36) 的栈空间,ASan 检测到了你访问了这块空间

修复建议

不要返回或存储局部变量的地址

// 错误
int *g;
void LeakLocal() {int local;g = &local;  //  local 是局部变量,生命周期短
}

正确做法:使用堆分配或传值

int *g;
void LeakLocal() {g = new int(42);  //  分配在堆上
}
int main() {LeakLocal();int result = *g;delete g;         // 别忘了释放return result;
}

补充:为什么 detect_stack_use_after_return=1 重要?

默认 ASan 并不会检测这种错误行为,因为栈帧地址可能被重用。加上这个选项后,ASan 会将局部变量放入“后备堆”,从而保留它的元数据以检测使用。

ASAN_OPTIONS=detect_stack_use_after_return=1 ./a.out

ThreadSanitizer (TSan) 是什么?

TSan(ThreadSanitizer) 是一种 动态检测工具,用于捕捉 并发相关的错误,如:

  • 数据竞争(Data Race
  • 死锁(Deadlock)
  • 锁使用错误(如两次解锁)
  • 条件变量误用
  • 多线程中的未定义行为

最常见的错误:数据竞争(Data Race)

数据竞争定义:

两个线程并发访问同一个内存地址,至少一个是写操作,并且它们之间没有同步机制(如锁)。

例子:

int counter = 0;
void *increment(void *) {counter++;  //  竞态:多个线程同时修改return nullptr;
}
int main() {pthread_t t1, t2;pthread_create(&t1, nullptr, increment, nullptr);pthread_create(&t2, nullptr, increment, nullptr);pthread_join(t1, nullptr);pthread_join(t2, nullptr);return 0;
}

这个代码在逻辑上没错,但多个线程访问 counter 没有加锁,会导致 不可预测结果

如何使用 TSan ?

使用支持 TSan 的编译器(如 Clang):

clang++ -fsanitize=thread -g -O1 race.cc -o race
./race

TSan 报告内容(典型):

WARNING: ThreadSanitizer: data race (pid=1234)Write of size 4 at 0x0000004005d0 by thread T1:#0 increment race.cc:4Previous write of size 4 at 0x0000004005d0 by thread T2:#0 increment race.cc:4Location is global 'counter' of size 4 at 0x0000004005d0
SUMMARY: ThreadSanitizer: data race in counter

修复建议:使用同步机制

使用 std::mutex 保护共享数据:

#include <mutex>
std::mutex mtx;
int counter = 0;
void *increment(void *) {std::lock_guard<std::mutex> lock(mtx);counter++;  //  安全访问return nullptr;
}

小结

工具用于检测
ASan内存越界、use-after-free、栈错误等
TSan并发错误:数据竞争、死锁等
UBSan未定义行为(例如整数溢出、类型转换)
MSan未初始化内存读取(MemorySanitizer)

这是一个经典的 数据竞争(Data Race) 例子,使用了 C++11 的 std::thread

示例代码:

int X;
std::thread t([&]{ X = 42; });  // 线程 T1 写入 X
X = 43;                         // 主线程同时写入 X
t.join();                       // 等待 T1 完成

问题说明:

  • 两个线程 没有同步机制 地同时写入 X
  • 由于 X 是共享变量,多个写入操作之间未加锁,形成数据竞争
  • 编译器或 CPU 可能对这些写入操作重排,结果 不可预测
  • -fsanitize=thread 捕捉到这一点,并报告了数据竞争。

TSan 报告解释:

WARNING: ThreadSanitizer: data race
Write of size 4 at ... by thread T1:#0 main::$_0::operator()() const race.cc:4
Previous write ... by main thread:#0 main race.cc:5
  • 两个写操作 都对 X 进行了修改,但彼此没有任何同步。
  • T1 和主线程 同时访问同一地址 → 违反了 C++ 的数据竞争定义。

正确做法:同步

std::mutex 保护共享变量:

#include <mutex>
int X;
std::mutex m;
int main() {std::thread t([&] {std::lock_guard<std::mutex> lock(m);X = 42;});{std::lock_guard<std::mutex> lock(m);X = 43;}t.join();
}

或者使用原子变量:

#include <atomic>
std::atomic<int> X;
int main() {std::thread t([&]{ X.store(42); });X.store(43);t.join();
}

MSan(MemorySanitizer)简介:

MSan 是一种检测 未初始化内存读取(use of uninitialized memory) 的工具。它不会像 ASan/TSan 那样关注内存越界或数据竞争,而是专注于:

变量或内存内容还未被初始化,就被读取或使用了。

常见问题:

1. 使用未初始化的栈变量:
int main() {int x;int y = x + 1;  //  x 没有初始化return y;
}

运行时会被 MSan 报告为 使用未初始化值

2. 使用未初始化的结构体字段:
struct Point {int x, y;
};
int main() {Point p;int z = p.x + p.y;  //  p.x 和 p.y 未初始化return z;
}

使用 MSan 的编译方式:

clang++ -fsanitize=memory -fPIE -pie -g test.cc -o test

注意:MSan 只在 Clang 上支持,并且不支持一些系统库(如 glibc),常用于 LLVM instrumented 环境或自定义 libc(如 musl)

与其他 Sanitizer 的对比:

Sanitizer检查内容
ASan内存越界、use-after-free 等
TSan多线程数据竞争
MSan读取未初始化内存
UBSan未定义行为(除零、溢出等)

这个例子展示了 MSan(MemorySanitizer) 如何检测栈上数组中未初始化的元素被访问的问题。

逐行解释:

int main(int argc, char **argv) {int x[10];       //  数组 x 没有被完全初始化x[0] = 1;        //  x[0] 初始化了return x[argc];  //  x[argc] 可能未初始化
}
  • 如果 argc == 0x[0] 会被访问,是安全的;
  • 如果 argc == 1,会访问 x[1] —— 没有被初始化,MSan 报告错误

MSan 报告内容解读:

WARNING: Use of uninitialized value#0 0x7f1c31f16d10 in main a.cc:4
Uninitialized value was created by an 
allocation of 'x' in the stack frame of 
function 'main'

意思是:

  • 程序尝试读取 x[...],但那个位置没有被赋过值;
  • 堆栈分配的 x[10] 数组中,除 x[0] 外,其余元素未初始化;
  • 静态分析看不出来这个问题,但运行时 MSan 抓到了。

修复方式:

int main(int argc, char **argv) {int x[10] = {0};  // 初始化所有元素为 0x[0] = 1;return x[argc];
}

或者更安全地,使用 std::arraystd::vector 并进行显式初始化。

UBSan(UndefinedBehaviorSanitizer)概览:

UBSan 是用于检测 C/C++ 程序中未定义行为(UB, Undefined Behavior) 的工具。
它不像 ASan(内存)或 TSan(线程)那样专注于特定问题,而是涵盖一大类语言层级的错误。

UBSan 可以检测的问题类型包括:

问题类型示例代码描述
整数溢出int x = INT_MAX + 1;有符号整数溢出
除以零int x = 1 / 0;数学未定义
不对齐访问强制访问未对齐的结构体字段会在某些硬件上崩溃
类型混淆(type punning)错误地使用 union 来存不同类型会破坏内存语义
无效的虚函数调用基类未构造完成就调用虚函数未定义行为
空指针解引用int *p = nullptr; *p = 5;经典崩溃场景
访问已析构的对象(use-after-lifetime)用完对象后还访问其成员会产生悬空指针
enum 值超范围设置了未定义的枚举值违反类型安全

如何使用 UBSan:

clang++ -fsanitize=undefined -g main.cpp -o main
./main

你还可以加上 -fno-sanitize-recover=undefined 让程序遇到 UB 直接崩溃。

示例:

int main() {int a = 10;int b = 0;int c = a / b; //  除以 0return c;
}

UBSan 输出:

runtime error: division by zero

解释:UBSan 报告中的 int overflow

示例代码:
int main(int argc, char **argv) {int t = argc << 16;return t * t;
}

假设 argc = 1,那么:

  • t = 1 << 16 = 65536
  • t * t = 65536 * 65536 = 4,294,967,296
    这个结果超出了 int 所能表示的范围(通常为 −2,147,483,648 到 2,147,483,647),属于有符号整数溢出,根据 C++ 标准,这是 未定义行为(UB)

UBSan 检测结果:

runtime error: signed integer overflow: 65536 * 65536 cannot be represented in type 'int'

UBSan 成功捕捉到了这个未定义行为并发出警告。

总结:

  • -fsanitize=undefined 能在运行时捕捉很多细节错误,像整数溢出、除零、虚函数错误等。
  • 编写性能关键或安全敏感的 C++ 程序时强烈建议启用 UBSan。

说明:Sanitizers 找出了成千上万的 bug

Sanitizers(如 ASan、TSan、MSan、UBSan)是 Clang/LLVM 提供的一组运行时工具,能在开发和测试阶段捕捉各种常见但难以发现的错误。

它们能发现的问题包括:

Sanitizer检测内容示例错误类型
ASan (AddressSanitizer)内存地址相关Use-after-free、越界访问
TSan (ThreadSanitizer)多线程数据竞争Data race、竞争条件
MSan (MemorySanitizer)未初始化内存读取使用未初始化变量
UBSan (UndefinedBehaviorSanitizer)未定义行为整数溢出、无效类型转换

为什么重要?

  • 可以自动检测 微妙且难以调试的问题
  • 广泛用于 Chromium、Firefox、Linux kernel、LLVM 等大型项目
  • 显著减少生产环境中的崩溃和安全漏洞
    如果你正在开发 C/C++ 项目,强烈建议:
clang++ -fsanitize=address,undefined -g your_file.cc

来在开发阶段抓住更多潜在问题。

补充说明:Sanitizers 不够,还需要更多手段

虽然 ASan、TSan、MSan、UBSan 非常强大,但它们 不是银弹(silver bullet)

为什么它们“不够”?

  • 仅在运行测试时生效
    → 如果你的测试用例覆盖不到某路径,Sanitizer 就无法发现那里的问题。
  • 不能证明程序正确性
    → 它们只能发现具体的已触发的 bug,而不是形式验证(formal verification)

那我们还能做什么?

1. Fuzzing(模糊测试)
  • 自动生成大量随机输入来覆盖不同路径
  • 特别适合发现崩溃、内存越界等
  • 与 Sanitizers 组合使用效果最佳
    libFuzzer + ASan 是黄金搭档
2. Hardening(安全加固)
  • 即便 bug 存在,也让攻击者难以利用
  • 举例:
    • 堆栈保护(stack canaries)
    • 控制流完整性(CFI)
    • 堆隔离(heap isolation)
    • clang 的 -fstack-protector-strong, -fsanitize=cfi, 等

总结

技术用途
Sanitizers找 bug(依赖测试)
Fuzzing提升测试质量、触发隐藏 bug
Hardening让 bug 更难被攻击者利用
Formal Methods (进阶)用数学方法“证明”程序正确

Fuzzing (模糊测试) 简单理解:

  • 自动或半自动地给程序输入大量随机、无效、意料之外的数据
  • 观察程序是否崩溃、异常或出现安全漏洞
  • 帮助发现输入验证不足或边界条件错误

为什么用Fuzzing?

  • 人工写测试用例不可能覆盖所有路径
  • 随机试探程序极限,更容易找到隐藏bug
  • 很适合发现内存越界、空指针解引用、格式化字符串漏洞等

例子:

你有个函数处理网络数据包:

  • 正常输入:符合协议的包
  • Fuzzing 输入:随意乱写的包,或不完整的包
    程序如果没做好异常处理,可能崩溃或挂起,fuzzer就能发现这类问题。

Generation-based Fuzzing 就是:

  • 自动生成大量测试输入,直接喂给目标程序
  • 结合 Sanitizers 一起用,能即时发现崩溃、内存错误等
  • 输入可以是完全随机的无效数据(用来测试程序对异常输入的鲁棒性)
  • 也可以是符合规则的有效数据,比如像 csmith 生成的 C 代码,确保测试语法和语义的复杂性
  • Chromium 安全团队用它找了成千上万的漏洞
  • 虽然强大,但依旧只是测试的开始,不能保证覆盖所有场景

Mutation-based Fuzzing 主要流程是:

  • 先收集一批真实的测试样本(corpus),比如从网络抓取或从已有测试用例集里拿
  • 对这些样本做变异(mutation),比如随机修改、插入、删除某些字节
  • 用变异后的输入继续测试程序,看程序是否崩溃或出错
  • 通常在实战中比纯生成式 fuzzing 效果更好,因为基于真实数据的变异更贴近真实使用场景
  • 但面对高度结构化的输入格式(比如 C++ 代码)时,变异很容易破坏格式,导致无效输入,测试难度加大

“控制流引导(覆盖率引导)模糊测试”,它的原理就是:

  • 在程序运行时,插入代码来监控哪些代码路径被执行了,也就是收集代码覆盖率信息;
  • 在对输入数据进行变异后,运行程序观察是否触发了新的代码路径;
  • 如果新输入覆盖了之前未执行过的代码,就把它加入到测试输入库(corpus)中;
  • 这样不断反馈,模糊测试就能更高效地探索程序的不同执行路径,发现更多潜在的bug。
    相比普通的随机变异,覆盖率引导的模糊测试效率可以提高十倍甚至上千倍。
    比较著名的模糊测试工具有AFL(American Fuzzy Lop)和libFuzzer,都是基于这个思想。

AFL-fuzz的工作机制:

  • 编译时插桩:在程序编译阶段插入额外代码,记录控制流信息。
    • 传统方式是在汇编层面插桩
    • 现代方式利用LLVM编译器的插桩功能
  • 边计数器(edges counters):使用64K个计数器来跟踪程序中所有控制流边(从一个代码块到另一个代码块的跳转)的执行次数。
    • 每条边的计数器只有8位,表示执行次数的区间(1次,2次,3次,4-7次,8-15次,16-31次,32-127次,128次以上)
    • 计数器使用哈希映射,有可能发生碰撞,但这样做极大提升了效率
  • 驱动进程和目标进程分离:AFL-fuzz作为驱动,负责生成测试输入、启动目标程序(被测试程序)作为独立进程,收集目标程序的执行反馈。
    这个设计保证了高效的反馈循环,能够快速发现新覆盖的代码路径,从而智能地变异输入。
    理# AFL-fuzz 真不是简单的玩具,而是实实在在用于工业级项目的强大工具。它已经发现了大量漏洞,涵盖了广泛的开源和闭源软件库,包括:
  • 图像处理库:libjpeg-turbo、libpng、libtiff、mozjpeg、ImageMagick、libraw、libde265 等
  • 浏览器和插件:Mozilla Firefox、Internet Explorer、Apple Safari、Adobe Flash、JavaScriptCore
  • 安全和网络:OpenSSL、GnuTLS、OpenSSH、tcpdump、wireshark、wpa_supplicant
  • 系统和工具:glibc、clang/llvm、systemd、bash、dpkg、curl、libyaml
  • 多媒体处理:ffmpeg、FLAC、libsndfile、wavpack
  • 其他各种:SQLite、LibreOffice、poppler、clamav、redis、perl、Qt、SleuthKit、dnsmasq
    并且还有更多广泛的工具、库、系统组件都受益于AFL的发现能力。

LLVM libFuzzer 利用 -fsanitize-coverage= 选项做代码覆盖率的插桩,支持以下模式:

  • func / bb / edge:跟踪函数、基本块或边是否被执行
  • indirect-calls:跟踪间接调用的调用者-被调用者对
  • 8bit-counters:类似AFL的8状态计数器,记录边的执行次数分段(1, 2, 3, 4-7, 8-15, 等)
    它支持在进程内统计和程序退出时写入磁盘数据,方便实时反馈和后续分析。
    推荐结合 ASan、MSan、UBSan 一起使用,检测内存、线程和未定义行为。
    性能开销约10%以内,8bit计数器在多线程环境可能表现不佳。

LLVM libFuzzer 是一个轻量级的进程内控制流引导模糊测试器,特点包括:

  • 需要你自己提供一个测试入口函数,标准接口是:
    extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size);
    
  • 通过编译时参数 -fsanitize-coverage=edge[,indirect-calls][,8bit-counters] 实现代码覆盖率插桩。
  • 可以配合 Sanitizers (ASan、MSan、UBSan、LeakSanitizer)一起用,提高检测能力。
  • 链接时需要加上 libFuzzer 库。
  • 虽然算法上没有 AFL-fuzz 那么复杂和成熟,但对库和API的模糊测试非常合适,尤其是针对接口、函数等。
  • 主要目标是库级别测试,而非完整大型应用。

libFuzzer 的使用流程总结:

  • 准备测试语料库(corpus)
    把已有的测试样例放进一个文件夹(比如叫 CORPUS),可以为空目录,libFuzzer 也会自动生成变异测试用例。
  • 运行fuzzer
    ./my-fuzzer CORPUS
    
    • -jobs=N:指定并行任务数,多个工作线程共同作用于同一个语料库。
    • -max_len=N:限制单个测试输入的最大长度(默认64字节)。
    • -help:查看更多可调节参数。
  • 输出
    libFuzzer会将新发现的有价值的测试输入保存回 CORPUS 目录中,方便持续扩大和改进语料库。
  • 遇到bug或超时
    fuzz过程会停止,并将导致崩溃的输入保存,方便调试。
  • 可选
    把 libFuzzer 产生的语料库拿去给 AFL-fuzz 用,可以结合两者优点。
    这个 FreeType fuzzer 例子展示了用 libFuzzer 对 FreeType 字体库进行模糊测试,结合了 ASan 和 UBSan 来捕获各种问题,比如:
void TestOneInput(const uint8_t *data, size_t size) {FT_Face face;if (size < 1) return;if (!FT_New_Memory_Face(library, data, size, 0, &face)) {FT_Done_Face(face);}}

Results with FreeType (ASan+UBsan)
#45999 left shift of negative value -4592
#45989 leak in t42_parse_charstrings
#45987 512 byte input consumes 1.7Gb / 2 sec to process
#45986 leak in ps_parser_load_field
#45985 signed integer overflow: -35475362522895417 * -8256 cannot be represented in t
#45984 signed integer overflow: 2 * 1279919630 cannot be represented in type 'int
#45983 runtime error: left shift of negative value -9616
#45966 leaks in parse_encoding, parse_blend_design_map, t42_parse_encoding
#45965 left shift of 184 by 24 places cannot be represented in type ‘int’
#45964 signed integer overflow: 6764195537992704 * 7200 cannot be represented in type
#45961 FT_New_Memory_Face consumes 6Gb+
#45955 buffer overflow in T1_Get_Private_Dict/strncmp
#45938 shift exponent 2816 is too large for 64-bit type ‘FT_ULong’
#45937 memory leak in FT_New_Memory_Face/FT_Stream_OpenGzip
#45923 buffer overflow in T1_Get_Private_Dict while doing FT_New_Memory_Face
#45922 buffer overflow in skip_comment while doing FT_New_Memory_Face
#45920 FT_New_Memory_Face takes infinite time (in PS_Conv_Strtol)
#45919 FT_New_Memory_Face consumes 17Gb on a small input

  • 整数溢出(signed integer overflow)
  • 内存泄漏(leak)
  • 缓冲区溢出(buffer overflow)
  • 非法移位操作(left shift of negative value)
  • 性能问题(某些输入导致极大内存消耗或无限时间运行)
    通过 fuzzing,FreeType 发现了大量潜在缺陷和安全隐患,且给出了编号(如 #45999),方便追踪和修复。
    这种自动化测试大幅提升了代码质量和安全性,尤其是对复杂的二进制格式解析器(如字体文件)非常有效。
 SSL_CTX *sctx;int Init() { ... }extern "C" void LLVMFuzzerTestOneInput(unsigned char * Data, size_t Size) {static int unused = Init();SSL *server = SSL_new(sctx);BIO *sinbio = BIO_new(BIO_s_mem());BIO *soutbio = BIO_new(BIO_s_mem());SSL_set_bio(server, sinbio, soutbio);SSL_set_accept_state(server);BIO_write(sinbio, Data, Size);SSL_do_handshake(server);SSL_free(server);}

这是一个用 libFuzzer 测试 OpenSSL 的例子,关键点:

  • Init() 函数初始化 SSL 环境,只执行一次(用 static int unused = Init(); 保证)
  • 创建新的 SSL 对象 SSL_new(sctx)
  • 新建两个内存 BIO(BIO_s_mem()),分别用于模拟输入输出
  • 绑定 BIO 给 SSL(SSL_set_bio),设定为服务端接受状态(SSL_set_accept_state
  • 把 fuzz 输入数据写进输入 BIO(BIO_write(sinbio, Data, Size);
  • 触发 SSL 握手过程(SSL_do_handshake(server);),这里可能暴露握手相关漏洞
  • 最后释放 SSL 对象(SSL_free
    这个 fuzzer 通过模糊传入各种不确定的数据,测试握手流程是否有崩溃、内存错误等,适合捕捉解析输入数据时的漏洞。

Control-flow-guided fuzzing is not the end

Concolic execution(结合具体执行与符号执行)的核心思路是:

  • 具体执行(Concrete Execution):运行程序,实际输入驱动代码执行路径。
  • 符号执行(Symbolic Execution):用符号变量代替具体输入,跟踪程序路径条件。
  • 分析未覆盖的分支:通过跟踪路径条件,找出程序中哪些分支路径还没被执行过。
  • SMT求解器(Satisfiability Modulo Theories solver):利用逻辑求解器求出新的输入,使程序沿着未走过的路径执行。
    这样,测试工具不断产生新的测试用例,有针对性地覆盖更多代码路径,极大提高测试覆盖率。
    不过,这技术计算量大,执行慢,尤其是复杂程序时,开销非常大,所以说“很重”。

Data-flow-guided fuzzing 的关键点是:

  • 拦截数据流:在程序执行时,监控数据如何在代码中流动,尤其是关注条件判断中的输入。
  • 分析比较操作的输入:找出哪些输入数据被用来做条件判断(if、switch等)。
  • 智能修改测试输入:根据比较操作的输入,针对性地修改测试用例,尝试触发新的代码路径。
  • 基于 LLVM libFuzzer 和 go-fuzz 实现:这些工具利用数据流信息来引导模糊测试,提升效率。
  • 结合污点分析(如 DFSan):通过污点追踪技术,准确定位输入数据影响的代码位置,做更精细的变异。
    这种方法比纯随机变异更“聪明”,因为它理解输入如何影响程序执行,从而更快发现隐藏的 bug。

威胁 #1:

  • 缓冲区溢出(buffer overflow)或使用后释放(use-after-free)漏洞,攻击者通过这些漏洞,
  • 篡改虚函数指针(vptr)或函数指针
  • 使得程序执行被攻击者控制的代码。
    也就是说,攻击者利用内存错误,将程序中用于多态调用的指针(vptr)或普通函数指针改写成任意地址,从而劫持控制流。
    这种攻击极具破坏力,通常导致远程代码执行或提权。

解决方案就是用 Control Flow Integrity (CFI)

  • CFI 通过在编译时插入检查,确保程序运行时的间接调用(比如虚函数调用)只能跳转到合法的函数地址,防止攻击者修改指针后跳转到恶意代码。
  • clang++ 加上 -fsanitize=cfi-vcall-flto 编译链接,即可启用CFI对虚函数调用的保护。
  • -flto(Link Time Optimization)能让编译器在链接阶段对整个程序做优化,确保CFI检查能够覆盖跨模块的调用。
    这能有效防止利用缓冲区溢出或use-after-free改写vptr导致的控制流劫持。

Clang/LLVM 实现的 CFI 具体机制是:

  • 独立处理每个不相交的类层次结构
    认为每个类层次是封闭的(比如 Chrome 代码库这样),不允许外部类扩展。
  • 统一布局所有 vtable
    把每个类层次的 vtable 按照一定的幂次方大小对齐,放在连续的内存区域里。
  • 虚函数调用时的检查
    • 编译时确定该调用点允许调用的函数集合(严格限制调用目标)
    • 运行时,执行一系列检查:
      • 地址是否在允许范围内(range check)
      • 地址对齐是否正确(alignment check)
      • 地址对应的函数是否在允许集合内(bitset lookup)
        这些检查确保虚函数调用只能跳转到合法的目标函数,防止攻击者利用内存漏洞劫持控制流。

Clang/LLVM CFI 里的 bitset lookup 优化,具体包括:

  • 小于等于64位的bitset可以直接用寄存器操作,不需要额外内存加载,提高运行时性能。
  • 如果bitset是全1(即允许调用所有函数),则跳过检查,避免无用开销。
  • 通过优化vtable布局,使得同一个类层次里的虚函数地址更加集中,从而缩小bitset的大小,进一步提升效率。
    总结就是:用位图(bitset)快速判断调用合法性,结合这些技巧减少性能损耗。

Clang/LLVM CFI 在 x86_64 汇编层的典型实现示例:

  • All ones(允许所有函数调用):
    • 只做最简单的范围检查,没用 bitset,直接允许调用。
    • 如果偏移超范围,跳转到 CRASH。
    • 否则调用虚函数指针。
  • <= 64 bits(小于等于64位的 bitset 优化):
    • 计算偏移(rax - base)
    • 用 rol (rotate left) 对偏移做混淆处理,防止攻击。
    • 比较偏移是否越界。
    • bt 指令检查偏移位是否在允许的 bitset 中。
    • 如果检查失败,跳 CRASH。
    • 成功则调用。
  • Full check(完整的多字节 bitset 检查):
    • 类似上面,但 bitset 比较大(64位)
    • testb 测试某个位是否允许
    • 失败跳 CRASH。
      CRASH 位置通过 ud2 指令(非法指令)终止程序,防止利用漏洞继续执行。
      这体现了 CFI 的实时动态检查,既保证安全,又通过优化减少性能损失。

Clang/LLVM CFI 的更多用法和实际应用情况,关键点总结如下:

更多 CFI 类型支持

  • 非虚函数调用(non-virtual calls):用 -fsanitize=cfi-nvcall 做检查,防止非法非虚函数调用。
  • C 风格间接调用(通过函数指针):-fsanitize=cfi-icall 保障间接调用安全。
  • 多态类型的类型转换
    • 基类到派生类转换(downcast):-fsanitize=cfi-derived-cast
    • void* 转换为类指针:-fsanitize=cfi-unrelated-cast
      这些能检测非法类型转换,避免潜在的类型混淆漏洞。

CFI 在 Chromium 里的使用情况

  • 在 Linux 和 Android 下支持,开启了多种 CFI 检查项。
  • macOS 和 Windows 平台支持正在完善中。
  • 运行时开销极低,CPU 占用增加不到 1%。
  • 代码体积增加大约 7%。
  • 在迁移过程中发现并修复了很多真实的漏洞和错误。

更好的或不同的 CFI 方案

  • 不强制需要 LTO(Link Time Optimization),有些方案允许模块边界跨 DSO(动态共享对象)界限做 CFI 检查。
  • 例如 MSVC 的 Control Flow Guard(CFG)用 /d2guard4/Guard:cf 实现,支持跨模块保护。
  • 不过跨 DSO 的 CFI 设计可能带来复杂性或安全隐患,需要权衡。
    总结来说,CFI 技术正在不断完善,目标是以最小的性能和代码开销,实现对 C++ 虚函数调用、间接调用及类型转换的有效保护。Chromium 是实际应用中的典范案例。

威胁 #2:栈缓冲区溢出(stack-buffer-overflow),攻击者通过溢出栈上的缓冲区,覆盖了返回地址,从而控制程序流程,执行任意代码。

具体威胁

  • 攻击者向局部数组写入超过其边界的数据
  • 覆盖函数调用栈中的返回地址(ret address)
  • 函数返回时跳转到攻击者指定的地址,执行恶意代码(典型的栈溢出攻击)

为什么严重?

  • 返回地址是函数调用流程控制关键
  • 一旦被覆盖,攻击者可劫持程序执行流
  • 可造成任意代码执行、权限提升、系统破坏

常见防护措施

  • 栈保护机制(Stack Canaries):在返回地址之前放置随机值,函数返回时校验,检测溢出
  • 地址空间布局随机化(ASLR):随机化栈、堆和库的内存地址,难以猜测溢出目标地址
  • 控制流保护(比如前面提到的 CFI):减少溢出后控制流被篡改的机会
  • 现代编译器选项,如 -fstack-protector 系列
  • 内存安全语言或工具辅助,比如使用 Sanitizers 检测溢出

SafeStack 的核心思想是 将局部变量和控制流数据(如返回地址)分开存放

  • 本地变量(特别是数组等可能溢出的数据)放在独立的、专门 mmap 的内存区域
  • 返回地址、帧指针等保存在正常的栈上
  • 这样即使发生栈缓冲区溢出,也无法覆盖返回地址,攻击者无法轻易劫持程序流程

SafeStack 特点

  • 保护返回地址免受栈溢出攻击
  • 但仍有部分隐患,比如虚函数表指针(vptr)和函数指针可能被覆盖
  • 建议和 CFI(Control Flow Integrity) 结合使用,增强安全性

使用示例

clang++ -fsanitize=safe-stack your_code.cpp -o your_program

性能

  • Chromium 实际测试 CPU 开销 < 1%,相当轻量

这段汇编代码展示了 SafeStack 在函数调用时如何操作“unsafe stack”(存放局部变量的安全外栈)指针:

int main() {int local_var = 0x123456;bar(&local_var);
}
push   %r14
push   %rbx
push   %rax
mov    0x207d0d(%rip),%r14
mov    %fs:(%r14),%rbx  # Get unsafe_stack_ptr
lea    -0x10(%rbx),%rax # Update unsafe_stack_ptr
mov    %rax,%fs:(%r14)  # Store unsafe_stack_ptr
lea    -0x4(%rbx),%rdi
movl   $0x123456,-0x4(%rbx)
callq  40f2c0 <_Z3barPi>
mov    %rbx,%fs:(%r14)  # Restore unsafe_stack_ptr
xor    %eax,%eax
add    $0x8,%rsp
pop    %rbx
pop    %r14
retq  
  • mov %fs:(%r14),%rbx:从线程局部存储(TLS)读取当前 unsafe_stack_ptr
  • lea -0x10(%rbx),%rax:在 unsafe stack 上为局部变量预留空间
  • mov %rax,%fs:(%r14):更新 unsafe stack 指针,表示空间已分配
  • movl $0x123456,-0x4(%rbx):把局部变量 local_var = 0x123456 写入 unsafe stack
  • 调用函数 bar,传入局部变量地址(指向 unsafe stack)
  • 返回时,恢复 unsafe stack 指针和调用栈指针,保护了正常栈上的返回地址不被局部变量覆盖
    关键点:
  • 传统栈(%rsp) 仅存放返回地址、保存寄存器等,局部变量移至单独的 unsafe stack 区域
  • 即使局部变量溢出也不会破坏返回地址
  • %fs:(%r14) 是一个线程局部存储段寄存器的偏移,存储 unsafe stack 指针(实现线程安全)

总结

  • 传统测试容易让人产生“代码安全”错觉,但不能保证没有漏洞。
  • 使用 Sanitizers(ASan、TSan、MSan、UBSan)可以快速发现基本的内存和线程问题。
  • 借助引导型模糊测试(guided fuzzing),比如 LLVM libFuzzer 和 AFL-fuzz,可以更深入地挖掘隐藏的安全隐患和漏洞。
  • 代码加固是进一步提升安全的关键手段,包括:
    • **CFI(Control Flow Integrity)**保护虚函数调用、非虚成员调用、各种类型转换和间接调用,防止控制流被劫持。
    • SafeStack隔离局部变量和返回地址,防止栈缓冲区溢出导致返回地址被篡改。

相关文章:

  • 2025年渗透测试面试题总结-腾讯[实习]科恩实验室-安全工程师(题目+回答)
  • Mysql的B-树和B+树的区别总结
  • stripe支付测试,ngrok无法使用?免费vscode端口转发,轻松简单!
  • 【输入URL到页面展示】
  • OurBMC技术委员会2025年二季度例会顺利召开
  • Android 项目的核心配置文件
  • 解决fastadmin、uniapp打包上线H5项目路由冲突问题
  • 【Linux】centos软件安装
  • macOS 连接 Docker 运行 postgres,使用navicat添加并关联数据库
  • OpenAI API 流式传输
  • 2.0 阅读方法论与知识总结
  • 软件功能鉴定需要注意哪些内容?
  • Windows GDI 对象泄漏排查实战
  • Vue 生命周期全解析:从创建到销毁的完整旅程
  • [网页五子棋][匹配模块]实现胜负判定,处理玩家掉线
  • 测试面试题 手机号验证码登录测试用例
  • 论文导读 | 动态图存储与事务处理系统总结
  • 敏捷开发中如何避免过度加班
  • 代码随想录 算法训练 Day22:回溯算法part01
  • AIGC 基础篇 高等数学篇 03 中值定理与导数应用
  • 运城建设局网站/做推广的都是怎么推
  • 搭建正规网站/公司网站建设服务机构
  • 徐州市鼓楼区建设局网站/百度论坛首页官网
  • 创建网站的目的是什么/新乡seo推广
  • 银川网站推广/宁波seo网络推广公司排名
  • 石家庄网站建设石家庄/网络推广外包想手机蛙软件