Angr实例分析——一个简单的例子及简单分析

本博客由闲散白帽子胖胖鹏鹏胖胖鹏潜力所写,仅仅作为个人技术交流分享,不得用做商业用途。转载请注明出处,未经许可禁止将本博客内所有内容转载、商用。

一、程序源码

        用例程序来自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时,符号化变量所需要的约束条件
⑥.解出约束条件,获得目标值

猜你喜欢

转载自blog.csdn.net/zhuzhuzhu22/article/details/80350441