afl-fuzz.c 源码全分析

afl-fuzz.c 源码全分析

picasso big sb

这是整个 afl 项目的核心,本文根据 clion 调试的执行顺序进行分析。

首先使用 afl-gcc 编译并插桩程序:

afl-gcc -g test1.c -o afl-test

然后设置 afl-fuzz 的参数,fuzz_in 和 fuzz_out 是新建的,fuzz_in 中新建了一个 testcase 用来 fuzz:

-i
/home/picasso/Desktop/fuzz_in
-o
/home/picasso/Desktop/fuzz_out
--
/home/picasso/Desktop/afl_test
@@

流程跟进

在进入 fuzz 部分前,根据输入和环境变量对 fuzz 环境进行初始配置,然后进入 fuzz 阶段。

初始配置

在 main 函数开始的时候设置了 doc_path 并利用当前时间初始化了随机数种子。然后进入while循环,处理命令行参数。

while循环

使用 getopt 逐个处理参数,包括 -i -o 等等,他们的用法在 usage 函数中提及:

static void usage(u8* argv0) {SAYF("\n%s [ options ] -- /path/to/fuzzed_app [ ... ]\n\n""Required parameters:\n\n""  -i dir        - input directory with test cases\n""  -o dir        - output directory for fuzzer findings\n\n""Execution control settings:\n\n""  -f file       - location read by the fuzzed program (stdin)\n""  -t msec       - timeout for each run (auto-scaled, 50-%u ms)\n""  -m megs       - memory limit for child process (%u MB)\n""  -Q            - use binary-only instrumentation (QEMU mode)\n\n"     "Fuzzing behavior settings:\n\n""  -d            - quick & dirty mode (skips deterministic steps)\n""  -n            - fuzz without instrumentation (dumb mode)\n""  -x dir        - optional fuzzer dictionary (see README)\n\n""Other stuff:\n\n""  -T text       - text banner to show on the screen\n""  -M / -S id    - distributed mode (see parallel_fuzzing.txt)\n""  -C            - crash exploration mode (the peruvian rabbit thing)\n""  -V            - show version number and exit\n\n""  -b cpu_id     - bind the fuzzing process to the specified CPU core\n\n""For additional tips, please consult %s/README.\n\n",argv0, EXEC_TIMEOUT, MEM_LIMIT, doc_path);exit(1);}

紧接着依次调用 setup_signal_handlers()check_asan_opts()

setup_signal_handlers()

EXP_ST void setup_signal_handlers(void) // EXP_ST 被宏定义为 static
{struct sigaction sa;sa.sa_handler = NULL;sa.sa_flags = SA_RESTART;sa.sa_sigaction = NULL;sigemptyset(&sa.sa_mask);/* Various ways of saying "stop". */sa.sa_handler = handle_stop_sig;sigaction(SIGHUP, &sa, NULL);sigaction(SIGINT, &sa, NULL);sigaction(SIGTERM, &sa, NULL);/* Exec timeout notifications. */sa.sa_handler = handle_timeout;sigaction(SIGALRM, &sa, NULL);/* Window resize */sa.sa_handler = handle_resize;sigaction(SIGWINCH, &sa, NULL);/* SIGUSR1: skip entry */sa.sa_handler = handle_skipreq;sigaction(SIGUSR1, &sa, NULL);/* Things we don't care about. */sa.sa_handler = SIG_IGN;sigaction(SIGTSTP, &sa, NULL);sigaction(SIGPIPE, &sa, NULL);
}

sigaction 函数用于注册信号处理函数,更改接收到信号的默认行为,old_act 保存之前的行为。

int sigaction(int signum, const struct sigaction *act, struct sigaction *old_act);
struct sigaciton{void (*) (int) sa_handler;  //处理函数指针,设置为SIG_DFL时表示默认操作,SIG_IGN忽略信号sigset_t sa_mask;  //指定一个信号集,在调用sa_handler所指向的信号处理函数之前被阻塞int sa_flags; //信号处理修改器,RESTART重启系统调用,...
};

check_asan_opts()

检查了 asan 的相关设置。

fix_up_sync()

如果设置了 sync_id,进入这个函数,根据 sync_idout_dirsync_dir 赋值。

处理环境变量

根据环境变量做了很多检查和初始化:

if (!strcmp(in_dir, out_dir))FATAL("Input and output directories can't be the same");if (dumb_mode) {if (crash_mode) FATAL("-C and -n are mutually exclusive");if (qemu_mode)  FATAL("-Q and -n are mutually exclusive");}if (getenv("AFL_NO_FORKSRV"))    no_forkserver    = 1;if (getenv("AFL_NO_CPU_RED"))    no_cpu_meter_red = 1;if (getenv("AFL_NO_ARITH"))      no_arith         = 1;if (getenv("AFL_SHUFFLE_QUEUE")) shuffle_queue    = 1;if (getenv("AFL_FAST_CAL"))      fast_cal         = 1;if (getenv("AFL_HANG_TMOUT")) {hang_tmout = atoi(getenv("AFL_HANG_TMOUT"));if (!hang_tmout) FATAL("Invalid value of AFL_HANG_TMOUT");}if (dumb_mode == 2 && no_forkserver)FATAL("AFL_DUMB_FORKSRV and AFL_NO_FORKSRV are mutually exclusive");if (getenv("AFL_PRELOAD")) {setenv("LD_PRELOAD", getenv("AFL_PRELOAD"), 1);setenv("DYLD_INSERT_LIBRARIES", getenv("AFL_PRELOAD"), 1);}if (getenv("AFL_LD_PRELOAD"))FATAL("Use AFL_PRELOAD instead of AFL_LD_PRELOAD");

save_cmdline()

保存命令行参数到 origin_cmdline 里。

fix_up_banner()

修剪并创建一个运行横幅。

check_if_tty()

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

get_core_count()

获取系统中的 CPU 核心数量以及当前正在运行的任务数量,并输出相关信息。

  1. 首先,函数会根据操作系统的不同,使用不同的方法获取 CPU 核心数量。对于 BSD 系统,使用 sysctl 函数来获取核心数量;对于其他系统,会尝试使用 sysconf 函数获取核心数量,或者读取 /proc/stat 文件来解析核心数量。
  2. 如果成功获取到核心数量(cpu_core_count > 0),则调用 get_runnable_processes 函数获取当前正在运行的任务数量,并存储在 cur_runnable 变量中。
  3. 输出核心数量、运行任务数量和系统利用率的相关信息。
  4. 根据条件判断,如果核心数量大于 1,则根据任务数量的不同情况输出不同的建议信息。如果任务数量超过核心数量的 1.5 倍,则输出警告信息;如果任务数量加上当前进程的数量小于等于核心数量,则输出建议使用并行任务的信息。
  5. 如果无法获取到核心数量,则将 cpu_core_count 设置为 0,并输出警告信息。

在这里插入图片描述

在 Linux 系统中,可以用 /proc/stat 文件来计算 cpu 的利用率。这个文件包含了所有 CPU 活动的信息,该文件中的所有值都是从系统启动开始累计到当前时刻。

bind_to_free_cpu()

bind_to_free_cpu 函数是用于将当前进程绑定到空闲的 CPU 核心上的函数。它根据 /proc 目录下的进程状态文件来查找已绑定到特定 CPU 的进程,并标记这些 CPU 为已使用状态。然后,它根据标记的情况,找到一个空闲的 CPU 核心,并将当前进程绑定到该核心上。

check_crash_handling()

用于检查系统的崩溃处理设置,以避免将崩溃误认为是超时。

check_cpu_governor()

用于检查系统的 CPU 频率调节器(governor)设置,以确保系统在运行模糊测试时的性能表现。

setup_post()

加载环境变量里提供的动态链接库,主要是设置 post_handler ,后面会用到。

setup_shm()

配置共享内存和virgin_bits :

/* 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);memset(virgin_tmout, 255, MAP_SIZE);memset(virgin_crash, 255, MAP_SIZE);   // MAP_SIZE大小,全部初始化为 '\xff'shm_id = shmget(IPC_PRIVATE, MAP_SIZE, IPC_CREAT | IPC_EXCL | 0600); //申请共享内存//注意是 MAP_SIZE 个字节而不是 bitif (shm_id < 0) PFATAL("shmget() failed");atexit(remove_shm);  // 注册 atexit handler 为 remove_shmshm_str = alloc_printf("%d", shm_id);/* 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 sendingfork server commands. This should be replaced with better auto-detectionlater on, perhaps? */if (!dumb_mode) setenv(SHM_ENV_VAR, shm_str, 1);ck_free(shm_str);trace_bits = shmat(shm_id, NULL, 0);  //现在trace_bits 指向了共享内存头部,它是个 u8*//这意味着trace_bits的单位是字节,它的 ++ 行为相当于字节移动if (trace_bits == (void *)-1) PFATAL("shmat() failed");}

trace_bits对应的位图大概是这样的原理:

在这里插入图片描述

其中涉及到共享内存的相关知识,linux 中主要用下面几个函数进行共享内存的操作:

// key差不多相当于名字,shmflg是一些权限,和文件权限类似,可以设置为0666之类的
// 三个参数都相同就能申请到同一片共享内存,否则会申请到不同内存或者直接报错返回-1
// 函数会返回一个共享内存标识符,后续通过这个标识符进行操作 int shmid = shmget(...);
int shmget(key_t key, size_t size, int shmflg);
/*
第三个参数,shmflg是权限标志
IPC_CREAT 如果共享内存不存在,则创建一个共享内存,否则打开操作。
IPC_EXCL 只有在共享内存不存在的时候,新的共享内存才建立,否则就产生错误。
*/// 第一次创建完共享内存时它还不能被访问,需要使用shmat()把共享内存attach到当前进程的地址空间
// shm_id就是上面的shmid,shm_addr设置为0时会由系统选一个内存地址,shmflg一般都是0
// 函数会返回指向共享内存第一个字节的指针,类似于malloc
void *shmat(int shm_id, const void *shm_addr, int shmflg);// detach, similar to free
int shmdt(const void *shmaddr);// 用来控制共享内存
int shmctl(int shm_id, int command, struct shmid_ds *buf);
/*
第二个参数,command是要采取的操作,它可以取下面的三个值 :
IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。
IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
IPC_RMID:删除共享内存段上面源码中remove_shm函数就被定义为:static void remove_shm(void) {shmctl(shm_id, IPC_RMID, NULL);}
*/

init_count_class16()

利用count_class_lookup8[256]来初始化count_class_lookup16[65536]

static const u8 count_class_lookup8[256] = {[0]           = 0,[1]           = 1,[2]           = 2,[3]           = 4,[4 ... 7]     = 8,[8 ... 15]    = 16,[16 ... 31]   = 32,[32 ... 127]  = 64,[128 ... 255] = 128};

在 sakura 师傅的解释中:

这其实是因为trace_bits是用一个字节来记录是否到达这个路径,和这个路径被命中了多少次的,而这个次数在 0 - 255 之间,但比如一个循环,它循环 5 次和循环 6 次可能是完全一样的效果,为了避免被当成不同的路径,或者说尽可能减少因为命中次数导致的区别。

在每次去计算是否发现了新路径之前,先把这个路径命中数进行规整,比如把命中 5 次和 6 次都统一认为是命中了 8 次,而为什么又需要用一个count_class_lookup16呢,是因为AFL在后面实际进行规整的时候,是一次读两个字节去处理的,为了提高效率,这只是出于效率的考量,实际效果还是上面这种效果。

setup_dir_fds()

out_dir 是我们设置的输出文件夹:

在这里插入图片描述

这个函数的作用是准备输出文件夹,在里面 mkdir 一些子文件夹等,在这之前会进入 maybe_delete_out_dir()函数对输出目录进行清理。

read_testcases()

  • 首先尝试访问in_dir/queue文件夹,如果存在就把in_dir设置为该文件夹;
  • 然后读取in_dir中的所有文件,如果设置了shuffle_queue的值,就用经典的洗牌算法对in_dir中的文件指针重排序;
  • 然后读取in_dir 中的所有文件,设置passed_det的值,在输入文件通过验证后,对其进行add_to_queue操作,并将last_path_time更新为 0 ,将queue_at_start更新为queue_paths

add_to_queue函数的代码如下,它设置了queue_entry的相关内容:

static struct queue_entry *queue,     /* Fuzzing queue (linked list)      */*queue_cur, /* Current offset within the queue  */*queue_top, /* Top of the list                  */*q_prev100; /* Previous 100 marker              *//* 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));q->fname        = fname;q->len          = len;q->depth        = cur_depth + 1;   // 感觉这里一开始应该都是 1q->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++;pending_not_fuzzed++;cycles_wo_finds = 0;  /* Cycles without any new paths     *//* Set next_100 pointer for every 100th element (index 0, 100, etc) to allow faster iteration. */// 每一百个标记一下,类似于 largebi 里面那种 fd_nextsizeif ((queued_paths - 1) % 100 == 0 && queued_paths > 1) {q_prev100->next_100 = q;q_prev100 = q;}last_path_time = get_cur_time();}

load_auto()

加载 AFL 自动生成的一些 token ,也就是比较容易触发漏洞的输入字典,比如边界条件啥的。

一个大循环,加载每一个in_dir/.state/auto_extras/目录下的 auto 文件,进入maybe_add_auto()函数:

/* Helper function for maybe_add_auto() */static inline u8 memcmp_nocase(u8* m1, u8* m2, u32 len) {while (len--) if (tolower(*(m1++)) ^ tolower(*(m2++))) return 1;return 0;}/* Maybe add automatic extra. */static void maybe_add_auto(u8* mem, u32 len) {u32 i;/* Allow users to specify that they don't want auto dictionaries. */// 用户设置了这两个全局变量之一为 0,表示不用 auto 字典if (!MAX_AUTO_EXTRAS || !USE_AUTO_EXTRAS) return;/* Skip runs of identical bytes. *///与第一个字符比较 相同就退出for (i = 1; i < len; i++)if (mem[0] ^ mem[i]) break;//都不相同,直接返回if (i == len) return;/* Reject builtin interesting values. */// 如果len=2或4,就去与interesting数组比较,有一个一样就返回if (len == 2) {i = sizeof(interesting_16) >> 1;  //定义在config.h中while (i--) if (*((u16*)mem) == interesting_16[i] ||*((u16*)mem) == SWAP16(interesting_16[i])) return;//SWAP函数是改变字节顺序,比如从大端序改为小端序,定义在type.h中}if (len == 4) {i = sizeof(interesting_32) >> 2;while (i--) if (*((u32*)mem) == interesting_32[i] ||*((u32*)mem) == SWAP32(interesting_32[i])) return;}/* Reject anything that matches existing extras. Do a case-insensitivematch. We optimize by exploiting the fact that extras[] are sortedby size. *///extras数组按照len由小到大排序了,找到与mem长度相同的项进行比较看看是否已经有了for (i = 0; i < extras_cnt; i++)if (extras[i].len >= len) break;for (; i < extras_cnt && extras[i].len == len; i++)if (!memcmp_nocase(extras[i].data, mem, len)) return;/* Last but not least, check a_extras[] for matches. There are noguarantees of a particular sort order. */auto_changed = 1;//检查a_extras数组里有没有mem,有的话就把命中数++,然后对a_extras数组排序//首先按照命中计数进行降序排序,然后在前面的排序结果中,按照长度进行排序for (i = 0; i < a_extras_cnt; i++) {if (a_extras[i].len == len && !memcmp_nocase(a_extras[i].data, mem, len)) {a_extras[i].hit_cnt++;goto sort_a_extras;}}/* At this point, looks like we're dealing with a new entry. So, let'sappend it if we have room. Otherwise, let's randomly evict some otherentry from the bottom half of the list. */// a_extras没满,申请空间添加进去if (a_extras_cnt < MAX_AUTO_EXTRAS) {a_extras = ck_realloc_block(a_extras, (a_extras_cnt + 1) *sizeof(struct extra_data));a_extras[a_extras_cnt].data = ck_memdup(mem, len);a_extras[a_extras_cnt].len  = len;a_extras_cnt++;} else {// a_extras满了,在后面1/2的数组中随机选一个替换掉,hit_cnt 设置为0i = MAX_AUTO_EXTRAS / 2 +UR((MAX_AUTO_EXTRAS + 1) / 2);ck_free(a_extras[i].data);a_extras[i].data    = ck_memdup(mem, len);a_extras[i].len     = len;a_extras[i].hit_cnt = 0;}sort_a_extras://首先按照命中计数进行降序排序,然后在前面的排序结果中,按照长度进行排序/* First, sort all auto extras by use count, descending order. */qsort(a_extras, a_extras_cnt, sizeof(struct extra_data),compare_extras_use_d);/* Then, sort the top USE_AUTO_EXTRAS entries by size. */qsort(a_extras, MIN(USE_AUTO_EXTRAS, a_extras_cnt),sizeof(struct extra_data), compare_extras_len);}

pivot_inputs()

对所有的输入文件创建硬链接。

首先进入while大循环遍历queue,对前缀进行一些匹配,比如经过处理后的文件名中就有id,如果文件名的文法满足我们定义的就直接使用,否则弄一些出来,最后就像下面这样:

在这里插入图片描述

然后调用link_or_copy()函数,尝试创建硬链接,如果失败则进行文件拷贝。它打开源文件和目标文件,逐块读取源文件的数据,并写入目标文件,直到源文件读取完毕:

static void link_or_copy(u8* old_path, u8* new_path) {s32 i = link(old_path, new_path);s32 sfd, dfd;u8* tmp;if (!i) return;sfd = open(old_path, O_RDONLY);if (sfd < 0) PFATAL("Unable to open '%s'", old_path);dfd = open(new_path, O_WRONLY | O_CREAT | O_EXCL, 0600);if (dfd < 0) PFATAL("Unable to create '%s'", new_path);tmp = ck_alloc(64 * 1024);while ((i = read(sfd, tmp, 64 * 1024)) > 0) ck_write(dfd, tmp, i, new_path);if (i < 0) PFATAL("read() failed");ck_free(tmp);close(sfd);close(dfd);}

如果原先设置了passed_det为1,则mark_as_det_done(q),这主要是对应上面的resuming_fuzz的情况。mark_as_det_done()简单的说就是打开out_dir/queue/.state/deterministic_done/use_name这个文件,如果不存在就创建这个文件,然后设置 q 的passed_det为 1 。

最后如果设置了in_place_resume为 1 ,则nuke_resume_dir(),这个函数会删除out_dir/_resume及其子目录下所有id:前缀的文件。

load_extras()

如果存在extra_dir,就打开该目录,将其中大小符合要求的文件内容添加到extras[]数组中,具体的实现还写了挺多的。

find_timeout()

如果没有设置timeout_given,就进入这个函数。

如果resume_fuzzing为 0 ,直接返回,之后去fuzzer_stats里搜索”exec_timeout”,读取其后面的数字,如果大于 4 ,就设置为exec_tmout的值,并把timeout_given的值设置为 3 。

这个想法是,在不指定 -t 的情况下 resuming sessions 时,我们不希望一遍又一遍地自动调整超时时间,以防止超时值因随机波动而增长。

detect_file_args()

检测参数中是否有@@,如果有,就替换为out_dir/.cur_input,并把out_file也设置为这个;如果没有就返回。

setup_stdio_file()

如果 out_file 为 NULL ,如果没有使用 -f ,就删除原本的out_dir/.cur_input,创建一个新的out_dir/.cur_input,保存其文件描述符在out_fd 中。

check_binary()

检查需要被 fuzz 的二进制文件:

  • 如果没有执行权限,报错;
  • 如果设置了 AFL_SKIP_BIN_CHECK ,返回;
  • 如果放在 /tmp 文件中,报错;
  • 如果文件以 #! 开头,判断为 shell script ,报错;
  • 在 Linux 和 APPLE 平台下检测是否为可执行文件;
  • 检测是否插桩;
  • 如果以 afl-gcc 插桩但是是 qemu 模式,报错;
  • 根据插桩标志分别设置 use_asanpersist_modedeferred_mode

get_cur_time()

获取当前时间。

get_qemu_argv()

使用 QEMU 进行模拟执行时,重写命令行参数,以便正确指定 QEMU 二进制文件的路径。

处理输入目录

perform_dry_run()

简单执行所有测试用例,确保其按预期工作,这段程序只执行一次。

循环遍历每个testcase(q = queue),打开相应文件,读取到use_mem中,执行calibrate_case()校准(该函数见下文分析),返回值保存在res中:

  • 设置了stop_soon,直接返回,如果rescrash_mode或者FAULT_NOBITS,打印相关信息;
  • 根据res的结果检查是哪几种错误:
    • FAULT_NONE:

      • 如果q是头节点,执行check_map_coverage()
      static void check_map_coverage(void) {u32 i;//计算 trace_bits 发现的路径数,小于 100 就返回if (count_bytes(trace_bits) < 100) return;// MAP_SIZE_POW2 初始设置为 16,MAP_SIZE = 1 << MAP_SIZE_POW2//遍历 trace_bits 后 1/2 ,有值就返回for (i = (1 << (MAP_SIZE_POW2 - 1)); i < MAP_SIZE; i++)if (trace_bits[i]) return;WARNF("Recompile binary with newer version of afl to improve coverage!");}
      
      • 如果是crash_mode,抛出异常,该文件不崩溃。
    • FAULT_TMOUT:

      • 命令行中的 -t 参数设定了 timeout_given ,如果大于 1 就跳过该测试用例,并设置 q->cal_failed = CAL_CHANCEScal_failures++;
      /* Number of chances to calibrate a case before giving up: */#define CAL_CHANCES         3
      
    • FAULT_CRASH:

      • crash_mode不用管;
      • 设置了skip_crashes 不用管,同样设置q->cal_failedcal_failures++
      • 根据 mem_limit 设置与否打印不同的反馈,测试用例可能引起崩溃,内存限制可能引起崩溃,macOS上fork()语义不同会是的forkserver崩溃,等等,最后 FATAL 一下;
    • FAULT_ERROR:

      • 抛出异常,无法执行;
    • FAULT_NOINST:

      • 报出异常,未监测到插桩;
    • FAULT_NOBITS:

      • 有路径信息但没有新路径,认为无用,useless_at_start++
  • 如果 qvar_behavior 为真,则代表它多次运行,同样的输入条件下,却出现不同的覆盖信息;

最后根据校准失败的次数输出相应的提示信息,首次执行完毕。下面是校准函数:

calibrate_case()

该函数在诸多函数中都有调用,结合源码分析:

/* Calibrate a new test case. This is done when processing the input directoryto warn about flaky or otherwise problematic test cases early on; and whennew 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]; // 保存 testcase 运行前的 bitmapu8  fault = 0, new_bits = 0, var_detected = 0, hnb = 0,first_run = (q->exec_cksum == 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; // 保存开始的阶段信息/* Be a bit more generous about timeouts when resuming sessions, or whentrying to calibrate already-added finds. This helps avoid trouble dueto intermittent latency. */if (!from_queue || resuming_fuzz) //是否是来自queue里的,是否是resuming sessionuse_tmout = MAX(exec_tmout + CAL_TMOUT_ADD,exec_tmout * CAL_TMOUT_PERC / 100); //延长q->cal_failed++; //直接加stage_name = "calibration";stage_max  = fast_cal ? 3 : CAL_CYCLES; //快速模式执行3个周期,否则8个周期(默认)/* Make sure the forkserver is up before we do anything, and let's notcount its spin-up time toward binary calibration. *///不处于dumb模式,没有禁用forkserver,还没有初始化,就初始化if (dumb_mode != 1 && !no_forkserver && !forksrv_pid)init_forkserver(argv);//该用例不是来自input,而是评估新case,看看有没有发现new_bits//virgin_bits在初始化共享内存时设置为 0xffif (q->exec_cksum) {memcpy(first_trace, trace_bits, MAP_SIZE);hnb = has_new_bits(virgin_bits); //相同路径命中数增加返回 1 ,发现新路径返回 2if (hnb > new_bits) new_bits = hnb;}start_us = get_cur_time_us(); //获取当前时间//执行stage_max轮校准for (stage_cur = 0; stage_cur < stage_max; stage_cur++) {u32 cksum;//不是第一次跑,而且达到刷新频率(一开始模数为1),就刷新状态展板if (!first_run && !(stage_cur % stats_update_freq)) show_stats();//将数据写入.cur_input中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;//如果不是dumb模式,且是第一次校准,而且bitmap没有任何记录,判断为未插桩if (!dumb_mode && !stage_cur && !count_bytes(trace_bits)) {fault = FAULT_NOINST;goto abort_calibration;}//获取当前的覆盖情况的哈希值,32位大小cksum = hash32(trace_bits, MAP_SIZE, HASH_CONST);// 要么是第一次运行;要么是在相同参数下每次执行cksum不同,是一个路径可变的testcaseif (q->exec_cksum != cksum) {hnb = has_new_bits(virgin_bits);if (hnb > new_bits) new_bits = hnb; //更新new_bitsif (q->exec_cksum) { //第一次运行的话这个为0,现在代表这是一条可变路径u32 i;for (i = 0; i < MAP_SIZE; i++) {if (!var_bytes[i] && first_trace[i] != trace_bits[i]) {// 记录可变路径var_bytes[i] = 1;stage_max    = CAL_CYCLES_LONG; //40次}}//检测到可变路径的标志置 1var_detected = 1;} else {//更新testcase的状态,更新first_traceq->exec_cksum = 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(). *///更新queue中testcase的状态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;total_bitmap_entries++;update_bitmap_score(q); //评估新路径/* If this case didn't result in new output from the instrumentation, tellparent. This is a non-critical problem, but something to warn the userabout. *///这个样例没有发现任何的新路径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; //有新的coveragequeued_with_cov++;}/* Mark variable paths. *///发现路径可变的testcaseif (var_detected) {var_byte_count = count_bytes(var_bytes);//还没标记为可变if (!q->var_behavior) {mark_as_variable(q);//设置var_behavior的值,并创建到一个文件的符号链接queued_variable++;}}stage_name = old_sn;stage_cur  = old_sc;stage_max  = old_sm;if (!first_run) show_stats();return fault;}

当遇到新路径时,程序会调用update_bitmap_score()评估其是否比已经存在的路径更优,这里更优的标准是它能否用更少的路径触发 bitmap 中所有到达的边,这样可以专注于这些优秀的路径,提升模糊测试的效率,下面分析一下这个函数:

static void update_bitmap_score(struct queue_entry* q) {u32 i;u64 fav_factor = q->exec_us * q->len;//以 执行时间*用例长度 来作为评判标准/* For every byte set in trace_bits[], see if there is a previous winner,and how it compares to us. *///对trace_bits中到达的边,看是否已经存在一个目前的最优解,与之较量一番for (i = 0; i < MAP_SIZE; i++)if (trace_bits[i]) {if (top_rated[i]) {/* Faster-executing or smaller test cases are favored. *///这个factor不如别人,跳过if (fav_factor > top_rated[i]->exec_us * top_rated[i]->len) continue;/* Looks like we're going to win. Decrease ref count for theprevious winner, discard its trace_bits[] if necessary. *///top_rated是一个queue_entry类型的指针数组/*u8* trace_mini;                     // Trace bytes, if kept u32 tc_ref;                         // Trace bytes ref count*/if (!--top_rated[i]->tc_ref) {ck_free(top_rated[i]->trace_mini);top_rated[i]->trace_mini = 0;}}/* Insert ourselves as the new winner. *///更新top_rated保存的用例top_rated[i] = q;q->tc_ref++;if (!q->trace_mini) {q->trace_mini = ck_alloc(MAP_SIZE >> 3);minimize_bits(q->trace_mini, trace_bits); //精简trace_bits并进行保存,下文分析}score_changed = 1;}}

minimize_bits函数压缩位图,去掉了其计数功能,仅用 0 和 1 表示一条路径存在与否:

static void minimize_bits(u8* dst, u8* src) {u32 i = 0;while (i < MAP_SIZE) {if (*(src++)) dst[i >> 3] |= 1 << (i & 7);i++;}}

init_forkserver()

  • 该函数首先打开两个管道st_pipe[2]ctl_pipe[2],并fork出一个子进程作为 forkserver ,我们看看在 forkserver 中需要做什么:

    1. 进程资源限制,使用setrlimit函数配置子进程的资源限制:
    struct rlimit {  // resource limitrlim_t rlim_cur; //current limitrlim_t rlim_max; //max limit value for "rlim_cur"
    };int getrlimit(int resource, struct rlimit *rlptr);       //获取
    int setrlimit(int resource, const struct rlimit *rlptr); //重新设置/*
    RLIMIT_AS/RLIMIT_VMEM: 这两个资源表示的是同一个含义,都是只address space限制,可用内存用户地址空间最大长度,会影响到sbrk和mmap函数。
    RLIMIT_STACK:栈的长度,默认一般是8K
    RLIMIT_CORE:程序crash后生成的core dump文件的大小,如果为0将不生成对应的core文件。
    RLIMIT_NOFILE:进程能够打开的最多文件数目,此限制会影响到sysconf的_SC_OPEN_MAX的返回值。
    RLIMIT_NPROC:每个用户ID能够拥有的最大子进程数目,此限制会影响到sysconf的_SC_CHILD_MAX的返回值。
    RLIMIT_NICE:对应进程的优先级nice值。
    RLIMIT_SWAP:进程能够消耗的最大swap空间。
    RLIMIT_CPU:CPU时间的最大值(秒单位),超过此限制后会发送SIGXCPU信号给进程。
    RLIMIT_DATA:数据段的最大长度。默认为unlimited
    RLIMIT_FSIZE:创建文件的最大字节长度。默认为ulimited
    RLIMIT_MSGQUEUE:为posix消息队列可分配的最大存储字节数
    RLIMIT_SIGPENDING:可排队的信号最大数量
    RLIMIT_NPTS:可同时打开的伪终端数目
    RLIMIT_RSS:最大可驻内存字节长度
    RLIMIT_SBSIZE:单个用户所有套接字缓冲区的最大长度
    RLIMIT_MEMLOCK:一个进程使用mlock能够锁定存储空间中的最大字节长度
    */
    
    • 设置文件描述符限制,不小于 FORKSRV_FD+2;
    • 设置内存限制 mem_limit ;
    • 禁用 core dump 。
    1. 建立新会话:setsid()调用成功后,返回新的会话的 ID ,调用 setsid 函数的进程成为新的会话的领头进程,并与其父进程的会话组和进程组脱离。由于会话对控制终端的独占性,进程同时与控制终端脱离。
    2. 操纵文件描述符,dup/dup2 函数的使用如下:
    #include <unsitd.h>int dup(int oldfd); //复制文件描述符,两个描述符指向同一个打开文件,返回未使用的最小 fd 值
    int dup2(int oldfd,int newfd); //使 newfd 也指向 oldfd 指向的打开文件,返回 newfd 
    /*经下面调用:n_fd = dup2(fd3, STDOUT_FILENO);后进程状态如下:进程的文件描述符表(after dup2)------------fd0  0   | p0------------n_fd 1   | p1 ------------------------             \fd2  2   | p2                \------------              _\|fd3  3   | p3 -------------> 文件表2 ---------> vnode2
    */
    

    forkserver 重定向了自己的标准输出和标准错误输出到 /dev/null ,如果 out_file 存在,重定向标准输入到/dev/null,否则,重定向标准输入到out_fd;然后配置管道,保留ctl_pipe的读出端和st_pipe的写入端,并关闭其他所有不需要的文件描述符。

    1. 设置延迟绑定,设置ASAN_OPTIONSMSAN_OPTION的环境变量。
    2. 替换当前子进程执行目标二进制文件,这个函数除非出错否则不会返回:

    这里非常特殊。第一个目标程序会进入__afl_maybe_log里的__afl_fork_wait_loop,并充当 forkserver 。在整个过程中,它都不会结束,每次要 fuzz 一次目标程序,都会从这个 forkserver 再 fork 出来一个子进程去 fuzz ,具体解释在 afl-as.c/afl-as.h 源码分析中。

    因此可以看作是三段式:fuzzer -> fork server -> target子进程,如同下图:

    在这里插入图片描述

    1. 将一个特殊值 EXEC_FAIL_SIG(0xfeeldead) 复制给 trace_bits 告诉父进程执行失败,结束子进程。
  • 在父进程中:

    1. 关闭 ctl_pipe 的写入端和 st_pipe 的读出端,保存这两个管道的另两端,父进程只能发送写出命令,只能读取状态。
    2. 在一定时间范围内等待 fork server 启动。
    3. 启动成功输出 “All right - fork server is up.”,函数返回;否则根据错误情况输出相应提示:
      1. 如果 child_timed_out 为真,表示 fork server 启动超时,输出超时错误提示。
      2. 如果 fork server 进程发生了异常终止(收到了一个信号),输出相应的错误提示。
      3. 如果握手消息的第一个字节是 EXEC_FAIL_SIG,表示执行目标程序失败,输出相应的错误提示。
      4. 如果目标程序在握手过程中异常终止(可能是由于内存限制等原因),输出相应的错误提示。

    在输出错误提示时,会根据内存限制的情况(mem_limit)以及是否使用了 AddressSanitizer(uses_asan)来选择合适的提示内容。对于内存限制不足导致的问题,会给出调整内存限制的建议。对于使用了 deferred fork server 的情况,还会输出相应的提示信息。

run_target()

这函数的作用是执行目标程序,标记相应的trace_bits:

  • 清空trace_bits数组,建立内存屏障:

    #define MEM_BARRIER() \__asm__ volatile("" ::: "memory")
    /*1. __asm__ 用于指示编译器在此插入汇编语句。2. volatile 用于告诉编译器,严禁将此处的汇编语句与其它的语句重组合优化。3. memory 强制gcc编译器假设RAM所有内存单元均被汇编指令修改,这样cpu中的registers和cache中已缓存的内存单元中的数据将作废。cpu将不得不在需要的时候重新读取内存中的数据。这就阻止了cpu又将registers,cache中的数据用于去优化指令,而避免去访问内存。4. "":::表示这是个空指令。barrier()不用在此插入一条串行化汇编指令。
    */MEM_BARRIER(); //告诉编译器不要越过该屏障优化存储器的访问顺序
    
  • 在 dumb 模式下或者禁用了 forkserver 时,fork 一个子进程,和 init_forkserver() 中的子进程代码非常类似,但稍微简化,没有设置文件描述符上限,没有用管道进行通信,没有设置延迟绑定,MASN_OPTION的参数也更少,然后 execv() 了目标程序;

  • 否则,向控制管道写入 prev_timed_out(static变量),命令 forkserver 行动;然后从状态管道读取 forkserver 产生的子进程的 pid 到 child_pid 中;

  • 设置 timeout,等待子进程结束,total_execs 计数器 ++:

    pid_t waitpid(pid_t pid, int *wstatus, int options);
    /*进程会进入阻塞状态,等待进程 ID 为 pid 的子进程退出,保存其状态信息到 wstatus 中。
    */
    

    可以用下方的宏来判断子进程的退出状态:

    宏定义描述
    WIFEXITED(status)如果子进程正常结束,返回非0值
    WEXITSTATUS(status)如果WIFEXITED非0,也就是正常退出,返回子进程退出码
    WIFSIGNALED(status)子进程如果是因为捕获信号而终止,返回非0值
    WTERMSIG(status)如果WIFSIGNALED非0,返回信号代码
    WIFSTOPPED(status)如果子进程被暂停,返回非0值
    WSTOPSIG(status)如果WIFSTOPPED非0,返回信号代码
  • 再次建立内存屏障,保存trace_bits值,执行classify_counts(),对分支执行次数进行一个简单归类,更新trace_bits值,具体的映射规则如下,同时把prev_timed_out赋值为 child_timed_out

    static const u8 count_class_lookup8[256] = {[0]           = 0,[1]           = 1,[2]           = 2,[3]           = 4,[4 ... 7]     = 8,[8 ... 15]    = 16,[16 ... 31]   = 32,[32 ... 127]  = 64,[128 ... 255] = 128};
    
  • 最后检查子进程的终止状态,返回相应的错误类型:

    1. 如果子进程是被信号终止的,并且没有要求立即停止(!stop_soon),则根据终止信号的不同返回不同的故障类型。如果子进程因为超时被 SIGKILL 信号终止,则返回 FAULT_TMOUT。否则,返回 FAULT_CRASH,表示子进程因为其他信号终止(通常表示发生了崩溃)。
    2. 如果使用了 AddressSanitizer (ASAN) 并且子进程的退出状态是 MSAN_ERROR(这是一个特殊的退出码用于 ASAN 检测到内存错误),则返回 FAULT_CRASH,表示子进程发生了内存错误。
    3. 如果是在 “dumb_mode” 或者没有启用 fork server 模式下运行,并且 tb4 的值等于 EXEC_FAIL_SIG(表示执行出错),则返回 FAULT_ERROR
    4. 最后,如果以上条件都不满足,并且超时时间没有大于 exec_tmout,则将 exec_ms(当前执行的时间)与 slowest_exec_ms(最慢执行的时间)进行比较,如果当前执行时间更长,则将 slowest_exec_ms 更新为 exec_ms

    函数最终返回 FAULT_NONE,表示执行没有发生故障,执行正常。

cull_queue()

这是上面在 update_bitmap_score() 函数中引入的 top_rated 机制的补充,它遍历 top_rated[] 条目,直到填充所有路径,并将它们标记为优选项。优选项在所有模糊测试步骤中会获得更多的执行时间。

  • 首先定义了一个 temp_v[MAPSIZE >> 3] 数组,从它的大小可以发现这显然是一个 minimize 后的 bitmap;

  • 如果是 dumb 模式或者 top_rated 没有变,就直接返回;

  • 设置 score_changed = 0,把 temp_v 每一位置 1 ,代表一开始所有边都未到达;

  • 标记 queue 中所有的 q->favored 为0;

  • 从 0 到 MAP_SIZE 顺序遍历位图,如果 top_rated[i] 不为空,且 temp_v[i >> 3] 的第 (i & 7) 位还是 1,就执行后续操作,这里有一个难懂的位运算:

    //这个位运算的目的是检查在 temp_v 的字节中,对应于 top_rated[i] 条目的特定位是否为 1。
    if (top_rated[i] && (temp_v[i >> 3] & (1 << (i & 7)))) {/*由于 temp_v 是简化后的位图,所以其一个字节 temp_v[] 对应了 MAP_SIZE 里的八个字节,比如 i 从 0 遍历到 7 时对应的位都在 temp_v[0]里;后面的(1 << (i & 7)) 能对应到具体的位上,比如 i = 4 时, 后面就是 1 << 4 。 */...
    }
    
  • 通过上方的检测后,用一个循环把 top_rated[i] 里保存的测试用例所能到达的所有边在 temp_v 里置 0,表达该边已到达,并把这个用例的 favored 属性置 1,增加 queue_favored计数器,如果这个用例还没有被 fuzz ,增加 pending_favored 计数器;

  • 最后对 queue 中每一个 q 执行 mark_as_redundant(),也就是说,如果不是 favored 的case,就被标记成 redundant_edges,位置在**/**queue**/**.state**/**redundent_edges中。

show_init_stats()

在处理输入目录最后,展示一些统计信息和警告信息,还有几个硬编码的常量。

  • 首先遍历所有队列条目,计算出最小和最大的执行时间(exec_us)、最小和最大的位图大小(bitmap_size)以及最长的测试用例长度(len),

  • 然后根据计算得来的校准时的单轮执行时间设置 havoc_div

    if (avg_us > 50000) havoc_div = 10;     /* 0-19 execs/sec   */
    else if (avg_us > 20000) havoc_div = 5; /* 20-49 execs/sec  */
    else if (avg_us > 10000) havoc_div = 2; /* 50-100 execs/sec */
    
  • 如果不是 resuming session,根据最长的测试用例长度,显示一些关于测试用例大小的警告信息,并检查队列中的输入文件数量。如果队列中的输入文件数量超过阈值,会显示一个有关文件数量过多的警告;

  • 最后,显示一些统计信息,包括测试用例数量、位图范围、执行时间范围等等。还根据情况设置了执行超时时间(exec_tmout),如果未指定超时选项,则根据平均执行时间和最长执行时间计算默认超时时间。

find_start_position()

仅对 resume 操作以及当我们可以找到原始的 fuzzer_stats 时才有意义:找到从 queue 中的哪一个 case 开始。

  • 如果不是 resuming_fuzz ,直接返回;
  • 打开 fuzzer_stats 文件,strstr() 搜索 “cur_path” 标记,设置为 ret 的值,如果 retqueued_paths 大就设置为0 ,最后返回ret

返回值被保存在 seek_to 变量中。

write_stats_file()

更新状态文件以进行无人值守的监视。

  • 创建文件 out_dir/fuzzer_stats,写入信息:
    • start time,fuzz运行的开始时间;

    • last_update,当前时间;

    • fuzzer pid,当前进程的pid;

    • cycles done,即 queue_cycle ? (queue_cycle - 1) : 0,sakura 解释道:

      queue_cyclequeue_cur为空,即执行到当前队列尾的时候才增加1,所以这代表queue队列被完全变异一次的次数。

    • execs_done,target 执行次数;

    • execs_per_sec,每秒执行次数;

    • paths_total,queue里样例总数;

    • path_favored,上面标记为 queue_favored 的样例总数;

    • path_found,sakura 解释道:

      queued_discovered在每次common_fuzz_stuff去执行一次fuzz时,发现新的interesting case的时候会增加1,代表在fuzz运行期间发现的新queue entry。

    • paths_imported,sakura解释道:

      queued_imported是master-slave模式下,如果sync过来的case是interesting的,就增加1

    • max_depth,最大路径深度

    • cur_path,current_entry,一般情况下代表的是正在执行的queue entry的整数ID, queue首节点的ID是0

    • pending_favs,pending_favored 等待fuzz的favored paths数

    • pending_total,pending_not_fuzzed 在queue中等待fuzz的case数

    • variable_paths,queued_variable在calibrate_case去评估一个新的test case的时候,如果发现这个case的路径是可变的,则将这个计数器加一,代表发现了一个可变case

    • stability

    • bitmap_cvg

    • unique_crashes,sakura 解释道:

      unique_crashes这是在save_if_interesting时,如果fault是FAULT_CRASH,就将unique_crashes计数器加一

    • unique_hangs,sakura 解释道:

      unique_hangs这是在save_if_interesting时,如果fault是FAULT_TMOUT,且exec_tmout小于hang_tmout,就以hang_tmout为超时时间再执行一次,如果还超时,就让hang计数器加一。

    • last_path,在add_to_queue里将一个新case加入queue时,就设置一次last_path_time为当前时间,last_path_time / 1000

    • last_crash,同上,在unique_crashes加一的时候,last_crash也更新时间,last_crash_time / 1000

    • last_hang,同上,在unique_hangs加一的时候,last_hang也更新时间,last_hang_time / 1000

    • execs_since_crash,total_execs - last_crash_execs,这里last_crash_execs是在上一次crash的时候的总计执行了多少次。

    • exec_tmout,配置好的超时时间。

  • 在杀死 forkserver 以及被调用的waitpid函数后可调用 getrusage() 获取进程的资源信息。

save_auto()

生成 out_dir/queue/.state/auto_extras/auto_i文件保存自动生成的 extra 样例。

调整节奏,开始fuzz

Fuzz 执行

while主循环及收尾

  • 首先调用 cull_queue() ,标记 favored ;

  • 如果 queue_cur 为 0 ,代表 queue 中所有的 case 被执行完一轮,或者刚开始执行:

    • queue_cycle++,代表整个 queue 执行的轮次增加了;

    • current_entrycur_skipped_paths 置 0 ,从头开始,执行完的 entry 数从 0 开始,cur_queue 也设置为队列头;

    • 根据之前的 seek_to 恢复 resuming session 的起始状态;

    • show_stats() 展示一波当前的数据;

    • 在一轮执行后,queued_paths == prev_queued 代表没有新的 case 被发现,尝试改变策略:

      • 如果已经启用了 use_splicing ,就把 cycles_wo_finds 计数器 ++;
      • 否则启用 use_splicing 策略重组 queue 中的 case;

      否则代表有新发现,把 cycles_wo_finds 计数器置 0 ;

    • prev_queued 设置为 queue_paths 即上一轮的路径数,开始新的一轮;

    • 如果设置了 sycn_idqueue_cyclye == 1(第一轮)且有环境变量 AFL_IMPORT_FIRST,则执行 sync_fuzzers()

  • 执行 fuzz_one() 进行一次 fuzz ,根据策略,它不一定真的执行 queue_cur ,如果不执行函数会返回 1 赋值给 skipped_fuzz,否则返回 0 ;

  • 如果设置了 sync_id 并且上面的样例执行了,那么对 sync_interval_cnt 计数器 ++,并在达到 SYNC_INTERVAL 周期时执行一次 sync_fuzzers()

  • 根据 stop_soonexit_1 的值即时退出;

  • queue_cur 移动到 next,并把 current_entry 计数器加一;

  • 这部分在 while 循环外,算是一点收尾工作,不做过多解释了:

    if (queue_cur) show_stats();/* If we stopped programmatically, we kill the forkserver and the current runner. If we stopped manually, this is done by the signal handler. */if (stop_soon == 2) {if (child_pid > 0) kill(child_pid, SIGKILL);if (forksrv_pid > 0) kill(forksrv_pid, SIGKILL);}/* Now that we've killed the forkserver, we wait for it to be able to get rusage stats. */if (waitpid(forksrv_pid, NULL, 0) <= 0) {WARNF("error waitpid\n");}write_bitmap();write_stats_file(0, 0, 0);save_auto();stop_fuzzing:SAYF(CURSOR_SHOW cLRD "\n\n+++ Testing aborted %s +++\n" cRST,stop_soon == 2 ? "programmatically" : "by user");/* Running for more than 30 minutes but still doing first cycle? */if (queue_cycle == 1 && get_cur_time() - start_time > 30 * 60 * 1000) {SAYF("\n" cYEL "[!] " cRST"Stopped during the first cycle, results may be incomplete.\n""    (For info on resuming, see %s/README.)\n", doc_path);}fclose(plot_file);destroy_queue();destroy_extras();ck_free(target_path);ck_free(sync_id);alloc_report();OKF("We're done here. Have a nice day!\n");exit(0);}
    

sync_fuzzers()

读取其它 sync fuzzer 中的 queue ,执行一下,如果有价值就添加到当前 sync_id 的 fuzzer 的 queue 文件夹里。

fuzz_one()

囊括变异的 fuzz 。

  • 如果定义了 IGNORE_FINDS ,就在 queue_cur->depth > 1 时返回 1 ,depth 大于 1 代表这不是初代的 testcase 了(input文件里的);

  • 否则,对满足条件的样例以一个大概率 skip 掉这次 fuzz :

    if (pending_favored) {
    // 有 favored 但 cur 不是或者已经被 fuzz 过了,有 99% 的概率返回 1if ((queue_cur->was_fuzzed || !queue_cur->favored) &&UR(100) < SKIP_TO_NEW_PROB) return 1;} else if (!dumb_mode && !queue_cur->favored && queued_paths > 10) {
    // 测试样例总数大于 10if (queue_cycle > 1 && !queue_cur->was_fuzzed) {
    // queue 被 fuzz 轮数大于 1 且 cur 没有被 fuzz 过,有 75% 的概率返回 1if (UR(100) < SKIP_NFAV_NEW_PROB) return 1;} else {
    // cur 被 fuzz 过,有 95% 的概率返回 1if (UR(100) < SKIP_NFAV_OLD_PROB) return 1;}
    }
    
  • 即时刷新,打开 queue->cur 的源文件,将长度赋值给 queue_cur->len,并把文件内容 mmap()到内存中,地址赋值给了 orig_inin_buf

  • 分配同样长度大小的全 0 内存到 out_buf 变量中,设置 subseq_tmouts 值为 0 ,获取当前的 depth 到 cur_path 变量中;

  • 如果这个 case 在校准时 fail 了,但错误次数小于 3 次,就再次校准;


  • trim阶段开始。

  • 如果不在 dumb 模式且这个 case 还没有被 trim 过,则 trim_case() 对测试样例进行修剪,并标记其已被 trim ,即便 trim 失败:

    trim_case()

    这个函数的主要功能是对给定的测试用例进行削减,以减少测试用例的大小,从而提高测试效率。具体来说,它会按照一定的步长对测试用例进行削减,并在每次削减后运行目标程序,以检查是否发现了崩溃或其他异常行为。如果发现异常行为,则说明该削减过程导致了程序行为的变化,因此该削减就被撤销。如果没有发现异常行为,则说明该削减没有影响程序行为,因此该削减可以被保留。

    • 如果这个 case 的长度小于 5 ,直接返回 0;
    • 设置 stage_name 为 tmp(static 类型),bytes_trim_in 加上整个 case 的长度代表已经修剪过的字节长度;
    • next_p2(q->len) 获取大于 len 的 2 的幂赋值给 len_p2,比如 next_p2(770) == 1024;
    • 设置初始的削减步长 remove_len 为 max(len_p2 / 16, 4)
    • 步长逐渐缩短,开启一个 while 循环来进行修剪,直到步长小于 len_p2 / 1024:
      • 设置 remove_res 值为 remove_len

      • 设置 tmp 为字符串 ”trim remove_len/remove_len”;

      • 设置 stage_cur 为 0, stage_maxq->len/remove_len

      • 开始以 remove_res 为步长进行迭代削减:

        • trim_avail 保存这次迭代能 trim 的字节,最多一个步长;
        • write_with_gap(in_buf, q->len, remove_pos, trim_avail):覆写 out_file 文件,也就是保存 fuzz 输入的文件,这个函数的作用是把 in_buf 中间下标 remove_pos开始 trim_avail 长度的字节给舍弃掉,把剩下的字节也就是 0 到 remove_pos - 1 和 remove_pos + trim_avail 到 q->len 的数据写入 out_file

        在这里插入图片描述

        • 用削减后的 case 作为输入执行目标程序,trim_execs计数器加一;
        • 程序崩了就 abort ,否则记录执行后的 checksum ,如果和没有削减时的覆盖率一样,就代表削减对执行没什么影响,保存这种修改:
          • q->len 减掉刚才削减的字节数 trim_avail,获得下一个len_p2,这影响了外部循环的结束条件;
          • 把削减的字节memmove()到这个 case 的最后,由于前面减短了 q->len,就相当于把那些字节删了;
          • 如果 need_write 为 0,设置其为 1,并把 trace_bits 复制到 clean_bits 中;
        • 否则,也就是说削减这个 case 后其覆盖率变了,就把要削减的 pos 前进 remove_len,代表这个削减不可行;
        • stage_cur++,并且这可能有点慢,时不时更新展示数据;
      • remove_len右移一位也就是除以 2 ,注意这在上面的迭代循环外,是控制外层循环结束的变量;

      • 上面更新了 case 的话 need_write 此时就为1,我们需要把内存里的更新映射到磁盘上的 .cur_input 文件中,重新创建一个当前queue_cur对应的输入文件,写入削减后的数据,用 clean_bits 更新 trace_bits 来进行update_bitmap_score(),以更新 top_rated[]

  • 拷贝 in_buflen 个字节到 out_buf 中;


  • 打分阶段开始。

  • calculate_score() 函数给这个测试用例打分来调整 havoc fuzzing 的长度,将分数赋值给orig_perfperf_score

    calculate_score()

    1. 计算全局平均执行时间(avg_exec_us)和全局平均位图大小(avg_bitmap_size)。
    2. 根据执行速度和全局平均执行时间,调整可取性分数。执行速度越快的测试用例分数越高,乘数范围从0.1x到3x。
    3. 根据位图大小和全局平均位图大小,调整可取性分数。位图大小越大的测试用例分数越高,乘数范围从0.25x到3x。
    4. 根据测试用例的深度,调整可取性分数。测试用例的深度越深,分数越高,乘数范围从1x到5x。
    5. 根据队列条目的手动设置的 “handicap” 值,调整可取性分数。手动设置的 “handicap” 值越高,分数越高,乘数范围从2x到4x。
    6. 最后,根据HAVOC_MAX_MULT * 100设置的上限,确保可取性分数不会超过预设的上限。
  • 如果 skip_deterministic 为 1 ,或者当前 case 被 fuzz 过了,或者当前 case 的 passed_det 为 1 ,直接跳到 havoc_stage 阶段;

  • 关于 master_max,chatgpt 分析道:

    如果master_max变量被设置为一个非零值,那么判断当前测试用例的执行路径校验和是否在当前主机实例的模糊测试范围内。如果不在范围内,则跳过确定性模糊测试,直接进入Havoc模式的模糊测试。

  • 设置 doing_det 为 1,没有直接跳到 havoc 阶段,说明会经历下面的确定性变异;


  • 反转比特阶段开始

  • 设置 stage 来到 “bitflip 1/1” 阶段。保存初始命中数为 queue 中用例数与 unique_crashes 的和,保存翻转比特前的 cksum 到 prev_cksum 中,并设置 stage_maxlen << 3,也就是这个 case 的比特数;

  • 一个 for 循环以 stage_cur 变量遍历每个比特,此时 out_bufin_buf 的拷贝:

    • 获得当前比特所在字节编号,保存在 stage_cur_byte中;
    • FLIP_BIT 宏翻转 out_buf 的比特 stage_cur(下面注释中提到的实际上不是从小往大数第 stage_cur个,但为了方便还是这样说),进行一次 common_fuzz_stuff(),将out_buf写入 .cur_input 并执行,如果失败就 abandon_entry,然后把这个比特翻转回来:
    // 用一个 do{...}while 循环,可以将多个语句组合成为一个语法快,避免宏展开的错误。
    #define FLIP_BIT(_ar, _b) do { \u8* _arf = (u8*)(_ar); \u32 _bf = (_b); \_arf[(_bf) >> 3] ^= (128 >> ((_bf) & 7)); \} while (0)
    /*首先用 _bf>>3 找到比特对应字节,(_bf & 7) 相当于模 8,但是异或上 128 的右移而不是1 的左移,说明是在每个字节中从高位到低位翻转的,比如该宏若传入 FLIP_BIT(out_buf,3)则是翻转字节 out_buf[0] 的第 5 位。
    */
    
    • 如果执行没有超时或者被用户主动打断,就用 save_if_interesting() 判断其是否有价值,如果有就把 queue_discovered 加一:

      save_if_interesting()

      检查这个 case 的执行结果是否是 interesting 的,决定是否保存或跳过。如果保存了这个case,则返回 1 ,否则返回 0 。

    • 如果不是 dumb 模式且 stage_cur 跑到了一个字节的最后(least significant),尝试去猜测其是否是关键字 syntax tokens,就比如 sql 语句中的 SELECT 关键字,他一旦被破坏,覆盖的路径都是一样的即报错。

      这里的思想就是利用这一点,如果连续多个字节的最低位被翻转后目标程序的执行路径未改变,那么就把这一段连续的字节标记为一条 token,需要注意的是 prev_cksum的值,它在一开始是未翻转直接 run 出来的 cksum,之后在翻转到每个字节最低位后有机会更新一次:

      • 如果比特翻转了到了 out_buf 末尾仍然检测到覆盖路径和之前相同,把最后一个字节也装入数组 a_collect 中,若长度合适(大于 3 ,小于 32 )就 maybe_add_auto()
      • 而如果没有到整个out_buf的最后一个字节,且 cksum 与上次检测 token 时不同,就置零 a_len,更新 prev_cksum,如果长度合适,就把已经收集到的字节 maybe_add_auto(),代表已经检测到一段完整的 token(不包含当前字节);
      • 独立于上面两条 if 和 else if 判断,当前的 cksumqueue_cur的 exec_cksum 不同的话,就把这个字节放入a_collect[]中,长度计数器加一;

      也就是说,只要和不翻转的校验和不一样就会在一开始被标记为 token ,但要前面的 if else判断条件里会通过维护 prev_cksum 来判断他们是不是错的一样,错的不一样的会把长度a_len置零,也就相当于把标记的 token 作废了;

  • 保存新的 case 数到 new_hit_cnt,相比之前增加的 case 数保存在 stage_finds[STAGE_FLIP1]中,迭代次数保存到stage_cycles[STAGE_FLIP1]中;

  • 设置 stage 来到 “bitflip 2/1” 阶段。重新设置 orig_hit_cntstage_max,这次从 0 单比特枚举到 (len << 3 - 1),每次 flip 两位进行 common_fuzz_stuff(),然后翻转回来,但是这次没有猜测 tokens,同样将结果记录到 stage_finds[STAGE_FLIP2]stage_cycles[STAGE_FLIP2]

  • 设置 stage 来到 “bitflip 4/1” 阶段。单比特枚举,每次翻转 4 个比特,原理同上;

  • 为下面的全字节翻转设置 effector map:

    	/* Effector map setup. These macros calculate:EFF_APOS      - position of a particular file offset in the map.EFF_ALEN      - length of a map with a particular number of bytes.EFF_SPAN_ALEN - map span for a sequence of bytes.*/// EFF_MAP_SCALE2 被设置为 3 ,EFF_APOS 左移三位进行定位
    #define EFF_APOS(_p)          ((_p) >> EFF_MAP_SCALE2)// EFF_REM 求 _x mod 8
    #define EFF_REM(_x)           ((_x) & ((1 << EFF_MAP_SCALE2) - 1))// 取非两次不是 0 就是 1 ,只有在 _l刚好是 8 的倍数时 EFF_ALEN(_l) == EFF_APOS(_l)
    #define EFF_ALEN(_l)          (EFF_APOS(_l) + !!EFF_REM(_l))// 求_p 到 _l 之间的字节数,包含 _p 和 _l 在内
    #define EFF_SPAN_ALEN(_p, _l) (EFF_APOS((_p) + (_l) - 1) - EFF_APOS(_p) + 1)/* Initialize effector map for the next step (see comments below). Alwaysflag first and last byte as doing something. */// 初始化,并直接标记首尾为有效数据eff_map    = ck_alloc(EFF_ALEN(len));eff_map[0] = 1;if (EFF_APOS(len - 1) != 0) {eff_map[EFF_APOS(len - 1)] = 1;eff_cnt++;}
    /*可以看到 eff_map 也是一个压缩后的图,他把 out_buf 的 8 个字节对应到 1 个字节里,并且没有在一开始 malloc 的时候分配全 0 内存。
    */
    
  • 设置 stage 来到 “bitflip 8/8” 阶段。单字节枚举,每次翻转一个字节,进行common_fuzz_stuff()后,进行一个标记,把那些反转后对覆盖率没有影响的字节标记出来,在更金贵的 deterministic stage 中就能把它跳过了,如果没有标记这个字节,就进入这个判断分支:

    • 如果不在 dumb 模式或者 case 长度大于 128 就算一个新的 cksum ,否则直接对原来文件的exec_cksum 取反;
    • 如果 cksumqueue_cur->exec_cksum不同,在eff_map里标记这个字节,计数器++;

    可见在 dumb 模式和 len 比较短的时候相当于就直接标记为有效数据了;

  • 如果标记到 eff_map 的这个比例大于 90%了,那就直接标记这一整个 case 为有效数据,然后 blocks_eff_select 计数器加上所有被标记的字节数,blocks_eff_total 计数器加上所有检测了的字节数,也就是 EFF_ALEN(len)

  • 维护 new_hit_cntstage_finds[STAGE_FLIP8]stage_cycles[STAGE_FLIP8] 值;

  • 设置 stage 来到 “bitflip 16/8” 阶段,2 字节反转,如果 len 本身就小于 2 ,goto 到 skip_bitflip 代码块。设置 stage_max 为 len - 1 ,从 0 到 len - 1 单字节枚举,如果这两个字节对应的 eff_map 都是 0 ,stage_max 减一并直接continue,这说明 stage_max 统计的是实际的反转执行次数,其他操作都同上;

  • 设置 stage 来到 “bitflip 32/8” 阶段,4 字节反转,如果 len 小于 4 ,goto 到 skip_bitflip 代码块。其他操作同上;


  • ARITHMETIC INC/DEC 算术加减阶段开始,skip_bitflip 开始
  • 设置 stage 来到 “arith 8/8” 阶段。stage_max 设置为 2 * len * 35,单字节枚举,同样先访问 eff_map,然后对每个字节从 1 到 35 增减数值进行 common_fuzz_stuff(),同时对修改后的数值用 could_be_bitflip() 检测变异后的样例是否已经在 bitflip 阶段被测试过了,从而避免了重复测试带来的时间开销,同样保存相应的信息到 new_hit_cntstage_findsstage_cycles 中;
  • 设置 stage 来到 “arith 16/8” 阶段,如果 len 小于 2 ,goto 到 skip_arith 代码块。stage_max = 4 * (len - 1) * ARITH_MAX,单字节枚举,但是是对两个字节整体加减,意思就是说用 (u16*)强制类型转换了 out_buf 再进行加减变异,并且根据大端序和小端序分别实现了加减变异也就是 4 种变异,其它操作同上;
  • 设置 stage 来到 “arith 32/8” 阶段,如果 len 小于 4 ,goto 到 skip_arith 代码块。原理和操作都同上;

  • INTERESTING VALUES 阶段开始,skip_arith 开始。
  • 设置 stage 来到 “interest 8/8” 阶段。相比于上面多了 could_be_arith() 检测,变异的过程就是把out_buf 中的每个字节分别替换为 interesting_8[] 中的那些边界值;
  • 设置 stage 来到 “interest 16/8” 阶段。单字节枚举,2 字节替换;
  • 设置 stage 来到 “interest 32/8” 阶段。4 字节替换;

  • DICTIONARY STUFF 阶段开始,skip_interest 开始。
  • 设置 stage 来到 “user extras (over)” 阶段,如果没有用户设置的 extras 就跳过。从头开始,替换 out_buf 中的字节为 extras[].data ;
  • 设置 stage 来到 ”user extras (insert)” 阶段。不同于上面的替换,这里是开辟了新内存,插入字典中的数据;
  • 设置 stage 来到 ”auto extras (insert)” 阶段,没有就跳过。用于替换的字典来自于 a_extras[]

  • 确定性的 fuzz 结束,开始随机性的 fuzz 。
  • RANDOM HAVOC 阶段开始,havoc_stage 开始。
  • 根据 splice_cycle 的值来判断是否进行文件拼接,如果为 0 ,设置 stage 为 “havoc” ,并且根据是否进行过确定性的变异来为 stage_max赋值;如果 splice_cycle 不为 0 ,设置 stage 为 “splice [splice_cycle]”,并设置相应的 stage_max 值;stage_max 最小为 HAVOC_MIN 即 16;
  • 保存样例原始长度 len 到 temp_len 中,外部大循环进行 stage_max 次 fuzz:
    • 生成随机数 use_stackingHAVOC_STACK_POW2 为 7,所以 use_stacking 是 2 到 128 中 的一个2 的幂,代表这一次 fuzz 中会经历的变异次数;

    • 保存 use_stackingstage_cur_val 中,以其为上界开始内部变异 for 循环,每次循环获取 0 到 14 的随机值进入相应的 switch 分支,每个分支对应一种变异方法,如果用户设置了 extras 字典的话还会再多出 case 15 和 case 16 两种变异可能:

      • case 0:随机 flip 一个比特
      • case 1:随机替换一个字节interesting_8 中随机一个的值;
      • case 2:随机替换一个(2字节)为 interesting_16 中随机一个的值,并且随机选择大小端序;
      • case 3:随机替换一个双字(4字节)为 interesting_32 中随机一个的值,并且随机选择大小端序;
      • case 4:随机对一个字节的数值加上 1 到 35 中的随机值;
      • case 5:随机对一个字节的数值减去 1 到 35 中的随机值;
      • case 6:随机对一个的数值减去 1 到 35 中的随机值,大小端序随机;
      • case 7:随机对一个的数值加上 1 到 35 中的随机值,大小端序随机;
      • case 8:随机对一个双字的数值减去 1 到 35 中的随机值,大小端序随机;
      • case 9:随机对一个双字的数值加上 1 到 35 中的随机值,大小端序随机;
      • case 10:随即将一个字节替换为随机另一字节;
      • case 11 … 12:随机删除随机长度的连续字节,更容易随机出较短长度;
      • case 13:在 out_buf 中随机位置插入一段序列,这段序列有 75% 的可能是 out_buf 本身的随机长度的一段,有 25% 的可能是一段常数值,这个值一半概率是个 0 到 255 的随机值,一半概率是 out_buf 中随机字节的数值,更容易随机出较短长度;
      • case 14:替换 out_buf 随机位置随机长度的字节,75% 的可能是 out_buf 本身的随机长度的一段,有 25% 的可能是一段常数值,这个值一半概率是个 0 到 255 的随机值,一半概率是 out_buf 中随机字节的数值,更容易随机出较短长度;
      • case 15:替换 out_buf 随机位置的一段为 extras[] 或者 a_extras[] 中随机一个;
      • case 16:在 out_buf 中随机位置插入 extras[] 或者 a_extras[] 中随机一个;

      变异结束;

    • 进行一次 common_fuzz_stuff() 后恢复 out_buf

    • 如果探测到了新路径,说明 havoc 很有效,增加 havoc 环节次数;

  • 同样维护 stage_findsstage_cycles 数组;

  • 可能进入 Splicing 拼接阶段,retry_splicing 开始。
  • 在一轮测试后没有新发现时(use_splicing ≠ 0)才会触发这个策略,触发次数不多于 15 次,这一切在没有定义 IGNORE_FINDS 的条件编译下:
    • 恢复 case 最初的样子,因为 in_buf 为了 havoc 做了一些修改;
    • 随机选择另一个与 queue_cur 不同的样例 target
    • locate_diffs() 定位 queue_curtarget 第一个和最后一个不同字节的偏移位置到 f_diffl_diff 中;
    • 选取 f_diffl_diff 中间的一个位置,拼接这个位置前面的 queue_cur 和后面的 target ,把拼好的内存拷贝到 in_bufout_buf 里开始 havoc_stage

  • abandon_entry 收尾
  • fuzz 结束,减掉一些 pending 的计数器,释放相应内存,返回 0 或 1。

Picasso soooooooo great!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/50021.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++高级编程-高级特性

临时总结&#xff1a; <utility> std::move 获得右值引用 奇形怪状的函数 1.传统函数 void f&#xff08;int a&#xff09;{}2.<functional> bind bind( F&& f, Args&&... args ); 参数 f - 可调用 (Callable) 对象&#xff08;函数对象、指向函…

Vue——May(1)

VUE 一、vue基础1.1 引用1.2 基础应用1.3 模板语法1.4 数据处理1.5 el与 .$mount1.6 data的函数式写法1.7 架构模型——MVVM模型1.8 数据代理Object.defineproperty1.9 理解数据代理1.10 事件处理1.10.1 参数1.10.2 this1.10.3 简写1.10.4 传参 1.11 事件修饰符1.12 键盘事件1.…

三次输错密码后,系统是怎么做到不让我继续尝试的?

故事背景&#x1f34a; 忘记密码这件事&#xff0c;相信绝大多数人都遇到过&#xff0c;输一次错一次&#xff0c;错到几次以上&#xff0c;就不允许你继续尝试了。 但当你尝试重置密码&#xff0c;又发现新密码不能和原密码重复&#xff1a; 相信此刻心情只能用一张图形容&a…

chatgpt赋能Python-python_b怎么用

Python在SEO中的应用 介绍 Python是一种高级编程语言&#xff0c;具有清晰简洁的语法和强大的功能。它的应用场景非常广泛&#xff0c;除了在工业、科学、金融等领域中得到广泛应用&#xff0c;还可以在SEO中发挥重要作用。 SEO意为搜索引擎优化&#xff0c;是指通过优化网站…

【AI面试】hard label与soft label,Label Smoothing Loss 和 Smooth L1 Loss

往期文章: AI/CV面试,直达目录汇总【AI面试】NMS 与 Soft NMS 的辨析【AI面试】L1 loss、L2 loss和Smooth L1 Loss,L1正则化和L2正则化在一次询问chatGPT时候,在他的回答中,由smooth L1联想提到了Label Smoothing Loss 。我把问题贴到下面,和chatGPT的回答,供你参考。不…

2023 海外工具站 3 月复盘

3 月的碎碎念&#xff0c;大致总结了商业人生、付费软件、创业方向选择、创业感性还是理性、如何解决复杂问题及如何成长这几个方面的内容。 商业人生 商业人生需要试错能力和快速信息收集与验证校准&#xff1b; 商业逻辑需要试错能力&#xff0c;收集各种渠道信息后整理决…

零入门kubernetes网络实战-33->基于nat+brigde+veth pair形成的跨主机的内网通信方案

《零入门kubernetes网络实战》视频专栏地址 https://www.ixigua.com/7193641905282875942 本篇文章视频地址(稍后上传) 本文主要使用的技术是 nat技术Linux虚拟网桥虚拟网络设备veth pair来实现跨主机网桥的通信 1、测试环境介绍 两台centos虚拟机 # 查看操作系统版本 cat …

ai写作生成器有哪些?试试这几款工具吧

近年来&#xff0c;随着ai技术的飞速发展&#xff0c;越来越多的人开始意识到ai文本生成器的重要性和实用性。这种文本生成器可以帮助我们快速生成各种类型的文章&#xff0c;如报告、评论、新闻、邮件等&#xff0c;它的应用范围已经非常广泛了。不仅如此&#xff0c;随着机器…

公募基金投资者盈利洞察报告

导读&#xff1a; 近一年市场表现不佳&#xff0c;购买成长型风格基金的基民承受一定亏损&#xff0c;但是投资价值型基金的基民逆势取得正收益&#xff0c;均衡型基金基民也有所亏损&#xff0c;但幅度较小。近三年、近五年的时间维度里&#xff0c;成长型及均衡型基金的投资者…

2023年直播电商618创新趋势研究

导读&#xff1a; 竞争激烈的618已经落下帷幕&#xff0c;作为一年中重要的大促节点之一&#xff0c;618的发展变迁在一定程度上反映了电商行业兴起与成长的过程&#xff0c;见证着中国消费市场的日益繁荣。 从直播电商平台的崛起&#xff0c;到双11、618等年度购物节的涌现&am…

chatgpt赋能python:Python画在一张图上的SEO

Python画在一张图上的SEO 在SEO优化中&#xff0c;数据可视化是非常有效的工具。而Python作为数据可视化的利器&#xff0c;在实现数据的可视化方面也是非常得心应手的。在这篇文章中&#xff0c;我们将会介绍Python如何能够绘制多个图形在一张图片上&#xff0c;从而达到我们…

chatgpt赋能python:python绘图的优势与不足

python绘图的优势与不足 Python是一门高效的编程语言&#xff0c;近年来&#xff0c;Python在科学计算和数据可视化方面表现出色&#xff0c;成为绘制图形的首选语言之一。本文将介绍使用Python绘图的优势和不足&#xff0c;并重点介绍几种常用的Python绘图库。 优势 简单易…

chatgpt赋能python:Python散点图的颜色设置

Python散点图的颜色设置 什么是散点图&#xff1f; 散点图是一种数据可视化的图表类型。它用于观察两个变量之间的关系。通常&#xff0c;x轴表示一个变量&#xff0c;y轴表示另一个变量。每个点表示一个数据点&#xff0c;它在x和y轴上分别具有对应的值。我们可以通过比较散…

chatgpt赋能python:Python怎么画图形:从入门到精通

Python怎么画图形&#xff1a;从入门到精通 简介&#xff1a; Python 作为一种高级编程语言&#xff0c;在数据分析与可视化方面被广泛应用。在数据分析过程中&#xff0c;图形可视化是十分重要的一部分&#xff0c;因此也需要掌握可视化的技巧和方法。 基础知识 在 Python…

chatgpt赋能Python-python画笔的尺寸

介绍 Python是一种流行的编程语言&#xff0c;广泛应用于各种应用程序&#xff0c;包括可视化和图形应用。其中一个强大的库是matplotlib&#xff0c;它允许用户创建各种图形&#xff0c;例如线图、散点图和条形图。在matplotlib中&#xff0c;设置画笔尺寸是一个重要的概念&a…

chatgpt赋能python:Python画布调用方法介绍

Python画布调用方法介绍 Python的画布是绘制图形的工具之一&#xff0c;其灵活的调用方法可以让我们以多种方式绘制各种图形。本文将介绍Python画布的调用方法&#xff0c;包括Canvas、Tkinter以及Matplotlib。 1. Canvas Canvas是Python Tkinter中用于绘制图形的功能模块&a…

chatgpt赋能python:Python怎么搞动画

Python怎么搞动画 Python是一种功能强大的编程语言&#xff0c;不仅可以用来开发各种应用程序&#xff0c;还可以用来创建动画。动画是一种生动有趣的交互形式&#xff0c;适合于网站、游戏、教育和娱乐等多个领域。本文将介绍Python如何用于创建动画&#xff0c;包括以下几个…

chatgpt赋能python:如何在Python中画出正中的图形

如何在Python中画出正中的图形 介绍 Python是一种脚本语言&#xff0c;常用于编写各种类型的应用程序&#xff0c;包括绘图应用程序。在Python中&#xff0c;我们可以使用各种库和工具来创建各种类型的图形。本文将介绍如何使用Python中的Matplotlib库来在屏幕的正中央绘制一…

chatgpt赋能python:Python绘图简介

Python绘图简介 Python是一种广泛使用的编程语言&#xff0c;其提供了丰富的绘图工具&#xff0c;允许开发人员生成各种类型的图形&#xff0c;包括线性图、散点图、柱状图、饼图等。Python绘图是数据可视化的重要方式。本篇文章将介绍Python绘图的基础和如何使用它进行绘图。…

chatgpt赋能python:Python画能带图的SEO文章

Python画能带图的SEO文章 Python作为一款流行的编程语言&#xff0c;不仅可以进行数据处理、机器学习、自然语言处理等高端应用领域&#xff0c;也诞生了许多优秀的图形库&#xff0c;能够在文章撰写中轻松绘制各种图表&#xff0c;为SEO优化带来很大的便利。 1. Matplotlib …