anger符号执行

angr遇到每个分支都会产生新的后继状态,即使是简单的for循环,它也会在for循环的判断中不断产生新的状态,就等同trace代码一样每执行一次都会记录一次,而且有时也不好处理,这样就会造成状态爆炸,这么看来,如果一个函数非常复杂,而我们想要angr模拟整个函数,处理起来还是挺头痛的

一个程序收集所有的if语句,然后将这些条件形成一个不等式组,解出这个不等式组,就可以得到我们想要的输出了 当复杂程序时,人力不可能完全做到,自然通过程序实现,就是angr

下面是angr的基本示例

import angr
import claripy

proj = angr.Project('./asset/libanger.so', load_options = {'auto_load_libs': False})
m = proj.loader.main_object
func1_addr = m.get_symbol('_Z5funclii').rebased_addr

state = proj.factory.blank_state(addr = func1_addr)
solver = state.solver
x = solver.BVS('x', 32)
y = solver.BVS('y', 32)

simgr = proj.factory.simgr(state)
simgr.explore(find = 0x408301)

if simgr.found:
    s = simgr.found[0]
    print(s.solver.eval(s.regs.r0))

简单说一下其中的注意事项:
1.加载文件要注意是对依赖库的加载,一般情况下建议不要加载依赖库(使用auto_load_libs设置),因为它会造成符号执行非常缓慢,对于那些依赖其他库的变量和函数,angr解决办法:手动提供实现替换一下就行了。对于变量,可以通过继承SimData实现,对于函数可以通过继承SimProcedure,需要hook的话也是使用SimProcedure。
对于加载文件,没有更多需要说的,如果要理解更清晰,需要懂得ELF文件结构,PE文件结构,重定位和内存映射等

angr的基本数据结构:一切都是AST

angr中最基本的结构应视为AST(抽象语法树),或可一般称为:符号(类似代数式),例如:
x = claripy.BVS('x', 32)
y = claripy.FPV(3.14,claripy.FSORT_FLOAT)
x和y变量都是一个符号,如果x有一个具体值,有时又称为位向量,具有一定长度的二进制位。有点乱,那么统称为AST,就是代数式那样的东西。
angr的符号数据是由Clarpy支持的,符号的相关操作都可以在api文档中找到。
接触汇编分析都知道,计算机只会处理数字,不管是什么东西,它终究是数字,只是这个数字可能是整数也可能是浮点数,一个数可能是一个地址也可能是实际数据。所以AST符号是能够满足表示计算机的一般数据的,如BV是针对整数的,FP是针对浮点数的。
给定一个函数F,参数集为X,表达式为g(x),在数学可表示为F(X)=g(x)。由于angr提供了使用符号表示数据,那么一个程序的函数执行也可以这样表示,只是没有数学中的那样直观。
例如一个计算从a到b的和的函数可能表示为f(a)=c+k*a,当然,这不是乱举例的,是真的可以这样表示。

那么寄存器、内存和文件呢?

寄存器中的数据是符号,内存中的数据是符号,文件数据也是符号——全是代数式(angr的CTF例子中有这三种数据的模拟操作)。如此一来,那我们是不是可以通过符号模拟执行一个md5函数就可以拿到它的符号表达式了?原理上是的,但是实际上会有各种问题,其中一个问题就是太复杂的函数执行起来效率非常低,更多的问题下面也会谈到一些。

angr被分析对象的数据结构:SimState、Block

angr最常用的是针对SimState进行分析,但是不止状态,还有Block,Block称为基本代码块或基本块。其中SimState表示程序某一时刻的所有瞬时状态数据,包括寄存器、内存和文件系统数据,而Block表示一个没有分支跳转的代码块,angr不少地方的处理都是以基本块为单元进行处理的,例如step(),每执行一次step()执行完一个基本块中的所有指令,此时你去获取active中的状态的地址会发现等于基本块的起始地址。
所有的基本块组成一个图(Graph),所以程序的指令是图进行组织的,准确来说是一个有向图,也就是程序控制流程图。所有的SimState构成也可以看成一个图,当前的状态也可以通过history进行回溯,只是一个SimState还有各种插件,其中history也被视为插件。

状态模拟管理器(SimulationMnager)

其中最主要的方法是step、run和explore,这三个方法都是模拟器执行,step默认每次单步执行一个基本块;run和explore执行到指定位置或直至结束。
另外,step和run默认是按照BFS广度优先进行执行的:先处理完下一层的所有分支,再进行深入执行。而explore是按照DFS深度优先执行的:从起始地址一直执行到结束,然后再更换分支。对于explore,如果找到目标状态,会把目标状态添加到found中,这样你就可以从found状态列表中获取状态进一步处理。
另外要说的一个是状态集合(stash),官方文档是称为stash,实际上是一个集合或列表,是对状态的分类集:不同类型的状态放到不同的集合中。对于即将要解析执行的状态会被放到active中,一般来说active每个状态的地址就等于基本块的起始地址,当active列表为空时,就表示模拟执行已经全部处理完成。至于其它类型的状态集可以参考官方文档看看。

angr符号求解:Claripy约束求解器

一般的符号求解代码如下:

import claripy
 
x = claripy.BVS('x', 32)  # 定义AST
y = claripy.BVS('y', 32)
exp = x * y + x / y + 100  # 符号表达式
solver = claripy.Solver()  # 获取求解器
solver.add(exp == 300)  # 添加表达式符号约束
print(solver.eval(x * y, 2))  # 求解
print(solver.eval(x / y, 2))
 
a = claripy.FPV(3.14, claripy.FSORT_DOUBLE)
b = claripy.FPS('b', claripy.FSORT_DOUBLE)
print(a - b)
solver.add(a + 0.1 <= b)
print(solver.eval(b, 1)[0])

其中solver.add是添加符号约束,参数通常是符号比较表达式。solver.eval是求解函数,用于求解一个表达式或符号的可能值,第二个参数是解的个数,返回值可能包含多个解。
这里不深入求解器的原理,对于使用来说,符号求解其实就等于解方程,只是有可能无解就是。
以上基本介绍了angr的基本内容,其它还有两个重点是:探索技术(angr.exploration_techniques)和分析技术(project.analyses),前者是针对模拟执行过程的处理,后者是一些专门分析的技术,分析技术中,其中有控制流程图、值流程图、数据依赖图、后向切片等,以后有机会再讨论,现在我们先针对angr的符号执行。
下面我们开始尝试使用angr的符号执行进行更自由的使用,首先来看一个最简单的例子:

https://bbs.kanxue.com/thread-281552.htm
偏向于思考

理论理清楚了,接下来从代码层面简单学习怎么使用

proj = angr.Project('程序的路径') # 相对路径和绝对路径均可

创建一个模拟运行管理器,让angr搜索需要的分支信息

sm = proj.factory.simulation_manager()
sm = sm.explore(find = 0x08048675)

其中factory这个对象是用来生成和项目有关的一些分析工具,然后我们用这个对象生成了一个SimulationManager对象,存储在sm中。
那么什么是模拟运行管理器呢,那就要涉及到angr是如何收集所有的分支信息了。

在IDA流程图里

可以说,这个图中一个个代码块就是angr框架中所谓的state(这只是方便说明的说辞,实际上angr的一个state还存储了执行这个代码块后的寄存器状态,内存状态等信息),这一个一个state组合在一起,形成了执行路径,然后我们用SimulationManager来生成,管理执行路径。

然后angr会将入口点那块state作为七点,将find的值对应的那块汇编代码作为终点,进行宽度优先搜索(还是要了解一下这个算法,这样才能理解其他的一些参数设置,如avoid参数的含义)。找到一条从起点到达终点的可行路径,然后将这条路上的所有if条件收集起来。最后自动对收集起来的条件形成的不等式组进行求解。(貌似具体的方法较为复杂,这里可以稍微这样简单理解一下)

其实我们还可以用 proj.factory.entry_state()来生成一个关于该程序入口点的state。然后将这个state作为proj.factory.simulation_manager()的参数,实际上proj.factory.simulation_manager()的默认参数就是proj.factory.entry_state()所以我就不手动指定了。

如果这步执行结束了,那么我们就可以导出能够让程序到达成功状态的输入了,

final_state = sm.found[0] #0 代表找到的第一个结果
final_state.posix.dumps(0) #导出结果

其中posix表示终点状态中存储的系统接口信息,然后通过dumps函数来导出系统输入,系统输出等信息

final_state.posix.dumps(0)表示导出输入,final_state.posix.dumps(1)表示导出输出

然后我们就可以找到angr爆破出来的输出b'JXWVRKX'

参考博客:https://www.cnblogs.com/Node-Sans-Blog/p/14403013.html

最后,贴一下博主用angr分析普通程序

这么复杂,更别提用这个来分析加密函数了,所以angr其实也有其局限性

posted @ 2025-05-03 13:45  Bri1  阅读(145)  评论(0)    收藏  举报