动若脱兔:深入浅出angr--初步理解符号执行以及angr架构
一:概论
angr作为符号执行的工具,集成了过去的许多分析方式,它不仅能进行动态符号执行,而且还能进行很多静态分析,他在分析二进制程序中能发挥很大的作用,下面为一些应用:
1:利用符号执行探究执行路径,自动解ctf逆向题
2:利用angr获取程序控制流(CFG)
3:利用angr生成rop链
4:利用angr发现漏洞
5:利用angr加密程序
6:进行污点跟踪
由上可以发现,angr的应用是非常多的,里面关于符号执行的应用和思路(特别是自动化相关的思路)是非常值得学习的,本篇不涉及angr的具体应用,主要讲一下angr整个设计的架构。
二:符号执行
先初略了解一下符号执行,对angr有个大致的了解,后续利用angr再对符号执行的理解进行加深。最传统的符号执行是静态符号执行,首先将输入的变量符号化,如果通俗点的话就是设置输入变量为x,然后通过静态分析程序的cfg流,转化为中间语言,获得符号化的变量在程序流程中的改变,从而输出一个带符号化变量的值。举个例子:
a = raw_input()
b = 2*a
if( b == 10):
print "win"
else:
print "lose"
在这个简单的代码段里,传统的运行是 先输入a的值,再运行下来的代码。在静态符号执行的过程中,首先将a进行符号化,就是转化为x,所以b就是 2*x,当b == 2*x时,则走入”win"路径,如果 b!=2*x时,则走入lose路径。路径合起来称之为执行树,(b==2*x)和(b!=2*x)即为路径约束式,当符号执行结束时(程序正常或者异常退出),约束求解器就会对路径约束式进行求解(可以简单理解为解方程),解出的答案就是走到这个路径需要的值。
当然,这种方式看起来很美丽,但是在实际执行过程中会出现很多问题,其中一个就是约束式无法通过约束式求解的问题,这里的解决方案是将传统的静态符号执行和实际执行结合起来,称之为动态符号执行(concolic execution),concolic维持了两个状态。一种是实际变量的状态,另一种是符号化的状态。实际状态将随机生成值映射到变量中,而符号化状态将变量进行符号化。concolic首先将实际状态运行,并收集实际运行时该路径的变量符号化的约束式,i求解。并将约束式取反,获取另一条路径的约束式并求解。过程不断重复,知道路径被探索完,或者达到用户设置的限制。
以上面的代码为示例,Concolic随机生成变量(a = 7),然后实际运行走了lose路径。在判断语句中,根据收集的约束式取反(b== 2*x),可以得到另一条路径。通过实际运行这种方式,可以很好的避免了约束式无法识别和求解的问题。
三:Angr架构
angr架构非常清晰,主要分为下图这些模块,每个模块的功能以及彼此间的联系。
3.1 CLE模块
二进制的装载组建是CLE(CLE Load Everything),它负责装载二进制对象以及它所依赖的库,将自身无法执行的操作转移给angr的其它组件,最后生成地址空间,表示该程序已加载并可以准备运行。
>>> import angr, monkeyhex >>> proj = angr.Project('/bin/true') >>> proj.loader <Loaded true, maps [0x400000:0x5008000]>
cle.loader代表着将整个程序映射到某个地址空间,而地址空间的每个对象都可以由一个加载器后端加载,例如cle.elf用于加载linux的32位程序。下面是地址空间的分类
>>> proj.loader.all_objects [<ELF Object fauxware, maps [0x400000:0x60105f]>, <ELF Object libc.so.6, maps [0x1000000:0x13c42bf]>, <ELF Object ld-linux-x86-64.so.2, maps [0x2000000:0x22241c7]>, <ELFTLSObject Object cle##tls, maps [0x3000000:0x300d010]>, <KernelObject Object cle##kernel, maps [0x4000000:0x4008000]>, <ExternObject Object cle##externs, maps [0x5000000:0x5008000]>
其中,类型可以分为 proj.loader.main_object,proj.loader.share_object,proj.loader.kernel_object等等...获取特定object之后,可以与object进行交互获取更详细的信息
3.2 ArchInfo模块
archinfo是包含特定于体系结构的信息的类的集合。太过于底层,在日后的分析中逐步解释
3.3 PyVex模块
angr需要处理不同的架构,所以它选择一种中间语言来进行它的分析,angr使用Valgrind的中间语言——VEX来完成这方面的内容。VEX中间语言抽象了几种不同架构间的区别,允许在他们之上进行统一的分析。各种中间语言在设计理念上有很多的共通点,这里又会是一个很大的话题,所以暂且抛开,具体关于IR语言的语法规则请查阅 https://docs.angr.io/docs/ir.html。
3.4 SimuVEX模块
这里是中间语言VEX执行的模拟器,它允许你控制符号执行。
3.5 Clarity
这个模块主要专注于将变量符号化,生成约束式并求解约束式,这也是符号执行的核心所在,在angr中主要是利用微软提供的z3库去解约束式
3.6 angr以及以上
这些则为上层封装好的接口,后续使用时在描述。
四:angr的输入输出
一般来说,命令行程序主要有两种数据输入的方式,第一种是利用api(get,read),第二种是放在argc上,其它的方法有很多,最后也会提供一种通用的解法。
当数据输入在argc上时,一般使用claripy库,将输入的数据符号化,具体代码如下:
import angr import claripy p = angr.Project("test") args = claripy.BVS('args', 8*16) initial_state = prog.factory.entry_state(args=["./vul", args])
for i in range(0,8):
initial_state.add_constraints(argc.get_byte(0) >= argvc.get_byte(1)) pg = p.factory.path_group(initial_state) pg.explore(find=(0x4005d1,)) print pg # <PathGroup with 18 deadended, 4 active, 1 found> print pg.found[0] # <Path with 64 runs (at 0x4005d1)> print pg.found[0].state.posix.dumps(0)
claripy库是求解器引擎,绝大部分只是用来做z3的前端,而在这里起到的作用主要是将参数符号化,核心代码为第四行。
当利用api时,主要通过对st.posix.files[0]进行符号化,具体代码如下:
p = angr.Project('wyvern') st = p.factory.full_init_state(args=['./wyvern'], add_options=angr.options.unicorn) for _ in xrange(28): k = st.posix.files[0].read_from(1) st.solver.add(k != 0) st.solver.add(k != 10) k = st.posix.files[0].read_from(1) st.solver.add(k == 10) st.posix.files[0].seek(0) st.posix.files[0].length = 29
state.pix在angr中是 angr.state_plugins.posix.SimSystemPosix类,该类的主要作用是用于模拟符合posix环境的数据存储和输入输出。其中files[0]代表着数据的输入,read_from表示读取输入的数据。第3到第8行对输入的数据进行限制,
最后两行将指针重新指向开头并设置长度。
第三种是最通用的方式,直接访问并修改内存,无论程序是通过何种方式进行输入,输入的数据总是在内存中,可以通过对内存进行符号化。具体代码示例如下:
import angr p = angr.Project('./vul') s = p.factory.blank_state(addr=0x80485c8) bvs = s.se.BVS('to_memory', 8*4) s.se.add(bvs > 1000) s.memory.store(0x08049b80, bvs, endness='Iend_LE') pg = p.factory.path_group(s, immutable=False)
其中 endness有三个值,分别为
Variables: LE – little endian, least significant byte is stored at lowest address BE – big endian, most significant byte is stored at lowest address ME – Middle-endian. Yep.
关于内存操作还可以多说一下,s.memory.store可以用于存储数据,s.memory.load用于读取数据.。
五:angr解题步骤:
这里本篇主要利用simulation_manager(老版本为factory_group)求解
#!/usr/bin/env python # -*- coding: utf-8 -*- import angr import claripy angr.l.setLevel('DEBUG') p = angr.Project('./vul', load_options={"auto_load_libs": False}) args = claripy.BVS('args', 8*100) initial_state = p.factory.entry_state(args=[p.filename, args], add_options={'BYPASS_UNSUPPORTED_SYSCALL'}) #pg = p.factory.path_group(initial_state, immutable=False),在新版本被代替,和simlation_manager等效 pg = p.factory.simulation_manager(initial_state) find_addrs = (0x400546, ) avoid_addrs = () pg.explore(find=find_addrs, avoid=avoid_addrs) print pg print ans = pg.found[0].state.se._solver.result.model
print pg.found[0].state.posix.dumps(0) //代表该状态程序的所有输入
print pg.found[0].state.posix.dumps(1) //代表该状态程序的所有输出
simualtion_manager初始化运行之后,一般有以下几种状态
step()表示向下执行一个block(42bytes),step()函数产生active状态,表示该分支在执行中;
run()表示运行到结束,run()函数产生deadended状态,表示分支结束;
explore()可以对地址进行限制以减少符号执行遍历的路径。例如 sm.explore(find=0x400676,avoid=[0x40073d]) explore()产生found状态,表示探索的结果等等。也可以使用条件来进行find匹配
1
|
pg.explore(find=lambda s: "Congrats" in s.posix.dumps(1)) |
simulation_manager式angr最重要的控制接口,模拟管理器的最基本功能是利用step()通过一个基本块将给定存储中的所有状态向前推进。当然,如果不对路径探究过程进行细致研究,只需要使用run()和explore()就好了,在简述run和explore之前,首先对路径的stashes进行描述。
当explore()以find参数运行时,程序会一直运行,直到找到与查找条件相匹配的状态,该条件可以是要停止的指令的地址(地址列表),或者是一些运行时是否符合的状态。当条件存储的任何条件与find条件匹配时,他们将置于found存储中,同时也可以设置avoid,符合avoid条件时也会将其置于avoid中。和run()函数不同的是,run()函数会存储所有的路径状态,而expore()只会存储find的状态。
在路径探索中,一般使用广度优先算法进行探究,当然我们也可以自由设置使用其它方法,例如深度优先...具体可以使用angr.exploration_techniques