在阅读了Neuzz的源码之后,决定记录下阅读源码整理出的Neuzz具体工作机制,以供日后回顾。如有出现理解错误,烦请路过的大佬们多指点,不胜感激。
综述
NEUZZ最主要的思想是:利用神经网络来模拟程序的分支行为。
neuzz是想通过有策略地修改现有seeds的一些bytes以期来产生interesting seeds从而能触发未执行过的edge。而这个策略要借助神经网络才能得以具体实施。因此neuzz中的神经网络输入层是代表seed文件内容的向量,输出层表示该seed经过程序中各edge的概率。通过随机的挑选一些output neuron代表未执行过的edge,然后计算各neuron关于一些seed的bytes的梯度。根据梯度提供的信息对这些bytes有策略的改动,从而得到新的seeds去测试程序。
程序结构
NEUZZ的主要工作机制是一个C文件和一个py文件交互的工作过程。两个文件的通信是利用socket通信进行的。
neuzz.c的工作
在main()函数中,对执行该文件时的各项命令参数进行解析之后,就是众多封装好的函数了。
- setup_signal_handers(); 设置信号处理程序
- check_cpu_governor(); 检查cpu的调速器
- get_core_count();
- bind_to_free_cpu();
- setup_shm(); 注册共享内存和virgin_bits数组
- init_count_class16();
- setup_dirs_fds();
… - copy_seeds(in_dir, out_dir); 将作者在neuzz_in文件夹中准备的seeds复制到seeds/文件夹下。(当然这里仅指按作者github上所指示的操作时,最本意就是从你存放seeds的文件夹复制到最终存储程序执行过程中的所有输出的文件夹)。
- init_forkserver(argv+optind); emmmm, forkserver机制暂时还不懂,好像是能让程序并行测试seeds的。
- start_fuzz(len); 前面的那些程序都是完成准备工作,这个函数开始正式执行。所以下面对该函数的具体工作流进行详细分析。
start_fuzz(len)
- 首先,通过socket通信与python程序建立通信连接。在这里python程序端充当服务端,c程序端充当客户端。这也是为什么在执行neuzz的时候,要先执行py程序,再执行c程序的原因。
- dry_run(dir, stage); 在stage = 1时,在dir处运行种子,然后将有用的种子保存到out_dir中; 当stage = 2时,计算平均执行时间。
- fuzz_lop(); 在该函数中利用梯度信息指导fuzzing。下面对该函数进行详细解析。
fuzz_lop(char * grad_file, int sock)
- dry_run("./splice_seeds", 1); 将splice_seeds中有用的seeds保存到输出文件夹中。(splice_seeds文件夹存放的是Python程序计算梯度时将两个seeds按某种拼接策略拼接成的新seeds,splice_seeds在后面的循环中文件数维持在1000,因为每但单数轮就覆盖上一次单数轮的文件,双数论就覆盖)。
- 将python程序计算得到的记录梯度信息的文件拷贝一份为"gradient_info"。
- 设置重新训练阈值。当处于第一轮训练时,程序处理完750行梯度文件后重新训练;此后的重新训练阈值为1000行。这里的重新训练是指:达到阈值之后,给py程序通信,重新获取梯度文件,以供神经网络再训练。
- while循环处理梯度文件。每次读取一行,将这一行中的三部分信息提取出来,之后执行
gen_mutate()
。
gen_mutate
- 根据当前行提取出的seed文件名将文件中的内容加载到数组out_buf中。然后根据之前记录的输入的bytes的梯度值排序的下标以前0-2, 2-4,4-8,… 这种趋势将这一范围内的bytes都加一以后作为新的seed。(这一策略就是让一区间内的bytes在0-255的范围内同增同减)
- 产生新的seed之后通知fork_server产生一个子进程去以该seed为输入执行被测程序。根据其结果存在不同的文件夹下。
- 通过随机的插入或者删除一些byte来产生新的seeds。
nn.py的工作
- main()函数中调用函数setup_server();
setup_server()
- setup_server()开启socket通信,作为服务端,监听信息;
- 一旦收到客户端信息之后,执行
gen_grad()
;(gen_grad函数的具体功能看下面单独介绍) - socket执行sendall函数,发送TCP数据通知客户端已生成梯度信息文件;
- 进入一个while循环,反复执行步骤3,4;while结束条件是未收到客户端的通信信息。
gen_grad(data)
- process_data(); 对通过afl收集的seeds进行预处理。
- 获取当前输出文件夹中最大的文件大小值赋值给MAX_BITMAP_SIZE,用于后面矩阵列的初始化。
- 利用afl-showmap获取每一个seeds的输出路径。
- 将所有seeds的输出路径生成一个01矩阵,再经过去重处理后,用新矩阵中的列的数量更新MAX_BITMAP_SIZE以及将矩阵的每一行储存到bitmaps/文件夹下对应的seeds名文件中
- model = build_model(); 在建立神经网络模型时,利用gen_grad()中获取的MAX_BITMAP_SIZE作为神经网络的输出层神经元个数;
该神经网络的输入层是seed文件的向量化的结果,输出层表示该seed经过程序各edge的可能性。所以整个输出层就是该程序中的“所有”edge。
- train(model);建立模型之后,对模型进行训练;训练数据是seeds/文件夹下的所有数据,对数据进行向量化,长度不够则补0等;最后将训练完成的模型权重保存到文件hard_label.h5中;
gen_mutate2(model, 500, data[:5] == b"train" )
;生成梯度信息去指导未来的mutation;在这里有一个magic number的问题,函数实参500是表达在输出层挑选多少个output_neuron;- 给标记训练次数的round_cnt变量加一。
gen_mutate2(model, edge_num, sign)
-
从seed_list中随机抽取500个(其值也就是edge_num的值)seeds将其对应的文件名存放在数组rand_seed2中;seed_list中元素的数量小于edge_num时,有放回的抽取;否则,无放回的抽取。(np.random.choice)
seed_list = glob.glob(’./seeds/*’)
-
从new_seeds中随机抽取500个(其值也就是edge_num的值)seeds将其对应的文件名存放在数组rand_seed1中;抽取规则同上。(np.random.choice)
new_seeds = glob.glob(’./seeds/id_*’);再记录下那个疑问:我发现所有的seeds的名称都是id_开头的,感觉new_seeds = seed_list恒成立。
-
interested_indice = np.random.choice(MAX_BITMAP_SIZE, edge_num);挑选输出层用于计算梯度的output_neuron;
-
执行
adv_list = fn(index, fl, model, layer_list, idxx, 1)
,对于给定的输入计算梯度信息。fn =
gen_adv2
if sign else gen_adv3
index就是依次从interested_indice中读取的值,也就是用于计算梯度的输出层神经元下标
fl = [rand_seed1[idxx], rand_seed2[idxx]]
idxx就是从1到500的递增变量 -
将返回的adv_list中的每个元素所包含的三部分提取出来,存入文件gradient_info_p中。
gen_adv2(f, fl, model, layer_list, idxx, splice)
其功能是对于传入的fl数组中的seeds计算其梯度。
- 首先利用loss = layer_list[-2][1].output[:, f],提取出下标为f的输出神经元的loss。layer_list[-2]表示从后往前数,也就是指激活函数前的输出层。
- 利用grads = K.gradients(loss, model.input)[0],获取指定输出神经元对于输入层的gradients。
keras.backend.gradients(loss, variables) 返回variables在loss上的梯度。
loss: 需要最小化的标量张量。
variables: 变量列表。 - 利用iterate = K.function([model.input], [loss, grads]),构建一个keras函数,该函数将在给定输入的情况下返回对应的输出。
我对于K.function的理解是:使用该函数时的参数都是象征意义的。相当于指明了要使用的函数的输入和输出,在神经网络层面上的映射关系。
- 将fl数组中的两个seed分别作为函数的输入,得到对应的输入层的梯度,并对梯度值进行处理得到其从大到小排列对应的下标。
- 将梯度从大到小排列对应的下标,以及对应的正负情况和该seed文件名保存在list中返回。
- 在非第一轮训练的时候,还要将fl数组中的两个seeds进行拼接,产生新的seed,再利用这个新的seed进行与3,4,5步的操作。
总结一下:如果是第一轮训练的话,这个函数会返回一个包含两个元素的list,每个元素对应这fl数组中的两个seeds所计算得到的梯度下标、梯度符号及seed文件名;此后的训练,该函数返回的list包含三个元素,增加了一个fl数组中的两个seed拼接成的第三个seed所计算出的梯度下标、梯度符号及seed文件名。