首页
社区
课程
招聘
AFL二三事 -- 源码分析 3
发表于: 2021-9-27 16:53 34233

AFL二三事 -- 源码分析 3

2021-9-27 16:53
34233

AFL二三事 -- 源码分析 3

本文为《AFL二三事》-- 源码分析系列的第三篇,主要阅读AFL的fuzzer部分的源码,学习AFL的fuzz核心。

当别人都要快的时候,你要慢下来。

AFL中最重要的部分便是fuzzer的实现部分——afl_fuzz.c ,其主要作用是通过不断变异测试用例来影响程序的执行路径。该文件代码量在8000行左右,处于篇幅原因,我们不会对每一个函数进行源码级分析,而是按照功能划分,介绍其中的核心函数。该文件属于AFL整个项目的核心中的核心,强烈建议通读该文件。

在介绍源码的同时,会穿插AFL的整体运行过程和设计思路,辅助理解源码的设计思路。

在功能上,可以总体上分为3部分:

我们将按照以上3个功能对其中的关键函数和流程进行分析。

该循环主要通过 getopt 获取各种环境配置、选项参数等。

调用 sigaction ,注册信号处理函数,设置信号句柄。具体的信号内容如下:

读取环境变量 ASAN_OPTIONSMSAN_OPTIONS,做一些必要性检查。

如果通过 -M或者-S指定了 sync_id,则更新 out_dirsync_dir 的值:设置 sync_dir 的值为 out_dir,设置 out_dir 的值为out_dir/sync_id

copy当前命令行参数,保存。

检查是否在tty终端上面运行:读取环境变量 AFL_NO_UI ,如果存在,设置 not_on_tty 为1,并返回;通过 ioctl 读取window size,如果报错为 ENOTTY,表示当前不在一个tty终端运行,设置 not_on_tty

该函数用于设置共享内存和 virgin_bits,属于比较重要的函数,这里我们结合源码来解析一下:

这里通过 trace_bitsvirgin_bits 两个 bitmap 来分别记录当前的 tuple 信息及整体 tuple 信息,其中 trace_bits 位于共享内存上,便于进行进程间通信。通过 virgin_tmoutvirgin_crash 两个 bitmap 来记录 fuzz 过程中出现的所有目标程序超时以及崩溃的 tuple 信息。

该函数用于准备输出文件夹和文件描述符,结合源码进行解析:

该函数的源码中,开发者对关键位置均做了清楚的注释,很容易理解,不做过多解释。

该函数会将 in_dir 目录下的测试用例扫描到 queue 中,并且区分该文件是否为经过确定性变异的input,如果是的话跳过,以节省时间。
调用函数 add_to_queue() 将测试用例排成queue队列。该函数会在启动时进行调用。

该函数主要用于将新的test case添加到队列,初始化 fname 文件名称,增加cur_depth 深度,增加 queued_paths 测试用例数量等。

首先,queue_entry 结构体定义如下:

然后在函数内部进行的相关操作如下:

在输出目录中为输入测试用例创建硬链接。

变量 timeout_given 没有被设置时,会调用到该函数。该函数主要是在没有指定 -t 选项进行 resuming session 时,避免一次次地自动调整超时时间。

识别参数中是否有“@@”,如果有,则替换为 out_dir/.cur_input ,没有则返回:

检查指定路径要执行的程序是否存在,是否为shell脚本,同时检查elf文件头是否合法及程序是否被插桩。

调用 get_cur_time() 函数获取开始时间,检查是否处于 qemu_mode

该函数是AFL中的一个关键函数,它会执行 input 文件夹下的预先准备的所有测试用例,生成初始化的 queue 和 bitmap,只对初始输入执行一次。函数控制流程图如下:

image-20210907201943574

下面将结合函数源码进行解析(删除部分非关键代码):

总结以上流程:

该函数同样为AFL的一个关键函数,用于新测试用例的校准,在处理输入目录时执行,以便在早期就发现有问题的测试用例,并且在发现新路径时,评估新发现的测试用例的是否可变。该函数在 perform_dry_runsave_if_interestingfuzz_onepilot_fuzzingcore_fuzzing函数中均有调用。该函数主要用途是初始化并启动fork server,多次运行测试用例,并用 update_bitmap_score 进行初始的byte排序。

函数控制流程图如下:

image-20210907203020918

结合源码进行解读如下:

总结以上过程如下:

AFL的fork server机制避免了多次执行 execve() 函数的多次调用,只需要调用一次然后通过管道发送命令即可。该函数主要用于启动APP和它的fork server。函数整体控制流程图如下:

image-20210908115123089

结合源码梳理一下函数流程:

我们结合fuzzer对该函数的调用来梳理完整的流程如下:

启动目标程序进程后,目标程序会运行一个fork server,fuzzer自身并不负责fork子进程,而是通过管道与fork server通信,由fork server来完成fork以及继续执行目标程序的操作。

未命名绘图.drawio

对于fuzzer和目标程序之间的通信状态我们可以通过下图来梳理:

未命名绘图.drawio

结合前面的插桩部分一起来看:

首先,afl-fuzz 会创建两个管道:状态管道和控制管道,然后执行目标程序。此时的目标程序的 main() 函数已经被插桩,程序控制流进入 __afl_maybe_log 中。如果fuzz是第一次执行,则此时的程序就成了fork server们之后的目标程序都由该fork server通过fork生成子进程来运行。fuzz进行过程中,fork server会一直执行fork操作,并将子进程的结束状态通过状态管道传递给 afl-fuzz

(对于fork server的具体操作,在前面插桩部分时已经根据源码进行了说明,可以回顾一下。)

该函数主要执行目标应用程序,并进行超时监控,返回状态信息,被调用的程序会更新 trace_bits[]

结合源码进行解释:

当我们发现一个新路径时,需要判断发现的新路径是否更“favorable”,也就是是否包含最小的路径集合能遍历到所有bitmap中的位,并在之后的fuzz过程中聚焦在这些路径上。

以上过程的第一步是为bitmap中的每个字节维护一个 top_rated[] 的列表,这里会计算究竟哪些位置是更“合适”的,该函数主要实现该过程。

函数的控制流程图如下:

image-20210909111119930

结合源码进行解释:

在前面讨论的关于case的 top_rated 的计算中,还有一个机制是检查所有的 top_rated[] 条目,然后顺序获取之前没有遇到过的byte的对比分数低的“获胜者”进行标记,标记至少会维持到下一次运行之前。在所有的fuzz步骤中,“favorable”的条目会获得更多的执行时间。

函数的控制流程图如下:

image-20210909141024743

结合源码解析如下:

这里根据网上公开的一个例子来理解该过程:

现假设有如下tuple和seed信息:

tuple: t0, t1, t2, t3, t4

seed: s0, s1, s2

将按照如下过程进行筛选和判断:

进入主循环前的准备工作使用的函数之一,主要作用为在处理输入目录的末尾显示统计信息,警告信息以及硬编码的常量;

进入主循环前的准备工作使用的函数之一,主要作用为在resume时,尝试查找要开始的队列的位置。

也是准备工作函数之一,主要作用为更新统计信息文件以进行无人值守的监视。

该函数主要保存自动生成的extras。

这里是seed变异的主循环处理过程,我们将结合流程图和源码进行详细解读。

主循环的控制流程图如下(将while部分单独设置为了一个函数,只看循环部分即可):

image-20210909164334608

主循环源码:

总结以上内容,该处该过程整体如下:

该函数源码在1000多行,出于篇幅原因,我们简要介绍函数的功能。但强烈建议通读该函数源码,

函数主要是从queue中取出entry进行fuzz,成功返回0,跳过或退出的话返回1。

整体过程:

这里涉及到AFL中的变异策略,不在本次的讨论中,感兴趣的小伙伴可以结合源码自行进行研究。

该函数的主要作用是进行queue同步,先读取有哪些fuzzer文件夹,然后读取其他fuzzer文件夹下的queue文件夹中的测试用例,然后以此执行。如果在执行过程中,发现这些测试用例可以触发新路径,则将测试用例保存到自己的queue文件夹中,并将最后一个同步的测试用例的id写入到 .synced/fuzzer文件夹名 文件中,避免重复运行。

分析完源码,可以感受到,AFL遵循的基本原则是简单有效,没有进行过多的复杂的优化,能够针对fuzz领域的痛点,对症下药,拒绝花里胡哨,给出切实可行的解决方案,在漏洞挖掘领域的意义的确非同凡响。后期的很多先进的fuzz工具基本沿用了AFL的思路,甚至目前为止已基本围绕AFL建立了“生态圈”,涉及到多个平台、多种漏洞挖掘对象,对于安全研究员来说实属利器,值得从事fuzz相关工作的研究员下足功夫去体会AFL的精髓所在。

考虑到篇幅限制,我们没有对AFL中的变异策略进行源码说明,实属遗憾。如果有机会,将新开文章详细介绍AFL的变异策略和源码分析。

 
 
 
while ((opt = getopt(argc, argv, "+i:o:f:m:b:t:T:dnCB:S:M:x:QV")) > 0)
  ... ...
while ((opt = getopt(argc, argv, "+i:o:f:m:b:t:T:dnCB:S:M:x:QV")) > 0)
  ... ...
信号 作用
SIGHUP/SIGINT/SIGTERM 处理各种“stop”情况
SIGALRM 处理超时的情况
SIGWINCH 处理窗口大小
SIGUSER1 用户自定义信号,这里定义为skip request
SIGSTP/SIGPIPE 不是很重要的一些信号,可以不用关心
/* Configure shared memory and virgin_bits. This is called at startup. */
 
EXP_ST void setup_shm(void) {
 
  u8* shm_str;
 
  if (!in_bitmap) memset(virgin_bits, 255, MAP_SIZE);
  // 如果 in_bitmap 为空,调用 memset 初始化数组 virgin_bits[MAP_SIZE] 的每个元素的值为 ‘255’。
 
  memset(virgin_tmout, 255, MAP_SIZE); // 调用 memset 初始化数组 virgin_tmout[MAP_SIZE] 的每个元素的值为 ‘255’。
  memset(virgin_crash, 255, MAP_SIZE); // 调用 memset 初始化数组 virgin_crash[MAP_SIZE] 的每个元素的值为 ‘255’。
 
  shm_id = shmget(IPC_PRIVATE, MAP_SIZE, IPC_CREAT | IPC_EXCL | 0600);
  // 调用 shmget 函数分配一块共享内存,并将返回的共享内存标识符保存到 shm_id
 
  if (shm_id < 0) PFATAL("shmget() failed");
 
  atexit(remove_shm); // 注册 atexit handler 为 remove_shm
 
  shm_str = alloc_printf("%d", shm_id); // 创建字符串 shm_str
 
  /* If somebody is asking us to fuzz instrumented binaries in dumb mode,
     we don't want them to detect instrumentation, since we won't be sending
     fork server commands. This should be replaced with better auto-detection
     later on, perhaps? */
 
  if (!dumb_mode) setenv(SHM_ENV_VAR, shm_str, 1);
  // 如果不是dumb_mode,设置环境变量 SHM_ENV_VAR 的值为 shm_str
 
  ck_free(shm_str);
 
  trace_bits = shmat(shm_id, NULL, 0);
  // 设置 trace_bits 并初始化为0
 
  if (trace_bits == (void *)-1) PFATAL("shmat() failed");
 
}
/* Configure shared memory and virgin_bits. This is called at startup. */
 
EXP_ST void setup_shm(void) {
 
  u8* shm_str;
 
  if (!in_bitmap) memset(virgin_bits, 255, MAP_SIZE);
  // 如果 in_bitmap 为空,调用 memset 初始化数组 virgin_bits[MAP_SIZE] 的每个元素的值为 ‘255’。
 
  memset(virgin_tmout, 255, MAP_SIZE); // 调用 memset 初始化数组 virgin_tmout[MAP_SIZE] 的每个元素的值为 ‘255’。
  memset(virgin_crash, 255, MAP_SIZE); // 调用 memset 初始化数组 virgin_crash[MAP_SIZE] 的每个元素的值为 ‘255’。
 
  shm_id = shmget(IPC_PRIVATE, MAP_SIZE, IPC_CREAT | IPC_EXCL | 0600);
  // 调用 shmget 函数分配一块共享内存,并将返回的共享内存标识符保存到 shm_id
 
  if (shm_id < 0) PFATAL("shmget() failed");
 
  atexit(remove_shm); // 注册 atexit handler 为 remove_shm
 
  shm_str = alloc_printf("%d", shm_id); // 创建字符串 shm_str
 
  /* If somebody is asking us to fuzz instrumented binaries in dumb mode,
     we don't want them to detect instrumentation, since we won't be sending
     fork server commands. This should be replaced with better auto-detection
     later on, perhaps? */
 
  if (!dumb_mode) setenv(SHM_ENV_VAR, shm_str, 1);
  // 如果不是dumb_mode,设置环境变量 SHM_ENV_VAR 的值为 shm_str
 
  ck_free(shm_str);
 
  trace_bits = shmat(shm_id, NULL, 0);
  // 设置 trace_bits 并初始化为0
 
  if (trace_bits == (void *)-1) PFATAL("shmat() failed");
 
}
EXP_ST void setup_dirs_fds(void) {
 
  u8* tmp;
  s32 fd;
 
  ACTF("Setting up output directories...");
 
  if (sync_id && mkdir(sync_dir, 0700) && errno != EEXIST)
      PFATAL("Unable to create '%s'", sync_dir);
  /* 如果sync_id,且创建sync_dir文件夹并设置权限为0700,如果报错单errno不是 EEXIST ,抛出异常 */
 
  if (mkdir(out_dir, 0700)) { // 创建out_dir, 权限为0700
 
    if (errno != EEXIST) PFATAL("Unable to create '%s'", out_dir);
 
    maybe_delete_out_dir();
 
  } else {
 
    if (in_place_resume) // 创建成功
      FATAL("Resume attempted but old output directory not found");
 
    out_dir_fd = open(out_dir, O_RDONLY); // 以只读模式打开,返回fd:out_dir_fd
 
#ifndef __sun
 
    if (out_dir_fd < 0 || flock(out_dir_fd, LOCK_EX | LOCK_NB))
      PFATAL("Unable to flock() output directory.");
 
#endif /* !__sun */
 
  }
 
  /* Queue directory for any starting & discovered paths. */
 
  tmp = alloc_printf("%s/queue", out_dir);
  if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp); 
  // 创建 out_dir/queue 文件夹,权限为0700
 
  ck_free(tmp);
 
  /* Top-level directory for queue metadata used for session
     resume and related tasks. */
 
  tmp = alloc_printf("%s/queue/.state/", out_dir);
 
  // 创建 out_dir/queue/.state 文件夹,用于保存session resume 和相关tasks的队列元数据。
  if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
  ck_free(tmp);
 
  /* Directory for flagging queue entries that went through
     deterministic fuzzing in the past. */
 
  tmp = alloc_printf("%s/queue/.state/deterministic_done/", out_dir);
  if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
  ck_free(tmp);
 
  /* Directory with the auto-selected dictionary entries. */
 
  tmp = alloc_printf("%s/queue/.state/auto_extras/", out_dir);
  if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
  ck_free(tmp);
 
  /* The set of paths currently deemed redundant. */
 
  tmp = alloc_printf("%s/queue/.state/redundant_edges/", out_dir);
  if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
  ck_free(tmp);
 
  /* The set of paths showing variable behavior. */
 
  tmp = alloc_printf("%s/queue/.state/variable_behavior/", out_dir);
  if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
  ck_free(tmp);
 
  /* Sync directory for keeping track of cooperating fuzzers. */
 
  if (sync_id) {
 
    tmp = alloc_printf("%s/.synced/", out_dir);
 
    if (mkdir(tmp, 0700) && (!in_place_resume || errno != EEXIST))
      PFATAL("Unable to create '%s'", tmp);
 
    ck_free(tmp);
 
  }
 
  /* All recorded crashes. */
 
  tmp = alloc_printf("%s/crashes", out_dir);
  if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
  ck_free(tmp);
 
  /* All recorded hangs. */
 
  tmp = alloc_printf("%s/hangs", out_dir);
  if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
  ck_free(tmp);
 
  /* Generally useful file descriptors. */
 
  dev_null_fd = open("/dev/null", O_RDWR);
  if (dev_null_fd < 0) PFATAL("Unable to open /dev/null");
 
  dev_urandom_fd = open("/dev/urandom", O_RDONLY);
  if (dev_urandom_fd < 0) PFATAL("Unable to open /dev/urandom");
 
  /* Gnuplot output file. */
 
  tmp = alloc_printf("%s/plot_data", out_dir);
  fd = open(tmp, O_WRONLY | O_CREAT | O_EXCL, 0600);
  if (fd < 0) PFATAL("Unable to create '%s'", tmp);
  ck_free(tmp);
 
  plot_file = fdopen(fd, "w");
  if (!plot_file) PFATAL("fdopen() failed");
 
  fprintf(plot_file, "# unix_time, cycles_done, cur_path, paths_total, "
                     "pending_total, pending_favs, map_size, unique_crashes, "
                     "unique_hangs, max_depth, execs_per_sec\n");
                     /* ignore errors */
EXP_ST void setup_dirs_fds(void) {
 
  u8* tmp;
  s32 fd;
 
  ACTF("Setting up output directories...");
 
  if (sync_id && mkdir(sync_dir, 0700) && errno != EEXIST)
      PFATAL("Unable to create '%s'", sync_dir);
  /* 如果sync_id,且创建sync_dir文件夹并设置权限为0700,如果报错单errno不是 EEXIST ,抛出异常 */
 
  if (mkdir(out_dir, 0700)) { // 创建out_dir, 权限为0700
 
    if (errno != EEXIST) PFATAL("Unable to create '%s'", out_dir);
 
    maybe_delete_out_dir();
 
  } else {
 
    if (in_place_resume) // 创建成功
      FATAL("Resume attempted but old output directory not found");
 
    out_dir_fd = open(out_dir, O_RDONLY); // 以只读模式打开,返回fd:out_dir_fd
 
#ifndef __sun
 
    if (out_dir_fd < 0 || flock(out_dir_fd, LOCK_EX | LOCK_NB))
      PFATAL("Unable to flock() output directory.");
 
#endif /* !__sun */
 
  }
 
  /* Queue directory for any starting & discovered paths. */
 
  tmp = alloc_printf("%s/queue", out_dir);
  if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp); 
  // 创建 out_dir/queue 文件夹,权限为0700
 
  ck_free(tmp);
 
  /* Top-level directory for queue metadata used for session
     resume and related tasks. */
 
  tmp = alloc_printf("%s/queue/.state/", out_dir);
 
  // 创建 out_dir/queue/.state 文件夹,用于保存session resume 和相关tasks的队列元数据。
  if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
  ck_free(tmp);
 
  /* Directory for flagging queue entries that went through
     deterministic fuzzing in the past. */
 
  tmp = alloc_printf("%s/queue/.state/deterministic_done/", out_dir);
  if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
  ck_free(tmp);
 
  /* Directory with the auto-selected dictionary entries. */
 
  tmp = alloc_printf("%s/queue/.state/auto_extras/", out_dir);
  if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
  ck_free(tmp);
 
  /* The set of paths currently deemed redundant. */
 
  tmp = alloc_printf("%s/queue/.state/redundant_edges/", out_dir);
  if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
  ck_free(tmp);
 
  /* The set of paths showing variable behavior. */
 
  tmp = alloc_printf("%s/queue/.state/variable_behavior/", out_dir);
  if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
  ck_free(tmp);
 
  /* Sync directory for keeping track of cooperating fuzzers. */
 
  if (sync_id) {
 
    tmp = alloc_printf("%s/.synced/", out_dir);
 
    if (mkdir(tmp, 0700) && (!in_place_resume || errno != EEXIST))
      PFATAL("Unable to create '%s'", tmp);
 
    ck_free(tmp);
 
  }
 
  /* All recorded crashes. */
 
  tmp = alloc_printf("%s/crashes", out_dir);
  if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
  ck_free(tmp);
 
  /* All recorded hangs. */
 
  tmp = alloc_printf("%s/hangs", out_dir);
  if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
  ck_free(tmp);
 
  /* Generally useful file descriptors. */
 
  dev_null_fd = open("/dev/null", O_RDWR);
  if (dev_null_fd < 0) PFATAL("Unable to open /dev/null");
 
  dev_urandom_fd = open("/dev/urandom", O_RDONLY);
  if (dev_urandom_fd < 0) PFATAL("Unable to open /dev/urandom");
 
  /* Gnuplot output file. */
 
  tmp = alloc_printf("%s/plot_data", out_dir);
  fd = open(tmp, O_WRONLY | O_CREAT | O_EXCL, 0600);
  if (fd < 0) PFATAL("Unable to create '%s'", tmp);
  ck_free(tmp);
 
  plot_file = fdopen(fd, "w");
  if (!plot_file) PFATAL("fdopen() failed");
 
  fprintf(plot_file, "# unix_time, cycles_done, cur_path, paths_total, "
                     "pending_total, pending_favs, map_size, unique_crashes, "
                     "unique_hangs, max_depth, execs_per_sec\n");
                     /* ignore errors */
 
struct queue_entry {
 
  u8* fname;                          /* File name for the test case      */
  u32 len;                            /* Input length                     */
 
  u8  cal_failed,                     /* Calibration failed?              */
      trim_done,                      /* Trimmed?                         */
      was_fuzzed,                     /* Had any fuzzing done yet?        */
      passed_det,                     /* Deterministic stages passed?     */
      has_new_cov,                    /* Triggers new coverage?           */
      var_behavior,                   /* Variable behavior?               */
      favored,                        /* Currently favored?               */
      fs_redundant;                   /* Marked as redundant in the fs?   */
 
  u32 bitmap_size,                    /* Number of bits set in bitmap     */
      exec_cksum;                     /* Checksum of the execution trace  */
 
  u64 exec_us,                        /* Execution time (us)              */
      handicap,                       /* Number of queue cycles behind    */
      depth;                          /* Path depth                       */
 
  u8* trace_mini;                     /* Trace bytes, if kept             */
  u32 tc_ref;                         /* Trace bytes ref count            */
 
  struct queue_entry *next,           /* Next element, if any             */
                     *next_100;       /* 100 elements ahead               */
 
};
struct queue_entry {
 
  u8* fname;                          /* File name for the test case      */
  u32 len;                            /* Input length                     */
 
  u8  cal_failed,                     /* Calibration failed?              */
      trim_done,                      /* Trimmed?                         */
      was_fuzzed,                     /* Had any fuzzing done yet?        */
      passed_det,                     /* Deterministic stages passed?     */
      has_new_cov,                    /* Triggers new coverage?           */
      var_behavior,                   /* Variable behavior?               */
      favored,                        /* Currently favored?               */
      fs_redundant;                   /* Marked as redundant in the fs?   */
 
  u32 bitmap_size,                    /* Number of bits set in bitmap     */
      exec_cksum;                     /* Checksum of the execution trace  */
 
  u64 exec_us,                        /* Execution time (us)              */
      handicap,                       /* Number of queue cycles behind    */
      depth;                          /* Path depth                       */
 
  u8* trace_mini;                     /* Trace bytes, if kept             */
  u32 tc_ref;                         /* Trace bytes ref count            */
 
  struct queue_entry *next,           /* Next element, if any             */
                     *next_100;       /* 100 elements ahead               */
 
};
/* Append new test case to the queue. */
 
static void add_to_queue(u8* fname, u32 len, u8 passed_det) {
 
  struct queue_entry* q = ck_alloc(sizeof(struct queue_entry));
  // 通过ck_alloc分配一个 queue_entry 结构体,并进行初始化
 
  q->fname        = fname;
  q->len          = len;
  q->depth        = cur_depth + 1;
  q->passed_det   = passed_det;
 
  if (q->depth > max_depth) max_depth = q->depth;
 
  if (queue_top) {
 
    queue_top->next = q;
    queue_top = q;
 
  } else q_prev100 = queue = queue_top = q;
 
  queued_paths++; // queue计数器加1
  pending_not_fuzzed++; // 待fuzz的样例计数器加1
 
  cycles_wo_finds = 0;
 
  /* Set next_100 pointer for every 100th element (index 0, 100, etc) to allow faster iteration. */
  if ((queued_paths - 1) % 100 == 0 && queued_paths > 1) {
 
    q_prev100->next_100 = q;
    q_prev100 = q;
 
  }
 
  last_path_time = get_cur_time();
 
}
/* Append new test case to the queue. */
 
static void add_to_queue(u8* fname, u32 len, u8 passed_det) {
 
  struct queue_entry* q = ck_alloc(sizeof(struct queue_entry));
  // 通过ck_alloc分配一个 queue_entry 结构体,并进行初始化
 
  q->fname        = fname;
  q->len          = len;
  q->depth        = cur_depth + 1;
  q->passed_det   = passed_det;
 
  if (q->depth > max_depth) max_depth = q->depth;
 
  if (queue_top) {
 
    queue_top->next = q;
    queue_top = q;
 
  } else q_prev100 = queue = queue_top = q;
 
  queued_paths++; // queue计数器加1
  pending_not_fuzzed++; // 待fuzz的样例计数器加1
 
  cycles_wo_finds = 0;
 
  /* Set next_100 pointer for every 100th element (index 0, 100, etc) to allow faster iteration. */
  if ((queued_paths - 1) % 100 == 0 && queued_paths > 1) {
 
    q_prev100->next_100 = q;
    q_prev100 = q;
 
  }
 
  last_path_time = get_cur_time();
 
}
/* Detect @@ in args. */
 
EXP_ST void detect_file_args(char** argv) {
 
  u32 i = 0;
  u8* cwd = getcwd(NULL, 0);
 
  if (!cwd) PFATAL("getcwd() failed");
 
  while (argv[i]) {
 
    u8* aa_loc = strstr(argv[i], "@@"); // 查找@@
 
    if (aa_loc) {
 
      u8 *aa_subst, *n_arg;
 
      /* If we don't have a file name chosen yet, use a safe default. */
 
      if (!out_file)
        out_file = alloc_printf("%s/.cur_input", out_dir);
 
      /* Be sure that we're always using fully-qualified paths. */
 
      if (out_file[0] == '/') aa_subst = out_file;
      else aa_subst = alloc_printf("%s/%s", cwd, out_file);
 
      /* Construct a replacement argv value. */
 
      *aa_loc = 0;
      n_arg = alloc_printf("%s%s%s", argv[i], aa_subst, aa_loc + 2);
      argv[i] = n_arg;
      *aa_loc = '@';
 
      if (out_file[0] != '/') ck_free(aa_subst);
 
    }
    i++;
  }
  free(cwd); /* not tracked */
 
}
/* Detect @@ in args. */
 
EXP_ST void detect_file_args(char** argv) {
 
  u32 i = 0;
  u8* cwd = getcwd(NULL, 0);
 
  if (!cwd) PFATAL("getcwd() failed");
 
  while (argv[i]) {
 
    u8* aa_loc = strstr(argv[i], "@@"); // 查找@@
 
    if (aa_loc) {
 
      u8 *aa_subst, *n_arg;
 
      /* If we don't have a file name chosen yet, use a safe default. */
 
      if (!out_file)
        out_file = alloc_printf("%s/.cur_input", out_dir);
 
      /* Be sure that we're always using fully-qualified paths. */
 
      if (out_file[0] == '/') aa_subst = out_file;
      else aa_subst = alloc_printf("%s/%s", cwd, out_file);
 
      /* Construct a replacement argv value. */
 
      *aa_loc = 0;
      n_arg = alloc_printf("%s%s%s", argv[i], aa_subst, aa_loc + 2);
      argv[i] = n_arg;
      *aa_loc = '@';
 
      if (out_file[0] != '/') ck_free(aa_subst);
 
    }
    i++;
  }
  free(cwd); /* not tracked */
 
}
 
 
/* Perform dry run of all test cases to confirm that the app is working as
   expected. This is done only for the initial inputs, and only once. */
 
static void perform_dry_run(char** argv) {
 
  struct queue_entry* q = queue; // 创建queue_entry结构体
  u32 cal_failures = 0;
  u8* skip_crashes = getenv("AFL_SKIP_CRASHES"); // 读取环境变量 AFL_SKIP_CRASHES
 
  while (q) { // 遍历队列
 
    u8* use_mem;
    u8  res;
    s32 fd;
 
    u8* fn = strrchr(q->fname, '/') + 1;
 
    ACTF("Attempting dry run with '%s'...", fn);
 
    fd = open(q->fname, O_RDONLY);
    if (fd < 0) PFATAL("Unable to open '%s'", q->fname);
 
    use_mem = ck_alloc_nozero(q->len);
 
    if (read(fd, use_mem, q->len) != q->len)
      FATAL("Short read from '%s'", q->fname); // 打开q->fname,读取到分配的内存中
 
    close(fd);
 
    res = calibrate_case(argv, q, use_mem, 0, 1); // 调用函数calibrate_case校准测试用例
    ck_free(use_mem);
 
    if (stop_soon) return;
 
    if (res == crash_mode || res == FAULT_NOBITS)
      SAYF(cGRA "    len = %u, map size = %u, exec speed = %llu us\n" cRST,
           q->len, q->bitmap_size, q->exec_us);
 
    switch (res) { // 判断res的值
 
      case FAULT_NONE:
 
        if (q == queue) check_map_coverage(); // 如果为头结点,调用check_map_coverage评估覆盖率
 
        if (crash_mode) FATAL("Test case '%s' does *NOT* crash", fn); // 抛出异常
 
        break;
 
      case FAULT_TMOUT:
 
        if (timeout_given) { // 指定了 -t 选项
 
          /* The -t nn+ syntax in the command line sets timeout_given to '2' and
             instructs afl-fuzz to tolerate but skip queue entries that time
             out. */
 
          if (timeout_given > 1) {
            WARNF("Test case results in a timeout (skipping)");
            q->cal_failed = CAL_CHANCES;
            cal_failures++;
            break;
          }
 
          SAYF(... ...);
 
          FATAL("Test case '%s' results in a timeout", fn);
 
        } else {
 
          SAYF(... ...);
 
          FATAL("Test case '%s' results in a timeout", fn);
 
        }
 
      case FAULT_CRASH: 
 
        if (crash_mode) break;
 
        if (skip_crashes) {
          WARNF("Test case results in a crash (skipping)");
          q->cal_failed = CAL_CHANCES;
          cal_failures++;
          break;
        }
 
        if (mem_limit) { // 建议增加内存
 
          SAYF(... ...);
        } else {
 
          SAYF(... ...);
 
        }
 
        FATAL("Test case '%s' results in a crash", fn);
 
      case FAULT_ERROR:
 
        FATAL("Unable to execute target application ('%s')", argv[0]);
 
      case FAULT_NOINST: // 测试用例运行没有路径信息
 
        FATAL("No instrumentation detected");
 
      case FAULT_NOBITS:  // 没有出现新路径,判定为无效路径
 
        useless_at_start++;
 
        if (!in_bitmap && !shuffle_queue)
          WARNF("No new instrumentation output, test case may be useless.");
 
        break;
 
    }
 
    if (q->var_behavior) WARNF("Instrumentation output varies across runs.");
 
    q = q->next; // 读取下一个queue
 
  }
 
  if (cal_failures) {
 
    if (cal_failures == queued_paths)
      FATAL("All test cases time out%s, giving up!",
            skip_crashes ? " or crash" : "");
 
    WARNF("Skipped %u test cases (%0.02f%%) due to timeouts%s.", cal_failures,
          ((double)cal_failures) * 100 / queued_paths,
          skip_crashes ? " or crashes" : "");
 
    if (cal_failures * 5 > queued_paths)
      WARNF(cLRD "High percentage of rejected test cases, check settings!");
 
  }
 
  OKF("All test cases processed.");
 
}
/* Perform dry run of all test cases to confirm that the app is working as
   expected. This is done only for the initial inputs, and only once. */
 
static void perform_dry_run(char** argv) {
 
  struct queue_entry* q = queue; // 创建queue_entry结构体
  u32 cal_failures = 0;
  u8* skip_crashes = getenv("AFL_SKIP_CRASHES"); // 读取环境变量 AFL_SKIP_CRASHES
 
  while (q) { // 遍历队列
 
    u8* use_mem;
    u8  res;
    s32 fd;
 
    u8* fn = strrchr(q->fname, '/') + 1;
 
    ACTF("Attempting dry run with '%s'...", fn);
 
    fd = open(q->fname, O_RDONLY);
    if (fd < 0) PFATAL("Unable to open '%s'", q->fname);
 
    use_mem = ck_alloc_nozero(q->len);
 
    if (read(fd, use_mem, q->len) != q->len)
      FATAL("Short read from '%s'", q->fname); // 打开q->fname,读取到分配的内存中
 
    close(fd);
 
    res = calibrate_case(argv, q, use_mem, 0, 1); // 调用函数calibrate_case校准测试用例
    ck_free(use_mem);
 
    if (stop_soon) return;
 
    if (res == crash_mode || res == FAULT_NOBITS)
      SAYF(cGRA "    len = %u, map size = %u, exec speed = %llu us\n" cRST,
           q->len, q->bitmap_size, q->exec_us);
 
    switch (res) { // 判断res的值
 
      case FAULT_NONE:
 
        if (q == queue) check_map_coverage(); // 如果为头结点,调用check_map_coverage评估覆盖率
 
        if (crash_mode) FATAL("Test case '%s' does *NOT* crash", fn); // 抛出异常
 
        break;
 
      case FAULT_TMOUT:
 
        if (timeout_given) { // 指定了 -t 选项
 
          /* The -t nn+ syntax in the command line sets timeout_given to '2' and
             instructs afl-fuzz to tolerate but skip queue entries that time
             out. */
 
          if (timeout_given > 1) {
            WARNF("Test case results in a timeout (skipping)");
            q->cal_failed = CAL_CHANCES;
            cal_failures++;
            break;
          }
 
          SAYF(... ...);
 
          FATAL("Test case '%s' results in a timeout", fn);
 
        } else {
 
          SAYF(... ...);
 
          FATAL("Test case '%s' results in a timeout", fn);
 
        }
 
      case FAULT_CRASH: 
 
        if (crash_mode) break;
 
        if (skip_crashes) {
          WARNF("Test case results in a crash (skipping)");
          q->cal_failed = CAL_CHANCES;
          cal_failures++;
          break;
        }
 
        if (mem_limit) { // 建议增加内存
 
          SAYF(... ...);
        } else {
 
          SAYF(... ...);
 
        }
 
        FATAL("Test case '%s' results in a crash", fn);
 
      case FAULT_ERROR:
 
        FATAL("Unable to execute target application ('%s')", argv[0]);
 
      case FAULT_NOINST: // 测试用例运行没有路径信息
 
        FATAL("No instrumentation detected");
 
      case FAULT_NOBITS:  // 没有出现新路径,判定为无效路径
 
        useless_at_start++;
 
        if (!in_bitmap && !shuffle_queue)
          WARNF("No new instrumentation output, test case may be useless.");
 
        break;
 
    }
 
    if (q->var_behavior) WARNF("Instrumentation output varies across runs.");
 
    q = q->next; // 读取下一个queue
 
  }
 
  if (cal_failures) {
 
    if (cal_failures == queued_paths)
      FATAL("All test cases time out%s, giving up!",
            skip_crashes ? " or crash" : "");
 
    WARNF("Skipped %u test cases (%0.02f%%) due to timeouts%s.", cal_failures,
          ((double)cal_failures) * 100 / queued_paths,
          skip_crashes ? " or crashes" : "");
 
    if (cal_failures * 5 > queued_paths)
      WARNF(cLRD "High percentage of rejected test cases, check settings!");
 
  }
 
  OKF("All test cases processed.");
 
}
 
 
 
/* Calibrate a new test case. This is done when processing the input directory
   to warn about flaky or otherwise problematic test cases early on; and when
   new paths are discovered to detect variable behavior and so on. */
 
static u8 calibrate_case(char** argv, struct queue_entry* q, u8* use_mem,
                         u32 handicap, u8 from_queue) {
 
  static u8 first_trace[MAP_SIZE]; // 创建 firts_trace[MAP_SIZE]
 
  u8  fault = 0, new_bits = 0, var_detected = 0, hnb = 0,
      first_run = (q->exec_cksum == 0); // 获取执行追踪结果,判断case是否为第一次运行,若为0则表示第一次运行,来自input文件夹
 
  u64 start_us, stop_us;
 
  s32 old_sc = stage_cur, old_sm = stage_max;
  u32 use_tmout = exec_tmout;
  u8* old_sn = stage_name; // 保存原有 stage_cur, stage_max, stage_name
 
  /* Be a bit more generous about timeouts when resuming sessions, or when
     trying to calibrate already-added finds. This helps avoid trouble due
     to intermittent latency. */
 
  if (!from_queue || resuming_fuzz)
    // 如果from_queue为0(表示case不是来自queue)或者resuming_fuzz为1(表示处于resuming sessions)
    use_tmout = MAX(exec_tmout + CAL_TMOUT_ADD,
                    exec_tmout * CAL_TMOUT_PERC / 100); // 提升 use_tmout 的值
 
  q->cal_failed++;
 
  stage_name = "calibration"; // 设置 stage_name
  stage_max  = fast_cal ? 3 : CAL_CYCLES; // 设置 stage_max,新测试用例的校准周期数
 
  /* Make sure the fork server is up before we do anything, and let's not
     count its spin-up time toward binary calibration. */
 
  if (dumb_mode != 1 && !no_fork server && !forksrv_pid)
    init_fork server(argv); // 没有运行在dumb_mode,没有禁用fork server,切forksrv_pid为0时,启动fork server
 
  if (q->exec_cksum) { // 判断是否为新case(如果这个queue不是来自input文件夹)
 
    memcpy(first_trace, trace_bits, MAP_SIZE);
    hnb = has_new_bits(virgin_bits);
    if (hnb > new_bits) new_bits = hnb;
 
  }
 
  start_us = get_cur_time_us();
 
  for (stage_cur = 0; stage_cur < stage_max; stage_cur++) { // 开始执行 calibration stage,总计执行 stage_max 轮
 
    u32 cksum;
 
    if (!first_run && !(stage_cur % stats_update_freq)) show_stats(); // queue不是来自input,第一轮calibration stage执行结束,刷新一次展示界面
 
    write_to_testcase(use_mem, q->len);
 
    fault = run_target(argv, use_tmout);
 
    /* stop_soon is set by the handler for Ctrl+C. When it's pressed,
       we want to bail out quickly. */
 
    if (stop_soon || fault != crash_mode) goto abort_calibration;
 
 
    if (!dumb_mode && !stage_cur && !count_bytes(trace_bits)) {
      // 如果 calibration stage第一次运行,且不在dumb_mode,共享内存中没有任何路径
      fault = FAULT_NOINST;
      goto abort_calibration;
    }
 
    cksum = hash32(trace_bits, MAP_SIZE, HASH_CONST);
 
    if (q->exec_cksum != cksum) {
 
      hnb = has_new_bits(virgin_bits);
      if (hnb > new_bits) new_bits = hnb;
 
      if (q->exec_cksum) { // 不等于exec_cksum,表示第一次运行,或在相同参数下,每次执行,cksum不同,表示是一个路径可变的queue
 
        u32 i;
 
        for (i = 0; i < MAP_SIZE; i++) {
 
          if (!var_bytes[i] && first_trace[i] != trace_bits[i]) {
                    // 0到MAP_SIZE进行遍历, first_trace[i] != trace_bits[i],表示发现了可变queue
            var_bytes[i] = 1;
            stage_max    = CAL_CYCLES_LONG;
 
          }
 
        }
 
        var_detected = 1;
 
      } else {
 
        q->exec_cksum = cksum; // q->exec_cksum=0,表示第一次执行queue,则设置计算出来的本次执行的cksum
        memcpy(first_trace, trace_bits, MAP_SIZE);
 
      }
 
    }
 
  }
 
  stop_us = get_cur_time_us();
 
  total_cal_us     += stop_us - start_us;  // 保存所有轮次的总执行时间
  total_cal_cycles += stage_max; // 保存总轮次
 
  /* OK, let's collect some stats about the performance of this test case.
     This is used for fuzzing air time calculations in calculate_score(). */
 
  q->exec_us     = (stop_us - start_us) / stage_max; // 单次执行时间的平均值
  q->bitmap_size = count_bytes(trace_bits); // 最后一次执行所覆盖的路径数
  q->handicap    = handicap;
  q->cal_failed  = 0;
 
  total_bitmap_size += q->bitmap_size; // 加上queue所覆盖的路径数
  total_bitmap_entries++;
 
  update_bitmap_score(q);
 
  /* If this case didn't result in new output from the instrumentation, tell
     parent. This is a non-critical problem, but something to warn the user
     about. */
 
  if (!dumb_mode && first_run && !fault && !new_bits) fault = FAULT_NOBITS;
 
abort_calibration:
 
  if (new_bits == 2 && !q->has_new_cov) {
    q->has_new_cov = 1;
    queued_with_cov++;
  }
 
  /* Mark variable paths. */
 
  if (var_detected) { // queue是可变路径
 
    var_byte_count = count_bytes(var_bytes);
 
    if (!q->var_behavior) {
      mark_as_variable(q);
      queued_variable++;
    }
 
  }
 
  // 恢复之前的stage值
  stage_name = old_sn;
  stage_cur  = old_sc;
  stage_max  = old_sm;
 
  if (!first_run) show_stats();
 
  return fault;
 
}
/* Calibrate a new test case. This is done when processing the input directory
   to warn about flaky or otherwise problematic test cases early on; and when
   new paths are discovered to detect variable behavior and so on. */
 
static u8 calibrate_case(char** argv, struct queue_entry* q, u8* use_mem,
                         u32 handicap, u8 from_queue) {
 
  static u8 first_trace[MAP_SIZE]; // 创建 firts_trace[MAP_SIZE]
 
  u8  fault = 0, new_bits = 0, var_detected = 0, hnb = 0,
      first_run = (q->exec_cksum == 0); // 获取执行追踪结果,判断case是否为第一次运行,若为0则表示第一次运行,来自input文件夹
 
  u64 start_us, stop_us;
 
  s32 old_sc = stage_cur, old_sm = stage_max;
  u32 use_tmout = exec_tmout;
  u8* old_sn = stage_name; // 保存原有 stage_cur, stage_max, stage_name
 
  /* Be a bit more generous about timeouts when resuming sessions, or when
     trying to calibrate already-added finds. This helps avoid trouble due
     to intermittent latency. */
 
  if (!from_queue || resuming_fuzz)
    // 如果from_queue为0(表示case不是来自queue)或者resuming_fuzz为1(表示处于resuming sessions)
    use_tmout = MAX(exec_tmout + CAL_TMOUT_ADD,
                    exec_tmout * CAL_TMOUT_PERC / 100); // 提升 use_tmout 的值
 
  q->cal_failed++;
 
  stage_name = "calibration"; // 设置 stage_name
  stage_max  = fast_cal ? 3 : CAL_CYCLES; // 设置 stage_max,新测试用例的校准周期数
 
  /* Make sure the fork server is up before we do anything, and let's not
     count its spin-up time toward binary calibration. */
 
  if (dumb_mode != 1 && !no_fork server && !forksrv_pid)
    init_fork server(argv); // 没有运行在dumb_mode,没有禁用fork server,切forksrv_pid为0时,启动fork server
 
  if (q->exec_cksum) { // 判断是否为新case(如果这个queue不是来自input文件夹)
 
    memcpy(first_trace, trace_bits, MAP_SIZE);
    hnb = has_new_bits(virgin_bits);
    if (hnb > new_bits) new_bits = hnb;
 
  }
 
  start_us = get_cur_time_us();
 
  for (stage_cur = 0; stage_cur < stage_max; stage_cur++) { // 开始执行 calibration stage,总计执行 stage_max 轮
 
    u32 cksum;
 
    if (!first_run && !(stage_cur % stats_update_freq)) show_stats(); // queue不是来自input,第一轮calibration stage执行结束,刷新一次展示界面
 
    write_to_testcase(use_mem, q->len);
 
    fault = run_target(argv, use_tmout);
 
    /* stop_soon is set by the handler for Ctrl+C. When it's pressed,
       we want to bail out quickly. */
 
    if (stop_soon || fault != crash_mode) goto abort_calibration;
 
 
    if (!dumb_mode && !stage_cur && !count_bytes(trace_bits)) {
      // 如果 calibration stage第一次运行,且不在dumb_mode,共享内存中没有任何路径
      fault = FAULT_NOINST;
      goto abort_calibration;
    }
 
    cksum = hash32(trace_bits, MAP_SIZE, HASH_CONST);
 
    if (q->exec_cksum != cksum) {
 
      hnb = has_new_bits(virgin_bits);
      if (hnb > new_bits) new_bits = hnb;
 
      if (q->exec_cksum) { // 不等于exec_cksum,表示第一次运行,或在相同参数下,每次执行,cksum不同,表示是一个路径可变的queue
 
        u32 i;
 
        for (i = 0; i < MAP_SIZE; i++) {
 
          if (!var_bytes[i] && first_trace[i] != trace_bits[i]) {
                    // 0到MAP_SIZE进行遍历, first_trace[i] != trace_bits[i],表示发现了可变queue
            var_bytes[i] = 1;
            stage_max    = CAL_CYCLES_LONG;
 
          }
 
        }
 
        var_detected = 1;
 
      } else {
 
        q->exec_cksum = cksum; // q->exec_cksum=0,表示第一次执行queue,则设置计算出来的本次执行的cksum
        memcpy(first_trace, trace_bits, MAP_SIZE);
 
      }
 
    }
 
  }
 
  stop_us = get_cur_time_us();
 
  total_cal_us     += stop_us - start_us;  // 保存所有轮次的总执行时间
  total_cal_cycles += stage_max; // 保存总轮次
 
  /* OK, let's collect some stats about the performance of this test case.
     This is used for fuzzing air time calculations in calculate_score(). */
 
  q->exec_us     = (stop_us - start_us) / stage_max; // 单次执行时间的平均值
  q->bitmap_size = count_bytes(trace_bits); // 最后一次执行所覆盖的路径数
  q->handicap    = handicap;
  q->cal_failed  = 0;
 
  total_bitmap_size += q->bitmap_size; // 加上queue所覆盖的路径数
  total_bitmap_entries++;
 
  update_bitmap_score(q);
 
  /* If this case didn't result in new output from the instrumentation, tell
     parent. This is a non-critical problem, but something to warn the user
     about. */
 
  if (!dumb_mode && first_run && !fault && !new_bits) fault = FAULT_NOBITS;
 
abort_calibration:
 
  if (new_bits == 2 && !q->has_new_cov) {
    q->has_new_cov = 1;
    queued_with_cov++;
  }
 
  /* Mark variable paths. */
 
  if (var_detected) { // queue是可变路径
 
    var_byte_count = count_bytes(var_bytes);
 
    if (!q->var_behavior) {
      mark_as_variable(q);
      queued_variable++;
    }
 
  }
 
  // 恢复之前的stage值
  stage_name = old_sn;
  stage_cur  = old_sc;
  stage_max  = old_sm;
 
  if (!first_run) show_stats();
 
  return fault;

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2021-9-27 16:58 被有毒编辑 ,原因:
收藏
免费 7
支持
分享
最新回复 (8)
雪    币: 7
活跃值: (4331)
能力值: ( LV9,RANK:270 )
在线值:
发帖
回帖
粉丝
2
太硬了,膜!
2021-9-28 10:18
0
雪    币: 15519
活跃值: (16882)
能力值: (RANK:730 )
在线值:
发帖
回帖
粉丝
3
0x2l 太硬了,膜!
感觉好久没见你了
2021-9-28 14:09
0
雪    币: 7
活跃值: (4331)
能力值: ( LV9,RANK:270 )
在线值:
发帖
回帖
粉丝
4
有毒 感觉好久没见你了
一个月写一篇,稳定输出
2021-9-28 14:44
0
雪    币: 4904
活跃值: (1440)
能力值: ( LV9,RANK:246 )
在线值:
发帖
回帖
粉丝
5

M,YDYYDS

最后于 2021-9-29 10:58 被Saturn35编辑 ,原因: ~
2021-9-29 10:57
0
雪    币: 855
活跃值: (78)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
学习 学习
2021-9-29 22:48
0
雪    币: 420
活跃值: (700)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
7
请问其中的控制图是用什么软件绘制的呢
2021-10-5 09:51
0
雪    币: 15519
活跃值: (16882)
能力值: (RANK:730 )
在线值:
发帖
回帖
粉丝
8
OneShell 请问其中的控制图是用什么软件绘制的呢
一个源码分析工具:understand
2021-10-8 08:45
0
雪    币: 15519
活跃值: (16882)
能力值: (RANK:730 )
在线值:
发帖
回帖
粉丝
9
有小伙伴私信我这个流程图工具,全名就叫understand,有Windows版和Mac版。个人感觉属于SourceInsigt的现代化替代版本,功能还是十分强大的。
2021-10-19 14:27
0
游客
登录 | 注册 方可回帖
返回
//