angr-basic

学习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,可以提供基本块的解析,并且解析出指令内容,指令地址,指令数量。

image-20230802114515776

state

此外,angr在加载完文件之后只是一个”被初始化的image”,在模拟执行的过程中,需要一些state。因此angr内部还包括模拟的程序状态,SimState

image-20230802114708883

image-20230802114705175

在state中,所有数字都是用bitvec表示的,这一点在后面也会提到。除此以外,程序还需要有内存,这就是mem

image-20230802114941840

对于mem的数据,使用resolved可以将其变为bitvec,使用type可以将其解释称不同的类型。(原来angr也有state,一直以为只有类似unicorn的模拟执行工具才会有)

然而有的时候当我们查看运行中程序的内存时,会发现显示的并不是一个数值,而是一个符号。因此这也被称为符号执行。下面的rdi寄存器储存的,就是一个符号。

image-20230802115213341

simulation manager

上述的state只是表示了一个具体时刻的程序状态,如果想要真正运行起来,完成完整的模拟,还有一个重要的概念是Simulation Managers。下面是建立一个simulation的具体方式,一个simulation可以接受一个或者一个list的states。

image-20230802115636936

使用step()方法完成一次符号执行

image-20230802115808785

接下来看寄存器、内存就可以发现部分内容发生了变化

image-20230802115847445

最后,angr的相关API文档都在API Reference - angr documentation中,可以方便查阅

加载二进制文件

angr中,负责加载二进制的部分叫做CLE(全称是CLE Loads Everything),它负责将binary以及其相关的链接库加载到angr中进行分析。

使用loader.all_projects可以得到所有被CLE加载的内容

image-20230801134051884

还可以通过main_objectshared_object找到关于主函数和一些共享库的映射。这里由于架构问题,导致没有成功解析出共享库。

image-20230801134411503

通过指定每一个object,可以确定一些object特殊相关的性质。然而,在mips32架构下,似乎angr无法解析PLT。

image-20230801134958928

image-20230801135007433

通过寻找symbols的方式可以找到memset的符号位置,但是这个地址应该不是binary中的。

image-20230801135154222

疑问:那么SaTC是怎么找到一些sink函数的,可能需要看他们的代码

加载选项

在使用默认加载方式之外,还可以指定一些加载选项,有以下几种

image-20230801140114101

主要是backend比较难理解,这个指的是对于不同文件类型选择的不同加载方式。例如ELF、PE、mach-o格式的文件。例如image文件可能不包含文件类型信息,需要手动指定。

image-20230801140318629

function hook

由于调用libc比较麻烦,并且angr是基于python的,直接call c语言函数牵涉到链接、重定位等。因此angr用python重写了很多库函数,并用他们来代替原先c库函数,成为generic stub。这些函数执行完成将返回一个symbolic value。但是只有部分函数可以被替代。一些比较复杂额例如malloc依然是无法替代的。

这一函数替代技术也被称为hook。angr在符号执行时,每一步都会检查当前地址是否被hook过,如果是,将会执行hook过后的地址的指令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 使用SIM_procedures,获取其中的object
stub_func = angr.SIM_PROCEDURES['stubs']['ReturnUnconstrained']
# 设置0x10000地址对应的hook函数为stub_func
proj.hook(0x10000, stub_func()) # hook with an instance of the class
# 检查是否hook过
proj.is_hooked(0x10000)
>> True
## 自定义函数进行hook
@proj.hook(0x20000, length=5)
def my_hook(state):
state.regs.rax = 1

proj.is_hooked(0x20000)
>> True

除此以外,还可以hook一些symbols对应的函数。这将我们hook一些危险api的使用变成现实。

1
proj.hook_symbol(name, hook)

符号执行、约束求解

底层

在angr底层,采用符号表示一个输入,那么符号是什么?他就是BitVec。BitVec表示不同比特大小的一段内存空间。对于BitVec的操作并不会直接计算,而是转化为抽象参数树,并以表达式的形式参与运算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
x = state.solver.BVS("x", 64)
y = state.solver.BVS("y", 64)
tree = (x + 1) / (y + 2)
>> <BV64 (x_9_64 + 0x1) / (y_10_64 + 0x2)>
# 输出operand,直接输出表示输出顶层的
print(tree.op)
>> '__floordiv__'
print(tree.args)
>> (<BV64 x_9_64 + 0x1>, <BV64 y_10_64 + 0x2>)
# 输出子层的operand
print(tree.args[0].op)
>> '__add__'
# 输出计算表达式的中间参数
print(tree.args[0].args[1].args)
>> (1, 64)

注意计算操作符下,生成的结果仍然是bitVec(上述输出的BV64符号),但是比较操作符下,结果将变成boolean类型的。注意这里的比较默认是unsigned

1
2
3
4
x + y == one_hundred + 5
>> <Bool (x_9_64 + y_10_64) == 0x69>
## 有符号比较
one_hundred.SGT(-5)

约束求解

这里和z3比较像,通过加入一些限制条件,计算出在此条件下可以得到结果的输入。下面就是一个简单的解方程。

image-20230801150132640

通过检查state.satisfiable()可以砍断state是否可解

image-20230801150313209

注意到上述只采用eval作为求解方式,我们也可以使用别的,如下。

image-20230801150406295

模拟运行的内存 、寄存器

angr提供了非常方便的内存操作,如下。resolved表示获取某个内存或者寄存器中的数值。

image-20230802120614747

angr再碰到分支语句时,会产生两种不同的state,并且用符号来表示每一种state的限制。下面是一个例子,源代码如下,是一个含有后门的验证函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int authenticate(char *username, char *password)
{
char stored_pw[9];
stored_pw[8] = 0;
int pwfile;

// evil back d00r
if (strcmp(password, sneaky) == 0) return 1;

pwfile = open(username, O_RDONLY);
read(pwfile, stored_pw, 8);

if (strcmp(password, stored_pw) == 0) return 1;
return 0;

}

对应的,我们可以查看在不同分支语句下(这里就是strcmp语句所在的if会产生两个分支),然后查看其中的内存。单步运行直到产生分支语句,之后使用eval或者dump可以获得到达每个分支的约束条件

image-20230802123216589

image-20230802123940509

不同的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
2
3
4
5
6
# This change to the settings will be propagated to all successor states created from this state after this line.
s.options.add(angr.options.LAZY_SOLVES)
# Create a new state with lazy solves enabled
s = proj.factory.entry_state(add_options={angr.options.LAZY_SOLVES})
# Create a new state without simplification options enabled
s = proj.factory.entry_state(remove_options=angr.options.simplification)

state plugins

之前讲到的mem,regs,solvers等,都是state plugins的一部分。此外,还有history和callstack插件,可以用来查看函数调用的特征。

Machine State - memory, registers, and so on - angr documentation

state也支持merging,将两个状态合并并不会发生覆盖,而是使变量变为既A又B的状态

1
2
3
4
5
6
7
# merge will return a tuple. the first element is the merged state
# the second element is a symbolic variable describing a state flag
# the third element is a boolean describing whether any merging was done
>>> (s_merged, m, anything_merged) = s1.merge(s2)

# this is now an expression that can resolve to "AAAA" *or* "BBBB"
>>> aaaa_or_bbbb = s_merged.mem[0x1000].uint32_t

sumulation managers

在模拟运行过程中,由simulation manager管理各个状态。如下所示(懒得翻译了==)这里的stash就是一些满足特定状态的state的集合。

image-20230802135304163

这里也提到一个explore大法。通过explore,并指定find=addr时,可以找到所有能够到达某地址的执行流程,放入found stash中,也可以指定avoid参数,那么也会有一个avoid stash。

image-20230802135840129

image-20230802135939986

我们也可以通过插件的方式指定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
2
3
4
5
6
7
8
# This will break before a memory write if 0x1000 is a possible value of its target expression
>>> s.inspect.b('mem_write', mem_write_address=0x1000)

# This will break before a memory write if 0x1000 is the *only* value of its target expression
>>> s.inspect.b('mem_write', mem_write_address=0x1000, mem_write_address_unique=True)

# This will break after instruction 0x8000, but only 0x1000 is a possible value of the last expression that was read from memory
>>> s.inspect.b('instruction', when=angr.BP_AFTER, instruction=0x8000, mem_read_expr=0x1000)

例如我们想去分析所有websgetvar()函数,一般而言对于arm,mips来说是直接对寄存器指向的地址写入,那么可以先识别接受输入的api(或许这就是signature的作用!),之后设置观察点来debug。我们还可以设置条件断点,下面是条件断点的一个例子,他判断当前状态的寄存器rax是否是AAAA,不过应该指的是内存中的rax指向的位置。

1
2
3
4
5
6
# this is a complex condition that could do anything! In this case, it makes sure that RAX is 0x41414141 and
# that the basic block starting at 0x8004 was executed sometime in this path's history
>>> def cond(state):
... return state.eval(state.regs.rax, cast_to=str) == 'AAAA' and 0x8004 in state.inspect.backtrace

>>> s.inspect.b('mem_write', condition=cond)

Analysis

这里主要是如何用angr写自己的分析框架。这一部分放到单独一篇来写!Writing Analyses - angr documentation

总结

阅读完angr core,了解到angr本质上是一个模拟器,是一个静态的模拟器,给我的感觉类似于一个支持多架构翻译的虚拟机,并且在得到虚拟机的指令内容之后,可以利用符号的方式模拟执行(simulate),并在分支位置设置新的状态(state)。每一个状态就是程序执行到某一个汇编位置时的观察点,代表了某一时刻程序的状态。

在此基础上,angr设置了断点、约束求解、函数hook等等方便我们对观察点位置的程序进行分析的工具。我的目的是backward taint analysis,目前看来angr没有提供很好的接口,需要后续自己尝试

文章目录
  1. 1. 前言
    1. 1.1. 核心概念
      1. 1.1.1. 加载
      2. 1.1.2. factory
      3. 1.1.3. state
      4. 1.1.4. simulation manager
    2. 1.2. 加载二进制文件
      1. 1.2.1. 加载选项
      2. 1.2.2. function hook
    3. 1.3. 符号执行、约束求解
      1. 1.3.1. 底层
      2. 1.3.2. 约束求解
    4. 1.4. 模拟运行的内存 、寄存器
      1. 1.4.1. 不同的states
      2. 1.4.2. state option
      3. 1.4.3. state plugins
    5. 1.5. sumulation managers
      1. 1.5.1. 模拟和插桩
      2. 1.5.2. successor的属性
      3. 1.5.3. 断点
    6. 1.6. Analysis
  2. 2. 总结
|