开篇
今天我们来介绍一款python实现的二进制分析工具 — angr,由加州大学圣巴巴拉分校的计算机安全实验室开发。
angr是一个支持多CPU架构的二进制分析python工具包,可以对二进制文件进行各种静态分析,以及具有进行动态符号执行的能力,比如:
- 将二进制代码反汇编为中间表示(IR,Intermediate Representation);
- 程序插桩;
- 符号执行;
- 控制流分析;
- 数据依赖性分析;
- 值集分析(VSA,Value Set Analysis);
- …
angr项目的目标是创建一个用户友好的二进制分析套件,允许用户简单地启动iPython并通过几个命令就可以轻松执行复杂的二进制分析。
要以编程的方式分析二进制文件,通常按照以下几个步骤,大致是:
1.将二进制文件载入分析程序;
2.将二进制转换为中间表示(IR);
3.进行实际分析,比如:
- 部分或完整程序静态分析,比如依赖分析,程序切片等等;
- 对程序状态空间的象征性探索,比如模拟执行直到发现溢出漏洞;
- 上述方法的组合,比如只模拟执行导致内存写入的程序片段,以便找到溢出漏洞。
angr提供了各种组件来解决上面的各个步骤,接下来我们从安装开始,并通过示例来讲解下angr的能力。
安装
angr是Python 3.8+的库,因此必须安装到Python环境中才能使用。在安装之前我们可以通过conda创建一个python 3.10的虚拟环境:
conda create --name py310 python=3.10
conda activate py310
然后通过pip安装angr:
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple angr
核心概念
要开始使用angr,我们需要对一些基本的angr概念,以及如何构造一些基本的angr对象有一个基本的了解。
Project对象
使用angr的第一个操作总是将二进制文件加载进来,如下:
>>> import angr
>>> proj = angr.Project('/bin/true')
Project对象是angr中最先创建的,也是最基础的对象,angr中其它类型的对象都依赖于该对象。有了它,我们才能在加载的二进制文件上进行分析和模拟。
Project对象中包含一些基本属性,这些属性说明了加载的二进制文件的一些基本信息:
import monkeyhex # 以十六进制方式查看打印的数字
>>> proj.arch # 二进制文件所属的CPU架构,为archinfo.Arch对象的实例
<Arch AMD64 (LE)>
>>> proj.arch.name # CPU架构名称
'AMD64'
>>> proj.arch.bits # CPU架构的位数,此处为64位
0x40
>>> proj.entry # 二进制文件的程序入口点地址
0x401670
>>> proj.filename # 二进制文件名
'/bin/true'
Loader加载器
获取静态的二进制文件在内存虚拟地址空间中的表示是相当复杂的,angr内部有一个叫做CLE的模块来处理这个问题。CLE模块执行的结果称为加载器,可以在proj.loader 属性中找到。我们可以用它来查看与你的程序一起加载的共享库,以及对加载后的地址空间执行基本查询:
>>> proj.loader # 查看加载的二进制对象和其内存映射区域的起始和终止地址
<Loaded true, maps [0x400000:0x5004000]>>>> proj.loader.shared_objects # 查看链接的共享库,以及各共享库的内存映射起始和终止地址
{'ld-linux-x86-64.so.2': <ELF Object ld-2.24.so, maps [0x2000000:0x2227167]>,'libc.so.6': <ELF Object libc-2.24.so, maps [0x1000000:0x13c699f]>}>>> proj.loader.min_addr # 查看加载的二进制对象的内存映射区域的起始地址
0x400000
>>> proj.loader.max_addr # 查看加载的二进制对象的内存映射区域的终止地址
0x5004000>>> proj.loader.main_object # 查看加载的主对象和它的内存映射区域的起始地址
<ELF Object true, maps [0x400000:0x60721f]>>>> proj.loader.main_object.execstack # 查看主对象的栈区域是否是可执行的
False
>>> proj.loader.main_object.pic # 该主对象是否是位置无关的(位置无关的代码是一种在内存中可以加载到任意位置并且仍然能够正确执行的代码)
True
Factory工厂
在angr中有很多类,它们中的大多数都需要一个project对象来实例化。angr提供了 proj.factory属性,基于该属性可以创建多个需要经常使用的对象。
Block对象
block对象用于从给定地址开始,提取一个指令基本块,该对象中包含了该基本块的很多有用信息。
>>> block = proj.factory.block(proj.entry) # 从程序的入口点地址开始,进行反编译,并获取第一个指令基本块
<Block for 0x401670, 42 bytes>>>> block.pp() # 输出该基本块的指令
0x401670: xor ebp, ebp
0x401672: mov r9, rdx
0x401675: pop rsi
0x401676: mov rdx, rsp
0x401679: and rsp, 0xfffffffffffffff0
0x40167d: push rax
0x40167e: push rsp
0x40167f: lea r8, [rip + 0x2e2a]
0x401686: lea rcx, [rip + 0x2db3]
0x40168d: lea rdi, [rip - 0xd4]
0x401694: call qword ptr [rip + 0x205866]>>> block.instructions # 该基本块的指令数量
0xb
>>> block.instruction_addrs # 该基本块中所有指令的地址
[0x401670, 0x401672, 0x401675, 0x401676, 0x401679, 0x40167d, 0x40167e, 0x40167f, 0x401686, 0x40168d, 0x401694]
State状态
Project对象只代表程序的“初始化状态”。当使用angr模拟执行时,你可以获得一个代表模拟程序状态的特定对象—SimState对象 。
>>> state = proj.factory.entry_state()
<SimState @ 0x401670>
SimState对象包含程序的内存、寄存器、文件系统数据,可以使用state.regs 和 state.mem 来访问此状态的寄存器和内存:
>>> state.regs.rip # 获取rip寄存器中的值,即当前执行的指令地址
<BV64 0x401670>
>>> state.regs.rax # 获取rax寄存器中的值
<BV64 0x1c>
>>> state.mem[proj.entry].int.resolved # 获取内存中程序入口点地址起始的4字节的内容,以int方式解释该内容
<BV32 0x8949ed31>
Simulation Managers模拟管理器
如果一个SimState对象允许我们在给定的时间点表示一个程序,那么一定有一种方法可以让它到达下一个时间点。模拟管理器是angr中的主要接口,用于进行模拟执行。作为一个简短的介绍,让我们展示如何使用我们前面创建的状态对象,向前模拟执行推进几个基本块。
首先,我们创建将要使用的模拟管理器。构造函数可以接受一个状态对象或状态对象列表。
>>> simgr = proj.factory.simulation_manager(state)
<SimulationManager with 1 active>
>>> simgr.active
[<SimState @ 0x401670>]
一个模拟管理器可以包含多种状态,默认的是active类状态,该类状态由我们传入的状态对象初始化。接下来,我们可以进行模拟执行了:
>>> simgr.step()
我们刚刚进行了一个基本块的符号执行,现在可以再次查看active的状态对象,注意到它已经更新,而且它没有修改我们的原始状态,而是创建了一个新的状态对象。SimState对象在执行时被视为不可变的,是一个不可变对象。
>>> simgr.active
[<SimState @ 0x1020300>]
>>> simgr.active[0].regs.rip
<BV64 0x1020300>
>>> state.regs.rip
<BV64 0x401670>
Analyses分析
angr预先内置了多个个分析模块,可以使用它们从程序中提取有用的信息。它们是:
>>> proj.analyses. # 使用tab显示所有的分析模块proj.analyses.BackwardSlice proj.analyses.CongruencyCheck proj.analyses.reload_analysesproj.analyses.BinaryOptimizer proj.analyses.DDG proj.analyses.StaticHookerproj.analyses.BinDiff proj.analyses.DFG proj.analyses.VariableRecoveryproj.analyses.BoyScout proj.analyses.Disassembly proj.analyses.VariableRecoveryFastproj.analyses.CDG proj.analyses.GirlScout proj.analyses.Veritestingproj.analyses.CFG proj.analyses.Identifier proj.analyses.VFGproj.analyses.CFGEmulated proj.analyses.LoopFinder proj.analyses.VSA_DDGproj.analyses.CFGFast proj.analyses.Reassembler
举一个例子,下面是如何构建和使用快速控制流图:
>>> proj = angr.Project('/bin/true', auto_load_libs=False)
>>> cfg = proj.analyses.CFGFast()
<CFGFast Analysis Result at 0x2d85130>>>> cfg.graph
<networkx.classes.digraph.DiGraph at 0x2da43a0>
>>> len(cfg.graph.nodes())
951>>> entry_node = cfg.get_any_node(proj.entry)
>>> len(list(cfg.graph.successors(entry_node)))
2
总结
二进制分析是一个庞大又复杂的领域,里面有很多吸引人的内容,包括漏洞分析、逆向工程、安全研究等。angr 的出现为研究人员和安全专业人员提供了一个有力的工具,可以更好地理解和分析二进制程序。
本文介绍了angr的一些基本概念和用法,这些内容只是触及这个强大工具的表面而已。因此本文算作抛砖引玉吧,感兴趣的同学可以进一步阅读它的官方文档来学习更高级的用法,并将angr融入到二进制分析的工作中。