本博客由闲散白帽子胖胖鹏鹏胖胖鹏潜力所写,仅仅作为个人技术交流分享,不得用做商业用途。转载请注明出处,未经许可禁止将本博客内所有内容转载、商用。
一、程序源码
用例程序来自https://github.com/angr/angr-doc/tree/master/examples/sym-write。下载之后有一个c,有一个可执行文件,还有一个py文件,这个python脚本就是我们用来分析的。首先来看程序的代码。
#include <stdio.h> char u=0; int main(void) { int i, bits[2]={0,0}; for (i=0; i<8; i++) { bits[(u&(1<<i))!=0]++; } if (bits[0]==bits[1]) { printf("you win!"); } else { printf("you lose!"); } return 0; }太简单了,不再过多的介绍。
二、Angr分析源码及API说明
接下来就是我们的分析脚本。附上整体的代码。我已经对每句代码进行了注释。
import angr import claripy def main(): #新建一个工程,导入二进制文件,example就是文件的名字,后面的选项是选择不自动加载依赖项 proj = angr.Project('example',load_options={"auto_load_libs":False}) #创建一个SimState对象,这也就是我们进行符号执行的核心对象,包括了符号信息、内存信息、寄存器信息等 state = proj.factory.entry_state(add_options={"SYMBPLIC_WRITE_ADDRESS"}) #创建一个符号变量,这个符号变量以8位bitvector形式存在,名称为u u = claripy.BVS("u",8) #把符号变量保存到指定的地址中,这个地址是和二进制文件相关联的,使用IDA打开,此地址对应的.bss段全局变量u的地址 state.memory.store(0x601041,u) #我对代码进行过重新编译,因此和github提供的二进制文件的地址不相同 #创建一个Simulation Manager对象,这个对象和我们的状态有关系 sm = proj.factory.simulation_manager(state) #step开始执行,直到路径超过一条的时候停止,也就是我们的if else那里 sm.step(until = lambda lpg:len(lpg.active) > 1) #对于每个可能的state都进行检查, for I in range(len(sm.active)): print "possible %d : " %I , sm.active[I].state.se.any_int(u) return sm.active[0].state.se.any_int(u) if __name__ == "__main__": print repr(main())
以上我们就能够得到正确的值:240。但是有些地方我们需要着重说明。
state = proj.factory.entry_state(add_options={"SYMBPLIC_WRITE_ADDRESS"})
在Angr中,我们使用state表示程序(二进制文件)的运行状态,这个状态机就包括内存、寄存器等。在Angr 7.9.2.21的API文档中,对entry_state函数的定义如下:
entry_state(**kwargs) 返回一个代表着程序入口点的state对象。所有的参数都是可选的。 参数: .addr-state开始的程序地址(非入口点) .initial_prefix-如果提供了这个参数,所有的符号化寄存器以及符号变量的名字前面都带有此字符串 .fs-一个字典,字典的键为文件名,值为SimFile对象 .concrete_fs-boolean;指明了在打开文件时,主文件系统是否应该被询问 .chroot- 一个伪造的root路径,和真实chroot指令等效。只有在concrete_fs为True时生效 .argc- 程序所需要的argc。可以是int或者bitvector。如果不提供,默认为args的长度 .args- 程序所需要的参数列表。可以是string或者bitvector .env- 字典;用于指明程序运行环境,键和值都可以是strings或者bitvector 返回值:初始状态 返回类型:SiMState
我们而我们使用add_options可以增加选项。
下一个可能会有疑惑的点就是Simulation_Manager。在Angr中,程序没每执行一步(也就我们的step)就会产生一个状态(state,在SM这里我们乘坐stashe),Simulation_Manager就是提供给我们管理这些state的接口。我们可以对state执行运行下一步、过滤、合并、移动等动作。我们设置可以让两个state以不同速率同时执行,然后将他们合并在一起。而这些statshe有各种属性,属性包括active(激活,可执行一步)、deadended(无法继续执行,或因程序结束或不满足条件或指令指针不可用等)、pruned(已删除,如果使用LAZY_SOLVES进行求解,当一个状态不满足LAZY_SOLVES中的条件时,就会遍历回溯历史状态,找到第一个的不满足条件的初始状态,从初始状态开始的后续状态都会被标记成“已删除”)、unconstrained(未约束的,如果提供了save_unconstrained选项,使用了诸如用户控制的指令中指针、其他符号数据源等state都会保存在这里)、unsat(不满足条件,和约束条件相冲突的state,设置了save_unsat选项时保存),这里简单地介绍,稍后我们阅读源码详细解释,具体含义查看官方文档。简单来说,Simulation_Manager就是对程序运行状态进行管理的类。
sm.step()是将stashe向前执行,并且我们制定的util参数是指,当满足util的条件时,程序执行停止。这个util应该是一个函数,该函数输入变量为SimulationManager,输出为True或False。当条件返回True的时候,运行停止。在本例中,我们使用的便是一个lambda进行函数的定义(官方已经不再建议使用此种做法,稍后提供一种官方解法)。lpg即输入的SimulationManager,我们判断这个SM中存在多于两个active的stashe时,就停止运行。(补充说明:当Angr执行时,遇到if等分支条件会为每个分支条件产生一个stashe)。
最后要说说明的就是这条语句:sm.active[0].state.se.any_int(u)。我们一层一层看看这句话到底做了什么。首先是sm.active[0],我们上段说过,sm的active属性是一个列表,指的是所有能够继续执行的状态。我们选择第一个装填也就是if=True的情况,他会返回一个SimState类,SimState.se返回一个solver(求解器),继续深入solver.anyint可以发现,any_int调用了eval进行约束条件的求解,所以这条语句的含义就是对进入到这个state的约束条件进行求解。
三、官方提供的求解代码
先贴上代码,随后分析。
#!/usr/bin/env python2 # -*- coding: utf-8 -*- """ Author: xoreaxeaxeax Modified by David Manouchehri <[email protected]> Original at https://lists.cs.ucsb.edu/pipermail/angr/2016-August/000167.html The purpose of this example is to show how to use symbolic write addresses. """ import angr import claripy def main(): p = angr.Project('./issue', load_options={"auto_load_libs": False}) # By default, all symbolic write indices are concretized. state = p.factory.entry_state(add_options={"SYMBOLIC_WRITE_ADDRESSES"}) u = claripy.BVS("u", 8) state.memory.store(0x804a021, u) sm = p.factory.simulation_manager(state) def correct(state): try: return 'win' in state.posix.dumps(1) except: return False def wrong(state): try: return 'lose' in state.posix.dumps(1) except: return False sm.explore(find=correct, avoid=wrong) # Alternatively, you can hardcode the addresses. # sm.explore(find=0x80484e3, avoid=0x80484f5) return sm.found[0].solver.eval(u) def test(): assert '240' in str(main()) if __name__ == '__main__': print(repr(main()))
和官方有差距的地方就在26行之后。官方解法定义了两个函数,state.posix.dumps(1)代表的是stdout,即printf出的字符信息。这两个函数就是检查输出字符串中如果有success就是成功,如果有lose就是失败。官方使用了explore函数进行状态的搜寻。我们先来看看官方是如何解释这个函数的。
“在符号执行中,一个比较常见的做法是寻找到到达某一地址的state,并且丢弃到达其他地址的state。SimulationManager提供了这种快捷方式,.explore方法。
我们使用.explore方法的find参数,程序会执行到满足find条件的地方停止,这个find条件可以是一个指令、一个地址、或者是一个满足某些约束的函数。当有任意一个state符合find条件时,这个state将被置入found区域,并且停止执行。接下来搜索found区域,就可以忽略此state继续执行,或者对次装填进行操作。当然可以制定avoid参数规避满足某些条件的state。”
根据官方的解释,我们可以知道第37行sm.explore(find=correct, avoid=wrong)的含义是,寻找到满足correct条件且不满足wrong条件的state,即我们最终所要的校验成功的state。获得到state之后,运行state.solver.eval(u)求解u的值。
至此,我们第一个使用Angr分析的例子做完了。本文分两个做法对约束条件进行了求解,但是其核心思想是相同的。简单总结一下使用Angr分析的流程。源码的分析以及更多Angr实例分析我回持续更新。
①.新建一个Angr工程,并且载入二进制文件 ②.创建程序入口点的state ③.将要求解的变量符号化 ④.创建SimulationManager进行程序执行管理 ⑤.搜索满足我们目标的state ⑥.求解程序执行到state时,符号化变量所需要的约束条件 ⑥.解出约束条件,获得目标值