0%

Unicorn 学习

Unicorn 是一个常用的模拟执行框架,能够方便地完成常用 OS 二进制文件的模拟执行。它不仅能够应用于 CTF 解题,在学术界也是重要的基础工具。目前,Unicorn 的最晚发行版本发行于2022年末,目前似乎已经停止维护,但它是多个功能更加全面的模拟器(如 qiling )的基础。因此有必要进行学习。

下面,我们通过几个代码示例学习 Unicorn Python API 的使用。

1. Hello-Unicorn

这个示例来自官方文档。

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
from unicorn import *
from unicorn.x86_const import *

# code to be emulated
X86_CODE32 = b"\x41\x4a" # INC ecx; DEC edx

# memory address where emulation starts
ADDRESS = 0x1000000

print("Emulate i386 code")
try:
# Initialize emulator in X86-32bit mode
mu = Uc(UC_ARCH_X86, UC_MODE_32)

# map 2MB memory for this emulation
mu.mem_map(ADDRESS, 2 * 1024 * 1024)

# write machine code to be emulated to memory
mu.mem_write(ADDRESS, X86_CODE32)

# initialize machine registers
mu.reg_write(UC_X86_REG_ECX, 0x1234)
mu.reg_write(UC_X86_REG_EDX, 0x7890)

# emulate code in infinite time & unlimited instructions
mu.emu_start(ADDRESS, ADDRESS + len(X86_CODE32))

# now print out some registers
print("Emulation done. Below is the CPU context")

r_ecx = mu.reg_read(UC_X86_REG_ECX)
r_edx = mu.reg_read(UC_X86_REG_EDX)
print(f'>>> ECX = 0x{r_ecx:x}')
print(f">>> EDX = 0x{r_edx:x}")

except UcError as e:
print("ERROR: %s" % e)
  • from unicorn.x86_const import *:在 unicorn 中定义有针对不同架构的枚举定义文件,其中为该架构的所有寄存器、指令赋予一个整数值便于表示。后面的UC_X86_REG_ECX即为引用 x86_const.py 中的常量。
  • Uc(UC_ARCH_X86, UC_MODE_32):实例化一个模拟器对象,这个类的构造函数只有 2 个参数,没有其他任何可选参数。只需要指定架构和运行模式即可。这里是以 x86 架构,32 位模式构建一个模拟器。
  • mem_map(ADDRESS, 2 * 1024 * 1024):为一个对象映射一块新的内存空间。这个方法有3个参数,分别为地址、长度、权限(默认为 RWX )。
  • mem_write(ADDRESS, X86_CODE32):在一个地址处写入内容。
  • reg_write(UC_X86_REG_ECX, 0x1234):为一个寄存器写入值。
  • emu_start(ADDRESS, ADDRESS + len(X86_CODE32)):开始执行模拟器,该方法有 4 个参数,分别为开始地址、结束地址、超时(默认为0)、指令数量(默认为0,为0时将执行所有可执行的指令)。
  • reg_read(UC_X86_REG_ECX):读取寄存器ecx的值。

由于 Unicorn 仅实现了最为基础的模拟仿真功能,因此它具备轻量级的优势,API 规范很少,理解起来较为简单。

2. Instruction-Hooks

这个示例选自Unicorn仓库内的测试代码。

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
from unicorn import *
from unicorn.x86_const import *

X86_CODE64_SYSCALL = b'\x0f\x05' # SYSCALL

# memory address where emulation starts
ADDRESS = 0x1000000

print("Emulate x86_64 code with 'syscall' instruction")
try:
# Initialize emulator in X86-64bit mode
mu = Uc(UC_ARCH_X86, UC_MODE_64)

# map 2MB memory for this emulation
mu.mem_map(ADDRESS, 2 * 1024 * 1024)

# write machine code to be emulated to memory
mu.mem_write(ADDRESS, X86_CODE64_SYSCALL)

def hook_syscall(mu, user_data):
rax = mu.reg_read(UC_X86_REG_RAX)
if rax == 0x100:
mu.reg_write(UC_X86_REG_RAX, 0x200)
else:
print('ERROR: was not expecting rax=%d in syscall' % rax)

# hook interrupts for syscall
mu.hook_add(UC_HOOK_INSN, hook_syscall, None, 1, 0, UC_X86_INS_SYSCALL)

# syscall handler is expecting rax=0x100
mu.reg_write(UC_X86_REG_RAX, 0x100)

try:
# emulate machine code in infinite time
mu.emu_start(ADDRESS, ADDRESS + len(X86_CODE64_SYSCALL))
except UcError as e:
print("ERROR: %s" % e)

# now print out some registers
print(">>> Emulation done. Below is the CPU context")

rax = mu.reg_read(UC_X86_REG_RAX)
print(f">>> RAX = 0x{rax:x}")

except UcError as e:
print("ERROR: %s" % e)
  • hook_add(UC_HOOK_INSN, hook_syscall, None, 1, 0, UC_X86_INS_SYSCALL):在执行过程中添加 hook。参数列表:
    • htype:hook 类型。Unicorn 可以对很多程序行为添加 Hook,包括指定指令、指定基本块、指定内存行为等。可以在 unicorn_const.py 中查看所有UC_HOOK开头的常数定义。
    • callback:回调函数,即 hook 函数。
    • user_data:用于回调函数的所有参数。
    • begin:Hook 能够触发的开始 PC。
    • end:Hook 能够触发的结束 PC。
    • arg1/arg2:与 Hook 相关的参数,如对于UC_HOOK_INSN,则只需要arg1指定特定的指令类型。

3. Snapshot

这个示例选自Unicorn仓库内的测试代码。

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
from unicorn import *
from unicorn.x86_const import *
import pickle

print("Save/restore CPU context in opaque blob")
address = 0
code = b'\x40' # inc eax
try:
# Initialize emulator
mu = Uc(UC_ARCH_X86, UC_MODE_32)

# map 8KB memory for this emulation
mu.mem_map(address, 8 * 1024, UC_PROT_ALL)

# write machine code to be emulated to memory
mu.mem_write(address, code)

# set eax to 1
mu.reg_write(UC_X86_REG_EAX, 1)

print(">>> Running emulation for the first time")
mu.emu_start(address, address+1)

print(">>> Emulation done. Below is the CPU context")
print(">>> EAX = 0x%x" %(mu.reg_read(UC_X86_REG_EAX)))
print(">>> Saving CPU context")
saved_context = mu.context_save()

print(">>> Pickling CPU context")
pickled_saved_context = pickle.dumps(saved_context)

print(">>> Running emulation for the second time")
mu.emu_start(address, address+1)
print(">>> Emulation done. Below is the CPU context")
print(">>> EAX = 0x%x" %(mu.reg_read(UC_X86_REG_EAX)))

print(">>> Unpickling CPU context")
saved_context = pickle.loads(pickled_saved_context)

print(">>> Modifying some register.")
saved_context.reg_write(UC_X86_REG_EAX, 0xc8c8)

print(">>> CPU context restored. Below is the CPU context")
mu.context_restore(saved_context)
print(">>> EAX = 0x%x" %(mu.reg_read(UC_X86_REG_EAX)))

except UcError as e:
print("ERROR: %s" % e)
  • context_save:保存当前位置的所有上下文信息,这里使用pickle库是为了将保存的内容进行序列化。
  • context_restore:将上下文信息载入到当前执行环境中。本示例表明上下文信息可以在不同次执行中相互使用。

4. 其他 API

  • mem_protect:设置某段地址的访问权限。参数有地址、长度、权限类型。
  • hook_del:删除某个 Hook。
  • mem_regions:返回当前模拟器的内存状态,返回的是一个生成器,每次调用next后获取一个内存区域的地址、长度与权限。
  • ctl_xxx:一系列方法,用于获取/设置一些配置,如内存、架构等。
  • mmio_map:映射一块用于 IO 的内存空间。包含 6 个参数:
    • address:起始地址
    • size:大小
    • read_cb:用于读的回调函数
    • user_data_read:传递给读回调函数的用户自定义数据
    • write_cb:用于写的回调函数
    • user_data_write:传递给写回调函数的用户自定义数据
  • query:查询引擎内部状态,只有 1 个参数表示查询的对象,可查询架构、处理器模式、超时时间、内存页大小,使用UC_QUERY_xxx形式传入

可以看出,Unicorn 轻量到甚至没有实现自行加载 ELF 文件的 API,更多地是用于调试。在实际使用中,它不如以其为基础的更为成熟的仿真工具好用。

下面是一位 Android 大神的 blog,其中包含了开发基于 Unicorn 的简易调试器与 Android so 库的加载器:链接