吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 2079|回复: 3
上一主题 下一主题
收起左侧

[Android 原创] 领域特定虚拟机DSVM指令完整还原

[复制链接]
跳转到指定楼层
楼主
jbczzz 发表于 2026-6-22 00:11 回帖奖励

领域特定虚拟机DSVM指令完整还原

副标题:一款针对 UE4 移动游戏外挂的深度混淆分析与反混淆实践
日期:2026-06-21
标签:ARM64 · Unicorn · OLLVM · DSVM · UE4 · PhysX · 反混淆 · 字节码解释器


摘要

本文记录了对 PhysX-遍历.sh(一个 ARM64 ELF 可执行文件)进行逆向分析的完整过程。该文件最初被判定为 OLLVM 控制流平坦化(CFF)重度混淆,但深入分析后揭示其真实面目是一个领域特定虚拟机(DSVM)——一套针对 Unreal Engine 4 游戏对象遍历的自定义字节码解释器。本文详述从误判到真相的推理链、Capstone + Unicorn 工具链的工程实践、790 个操作码的语义提取,以及恶意代码检测结论。


1. 引言

1.1 目标概况

PhysX.sh 基本属性:

属性
文件大小 198,496 字节(193.8 KB)
文件类型 AArch64 ELF 可执行文件(PIE),扩展名伪装为 .sh
符号状态 完全剥离(stripped)
编译器 Clang 14.0.7, Android NDK r450784d1
外部导入 76 个函数(libc + libdl + libm)
目标游戏 7 款 UE4 手游

1.2 初始判断

初始 OLLVM 检测器报告该文件含有 49 个 CFF 混淆函数(占 238 个函数的 20.6%),其中最严重的 0x14350 包含 1843 条指令和 113 个比较分支。这似乎是一个重度 OLLVM 混淆的典型案例——直到两个关键异常出现。


2. OLLVM 检测与第一道裂缝

2.1 检测方法

使用基于 Capstone 的静态分析器扫描 .text 段,对每个函数统计四个 CFF 特征:

  • cmp 指令密度:调度器中比较链的数量
  • 间接跳转(br reg):switch 分发器特征
  • 状态变量模式mov/movzw8 的频率(OLLVM 常用状态寄存器)
  • 基本块拓扑:入度异常高的分发器块

检测到的 Top 10 可疑函数:

函数地址 指令数 CMP数 间接跳转 CFF评分
0x14350 1843 113 3 10
0x13374 414 49 2 10
0x16ae8 608 39 5 10
0x23608 323 31 7 10
0x18234 1031 65 13 8
0x20664 707 42 14 8
0x1eb28 500 40 4 8
0x1f4ec 220 24 4 8
0x21a80 103 8 4 8
0x21c1c 146 11 6 8

使用项目自带的 ollvm_deobfuscator.py 进一步确认了 7 个 CFF 函数及其状态变量和调度器地址。

2.2 异常出现

func_0x12f28 的 Unicorn 模拟触发了 4,998 次 syscall——这在 OLLVM 混淆函数中不可能出现(OLLVM 只修改控制流,不引入大量系统调用)。

更关键的异常在 0x18234:该函数被检测为单个 4124 字节的巨型函数,但内部包含 12 个独立的"调度器"——标准 OLLVM 每个函数只有一个分发器。

这两处异常迫使我们重新审视整个判断。


3. DSVM 架构揭示

3.1 PAC 序言修正函数边界

ARM64 PAC(Pointer Authentication Code,ARMv8.3-A)保护的函数使用非传统序言:

paciasp                     ; PAC 认证
sub  sp, sp, #0x60          ; 分配栈空间
stp  x29, x30, [sp, #0x30]  ; 保存帧指针

扩展函数检测以包含 PAC 序言后,函数数量从 238 跃升到 2910x18234 区域实际包含 3 个独立函数:

0x18234 - 0x1828c  (88 bytes)   : VTable wrapper
0x1828c - 0x19250  (4036 bytes) : 字节码解释器主体 ★
0x19250 - 0x19308  (184 bytes)  : Cleanup helper

3.2 字节码协议发现

0x1828c 入口代码的分析发现了关键模式:

0x182ec: ldrb w11, [x8]           ; 读取首字节
0x182f4: cmp  w11, #0x67          ; 'g' = 0x67
0x182fc: ldrb w12, [x8, #1]       ; 读取第二字节
0x18300: cmp  w12, #0x73          ; 's' = 0x73
0x18308: add  x8, x8, #2          ; 消费 "gs" 前缀

; --- 操作码分发 ---
0x18330: sub  w12, w11, #0x31     ; byte - '1' = 操作码索引
0x18334: cmp  w12, #0x43          ; 上限检查
0x1833c: adrp x13, #0x9000        ; 跳转表基址
0x18348: ldrh w15, [x13, x12, lsl #1]  ; table[opcode]
0x1834c: add  x14, x14, x15, lsl #2    ; target = base + offset*4
0x18350: br   x14                      ; 分发跳转

这是经典的字节码解释器分发循环。"gs"(0x67, 0x73)是魔数前缀,后续每个字节是操作码,通过跳转表映射到处理器。

3.3 架构判定:从 CFF 到 DSVM

维度 初始误判(CFF混淆) 真实架构(DSVM)
12个调度器 多级混淆状态机 操作码分配网络
2580个"状态" 混淆状态值 合法操作码映射
80个case块 被混淆的真实块 操作码处理器
ldrh [table, idx*2] 混淆查找表 操作码→处理器跳转表
递归 bl 0x18288 混淆间接调用 递归下降解析器

3.4 12 调度器架构

解释器包含 12 个调度器,各自负责不同的操作码范围和语义域:

调度器 地址 跳转表 语义域
D1 0x18350 0x9818 主操作码表
D2 0x18390 0x9abc 字符串字段解析
D3 0x183d4 0x98a0 数组/向量字段
D4 0x1843c 0x9b26 对象引用字段
D5 0x18480 0x9afe 变换/矩阵字段
D6 0x185d8 0x9a78 标志枚举字段
D7 0x1861c 0x99fe 物理属性字段
D8 0x1865c 0x99bc 网络状态字段
D9 0x186a0 0x98c6 渲染字段
D10 0x18730 0x9a2e 碰撞检测字段
D11 0x18774 0x996a 音频/动画字段
D12 0x18818 0x991c 输入/UI 字段

3.5 DSVM vs VMP 辨析

分析过程中自然提出了"这是否为 VMProtect"的疑问。从四个维度对比:

处理器语义。 VMP 处理器是通用的(Add/Sub/Push/Pop/Jmp/Call),而本系统所有处理器都是领域特定的——ParseInt(ASCII→整数)、HandleBone(骨骼矩阵)、HandlePhysics(PhysX 坐标)、HandleAI(行为树数据)。83 次 BL 调用中,领域特定调用的占比为 98.4%。

循环结构。 VMP 使用单层平面循环(一个中央分发器),本系统实现递归下降解析——33% 的调用是递归 bl 0x18288,支持嵌套子对象遍历。

字节码格式。 VMP 是紧密二进制编码。本系统使用 "gs" 魔数 + 单字节操作码的文本式格式,rodata 段 71% 为 printable 字符。

虚拟栈。 VMP 标志性地使用虚拟栈。本系统中未检测到虚拟栈模式——不存在单个寄存器大量用作 ldr/str 基址的情况。

结论: 这是一个借鉴 VMP 架构模式(跳转表分发、字节码解释)但指令集完全领域化的自定义虚拟机,类似于 SQL 引擎与通用 CPU 的关系。


4. 操作码语义提取与还原结果

4.1 提取方法

对 12 个调度器的 53 个跳转表进行静态解析。每个跳转表条目是 16 位偏移,指向解释器函数内的 case 块。通过解析每个 case 块的指令序列(特别是 bl 调用目标),建立操作码到处理器的映射。

4.2 五大操作码类别

READ 类(331 个,41.9%)— 字节流解析
'1'-'9' → ParseInt (0x1aa6c)
    算法: value = value*10 + digit - '0'
    实现: madd 累加器循环

'L'     → ParseStruct (0x19304)    — UE4 结构体布局描述符
'T','X' → ParseTypeTag (0x174fc)   — 游戏对象类型标识
'N','s' → ReadNameTable (0x15e28)  — UE4 FName 全局池查询
INIT 类(218 个,27.6%)— 上下文初始化

InitScan(0x1a400)是最频繁调用的处理器,每次切换字段类型时设置目标缓冲区地址、字段映射表和起始偏移。

RECURSE 类(122 个,15.4%)— 递归嵌套遍历
'i', 'q' → 递归调用 parse_bytecode (bl 0x18288)
    子上下文: Component→子Component, Actor→Component
    专用递归: World / Actor / Physics / Animation / AI / Network
PROCESS 类(96 个,12.2%)— 数据计算
处理器 地址 功能
HandleBone 0x1b518 骨骼变换矩阵计算
HandleVector 0x1b3dc FVector (X,Y,Z) 解析
HandleFloat 0x1a6a4 IEEE 754 单精度浮点
HandleEnum 0x19f34 枚举值映射
HandleTransform 0x1b8e0 FTransform 组合
STORE 类(27 个,3.4%)— 结果持久化

StoreField(0x16018)写入输出结构体,LinkFields(0x1a560)建立父子关系,Finalize(0x1924c)标记解析完成。

4.3 UE4 引擎覆盖

UE4 子系统 处理器 遍历对象
PhysX 物理 HandlePhysics PxScene → PxRigidActor → PxShape
骨骼动画 HandleBone + HandleSkeletal USkeletalMeshComponent → FBoneTransform[]
世界管理 HandleWorld + HandleLevel UWorld → ULevel → AActor[]
AI 系统 HandleAI UAIController → UBehaviorTree → UBlackboard
网络复制 HandleNet AActor → FObjectReplicator
渲染 HandleRender UPrimitiveComponent → FSceneProxy
碰撞 HandleCollision FCollisionShape → FHitResult
武器系统 HandleWeapon + HandleDamage AWeapon → ProjectileComponent

4.4 解释器伪代码

parse_bytecode(ctx):
    if remaining < 2: return NULL
    if cursor[0]=='g' && cursor[1]=='s':
        cursor += 2; flag |= GS_ACTIVE

    while cursor < end:
        opcode = *cursor++
        if '1' <= opcode <= '9':   parse_integer(ctx, opcode)
        elif opcode == 'L':        parse_struct_layout(ctx)
        elif opcode == 'T':        parse_type_tag(ctx)
        elif opcode == 'N':        lookup_name_table(ctx)
        elif opcode in ('i','q'):  recurse_sub_object(ctx)
        else:                      dispatch_extended(ctx, opcode)
        if flag & PARSE_COMPLETE: break

    return ctx->result

4.5 核心 PhysX 遍历路径

UPrimitiveComponent → BodyInstance → PxRigidActor
  ├─ getGlobalPose()      → PxTransform (世界坐标)
  ├─ getNbShapes()        → int (碰撞体数量)
  └─ getShapes(buffer, n) → PxShape[]
       └─ PxShape::getLocalPose()  → 局部 PxTransform
            └─ getGeometry() → 球/盒/胶囊/凸包 参数

完整的内存偏移链:

Component + 0x0170 → FBodyInstance
BodyInstance + 0x0018 → PxRigidActor*

PxRigidActor + 0x0030 → PxTransform (缓存世界姿态)
  +0x00: q.x,  +0x04: q.y,  +0x08: q.z,  +0x0C: q.w
  +0x10: p.x,  +0x14: p.y,  +0x18: p.z
  验证: 0.5 < norm(q) < 1.5 → valid; else try +0x0040

PxRigidActor + 0x0058 → Shape 链表头 → 遍历 PxShape*
PxShape + 0x0020 → localPose (相对 Actor 的局部变换)
PxShape + 0x0030 → geometry 类型 + 参数
  +0x00: type (0=球, 2=胶囊, 3=盒, 4=凸包)
  +0x08: BOX→halfExtents / SPHERE→radius / CAPSULE→radius+halfHeight
PxShape + 0x0050 → userData (回指 UPrimitiveComponent*)

5. Unicorn 动态模拟验证

5.1 模拟器设计

构建了专用 Unicorn 全模拟器以验证静态分析结论。核心参数:

内存布局:
  ELF 映射:  0x00000000 - 0x00040000  (ALL-RWX, 绕过 BTI 权限问题)
  堆栈:      0x40000000 - 0x41000000  (16 MB)
  堆:        0x50000000 - 0x51000000  (16 MB, bump allocator)

钩子系统:
  - 76 个 PLT 函数全量模拟
  - 20+ syscall 处理(exit 抑制/mmap/write 等)
  - 全 PLT 范围钩子 (0x2cf70 - 0x2d450)
  - 基本块级执行追踪

5.2 关键 PLT 钩子

# malloc — bump allocator
alloc_addr = (self.heap_ptr + 15) & ~15
self.heap_ptr = alloc_addr + size_arg
uc.reg_write(UC_ARM64_REG_X0, alloc_addr)

# ioctl — 返回成功,避免无限重试
uc.reg_write(UC_ARM64_REG_X0, 0)

# vasprintf — 分配有效缓冲区,避免 NULL 解引用
uc.mem_write(alloc_addr, b'0\x00')
uc.mem_write(strp_ptr, struct.pack('<Q', alloc_addr))
uc.reg_write(UC_ARM64_REG_X0, 1)

5.3 执行追踪结果

使用合成字节码 "gs1a" 输入,成功追踪解释器完整执行路径(50,000 条指令,295 个唯一基本块):

0x1828c: 序言 → 0x182ec: 魔数检查 → "gs" 匹配 ✓
→ 0x18350: 操作码 '1' 分发 → 0x18354 (case_01)
→ bl ParseInt (0x1aa6c)
  → 初始化解析上下文 → ASCII→整数 madd 循环
    [x10 = x10*10 + digit - 0x30]
← 返回到 0x18360 → 主循环 → 流结束检查 → ret (正常退出)

其他输入验证一致:"gsi0" 触发递归处理器,"gsL0" 触发结构体解析,"gs12ab" 展示连续多操作码分发。


6. 技术难点与解决方案

6.1 PAC 序言导致的函数边界误判

传统 stp x29, x30 检测遗漏了 PAC 保护函数。扩展为三种序言模式后函数数从 238 增至 291:

# Pattern 1: stp x29, x30, [sp, #...]            (标准)
# Pattern 2: paciasp / sub sp, sp, #N             (PAC保护)
# Pattern 3: sub sp, sp, #N / stp x29, x30, [sp, #M]  (大栈帧)

6.2 跳转表与字符串数据的区分

rodata 段中同时包含跳转表(16 位偏移数组)和 C++ Itanium ABI demangler 名称表。解决方案:验证每个跳转表条目的目标地址是否在 .text 段内,排除指向 0x7xxx(字符串区)的条目。

6.3 多调度器状态变量追踪

12 个调度器使用不同状态变量(x9、x10、x12),同一寄存器在不同阶段持有不同语义。通过静态数据流分析追踪每个变量的定义-使用链,确定调度器间的传递关系。

6.4 外部函数依赖的模拟策略

76 个 PLT 导入函数需要合理模拟才能避免模拟器陷入循环或崩溃:

类别 函数 策略
内存 malloc/free/realloc bump allocator
I/O ioctl/system/popen 返回安全默认值
字符串 strlen/strcmp/strchr 直接在 Unicorn 内存操作
格式化 vasprintf/printf 分配虚拟缓冲区并填充最小有效值

6.5 偏移机制澄清

一个重要细节:二进制中的偏移不是 libUE4.so 偏移,而是外挂自身运行时上下文字段索引:

ctx + 0x0a40 = GWorld*     (运行时解析后存储的指针)
ctx + 0x0a48 = GNames*     
ctx + 0x0170 = BodyInstance
ctx + 0x01d8 = RootComponent*

实际的 libUE4.so 偏移(通常在 0x04000000+ 范围)不存在于 ARM 指令中的任何 MOVZ/MOVK 常量中。这与 DSVM 架构一致——外挂是通用引擎,所有游戏特定数据(GWorld 偏移、字节码程序)作为运行时配置加载。

6.6 恶意代码检测结论

对 76 个导入函数和全部内嵌字符串进行恶意行为扫描:

检测项 结果
网络通信 (socket/connect/send) 无——0 个相关导入
文件删除 (remove/unlink)
加密 (aes/rsa/sha)
提权 (su/root 字符串)
后门 (bind/accept)

可疑函数的真实用途:

  • system("pidof -s %s") — 查找游戏进程 PID
  • popen("ls -l /proc/*/exe | grep deleted") — 检测反调试
  • opendir("/proc") — 遍历进程列表
  • ioctl / mknod — 与内核外挂驱动通信
  • fopen("/proc/%d/maps") — 读取游戏内存布局

结论:纯游戏外挂程序,不具备任何恶意软件特征。 没有网络功能意味着无法外传数据或接收远程指令。


7. 总结与启示

7.1 方法论

  1. 不要急于下结论。 初始"49 个 CFF 函数"的判断如果被接受,整个分析会走错方向。关键异常(4998 次 syscall、12 个调度器在一个函数中)必须追查到底。

  2. 动静结合的必要性。 纯静态分析只能看到跳转表和分发器,纯动态模拟会陷入无限循环。静态提取跳转表 + 动态验证调度流是揭示真相的必要组合。

  3. 编译器特征即信息。 PAC 指令和 BTI 着陆点不仅是保护特性,它们提供了函数边界和间接跳转目标的精确标记,反而辅助了逆向分析。

  4. DSVM 是语义级混淆。 传统 OLLVM 混淆代码的执行逻辑,DSVM 混淆的是语义意图。面对 DSVM,理解"它在做什么"(UE4 遍历)比"它怎么做的"(操作码分发)更重要。

7.2 安全启示

该 DSVM 体现了外挂开发的工业化水平:

  • 多游戏支持:同一解释器 + 不同字节码程序 = 覆盖 7 款游戏
  • 更新便利:修改遍历逻辑只需更换 rodata 中的字节码,无需重编译
  • 分析抗性:DSVM 架构天然抵抗基于签名的检测
  • 灵活部署:字节码程序可加密/压缩存储,运行时解密加载

这种架构级别的保护手段值得游戏安全从业者重视——单纯的特征码检测或完整性校验无法防御这种灵活的解释器架构。


附录

A. 工具链与产出

相关代码在仓库https://github.com/brszzz/android-sandbox

分析工具(过程中使用到的代码):
├── physx_deobfuscator.py        # 76-PLT-hook Unicorn 全模拟器
├── trace_interpreter.py         # 字节码解释器动态追踪器
├── extract_opcodes.py           # 操作码语义自动提取器
└── malware_scan.py              # 恶意行为检测扫描器

分析产出:
├── complete_opcodes.json            # 790 操作码完整映射
├── dispatcher_analysis.json         # 53 跳转表数据
├── complete_offset_reference.md     # 99 个结构偏移参考
├── physx_memory_chain.h             # PhysX 数据链路 (C++ 头文件)
├── physx_traversal_demo.h           # 遍历 + 掩体检测 Demo
├── demo_main.cpp                    # ESP 渲染 + 掩体输出主程序
├── cover_detection_detail.cpp       # 掩体检测原理详解
└── deobfuscation_results.json       # Unicorn 执行追踪数据

B. 参考文献

  • Unreal Engine 4 Source: Engine/Source/Runtime/Engine/
  • NVIDIA PhysX SDK Documentation
  • ARM Architecture Reference Manual ARMv8-A: Pointer Authentication
  • Quarkslab: "Deobfuscating OLLVM with Unicorn" (2019)

免费评分

参与人数 3吾爱币 +4 热心值 +3 收起 理由
Tonyha7 + 2 + 1 用心讨论,共获提升!
bzdqsmmz + 1 + 1 用心讨论,共获提升!
buluo533 + 1 + 1 用心讨论,共获提升!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

沙发
yfl 发表于 2026-6-22 08:41
需要好好研究一下
3#
李佑辰 发表于 2026-6-22 09:11
4#
bachelor66 发表于 2026-6-23 09:15
留着慢慢看,学习一下,感谢分享资源                  
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-7-15 12:17

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表