Angr 学习笔记

本文最后更新于:2024年7月12日 上午

Angr学习笔记

前言

本文记录一下Angr的基本使用方法,主要是基于Github上的开源项目以及笔记AngrCTF_FITM整理,Angr在逆向方面确实用处比较大,特此记录一下。

什么是Angr

angr是一个用于分析二进制文件的python框架。它专注于静态和符号分析,使其适用于各种任务。项目地址:https://github.com/angr

符号执行

​ 符号执行就是在运行程序时,用符号来替代真实值。符号执行相较于真实值执行的优点在于,当使用真实值执行程序时,我们能够遍历的程序路径只有一条, 而使用符号进行执行时,由于符号是可变的,我们就可以利用这一特性,尽可能的将程序的每一条路径遍历,这样的话,必定存在至少一条能够输出正确结果的分支, 每一条分支的结果都可以表示为一个离散关系式,使用约束求解引擎即可分析出正确结果。

​ 个人理解为Angr就是将输入作为一种可变符号,各种条件分支作为路径分支,符号执行实际上就是在走迷宫,最终得到到达终点的正确路径。

0x00 一般模板

题目:00_angr_find

直接拖到ida中查看源码:

image-20230315100537072

主要逻辑就是对于输入的字符串进行complex_function变换然后比较。

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
import angr
import sys


def Run():
bin_path = "./00_angr_find"
project = angr.Project(bin_path, auto_load_libs=False)
initial_state = project.factory.entry_state()
simulation = project.factory.simgr(initial_state)

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
if b'Good Job.' in stdout_output:
return True
else:
return False

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
if b'Try again.' in stdout_output:
return True
else:
return False

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

if simulation.found:
for state in simulation.found:
solution = state.posix.dumps(sys.stdin.fileno())
# solution0 = state.solver.eval(passwd0,cast_to=bytes)
# solution1 = state.solver.eval(passwd1,cast_to=bytes)
# solution = solution0+b" "+solution1
print("[+] Success! Solution is: {}".format(solution.decode("utf-8")))
else:
raise Exception('Could not find the solution')

if __name__ == "__main__":
Run()
  • project = angr.Project(bin_path, auto_load_libs=False)

    Angr使用Project作为二进制文件的基本映像,auto_load_libs=False避免程序导入不必要的库,否则分析到库函数调用时也会进入库函数,这样会增加分析的工作量,也有可能会跑挂。

  • initial_state = project.factory.entry_state()

    Angr并不是真正的运行程序,而是模拟程序的运行路径,因此Angr提供state来记录程序模拟时的状态(记录一系列程序运行时的信息,如内存/寄存器/文件等,类似于快照),project.factory.entry_state用于提供程序初始化状态。

  • simulation = project.factory.simgr(initial_state)

    基于状态创建程序模拟管理器simulation,用于控制程序的模拟执行,从我们提供的初始化状态initial_state开始。

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

    符号执行最普遍的操作时找到能够到达某个地址的状态,simulation提供了``explore()方法寻找路径,启动后程序会一直执行,直到发现了一个和find参数指定的条件相匹配的状态。其中findavoid参数可以为;

    • 具体地址
    • 具体地址的列表集合
    • state为参数的判断函数(本题解中就是用的这种方式)
  • sys.*.fileno()

    • sys.stdin.fileno()标准输入文件描述符,值为0
    • sys.stdout.fileno()标准输出文件描述符,值为1
    • sys.stderr.fileno()标准错误文件描述符,值为2
  • state.posix.dumps

    state.posix.dumps(0)代表该状态程序的所有输入,state.posix.dumps(1)代表该状态程序的所有输出。

0x01 输入的参数存放在寄存器中

题目:03_angr_symbolic_registers

日常IDA查看一下主函数:

image-20230314140123976

程序通过get_user_input读取输入,并存放至eaxebxedx,当然我们可以使用上一题那样直接进行路径搜索,但是angr在处理复杂格式的字符串输入时优化不是很好,最好的办法是绕过scanf函数,直接将符号注入到寄存器中。

image-20230314140314978

complex_function_1complex_function_2complex_function_3密码加密函数,也是我们用于路径寻找的点。

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 sys
import claripy

def Run():
bin_path = "./03_angr_symbolic_registers"
project = angr.Project(bin_path, auto_load_libs=False)
# initial_state = project.factory.entry_state()
start_address = 0x08048980
initial_state = project.factory.blank_state(addr=start_address)

passwd_size_in_bits =32
passwd0 = claripy.BVS('passwd0',passwd_size_in_bits)
passwd1 = claripy.BVS('passwd1', passwd_size_in_bits)
passwd2 = claripy.BVS('passwd2', passwd_size_in_bits)

initial_state.regs.eax = passwd0
initial_state.regs.ebx = passwd1
initial_state.regs.edx = passwd2

simulation = project.factory.simgr(initial_state)

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
if b'Good Job.' in stdout_output:
return True
else:
return False

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
if b'Try again.' in stdout_output:
return True
else:
return False

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

if simulation.found:
for i in simulation.found:
solution_state = i
solution0 = format(solution_state.solver.eval(passwd0), 'x')
solution1 = format(solution_state.solver.eval(passwd1), 'x')
solution2 = format(solution_state.solver.eval(passwd2), 'x')
solution = solution0 + " " + solution1 + " " + solution2
print("[+] Success! Solution is: {}".format(solution))
else:
raise Exception('Could not find the solution')


if __name__ == "__main__":
Run()
  • 为了绕过get_user_input输入函数,我们不能从main函数的开头开始,通过使用start_address定位到get_user_input下一条指令,即0x08048980,在此处构造程序状态。

  • project.factory.blank_state

    当我们不使用main函数作为程序入口时,初始化状态就不能够使用entry_state(),好在project.factory提供了其他的函数初始化状态:

    名称 描述
    entry_state() 构造一个从函数入口点执行的已初始化状态
    blank_state() 在指定的入口地址处构造一个“空状态”,该的数据都是未初始化的,当使用未初始化的的数据时,一个不受约束的符号值将会被返回。
    call_state() 构造一个已经准备好执行某个函数的状态
    full_init_state() 构造一个已经执行过所有与需要执行的初始化函数,并准备从函数入口点执行的状态。比如,共享库构造函数(constructor)或预初始化器。当这些执行完之后,程序将会跳到入口点。
  • passwd0 = claripy.BVS('passwd0',passwd_size_in_bits)

    构造了一个名为passwd0的符号位变量,符号位向量是angr用于将符号值注入程序的数据类型。这些将是angr将解决的方程式的“x”,也就是约束求解时的自变量。可以通过 BVV(value,size)BVS( name, size) 接口创建位向量,也可以用 FPVFPS 来创建浮点值和符号。

  • initial_state.regs.eax = passwd0

    将符号变量passwd0注入eax寄存器中。

  • solution_state.solver.eval(passwd0)

    返回的是passwd0的一个十进制解,用format将其16进制化。这里:

    • solver.eval(expression):将会解出expression一个可行解。
    • solver.eval_one(expression):将会给出expression的可行解,若有多个可行解,则抛出异常。
    • solver.eval_upto(expression, n):将会给出最多n个可行解,如果不足n个就给出所有的可行解。
    • solver.eval_exact(expression, n):将会给出n个可行解,如果解的个数不等于n个,将会抛出异常。
    • solver.min(expression):将会给出最小可行解。
    • solver.max(expression):将会给出最大可行解。

    solution_state.solver.eval与state.posix.dumps的区别:

    state.posix.dumps一般在未定义BVS时打印我们想要的结果,而solution_state.solver.eval一般用于定义了BVS,打印BVS的结果。

0x02 输入参数存放在栈上

题目:04_angr_symbolic_stack

直接拖进IDA进行分析,可以看的handle_user为主逻辑,并且scanf读取的数据是直接写在栈上的,那么我们注入寄存器的方法就是失效了,现在尝试直接注入栈空间。

image-20230315153909020

同样设置初始化状态为scanf之后,即0x08048697,这里有小伙伴可能会有疑问了,scanf后面明明是0x08048694为什么是要使用下一条指令呢,原因是add esp,10h是在清理scanf的栈帧,可能会对我们的esp造成影响,因此我们这里直接跳过了scanf栈帧的清理,否则还需要对esp进行相应的处理。

image-20230315192238892

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 sys
import claripy

def Run():
bin_path = "./04_angr_symbolic_stack"
project = angr.Project(bin_path, auto_load_libs=False)
# initial_state = project.factory.entry_state()
start_address = 0x08048697
initial_state = project.factory.blank_state(addr=start_address)

initial_state.regs.ebp = initial_state.regs.esp

passwd_size_in_bits = 32
passwd0 = claripy.BVS('passwd0', passwd_size_in_bits)
passwd1 = claripy.BVS('passwd1', passwd_size_in_bits)

# 构造主函数栈帧
padding_length_in_bytes = 0x8
initial_state.regs.esp -= padding_length_in_bytes
initial_state.stack_push(passwd0)
initial_state.stack_push(passwd1)

simulation = project.factory.simgr(initial_state)

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
if b'Good Job.' in stdout_output:
return True
else:
return False

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
if b'Try again.' in stdout_output:
return True
else:
return False

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

if simulation.found:
solution_state = simulation.found[0]
solution0 = format(solution_state.solver.eval(passwd0), 'd')
solution1 = format(solution_state.solver.eval(passwd1), 'd')
solution = solution0 + " " + solution1
print("[+] Success! Solution is: {}".format(solution))
else:
raise Exception('Could not find the solution')


if __name__ == "__main__":
Run()
  • initial_state.regs.ebp = initial_state.regs.esp

    由于我们使用的是project.factory.blank_state初始化的状态,该状态内的数据都是未初始化的,并且后面执行的代码使用到了栈上的数据,因此我们需要手动构建当前状态的栈帧。

  • initial_state.regs.esp -= padding_length_in_bytes

    通过汇编代码我们可以得知:求解的passwd0passwd1分别位于栈上ebp-0xcebp-0x10位置,也就是说栈上有8个字节是被占用的,我们需要手动填充这些字节,然后将符号入变量入栈,保证汇编代码的正确性。另一种不用压栈的方法就是直接将esp移动至相应位置,直接向该地址写入符号位变量,但是下面的解法是有问题,最终输出的结果不对,我也不清楚那里的问题,望大佬告知。

    1
    2
    3
    4
    5
    6
    initial_state.regs.rbp = initial_state.regs.rsp
    passwd0_addr = initial_state.regs.esp - 0xc
    passwd1_addr = initial_state.regs.esp - 0x10
    initial_state.regs.esp -= 0x10
    initial_state.memory.store(passwd0_addr, passwd0)
    initial_state.memory.store(passwd1_addr, passwd1)

0x03 传入的参数存在全局变量区

题目:05_angr_symbolic_memory

主函数:

image-20230315200647631

这道题目解法与之前的类似,主要区别在于scanf输入的内容存储在.bss段,而.bss段默认是程序启动时,由系统自动分配的空间,因此如果我们想要跳过scanf时,就必须要将符号位变量写入至.bss段的相应位置。

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
import angr
import sys

import claripy

def Run():
bin_path = "./05_angr_symbolic_memory"
project = angr.Project(bin_path, auto_load_libs=False)

# 设置起始状态
start_address =0x08048601
initial_state = project.factory.blank_state(addr=start_address)

passwd_size_in_bits = 64
passwd0 = claripy.BVS('passwd0', passwd_size_in_bits)
passwd1 = claripy.BVS('passwd1', passwd_size_in_bits)
passwd2 = claripy.BVS('passwd2', passwd_size_in_bits)
passwd3 = claripy.BVS('passwd3', passwd_size_in_bits)

passwd0_address = 0x0A1BA1C0

initial_state.memory.store(passwd0_address,passwd0)
initial_state.memory.store(passwd0_address + 0x8, passwd1)
initial_state.memory.store(passwd0_address + 0x10, passwd2)
initial_state.memory.store(passwd0_address + 0x18, passwd3)

simulation = project.factory.simgr(initial_state)

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
if b'Good Job.' in stdout_output:
return True
else:
return False

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
if b'Try again.' in stdout_output:
return True
else:
return False

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

if simulation.found:
for state in simulation.found:
solution0 = state.solver.eval(passwd0,cast_to=bytes)
solution1 = state.solver.eval(passwd1, cast_to=bytes)
solution2 = state.solver.eval(passwd2, cast_to=bytes)
solution3 = state.solver.eval(passwd3, cast_to=bytes)
solution = solution0 + b" " + solution1 + b" " + solution2 + b" " + solution3
print("[+] Success! Solution is: {}".format(solution.decode("utf-8")))
else:
raise Exception('Could not find the solution')

if __name__ == "__main__":
Run()
  • initial_state.memory.store

    这里用到的访问方式是state.memory.storestate.memory.load,可以用来访问一段连续的内存。由于4个变量是连续存储,直接按8字节叠加即可。

    • load(addr,...): 读取指定地址的内存
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    def load(self, addr, size=None, condition=None, fallback=None, add_constraints=None, action=None, 	      endness=None, inspect=True, disable_actions=False, ret_on_segv=False):
    """
    Loads size bytes from dst.
    :param addr: The address to load from.
    :param size: The size (in bytes) of the load.
    :param condition: A claripy expression representing a condition for a conditional load.
    :param fallback: A fallback value if the condition ends up being False.
    :param add_constraints: Add constraints resulting from the merge (default: True).
    :param action: A SimActionData to fill out with the constraints.
    :param endness: The endness to load with.
    """
    • store(addr, ...): 向指定内存写入数据
    1
    2
    3
    4
    5
    6
    7
    8
    def store(self, addr, data, size=None, condition=None, add_constraints=None, endness=None, action=None,
    inspect=True, priv=None, disable_actions=False):
    """
    Stores content into memory.
    :param addr: A claripy expression representing the address to store at.
    :param data: The data to store (claripy expression or something convertable to a claripy expression).
    :param size: A claripy expression representing the size of the data to store. #大小
    ...

0x04 传入的参数存放在堆上

题目:06_angr_symbolic_dynamic_memory

image-20230315203341304

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
import angr
import sys

import claripy


def Run():
bin_path = "./06_angr_symbolic_dynamic_memory"
project = angr.Project(bin_path, auto_load_libs=False)
# initial_state = project.factory.entry_state()
start_address = 0x08048699
initial_state = project.factory.blank_state(addr=start_address)

passwd_size_in_bits = 64
passwd0 = claripy.BVS('passwd0', passwd_size_in_bits)
passwd1 = claripy.BVS('passwd1', passwd_size_in_bits)

fake_heap_address0 = 0xffffc900
pointer_to_malloc_memory_address0 = 0xabcc8a4
fake_heap_address1 = 0xffffc955
pointer_to_malloc_memory_address1 = 0xabcc8ac

initial_state.memory.store(fake_heap_address0, passwd0)
initial_state.memory.store(fake_heap_address1, passwd1)

initial_state.memory.store(pointer_to_malloc_memory_address0,
fake_heap_address0, endness=project.arch.memory_endness)
initial_state.memory.store(pointer_to_malloc_memory_address1,
fake_heap_address1, endness=project.arch.memory_endness)

simulation = project.factory.simgr(initial_state)

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
if b'Good Job.' in stdout_output:
return True
else:
return False

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
if b'Try again.' in stdout_output:
return True
else:
return False

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

if simulation.found:
for state in simulation.found:
solution0 = state.solver.eval(passwd0, cast_to=bytes)
solution1 = state.solver.eval(passwd1, cast_to=bytes)
solution = solution0+b" "+solution1
print("[+] Success! Solution is: {}".format(solution.decode("utf-8")))
else:
raise Exception('Could not find the solution')


if __name__ == "__main__":
Run()
  • initial_state.memory.store(fake_heap_address1, passwd1)

    由于我们跳过了程序初始阶段,buffer0buffer1并未进行malloc,因此我们需要手动模拟分配空间malloc的操作。initial_state.memory.store能够在内存中写入数据,因此完全可以用来模拟malloc,直接将符号位向量写入内存空间。buffer0buffer1存储的是申请到的堆内存地址,angr并没有真正“运行”二进制文件,它只是在模拟运行状态,因此它实际上不需要将内存分配到堆中,实际上可以伪造任何地址。而需要使用者做的就是选择两个地址存放的堆区地址,buffer0buffer1就是可选项。0xffffc900和0xffffc955随机伪造的地址。

  • initial_state.memory.store(pointer_to_malloc_memory_address0,fake_heap_address0, endness=project.arch.memory_endness)

    空间分配好之后,我们直接将空间地址写入buffer0buffer1,这两个变量又是在.bss,因此需要进行内存写入。.store参数endness 用于设置端序,angr默认为大端序,总共可选的值如下:

    • LE – 小端序
    • BE – 大端序
    • ME – 中间序

总结

今天简单练习了angr_ctf的前六道题,感觉angr比我想象中的要强大,但是使用angr过程中也应该注意:angr实际上是一种路径探索的方法,在处理分支时,采取统统收集的策略,因此每当遇见一个分支,angr的路径数量就会乘2,这是一种指数增长,也就是所说的路径爆炸。执行上很像BFS,一旦分支过多,angr就无法较为快速的求解了,因此在使用过程中应该尽可能对源程序进行处理,最好能够减少分支路径。

参考链接

angr初探

angr_进阶

angr_ctf

angr-doc-zh_CN

Angr入门(二)- 一些CTF的Angr分析


Angr 学习笔记
https://genioco.github.io/2023/03/15/Learn/Angr学习笔记/
作者
BadWolf
发布于
2023年3月15日
许可协议