云计算百科
云计算领域专业知识百科平台

Android SO 逆向实战:基于 Unicorn 引擎的 Native 算法模拟执行与脱机解密

封面信息图

在移动端安全与业务风控对抗中,逆向工程师最常面对的一道难关,莫过于电商 App 核心资产模块(如双 11 秒杀请求签名生成、高危接口设备指纹加密)的逆向还原。开发团队为了对抗逆向,通常会将核心 C/C++ 算法编译进 Android Native SO 库(如 libsec_sign.so),并套上高强度的商业加固外壳:双层控制流平坦化(OLLVM Flattening)、上万条由宏展开构成的虚假指令(Bogus Control Flow)、以及动态指令替换与自修改代码(Self-Modifying Code)。

面对这种动辄数万行反编译伪代码的庞然大物,如果坚持“纯手工静态还原每一个位运算、用 Python 重写一套数学逻辑”,即便是一个经验极为丰富的高级逆向专家,往往也需要消耗数周甚至整月的时间;而如果采用基于真机的 Frida RPC 挂钩方案,由于需要依赖真实的手机硬件、USB 调试线以及真机环境反检测维护,其在工业级自动化测试或高并发批量调用时不仅极其笨重,且极易因手机发热掉线而崩溃。

面对这一困境,现代高阶逆向工程催生出了一种具备降维打击能力的优雅流派——基于 Unicorn 引擎的纯脱机 CPU 模拟执行(CPU Emulation)。

我们根本不需要去关心算法内部经过了多么复杂的 OLLVM 混淆,也不需要一台真实的 Android 手机。我们直接在 PC 端的 Python 进程中,用纯软件虚拟出一个完整的 ARM64 CPU 物理仿真沙箱,直接把 SO 库中的加密函数加载进仿真内存跑一遍,直接捞取最终的计算结果!


一、 模拟执行的破局哲学:为什么 Unicorn 是混淆代码的克星?

1. 软件虚拟化 vs 算法还原
  • 传统代码还原的痛点:无论混淆器插入了多少不透明谓词、打平了多少循环,所有的混淆指令归根到底都必须在 CPU 硬件上真实执行一遍,才能得出正确的结果;
  • Unicorn 的降维破局:Unicorn Engine(由权威安全团队基于 QEMU 的微内核剥离研发)是一个纯粹的 CPU 指令集模拟器。它不包含任何操作系统的庞杂概念,只专注于忠实地执行机器指令、更新寄存器状态和内存页表。

[传统逆向还原路线]:
混淆的 ARM64 汇编 —> 艰苦的人工去平坦化 —> 还原数学逻辑 —> 手写 Python (极度耗时)

[Unicorn 模拟执行路线]:
混淆的 ARM64 汇编 —> 纯内存直接映射进 Unicorn 仿真 CPU —> 运行 20000 条指令 —> 直接读返回值!
(整个过程完全无视任何代码混淆,耗时仅需数毫秒!)


二、 脱机模拟执行的四大核心工程支柱

要让一个原本依赖 Android 操作系统(Linux 内核 + Bionic Libc + ART 虚拟机)的 Native SO 库在纯 PC 端脱机运行起来,必须在用户态完成四大环境虚拟化:

+————————————————————-+
| 支柱 1: 内存空间布局虚拟化 (Memory Layout Emulation) |
| -> 利用 uc.mem_map 分配仿真内存空间: 代码段、数据段、堆栈空间 |
+————————————————————-+
|
v
+————————————————————-+
| 支柱 2: ELF 二进制装载与重定位 (ELF Loader & Relocation) |
| -> 解析 SO 的 Program Headers,将 LOAD 段拷贝至指定基址 |
+————————————————————-+
|
v
+————————————————————-+
| 支柱 3: 寄存器环境初始化 (Register State Setup) |
| -> 设置 SP (栈顶指针)、配置 X0 (JNIEnv*)、X2 (输入参数指针) |
+————————————————————-+
|
v
+————————————————————-+
| 支柱 4: 外部系统调用与 libc 函数打桩 (Hooking & Stubbing) |
| -> 针对 malloc, memcpy, __android_log 等外部符号进行内存打桩 |
+————————————————————-+


三、 工程实战:基于 Python 与 Unicorn 还原签名算法

假设我们从 APK 中提取出了 libsecurity.so,静态逆向已经定位到核心加密函数的偏移为 0x12A40。该函数是一个纯数学哈希函数,接收两个参数:输入文本指针与文本长度。

以下展示完整的 Python 脱机模拟执行工程代码骨架:

from unicorn import *
from unicorn.arm64_const import *
from elftools.elf.elffile import ELFFile
import struct

# 1. 虚拟内存基址规划 (对齐到 4KB 页面)
SO_BASE_ADDR = 0x40000000 # SO 库加载基址
STACK_ADDR = 0x80000000 # 虚拟栈基址
STACK_SIZE = 1024 * 1024 # 1MB 栈空间
HEAP_ADDR = 0x90000000 # 虚拟堆空间基址
HEAP_SIZE = 1024 * 1024 # 1MB 堆空间

def load_and_emulate_native_sign(so_path: str, input_payload: bytes) -> str:
print(f"[*] 启动 Unicorn ARM64 模拟执行引擎…")
# 实例化 64 位 ARM 仿真引擎
uc = Uc(UC_ARCH_ARM64, UC_MODE_ARM)

# 2. 映射虚拟栈与堆
uc.mem_map(STACK_ADDR, STACK_SIZE)
uc.mem_map(HEAP_ADDR, HEAP_SIZE)
# 初始化栈指针 (SP 位于栈顶高地址,向下增长)
uc.reg_write(UC_ARM64_REG_SP, STACK_ADDR + STACK_SIZE – 0x1000)

# 3. 解析并映射 ELF 文件各个 LOAD 段
print(f"[*] 解析并映射目标 SO 文件: {so_path}")
with open(so_path, 'rb') as f:
elf = ELFFile(f)
for segment in elf.iter_segments():
if segment['p_type'] == 'PT_LOAD':
# 页面对齐计算
seg_vaddr = segment['p_vaddr']
seg_memsz = segment['p_memsz']
page_start = (SO_BASE_ADDR + seg_vaddr) & ~0xFFF
page_end = (SO_BASE_ADDR + seg_vaddr + seg_memsz + 0xFFF) & ~0xFFF
page_size = page_end – page_start

# 映射内存页并写入原始二进制数据
uc.mem_map(page_start, page_size)
uc.mem_write(SO_BASE_ADDR + seg_vaddr, segment.data())

# 4. 将输入参数写入虚拟堆内存中
input_str_addr = HEAP_ADDR + 0x100
uc.mem_write(input_str_addr, input_payload + b'\\x00')

# 为输出结果预分配一个缓冲区
output_buf_addr = HEAP_ADDR + 0x200

# 5. 配置 ARM64 函数调用寄存器参数 (遵循 AAPCS 规约)
# 假设目标函数原型: char* compute_sign(const char* input, int len, char* out_buf)
uc.reg_write(UC_ARM64_REG_X0, input_str_addr) # X0: 输入参数指针
uc.reg_write(UC_ARM64_REG_X1, len(input_payload)) # X1: 输入长度
uc.reg_write(UC_ARM64_REG_X2, output_buf_addr) # X2: 结果回写缓冲区

# 设置返回链接寄存器 LR (X30),当函数执行 ret 时跳回一个魔数地址作为终止信号
MAGIC_EXIT_ADDR = 0x50000000
uc.mem_map(MAGIC_EXIT_ADDR, 0x1000)
uc.reg_write(UC_ARM64_REG_X30, MAGIC_EXIT_ADDR)

# 6. 核心仿真启动!
target_func_addr = SO_BASE_ADDR + 0x12A40
print(f"[*] 启动机器码执行: 0x{target_func_addr:X} —-> 终止退出点: 0x{MAGIC_EXIT_ADDR:X}")

# 仿真执行,直到 PC 达到 MAGIC_EXIT_ADDR
uc.emu_start(target_func_addr, MAGIC_EXIT_ADDR, timeout=5000000, count=0)

# 7. 从输出缓冲区读取最终的加密字符串
result_bytes = uc.mem_read(output_buf_addr, 32)
final_sign = bytes(result_bytes).split(b'\\x00')[0].decode('utf-8', errors='ignore')

print(f"[+] 模拟执行成功!提取出脱机加密结果: {final_sign}")
return final_sign

# 验证调用
test_payload = b"timestamp=1791518400&device_id=sec_009"
load_and_emulate_native_sign("libsecurity.so", test_payload)


四、 高级对抗排错:未打桩系统调用与环境检测破除

在真实的复杂 SO 库中,算法往往会穿插少量的系统函数调用,模拟执行可能会报错 UC_ERR_READ_UNMAPPED(访问未映射内存):

  • JNIEnv 与 Java 回调打桩(Stubbing):若 SO 库试图调用 env->CallStaticObjectMethod 反射获取系统版本,我们只需在内存中虚拟伪造一个包含全部函数指针的 JNIInvokeInterface 结构体表,在所有函数指针位置填充指向一条空返回指令(ret)的内存页即可;
  • 反模拟器指纹破除:部分算法在执行时会读取系统时间 gettimeofday() 或文件 /proc/cpuinfo。在 Unicorn 中,我们可以利用 uc.hook_add(UC_HOOK_CODE, …) 注册指令级钩子,一旦检测到即将执行 svc #0 系统调用,直接用 Python 修改寄存器伪造合法的返回值,完美欺骗算法逻辑。

  • 五、 结语

    在现代二进制对抗的博弈棋局中,Unicorn 引擎的出现彻底重构了逆向与反逆向的成本天平。面对加固厂商精心编排的数万行代码平坦化与虚假控制流,逆向工程师不再需要深陷于逻辑迷宫,而是站在体系结构的高维视角,借力打力,以机器指令模拟机器运算。深刻理解 ARM64 指令集的内存映射与调用规范,方能在脱机环境中游刃有余地驾驭任何复杂的原生二进制逻辑,为业务风控与安全审计提供绝对降维的分析利刃。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Android SO 逆向实战:基于 Unicorn 引擎的 Native 算法模拟执行与脱机解密
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!