# Introduction
SYMCC 替换了编译器 做了一些插桩
# Background
符号执行框架分为前端后端,前端负责计算 expr 传递给后端的 solver (e.g. Z3).
Concolic execution 和 symbolic execution 的主要区别在于它们如何处理程序中的条件分支语句。 在 concolic execution 中,程序的某些路径会被选择执行,这取决于之前执行过程中的输入值和执行顺序。然后,这些已经执行过的路径将会被记录下来,而未执行过的路径将会被探索并执行,以便发现程序中的错误。
所以 CE 的本质是记录输入下的路径和路径的约束条件 而不是直接利用 CE 求解一个路径。
目前的符号执行技术有两种形式
IR-based : 逐行对 ir 语句解释 进行符号计算 优点是任意实现 缺点是 远离机器码 IR 到机器码的过程中应该还有问题
IR-less : 在机器码上通过 CPU 提供的功能对某些指令 hook 能够跳转进自己的代码进行执行和分析 缺点就是指令很多 要对大部分指令都进行一个符号的处理 但是速度快
这里减少 overhead 的方法就是避免将只涉及到 concrete value 的指令送给 SE backend。这部分不是很 care。
# Compilation-based symbolic execution
作者的做法就是直接写个 pass 编译进目标程序 其实就是插桩。
1 2 3 4 5 6 7 define i32 @is_double (i32, i32) { %3 = shl nsw i32 %1 , 1 %4 = icmp eq i32 %3 , %0 %5 = zext i1 %4 to i32 ret i32 %5 }
插桩后,就是格外多走一步生成符号表达式丢给后端,程序逻辑并没有变.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 define i32 @is_double (i32, i32) { ; symbolic computation %3 = call i8* @_sym_get_parameter_expression (i8 0 ) %4 = call i8* @_sym_get_parameter_expression (i8 1 ) %5 = call i8* @_sym_build_integer (i64 1 ) %6 = call i8* @_sym_build_shift_left (i8* %4 , i8* %5 ) %7 = call i8* @_sym_build_equal (i8* %6 , i8* %3 ) %8 = call i8* @_sym_build_bool_to_bits (i8* %7 ) ; concrete computation (as before) %9 = shl nsw i32 %1 , 1 %10 = icmp eq i32 %9 , %0 %11 = zext i1 %10 to i32 call void @_sym_set_return_expression (i8* %8 ) ret i32 %11 }
这里插桩的方法就是先用其他 IR-based 的解释器扫描一下 然后找出应该进行符号执行的地方 插入代码而不是执行代码。
另外的问题就是优化问题,由于是在 IR 层扫描语义后插桩,llvm 后端优化后可能会导致出现问题,而且优化后也会优化插桩的需要。
In the current implementation, our pass runs in the middle of the optimization pipeline, after basic optimizations like dead-code elimination and strength reduction but before the vectorizer
最后就是 SE 的边界问题,一个符号值调用外部库再回来其实就算作 concrete 了,不过可以通过对 libc 的某些函数进行一些简单的,比如 memcpy,还有 read (这里没对 scanf 进行实现 没法跟踪 scanf 的符号执行), 这里也有个 c++ 库的插桩版本。
# practice
这里没开 Z3 (高性能的无约束、位向量和有限域布尔求解器)。
symcc 的每个指导下一次数据输入的生成数据就放在 output 这个宏下面,需要指定。
symcc 提出了一个协调 fuzzer 数据交换的工具 fuzzing_helper (rust)
1 2 3 4 cmake -G Ninja -DQSYM_BACKEND=ON -DZ3_TRUST_SYSTEM_VERSION=on . export SYMCC_OUTPUT_DIR=`pwd`/results cargo install --path util/symcc_fuzzing_helper
这里随便写个相对复杂的 c 程序让 afl+symcc 来 fuzz(这里有个小坑就是 scanf 这里没实现符号化的追踪 只能 read)
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 #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <stdint.h> void foo () { uint32_t input; char buf[0x21 ]; int *ptr = NULL ; read(0 , &input, 4 ); int n = read(0 , &buf, 0x20 ); buf[n] = '\x00' ; printf ("[D] input : %d\n" , input); printf ("[D] buf : %s\n" , buf); if (input < 0xffffff && input > 0xfffff ) { if (input % 2 == 0 ) { goto out; } if (buf[1 ] == 'A' && buf[3 ] == 'C' ) { if (buf[0 ] != 'A' ) goto out; if (strlen (buf) == 5 ) { printf ("[+] reach out destination\n" ); *ptr = 1 ; } } } out: return ; } int main () { foo(); return 1 ; }
1 2 3 4 5 6 7 8 9 10 11 12 md afl_build; cd afl_build cp ../test.c . CC=$AFL/afl-clang-fast $ CC -o test test.c -g -no-pie md symcc_build; cd symcc_build cp ../test.c . CC=~/proj/symcc/symcc $ CC -o test test.c -g -no-pie mkdir corpus echo A > corpus/dummy
注意这里必须要两个 afl
1 2 3 $ AFL/afl-fuzz -M afl-master -i corpus -o afl_out -m none -- afl_build/test $ AFL/afl-fuzz -M afl-slave -i corpus -o afl_out -m none -- afl_build/test symcc_fuzzing_helper -o afl_out -a afl-slave -n symcc -- symcc_build/test
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $ tree -L 2 . ├── afl_build │ ├── test │ └── test.c ├── afl_out │ ├── afl-master │ └── symcc ├── corpus │ └── dummy ├── symcc_build │ ├── afl_out │ ├── test │ └── test.c └── test.c
效果如下 看来还是不错的 单 afl 测要个半天 而且最重要的这个不会干扰 afl 的插桩 编译一个 afl 的版本和 symcc 的版本 这就非常 nice.
counter “imported” in the “path geometry” section increase after a short time - this means that the fuzzer instances and SymCC are exchanging inputs.
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 afl_out ├── afl-master │ ├── crashes │ │ ├── id:000000,sig:04,src:000003,op:havoc,rep:2 │ │ └── README.txt │ ├── fuzz_bitmap │ ├── fuzzer_stats │ ├── hangs │ ├── plot_data │ └── queue │ ├── id:000000,orig:dummy │ ├── id:000001,src:000000,op:havoc,rep:4,+cov │ ├── id:000002,src:000000,op:havoc,rep:8,+cov │ └── id:000003,src:000001+000002,op:splice,rep:8,+cov ├── afl-slave │ ├── crashes │ │ ├── id:000000,sig:04,src:000002+000003,op:splice,rep:2 │ │ └── README.txt │ ├── fuzz_bitmap │ ├── fuzzer_stats │ ├── hangs │ ├── plot_data │ └── queue │ ├── id:000000,orig:dummy │ ├── id:000001,src:000000,op:havoc,rep:2,+cov │ ├── id:000002,sync:afl-master,src:000002,+cov │ └── id:000003,sync:afl-master,src:000003,+cov └── symcc ├── bitmap ├── crashes ├── hangs ├── queue │ ├── id:000000,src:000001 │ ├── id:000001,src:000002 │ └── id:000002,src:000002 └── stats
# helper 原理
大概看了一下
从目标 slave 的 queue 读取 queue 上的 testcase 查 hash 表看是不是新的 case
用这个 testcase 去执行一次 symcc 编译过的程序 获取多个指导输出
分别用这几个 symcc 的指导输出 送给 afl-showmap 去测试能不能触发新的路径 根据它的返回值 决定挂到 afl 的哪个队列上
afl-showmap
1 afl-showmap -t 5000 -m none -b -o map.data -- ~/vuln-discovery/symcc_test/afl_build/test < ./Cargo.toml
根据一个输入 将被测程序本次的位图写入一个 map.data 文件