学习https://docs.angr.io/ 中cure concepts的学习笔记和一些思考
前言
这次想学习angr的主要目的是想要做一些data-flow analysis,因为想到了一个关于iot漏洞检测的一个比较有意思的idea,想要自己先试验一下。
docs.angr.io提供了一些关于core concept的解释,希望可以通过将这些core concept应用在这个mips binary的数据流分析上。
核心概念
加载
加载二进制文件过程中,可以通过一些API判断加载的起始地址和最终地址,并判断这个二进制文件是有可执行栈还是PIE。
factory
这个不知道怎么翻译了…,可能指的是模块。angr有很多模块,例如blocks,可以提供基本块的解析,并且解析出指令内容,指令地址,指令数量。
state
此外,angr在加载完文件之后只是一个”被初始化的image”,在模拟执行的过程中,需要一些state。因此angr内部还包括模拟的程序状态,SimState
。
在state中,所有数字都是用bitvec表示的,这一点在后面也会提到。除此以外,程序还需要有内存,这就是mem
。
对于mem
的数据,使用resolved
可以将其变为bitvec
,使用type
可以将其解释称不同的类型。(原来angr也有state,一直以为只有类似unicorn的模拟执行工具才会有)
然而有的时候当我们查看运行中程序的内存时,会发现显示的并不是一个数值,而是一个符号。因此这也被称为符号执行。下面的rdi寄存器储存的,就是一个符号。
simulation manager
上述的state只是表示了一个具体时刻的程序状态,如果想要真正运行起来,完成完整的模拟,还有一个重要的概念是Simulation Managers。下面是建立一个simulation
的具体方式,一个simulation可以接受一个或者一个list的states。
使用step()
方法完成一次符号执行
接下来看寄存器、内存就可以发现部分内容发生了变化
最后,angr
的相关API文档都在API Reference - angr documentation中,可以方便查阅
加载二进制文件
angr中,负责加载二进制的部分叫做CLE
(全称是CLE Loads Everything),它负责将binary以及其相关的链接库加载到angr中进行分析。
使用loader.all_projects
可以得到所有被CLE加载的内容
还可以通过main_object
和shared_object
找到关于主函数和一些共享库的映射。这里由于架构问题,导致没有成功解析出共享库。
通过指定每一个object,可以确定一些object特殊相关的性质。然而,在mips32架构下,似乎angr无法解析PLT。
通过寻找symbols的方式可以找到memset的符号位置,但是这个地址应该不是binary中的。
疑问:那么SaTC是怎么找到一些sink函数的,可能需要看他们的代码
加载选项
在使用默认加载方式之外,还可以指定一些加载选项,有以下几种
主要是backend比较难理解,这个指的是对于不同文件类型选择的不同加载方式。例如ELF、PE、mach-o格式的文件。例如image文件可能不包含文件类型信息,需要手动指定。
function hook
由于调用libc比较麻烦,并且angr是基于python的,直接call c语言函数牵涉到链接、重定位等。因此angr用python重写了很多库函数,并用他们来代替原先c库函数,成为generic stub
。这些函数执行完成将返回一个symbolic value
。但是只有部分函数可以被替代。一些比较复杂额例如malloc依然是无法替代的。
这一函数替代技术也被称为hook
。angr在符号执行时,每一步都会检查当前地址是否被hook过,如果是,将会执行hook过后的地址的指令。
1 | # 使用SIM_procedures,获取其中的object |
除此以外,还可以hook一些symbols对应的函数。这将我们hook一些危险api的使用变成现实。
1 | proj.hook_symbol(name, hook) |
符号执行、约束求解
底层
在angr底层,采用符号表示一个输入,那么符号是什么?他就是BitVec
。BitVec表示不同比特大小的一段内存空间。对于BitVec的操作并不会直接计算,而是转化为抽象参数树,并以表达式的形式参与运算
1 | x = state.solver.BVS("x", 64) |
注意计算操作符下,生成的结果仍然是bitVec(上述输出的BV64符号),但是比较操作符下,结果将变成boolean
类型的。注意这里的比较默认是unsigned
。
1 | x + y == one_hundred + 5 |
约束求解
这里和z3比较像,通过加入一些限制条件,计算出在此条件下可以得到结果的输入。下面就是一个简单的解方程。
通过检查state.satisfiable()可以砍断state是否可解
注意到上述只采用eval
作为求解方式,我们也可以使用别的,如下。
模拟运行的内存 、寄存器
angr提供了非常方便的内存操作,如下。resolved
表示获取某个内存或者寄存器中的数值。
angr再碰到分支语句时,会产生两种不同的state,并且用符号来表示每一种state的限制。下面是一个例子,源代码如下,是一个含有后门的验证函数。
1 | int authenticate(char *username, char *password) |
对应的,我们可以查看在不同分支语句下(这里就是strcmp语句所在的if会产生两个分支),然后查看其中的内存。单步运行直到产生分支语句,之后使用eval
或者dump
可以获得到达每个分支的约束条件
不同的states
在上面的例子中,我们使用entry_state
来创建了一个指向二进制程序入口点的state
。此外,还有以下几种不同的states。
- .blank_state(),创建一个大多数数据都没有被初始化的state,这个时候所有访问数据都会返回
uninitialized
。(不清楚是干什么用的) - .entry_state(),建立一个可以运行binary的entry point的state
- .full_init_state(),首先完成一系列初始化工作,例如加载动态链接库等,之后再指向
entry
。 - .call_state(),建立一个准备好执行指定函数的状态(这个可能很有用,可以单独测试函数)
为了初始化上述state,可以执行以下参数
- 地址,不多说了
argc,argv
,也就是命令行参数,可以通过env的一个list传递给上述states。- 参数,在调用
call_state
时,使用.call_state(addr, arg1, arg2, ...)
。addr是指被调用函数的地址,如果要传入指针,需要用到例如angr.PointerWrapper("point to me!")
。
state option
对于不同状态的处理,angr提供了一些有用的option。List of State Options - angr documentation例如
- LAZY_SOLVES, 直到执行完全之后才检查是否满足可解的约束
- ABSTRACT_SOLVER, 允许简化过程中分离约束
此外,有一些options sets,里面包含了很多功能类似的options。例如
- simplification: 在运算过程中使用z3的优化
- refs, 疾苦angr的mem,reg等内容的记录
使用如下命令开启或者关闭options。
1 | # This change to the settings will be propagated to all successor states created from this state after this line. |
state plugins
之前讲到的mem,regs,solvers等,都是state plugins的一部分。此外,还有history和callstack插件,可以用来查看函数调用的特征。
Machine State - memory, registers, and so on - angr documentation
state也支持merging,将两个状态合并并不会发生覆盖,而是使变量变为既A又B的状态
1 | # merge will return a tuple. the first element is the merged state |
sumulation managers
在模拟运行过程中,由simulation manager
管理各个状态。如下所示(懒得翻译了==)这里的stash就是一些满足特定状态的state的集合。
这里也提到一个explore
大法。通过explore,并指定find=addr时,可以找到所有能够到达某地址的执行流程,放入found
stash中,也可以指定avoid
参数,那么也会有一个avoid stash。
我们也可以通过插件的方式指定simulation manager的行为。使用simgr.use_technique(tech)
来指定tech,从而实现定制化模拟执行方式(?),一些可选项例如DFS,内存监视(防止剩余内存过少),spiller用来在active内存过多的时候放弃一些,防止占用过大等。Simulation Managers - angr documentation
模拟和插桩
successor的属性
在模拟运行中,我们可以设置一系列断点和监视点。在模拟过程中,successor有一些属性,例如unsat_successors
表示这个后继block不能满足他的前置约束条件,flat_successors
表示由于符号化指针的出现,需要堆此successor进行一个up to 256的可能结果计算。
断点
在angr中,可以很方便的对寄存器、内存读写、系统调用等进行插桩,如下。
1 | # This will break before a memory write if 0x1000 is a possible value of its target expression |
例如我们想去分析所有websgetvar()函数,一般而言对于arm,mips来说是直接对寄存器指向的地址写入,那么可以先识别接受输入的api(或许这就是signature的作用!),之后设置观察点来debug。我们还可以设置条件断点,下面是条件断点的一个例子,他判断当前状态的寄存器rax是否是AAAA,不过应该指的是内存中的rax指向的位置。
1 | # this is a complex condition that could do anything! In this case, it makes sure that RAX is 0x41414141 and |
Analysis
这里主要是如何用angr写自己的分析框架。这一部分放到单独一篇来写!Writing Analyses - angr documentation
总结
阅读完angr core,了解到angr本质上是一个模拟器,是一个静态的模拟器,给我的感觉类似于一个支持多架构翻译的虚拟机,并且在得到虚拟机的指令内容之后,可以利用符号的方式模拟执行(simulate),并在分支位置设置新的状态(state)。每一个状态就是程序执行到某一个汇编位置时的观察点,代表了某一时刻程序的状态。
在此基础上,angr设置了断点、约束求解、函数hook等等方便我们对观察点位置的程序进行分析的工具。我的目的是backward taint analysis,目前看来angr没有提供很好的接口,需要后续自己尝试