angr 使用详细步骤

0.引入angr 和claripy

1.创建项目project

project=angr.Project(path,auto_load_libs=False)

其中path为要运行的文件的路径比如这样

path="C:\\Users\\DELL\\Desktop\\angr_ctf-master\\solutions\\11_angr_sim_scanf\\11_angr_sim_scanf"

auto_load_libs=False的意思是不自动自动载入依赖的库

如果是位置无关文件,还要加上载入的基地址,比如这样

base = 0x66666666     # 动态链接库被装载的时候需要一个基址
proj = angr.Project(path, main_opts={'base_addr': base})

2.设置初始状态initial_state

initial_state = project.factory.entry_state()

其中entry_state()意思是从程序入口点开始的,在elf文件里也就是start函数的开始位置

如果想知道设置状态的开始地址可以这样

print(intial_state.addr)

打印出这个state的地址

当然我们可以不选择entry_state(),而是我们自定义的地址

比如这样

start_address = 0x08048980
initial_state = project.factory.blank_state(addr=start_address)

blank_state:构造一个“空状态”,它的大多数数据都是未初始化的。当使用未初始化的的数据时,一个不受约束的符号值将会被返回

.entry_state:构造一个已经准备好从函数入口点执行的状态

  • 在这个地方还可以添加veritesting 具体用法看文章末尾部分

3. 创建符号位向量,并将其存入内存或者其他地方(非必须)

首先应当说的是,这一部分是为了解决scanf问题而存在的,针对不同的情况我们将会将变量存入不同的地方

创建符号位向量

符号位向量是angr用于将符号值注入程序的数据类型。这些将是angr将解决的方程式的“ x”,也就是约束求解时的自变量。可以通过 BVV(value,size)BVS( name, size) 接口创建位向量

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)

应当注意的是,第二个参数时位(bit)的个数,而不是字节的数量

存入寄存器

init_state.regs.eax = password0
init_state.regs.ebx = password1
init_state.regs.edx = password2

存入内存(全局变量)

pwd0_address = 0x0A29FAA0
pwd1_address = 0x0A29FAA8
pwd2_address = 0x0A29FAB0
init_state.memory.store(pwd0_address, password0)
init_state.memory.store(pwd1_address, password1)
init_state.memory.store(pwd2_address, poassword2)

存入栈空间(局部变量)

这里涉及到栈平衡,需要先将esp抬高到局部变量之前的位置

# 下面我们手动创建本函数的栈,并在对应v2和v1的栈的位置放入我们要求的符号变量
init_state.regs.ebp = init_state.regs.esp
init_state.regs.esp -= 8
v2 = init_state.solver.BVS("ebp-12", 32) # 创建两个符号变量
v1 = init_state.solver.BVS("ebp-16", 32)
init_state.stack_push(v2)
init_state.stack_push(v1) # 放入对应位置

接收动态内存(new malloc)

我们需要将原本应该存变量的堆空间改成虚假的占空间

# 下面将接收malloc分配的空间的指针覆盖为指向另外的空内存的指针(地址)用来模拟malloc分配的空间
virtual_heap_address0 = 0x44444444
pointer_to_recv_mallocspace0 = 0x0A79A118
virtual_heap_address1 = 0x44444454
pointer_to_recv_mallocspace1 = 0x0A79A120
init_state.memory.store(pointer_to_recv_mallocspace0, virtual_heap_address0, endness=proj.arch.memory_endness)
init_state.memory.store(pointer_to_recv_mallocspace1, virtual_heap_address1, endness=proj.arch.memory_endness)
# 创建两个未知的符号变量
pwd0 = init_state.solver.BVS('pwd0', 8*8)
pwd1 = init_state.solver.BVS('pwd1', 8*8) # 8个字节,一个字节占8位

# 下面我们往虚拟的malloc分配的空间写入我们要求解的符号变量
init_state.memory.store(virtual_heap_address0, pwd0)
init_state.memory.store(virtual_heap_address1, pwd1)

4.对函数进行hook(非必须)

当然这个部分也不是必须的

若只有一个函数让路径爆炸,通过hook调用要爆炸的那个函数的指令,替换成执行我们的回调函数(里面手动添加约束条件)

对函数hook有两种方式,一种是通过原函数的地址进行hook,一种是通过原函数的名字进行hook

通过地址进行hook

先看一个例子

hook_address = 0x080486B8                             # call check_equals_XKSPZSJKJYQCQXZV的指令地址,钩取了这个地址就不会执行那条调用让路径爆炸的指令
step_length = 5 # hook设置的钩子函数执行完之后,从被钩取的那个地址开始要跳过的字节数(这里就是call .. 的长度,为5字节)


@proj.hook(hook_address, length=step_length) # 在对应地址设置钩子,length=参数是设置要跳过的字节数
def our_function(state):
input_address = 0x0804A054
size_bytes = 16
input_string = state.memory.load(input_address, size_bytes) # 获取complex_function函数加密之后的符号变量(user_input)
cmp_string = "XKSPZSJKJYQCQXZV"
# 下面是claripy来处理if,gcc32的函数是用eax来传回返回值的,根据字符串是否相等传回1或0
state.regs.eax = claripy.If(
input_string == cmp_string,
claripy.BVV(1, 32),
claripy.BVV(0, 32)
)
# claripy.BVV是设置符号常量,如果字符串相等,就将eax设置为1的BVV,不等就设置为0的BVV

这里来源于大佬的原文章,注释已经非常详细了

通过函数名进行hook

如果那个路径爆炸的函数被调用多次,直接通过符号名来hook函数所有的调用地址

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())

再介绍一种hook scanf函数的方法,这里要涉及到将位向量转换成全局变量的过程

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())

5. 设置Simulation Managers

SimState 对象包含程序运行时信息,如内存/寄存器/文件系统数据等。SM(Simulation Managers)是angr中最重要的控制接口,它使你能够同时控制一组状态(state)的符号执行,应用搜索策略来探索程序的状态空间。

simulation = project.factory.simgr(initial_state)

6. 运行,探索满足路径需要的值

simulation.explore(find=···,avoid=···)

这里有两种方式设置成功和避免的条件,一种是通过地址,一种是通过检验输出

通过地址的方式

即在find=avoid=后面添加地址

simulation.explore(find=0x405000,avoid=0x404700)

通过检验输出的方式

很多时候,成功的标志都是输出一个提示符,失败也一样,因此我们可以通过检验输出来判断是否成功

def is_successful(state):    # 判断是否输出'Good Job.'的state
stdout = state.posix.dumps(1)
return b'Good Job.' in stdout
def should_abort(state): # 避免输出'Try again.'的state
stdout_output = state.posix.dumps(1)
return b'Try again.' in stdout_output
simulation.explore(find=is_successful, avoid=should_abort)

要注意这里findavoid后面的函数不加()

这里的 .posix.dumps() 中 ,括号里面是0的时候代表输入,是1的时候代表输出

7. 获取执行结果

if simulation.found:
solution_state = simulation.found[0] # 获取通过 explore 找到符合条件的状态
solution = solution_state.posix.dumps(0)
print("[+] Success! Solution is: {}".format(solution.decode("utf-8")))
else:
print("no!")

这个例子只获取了一个解,如果想要获取多解,可以遍历simulation.found

for i in simulation.found:
solution_state=i
···

8. 添加约束条件(非必须)

在第7步中间可以添加约束条件,这往往用在路径爆炸的时候,explore的成功标志在在路径爆炸之前,然后在第7步的时候手动添加限制条件

比如这样:

if simulation.found:                                       # 如果找到了可以执行到stop_address的state就往下执行
for x in simulation.found:
solution_state = x
constrained_input_address = 0x804a050 # 符号变量存放的地址(模拟输入)
constrained_size_bytes = 16 # 0x804a050地址开始存放了16字节
# 取出了我们的符号变量被complex_function函数加密之后的结果
constrained_bitvector = solution_state.memory.load(constrained_input_address, constrained_size_bytes)
# 下面手动添加约束条件,让其和比较字符串相等
cmp_string = "BWYRUBQCMVSBRGFU"
# solution_state.add_constraints(constrained_bitvector == cmp_string) 也可像下方一样写
solution_state.solver.add(constrained_bitvector == cmp_string)
# 得到结果
solved_string = solution_state.se.eval(string, cast_to=bytes).decode('utf-8')
print(solved_string)

先取变量,使用.solver.add添加约束条件,使用.se.eval获得符合条件的值

其他的一些东西

veritesting

简单来说就是Veritesting结合了静态符合执行与动态符号执行,减少了路径爆炸的影响,在angr里我们只要在构造模拟管理器时,启用Veritesting了就行

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

静态编译的处理方法

这里其实就是对库函数进行一些hook所在的步骤位置在第4步

angr.SIM_PROCEDURES['libc']['malloc']
angr.SIM_PROCEDURES['libc']['fopen']
angr.SIM_PROCEDURES['libc']['fclose']
angr.SIM_PROCEDURES['libc']['fwrite']
angr.SIM_PROCEDURES['libc']['getchar']
angr.SIM_PROCEDURES['libc']['strncmp']
angr.SIM_PROCEDURES['libc']['strcmp']
angr.SIM_PROCEDURES['libc']['scanf']
angr.SIM_PROCEDURES['libc']['printf']
angr.SIM_PROCEDURES['libc']['puts']
angr.SIM_PROCEDURES['libc']['exit']
proj.hook(0x08048D10, angr.SIM_PROCEDURES['glibc']['__libc_start_main']())
proj.hook(0x0804ED40, angr.SIM_PROCEDURES['libc']['printf']())
proj.hook(0x0804ED80, angr.SIM_PROCEDURES['libc']['scanf']())
proj.hook(0x0804F350, angr.SIM_PROCEDURES['libc']['puts']())
proj.hook(0x0805B450, angr.SIM_PROCEDURES['libc']['strcmp']())

栈溢出利用

angr可以实现对漏洞的利用,在这里不做详细解释(因为笔者不太会pwn)

附加 公式模板

import angr
import sys
def main():
path='D:\\CTF_tools\\ez_fardebug.exe'
project =angr.Project(path)
initial_state=project.factory.entry_state(
add_options ={ angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS
}
)
simulation =project.factory.simgr(initial_state)
def is_successful(state):
stdout_ouput=state.posix.dumps(sys.stdout.fileno())
return stdout_ouput == b'you got it'
def should_abort(state):
stdout_ouput=state.posix.dumps(sys.stdout.fileno())
return stdout_ouput == b'wrong'
simulation.explore(find=is_successful,avoid=should_abort)

if simulation.found:
solution_state =simulation.found[0]
print(solution_state.posix.dumps(sys.stdin.fileno()).decode())
else:
print("no!")

main()