Repository: lcatro/How-to-Read-Source-and-Fuzzing Branch: master Commit: 35f451724975 Files: 56 Total size: 82.5 MB Directory structure: gitextract_e897ksuu/ ├── 1.Github.md ├── 11.AI 算法挖洞的一些尝试.md ├── 12.深入解析libfuzzer与asan.md ├── 13.逻辑漏洞自动化思路.emmx ├── 2.Fuzzing 模糊测试之数据输入.md ├── 3.Fuzzing 模糊测试之异常检测.md ├── 4.阅读源码.md ├── 5.程序编译原理.md ├── 6.静态程序分析原理.md ├── 7.动态程序分析原理.md ├── 8.玩转LLVM.md ├── 9.KLEE符号执行框架.md ├── P3 Ethereum 智能合约自动化审计议题投稿.pptx ├── P4 REX 框架与Auto Exploit Generation 符号执行原理.md ├── Think代码分析.emmx ├── hyper-v vmswitch debug.emmx ├── readme.md └── 第12章附录数据/ ├── diy_instrument_in_qemu_fuzzing/ │ ├── Makefile │ ├── example1.c │ ├── example2.c │ ├── example3.c │ ├── example4.c │ ├── example5.c │ ├── example6.c │ ├── example7.c │ ├── example8.c │ ├── llvm-sanitizer/ │ │ ├── clang-fix.txt │ │ ├── llvm/ │ │ │ └── lib/ │ │ │ └── Transforms/ │ │ │ └── Instrumentation/ │ │ │ └── SanitizerCoverage.cpp │ │ └── llvm_compile.sh │ ├── qemu_diy_device/ │ │ ├── Kconfig │ │ ├── Makefile.objs │ │ ├── diy_pci.c │ │ ├── diy_pci_coverage.c │ │ └── qemu_compile.sh │ ├── sanitize_black_list.txt │ ├── sanitize_converage.c │ ├── sanitize_converage.h │ └── signal_number.h ├── test_asan_granularity_int_2 ├── test_asan_granularity_int_2.i64 ├── test_case.i64 ├── test_case_de ├── test_case_no ├── test_libfuzzer_1_with_libfuzzer ├── test_libfuzzer_1_with_libfuzzer_pc_table ├── test_libfuzzer_1_with_libfuzzer_pc_table.i64 ├── test_libfuzzer_1_without_sanitize_coverage ├── test_libfuzzer_1_without_sanitize_coverage.i64 ├── test_libfuzzer_2_with_old_libfuzzer ├── test_libfuzzer_2_with_old_libfuzzer.i64 ├── test_libfuzzer_2_without_sanitize_coverage ├── test_libfuzzer_2_without_sanitize_coverage.i64 ├── test_libfuzzer_3_with_new_libfuzzer ├── test_libfuzzer_3_with_new_libfuzzer.i64 ├── test_libfuzzer_3_with_old_libfuzzer └── test_libfuzzer_3_with_old_libfuzzer.i64 ================================================ FILE CONTENTS ================================================ ================================================ FILE: 1.Github.md ================================================ ## 必备工具 Git ,Github ## 从Github 开始 Github 是代码分享平台,使用Github 能够找到很多开源项目,关于Github 不多做介绍了,下面分享些使用Github 读代码的操作 ## Github commits Github commits 的功能是用来记录每一次Git 提交代码的信息,里面包含了修改代码的原因,还有修改了哪些代码.Github commits 的功能在这里 ![](pic1/github_commit.png) 点击之后,可以看到很多Git 提交代码的记录 ![](pic1/github_commit_info.png) 随意点开一条记录,可以看到很多关于这条Commit 的信息 ![](pic1/github_commit_record.png) 使用Github commits 有一个操作就是:**一般来说,部分安全告警或者存在特别严重漏洞的开源项目向外发出通知的时候,往往只是提醒漏洞是影响了哪些版本,什么时候修复,要更新到最新的版本.关于漏洞的详情是很少提及的,甚至PoC 也没有.那么这个时候要怎么去研究漏洞呢?答案是追踪Commit 提交记录** 以CVE-2018-1305 为例子,关于绿盟的对外的通告如下(其他通告都大同小异): ![](pic1/nsfocus_record.png) 里面只有一个邮件通信记录,我们进去看看有什么(https://lists.apache.org/thread.html/d3354bb0a4eda4acc0a66f3eb24a213fdb75d12c7d16060b23e65781@%3Cannounce.tomcat.apache.org%3E) ![](pic1/CVE-2018-1305_email.png) 邮件最低下面有个References ,翻译为中文是引用的意思,在这里多插一句话:文章里面的引用一般是拓展阅读或者理论/数据的来源依据,如果读者需要进一步去深入这个文章,引用来源就是最好的入手点**.我们挑其中一个引用的URL 来看看(http://tomcat.apache.org/security-9.html),下面是我挑出的重点信息 ![](pic1/CVE-2018-1305_info.png) 圆圈里的意思是漏洞的描述,方框里标明的是其他有用的信息:影响的版本(Affects: 9.0.0.M1 to 9.0.4),最新修复的版本号(Fixed in Apache Tomcat 9.0.5),公开漏洞的时间(11 February 2018),Commit ID (This was fixed in revisions 1823310 and 1824323.). 找到Commit ID ,点进去看看,这个时候就跳转到了Apache 的SVN Commit 记录里边了(http://svn.apache.org/viewvc?view=revision&revision=1823310).[PS:SVN 和Git 都是版本管理工具] ![](pic1/CVE-2018-1305_svn_commit.png) 我们可以看到这次修复漏洞修改了哪些代码.但是点进去代码里看,也没有diff ,所以现在回到Git commits 里继续找修复代码的Commit .那么要怎么去找Commit 呢?这个时候,漏洞修复时间就派上用场了. SVN 的Commit 里面有一个Commit 时间(如果没有找到对应的Commit ,就在漏洞报告时间(2018/2/1)到漏洞公开时间(2018/2/23)搜索Commit ) ![](pic1/CVE-2018-1305_fix_time.png) 然后去找Commit ,发现没有找到 ![](pic1/CVE-2018-1305_master_commit.png) 这就很迷了,为啥会找不到呢.读者们回到主页,点击这里 ![](pic1/github_version.png) 这个时候,漏洞影响版本号就派上用场了,嘿嘿嘿 ![](pic1/github_version_select.png) ...这里找了个遍都没有找到这个版本,太神奇了,咱们再细细看看漏洞信息哈 ![](pic1/tips_tomcat.png) ??? 难道tomcat 和apache 是不同的?那我去搜索一下tomcat [PS:Github 搜索有很多很有趣的使用套路,待会和大家分享一个学习漏洞原理的骚操作] ![](pic1/github_search.png) ![](pic1/github_tomcat.png) 看来找错了开源项目,那就先看看版本分支吧 ![](pic1/tomcat_trunk.png) 有些开源项目是有设置不同的版本分支管理的,没有也没关系,那就来找Commit 吧 ![](pic1/CVE-2018-1305_git_commit.png) 现在已经定位到了2018/2/6 号的Commit 信息,这里有几个Commit ,一个一个慢慢看吧,搜素的过程就不多说了,最后定位到这两个Commit ![](pic1/CVE-2018-1305_git_commit_1.png) 修复代码:https://github.com/apache/tomcat/commit/3e54b2a6314eda11617ff7a7b899c251e222b1a1 测试用例:https://github.com/apache/tomcat/commit/af0c19ffdbe525ad690da4fd7e988c7788d00141 在Git 的Commit 里还能看到Diff ,很容易就知道到底哪些代码被修改过(包括代码注释) ![](pic1/CVE-2018-1305_git_diff.png) 在测试用例里面就可以直接找到PoC 了 ![](pic1/CVE-2018-1305_test_case.png) ## Github Search 前面已经说到了如何使用Commit 了,相信读者也已经去秀了一波操作,找到更多关于漏洞修复的细节,上一节有提到,关于Github Search 有一个学习代码的骚操作,当年我就是用这一招弄明白了JavaScript 这种脚本解析引擎的漏洞应该要怎么挖,是不是很想知道到底是啥套路. 在搜索框里输入`CVE` ,记住,要想挖哪个开源项目就去那个开源项目的Github 上搜素CVE 三个字 ![](pic1/github_search_cve.png) ![](pic1/github_search_cve_result.png) 结果如上,这个是Code 搜素,搜素出来的结果比较少,咱们切换到Commits 来看看 ![](pic1/github_search_cve_commit_result.png) 是不是发现了新世界 :) ![](pic1/github_search_1.png) ![](pic1/github_search_2.png) ![](pic1/github_search_3.png) 洞海无涯苦作舟,用这种方法可以从issus 和Commit 里面学到很多,但是要看懂整个Commit 不只是要看Diff ,还要下载代码到本地一步一步分析漏洞成因 ## Github Issus Issus 可以看到很多漏洞挖掘的操作,特别是AFL 和libFuzzer 的怎么样使用的,同时在这些提交漏洞的Issus 里还能收集到很多样本,可以直接拿下来到其他的开源项目里继续使用,举个例子,ImageMagick 的Issus :https://github.com/ImageMagick/ImageMagick/issues ![](pic1/github_issus.png) ![](pic1/github_issus_info.png) 这里告诉大家样本在哪儿可以下载,重点是触发的命令是什么,有了这个触发命令之后,我们也可以去照猫画虎拿到AFL 里去跑Fuzzing 啦,美滋滋 ## 在Github 上读代码 一般我都是先在Github 上阅读代码,然后再下载代码到本地Source Insight 继续读.我们有两种方式在Github 上开始阅读 ### 根据文件夹来阅读 简单地来说:**关注文件/文件夹的名字** ![](pic1/dir_php.png) ![](pic1/dir_antminer.png) ![](pic1/file_redis.png) 多翻一下目录和文件,总会遇到你感兴趣的一个地方来读 ### 根据敏感函数来阅读 善用Github 的搜索功能,它能够帮你搜索代码或者其他信息 ![](pic1/github_search_function.png) ![](pic1/github_search_function1.png) 找到了一个感兴趣的地方开始阅读代码之后,Github 的搜素功能可以帮助你向上回溯代码 ![](pic1/save_command.png) ![](pic1/save_command_find.png) ![](pic1/save_command_find_in_browser.png) 在网页和普通编辑器阅读源码记得要多使用`Ctrl + F` ,它能够帮你快速定位当前代码文件的函数定位 ![](pic1/ctrl_f_find.png) ## Git Clone 这个就不多介绍了,下载代码到本地 ## Example 去年挖到一个蚂蚁矿机的远程代码执行漏洞,发现这个问题是直接在Github 上读代码的找到的,附上源码分析. ![](pic1/source.png) ================================================ FILE: 11.AI 算法挖洞的一些尝试.md ================================================ ## 漏洞特征码筛选 #### 漏洞代码特征对比 NLP 算法普遍运用在恶意代码识别分类,最核心的一点还是通过黑白代码样本进行分类(参考https://xz.aliyun.com/t/5666 ,https://xz.aliyun.com/t/5848 ).NLP 算法对数据分类来说是很友好的,因为它能够通过给定的分类样本和特征来对数据进行识别,但是要使用这些算法应用到漏洞挖掘,除了分类识别还需要一步就是要对漏洞进行校验(符号执行在从入口点开始递归路径时,因为条件分支和求解速度的问题往往会导致性能非常慢,那么能不能通过事先筛选一些可以的特征然后来探索可执行的路径再检验漏洞呢?).接下来分别探讨这两个步骤的一些细节. #### BasicBlock 剪枝 我们用第五章里的一个示例来研究,因为Condition 条件判断的引入,代码结构其实是二维的. ![](pic11/pic20.png) 如果需要使用NLP 的方式来对代码进行识别,那么就需要把二维的代码结构转化为一维,这样代码序列看起来才会和文章的内容一样(转化成为一段英文语句),所以就需要对函数内的BasicBlock 进行剪枝,修剪之后的结构如下. ![](pic11/pic21.png) 代码实现不难,主要是通过if /switch 等语句进行处理,for /while 语句可以忽略不处理. ```python def basic_block_preprocess(code_ast_subnode) : # BasicBlock 剪枝 flatten_basic_block_list = [] root_basic_block = [] for root_ast_node_index in code_ast_subnode : ast_node_type = get_type(root_ast_node_index) # 获取AST 节点类型 if 'CIfStatement' == ast_node_type : # 目前只筛选if 语句 if_ast_node = root_ast_node_index.body # 获取if AST 的内容 if_ast_node_type = get_type(if_ast_node) if 'CBody' == if_ast_node_type : # 对应的是if (???) {xxx} 的写法 sub_basic_block_list = basic_block_preprocess(if_ast_node.contentlist) # 递归遍历if 语句 for sub_basic_block_index in sub_basic_block_list : flatten_basic_block_list.append( root_basic_block + sub_basic_block_index) # 合并剪枝之后的代码序列 else : # 对应的是if (???) xxx; 的写法 if_basic_block_ast_node = root_ast_node_index.body # if 里面语句代码块的内容 if_basic_block_ast_node_type = get_type(if_basic_block_ast_node) # 获取这个语句的类型 flatten_basic_block_list.append(root_basic_block + [ (if_basic_block_ast_node_type,if_basic_block_ast_node) ]) # 合并代码序列 if root_ast_node_index.elsePart : # 如果这个if 语句还存在else if 或else .. if_else_ast_node_type = get_type(root_ast_node_index.elsePart.body) if 'CBody' == if_else_ast_node_type : if_else_ast_node = root_ast_node_index.elsePart.body else : if_else_ast_node = root_ast_node_index.elsePart.body.body sub_basic_block_list = basic_block_preprocess(if_else_ast_node.contentlist) # 继续递归它的body 代码 for sub_basic_block_index in sub_basic_block_list : flatten_basic_block_list.append( root_basic_block + sub_basic_block_index) # 合并剪枝之后的代码序列 continue root_basic_block.append((ast_node_type,root_ast_node_index)) # 这是当前层的代码序列 flatten_basic_block_list.append(root_basic_block) return flatten_basic_block_list ``` #### AST 特征序列化 AST 结构树并不合适直接使用NLP 算法来对它进行识别,我们需要对它进行预处理,变成合适由NLP 算法处理的格式. ```python def reduce_ast_node_list(code_ast_list) : # AST 预处理 def get_var_type(var_type) : # 获取变量类型 var_type_string = '' for var_type_index in var_type : if 'unsigned' == var_type_index : # unsigned int ,unsigned char .drop the keyword unsigned continue var_type_string += var_type_index + '.' if var_type_string : var_type_string = var_type_string[ : -1 ] return var_type_string result_list = [] for code_ast_index in code_ast_list : code_ast_node_type = code_ast_index[0] # AST 节点类型 code_ast_node_data = code_ast_index[1] # AST 节点数据 if 'CVarDecl' == code_ast_node_type : # 变量声明 is_type = get_type(code_ast_node_data.type) # 变量类型 if 'CArrayType' == is_type : # 数组 var_type = get_var_type(code_ast_node_data.type.arrayOf.builtinType) result_list.append('variable_define_array:%s' % (var_type)) elif 'CBuiltinType' == is_type : # 普通变量 var_type = get_var_type(code_ast_node_data.type.builtinType) result_list.append('variable_define:%s' % (var_type)) elif 'CPointerType' == is_type : # 指针变量 var_type = get_var_type(code_ast_node_data.type.pointerOf.builtinType) result_list.append('variable_define_point:%s' % (var_type)) elif 'CStatement' == code_ast_node_type : # 赋值 assigment_data = code_ast_node_data._leftexpr sub_ast_node_type = get_type(assigment_data) if 'CFuncCall' == sub_ast_node_type : # 函数调用 function_name = assigment_data.base.name result_list.append('function_call:%s' % (function_name)) elif 'CArrayIndexRef' == sub_ast_node_type : # 数组引用 access_type = assigment_data.base.type if 'CArrayType' == access_type : # buffer[10] = ???; result_list.append('assigment_array_index') elif 'CPointerType' == access_type : # *buffer[10] = ???; result_list.append('assigment_point_index') elif 'CStatement' == sub_ast_node_type : # 变量数据值 assigment_data = assigment_data._rightexpr._leftexpr assigment_type = assigment_data.type if 'CArrayType' == assigment_type : # buffer[10] = ???; result_list.append('assigment_array_index') elif 'CPointerType' == assigment_type : # *buffer[10] = ???; result_list.append('assigment_point_index') #.pointerOf.builtinType else : result_list.append(code_ast_node_type) return result_list ``` #### Doc2Vec 算法与特征对比 Doc2Vec 算法用来对一段文本进行识别,判断这段文本属于哪一类型.我们假设了一系列的黑白样本: ```python code_sample_memcpy_check_1 = ''' void main() { char* buffer = (char*)malloc(20); char* command_buffer = (char*)malloc(10); memcpy(&command_buffer,&buffer,20); } ''' code_sample_memcpy_check_2 = ''' void main() { char buffer[20] = {0}; char command_buffer[10] = {0}; memcpy(&command_buffer,&buffer,20); } ''' code_sample_buffer_check_1 = ''' void main() { char buffer[10] = {0}; buffer[10] = '\0'; } ''' code_sample_buffer_check_2 = ''' void main() { char* buffer = (char*)malloc(10); buffer[20] = '\0'; } ''' code_sample_arbitrarily_write_check_1 = ''' void main(char* point) { char* buffer = point; *buffer = 0x1; } ''' code_sample_arbitrarily_write_check_2 = ''' void main(char* offset) { char buffer[10] = {0}; *(buffer + offset) = 0x1; } ''' code_sample_arbitrarily_read_check_1 = ''' void main(char* point) { char* buffer = point; char data = *buffer; } ''' code_sample_arbitrarily_read_check_2 = ''' void main(char* offset) { char buffer[10] = {0}; char data = *(buffer + offset); } ''' code_sample_white_call_1 = ''' void main() { printf("123123"); } ''' code_sample_white_return_1 = ''' int main() { int result = 1; return result; } ''' code_sample_white_add_1 = ''' void main() { int a = 1; int b = 2; int result = 0; result = a + b; } ''' tranning_sample_code = { 'memcpy' : [ # ... ] , 'overflow' : [ # ... ] , 'null_access' : [ # ... ] , 'arbitrarily_write' : [ # ... ] , 'arbitrarily_read' : [ # ... ] , 'white_code' : [ # ... ] , } ``` 经过之前的预处理之后,返回的代码序列如下(演示的Demo 对AST 处理比较粗糙,是导致后面分类出现误差的主要原因): ```text [['variable_define_point:char', 'variable_define_point:char', 'function_call:memcpy']] [['variable_define_array:char', 'variable_define_array:char', 'function_call:memcpy']] [['variable_define_point:char', 'variable_define_array:char', 'function_call:memcpy']] [['variable_define_array:char', 'variable_define_point:char', 'function_call:memcpy']] [['variable_define_point:char', 'variable_define_point:char', 'function_call:memset', 'function_call:memcpy']] [['variable_define_array:char', 'variable_define_array:char', 'function_call:memset', 'function_call:memcpy']] [['variable_define_point:char', 'variable_define_array:char', 'function_call:memset', 'function_call:memcpy']] [['variable_define_array:char', 'variable_define_point:char', 'function_call:memset', 'function_call:memcpy']] [['variable_define_array:char']] [['variable_define_point:char']] [['variable_define_point:char']] [['variable_define_point:char']] [['variable_define_array:char']] [['variable_define_point:char']] [['variable_define_point:char', 'variable_define:char']] [['variable_define_array:char', 'variable_define:char']] [['variable_define_point:char', 'variable_define:char']] [['function_call:printf']] [['function_call:printf', 'function_call:printf', 'function_call:printf', 'function_call:printf', 'function_call:printf']] [['variable_define:int', 'function_call:printf']] [['variable_define_array:char', 'function_call:memset', 'function_call:printf']] [['variable_define_point:char', 'function_call:printf']] [['variable_define:int', 'CReturnStatement']] [['variable_define_point:char', 'CReturnStatement']] [['variable_define_array:char', 'CReturnStatement']] [['variable_define:int', 'variable_define:int', 'variable_define:int', 'assigment_value:int']] [['variable_define:int', 'variable_define:int', 'function_call:printf'], ['variable_define:int', 'variable_define:int', 'function_call:printf'], ['variable_defi ne:int', 'variable_define:int']] [['variable_define:int', 'function_call:printf']] ``` 接下来使用Gensim Doc2ver 对样本进行训练,代码如下: ```python TaggededDocument = gensim.models.doc2vec.TaggedDocument model_tranning_sample_list = [] for tranning_sample_code_type,tranning_sample_code_data_list in tranning_sample_code.items() : for tranning_sample_code_data in tranning_sample_code_data_list : model_tranning_sample_list.append(TaggededDocument(tranning_sample_code_data, tags = [ tranning_sample_code_type ])) model = gensim.models.Doc2Vec(model_tranning_sample_list,min_count = 1,window = 3,vector_size = 200,workers = 4) model.train(model_tranning_sample_list, total_examples = model.corpus_count, epochs=70) ``` 我们构造一些测试代码,然后使用样本进行识别: ```python code_test_1 = ''' int main(const unsigned char* buffer) { unsigned char buffer_l[10] = {0}; unsigned char buffer_length = buffer[0]; if (2 <= buffer_length) return 0; if (MessageType_Hello == buffer[1]) { printf("Hello\n"); } else if (MessageType_Execute == buffer[1]) { unsigned char* command_buffer = (unsigned char*)malloc(buffer_length - 1); memset(&command_buffer,0,buffer_length); memcpy(&command_buffer,&buffer[2],buffer_length - 2); execute_command(command_buffer); } else if (MessageType_Data == buffer[1]) { decrypt_data(&buffer[2],buffer_length - 2); } return 1; } ''' # ... Sample code so more that we leave out it . test_code = [ make_code(test_sample_code.code_test_1) , # .... ] for test_code_index in test_code : print ' ---- ' for test_code_flatten_index in test_code_index : inferred_vector = model.infer_vector(test_code_flatten_index) output.valid_state_output(str(test_code_flatten_index),str(model.docvecs.most_similar([inferred_vector], topn=10))) ``` 训练样本再分类的效果如下: ![](pic11/pic19.png) 第一部分特征识别的整体难度不大,最困难的一步是要在代码序列中做好预处理,保证特征容易被算法识别而且又不能从AST 精简转化特征的过程中丢掉太多的细节,最后让Doc2Vec 来更准确地对代码序列进行识别. 定位出了可以的代码序列之后,下一步就是要对漏洞进行验证,到这一步骤一定是要使用符号执行来对变量进行取值范围的构建,然后再引入漏洞判断的条件组合起来交由求解器来实现,但是这样就不够"AI" 了.**符号执行的步骤是不能够缺少的,如果没有符号执行,那就无法知道某个特定变量的变化函数与取值范围**.我们常说深度学习的算法都是由样本来拟合出一条回归函数,让回归函数和算法来对数据进行分类计算,那么能不能拟合出这么样的一条曲线呢?漏洞判断的条件能不能推断出一条回归函数呢?下面就绕过符号执行技术直接来探讨这个问题. ---- ## 漏洞验证阶段 #### Example 1 -- 单变量与常数值判断 ```c int calcu(int a) { int number = a * a; // pow(a,2); if (number > 100) return 1; return 0; } ``` calcu() 函数输入输出关系 ![pic11/pic1.png](pic11/pic1.png) 我们知道,calcu() 函数是由if 判断来控制不同的return 返回的,那么calcu() 函数的输出因果关系如下(注意,C_100 特指if 判断表达式的右侧常数值100 ;Symblo(x) 则是指if 表达式的左则number 变量的符号表达式number = a*a): ![](pic11/pic2.png) 以(10,100)为交点,左侧黄色虚线勾画的区域是Zero (此时C_100 > Symblo(x)),右侧灰色区域是One (此时Symblo(x) > C_100).Zero 代表函数返回0 ,One 代表函数返回1 . ![](pic11/pic3.png) 所以,函数返回值是0/1 取决于函数Symblo(x) 和直线C_100 的关系.我们回过头来详细分析上述例子if 判断. ```c if (number > 100) return 1; return 0; // number = Symblo(x) ; C_100 = 100 ``` 那么可知,**当if 要执行到return 1 时,必须要number > 100,就是Symblo(x) > C_100;反之则是number <= 100,也就是Symblo(x) <= C_100)**.基于这个原理,总结如下: ```text Condition_True => Symblo(x) Condition_Flag C_? => Symblo(x) - C_? Condition_Flag 0 Condition_False => Symblo(x) !Condition_Flag C_? => Symblo(x) - C_? Condition_Flag 0 Symblo(x) 指的是number ,Condition_Flag 是指逻辑运算符,C_? 指常数值 例子: 1.if (number == 10086) Condition_True => Symblo(number) == 10086 => Symblo(number) - 10086 = 0 Condition_False => Symblo(number) != 10086 => Symblo(number) - 10086 != 0 2.if (number <= 72) Condition_True => Symblo(number) <= 72 => Symblo(number) - 72 <= 0 Condition_False => Symblo(number) > 72 => Symblo(number) - 72 > 0 ``` #### Example 2 -- 单变量与单变量判断 ```c int valid_key(int number) { int a = cos(number); int b = sin(number); if (a / b > number) return 1; return 1; } ``` calcu() 函数输入输出关系 ![](pic11\pic4.png) calcu() 函数的输出因果关系 ![](pic11/pic5.png) 在if 判断这里,我们可知Symblo(a_b) = cos(x)/sin(x) ,Symblo(number) = x .那么这个坐标系的横坐标就是x ,纵坐标就是y (**y = Symblo(x) ,意思是变量经过一系列的运算然后得出的结果,因为是对单个变量进行操作,所以就很容易知道这个变量经过很多次操作之后的具体函数.比如当前示例的calcu() ,有一个传递进来的参数number ,然后我们遍历到分支判断if 时,发现当前if 表达式的左值和右值都是对calcu() 函数的参数number 的值进行引用对比,那么通过符号执行可以推测出if 表达式左值:Symblo(a_b) = a / b = cos(number) / sin(number),右值:Symblo(number) = number**).黄色虚线区域代表Condition True ,灰色代表Condition False .使用上一个示例的总结,我们可以知道: ```text Conditon_True => Symblo(a_b) > Symblo(number) => cos(number) / sin(number) > number => cot(number) > number => cot(number) - number > 0 Conditon_False => Symblo(a_b) <= Symblo(number) => cos(number) / sin(number) <= number => cot(number) <= number => cot(number) - number <= 0 ``` 事实上,我们所列出的Condition_True 和Condition_False ,实际上指的是一个取值范围(Value_Range),**当变量值出现在某个区域时,我们就认为它属于True / False** .所以,也可以说是Condition_True_Range ,Condition_False_Range . #### Example 3 -- 单变量与常数值多次判断 ```c int calcu(int number) { int result = pow(number,2); if (result > 100) return 2; else if (result > 25) return 1; return 0; } ``` calcu() 函数输入输出关系 ![](pic11/pic6.png) calcu() 函数的输出因果关系 ![](pic11/pic7.png) 我们分析上述两个if 判断.判断1: result > 100 ,满足符合条件的区域为Symblo(number) > 100 ,对应上图紫色区域;判断2: result > 25 && result <=100 (注意,**result <= 100 是隐含条件,在这个else if 前面还有一个先决条件,所以不能忽略这个result <=100 这个表达式**),满足符合条件的区域为**Symblo(number) <= 100 ∩ Symblo(number) > 25 <=> x^2 <= 100 ∩ x^2 > 25 **,对应上图黄色区域;最后的return 0 对应上述的两个先决条件:1.result<=100 ;2.result<=25 ,合并起来就是**result <= 100 && result <= 25 <=> Symblo(number) <= 25 && Symblo(number) <= 100 <=> Symblo(number) <= 25 **,对应上图橙色区域.总结如下: ```text Condition_Express_1 && Condition_Express_2 => Condition_True_Range_1 ∩ Condition_True_Range_2 (&& 是逻辑And ) Condition_Express_1 || Condition_Express_2 => Condition_True_Range_1 ∪ Condition_True_Range_2 (|| 是逻辑Or ) ``` **变量经过多次赋值和运算,那么它的值一定能够可以通过一条函数表达式来计算的(参考符号执行原理,通过变量间的赋值与计算关系推导出结果).那么if 判断的实质就是要限制变量的取值范围,所以Symblo(x) 是变量结果的计算函数,Condition_Range 则是变量的取值范围** #### Example 4 -- 多变量引用与单次判断 ```c int calcu(int a,int b) { if (a > b) return 1; return 0; } ``` calcu() 函数的输出因果关系 ![](pic11/pic8.png) 首先,calcu() 函数引入了两个变量,在if 判断这里引用两个变量a 和b ,因为a 与b 的值关系最终决定了y (返回值)的值,所以这就需要构造三维坐标系.于是Condition_True_Range <=> Symblo(a) > Symblo(b) .当a = b 相等时,我们可以在a b 的二维平面上勾画出一条直线,f(b) = b .那么当a > b 时,也就是在One 区域;当b < a 时,那就在Zero 区域. #### Example 5 -- 数组引用 ```c void access(int index) { char buffer[10] = {0}; buffer[index] = 'A'; } ``` 现在我们要研究的是index 与buffer 变量的关系.我们在写白盒审计工具时,如果要对代码`buffer[index] = 'A';` 漏洞校验时,如果index >= sizeof(buffer) 那就认为这行代码存在越界漏洞,我们把index 变量的关系和buffer 这么来处理.先来看看index 变量与buffer 索引之间的关系函数. ![](pic11/pic11.png) 可以知道,这是一个`buffer_index = Symblo(index) = index`,横坐标是index 变量,纵坐标是buffer_index .再看看看其它例子: ```c void access(int index) { char buffer[10] = {0}; buffer[index + 2] = 'A'; } ``` 此时index 变量与buffer 索引之间的关系函数为`buffer_index = Symblo(index) = index + 2`. ![](pic11/pic12.png) ```c void access(int index) { char buffer[10] = {0}; buffer[index & 8] = 'A'; } ``` 此时index 变量与buffer 索引之间的关系函数为`buffer_index = Symblo(index) = index & 8`.因为引入了逻辑运算,其实上也可以通过坐标系画出Symblo(index) 的函数曲线的,图像如下. ![](pic11/pic13.png) 回过头来继续深入数组访问的第一个示例程序,我们把buffer_size (由`char buffer[10]={0}` 可知buffer_size = (x = 10))也引入到坐标系中,得到下图: ![](pic11/pic14.png) 在此我们分为两条函数:Symblo(buffer_size) 和Symblo(index) ,两条函数相交于(10,10) .那么有:**1.Symblo(buffer_size) > Symblo(index) 意味着对这个数组的访问是正常的; 2.Symblo(buffer_size) < Symblo(index) 意味着访问这个数组是异常的(越界访问)** .所以我们就需要对**变量index 的取值范围进行限制**,示例代码修改如下: ```c void access(int index) { char buffer[10] = {0}; if (index < sizeof(buffer)) buffer[index] = 'A'; } ``` 现在我们引入了if 判断,对变量index 的取值范围进行了限制(x = 10),对应图像如下: ![](pic11/pic15.png) 橙色区域是合法的buffer 引用范围,红色区域是buffer 引用越界的范围.**横坐标的index 经过一系列的运算(Symblo(index)) 最后得出纵坐标buffer_index 的值;而且,对index 所做的if 校验,实际上都是对index 的范围进行限制,只有Symblo(index) 在符合index 的取值范围内能够让buffer_index 的值大于10 才能导致的越界,所以就把y = 10 表示为漏洞边界表达式,只要存在越过这一边界的值,那么就存在越界漏洞.** #### Example 6 -- 任意地址读写漏洞分析 ```c int resolve_buffer(int* recv_buffer) { int offset = recv_buffer[1]; return *(recv_buffer + offset); } ``` 代码语句*(recv_buffer + offset) 的意思是要获取这个地址中的内容.一般来说,recv_buffer 是一个特定的内存地址(一个固定的常数值),offset 则是一个变量值(因为是来自用户输入),那么最后**读取的地址函数式与recv_buffer ,offset 对应的关系为address = Symblo(offset) <=> recv_buffer + offset <=> C_recv_buffer + offset.**对应的变化关系图如下,横坐标为offset ,纵坐标为address : ![](pic11/pic16.gif) 我们知道,C_recv_buffer 是一个正整数常数(0 <= C_recv_buffer <= max(int)),offset 则是一个变量,接下来对offset 的长度进行校验,代码如下: ```c int resolve_buffer(int* recv_buffer) { int offset = recv_buffer[1]; if (offset < 20) return *(recv_buffer + offset); return 0; } ``` 对应的关系图如下: ![](pic11/pic17.png) 在此我们假设C_recv_buffer 的值为25 ,offset 是变量,但是被约束offset < 20 ,那么橙色区域是合法的访问区域,红色区域则是不合法的访问区域,因为offset 是int 类型,可以取值为负数,那么久可以越过recv_buffer 的合法读取往前读取地址空间小于20 的位置. ![](pic11/pic18.png) 所以我们分析这个图形,漏洞的边界函数有两个,分别是C_recv_buffer_lower_bound = 25 与C_recv_buffer_upper_bound = 45 .只有**C_recv_buffer_lower_bound <= address < C_recv_buffer_upper_bound** 时访问数组才是合法的. 实际上,我们在用Symblo Executge (符号执行)和Coverage (代码覆盖率)就是为了不断探索出if 判断对于变量所设定的取值范围.对变量每增多一个if 判断,那么取值范围就会相应地减小. #### 取值范围与函数相交 下面是一个函数C1 穿过一个数据集R1 的例子. ![](pic11/pic9.png) 我们假设R1 是某个变量的取值范围,C1 是边界函数.**使用机器学习的思想,边界函数C1 根据数据出现在边界函数的左右两则位置而确定数据的分类,我们只需要给定数据,那么就可以拟合出边界函数C1 的曲线,然后给数据进行分类**.*那么我们能不能通过对这些数据集进行分类进而确定是否存在漏洞呢?* ##### 曲线拟合 我们知道,对一些已经打好分类标签的数据集再传递给模型学习,那么模型就能够拟合出一条曲线C(x) ,如下图: ![](pic11/pic10.png) 但是,**对于某种特定的漏洞检验函数,它是唯一的**.比如说:任意地址写对应的检验函数为C(x) = x ;数组越界检验函数为C_upper(buffer) = C1 ,C_lower(buffer) = C2 .这些漏洞边界函数都是较为**固定**的,并不是像需要依靠分类的样本数据使用算法来拟合出的边界函数,**漏洞的产生存在因果关系,而不是相关性**. #### 漏洞样本检验 我们研究一下样本代码,看看能不能发现些什么. ```c // Buffer Overflow code_sample_buffer_check_1 = ''' void main() { char buffer[10] = {0}; buffer[10] = '\0'; } ''' code_sample_buffer_check_2 = ''' void main() { char* buffer = (char*)malloc(10); buffer[20] = '\0'; } ''' ``` 前面已经说过,漏洞之所以会产生,那是因为buffer_size 和buffer_index 的关系.**因为只有buffer_index > buffer_size 时,才导致了buffer 访问溢出.**我们使用ASAN ,Gflags 的目的就是要挖掘出来这两者之间的关系(通过代码插桩或者内存读写权限控制实现越界检测).对于code_sample_buffer_check_1 来说,buffer 大小是显式表达的(变量语句中已经声明了buffer 大小为10 );code_sample_buffer_check_2 则是隐式表达的(因为是通过malloc 分配指定,大部分情况下不容易确定它具体的值). ```c // Arbitrarily_Write code_sample_arbitrarily_write_check_1 = ''' void main(char* point) { char* buffer = point; *buffer = 0x1; } ''' code_sample_arbitrarily_write_check_2 = ''' void main(char* offset) { char buffer[10] = {0}; *(buffer + offset) = 0x1; } ''' code_sample_arbitrarily_read_check_1 = ''' void main(char* point) { char* buffer = point; char data = *buffer; } ''' code_sample_arbitrarily_read_check_2 = ''' void main(char* offset) { char buffer[10] = {0}; char data = *(buffer + offset); } ''' ``` 任意地址读写也是一样的,对于buffer 的边界还是需要上下文来推断,offset 的变化函数容易推算出来,但是buffer 的边界函数却不容易推算. #### 总结 漏洞验证阶段是最头疼的,**验证一个漏洞是否有效,本质上是对变量的取值范围与漏洞边界进行探讨.通过上述的一些讨论发现如果要使用AI 的算法去拟合出漏洞边界函数其实是不现实的,因为这些边界函数是根据相关变量动态变化的,倒不如让我们把所有的限制条件和变量初值设置好让求解器来运算.**博主太菜了,他真的没有办法了,写到这里的时候,不知不觉留下了没有技术的泪水... 那么因果推断能用上来吗? https://zhuanlan.zhihu.com/p/33860572 ![](pic11/pic22.png) 在线函数画图URL https://zh.numberempire.com/graphingcalculator.php https://www.desmos.com/calculator ================================================ FILE: 12.深入解析libfuzzer与asan.md ================================================ ## LLVM下的插桩简述 关于LLVM的编译过程网上已经有很多的分析,在此挑选出与本文相关的地方做简单的复述: 1. LLVM前端把代码序列化为AST树,编译成LLVM IR. 2. 编译为LLVM IR后,通过各个模块(Pass)进行分析,优化与插桩. 3. 编译为目标平台二进制字节码. 4. 符号链接,生成可执行文件. ![](./pic12/Compile-time-instrumentation-flow-in-LLVM.png) 本文要讨论的插桩技术包含Sanitizer-Coverage和ASAN,它们在LLVM中分别存在于Pass和Compiler-RT中.简单地说,Pass提供插桩的功能,Compiler-RT中提供了运行时支持的内部接口函数,下面从最容易入手的Sanitizer-Coverage开始实现代码覆盖率的统计. ## 玩转Sanitizer-Coverage #### Sanitizer-Coverage初体验 接触过二进制Fuzzing的朋友们应该知道,代码覆盖率的用意是了解当前的模糊测试方式与用例触发程序执行的代码占整体代码的百分比,这个比值越高,越说明有很多的代码分支和函数被执行到,能够挖掘到隐藏在代码的漏洞的概率就更大. 下面是一段简单的测试代码: ```c #include int function1(int a) { if (1 == a) return 0; return 1; } int function2() { return -1; } int main() { if (rand() % 2) function1(rand() % 3); else function2(); return 0; } ``` 要想Clang引入Sanitizer-Coverage,需要提供编译参数`-fsanitize-coverage=trace-pc-guard`,编译命令如下: ```makefile all: clang -fsanitize-coverage=trace-pc-guard ./test_case.c -g -o ./test_case ``` 把编译后的可执行程序`./test_case`拿到IDA逆向,可以发现LLVM Sanitizer-Coverage的插桩原理: ```c int __cdecl main(int argc, const char **argv, const char **envp) { int v3; // eax __int64 v4; // rdx int v5; // eax _sanitizer_cov_trace_pc_guard(&unk_439BC0, argv, envp); v3 = rand(); v4 = (unsigned int)(v3 >> 31); LODWORD(v4) = v3 % 2; if ( v3 % 2 ) { _sanitizer_cov_trace_pc_guard((char *)&unk_439BC0 + 4, argv, v4); v5 = rand(); function1(v5 % 3); } else { _sanitizer_cov_trace_pc_guard((char *)&unk_439BC0 + 8, argv, v4); function2(); } return 0; } ``` 其中**_sanitizer_cov_trace_pc_guard()**就是插桩回调函数,如果没有重写该函数,那就LLVM就会使用默认版本,官方文档有一处示例代码,使用自定义该回调函数打印插桩分支信息. ```c // 多余注释已经删除,感兴趣可自行到官网查看 extern "C" void __sanitizer_cov_trace_pc_guard(uint32_t *guard) { if (!*guard) return; void *PC = __builtin_return_address(0); char PcDescr[1024]; __sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr)); printf("guard: %p %x PC %s\n", guard, *guard, PcDescr); } ``` 把函数代码放到test_case.c中并添加相关头文件后,编译后执行效果如下: ```sh ubuntu@ubuntu-virtual-machine:~/Desktop/instrument_note$ ./test_case guard: 0x439bc0 5 PC 0x423c06 in main /home/ubuntu/Desktop/instrument_note/./test_case.c:17 guard: 0x439bc4 6 PC 0x423c3b in main /home/ubuntu/Desktop/instrument_note/./test_case.c:19:19 guard: 0x439bb0 1 PC 0x423b6c in function1 /home/ubuntu/Desktop/instrument_note/./test_case.c:6 guard: 0x439bb4 2 PC 0x423b98 in function1 /home/ubuntu/Desktop/instrument_note/./test_case.c:8:9 ubuntu@ubuntu-virtual-machine:~/Desktop/instrument_note$ ``` #### 一个简单的代码覆盖率Demo 统计程序的代码覆盖率需要两个要素:`当前程序所有分支总数/执行过的程序路径总数`.对于当前程序所有分支总数的获取,我们可以直接通过`__sanitizer_cov_trace_pc_guard()`统计得到,那么当前程序所有分支总数怎么获取呢?我们发现LLVM还提供了Sanitizer-Coverage初始化函数`__sanitizer_cov_trace_pc_guard_init()`,来看看它的声明. ```c void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop); ``` 其中,start和stop参数分别指的是插桩数据开始到结束的指针,那么只需要计算`stop-start`即可获取当前程序所有分支总数. ```c uint32_t __sancov_current_all_guard_count = 0; void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,uint32_t *stop) { __sancov_current_all_guard_count = (stop - start); printf("Sanitizer All Coverage edges: 0x%X \n",__sancov_current_all_guard_count); } ``` ```sh ubuntu@ubuntu-virtual-machine:~/Desktop/instrument_note$ make && ./test_case clang -fsanitize-coverage=trace-pc-guard ./test_case.c -g -o ./test_case ./test_case.c:31:3: warning: implicit declaration of function '__sanitizer_symbolize_pc' is invalid in C99 [-Wimplicit-function-declaration] __sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr)); ^ 1 warning generated. Sanitizer All Coverage edges: 0x7 ubuntu@ubuntu-virtual-machine:~/Desktop/instrument_note$ ``` 稍微对代码进行修改,就可以完成一个简单的代码覆盖率统计Demo ```c uint32_t __sancov_current_all_guard_count = 0; uint32_t __sancov_current_execute_guard_count = 0; void __sanitizer_cov_trace_pc_guard(uint32_t *guard) { if (!*guard) return; void *PC = __builtin_return_address(0); char PcDescr[1024]; __sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr)); printf("guard: %p %x PC %s\n", guard, *guard, PcDescr); ++__sancov_current_execute_guard_count; } void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,uint32_t *stop) { int index = 0; for (uint32_t *p = start;p < stop;++p) // 为什么这里要需要for循环初始化呢,下一章会提到 *p = ++index; __sancov_current_all_guard_count = (stop - start); printf("Sanitizer All Coverage edges: 0x%X \n",__sancov_current_all_guard_count); } int main() { if (rand() % 2) function1(rand() % 3); else function2(); printf("Coverage Rate:%.2f% (%d/%d)\n", __sancov_current_execute_guard_count, __sancov_current_all_guard_count, ((float)__sancov_current_execute_guard_count/(float)__sancov_current_all_guard_count) * 100); return 0; } ``` ```sh ubuntu@ubuntu-virtual-machine:~/Desktop/instrument_note$ make && ./test_case clang -fsanitize-coverage=trace-pc-guard ./test_case.c -g -o ./test_case Sanitizer All Coverage edges: 0x7 guard: 0x439bc0 5 PC 0x423ca6 in main /home/ubuntu/Desktop/instrument_note/./test_case.c:41 guard: 0x439bc4 6 PC 0x423cdb in main /home/ubuntu/Desktop/instrument_note/./test_case.c:43:19 guard: 0x439bb0 1 PC 0x423c0c in function1 /home/ubuntu/Desktop/instrument_note/./test_case.c:30 guard: 0x439bb4 2 PC 0x423c38 in function1 /home/ubuntu/Desktop/instrument_note/./test_case.c:32:9 Coverage Rate:57.14% (4/7) ubuntu@ubuntu-virtual-machine:~/Desktop/instrument_note$ ``` #### 深入探索Sanitizer-Coverage实现 前一章节中留下了一个疑问,如果有自行使用这段代码编译运行就会发现,为什么用户自定义函数`__sanitizer_cov_trace_pc_guard_init()`之后,`__sanitizer_cov_trace_pc_guard()`就没有任何程序执行输出了?为什么`__sanitizer_cov_trace_pc_guard_init()`对start和stop初始化之后就可以成功运行了?为了深入理解这个问题,我们需要逆向Sanitizer-Coverage编译后的二进制程序. 我们阅读默认版本的`__sanitizer_cov_trace_pc_guard_init()`代码: ```c // 默认版本() unsigned __int64 __usercall _sanitizer_cov_trace_pc_guard_init@(unsigned __int64 result@, unsigned __int64 a2@, __sancov *a3@, __m128i a4@, __m128i a5@) { // 省略很多代码 v5 = (_DWORD *)a2; // start if ( *(_DWORD *)a2 ) return result; v6 = (unsigned __int64)a3; // stop // 省略很多代码 do { *v5 = ++v8; ++v5; } while ( (unsigned __int64)v5 < v6 ); // 省略很多代码 return result; } ``` 初始化函数会对start和stop这块内存区域进行计数写入,再来看看这块内存的分布. ```assembly __sancov_guards:0000000000439BB0 ; =========================================================================== __sancov_guards:0000000000439BB0 __sancov_guards:0000000000439BB0 ; Segment type: Pure data __sancov_guards:0000000000439BB0 ; Segment permissions: Read/Write __sancov_guards:0000000000439BB0 __sancov_guards segment dword public 'DATA' use64 __sancov_guards:0000000000439BB0 assume cs:__sancov_guards __sancov_guards:0000000000439BB0 ;org 439BB0h __sancov_guards:0000000000439BB0 public __start___sancov_guards __sancov_guards:0000000000439BB0 ; uint32_t _start___sancov_guards[3] __sancov_guards:0000000000439BB0 __start___sancov_guards dd 0 ; start参数起始地址 __sancov_guards:0000000000439BB4 db 0 __sancov_guards:0000000000439BB5 db 0 __sancov_guards:0000000000439BB6 db 0 __sancov_guards:0000000000439BB7 db 0 __sancov_guards:0000000000439BB8 db 0 __sancov_guards:0000000000439BB9 db 0 __sancov_guards:0000000000439BBA db 0 __sancov_guards:0000000000439BBB db 0 __sancov_guards:0000000000439BBC ; uint32_t guard __sancov_guards:0000000000439BBC guard dd 0 __sancov_guards:0000000000439BC0 ; uint32_t dword_439BC0[3] __sancov_guards:0000000000439BC0 dword_439BC0 dd 0 __sancov_guards:0000000000439BC0 __sancov_guards:0000000000439BC4 db 0 __sancov_guards:0000000000439BC5 db 0 __sancov_guards:0000000000439BC6 db 0 __sancov_guards:0000000000439BC7 db 0 __sancov_guards:0000000000439BC8 db 0 __sancov_guards:0000000000439BC9 db 0 __sancov_guards:0000000000439BCA db 0 __sancov_guards:0000000000439BCB db 0 __sancov_guards:0000000000439BCB __sancov_guards ends __sancov_guards:0000000000439BCB LOAD:0000000000439BCC ; stop结束地址 ``` 这样来看这块内存数据不太容易理解,我们再读一下funtion1()的反汇编代码. ```c int __cdecl function1(int a) { int v2; // [rsp+Ch] [rbp-4h] _sanitizer_cov_trace_pc_guard(_start___sancov_guards); // 从start[0]读取数据调用trace_pc_guard() if ( a == 1 ) { _sanitizer_cov_trace_pc_guard(&_start___sancov_guards[1]); // 从start[1]读取数据调用trace_pc_guard() v2 = 0; } else { _sanitizer_cov_trace_pc_guard(&_start___sancov_guards[2]); // 从start[2]读取数据调用trace_pc_guard() v2 = 1; } return v2; } // 执行function1()后的输出如下: // guard: 0x439bb0 1 PC 0x423c1c in function1 /home/ubuntu/Desktop/instrument_note/./test_case.c:31 // guard: 0x439bb4 2 PC 0x423c48 in function1 /home/ubuntu/Desktop/instrument_note/./test_case.c:33:9 ``` 对`function1()`的逆向和运行可以发现,start[0]-start[2]的内存数据是用于保存当前执行的分支ID数据.综上所述,Sanitizer-Coverage会创造一块专用的区段用于保存插桩分支ID信息,但是这块内存默认是空数据,所以才需要`__sanitizer_cov_trace_pc_guard_init`遍历生成ID写入这块内存,后续`__sanitizer_cov_trace_pc_guard()`就可以成功从这里读取到分支ID数据.理解这个细节之后,再回来阅读上面的自定义`__sanitizer_cov_trace_pc_guard_init()`容易明白意义何在了. ```c // 用户自定义版本 void __cdecl _sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop) { uint32_t *p; // [rsp+0h] [rbp-20h] int index; // [rsp+Ch] [rbp-14h] index = 0; for ( p = start; p <= stop; ++p ) // 初始化分支ID表 *p = ++index; _sancov_current_all_guard_count = stop - start; // 计算所有程序分支总数 printf("Sanitizer All Coverage edges: 0x%X \n", (unsigned int)(stop - start), p); } ``` #### LLVM Pass for SanitizerCoverage.cpp实现细节 了解Sanitizer-Coverage的运行原理后,现在从Clang编译的角度去探索它是怎么做实现的.SanitizerCoverage的实现代码在LLVM的`\llvm-project\llvm\lib\Transforms\Instrumentation\SanitizerCoverage.cpp`目录.在阅读插桩代码之前简短提示下LLVM的Pass(优化模块)运行过程,插桩时一般用到ModulePass和FunctionPass,如果对整个代码文件进行处理时,那就用到ModulePass对象;如果对所有函数都处理,那就用到FunctionPass.PassManager控制所有Pass的执行过程. ```c++ class ModuleSanitizerCoverageLegacyPass : public ModulePass { public: bool runOnModule(Module &M) override { ModuleSanitizerCoverage ModuleSancov(Options, Allowlist.get(), Blocklist.get()); // Allowlist/Blocklist由参数-fsanitize-coverage-allowlist/-fsanitize-coverage-blocklist指定函数列表,有些场景下会用到 auto DTCallback = [this](Function &F) -> const DominatorTree * { return &this->getAnalysis(F).getDomTree(); }; auto PDTCallback = [this](Function &F) -> const PostDominatorTree * { return &this->getAnalysis(F) .getPostDomTree(); }; return ModuleSancov.instrumentModule(M, DTCallback, PDTCallback); } } ``` ModulePass执行时的入口点在`runOnModule()`中,这里主要是把相关的参数传递给`instrumentModule()`. ```c++ bool ModuleSanitizerCoverage::instrumentModule( Module &M, DomTreeCallback DTCallback, PostDomTreeCallback PDTCallback) { if (Options.CoverageType == SanitizerCoverageOptions::SCK_None) return false; if (Allowlist && !Allowlist->inSection("coverage", "src", M.getSourceFileName())) return false; if (Blocklist && Blocklist->inSection("coverage", "src", M.getSourceFileName())) return false; C = &(M.getContext()); DL = &M.getDataLayout(); CurModule = &M; CurModuleUniqueId = getUniqueModuleId(CurModule); TargetTriple = Triple(M.getTargetTriple()); FunctionGuardArray = nullptr; Function8bitCounterArray = nullptr; FunctionBoolArray = nullptr; FunctionPCsArray = nullptr; IntptrTy = Type::getIntNTy(*C, DL->getPointerSizeInBits()); IntptrPtrTy = PointerType::getUnqual(IntptrTy); Type *VoidTy = Type::getVoidTy(*C); IRBuilder<> IRB(*C); Int64PtrTy = PointerType::getUnqual(IRB.getInt64Ty()); Int32PtrTy = PointerType::getUnqual(IRB.getInt32Ty()); Int8PtrTy = PointerType::getUnqual(IRB.getInt8Ty()); Int1PtrTy = PointerType::getUnqual(IRB.getInt1Ty()); Int64Ty = IRB.getInt64Ty(); Int32Ty = IRB.getInt32Ty(); Int16Ty = IRB.getInt16Ty(); Int8Ty = IRB.getInt8Ty(); Int1Ty = IRB.getInt1Ty(); SanCovTracePCIndir = M.getOrInsertFunction(SanCovTracePCIndirName, VoidTy, IntptrTy); // Make sure smaller parameters are zero-extended to i64 as required by the // x86_64 ABI. AttributeList SanCovTraceCmpZeroExtAL; if (TargetTriple.getArch() == Triple::x86_64) { SanCovTraceCmpZeroExtAL = SanCovTraceCmpZeroExtAL.addParamAttribute(*C, 0, Attribute::ZExt); SanCovTraceCmpZeroExtAL = SanCovTraceCmpZeroExtAL.addParamAttribute(*C, 1, Attribute::ZExt); } SanCovTraceCmpFunction[0] = M.getOrInsertFunction(SanCovTraceCmp1, SanCovTraceCmpZeroExtAL, VoidTy, IRB.getInt8Ty(), IRB.getInt8Ty()); SanCovTraceCmpFunction[1] = M.getOrInsertFunction(SanCovTraceCmp2, SanCovTraceCmpZeroExtAL, VoidTy, IRB.getInt16Ty(), IRB.getInt16Ty()); SanCovTraceCmpFunction[2] = M.getOrInsertFunction(SanCovTraceCmp4, SanCovTraceCmpZeroExtAL, VoidTy, IRB.getInt32Ty(), IRB.getInt32Ty()); SanCovTraceCmpFunction[3] = M.getOrInsertFunction(SanCovTraceCmp8, VoidTy, Int64Ty, Int64Ty); SanCovTraceConstCmpFunction[0] = M.getOrInsertFunction( SanCovTraceConstCmp1, SanCovTraceCmpZeroExtAL, VoidTy, Int8Ty, Int8Ty); SanCovTraceConstCmpFunction[1] = M.getOrInsertFunction( SanCovTraceConstCmp2, SanCovTraceCmpZeroExtAL, VoidTy, Int16Ty, Int16Ty); SanCovTraceConstCmpFunction[2] = M.getOrInsertFunction( SanCovTraceConstCmp4, SanCovTraceCmpZeroExtAL, VoidTy, Int32Ty, Int32Ty); SanCovTraceConstCmpFunction[3] = M.getOrInsertFunction(SanCovTraceConstCmp8, VoidTy, Int64Ty, Int64Ty); { AttributeList AL; if (TargetTriple.getArch() == Triple::x86_64) AL = AL.addParamAttribute(*C, 0, Attribute::ZExt); SanCovTraceDivFunction[0] = M.getOrInsertFunction(SanCovTraceDiv4, AL, VoidTy, IRB.getInt32Ty()); } SanCovTraceDivFunction[1] = M.getOrInsertFunction(SanCovTraceDiv8, VoidTy, Int64Ty); SanCovTraceGepFunction = M.getOrInsertFunction(SanCovTraceGep, VoidTy, IntptrTy); SanCovTraceSwitchFunction = M.getOrInsertFunction(SanCovTraceSwitchName, VoidTy, Int64Ty, Int64PtrTy); Constant *SanCovLowestStackConstant = M.getOrInsertGlobal(SanCovLowestStackName, IntptrTy); SanCovLowestStack = dyn_cast(SanCovLowestStackConstant); if (!SanCovLowestStack) { C->emitError(StringRef("'") + SanCovLowestStackName + "' should not be declared by the user"); return true; } SanCovLowestStack->setThreadLocalMode( GlobalValue::ThreadLocalMode::InitialExecTLSModel); if (Options.StackDepth && !SanCovLowestStack->isDeclaration()) SanCovLowestStack->setInitializer(Constant::getAllOnesValue(IntptrTy)); SanCovTracePC = M.getOrInsertFunction(SanCovTracePCName, VoidTy); SanCovTracePCGuard = M.getOrInsertFunction(SanCovTracePCGuardName, VoidTy, Int32PtrTy); /* static const char *const SanCovTracePCName = "__sanitizer_cov_trace_pc"; static const char *const SanCovTraceCmp1 = "__sanitizer_cov_trace_cmp1"; static const char *const SanCovTraceCmp2 = "__sanitizer_cov_trace_cmp2"; static const char *const SanCovTraceCmp4 = "__sanitizer_cov_trace_cmp4"; static const char *const SanCovTraceCmp8 = "__sanitizer_cov_trace_cmp8"; */ ``` 上面的逻辑代码逻辑主要就是从LLVMContext中获取常见变量类型和根据函数名获取SanitizerCoverage的内部函数以初始化,然后就遍历Module中的所有Function,开始插桩. ```c++ for (auto &F : M) instrumentFunction(F, DTCallback, PDTCallback); ``` ```c++ void ModuleSanitizerCoverage::instrumentFunction( Function &F, DomTreeCallback DTCallback, PostDomTreeCallback PDTCallback) { if (F.empty()) return; if (F.getName().find(".module_ctor") != std::string::npos) return; // Should not instrument sanitizer init functions. if (F.getName().startswith("__sanitizer_")) return; // Don't instrument __sanitizer_* callbacks. // 省略很多不插桩的逻辑 SmallVector IndirCalls; SmallVector BlocksToInstrument; SmallVector CmpTraceTargets; SmallVector SwitchTraceTargets; SmallVector DivTraceTargets; SmallVector GepTraceTargets; // 这些变量分别用于不同参数的插桩方法 // -fsanitize-coverage=trace-pc-guard,indirect-calls,trace-cmp,trace-div,trace-gep for (auto &BB : F) { // 遍历当前函数所有BasicBlock代码块 if (shouldInstrumentBlock(F, &BB, DT, PDT, Options)) BlocksToInstrument.push_back(&BB); // 记录所有可以进行插桩的BasicBlock for (auto &Inst : BB) { // 遍历BasicBlock中所有指令 if (Options.IndirectCalls) { // 如果启用参数-fsanitize-coverage=indirect-calls CallBase *CB = dyn_cast(&Inst); if (CB && !CB->getCalledFunction()) // 如果是Call指令,dyn_case会返回非NULL指针 IndirCalls.push_back(&Inst); // 记录所有Call指令 } if (Options.TraceCmp) { if (ICmpInst *CMP = dyn_cast(&Inst)) if (IsInterestingCmp(CMP, DT, Options)) CmpTraceTargets.push_back(&Inst); if (isa(&Inst)) SwitchTraceTargets.push_back(&Inst); } if (Options.TraceDiv) if (BinaryOperator *BO = dyn_cast(&Inst)) if (BO->getOpcode() == Instruction::SDiv || BO->getOpcode() == Instruction::UDiv) DivTraceTargets.push_back(BO); if (Options.TraceGep) if (GetElementPtrInst *GEP = dyn_cast(&Inst)) GepTraceTargets.push_back(GEP); if (Options.StackDepth) if (isa(Inst) || (isa(Inst) && !isa(Inst))) IsLeafFunc = false; } } // 经过多次遍历之后获取到很多BasicBlock和Inst,然后分别使用不同方法进行插桩 InjectCoverage(F, BlocksToInstrument, IsLeafFunc); InjectCoverageForIndirectCalls(F, IndirCalls); InjectTraceForCmp(F, CmpTraceTargets); InjectTraceForSwitch(F, SwitchTraceTargets); InjectTraceForDiv(F, DivTraceTargets); InjectTraceForGep(F, GepTraceTargets); } ``` 由于文章篇幅关系,在此就只介绍`InjectCoverage()`的插桩逻辑,简单地说,接下来`InjectCoverage()`会直接根据前面的筛选出来的BlocksToInstrument的入口处插入对`__sanitizer_cov_trace_pc_guard()`函数调用. ```c++ bool ModuleSanitizerCoverage::InjectCoverage(Function &F, ArrayRef AllBlocks,bool IsLeafFunc) { if (AllBlocks.empty()) return false; CreateFunctionLocalArrays(F, AllBlocks); // 这里就是创建SantizerCoverage的分支ID记录内存区域 for (size_t i = 0, N = AllBlocks.size(); i < N; i++) InjectCoverageAtBlock(F, *AllBlocks[i], i, IsLeafFunc); // 遍历所有BasicBlock return true; } void ModuleSanitizerCoverage::CreateFunctionLocalArrays( Function &F, ArrayRef AllBlocks) { if (Options.TracePCGuard) FunctionGuardArray = CreateFunctionLocalArrayInSection( AllBlocks.size(), F, Int32Ty, SanCovGuardsSectionName); // 记住这个变量,这里的意思是根据当前获取到的所有BasicBlock的数量去创建一个整数数组,用于收集TracePCGuard插桩方法的分支ID记录内存区域 // 省略其它代码 } void ModuleSanitizerCoverage::InjectCoverageAtBlock(Function &F, BasicBlock &BB,size_t Idx,bool IsLeafFunc) { BasicBlock::iterator IP = BB.getFirstInsertionPt(); bool IsEntryBB = &BB == &F.getEntryBlock(); DebugLoc EntryLoc; if (IsEntryBB) { if (auto SP = F.getSubprogram()) EntryLoc = DebugLoc::get(SP->getScopeLine(), 0, SP); // Keep static allocas and llvm.localescape calls in the entry block. Even // if we aren't splitting the block, it's nice for allocas to be before // calls. IP = PrepareToSplitEntryBlock(BB, IP); } else { EntryLoc = IP->getDebugLoc(); } IRBuilder<> IRB(&*IP); // 前面一通操作是为了获取BasicBlock的第一条指令 // 省略其它代码 if (Options.TracePCGuard) { auto GuardPtr = IRB.CreateIntToPtr( IRB.CreateAdd(IRB.CreatePointerCast(FunctionGuardArray, IntptrTy), ConstantInt::get(IntptrTy, Idx * 4)), Int32PtrTy); // 创建整数指针引用,等价于FunctionGuardArray[Idx] IRB.CreateCall(SanCovTracePCGuard, GuardPtr)->setCannotMerge(); // 使用前面创建的引用来创建函数调用,等价于__sanitizer_cov_trace_pc_guard(FunctionGuardArray[Idx]); } // 省略其它代码 } ``` 完成所有插桩之后,最后一步就是程序启动时插入对`__sanitizer_cov_trace_pc_guard_init()`函数的调用. ```c++ Function *Ctor = nullptr; if (FunctionGuardArray) Ctor = CreateInitCallsForSections(M, SanCovModuleCtorTracePcGuardName, SanCovTracePCGuardInitName, Int32PtrTy, SanCovGuardsSectionName); ``` 细心的读者可能会想起还有个细节没有提到,那就是LLVM默认的`__sanitizer_cov_trace_pc_guard_init()`函数再哪个地方声明引入的呢?其实这些LLVM内置的函数都在`Compiler-RT`中实现(后面ASAN会用到),代码目录在`\llvm-project\compiler-rt\lib\sanitizer_common`. ```c++ // sanitizer_coverage_fuchsia.cpp SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_pc_guard_init, u32 *start, u32 *end) { // LLVM默认__sanitizer_cov_trace_pc_guard_init()函数实现代码 if (start == end || *start) return; __sancov::pc_guard_controller.InitTracePcGuard(start, end); } void InitTracePcGuard(u32 *start, u32 *end) { // 初始化分支ID内存区域 if (end > start && *start == 0 && common_flags()->coverage) { // Complete the setup before filling in any guards with indices. // This avoids the possibility of code called from Setup reentering // TracePcGuard. u32 idx = Setup(end - start); for (u32 *p = start; p < end; ++p) { *p = idx++; } } } ``` #### 定制SanitizerCoverage 笔者在实现Fuzzer的时候,遇到了个真实的场景.在使用二次开发或者针对某个模块做单元测试时,往往这个模块的代码只占程序全部代码的很小的部分.举个例子,如果模块代码只占全部代码的5%,但是Fuzzer的测试用例可以覆盖这个模块的80%代码,那么最后统计代码覆盖率是使用5%还是80%呢?笔者认为应该是80%的代码覆盖率才是最接近真实的,所以我的思路是:根据执行过的每个函数的总分支数除以每个函数执行过的分支数即可,示例图如下: ![](./pic12/1.png) 最终的结果是 ``` (6 + 2 + 1 + 1) / (10 + 4 + 1 + 2) = 58.82% ``` 现在遇到的难题有两个: * 每个函数的分支总数怎么获取呢? * 插桩只能获取到插桩处的PC地址,怎么样知道我们当前执行到了哪个函数地址? 为了实现这个功能,需要对原有的插桩代码做一些简短的修改,改动如下: ```c++ bool ModuleSanitizerCoverage::instrumentModule() { // ... SanCovTracePCGuard = M.getOrInsertFunction(SanCovTracePCGuardName, VoidTy, Int32PtrTy, Int32PtrTy, Int32PtrTy); // 修改__sanitizer_cov_trace_pc_guard()的调用声明,改成__sanitizer_cov_trace_pc_guard(int,int,int) // ... } bool ModuleSanitizerCoverage::InjectCoverage(Function &F,ArrayRef AllBlocks,bool IsLeafFunc) { if (AllBlocks.empty()) return false; CreateFunctionLocalArrays(F, AllBlocks); for (size_t i = 0, N = AllBlocks.size(); i < N; i++) InjectCoverageAtBlock(F, *AllBlocks[i], i, IsLeafFunc, N); // 遍历出来的BasicBlock总数其实就是当前函数的所有分支 return true; } void ModuleSanitizerCoverage::InjectCoverageAtBlock(Function &F, BasicBlock &BB,size_t Idx,bool IsLeafFunc,size_t EdgeCount) { // 新增参数EdgeCount // ... if (Options.TracePCGuard) { std::vector SanCovTracePCGuardArgumentList; // 创建参数调用列表 auto GuardPtr = IRB.CreateIntToPtr( IRB.CreateAdd(IRB.CreatePointerCast(FunctionGuardArray, IntptrTy), ConstantInt::get(IntptrTy, Idx * 4)), Int32PtrTy); // 从FunctionGuardArray中获取到的分支ID数据 auto FunctionPtr = IRB.CreateIntToPtr(IRB.CreatePointerCast(static_cast(&F), IntptrTy),Int32PtrTy); // 获取当前函数地址,转换为指针传递 Constant* ConstFunctionInsideEdgeCount = ConstantInt::get(IntptrTy, EdgeCount); // 获取当前函数分支总数,作为int值传递 SanCovTracePCGuardArgumentList.push_back(GuardPtr); SanCovTracePCGuardArgumentList.push_back(ConstFunctionInsideEdgeCount); SanCovTracePCGuardArgumentList.push_back(FunctionPtr); IRB.CreateCall(SanCovTracePCGuard, static_cast>(SanCovTracePCGuardArgumentList))->setCannotMerge(); } // ... } ``` ## libFuzzer原理 用过libFuzzer和AFL的读者们应该知道,这两款Fuzzer工具核心原理是:它们都会使用数据样本来生成测试数据集,然后使用新生成的测试数据调用程序执行并根据程序插桩的逻辑捕获到执行的分支信息,以此判断新的测试数据有没有发现新的执行路径,如果有发现新执行路径则记录测试数据,后续继续使用基于测试数据变异,不断以此循环. 关于libFuzzer最经典的教程在这里(https://github.com/Dor1s/libfuzzer-workshop),本文着重介绍libFuzzer工具本身的原理,不再复述libFuzzer的用法. 关于libFuzzer的实现代码,在LLVM的`\llvm-project\compiler-rt\lib\fuzzer\`目录下. #### libFuzzer执行Fuzzer过程 本章使用libFuzzer-workshop教程的OpenSSL心脏滴血漏洞来做讲解,相关代码在这里(https://github.com/Dor1s/libfuzzer-workshop/blob/master/lessons/05/openssl_fuzzer.cc),其中核心Fuzzing代码如下: ```c++ extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { static SSL_CTX *sctx = Init(); SSL *server = SSL_new(sctx); BIO *sinbio = BIO_new(BIO_s_mem()); BIO *soutbio = BIO_new(BIO_s_mem()); SSL_set_bio(server, sinbio, soutbio); SSL_set_accept_state(server); BIO_write(sinbio, data, size); SSL_do_handshake(server); SSL_free(server); return 0; } ``` libFuzzer以`LLVMFuzzerTestOneInput()`作为用户自定义的模糊测试入口点,用户只需要关注为libFuzzer生成的数据编写接口调用逻辑,而libFuzzer本身只需要做好数据生成即可.libFuzzer的数据生成主要由三部分构成: 1. 用户指定的初始数据 2. 数据变异 3. 新路径发现 libFuzzer工作过程也可以简单地归纳为: 1. 初始化 2. 生成数据 3. 开始测试 4. 收集代码覆盖率信息 5. 生成数据 6. 开始测试 7. ...以此类推 对以上的执行过程有了印象之后,那么我们就开始对libFuzzer的源码进行探索.FuzzerDriver.cpp文件的FuzzerDriver()函数是libFuzzer的入口点. ```c++ int FuzzerDriver(int *argc, char ***argv, UserCallback Callback) { // 省略代码 if (EF->LLVMFuzzerInitialize) // 如果用户有自定义LLVMFuzzerInitialize()实现,那么就执行该函数,提供这个函数的作为用户自定义实现接口是因为要对库/程序进行初始化 EF->LLVMFuzzerInitialize(argc, argv); // 省略程序解析外部参数代码 unsigned Seed = Flags.seed; // 如果外部有传递随机数种子的话.参数为-seed=? if (Seed == 0) Seed = std::chrono::system_clock::now().time_since_epoch().count() + GetPid(); // 外部没有指定随机数种子,那就使用时间戳+pid if (Flags.verbosity) // 调试输出,参数为-verbosity Printf("INFO: Seed: %u\n", Seed); Random Rand(Seed); // 随机数生成器 auto *MD = new MutationDispatcher(Rand, Options); // 数据变异生成器 auto *Corpus = new InputCorpus(Options.OutputCorpus); // 数据收集器 auto *F = new Fuzzer(Callback, *Corpus, *MD, Options); // Fuzzer核心逻辑模块 StartRssThread(F, Flags.rss_limit_mb); // 创建内存检测线程,如果当前进程的内存占用超过阈值之后就退出Fuzzer报告异常 Options.HandleAbrt = Flags.handle_abrt; Options.HandleBus = Flags.handle_bus; Options.HandleFpe = Flags.handle_fpe; Options.HandleIll = Flags.handle_ill; Options.HandleInt = Flags.handle_int; Options.HandleSegv = Flags.handle_segv; Options.HandleTerm = Flags.handle_term; Options.HandleXfsz = Flags.handle_xfsz; SetSignalHandler(Options); // 初始化信号捕获回调函数 // 省略代码 F->Loop(); // 开始Fuzzing exit(0); } ``` 通过分析libFuzzer的启动过程我们可知,它整个框架的核心由: * 数据变异生成器 * 数据收集器 * Fuzzer核心逻辑模块 组成.接下来我们应该梳理清楚这三个模块之间的关系,接着前面的分析,我们继续阅读`Fuzzer::Loop()`的代码. ```c++ void Fuzzer::Loop() { // 省略代码 while (true) { // 省略代码 if (TimedOut()) break; // 由参数-max_total_time指定的运行时间控制,超时执行就退出 // Perform several mutations and runs. MutateAndTestOne(); // 执行一次Fuzzing } // 省略代码 } void Fuzzer::MutateAndTestOne() { auto &II = Corpus.ChooseUnitToMutate(MD.GetRand()); // 从数据收集器中随机挑一个测试数据出来,要结合下面的核心逻辑代码才能理解它的用意 const auto &U = II.U; size_t Size = U.size(); memcpy(CurrentUnitData, U.data(), Size); // 获取测试数据 // 省略代码 for (int i = 0; i < Options.MutateDepth; i++) { // 对数据变异多次.由参数-mutate_depth控制,默认值是5 size_t NewSize = 0; NewSize = MD.Mutate(CurrentUnitData, Size, CurrentMaxMutationLen); // 使用前面随机抽取获取到的测试数据作为变异输入生成测试数据 Size = NewSize; if (i == 0) // 注意,第一次Fuzzing时,会启用数据追踪功能,简而言之就是hook strstr(),strcasestr(),memmem()函数,然后从参数中获取到一些有意思的字符串 StartTraceRecording(); II.NumExecutedMutations++; if (size_t NumFeatures = RunOne(CurrentUnitData, Size)) { // 开始Fuzzing,如果使用前面生成的变异数据拿去Fuzzing,发现了新的路径数量,就会保存到NumFeatures,没有发现新路径则NumFeatures=0. Corpus.AddToCorpus({CurrentUnitData, CurrentUnitData + Size}, NumFeatures, /*MayDeleteFile=*/true); // 注意,这一段代码是libFuzzer的核心逻辑之一,如果变异数据发现新路径,那就记录该数据到数据收集器.这是libFuzzer路径探测的核心原理. ReportNewCoverage(&II, {CurrentUnitData, CurrentUnitData + Size}); CheckExitOnSrcPosOrItem(); } StopTraceRecording(); TryDetectingAMemoryLeak(CurrentUnitData, Size, /*DuringInitialCorpusExecution*/ false); } } size_t Fuzzer::RunOne(const uint8_t *Data, size_t Size) { ExecuteCallback(Data, Size); // 往下就是调用到LLVMFuzzerTestOneInput() TPC.UpdateCodeIntensityRecord(TPC.GetCodeIntensity()); // 获取当前执行过的代码分支总数 size_t NumUpdatesBefore = Corpus.NumFeatureUpdates(); TPC.CollectFeatures([&](size_t Feature) { Corpus.AddFeature(Feature, Size, Options.Shrink); }); size_t NumUpdatesAfter = Corpus.NumFeatureUpdates(); // 从SanitizerCoverage插桩记录的信息中获取分支数据 // 省略代码 return NumUpdatesAfter - NumUpdatesBefore; // 计算发现了多少新分支路径 } ``` 明白libFuzzer的主要Fuzzing原理后,我们现在探讨下代码覆盖率的实现细节.首先,libFuzzer TracePC(TPC)类是专门用于收集使用SanitizerCoverage插桩获取到的信息,代码实现在FuzzerTracePC.cpp文件下. ```c++ ATTRIBUTE_INTERFACE void __sanitizer_cov_trace_pc_guard_init(uint32_t *Start, uint32_t *Stop) { fuzzer::TPC.HandleInit(Start, Stop); } ATTRIBUTE_INTERFACE ATTRIBUTE_NO_SANITIZE_ALL void __sanitizer_cov_trace_pc_guard(uint32_t *Guard) { uintptr_t PC = reinterpret_cast(__builtin_return_address(0)); uint32_t Idx = *Guard; getStackDepth(); fuzzer::codeIntensity++; __sancov_trace_pc_pcs[Idx] = PC; __sancov_trace_pc_guard_8bit_counters[Idx]++; } ``` 理解这个细节后,再回来看核心逻辑`Fuzzer::ExecuteCallback()`. ```c++ void Fuzzer::ExecuteCallback(const uint8_t *Data, size_t Size) { // 省略代码 uint8_t *DataCopy = new uint8_t[Size]; memcpy(DataCopy, Data, Size); // 从变异的数据中复制一份到这个内存,后面会用到 // 省略代码 TPC.ResetMaps(); // 清空所有路径信息 RunningCB = true; int Res = CB(DataCopy, Size); // 执行用户自定义的LLVMFuzzerTestOneInput() RunningCB = false; // 省略代码 if (!LooseMemeq(DataCopy, Data, Size)) // 注意这个坑,如果传递给LLVMFuzzerTestOneInput()的data会被程序修改,那么libFuzzer会强制退出 CrashOnOverwrittenData(); delete[] DataCopy; } ``` #### 数据生成原理 对整个Fuzzing过程清晰后,我们回来探索libFuzzer的数据生成原理,对应数据变异模块`MutationDispatcher`. ```c++ MutationDispatcher::MutationDispatcher(Random &Rand,const FuzzingOptions &Options) : Rand(Rand), Options(Options) { // Rand是随机数生成器 DefaultMutators.insert( DefaultMutators.begin(), // 添加数据变异算法 { {&MutationDispatcher::Mutate_EraseBytes, "EraseBytes"}, {&MutationDispatcher::Mutate_InsertByte, "InsertByte"}, {&MutationDispatcher::Mutate_InsertRepeatedBytes, "InsertRepeatedBytes"}, {&MutationDispatcher::Mutate_ChangeByte, "ChangeByte"}, {&MutationDispatcher::Mutate_ChangeBit, "ChangeBit"}, {&MutationDispatcher::Mutate_ShuffleBytes, "ShuffleBytes"}, {&MutationDispatcher::Mutate_ChangeASCIIInteger, "ChangeASCIIInt"}, {&MutationDispatcher::Mutate_ChangeBinaryInteger, "ChangeBinInt"}, {&MutationDispatcher::Mutate_CopyPart, "CopyPart"}, {&MutationDispatcher::Mutate_CrossOver, "CrossOver"}, {&MutationDispatcher::Mutate_AddWordFromManualDictionary, "ManualDict"}, {&MutationDispatcher::Mutate_AddWordFromTemporaryAutoDictionary, "TempAutoDict"}, {&MutationDispatcher::Mutate_AddWordFromPersistentAutoDictionary, "PersAutoDict"}, }); if(Options.UseCmp) DefaultMutators.push_back( {&MutationDispatcher::Mutate_AddWordFromTORC, "CMP"}); if (EF->LLVMFuzzerCustomMutator) // 如果存在用户自定义的数据变异方法,那就使用它 Mutators.push_back({&MutationDispatcher::Mutate_Custom, "Custom"}); else Mutators = DefaultMutators; if (EF->LLVMFuzzerCustomCrossOver) Mutators.push_back( {&MutationDispatcher::Mutate_CustomCrossOver, "CustomCrossOver"}); } ``` 关于数据变异的算法读者们自行阅读,这些变异方法基本上都差不多.笔者画图整理全部的逻辑,读者们就能对此一目了然. ![](./pic12/libFuzzer-Mutate.png) #### 路径探测原理 前面有简略地提到这点,简单总结整体流程如下: ![](./pic12/libFuzzer-PathSearch.png) 本章最后,把libFuzzer数据变异和路径探测结合在一起的完整过程如下所示. ![](./pic12/libFuzzer-Arch.png) ## 深入解析libFuzzer参数与回显 本小节着重于对实用情景下对libFuzzer的用法和坑(参数,回显,bug等)做深入的分析,为什么要将它放到最后来解释呢?笔者在实际工作中遇到了一些难以处理问题,都是依靠前面对libFuzzer源码的浅薄理解而解决的. #### 编译时使用 libFuzzer.a和-fsanitize=fuzzer有区别嘛? 回顾libfuzzer-workshop的例子,示例的第一步要求我们先对libFuzzer的源码进行编译,生成libFuzzer.a静态库,然后再自行编写Fuzz逻辑入口,把Fuzzer,库源码,libFuzzer.a同时链接,生成可执行Fuzzer.实际上clang中已经内置了libFuzzer,我们使用-fsanitize=fuzzer也可以引入它.举个例子: ```c #include int LLVMFuzzerTestOneInput(const char* Data,unsigned int Size) { if (Size > 4) { if (Data[1] == 'F' && Data[3] == 'A') { printf("bingo \n"); exit(0); } } return 0; } ``` 命令行下执行结果: ```sh ubuntu@ubuntu-virtual-machine:~/Desktop/temp$ clang -fsanitize=fuzzer 1.c -o 1_fuzzer && ./1_fuzzer INFO: Seed: 3655122303 INFO: Loaded 1 modules (5 inline 8-bit counters): 5 [0x4e8080, 0x4e8085), INFO: Loaded 1 PC tables (5 PCs): 5 [0x4bee00,0x4bee50), INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes INFO: A corpus is not provided, starting from an empty corpus #2 INITED cov: 2 ft: 2 corp: 1/1b exec/s: 0 rss: 28Mb #219 NEW cov: 3 ft: 3 corp: 2/7b lim: 6 exec/s: 0 rss: 28Mb L: 6/6 MS: 2 CopyPart-CrossOver- #245 REDUCE cov: 3 ft: 3 corp: 2/6b lim: 6 exec/s: 0 rss: 28Mb L: 5/5 MS: 1 EraseBytes- #4770 REDUCE cov: 4 ft: 4 corp: 3/12b lim: 48 exec/s: 0 rss: 28Mb L: 6/6 MS: 5 CrossOver-ShuffleBytes-EraseBytes-ChangeBinInt-ShuffleBytes- #4773 REDUCE cov: 4 ft: 4 corp: 3/11b lim: 48 exec/s: 0 rss: 28Mb L: 5/5 MS: 3 CopyPart-ShuffleBytes-EraseBytes- bingo ==822227== ERROR: libFuzzer: fuzz target exited #0 0x4adb40 in __sanitizer_print_stack_trace (/home/ubuntu/Desktop/temp/1_fuzzer+0x4adb40) #1 0x459498 in fuzzer::PrintStackTrace() (/home/ubuntu/Desktop/temp/1_fuzzer+0x459498) #2 0x43f58c in fuzzer::Fuzzer::ExitCallback() (/home/ubuntu/Desktop/temp/1_fuzzer+0x43f58c) #3 0x7f064ebb9a56 in __run_exit_handlers stdlib/exit.c:108:8 #4 0x7f064ebb9bff in exit stdlib/exit.c:139:3 #5 0x4adf32 in LLVMFuzzerTestOneInput (/home/ubuntu/Desktop/temp/1_fuzzer+0x4adf32) #6 0x440a31 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/home/ubuntu/Desktop/temp/1_fuzzer+0x440a31) #7 0x440175 in fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned long, bool, fuzzer::InputInfo*, bool*) (/home/ubuntu/Desktop/temp/1_fuzzer+0x440175) #8 0x441ba0 in fuzzer::Fuzzer::MutateAndTestOne() (/home/ubuntu/Desktop/temp/1_fuzzer+0x441ba0) #9 0x442615 in fuzzer::Fuzzer::Loop(std::__Fuzzer::vector >&) (/home/ubuntu/Desktop/temp/1_fuzzer+0x442615) #10 0x432025 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/home/ubuntu/Desktop/temp/1_fuzzer+0x432025) #11 0x459c72 in main (/home/ubuntu/Desktop/temp/1_fuzzer+0x459c72) #12 0x7f064eb9dcb1 in __libc_start_main csu/../csu/libc-start.c:314:16 #13 0x40684d in _start (/home/ubuntu/Desktop/temp/1_fuzzer+0x40684d) SUMMARY: libFuzzer: fuzz target exited MS: 1 InsertByte-; base unit: 0c7d9271cf3d2a4e2c3eec3e76a2d1dc1431af36 0xa,0x46,0xf6,0x41,0xa,0xa, \x0aF\xf6A\x0a\x0a artifact_prefix='./'; Test unit written to ./crash-8860dc7909080bcb9ca9827f67704611bbdf02b9 Base64: Ckb2QQoK ubuntu@ubuntu-virtual-machine:~/Desktop/temp$ ``` 这看起来和直接引入libFuzzer.a的效果一样,那么接下来我们再引入**-fsanitize-coverage=trace-pc-guard**重新编译运行.结果如下: ```sh ubuntu@ubuntu-virtual-machine:~/Desktop/temp$ clang -v Ubuntu clang version 11.0.0-2 Target: x86_64-pc-linux-gnu ubuntu@ubuntu-virtual-machine:~/Desktop/temp$ clang -fsanitize=fuzzer -fsanitize-coverage=trace-pc-guard 1.c -o 1_fuzzer && ./1_fuzzer -fsanitize-coverage=trace-pc-guard is no longer supported by libFuzzer. Please either migrate to a compiler that supports -fsanitize=fuzzer or use an older version of libFuzzer ubuntu@ubuntu-virtual-machine:~/Desktop/temp$ ``` 这是因为高版本的clang不支持trace-pc-guard和trace-pc了.对此有两个解决方法: * 使用-fsanitize-coverage=trace-gep,trace-div,trace-cmp替代trace-pc-guard.(适用于Windows平台) * 使用低版本的libFuzzer编译出静态库然后导入链接.因为不支持trace-pc-guard的逻辑是在libFuzzer中写死的(参考FuzzerTracePC.cpp __sanitizer_cov_trace_pc_guard()函数),即使换成高版本libFuzzer的静态库也是一样的提示. #### 为什么libFuzzer要删除对trace-pc的支持? libFuzzer开发者kcc在2019年1月的Commit中删除了libFuzzer对trace-pc的支持,相关diff如下: * https://github.com/llvm/llvm-project/commit/62d727061053dac28447a900fce064c54d366bd6# * https://github.com/llvm/llvm-project/commit/62d727061053dac28447a900fce064c54d366bd6# 笔者找遍了文档和提交记录,对于为什么要删除trace-pc的支持找不到任何相关信息,于是只能通过阅读源码和效果对比测试来理解和推测.相关结论如下: * 删除trace-pc是因为trace-pc的代码覆盖率统计方法可以被替代. * trace-pc随后被inline-8bit-counter(统计BasicBlock执行次数)和trace-cmp(在分支之前插桩)替代,因为trace-cmp可以主动发现逻辑判断中对比的数值,部分场景下能够增强主动模糊测试效果. 我们先对比一下改变前后的libFuzzer编译结果.旧版本的libFuzzer使用trace-pc插桩之后的代码逻辑如下,`生成的Data让逻辑执行到某个特定的BasicBlock时才记录代码覆盖`,这样模糊测试工具相对*被动*. ![](./pic12/old_libfuzzer_santizer_coverage.png) 新版本的libFuzzer默认使用trace-cmp插桩之后,会在判断逻辑前面插桩并收集判断逻辑的数据(比如下面的反编译就是收集判断`if(Data[0] = '1')`的字符1),然后回馈到语料库(fuzzer::TracePC::TableOfRecentCompares).有了这些判断中的数据,生成模糊测试的数据就能相对有个方向,更为*主动*.其中__santizer_cov_trace_const_cmp4是由trace-cmp插桩的逻辑,++byte_4EB071是由inline-8bit-Counter插桩的逻辑. ![](./pic12/new_libfuzzer_santizer_coverage.png) 两种插桩模式的模糊测试效果对比如下: ![](./pic12/new_libfuzzer_effect.png) 附加参考链接:https://reviews.llvm.org/rC352818 #### Windows平台下怎么引用libFuzzer? Windows平台下使用libFuzzer建议还是使用LLVM官网的Windows编译套件,因为使用Visutal Studio Installer下载的LLVM版本只支持32位编译(只有32位的静态库),LLVM官网的Windows编译套件32/64位都支持. Visual Studio项目需要修改编译工具集为LLVM-clang和正确平台SDK的即可.有几点需要注意: * clang的编译语法和MSVC不一样,有一些不应该提示的错误可以使用-Wno-xxx关闭警告. * clang甚至不支持一些MSVC内置函数(比如__cpuid等),可以尝试引入intrin.h解决. * 有一些MSVC或者WinAPI符号无法被clang识别,这是因为C++重载问题导致clang找不到符号.比如InternalLockAdd(LONG)和InternalLockAdd(ULONG),clang会认为是两个不一样的函数,但是WinAPI只有一个.所以建议直接对函数参数传参做强制转换,.对齐标准WinAPI声明. * 链接时需要手工引入.lib库,还记得前面的compiler-RT库嘛,插桩逻辑和Fuzzer调度逻辑都在这里,clang默认不会加载. #### libFuzzer怎么样提高模糊测试效果? `-dict`参数指定一个语料库,后续ManualDict这些数据变异模块就可以从这里拿到**和当前被测试的逻辑强相关的关键词**.举个例子,我们对SQL注入做测试,这些关键词是不是就包含了:union select,from,count()等;对文件解析测试,是不是就需要包含7zip,PE,MZ,Rar!等关键词呢.我们传递的这些关键词,最终会被拼接到LLVMFuzzerTestOneInput()的data参数中. 实际上,libFuzzer也能够像AFL一样接受一批样本数据作为初始化输入来做模糊测试.这样的话我们就可以根据模糊测试的对象的业务去github和各个项目的测试用例中搜罗样本数据了. 上面两个参数是可以结合使用的,不带参数和带参数的对路径探测的结果影响如下: ```sh ubuntu@ubuntu-virtual-machine:~/Desktop/fuzz$ ./test_case INFO: Seed: 1117474860 INFO: Loaded 1 modules (10682 guards): [0x110c9b8, 0x11170a0), INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes INFO: A corpus is not provided, starting from an empty corpus #0 READ units: 1 #1 INITED cov: 869 ci: 0K ft: 180 corp: 1/1b exec/s: 0 rss: 135Mb #2 NEW cov: 869 ci: 0K ft: 208 corp: 2/2b exec/s: 0 rss: 135Mb L: 1 MS: 1 ShuffleBytes- #3 NEW cov: 885 ci: 0K ft: 229 corp: 3/4b exec/s: 0 rss: 135Mb L: 2 MS: 2 ShuffleBytes-CrossOver- #5 NEW cov: 886 ci: 0K ft: 239 corp: 4/5b exec/s: 0 rss: 135Mb L: 1 MS: 4 ShuffleBytes-CrossOver-EraseBytes-ChangeBit- #8 NEW cov: 886 ci: 0K ft: 248 corp: 5/8b exec/s: 0 rss: 135Mb L: 3 MS: 2 ChangeBit-CrossOver- #9 NEW cov: 887 ci: 0K ft: 249 corp: 6/12b exec/s: 0 rss: 135Mb L: 4 MS: 3 ChangeBit-CrossOver-InsertByte- #12 NEW cov: 898 ci: 0K ft: 264 corp: 7/89b exec/s: 0 rss: 145Mb L: 77 MS: 1 InsertRepeatedBytes- #13 NEW cov: 898 ci: 1K ft: 267 corp: 8/210b exec/s: 0 rss: 145Mb L: 121 MS: 2 InsertRepeatedBytes-CopyPart- #14 NEW cov: 898 ci: 2K ft: 269 corp: 9/401b exec/s: 0 rss: 145Mb L: 191 MS: 3 InsertRepeatedBytes-CopyPart-CopyPart- #15 NEW cov: 898 ci: 2K ft: 270 corp: 10/593b exec/s: 0 rss: 145Mb L: 192 MS: 4 InsertRepeatedBytes-CopyPart-CopyPart-InsertByte- #21 NEW cov: 899 ci: 41K ft: 271 corp: 11/4689b exec/s: 0 rss: 145Mb L: 4096 MS: 5 ChangeBit-EraseBytes-EraseBytes-ChangeBit-CrossOver- #25 NEW cov: 899 ci: 41K ft: 272 corp: 12/5513b exec/s: 0 rss: 145Mb L: 824 MS: 4 ChangeByte-CMP-CrossOver-CrossOver- DE: "\xef\x0f"- #43 NEW cov: 899 ci: 41K ft: 280 corp: 13/5516b exec/s: 0 rss: 145Mb L: 3 MS: 2 ChangeBinInt-CopyPart- #90 NEW cov: 899 ci: 41K ft: 281 corp: 14/5746b exec/s: 0 rss: 145Mb L: 230 MS: 4 ChangeByte-PersAutoDict-CopyPart-InsertRepeatedBytes- DE: "\xef\x0f"- #106 NEW cov: 899 ci: 41K ft: 283 corp: 15/6032b exec/s: 0 rss: 145Mb L: 286 MS: 5 EraseBytes-ChangeBit-ShuffleBytes-ChangeBit-InsertRepeatedBytes- #117 NEW cov: 899 ci: 41K ft: 284 corp: 16/6071b exec/s: 0 rss: 145Mb L: 39 MS: 1 EraseBytes- #124 NEW cov: 899 ci: 41K ft: 286 corp: 17/6076b exec/s: 0 rss: 145Mb L: 5 MS: 3 ChangeByte-ShuffleBytes-PersAutoDict- DE: "\xef\x0f"- #163 NEW cov: 899 ci: 41K ft: 288 corp: 18/6093b exec/s: 0 rss: 145Mb L: 17 MS: 2 CMP-CMP- DE: "\x00\x00\x00\x00\x00\x00\x00\x00"-"\xff\xff\xff\xff\xff\xff\x0b["- #164 NEW cov: 899 ci: 41K ft: 290 corp: 19/6106b exec/s: 0 rss: 145Mb L: 13 MS: 3 CMP-CMP-EraseBytes- DE: "\x00\x00\x00\x00\x00\x00\x00\x00"-"\xff\xff\xff\xff\xff\xff\x0b["- #169 NEW cov: 899 ci: 41K ft: 292 corp: 20/6118b exec/s: 0 rss: 145Mb L: 12 MS: 3 ChangeByte-ChangeBinInt-CMP- DE: "objective"- #182 NEW cov: 899 ci: 41K ft: 294 corp: 21/6129b exec/s: 0 rss: 145Mb L: 11 MS: 1 CMP- DE: "\xff\xff\xff\xff\xff\xff\xff\xff"- #529 NEW cov: 899 ci: 41K ft: 295 corp: 22/6131b exec/s: 0 rss: 146Mb L: 2 MS: 3 ChangeBit-InsertByte-ChangeBit- #634 NEW cov: 899 ci: 41K ft: 296 corp: 23/10227b exec/s: 0 rss: 146Mb L: 4096 MS: 3 CopyPart-CrossOver-ChangeBit- #702 NEW cov: 899 ci: 41K ft: 298 corp: 24/10418b exec/s: 702 rss: 146Mb L: 191 MS: 1 ChangeBinInt- #1041 NEW cov: 899 ci: 41K ft: 299 corp: 25/10424b exec/s: 1041 rss: 146Mb L: 6 MS: 5 ShuffleBytes-CMP-CrossOver-EraseBytes-EraseBytes- DE: "\x00\x00\x00Z"- ``` ```sh ubuntu@ubuntu-virtual-machine:~/Desktop/fuzz$ ./test_case -dict=./libfuzzer_keywork.txt sample/ Dictionary: 375 entries INFO: Seed: 1048768538 INFO: Loaded 1 modules (10682 guards): [0x110c9b8, 0x11170a0), Loading corpus dir: sample/ Loaded 1024/2640 files from sample/ Loaded 2048/2640 files from sample/ INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 1048576 bytes #0 READ units: 2638 #1024 pulse cov: 1805 ci: 609K ft: 1585 corp: 73/15289b exec/s: 512 rss: 748Mb #2048 pulse cov: 3774 ci: 5983K ft: 6522 corp: 259/3660Kb exec/s: 341 rss: 785Mb #2638 INITED cov: 4286 ci: 18481K ft: 9928 corp: 490/79Mb exec/s: 131 rss: 997Mb #2639 NEW cov: 4286 ci: 18481K ft: 9929 corp: 491/79Mb exec/s: 131 rss: 997Mb L: 94081 MS: 1 CMP- DE: "N4LIEF17read_out_"- #2665 NEW cov: 4325 ci: 18481K ft: 9988 corp: 492/79Mb exec/s: 133 rss: 997Mb L: 165541 MS: 2 CMP-PersAutoDict- DE: "\x0a\x00"-"N4LIEF17read_out_"- #2713 NEW cov: 4325 ci: 18481K ft: 9999 corp: 493/79Mb exec/s: 129 rss: 997Mb L: 103145 MS: 5 CMP-InsertRepeatedBytes-EraseBytes-ManualDict-ChangeByte- DE: "\x01\x00\x00\x00\x00\x01\x89\xca"-"PowerPoint"- #2804 NEW cov: 4326 ci: 18481K ft: 10006 corp: 494/79Mb exec/s: 127 rss: 997Mb L: 94081 MS: 1 ChangeBit- #2960 NEW cov: 4326 ci: 18481K ft: 10007 corp: 495/79Mb exec/s: 118 rss: 997Mb L: 67143 MS: 2 PersAutoDict-CMP- DE: "\x01\x00\x00\x00\x00\x01\x89\xca"-"\x00\x00\x00\x00\x00\x00\x13}"- #3011 NEW cov: 4326 ci: 18481K ft: 10008 corp: 496/79Mb exec/s: 120 rss: 997Mb L: 94205 MS: 3 ChangeASCIIInt-InsertRepeatedBytes-ChangeBinInt- #3014 NEW cov: 4327 ci: 18481K ft: 10009 corp: 497/79Mb exec/s: 120 rss: 997Mb L: 67143 MS: 1 CopyPart- #3102 NEW cov: 4327 ci: 18481K ft: 10013 corp: 498/79Mb exec/s: 114 rss: 997Mb L: 81784 MS: 4 ManualDict-ShuffleBytes-ChangeBit-CopyPart- DE: "Jet\x00"- #3106 NEW cov: 4327 ci: 18481K ft: 10027 corp: 499/79Mb exec/s: 115 rss: 997Mb L: 72629 MS: 3 CopyPart-ChangeByte-CMP- DE: "\x0b\x00"- #3109 NEW cov: 4327 ci: 18481K ft: 10030 corp: 500/79Mb exec/s: 115 rss: 997Mb L: 65197 MS: 1 CMP- DE: "\x1f\x00\x00\x00\x00\x00\x00\x00"- #3522 NEW cov: 4328 ci: 18481K ft: 10031 corp: 501/80Mb exec/s: 103 rss: 997Mb L: 39524 MS: 4 InsertByte-ChangeBit-EraseBytes-InsertByte- ``` 其中cov值则是执行分支总数,可以看到两次Fuzzer运行之间的巨大差异(第一次执行只覆盖899个分支,第二次执行覆盖4328个分支).这里-worker是指同时使用多个进程来执行Fuzzer(源码实现在FuzzerDriver.cpp WorkerThread()). #### 多个libFuzzer同时启动时,怎么样区分不同的libFuzzer生成的Crash? libFuzzer产生崩溃时,会记录当前的崩溃样本到本地,但是这些崩溃样本都是以crash slow-unit oom作为前缀.在一些模糊测试场景中,我们会在当前目录下执行多个libFuzzer程序,那就会导致多个libFuzzer产生的崩溃样本无法辨识,此时引入-artifact_prefix参数为崩溃样本自定义前缀即可. ![](./pic12/3.png) #### 依赖库没有源码时有没有必要使用libFuzzer? 通过前面的分析,我们知道libFuzzer想要实现遗传算法进行模糊测试,那就需要依赖代码覆盖率Sanitize-Coverage进行插桩.但是有时候要进行模糊测试的程序要依赖到动态库和静态库,此时我们就无法对此进行插桩了.笔者在工作中遇到这种情况,处理思路如下: * **依赖库中的源码和被模糊测试的源码之间有很大的关联性**.举个例子,项目中依赖7zip的解压库,然后项目对此进行一层性能优化封装,那么要测试的代码也就必须要包含项目中的封装层代码和7zip依赖库源码,因为缺少了其中任意一部分的代码,就会很难覆盖全整个功能所需要的代码,会导致有些逻辑没法被覆盖到. * **依赖库中的源码和被模糊测试的源码之间关联系不大**.比如项目中引用了protobuf进行数据解析,然后具体的处理逻辑则是由项目处理,那么依赖库protobuf就不应该包含在模糊测试范围中. #### 如何并行执行多个libFuzzer? libFuzzer默认只执行一个进程来做模糊测试.我们使用-jobs和-workers参数就能让libFuzzer创建多个**进程**并行执行.命令如下: ```sh fuzzing@fuzzing-virtual-machine:~/Desktop/test_code$ ./test_code -jobs=10 ``` ![](./pic12/4.png) 假设读者的电脑配置是4核8G内存的话,那么就会同时有两个进程在执行.如果要同时跑四个进程的话,那就设置为`-workers=4`,workers的值默认是当前CPU核数/2. -jobs参数是指完成了n个libFuzzer进程之后就退出程序,默认值为0.如果我们要同时执行8个进程并行执行libFuzzer的命令如下: ```sh fuzzing@fuzzing-virtual-machine:~/Desktop/test_code$ ./test_code -jobs=8 -workers=8 ``` 需要特别注意的是,如果读者们要并行执行libFuzzer,jobs和workers参数的传值缺一不可.因为libFuzzer代码中的逻辑就是workers和jobs必须要大于0才可以执行并行多进程,所以这个隐藏的坑就是为什么只设置了`-workers=8`但是libFuzzer没有执行并行的原因,因为此时jobs值为默认为0. 还有一点值得提示一下,libFuzzer所做的模糊测试,实际上并没有尝试去维护*干净的上下文*然后重新模糊测试.笔者举个Qemu Fuzzer的例子来说明一下. 我们在对Qemu进行模糊测试时,会生成大量的MMIO和Port IO的方式来进行设备通信.那么大量的测试数据会导致Qemu设备的*状态不断发生改变,而不是从一个初始的状态开始执行*.比如说解析PE文件的接口,我们实例化类之后把文件内容传递到接口去测试,那么这个类都是从初始的状态去执行数据解析然后改变状态.但是对于Qemu虚拟机这样复杂的系统,它需要维护很多上下文相关的对象,所以每次模糊测试和设备交互时,都会对设备的状态进行改变,导致无法从*初始的状态*开始测试,影响最终复现漏洞的结果.为了解决这个问题,Qemu 5.2.0中支持的模糊测试方法是在libFuzzer LLVMFuzzerTestOneInput()中为TracePC创建共享内存,然后fork()出子进程执行模糊测试,父进程wait()等待子进程模糊测试结束,然后从共享内存中收集代码覆盖信息,回馈到libFuzzer核心逻辑中去. ![](./pic12/11.png) #### libFuzzer输出哪些信息,怎么样根据这些信息优化Fuzzer? 运行libFuzzer编译的程序,从启动到崩溃输出的信息如下: ![](./pic12/5.png) 第一行输出`INFO: Seed: 1410507973`,意思是当前的随机数种子的初始值.因为每一次执行libFuzzer随机数种子的值都是随机的,但是如果读者们想要复现libFuzzer模糊测试,那就需要使用参数`-seed`指定随机数种子的值. 第二行输出`INFO: Loaded 1 modules (10 inline 8-bit counters): 10 [0x5a6ed0, 0x5a6eda)`,意思是显示当前libFuzzer中使用到的插桩方式(inline 8-bit counters)以及Edge分支信息的内存区域开始和结束信息(Start:0x5a6ed0,End:0x5a6eda,总分支数:10).第三行输出亦是同样意义. 第四行输出`INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes`,意思是当前没有指定参数`-max_len`,默认max_len的值是4096字节(4K).max_len参数的意义在于限制libFuzzer生成的Data内容大小.有时候在对协议处理的功能进行模糊测试,那么max_len相对的取值是小一点的,但是对于文件处理的功能进行模糊测试,那么max_len有可能需要设置为100K,具体场景具体分析. 第五行输出`INFO: A corpus is not provided, starting from an empty corpus`,意思是没有指定输入样本,从空数据生成开始模糊测试. 第六行输出`#2 INITED cov: 2 ft: 2 corp: 1/1b exec/s: 0 rss: 31Mb`,意思是执行完所有初始样本输入数据,代码覆盖(cov字段)2个块(以BasicBlock或Edge为单位),2条执行路径(ft字段);每秒执行次数0(exec/s字段);内存占用为31 MB(rss字段).libFuzzer会对程序执行内存有限制,默认内存上限是2 GB.在模糊测试的过程中因为大量内存分配超出rss,那就会导致libFuzzer崩溃,记录当前样本到OOM-xxx文件,如果读者们需要控制libFuzzer的内存上限值,那就使用`-rss_limit_mb`参数(注意,内存泄露也记录在这个范畴中). 第七行输出`#1949 NEW cov: 3 ft: 3 corp: 2/21b lim: 21 exec/s: 0 rss: 31Mb L: 20/20 MS: 2 InsertByte-InsertRepeatedBytes-`,意思是发现程序执行到新路径,此时代码覆盖3个块,3条执行路径;本次发现新路径时生成模糊测试数据大小为20字节(L字段);数据生成使用了2个数据变异模块串联生成(MS字段);所使用的模块顺序是InsertByte=>InsertRepeatedBytes. libFuzzer输出模糊测试状态时,每行的第二个字段代表的含义如下: * READ ,意思是当前模糊测试阶段是从给定的样本文件夹中读取模糊测试数据来执行. * INITED ,意思是使用所有初始样本执行完LLVMFuzzerTestOneInput之后的状态信息. * NEW ,意思是发现新路径时的状态信息. * REDUCE ,意思是已经执行过的路径发现了更精简的输入. * RELOAD ,意思是从样本文件夹中发现并加载了新样本. * pulse ,没有什么特别的意义,就是定时告诉用户libFuzzer还在运行. * DONE ,libFuzzer执行结束. !!!!! 如何优化再补充一下~~~~ ## ASAN原理 读过libFuzzer-workshop或者有libFuzzer使用经验的读者应该对以下的命令很熟悉 ```sh clang++ -g openssl_fuzzer.cc -O2 -fno-omit-frame-pointer -fsanitize=address \ -fsanitize-coverage=trace-pc-guard,trace-cmp,trace-gep,trace-div \ -Iopenssl1.0.1f/include openssl1.0.1f/libssl.a openssl1.0.1f/libcrypto.a \ ../../libFuzzer/libFuzzer.a -o openssl_fuzzer ``` 我们在引入libFuzzer时,还会引入ASAN(clang命令参数-fsanitize=address).也就是说,我们使用libFuzzer作为Fuzzer驱动,进行接口构造调用/数据生成/路径探测,然后使用ASAN作为内存异常检测工具.下面是使用ASAN的简单例子: ```sh ubuntu@ubuntu-virtual-machine:~/Desktop/instrument_note$ cat ./test_case_2.c #include int main() { char buffer[10] = {0}; printf("Try Crash!\n"); buffer[10] = 'C'; printf("Oops \n"); return 1; } ubuntu@ubuntu-virtual-machine:~/Desktop/instrument_note$ clang ./test_case_2.c -o ./test_case_2 && ./test_case_2 Try Crash! Oops ubuntu@ubuntu-virtual-machine:~/Desktop/instrument_note$ clang -fsanitize=address ./test_case_2.c -o ./test_case_2 && ./test_case_2 Try Crash! ================================================================= ==520651==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffeea008e8a at pc 0x0000004c5098 bp 0x7ffeea008e50 sp 0x7ffeea008e48 WRITE of size 1 at 0x7ffeea008e8a thread T0 #0 0x4c5097 in main (/home/ubuntu/Desktop/instrument_note/test_case_2+0x4c5097) #1 0x7fbfa3d56cb1 in __libc_start_main csu/../csu/libc-start.c:314:16 #2 0x41b2bd in _start (/home/ubuntu/Desktop/instrument_note/test_case_2+0x41b2bd) Address 0x7ffeea008e8a is located in stack of thread T0 at offset 42 in frame #0 0x4c4f5f in main (/home/ubuntu/Desktop/instrument_note/test_case_2+0x4c4f5f) ... ``` 本章着重于探索ASAN的实现原理,关于ASAN的更深入用法建议参考官方文档(https://clang.llvm.org/docs/AddressSanitizer.html ;https://github.com/google/sanitizers/wiki/AddressSanitizer ). #### ASAN异常检测原理 使用前面的演示例子,当buffer越界时,它必然会修改越界后内存的数据(...虽然这句是废话,但还是要提一下).我们用gdb调试没有引入ASAN编译的示例代码: ```text (gdb) n Try Crash! 10 buffer[10] = 'C'; (gdb) info local buffer = "\000\000\000\000\000\000\000\000\000" (gdb) x /16x buffer 0x7fffffffdfb2: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x7fffffffdfba: 0x00 0x00 0x00 0x00 0x00 0x00 0xa0 0x11 (gdb) n 12 printf("Oops \n"); (gdb) x /16x buffer 0x7fffffffdfb2: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x7fffffffdfba: 0x00 0x00 0x43 0x00 0x00 0x00 0xa0 0x11 (gdb) ``` 可以看到这里已经越界写数据成功了.一般地,我们要检测越界读写问题时,就需要专门创建一块内存用来做越界对比.下面的示例代码将引入检测逻辑: ```c #include #include #include #define CHECK_MEMORY_LEFT_SIZE (0x8) #define CHECK_MEMORY_RIGHT_SIZE (0x8) #define CHECK_MEMORY_NORMAL_FLAG (0x00) #define CHECK_MEMORY_EXCEPT_FLAG (0xFF) void* create_check_memory(int buffer_size) { // 创建内存映射,并且给这块映射内存两则边缘 int real_buffer_size = CHECK_MEMORY_LEFT_SIZE + CHECK_MEMORY_RIGHT_SIZE + buffer_size; char* buffer = malloc(real_buffer_size); memset(buffer,CHECK_MEMORY_EXCEPT_FLAG,real_buffer_size); // 填充异常Flag memset(&buffer[CHECK_MEMORY_LEFT_SIZE],CHECK_MEMORY_NORMAL_FLAG,buffer_size); // 内存中标识为正常值则说明这块区域是可以任意操作的 return buffer; } void free_check_memory(void* buffer,int buffer_size) { // 释放内存 memset(buffer,CHECK_MEMORY_FREE_FLAG, CHECK_MEMORY_LEFT_SIZE + CHECK_MEMORY_RIGHT_SIZE + buffer_size); // 不要free()释放,而是填充异常Flag,后续如果遇到UAF类漏洞就可以检测到 } int is_overflow(void* buffer,int offset,int is_write) { // 检测内存异常 unsigned char data = ((unsigned char*)buffer)[CHECK_MEMORY_LEFT_SIZE + offset]; if (CHECK_MEMORY_NORMAL_FLAG != data) { switch (data) { case CHECK_MEMORY_EXCEPT_FLAG: if (is_write) printf(" ==== Write OverFlow !! ====\n"); else printf(" ==== Read OverFlow !! ====\n"); break; case CHECK_MEMORY_FREE_FLAG: printf(" ==== Use After Free !! ====\n"); break; default: printf("Unknow Except\n"); } exit(0); } return 0; } int main() { char buffer[10] = {0}; char* shadow_buffer = create_check_memory(sizeof(buffer)); // 为buffer变量创建检测映射内存 if (is_overflow(shadow_buffer,5,0)) // 向真实内存中写入数据之前先到检测内存中判断是否有异常 exit(0); int data = buffer[5]; // 正常的读操作 printf("Try Crash!\n"); if (is_overflow(shadow_buffer,10,1)) exit(0); buffer[10] = 'C'; // 异常的写操作 printf("Oops \n"); free_check_memory(shadow_buffer,sizeof(buffer)); return 1; } ``` 运行结果如下: ```sh ubuntu@ubuntu-virtual-machine:~/Desktop/instrument_note$ clang -g ./test_case_2.c -o ./test_case_2 && ./test_case_2 Try Crash! ==== Write OverFlow !! ==== ubuntu@ubuntu-virtual-machine:~/Desktop/instrument_note$ ``` 这短短几十行代码就是ASAN异常检测的核心原理,它包含了: * 每个缓冲区中对应的异常检测内存,对应的是ASAN的Shadow Table概念. * 每个异常检测内存中都会插入正常/异常标识,对应的是ASAN的投毒(Poison)概念. * 每次进行真实内存操作之前必须获取异常检测内存的内容,判断该地址是否被投毒过,对应的是ASAN的插桩检测概念. 聪明的读者可能会提出这个疑问:因为在异常检测内存的左边和右边八字节范围的内存被污染过,如果读写的偏移足够大,是不是检测逻辑就失效了呢?很遗憾,确实会存在这样的问题. ```text (gdb) x /8x buffer 0x4052a0: 0xffffffff 0xffffffff 0x00000000 0x00000000 0x4052b0: 0xffff0000 0xffffffff 0x0000ffff 0x00000000 ``` 笔者在现实场景中遇到ASAN也存在这样的问题. ```c #include int main(void) { char buffer[10] = {0}; printf("no crash!\n"); buffer[0x1001] = 0xFF; printf("crash!\n"); buffer[10] = 0xFF; return 0; } ``` ```sh ubuntu@ubuntu-virtual-machine:~/Desktop/instrument_note$ clang -g -fsanitize=address ./test_case_3.c -o ./test_case_3 && ./test_case_3 no crash! crash! ================================================================= ==521485==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7fffb261710a at pc 0x0000004c50f9 bp 0x7fffb26170d0 sp 0x7fffb26170c8 WRITE of size 1 at 0x7fffb261710a thread T0 #0 0x4c50f8 in main /home/ubuntu/Desktop/instrument_note/./test_case_3.c:12:16 #1 0x7f54faa5dcb1 in __libc_start_main csu/../csu/libc-start.c:314:16 #2 0x41b2bd in _start (/home/ubuntu/Desktop/instrument_note/test_case_3+0x41b2bd) Address 0x7fffb261710a is located in stack of thread T0 at offset 42 in frame #0 0x4c4f5f in main /home/ubuntu/Desktop/instrument_note/./test_case_3.c:5 ... ``` 理解核心原理之后,接下来就探索LLVM怎么样实现ASAN.在深入ASAN实现之前,我们必须要知道的一点就是:ASAN分为两部分,插桩(Instrumentation Pass)和运行时逻辑(Compiler-RT). 代码插桩负责: * 在代码中符合条件的数据操作之前插入异常检测逻辑. * 引入对全局/栈空间的检测逻辑. 运行时逻辑负责: * 内存分配/投毒逻辑 * 内存操作hook * ... 明白这些概念之后,直接逆向简单的ASAN插桩后的程序,代码如下: ```c int __cdecl main(int argc, const char **argv, const char **envp){ if ( _asan_option_detect_stack_use_after_return ) v24 = (_QWORD *)_asan_stack_malloc_3(0LL, (__asan *)0x180); stack_point = v24; if ( !v24 ) stack_point = (_QWORD *)((unsigned __int64)(&v11 - 48) & 0xFFFFFFFFFFFFFFE0LL); stack_point_ = (unsigned __int64)stack_point; v25 = stack_point; *stack_point = 0x41B58AB3LL; // 填充栈开始Magic Code *(_QWORD *)(stack_point_ + 8) = "1 32 272 4 test"; *(_QWORD *)(stack_point_ + 0x10) = main; shadow_memory = stack_point_ >> 3; *(_QWORD *)(shadow_memory + 0x7FFF8000) = 0xF8F8F8F8F1F1F1F1LL; // 对ShadowTable中分配的栈内存进行投毒 *(_QWORD *)(shadow_memory + 0x7FFF8008) = 0xF8F8F8F8F8F8F8F8LL; *(_QWORD *)(shadow_memory + 0x7FFF8010) = 0xF8F8F8F8F8F8F8F8LL; *(_QWORD *)(shadow_memory + 0x7FFF8018) = 0xF8F8F8F8F8F8F8F8LL; *(_QWORD *)(shadow_memory + 0x7FFF8020) = 0xF3F3F8F8F8F8F8F8LL; *(_QWORD *)(shadow_memory + 0x7FFF8028) = 0xF3F3F3F3F3F3F3F3LL; v26 = 0; *(_QWORD *)(shadow_memory + 0x7FFF8004) = 0LL; // 初始化可用栈区域 *(_QWORD *)(shadow_memory + 0x7FFF800C) = 0LL; *(_QWORD *)(shadow_memory + 0x7FFF8014) = 0LL; *(_QWORD *)(shadow_memory + 0x7FFF801C) = 0LL; *(_WORD *)(shadow_memory + 0x7FFF8024) = 0; v7 = *(_BYTE *)(((unsigned __int64)(real_data_ + 0x22) >> 3) + 0x7FFF8000); v13 = (unsigned __int64)(real_data_ + 0x22); // 计算偏移,获取到ShadowTable中的内存 v12 = v7; if ( v7 ) // ASAN内存异常检测插桩判断,内存中是0值表示为正常内存,可以使用,如果为非0值那就认为是被污染过的 _asan_report_store1(v13); // 提示报错 *(_BYTE *)v13 = -1; // 写入真实内存,注意,ShadowTable中的数据全部都是标识这块内存是否被污染过,用了什么方式污染,并不会保存真实的数据到ShadowTable中,所以它才被称为影子页表. v8 = v21; *(_QWORD *)((char *)v21 + 4) = 0xF8F8F8F8F8F8F8F8LL; // 释放栈时不是直接free(),而是填充Stack use after标志 *(_QWORD *)((char *)v8 + 12) = 0xF8F8F8F8F8F8F8F8LL; *(_QWORD *)((char *)v8 + 20) = 0xF8F8F8F8F8F8F8F8LL; *(_QWORD *)((char *)v8 + 28) = 0xF8F8F8F8F8F8F8F8LL; *((_WORD *)v8 + 18) = -1800; return 0; ``` #### 编译时插桩原理 ASAN的插桩原理比SanitizerCoverage复杂得多,为了容易理解,后续分析实现过程时会省略很多细节.ASAN的插桩过程简单来说就是: 1. 筛选合适的指令 2. 填充插桩代码 3. 进行栈平衡 整体的逻辑示意图如下,先理解过程之后再带着印象去探索源码才能事半功倍: ![](C:\Users\Fremy\Desktop\vm\instrument\pic12\Asan-Arch.png) ASAN的实现代码在\llvm-project\llvm\lib\Transforms\Instrumentation\AddressSanitizer.cpp.遍历每个函数进行插桩的入口点在`AddressSanitizer::instrumentFunction()`函数. ```c++ bool AddressSanitizer::instrumentFunction(Function &F,const TargetLibraryInfo *TLI) { // 省略代码 SmallVector OperandsToInstrument; SmallVector IntrinToInstrument; SmallVector AllBlocks; int NumAllocas = 0; // 这些Vector用于保存筛选出来的指令对象和信息 for (auto &BB : F) { // 遍历BasicBlock AllBlocks.push_back(&BB); for (auto &Inst : BB) { // 遍历指令 SmallVector InterestingOperands; getInterestingMemoryOperands(&Inst, InterestingOperands); if (!InterestingOperands.empty()) { // 如果当前指令属于需要插桩的位置,那就记录一下,后面会用到 for (auto &Operand : InterestingOperands) { OperandsToInstrument.push_back(Operand); } } else if (MemIntrinsic *MI = dyn_cast(&Inst)) { // memset/memcpy/memmove操作 IntrinToInstrument.push_back(MI); } } } // ... for (auto &Operand : OperandsToInstrument) { // 对数据访问指令进行操作 instrumentMop(ObjSizeVis, Operand, UseCalls, F.getParent()->getDataLayout()); FunctionModified = true; } for (auto Inst : IntrinToInstrument) { // 对内存操作指令进行操作 instrumentMemIntrinsic(Inst); FunctionModified = true; } FunctionStackPoisoner FSP(F, *this); bool ChangedStack = FSP.runOnFunction(); // 对插桩之后的函数进行栈调整 // ... return FunctionModified; } void AddressSanitizer::getInterestingMemoryOperands( Instruction *I, SmallVectorImpl &Interesting) { if (LoadInst *LI = dyn_cast(I)) { // LLVM IR Load指令,用于读取数据 if (ignoreAccess(LI->getPointerOperand())) // 判断指令中的操作数是否为指针 return; Interesting.emplace_back(I, LI->getPointerOperandIndex(), false, LI->getType(), LI->getAlign()); } else if (StoreInst *SI = dyn_cast(I)) { // LLVM IR Store指令,用于保存数据 if (ignoreAccess(SI->getPointerOperand())) return; Interesting.emplace_back(I, SI->getPointerOperandIndex(), true, SI->getValueOperand()->getType(), SI->getAlign()); } } ``` 获得筛选出来的指令后,接下来就进行插桩操作.下面的插桩核心原理,就是在**Load/Store**指令前面插入异常检测逻辑,如果没有异常才可以执行真实的数据读写操作. ```c++ void AddressSanitizer::instrumentMop(ObjectSizeOffsetVisitor &ObjSizeVis,InterestingMemoryOperand &O, bool UseCalls,const DataLayout &DL) { Value *Addr = O.getPtr(); // 获取指令操作的指针地址 // ... unsigned Granularity = 1 << Mapping.Scale; // 内存检测粒度,后续再详解 doInstrumentAddress(this, O.getInsn(), O.getInsn(), Addr, O.Alignment, Granularity, O.TypeSize, O.IsWrite, nullptr, UseCalls, Exp); } static void doInstrumentAddress(AddressSanitizer *Pass, Instruction *I, Instruction *InsertBefore, Value *Addr, MaybeAlign Alignment, unsigned Granularity, uint32_t TypeSize, bool IsWrite, Value *SizeArgument, bool UseCalls, uint32_t Exp) { if ((TypeSize == 8 || TypeSize == 16 || TypeSize == 32 || TypeSize == 64 || TypeSize == 128) && (!Alignment || *Alignment >= Granularity || *Alignment >= TypeSize / 8)) return Pass->instrumentAddress(I, InsertBefore, Addr, TypeSize, IsWrite, nullptr, UseCalls, Exp); // 如果当前指令的访问方式是按字节大小访问的话(char,short,long,uint64_t这些方式) Pass->instrumentUnusualSizeOrAlignment(I, InsertBefore, Addr, TypeSize, IsWrite, nullptr, UseCalls, Exp); } void AddressSanitizer::instrumentAddress(Instruction *OrigIns,Instruction *InsertBefore, Value *Addr,uint32_t TypeSize, bool IsWrite,Value *SizeArgument, bool UseCalls,uint32_t Exp) { bool IsMyriad = TargetTriple.getVendor() == llvm::Triple::Myriad; IRBuilder<> IRB(InsertBefore); // LLVM IR指令生成器 Value *AddrLong = IRB.CreatePointerCast(Addr, IntptrTy); size_t AccessSizeIndex = TypeSizeToSizeIndex(TypeSize); Type *ShadowTy = IntegerType::get(*C, std::max(8U, TypeSize >> Mapping.Scale)); Type *ShadowPtrTy = PointerType::get(ShadowTy, 0); Value *ShadowPtr = memToShadow(AddrLong, IRB); Value *CmpVal = Constant::getNullValue(ShadowTy); Value *ShadowValue = IRB.CreateLoad(ShadowTy, IRB.CreateIntToPtr(ShadowPtr, ShadowPtrTy)); Value *Cmp = IRB.CreateICmpNE(ShadowValue, CmpVal); Instruction *CrashTerm = nullptr; /* 上面这段指令生成的意思是创建if判断: shadow_page_flag = *(_BYTE *)((((unsigned __int64)real_data + 0x1001) >> 3) + 0x7FFF8000); real_data_offset = (unsigned __int64)real_data + 0x1001; if ( shadow_page_flag ) // ASAN内存异常检测插桩判断 */ CrashTerm = SplitBlockAndInsertIfThen(Cmp, InsertBefore, !Recover); Instruction *Crash = generateCrashCode(CrashTerm, AddrLong, IsWrite, AccessSizeIndex, SizeArgument, Exp); /* 上面这段指令生成的意思是if判断成功时,在它的子BasicBlock中创建函数调用: _asan_report_store1(v13); // 提示报错 所以合并起来插桩代码就是: shadow_page_flag = *(_BYTE *)((((unsigned __int64)real_data + 0x1001) >> 3) + 0x7FFF8000); real_data_offset = (unsigned __int64)real_data + 0x1001; if ( shadow_page_flag ) // ASAN内存异常检测插桩判断 _asan_report_store1(real_data_offset); // 提示报错 */ } ``` 对所有关键位置进行插入了异常判断后,最后一步就是调整函数的栈空间,把ShadowTable的分配和销毁引入进来. ```c++ bool AddressSanitizer::instrumentFunction() { // ... FunctionStackPoisoner FSP(F, *this); bool ChangedStack = FSP.runOnFunction(); // ... } bool runOnFunction() { // ... // 遍历函数中所有指令,筛选出内存分配操作 for (BasicBlock *BB : depth_first(&F.getEntryBlock())) visit(*BB); // ... processDynamicAllocas(); processStaticAllocas(); // ... return true; } void visitAllocaInst(AllocaInst &AI) { // 遍历指令时遇到AllocaInst,它的意义是在栈内分配指定大小内存 if (!AI.isStaticAlloca()) // 只要在当前函数声明的变量,无论在if/switch/while/for里面哪个BasicBlock,编译时都会把这块内存的申请放到函数的入口BasicBlock中.isStaticAlloca的用意就在于判断这个AllocInst是否在当前函数的入口BasicBlock中执行,而且还判断AllocInst创建的内存大小的值是否会变而不是指定的大小. DynamicAllocaVec.push_back(&AI); else AllocaVec.push_back(&AI); } void visitIntrinsicInst(IntrinsicInst &II) { bool DoPoison = (ID == Intrinsic::lifetime_end); AllocaPoisonCall APC = {&II, AI, SizeValue, DoPoison}; if (AI->isStaticAlloca()) // 同上 StaticAllocaPoisonCallVec.push_back(APC); // 记录栈中分配对象大小和偏移信息 else if (ClInstrumentDynamicAllocas) DynamicAllocaPoisonCallVec.push_back(APC); } ``` `processDynamicAllocas()`的逻辑就不深入探索了,我们主要研究的是`processStaticAllocas()`函数的实现. ```c++ void FunctionStackPoisoner::processStaticAllocas() { // ... Instruction *InsBefore = AllocaVec[0]; IRBuilder<> IRB(InsBefore); // 在函数的第一个AllocaInst指令前插入新代码 SmallVector SVD; SVD.reserve(AllocaVec.size()); for (AllocaInst *AI : AllocaVec) { // 遍历所有在函数入口点声明的AllocaInst指令,收集这些AllocaInst指令的信息 ASanStackVariableDescription D = {AI->getName().data(), ASan.getAllocaSizeInBytes(*AI), 0, AI->getAlignment(), AI, 0, 0}; SVD.push_back(D); } size_t Granularity = 1ULL << Mapping.Scale; // 内存粒度,后面再具体说明 size_t MinHeaderSize = std::max((size_t)ASan.LongSize / 2, Granularity); const ASanStackFrameLayout &L = ComputeASanStackFrameLayout(SVD, Granularity, MinHeaderSize); // 调整ASAN插桩后的整个栈布局 uint64_t LocalStackSize = L.FrameSize; // 获取调整之后的栈布局大小 Value *StaticAlloca = DoDynamicAlloca ? nullptr : createAllocaForLayout(IRB, L, false); // 调整新栈空间,这块栈内存是真实使用的 Value *FakeStack; Value *LocalStackBase; Value *LocalStackBaseAlloca; uint8_t DIExprFlags = DIExpression::ApplyOffset; LocalStackBaseAlloca = IRB.CreateAlloca(IntptrTy, nullptr, "asan_local_stack_base"); Constant *OptionDetectUseAfterReturn = F.getParent()->getOrInsertGlobal( kAsanOptionDetectUseAfterReturn, IRB.getInt32Ty()); Value *UseAfterReturnIsEnabled = IRB.CreateICmpNE( IRB.CreateLoad(IRB.getInt32Ty(), OptionDetectUseAfterReturn), Constant::getNullValue(IRB.getInt32Ty())); Instruction *Term = SplitBlockAndInsertIfThen(UseAfterReturnIsEnabled, InsBefore, false); IRBuilder<> IRBIf(Term); StackMallocIdx = StackMallocSizeClass(LocalStackSize); assert(StackMallocIdx <= kMaxAsanStackMallocSizeClass); Value *FakeStackValue = IRBIf.CreateCall(AsanStackMallocFunc[StackMallocIdx], ConstantInt::get(IntptrTy, LocalStackSize)); IRB.SetInsertPoint(InsBefore); FakeStack = createPHI(IRB, UseAfterReturnIsEnabled, FakeStackValue, Term, ConstantInt::get(IntptrTy, 0)); Value *NoFakeStack = IRB.CreateICmpEQ(FakeStack, Constant::getNullValue(IntptrTy)); Term = SplitBlockAndInsertIfThen(NoFakeStack, InsBefore, false); IRBIf.SetInsertPoint(Term); Value *AllocaValue = DoDynamicAlloca ? createAllocaForLayout(IRBIf, L, true) : StaticAlloca; IRB.SetInsertPoint(InsBefore); LocalStackBase = createPHI(IRB, NoFakeStack, AllocaValue, Term, FakeStack); IRB.CreateStore(LocalStackBase, LocalStackBaseAlloca); // 生成的插桩代码等价于: // void *FakeStack = __asan_option_detect_stack_use_after_return // ? __asan_stack_malloc_N(LocalStackSize) // : nullptr; // void *LocalStackBase = (FakeStack) ? FakeStack : alloca(LocalStackSize); // 意思是从ShadowTable中分配一块栈内存,这块栈内存是用于异常检测的.__asan_stack_malloc_N()的实现代码在Compiler-RT. Value *LocalStackBaseAllocaPtr = isa(LocalStackBaseAlloca) ? cast(LocalStackBaseAlloca)->getPointerOperand() : LocalStackBaseAlloca; // 获取ShadowTable中的栈起始地址 for (const auto &Desc : SVD) { // 根据AllocaInst的申请栈分配内存大小和位置,在ShadowTable中重新调整到对应的位置 AllocaInst *AI = Desc.AI; Value *NewAllocaPtr = IRB.CreateIntToPtr( IRB.CreateAdd(LocalStackBase, ConstantInt::get(IntptrTy, Desc.Offset)), AI->getType()); AI->replaceAllUsesWith(NewAllocaPtr); } // 这些插桩代码都不太重要,意义就是在ShadowTable中创建的栈内存记录当前函数的信息 Value *BasePlus0 = IRB.CreateIntToPtr(LocalStackBase, IntptrPtrTy); IRB.CreateStore(ConstantInt::get(IntptrTy, kCurrentStackFrameMagic), BasePlus0); // Write the frame description constant to redzone[1]. Value *BasePlus1 = IRB.CreateIntToPtr( IRB.CreateAdd(LocalStackBase, ConstantInt::get(IntptrTy, ASan.LongSize / 8)), IntptrPtrTy); GlobalVariable *StackDescriptionGlobal = createPrivateGlobalForString(*F.getParent(), DescriptionString, /*AllowMerging*/ true, kAsanGenPrefix); Value *Description = IRB.CreatePointerCast(StackDescriptionGlobal, IntptrTy); IRB.CreateStore(Description, BasePlus1); // Write the PC to redzone[2]. Value *BasePlus2 = IRB.CreateIntToPtr( IRB.CreateAdd(LocalStackBase, ConstantInt::get(IntptrTy, 2 * ASan.LongSize / 8)), IntptrPtrTy); IRB.CreateStore(IRB.CreatePointerCast(&F, IntptrTy), BasePlus2); const auto &ShadowAfterScope = GetShadowBytesAfterScope(SVD, L); // 根据SVD中记录栈中各个变量对应的内存位置初始化ShadowTable的栈内存 Value *ShadowBase = ASan.memToShadow(LocalStackBase, IRB); // ASan.memToShadow()用于计算进程内存在ShadowTable的偏移位置 copyToShadow(ShadowAfterScope, ShadowAfterScope, IRB, ShadowBase); // 21给函数栈内存投毒 if (!StaticAllocaPoisonCallVec.empty()) { // 2.对栈中分配的变量在ShadowTable中消毒 const auto &ShadowInScope = GetShadowBytes(SVD, L); for (const auto &APC : StaticAllocaPoisonCallVec) { const ASanStackVariableDescription &Desc = *AllocaToSVDMap[APC.AI]; assert(Desc.Offset % L.Granularity == 0); size_t Begin = Desc.Offset / L.Granularity; size_t End = Begin + (APC.Size + L.Granularity - 1) / L.Granularity; IRBuilder<> IRB(APC.InsBefore); copyToShadow(ShadowAfterScope, APC.DoPoison ? ShadowAfterScope : ShadowInScope, Begin, End, IRB, ShadowBase); } } /* 投毒再消毒后,ShadowTable的内存数据布局如下: 1.ShadowTable分配栈后对内存投毒 => F3F3F8F8F1F1F1F1 2.对栈中需要用到的变量位置消毒 => F3F30000F1F1F1F1 此时访问栈变量,获取到的数据就是0x00,为正常数据访问;如果是不允许访问的话,那就必定不为0 */ SmallVector ShadowClean(ShadowAfterScope.size(), 0); SmallVector ShadowAfterReturn; for (auto Ret : RetVec) { IRBuilder<> IRBRet(Ret); // Mark the current frame as retired. IRBRet.CreateStore(ConstantInt::get(IntptrTy, kRetiredStackFrameMagic), BasePlus0); // 简单总结就是在函数返回时清空ShadowTable中的栈数据为0xF5 // if FakeStack != 0 // LocalStackBase == FakeStack // // In use-after-return mode, poison the whole stack frame. // if StackMallocIdx <= 4 // // For small sizes inline the whole thing: // memset(ShadowBase, kAsanStackAfterReturnMagic, ShadowSize); // **SavedFlagPtr(FakeStack) = 0 // else // __asan_stack_free_N(FakeStack, LocalStackSize) // else // Value *Cmp = IRBRet.CreateICmpNE(FakeStack, Constant::getNullValue(IntptrTy)); Instruction *ThenTerm, *ElseTerm; SplitBlockAndInsertIfThenElse(Cmp, Ret, &ThenTerm, &ElseTerm); IRBuilder<> IRBPoison(ThenTerm); if (StackMallocIdx <= 4) { int ClassSize = kMinStackMallocSize << StackMallocIdx; ShadowAfterReturn.resize(ClassSize / L.Granularity, kAsanStackUseAfterReturnMagic); copyToShadow(ShadowAfterReturn, ShadowAfterReturn, IRBPoison, ShadowBase); Value *SavedFlagPtrPtr = IRBPoison.CreateAdd( FakeStack, ConstantInt::get(IntptrTy, ClassSize - ASan.LongSize / 8)); Value *SavedFlagPtr = IRBPoison.CreateLoad( IntptrTy, IRBPoison.CreateIntToPtr(SavedFlagPtrPtr, IntptrPtrTy)); IRBPoison.CreateStore( Constant::getNullValue(IRBPoison.getInt8Ty()), IRBPoison.CreateIntToPtr(SavedFlagPtr, IRBPoison.getInt8PtrTy())); } else { // For larger frames call __asan_stack_free_*. IRBPoison.CreateCall( AsanStackFreeFunc[StackMallocIdx], {FakeStack, ConstantInt::get(IntptrTy, LocalStackSize)}); } IRBuilder<> IRBElse(ElseTerm); copyToShadow(ShadowAfterScope, ShadowClean, IRBElse, ShadowBase); } } ``` ASAN的实现中有大量的内存分配/操作功能,很显然,如果通过Pass模块把这些函数插入到Module会让Pass非常臃肿,所以ASAN把它的一些核心功能写在了Compiler-RT中,让Clang在链接阶段引入它们. #### LLVM-CompilerRT与ASAN内置函数 实际上,ASAN在程序运行main()之前就会执行初始化,ASAN把`asan_module_ctor()`函数地址写到.init_array区段,当程序启动时执行`__libc_csu_init()`函数时就会执行`asan_module_ctor()`初始化ASAN内部运行环境. ```assembly .init_array:00000000004F65D8 ; ELF Initialization Function Table .init_array:00000000004F65D8 ; =========================================================================== .init_array:00000000004F65D8 .init_array:00000000004F65D8 ; Segment type: Pure data .init_array:00000000004F65D8 ; Segment permissions: Read/Write .init_array:00000000004F65D8 ; Segment alignment 'qword' can not be represented in assembly .init_array:00000000004F65D8 _init_array segment para public 'DATA' use64 .init_array:00000000004F65D8 assume cs:_init_array .init_array:00000000004F65D8 ;org 4F65D8h .init_array:00000000004F65D8 __init_array_start dq offset asan_module_ctor // ASAN初始化函数 .init_array:00000000004F65D8 ; DATA XREF: __libc_csu_init+6↑o .init_array:00000000004F65E0 __frame_dummy_init_array_entry dq offset frame_dummy .init_array:00000000004F65E8 dq offset _GLOBAL__sub_I_asan_rtl_cpp .init_array:00000000004F65E8 _init_array ends .fini_array:00000000004F65F0 __init_array_end dq offset asan_module_dtor ``` 接下来,`__asan_init()`就会初始化.代码目录在\llvm-project\compiler-rt\lib\asan\asan_activation.cpp. ```c++ void __asan_init() { AsanActivate(); AsanInitInternal(); } void AsanActivate() { asan_deactivated_flags.OverrideFromActivationFlags(); // 从环境变量ASAN_ACTIVATION_OPTIONS中获取ASAN配置 SetCanPoisonMemory(asan_deactivated_flags.poison_heap); SetMallocContextSize(asan_deactivated_flags.malloc_context_size); ReInitializeAllocator(asan_deactivated_flags.allocator_options); } static void AsanInitInternal() { InitializeHighMemEnd(); InitializeShadowMemory(); AllocatorOptions allocator_options; allocator_options.SetFrom(flags(), common_flags()); InitializeAllocator(allocator_options); InitializeCoverage(common_flags()->coverage, common_flags()->coverage_dir); } ``` 这只是Compiler-RT ASAN功能的小部分,还有更多有趣的细节读者们可以自行探索. #### ASAN检测结构体的Bug 笔者在实际场景中发现了个这样问题:**ASAN对结构体内的buffer溢出是不支持检测的**.举个例子: ```c #include #include #define BUFFER_MAX (0x10) typedef struct { int a; char buffer[BUFFER_MAX]; int b; int c; } no_check; typedef struct { int a; char* buffer; int b; int c; } check; int main() { no_check test_obj1 = {0}; check test_obj2 = {}; printf("no crash!\n"); test_obj1.buffer[BUFFER_MAX] = 0xFF; printf("crash!\n"); test_obj2.buffer = malloc(BUFFER_MAX); test_obj2.buffer[BUFFER_MAX] = 0xFF; return 0; } ``` ```sh ubuntu@ubuntu-virtual-machine:~/Desktop/instrument_note$ clang -fsanitize=address ./test_case_6.c -o ./test_case_6 && ./test_case_6 no crash! crash! ================================================================= ==529347==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000020 at pc 0x0000004c51f4 bp 0x7ffe84ee9fb0 sp 0x7ffe84ee9fa8 WRITE of size 1 at 0x602000000020 thread T0 #0 0x4c51f3 in main (/home/ubuntu/Desktop/instrument_note/test_case_6+0x4c51f3) #1 0x7f99e1bb6cb1 in __libc_start_main csu/../csu/libc-start.c:314:16 #2 0x41b2bd in _start (/home/ubuntu/Desktop/instrument_note/test_case_6+0x41b2bd) ... ``` ASAN为什么会检测失败呢?简单地说,ASAN认为struct结构体是一块连续的内存,即使在内部出现了**char[]**这样的连续数组,即使是触发越界都不会认为是错误.那么我们来看一个真实的例子,qemu usb模块越权读写漏洞CVE-2020-14364. ```c struct USBDevice { DeviceState qdev; // ... int32_t state; uint8_t setup_buf[8]; uint8_t data_buf[4096]; int32_t remote_wakeup; int32_t setup_state; int32_t setup_len; int32_t setup_index; USBEndpoint ep_ctl; USBEndpoint ep_in[USB_MAX_ENDPOINTS]; USBEndpoint ep_out[USB_MAX_ENDPOINTS]; // ... }; ``` 对data_buf进行越界操作,此时ASAN就无法检测出来漏洞了.qemu的设备对象大部分都是这样声明的,所以就导致即使fuzzer跑出了漏洞,ASAN仍然认为是*正常的*. 为了解决这个问题,笔者重新对ASAN的插桩代码进行修改,核心思想是: 1. 插桩阶段遍历所有GetElementPtrInst指令,如果指令中存在`char[]`这种Array型变量,那么就在该结构体最后创建新字段用于保存该buffer在ShadowTable中的映射.然后再调整结构体中新字段偏移,让`instrumentMop()`生成的插桩检测逻辑使用ShadowTable中的分配内存来检测而不是原来的结构体内存. 2. 栈平衡阶段对所有结构体中新创建的字段保存`malloc()`分配的内存,此时越界是可以被内存检测的. 修改过后的Demo效果如下: ![](./pic12/Asan-fix-Demo.png) ## 实战中ASAN会有哪些坑 #### libasan库缺失如何解决? 使用ASAN时有概率会出现下面的问题. ```sh $ g++ -O -g -fsanitize=address heap-use-after-free.cpp /usr/bin/ld: cannot find /usr/lib64/libasan.so.0.0.0 collect2: error: ld returned 1 exit status ``` 这是因为在链接阶段没有找到libasan库,前文提到,ASAN的运行时函数是封装在Compiler-RT库中的.所以在正常的编译环境下它会在/usr/lib中出现. ```sh fuzzing@fuzzing-virtual-machine:~/Desktop/test_code$ find /usr/ | grep libasan /usr/share/doc/libasan5 /usr/share/doc/libasan6 /usr/lib/gcc/x86_64-linux-gnu/8/libasan_preinit.o /usr/lib/gcc/x86_64-linux-gnu/8/libasan.a /usr/lib/gcc/x86_64-linux-gnu/8/libasan.so /usr/lib/gcc/x86_64-linux-gnu/10/libasan_preinit.o /usr/lib/gcc/x86_64-linux-gnu/10/libasan.a /usr/lib/gcc/x86_64-linux-gnu/10/libasan.so /usr/lib/x86_64-linux-gnu/libasan.so.5 /usr/lib/x86_64-linux-gnu/libasan.so.6 /usr/lib/x86_64-linux-gnu/libasan.so.6.0.0 /usr/lib/x86_64-linux-gnu/libasan.so.5.0.0 ``` 找不到libasan库有两种解决方法: * 联网环境下,使用`sudo apt install libasan`即可安装. * 非联网环境下,找到LLVM Compiler-RT的源码下载编译并`make install`即可. #### 模糊测试中遇到老旧不维护的库一直产生崩溃,怎么样让ASAN屏蔽对它的检测? 对于这类一直让ASAN产生崩溃但是不知道如何修复的代码,我们可以使用ASAN的黑名单来禁止对这些指定的函数插桩,甚至只对某几个特定的函数做插桩检测.详情参考官方文档 https://clang.llvm.org/docs/SanitizerSpecialCaseList.html #### ASAN有哪些常用设置? ASAN_OPTIONS https://github.com/google/sanitizers/wiki/AddressSanitizerFlags #### Shadow Table内存粒度有什么意义? Shadow Table需要分配一块比较大的内存,用于对程序对的堆和栈做映射.这块内存能够映射的大小是有限的,所以就需要找到一种方式在尽可能少的内存里面保存更多的内存映射.举个例子: ```text FF FF FF FF 00 00 00 00 00 00 00 00 FF FF FF FF ``` 这块内存数据表示占用8字节缓冲区进行投毒的内存布局.如果程序中大量使用这样的内存,那么很容易就把ASAN的Shadow Table占满,于是我们就有压缩Shadow Table的需求.压缩之后,Shadow Table的内存布局就变成了: ```text FF FF FF FF 00 FF FF FF FF ``` 此时内存占用变小了一半.现在我们再回过来理解内存粒度的概念,未优化时的内存粒度为1,优化之后的内存粒度为8.先来看直观的例子: ```c int main(int argc,char** argv) { char buffer[0x10] = {0}; buffer[0x10] = 'C'; return 1; } // 编译参数:clang -fsanitize=address ./test_asan_granularity.c -o ./test_asan_granularity ``` ![](./pic12/6.png) 接下来尝试编译`clang -fsanitize=address -mllvm -asan-mapping-scale=4 ./test_asan_granularity.c -o ./test_asan_granularity`,ASAN的崩溃内容出现了异常. ![](./pic12/7.png) 接下来再观察这个测试用例.因为内存粒度为8字节(为什么要取值为8字节压缩呢?笔者猜测应该是对齐x64平台的数据类型),此时buffer占用4字节,剩下4字节变量a也在Shadow Table压缩的这块内存里.ASAN的处理方法是在这一字节的Shadow Table内存中记录一个标记,标识这里可能会存在内存越界(只要Shadow Table的值不为0就认为是有异常的). ```c int main(int argc,char** argv) { short buffer[2] = {0}; long a; buffer[3] = 1024; return 1; } ``` ![](C:\Users\Fremy\Desktop\vm\instrument\pic12\8.png) 现在我们就可以理解ASAN的这一行输出的意义了: ```text Shadow byte legend (one shadow byte represents 8 application bytes): Addressable: 00 Partially addressable: 01 02 03 04 05 06 07 // <<<< 这里呀 Heap left redzone: fa Freed heap region: fd Stack left redzone: f1 Stack mid redzone: f2 Stack right redzone: f3 Stack after return: f5 Stack use after scope: f8 ``` 它的意思是,当前被压缩的内存中存在n字节其它变量占用的内存(n=1-7). #### 如何调试使用ASAN的程序? 一般地,ASAN崩溃有几种可能: * ASAN初始化时崩溃,可能是机器上内存不足导致. * 全局对象初始化时崩溃,比如说C++全局声明的类对象,它会在程序初始化阶段(还记得init_array嘛,就是在这里插入了回调函数实例化全局对象)执行,也会存在内存问题. * 运行时库异常,常见于Windows平台上. * 创建栈时崩溃. * 项目代码崩溃. 笔者在上一小节测试内存粒度时遇到了ASAN在函数初始化的阶段创建Shadow Table时直接崩溃了. ![](./pic12/9.png) 从输出我们可以知道,main函数的断点命中之后,接下来执行一次单步调试时就抛出ASAN的检测异常了,也就是说没有执行到用户在main函数中写的任何代码就崩溃了,那么产生崩溃肯定是在ASAN在创建Shadow Table初始化函数栈时触发的崩溃.我们把源程序反编译,查看0x4C500B的汇编. ![](C:\Users\Fremy\Desktop\vm\instrument\pic12\10.png) 对应的LLVM IR: ```llvm %21 = inttoptr i64 %20 to i64* store i64 -1012762419733073423, i64* %21, align 1 ``` 原来是对Shadow Table进行投毒时触发了内存异常,导致程序异常崩溃了,知道原因之后就有思路再去寻找办法解决问题,像这样奇奇怪怪的问题还有很多,只能通过调试去找到问题的根源再解决. 那么如何调试一次由用户代码触发的崩溃呢?笔者的方法是: * 根据ASAN栈崩溃信息定位到触发崩溃的代码,并分析漏洞原因是因为那些判断逻辑没有做好检验和关注变量内容. * 根据猜想编写gdb脚本. * 运行gdb观察值的变化. 举个例子,代码某个位置产生了越界访问,于是猜想是不是长度校验判断有问题,编写gdb脚本来监控这两个值的变化: ```gdb b func1 command b 1031 command print ">>>>" print "Size=" print array_size print "offset=" print offset c end c end ``` 然后使用gdb命令执行`gdb --command=./gdb_crash_analysis.gsh -arg ./fuzzer file ./crash`,观察崩溃前对应的数值. ``` Breakpoint 2, func1 (this=0x7fffffffbae0, stream_0=0x7fffffffbb00, int_2=59852, int_3=1668261324, int_4=60, int_5=45056, int_6=1668246528, int_7=204, class508_0=..., rangeList1_0=0x7fffffff7820, list_0=std::vector of length 2, capacity 2 = {...}, list_1=std::vector of length 2, capacity 2 = {...}, list_0_types=std::vector of length 2, capacity 2 = {...}) at Process.cpp 1468 arrays[j] = *(stream_0->begin() + Position + j); $6 = ">>>>" $7 = "Size=" $8 = 20 $9 = "offset=" $10 = 1668261324 // <<<< Overflow ! Program received signal SIGSEGV, Segmentation fault. 0x000000000060980e in func1 (this=0x7fffffffbae0, stream_0=0x7fffffffbb00, int_2=59852, int_3=1668261324, int_4=60, int_5=45056, int_6=1668246528, int_7=204, class508_0=..., rangeList1_0=0x7fffffff7820, list_0=std::vector of length 2, capacity 2 = {...}, list_1=std::vector of length 2, capacity 2 = {...}, list_0_types=std::vector of length 2, capacity 2 = {...}) at Process.cpp 1468 arrays[j] = *(stream_0->begin() + Position + j); //// <<< Position = 1668261324 (gdb) AddressSanitizer:DEADLYSIGNAL ================================================================= ==2995382==ERROR: AddressSanitizer: SEGV on unknown address 0x7ff2203d3dcc (pc 0x000000609846 bp 0x7ffc18159c50 sp 0x7ffc18157760 T0) ==2995382==The signal is caused by a READ memory access. #0 0x609846 in func1(std::vector >*, int, int, int, int, int, int, C508, RangeList*, std::vector, std::allocator >, std::vector, std::allocator >, std::allocator, std::allocator > > > >, std::allocator, std::allocator >, std::vector, std::allocator >, std::allocator, std::allocator > > > > > >&, std::vector >&, std::vector, std::allocator >, std::vector, std::allocator >, std::allocator, std::allocator > > > >, std::allocator, std::allocator >, std::vector, std::allocator >, std::allocator, std::allocator > > > > > >&) Process.cpp #1 0x6006e7 in func1(FileReaderHelp*, FileInfo, std::vector >&, St*, Struct90) Process.cpp # .... #9 0x54c65b in main fuzz_main.cpp #10 0x7ff1c3c26cb1 in __libc_start_main csu/../csu/libc-start.c:314:16 #11 0x411c0d in _start (v5+0x411c0d) ``` #### x64 ASAN为什么不兼容? 有时候在64位平台上使用ASAN编译会提示以下错误(参考链接:https://stackoverflow.com/questions/59007118/how-to-enable-address-sanitizer-at-godbolt-org/59010436#59010436): ```text ==3==ERROR: AddressSanitizer failed to allocate 0xdfff0001000 (15392894357504) bytes at address 2008fff7000 (errno: 12) ==3==ReserveShadowMemoryRange failed while trying to map 0xdfff0001000 bytes. Perhaps you're using ulimit -v ``` 在32位引入ASAN编译时,Shadow Table分配内存占用几百MB.但是使用64位ASAN编译时会占用20T内存,因为malloc分配这么大的内存失败,才提示了这个错误.解决方法一般有两个:1.直接限制内存分配大小,让malloc()成功分配;2.设置虚拟内存到交换分区. ASAN官方的解决方法是使用ulimit命令来限制内存使用(参考引用:https://github.com/mirrorer/afl/blob/master/docs/notes_for_asan.txt),但是这个方式并不一定有效.所以我们可以使用虚拟内存映射到磁盘交互分区的方式再次尝试(参考引用:https://qastack.cn/unix/44985/limit-memory-usage-for-a-single-linux-process) #### ASAN for Windows使用MSVC还是LLVM? https://developercommunity.visualstudio.com/t/enabled-asan-address-sanitizer-for-x64-build-cause/1139763 https://devblogs.microsoft.com/cppblog/asan-for-windows-x64-and-debug-build-support/ https://docs.microsoft.com/en-us/cpp/build/reference/incremental-link-incrementally?view=msvc-160 https://github.com/microsoft/WSL/issues/121 #### 主程序和动态链接库的ASAN兼容吗? 有空再写 ## 参考引用 1. Compile-time-instrumentation-flow-in-LLVM(https://www.researchgate.net/figure/Compile-time-instrumentation-flow-in-LLVM_fig1_262175489) 2. LLVM Sanitizer-Coverage Document(https://clang.llvm.org/docs/SanitizerCoverage.html) 3. LLVM Source-based Code Coverage(https://bcain-llvm.readthedocs.io/projects/clang/en/release_50/SourceBasedCodeCoverage/) 4. libfuzzer-workshop(https://github.com/Dor1s/libfuzzer-workshop) 5. LLVM AddressSanitizer Document(https://clang.llvm.org/docs/AddressSanitizer.html) 6. AddressSanitizer Wiki(https://github.com/google/sanitizers/wiki/AddressSanitizer) 7. llvm::MemIntrinsic Class Reference(https://llvm.org/doxygen/classllvm_1_1MemIntrinsic.html) 8. llvm::IntrinsicInst Class Reference(https://llvm.org/doxygen/classllvm_1_1IntrinsicInst.html) 9. LLVM llvm-lifetime-start-intrinsic(https://llvm.org/docs/LangRef.html#llvm-lifetime-start-intrinsic) 10. llvm::AllocaInst Class Reference(https://llvm.org/doxygen/classllvm_1_1AllocaInst.html) 11. llvm::IRBuilder Class Template Reference(https://llvm.org/doxygen/classllvm_1_1IRBuilder.html) 12. C++全局构造和析构(https://www.jianshu.com/p/56ea6e9d00e9) 13. CVE-2020-14364 QEMU逃逸 漏洞分析 (含完整EXP)(https://mp.weixin.qq.com/s/MQyczZXRfOsIQewNf7cfXw) 14. libFuzzer Document(https://llvm.org/docs/LibFuzzer.html) 15. libFuzzer Source by guidovranken(https://github.com/guidovranken/libfuzzer-gv) ================================================ FILE: 2.Fuzzing 模糊测试之数据输入.md ================================================ ## 必备工具 Python ,Source Insight ## Fuzzing 与代码覆盖率 前面一章说到在Github 上快速阅读代码,这样有助于我们去了解关于我们要挖掘漏洞的目标的一些理解,对于程序有了一些理解之后,接下来就可以尝试写些Fuzzing 来跑跑漏洞了. Fuzzing 是模糊测试的意思,我们可以按照给定的格式来生成数据或者随机生成,观察程序有没有处理异常或者程序崩溃.读者要注意的一点是:**二进制Fuzzing 的思路和WEB Fuzzing 的思路是完全不同的**,后面会通过许多的例子来告诉大家二进制和WEB Fuzzing 到底差异在哪里.**Fuzzing 和源码挖洞是相互辅助的!不要把能不能挖到漏洞的锅都丢给Fuzzing ,Fuzzing 不出来就是没有漏洞;也不要把全部的精力都花在阅读源码上,有很多时候会迷失在代码里,忘记上下文到底在做些什么,越看越迷茫.这是成本与收益的博弈,对于代码量较大的程序来说,偏向Fuzzing 的投入产出比较高;对于代码量较小的程序来说,偏向阅读源码的投入产出比较高**. 代码覆盖率是说,这次自动化测试触发了的代码占整体代码的比率是多少.要想对一个程序的所有代码都要测试到,这样的代码覆盖率就是100% ,这是不可能的,因为会有很多的功能和代码是需要联合起来触发的,有的代码触发条件逻辑非常复杂,这些都是Fuzzing 的短板,Fuzzing 在对某一个攻击点测试上效果是很好的,一个程序会有很多的攻击点,所以要针对各个不同的攻击点都要写不同的Fuzzer ,提高Fuzzing 代码覆盖率. ## Fuzzing 攻击点 以Python 为例子,Python 的攻击点有三处:库,内部对象,运行环境 ### Python 库 关于Python 的库代码,我们可以从Python 安装路径下的`Lib` 目录中找到 ![](pic2/python_lib.png) 每个库都能够去找个针对性的Fuzzer 跑一跑,不过有些Python 库是做系统操作的,**重点挑一些外部数据可以流进来,然后又可以进行处理的**,比如:json,urllib,requests 这些库.本地库找到漏洞有时候利用会比较鸡肋,除非你的渗透对象是云服务(比如SAE 这种只提供一个执行容器的云,那么我们就需要找到一个可以绕过Python 解析器能够直接执行二进制代码的方式来绕过沙盒,如果读者不是很理解这个操作,同样的原理请参考pwn2own 从浏览器到系统system/root 提权),否则能够用利用的地方比较少.这个是我在挖requests 的时候挖到的一个洞,可以在Cookie `max-age` 中设置字符串值触发Cookielib 处理异常,丢弃掉这个Set-cookie 字段,让爬虫无法获取Cookie .传送门:https://github.com/lcatro/Python_CookieLib_0day 上面提到的库都是Python 的代码相互调用,但是有一些库是能触发二进制代码的,比如:http://www.freebuf.com/articles/network/27817.html ### Python 内部对象 说到Python 内部对象,对没有了解过解析器是如何做到远程代码执行的同学推荐阅读:https://github.com/lcatro/vuln_javascript 对内部对象做一些的操作,最后可以达到RCE (远程代码执行)的结果.我们先看看PoC 是怎么样的(以JavaScript 为例子,Python 版没有找到,原理都差不多的),Link :https://github.com/tunz/js-vuln-db ``` PoC 1 : function opt() { let obj = '2.3023e-320'; for (let i = 0; i < 1; i++) { obj.x = 1; obj = +obj; obj.x = 1; } } function main() { for (let i = 0; i < 100; i++) { opt(); } } main(); PoC 2 : function opt() { let arr = []; return arr['x']; } function main() { let arr = [1.1, 2.2, 3.3]; for (let i = 0; i < 0x10000; i++) { opt(); } Array.prototype.__defineGetter__('x', Object.prototype.valueOf); print(opt()); } main(); PoC 3 : var f = function() { var o = { a: {}, b: { ba: { baa: 0, bab: [] }, bb: {}, bc: { bca: {bcaa: 0, bcab: 0, bcac: this} } } }; o.b.bc.bca.bcab = 0; o.b.bb.bba = Array.prototype.slice.apply(o.b.ba.bab); }; while(true) f(f); ``` 细心的你应该能从这些PoC 里面发现了很多Fuzzing 的痕迹,对于这种涉及到解析器运行时产生的问题,是需要构造代码来Fuzzing 的.要展开来讲还需要用很多篇幅,后面还会介绍到一个东西叫AST (抽象语法树),读者们可以结合AST 和js-vuln-db 的PoC 这两个东西一起细细琢磨,很有意思的. ### Python 运行环境 Python 运行环境有两部分:编译和执行.Python 的编译请参考`compile()` 函数,我们关注Python 运行环境的执行部分,对应的源码在`Python/Ceval.c PyEval_EvalFrameEx()`.Python 的OpCode 的格式如下: ``` | OpCode | 没有操作数的OpCode | OpCode | OpNum1 | 一个操作数的OpCode | OpCode | OpNum1 | OpNum2 | 两个操作数的OpCode ``` 关于操作码的具体信息在`Include/Opcode.h` 里.那么我们生成的Python 字节码要怎么样才能传递到`PyEval_EvalFrameEx()` 里执行呢? ## Fuzzing 的入口点 找到攻击点之后,还需要给Fuzzing 构建一个入口点,让我们的Fuzzing 生成的数据流能够进入到这些地方去.以AFL 为例子(本篇文章没有介绍AFL 的使用,读者们可以从这里了解更多关于AFL 的使用:https://github.com/lcatro/Fuzzing-ImageMagick ;关于libFuzzer 推荐阅读:https://github.com/Dor1s/libfuzzer-workshop (入门教程);https://github.com/google/fuzzer-test-suite (真实的测试用例)),我们给AFL Fuzzing 的入口点就是命令行,通过使用不同的命令参数组合来触发更多的代码覆盖率,举个例子 ```bash afl-fuzz -i samples -o output ./magick convert @@ /dev/null afl-fuzz -i samples -o output ./magick composite @@ /dev/null afl-fuzz -i samples -o output ./magick compare @@ /dev/null afl-fuzz -i samples -o output ./magick montage @@ /dev/null ``` AFL 就会把变异的样本传递进去测试,有些库是完全没有像ImageMagick 这种入口的,比如:libGif ,libxml 这些,就得要手工构造入口点,再提供给AFL 来Fuzzing 对于Python 来说,我们还有pyc 文件,pyc 文件里面保存的是Python OpCode ,使用Python 执行pyc 之后,最后会将OpCode 传递到`PyEval_EvalFrameEx()` 执行,关于pyc 的文件结构读者们可以自行搜素,下面放一段打包字节码成pyc 结构的代码 ```python import marshal class code_object(object): def __init__(self) : self.co_argcount=0 self.co_nlocals=0 self.co_stacksize=1 self.co_flags=0x40 self.co_code=b'' self.co_consts=() self.co_names=() self.co_varnames=() self.co_filename='' self.co_name='' self.co_firstlineno=1 self.co_lnotab=b'\x00\x01' self.co_freevars=() self.co_cellvars=() def serialize_code_object(code_object) : code_buffer=b'\x63' code_buffer+=marshal.dumps(code_object.co_argcount)[1:] code_buffer+=marshal.dumps(code_object.co_nlocals)[1:] code_buffer+=marshal.dumps(code_object.co_stacksize)[1:] code_buffer+=marshal.dumps(code_object.co_flags)[1:] code_buffer+=marshal.dumps(code_object.co_code) code_buffer+=marshal.dumps(code_object.co_consts) code_buffer+=marshal.dumps(code_object.co_names) code_buffer+=marshal.dumps(code_object.co_varnames) code_buffer+=marshal.dumps(code_object.co_freevars) code_buffer+=marshal.dumps(code_object.co_cellvars) code_buffer+=marshal.dumps(code_object.co_filename) code_buffer+=marshal.dumps(code_object.co_name) code_buffer+=struct.pack('L',code_object.co_firstlineno) code_buffer+=marshal.dumps(code_object.co_lnotab) return code_buffer def save_to_pyc(file_path,code_object) : file=open(file_path, 'wb') if file : file.write(imp.get_magic()) file.write(struct.pack('L',time.time())) file.write(serialize_code_object(code_object)) file.close() def make_code_object(opcode_data) : compile_code_object = python_opcode_build.code_object() compile_code_object.co_argcount = 0 compile_code_object.co_code = packet_code_object_in_try_block(opcode_data) compile_code_object.co_consts = tuple(make_random_string_list(3,8),) compile_code_object.co_names = tuple(make_random_string_list(3,8)) compile_code_object.co_varnames = tuple(make_random_string_list(3,8)) return compile_code_object ``` ## Fuzzing 数据生成 找到了一个攻击点并且构造好Fuzzing 入口点之后,这个时候就需要传递一些数据进去测试了,一般有两种方式进行Fuzzing ### 随机生成数据 随机生成数据是真的随机,我们来看看Fuzzer 的代码 ```python import imp import marshal import os import random import struct import time class code_object_class(object): def __init__(self) : self.co_argcount=0 self.co_nlocals=0 self.co_stacksize=1 self.co_flags=0x40 self.co_code=b'' self.co_consts=() self.co_names=() self.co_varnames=() self.co_filename='' self.co_name='' self.co_firstlineno=1 self.co_lnotab=b'\x00\x01' self.co_freevars=() self.co_cellvars=() def serialize_code_object(code_object) : code_buffer=b'\x63' code_buffer+=marshal.dumps(code_object.co_argcount)[1:] code_buffer+=marshal.dumps(code_object.co_nlocals)[1:] code_buffer+=marshal.dumps(code_object.co_stacksize)[1:] code_buffer+=marshal.dumps(code_object.co_flags)[1:] code_buffer+=marshal.dumps(code_object.co_code) code_buffer+=marshal.dumps(code_object.co_consts) code_buffer+=marshal.dumps(code_object.co_names) code_buffer+=marshal.dumps(code_object.co_varnames) code_buffer+=marshal.dumps(code_object.co_freevars) code_buffer+=marshal.dumps(code_object.co_cellvars) code_buffer+=marshal.dumps(code_object.co_filename) code_buffer+=marshal.dumps(code_object.co_name) code_buffer+=struct.pack('L',code_object.co_firstlineno) code_buffer+=marshal.dumps(code_object.co_lnotab) return code_buffer def save_to_pyc(file_path,code_object) : file=open(file_path, 'wb') if file : file.write(imp.get_magic()) file.write(struct.pack('L',time.time())) file.write(serialize_code_object(code_object)) file.close() def make_random_string(length) : data = '' for index in range(length) : data += chr(random.randint(0,255)) return data def make_random_string_list(list_count,string_length) : return_list = [] for list_index in range(list_count) : for string_index in range(list_count) : return_list.append(make_random_string(string_length)) return return_list def packet_code_object_in_try_block(code) : code_length_low = (len(code) % 0x100) & 0xFF code_length_height = (len(code) >> 8) & 0xFF try_block = b'\x79' try_block += chr(code_length_height) try_block += chr(code_length_low) try_block += code try_block += b'\x6e\x07\x00\x01\x01\x01\x6e\x01\x00\x58\x64\x01\x00\x53' return try_block def make_code_object(opcode_data) : compile_code_object = code_object_class() compile_code_object.co_argcount = 0 compile_code_object.co_code = packet_code_object_in_try_block(opcode_data) compile_code_object.co_consts = tuple(make_random_string_list(3,8),) compile_code_object.co_names = tuple(make_random_string_list(3,8)) compile_code_object.co_varnames = tuple(make_random_string_list(3,8)) return compile_code_object if __name__ == '__main__' : while True : code_object = make_code_object(make_random_string(64)) save_to_pyc('python_fuzzing.tmp.pyc',code_object) os.system('python python_fuzzing.tmp.pyc') ``` 运行效果 ![](pic2/fuzzing_state.png) ### 按结构生成数据 上面的Fuzzing 已经出现了崩溃的结果,现在我们可以开开心心地拿样本来分析漏洞崩溃原因了,不过这里是在讨论如何Fuzzing ,所以就不多做漏洞分析了,细心的你应该观察到了这一点 ![](pic2/error_fuzzing.png) 这些OpCode 无法被运行环境所识别,所以提示了异常.重复来跑这种没有意义的Fuzzing 其实是很低效的,我们回去阅读`PyEval_EvalFrameEx()` 找到解决问题的答案. 在`Python/Ceval.c:1199` 行代码里,这里是OpCode 的解析执行部分,我们看这个switch 的default 部分(`Python/Ceval.c:3134`) ```c default: fprintf(stderr, "XXX lineno: %d, opcode: %d\n", PyFrame_GetLineNumber(f), opcode); PyErr_SetString(PyExc_SystemError, "unknown opcode"); why = WHY_EXCEPTION; break; ``` 原来是OpCode 没有被case 语句判断成功,那么再去看看`include\Opcode.h` 的OpCode 都有哪些取值 ```c #ifndef Py_OPCODE_H #define Py_OPCODE_H #ifdef __cplusplus extern "C" { #endif /* Instruction opcodes for compiled code */ #define STOP_CODE 0 #define POP_TOP 1 #define ROT_TWO 2 #define ROT_THREE 3 // ..... #define SETUP_WITH 143 /* Support for opargs more than 16 bits long */ #define EXTENDED_ARG 145 #define SET_ADD 146 #define MAP_ADD 147 enum cmp_op {PyCmp_LT=Py_LT, PyCmp_LE=Py_LE, PyCmp_EQ=Py_EQ, PyCmp_NE=Py_NE, PyCmp_GT=Py_GT, PyCmp_GE=Py_GE, PyCmp_IN, PyCmp_NOT_IN, PyCmp_IS, PyCmp_IS_NOT, PyCmp_EXC_MATCH, PyCmp_BAD}; #define HAS_ARG(op) ((op) >= HAVE_ARGUMENT) #ifdef __cplusplus } #endif #endif /* !Py_OPCODE_H */ ``` 这些OpCode 都是连续的,从0 到147 这个范围里取值,那么就可以确定`OpCode = range(0,104)` ,接下来再看第94 行和第166 行代码 ```c #define HAVE_ARGUMENT 90 /* Opcodes from here have an argument: */ // OpCode.h:90 #define STORE_NAME 90 /* Index in name list */ #define DELETE_NAME 91 /* "" */ #define UNPACK_SEQUENCE 92 /* Number of sequence items */ #define FOR_ITER 93 #define LIST_APPEND 94 // ... #define HAS_ARG(op) ((op) >= HAVE_ARGUMENT) // OpCode.h:166 ``` 现在我们知道OpCode 的数值大于90 就是需要带参数的OpCode ,现在就需要找到OpNumber 的格式到底是怎么样的,来看看`Ceval.c:1167` 行代码 ```c opcode = NEXTOP(); oparg = 0; /* allows oparg to be stored in a register because it doesn't have to be remembered across a full loop */ if (HAS_ARG(opcode)) oparg = NEXTARG(); #define NEXTOP() (*next_instr++) #define NEXTARG() (next_instr += 2, (next_instr[-1]<<8) + next_instr[-2]) ``` 现在可以知道,OpCode 格式如下: ``` 不带参数: OpCode (1 Byte) 带参数: OpCode (1 Byte) | OpNumber (2 Byte) ``` 根据上面得到的信息,可以写一个按照结构生成数据的模块. ```python def opcode_no_opnumber() : # 针对无操作数的指令进行数据生成 opcode = random.randint(0,89) return chr(opcode) def opcode_has_opnumber() : # 针对有操作数的指令进行数据生成 opcode = random.randint(90,104) opnumber1 = random.randrange(0xFF) opnumber2 = random.randrange(0xFF) return chr(opcode) + chr(opnumber1) + chr(opnumber2) def make_opcode_stream(opcode_length = 6) : opcode_stream = '' for index in range(opcode_length) : if random.randint(0,1) : # 50% 的选择概率 opcode_stream += opcode_no_opnumber() else : opcode_stream += opcode_has_opnumber() return opcode_stream ``` 写好了这两个模块之后,还需要修改这些代码 ```python def make_code_object(opcode_data) : compile_code_object = code_object_class() compile_code_object.co_argcount = 0 compile_code_object.co_code = opcode_data compile_code_object.co_consts = tuple(make_random_string_list(3,8),) compile_code_object.co_names = tuple(make_random_string_list(3,8)) compile_code_object.co_varnames = tuple(make_random_string_list(3,8)) return compile_code_object if __name__ == '__main__' : while True : code_object = make_code_object(make_opcode_stream()) save_to_pyc('python_fuzzing.tmp.pyc',code_object) os.system('python python_fuzzing.tmp.pyc') ``` 然后就可以继续跑Fuzzing 了,效果如下 ![](pic2/fuzzing_update_state.png) ![](pic2/fuzzing_update_state_1.png) 往下继续运行,我们还是可以看到Python 运行环境抛出了OpCode 识别失败,我们再回去读读`Opcode.h` 的代码 ```c #define SLICE 30 /* Also uses 31-33 */ #define SLICE_1 31 #define SLICE_2 32 #define SLICE_3 33 #define STORE_SLICE 40 /* Also uses 41-43 */ #define STORE_SLICE_1 41 #define STORE_SLICE_2 42 #define STORE_SLICE_3 43 #define DELETE_SLICE 50 /* Also uses 51-53 */ #define DELETE_SLICE_1 51 #define DELETE_SLICE_2 52 #define DELETE_SLICE_3 53 ``` 原来OpCode 的取值并不是连续的,这就解开了困扰我们的问题,读者们可以拿上面的代码去继续完善 ## 什么时候选择随机生成数据,什么时候选择按结构生成数据 **如果输入是有限制的,那就按结构生成数据,如果输入是无限制的,那就随机生成数据** 输入限制是什么意思呢?不妨来看几个例子 ### Fuzzing WAF 对WAF Fuzzing .攻击点有:SQL Payload 拦截,XSS Payload 拦截,WebShell 拦截,文件目录拦截,系统命令拦截等部分.现在拿出SQL Payload Fuzzing 来说,SQL Payload 是有限的,比如:' ," ,select ,union ,where 等关键字,还有SQL Bypass Payload ,可以参考SQLMAP 的Bypass 套路:https://github.com/sqlmapproject/sqlmap/tree/master/tamper 对于输入是有限的Fuzzing ,**一定要尽可能搜集多的关键字,提高Fuzzing 代码覆盖率** ```python sql_tiny_dict = ['select','from','*','where','order by','desc','asc','insert','top','limit', # SQL 基础语句 'update','delete','set','as','in','create','table','db' '\'','"','%','_', # 特殊符号 '(',')','=','<','>','<>','<=','=>','between','like','+','-','and','or','not','|', # 运算符 'NULL' ] sql_function = [ # 函数 'avg','count','first','last','max','min','sum','ucase','lcase','mid','len','round','now','format', 'ascii','char','nchar','lower','upper','str','ltrim','rtrim','left','right','substring','charindex','patindex', 'quotename','replicate','reverse','replace','space','stuff','cast','convert','day','month','year','dateadd', 'datediff','datename','datepart','getdate','suser_name','user_name','user','show_role','db_name','object_name', 'col_name','col_length','valid_name','charindex','rank','column_name' ] bypass_string = [ # Bypass 关键字 '\'','"', '/*','*','*/','/**/',' ',b'\0','%00','()','//','\\','--','#','--+','-- -',';','#', '%27','%u0027','%u02b9','%u02bc','%u02c8','%u2032','%uff07','%c0%27','%c0%a7','%e0%80%a7', # ' '%20','%u0020','%uff00','%c0%20','%c0%a0','%e0%80%a0', # space '%28','%u0028','%uff08','%c0%28','%c0%a8','%e0%80%a8', # ( '%29','%u0029','%uff09','%c0%29','%c0%a9','%e0%80%a9', # ) '\r','%0D','\n','%0A','%0B','\r\n' ] def make_payload(using_payload_count,using_bypass_count) : output_payload = '' if not random.randint(0,9) : # 10% output_payload += random.choice(['\'','"','%27','%28']) if not random.randint(0,1) : # 5% output_payload += make_space() if not random.randint(0,3) : # 25% output_payload += random.choice(bypass_string) if not random.randint(0,1) : # 12.5% output_payload += make_space() for using_payload_count_index in range(using_payload_count) : if random.randint(0,1) : sql_tiny_element = random.choice(sql_tiny_dict) #fuzzing_entry.random_encode_string( ) bypass_payload = '' for using_bypass_count_index in range(using_bypass_count) : bypass_payload += random.choice(bypass_string) sql_tiny_element += bypass_payload + make_space() else : sql_tiny_element = random.choice(sql_function) if random.randint(0,1) : # 25% for using_bypass_count_index in range(using_bypass_count) : sql_tiny_element += random.choice(bypass_string) sql_tiny_element += '(' + make_argument() + ')' output_payload += sql_tiny_element + make_space() return output_payload def fuzzing() : sql_url = 'https://cloud.tencent.com/?test=' while True : sql_payload = make_payload(random.randint(1,6),random.randint(0,3)) print 'SQL payload :', fuzzing_output.green_output(sql_payload) print 'is pass:' , responed = requests.get(sql_url + sql_payload) if 200 == responed.status_code : fuzzing_output.red_output(str(responed.status_code))# + ' ' + responed.text) else : fuzzing_output.bule_output(str(responed.status_code)) time.sleep(0.5) ``` 运行结果如下,利用这个简单的Fuzzer 还找到了个腾讯云SQL Bypass 的洞. ![](pic2/fuzzing_waf.png) ### Fuzzing Windows 内核 Windows 内核的攻击点有很多,在这里我们只讨论内核函数syscall 的Fuzzing .参考链接:https://github.com/mwrlabs/KernelFuzzer ,https://github.com/tinysec/windows-syscall-table syscall 是有固定格式的:内核函数SSDT 索引号,参数1,参数2 等等.我们来看看KernelFuzzer 里面是怎么样做Fuzzing 输入点的,代码在`bughunt_syscall.asm` 和`bughunt_syscall_x64.asm` 中 ```asm mov ecx, [ebp + 18h] ; main (argv[5]) = dw0x04 push ecx mov ecx, [ebp + 14h] ; main (argv[4]) = dw0x03 push ecx mov ecx, [ebp + 10h] ; main (argv[3]) = dw0x02 push ecx mov ecx, [ebp + 0Ch] ; main (argv[2]) = dw0x01 push ecx ; push argument in stack .. mov eax, [ebp + 08h] ; main (argv[1]) = syscall_uid mov edx, 7FFE0300h call dword ptr [edx] ; call syscall .. ``` 我们再来看看SSDT 内核函数的信息 | name | id32 | id64 | argc32 | argc64 | argcFrom | ------| ------ | ------ | ------ | ------ | ------ | NtAcceptConnectPort | 2 | 2 | 6 | 6 | wow64 | NtAccessCheck | 0 | 0 | 8 | 8 | wow64 | NtAccessCheckAndAuditAlarm | 439 | 41 | 11 | 11 | wow64 | NtAccessCheckByType | 438 | 99 | 11 | 11 | wow64 | NtAccessCheckByTypeAndAuditAlarm | 437 | 89 | 16 | 16 | wow64 | NtAccessCheckByTypeResultList | 436 | 100 | 11 | 11 | wow64 | NtAccessCheckByTypeResultListAndAuditAlarm | 435 | 101 | 16 | 16 | wow64 | NtAccessCheckByTypeResultListAndAuditAlarmByHandle | 434 | 102 | 17 | 17 | wow64 id32 ,id64 指的是32 和64 位平台下的内核函数序号.argc32 和argc64 是指32 和64 位平台下的内核函数参数个数.所以我们可以知道Fuzzing 数据的构造方式了 ``` syscall 2 a,b,c,d,e,f syscall 0 a,b,c,d,e,f,g,h syscall 439 a,b,c,d,e,f,g,h,i,j,k ... ``` 再精细一点来设计这个Fuzzing ,就还需要考虑到句柄,内核缓冲区等各种信息,更多细节在此就不多细说了,感兴趣的读者可以阅读:https://github.com/mwrlabs/KernelFuzzer/blob/master/bughunt_thread.h ### Fuzzing ImageMagick 如果读者已经读过了我之前写过的那篇Fuzzing Imagemagick 的文章,可能你会对这段代码有疑惑,Link:https://github.com/lcatro/Fuzzing-ImageMagick/blob/master/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8Fuzzing%E6%8C%96%E6%8E%98ImageMagick%E7%9A%84%E6%BC%8F%E6%B4%9E.md#5-如何使用libfuzzer-fuzzing-imagemagick ,摘录部分代码 ```c static const struct { char *name; unsigned char *magic; unsigned int length, offset; } StaticMagic[] = { #define MAGIC(name,offset,magic) {name,(unsigned char *)magic,sizeof(magic)-1,offset} MAGIC("WEBP", 8, "WEBP"), MAGIC("AVI", 0, "RIFF"), MAGIC("8BIMWTEXT", 0, "8\000B\000I\000M\000#"), MAGIC("8BIMTEXT", 0, "8BIM#"), MAGIC("8BIM", 0, "8BIM"), MAGIC("BMP", 0, "BA"), MAGIC("BMP", 0, "BM"), MAGIC("BMP", 0, "CI"), MAGIC("BMP", 0, "CP"), MAGIC("BMP", 0, "IC"), MAGIC("BMP", 0, "PI"), MAGIC("CALS", 21, "version: MIL-STD-1840"), MAGIC("CALS", 0, "srcdocid:"), MAGIC("CALS", 9, "srcdocid:"), MAGIC("CALS", 8, "rorient:"), MAGIC("CGM", 0, "BEGMF"), //... }; extern "C" int LLVMFuzzerTestOneInput(const unsigned char* data,unsigned int size) { int random_image_flag_index = random(data,size); unsigned int random_image_flag_offset = StaticMagic[random_image_flag_index].offset; unsigned int random_image_flag_length = StaticMagic[random_image_flag_index].length; unsigned int image_buffer_length = random_image_flag_offset + random_image_flag_length + size; unsigned char* image_buffer = (unsigned char*)malloc(image_buffer_length); memset(image_buffer,0,image_buffer_length); memcpy(image_buffer,StaticMagic[random_image_flag_index].name,StaticMagic[random_image_flag_index].length); FILE* file = fopen(GENARATE_FILE_NAME,"w"); if (NULL != file) { fwrite(image_buffer,1,image_buffer_length,file); fclose(file); printf("buffer=%s(0x%X), size=%d,input format=%s\n",image_buffer,image_buffer,image_buffer_length,StaticMagic[random_image_flag_index].name); ExceptionInfo exception; ImageInfo* read_image_info; ImageInfo* write_image_info; Image* image; GetExceptionInfo(&exception); read_image_info = CloneImageInfo((ImageInfo*)NULL); write_image_info = CloneImageInfo((ImageInfo*)NULL); strlcpy(read_image_info->filename,GENARATE_FILE_NAME,MaxTextExtent); strlcpy(write_image_info->filename,"/dev/null",MaxTextExtent); SetImageInfo(read_image_info,SETMAGICK_READ,&exception); SetImageInfo(write_image_info,SETMAGICK_WRITE,&exception); image = ReadImage(read_image_info,&exception); if (NULL != image) WriteImage(write_image_info,image); DestroyImageInfo(read_image_info); DestroyImageInfo(write_image_info); DestroyExceptionInfo(&exception); } free(image_buffer); return 0; } ``` 这段代码的意思是,随机从`StaticMagic` 中选择一个图像头部特征,然后和libFuzzer 生成的数据拼接到一起.格式如下 ``` 图像格式特征码 | libFuzzer 生成的数据 ``` 为什么要这么做呢?我们需要来阅读一下ImageMagick 的代码.图像格式特征码可以到`MagickCore/magic.c:90` 行找到声明. ```c static const MagicMapInfo MagicMap[] = { { "8BIMWTEXT", 0, MagicPattern("8\000B\000I\000M\000#") }, { "8BIMTEXT", 0, MagicPattern("8BIM#") }, { "8BIM", 0, MagicPattern("8BIM") }, { "BMP", 0, MagicPattern("BA") }, { "BMP", 0, MagicPattern("BM") }, { "BMP", 0, MagicPattern("CI") }, //... { "XEF", 0, MagicPattern("FOVb") }, { "XPM", 1, MagicPattern("* XPM *") } }; ``` ImageMagick 识别图片格式在`MagickCore/magic.c:368` 行找到函数. ```c MagickExport const MagicInfo *GetMagicInfo(const unsigned char *magic, const size_t length,ExceptionInfo *exception) { register const MagicInfo *p; assert(exception != (ExceptionInfo *) NULL); if (IsMagicCacheInstantiated(exception) == MagickFalse) return((const MagicInfo *) NULL); /* Search for magic tag. */ LockSemaphoreInfo(magic_semaphore); ResetLinkedListIterator(magic_cache); p=(const MagicInfo *) GetNextValueInLinkedList(magic_cache); if (magic == (const unsigned char *) NULL) { UnlockSemaphoreInfo(magic_semaphore); return(p); } while (p != (const MagicInfo *) NULL) { assert(p->offset >= 0); if (((size_t) (p->offset+p->length) <= length) && (memcmp(magic+p->offset,p->magic,p->length) == 0)) // 注意这里,判断图片特征码 break; p=(const MagicInfo *) GetNextValueInLinkedList(magic_cache); } if (p != (const MagicInfo *) NULL) (void) InsertValueInLinkedList(magic_cache,0, RemoveElementByValueFromLinkedList(magic_cache,p)); UnlockSemaphoreInfo(magic_semaphore); return(p); } ``` 我们来读一下ImageMagick 的读取图片部分的代码,位置在`MagickCore/constitute.c:410` ```c magick_info=GetMagickInfo(read_info->magick,sans_exception); sans_exception=DestroyExceptionInfo(sans_exception); if (magick_info != (const MagickInfo *) NULL) { // 读取图像信息 // ... if ((magick_info != (const MagickInfo *) NULL) && (GetImageDecoder(magick_info) != (DecodeImageHandler *) NULL)) { if (GetMagickDecoderThreadSupport(magick_info) == MagickFalse) LockSemaphoreInfo(magick_info->semaphore); image=GetImageDecoder(magick_info)(read_info,exception); // ... ``` 由此可知,只有能被ImageMagick 识别到的图像格式才可以被传递到对应的图像解析decoder 里面去解析数据.但是我们并不关心decoder 是怎么样去解析的,所以这部分我们使用随机生成数据.为什么不全部都用随机生成数据呢,这样会导致生成的图像特征码都是随机的,碰撞到正确的图像特征码的概率很低,浪费很多时间和资源在这些没有意义的地方.所以我们需要给定一个区间来让Fuzzer 生成数据,这样才能让ImageMagick 选择到decoder 来解析,如果我们要对所有的图像格式都要做对应的针对适配,**没有统一的格式**,按照特定的格式来生成数据,这样的人力成本太大了,不如让Fuzzer 随机生成数据. ### Fuzzing 网络协议 网络协议都是已经固定好的数据格式,然后由一方发出到另一方来解析执行.我们来回顾一下C 语言的struct 结构 ```c typedef struct { int packet_type; int packet_data_length; char packet_data; } packet ; ``` 这个结构对应的内存布局 ``` packet_type(4 Byte) | packet_data_length(4 Byte) | packet_data(packet_data_length Byte) ``` 根据这个内存布局,我们来举几个例子,(为了方便阅读数据顺序是大端字节) ``` 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x65 0x65 => 0 | 2 | AA 0x00 0x00 0xFF 0x00 0x00 0x00 0x00 0x04 0x01 0x02 0x03 0x04 => 0xFF00 | 4 | 0x01 0x02 0x03 0x04 0x1d 0xb2 0xaa 0x42 0x00 0x21 0xd2 0x23 0x65 => 0x1db2aa42 | 0x21d223 | A ... ``` 前面两个例子是按照格式构造的,最后那个例子是瞎写的.用前面提到的两种方式(随机生成数据和按结构生成数据)来生成Fuzzing 数据,例子如下 ``` 按结构生成数据:random.randint() | random.randint() | random.randstring(4) 随机生成数据:random.randstring(12) ``` 不知道读者们有没有注意到非常重要的一点,这两条例子是等价的.也就是说`random.randint() | random.randint() | random.randstring(4)` 等价于`random.randstring(12)` .所以这个时候按结构生成数据的意义就没有了,我们来看一下ImageMagick 的图片解析部分代码`coders/icon.c:295` ```c icon_file.reserved=(short) ReadBlobLSBShort(image); icon_file.resource_type=(short) ReadBlobLSBShort(image); icon_file.count=(short) ReadBlobLSBShort(image); if ((icon_file.reserved != 0) || ((icon_file.resource_type != 1) && (icon_file.resource_type != 2)) || (icon_file.count > MaxIcons)) ThrowReaderException(CorruptImageError,"ImproperImageHeader"); extent=0; for (i=0; i < icon_file.count; i++) { icon_file.directory[i].width=(unsigned char) ReadBlobByte(image); icon_file.directory[i].height=(unsigned char) ReadBlobByte(image); icon_file.directory[i].colors=(unsigned char) ReadBlobByte(image); icon_file.directory[i].reserved=(unsigned char) ReadBlobByte(image); icon_file.directory[i].planes=(unsigned short) ReadBlobLSBShort(image); icon_file.directory[i].bits_per_pixel=(unsigned short) ReadBlobLSBShort(image); icon_file.directory[i].size=ReadBlobLSBLong(image); icon_file.directory[i].offset=ReadBlobLSBLong(image); if (EOFBlob(image) != MagickFalse) break; extent=MagickMax(extent,icon_file.directory[i].size); } ``` 这部分的代码相当于按照这个格式来读取数据 ``` short | short | short | [ Byte | Byte | Byte | Byte | long | long ] | [ Byte | Byte | Byte | Byte | long | long ] | ... ``` 所以,直接使用random.randstring() 直接生成随机数据传递到这里Fuzzing 即可,无需再按结构生成数据. 回到这个小节提出的问题:那么什么时候选择随机生成数据,什么时候选择按结构生成数据呢?**如果输入是有限制的,那就按结构生成数据,如果输入是无限制的,那就随机生成数据;如果按结构生成数据可以触发更多的代码执行,那就按结构生成数据,否则就使用随机生成数据**. ## 结尾 阅读源码是一个非常有用的技能,**绝大多数的疑惑,都能在源码里面找到**,这是以前在腾讯的一位T3 的同事给我的教诲,受益至今.前面用了很多的篇幅,介绍了Fuzzing 和阅读源码之间的关系是有多么重要.希望这些经验能够帮助在学习挖掘漏洞的读者们.本章是偏向于二进制的Fuzzing 的,关于二进制还有一些其他的Fuzzing 经验和大家分享. ![](pic2/tips.png) ## AFL 和libFuzzer 的演示 多开AFL Fuzzing 库 ![](pic2/afl.png) 跑到内存异常的libFuzzer ![](pic2/libfuzzer.png) ================================================ FILE: 3.Fuzzing 模糊测试之异常检测.md ================================================ ## 必备工具 Python ,Python-face_recognition ,PHP ,Python-Requests ,Pydasm ,Pydbg ## Fuzzing 的异常检测 上一章的结尾部分提到,程序运行包含:输入->解析->内部逻辑处理->数据组装->输出,每一个部分都可能存在漏洞.总结一下Fuzzing 的异常检测分为两种:检测输出数据异常和检测运行异常. ### 检测输出数据异常 程序需要依赖用户输入,然后进行处理,最后再输出程序处理的数据.以人脸识别为例子,我们同样可以使用Fuzzing 来让识别算法判断两个人脸为同一个人.先来看一下Python 库`face_recognition` 是怎么做识别的. ```python def compare_faces(known_face_encodings, face_encoding_to_check, tolerance=0.6): """ Compare a list of face encodings against a candidate encoding to see if they match. :param known_face_encodings: A list of known face encodings :param face_encoding_to_check: A single face encoding to compare against the list :param tolerance: How much distance between faces to consider it a match. Lower is more strict. 0.6 is typical best performance. :return: A list of True/False values indicating which known_face_encodings match the face encoding to check """ return list(face_distance(known_face_encodings, face_encoding_to_check) <= tolerance) ``` compare_faces() 函数根据face_distance() 函数对比两张图片的相似度做对比,当相似度小于默认值0.6 时,数值越小两张人脸越相同,即判断为同一个人.让两张不是同一个人的识别成为同一个人,本质上是干扰人脸识别算法的判断概率.**只需要对每一个像素点做极小的修改,就能影响数据点被分类的结果.必定存在一个与原本数据相差极小而被判断为任意一个类别的数据**.简单地来说,通过在脸部附近随机生成一些像素点,会影响到最终识别结果.Fuzzer 构造如下 ```python import random from numpy import array from PIL import Image from PIL import ImageDraw import face_recognition def load_image_encoding(file_path) : image = face_recognition.load_image_file(file_path) image_encoding = face_recognition.face_encodings(image)[0] return image_encoding def get_face_location(file_path) : image = face_recognition.load_image_file(file_path) face_location = face_recognition.face_locations(image)[0] return face_location def fuzzing(source_image_path = 'obama/obama.jpg',fuzzing_image_path = 'ponyma/ponyma.png',target_compare_rate = 0.4) : source_face_data = load_image_encoding(source_image_path) image = Image.open(fuzzing_image_path) draw = ImageDraw.Draw(image) last_best_compare_rate = 1 face_location = get_face_location(fuzzing_image_path) # 获取人脸位置 random_fuzzing_location_top = face_location[0] random_fuzzing_location_bottom = face_location[2] random_fuzzing_location_left = face_location[3] random_fuzzing_location_right = face_location[1] while True : random_pixel_data = (random.randint(0,255), # 随机像素值 random.randint(0,255), random.randint(0,255)) random_location = (random.randint(random_fuzzing_location_left,random_fuzzing_location_right), # 随机位置 random.randint(random_fuzzing_location_top,random_fuzzing_location_bottom)) last_pixel_data = image.getpixel(random_location) draw.point(random_location,random_pixel_data) # 在人脸的区域上随机画一个像素 fuzzing_face_image = image.convert('RGB') fuzzing_face_array = array(fuzzing_face_image) fuzzing_face_data = face_recognition.face_encodings(fuzzing_face_array)[0] compare_rate = face_recognition.face_distance([source_face_data],fuzzing_face_data) # 重新对比两个人脸 print 'Compare Rate =',compare_rate,' Random Location =',random_location,' Random Pixel Data =',random_pixel_data del fuzzing_face_image # 防止内存泄漏 .. del fuzzing_face_array del fuzzing_face_data if compare_rate < target_compare_rate : # 两张人脸识别结果的判断率达到要求之后,就退出 break if compare_rate < last_best_compare_rate : # 如果随机的像素没有提升识别率,那就恢复原来的那个像素,如果有提升识别率,那就保存这个像素 last_best_compare_rate = compare_rate print 'New Study Rate:' ,last_best_compare_rate else : draw.point(random_location,last_pixel_data) image.save(fuzzing_image_path + '_bypass_check.jpg') obama_image = face_recognition.load_image_file('obama/obama.jpg') obama1_image = face_recognition.load_image_file('obama/obama2.jpg') ponyma_image = face_recognition.load_image_file('ponyma/ponyma.png') ponyma1_image = face_recognition.load_image_file('ponyma/ponyma.png_bypass_check.jpg') obama_encoding = face_recognition.face_encodings(obama_image)[0] obama1_encoding = face_recognition.face_encodings(obama1_image)[0] ponyma_image = face_recognition.face_encodings(ponyma_image)[0] ponyma1_image = face_recognition.face_encodings(ponyma1_image)[0] print 'Obama Test:' print face_recognition.compare_faces([obama_encoding], obama1_encoding),\ face_recognition.face_distance([obama_encoding], obama1_encoding) print 'PonyMa test:' print face_recognition.compare_faces([obama_encoding], ponyma_image),\ face_recognition.face_distance([obama_encoding], ponyma_image) print 'PonyMa Bypass test:' print face_recognition.compare_faces([obama_encoding], ponyma1_image),\ face_recognition.face_distance([obama_encoding], ponyma1_image) print 'Ready Fuzzing ..' fuzzing() ``` 运行过程与结果 ![](pic3/study.gif) ![](pic3/study2.gif) ![](pic3/valid.png) 看到这里,相信读者应该理解到:**用户的输入是不可信的,精心构造的输入会影响程序的输出结果甚至远程代码执行**.下面再举一个SQL 注入的例子,先来看看页面的源码 ```php '.mysql_error().''); while($row = mysql_fetch_array($result)) { echo $row['0'] . ' ' . $row['1']; echo '
'; } echo '
'; echo $query; mysql_close($connect); ?> ``` 访问URL:http://127.0.0.1/sql.php?name=root ,页面返回的数据如下 ![](pic3/php_sql_result1.png) 此时我们在root 后面加上" ,URL 变为:http://127.0.0.1/sql.php?name=root" ,再次访问链接 ![](pic3/php_sql_result2.png) 可以看到,现在SQL 语句出现异常了.对于URL 进行做SQL 注入Fuzzing ,我们通过`' "` 这两个符号插入到URL 的参数里来判断能否导致SQL 字符串闭合异常.Fuzzer 代码如下: ```python import sys import requests def get_url_path(url) : argument_offset = url.find('?') if -1 == argument_offset : return False url = url[ : argument_offset ] return url def get_url_argument(url) : argument_offset = url.find('?') if -1 == argument_offset : return False url = url[ argument_offset + 1 : ] argument = url.split('&') return argument def check_url_inject(url_path,url_argument) : sql_inject_flag = [ '\'' , '"' ] sql_inject_error_flag = 'SQL syntax' inject_result = False for url_argument_index in url_argument : for sql_inject_flag_index in sql_inject_flag : responed = requests.get(url_path + '?' + url_argument_index + sql_inject_flag_index) if not -1 == responed.text.find(sql_inject_error_flag) : inject_result = True break return inject_result if __name__ == '__main__' : if not 2 == len(sys.argv) : print 'sql.py URL' exit() url = sys.argv[1] url_argument = get_url_argument(url) url_path = get_url_path(url) print 'Is Inject:' ,check_url_inject(url_path,url_argument) ``` 运行结果如下 ![](pic3/sql_inject.png) 同样的原理,我们来做WAF 检测,示例URL:https://cloud.tencent.com/ .如果URL 里面还有敏感Payload 时,腾讯云的WAF 会返回501 的回应,否则会返回正常的请求 ```python import sys import requests def check_waf(url_path) : responed = requests.get(url_path + '?test=