23、漏洞挖掘基础(上):什么是漏洞
各位同学,欢迎来到漏洞挖掘的章节。说实话,我做了这么多年逆向,真正让我觉得「这行有意思」的,就是漏洞挖掘。你想想看,一行代码写错了,整个系统就可能被攻破。这感觉,就像在代码的迷宫里找一扇隐藏的门。
今天我们先打基础。把几个最经典的漏洞类型讲透,再聊聊怎么用工具自动化地找漏洞。嗯,咱们开始。
23.1 什么是漏洞?
漏洞,说白了就是程序里的「意外」。开发者没想到的情况,被攻击者利用了。我个人的理解是:漏洞 = 错误的假设 + 未检查的边界。
举个例子,你写了个函数,假设用户输入不会超过100字节。但用户偏要输入1000字节。如果程序没检查,那多出来的900字节就会覆盖到别的地方去。这就是漏洞的根源。
23.2 缓冲区溢出
这是最经典的漏洞类型,没有之一。我在刚入行时,第一个实战分析的就是一个缓冲区溢出漏洞。
什么叫缓冲区溢出?程序在内存里划了一块区域(缓冲区),用来存数据。但写入的数据超过了这块区域的大小,多余的数据就「溢」出去了。
看个简单的例子:
void vulnerable_function(char *input) {
char buffer[64];
strcpy(buffer, input); // 没有检查长度!
printf("Copied: %s\n", buffer);
}
这里 strcpy 不会检查 input 的长度。如果 input 超过64字节,就会覆盖栈上 buffer 之后的内容。包括返回地址、局部变量等。
攻击者可以精心构造输入,覆盖返回地址,让程序跳转到恶意代码。这就是经典的「栈溢出攻击」。
strcpy 处理网络数据包。我当时就想,这肯定有问题。果然,一个简单的长字符串就导致了设备重启。后来我建议他们改用 strncpy 并明确指定长度。
23.3 整数溢出
整数溢出,听起来没缓冲区溢出那么「暴力」,但危害一点不小。我见过不少因为整数溢出导致的远程代码执行漏洞。
整数溢出的原理很简单:整数在计算机里是有范围的。比如32位无符号整数,范围是0到4294967295。如果你给这个数加1,它超过了最大值,就会「绕回」到0。
看代码:
unsigned int size = 0xFFFFFFFF;
size += 1; // 结果变成了0,而不是0x100000000
这有什么危害呢?假设你有一个函数,先检查用户输入的大小,然后分配内存:
void process_data(unsigned int user_size) {
unsigned int total_size = user_size + 256; // 可能溢出!
char *buffer = malloc(total_size);
if (user_size > 1024) {
printf("Size too large!\n");
return;
}
// 这里假设 buffer 足够大,但实际上可能很小
read_data(buffer, user_size);
}
如果 user_size 是 0xFFFFFF00,加上256后溢出为0。malloc(0) 返回一个很小的堆块。但后续 read_data 却试图读取 0xFFFFFF00 字节的数据。这就造成了堆溢出。
23.4 格式化字符串漏洞
这个漏洞很有意思。我第一次遇到时,还以为是程序「抽风」了。后来才明白,这是 printf 家族的「特性」被滥用了。
看这段代码:
void print_user(char *user_input) {
printf(user_input); // 危险!
}
如果用户输入的是 "Hello, %s!",那没问题。但如果用户输入的是 "%x %x %x %x",printf 就会从栈上读取数据,并打印出来。这就是信息泄露。
更危险的是 "%n" 格式符。它会把已经输出的字符数写入一个地址。攻击者可以用它来改写任意内存地址的内容。
我举个例子:
// 假设用户输入: "\x10\x20\x30\x40%x%x%x%n"
// 这会把某个值写入 0x40302010 这个地址
printf。我当时就提醒他们:永远不要用用户输入作为格式化字符串。用 printf("%s", user_input) 才是安全的。
23.5 Fuzzing 基础
手动找漏洞太慢了。我们得用工具。Fuzzing(模糊测试)就是自动化地给程序喂各种「奇怪」的输入,看它会不会崩溃。
我常用的两个工具:AFL 和 LibFuzzer。
AFL(American Fuzzy Lop)
AFL 是个基于覆盖率的模糊测试工具。它会记录哪些代码路径被执行过,然后尝试生成新的输入来覆盖更多路径。
基本用法:
# 编译目标程序,需要插桩
afl-gcc -o target target.c
# 开始fuzzing
afl-fuzz -i input_dir -o output_dir -- ./target @@
这里的 @@ 表示 AFL 会把生成的测试用例作为文件路径传给程序。
LibFuzzer
LibFuzzer 是 LLVM 项目的一部分。它和 AFL 的思路类似,但更轻量,直接链接到你的程序中。
使用步骤:
// 你需要写一个 fuzz target 函数
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
// 在这里处理数据
process_data(Data, Size);
return 0;
}
编译时加上 -fsanitize=fuzzer:
clang++ -fsanitize=fuzzer -o fuzz_target fuzz_target.cpp
23.6 实战:对一个简单的网络服务进行 Fuzzing
光说不练假把式。我们来实战一把。
假设有一个简单的网络服务,监听在 8080 端口,接收 HTTP 请求并返回响应。代码大概长这样:
void handle_request(int client_fd) {
char buffer[256];
int n = read(client_fd, buffer, sizeof(buffer));
// 解析 HTTP 请求头
char *method = strtok(buffer, " ");
char *path = strtok(NULL, " ");
// ... 处理请求
}
这里有个明显的缓冲区溢出:read 最多读256字节,但 strtok 会修改 buffer 内容。如果输入构造得当,可能触发问题。
我们用 AFL 来 fuzz 这个服务。但 AFL 需要输入是文件,而我们的目标是网络服务。怎么办?
有两种方法:
- 修改目标程序:让它从文件读取数据,而不是从网络。这是最常用的方法。
- 使用 AFL 的网络模式:AFL 支持
-N参数,可以 fuzz 网络服务。
我推荐第一种,因为更可控。我们写个包装程序:
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Usage: %s <input_file>\n", argv[0]);
return 1;
}
int fd = open(argv[1], O_RDONLY);
handle_request(fd);
close(fd);
return 0;
}
然后编译并 fuzz:
afl-gcc -o fuzz_wrapper wrapper.c server.c
afl-fuzz -i testcases/ -o findings/ -- ./fuzz_wrapper @@
这里的 testcases/ 目录放一些初始的 HTTP 请求样本,比如:
GET / HTTP/1.1
Host: example.com
AFL 会基于这些样本不断变异,生成新的输入。如果某个输入导致程序崩溃,AFL 会把它保存到 findings/crashes/ 目录下。
23.7 知识体系图
下面这张图总结了本章的核心内容:
23.8 总结
今天咱们聊了三种经典漏洞:缓冲区溢出、整数溢出、格式化字符串。每种都有各自的特点和利用方式。然后介绍了 Fuzzing 的基本概念和两个主流工具:AFL 和 LibFuzzer。最后实战演示了如何对一个网络服务进行 Fuzzing。
记住一句话:漏洞挖掘不是玄学,是科学。它需要扎实的基础知识,加上大量的实践。别指望看一遍就会,动手去 fuzz 一个真实程序,你会有完全不同的体会。
公众号:蓝海资料掘金营,微信deep3321