libfuzz

libfuzzer

llvm、clang-3.5 是不行的,fuzz编译选项无法使用
多参考下 google libfuzzer 学会写测试代码,其中还有大量的测试字典 fuzzers/dict

libfuzzer build:


libfuzzer

  • 灵活:通过实现接口的方式使用,可以对任意函数进行fuzzing

  • 高效:在同一进程中进行fuzzing,无需大量fork()进程

  • 便捷:提供了API接口,便于定制化和集成

    但是没有dirty mode,需要编译时插桩

1
2
3
4
apt-get install -y make autoconf automake libtool pkg-config zlib1g-dev
git clone https://github.com/Dor1s/libfuzzer-workshop.git ~/libfuzzer
cd ~/libfuzzer/libFuzzer/ && ./Fuzzer/build.sh
当前目录生成一个 libFuzzer.a

usage


libFuzzer.a需要被静态链接到测试程序,并且会调用入口函数

fuzzer会跟踪哪些代码区域已经测试过,然后在输入数据的语料库上进行变异,来使代码覆盖率最大化。代码覆盖率的信息由 LLVMSanitizerCoverage 插桩提供。

1
int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
编译一个cpp
#include <stdint.h>
#include <stddef.h>
#pragma comment(lib,"libFuzzer.a")

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
bool result = false;
if (size >= 3) {
result = data[0] == 'g' && data[1] == 'o' && data[2] == 'o' && data[3] == 'd';
}
return result;
}
// 代码覆盖率测试工具 -fprofile-arcs -ftest-coverage 用 afl-cov 才需要加,可不加
// -fsanitize-coverage=trace-pc-guard 提供代码覆盖率信息 生成 .sancov 文件才需要加
clang++ -g -std=c++11 -fsanitize=fuzzer,address -fprofile-arcs -ftest-coverage -fsanitize-coverage=trace-pc-guard demo.cxx ~/libfuzzer/libFuzzer/libFuzzer.a -o demo
code

效率感觉低了很多黄色框是检测是否错误读写,0x7fff8000是一个偏移,而不是数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//block_num是所有的block数量
uint64_t llvm_gcov_ctr[block_num];
//guard_block_num是特殊路径的block数量,也等于所有if数量+1,比如上面的 result = data[0] == 'g' && data[1] == 'o' && data[2] == 'o' && data[3] == 'd'; 有4个if, 那么就有6个
char __stop___sancov_guards[guard_block_num];
char __start___sancov_cntrs[guard_block_num];

//伪代码
int __cdecl LLVMFuzzerTestOneInput(const uint8_t *data, size_t size){
//到达block_n的条件分支就将cmp操作数记录下来
_sanitizer_cov_trace_const_cmp4(opa, opb) // -fsanitize=fuzzer
if(block_n_guard_true){
++_llvm_gcov_ctr[block_n];//-fprofile-arcs
}else{
#defined(FLAG(-fsanitize-coverage=trace-pc-guard))
_sanitizer_cov_trace_pc_guard(_start___sancov_guards[block_n])
++__stop___sancov_guards[block_n];
#elif defined(FLAG(-fsanitize=fuzzer))
++__start___sancov_cntrs[block_n];
#endif
++_llvm_gcov_ctr[block_n]; //-fprofile-arcs
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);
ASAN:
//data是需要对齐的,8字节对齐. 下面的右移3,所以就是对齐8,如果右移6就是对齐64
//当传入 size = 4 时,那么
//0x7FFF8000+(data>>3)有如下内存值
// (Shadow bytes mask) 04 FA FA FA FA FA FA FA FA FA
//04就是只能访问4字节,必定==size,
检测原理:
//读取1字节,当访问data[4]时,
//编译器加代码判断
register size_t data_ptr = (size_t)(data + 4);
if(byte(0x7FFF8000+(data_ptr>>3)) && (data_ptr&7) >= byte(0x7FFF8000+(data_ptr>>3))){
_asan_report_load1(data_ptr);//error buffer-overflow
}

//读取2字节,当访问((uint16_t*)data)[2]时,那么就是如下代码:
register size_t data_ptr = (size_t)(data + 4);
if(byte(0x7FFF8000+(data_ptr>>3)) && (data_ptr&7) + 1 >= byte(0x7FFF8000+(data_ptr>>3))){
_asan_report_load2(data_ptr);//error buffer-overflow
}

//这就是检测原理了-fsanitize=address
//所以crash不一定是真的crash,可能是越界读取,这并不一定造成漏洞

libfuzzer根据_sanitizer_cov_trace_const_xxx函数和 __start___sancov_cntrs数组就可以很好的 构造出适合的测试数据来访问未探索的block

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (size >= 3) {
//当size=3,访问了data[3],就直接crash
result = data[0] == 'g' && data[1] == 'o' && data[2] == 'o' && data[3] == 'd';
}

$ ./demo out_directory
//看下结果

==20205==ABORTING
MS: 1 EraseBytes-; base unit: 87967cad164ed1722d0cbd4778fbb298016acc07
0x67,0x6f,0x6f,
goo
artifact_prefix='./'; Test unit written to ./crash-3f95edc0399d06d4b84e7811dd79272c69c8ed3a
Base64: Z29v

那么简单的密码他能解决吗?便随手写了2个算法整个活

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stddef.h>
#pragma comment(lib,"libFuzzer.a")
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);

#if 0
char data[] = { 10,13,6,28,12,4,21,57,49,12,61,49,1,27,9,125 };
//算法1
int checkinput(const char* a, int c) {
int i = 0;
if (!c) return 0;
uint8_t* enc = (uint8_t*)malloc(c);
memcpy(enc, a, c);
for (i = 0; i < c - 1; i++) {
enc[i] = enc[i] ^ enc[i + 1];
if (enc[i] != data[i]) {
free(enc);
return 0;
}
}
free(enc);
return c == sizeof(data);
}

#else
//算法2
int checkinput(const char* a, int c) {
int w, i, j, k;
char b[60] = { 20 ,20 ,20 ,05 ,20 ,20 ,20 ,20 ,00, 20, 05, 20, 05 ,20, 05 ,05 ,
20 ,00 ,20 ,05 ,05 ,20 ,20 ,05 ,20 ,20 ,00 ,20 ,05 ,20 ,20 ,20 ,
20 ,05 ,20 ,00 ,20 ,05 ,20 ,05 ,20 ,05 ,05 ,05 ,00 ,20 ,20 ,20 ,
05 ,20 ,20 ,20 ,20 };
int v18 = 0;
int v14 = 0;
for (i = 0; i <= c - 1; i++) {
if (i != (c - 1)) {
if (a[i] == '1')
v18--;
else if (a[i] == '2')
v18++;
else if (a[i] == '3')
v14--;
else if (a[i] == '4')
v14++;
else
return 0;
if (((v14 * 9 + v18) < 60) && ((v14 * 9 + v18) > -20))
if (v18 >= 0 && v14 >= 0 && b[v14 * 9 + v18] != 20)
return 0;
else
return 0;
}
}
return v18 == 7 && v14 == 5;
}

#endif

size_t ccount = 0;
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
ccount++;
if(checkinput(data, size)){
printf("Good boy. count:%zx\n", ccount);
int t=data[-1];// 产生leak错误 作为crash
return 0;
}
return 1;
}
1
2
3
mkdir crackme_out # fuzzer 程序可以有多个目录作为参数,此时 fuzzer 会递归遍历所有目录,把目录中的文件读入最为样本数据传给测试函数,同时会把那些可以产生新的的代码路径的样本保存到第一个目录里面。
./crackme -use_cmp=1 -dump_coverage=1 -print_final_stats=1 -max_len=15 ./crackme_out #不加-max_len费时几十倍

out

其他项目的编译方法

1
2
3
4
5
6
7
8
9
10
11
12
#使用-fsanitize-coverage = trace-cmp,编译器将在比较指令和switch语句周围插入额外的工具。
#使用-fsanitize-coverage = trace-div,编译器将检测整数除法指令(以捕获除法的正确参数)
#并使用 -fsanitize-coverage = trace-gep – LLVM GEP指令 (以捕获数组索引)。

#-fsanitize-coverage=[func,bb,edge] bb:basic block
#With -fsanitize-coverage=trace-bb the compiler will insert __sanitizer_cov_trace_basic_block(s32 *id) before every function, basic block, or edge (depending on the value of -fsanitize-coverage=[func,bb,edge]).

export FUZZ_CXXFLAGS="-O2 -fno-omit-frame-pointer -g -fsanitize=address \
-fsanitize-coverage=edge,indirect-calls,trace-cmp,trace-div,trace-gep,trace-pc-guard"

CXX="clang++ $FUZZ_CXXFLAGS" CC="clang $FUZZ_CXXFLAGS" \
CCLD="clang++ $FUZZ_CXXFLAGS" ./configure

coverage report

覆盖率就是整个fuzzer一趟测试触及的 basic-block 总个数。

文件./crackme.26107.sancov

1
2
# 转换成 .symcov
sancov-8 -symbolize ./crackme crackme.26107.sancov > crackme.26107.symcov

然后使用 coverage-report-server 解析这个文件。这里也有libfuzzer/lessons/08/coverage-report-server.py

1
2
3
4
$ curl http://llvm.org/svn/llvm-project/llvm/trunk/tools/sancov/coverage-report-server.py  -o ./coverage-report-server.py
$ python3 ~/libfuzzer/lessons/08/coverage-report-server.py --symcov crackme.26107.symcov --srcpath ./
Loading coverage...
Serving at 127.0.0.1:8001
cov

算法1:事实证明它还是不能解密如算法1的简单算法,更不说解循环xor :

1
2
enc[i+1] = enc[i] ^ enc[i + 1];
enc[0] = enc[0] ^ enc[c-1];

原因是,它依旧不能探查前后数据的关系几乎不可能,仅仅对一些简单条件的路径能较好的探索,一般来说足够了

1
2
3
4
5
6
7
./demo -help=1 #可以看到很多fuzz选项
-use_cmp :根据trace guard的条件追踪来引导突变
-dump_coverage :可视化覆盖率
-max_len :测试数据最大长度
-max_total_time : 设置最长的运行时间, 单位是 秒, 这里是 300s , 也就是 5 分钟
-print_final_stats:执行完 fuzz 后 打印统计信息
#....待补充

移步【参考2】实战两个简单的CVE

Dictionary


就是输入的关键字合集,比如 png图片 就有 png 图片头。strcmp(phead,”xxxx”) 会直接将整个xxxx关键字替换进去,提高速度。就好比选择联想输入的候选词一样。

dictionary 文件-> google git or google afl

Dictionary 就是实现了这种思路。 libfuzzerafl 使用的 dictionary 文件的语法是一样的, 所以可以直接拿 afl 里面的 dictionary 文件来给 libfuzzer 使用。

libfuzzer 官网

dict文件有用的只是由 "" 包裹的字串libfuzzer 会用它们进行组合来生成样本。

1
./program -dict=./xxx.dict -max_total_time=300 -print_final_stats=1 input_dir

google AFL :

1
-x dir        - optional fuzzer dictionary (see README)

merge


可以使用 libfuzzer 把样本集进行精简。

1
2
mkdir output_min_dir
./program -merge=1 output_min_dir input_dir

output_min_dir: 精简后的样本集存放的位置
input_dir: 原始样本集存放的位置

参考&推荐:

  1. 基于Unicorn和LibFuzzer的模拟执行fuzzing
  2. fuzz实战之libfuzzer
  3. Dictionary
  4. libfuzzer-workshop
  5. fuzz总结
  6. [p1umer-2019/02/20/libfuzzer & LLVM 初探](https://p1umer.github.io/2019/02/20/libfuzzer & LLVM 初探/)