Unicorn 工具的入门和实际使用
Unicorn 工具的入门和实际使用
Unicorn 是一个开源的跨平台 CPU 模拟器框架,基于 QEMU 二进制翻译技术构建,支持 ARM、ARM64、x86、x64、MIPS、PowerPC、SPARC 等多种 CPU 架构。它是逆向工程、安全研究、恶意代码分析等领域中非常重要的工具之一。本文将从基础概念入手,详细介绍 Unicorn 的实际使用方法。
Unicorn 引擎架构
Unicorn 的整体架构可以分为三个核心模块:
CPU 模拟模块
CPU 模拟模块负责指令的解码和执行。它从 QEMU 中提取了 CPU 模拟的核心部分,去掉了设备模拟、操作系统模拟等无关功能,只保留纯粹的指令执行能力。这意味着 Unicorn 可以精确模拟 CPU 的行为,但不会模拟任何外设或系统调用。
支持的架构包括:
- ARM:ARM32(ARM 和 Thumb 指令集)
- ARM64(AArch64)
- x86:32 位和 64 位
- MIPS:MIPS32 和 MIPS64(大端和小端)
- PowerPC、SPARC、M68K 等其他架构
内存管理模块
Unicorn 提供了独立的虚拟内存空间,支持内存映射(map)、读写和保护属性设置。内存管理的 API 与操作系统的 mmap/mprotect 类似,允许精确控制内存区域的权限:
UC_PROT_READ:可读UC_PROT_WRITE:可写UC_PROT_EXEC:可执行UC_PROT_ALL:全部权限
中断处理模块
Unicorn 通过回调机制处理中断和系统调用。当模拟的代码执行到中断指令(如 ARM 的 SVC)时,Unicorn 会调用用户注册的中断回调函数,允许用户自行决定如何处理。
安装和环境配置
Python 安装
pip install unicorn
C/C++ 安装
# 克隆仓库
git clone https://github.com/unicorn-engine/unicorn.git
cd unicorn
# 编译
make -j$(nproc)
sudo make install
验证安装
from unicorn import *
from unicorn.arm_const import *
print(f"Unicorn 版本: {uc_version()}")
# 输出: Unicorn 版本: (2, 0, 1)
API 介绍
Unicorn 提供的 API 可以分为以下几类:
引擎管理
# 创建模拟器实例
# 参数1: CPU 架构 (UC_ARCH_ARM, UC_ARCH_ARM64, UC_ARCH_X86 等)
# 参数2: CPU 模式 (UC_MODE_ARM, UC_MODE_THUMB, UC_MODE_32, UC_MODE_64 等)
uc = Uc(UC_ARCH_ARM, UC_MODE_ARM)
# 释放资源
del uc
内存操作
# 映射内存区域
# 参数: 起始地址, 大小, 权限
uc.mem_map(0x1000, 0x1000, UC_PROT_ALL)
# 写入内存
uc.mem_write(0x1000, b'\x01\x02\x03\x04')
# 读取内存
data = uc.mem_read(0x1000, 4) # 返回 bytes 对象
print(data.hex()) # 输出: 01020304
# 取消内存映射
uc.mem_unmap(0x1000, 0x1000)
寄存器操作
# 写入寄存器
uc.reg_write(UC_ARM_REG_R0, 0x12345678)
uc.reg_write(UC_ARM_REG_SP, 0x20000)
# 读取寄存器
r0_val = uc.reg_read(UC_ARM_REG_R0)
sp_val = uc.reg_read(UC_ARM_REG_SP)
# 批量读写寄存器
regs = [UC_ARM_REG_R0, UC_ARM_REG_R1, UC_ARM_REG_R2]
values = uc.reg_read_batch(regs)
执行控制
# 开始执行
# 参数: 起始地址, 结束地址, 可选(timeout, count)
uc.emu_start(begin_addr, until_addr)
# 带超时执行(单位:微秒)
uc.emu_start(0x1000, 0x2000, timeout=5*UC_SECOND_SCALE)
# 限制指令执行数量
uc.emu_start(0x1000, 0x2000, count=1000)
# 停止执行
uc.emu_stop()
基本使用流程
Unicorn 的典型使用流程为:初始化引擎 → 映射内存 → 写入代码和数据 → 设置寄存器 → 执行 → 读取结果。
示例:执行 ARM 汇编代码
下面通过一个简单的例子演示完整流程——计算两个数的和:
from unicorn import *
from unicorn.arm_const import *
# 要执行的 ARM 汇编代码
# MOV R0, #5
# MOV R1, #3
# ADD R2, R0, R1
# BX LR
ARM_CODE = b'\x05\x00\xa0\xe3\x03\x10\xa0\xe3\x02\x20\x80\xe0\x1e\xff\x2f\xe1'
# 内存地址
ADDRESS = 0x10000
# 1. 创建 ARM 模拟器
uc = Uc(UC_ARCH_ARM, UC_MODE_ARM)
# 2. 映射内存(代码 + 栈)
uc.mem_map(ADDRESS, 0x1000) # 代码区 4KB
uc.mem_map(0x20000, 0x1000) # 栈区 4KB
# 3. 写入代码
uc.mem_write(ADDRESS, ARM_CODE)
# 4. 设置栈指针和链接寄存器
uc.reg_write(UC_ARM_REG_SP, 0x20000 + 0x800) # 栈顶
uc.reg_write(UC_ARM_REG_LR, 0x99999) # 返回地址
# 5. 执行代码
try:
uc.emu_start(ADDRESS, 0x99999)
# 6. 读取结果
r2 = uc.reg_read(UC_ARM_REG_R2)
print(f"计算结果 (R2): {r2}") # 输出: 8
except UcError as e:
pc = uc.reg_read(UC_ARM_REG_PC)
print(f"执行错误 at PC={pc:#x}: {e}")
模拟 ARM/ARM64 代码
ARM32 模拟
ARM32 支持 ARM(32位指令)和 Thumb(16/32位混合指令)两种模式:
# ARM 模式
uc_arm = Uc(UC_ARCH_ARM, UC_MODE_ARM)
# Thumb 模式
uc_thumb = Uc(UC_ARCH_ARM, UC_MODE_THUMB)
# Thumb 模式下执行代码时,PC 的最低位需要设为1
uc_thumb.reg_write(UC_ARM_REG_PC, code_addr | 1)
ARM64 模拟
ARM64(AArch64)使用独立的架构常量和寄存器定义:
from unicorn.arm64_const import *
uc = Uc(UC_ARCH_ARM64, UC_MODE_ARM)
# AArch64 寄存器名称
# X0-X30, SP, PC, NZCV 等
# 写入 64 位寄存器
uc.reg_write(UC_ARM64_REG_X0, 0x123456789ABCDEF0)
# 写入 32 位子寄存器(W0 = X0 的低32位)
uc.reg_write(UC_ARM64_REG_W0, 0x12345678)
# 设置栈指针
uc.reg_write(UC_ARM64_REG_SP, 0x80000)
ARM64 示例——调用一个简单的函数:
from unicorn import *
from unicorn.arm64_const import *
# AArch64: MOV X0, #100; RET
ARM64_CODE = b'\x40\x00\x80\xd2\xc0\x03\x5f\xd6'
ADDRESS = 0x40000
uc = Uc(UC_ARCH_ARM64, UC_MODE_ARM)
uc.mem_map(ADDRESS, 0x1000)
uc.mem_map(0x80000, 0x1000)
uc.mem_write(ADDRESS, ARM64_CODE)
uc.reg_write(UC_ARM64_REG_SP, 0x80000 + 0x800)
uc.reg_write(UC_ARM64_REG_LR, 0x99999)
uc.emu_start(ADDRESS, 0x99999)
x0 = uc.reg_read(UC_ARM64_REG_X0)
print(f"X0 = {x0}") # 输出: 100
断点和单步调试
代码断点
Unicorn 支持通过 hook 机制实现断点功能。通过注册指令执行回调,在执行到指定地址时中断:
# 设置断点
breakpoints = {0x1008, 0x1010}
def hook_code(uc, address, size, user_data):
if address in breakpoints:
print(f"[断点] PC={address:#x}")
# 打印寄存器状态
for reg_name, reg_id in [('R0', UC_ARM_REG_R0),
('R1', UC_ARM_REG_R1)]:
val = uc.reg_read(reg_id)
print(f" {reg_name} = {val:#x}")
# 停止执行
uc.emu_stop()
uc.hook_add(UC_HOOK_CODE, hook_code)
单步调试
单步执行通过设置 count=1 的方式实现,每次只执行一条指令:
def single_step_debug(uc, start, end):
"""单步执行并打印每条指令"""
pc = start
def hook_step(uc, address, size, user_data):
nonlocal pc
# 读取当前指令
code = uc.mem_read(address, size)
print(f"[{address:#x}] {code.hex()}")
# 打印关键寄存器
r0 = uc.reg_read(UC_ARM_REG_R0)
r1 = uc.reg_read(UC_ARM_REG_R1)
print(f" R0={r0:#x}, R1={r1:#x}")
uc.hook_add(UC_HOOK_CODE, hook_step)
uc.emu_start(start, end)
内存访问断点
可以 hook 内存的读写操作,监控特定内存区域的访问:
# 监控对特定地址的内存写入
WATCH_ADDR = 0x20000
def hook_mem_write(uc, access, address, size, value, user_data):
if address == WATCH_ADDR:
print(f"[内存写入] 地址={address:#x}, 大小={size}, 值={value:#x}")
uc.hook_add(UC_HOOK_MEM_WRITE, hook_mem_write)
# 监控内存读取
def hook_mem_read(uc, access, address, size, value, user_data):
if 0x1000 <= address < 0x2000:
print(f"[内存读取] 地址={address:#x}, 大小={size}")
uc.hook_add(UC_HOOK_MEM_READ, hook_mem_read)
代码注入技术
代码注入是在模拟执行过程中,动态修改或插入代码的技术。这在逆向分析中非常有用,可以用来绕过检测逻辑、修改函数行为等。
内联 hook
def hook_target_function(uc, address, size, user_data):
"""hook 指定函数,修改其行为"""
print(f"已进入目标函数 {address:#x}")
# 修改函数的参数
uc.reg_write(UC_ARM_REG_R0, 0xDEADBEEF)
# 修改返回地址,跳过函数原始逻辑
# 让函数直接返回到我们的代码
uc.reg_write(UC_ARM_REG_PC, replacement_code_addr)
uc.hook_add(UC_HOOK_CODE, hook_target_function,
begin=0x1000, end=0x1004)
修改执行流程
# 在原始代码中插入跳转指令
# ARM: LDR PC, [PC, #0] 然后放目标地址
def redirect_execution(uc, from_addr, to_addr):
"""将 from_addr 的执行重定向到 to_addr"""
# B to_addr (相对跳转)
offset = (to_addr - from_addr - 8) >> 2
offset &= 0x00FFFFFF
branch_instr = 0xEA000000 | offset
uc.mem_write(from_addr, branch_instr.to_bytes(4, 'little'))
# 示例:将 0x1000 处的执行重定向到 0x2000
redirect_execution(uc, 0x1000, 0x2000)
插入 shellcode
# 在空闲内存区域写入自定义代码
SHELLCODE_ADDR = 0x70000
# 自定义 ARM 代码:修改 R0 后返回
# MOV R0, #0x42; BX LR
shellcode = b'\x42\x00\xa0\xe3\x1e\xff\x2f\xe1'
uc.mem_write(SHELLCODE_ADDR, shellcode)
# 将原始调用重定向到 shellcode
redirect_execution(uc, target_func_addr, SHELLCODE_ADDR)
反汇编引擎集成(Capstone)
Unicorn 本身不提供反汇编功能,但可以与 Capstone 反汇编引擎完美配合,实现实时的反汇编输出。
集成方法
from capstone import *
# 创建 Capstone 反汇编器
md = Cs(CS_ARCH_ARM, CS_MODE_ARM)
md.detail = True # 启用详细信息
# 在 hook 中实时反汇编
def hook_code_disasm(uc, address, size, user_data):
code = uc.mem_read(address, size)
# 反汇编当前指令
for insn in md.disasm(bytes(code), address):
print(f" {insn.address:#x}:\t{insn.mnemonic}\t{insn.op_str}")
break
uc.hook_add(UC_HOOK_CODE, hook_code_disasm)
ARM64 集成
from capstone import *
md64 = Cs(CS_ARCH_ARM64, CS_MODE_ARM)
def hook_aarch64(uc, address, size, user_data):
code = bytes(uc.mem_read(address, size))
for insn in md64.disasm(code, address):
print(f" {insn.address:#010x}:\t{insn.mnemonic}\t{insn.op_str}")
break
uc.hook_add(UC_HOOK_CODE, hook_aarch64)
完整调试器示例
将断点、单步、反汇编、寄存器监控整合到一个完整的调试器中:
from unicorn import *
from unicorn.arm_const import *
from capstone import *
class MiniDebugger:
def __init__(self, uc, arch=CS_ARCH_ARM, mode=CS_MODE_ARM):
self.uc = uc
self.cs = Cs(arch, mode)
self.breakpoints = set()
self.step_mode = False
def add_breakpoint(self, addr):
self.breakpoints.add(addr)
def hook_code(self, uc, address, size, user_data):
# 反汇编当前指令
code = bytes(uc.mem_read(address, size))
for insn in self.cs.disasm(code, address):
disasm = f"{insn.mnemonic}\t{insn.op_str}"
break
# 断点检查
if address in self.breakpoints:
print(f"[BP] {address:#x}: {disasm}")
self.step_mode = True
# 单步模式
if self.step_mode:
print(f"[STEP] {address:#x}: {disasm}")
# 打印关键寄存器
r0 = uc.reg_read(UC_ARM_REG_R0)
r1 = uc.reg_read(UC_ARM_REG_R1)
print(f" R0={r0:#x} R1={r1:#x} SP={uc.reg_read(UC_ARM_REG_SP):#x}")
# 使用调试器
uc = Uc(UC_ARCH_ARM, UC_MODE_ARM)
debugger = MiniDebugger(uc)
debugger.add_breakpoint(0x1008)
uc.hook_add(UC_HOOK_CODE, debugger.hook_code)
实际应用场景
Unicorn 在逆向工程中的典型应用包括:
- 算法还原:模拟执行加密/签名函数,输入测试数据获取输出结果
- 漏洞分析:在可控环境中复现漏洞触发路径
- 反混淆:模拟执行混淆代码,观察其真实行为
- CTF 解题:模拟执行题目中的自定义虚拟机或加密算法
- Firmware 分析:模拟执行物联网设备固件中的函数
Unicorn 作为底层模拟引擎,虽然使用门槛较高,但提供了无与伦比的灵活性和控制力。掌握了 Unicorn 的使用,就等于拥有了一个可以在任何架构上精确执行代码的通用工具。