PART 1
有这么串代码,需要找到执行至Success的路径。
user_input = ?
if user_input = "yes":
print 'Success'
else:
print 'Try again'
可以把条件,user_input想象成为一个未知变量,而获取路径的过程就是一个解方程的过程。于是,我们设user_input为未知变量,然后进行到if判断的时候,就会进入两条路径,一条是输出Success,一条是输出Try again,当我们找到我们想要的结果时,就能轻易根据约束条件推出一开始的user_input。
这时候我们换一个复杂一点的例子
#define SECRET 100
int check_code(int input) {
if (input >= SECRET+88) return 0;
if (input > SECRET+100) return 0;
if (input == SECRET+68) return 0;
if (input < SECRET) return 0;
if (input <= SECRET+78) return 0;
if (input & 0x1) return 0;
if (input & 0x2) return 0;
if (input & 0x4) return 0;
return 1;
}
当这一串代码中想要获取return 1的路径,首先获取代码的所有路径,并整理为树形结构。这种情况就要运用深度优先算法来得到return 1的路径。
获取到了路径之后,我们就可以构建一个可以用SMT求解器求解的方程,就能快速的得到input的值。
input >= SECRET+88
∧ input > SECRET+100
∧ input == SECRET68
∧ input < SECRET
∧ input <= SECRET+78
∧ input & 0x1
∧ input & 0x2
∧ input & 0x4
当然,随着复杂度的增加就需要运用到这次的妙妙工具了Angr。Angr是一个符号执行引擎。 它可以遍历二进制文件并搜索符合给定条件的程序状态然后在给定路径约束的情况下求解符号变量。
执行路径表示程序从某个地方开始到另一个地方结束的可能执行流程。 Angr中执行路径的节点由“SimState”对象表示。顾名思义,它存储程序的状态以及以前状态的历史。 Angr在“simulation manager”对象中存储和处理给定程序的一组可能路径。Simulation manage提供逐步执行程序以生成可能的路径/状态的功能。
1.Angr在指示程序启动的任何地方启动程序
2.在每个活动(未终止)状态下执行指令,直到到达分支点或状态终止
3.在每个分支点,将状态拆分为多个状态,并将它们添加到活动状态集中
4.重复步骤2…4,直到找到所需内容或所有状态终止
但是用这个方法每次IF命令都会使存储的路径翻倍,所以每次应当尽早地排除掉错误地路径。这个过程在Angr中也能表示出来simulation.explore(find=你想要去的地址,avoid=你想要避免的地址)。比如simulation.explore(find=0x801000,avoid=0x802000),意思就是找到去801000地址的路径,并避免到达802000。
PART 2
OK Part 1讲的是Angr是怎么创建方程的,那么Angr又是怎么设未知数的呢?在某些情况下,当从stdin文件查询用户输入时,Angr会自动注入符号,符号就是Angr的未知数,而当Angr没有自动在我们想要的地方注入符号时,我们也可以手动这样做。
Angr的符号由它所谓的位向量表示。 比特向量有一个大小,即它们表示的比特数。 与编程中的所有数据一样,位向量可以表示任何适合的类型。最常见的是,它们表示n位整数或字符串。位向量和典型变量之间的区别在于,虽然典型变量存储单个值,但位向量存储满足某些约束的每个值。设位向量λ为8位,并受以下约束:
(λ>0,λ≤4,λ mod 2=0)∨(λ=1)
以上是位向量的存储方式。 但是,如果我们要具体化位向量(将其折叠为特定值),它可以取以下任何值:2、4或1。
位向量的定义:
一个具体的位向量:一个可以取1个值的位向量。 (例如:{λ:λ=1})
符号位向量:可以取多个值的位向量。 (例如:{λ:λ>10})
不可满足的位向量:不能接受任何值的位向量。 (例如:{λ:λ=10,λ≠10})
无约束位向量:可以取任何值的位向量,在其大小的界限。
这里Angr使用了z3来对位向量进行计算,具体步骤在Angr中我之前有写到。只要位向量的大小等于变量的大小,我们就可以将符号注入变量中。 当引擎遵循给定路径时,会自动生成约束(例如:λ=‘hunter2’,或者对于另一条路径,λ≠’hunter2])。 如果我们愿意,我们可以在程序执行过程中的任何时候手动向任何位向量添加约束。
user_input = λ
encrypted_input0 = user_input - 3
encrypted_input1 = encrypted_input0 + 15
encrypted_input2 = encrypted_input1 * 7
.........
同时位向量也有传递性,约束会向后传播。比如上面这串就是约束条件向后传播的例子。
OK,那么符号的问题解决了,怎么去注入符号呢?Angr可以注入简单的“scanf”调用(没有复杂的格式字符串) 如果输入更复杂,则需要手动注入符号,例如: scanf的复杂格式字符串,从文件输入 ,来自网络的输入 ,来自与UI的交互。
如果简单情况Angr能够注入到User Input中,如果遇到复杂情况则需要自行修改寄存器完成注入。因为输入是符号性的,所以输出也是符号性的,然后我们可以在给定我们想要的输出的情况下求解输入。
在Angr中,可以把具体值或符号值写入寄存器: state.regs.eax=my_bitvector。将my_bitvector的值写入eax。也可以把具体的值或符号值写入全局内存:state.memory.store(0x8000000, my_bitvector),将my_bitvector的值写到0x8000000地址上。也可以使用具体值或符号值推送到堆栈: state.stack_push(my_bitvector) 将把my_bitvector的值推到堆栈的顶部。 也可以通过添加填充来去除堆栈开始时一些不重要的数据:state.regs.esp-=4 添加4个字节的填充。
如果无法确定scanf写入的地址,存储在指针中,可以覆盖指针的值,指向选择的未使用的位置:
state.memory.store(0xaf84dd8、0x4444444)
state.memory.store(0x4444444,my_bitvector)
此时,0xaf84dd8处的指针将指向0x4444444,它将存储my_bitvector位向量。
如果有网络,文件或是一些ui的交互的话,可以把数据直接通过state.memory.store注入到内存中。
PART 3 & 4
那么好 PART 1讲的是怎么创建方程,PART 2讲的是怎么设未知数约束条件,下面就是要怎么快速的解出方程了。
HOOK是一个好方法,人工的让程序尽量少走一些路程。下面这一串代码如果要让程序挨个检查输入的是否是Z,那么需要走过16个分支,如果每个分支都要走一遍,那么会走2的16次方个结果出来,然后接下来可能还会有其他的运算,这样运算量会呈指数增长。
def check_all_Z(user_input):
num_Z = 0
for i in range(0, 16):
if user_input[i] == 'Z':
num_Z += 1
else:
pass
return num_Z == 16
这个时候就需要人手动去hook掉一些不那么重要的函数,比如这里能看出是需要连续输入16个Z,那么就可以把这个函数hook掉。
def replacement_check_all_Z():
eax = (*0x804a420 == 'ZZZZZZZZZZZZZZZZ’)
Call: binary.hook(0x8048776, length=16, replacement_check_all_Z)
length是我们要跳过指令的字节数,如果不想跳过指令那么length可以等于0
也可以改进一下replacemen_check_all_Z
def replacement_check_all_Z(input):
return input == 'ZZZZZZZZZZZZZZZZ'
同时HOOK也可以做到替换一些复杂功能,或是一些系统调用。为了用Angr已经实现的一些系统函数替换libc函数,Angr会寻找符号比如scanf@plt以及rand@plt. 如果程序是静态链接的,它会不知道怎么进行替换,这时候又需要进行手动替换了。我们可以在可执行文件中的任何地方开始,对于位置无关的代码,我们可能需要为地址空间指定一个基址。
同时Angr也能进行漏洞检测
1.确定要搜索的漏洞类型,例如:
任意读取(程序崩溃、读取密码等)
任意写入(注入外壳代码、覆盖返回地址等)
任意跳转(跳转到shell代码等)
2.使用Angr编写一个Python函数,以确定我们是否达到了漏洞利用的必要条件。
3.以一种可能引发攻击的方式约束系统。
4.允许Angr求解满足约束的输入。