模糊测试

模糊测试

模糊测试,又称为fuzzing,是一种软件测试技术。原理是自动生成大量随机输入到程序中,监视程序异常信息,尽可能发现导致目标程序错误的非法输入,找到目标程序的脆弱点。

模糊测试主要分为两个关键模块:程序监控器和输入生成器

其中模糊器的好坏,通常取决于

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

AFL

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

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

下面介绍AFL工具的使用。AFL,全称“American Fuzzy Lop”,是由安全研究员Michal Zalewski开发的一款基于覆盖引导(Coverage-guided)的模糊测试工具,它通过记录输入样本的代码覆盖率(代码执行路径的覆盖情况),以此进行反馈,对输入样本进行调整以提高覆盖率,从而提升发现漏洞的可能性。

AFL工具有两种Fuzz方式。对于开源软件来说,AFL可以再进行编译的同时进行插桩;对于闭源软件来说,可以通过和QEMU直接对闭源的二进制代码进行fuzz。

插桩:指的是在不影响程序正常逻辑运行的完整性下,在程序中插入程序码来采集程序运行期间的执行状态。在模糊器中,插桩常用于进行覆盖,记录多少程序码被执行到。如下:

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. 普通插桩模式:afl-as.h, afl-as.c, afl-gcc.c,针对源码插桩,编译器可以使用gcc, clang;
  2. llvm插桩模式:针对源码插桩,编译器使用clang;
  3. qemu插桩模式:针对二进制文件插桩
  • fuzzer模块

​ fuzzer 实现的核心代码,AFL 的主体

  • 辅助模块
  1. afl-analyze:对测试用例进行分析,通过分析给定的用例,确定是否可以发现用例中有意义的字段;
  2. afl-plot:生成测试任务的状态图;
  3. afl-tmin:对测试用例进行最小化;
  4. afl-cmin:对语料库进行精简操作;
  5. afl-showmap:对单个测试用例进行执行路径跟踪;
  6. afl-whatsup:各并行例程fuzzing结果统计;
  7. afl-gotcpu:查看当前CPU状态。

种子语料库

AFL需要提供初始的种子输入,变异算法会根据此种子变异生成各种测试用例,然后输入给程序得到反馈。这里给出一些开源的语料库:

Demo

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

int main()
{
int a,index;
char buf[100];
scanf("%d",&index);
buf[index]='\0';

read(0,&a,0x2);
if(a== 0x1234)
{
*(int *)0 = 0xdeadabcd;
}

return 0;
}

使用普通插桩模式进行编译,首先使用afl-gcc进行编译,命令如下

1
afl-gcc -o test test.c

可以从上面的输出结果中看到在编译的时候也使用afl-as进行了插桩了9个位置。而afl-as首先会执行add_instrumentation()做插桩,做完插桩后会执行调整后的参数来汇编形成新的asm文件,所以最后产生的test文件是存在插桩信息的。可以使用objdump命令查看,

随后使用afl-fuzz进行fuzz,命令如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
afl-fuzz -i seed-dir -o out-dir -m none ./test
//
-i - 存放测试用例的资料夹

-o - 搁置执行结果资料夹

-f - 从指定文件读取输入

-t - timeout,执行时间超过的话就会被kill掉

-m - 内存限制,执行时所能使用的内存体上限

-d - 跳过确定性,突变阶段跳过最初的处理

-n - 对没有插桩的目标进行模糊测试
//

运行结果如下

结果解读

AFL在运行过程中,会不断地产生输出。输出目录结构如下:

  • queue/ - 存放AFL生成的触发新代码路径的测试样本
  • crashes/ - 存放能触发待测程序崩溃的测试样本
  • hangs/ - 存发导致待测程序超时的测试样本
  • fuzzer_stats - 文本文件,包含了fuzzer的实时统计信息,如执行速度、路径覆盖等度量指标。这个文件不断更新以反映当前的fuzzing状态。
  • plot_data - 文本文件,包含了AFL执行过程中的统计数据。使用AFL的afl-plot工具处理plot_data文件,可以生成fuzz过程的可视化图像。
  • fuzz_bitmap - 这是用来记录路径覆盖率的位图(coverage bitmap),非人类可读。AFL使用这个位图来跟踪程序在处理不同输入时执行的不同分支,用来帮助AFL识别新的、唯一的代码路径,以便后续生成更具有探索性的测试样本

Sanitizer

即使程序存在漏洞,也不一定会在执行到有漏洞的程式码时触发异常,例如上面的代码中存在数组越界漏洞

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

即使存在溢出漏洞,但如果buf[101]对应的地址正好没有被使用到,fuzzer也不感兴趣。

常见的Sanitizer有:

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

下面对AddressSanitizer原理进行简介

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

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

Address Sanitizer在堆和栈中都对此进行了防护措施,在堆中通过使用它自己的分配实现来替换默认的 Malloc 实现,该实现将对象彼此分开

在两个堆栈变量之间插入红色区域,防止越界

实践Libpng

0x0.编译目标程序

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

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 @@
打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2021-2024 John Doe
  • 访问人数: | 浏览次数:

让我给大家分享喜悦吧!

微信