深入浅出angr--angr架构

深入浅出angr–angr架构

简介

angr作为符号执行的工具,集成了过去的许多分析方式,它不仅能进行动态符号执行,而且还能进行很多静态分析,他在分析二进制程序中能发挥很大的作用,下面为一些应用:

  1:利用符号执行探究执行路径,自动解ctf逆向题

  2:利用angr获取程序控制流(CFG)

  3:利用angr生成rop链

  4:利用angr发现漏洞

  5:利用angr加密程序

  6:进行污点跟踪

  由上可以发现,angr的应用是非常多的,里面关于符号执行的应用和思路(特别是自动化相关的思路)是非常值得学习的。

符号执行

将程序的输入符号化,对执行路径构建约束集合,通过约束求解来确定执行某条路径的输入,定义约束方程来发现漏洞。符号执行使用符号值来执行程序,通过程序分析的方法,确定哪些输入向量会对应导致程序的执行结果向量的方法,从而输出一个带符号化变量的值。举个例子

1
2
3
4
5
6
a = random()
b = a*2
if(b==10):
print("ok!")
else:
print("thanks!")

在符号执行过程中,先将a进行符号化转换为x,则b为2x,此时进入路径约束式(10是否等于2x),条件判断语句为执行树,当符号执行结束后,约束求解器会通过输出结果来对路径约束式进行求解,来获得走到这个路径的输入值

这里存在的问题就是约束求解器对约束项求解困难的问题

这里就需要使用动态符号执行(concolic execution)。concolic首先将实际状态运行,并收集实际运行时该路径的变量符号化的约束式,i求解。并将约束式取反,获取另一条路径的约束式并求解。过程不断重复直到路径执行完,或者达到用户设置的限制。

以上面的例子为例,Concolic随机生成变量(a=7),实际走thanks路径。此时会根据判断语句中收集的约束项取反(10==2x)来得到另一条路径。这样避免约束项无法识别和求解问题。

Angr架构

架构如下

  • 可执行文件和库的加载器:CLE
    • CLE(CLE Load Everything) 负责装载二进制对象以及它所依赖的库,将自身无法执行的操作转移给angr的其它组件,最后生成地址空间,表示该程序已加载并可以准备运行。
  • 描述各种架构的库:archinfo
  • 关于二进制代码转换VEX的python包装器 :PyVEX
    • angr需要处理不同的架构,所以它选择一种中间语言来进行它的分析,angr使用Valgrind的中间语言——VEX来完成这方面的内容。VEX中间语言抽象了几种不同架构间的区别,允许在他们之上进行统一的分析。
  • 中间语言VEX执行的模拟器:SimuVEX
    • 它允许你控制符号执行。
  • 抽象的约束求解包装器:Claripy(大部分用法和Z3类似)
    • 这个模块主要专注于将变量符号化,生成约束式并求解约束式,这也是符号执行的核心所在。
  • 程序分析套件:angr(上层封装好的接口)

顶层接口

加载

使用angr第一步就是将一个二进制文件加载到分析平台,并获取文件的基础属性,如下

1
2
3
4
5
6
7
8
9
import angr,monkeyhex
project = angr.Project('./00_angr_find')

print(project.arch)#CPU架构
print(project.entry)#文件入口地址
print(project.filename)#文件绝对路径
print(project.factory)#工厂对象
print(project.loader)#加载器
print(project.concrete_target)

加载器

angr使用CLE模块来将二进制文件转换为在虚拟地址空间的表示,属性.load进行调用,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import angr,monkeyhex
project = angr.Project('./00_angr_find')

print(project.loader)
#<Loaded 00_angr_find, maps [0x8048000:0x8407fff]>
print(project.loader.shared_objects)
#OrderedDict([('00_angr_find', <ELF Object 00_angr_find, maps [0x8048000:0x804a03f]>), ('extern-address space', <ExternObject Object cle##externs, maps [0x8200000:0x8207fff]>), ('cle##tls', <ELFTLSObjectV2 Object cle##tls, maps [0x8300000:0x8314807]>)])

print(project.loader.min_addr)
#134512640
print(project.loader.max_addr)
#138444799
print(project.loader.main_object)#加载多个二进制程序,这是主对象
#<ELF Object 00_angr_find, maps [0x8048000:0x804a03f]>
print(project.loader.main_object.execstack)
#False 检查主对象是否开启NX
print(project.loader.main_object.pic)
#False 检查主队象是否开启PIC

工厂对象

angr中很多类需要一个实例化的project,可以使用project.factory打包方便构造。

Blocks

project.factory.block()用于通过给定的地址提取一个基本块(basic block)的代码(P.S. 关于angr的一个重要的点:angr以基本块为单位来分析代码)。使用它将得到一个Block对象,这个Block对象能告诉我们关于块代码的许多有趣的信息。

1
2
3
4
5
block = project.factory.block(project.entry)#从程序入口提取代码块
print(block.pp())#打印反汇编代码
print(block)
print(block.instructions)
print(block.instruction_addrs)

得到一个基本块的反汇编代码

1
2
3
4
5
6
7
8
9
10
         _start:
8048450 xor ebp, ebp
8048452 pop esi
8048453 mov ecx, esp
8048455 and esp, 0xfffffff0
8048458 push eax
8048459 push esp
804845a push edx
804845b call 0x8048483
None
1
2
3
4
<Block for 0x8048450, 16 bytes>#得到block对象
8#基本块中指令条数
(134513744, 134513746, 134513747, 134513749, 134513752, 134513753, 134513754, 134513755)
#块中所有指令对应的地址

还能通过block对象得到块代码的其他形式(capstone、VEX IRSB)

1
2
print(block.capstone)
print(block.vex)

得到capstone

capstone 是一个反汇编引擎。它是基于LLVM框架中的MC组件部分移植过来,所以LLVM支持的CPU构架,capstone也都支持。 它支持的CPU构架有:Arm, Arm64 (Armv8), M68K, Mips, PowerPC, Sparc, SystemZ, XCore & X86 (include X86_64)

1
2
3
4
5
6
7
8
0x8048450:	xor	ebp, ebp
0x8048452: pop esi
0x8048453: mov ecx, esp
0x8048455: and esp, 0xfffffff0
0x8048458: push eax
0x8048459: push esp
0x804845a: push edx
0x804845b: call 0x8048483

得到VEX IR中间代码表示

VEX IR 是一种中间表示,是从机器码转化而来的一种中间表达式。因为不同的处理器有不同的架构,其机器码的表现形式也是不一样的,所以为了屏蔽这种差异性,就有了中间表示IR。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
IRSB {
t0:Ity_I32 t1:Ity_I32 t2:Ity_I32 t3:Ity_I32 t4:Ity_I32 t5:Ity_I32 t6:Ity_I32 t7:Ity_I32 t8:Ity_I32 t9:Ity_I32 t10:Ity_I32 t11:Ity_I32 t12:Ity_I32 t13:Ity_I32 t14:Ity_I32 t15:Ity_I32 t16:Ity_I32 t17:Ity_I32 t18:Ity_I32 t19:Ity_I32 t20:Ity_I32 t21:Ity_I32 t22:Ity_I32 t23:Ity_I32 t24:Ity_I32 t25:Ity_I32

00 | ------ IMark(0x8048450, 2, 0) ------
01 | PUT(ebp) = 0x00000000
02 | PUT(eip) = 0x08048452
03 | ------ IMark(0x8048452, 1, 0) ------
04 | t4 = GET:I32(esp)
05 | t3 = LDle:I32(t4)
06 | t15 = Add32(t4,0x00000004)
07 | PUT(esi) = t3
08 | ------ IMark(0x8048453, 2, 0) ------
09 | PUT(ecx) = t15
10 | ------ IMark(0x8048455, 3, 0) ------
11 | t5 = And32(t15,0xfffffff0)
12 | PUT(cc_op) = 0x0000000f
13 | PUT(cc_dep1) = t5
14 | PUT(cc_dep2) = 0x00000000
15 | PUT(cc_ndep) = 0x00000000
16 | PUT(eip) = 0x08048458
17 | ------ IMark(0x8048458, 1, 0) ------
18 | t8 = GET:I32(eax)
19 | t17 = Sub32(t5,0x00000004)
20 | PUT(esp) = t17
21 | STle(t17) = t8
22 | PUT(eip) = 0x08048459
23 | ------ IMark(0x8048459, 1, 0) ------
24 | t19 = Sub32(t17,0x00000004)
25 | PUT(esp) = t19
26 | STle(t19) = t17
27 | PUT(eip) = 0x0804845a
28 | ------ IMark(0x804845a, 1, 0) ------
29 | t12 = GET:I32(edx)
30 | t21 = Sub32(t19,0x00000004)
31 | PUT(esp) = t21
32 | STle(t21) = t12
33 | PUT(eip) = 0x0804845b
34 | ------ IMark(0x804845b, 5, 0) ------
35 | t23 = Sub32(t21,0x00000004)
36 | PUT(esp) = t23
37 | STle(t23) = 0x08048460
NEXT: PUT(eip) = 0x08048483; Ijk_Call
}

States

Project对象只代表程序的初始化状态,可以使用SimState模拟执行某个时刻的状态(包括程序内存、寄存器、文件系统数据)

1
2
3
4
5
state = project.factory.entry_state()
print(state) #获取程序入口点的状态
print(state.regs.eip)#访问eip,获取当前指令地址
print(state.regs.eax)#访问eax
print(state.mem[project.entry].int.resolved)
1
2
3
4
<SimState @ 0x8048450>
<BV32 0x1c>
<BV32 0x8048450>
<BV32 0x895eed31>

我们可以看到上面的输出中(BV),指的是Bitvector位向量,angr使用BV来表示CPU数据,我们来看看BV和整数的转换

1
2
3
4
bv = state.solver.BVV(0X8048450,32)
print(bv)
print(state.solver.eval(bv))
print(bv.length)

上面将0x8048450的BV转换为整数

1
2
3
<BV32 0x8048450>
134513744
32

还可以直接将BV数据存储到寄存器和内存中

1
2
3
4
5
6
7
8
state.regs.esi = state.solver.BVV(1234,32)
#将1234整数存储到esi中,自动将整数转换为BV
print(state.regs.esi)
state.mem[0x1000].long = 1234
#将整数1234存储到0x1000的内存中
print(state.mem[0x1000].long.resolved)
print(state.mem[0x1000].long.concrete)
#输出内存中的具体整数数值

对于mem接口:

  • 使用array[index]的形式来指定地址
  • 使用.<type>来指定内存需要把数据解释成什么样的类型(char, short, int, long, size_t, uint8_t, uint16_t…)
  • 存储一个值,这个值可以为bitvector或者python整数
  • 使用.resolved 来将数据输出为bitvector
  • 使用.concrete 来将数据输出为python整数

但若需要读取没有具体值的寄存器时,会出现警告报错信息,这杯叫做符号变量,是符号执行的基础

==<BV32 reg_edi_0_32{UNINITIALIZED}>==
==WARNING | 2024-05-29 13:10:30,298 | angr.storage.memory_mixins.default_filler_mixin | The program is accessing register with an unspecified value. This could indicate unwanted behavior.==
==WARNING | 2024-05-29 13:10:30,298 | angr.storage.memory_mixins.default_filler_mixin | angr will cope with this by generating an unconstrained symbolic variable and continuing. You can resolve this by:==
==WARNING | 2024-05-29 13:10:30,298 | angr.storage.memory_mixins.default_filler_mixin | 1) setting a value to the initial state==
==WARNING | 2024-05-29 13:10:30,298 | angr.storage.memory_mixins.default_filler_mixin | 2) adding the state option ZERO_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to make unknown regions hold null
WARNING | 2024-05-29 13:10:30,298 | angr.storage.memory_mixins.default_filler_mixin | 3) adding the state option SYMBOL_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to suppress these messages.==
==WARNING | 2024-05-29 13:10:30,299 | angr.storage.memory_mixins.default_filler_mixin | Filling register edi with 4 unconstrained bytes referenced from 0x8048450 (_start+0x0 in 00_angr_find (0x8048450))==

仿真管理器

State表示程序运行中的一个点,而Simulation Manager(仿真管理器)在angr中是对state进行操作的基本接口。

首先,我们创建一个simulation manager。构造函数可以接受一个state或者state列表。单个 Simulation Manager 可以包含多个存放state的 stash, 默认的stash 是 active stash,是使用我们传入的 state初始化的。调用step()会执行一个基本块的符号执行

1
2
3
4
5
6
7
simgr = project.factory.simulation_manager(state)
print(simgr)
print(simgr.active)
simgr.step()
print(simgr.active)
print(simgr.active[0].regs.eip)
print(state.regs.eip)

运行结果如下

可以看到SimState执行的时候,原对象在执行时是不可变的

分析

angr中内置了一些分析方法,用于提取程序信息

1
2
3
4
AILBlockSimplifier             BasePointerSaveSimplifier    CalleeCleanupFinder            CFBlanket                    CFGFastSoot                    CongruencyCheck              Disassembly
AILCallSiteMaker BinaryOptimizer CallingConvention CFG Clinic ConstantDereferencesSimplifier discard_plugin_preset()
AILSimplifier BinDiff CDG CFGEmulated CodeTagging DDG DivSimplifier >
BackwardSlice BoyScout CFB CFGFast CompleteCallingConventions Decompiler DominanceFrontier

使用angr构造控制流图,如下

1
2
3
4
5
6
7
8
9
10
11
12
cfg = project.analyses.CFGFast()
cfg1 = project.analyses.CFGEmulated()
print(cfg.graph)
print(len(cfg.graph.nodes()))
entry_node = cfg.get_any_node(project.entry)
print(len(list(cfg.graph.successors(entry_node))))
entry_node = cfg.model.get_any_node(project.entry)
print(len(list(cfg.graph.successors(entry_node))))

cg = cfg.functions.callgraph
print(cg)
plot_cg(cfg, "生成的cfg文件名")
打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2021-2024 John Doe
  • 访问人数: | 浏览次数:

让我给大家分享喜悦吧!

微信