23、漏洞挖掘基础(上):什么是漏洞

各位同学,欢迎来到漏洞挖掘的章节。说实话,我做了这么多年逆向,真正让我觉得「这行有意思」的,就是漏洞挖掘。你想想看,一行代码写错了,整个系统就可能被攻破。这感觉,就像在代码的迷宫里找一扇隐藏的门。

今天我们先打基础。把几个最经典的漏洞类型讲透,再聊聊怎么用工具自动化地找漏洞。嗯,咱们开始。

23.1 什么是漏洞?

漏洞,说白了就是程序里的「意外」。开发者没想到的情况,被攻击者利用了。我个人的理解是:漏洞 = 错误的假设 + 未检查的边界

举个例子,你写了个函数,假设用户输入不会超过100字节。但用户偏要输入1000字节。如果程序没检查,那多出来的900字节就会覆盖到别的地方去。这就是漏洞的根源。

核心观点: 漏洞不是bug,bug是程序不符合预期。漏洞是程序符合预期,但预期本身是错的。

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
对比: AFL 适合测试完整的二进制程序,LibFuzzer 适合测试库函数或特定模块。我个人习惯:如果目标是个独立的命令行工具,用 AFL;如果是某个库的API,用 LibFuzzer。

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 需要输入是文件,而我们的目标是网络服务。怎么办?

有两种方法:

  1. 修改目标程序:让它从文件读取数据,而不是从网络。这是最常用的方法。
  2. 使用 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/ 目录下。

我的经验: 刚开始 fuzz 时,别指望马上找到漏洞。先跑个几小时,看看覆盖率增长情况。如果覆盖率一直不涨,说明你的初始样本太单一,或者程序逻辑太复杂。这时候需要调整策略。

23.7 知识体系图

下面这张图总结了本章的核心内容:

漏洞挖掘基础(上)知识体系 漏洞类型与Fuzzing 缓冲区溢出 整数溢出 格式化字符串 栈溢出 堆溢出 无符号溢出 有符号溢出 信息泄露 任意写 Fuzzing 工具 AFL(覆盖率引导) LibFuzzer(轻量级)

23.8 总结

今天咱们聊了三种经典漏洞:缓冲区溢出、整数溢出、格式化字符串。每种都有各自的特点和利用方式。然后介绍了 Fuzzing 的基本概念和两个主流工具:AFL 和 LibFuzzer。最后实战演示了如何对一个网络服务进行 Fuzzing。

记住一句话:漏洞挖掘不是玄学,是科学。它需要扎实的基础知识,加上大量的实践。别指望看一遍就会,动手去 fuzz 一个真实程序,你会有完全不同的体会。

最后提醒: 所有漏洞挖掘和利用技术,请只在授权范围内使用。未经授权的测试是违法的。我见过太多人因为「好奇」而惹上麻烦。

公众号:蓝海资料掘金营,微信deep3321