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
- 内存映射:
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表:
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实现有几个特点:
- 轮数被修改为0xFA
- 是对称加密,可以用相同算法解密
- 需要处理字符串长度动态分配内存的问题
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 关键技巧与注意事项
-
机器码处理:
- 需要避免模拟执行到程序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。