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 在逆向工程中的典型应用包括:

  1. 算法还原:模拟执行加密/签名函数,输入测试数据获取输出结果
  2. 漏洞分析:在可控环境中复现漏洞触发路径
  3. 反混淆:模拟执行混淆代码,观察其真实行为
  4. CTF 解题:模拟执行题目中的自定义虚拟机或加密算法
  5. Firmware 分析:模拟执行物联网设备固件中的函数

Unicorn 作为底层模拟引擎,虽然使用门槛较高,但提供了无与伦比的灵活性和控制力。掌握了 Unicorn 的使用,就等于拥有了一个可以在任何架构上精确执行代码的通用工具。