unicorn模拟执行在逆向中的妙用-以2024古剑山India Pale Ale为例
字数 1402 2025-08-22 12:23:30

Unicorn模拟执行在逆向分析中的高级应用

1. Unicorn引擎基础

Unicorn是一款轻量级、多平台、多架构的CPU模拟器框架,基于QEMU开发,支持以下架构:

  • ARM (ARM32, ARM64/ARMv8)
  • M68K
  • MIPS
  • PowerPC
  • RISC-V
  • S390x (SystemZ)
  • SPARC
  • TriCore
  • x86 (包括x86_64)

基本使用方法

from unicorn import *
from unicorn.arm64_const import *

# 初始化ARM64架构的模拟器
uc = Uc(UC_ARCH_ARM64, UC_MODE_ARM)

核心API

  1. 内存映射uc_mem_map(address, size) - 在执行前映射虚拟内存
  2. 内存读写
    • uc_mem_read(address, size) - 读取内存
    • uc_mem_write(address, code) - 写入内存
  3. Hook机制uc.hook_add(UC_HOOK_CODE, code_hook) - 添加Hook,可以访问和修改寄存器/内存

2. 实际案例分析:2024古剑山India Pale Ale

2.1 题目分析

这是一个iOS逆向题目,提供ipa文件。主要特点:

  • 通过3个init函数修改了:
    • base64表
    • RC4的key
    • 密文(key和密文都经过简单异或)

2.2 Base64换表分析

使用uEmu(基于Unicorn的IDA插件)模拟执行获取修改后的base64表:

STP Q0, Q1, [X8]       ; 原始表:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklm..."
LDP Q0, Q1, [SP,#0x90+var_70]
STP Q0, Q1, [X8,#(aAbcdefghijklmn+0x20 - 0x10000D848)] ; 新表:"ghijklmnopqrstuvwxyz0123456789+/"

关键点:修改后的表存储在X8寄存器指向的地址。

2.3 RC4算法分析

题目中的RC4实现有几个特点:

  1. 轮数被修改为0xFA
  2. 是对称加密,可以用相同算法解密
  3. 需要处理字符串长度动态分配内存的问题

RC4关键代码片段

void __usercall sub_1000057E0(__int64 *a1, __int64 *a2, std::string *a3) {
    // 初始化S盒
    do {
        v6[v7] = v7;
        ++v7;
    } while (v7 < v25 - (_BYTE *)__p);
    
    // 密钥调度算法(KSA)
    do {
        v11 = (v13 + *((char *)v16 + v10 % v14)) % v9;
        v6[v10] = v6[v11];
        v6[v11] = v12;
        ++v10;
    } while (v10 < v25 - (_BYTE *)__p);
    
    // 伪随机生成算法(PRGA)
    for (i = v17 - 1; ; --i) {
        v19 = (v19 + 1) % v22;
        v20 = (v20 + v23) % v22;
        v6[v19] = v6[v20];
        v6[v20] = v23;
        // 异或生成密文
        std::string::push_back(a3, *(_BYTE *)a1 ^ ...);
        if (!i) break;
    }
}

2.4 Unicorn模拟实现

初始化设置

from unicorn import *
from unicorn.arm64_const import *

# 初始化ARM64模拟器
uc = Uc(UC_ARCH_ARM64, UC_MODE_ARM)

# 内存映射
uc.mem_map(0x0, 0x1000)
uc.mem_map(0x100005000, 0x9000)
uc.mem_map(0x10000E000, 0x10000)
uc.mem_map(0x10001E000, 0x20000)

# 写入机器码
text = bytes.fromhex("0A0080D2E00308AA...")  # 截取的RC4算法机器码
uc.mem_write(0x10000582C, text)

# 设置密钥和密文
uc.mem_write(0x10001F000, bytes.fromhex('f6ccc8d5c9c0eec0dcedc0d7c0'))  # key
uc.mem_write(0x10001F017, b'\x0D')  # key_length
uc.mem_write(0x100020000, bytes.fromhex('f10a192a76f635cf0d87480d4749d8a42701821d331d0d66973b6658c3f5e2c6f6'))  # 密文

# 设置栈和寄存器
uc.reg_write(UC_ARM64_REG_SP, 0x10000E000)
stack_ptr = 0x10000E000
uc.mem_write(stack_ptr + 0x8, b'\x00\x00\x00\x00\x00\x00\x00\x00')  # x0
uc.mem_write(stack_ptr + 0x10, b'\xfa\x00\x00\x00\x00\x00\x00\x00')  # x8

input = 0x10001E000
key = 0x10001F000
che = 0x100020000
uc.reg_write(UC_ARM64_REG_X20, che)
uc.reg_write(UC_ARM64_REG_X21, key)
uc.reg_write(UC_ARM64_REG_X19, input)

Hook函数实现

flag = ''

def code_hook(mu: Uc, addr, size, userdata):
    global flag
    if addr == 0x1000058B8:  # 密文长度赋值点
        mu.reg_write(UC_ARM64_REG_W9, 0x21)
        mu.reg_write(UC_ARM64_REG_PC, addr + size)
    if addr == 0x100005938:  # 异或结果点
        w1 = mu.reg_read(UC_ARM64_REG_W1)
        w8 = mu.reg_read(UC_ARM64_REG_W8)
        flag += chr(w8)

uc.hook_add(UC_HOOK_CODE, code_hook)

执行模拟

uc.emu_start(0x10000582c, 0x100005954)
print(flag)  # 输出flag{45_4_105_r3v3r51n6_b361nn3r}

2.5 关键技巧与注意事项

  1. 机器码处理

    • 需要避免模拟执行到程序API,可以NOP掉相关调用
    • 示例:BL ZNSt3... 被NOP掉
  2. 内存管理

    • 对于长度大于0x17的字符串,程序会重新分配内存
    • 需要正确模拟内存分配行为
  3. 寄存器处理

    • 需要手动设置关键寄存器值
    • 示例中处理了W9寄存器(密文长度)
  4. Hook点选择

    • 选择算法关键点进行Hook
    • 示例中Hook了异或操作点获取flag字符

3. 高级技巧与最佳实践

  1. 处理无法模拟的调用

    • 识别并跳过系统/库函数调用
    • 可以手动模拟这些函数的行为
  2. 内存访问优化

    • 只映射必要的内存区域
    • 合理设置内存权限
  3. 性能考虑

    • 限制模拟执行的代码范围
    • 使用Hook减少不必要的回调
  4. 调试技巧

    • 实现内存访问Hook来跟踪数据流
    • 记录寄存器变化历史

4. 扩展工具与替代方案

  1. uEmu

    • IDA插件,基于Unicorn
    • 适合快速分析代码片段
    • 提供图形界面,操作更方便
  2. Unidbg

    • 专门用于Android/iOS模拟执行的框架
    • 内置常见库函数的模拟实现
    • 支持JNI调用等复杂场景
  3. Qiling

    • 更高级的二进制模拟框架
    • 支持完整系统调用模拟
    • 提供更丰富的API

5. 总结

Unicorn在逆向工程中非常实用,特别是当:

  • 缺少实际调试环境时
  • 需要分析特定算法片段时
  • 处理混淆/加壳代码时

关键成功因素:

  1. 对目标代码的充分理解
  2. 正确的初始状态设置(寄存器、内存)
  3. 合理的Hook策略
  4. 对模拟限制的认识和处理

通过本案例,我们展示了如何利用Unicorn模拟执行复杂的加密算法,即使在没有实际设备的情况下也能成功逆向分析出flag。

Unicorn模拟执行在逆向分析中的高级应用 1. Unicorn引擎基础 Unicorn是一款轻量级、多平台、多架构的CPU模拟器框架,基于QEMU开发,支持以下架构: ARM (ARM32, ARM64/ARMv8) M68K MIPS PowerPC RISC-V S390x (SystemZ) SPARC TriCore x86 (包括x86_ 64) 基本使用方法 核心API 内存映射 : uc_mem_map(address, size) - 在执行前映射虚拟内存 内存读写 : uc_mem_read(address, size) - 读取内存 uc_mem_write(address, code) - 写入内存 Hook机制 : uc.hook_add(UC_HOOK_CODE, code_hook) - 添加Hook,可以访问和修改寄存器/内存 2. 实际案例分析:2024古剑山India Pale Ale 2.1 题目分析 这是一个iOS逆向题目,提供ipa文件。主要特点: 通过3个init函数修改了: base64表 RC4的key 密文(key和密文都经过简单异或) 2.2 Base64换表分析 使用uEmu(基于Unicorn的IDA插件)模拟执行获取修改后的base64表: 关键点:修改后的表存储在X8寄存器指向的地址。 2.3 RC4算法分析 题目中的RC4实现有几个特点: 轮数被修改为0xFA 是对称加密,可以用相同算法解密 需要处理字符串长度动态分配内存的问题 RC4关键代码片段 2.4 Unicorn模拟实现 初始化设置 Hook函数实现 执行模拟 2.5 关键技巧与注意事项 机器码处理 : 需要避免模拟执行到程序API,可以NOP掉相关调用 示例: BL ZNSt3... 被NOP掉 内存管理 : 对于长度大于0x17的字符串,程序会重新分配内存 需要正确模拟内存分配行为 寄存器处理 : 需要手动设置关键寄存器值 示例中处理了W9寄存器(密文长度) Hook点选择 : 选择算法关键点进行Hook 示例中Hook了异或操作点获取flag字符 3. 高级技巧与最佳实践 处理无法模拟的调用 : 识别并跳过系统/库函数调用 可以手动模拟这些函数的行为 内存访问优化 : 只映射必要的内存区域 合理设置内存权限 性能考虑 : 限制模拟执行的代码范围 使用Hook减少不必要的回调 调试技巧 : 实现内存访问Hook来跟踪数据流 记录寄存器变化历史 4. 扩展工具与替代方案 uEmu : IDA插件,基于Unicorn 适合快速分析代码片段 提供图形界面,操作更方便 Unidbg : 专门用于Android/iOS模拟执行的框架 内置常见库函数的模拟实现 支持JNI调用等复杂场景 Qiling : 更高级的二进制模拟框架 支持完整系统调用模拟 提供更丰富的API 5. 总结 Unicorn在逆向工程中非常实用,特别是当: 缺少实际调试环境时 需要分析特定算法片段时 处理混淆/加壳代码时 关键成功因素: 对目标代码的充分理解 正确的初始状态设置(寄存器、内存) 合理的Hook策略 对模拟限制的认识和处理 通过本案例,我们展示了如何利用Unicorn模拟执行复杂的加密算法,即使在没有实际设备的情况下也能成功逆向分析出flag。