Featured image of post Quick_fuzz

Quick_fuzz

QuickFuzz

Fuzz入门,学习参考:Baby Fuzz · Home,BV1ZM4m1R7gZ

模糊测试理论与工具实践

总览

模糊测试又称为fuzzing,是一种软件测试技术。其核心概念为自动产生随机输入到一个程序中,并监视程序异常,如崩溃、断言失败,以发现可能的程序错误。


举例

测试.c:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// gcc -o test test.c
#include <unistd.h>

int main()
{
    char input[8] = {0};
    read(STDIN_FILENO, input, 8);
    if (input[0] == 'A' && input[1] == 'B') // (1)
        *((unsigned int *)0) = 0xdeadbeef; // (2),将空指针赋值为0xdeadbeef,引发程序崩溃
    write(STDOUT_FILENO, input, 8);
    return 0;
}

模糊器.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import subprocess

target = './test'
inps = ['AA', 'BB', 'BA', 'AB']

for inp in inps:
    // 不断测试哪些输入会引发程序崩溃
    try:
        subprocess.run([target], input=inp.encode(), capture_output=True, check=True)
    except subprocess.CalledProcessError: # (1)
        print(f"bug found with input: '{inp}'")

# (output)
# bug found with input: 'AB'

内部架构


在执行时会因为不同的条件执行不同的程序码,而不同的条件主要if就是定义

1
2
3
4
if (a == 1 && b == 2)
    puts("condition 1");
else
    puts("condition 2");

img


IDA pro生产出来的指令级别的控制流图(CFG)

img


fuzzing流程大致可以拆成三个组件分别为:

1.种子选择、2.突变、3.覆盖范围。

img


举例:

测试.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// gcc -o test test.c
#include <unistd.h>
#include <stdio.h>

int main()
{
    char input[8] = {0};
    read(STDIN_FILENO, input, 8);

    if (input[0] == 'A') {
        puts("AAA");
        if (input[1] == 'B') {
           puts("BBB");
           if (input[2] == 'C') {
            *((unsigned int *)0) = 0xdeadbeef; // bug
           }
        }
    }
    return 0;
}

测试器.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import subprocess
import random

target = './test'
inps = ['A', 'B'] # 语料库
count = 1

while True:
    inp = inps[0] # 种子选择演算法比较简单,使用最新的的种子作为下一个输入
    inp += random.choice(['A', 'B', 'C']) # 变异演算会挑选出来的种子加上一个随机字元作为最终的输入。
    del inps[0] #加一个删一个,保证下次取的是下一个
    count += 1 #记录处理了多少个输入

    try:
        comp = subprocess.run([target], input=inp.encode(), capture_output=True, check=True)
        if comp.stdout != b'':
            inps.append(inp) # 如果有输出的话则代表此输入为有趣
    except subprocess.CalledProcessError:
        print(f"bug found with input: '{inp}'")
        break

    if count % 100 == 0 or len(inps) == 0: # 定期打乱语料库,避免变异效果不好导致输入无法取得新的覆盖范围
        inps = ['A', 'B']

模糊器的好坏,通常是:

  1. 种子选择是否能挑出真正有意义的种子
  2. 变异的随机是否有效率
  3. 覆盖实现的方式是否会造成大量的开销。

AFL(American Fuzz Loop)

– 简介 & 安装


以下是一些比较有名的开源模糊测试工具:

  1. American Fuzzy Lop (AFL): AFL 是一个高效的模糊测试工具
  2. libFuzzer: libFuzzer 是 LLVM/Clang 提供的一个模糊测试引擎,它可以轻松地集成到现有的代码中
  3. Syzkaller: Syzkaller 是一个专注于系统调用接口的模糊测试工具,它可以自动生成各种系统调用序列,并对内核进行测试以发现漏洞和错误。
  4. OSS-Fuzz: OSS-Fuzz旨在通过自动化模糊测试发现开源软件中的安全漏洞和错误。

Fuzz方式

AFL有两种fuzz途径:

  1. 开源软件:AFL软件进行编译的同时进行插桩,以方便fuzz
  2. 闭源软件:配合QEMU直接对闭源的二进制代码进行fuzz

环境搭建

安装

Linux包管理(deb):

1
$ sudo apt install afl++

源码编译安装 :

下载源码自行编译:(推荐安装AFL++,AFl的话如果开ASAN可能有问题)

1
2
3
4
$ git clone https://github.com/AFLplusplus/AFLplusplus.git
$ cd AFLplusplus
$ make
$ sudo make install

AFL(American Fuzz Loop)

插桩(instrumentation)

在保证原程序逻辑的完整性下,在程序中插入一些程序码来采集运行期间的执行状态。

1
2
3
4
5
6
7
8
9
int test_var = 0;

// original (1)
void b() { ...; }
void a() { ...; }

// instrumented (2)
void b() { printf("test_var: %d\n", test_var); ...; }
void a() { printf("test_var: %d\n", test_var); ...; }

特点:

  1. 插桩的对象通常都具有相同的属性或类别涉及所有的功能、所有的基本块,比较少针对单一目标。
  2. 插桩的程序代码通常只有几行汇编代码,并且不会做太复杂的操作
  3. 模糊器中,插桩被用来进行覆盖,那么记录多少程序码被执行到。

举例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
int had_exec[100] = {0};

void a()
{
    had_exec[0] = 1; // (1)
    // ...
}
void b() { had_exec[1] = 1; ...; }
void c() { had_exec[2] = 1; ...; }

int main()
{
    // ...
    if (had_exec[0]) // (2)
        puts("function a had been called");
}

Demo

演示


测试程序test.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <stdio.h>
#include <unistd.h>

int main()
{
    int a, idx;
    char buf[100];

    scanf("%d", &idx);
    buf[idx] = '\0';

    read(0, &a, 0x2);
    if (a == 0xdead)
        *(int *)0 = 0xdeadbeef;
    return 0;
}

afl-gcc

1
2
$ export AFL_USE_ASAN=1
$ afl-gcc -fsanitize=address -o test test.c
  • 最后会我们的命令会变成这样

gcc选用afl的汇编器来编译

1
$ gcc -fsanitize=address -o test test.c -B ~/fuzz/AFLplusplus -g -O3 -funroll-loops -D__AFL_COMPILER=1 -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1
  • 有趣的是-B ~/fuzz/AFL,gcc 会尝试在这里寻找路径工具链中的汇编器来执行
1
2
$ ls -al ~/fuzz/AFLplusplus/as
$ lrwxrwxrwx 1 lidaxian lidaxian 6 Mar 29 14:53 as -> afl-as

afl-as

  • afl-as首先会执行函数add_instrumentation()做插桩(对汇编代码),最后执行as做汇编(编译成机械代码)。所以插桩是在编译之后汇编之前。
  • 做完插桩后会执行调整后的参数来汇编新的asm文件,最后产生的执行文件test即是有插桩的版本,简单用objdump就可以看到许多以__afl为前缀的函数(说明已完成插桩):
1
2
3
4
5
$ objdump -M intel -d test | grep afl
    119d:       e8 1e 02 00 00          call   13c0 <__afl_maybe_log>
    120d:       e8 ae 01 00 00          call   13c0 <__afl_maybe_log>
    1255:       e8 66 01 00 00          call   13c0 <__afl_maybe_log>
    12a1:       e8 1a 01 00 00          call   13c0 <__afl_maybe_log>

afl-fuzz

1
afl-fuzz -i seed-dir -o out-dir -m none ./test
  • -i - 存放测试用例的资料夹
  • -o - 搁置执行结果资料夹
  • -f - 从指定文件读取输入
  • -t - timeout,执行时间超过的话就会被kill掉
  • -m - 内存限制,执行时所能使用的内存体上限
  • -d - 跳过确定性,突变阶段跳过最初的处理
  • -n - 对没有插桩的目标进行模糊测试

tips:

运行后遇到一些问题:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
demian@Friday:~/C_lab/fuzz$ afl-fuzz -i seed-dir -o out-dir -m none ./test~
afl-fuzz++4.09c based on afl by Michal Zalewski and a large online community
[+] AFL++ is maintained by Marc "van Hauser" Heuse, Dominik Maier, Andrea Fioraldi and Heiko "hexcoder" Eißfeldt
[+] AFL++ is open source, get it at https://github.com/AFLplusplus/AFLplusplus
[+] NOTE: AFL++ >= v3 has changed defaults and behaviours - see README.md
[+] No -M/-S set, autoconfiguring for "-S default"
[*] Getting to work...
[+] Using exponential power schedule (FAST)
[+] Enabled testcache with 50 MB
[+] Generating fuzz data with a length of min=1 max=1048576
[*] Checking core_pattern...

[-] Hmm, your system is configured to send core dump notifications to an
    external utility. This will cause issues: there will be an extended delay
    between stumbling upon a crash and having this information relayed to the
    fuzzer via the standard waitpid() API.
    If you're just testing, set 'AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1'.

    To avoid having crashes misinterpreted as timeouts, please log in as root
    and temporarily modify /proc/sys/kernel/core_pattern, like so:

    echo core >/proc/sys/kernel/core_pattern

[-] PROGRAM ABORT : Pipe at the beginning of 'core_pattern'
         Location : check_crash_handling(), src/afl-fuzz-init.c:2361

从错误信息来看,系统配置了外部工具来处理核心转储(core dump),这会导致 AFL++ 无法正确处理崩溃信息。

所以我们要修改系统的 core_pattern 配置。

首先,运行以下命令查看当前的 core_pattern 配置:

1
cat /proc/sys/kernel/core_pattern

输出类似于 |/usr/lib/systemd/systemd-coredump %p %u %g %s %t %c %h,说明系统配置了外部工具来处理核心转储。

然后以 root 用户身份运行以下命令,将 core_pattern 修改为 core

1
echo core | sudo tee /proc/sys/kernel/core_pattern

再次查看 core_pattern 配置,确认修改成功:

1
cat /proc/sys/kernel/core_pattern

如果输出为 core,说明修改成功。

接下来就可以重新运行 AFL-fuzz。

1
afl-fuzz -i seed-dir -o out-dir -m none ./test

在创建了相关目录和种子文件后,命令运行成功就能在终端看见如下结果了:

fuzzing

Crash分析

out-dir/crashes目录下的内容是引发崩溃的输入。


Sanitizer


即使程序存在漏洞,也不一定会在执行到有漏洞的程式码时触发异常

举例:

1
2
3
char buf[100];
scanf("%d", &idx);
buf[idx] = '\0'; // (1)

然而即便会有out-of-bound write 的漏洞发生,但如果buf[101]对应到的地址正好没被使用到,那么fuzzer也不会感兴趣。

所以我们需要“Sanitizer”来辅助检测程序问题,它是一种用于检测程序中各种错误(如内存错误、数据竞争等)的工具。它通常集成在编译器中,可以在程序运行时检测问题。

能辅助检测,但是有额外性能和时间需求。


常见的Sanitizer有:

  1. AddressSanitizer (+LeakSanitizer)
  2. ThreadSanitizer
  3. UndefinedBehaviorSanitizer
  4. MemorySanitizer

AddressSanitizer原理简介

这个内存检查是如何工作的?


左侧,蓝色区域是我们分配的内存在右侧

右侧,Redzones是中毒的内存,访问即报错。

图片20200816120042864


如果通过启用Address Sanitizer来编译可执行文件,则每次访问内存之前,都会有前缀指令来检查该内存是否为poisoned.如果是,Address Sanitizer 将生成如上所示的诊断报告。

图片20200816120700174


下图显示该进程正在尝试访问中毒内存,并触发Crash并生成诊断报告。

图片20200816120921377


堆对象分配

Address Sanitizer通过使用它自己的分配实现来替换默认的 Malloc 实现,该实现将对象彼此分开,中间插入有毒内存。

图片20200816123141896


堆栈变量

在两个堆栈变量之间插入一些红色区域,因此堆栈红色区域在运行时中毒。

图片20200816150456865


额外的开销

  • CPU 减速通常在 2 倍到 5 倍之间 正常情况下,CPU 速度减慢 2 倍至 3 倍。在某些极端情况下,他们的速度下降了 5 倍。
  • 内存开销 2x–3x
  • AddressSanitizer 使用比本机运行更多的实际内存。确切的开销取决于分配大小。分配越小,开销就越大。
  • AddressSanitizer 使用更多的堆栈内存。我们看到增长高达 3 倍。

实战演示-libpng


0x0.编译fuzz目标

libpng是开源的png解析库

1
2
3
4
5
6
$ wget https://nchc.dl.sourceforge.net/project/libpng/libpng16/1.6.36/libpng-1.6.36.tar.xz
$ tar xvf libpng-1.6.36.tar.xz
$ cd libpng-1.6.36
$ ./autogen.sh 
$ CC=afl-clang CXX=afl-g++ ./configure --enable-static
$ make -j4

--enable-static : 用于生成静态库,fuzz开源库时会需要


0x1.准备环境(准备种子)

获取官网提供的测试集作为输入

1
2
3
4
$ mkdir fuzz_in fuzz_out
$ cd fuzz_in
$ wget http://lcamtuf.coredump.cx/afl/demo/afl_testcases.tgz
$ tar xvf afl_testcases.tgz

0x2.开始fuzz#

1
$ afl-fuzz -i ../fuzz_in/png/full/images -o ../fuzz_out ../.libs/pngimage @@

../fuzz_in/png/full/images为afl测试集

../.libs/pngimage是编译出来的被测试程序

@@代表测试输入样本


0x3.报错处理(如果安装在系统上时)

AFL测试时用到功能需要还没有开启

1
2
3
4
sudo su
echo core >/proc/sys/kernel/core_pattern
cd /sys/devices/system/cpu
echo performance | tee cpu*/cpufreq/scaling_governor

总结


  • 使用AFL在linux上fuzz开源软件十分简单
  • 大多数的lib/开源软件的源代码都是可以获取的
  • 在编译时插桩是可行的
  • 在Fuzz时要用ASAN,MSAN,UBSAN
  • 有时最需要花费时间的过程是项目编译
    • 缺失引用的第三方库(lib)
    • 编译过程中的各种错误
    • 不同项目不同的编译方法与各种选项
Licensed under CC BY-NC-SA 4.0
© 2023-2025 Ch0ser. All Rights Reserved.
使用 Hugo 构建
主题 StackJimmy 设计