Angr 学习

文章参考:angr初探 | moyaoxueの小屋Angr入门Angr:一个具有动态符号执行和静态分析的二进制分析工具-腾讯云开发者社区-腾讯云

Angr简介

angr是一个支持多处理架构的用于二进制文件分析的工具包,它提供了动态符号执行的能力以及多种静态分析的能力。项目创建的初衷,是为了整合此前多种二进制分析方式的优点,并开发一个平台,以供二进制分析人员比较不同二进制分析方式的优劣,并根据自身需要开发新的二进制分析系统和方式。

也正是因为angr是一个二进制文件分析的工具包,因此它可以被使用者扩展,用于自动化逆向工程、漏洞挖掘等多个方面。

angr 官方文档

angr_ctf项目GitHub - jakespringer/angr_ctf

angr_ctf则是一个专门针对angr的项目,里面有17个angr相关的题目。这些题目只有一个唯一的要求:你需要找出能够使程序输出“Good Job”的输入,这也是符号执行常见的应用场景。

本系列教程是angr的入门教程,将通过做angr_ctf中的题目的形式来介绍angr。

Angr初探

Angr Project

angr的基本过程:

  1. 将二进制程序载入angr分析系统
  2. 将二进制程序转换成中间语言(intermediate representation, IR)
  3. 将IR语言转换成语义较强的表达形式,比如,这个程序做了什么,而不是它是什么。
  4. 执行进一步的分析,比如,完整的或者部分的静态分析(依赖关系分析,程序分块)、程序空间的符号执行探索(挖掘溢出漏洞)、一些对于上面方式的结合。

导入模块:Project类是angr的主类,也是angr的开始,通过初始化该类的对象,可以将你想要分析的二进制文件加载进来,就像这样

angr-CLE:CLE是angr加载二进制文件的组建,在加载二进制文件的时候会分析病读取binary的信息,包括指令地址、shared library、arch information等等。

1
2
import angr
proj = angr.Project('./00_angr_find')

参数为待分析的文件路径,它是唯一必须传入的参数,此外还有一个比较常用的参数load-options,它指明加载的方式,如下:

名称 描述 传入参数
auto_load_libs 是否自动加载程序的依赖 布尔
skip_libs 希望避免加载的库 库名
except_missing_libs 无法解析库时是否抛出异常 布尔
force_load_libs 强制加载的库 库名
ld_path 共享库的优先搜索路径 路径名

少加载一些无关结果的库能够提升angr的效率,Project类中有许多方法和属性,例如加载的文件名、架构、程序入口点、大小端等

angr_IR:angr用VEX IR将指令转化为中间语言IR,分析IR并且模拟,搞清楚它是什么并且做了什么。如下的ARM指令

1
subs R2, R2, #8

转化为VEX IR

1
2
3
4
5
t0 = GET:I32(16)
t1 = 0x8:I32
t3 = Sub32(t0,t1)
PUT(16) = t3
PUT(68) = 0x59FC8:I32

angr-Solver Engine:angr的求解引擎叫Claripy,具体这一步做什么呢,根据程序所需要的输入设置符号变量以及收集限制式等等。

project的基础属性

命令行使用时可以导入monkeyhex转化为十六进制输出

1
2
3
4
5
6
7
8
9
proj.entry # 文件的入口点

proj.filename # 文件名

proj.arch # 文件的架构
- proj.arch.name # x86/x86-64/ARM
- proj.arch.bits # 32/64
- proj.arch.bytes # bytes per instruction, eg : 4/8
- proj.arch.memory_endness # 字节序,例如 Iend_LE代表小端序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
proj.loader # 显示已加载对象,内存映射的地址范围
# <Loaded [file_name], maps [0x400000:0x5004000]>

proj.loader.shared_objects # 已加载的所有共享对象,共享库或动态链接库及其内存映射
# OrderedDict([('angr', <ELF Object angr, maps [0x8048000:0x804c033]>), ('libc.so.6', <ELF Object libc.so.6, maps [0x8100000:0x83347bb]>), ('ld-linux.so.2', <ELF Object ld-linux.so.2, maps [0x8400000:0x8437a37]>), ('extern-address space', <ExternObject Object cle##externs, maps [0x8500000:0x8507fff]>), ('cle##tls', <ELFTLSObjectV2 Object cle##tls, maps [0x8600000:0x8614807]>)])

proj.loader.min_addr # 加载的二进制文件占用的内存空间的界限
# 0x8048000
proj.loader.max_addr
# 0x8707fff

proj.loader.main_object # 返回代表主要加载的二进制文件的对象
# <ELF Object angr, maps [0x8048000:0x804c033]>

proj.loader.main_object.execstack # 返回bool,代表主二进制文件是否具有可执行堆栈
# False

proj.loader.main_object.pic # 二进制文件是否为位置独立代码,若返回True,则说明开启了ASLR
# False

对基本块的操作

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
block = proj.factory.block(proj.entry) # 打印入口的基本块的汇编
block.pp()
"""
80490b0 endbr32
80490b4 xor ebp, ebp
80490b6 pop esi
80490b7 mov ecx, esp
80490b9 and esp, 0xfffffff0
80490bc push eax
80490bd push esp
80490be push edx
80490bf call 0x80490dd
"""

block.instructions # 该基本块的指令数量
# 9
block.instruction_addrs # 该基本块指令地址
# (134516912, 134516916, 134516918, 134516919, 134516921, 134516924, 134516925, 134516926, 134516927)

block.capstone # 打印人类可读汇编形式(与.pp()类同)
block.vex # 打印IR代码形式
"""
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(0x80490b0, 4, 0) ------
01 | ------ IMark(0x80490b4, 2, 0) ------
02 | PUT(ebp) = 0x00000000
03 | PUT(eip) = 0x080490b6
04 | ------ IMark(0x80490b6, 1, 0) ------
05 | t4 = GET:I32(esp)
06 | t3 = LDle:I32(t4)
07 | t15 = Add32(t4,0x00000004)
08 | PUT(esi) = t3
09 | ------ IMark(0x80490b7, 2, 0) ------
10 | PUT(ecx) = t15
11 | ------ IMark(0x80490b9, 3, 0) ------
12 | t5 = And32(t15,0xfffffff0)
13 | PUT(cc_op) = 0x0000000f
14 | PUT(cc_dep1) = t5
15 | PUT(cc_dep2) = 0x00000000
16 | PUT(cc_ndep) = 0x00000000
17 | PUT(eip) = 0x080490bc
18 | ------ IMark(0x80490bc, 1, 0) ------
19 | t8 = GET:I32(eax)
20 | t17 = Sub32(t5,0x00000004)
21 | PUT(esp) = t17
22 | STle(t17) = t8
23 | PUT(eip) = 0x080490bd
24 | ------ IMark(0x80490bd, 1, 0) ------
25 | t19 = Sub32(t17,0x00000004)
26 | PUT(esp) = t19
27 | STle(t19) = t17
28 | PUT(eip) = 0x080490be
29 | ------ IMark(0x80490be, 1, 0) ------
30 | t12 = GET:I32(edx)
31 | t21 = Sub32(t19,0x00000004)
32 | PUT(esp) = t21
33 | STle(t21) = t12
34 | PUT(eip) = 0x080490bf
35 | ------ IMark(0x80490bf, 5, 0) ------
36 | t23 = Sub32(t21,0x00000004)
37 | PUT(esp) = t23
38 | STle(t23) = 0x080490c4
NEXT: PUT(eip) = 0x080490dd; Ijk_Call
}
"""

状态State

Project实际上只是将二进制文件加载进来了,要执行它,实际上是对SimState对象进行操作,它是程序的状态。用docker来比喻,Project相当于开发环境,State则是使用开发环境制作的镜像。

要创建状态,需要使用Project对象中的factory,它还可以用于创建模拟管理器和基本块(后面提到),如下:

1
init_state = p.factory.entry_state()

预设状态有四种方式如下:

预设状态方式 描述
entry_state 初始化状态为程序运行到程序入口点处的状态
blank_state(addr=) 大多数数据都没有初始化,状态中下一条指令为addr处的指令
full_init_state 共享库和预定义内容已经加载完毕,例如刚加载完共享库
call_state 准备调用函数的状态

状态包含了程序运行时的一切信息,寄存器、内存的值、文件系统以及符号变量等,这些信息的使用等用到时再进一步说明。

entry_state和blank_state是常用的两种方式,后者通常用于跳过一些极大降低angr效率的指令,它们间的对比如下:

1
2
3
4
5
6
>>> state = p.factory.entry_state()
>>> print(state.regs.rax, state.regs.rip)
<BV64 0x1c> <BV64 0x4023c0>
>>> state = p.factory.blank_state(addr=0x4023c0)
>>> print(state.regs.rax, state.regs.rip)
<BV64 reg_rax_42_64{UNINITIALIZED}> <BV64 0x4023c0>

在blank_state方式中,我们仍将地址设定为程序的入口点,然而rax中的值由于没有初始化,它现在是一个名字,也即符号变量,这是符号执行的基础,后续在细说。

此外,可以看到寄存器中的数据类型并不是int,而是BV64,它是一个位向量(Bit Vector),有关位向量的细节之后再说。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
state = proj.factory.entry_state()
# <SimState @ 0x80490b0>
print(state)
print(state.regs.eip)
print(state.regs.eax)
print(state.mem[proj.entry].int.resolved)
"""
<BV32 0x80490b0>
<BV32 0x1c>
<BV32 0xfb1e0ff3>
"""

state.solver.eval(state.regs.eax) # 转化为python int
bv = state.solver.BVV(0x1234, 32) # 反过来创建,create a 32-bit-wide bitvector with value 0x1234

bv = state.solver.BVV(0x1111, 32) # 修改寄存器值
state.regs.eax = bv

state.mem[0x1000].long = 4 # 修改内存中的值
print(state.mem[0x1000].long.resolved)
# <BV32 0x4>

模拟管理器

上述方式只是预设了程序开始分析时的状态,我们要分析程序就必须要让它到达下一个状态,这就需要模拟管理器的帮助(简称SM).

使用以下指令能创建一个SM,它需要传入一个state或者state的列表作为参数:

1
simgr  = p.factory.simgr(state)

SM中有许多列表,这些列表被称为stash,它保存了处于某种状态的state,stash有如下几种:

stash 描述
active 保存接下来可以执行并且将要执行的状态
deadended 由于某些原因不能继续执行的状态,例如没有合法指令,或者有非法指针
pruned 与solve的策略有关,当发现一个不可解的节点后,其后面所有的节点都优化掉放在pruned里
unconstrained 如果创建SM时启用了save_unconstrained,则被认定为不受约束的state会放在这,不受约束的state是指由用户数据或符号控制的指令指针(例如eip)
unsat 如果创建SM时启用了save_unsat,则被认为不可满足的state会放在这里

默认情况下,state会被存放在active中。

stash中的state可以通过move()方法来转移,将fulter_func筛选出来的state从from_stash转移到to_stash:

1
simgr.move(from_stash='deadended', to_stash='more_then_50', filter_func=lambda s: '100' in s.posix.dumps(1))

stash是一个列表,可以使用python支持的方式去遍历其中的元素,也可以使用常见的列表操作。但angr提供了一种更高级的方式,在stash名字前加上one_,可以得到stash中的第一个状态,加上mp_,可以得到一个mulpyplexed版本的stash

此外,稍微解释一下上面代码中的posix.dumps:

  • state.posix.dumps(0):表示到达当前状态所对应的程序输入
  • state.posix.dumps(1):表示到达当前状态所对应的程序输出

上述代码就是将deadended中输出的字符串包含’100’的state转移到more_then_50这个stash中。

可以通过step()方法来让处于active的state执行一个基本块,这种操作不会改变state本身:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> state = p.factory.entry_state()
>>> simgr = p.factory.simgr(state)
>>> print(state.regs.rax, state.regs.rip)
<BV64 0x1c> <BV64 0x4023c0>

>>> print(simgr.one_active)
<SimState @ 0x4023c0>

>>> simgr.step()
<SimulationManager with 1 active>
>>> print(simgr.one_active)
<SimState @ 0x529240>

>>> print(state.regs.rax, state.regs.rip)
<BV64 0x1c> <BV64 0x4023c0>
1
2
3
4
5
6
7
8
9
10
11
import angr
p = angr.Project('./00_angr_find') # 1. 加载一个二进制文件,创建 Project
state = p.factory.entry_state() # 2. 从程序入口(默认是 _start 或 main 前)构造一个初始状态
simgr = p.factory.simgr(state) # 3. 基于这个初始状态,生成一个 SimulationManager 来管理执行路径

print(state.regs.ax) # 4. 打印当前状态下寄存器 ax 的值
print(simgr.one_active) # 5. 打印当前活跃状态(active stash 里只有一个 state)
simgr.step() # 6. 让模拟器执行一步(相当于执行一条或几条指令)
print(simgr.one_active) # 7. 再次打印现在活跃状态(执行完之后的位置)
print(state.regs.ax) # 8. 再次打印最开始 state 对象里的 ax

simgr.one_active SimulationManager 会把状态放到不同的“stash”(active、deadended、errored 等)。

one_active 就是 当前活跃状态,如果只有一个,就直接取出来。

simgr.step() 让所有 active 状态都执行一步指令。

这一步之后,simgr.one_active 里的状态的 ip(指令指针/程序计数器)会移动到下一条指令。

最后也是SM最常用的技术:探索技术(explorer techniques)

可以使用explorer方法去执行某个状态,直到找到目标指令或者active中没有状态为止,它有如下参数:

  • find:传入目标指令的地址或地址列表,或者一个用于判断的函数,函数以state为形参,返回布尔值
  • avoid:传入要避免的指令的地址或地址列表,或者一个用于判断的函数,用于减少路径

此外还有一些搜索策略,之后会集中讲解,默认使用DFS(深度优先搜索)。

explorer找到的符合find的状态会被保存在simgr.found这个列表当中,可以遍历其中元素获取状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import angr
proj = angr.Project('./angr')
state = proj.factory.entry_state()
simgr = proj.factory.simulation_manager(state)
print(simgr)
print(simgr.active)
print(simgr.active[0].regs.eip)
print('---')
simgr.step()
print(simgr.active)
print(simgr.active[0].regs.eip)

""" 这里执行了一整个基本块(注意:smigr不会改变state的信息)
<SimulationManager with 1 active>
[<SimState @ 0x80490b0>]
<BV32 0x80490b0>
---
[<SimState @ 0x80490dd>]
<BV32 0x80490dd>
"""

符号执行

angr作为一个二进制分析的工具包,但它通常作为符号执行工具更为出名。

符号执行就是给程序传递一个符号而不是具体的值,让这个符号伴随程序运行,当碰见分支时,符号会进入哪个分支呢?

angr的回答是全都进入!angr会保存所有分支,以及分支后的所有分支,并且在分支时,保存进入该分支时的判断条件,通常这些判断条件时对符号的约束。

当angr运行到目标状态时,就可以调用求解器对一路上收集到的约束进行求解,最终得到某个符号能够到达当前状态的值。

例如,程序接收一个int类型的输入,当这个输入大于0小于5时,就会执行某条保存在该程序中,我们希望执行的指令(例如一个后门函数backdoor),具体而言如下图所示:

image-20250927155241557

angr会沿着分支按照某种策略(默认DFS)进行状态搜索,当达到目标状态(也就是backdoor能够执行的状态),此时angr已经收集了两个约束(x>0 以及x<=5),那么angr就通过这两个约束对x进行求解,解出来的x值就是能够让程序执行backdoor的输入。

在复杂的程序当中,从一个符号到backdoor的路径可能十分复杂,甚至包含一些加密解密的过程,这时就是angr大显身手的时候了。

angr在模拟执行指令时,对于遇到的分支和跳转,会全部进行保留,并且记录用于判断分支的条件(即约束),如下图所示

image-20250927175358404

这些状态都是程序运行到某些阶段时的信息,包括了内存、寄存器、文件系统等多个方面,这些状态中有满足条件的状态,就会被放入到found列表当中。

而在路径搜索时,对于满足avoid条件的状态,则会被丢弃,也就是说,该状态及该状态的后续路径都不会被进行搜索,因此简化了angr的搜索路径,从而提高效率。

Angr CTF

使用angr一般分为如下步骤:

  1. 创建Project,预设state
  2. 创建位向量和符号变量,保存在内存/寄存器/文件或其他地方
  3. 将state添加到SM中
  4. 运行,探索满足条件的路径
  5. 约束求解获取执行结果

SimulationManager.explore()

这三道题目学习给explore选择参数。

00_angr_find

程序逻辑

image-20250927160017349

image-20250927160640620

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import angr

# 加载文件,预设状态,执行状态
p = angr.Project('./dist/00_angr_find', auto_load_libs=False)
init_state = p.factory.entry_state()
simgr = p.factory.simgr(init_state)

# puts Good的指令地址
target = 0x08048678

# 搜索能够执行目标指令的状态
simgr.explore(find=target)

if simgr.found:
solution_state = simgr.found[0]
# 打印出符号条件的状态的输入
print(solution_state.posix.dumps(0))
1
b'JXWVXRKX

事实上,上述脚本能够解决一切有关”为了执行某条目标语句,我应该用怎样的输入”这样的问题,是一个万能脚本。区别在于由于程序的复杂程度和逻辑不同,耗费的时间不同,因此在解决这类问题上,编写angr脚本的本质是在使用angr提供的各种二进制分析方法去优化路径,提高它的运行效率。

01_angr_avoid

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import angr

# 加载文件,预设状态,执行状态
p = angr.Project('./dist/00_angr_find', auto_load_libs=False)
init_state = p.factory.entry_state()
simgr = p.factory.simgr(init_state)

# puts Good的指令地址
target = 0x080485E5

# 搜索能够执行目标指令的状态
simgr.explore(find=target)

if simgr.found:
solution_state = simgr.found[0]
# 打印出符号条件的状态的输入
print(solution_state.posix.dumps(0))

这样不行,因为直接崩了,中间函数太复杂太多了

可以发现该函数被main函数调用了多次,应该是导致main函数过大的原因,因此要对它进行避免,也就是使用explorer的avoid的参数。

这里需要让angr走到avoid_me函数后就剪枝

image-20250927161628031

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import angr

p = angr.Project('./dist/01_angr_avoid')
init_state = p.factory.entry_state()
simgr = p.factory.simgr(init_state)

good = 0x080485e5#avoid_me的函数起始的位置(并非调用该函数的位置,因为调用该函数的地方太多了)
bad = 0x080485a8
simgr.explore(find= good,avoid = bad)

if simgr.found:
solution = simgr.found[0]
print(solution.posix.dumps(0))else:
raise Exception("Could not find solution")
1
HUJOZMYS

simgr.found[0]

  • 遇到地址 good → 停下来放到 found 集合里
  • 遇到地址 bad → 扔掉放到 avoid 集合里

simgr.found 就是所有到达了 “good” 的路径的列表(state 列表)。

simgr.found[0] 取出第一个满足条件的 state,即程序在“成功位置”的状态。

solution.posix.dumps(0)

在 angr 里,state 有个 posix 接口,模拟 Linux/Unix 程序运行时的 I/O。

posix.dumps(fd) 的作用是:

把指定文件描述符(fd)的内容“dump”出来,返回字节串。

常见文件描述符号:

0 → 标准输入 stdin

1 → 标准输出 stdout

2 → 标准错误 stderr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import angr
proj = angr.Project('./angr', auto_load_libs=False)
simgr = proj.factory.simgr()
def should_avoid(state):
# 检查输出是否包含"Try again"
if b"Try again" in state.posix.dumps(1):
return True
# 检查当前地址是否是我们想要避免的地址
if state.addr == 0x8049243:
return True
# 如果以上条件都不满足,那么我们不避免这个状态
return False

simgr.explore(find = lambda s1: b"Good Job." in s1.posix.dumps(1), avoid = should_avoid)

s = simgr.found[0]
flag = s.posix.dumps(0)
print(flag)

也能解,但是should_avoid会有额外开销

02_angr_find_condition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import angr

p = angr.Project('./dist/02_angr_find_condition')
init_state = p.factory.entry_state()
simgr = p.factory.simgr(init_state)


def good(state):
tag = b'Good' in state.posix.dumps(1)
return True if tag else False

def bad(state):
tag = b'Try' in state.posix.dumps(1)
return True if tag else False

simgr.explore(find=good, avoid=bad)

if simgr.found:
solution = simgr.found[0]
print(solution.posix.dumps(0))
else:
raise Exception("Could not find solution")

Symbolic

学习怎么在寄存器,栈,堆,文件等处注入符号变量。

03_angr_symbolic_registers

image-20250927220900954

里面有三个复杂功能

image-20250927221008908

用IDA打开程序,get_user_input把三个输入分别放入寄存器eax、ebx、edx。我们需要跳过输入这一步,直接让Angr把用符号向量来代替输入字符串。因此,我们需要改变程序入口,直接跳转到参数入栈的位置,然后新建三个符号向量,并把三个符号向量分别放到寄存器eax、ebx、edx。

image-20250927221901590

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
44
45
import angr
import claripy
import sys

project = angr.Project('./dist/03_angr_symbolic_registers')

# 重新设定入口点,此处从sanf输入开始执行。此处不用entry_state,而是blank_state
start_address = 0x08048980
initial_state = project.factory.blank_state(addr=start_address)

# 创建三个个符号向量,第一个参数是用来引用的,第二个参数是CPU位数,如32
password0 = claripy.BVS('password0', 32)
password1 = claripy.BVS('password1', 32)
password2 = claripy.BVS('password2', 32)

# 根据IDA,分别设置eax、ebx、edx的符号值
initial_state.regs.eax = password0
initial_state.regs.ebx = password1
initial_state.regs.edx = password2

simulation = project.factory.simgr(initial_state)

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Good Job.' in stdout_output

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Try again.' in stdout_output

simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
solution_state = simulation.found[0]
# 求解
solution0 = solution_state.solver.eval(password0)
solution1 = solution_state.solver.eval(password1)
solution2 = solution_state.solver.eval(password2)

# 打印输出十六进制
solution = "Solutions:{:x} {:x} {:x}".format(solution0, solution1, solution2)
print(solution)
else:
raise Exception('Could not find the solution')

1
2
3
def should_abort(state):
stdout_output = state.posix.dumps(1)
return b'Try again.' in stdout_output

这样也行

04_angr_symbolic_stack

当符号值位于栈上时,需要提前做好栈布局,再将符号值放到栈上(push或直接内存赋值)。

state.stack_push(thing)

image-20250927233118025

输入后栈为这样的,一开始esp和ebp是同一位置

esp只需要抬高0x8即可输入,上面那张图截图的时机有点问题,esp在0x17下面时更好理解(也就是未输入时)

image-20250927233305665

05_angr_symbolic_memory

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
import angr
import sys

p = angr.Project('./dist/05_angr_symbolic_memory')

start_addr = 0x08048601
init_state = p.factory.blank_state(addr = start_addr)

p1 = init_state.solver.BVS('p1', 64)
p2 = init_state.solver.BVS('p2', 64)
p3 = init_state.solver.BVS('p3', 64)
p4 = init_state.solver.BVS('p4', 64)

p1_addr = 0x0a1ba1c0
p2_addr = 0x0a1ba1c8
p3_addr = 0x0a1ba1d0
p4_addr = 0x0a1ba1d8

init_state.memory.store(p1_addr, p1)
init_state.memory.store(p2_addr, p2)
init_state.memory.store(p3_addr, p3)
init_state.memory.store(p4_addr, p4)

sm = p.factory.simgr(init_state)
def is_good(state):
return b"Good Job" in state.posix.dumps(1)

def is_bad(state):
return b"Try again" in state.posix.dumps(1)

sm.explore(find=is_good, avoid=is_bad)
if sm.found:
found_state = sm.found[0]
pass1 = found_state.solver.eval(p1, cast_to=bytes)
pass2 = found_state.solver.eval(p2, cast_to=bytes)
pass3 = found_state.solver.eval(p3, cast_to=bytes)
pass4 = found_state.solver.eval(p4, cast_to=bytes)

print("Solution: {} {} {} {}".format(pass1.decode("utf-8"), pass2.decode("utf-8"), pass3.decode("utf-8"), pass4.decode("utf-8")))
else:
Exception("Solution not found")

06_angr_symbolic_dynamic_memory

image-20250928232415534

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
import angr
import sys
import claripy

project = angr.Project('./dist/06_angr_symbolic_dynamic_memory')
initial_state = project.factory.blank_state(addr=0x8048699)

arg1 = claripy.BVS('arg1', 64)
arg2 = claripy.BVS('arg2', 64)
addr1 = 0xABCC8A4
addr2 = 0xABCC8AC

# 直接分配地址,angr会自行分配内存
heap_ptr1 = 0x212340
heap_ptr2 = 0x312350

initial_state.memory.store(addr1, heap_ptr1, endness = 'LE')
initial_state.memory.store(addr2, heap_ptr2, endness = 'LE')
initial_state.memory.store(heap_ptr1, arg1)
initial_state.memory.store(heap_ptr2, arg2)

simgr = project.factory.simulation_manager(initial_state)

def right(state):
if b'Good' in state.posix.dumps(1):
return True
else:
return False

def wrong(state):
if b'Try' in state.posix.dumps(1):
return True
else:
return False

simgr.explore(find=right, avoid=wrong)

if simgr.found:
solution_state = simgr.found[0]
print(solution_state.solver.eval(arg1, cast_to=bytes))
print(solution_state.solver.eval(arg2, cast_to=bytes))

07_angr_symbolic_file

通过angr.storage.SimFilestate.fs.insert来插入符号化文件。

读取输入通过ignore_me函数存储入OJKSQYDP.txt中,后续再通过从OJKSQYDP.txt中取出进行校验

image-20250929231306768

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
import angr
import claripy
import sys
io = angr.Project('./dist/07_angr_symbolic_file',auto_load_libs=False)

state_addr = 0x80488E7
init_state = io.factory.blank_state(addr = state_addr)

passwd0 = claripy.BVS('passwd0',64*8)

file_name = 'OJKSQYDP.txt'
#%64s
#通过SimFile形成符号化文件
simfile = angr.storage.SimFile(name = file_name,content = passwd0,size = 64)
#将SimFile插入state文件系统
init_state.fs.insert(file_name,simfile)

simgr = io.factory.simgr(init_state)

def is_succ(state):
std_out = state.posix.dumps(sys.stdout.fileno())
if b'Good Job.' in std_out:
return True
else:
return False

def is_fail(state):
std_out = state.posix.dumps(sys.stdout.fileno())
if b'Try again.' in std_out:
return True
else:
return False

simgr.explore(find=is_succ,avoid=is_fail)

if simgr.found:
so_state = simgr.found[0]
print(so_state.solver.eval(passwd0,cast_to=bytes))

Hook

开始学怎么避免路径爆炸了,其实个人感觉hook,simProcedure,手动约束的思想都差不多。

08_angr_constraints

image-20250929232750517

image-20250929232759417

但是与之前直接使用strcmp校验不同,这里使用的是一个自定义的按位校验,并且由于输入是16位,就将会进行16次的循环,每次循环都将经历一次if判断

将会产生2^16 == 65536个判断分支,这么多的分支,就将会引发一个叫路径爆炸的问题,严重影响我们测试的效率

为此,我们可以自己去实现一个校验约束,直接跳过或者也可以理解为hook掉这个按位校验函数,这样就不会产生路径爆炸了

你可能会有疑问,strcmp函数在底层实现也是按位比较,为什么在前面的题目中并没有提及路径爆炸问题

原因是angr在对于strcmp这种标准库自己实现了一套hook,使用了angr实现的strcmp去替换掉了标准库中调用的strcmp函数,避免了路径爆炸,这在下文中也有提及

参考:通过Angr_CTF入门Angr | Closure

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
44
45
import angr
import claripy
import sys
io = angr.Project('./dist/08_angr_constraints',auto_load_libs=False)

state_addr = 0x8048622
init_state = io.factory.blank_state(addr = state_addr)

#%16s
passwd0 = claripy.BVS('passwd0',16*8)

buffer_addr = 0x804A050
init_state.memory.store(buffer_addr,passwd0)

simgr = io.factory.simgr(init_state)

#运行至调用check
check_addr = 0x8048565
simgr.explore(find=check_addr)

def is_succ(state):
std_out = state.posix.dumps(sys.stdout.fileno())
if b'Good Job.' in std_out:
return True
else:
return False

def is_fail(state):
std_out = state.posix.dumps(sys.stdout.fileno())
if b'Try again.' in std_out:
return True
else:
return False


if simgr.found:
so_state = simgr.found[0]

buffer_cu = so_state.memory.load(buffer_addr,16) #读出buffer处的数据
key = "AUPDNNPROEZRJWKB"
#添加约束条件,自己实现一个校验
so_state.solver.add(buffer_cu == key)
#eval将对约束进行求解,也就是获取符合条件的值
so0 = so_state.solver.eval(passwd0,cast_to=bytes)
print(format(so0.decode('utf-8')))

09_angr_hooks

image-20250930230542092

程序将获取两次输入,第一次输入经过complex_function处理后,再通过check_equals_XYMKBKUHNIQYNQXE与password进行比较;第二次输入将与经过complex_function处理后的password进行比较;并且可以看到在check_equals_XYMKBKUHNIQYNQXE中使用按位比较,将会出现路径爆炸问题

image-20250930230933054

跟上题的不同之处是:这道题在到达了check地址之后还会执行,使用hook来做更方便一些。

指令长度为5

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
44
45
46
47
48
49
50
import angr
import claripy
import sys

io = angr.Project('./dist/09_angr_hooks', auto_load_libs=False)

init_state = io.factory.entry_state()

check_addr = 0x80486B3 # call check指令地址
call_check_len = 5 # 指令长度

# 通过地址进行HOOK
@io.hook(check_addr, length=call_check_len)
def hook_check(state):
buffer_addr = 0x804A054
# %16s
buffer = state.memory.load(buffer_addr, 16) # 读取
key = "XYMKBKUHNIQYNQXE"
# 返回值存储在eax
state.regs.eax = claripy.If(
buffer == key,
claripy.BVV(1, 32), # 32位寄存器
claripy.BVV(0, 32) # 32位寄存器
)


def is_succ(state):
std_out = state.posix.dumps(sys.stdout.fileno())
if b'Good Job.' in std_out:
return True
else:
return False


def is_fail(state):
std_out = state.posix.dumps(sys.stdout.fileno())
if b'Try again.' in std_out:
return True
else:
return False


simgr = io.factory.simgr(init_state)

simgr.explore(find=is_succ, avoid=is_fail)

if simgr.found:
so_state = simgr.found[0]
so0 = so_state.posix.dumps(0)
print(format(so0.decode('utf-8')))

10_angr_simprocedures

与上一题类似,但是本题的check函数被多次调用,可以使用函数名进行hook

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
44
45
46
47
48
49
50
51
52
import angr
import claripy
import sys

io = angr.Project('./dist/10_angr_simprocedures',auto_load_libs=False)

init_state = io.factory.entry_state()

#继承SimProcedure
class Hook(angr.SimProcedure):
#参照函数原型进行hook
def run(self,a1,a2):
buffer_addr = a1 #原函数1参数
buffer_len = a2 #原函数2参数
#读取
buffer = self.state.memory.load(
buffer_addr,
buffer_len
)

key = "ORSDDWXHZURJRBDH"
#原函数有返回值
return claripy.If(
buffer == key,
claripy.BVV(1,32),
claripy.BVV(0,32)
)

def is_succ(state):
std_out = state.posix.dumps(sys.stdout.fileno())
if b'Good Job.' in std_out:
return True
else:
return False

def is_fail(state):
std_out = state.posix.dumps(sys.stdout.fileno())
if b'Try again.' in std_out:
return True
else:
return False

check_sym = "check_equals_ORSDDWXHZURJRBDH" #符号表获取
io.hook_symbol(check_sym,Hook()) #angr会自己去找和函数符号有关联的地址
simgr = io.factory.simgr(init_state)

simgr.explore(find = is_succ,avoid=is_fail)

if simgr.found:
so_state = simgr.found[0]
so0 = so_state.posix.dumps(0)
print(format(so0.decode('utf-8')))

11_angr_sim_scanf

image-20250930232439461

分段校验,对__isoc99_scanf进行hook

hook scanf函数来应对复杂格式的输入,向scanf的参数中存入内容,并且将值存到 globals 全局变量插件中

为什么要 hook scanf

  1. scanf 会做格式化解析(十进制字符串 → 二进制整数),如果不钩住,angr 要么模拟 scanf 的实现(复杂、慢),要么你需要把 stdin 做成符号并让 scanf 自己解析出整数,这会把“解析逻辑”也引入符号约束中,显著增加复杂度与求解难度。
  2. 更直接的语义建模:题里 scanf("%u %u", buffer0, buffer1) 的效果是“把两个 32-bit 无符号整数写到内存”。我们只关心写入后的内存值被 strncmp 比较这一事实,hook 可以直接创建两个 32-bit 的符号位向量(BVS)并写入 buffer0/1 地址,使后续的 strncmp 约束直接作用在这些符号上,干净利落。
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import angr
import claripy
import sys
io = angr.Project('./dist/11_angr_sim_scanf',auto_load_libs=False)

init_state = io.factory.entry_state()

class Hook(angr.SimProcedure):
#参照函数原型进行hook
def run(self,format_string,buffer0_addr,buffer1_addr):
scanf0 = claripy.BVS('scanf0',32)
scanf1 = claripy.BVS('scanf1',32)

#原函数向buffer写入数据
#向地址内写入符号位向量
self.state.memory.store(
buffer0_addr,
scanf0,
endness = io.arch.memory_endness
)
self.state.memory.store(
buffer1_addr,
scanf1,
endness = io.arch.memory_endness
)
#保存符号位变量(局部变量)为全局变量,方便我们后续访问
self.state.globals['so0'] = scanf0
self.state.globals['so1'] = scanf1

def is_succ(state):
std_out = state.posix.dumps(sys.stdout.fileno())
if b'Good Job.' in std_out:
return True
else:
return False

def is_fail(state):
std_out = state.posix.dumps(sys.stdout.fileno())
if b'Try again.' in std_out:
return True
else:
return False

scanf_sym = "__isoc99_scanf"
io.hook_symbol(scanf_sym,Hook())
simgr = io.factory.simgr(init_state)

simgr.explore(find = is_succ,avoid=is_fail)

if simgr.found:
so_state = simgr.found[0]

so0 = so_state.globals['so0'] #访问全局变量,获取指定的位向量
so1 = so_state.globals['so1']

scanf0_so = so_state.solver.eval(so0) #求解
scanf1_so = so_state.solver.eval(so1)

print(format(scanf0_so))
print(format(scanf1_so))

Veritesting

12_angr_veritesting

image-20251001231651918

程序将进行一个按位的加密,这里将会出现路径爆炸,angr提供了veritesting去避免路径爆炸,只需启用即可

符号执行,一种是动态符号执行(Dynamic Symbolic Execution,简称DSE),另一种是静态符号执行(Static Symbolic Execution,简称SSE)。

动态符号执行会去执行程序然后为每一条路径生成一个表达式。在生成表达式上引入了很多的开销,然而生成的表达式很容易求解。

而静态符号执行将程序转换为表达式,每个表达式都表示任意条路径的属性生成表达式容易,但是表达式难求解。

veritesting就是在这二者中做权衡,使得能够在引入低开销的同时,生成较易求解的表达式。

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
import angr
import claripy
import sys

io = angr.Project('./dist/12_angr_veritesting', auto_load_libs=False)

init_state = io.factory.entry_state()
def is_succ(state):
std_out = state.posix.dumps(sys.stdout.fileno())
if b'Good Job.' in std_out:
return True
else:
return False


def is_fail(state):
std_out = state.posix.dumps(sys.stdout.fileno())
if b'Try again.' in std_out:
return True
else:
return False

simgr = io.factory.simgr(init_state, veritesting=True) # 开启veritesting

simgr.explore(find=is_succ, avoid=is_fail)

if simgr.found:
so_state = simgr.found[0]
so0 = so_state.posix.dumps(0)
print(format(so0.decode('utf-8')))

Library

13_angr_static_binary

和00_angr_find唯一的区别是二进制文件被编译为静态二进制文件,我们主动替换库函数避免路径爆炸和加速。

angr提供了写好的SimProcdure,我们直接索引到对应的函数然后hook就行。

就像本来strcmp的实现也是按位比较,但是在前面为什么不会在strcmp上发生路径爆炸,因为angr已经自动给这些函数hook了

但是用静态编译,angr没法自动hook,需要手动去hook在使用的标准库的C函数:

image-20251001232741998

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
import angr
import claripy
import sys
io = angr.Project('/home/closure/Desktop/CTF/angr_ctf/dist/13_angr_static_binary',auto_load_libs=False)

init_state = io.factory.entry_state()

def is_succ(state):
std_out = state.posix.dumps(sys.stdout.fileno())
if b'Good Job.' in std_out:
return True
else:
return False

def is_fail(state):
std_out = state.posix.dumps(sys.stdout.fileno())
if b'Try again.' in std_out:
return True
else:
return False
#手动hook
io.hook(0x804ed80, angr.SIM_PROCEDURES['libc']['scanf']())
io.hook(0x804ed40, angr.SIM_PROCEDURES['libc']['printf']())
io.hook(0x804f350, angr.SIM_PROCEDURES['libc']['puts']())
io.hook(0x8048280, angr.SIM_PROCEDURES['libc']['strcmp']())
io.hook_symbol('__libc_start_main',angr.SIM_PROCEDURES['glibc']['__libc_start_main']())
simgr = io.factory.simgr(init_state)

simgr.explore(find = is_succ,avoid=is_fail)

if simgr.found:
so_state = simgr.found[0]
so0 = so_state.posix.dumps(0)
print(format(so0.decode('utf-8')))

14_angr_shared_library

image-20251001233034537

validate来自动态链接库lib14_angr_shared_library.so

image-20251001233237945

我们指定下共享库的基地址加上偏移就能定位到validate。这里用到一个新的内置state:call_state

  1. .blank_state():空白状态,其大部分数据未初始化。 访问未初始化的数据时,将返回一个不受约束的符号值。
  2. .entry_state() :构造一个准备在主二进制文件入口点执行的状态。
  3. .full_init_state():通过需要在主二进制文件入口点之前运行的任何初始化程序构造一个准备执行的状态
  4. .call_state():构造一个准备好执行给定函数的状态。
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
44
45
import angr
import claripy
import sys

def is_succ(state):
std_out = state.posix.dumps(sys.stdout.fileno())
if b'Good Job.' in std_out:
return True
else:
return False

def is_fail(state):
std_out = state.posix.dumps(sys.stdout.fileno())
if b'Try again.' in std_out:
return True
else:
return False

libc_so = '/home/closure/Desktop/CTF/angr_ctf/dist/lib14_angr_shared_library.so'
libc_base = 0x8048000 #基地址
io = angr.Project(libc_so,load_options={
'main_opts':{
'custom_base_addr':libc_base #基地址
}
})

validate_addr = libc_base + 0x6D7 # + 偏移 = 目标函数地址

buffer_pointer = claripy.BVV(0x3000000, 32) #位向量,0x3000000为缓冲区地址,只要不影响程序执行即可

init_state = io.factory.call_state(validate_addr, buffer_pointer, claripy.BVV(8, 32))

password = claripy.BVS('password', 8*8)
init_state.memory.store(buffer_pointer, password)

simgr = io.factory.simgr(init_state)

success_address = libc_base + 0x783
simgr.explore(find=success_address)

if simgr.found:
so_state = simgr.found[0]
so_state.add_constraints(so_state.regs.eax == 1)
so0 = so_state.solver.eval(password,cast_to=bytes)
print(format(so0.decode('utf-8')))

Overflow

15_angr_arbitrary_read

程序通过scanf获取输入,第一个key经过校验后将使用puts输出,但是都为try_again,但是一个s(try_again)在栈上,而我们的第二个输入也将写入栈上,并且输入长度并没有限制,也就是说可以覆盖s为Good Job.

image-20251002185442659

image-20251002185516504

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import angr
import claripy
import sys

io = angr.Project('./dist/15_angr_arbitrary_read', auto_load_libs=False)

init_state = io.factory.entry_state()

class Hook(angr.SimProcedure):
# hook scanf
def run(self, format_string, key_addr, password_addr):
# 无符号整数
key_bvs = claripy.BVS('key_bvs', 32)
# padding 20个字符 * 8 = 160比特
password_addr_bvs = claripy.BVS('password_addr_bvs', 20 * 8)
# 确保字符串中的每个字符都是可打印的,安装8比特1字符分组
for char in password_addr_bvs.chop(bits=8):
self.state.add_constraints(char >= 'A', char <= 'Z')
self.state.memory.store(
key_addr,
key_bvs,
endness=io.arch.memory_endness
)
self.state.memory.store(
password_addr,
password_addr_bvs
)
self.state.globals['solutions'] = (key_bvs, password_addr_bvs)


scanf_sym = "__isoc99_scanf"
io.hook_symbol(scanf_sym, Hook())
simgr = io.factory.simgr(init_state)

# 判断是否为正确状态
def success(state):
# 根据调用puts判断
jmp_puts_addr = 0x8048370
if state.addr != jmp_puts_addr: return False

goodjob_addr = 0x484F4A47
# 提取数据
puts_param = state.memory.load(state.regs.esp + 4,
4,
endness=io.arch.memory_endness)

if state.se.symbolic(puts_param):
# 拷贝状态,不对原状态产生影响
cp_state = state.copy()
# 判断提取出来的数据是否是为目标字符串所在地址
cp_state.add_constraints(puts_param == goodjob_addr)

if cp_state.satisfiable():
# 正确则返回
state.add_constraints(puts_param == goodjob_addr)
return True
else:
return False
else:
return False


simgr.explore(find=success)

if simgr.found:
so_state = simgr.found[0]

(key_so, password_so) = so_state.globals['solutions']

so0 = so_state.solver.eval(key_so)
so1 = so_state.solver.eval(password_so, cast_to=bytes)

print(so0, so1)

16_angr_arbitrary_write

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import angr
import claripy
import sys
io = angr.Project('/home/closure/Desktop/CTF/angr_ctf/dist/16_angr_arbitrary_write',auto_load_libs=False)

init_state = io.factory.entry_state()

class Hook(angr.SimProcedure):
#hook scanf
def run(self,format_string,key_addr,password_addr):
key_bvs = claripy.BVS('key_bvs',32)
password_addr_bvs = claripy.BVS('password_addr_bvs',20*8)
for char in password_addr_bvs.chop(bits=8):
self.state.add_constraints(char >= 'A', char <= 'Z')
self.state.memory.store(
key_addr,
key_bvs,
endness = io.arch.memory_endness
)
self.state.memory.store(
password_addr,
password_addr_bvs
)
self.state.globals['solutions'] = (key_bvs, password_addr_bvs)

def success(state):
#调用strncpy
strncpy_addr = 0x8048410
if state.addr == strncpy_addr:
return check_strncpy(state)
else:
return False

#从上一题检查puts变为检查strncpy
def check_strncpy(state):
#3个参数获取
strncpy_dest = state.memory.load(state.regs.esp+4,
4,
endness = io.arch.memory_endness)
#注意这边获取的是地址
strncpy_src_addr = state.memory.load(state.regs.esp+8,
4,
endness = io.arch.memory_endness)
strncpy_len = state.memory.load(state.regs.esp+12,
4,
endness = io.arch.memory_endness)
#这里获取的才是数据,请注意参考函数原型
src_contents = state.memory.load(strncpy_src_addr,strncpy_len)
if state.solver.symbolic(src_contents) and state.solver.symbolic(strncpy_dest):
password_str = "NDYNWEUJ"
buffer_addr = 0x57584344
#两项约束条件
#小端序获取字符串,验证src是否为目标字符串
check_content_password = src_contents[-1:-64] == password_str
#验证dest是否为password_buffer
check_dest_buffer_addr = strncpy_dest == buffer_addr

if state.satisfiable(extra_constraints = (check_content_password,check_dest_buffer_addr)):
state.add_constraints(check_content_password,check_dest_buffer_addr)
#条件通过
return True
else:
return False
else:
return False



scanf_sym = "__isoc99_scanf"
io.hook_symbol(scanf_sym,Hook())
simgr = io.factory.simgr(init_state)

simgr.explore(find = success)

if simgr.found:
so_state = simgr.found[0]

(key_so,password_so) = so_state.globals['solutions']

so0 = so_state.solver.eval(key_so)
so1 = so_state.solver.eval(password_so,cast_to=bytes)

print(so0,so1)

17_angr_arbitrary_jump

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
44
45
46
47
48
49
50
51
52
53
54
55
56
import angr
import claripy
import sys
io = angr.Project('/home/closure/Desktop/CTF/angr_ctf/dist/17_angr_arbitrary_jump',auto_load_libs=False)

init_state = io.factory.entry_state()

class Hook(angr.SimProcedure):
#hook scanf 与上文都类似,不再做过多解释
def run(self,format_string,scanf_input):
scanf_input_bvs = claripy.BVS('scanf_input',200*8)
for char in scanf_input_bvs.chop(bits=8):
self.state.add_constraints(char >= 'A', char <= 'Z')
self.state.memory.store(
scanf_input,
scanf_input_bvs
)
self.state.globals['scanf_input_bvs'] = scanf_input_bvs


scanf_sym = "__isoc99_scanf"
io.hook_symbol(scanf_sym,Hook())
#更改模拟引擎设置,使其不抛出无约束状态
simgr = io.factory.simgr(init_state,
save_unconstrained=True,
stashes={
'active': [init_state], #程序能进一步执行
'unconstrained': [], #无约束状态
'found': [], #找到目标路径的状态
})

#active为可以进一步搜索的所有状态的列表
#unconstrained为无约束状态
#found为已经找到目标路径的状态
while (simgr.active or simgr.unconstrained) and (not simgr.found):
#遍历所有无约束状态
for unconstrained_state in simgr.unconstrained:
#返回找到的无约束状态
def should_move(s):
return s is unconstrained_state
#将无约束状态移动至found中
simgr.move(from_stash='unconstrained',
to_stash='found',
filter_func=should_move)
simgr.step()

if simgr.found:
#取出第一个找到的状态
so_state = simgr.found[0]
print_goodjob = 0x42585249
#添加约束,验证eip是否指向目标地址
so_state.add_constraints(so_state.regs.eip == print_goodjob)

scanf_input_bvs_so = so_state.globals['scanf_input_bvs']

so0 = so_state.solver