Angr手册

一、初始化

▲简单使用

1
2
3
4
5
6
7
8
9
10
11

import angr
p=angr.Project("./signal.exe") #加载文件
state=p.factory.entry_state() #创造状态state, entry_state构造一个从函数入口点执行的状态
sm=p.factory.simgr(state) #模拟管理器
good=0x4017A5 #想要的地址
fail=0x4017A5
sm.explore(find=good,avoid=fail)
if sm.found: #如果found分类不为空
find_state=sm.found[0] #found里的状态给find_state
print(find_state.posix.dumps(0)) #获取输入

▲模板

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

filePath = "/home/hacker/Desktop/Reverse/Angr/AngrCTF_FITM/"


def Go():
path_to_binary = filePath + "./14_angr_shared_library/lib14_angr_shared_library.so"

base = 0x4000000
project = angr.Project(path_to_binary, load_options={
'main_opts' : {'custom_base_addr' : base}
})

buffer_pointer = claripy.BVV(0x3000000, 32)

validate_function_address = base + 0x6d7
initial_state = project.factory.call_state(validate_function_address, buffer_pointer, claripy.BVV(8, 32))

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

simulation = project.factory.simgr(initial_state)

success_address = base + 0x783
simulation.explore(find=success_address)

if simulation.found:
for i in simulation.found:
solution_state = i
solution_state.add_constraints(solution_state.regs.eax != 0)
solution = solution_state.solver.eval(password,cast_to=bytes)
print("[+] Success! Solution is: {0}".format(solution))
#print(scanf0_solution, scanf1_solution)
else:
raise Exception('Could not find the solution')

if __name__ == "__main__":
Go()

1.创建Project

1
2
3
4
5
path_to_binary = filePath + "00_angr_find/00_angr_find"
project = angr.Project(path_to_binary, auto_load_libs=False)
initial_state = project.factory.entry_state()
#initial_state = project.factory.blank_state(addr=start_address)
simulation = project.factory.simgr(initial_state)

auto_load_libs:表是否载入依赖库的函数,libc等,如果不载入,返回的值是不可约束的,但是载入可能会路径爆炸。

initial_state:告诉angr从入口处开始,或者从指定的地址start_address开始,不过这样一般需要设置一下寄存器。

simulation:获取我们操作的对象,之后的操作即对这个对象进行操作。

2.设置探索路径

(1)设置约束路径

①通过地址设置

1
2
print_good_address = 0x8048678
avoid_me_address = 0x080485A8

表如果程序到达print_good_address,即代表成功解题,一般题中就是输出flag的地址。

avoid_me_address则用在需要避免到达的地方

②通过打印内容设置

1
2
3
4
5
6
7
8
9
10
11
12
13
def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
if b'Good Job.' in stdout_output:
return True
else:
return False

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

当程序中有多个正确输出的时候,获取程序输出,如果输出中包含标志性的符号Good Job,则代表成功到达,或者设置其他的标志性符号。或者当程序中有多个相同的错误输出的时候,获取程序输出,如果输出中包含标志性的符号Try Again,则代表应该需要避免的地方,而不用输入多个地址。

③设置向量

一般而言,可能输入的长度或者字符有些限制,比如scanf的格式化获取输入,这时候我们就可以通过向量生成限制性的输入字符,缩小搜索路径

1
2
3
4
5
import claripy
passwd_size_in_bits = 32
passwd0 = claripy.BVS('passwd0', passwd_size_in_bits)
passwd1 = claripy.BVS('passwd1', passwd_size_in_bits)
passwd2 = claripy.BVS('passwd2', passwd_size_in_bits)

设置了三个向量,长度为32bit

BVS():代表位向量创建

FPS():代表符号向量创建

④设置寄存器

当程序有时候不需要从头开始,而是只测试某个地方或者在某个地方需要更改寄存器时,可以进行设置

1
2
3
4
#initial_state = project.factory.blank_state(addr=start_address)
initial_state.regs.eax = passwd0
initial_state.regs.ebx = passwd1
initial_state.regs.edx = passwd2

⑤设置栈上的数据

A.先设置栈帧指针
1
2
#initial_state = project.factory.blank_state(addr=start_address)
initial_state.regs.ebp = initial_state.regs.esp *#EBP=ESP*
B.观察栈上数据,然后进行设置

比如如下情形,需要0x8的栈空间

那么先将esp放到ebp-0x8的位置

1
2
3
#initial_state = project.factory.blank_state(addr=start_address)
padding_length_in_bytes = 0x08
initial_state.regs.esp -= padding_length_in_bytes

然后设置向量,压入栈中

1
2
3
4
5
6
7
#initial_state = project.factory.blank_state(addr=start_address)
passwd0 = claripy.BVS('var_10', 32) #s1
passwd1 = claripy.BVS('var_C', 32) #s2
#var_10只代表name

initial_state.stack_push(passwd0)
initial_state.stack_push(passwd1)

这样即可完成栈中数据的布置

🔺注:

这里符号化栈上数据时,只能一个一个push,即32位一次push进4字节,64位一次push进8字节,不能多,不然会造成符号截断。这里其实可以直接通过获取rsp,然后利用rsp来加载数据即可。

1
2
3
4
initial_state.regs.rsp = initial_state.regs.rbp
buf_addr = initial_state.regs.rsp - 0x10
passwd0 = claripy.BVS('password', 0x8 * 8)
initial_state.memory.store(buf_addr, passwd0)

⑥符号化内存

寻址需要符号化的内存地址,如下:

image-20211129163056788

即符号化四个八字节长度的user_input

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
passwd_size_in_bits = 64
passwd0 = claripy.BVS('passwd0', passwd_size_in_bits)
passwd1 = claripy.BVS('passwd1', passwd_size_in_bits)
passwd2 = claripy.BVS('passwd2', passwd_size_in_bits)
passwd3 = claripy.BVS('passwd3', passwd_size_in_bits)

#initial_state = project.factory.blank_state(addr=start_address)
passwd0_address = 0xA1BA1C0
#passwd1_address = 0xA1BA1C8
#passwd2_address = 0xA1BA1D0
#passwd3_address = 0xA1BA1D8
initial_state.memory.store(passwd0_address, passwd0)
initial_state.memory.store(passwd0_address + 0x8, passwd1)
initial_state.memory.store(passwd0_address + 0x10, passwd2)
initial_state.memory.store(passwd0_address + 0x18, passwd3)

⑦符号化堆内存

即在buffer0和buffer1中保存的是malloc出来的内存指针

image-20211129164136483

image-20211129164213775

这样我们就可以向这两个buf中写入内存地址,然后把需要符号化的内存写入到对应的内存地址中,这里选取的内存地址设置为

1
2
fake_heap_address0 = 0xffffc93c
fake_heap_address1 = 0xffffc94c

最终如下设置

1
2
3
4
5
6
7
8
9
10
#initial_state = project.factory.blank_state(addr=start_address)
fake_heap_address0 = 0xffffc93c
pointer_to_malloc_memory_address0 = 0xabcc8a4
fake_heap_address1 = 0xffffc94c
pointer_to_malloc_memory_address1 = 0xabcc8ac
initial_state.memory.store(pointer_to_malloc_memory_address0, fake_heap_address0, endness=project.arch.memory_endness)
initial_state.memory.store(pointer_to_malloc_memory_address1, fake_heap_address1, endness=project.arch.memory_endness)

initial_state.memory.store(fake_heap_address0, passwd0)
initial_state.memory.store(fake_heap_address1, passwd1)

⑧符号化文件

1
2
3
4
5
6
7
8
9
10
11
filename = 'OJKSQYDP.txt'
#64个字节
symbolic_file_size_bytes = 64
#创建文件向量
passwd0 = claripy.BVS('password', symbolic_file_size_bytes * 8)
passwd_file = angr.storage.SimFile(filename, content=passwd0, size=symbolic_file_size_bytes)


#initial_state = project.factory.blank_state(addr=start_address)
#将文件向量插入
initial_state.fs.insert(filename, passwd_file)

3.约束比较函数

当在循环里进行比较判断是,会产生很多分支

1
2
3
4
5
6
7
8
9
10
11
12
_BOOL4 __cdecl check(int a1, unsigned int a2)
{
int v3; // [esp+8h] [ebp-8h]
unsigned int i; // [esp+Ch] [ebp-4h]
v3 = 0;
for ( i = 0; i < a2; ++i )
{
if ( *(_BYTE *)(i + a1) == *(_BYTE *)(i + 0x804A040) )
++v3;
}
return v3 == a2;
}

这里的a2=16,即将16个字符都拿出来逐个比较,每次比较产生两个分支,16次比较就会产生2^16=65536个分支,会使得分支变得很大,但是这里其实只是一个比较函数,保证a1代表字符串的内容和0x804A040中保存的字符串内容相等就可以了,这里就可以运行到该函数之前,然后自己获取对应内存中值进行比较,添加限制即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#initial_state = project.factory.blank_state(addr=start_address)

#buff_addr中保存的即为我们运行到比较函数之前得到的字符串,使之与0x804A040中的字符串进行比较
buff_addr = 0x0804A050
address_to_check_constraint = 0x08048565
simulation = project.factory.simgr(initial_state)
#探索路径到进入比较函数之前
simulation.explore(find=address_to_check_constraint)
if simulation.found:
solution_state = simulation.found[0]
constrained_parameter_address = buff_addr
constrained_parameter_size_bytes = 16
constrained_parameter_bitvector = solution_state.memory.load(constrained_parameter_address,constrained_parameter_size_bytes)


#0x804A040中保存的即为"AUPDNNPROEZRJWKB"
constrained_parameter_desired_value = 'AUPDNNPROEZRJWKB'

#比较约束,获得使两值相等的符号量
solution_state.solver.add(constrained_parameter_bitvector == constrained_parameter_desired_value)

#得到最终解
solution0 = solution_state.solver.eval(passwd0,cast_to=bytes)

4.使用HOOK函数

(1)利用地址进行hook

即用自己编写的函数替代程序中的函数来执行,假设这里想hook掉以下函数,该函数即为之前的比较函数check

image-20211129172222733

那么看字节码可知对应的函数为5个字节,地址为0x80486B3

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
check_equals_called_address = 0x80486B3
instruction_to_skip_length = 5

#以下是一个整体
#利用hook函数进行替换
@project.hook(check_equals_called_address, length=instruction_to_skip_length)
def skip_check_equals_(state):
#函数的输入参数
user_input_buffer_address = 0x804A054
user_input_buffer_length = 16
user_input_string = state.memory.load(
user_input_buffer_address,
user_input_buffer_length,
endness=archinfo.Endness.LE
)
#一般都是小端,大端:BE

#需要比较的字符串
check_against_string = 'XKSPZSJKJYQCQXZV'

#设置返回的寄存器,只返回给了eax寄存器,所以只需要设置eax寄存器
register_size_bit = 32
state.regs.eax = claripy.If(
user_input_string == check_against_string,
claripy.BVV(1, register_size_bit),
claripy.BVV(0, register_size_bit)
)

(2)利用函数名进行hook

当函数被调用了多次,而我们又需要对每一个这个函数进行hook时,就可以通过函数名进行hook

1
2
3
4
5
6
7
8
9
10
11
12
13
_BOOL4 __cdecl check_equals_ORSDDWXHZURJRBDH(char *to_check, unsigned int length)
{
int v3; // [esp+8h] [ebp-8h]
unsigned int i; // [esp+Ch] [ebp-4h]

v3 = 0;
for ( i = 0; i < length; ++i )
{
if ( to_check[i] == *(_BYTE *)(i + 0x804C048) )
++v3;
}
return v3 == length;
}

比如替换上述的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ReplacementCheckEquals(angr.SimProcedure):
def run(self, to_check, length):
#第一个参数
user_input_buffer_address = to_check
#第二个参数
user_input_buffer_length = length

#依据参数获取输入的字符串
user_input_string = self.state.memory.load(
user_input_buffer_address,
user_input_buffer_length)

#执行比较功能并返回
check_against_string = 'ORSDDWXHZURJRBDH'
return claripy.If(
user_input_string == check_against_string,
claripy.BVV(1, 32),
claripy.BVV(0, 32))

check_equals_symbol = 'check_equals_ORSDDWXHZURJRBDH'
project.hook_symbol(check_equals_symbol, ReplacementCheckEquals())

主要就是定义一个类,将所有函数名称为check_equals_ORSDDWXHZURJRBDH的函数用类ReplacementCheckEquals中的run函数替换

(3)Hook掉Scanf函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ReplacementScanf(angr.SimProcedure):
def run(self, format_string, param0, param1):

#设置要存入对应参数内存中的数据
scanf0 = claripy.BVS('scanf0', 32)
scanf1 = claripy.BVS('scanf1', 32)

#获取需要存入的内存地址,然后以符号向量的形式存储进去
scanf0_address = param0
self.state.memory.store(scanf0_address, scanf0, endness=project.arch.memory_endness)
scanf1_address = param1
self.state.memory.store(scanf1_address, scanf1, endness=project.arch.memory_endness)

#将两个向量数据设置为全局的,方便之后求解打印
self.state.globals['solutions'] = (scanf0, scanf1)
scanf_symbol = '__isoc99_scanf'

project.hook_symbol(scanf_symbol, ReplacementScanf())

(4)Hook静态库函数

当程序使用静态编译时,可能需要Hook一些静态库函数

在探索开始之前添加即可

1
2
3
4
project.hook(0x804ed40, angr.SIM_PROCEDURES['libc']['printf']())
project.hook(0x804ed80, angr.SIM_PROCEDURES['libc']['scanf']())
project.hook(0x804f350, angr.SIM_PROCEDURES['libc']['puts']())
project.hook(0x8048d10, angr.SIM_PROCEDURES['glibc']['__libc_start_main']())

(5)处理函数在外部库so中

当程序对于flag的加解密处理函数在外部库中时,这时候可以直接调用外部库中的函数进行符号执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#因为库函数动态加载,所以需要指定偏移
base = 0x4000000
project = angr.Project(path_to_binary, load_options={
'main_opts' : {'custom_base_addr' : base}
})

#为输入参数创建内存向量的地址
buffer_pointer = claripy.BVV(0x3000000, 32)

#设置需要调用的函数地址
validate_function_address = base + 0x6d7
#设置状态,调用函数
initial_state = project.factory.call_state(validate_function_address, buffer_pointer, claripy.BVV(8, 32))

#装载内存向量进入地址指针中
password = claripy.BVS('password', 8*8)
initial_state.memory.store(buffer_pointer, password)

main_opts为主程序指定加载参数,常用以下参数

  • custom_base_addr —— 使用的基地址
  • custom_entry_point —— 使用的入口点
  • custom_arch —— 使用的处理器体系结构的名字

这样在之后的设置成功地址也需要用到base

1
success_address = base + 0x783

(6)快速hook

1
2
3
4
5
6
7
8
9
10
def hook_Normal(state):
state.regs.rax = 8
p.hook(0x40168e, hook_Normal, length=5)

#或者
p.hook_symbol('ptrace', angr.SIM_PROCEDURES['stubs']['ReturnUnconstrained'](return_value=0))

#直接pass
def patch_0(state):
pass

5.开始探索

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

simulation.explore表开始探索,试图进入到print_good_address

▲veritesting模式

该模式可以对路径爆炸进行一些解决

https://www.anquanke.com/post/id/251984

但是在需要探索所有路径的时候,veritesting会出错,将一些路径合并,从而无法抵达正确的deadend路径,详见csaw_wyvern/wyvern

1
simulation = project.factory.simgr(initial_state, veritesting=True)

6.输出答案

(1)符号输出

1
2
3
4
5
6
if simulation.found:
solution_state = simulation.found[0]
solution = solution_state.posix.dumps(sys.stdin.fileno())
print("[+] Success! Solution is: {}".format(solution.decode("utf-8")))
else:
raise Exception('Could not find the solution')

即获取找到的第一串符号,然后输出

(2)位向量输出

这个一般在最开始使用位向量BVS创建输入的时候使用

1
2
3
4
5
6
7
8
9
10
11
12
13
#passwd0 = claripy.BVS('passwd0', passwd_size_in_bits)
#passwd1 = claripy.BVS('passwd1', passwd_size_in_bits)
#passwd2 = claripy.BVS('passwd2', passwd_size_in_bits)
if simulation.found:
for solution_state in simulation.found:
solution0 = format(solution_state.solver.eval(passwd0), 'x')
solution1 = format(solution_state.solver.eval(passwd1), 'x')
solution2 = format(solution_state.solver.eval(passwd2), 'x')
solution = solution0 + " " + solution1 + " " + solution2
print("[+] Success! Solution is: {}".format(solution))
# print(simgr.found[0].posix.dumps(0))
else:
raise Exception('Could not find the solution')

输出的是十六进制的数字

(3)求解选项

接口 描述
solver.eval(expression) 将会解出一个可行解
solver.eval_one(expression) 将会给出一个表达式的可行解,若有多个可行解,则抛出异常
solver.eval_upto(expression, n) 将会给出最多n个可行解,如果不足n个就给出所有的可行解。
solver.eval_exact(expression, n) 将会给出n个可行解,如果解的个数不等于n个,将会抛出异常。
solver.min(expression) 给出最小可行解
solver.max(expression) 给出最大可行解

二、进阶管理

Stash: active 、deadend 、found等

1.运行方式

基本块的区分通常以callcmp-jz/jmp...ret等为标志。

(1)step()

每次向前运行一个基本块,并返回进行分类

让 stash 中的所有状态都执行一个基本块,默认的 stash 为 active

(2)run()

每次向前运行一个基本块,并返回进行分类

(3)explore()

根据findavoid进行基本块的执行,最后会返回foundavoid状态

2.跨平台使用

(1)ARM

直接对应使用,不用添加其他的什么配置

1
state.regs.r0 = concrete_addr

3.直接使用二进制里的函数

文件在”/home/hacker/Desktop/Reverse/Angr/myTest/callBinaryFunc”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#myFunc函数的地址,即函数最开始的汇编代码的地址
myFunc_addr = base + 0x64A
project = angr.Project(path_to_binary, auto_load_libs=True)

#(SimTypeInt(False)表设置参数,传参和返回值的类型设置
prototype = SimTypeFunction((SimTypeInt(False),SimTypeInt(False)),SimTypeInt(False))

myFunc = project.factory.callable(myFunc_addr, cc=project.factory.cc(func_ty=prototype), toc=None, concrete_only=True)

#调用函数
myFunc.perform_call(0,0x100)

#获取最终的状态state,即可从该state中获取返回值和各种寄存器状态
state = myFunc.result_state
print(state.regs.rax)

或者如下:

1
func = project.factory.callable(project.loader.find_symbol('myFunc).rebased_addr)

4.C++库的实现

只能从最开始进行

由于angr是只实现了C库,为了深入C++标准库中,我们需要在设置state时需要使用full_init_state方法,并且设置unicorn引擎

unicorn引擎可能可以加快运行速度,有时候也会放慢速度,就比如在只有一条路径,但是需要添加flag约束的:asisctffinals2015_fake

1
2
3
4
5
initial_state = project.factory.full_init_state(
args=path_to_binary,
add_options=angr.options.unicorn,
stdin=flag,
)

▲注:

这种从最开始执行的方法通常用在路径分支不多的时候,如果比较多就容易路径爆炸。

5.loader 加载模块

将二进制文件加载到虚拟的地址空间

(1)加载二进制选项

在加载二进制文件时可以设置特定的参数,使用 main_optslib_opts 参数进行设置。

  • backend - 指定 backend
  • base_addr - 指定基址
  • entry_point - 指定入口点
  • arch - 指定架构
1
2
>>> angr.Project('examples/fauxware/fauxware', main_opts={'backend': 'blob', 'arch': 'i386'}, lib_opts={'libc.so.6': {'backend': 'elf'}})
<Project examples/fauxware/fauxware>

(2)获取加载对象的信息

1
2
3
4
5
6
7
obj = proj.loader.main_object
obj = proj.loader.all_objects
obj.sections
obj.plt
obj.linked_base
obj.mapped_base
obj.max_addr

(3)符号和重定位信息

①符号对象信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
malloc = proj.loader.find_symbol('malloc')
malloc = proj.loader.main_object.get_symbol('malloc')
#之后可以使用malloc这个对象
>>> malloc.
malloc.is_common malloc.is_local malloc.owner_obj malloc.resolvedby
malloc.is_export malloc.is_static malloc.rebased_addr malloc.size
malloc.is_extern malloc.is_weak malloc.relative_addr malloc.subtype
malloc.is_forward malloc.linked_addr malloc.resolve( malloc.type
malloc.is_function malloc.name malloc.resolve_forwarder(
malloc.is_import malloc.owner malloc.resolved

#获取符号对象地址
>>> malloc.rebased_addr
0x10002c0
>>> malloc.linked_addr
0x2c0
>>> malloc.relative_addr
0x2c0

②共享库信息

选项

auto_load_libs 是否自动加载程序的依赖
skip_libs 避免加载的库
except_missing_libs 无法解析共享库时是否抛出异常
force_load_libs 强制加载的库
ld_path 共享库的优先搜索搜寻路径

信息

1
2
3
4
5
6
>>> proj = angr.Project('/bin/true')
>>> proj.loader.shared_objects
OrderedDict([('true', <ELF Object true, maps [0x400000:0x60721f]>), ('libc.so.6', <ELF Object libc-2.27.so, maps [0x1000000:0x13f0adf]>), ('ld-linux-x86-64.so.2', <ELF Object ld-2.27.so, maps [0x2000000:0x222916f]>)])
>>> proj = angr.Project('/bin/true', load_options={"auto_load_libs": False})
>>> proj.loader.shared_objects
OrderedDict([('true', <ELF Object true, maps [0x400000:0x60721f]>)])

③Libc函数信息

1
2
3
4
>>> angr.procedures.libc.malloc
<module 'angr.procedures.libc.malloc' from '/home/angr/angr-dev/angr/angr/procedures/libc/malloc.py'>
>>> angr.SIM_PROCEDURES['libc']['malloc']
<class 'angr.procedures.libc.malloc.malloc'>

6.Hook信息

查看hook相关的信息

1
2
3
4
5
6
7
8
9
10
11
12
>>> stub_func = angr.SIM_PROCEDURES['stubs']['ReturnUnconstrained'] # this is a CLASS
>>> proj.hook(0x10000, stub_func()) # hook with an instance of the class
>>> proj.is_hooked(0x10000) # these functions should be pretty self-explanitory
True
>>> proj.hooked_by(0x10000)
<ReturnUnconstrained>
>>> proj.unhook(0x10000)
>>> @proj.hook(0x20000, length=5)
... def my_hook(state):
... state.regs.rax = 1
>>> proj.is_hooked(0x20000)
True

三、命令行使用angr

1.docker环境

1
sudo docker run -it --rm -v $PWD/myTest:/myTest angr/angr

2.前期准备

(1)进入angr

1
2
3
4
5
6
7
8
import angr
import logging
project = angr.Project("/myTest/callBinaryFunc")
#使用unicorn引擎
initial_state = project.factory.entry_state(add_options=angr.options.unicorn)
logging.getLogger('angr').setLevel(logging.INFO)
#现在就是停在start函数的入口
hex(initial_state.addr)

3.中期调试

(1)中断调试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import logging
logging.getLogger('angr').setLevel(logging.INFO)
#from pwn import*


def hook(l=None):
if l:
locals().update(l)
import IPython
IPython.embed(banner1='',confirm_exit=False)
exit(0)

#然后在需要中断的地方调用该函数即可
hook(locals())

或者使用ipdb

1
2
3
4
import ipdb

#在需要断点的地方下断点即可
ipdb.set_trace()

使用手册

1
2
3
4
5
6
7
8
9
10
11
12
13
h(help):帮助命令
s(step into):进入函数内部
n(next):执行下一行
b(break): b line_number 打断点
cl(clear): 清除断点
c(continue): 一直执行到断点
r(return): 从当前函数返回
j(jump): j line_number,跳过代码片段,直接执行指定行号所在的代码
l(list): 列出上下文代码
a(argument): 列出传入函数所有的参数值
p/pp: print 和 pretty print 打印出变量值
r(restart): 重启调试器
q(quit): 推出调试,清除所有信息

(2)angr管理器

1
2
3
4
5
6
7
8
#获取路径管理器,从入口开始运行
sm = project.factory.simulation_manager()
sm.run()

#或者如下设置,设置初始状态和入口地址,从该状态开始探索,直到find
initial_state = project.factory.blank_state(addr=start_address)
simulation = project.factory.simgr(initial_state,veritesting=True)
simulation.explore(find=success_address)

以下为运行之后可能的状态路径

image-20211208111243588

image-20211208111727739

然后即可通过状态来查看当前状态的中的各种内存,寄存器等

image-20211208111922248

image-20211208111957281

(3)获取当前块相关信息

1
2
3
#查看block相关信息
block = state.block()
block.capstone.pp()

image-20211207175452322

(4)调试core

一般而言,core可以从gdb中获得,得到当前gdb状态的所有数据,包括加载的libc函数等,我们就可以中获得相关的数据来初始化我们的state

image-20211208155507602

然后就可以对应调试了,设置还是一样的设置,只不过地址直接变成当前的gdb中的地址即可。可以看到所有内存基本一致,只有rbp,rsp等栈指针不太一样,这样比较方便我们来获取数据调试。

image-20211208155814628

(5)继续探索

当使用过的simulation想接着某个状态继续探索时,直接使用以下代码不会成功

1
simulation.explore(find = addr)

这时候需要获取当前simulation的状态state,然后依据该状态state新建一个newSimulation才能继续探索

1
2
3
4
def nextFind(project,state,find=0,avoid=0):
simulation = project.factory.simgr(state,veritesting=True)
simulation.explore(find = find,avoid = avoid)
return simulation

▲TIPS

1
2
3
4
#输入?获取帮助
p.factory.simulation_manager?


1.获取字符串

1
2
3
4
5
6
7
8
#转成byte类型的字符串
#sm = p.factory.simulation_manager()
#sm.step()
#state = sm.active[0]
flag = state.mem[addr].string
flag.concrete

sm.deadended[1].posix.stdout.concretize()

image-20211207180744674

image-20211208111616726

TIPS

1.求值问题

(1)求BVV符号值

1
2
3
4
a = claripy.BVV(0xffffffff, 0x4*8)
s = claripy.Solver()
if(s.eval(a, 0)[0] == 0xffffffff):
print("Get!")

(2)获取内存符号值

1
2
3
4
5
6
7
state = proj.factory.entry_state()
#add rax, qword ptr [rsp + 8]
state.regs.rax += state.mem[state.regs.rsp + 8].uint64_t.resolved

result = simgr.found[0].memory.load(flag_addr, 40)
#or get_byte
simulation.found[0].solver.eval(result.get_bytes(0,0x17),cast_to=bytes)

(3)输入参数的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#set
initial_state = p.factory.entry_state(args=[binary,argv1])
simgr = p.factory.simgr(inital_state,veritesting=True)
found = simgr.found[0]
#get
found.solver.eval(argv1,cast_to=bytes)


#or such as
data = claripy.BVS('data',20*8)
p = angr.Project(path_to_binary, auto_load_libs=True)
inital_state = p.factory.entry_state(stdin=data)
simgr = p.factory.simgr(inital_state,veritesting=True)
#get
print(simgr.found[0].posix.stdin.concretize())

(4)向量连接

claripy.Concat方法用于bitVector的连接

1
2
3
4
5
6
7
flag_chars = claripy.BVS('flag_%d' % 1, 8)
flag = claripy.Concat(flag_chars + claripy.BVV(b'\n'))

#list中的向量
flag_chars = [claripy.BVS('flag_%d' % i, 8) for i in range(28)]
flag = claripy.Concat(*flag_chars + [claripy.BVV(b'\n')])
#之后在state中设置stdin=flag即可从命令行输入多组向量

(5)获取内存进行对比

1
found.add_constraints(found.memory.load(flag_addr, 5) == int(binascii.hexlify(b"ASIS{"), 16))

(6)常见添加flag的约束

当输入某个数字,经过计算打印flag,只有简单几条路径时,就需要对flag进行约束才能正常输出,asisctffinals2015_fake

添加约束的时候,不能使用eval求解,只能使用load

①整个Flag约束

1
2
3
4
5
6
7
8
9
10
11
12
13
#这里即代表flag中应该都是[0~9]或者[a~z][-,_],最后应该以'}'结尾
flag = found.memory.load(flag_addr, 40)
for i in range(5, 5+32):
cond_0 = flag.get_byte(i) >= ord('0')
cond_1 = flag.get_byte(i) <= ord('9')
cond_2 = flag.get_byte(i) >= ord('a')
cond_3 = flag.get_byte(i) <= ord('z')
cond_4 = found.solver.And(cond_0, cond_1)
cond_5 = found.solver.And(cond_2, cond_3)
cond_6 = flag.get_byte(i) == ord('-')
cond_7 = flag.get_byte(i) == ord('_')
found.add_constraints(found.solver.Or(cond_4, cond_5,cond_6,cond_7))
found.add_constraints(flag.get_byte(32+5) == ord('}'))

②向量约束

1
2
for b in arg1.chop(8):
initial_state.add_constraints(b != 0)

③字符约束

1
2
3
4
5
6
7
8
9
10
11
12
13
#可见
for i in range(36):
# We want those flags to be printable characters
state.add_constraints(flag.get_byte(i) >= 0x20)
state.add_constraints(flag.get_byte(i) <= 0x7e)

#Or
for i in range(dataLen):
state.solver.add(
claripy.Or(
*(data.get_byte(i) == x for x in printable)
)
)

④特殊约束

1
2
found.add_constraints(found.memory.load(flag_addr, 5) == int(binascii.hexlify(b"ASIS{"), 16))
found.add_constraints(flag.get_byte(32+5) == ord('}'))

(7)方便的栈数据存取操作

1
2
3
4
5
6
state.regs.rbp = state.regs.rsp
state.mem[state.regs.rbp - 0x74].uint32_t = 0x40
state.mem[state.regs.rbp - 0x70].uint64_t = 0x1000
state.mem[state.regs.rbp - 0x68].uint64_t = 0x1008
state.mem[state.regs.rbp - 0x60].uint64_t = 0x1010
state.mem[state.regs.rbp - 0x58].uint64_t = 0x1018

(8)获取state的输出

1
2
3
4
5
6
7
8
9
10
11
12
#p = angr.Project("/myTest/callBinaryFunc")
#sm = p.factory.simulation_manager()
print(sm.deadended[0].posix.stdout.concretize())


#当需要从多条路径遍历获取flag时
out = b''
for pp in simulation.deadended:
out = pp.posix.dumps(1)
if b'flag{' in out:
final_flag = next(filter(lambda s: b'flag{' in s, out.split()))
print("[+] Success! Solution is: {0}".format(final_flag))

2.无法识别的函数

strtol(),strlen()等一系列的字符串操作

3.末尾加’\x00’问题

有时候会向量设置,最好在末尾加上’\x00’,使得某些情况能够通过。同样的当有canary的时候,没办法从压入canary的地方开始的时候,这时候最好给canary向量化或者给’\x00’截断。sym-write

4.符号写问题

同样的,在很多时候,最好加上{angr.options.SYMBOLIC_WRITE_ADDRESSES}选项,这个其实也不是很清楚,有的题不加也可以,但是有的题不加就不行。

1
state = project.factory.entry_state(add_options={angr.options.SYMBOLIC_WRITE_ADDRESSES})

5.32位问题

x86的32位程序下,参数从栈上取,有时候通常需要设置这个才行的flareon2015_2

1
2
3
4
s.memory.store(s.regs.esp+12, s.solver.BVV(50,8*4))
s.mem[s.regs.esp+8].uint32_t = 0x402159
s.mem[s.regs.esp+4].uint32_t = 0x4010e4
s.mem[s.regs.esp].uint32_t = 0x401064

6.检查问题

正常情况下某个 state 被发现是不可满足条件的,那么state会进行回溯,来确定到底是哪个state不被满足,之后所有的state会被放入到stash中。但是有的时候回溯并不好,会比较不容易出结果,尤其是运算量比较大的时候,这时候就可以添加LAZY_SOLVES,不进行检查,先把所有路径跑完。

总的来说LAZY_SOLVES是在路径爆炸和花费在约束求解上的时间之间的平衡。

同样的,有的版本是默认开启,有的是默认关闭,对应修改即可

1
2
state = p.factory.blank_state(addr=START_ADDR, add_options={angr.options.LAZY_SOLVES})
state = p.factory.blank_state(addr=START_ADDR, remove_options={angr.options.LAZY_SOLVES})

详见题目google2016_unbreakable_1

比较适用于当find路径和avoid路径都比较容易抵达的时候,即某些库函数调用比较少,但是运算量比较庞大的时候,类似如下全是运算的

image-20211210175000853

https://docs.angr.io/appendix/options#option-sets

day1-r1-a-1.pdf (hitcon.org)

https://flagbot.ch/lesson5.pdf

7.执行非.text段代码

正常的angr只能执行.text段的代码,但是有的题会执行非.text段的代码,不过需要在创建Project的时候加入选项support_selfmodifying_code=True,详见题目tumctf2016_zwiebel

1
project = angr.Project("zwiebel", support_selfmodifying_code=True) 

8.指定运行代码块数

设定n,表示从state的状态开始运行3个基本块

1
simulation.run(n=3)

帮助

angr — Analysis and Coordination — angr 9.0.10689 documentation

https://xz.aliyun.com/t/3990

https://www.cnblogs.com/61355ing/p/10523147.html

angr Tutorials EP1 - Reverse Engineering 101 - YouTube

angr

关于Faster的那些事… (myts2.cn)

[原创]符号执行在自动化Pwn中的简单利用-Pwn-看雪论坛-安全社区|安全招聘|bbs.pediy.com

shellphish/driller: Driller: augmenting AFL with symbolic execution! (github.com)