本文是学习angr_ctf所写。这里是我的一点总结和想法。

安装angr

我安装的环境是ubuntu20.04+python3。首先安装python,确保python默认为python3而不是python2。这个问题在ubuntu20.04上面有体现。如果出现默认python是python2,建议先卸载python2,等python3的虚拟环境安装完成之后再重新安装python2。

1
2
3
4
5
sudo apt-get update
sudo apt-get autoremove --purge python2* #卸载python2
#进入/usr/bin目录给python3创建个软链接,这样默认python就是python3了
cd /usr/bin
ln -s /usr/bin/python3.8 python

接下来就是安装虚拟环境。

1
2
sudo apt-get install python-dev libffi-dev build-essential #安装依赖
pip3 install virtualenvwrapper

设置环境变量

1
2
export WORKON_HOME=$HOME/Python-workhome
source /usr/local/bin/virtualenvwrapper.sh

这里source /usr/local/bin/virtualenvwrapper.sh执行如果报错为没有找到文件,建议找一下自己的virtualenvwrapper安装目录。如果报错为没有模块的话,建议检查一下自己默认python是否为python3。如果这两步执行没有错误的话,建议写入~/.bashrc中,这样就不用每次启动bash都要输入一次。

接下来就是进入虚拟环境安装angr

1
2
3
mkvirtualenv virangr
workon #查看目前所在虚拟环境
pip3 install angr

如此angr就安装上了,可以正常使用了。后续python2安装上也不影响使用。只要保证默认python是python3即可。

angr笔记

本文所有程序全部是需要输入密码,然后判断密码是否正确,不同题目难度各异,依次递增。输入正确回显为”Good Job.”,反之则为”Try again.”

00_angr_find

这个例子就是angr的开端,脚本并不复杂,简单介绍一下angr的思路。angr为符号执行,在不运行程序的情况下,通过分析程序,计算出一系列的输入,这个输入可以使程序跑到你想要达到的特定位置。例如,在本题中,输入一串正确的字符串来使程序输出”Good Job.”。因此在程序里面输出”Good Job.”的地方就是我们想要达到的地方。angr通过你指定的参数脚本内容在不运行程序的情况下来计算出正确的输入字符串。

本题较为简单,主要目的是介绍一下angr脚本的初始框架,以后大致是在此基础上进行修修改改。

看题

要想得到这个正确的password,通常我们的思路就是对complex_function进行一个常规的逆向。但是学习了angr之后并不需要这么做,complex_function可以使用angr自动分析。如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import angr
import sys

def main(argv):
#简单创建一个angr的一个工程,指明需要分析的程序
path_to_binary = r'00_angr_find' # :string
project = angr.Project(path_to_binary)
#指定分析程序的起始地址,entry_state()为程序默认起始地址
initial_state = project.factory.entry_state()
#根据指定的入口地址创建一个模拟器
simulation = project.factory.simgr(initial_state)
#指定想要达到的地址,这里为0x804867D就是程序执行到puts("Good Job.")的地址。
print_good_address = 0x804867D # :integer (probably in hexadecimal)
#根据通过初始化的模拟器计算出想要到达地址的输入。
simulation.explore(find=print_good_address)
#找到正确的输入就打印
if simulation.found:
#正确的模拟路径不止一个,这里只选择第一个。
solution_state = simulation.found[0]
#打印出该次模拟器模拟路径的输入,即正确输入
print(solution_state.posix.dumps(sys.stdin.fileno()))
else:
#没找到就抛异常
raise Exception('Could not find the solution')

if __name__ == '__main__':
main(sys.argv)

框架很简单,angr分析程序大致流程也就这样。

01_angr_avoid

介绍了不想进入的分支的angr设置。如题

本题main函数分支十分庞大,同时要想正确的输出”Good Job.”不能进去一次avoid_me函数。因此需要在angr中设置不想进入的地方。如果设置该字段,如果模拟器分析到该地方则会跳过该次分析,进行下一次分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import angr
import sys

def main(argv):
path_to_binary = r"01_angr_avoid"
project = angr.Project(path_to_binary)
initial_state = project.factory.entry_state()
simulation = project.factory.simgr(initial_state)
print_good_address = 0x80485E5
will_not_succeed_address = 0x80485A8
#增加了avoid,设置该参数则分析的时候到该地方则会结束该次分析,进行下一次分析。
simulation.explore(find=print_good_address, avoid=will_not_succeed_address)
if simulation.found:
solution_state = simulation.found[0]
print(solution_state.posix.dumps(sys.stdin.fileno()))
else:
raise Exception('Could not find the solution')

if __name__ == '__main__':
main(sys.argv)

02_angr_find_condition

本题介绍了不知道正确地址的程序该如何使用。如果本题不知道打印”Good Job.”的地址,那么该怎么办?

本题介绍了simulation.explore(find=True,avoid=False). find和avoid参数不仅仅可以是函数地址,还可以是Boolean。当find=True时,证明本次分析成功。avoid=True时,则跳过该次分析,进入下一次分析。

此时我们只需要判断什么时候给find为True,什么时候给avoid为True.如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import angr
import sys

def main(argv):
path_to_binary = argv[1]
project = angr.Project(path_to_binary)
initial_state = project.factory.entry_state()
simulation = project.factory.simgr(initial_state)

def is_successful(state):
#可以通过获取程序的打印结果来判断是否执行成功
stdout_output = state.posix.dumps(sys.stdout.fileno())
#打印结果含有'Good Job.',则给find返回True
return b'Good Job.' in stdout_output

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
#打印结果含有'Good Job.',则给avoid返回True
return b"Try again." in stdout_output

simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
solution_state = simulation.found[0]
print(solution_state.posix.dumps(sys.stdin.fileno()))
else:
raise Exception('Could not find the solution')

if __name__ == '__main__':
main(sys.argv)

小总结01

如上三个题都是针对 simulation.explore()函数。想要找到的地址就设置给find或者想要得到的输出得到之后就给find设置为True.想要避免的函数地址就设置给avoid或者不想得到的输出得到之后给avoid设置为True.

03_angr_symbolic_registers

前三题都是只用一次输入一个字符串,本题引入一次输入多个输入变量。angr简单的处理不了一次输入多个变量输入,只能简单处理单个变量。因此这里介绍angr处理多个变量.

如题本题在get_user_input函数中需要输入三个数据,输入的数据存放在eax,ebx,edx中。三个参数进入三个不同的complex_function。angr的处理方式如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import angr
import claripy
import sys

def main(argv):
path_to_binary = argv[1]
project = angr.Project(path_to_binary)
#这里不能从程序起始地址开始,需要跳过函数输入的地方开始,然后采用符号插入的方式告诉angr输入变量。
start_address = 0x8048980 # :integer (probably hexadecimal)
#注意这里是blank_state
initial_state = project.factory.blank_state(addr=start_address)
#指定输入变量的变量占用bit数,多个变量就类似设置。这里有三个变量就设置三个
password0_size_in_bits = 32 # :integer
#创建位向量,angr通过改变该变量来模拟出正确的结果,
#第一个参数仅仅是一个angr引用的名字而已,随便取,建议别重复。
password0 = claripy.BVS('password0', password0_size_in_bits)
password1_size_in_bits = 32 # :integer
password1 = claripy.BVS('password1', password1_size_in_bits)
password2_size_in_bits = 32 # :integer
password2 = claripy.BVS('password2', password2_size_in_bits)

#这里进行符号插入,三个输入变量都是存放在寄存器,则使之一一对应。
initial_state.regs.eax = password0
initial_state.regs.ebx = password1
initial_state.regs.edx = password2

simulation = project.factory.simgr(initial_state)
#和题02一样
def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Good Job.' in stdout_output

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Try again." in stdout_output

simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
solution_state = simulation.found[0]
#处理之后得到的值有多个,使用eval函数,只保留一个
solution0 = solution_state.se.eval(password0)
solution1 = solution_state.se.eval(password1)
solution2 = solution_state.se.eval(password2)
#将值按照输入的格式打印出来
solution = r"%x %x %x"%(solution0,solution1,solution2) # :string
print(solution)
else:
raise Exception('Could not find the solution')

if __name__ == '__main__':
main(sys.argv)

04_angr_symbolic_stack

本题和上题类似,但是这次输入的变量是存放在栈上。

可以从汇编上看到,通过scanf输入的两个变量一个存放在距离栈底0xc的地方,一个存放在距离占地0x10的地方。angr处理如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import angr
import claripy
import sys

def main(argv):
path_to_binary = argv[1]
project = angr.Project(path_to_binary)
#这里开始位置很重要,开始位置决定了栈空间
#我们这里还是需要在scanf后面调用,但是是在0x8048694还是0x8048697.
#若是在0x8048694,这个汇编调用其实是在平衡scanf的参数调用栈的
#angr分析时构造的栈就会比原程序的栈向下偏移0x10
#因此我们需要从0x8048697开始,使之布局与原程序一致。
start_address = 0x8048697
initial_state = project.factory.blank_state(addr=start_address)
#初始化栈
initial_state.regs.ebp = initial_state.regs.esp
#如题03
password0 = claripy.BVS('password0', 32)
password1 = claripy.BVS('password1', 32)
#这里计算出填充字符,使构造栈布局与程序栈布局类似
#关键是让输入变量存放的栈与构造出来的栈布局对应上
padding_length_in_bytes = 0x8 # :integer
initial_state.regs.esp -= padding_length_in_bytes
#当构造栈向上抬了0x8个字节后,再push一个4个字节大小的angr变量就与存放在0xc的输入变量对应上了
initial_state.stack_push(password0) # :bitvector (claripy.BVS, claripy.BVV, claripy.BV)
#同理在push一个4个字节大小的angr变量就与存放在0x10的输入变量对应上了
initial_state.stack_push(password1)

#如下就与题03类似了
simulation = project.factory.simgr(initial_state)
def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Good Job.' in stdout_output

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Try again." in stdout_output

simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
solution_state = simulation.found[0]

solution0 = solution_state.se.eval(password0)
solution1 = solution_state.se.eval(password1)

solution = r"%u %u"%(solution0,solution1)
print(solution)
else:
raise Exception('Could not find the solution')

if __name__ == '__main__':
main(sys.argv)

本题关键就是要定位好原程序存放输入的变量与我们构造的位向量在栈上的位置保持一致,以便模拟器正确的计算出来正确的输入值。

05_angr_symbolic_memory

本题的存放输入的变量存放在bss段上

本题较为简单,只需要找到变量输入地址即可。存放在bss段,则地址固定,angr处理如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import angr
import claripy
import sys

def main(argv):
path_to_binary = argv[1]
project = angr.Project(path_to_binary)

start_address = 0x8048601
initial_state = project.factory.blank_state(addr=start_address)
#每个变量需要输入八个字节,因此8*8=64
#创建位向量
password0 = claripy.BVS('password0', 64)
password1 = claripy.BVS('password1', 64)
password2 = claripy.BVS('password2', 64)
password3 = claripy.BVS('password3', 64)

#通过memory.store()可以将angr变量与地址绑定
password0_address = 0xA1BA1C0
initial_state.memory.store(password0_address, password0)
password1_address = 0xA1BA1C8
initial_state.memory.store(password1_address, password1)
password2_address = 0xA1BA1D0
initial_state.memory.store(password2_address, password2)
password3_address = 0xA1BA1D8
initial_state.memory.store(password3_address, password3)

#如下处理一致
simulation = project.factory.simgr(initial_state)

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Good Job." in stdout_output

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Try again." in stdout_output

simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
solution_state = simulation.found[0]
#这里需要把正确结果处理为字符串输出
solution0 = solution_state.solver.eval(password0,cast_to=bytes).decode('utf-8')
solution1 = solution_state.solver.eval(password1,cast_to=bytes).decode('utf-8')
solution2 = solution_state.solver.eval(password2,cast_to=bytes).decode('utf-8')
solution3 = solution_state.solver.eval(password3,cast_to=bytes).decode('utf-8')
solution = r"%8s %8s %8s %8s"%(solution0,solution1,solution2,solution3)
print(solution)
else:
raise Exception('Could not find the solution')

if __name__ == '__main__':
main(sys.argv)

06_angr_symbolic_dynamic_memory

两个输入变量在堆上,地址随机。angr需要使用任意一个无用已知的内存块并且用这内存块地址覆盖指向原来数据内存的指针,这样就无需知道malloc分配的随机地址了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import angr
import claripy
import sys

def main(argv):
path_to_binary = argv[1]
project = angr.Project(path_to_binary)

start_address = 0x8048699
initial_state = project.factory.blank_state(addr=start_address)
password0 = claripy.BVS('password0', 64)
password1 = claripy.BVS('password1', 64)
#首先找到buffer0的地址0xABCC8A4
#然后用任意的一个内存块地址(这里用的0xABCC760)覆盖buffer0的值
fake_heap_address0 = 0xABCC760
pointer_to_malloc_memory_address0 = 0xABCC8A4
#这里是在进行覆盖,但是memory.store默认是大端序,需要指定endness设置为自己架构的端序。
#project.arch.memory_endness代表就是当前运行的架构端序
initial_state.memory.store(pointer_to_malloc_memory_address0, fake_heap_address0, endness=project.arch.memory_endness)
fake_heap_address1 = 0xABCC780
pointer_to_malloc_memory_address1 = 0x0ABCC8AC
initial_state.memory.store(pointer_to_malloc_memory_address1, fake_heap_address1, endness=project.arch.memory_endness)
#将指向数据指针修改为已知的地址之后,可以如题05一样把存放输入变量与我们创建的位向量绑定在一起。
initial_state.memory.store(fake_heap_address0, password0)
initial_state.memory.store(fake_heap_address1, password1)

simulation = project.factory.simgr(initial_state)

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Good Job." in stdout_output

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Try again." in stdout_output

simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
solution_state = simulation.found[0]

solution0 = solution_state.se.eval(password0,cast_to=bytes).decode('utf-8')
solution1 = solution_state.se.eval(password1,cast_to=bytes).decode('utf-8')
solution = r"%8s %8s"%(solution0,solution1)
print(solution)
else:
raise Exception('Could not find the solution')

if __name__ == '__main__':
main(sys.argv)

07_angr_symbolic_file

本题是通过从文件获取信息来读取password。原作者为了维持所有程序的一致性,因此在终端写入需要输入password。这个password在ignore_me的函数中将password写入了文件。正如作者所说,不用理会这个函数。我们就只需要当作程序是从文件中获取输入即可。

本题处理手法众多,可以用上述的05题的办法来弄出password。为了学习如何处理文件,这里只用符号化的手法来处理文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import angr
import claripy
import sys

def main(argv):
path_to_binary = argv[1]
project = angr.Project(path_to_binary)
#此时的开始地址为从读入文件开始
start_address = 0x080488ED
initial_state = project.factory.blank_state(addr=start_address)
#读取的文件名
filename = r'OJKSQYDP.txt' # :string
#看似读入64个字节,但是只处理和判断了前8个字节
symbolic_file_size_bytes = 8
#创建一个位向量
password = claripy.BVS('password', symbolic_file_size_bytes * 8)
# 创建一个符号文件,content指定的是文件内容
password_file = angr.storage.SimFile(filename, content=password)
# 将我们创建的符号文件加入到符号文件系统中,因此模拟器读取的文件就是我们创建符号文件。
initial_state.fs.insert(filename,password_file)

simulation = project.factory.simgr(initial_state)

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Good Job." in stdout_output

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Try again." in stdout_output


simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
solution_state = simulation.found[0]

solution = solution_state.se.eval(password,cast_to=bytes).decode("UTF-8")

print(solution)
else:
raise Exception('Could not find the solution')

if __name__ == '__main__':
main(sys.argv)

小总结 02

上述几个题的思路都是使我们创建的位向量与存放输入的变量的位置对应起来或者是用位向量的值代替输入值(如07题)。其实很容易联想到,位向量的改变就相当于原程序启动之后输入值的改变。angr就是在模拟程序运行,不断地改变位向量的值模拟运行到达预期结果,最后得到正确的输入值。

08_angr_constraints

本题介绍如何添加约束。

依据前面的经验,我们需要从scanf之后指定开始地址。但是本题的判断条件不像前面以字符常量的形式与我们输入的值进行比较。如果按照前面那么做,首先模拟器在模拟时,password上并没有值,给password赋值的代码在scanf前面。其次在check函数中,字符串比较时一个一个字符比较,这会产生路径爆炸。每一次循环会产生两个结果(正确或错误),这么来说会有2^16次比较结果,这样做很浪费时间。因此可以手动添加约束条件来代替这个判断函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import angr
import claripy
import sys

def main(argv):
path_to_binary = argv[1]
project = angr.Project(path_to_binary)

start_address = 0x8048625
initial_state = project.factory.blank_state(addr=start_address)

password = claripy.BVS('password', 16*8)

password_address = 0x804A050
initial_state.memory.store(password_address, password)

simulation = project.factory.simgr(initial_state)
#到达判断函数前停止
address_to_check_constraint = 0x8048673
simulation.explore(find=address_to_check_constraint)

if simulation.found:
solution_state = simulation.found[0]
#将计算过的password的位向量获取出来赋值给约束位向量
constrained_parameter_address = 0x804A050
constrained_parameter_size_bytes = 16
constrained_parameter_bitvector = solution_state.memory.load(
constrained_parameter_address,
constrained_parameter_size_bytes
)

#判断成功的结果
constrained_parameter_desired_value = r"AUPDNNPROEZRJWKB" # :string

#约束位向量与成功的结果比较,相等则继续,不相等则本次模拟结果不成功,跳过进行下一次模拟。
solution_state.add_constraints(constrained_parameter_bitvector == constrained_parameter_desired_value)

solution = solution_state.solver.eval(password,cast_to=bytes).decode("UTF-8")
print(solution)
else:
raise Exception('Could not find the solution')

if __name__ == '__main__':
main(sys.argv)

09_angr_hooks

本题介绍了Angr的hooks方式,将跳过的函数替换为自己的函数

如题,获取一段输入之后加密进入check_equals函数并将比较结果保存。然后再获取一段输入之后和加密之后的password直接比较。

依据上一题的经验,进入check_equals函数会造成路径爆炸。因此我们需要跳过它,上述红框的地方就是我们需要跳过的地方。后面的代码功能我们完全可以依靠angr完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import angr
import claripy
import sys

def main(argv):
path_to_binary = argv[1]
project = angr.Project(path_to_binary)
#这里我们可以从函数开头进入,因为没有多个变量接受输入值。
initial_state = project.factory.entry_state()

#我们需要跳过的函数地址的起始
check_equals_called_address = 0x80486A9
#需要跳过的字节长度=0x80486BB-0x80486A9
instruction_to_skip_length = 18
@project.hook(check_equals_called_address, length=instruction_to_skip_length)
#此函数用来代替check_equals函数
def skip_check_equals_(state):
#类似与上一题将加密后的值读出来
user_input_buffer_address = 0x804A054 # :integer, probably hexadecimal
user_input_buffer_length = 16
user_input_string = state.memory.load(
user_input_buffer_address,
user_input_buffer_length)

check_against_string = r'XYMKBKUHNIQYNQXE' # :string
#比较加密后的值与预期的值,并将返回值给eax.因为在原程序中,函数的返回结果给了eax.
state.regs.eax = claripy.If(
user_input_string == check_against_string,
claripy.BVV(1, 32),
claripy.BVV(0, 32))
simulation = project.factory.simgr(initial_state)
def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Good Job." in stdout_output

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Try again." in stdout_output

simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
solution_state = simulation.found[0]
#将正确路径的输入结果打印出来
solution = solution_state.posix.dumps(sys.stdin.fileno())
print(solution)
else:
raise Exception('Could not find the solution')

if __name__ == '__main__':
main(sys.argv)

10_angr_simprocedures

本题介绍了一个类似hook的方式。试想在上一题中,check_equals函数被多次调用,我们不能在每次调用的地方进行hook,这样肯定使不合理的而且也无聊。因此angr提供了一个simprocedures的功能,可以用来hook一个函数。我们可以将check_equals函数给hook为我们自己的函数。

如题我们只需要将check_equals_ORSDDWXHZURJRBDH函数hook为我们自己处理函数即可。angr操作如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import angr
import claripy
import sys

def main(argv):
path_to_binary = argv[1]
project = angr.Project(path_to_binary)

initial_state = project.factory.entry_state()
#定义一个类继承于simProcedure
class ReplacementCheckEquals(angr.SimProcedure):
#需要被hook函数的参数列表,这里被hook的是check_equals_ORSDDWXHZURJRBDH的参数列表
#to_check即为上图中的s,length为上图中16,hook成功则该函数的实参自动传递进来
def run(self, to_check, length):
user_input_buffer_address = to_check
user_input_buffer_length = length
user_input_string = self.state.memory.load(
user_input_buffer_address,
user_input_buffer_length)
check_against_string = r"ORSDDWXHZURJRBDH"
#函数返回值要与被hook函数的值类型相同
return claripy.If(check_against_string==user_input_string, claripy.BVV(1,32), claripy.BVV(0,32))

#被hook函数的符号,如果没有符号可以使用project.hook(),第一个参数即可以替换为被hook函数的起始地址。
check_equals_symbol = r"check_equals_ORSDDWXHZURJRBDH" # :string
project.hook_symbol(check_equals_symbol, ReplacementCheckEquals())

simulation = project.factory.simgr(initial_state)

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Good Job." in stdout_output

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Try again." in stdout_output


simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
solution_state = simulation.found[0]
solution = solution_state.posix.dumps(sys.stdin.fileno())
print(solution)
else:
raise Exception('Could not find the solution')

if __name__ == '__main__':
main(sys.argv)

11_angr_sim_scanf

本题介绍如何hook掉scanf函数。同时介绍了如何在hook函数内部将位向量传递出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import angr
import claripy
import sys

def main(argv):
path_to_binary = argv[1]
project = angr.Project(path_to_binary)

initial_state = project.factory.entry_state()

class ReplacementScanf(angr.SimProcedure):
#此时的参数列表要和scanf一样,同时scanf的实参是可以通过该参数列表获得的
def run(self, format_string, scanf0_address, scanf1_address):
#生成两个位向量,处理办法和前面绑定输入变量的方法一样
scanf0 = claripy.BVS('scanf0', 4*8)
scanf1 = claripy.BVS('scanf1', 4*8)
self.state.memory.store(scanf0_address, scanf0, endness=project.arch.memory_endness)
self.state.memory.store(scanf1_address, scanf1, endness=project.arch.memory_endness)
#因为这里生成的位向量是在simProcedure中,需要把他传递出去,因此设置给该路径的全局变量
self.state.globals['solution0'] = scanf0
self.state.globals['solution1'] = scanf1
#和上题hook的做法一样
scanf_symbol = r"__isoc99_scanf"
project.hook_symbol(scanf_symbol, ReplacementScanf())

simulation = project.factory.simgr(initial_state)

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Good Job." in stdout_output

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Try again." in stdout_output

simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
solution_state = simulation.found[0]
#找到正确路径,获取该路径在ReplacementScanf中设置的位向量,即正确的输入。
stored_solutions0 = solution_state.globals['solution0']
stored_solutions1 = solution_state.globals['solution1']
solution =r"%u %u"%(solution_state.solver.eval(stored_solutions0),solution_state.solver.eval(stored_solutions1))
print(solution)
else:
raise Exception('Could not find the solution')

if __name__ == '__main__':
main(sys.argv)

12_angr_veritesting

angr有两种路径生成方式,动态符号执行(DSE)和静态符号执行(SSE)。前者为路径生成公式,在生成公式时会产生很高的负载,但是公式很容易解。后者为语句生成公式,公式能覆盖很多路径,但是公式难解。SSE不能针对大规模程序分析。Veritesting结合了DSE和SSE,减少了路径爆炸的影响,因此在生成模拟器的时候将Veritesting设置为True即可。

可以看到本题,无法避免路径爆炸。比较字符时,使用输入字符串与生成的字符串一一比较。如果不设置veritesting,则会导致路径爆炸的情况出现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import angr
import claripy
import sys

def main(argv):
path_to_binary=argv[1]
project=angr.Project(path_to_binary)
initial_state=project.factory.entry_state()
#将veritesting设置为True
simulation = project.factory.simgr(initial_state,veritesting=True)

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Good Job." in stdout_output

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Try again." in stdout_output

simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
solution_state = simulation.found[0]
solution = solution_state.posix.dumps(sys.stdin.fileno())
print(solution)
else:
raise Exception('Could not find the solution')

if __name__ == '__main__':
main(sys.argv)

小总结03

如上的五个题都是在避免路径爆炸或者避免一些麻烦时引入的代替原程序部分代码办法或者采取优化性能的办法,使得路径能够完整,让模拟器能够模拟出原程序运行路径。以至于方便最后找到正确路径。

13_angr_static_binary

本题处理静态编译的程序。在动态链接的程序里面,对于标准库函数的调用,angr自动的将其替换为更相应高效的simProcedure.但是在静态编译的程序里面这些函数需要我们手动替换。替换时,我们只需要将需要用到的函数替换即可,大可不必全部替换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import angr
import claripy
import sys

def main(argv):
path_to_binary=argv[1]
project=angr.Project(path_to_binary)
initial_state=project.factory.entry_state()

__libc_start_main_address=0x8048D10
printf_address=0x804ED40
_isoc99_scanf_address=0x804ED80
strcmp_address=0x805B450
puts_address=0x804F350
project.hook(__libc_start_main_address,angr.SIM_PROCEDURES['glibc']['__libc_start_main']())
project.hook(printf_address, angr.SIM_PROCEDURES['libc']['printf']())
project.hook(_isoc99_scanf_address, angr.SIM_PROCEDURES['libc']['scanf']())
project.hook(strcmp_address, angr.SIM_PROCEDURES['libc']['strcmp']())
project.hook(puts_address, angr.SIM_PROCEDURES['libc']['puts']())

simulation = project.factory.simgr(initial_state,veritesting=True)

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Good Job." in stdout_output

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Try again." in stdout_output

simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
solution_state = simulation.found[0]
solution = solution_state.posix.dumps(sys.stdin.fileno())
print(solution)
else:
raise Exception('Could not find the solution')

if __name__ == '__main__':
main(sys.argv)

14_angr_shared_library

本题是告诉我们如何分析不是可执行程序的二进制文件。比如典型的动态链接库。本题关键函数是在lib14_angr_shared_library.so中。14_angr_shared_library调用so文件中的validate函数,成功就返回True,否则就是False.

关键验证代码都在so文件中,因此本题需要angr加载so文件进行分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import angr
import claripy
import sys

def main(argv):
#该路径因为so文件名,因为核心函数是在so文件中
path_to_binary =argv[1]
#随便指定一个基地址,模仿加载so文件
base = 0x4000000
project = angr.Project(path_to_binary, load_options={
'main_opts' : {
'base_addr' : base
}
})
#创建一个指针形式的位向量,当作参数传进去validate函数,第一个参数随便指定一个值
password_pointer=claripy.BVV(0x3000000,32)
#validate函数在so文件中的偏移加上基地址,模仿出程序运行时的真实地址。
validate_function_address = base+0x6D7
#创建初始路径,直接从validate真实函数地址开始,传入相应参数
initial_state = project.factory.call_state(
validate_function_address,
password_pointer,
8
)

#创建password的位向量,将password与之前的传入指针形式的参数绑定在一起
#确保validate验证的值是当前的password
password=claripy.BVS('password',8*8)
initial_state.memory.store(password_pointer,password)
simulation = project.factory.simgr(initial_state)
#validate函数的结束地址
success_address = base+0x783
simulation.explore(find=success_address)

if simulation.found:
solution_state = simulation.found[0]
#验证validate的返回值,如果是1则验证成功,打印出来,否则当前路径无效。
solution_state.add_constraints(solution_state.regs.eax != 0)
solution = solution_state.solver.eval(password,cast_to=bytes).decode()
print(solution)
else:
raise Exception('Could not find the solution')

if __name__ == '__main__':
main(sys.argv)

15_angr_arbitrary_read

本题咋一看不知道说的是什么。题目本身也比较简单,根据题目命猜测想要任意地址读。

分析本题,其实可以通过V4的栈溢出控制s,控制s的值就可以实现任意地址读了。这个是在pwn里面一眼就看出来的东西。我就一直不明白在angr中有什么用。然后我就仔细研究代码,发现angr并不需要你懂栈溢出的原理。根据题目规定的值写好变量,然后设置好想要的预期效果,angr自动帮你生成payload,前提是题目本身就有漏洞存在。同时还可以对payload进行一些规定,比如需要写入的都是可见字符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import angr
import claripy
import sys

def main(argv):
path_to_binary = argv[1]
project = angr.Project(path_to_binary)

initial_state =project.factory.entry_state()

#hook掉scanf,与11题一样
class ReplacementScanf(angr.SimProcedure):
def run(self, format_string, key , password):
scanf0 = claripy.BVS('scanf0', 32)
scanf1 = claripy.BVS('scanf1', 20*8)
#对字符串进行一个限制,限制为大写英文字符
for char in scanf1.chop(bits=8):
self.state.add_constraints(char >= 'A', char <= 'Z')

scanf0_address = key
self.state.memory.store(scanf0_address, scanf0, endness=project.arch.memory_endness)
scanf1_address = password
self.state.memory.store(scanf1_address, scanf1)

self.state.globals['solution0'] = scanf0
self.state.globals['solution1'] = scanf1

scanf_symbol = r'__isoc99_scanf' # :string
project.hook_symbol(scanf_symbol, ReplacementScanf())

#设置一个回调函数,在程序调用puts函数成功之后,使用这个函数
def check_puts(state):
#检查调用puts参数,注意这里是调用成功了puts函数,此时esp指向返回地址,返回地址下面一个栈单位是puts参数。
puts_parameter = state.memory.load(state.regs.esp+0x4, 4, endness=project.arch.memory_endness)
#检查puts参数是否与'Good Job'字符串地址相同
if state.se.symbolic(puts_parameter):
good_job_string_address = 0x484F4A47 # :integer, probably hexadecimal
is_vulnerable_expression = good_job_string_address==puts_parameter # :boolean bitvector expression
copied_state = state.copy()
copied_state.add_constraints(is_vulnerable_expression)
# Finally, we test if we can satisfy the constraints of the state.
if copied_state.satisfiable():
# Before we return, let's add the constraint to the solver for real.
state.add_constraints(is_vulnerable_expression)
return True
else:
return False
else: # not state.se.symbolic(???)
return False

simulation = project.factory.simgr(initial_state)
def is_successful(state):
#puts函数的plt表
puts_address = 0x8048370
#此处判断程序是否成功调用puts函数,调用成功就检查参数
#函数返回成功说明成功打印"Good JOb"
if state.addr == puts_address:
return check_puts(state)
else:
return False

simulation.explore(find=is_successful)

if simulation.found:
solution_state = simulation.found[0]
solution0=solution_state.globals["solution0"]
solution1=solution_state.globals["solution1"]
solution = r'%u %20s'%(solution_state.solver.eval(solution0),solution_state.solver.eval(solution1,cast_to=bytes).decode("UTF-8"))
print(solution)
else:
raise Exception('Could not find the solution')

if __name__ == '__main__':
main(sys.argv)

16_angr_arbitrary_write

使用angr生成对存在漏洞的程序进行任意地址写的payload。

如题,本题不加任何漏洞利用,则永远不可能打印出”Good Job.” 因此需要使用angr自动生成一串payload,将password_buffer指向的内存改为”NDYNWEUJ”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import angr
import claripy
import sys

def main(argv):
path_to_binary = argv[1]
project = angr.Project(path_to_binary)
initial_state = project.factory.entry_state()
#与上题一样
class ReplacementScanf(angr.SimProcedure):
def run(self, format_string,key , password):
scanf0 = claripy.BVS('scanf0', 32)
scanf1 = claripy.BVS('scanf1', 20*8)
for char in scanf1.chop(bits=8):
self.state.add_constraints(char >= 'A', char <= 'Z')

scanf0_address = key
self.state.memory.store(scanf0_address, scanf0, endness=project.arch.memory_endness)
scanf1_address = password
self.state.memory.store(scanf1_address, scanf1)

self.state.globals['solution0'] = scanf0
self.state.globals['solution1'] = scanf1

scanf_symbol = r'__isoc99_scanf' # :string
project.hook_symbol(scanf_symbol, ReplacementScanf())

#该函数是在嗲用strncpy起作用
def check_strncpy(state):
#strncpy(dest,src,len)
#因此第一个参数是dest,在调用strncpy之后 esp指向的是返回地址,下面一个栈单位是第一个参数,紧接着第二个参数,第三个参数。。。。
strncpy_dest = state.memory.load(state.regs.esp+0x4,4,endness=project.arch.memory_endness)
strncpy_src = state.memory.load(state.regs.esp+0x8,4,endness=project.arch.memory_endness)
strncpy_len = state.memory.load(state.regs.esp+0xc,4,endness=project.arch.memory_endness)
#读出src指向的内容
src_contents = state.memory.load(strncpy_src,strncpy_len)
if state.se.symbolic(src_contents) and state.se.symbolic(strncpy_dest):
password_string = r"NDYNWEUJ" # :string
buffer_address = 0x57584344 # :integer, probably in hexadecimal
#检查src的前八个字节是否与password相同
does_src_hold_password = src_contents[-1:-64] == password_string
#检查dest的地址是否与password_buffer地址相同
does_dest_equal_buffer_address = buffer_address == strncpy_dest
if state.satisfiable(extra_constraints=(does_src_hold_password, does_dest_equal_buffer_address)):
state.add_constraints(does_src_hold_password, does_dest_equal_buffer_address)
return True
else:
return False
else: # not state.se.symbolic
return False

simulation = project.factory.simgr(initial_state)
def is_successful(state):
#strncpy的plt表
strncpy_address = 0x8048410
#调用strncpy成功时,调用check_strncpy.
if state.addr == strncpy_address:
return check_strncpy(state)
else:
return False

simulation.explore(find=is_successful)

if simulation.found:
solution_state = simulation.found[0]
solution0=solution_state.globals["solution0"]
solution1=solution_state.globals["solution1"]
solution = r'%u %20s'%(solution_state.solver.eval(solution0),solution_state.solver.eval(solution1,cast_to=bytes).decode("UTF-8"))
print(solution)
else:
raise Exception('Could not find the solution')

if __name__ == '__main__':
main(sys.argv)

17_angr_arbitrary_jump

任意地址跳转。实际上就是通过栈溢出控制返回地址。不过payload使用angr自动生成。

在read_input()函数中,存在栈溢出漏洞。我们需要控制函数跳转到print_good函数。

首先需要介绍一些无约束的路径。当程序进入了下一条指令可以是任何地址的时候,该条路径就属于无约束路径。一般来说这种路径是没有意义的,angr会默认抛弃掉该条路径,使该条路径变为unconstrained。本题无约束的路径正是我们需要的。当初发栈溢出之后,该路径就会变为无约束路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import angr
import claripy
import sys

def main(argv):
path_to_binary = argv[1]
project = angr.Project(path_to_binary)
#将输入流换为指定的位向量
input_str=claripy.BVS("payload",100*8)
initial_state = project.factory.entry_state(stdin=input_str)
#stashes中的一些列表不是默认开启的。stashes中保存则不同类别的路径
#active为激活路径,unconstrained为无约束路径,found路径为成功找到的路径
simulation = project.factory.simgr(
initial_state, save_unconstrained=True,
stashes={
'active': [initial_state],
'unconstrained':[],
'found':[],
'not_needed':[]
})

def has_found_solution():
return simulation.found

def has_unconstrained():
return simulation.unconstrained

def has_active():
return simulation.active

#循环找到每条未成功但是是激活或者无约束的路径
while (has_active() or has_unconstrained()) and (not has_found_solution()):
for unconstrained_state in simulation.unconstrained:
#针对每个无约束的路径,约束该路径的eip的值
unconstrained_state.add_constraints(unconstrained_state.regs.eip == 0x42585249)
#如果该无约束路径的eip是我们想要的,则将该路径转为成功路径。
simulation.move('unconstrained', 'found')
simulation.step()

if simulation.found:
solution_state=simulation.found[0]
#约束该路径的输入为可见字符,若不是,则抛弃该路径。
for byte in input_str.chop(bits=8):
solution_state.add_constraints(byte >= 'A', byte <= 'Z')
solution = solution_state.solver.eval(input_str,cast_to=bytes).decode()
print(solution)
else:
raise Exception('Could not find the solution')

if __name__ == '__main__':
main(sys.argv)

总结

至此,angr的基础学习就已经结束了。通过上述十几个例题的展示,angr其实理解起来并不困难。当我们需要一串字符来触发程序中某种东西的时候,就可以使用angr。angr并不需要跑完整个程序的代码,可以只执行一个函数(调用链接库的函数),也可以跳过一个函数去执行其他代码(hook),还可以在原程序基础上加入自己想要的代码(约束)。而且通过在angr脚本中加入的约束和程序本身的代码,angr模拟原程序运行,不断改变输入,从而改变和生成不同路径,可以自动帮我们得出到达目的的输入。