🌟 建议打开目录阅读,阅读前应熟悉 AFL 源码的 LLVM 模式。见博客 AFL源码阅读笔记(二)—— llvm_mode 和 pass 源码
📰 论文讲解见知乎小号文章 a-21-CCS-Regression Greybox Fuzzing
以下代码来自 aflchurn/llvm_mode/afl-llvm-pass.so.cc
一、相关工具函数声明⚙️
1.1 startsWith(…)
该函数主要作用是做前缀匹配,函数名表示 “由…开始” 。
判断较长字符串的前缀(长度为较短字符串的长度)是否和较短字符串完全匹配,若匹配则返回 true;反之返回 false。
bool startsWith(std::string big_str, std::string small_str) {
if (big_str.compare(0, small_str.length(), small_str) == 0) return true;
else return false;
}
1.2 inst_norm_age(…)
该函数是对 age 指标做小信号放大操作,然后对放大的小信号做归一化处理。
文章中说明对 age 指标的放大是取倒数 age’ = 1 / age,放大后做归一化处理,保证值在[0,1]间。
![](https://i-blog.csdnimg.cn/blog_migrate/cb1e6588616d42fcd7d4f0bfcf3f0e5e.png)
当最久远的修改天数距今 <= 1 或上次修改距今 <= 0 时,归一化的 age 都是1。极端情况是,自从第一次提交后从未修改,max = last_change,归一化值为 0;或上次修改在一天前,last_change = 1,不管最大值多少,归一化值为 1。
double inst_norm_age(int max_days, int days_since_last_change) {
// norm_days为[0,1]间的浮点数
double norm_days;
/* Normalize 1/days 归一化放大后的age信号 */
if (days_since_last_change <= 0 || max_days <= 1) {
norm_days = 1;
WARNF("Current days are less than 0 or maximum days are less than 1.");
}
else {
norm_days = (double)(max_days - days_since_last_change)/(days_since_last_change * (max_days - 1));
}
return norm_days;
}
1.3 inst_norm_change(…)
该函数是对 churn 指标做小信号放大操作。
代码中的具体做法有取对数、保持不变和取平方,论文中介绍的做法是取对数。
double inst_norm_change(unsigned int num_changes, unsigned short change_select) {
double norm_chg = 0;
// 选取何种放大方式
switch(change_select) {
case CHURN_LOG_CHANGE: // 对数
if (num_changes < 0) norm_chg = 0;
else norm_chg = log2(num_changes + 1);
break;
case CHURN_CHANGE: // 不变
norm_chg = num_changes;
break;
case CHURN_CHANGE2: // 平方
norm_chg = (double)num_changes * num_changes;
break;
default:
FATAL("Wrong CHURN_CHANGE type!");
}
return norm_chg;
}
1.4 inst_norm_rank(…)
该函数计算总的归一化打分。
1.5 execute_git_cmd(…)
函数作用如其名,就是通过该函数执行 git 命令。为了理解函数,介绍一下函数中用到的第三方库,有些并不常见。
(1)引入头文件 sstream,可使用 ostringstream,它支持 << 操作,把数据写进流中。代码中写入的是 cd 目录地址 && 传入的cmd命令
。
(2)popen 是 Linux C 编程的一个重要函数,它的定义为 FILE* popen(const char * command, const char * type);
popen 会调用 fork()
产生子进程,然后从子进程中调用 /bin/sh -c
来执行参数 command 的指令。本段代码中,git_cmd.str().c_str()
是将 ostringstream 类型转为 const char * 以赋给 popen。“r” 参数表示读取 commond 参数,“w” 表示写入 command 参数。
(3)fscanf 读取文件数据,赋给对应的变量。本段代码中读取文件 fp 的字符串类型数据,赋给 ch_git_res。
/* use popen() to execute git command */
std::string execute_git_cmd(std::string directory, std::string str_cmd) {
FILE *fp; // 定义FILE文件指针
int rc = 0;
std::string str_res = "";
char ch_git_res[2048];
std::ostringstream git_cmd;
git_cmd << "cd " << directory << " && " << str_cmd;
fp = popen(git_cmd.str().c_str(), "r");
// popen fails,返回NULL
if (NULL == fp) return str_res;
// popen succeeds,返回文件指针
// 但cmd不一定执行成功,若失败会返回 "fatal: ..."
if (fscanf(fp, "%s", ch_git_res) == 1) {
str_res.assign(ch_git_res); // 清空str_res,将ch_git_res赋值给str_res
}
// 若前缀是 fatal,表示cmd执行失败
if (startsWith(str_res, "fatal")) {
str_res = "";
}
// popen和pclose配套使用
rc = pclose(fp);
if (-1 == rc) {
printf("git command pclose() fails.\n");
}
return str_res;
}
1.6 get_threshold_changes(…)
这个函数用于计算出变更数阈值 threshold,用于判断 “一定插桩” 还是 “随机插桩”。计算方法是,先得到所有文件的最大变更数,再乘上一个阈值百分比(默认为 10%)计算出 threshold。若基本块的变更数大于该阈值,则 “一定插桩”,否则是 “随机插桩”。
(1)对函数中用到的宏做一个说明。WRONG_VALUE
在 config.h 中,定义为 0。THRESHOLD_PERCENT_CHANGES
也在 config.h 中,定义为 10。
(2)对命令行参数做一个说明。
git log
查看提交历史;--name-only
是在默认格式(即 commit、Author 等信息)后显示修改过的文件名;--pretty="format:"
将不显示 commit、Author 和 Date 等信息,只显示所有修改过的文件名。sed '/^\s*$/d'
是 sed 命令,删除空行,这里的空行不是文件内的空行,而是文件名间的空行,因为不同的 commit 修改的文件间存在空行。sort
命令根据文件名排序(优先级:. > 大写字母 > 小写字母)。uniq -c
,uniq 命令用于检查和删除重复行,-c 表示 count,在每行左侧显示该行重复的次数。sort -n
在删除重复行后继续排序,-n 表示 number,按照数值(即重复次数)由小到大排序。tr -s ' '
tr 命令可以对来自标准输入的字符进行替换、压缩和删除,-s 表示 squeeze,若匹配到 ’ ’ 即存在连续空格字符时压缩为一个空格 ’ '。sed "s/^ //g"
表示将每行行首的空格删去,其中s
命令的格式s/text1/text2/
表示用 text2 替换 text1,/^
表示每行的行首,之后的空格是待替换的字符,/
后什么都不加表示以空替换空格,/g
表示全局有效。cut -d" " -f1
cut 命令,-d 表示自定义分隔符,默认为制表符,-d" " 表示以空格作为分隔符;-f 表示指定显示哪个区域,通常和 -d 一起使用,如 -f1 表示显示分隔符隔开的第一部分。tail -n1
tail 命令默认显示文件的最后十行,-n<行数x> 表示显示文件末尾的 x 行。
"tr -s 示例,o, a 和空格匹配到存在重复字符,压缩为一个字符"
$ echo "hellooooo is hexmaaaaa" | tr -s 'oa '
hello is hexma
/* 获得一个关于变更数的阈值,用于判断 "always instrument" 还是 "instrument randomly"。 */
/* Note: 文件的变更数总是比文件中某一行的变更数多,因此 THRESHOLD_PERCENT_CHANGES 设为一个较小的值 */
int get_threshold_changes(std::string directory) {
std::ostringstream changecmd;
unsigned int largest_changes = 0;
int change_threshold = 0;
FILE *dfp;
// changecmd执行后,得到所有文件的最大变更数 (number of changes)
changecmd << "cd " << directory
<< " && git log --name-only --pretty=\"format:\""
<< " | sed '/^\\s*$/d' | sort | uniq -c | sort -n"
<< " | tr -s ' ' | sed \"s/^ //g\" | cut -d\" \" -fi | tail -n1";
dfp = popen(changecmd.str().c_str(), "r");
if (NULL == dfp) return WRONG_VALUE;
if (fscanf(dfp, "%u", largest_changes) != 1) return WRONG_VALUE;
change_threshold = (THRESHOLD_PERCENT_CHANGES * largest_changes) / 100;
pclose(dfp);
return change_threshold;
}
1.7 get_commit_time_days(…)
该函数是获取 commit 提交时的日期,如求 HEAD 分支提交时的日期,git_cmd 为 git show -s --format=%ct HEAD
。%ct
表示以 Unix 时间戳(timestamp)的方式表示时间,Unix 时间戳定义为从格林威治时间1970年01月01日00时00分00秒起至现在的总秒数。86400 秒为一天的总秒数。
/* 若返回值为0,说明有问题 */
unsigned long get_commit_time_days(std::string directory, std::string git_cmd){
unsigned long unix_time = 0;
FILE *dfp;
std::ostringstream datecmd;
datecmd << "cd " << directory
<< "&& " << git_cmd;
dfp = popen(datecmd.str().c_str(), "r");
if (NULL == dfp) return WRONG_VALUE;
if (fscanf(dfp, "%lu", &unix_time) != 1) return WRONG_VALUE;
pclose(dfp);
return unix_time / 86400;
}
1.8 get_max_ranks(…)
获取在 HEAD 分支前共有多少次 commit,若返回 0,说明有问题。
/* 获取在 HEAD 前的提交数,若返回0,有问题 */
unsigned int get_max_ranks(std::string git_directory) {
FILE *dfp;
unsigned int head_num_parents;
std::ostringstream headcmd;
headcmd << "cd " << git_directory
<< "&& git rev-list --count HEAD";
dfp = popen(headcmd.str().c_str(), "r");
if (NULL == dfp) return WRONG_VALUE;
if (fscanf(dfp, "%u", &head_num_parents) != 1) return wRONG_VALUE;
pclose(dfp);
return head_num_parents;
}
1.9 git_diff_current_head(…)
该函数用于获得 HEAD 提交中指定文件,相较于当前提交(current commit)的所有被更改过的代码行区域。
执行 git diff
命令,具体格式为 git diff -U0 <当前commit的值> HEAD -- <指定文件路径>
。-U0
表示生成有 0 行上下文代码的差异(默认为 3 行),整体输出指定文件在当前提交和 HEAD 提交间的代码更改(删除了什么,添加了什么)。输出中有 @@ -0,0 +1,9 @@
,表示终端展示的文本在新旧文件中 从第几行开始,共有几行, -
表示当前提交中的文件,+
表示 HEAD 提交中的文件。例子中的输出表示旧文件中从第 0 行开始,有 0 行(即不存在终端展示的文本),新文件中从第 1 行开始,共有 9 行(即 1 ~ 9 行)。下图为具体例子:
执行命令 grep -o -P "^@@ -[0-9]+(,[0-9])? \+[0-9]+(,[0-9])? @@"
。
grep 命令用于查找文件中符合条件的字符串,-o
表示只显示匹配正则表达式的部分;-P
表示使用兼容 Perl 的正则表达式语法。
/* 命令:git diff current_commit HEAD -- file_path
帮助获取 HEAD 提交中的相关行,这些行与 git show 中得到的行有关。*/
void git_diff_current_head(std::string cur_commit_sha, std::string git_directory,
std::string relative_file_path, std::set<unsigned int> &changed_lines_from_show,
std::map <unsigned int, unsigned int> &lines2changes) {
// 函数内变量声明
std::ostringstream cmd;
char array_head_changes[32], array_current_changes[32], fatat[12], tatat[12];
std::string current_line_range, head_line_result;
size_t cur_comma_pos, head_comma_pos;
int rc = 0; FILE *fp;
int cur_line_num, cur_num_start, cur_num_count, head_line_num, head_num_start, head_num_count;
std::set<unsigned int> cur_changed_lines, head_changed_lines;
bool is_head_changed = false, cur_head_has_diff = false;
/* 命令:git diff -U0 cur_commit HEAD -- filename | grep ...
获取当前提交和 HEAD 提交之间的更改行范围; 帮助获得 HEAD 提交中更改的行; */
cmd << "cd "<< git_directory << "&& git diff -U0 " << cur_commit_sha << "HEAD -- "
<< relative_file_path << " | grep -o -P \"^@@ -[0-9]+(,[0-9])? \\+[0-9]+(,[0-9])? @@\"";
fp = popen(cmd.str().c_str(), "r");
if(NULL == fp) return;
/* 可能的结果: "@@ -8,0 +9,2 @@" 或 "@@ -10 +11,0 @@" 或 "@@ -466,8 +475 @@" 或 "@@ -8 +9 @@"
匹配读文件,fatat为头部@@,tatat为尾部@@ */
while(fscanf(fp, "%s %s %s %s", fatat, array_current_changes, array_head_changes, tatat) == 4) {
cur_head_has_diff = true;
current_line_range.clear(); /* The current commit side, (-) */
current_line_range.assign(array_current_changes); // 包含 "-"
current_line_range.erase(0,1); // 去掉 "-"
cur_comma_pos = current_line_range.find(",");
/* If the changed lines in current commit can be found in changed lines from show,
the related lines in HEAD commit should count for changes. */
if (curcomma_pos == std::string::npos){
cur_line_num = std::stoi(current_line_range);
if (changed_lines_from_show.count(cur_line_num)) is_head_changed = true;
} else{
cur_num_start = std::stoi(current_line_range.substr(0, cur_comma_pos));
cur_num_count = std::stoi(current_line_range.substr(cur_comma_pos + 1, current_line_range.length() - cur_comma_pos - 1));
for(int i=0; i < cur_num_count; i++){
if(changed_lines_from_show.count(cur_num_start + i)){
is_head_changed = true;
break;
}
}
}
/* Trace changes for head commit, increment lines2changes.
Some lines are changed in current commit, so trace these lines back to HEAD commit,
and increment the count of these lines in HEAD commit. */
if (is_head_changed) {
head_line_result.clear(); /* The head commit side, (+) */
head_line_result.assign(array_head_changes); // "+"
head_line_result.erase(0,1); //remove "+"
head_comma_pos = head_line_result.find(",");
if (head_comma_pos = std::string::npos) {
head_line_num = std::stoi(head_line_result);
if(lines2changes.count(head_line_num)) lines2changes[head_line_num]++;
else lines2changes[head_line_num] = 1;
} else {
head_num_start = std::stoi(head_line_result.substr(0, head_comma_pos));
head_num_count = std::stoi(head_line_result.substr(head_comma_pos + 1,
head_line_result.length() - head_comma_pos - 1));
for(int i=0; i< head_num_count; i++) {
if (lines2changes.count(head_num_start + i)) lines2changes[head_num_start + i]++;
else lines2changes[head_num_start + i] = 1;
}
}
}
memset (array_current_changes, 0, sizeof(array_current_changes));
memset (array_head_changes, 0, sizeof(array_head_changes));
}
/* 指定文件在当前提交和 HEAD 提交间没有发生更改。
so any change in current commit (compared to its parents) counts for the HEAD commit. */
if (!cur_head_has_diff) {
for (auto mit = changed_lines_from_show.begin(); mit != changed_lines_from_show.end(); ++mit) {
if (lines2changes.count(*mit)) lines2changes[*mit]++;
else lines2changes[*mit] = 1;
}
}
rc = pclose(fp);
if (-1 == rc) { printf("git diff pclose() fails.\n"); }
}
1.10 git_show_current_changes(…)
/* git show, get changed lines in current commits. */
1.11 calculate_line_change_git_cmd(…)
/* use git command to get line changes */
二、插桩代码
三、能量调度部分
这部分代码在 aflchurn/afl-fuzz.c 中。