angr 安装

angr 需要使用到 z3、pyvex ,但使用的版本会与原本有去区别,所以推荐在 virtualenv 虚拟 python 环境中安装 angr

自定义 docker

基于 debian 11 自定义制作的 docker 镜像,内含 aconda、jupyterlib、angr

1
2
3
git clone --depth=1 [email protected]:skyedai910/anaconda_angr_docker.git
cd anaconda_angr_docker
docker-compose up -d --build
  • 端口映射:8888:8888
  • 卷轴挂载:./volumes:/root

angr ctf

用于训练 angr 的题目仓库:https://github.com/jakespringer/angr_ctf

00_angr_find

简单加密程序,找到校验成功的内存地址之后用 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
#encoding:utf-8
import angr
import sys

binary_path = "00_angr_find"

def main(argv):
# 将二进制程序加载到项目中
project = angr.Project(binary_path)
# 从程序的入口点创建一个 state(程序状态)
initial_state = project.factory.entry_state()
# 初始化模拟程序状态的 SimState 对象,包含了程序的内存、
# 寄存器、文件系统数据等等模拟运行时动态变化的数据
simulation = project.factory.simgr(initial_state)

# 目标内存地址
print_good_address = 0x804867d
# 规避内存地址
bad_address = 0x0804866B
# 运行angr
simulation.explore(find=print_good_address,avoid=bad_address)

if simulation.found:
# 提起第一个到达目标内存地址的程序状态
solution_state = simulation.found[0]
# 获取输入
print(solution_state.posix.dumps(sys.stdin.fileno()))

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

01_angr_avoid

这题学习使用 avoid 规避某些路径。这里存在疑问就是:一开始将 good_address 设置在函数开头 0x080485B5;bad_address 也是设置在函数头部 0x080485a8,但这样 fuzz 出来的密码不对。将 good_address 设置在 puts 输出成功提示符 0x080485e5 就能出来正确结果,然后进一步测试将其设置为 0x080485C9 等函数内部的地址只有部分能成功 fuzz 出正确结果

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
#encoding:utf-8
import angr
import sys

binary_path = "01_angr_avoid"

def main(argv):
# 将二进制程序加载到项目中
project = angr.Project(binary_path)
# 从程序的入口点创建一个 state(程序状态)
initial_state = project.factory.entry_state()
# 初始化模拟程序状态的 SimState 对象,包含了程序的内存、
# 寄存器、文件系统数据等等模拟运行时动态变化的数据
simulation = project.factory.simgr(initial_state)

# 目标内存地址
print_good_address = 0x080485e5
# 规避内存地址
bad_address = 0x080485a8
# 运行angr
simulation.explore(find=print_good_address,avoid=bad_address)

if simulation.found:
# 提起第一个到达目标内存地址的程序状态
solution_state = simulation.found[0]
# 获取输入
print(solution_state.posix.dumps(sys.stdin.fileno()))

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

02_angr_find_condition

虽然汇编看不了,但是还是能查到输出成功和错误提示的地址,但利用上面两题脚本 fuzz 不出来。

这题不直接写成功路径的内存地址,而是用探索满足条件的状态(stdout 包含 Good Job )。

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
#encoding:utf-8
import angr
import sys

binary_path = "02_angr_find_condition"

# 满足条件的程序状态
def is_successful(state):
# 提取程序的输出内容
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Good Job." in stdout_output
# 需要规避的程序状态
def is_bad(state):
# 提取程序的输出内容
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Try again." in stdout_output

def main(argv):
# 将二进制程序加载到项目中
project = angr.Project(binary_path)
# 从程序的入口点创建一个 state(程序状态)
initial_state = project.factory.entry_state()
# 初始化模拟程序状态的 SimState 对象,包含了程序的内存、
# 寄存器、文件系统数据等等模拟运行时动态变化的数据
simulation = project.factory.simgr(initial_state)

# 运行angr
simulation.explore(find=is_successful,avoid=is_bad)

if simulation.found:
# 提起第一个到达目标内存地址的程序状态
solution_state = simulation.found[0]
# 获取输入
print(solution_state.posix.dumps(sys.stdin.fileno()))
else:
print("can't no find solve")

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

03_angr_symbolic_registers

操作 scanf 在寄存器上的值

程序用scanf("%x %x %x", &v1, &v2, &v3);读取 3 个值放在 eax、ebx、edx ,三者经过复杂运算后全为 0 则成功。

angr 不支持使用 scanf 读取多个内容(scanf("%u %u)),需要将程序起点设置在 scanf 之后,并手动将值传入寄存器

定义 BVS 与 z3 使用一样的

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
#encoding:utf-8
import angr
import claripy
import sys

binary_path = "03_angr_symbolic_registers"

# 满足条件的程序状态
def is_successful(state):
# 提取程序的输出内容
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Good Job." in stdout_output
# 需要规避的程序状态
def is_bad(state):
# 提取程序的输出内容
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Try again." in stdout_output

def main(argv):
# 将二进制程序加载到项目中
project = angr.Project(binary_path)
# 从scanf之后创建一个 state(程序状态)
start_address = 0x80488d1
# 用blank_state来实现从某个地址处载入
initial_state = project.factory.blank_state(addr=start_address)
# 初始化模拟程序状态的 SimState 对象,包含了程序的内存、
# 寄存器、文件系统数据等等模拟运行时动态变化的数据
simulation = project.factory.simgr(initial_state)

# 创建值为 passwordX 的 BV32 对象
password0 = claripy.BVS('password0', 32)
password1 = claripy.BVS('password1', 32)
password2 = claripy.BVS('password2', 32)

# 设置寄存器的值
initial_state.regs.eax = password0
initial_state.regs.ebx = password1
initial_state.regs.edx = password2

# 运行angr
simulation.explore(find=is_successful,avoid=is_bad)

if simulation.found:
# 提起第一个到达目标内存地址的程序状态
solution_state = simulation.found[0]
solution0 = solution_state.solver.eval(password0)
solution1 = solution_state.solver.eval(password1)
solution2 = solution_state.solver.eval(password2)
# 获取输入
print(hex(solution0), hex(solution1), hex(solution2))
else:
print("can't no find solve")

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

04_angr_symbolic_stack

操作 scanf 在栈上的值

这个程序 scanf 将内容存储在栈上,需要自行构造栈空间绕过 scanf 输入。

(32 位程序)call 后面调整栈顶 esp 也是算同一个栈帧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.text:08048679                 push    ebp
.text:0804867A mov ebp, esp
.text:0804867C sub esp, 18h
.text:0804867F sub esp, 4
.text:08048682 lea eax, [ebp+var_10]
.text:08048685 push eax
.text:08048686 lea eax, [ebp+var_C]
.text:08048689 push eax
.text:0804868A push offset aUU ; "%u %u"
.text:0804868F call ___isoc99_scanf
.text:08048694 add esp, 10h //scanf结束
.text:08048697 mov eax, [ebp+var_C]
.text:0804869A sub esp, 0Ch
.text:0804869D push eax
.text:0804869E call complex_function0
.text:080486A3 add esp, 10h //complex_function0结束
.text:080486A6 mov [ebp+var_C], eax
.text:080486A9 mov eax, [ebp+var_10]
.text:080486AC sub esp, 0Ch
.text:080486AF push eax
.text:080486B0 call complex_function1
.text:080486B5 add esp, 10h //complex_function1结束
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
#encoding:utf-8
import angr
import claripy
import sys

binary_path = "04_angr_symbolic_stack"

# 满足条件的程序状态
def is_successful(state):
# 提取程序的输出内容
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Good Job." in stdout_output
# 需要规避的程序状态
def is_bad(state):
# 提取程序的输出内容
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Try again." in stdout_output

def main(argv):
# 将二进制程序加载到项目中
project = angr.Project(binary_path)
# 从scanf之后创建一个 state(程序状态)
start_address = 0x8048697
# 用blank_state来实现从某个地址处载入
initial_state = project.factory.blank_state(addr=start_address)

# 创建值为 pX 的 BV32 对象,每个长度4字节
password0 = claripy.BVS('p0', 4*8)
password1 = claripy.BVS('p1', 4*8)
# 获取现在栈顶
initial_state.regs.ebp = initial_state.regs.esp
# 调整栈顶,申请栈空间
initial_state.regs.esp -= 0x8

# 初始化模拟程序状态的 SimState 对象,包含了程序的内存、
# 寄存器、文件系统数据等等模拟运行时动态变化的数据
simulation = project.factory.simgr(initial_state)
# 压入数据
initial_state.stack_push(password0)
initial_state.stack_push(password1)

# 运行angr
simulation.explore(find=is_successful,avoid=is_bad)

if simulation.found:
# 提起第一个到达目标内存地址的程序状态
solution_state = simulation.found[0]
solution0 = solution_state.solver.eval(password0)
solution1 = solution_state.solver.eval(password1)
# 获取输入
print(hex(solution0), hex(solution1))
else:
print("can't no find solve")

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

05_angr_symbolic_memory

操控 scanf 在全局变量上的值

memory.store 对内存操作需要指定 endness 为程序的端序(小端序),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
56
57
58
59
60
61
62
63
#encoding:utf-8
import angr
import claripy
import sys

binary_path = "05_angr_symbolic_memory"

# 满足条件的程序状态
def is_successful(state):
# 提取程序的输出内容
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Good Job." in stdout_output
# 需要规避的程序状态
def is_bad(state):
# 提取程序的输出内容
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Try again." in stdout_output

def main(argv):
# 将二进制程序加载到项目中
project = angr.Project(binary_path)
# 从scanf之后创建一个 state(程序状态)
start_address = 0x8048606
# 用blank_state来实现从某个地址处载入
initial_state = project.factory.blank_state(addr=start_address)

# 创建值为 pX 的 BV32 对象,每个变量空间是8字节
password0 = claripy.BVS('p0', 8*8)
password1 = claripy.BVS('p1', 8*8)
password2 = claripy.BVS('p2', 8*8)
password3 = claripy.BVS('p3', 8*8)

# 根据地址压入全局变量
password0_address = 0xa29faa0
initial_state.memory.store(password0_address, password0, endness=project.arch.memory_endness)
password1_address = 0xa29faa8
initial_state.memory.store(password1_address, password1, endness=project.arch.memory_endness)
password2_address = 0xa29fab0
initial_state.memory.store(password2_address, password2, endness=project.arch.memory_endness)
password3_address = 0xa29fab8
initial_state.memory.store(password3_address, password3, endness=project.arch.memory_endness)

# 初始化模拟程序状态的 SimState 对象,包含了程序的内存、
# 寄存器、文件系统数据等等模拟运行时动态变化的数据
simulation = project.factory.simgr(initial_state)

# 运行angr
simulation.explore(find=is_successful,avoid=is_bad)

if simulation.found:
# 提起第一个到达目标内存地址的程序状态
solution_state = simulation.found[0]
solution0 = solution_state.solver.eval(password0)
solution1 = solution_state.solver.eval(password1)
solution2 = solution_state.solver.eval(password2)
solution3 = solution_state.solver.eval(password3)
# 获取输入
print(hex(solution0), hex(solution1), hex(solution2), hex(solution3))
else:
print("can't no find solve")

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

06_angr_symbolic_dynamic_memory

操控 scanf 在堆上的值

  1. 向 chunk_list 写入 fake_heap_addr 。伪堆地址其他师傅文章说是可以随意找,我这里是关闭 aslr gdb 找堆地址
  2. 向堆写入内容

注意官方 exp 打出来答案是不能通过校验的,原因是向堆写入内容没有设置端序,记住使用 store 时一定要设置端序

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
#encoding:utf-8
import angr
import claripy
import sys

binary_path = "06_angr_symbolic_dynamic_memory"

# 满足条件的程序状态
def is_successful(state):
# 提取程序的输出内容
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Good Job." in stdout_output
# 需要规避的程序状态
def is_bad(state):
# 提取程序的输出内容
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Try again." in stdout_output

def main(argv):
# 将二进制程序加载到项目中
project = angr.Project(binary_path)
# 从scanf之后创建一个 state(程序状态)
start_address = 0x804869e
# 用blank_state来实现从某个地址处载入
initial_state = project.factory.blank_state(addr=start_address)

# 创建值为 pX 的 BV32 对象,每个变量空间是8字节
password0 = claripy.BVS('p0', 8*8)
password1 = claripy.BVS('p1', 8*8)

# 堆指针存储地址
chunk_list = 0xa79A118
# 堆地址
heap_addr = 0xa79b158
# 向列表写入堆地址
initial_state.memory.store(chunk_list, heap_addr, endness=project.arch.memory_endness)
initial_state.memory.store(chunk_list+0x8, heap_addr+0x10, endness=project.arch.memory_endness)
# 向堆写入内容
initial_state.memory.store(heap_addr, password0, endness=project.arch.memory_endness)
initial_state.memory.store(heap_addr+0x10, password1, endness=project.arch.memory_endness)

# 初始化模拟程序状态的 SimState 对象,包含了程序的内存、
# 寄存器、文件系统数据等等模拟运行时动态变化的数据
simulation = project.factory.simgr(initial_state)

# 运行angr
simulation.explore(find=is_successful,avoid=is_bad)

if simulation.found:
# 提起第一个到达目标内存地址的程序状态
solution_state = simulation.found[0]
solution0 = solution_state.solver.eval(password0)
solution1 = solution_state.solver.eval(password1)
# 获取输入
print(hex(solution0), hex(solution1))
else:
print("can't no find solve")

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

07_angr_symbolic_file

控制在 file 的值

程序先从 stdin 输入内容,将 stdin 内容写入到文件,然后再 file_read 从文件读取 stdin 输入的内容。程序开始绕过这个前面两步,直接使用 angr 虚拟文件,让程序读取虚拟文件内容

  1. 生成 angr 虚拟文件
  2. 注入到程序中

symbolic_file_backing_memory 在新版已被取消,官方 exp 可能打不通

由于不会设置控制 file 输入时的端序设置,所以下面 exp 结果是大端序,需要每 8 字节进行字节翻转。但是如果输出是字符串格式则是小端序(最后注释部分)

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
#encoding:utf-8
import angr
import claripy
import sys

binary_path = "07_angr_symbolic_file"

# 满足条件的程序状态
def is_successful(state):
# 提取程序的输出内容
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Good Job." in stdout_output
# 需要规避的程序状态
def is_bad(state):
# 提取程序的输出内容
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Try again." in stdout_output

def main(argv):
# 将二进制程序加载到项目中
project = angr.Project(binary_path)
# 从scanf之后创建一个 state(程序状态)
start_address = 0x80488db
# 用blank_state来实现从某个地址处载入
initial_state = project.factory.blank_state(addr=start_address)

# 程序读入内容的文件名
filename = 'WCEXPXBW.txt'
# 文件大小(程序给文件写入的缓冲区大小)
symbolic_file_size_bytes = 64

# 创建值为 pX 的 BV32 对象,每个变量空间是8字节
password0 = claripy.BVS('p0', symbolic_file_size_bytes*8)
# 生成虚拟文件
password_file = angr.storage.SimFile(filename, content=password0, size=symbolic_file_size_bytes)
# 注入到程序状态
initial_state.fs.insert(filename, password_file)

# 初始化模拟程序状态的 SimState 对象,包含了程序的内存、
# 寄存器、文件系统数据等等模拟运行时动态变化的数据
simulation = project.factory.simgr(initial_state)

# 运行angr
simulation.explore(find=is_successful,avoid=is_bad)

if simulation.found:
# 提起第一个到达目标内存地址的程序状态
solution_state = simulation.found[0]
'''
输出结果是小端序
solution0 = solution.solver.eval(password, cast_to=bytes)
print(solution0)
'''
# 输出结果是大端序
solution0 = solution_state.solver.eval(password0)
# 获取输入
print(hex(solution0))
else:
print("can't no find solve")

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

08_angr_constraints

添加约束条件,避免路径爆炸

  • 每个 if 判断 angr 会创建两条路径
  • 如果使用 strcmp 等结合 for 和 if 对一段字符串逐个字节比较会产生路径爆炸。长度 16 字节的字符串,会产生 2**16 条路径

示例程序中 check_equals_AUPDNNPROEZRJWKB 就是使用逐字节比较,需要添加约束条件避免路径爆炸文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int __cdecl main(int argc, const char **argv, const char **envp)
{
signed int i; // [esp+Ch] [ebp-Ch]

qmemcpy(password, "AUPDNNPROEZRJWKB", sizeof(password));
memset(&buffer, 0, 0x11u);
printf("Enter the password: ");
__isoc99_scanf("%16s", &buffer);
for ( i = 0; i <= 15; ++i )
*(_BYTE *)(i + 134520912) = complex_function(*(char *)(i + 134520912), 15 - i);
if ( check_equals_AUPDNNPROEZRJWKB((int)&buffer, 0x10u) )
puts("Good Job.");
else
puts("Try again.");
return 0;
}

和前面题目一样绕过 scanf ,手动向内存注入 password ,运行到 0x8048671 进入 check 函数之前,添加约束条件:

  1. 提取复杂运算后的密文
  2. 添加约束条件:密文需要等于 BWYRUBQCMVSBRGFU

这里比较麻烦的还是大小端序的问题,记住这个规律大部分情况能分辨怎么配置。

假设程序是小端序:

  • angr 模拟量与程序内存值比较:需要将模拟量配置为小端序

    例题:[第五题](# 05_angr_symbolic_memory)开始都是

  • angr 模拟量与已知值比较(自定义值、脚本里面写的):不一定需要配置为小端序,只要模拟量和已知值是同一端序即可

    例题:[08_angr_constraints](# 08_angr_constraints)

小端序

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
#encoding:utf-8
import angr
import claripy
import sys

binary_path = "08_angr_constraints"

def main(argv):
# 将二进制程序加载到项目中
project = angr.Project(binary_path)
# 从scanf之后创建一个 state(程序状态)
start_address = 0x804862a
# 用blank_state来实现从某个地址处载入
initial_state = project.factory.blank_state(addr=start_address)

# 创建值为 pX 的 BV32 对象,每个变量空间是8字节
password0 = claripy.BVS('p0', 16*8)

# scanf输入存储地址
password0_address = 0x804a050
# 注入密文到内存
initial_state.memory.store(password0_address, password0, endness=project.arch.memory_endness)

# 初始化模拟程序状态的 SimState 对象,包含了程序的内存、
# 寄存器、文件系统数据等等模拟运行时动态变化的数据
simulation = project.factory.simgr(initial_state)

# 运行到check函数之前
address_to_check_constraint = 0x8048671

# 运行angr
simulation.explore(find=address_to_check_constraint)

if simulation.found:
# 提起第一个到达目标内存地址的程序状态
solution_state = simulation.found[0]

# 加载运行到check之前内存上的明文
constrained_parameter_address = 0x804a050
constrained_parameter_size_bytes = 16
constrained_parameter_bitvector = solution_state.memory.load(
constrained_parameter_address,
constrained_parameter_size_bytes,
endness=project.arch.memory_endness
)

# 目标结果(密文)
constrained_parameter_desired_value = 'BWYRUBQCMVSBRGFU'[::-1]
# 添加约束条件
solution_state.add_constraints(constrained_parameter_bitvector == constrained_parameter_desired_value)

# 求解得到满足上述条件的输入
solution0 = solution_state.solver.eval(password0)
# 获取输入
print(hex(solution0))
else:
print("can't no find solve")

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

大端序

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
#encoding:utf-8
import angr
import claripy
import sys

binary_path = "08_angr_constraints"

def main(argv):
# 将二进制程序加载到项目中
project = angr.Project(binary_path)
# 从scanf之后创建一个 state(程序状态)
start_address = 0x804862a
# 用blank_state来实现从某个地址处载入
initial_state = project.factory.blank_state(addr=start_address)

# 创建值为 pX 的 BV32 对象,每个变量空间是8字节
password0 = claripy.BVS('p0', 16*8)

# scanf输入存储地址
password0_address = 0x804a050
# 注入密文到内存
initial_state.memory.store(password0_address, password0)

# 初始化模拟程序状态的 SimState 对象,包含了程序的内存、
# 寄存器、文件系统数据等等模拟运行时动态变化的数据
simulation = project.factory.simgr(initial_state)

# 运行到check函数之前
address_to_check_constraint = 0x8048671

# 运行angr
simulation.explore(find=address_to_check_constraint)

if simulation.found:
# 提起第一个到达目标内存地址的程序状态
solution_state = simulation.found[0]

# 加载运行到check之前内存上的明文
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 = 'BWYRUBQCMVSBRGFU'
# 添加约束条件
solution_state.add_constraints(constrained_parameter_bitvector == constrained_parameter_desired_value)

# 求解得到满足上述条件的输入
solution0 = solution_state.solver.eval(password0)
# 获取输入
print(hex(solution0))
else:
print("can't no find solve")

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

09_angr_hooks

通过地址 hook 函数

  • 新 angr 可以支持 scanf 输入
  • 两次输入,两次检查是否等于 XKSPZSJKJYQCQXZV

需要 hook check 函数替换为自定义比较函数,返回值需要根据原函数编写

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
#encoding:utf-8
import angr
import claripy
import sys

binary_path = "09_angr_hooks"

# 满足条件的程序状态
def is_successful(state):
# 提取程序的输出内容
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Good Job." in stdout_output
# 需要规避的程序状态
def is_bad(state):
# 提取程序的输出内容
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Try again." in stdout_output

def main(argv):
# 将二进制程序加载到项目中
project = angr.Project(binary_path)
# 从程序的入口点创建一个 state(程序状态)
initial_state = project.factory.entry_state()

# check函数调用地址
check_equals_called_address = 0x80486b8
# call汇编指令长度(x86 5字节)
instruction_to_skip_length = 5

# 将call check hook 为自定义函数
@project.hook(check_equals_called_address, length=instruction_to_skip_length)
def skip_check_equals_(state):
# buffer地址
user_input_buffer_address = 0x804a054
# buffer长度
user_input_buffer_length = 16
# 从内存加载用户输入内容
user_input_string = state.memory.load(
user_input_buffer_address,
user_input_buffer_length
)
# 密文
check_against_string = 'XKSPZSJKJYQCQXZV'
# 重写check函数
state.regs.eax = claripy.If(
user_input_string == check_against_string, # 判断条件
claripy.BVV(1, 32), # 匹配返回1(int型)
claripy.BVV(0, 32) # 不匹配返回0(int型)
)

# 初始化模拟程序状态的 SimState 对象,包含了程序的内存、
# 寄存器、文件系统数据等等模拟运行时动态变化的数据
simulation = project.factory.simgr(initial_state)
# 运行angr
simulation.explore(find=is_successful,avoid=is_bad)

if simulation.found:
# 提起第一个到达目标内存地址的程序状态
solution_state = simulation.found[0]
# 求解得到满足上述条件的输入
solution0 = solution_state.posix.dumps(sys.stdin.fileno())
# 获取输入
print((solution0))
else:
print("can't no find solve")

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

10_angr_simprocedures

通过符号名(函数名)hook 函数

通过汇编视图可以看到 check 调用关系极度复杂,已经不能地址 hook 函数解决(太多了),使用 SimProcedure 自定义函数并 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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#encoding:utf-8
import angr
import claripy
import sys

binary_path = "10_angr_simprocedures"

# 满足条件的程序状态
def is_successful(state):
# 提取程序的输出内容
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Good Job." in stdout_output
# 需要规避的程序状态
def is_bad(state):
# 提取程序的输出内容
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Try again." in stdout_output

def main(argv):
# 将二进制程序加载到项目中
project = angr.Project(binary_path)
# 从程序的入口点创建一个 state(程序状态)
initial_state = project.factory.entry_state()
# SimProcedure自定义hook函数
class ReplacementCheckEquals(angr.SimProcedure):
def run(self, to_check, length):
# buffer地址
user_input_buffer_address = to_check
# buffer长度
user_input_buffer_length = length
# 从内存加载用户输入内容
user_input_string = self.state.memory.load(
user_input_buffer_address,
user_input_buffer_length
)
# 密文
check_against_string = 'WQNDNKKWAWOLXBAC'
# 重写check函数
return claripy.If(
user_input_string == check_against_string, # 判断条件
claripy.BVV(1, 32), # 匹配返回1(int型)
claripy.BVV(0, 32) # 不匹配返回0(int型)
)
# 被hook的符号(函数)名
check_equals_symbol = 'check_equals_WQNDNKKWAWOLXBAC'
# hook
project.hook_symbol(check_equals_symbol, ReplacementCheckEquals())

# 初始化模拟程序状态的 SimState 对象,包含了程序的内存、
# 寄存器、文件系统数据等等模拟运行时动态变化的数据
simulation = project.factory.simgr(initial_state)
# 运行angr
simulation.explore(find=is_successful,avoid=is_bad)

if simulation.found:
# 提起第一个到达目标内存地址的程序状态
solution_state = simulation.found[0]
# 求解得到满足上述条件的输入
solution0 = solution_state.posix.dumps(sys.stdin.fileno())
# 获取输入
print((solution0))
else:
print("can't no find solve")

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

11_angr_sim_scanf

需要 hook scanf 函数,不能直接将程序起点设置到 scanf 之后,因为 scanf 前面有秘钥生成过程,如果跳过秘钥为空。

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
#encoding:utf-8
import angr
import claripy
import sys

binary_path = "11_angr_sim_scanf"

# 满足条件的程序状态
def is_successful(state):
# 提取程序的输出内容
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Good Job." in stdout_output
# 需要规避的程序状态
def is_bad(state):
# 提取程序的输出内容
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Try again." in stdout_output

def main(argv):
# 将二进制程序加载到项目中
project = angr.Project(binary_path)
# 从程序的入口点创建一个 state(程序状态)
initial_state = project.factory.entry_state()
# SimProcedure自定义hook函数
class ReplacementCheckEquals(angr.SimProcedure):
def run(self, format_string, param0, param1):
# scanf两个输入
scanf0 = claripy.BVS('scanf0', 32)
scanf1 = claripy.BVS('scanf1', 32)

# 向内存填入输入
scanf0_address = param0
self.state.memory.store(scanf0_address, scanf0, endness=project.arch.memory_endness)
scanf1_address = param1
self.state.memory.store(scanf1_address, scanf1, endness=project.arch.memory_endness)

# 全变量存储输入内容
self.state.globals['solutions'] = (scanf0, scanf1)

# 被hook的符号(函数)名
check_equals_symbol = '__isoc99_scanf'
# hook
project.hook_symbol(check_equals_symbol, ReplacementCheckEquals())

# 初始化模拟程序状态的 SimState 对象,包含了程序的内存、
# 寄存器、文件系统数据等等模拟运行时动态变化的数据
simulation = project.factory.simgr(initial_state)
# 运行angr
simulation.explore(find=is_successful,avoid=is_bad)

if simulation.found:
# 提起第一个到达目标内存地址的程序状态
solution_state = simulation.found[0]
# 从全局变量提取出输入内容
stored_solutions = solution_state.globals['solutions']
solution = ' '.join(map(str, map(solution_state.se.eval, stored_solutions)))
print((solution))
else:
print("can't no find solve")

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

12_angr_veritesting

开启Veritesting技术解决路径爆炸问题

创建 SimulationManager 时通过 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
31
32
33
34
35
36
37
38
39
40
41
42
43
#encoding:utf-8
import angr
import claripy
import sys

binary_path = "12_angr_veritesting"

# 满足条件的程序状态
def is_successful(state):
# 提取程序的输出内容
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Good Job." in stdout_output
# 需要规避的程序状态
def is_bad(state):
# 提取程序的输出内容
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Try again." in stdout_output

def main(argv):
# 将二进制程序加载到项目中
project = angr.Project(binary_path)
# 从程序的入口点创建一个 state(程序状态)
initial_state = project.factory.entry_state()

# 初始化模拟程序状态的 SimState 对象,包含了程序的内存、
# 寄存器、文件系统数据等等模拟运行时动态变化的数据
# veritesting=True 开启Veritesting技术解决路径爆炸问题
simulation = project.factory.simgr(initial_state, veritesting=True)
# 运行angr
simulation.explore(find=is_successful,avoid=is_bad)

if simulation.found:
# 提起第一个到达目标内存地址的程序状态
solution_state = simulation.found[0]
# 求解得到满足上述条件的输入
solution0 = solution_state.posix.dumps(sys.stdin.fileno())
# 获取输入
print((solution0))
else:
print("can't no find solve")

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

13_angr_static_binary

静态程序 hook 系统函数

由于是静态程序系统函数被包含在 elf 中,导致 angr 会对系统函数本身进行路径测试,而导致的效率低下、路径爆炸问题(可以用 00_angr_find exp 测试一下观察路径测试的内存地址就知道了)。

因此对于静态程序就需要 hook 系统函数提高效率,可以理解为告诉 angr 这是系统函数,这个函数不需要路径测试。

不是全部的系统函数都需要 hook ,只需要 hook 通向目标路径上所调用的系统函数。这题最后目标是 puts("Good Job.") ,涉及的系统调用顺序是:

_isoc99_scanf 原型是 scanf ,gcc 开启保护后会将 scanf 编译成 _isoc99_scanf

  1. __libc_start_main
  2. printf
  3. scanf
  4. strcmp
  5. puts

strcmp 可以不替换,因为 plt 表里面有 strcmp ,也可以双击 strcmp 进去,可以看到是一个跳转函数,而不是函数具体实现的代码,这里对比 strcmp 和其他函数就知道了。

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
#encoding:utf-8
import angr
import sys

binary_path = "13_angr_static_binary"

def main(argv):
# 将二进制程序加载到项目中
project = angr.Project(binary_path)
# 从程序的入口点创建一个 state(程序状态)
initial_state = project.factory.entry_state()

# hook 系统函数
project.hook(0x804f350, angr.SIM_PROCEDURES['libc']['puts']())
#project.hook(0x804f350, angr.SIM_PROCEDURES['libc']['strcmp']())
project.hook(0x804ed80, angr.SIM_PROCEDURES['libc']['scanf']())
project.hook(0x804ed40, angr.SIM_PROCEDURES['libc']['printf']())
project.hook(0x8048d10, angr.SIM_PROCEDURES['glibc']['__libc_start_main']())

# 初始化模拟程序状态的 SimState 对象,包含了程序的内存、
# 寄存器、文件系统数据等等模拟运行时动态变化的数据
simulation = project.factory.simgr(initial_state)

# 目标内存地址
print_good_address = 0x080489E6
# 规避内存地址
bad_address = 0x080489D4
# 运行angr
simulation.explore(find=print_good_address,avoid=bad_address)

if simulation.found:
# 提起第一个到达目标内存地址的程序状态
solution_state = simulation.found[0]
# 获取输入
print(solution_state.posix.dumps(sys.stdin.fileno()))

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

14_angr_shared_library

fuzz pie 程序设置加载基地址

1
2
3
4
5
6
base = 0
project = angr.Project(path_to_binary, load_options={
'main_opts' : {
'custom_base_addr' : base
}
})

当 angr fuzz 开启 pie 程序时,默认基地址是 0x4000000 。除了小部分题目(在固定地址上申请空间那种虚拟机题目),加载地址在哪里没有影响,因为 angr 对于 pie 程序是用偏移地址去定位代码,基地址只是影响 text 段位置而已。

总结起来就是:当程序有对明确固定地址的操作时,需要注意加载基地址,其他不需要。

对于这条题目将校验放在了函数库(开启 pie ),函数库没有对固定地址读写操作,不需要加载基地址,直接对函数库进行 fuzz

官方 exp base 是 0x4000000 默认值,可以任意改都能出正确结果

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
#encoding:utf-8
import angr
import claripy
import sys

binary_path = "lib14_angr_shared_library.so"

def main(argv):
# 函数库的基地址
base = 0
# 将二进制程序加载到项目中
project = angr.Project(binary_path, load_options={
'main_opts' : {
'custom_base_addr' : base
}
})

buffer_pointer = claripy.BVV(0x3000000, 32)
# 校验函数在函数库的绝对地址
validate_function_address = base + 0x6d7
# 调用校验函数
initial_state = project.factory.call_state(validate_function_address, buffer_pointer, claripy.BVV(8, 32))

# 写入密码变量
password = claripy.BVS('password', 8*8)
initial_state.memory.store(buffer_pointer, password)

# 初始化模拟程序状态的 SimState 对象,包含了程序的内存、
# 寄存器、文件系统数据等等模拟运行时动态变化的数据
simulation = project.factory.simgr(initial_state)

# 目标内存地址
print_good_address = base + 0x783
# 运行angr
simulation.explore(find=print_good_address)

if simulation.found:
# 提起第一个到达目标内存地址的程序状态
solution_state = simulation.found[0]
solution_state.add_constraints(solution_state.regs.eax != 0)
# 获取输入
print(solution_state.se.eval(password,cast_to=bytes))

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

参考资料