在「FuzzWiki」全新升级后,我们增加了工具分析梳理板块,期望通过持续的研究,分领域、分场景对现有知名模糊测试工具进行系统梳理和介绍。在该板块中,我们推出“看这一篇就够了”专题,通过一篇一工具的方式,逐步完成对现今大部分知名模糊测试工具的介绍,构建模糊测试工具知识图谱。今天,就为大家带来该专题的第一篇文章——《协议模糊测试工具AFLNet详解》。
各位如对内容有疑问欢迎留言共同探讨,同时,大家也可以通过留言的方式,将想要了解和学习的工具发送给我们,我们会结合读者的学习和阅读需求,优先发布相关工具的介绍哦!
如果需要系统地了解现有协议模糊测试工具情况,可以移步我们之前发布的文章《协议模糊测试》。
接下来,让我们一起开始对AFLNet的了解和学习吧!
一、 协议模糊测试简介
在互联网飞速发展的今天,许多本地应用都是在B/S模式下转化为网络服务:服务器部署在网络上,而客户端应用程序通过网络协议与服务器通信。协议中的安全问题可能会产生比本地应用更严重的损害,如拒绝服务、信息泄露、远程代码执行等。2017年影响全球的Wanna Cry勒索病毒事件,就是因为Windows系统中的SMB协议漏洞导致的。网络协议的安全性测试成为一个重要的问题。与基于文件的模糊测试相比,协议模糊测试面临着更多的挑战,主要体现在:
网络服务可能会定义它们自己特有的通信协议,有时我们也称其为“专有协议”或“私有协议”,而这些协议标准往往不公开。此外,即使是有文档规范的协议,其具体实现也不一定能严格遵循诸如RFC文档之类的规范。
协议模糊器需要与被测目标建立通信连接,然后将生成的符合协议规范的测试用例发送给被测目标。建立通信的过程和发送测试用例的过程会带来额外的开销,因此协议模糊测试吞吐量往往低于本地文件模糊测试的吞吐量。
协议模糊测试的被测目标通常具有很大的状态空间,有时需要精心构造特定顺序的消息序列才能到达某一状态。早期的协议模糊测试工具常采用基于生成的方式进行黑盒模糊测试,这种方式依赖于协议格式的先验知识来生成有效的模糊测试用例,并且由于黑盒的特性,很难探索深层次的漏洞。
由于AFLNet是第一个使用状态机的灰盒模糊器,能很好的对程序内部进行探索,所以本文主要对AFLNet这个工具进行详细解读。
二、 工具基本原理概要
AFLNet是一款基于AFL 的灰盒协议模糊测试工具,采用了代码覆盖率反馈、种子分割变异以及状态反馈等技术。AFLNet使用client发向server的数据包作为种子,无需掌握协议前置知识就能使用。墨尔本大学研究员Van-Thuan Pham在2020年,发表在ICST会议上的AFLNet论文中所介绍并开源的一款工具。
项目地址
github.com/aflnet/aflnet
论文原文地址
https://mboehme.github.io/paper/ICST20.AFLNet.pdf
2.1 SAMPLE
服务器的行为及其漏洞取决于一段时间内交换的一系列消息,这些消息决定了服务器的状态。众所周知的有状态协议的例子包括加密协议,如TLS,文件传输和消息传递协议,如FTP、SMB和SMTP,以及多媒体协议,如SIP和RSTP。根据会话中以前的消息,所有这些协议在给定时间可以接收哪些消息,以及可以执行哪些操作方面都是有选择性的。
下面是客户机和LightFTP[11]之间根据文件传输协议(FTP)进行的消息交换,LightFTP是一个实现FTP的服务器,也是我们评估的主题之一。客户端发送的消息序列以红色突出显示。FTP指定客户端必须首先在服务器上进行自身身份验证。只有在成功验证后,客户端才能发出其他命令。对于来自客户端的每个请求消息,FTP服务器用包含状态代码的响应消息进行回复(例如,230[登录成功]或430[无效用户/通过])。响应中的状态代码确保客户端请求得到确认,并将当前服务器状态通知客户端。
2.2 Implement
AFLNET是在AFL的基础上拓展实现,通过socket(C Socket APIs)的方法实现网络通信来方便与服务器交互。AFLNET作为客户端,拥有发送请求和接收响应两个Channels,待测服务器则作为服务端。AFLNET接收Channel除了代码覆盖率反馈外,还增加了状态反馈机制。为保证同步性,在发送请求之间添加了时延,不然服务器会丢弃发送对上一个报文的响应之前到达的数据包。AFLNET的输入是包含捕获的网络流量的pcap文件,可以使用Sniffer(如tcpdump)来嗅探网络流量,用数据包分析器(wireshark)提取相关的消息交换。
由图所示总体由5个部件组成,分别是报文解析器、状态学习机、目标状态选择器、序列选择器、序列变异器。
Request Sequence Parser 报文解析器
该组件用来生成消息序列的初始语料库。AFLNET使用特定于协议的消息结构的信息,以正确的顺序从pcap中提取单个请求,首先过滤掉pcap文件中的响应,以获得客户端请求数据包的跟踪。然后,它解析过滤后的跟踪,以识别跟踪中每条报文的开始和结束。
State Machine Learner 状态学习机
该组件接收服务器响应并将新发现的状态和transitions(转变?)添加到协议状态机IPSM。AFLNET读取响应报文并提取协议指定的状态码,确定当前的执行状态。下图中每个新的节点代表一个新的状态,根据响应中是否有新的状态码,将其添加到图中。这种形式仅用于输出可视化状态图。
而在代码中的引导机制的实现,则是使用哈希表的形式,如下图所示,以一个特定的状态序列来标记。所出现了新的状态序列哈希值则认为当前的测试用例是Interesting的。
Target State Selector 目标状态选择器
AFLNET使用了几种启发式算法,这些算法可以从学习到的IPSM中可用的统计数据计算出来,以帮助目标状态选择器选择下一个状态。一开始是随机的在IPSM中所有状态中选取,fuzz一段时间之后,就开始根据已有的数据决定那些状态fuzz权重更大,如选取那些更少被遍历的状态。
Sequence Selector 序列选择器
目标状态s被选择后,该组件从种子库哈希表中随机选取能够遍历到状态s的报文序列。同AFL一样,AFLNET将种子库做为一个队列实体,队列实体结构保存了种子的相关信息。此外,AFLNET维护了一个状态库,该状态库由两部分组成:1、一个状态实体列表,保存相关状态信息的一个数据结构;2、一个哈希表,该表将状态标识符映射到执行与状态标识符对应状态的队列列表。该组件利用哈希表随机选取能够遍历到状态s的报文序列。
Sequence Mutator 序列变异器
使用AFL的fuzz_one函数中的变异,同时结合协议感知的变异。这个模块在序列选取器选取报文序列后,把报文对话M拆分成三个部分,M1是报文中遍历到状态s的报文前缀,他保证在M1后能到达状态s。M2是变异部分,M2包含可在M1之后执行的所有消息,但仍处在状态s中。在M2实施AFL的字节翻转、插入、删除等变异操作。变异之后生成报文序列。如果生成的序列M`能够得到新的响应状态码或者带来程序新的覆盖率就认为是感兴趣的,然后存入我们的种子库。
三、 工具使用介绍
本文只列出简单使用方法,想了解详细指导步骤可参照官方仓库文档
afl-fuzz 为主程序, afl-fuzz --help 可查看全部选项,主要选项如下:
-N netinfo: 待测服务器信息 (如 tcp://127.0.0.1/
8554)
-P protocol: 指定待测协议 (如 RTSP, FTP, DTLS12, DNS, DICOM, SMTP, SSH, TLS, DAAP-HTTP,SIP, MODBUS)
-D usec: (可选) 等待服务器初始化时间 (毫秒级)
-K : (可选) 在服务器处理完所有请求后,发送SIGTERM信号结束服务器进程
-E : (可选) 开启状态感知模式(启动状态机)
-R : (可选) 开启region级别的变异操作
-c script : (可选) 服务器清理脚本名称或完整路径
-q algo: (可选) 指定状态选择算法 (如 1. RANDOM_SELECTION, 2. ROUND_ROBIN, 3. FAVOR)
-s algo: (可选) 种子选择算法 (如 1. RANDOM_SELECTION, 2. ROUND_ROBIN, 3. FAVOR)
其余选项与AFL相似
示例命令:
afl-fuzz -d -i in -o out -N <server info> -x <dictionary file> -P <protocol> -D 10000 -q 3 -s 3 -E -K -R <executable binary and its arguments (e.g., port number)>
四、 源码分析
4.1 源码概述
AFLNet代码主要实现:
01
socket通信发送测试用例
02
一套与代码覆盖率并行的状态机引导机制
03
增加了消息序列级别的变异策略
AFLNet在AFL基础上主要对afl-fuzz.c文件进行了修改,实现了一套socket通信,在fuzzer与被测服务器之间实现消息收发。增加了aflnet.c和aflnet.h两个主要文件,同时引入了klib和graphviz两个库文件。理解这部分内容需要读者有一点AFL源码的基础。
AFLNet内置了一些支持的协议,可以在aflnet.h内查看。若要新增协议,需要在aflnet.h和aflnet.c内自行实现对应的状态提取代码。
4.2 变量分析
AFLNet新增的一些比较重要数据结构
queue_entry新增成员
region_t *regions; /* Regions keeping information of message(s) sent to the server under test 种子的region指针 */
u32 region_count; /* Total number of regions in this seed 种子的region个数 */
u32 index; /* Index of this queue entry in the whole queue 该种子在种子队列中的下标 */
u32 generating_state_id; /* ID of the start at which the new seed was generated 新种子产生的起始状态ID */
u8 is_initial_seed; /* Is this an initial seed 是否是初始种子 */
u32 unique_state_count; /* Unique number of states traversed by this queue entry 该种子覆盖的唯一状态个数*/
regions
AFLNET根据不同协议定制extract_requests函数来对种子文件进行拆分,拆分结果保存在q->regions中,每个region代表客户端发送给服务器的一条请求。region_t结构体存储了该region的一些相关信息,构造region在read_testcases()函数中进行。
typedef struct {
int start_byte; /* The start byte, negative if unknown. */
int end_byte; /* The last byte, negative if unknown. */
char modifiable; /* The modifiable flag. */
unsigned int *state_sequence; /* The annotation keeping the state feedback. */
unsigned int state_count; /* Number of states stored in state_sequence. */
} region_t;
message
message结构,通过函数实现message到region、message到file的转换。用于构造kl_messages链表,在
perform_dry_run()中的construct_kl_messages()使用klib库的klist进行构造,后遍历该种子的每一个region块,将每一个region添加到kl_messages链表中。
typedef s truct {
char *mdata; /* Buffer keeping the message data */
int msize; /* Message size */
} message_t;
state
使用state_info_t对每个状态做了详细描述
typedef struct {
u32 id; /* state id */
u8 is_covered; /* has this state been covered 该状态是否被覆盖*/
u32 paths; /* total number of paths exercising this state */
u32 paths_discovered; /* total number of new paths that have been discovered when this state is targeted/selected 选择该状态下,总共发现的新路径数量*/
u32 selected_times; /* total number of times this state has been targeted/selected 此状态的选择次数*/
u32 fuzzs; /* Total number of fuzzs (i.e., inputs generated) */
u32 score; /* current score of the state */
u32 selected_seed_index; /* the recently selected seed index */
void **seeds; /* keeps all seeds reaching this state -- can be casted to struct queue_entry* 保存所有可以到达该状态的种子文件*/
u32 seeds_count; /* total number of seeds, it must be equal the size of the seeds array 上述数组中的所有种子的数量*/
} state_info_t;
其他一些全局变量
was_fuzzed_map:一个二维数组,记录状态与queue_entry的关系,数组中值-1表示状态不可到达,1表示fuzzed,0表示non-fuzzed。主要用与target_state的选择。行数表示状态数量,列数表示队列实体数量。
ipsm:使用graphviz构造的状态机指针,这个状态机实际上不影响fuzzing过程中的决策,只是用来可视化状态机。
khs_ipsm_paths、khms_states:两者都是哈希集合,前者用来记录去重之后的状态序列,其key值为状态序列的哈希值,用来判断一个状态序列是否Interesting;后者则用来记录状态。
M2_prev、M2_next:这一点对应论文中的变异策略,M2是感兴趣的序列包,前者是M2前一个包,后者是M2后一个包。
response_buf、response_buf_size、response_bytes:这组变量用来记录接收服务器相应包相关信息。
4.3 网络通信
架空原生AFL的write_to_testcase()函数,改用网络传输向服务器传输数据包。
send_over_network()
run_target()函数中调用send_over_network()函数,主要通过socket编程实现。
net_recv()是recv()函数的一个封装,调用循环将接收的数据放入response_buf,同时记录response_buf_size。超时或全部接受完返回0,出错返回1。
net_send()是send()函数的封装,调用循环确保将缓冲区的数据发送完,返回已发送的字节数
清空response_buf,先设置套接字及其选项,设置超时,接着配置服务器地址结构体(sockaddr_in)变量serv_addr,connect()连接服务器。
先调用net_recv()函数读取早期的响应内容
遍历kl_messages,依次发送链表内容,分配response_bytes存储每个message的响应包大小,检索服务器响应
进入HANDLE_RESPONSES阶段,对响应进行处理后,关闭套接字,关闭服务器子进程
4.4 状态机构建
setup_ipsm()初始化状态机,初始化graphviz图和khs_ipsm_paths、khms_states两个哈希表
若收到服务器的状态序列为interesting,则保存该kl_message。遍历该response的状态序列来更新状态机ipsm,将状态序列的状态和转移关系添加到graphviz图,同时更新两个哈希表。
4.5 状态选择
choose_target_state()从state_ids[]选择target_state_id
每次发现新的状态都会加入state_ids[]数组,且扩大was_fuzzed_map
有三种状态选择算法
01
RANDOM_SELECTION:随机选择状态
02
ROUND_ROBIN:轮询选择
03
FAVOR:轮询几轮获取足够信息开始启用FAVOR
state->score = ceil(1000 * pow(2, -log10(log10(state->fuzzs + 1) * state->selected_times + 1)) * pow(2, log(state->paths_discovered + 1)));
4.6 种子筛选
was_fuzzed_map二维数组记录了状态与队列实体间的关系,AFLNET在筛选种子的时候结合了以下条件
/* AFLNet takes into account more information to make this decision */
if ((top_rated[i]->generating_state_id == target_state_id ||
top_rated[i]->is_initial_seed) &&
(was_fuzzed_map[get_state_index(target_state_id)][top_rated[i]->index] == 0))
pending_favored++;
通过target_state_id来选择对应队列实体中比较好的种子文件
4.7 序列级别变异
AFLNET变异在fuzz_one()中做了些许改动
如果开启了state_aware_mode,则根据target_state_id来选定M2;否则随机选取M2
之后构造该实例的kl_messages,并切割成M1, M2, M3三部分,将M2作为变异目标复制到out_buf进行变异
确定性变异与原生AFL基本相同
HAVOC阶段增加了case17~20四个region级别的变异
case17: 用随机种子中的随机区域替换当前区域
case18: 将随机区域从随机种子插入到当前区域的开头
case19: 从随机种子中插入一个随机区域到当前区域的末尾
case 20: 复制当前区域
五、 结语
协议模糊测试的发展以AFLNET工具为分界线。在AFLNET提出之前,主流的协议模糊测试工具采用黑盒的方法,由于黑盒测试不能得到SUT的反馈,模糊测试很难有效探索协议的状态空间;在AFLNET提出之后,基于覆盖的有状态灰盒模糊测试成为协议模糊测试的主流方法。但AFLNet也存在较大的局限性,socket通信吞吐量较低,且服务器程序启动有初始化时间,所以相比文件类的模糊测试效率要大打折扣。