0%

angr学习

自己学校新生杯一道逆向题引导的angr学习

前言

上学期在图书馆研究室偶然提到了自己协会办比赛可以给学弟学妹们拿学分的想法,我当时的想法是办可以办,就是题目可能得非常简单他们才能写,嘛不过事实确实是这样。可是有外校师傅一起加入之后就变的很活跃了,室友请了启奡师傅还有福州大学的师傅来出题,自己也在二进制方向放了一波签到题,举行了一次面向大一大二的新生杯,也拉了一些外校的师傅来打,整体办的还是蛮成功的,室友看上去也挺开心,这里专门用启奡师傅给的逆向来记一篇一直没有接触的angr的学习

题目

看来以后写博客得把题目链接也给上了,所以去github新建了一个用来放题目的仓库,题目链接

拿到题目之后首先用IDA脚本去花指令,这里用的是IDApython

1
2
3
4
5
6
7
8
9
10
11
ads = 0x4005B0

end = 0x401DC0

codes = get_bytes(ads, end-ads)

codes = codes.replace("\x74\x03\x75\x01\xe8\x90", "\x90\x90\x90\x90\x90\x90")

patch_bytes(ads, codes)

print "[+] patch ok"

得到函数之后可以发现是一个很长的线性执行流程,直接手动把整个流程一步步过肯定很麻烦了,当然要自己写脚本来简化

1585656430766

出题人wp写的思路是:

  1. 简单花指令的去除
  2. 在有限域上运算的简化

ps:在逆向的时候突然发现原来IDA可以按=映射变量,噗,我尼玛玩了这么久的IDA居然才知道

第一种解法:IDApython

其中一种解题方法是用IDApython来解,这里也学习了一些IDApython脚本的用法,具体脚本如下:

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
def trans(xx, kk):
return [(x-kk) & 0xFF for x in xx]
def xor(xx, kk):
return [x^kk for x in xx]
def not_(xx):
return [~x for x in xx]

dt = [0xd9, 0x2c, 0x27, 0xd6, 0xd8, 0x2a, 0xda, 0x2d, 0xd7, 0x2c, 0xdc, 0xe1, 0xdb, 0x2c, 0xd9, 0xdd, 0x27, 0x2d, 0x2a, 0xdc, 0xdb, 0x2c, 0xe1, 0x29, 0xda, 0xda, 0x2c, 0xda, 0x2a, 0xd9, 0x29, 0x2a]

ads = 0x4005B0
end = 0x401DC0
i = PrevHead(end)
while i > ads: #获取不同的指令以及指令的操作数来进行求解
if GetMnem(i) == 'xor' and GetOpnd(i, 0) == 'byte ptr [rdx+rax+5]':
k = int(GetOpnd(i, 1).rstrip('h'), 16)
dt = xor(dt, k)
print("xor: {}".format(k))
if GetMnem(i) == 'add' and GetOpnd(i, 0) == 'byte ptr [rdx+rax+5]':
k = int(GetOpnd(i, 1).rstrip('h'), 16)
dt = trans(dt, k)
print("trans: {}".format(k))
if GetMnem(i) == 'not' and GetOpnd(i, 0) == 'byte ptr [rdx+rax+5]':
dt = not_(dt)
print("not: {}".format(k))
i = PrevHead(i)

print(dt)

第二种解法:angr

当然我这这篇文章主要要说的就是第二种解法了:angr求解

What is angr

首先angr是一个什么东西呢

angr is a suite of Python 3 libraries that let you load a binary and do a lot of cool things to it:

(angr是一个套用来加载二进制文件做一些很酷的事情的python3库)

具体可以用来做以下的一些事(虽然不知道大佬们把这些翻译成什么中文了,大致看看先)

  • Disassembly and intermediate-representation lifting
  • Program instrumentation
  • Symbolic execution
  • Control-flow analysis
  • Data-dependency analysis
  • Value-set analysis (VSA)
  • Decompilation

在使用之前强烈建议多了解一下符号执行的概念,要不然官方文档中很多英文还有概念可能会看不懂,特别是那些带数学符号的概念名词,不过细心一点看不会很难懂的。但是毕竟有些词语有些学术化,我也懒得用那么多俗语来解释了,下文只在一些重要地方做一些注释

例如:这篇文章

安装

安装环境:Ubuntu16.04

  1. 安装之前首先把自己默认的python3.5换成了python 3.7.1(编译安装)
  2. sudo apt-get install python3-dev libffi-dev build-essential virtualenvwrapper
  3. mkvirtualenv --python=$(which python3) angr && pip install angr

其中virtualenvwrapper是一个Python虚拟环境,使用虚拟环境的主要原因是angr会修改libz3和libVEX

创建虚拟环境之后每次使用workondeactivate即可在真实与虚拟环境切换

使用&examples

这里用一个简单的例子来开始直接上手学习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
55
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
char *sneaky = "SOSNEAKY";

int authenticate(char *username, char *password)
{
char stored_pw[9];
stored_pw[8] = 0;
int pwfile;

// evil back d00r
if (strcmp(password, sneaky) == 0) return 1;

pwfile = open(username, O_RDONLY);
read(pwfile, stored_pw, 8);

if (strcmp(password, stored_pw) == 0) return 1;
return 0;

}

int accepted()
{
printf("Welcome to the admin console, trusted user!\n");
}

int rejected()
{
printf("Go away!");
exit(1);
}

int main(int argc, char **argv)
{
char username[9];
char password[9];
int authed;

username[8] = 0;
password[8] = 0;

printf("Username: \n");
read(0, username, 8);
read(0, &authed, 1);
printf("Password: \n");
read(0, password, 8);
read(0, &authed, 1);

authed = authenticate(username, password);
if (authed) accepted();
else rejected();
}

这里程序给出了一个很简单的逻辑,具体是我们只要输入password"SOSNEAKY"就可以直接认证通过

然后这里是angr官网给出的solve.py,我在其中注释处做了一些补充笔记

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
#!/usr/bin/env python

import angr
import sys

# Look at fauxware.c! This is the source code for a "faux firmware" (@zardus
# really likes the puns) that's meant to be a simple representation of a
# firmware that can authenticate users but also has a backdoor - the backdoor
# is that anybody who provides the string "SOSNEAKY" as their password will be
# automatically authenticated.

def basic_symbolic_execution():
# We can use this as a basic demonstration of using angr for symbolic
# execution. First, we load the binary into an angr project.
# 这里先展示了一个很基础的符号执行,首先把对应的binary文件名填进一个angr Project
p = angr.Project('eg1',load_options={"auto_load_libs": False})
# 这里我加上了 "load_options={"auto_load_libs": False}"
# 在load了Project之后可以用p.loader.all_objects查看所有加载器已加载的对象

# Now, we want to construct a representation of symbolic program state.
# SimState objects are what angr manipulates when it symbolically executes
# binary code.
# SimState对象也主要保存着程序运行到某一阶段的状态信息。通过这个对象可以操作某一运行状态的上下文信息
# 比如内存,寄存器等

# 可以通过Project.factory这个容器中的任何一个方法来获取SimState对象,这个factory有多个构造函数
# 如:block、entry_state等。这里使用entry_state返回一个初始化到二进制entry point的SimState对象
# The entry_state constructor generates a SimState that is a very generic
# representation of the possible program states at the program's entry
# point. entry_state 主要是做一些初始化工作,然后在程序的入口处停下
# There are more constructors, like blank_state, which constructs a
# "blank slate" state that specifies as little concrete data as possible,
# or full_init_state, which performs a slow and pedantic initialization of
# program state as it would execute through the dynamic loader.
# 其中 full_init_state 会从动态链接时开始就记录

state = p.factory.entry_state()
# state对象一般是作为 符号执行开始前创建用来为后续的执行初始化一些数据,比如栈状态,寄存器值。
# 或者在 路径探索结束后 返回一个 state 对象供用户提取需要的值或进行约束求解
# 解出到达目标分支所使用的符号量的值。

# 当然了解了符号执行的概念之后就知道为什么有时候要默认在程序入口点停下了,毕竟我们暂时要分析的是程序的执行流

# Now, in order to manage the symbolic execution process from a very high
# level, we have a SimulationManager. SimulationManager is just collections
# of states with various tags attached with a number of convenient
# interfaces for managing them.

sm = p.factory.simulation_manager(state)
# 根据state设置 Simulation Managers ,这是一个进行路径探索的对象
# Uncomment the following line to spawn an IPython shell when the program
# gets to this point so you can poke around at the four objects we just
# constructed. Use tab-autocomplete and IPython's nifty feature where if
# you stick a question mark after the name of a function or method and hit
# enter, you are shown the documentation string for it.

# import IPython; IPython.embed()

# Now, we begin execution. This will symbolically execute the program until
# we reach a branch statement for which both branches are satisfiable.

sm.run(until=lambda sm_: len(sm_.active) > 1)
# 此示例代码采用的方法是用Simulation Managers的run方法
# 执行刚好出现 2个分支时就执行完毕,也就是我们后门函数的那个if条件被触发,此时程序产生两个执行分支,随即停止

# If you look at the C code, you see that the first "if" statement that the
# program can come across is comparing the result of the strcmp with the
# backdoor password. So, we have halted execution with two states, each of
# which has taken a different arm of that conditional branch. If you drop
# an IPython shell here and examine sm.active[n].solver.constraints
# you will see the encoding of the condition that was added to the state to
# constrain it to going down this path, instead of the other one. These are
# the constraints that will eventually be passed to our constraint solver
# (z3) to produce a set of concrete inputs satisfying them.

# As a matter of fact, we'll do that now.

input_0 = sm.active[0].posix.dumps(0)
input_1 = sm.active[1].posix.dumps(0)

# We have used a utility function on the state's posix plugin to perform a
# quick and dirty concretization of the content in file descriptor zero,
# stdin. One of these strings should contain the substring "SOSNEAKY"!

# 当然这里也可以选择把所有分支都打印出来看看,代码:
# for i in range(len(pathgroup.active)):
# print "possible %d: " % i, pathgroup.active[i].state.posix.dumps(0)
# 此时dump的就是字符串str
if b'SOSNEAKY' in input_0:
return input_0
else:
return input_1

def test():
r = basic_symbolic_execution()
assert b'SOSNEAKY' in r

if __name__ == '__main__':
sys.stdout.buffer.write(basic_symbolic_execution())

# You should be able to run this script and pipe its output to fauxware and
# fauxware will authenticate you.

其中在创建Project时有这么一个点要注意一下

auto_load_libs 设置是否自动载入依赖的库,如果设置为 True 的话会自动载入依赖的库,然后分析到库函数调用时也会进入库函数,这样会增加分析的工作量,也有能会跑挂

eg1的第二个solve代码:

1
2
3
4
5
6
7
8
proj = angr.Project('eg1')
state = proj.factory.entry_state()
while True:
succ = state.step() # 一次step记录一次执行分支的state
if len(succ.successors) == 2:#这个代码的意思也是在产生两个分支时停下,打印输出用上面代码的注释那段就好
break
state = succ.successors[0]
state1, state2 = succ.successors

这里step函数的返回值是 “an object called SimSuccessors

当然这里写法还有很多很多,具体强烈推荐参考官方文档,还有官方API文档,本博客在文末板块会尽量更新一下CTF中碰到的用法,限于英语原因很多原文中的core conception可能本人理解会有误

博主写到这里暂时感觉最好用的函数是这几个:

1
2
3
4
5
simgr.step()#这个就是上面代码中的,一个分支一个分支的记录也好像挺好用2333

sm.explore(find=0x400591) # 这个是直接设置符号执行遍历到哪个代码块停下来,很好用

simgr.explore(find=lambda s: b"Congrats" in s.posix.dumps(1))#这个是用标准输出中输出了什么来判断,直接用起来也很无脑

此道逆向题的angr解法

中间这些state操作建议先参考文末的设置&变量用法

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

p = angr.Project("./funre", load_options={"auto_load_libs": False})
f = p.factory
state = f.entry_state(addr=0x400605) # 设置state开始运行时运行到的地址
flag = claripy.BVS("flag", 8*32) #这里设置了一个flag VBS,长度是8*32bit
state.memory.store(0x603055+0x300+5, flag) #因为程序没有输入,所以直接把字符串设置到内存
state.regs.rdx = 0x603055+0x300
state.regs.rdi = 0x603055+0x300+5 # 然后设置两个寄存器

sm = p.factory.simulation_manager(state) # 准备从该state开始遍历执行路径

print("[+] init ok")

sm.explore(find=0x401DAE) # 遍历到成功的地址
if sm.found:
print("[+] found!")
x = sm.found[0].solver.eval(flag, cast_to=bytes)
print(x)

我跑下来大概只用了一两分钟时间不到?不过貌似还能加速,这里也介绍一下加速要用到的东西

加速模块安装


注意这里安装的方法本人安装失败了…介于之前装崩python的原因就暂时不继续探究原因了,只不过好像是一个什么版本的问题,具体参考这篇文章末尾

  1. sudo apt install pypy
  2. wget https://bootstrap.pypa.io/get-pip.py
  3. sudo pypy get-pip.py

第二种方法就是在init_state时加上add_options=angr.options.unicorn

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
################## 设置操作
state.solver.BVV(1, 64) # 设置一个Bitvector 第一个参数是初始值 第二个参数是长度 也可以用下面简单的写法

args = claripy.BVS("args", 8 * 16) # 用claripy包设置一个符号变量(十六进制字符串)

weird_nine.zero_extend(64 - 27) # Bitvector长度转换 这里示意是从27->64

p.hook(addr=0x08048485, hook=hook_demo, length=2) # 设置一个hook,address是执行到什么地方之后hook,hook_demo代表一个函数,length是hook_demo执行之后需要跳过的指令长度
#具体的hook操作在以后会慢慢写到


################## state操作
state.regs.X # X代表要操作的寄存器,这里可以设置寄存器的值,此时寄存器的类型是一个Bitvector

hex(state.se.eval(state.regs.X)) # 想要获取寄存器Bitvector的int值需要使用eval函数来获取 BVS同理

state.mem[state.regs.rsp].qword # 获取某个寄存器指向的内存 可以看到这里mem的参数也是BVV
# 但是返回值是类似这种:<uint64_t <BV64 0xdeadbeefdeadbeef> at 0x7fffffffffeff78>
state.se.eval(xxx.qword.resolved) # 通过eval拿到此地址对应类型的确切值

state.memory.store(state.regs.rsp,data)
state.memory.load(state.regs.rsp, 0x40) # 这里可以直接通过地址来进行存储不一定要寄存器,满足BVV的操作就行

state.posix.dumps(0) # dump运行时标准输入


################# SimulationManager操作
sm = p.factory.simgr(state)
sm.explore(find=0x400591)
st = sm.found[0] # 通过found[0]拿到的是此时的state,可以再通过上面state的操作dump标准输入

st.se.eval(args,cast_to=str) # 当然如果用到了 BVS 的话就可以使用cast_to来转成普通字符串了

参考文章:https://www.secpulse.com/archives/83197.html