AFL分享

AFL分享Fuzzing 是漏洞挖掘领域最有效的方法之一 可以用来发现大量的远程代码执行和提权的漏洞 然而 fuzzing 优势相对肤浅和盲目的 随机变异使得我们很难实现达到测试程序特定的代码路径 这就使得测试的代码覆盖率很低 有很多人试图去解决这个问题 TavisOrmandy 曾经提出一种 根据代码覆盖率 从大量高质量的输入文件语料中选取一个子集 然后按照传统方法去 fuzz 这种方法很有效 但前提是需要一个这样的语料 另一方面 代码覆盖率只提供了一个很简单的对程序状态的描述 当 Fuzzing 测试到了一定的程度 代码

AFL

1.介绍afl大致流程

①从源码编译程序时进行插桩,以记录代码覆盖率(Code Coverage);

②选择一些输入文件,作为初始测试集加入输入队列(queue);

③将队列中的文件按一定的策略进行“突变”;

④如果经过变异文件更新了覆盖范围,则将其保留添加到队列中;

⑤上述过程会一直循环进行,期间触发了crash的文件会被记录下来。

在这里插入图片描述

二、插桩:

为什么要插桩?:

1.代码覆盖率
代码覆盖率是一种度量代码的覆盖程度的方式,也就是指源代码中的某行代码是否已执行。这里涉及到三个概念,基本块(basic-block)、边界(edge)、元组(tuple)。

2.基本块(Basic Block)
将程序的代码划分为一个个基本块,在这个基本块中第一条指令被执行后,后续的指令也会被全部执行,每个基本块中所有指令的执行次数是相同的,下图就是对分块的一个简单示例。
在这里插入图片描述




3.边(edge)
AFL的技术白皮书中提到fuzzer通过插桩代码捕获边(edge)覆盖率。那么什么是edge呢?我们可以将程序看成一个控制流图(CFG),图的每个节点表示一个基本块,而edge就被用来表示在基本块之间的跳转。知道了每个基本块和跳转的执行次数,就可以知道程序中的每个语句和分支的执行次数,从而获得详细的覆盖率信息。
在这里插入图片描述




4.元组(tuple)
具体到AFL的实现中,使用二元组(branch_src, branch_dst)来记录当前基本块 + 前一基本块 的信息,从而获取目标的执行流程和代码覆盖情况。

怎么插桩?:

插什么?:

插桩代码的伪代码如下:

cur_location = <COMPILE_TIME_RANDOM>; shared_mem[cur_location ^ prev_location]++; prev_location = cur_location >> 1; 

cur_location的值是随机产生的,为的是简化连接复杂对象的过程和保持XOR输出分布是均匀的,后者可用减少tuple映射到shared_mem[] 时发生冲突的概率。

shared_mem[] 数组是一个64kb的共享空间。其中的每一字节里面存储着对于特定的Edge也就是tuple的命中次数,所以最多可用处理64k个Edge。

prev_location = cur_location >> 1; 如果不右移,A->B与B->A这两个不一样的执行路径生成的异或值是一样的,会导致无法区分。

三、准备测试用例:

在read_testcases函数中,会对用户给出的测试用例做筛选,比如判断是否是常规文件,而不是目录文件,设备文件等,判断测试用例是否为空文件等。因此部分无效的初始测试用例会被抛弃。

四、变异-确定性变异:

4.1确定性变异:(如果当前测试用例做过确定性变异,就会直接做非确定性变异,也就是每个测试用例只做依次确定性变异)

1.bitflip,位反转

基本原理:bitflip,按位翻转,1变为0,0变为1。

拿到一个原始文件,打头阵的就是bitflip,而且还会根据翻转量/步长进行多种不同的翻转,按照顺序依次为:

  • bitflip 1/1,每次翻转1个bit,按照每1个bit的步长从头开始
//stage_max是当前测试用例的bit数 for (stage_cur = 0; stage_cur < stage_max; stage_cur++) { 
    //翻转第stage_cur 位bit FLIP_BIT(out_buf, stage_cur); //将修改后的测试用例拿去测一下目标二进制,如果有触发新的tuple或者使某个tuple的命中数增加了合理值,就把翻转后的内容作为新的测试用例加入到queue种, //如果用户主动结束测试,或者用户主动请求放弃当前测试用例等,common_fuzz_stuff就返回1,执行abandon_entry if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry; //没出错就把修改的那一位改回来 FLIP_BIT(out_buf, stage_cur); ..... } 
  • bitflip 2/1,每次翻转相邻的2个bit,按照每1个bit的步长从头开始
  • bitflip 4/1,每次翻转相邻的4个bit,按照每1个bit的步长从头开始
  • bitflip 8/8,每次翻转相邻的8个bit,按照每8个bit的步长从头开始,即依次对每个byte做翻转
  • bitflip 16/8,每次翻转相邻的16个bit,按照每8个bit的步长从头开始,即依次对每个word做翻转
  • bitflip 32/8,每次翻转相邻的32个bit,按照每8个bit的步长从头开始,即依次对每个dword做翻转

每次翻转后会将翻转后测试用例作为输入检测一次程序,如果导致崩溃就放弃对这个测试用例的继续变异,直接对下一个测试用例进行如上操作

自动检测token

生成effector map

在进行bitflip 8/8变异时,AFL还生成了一个非常重要的信息:effector map。这个effector map几乎贯穿了整个deterministic fuzzing的始终。

具体地,在对每个byte进行翻转时,如果其造成执行路径与原始路径不一致,就将该byte在effector map中标记为1,即“有效”的,否则标记为0,即“无效”的。

这样做的逻辑是:如果一个byte完全翻转,都无法带来执行路径的变化,那么这个byte很有可能是属于”data”,而非”metadata”(例如size, flag等),对整个fuzzing的意义不大。所以,在随后的一些变异中,会参考effector map,跳过那些“无效”的byte,从而节省了执行资源。

2.arithmetic,进行加减操作

在bitflip变异全部进行完成后,便进入下一个阶段:arithmetic。与bitflip类似的是,arithmetic根据目标大小的不同,也分为了多个子阶段:

  • arith 8/8,每次对8个bit进行加减运算,按照每8个bit的步长从头开始,即对文件的每个byte进行整数加减变异
 for (i = 0; i < len; i++) { 
    u8 orig = out_buf[i];//指向当前操作的字节 /* Let's consult the effector map... */ if (!eff_map[EFF_APOS(i)]) { 
   //查看当前字节是否是有效字节,如果不是就跳过当前字节 stage_max -= 2 * ARITH_MAX; continue; } for (j = 1; j <= ARITH_MAX; j++) { 
    u8 r = orig ^ (orig + j);//用于判断这次计算是否只是一个简单的位翻转 /* Do arithmetic operations only if the result couldn't be a product of a bitflip. */ //用于比较arithmetic是否只是对目标字节进行了bitflip里的那几种位翻转,如果是,就没必要继续了,因为前面做过了 if (!could_be_bitflip(r)) { 
    out_buf[i] = orig + j; if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry; stage_cur++; } else stage_max--; r = orig ^ (orig - j); if (!could_be_bitflip(r)) { 
    out_buf[i] = orig - j; if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry; stage_cur++; } else stage_max--; out_buf[i] = orig;//恢复当前字节,单一变量 } } 
  • arith 16/8,每次对16个bit进行加减运算,按照每8个bit的步长从头开始,即对文件的每个word进行整数加减变异
  • arith 32/8,每次对32个bit进行加减运算,按照每8个bit的步长从头开始,即对文件的每个dword进行整数加减变异

加减变异的上限,在config.h中的宏ARITH_MAX定义,默认为35。所以,对目标整数会进行+1, +2, …, +35, -1, -2, …, -35的变异。特别地,由于整数存在大端序和小端序两种表示方式,AFL会贴心地对这两种整数表示方式都进行变异。

此外,AFL还会智能地跳过某些arithmetic变异。第一种情况就是前面提到的effector map:如果一个整数的所有bytes都被判断为“无效”,那么就跳过对整数的变异。第二种情况是之前bitflip已经生成过的变异:如果加/减某个数后,其效果与之前的某种bitflip相同,那么这次变异肯定在上一个阶段已经执行过了,此次便不会再执行。

3.interest

下一个阶段是interest,具体可分为:

  • interest 8/8,每次对8个bit进替换,按照每8个bit的步长从头开始,即对文件的每个byte进行替换
for (i = 0; i < len; i++) { 
    u8 orig = out_buf[i];//指向当前字节 /* Let's consult the effector map... */ if (!eff_map[EFF_APOS(i)]) { 
   //检测当前字节是不是有效位 stage_max -= sizeof(interesting_8); continue; } //对每个字节,用所有的interesting依次替换一次 for (j = 0; j < sizeof(interesting_8); j++) { 
    /* Skip if the value could be a product of bitflips or arithmetics. */ //检查这次替换是不是和之前的位翻转或者算术部分是一样的结果 if (could_be_bitflip(orig ^ (u8)interesting_8[j]) || could_be_arith(orig, (u8)interesting_8[j], 1)) { 
    stage_max--; continue; } out_buf[i] = interesting_8[j];//替换 if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry; out_buf[i] = orig;//复原 stage_cur++; } } 
  • interest 16/8,每次对16个bit进替换,按照每8个bit的步长从头开始,即对文件的每个word进行替换
  • interest 32/8,每次对32个bit进替换,按照每8个bit的步长从头开始,即对文件的每个dword进行替换

而用于替换的”interesting values”,是AFL预设的一些比较特殊的数:

static s8 interesting_8[] = { 
    INTERESTING_8 };//9 static s16 interesting_16[] = { 
    INTERESTING_8, INTERESTING_16 };//9+10=19 static s32 interesting_32[] = { 
    INTERESTING_8, INTERESTING_16, INTERESTING_32 };//9+10+8=27 

这些数的定义:

#define INTERESTING_8 \ -128, /* Overflow signed 8-bit when decremented */ \ -1, /* */ \ 0, /* */ \ 1, /* */ \ 16, /* One-off with common buffer size */ \ 32, /* One-off with common buffer size */ \ 64, /* One-off with common buffer size */ \ 100, /* One-off with common buffer size */ \ 127 /* Overflow signed 8-bit when incremented */ #define INTERESTING_16 \ -32768, /* Overflow signed 16-bit when decremented */ \ -129, /* Overflow signed 8-bit */ \ 128, /* Overflow signed 8-bit */ \ 255, /* Overflow unsig 8-bit when incremented */ \ 256, /* Overflow unsig 8-bit */ \ 512, /* One-off with common buffer size */ \ 1000, /* One-off with common buffer size */ \ 1024, /* One-off with common buffer size */ \ 4096, /* One-off with common buffer size */ \ 32767 /* Overflow signed 16-bit when incremented */ #define INTERESTING_32 \ -LL, /* Overflow signed 32-bit when decremented */ \ -, /* Large negative number (endian-agnostic) */ \ -32769, /* Overflow signed 16-bit */ \ 32768, /* Overflow signed 16-bit */ \ 65535, /* Overflow unsig 16-bit when incremented */ \ 65536, /* Overflow unsig 16 bit */ \ , /* Large positive number (endian-agnostic) */ \  /* Overflow signed 32-bit when incremented */ 

与之前类似,effector map仍然会用于判断是否需要变异;此外,如果某个interesting value,是可以通过bitflip或者arithmetic变异达到,那么这样的重复性变异也是会跳过的。

4.dictionary

进入到这个阶段,就接近deterministic fuzzing的尾声了。具体有以下子阶段:

  • user extras (over),从头开始,将用户提供的tokens依次替换到原文件中
  • user extras (insert),从头开始,将用户提供的tokens依次插入到原文件中
  • auto extras (over),从头开始,将自动检测的tokens依次替换到原文件中

对于用户提供的tokens,AFL先按照长度从小到大进行排序。这样做的好处是,只要按照顺序使用排序后的tokens,那么后面的token不会比之前的短,从而每次覆盖替换后不需要再恢复到原状。

随后,AFL会检查tokens的数量,如果数量大于预设的MAX_DET_EXTRAS(默认值为200),那么对每个token会根据概率来决定是否进行替换:

 for (j = 0; j < extras_cnt; j++) { 
    /* Skip extras probabilistically if extras_cnt > MAX_DET_EXTRAS. Also skip them if there's no room to insert the payload, if the token is redundant, or if its entire span has no bytes set in the effector map. */ //UR(extras_cnt) >= MAX_DET_EXTRAS)用于概率跳过 if ((extras_cnt > MAX_DET_EXTRAS && UR(extras_cnt) >= MAX_DET_EXTRAS) || extras[j].len > len - i || !memcmp(extras[j].data, out_buf + i, extras[j].len) || !memchr(eff_map + EFF_APOS(i), 1, EFF_SPAN_ALEN(i, extras[j].len))) { 
    stage_max--; continue; } 

user extras (insert)

这一子阶段是对用户提供的tokens执行插入变异。不过与上一个子阶段不同的是,此时并没有对tokens数量的限制,所以全部tokens都会从原文件的第1个byte开始,依次插入第1个byte的前面,由于token之前已经按小到大排序了,所以后面的token可以直接覆盖前面的,对第1个byte执行完所有token之后,会先将第1个byte复原,然后对第2个byte开始一样的操作;此外,由于原文件并未发生替换,所以effector map不会被使用。

 for (i = 0; i <= len; i++) { 
   //依次对当前测试用例的所有字节操作 stage_cur_byte = i; for (j = 0; j < extras_cnt; j++) { 
   //依次对tokens操作 if (len + extras[j].len > MAX_FILE) { 
   //token插入后不能超过file极限,否则跳过当前token stage_max--; continue; } /* Insert token */ memcpy(ex_tmp + i, extras[j].data, extras[j].len);//先把token插到对应位置 /* Copy tail */ memcpy(ex_tmp + i + extras[j].len, out_buf + i, len - i);//再把后面的原文件剩余字节插入 if (common_fuzz_stuff(argv, ex_tmp, len + extras[j].len)) { 
    ck_free(ex_tmp); goto abandon_entry; } stage_cur++; } /* Copy head */ ex_tmp[i] = out_buf[i];//接下来要测试第i+1个字节了,把前面的字节内容恢复 } 

auto extras (over)

这一项与”user extras (over)”很类似,区别在于,这里的tokens是最开始bitflip阶段自动生成的。另外,自动生成的tokens总量会由USE_AUTO_EXTRAS限制(默认为10)。

五、变异-非确定性变异:

1.havoc

对于非dumb mode的主fuzzer来说,完成了上述deterministic fuzzing后,便进入了充满随机性的这一阶段;对于dumb mode或者从fuzzer来说,则是直接从这一阶段开始。

havoc,顾名思义,是充满了各种随机生成的变异,是对原文件的“大破坏”。具体来说,havoc包含了对原文件的多轮变异,每一轮都是将多种方式组合(stacked)而成:

  • 随机选取某个bit进行翻转
 FLIP_BIT(out_buf, UR(temp_len << 3));//随机选取一位翻转 
  • 随机选取某个byte,将其设置为随机的interesting value
out_buf[UR(temp_len)] = interesting_8[UR(sizeof(interesting_8))];//随机选取一个字节,用interesting的随机字段替换 
  • 随机选取某个word,并随机选取大、小端序,将其设置为随机的interesting value
if (temp_len < 2) break; //随机选取两个字节,替换位interesting_16,并且还会随机大小端的方式替换 if (UR(2)) { 
    *(u16*)(out_buf + UR(temp_len - 1)) = interesting_16[UR(sizeof(interesting_16) >> 1)]; } else { 
    *(u16*)(out_buf + UR(temp_len - 1)) = SWAP16( interesting_16[UR(sizeof(interesting_16) >> 1)]); } 
  • 随机选取某个dword,并随机选取大、小端序,将其设置为随机的interesting value
  • 随机选取某个byte,对其减去一个随机数
  • 随机选取某个byte,对其加上一个随机数
  • 随机选取某个word,并随机选取大、小端序,对其减去一个随机数
  • 随机选取某个word,并随机选取大、小端序,对其加上一个随机数
  • 随机选取某个dword,并随机选取大、小端序,对其减去一个随机数
  • 随机选取某个dword,并随机选取大、小端序,对其加上一个随机数
  • 随机选取某个byte,将其设置为随机数
  • 随机删除一段bytes
  • 随机选取一个位置,插入一段随机长度的内容,其中75%的概率是插入原文中随机位置的内容,25%的概率是插入一段随机选取的数
  • 随机选取一个位置,替换为一段随机长度的内容,其中75%的概率是替换成原文中随机位置的内容,25%的概率是替换成一段随机选取的数
  • 随机选取一个位置,用随机选取的token(用户提供的或自动生成的)替换
  • 随机选取一个位置,用随机选取的token(用户提供的或自动生成的)插入

不仅如此,AFL还会设置一个随机值use_stacking ,对当前测试用例做use_stacking 次上述的随机变异

u32 use_stacking = 1 << (1 + UR(HAVOC_STACK_POW2));//1随机左移1-7位,也是2,4,8,16,32,64,128, for (i = 0; i < use_stacking; i++) { 
    ...... havoc代码; ...... } 

2.splice

历经了如此多的考验,文件的变异也进入到了最后的阶段:splice。如其意思所说,splice是将两个seed文件拼接得到新的文件,并对这个新文件继续执行havoc变异。

locate_diffs(in_buf, new_buf, MIN(len, target->len), &f_diff, &l_diff); /* f_diff指向两个测试用例第一个不同的字节,如果<0,就说明两个测试用例一样 l_diff指向两个测试用例最后一个不同的字节,如果<2,说明从第3个字节开始的内容都一样 f_diff == l_diff说明就一个字节不同 以上三种情况就不需要拼接了 */ if (f_diff < 0 || l_diff < 2 || f_diff == l_diff) { 
    ck_free(new_buf); goto retry_splicing; } 

寻找插入位置:

//在第一个不同和最后一个不同的字节间随机找一个拼接位置 split_at = f_diff + UR(l_diff - f_diff); 
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/205596.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月19日 下午5:39
下一篇 2026年3月19日 下午5:39


相关推荐

  • 5G nr频段_5g哪个信道信号强

    5G nr频段_5g哪个信道信号强文章目录1.工作频段2.基站信道带宽2.1传输带宽配置2.2最小保护带3.信道安排3.1信道栅格3.2同步栅格参考文献1.工作频段NR工作在两大频率范围(FrequencyRange,FR):FR1和FR2,如下表1-1所示[1]。表1-1.频率范围的定义[1](TS38.104Table5.1-1)FR1和FR2中,又划分了多个不同的工作频段,如下表1-2和下表1-3所示[1]。表中的n代表NR。表1-2.NR在FR1中的工作频段[1](TS38

    2022年10月6日
    6
  • 高校集体官宣:“严禁在任何办公设备,安装使用‘龙虾’”

    高校集体官宣:“严禁在任何办公设备,安装使用‘龙虾’”

    2026年3月14日
    3
  • mysql存储引擎

    mysql存储引擎mysql存储引擎

    2022年4月24日
    37
  • 基于1DCNN(一维卷积神经网络)的机械振动故障诊断

    基于1DCNN(一维卷积神经网络)的机械振动故障诊断基于1DCNN(一维卷积神经网络)的机械振动故障诊断机械振动故障诊断最为经典的还是凯斯西储实验室的轴承故障诊断,开学一周了,上次改编鸢尾花分类的代码可用,但是并不准确。开学一周重新改编了别人的一篇代码,亲测好用。不多咧咧直接放上去(基于Tensorflow2.0)(Spyder4软件上跑的)数据集时本人把凯西轴承实验驱动端内圈损坏尺寸0.14和0.21做的二分类,数据集中0代表的0.14而1代表的0.21具体看下面最后#-*-coding:utf-8-*-“””CreatedonTue

    2022年6月8日
    101
  • URG和PSH

    URG和PSHURG 与 PSHURG 和 PSH 是 TCP 协议中的两个控制位 URG 紧急位 当 URG 1 时 表明紧急指针字段有效 它告诉系统此报文中有紧急数据 应尽快传送 相当于高优先级的数据 而不需要按原来的排队顺序来传送 当 URG 1 时 发送应用进程告诉发送方的 TCP 有紧急数据要传送 于是紧急发送方就把紧急数据插入到本报文段数据的最前面 而紧急数据后面的数据依然是普通数据 这时要与首部中的紧急指针字

    2026年3月17日
    1
  • Amazon DynamoDB简介

    Amazon DynamoDB简介一 DynamoDB 的数据是存储在 SSD 固态硬盘 固态硬盘 这样可以在预测的低延迟响应时间之内 存储和访问任何规模的数据 另外 SSD 还具有很高的 I O 性能 能够处理大规模请求工作负载我们来看看 DynamoDB 的不适合的使用场景 如果需要存储大量数据 但这些数据的访问频率很低 但 DynamoDB 可能不太适合 DynamoDb 的数据模型是无模式的 可认为是简单的键值模式不过特殊的处理在它的主键

    2026年3月16日
    2

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号