当前位置: 代码迷 >> 综合 >> NEUZZ: Efficient Fuzzing with Neural Program Smoothing 源码阅读
  详细解决方案

NEUZZ: Efficient Fuzzing with Neural Program Smoothing 源码阅读

热度:86   发布时间:2023-12-05 17:10:33.0

在阅读了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)

  1. 首先,通过socket通信与python程序建立通信连接。在这里python程序端充当服务端,c程序端充当客户端。这也是为什么在执行neuzz的时候,要先执行py程序,再执行c程序的原因。
  2. dry_run(dir, stage); 在stage = 1时,在dir处运行种子,然后将有用的种子保存到out_dir中; 当stage = 2时,计算平均执行时间。
  3. fuzz_lop(); 在该函数中利用梯度信息指导fuzzing。下面对该函数进行详细解析。

fuzz_lop(char * grad_file, int sock)

  1. dry_run("./splice_seeds", 1); 将splice_seeds中有用的seeds保存到输出文件夹中。(splice_seeds文件夹存放的是Python程序计算梯度时将两个seeds按某种拼接策略拼接成的新seeds,splice_seeds在后面的循环中文件数维持在1000,因为每但单数轮就覆盖上一次单数轮的文件,双数论就覆盖)。
  2. 将python程序计算得到的记录梯度信息的文件拷贝一份为"gradient_info"。
  3. 设置重新训练阈值。当处于第一轮训练时,程序处理完750行梯度文件后重新训练;此后的重新训练阈值为1000行。这里的重新训练是指:达到阈值之后,给py程序通信,重新获取梯度文件,以供神经网络再训练。
  4. while循环处理梯度文件。每次读取一行,将这一行中的三部分信息提取出来,之后执行gen_mutate()

gen_mutate

  1. 根据当前行提取出的seed文件名将文件中的内容加载到数组out_buf中。然后根据之前记录的输入的bytes的梯度值排序的下标以前0-2, 2-4,4-8,… 这种趋势将这一范围内的bytes都加一以后作为新的seed。(这一策略就是让一区间内的bytes在0-255的范围内同增同减)
  2. 产生新的seed之后通知fork_server产生一个子进程去以该seed为输入执行被测程序。根据其结果存在不同的文件夹下。
  3. 通过随机的插入或者删除一些byte来产生新的seeds。

nn.py的工作

  1. main()函数中调用函数setup_server();

setup_server()

  1. setup_server()开启socket通信,作为服务端,监听信息;
  2. 一旦收到客户端信息之后,执行gen_grad();(gen_grad函数的具体功能看下面单独介绍)
  3. socket执行sendall函数,发送TCP数据通知客户端已生成梯度信息文件;
  4. 进入一个while循环,反复执行步骤3,4;while结束条件是未收到客户端的通信信息。

gen_grad(data)

  1. process_data(); 对通过afl收集的seeds进行预处理。
    • 获取当前输出文件夹中最大的文件大小值赋值给MAX_BITMAP_SIZE,用于后面矩阵列的初始化。
    • 利用afl-showmap获取每一个seeds的输出路径。
    • 将所有seeds的输出路径生成一个01矩阵,再经过去重处理后,用新矩阵中的列的数量更新MAX_BITMAP_SIZE以及将矩阵的每一行储存到bitmaps/文件夹下对应的seeds名文件中
  2. model = build_model(); 在建立神经网络模型时,利用gen_grad()中获取的MAX_BITMAP_SIZE作为神经网络的输出层神经元个数;

该神经网络的输入层是seed文件的向量化的结果,输出层表示该seed经过程序各edge的可能性。所以整个输出层就是该程序中的“所有”edge。

  1. train(model);建立模型之后,对模型进行训练;训练数据是seeds/文件夹下的所有数据,对数据进行向量化,长度不够则补0等;最后将训练完成的模型权重保存到文件hard_label.h5中;
  2. gen_mutate2(model, 500, data[:5] == b"train" );生成梯度信息去指导未来的mutation;在这里有一个magic number的问题,函数实参500是表达在输出层挑选多少个output_neuron;
  3. 给标记训练次数的round_cnt变量加一。

gen_mutate2(model, edge_num, sign)

  1. 从seed_list中随机抽取500个(其值也就是edge_num的值)seeds将其对应的文件名存放在数组rand_seed2中;seed_list中元素的数量小于edge_num时,有放回的抽取;否则,无放回的抽取。(np.random.choice)

    seed_list = glob.glob(’./seeds/*’)

  2. 从new_seeds中随机抽取500个(其值也就是edge_num的值)seeds将其对应的文件名存放在数组rand_seed1中;抽取规则同上。(np.random.choice)

    new_seeds = glob.glob(’./seeds/id_*’);再记录下那个疑问:我发现所有的seeds的名称都是id_开头的,感觉new_seeds = seed_list恒成立。

  3. interested_indice = np.random.choice(MAX_BITMAP_SIZE, edge_num);挑选输出层用于计算梯度的output_neuron;

  4. 执行adv_list = fn(index, fl, model, layer_list, idxx, 1),对于给定的输入计算梯度信息。

    fn = gen_adv2if sign else gen_adv3
    index就是依次从interested_indice中读取的值,也就是用于计算梯度的输出层神经元下标
    fl = [rand_seed1[idxx], rand_seed2[idxx]]
    idxx就是从1到500的递增变量

  5. 将返回的adv_list中的每个元素所包含的三部分提取出来,存入文件gradient_info_p中。

gen_adv2(f, fl, model, layer_list, idxx, splice)

其功能是对于传入的fl数组中的seeds计算其梯度。

  1. 首先利用loss = layer_list[-2][1].output[:, f],提取出下标为f的输出神经元的loss。layer_list[-2]表示从后往前数,也就是指激活函数前的输出层。
  2. 利用grads = K.gradients(loss, model.input)[0],获取指定输出神经元对于输入层的gradients。

    keras.backend.gradients(loss, variables) 返回variables在loss上的梯度。
    loss: 需要最小化的标量张量。
    variables: 变量列表。

  3. 利用iterate = K.function([model.input], [loss, grads]),构建一个keras函数,该函数将在给定输入的情况下返回对应的输出。

    我对于K.function的理解是:使用该函数时的参数都是象征意义的。相当于指明了要使用的函数的输入和输出,在神经网络层面上的映射关系。

  4. 将fl数组中的两个seed分别作为函数的输入,得到对应的输入层的梯度,并对梯度值进行处理得到其从大到小排列对应的下标。
  5. 将梯度从大到小排列对应的下标,以及对应的正负情况和该seed文件名保存在list中返回。
  6. 在非第一轮训练的时候,还要将fl数组中的两个seeds进行拼接,产生新的seed,再利用这个新的seed进行与3,4,5步的操作。

总结一下:如果是第一轮训练的话,这个函数会返回一个包含两个元素的list,每个元素对应这fl数组中的两个seeds所计算得到的梯度下标、梯度符号及seed文件名;此后的训练,该函数返回的list包含三个元素,增加了一个fl数组中的两个seed拼接成的第三个seed所计算出的梯度下标、梯度符号及seed文件名。

  相关解决方案