首页
社区
课程
招聘
[原创]领域特定虚拟机DSVM指令完整还原
发表于: 5小时前 123

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

5小时前
123

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

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

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

PhysX.sh 基本属性:

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

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

检测到的 Top 10 可疑函数:

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

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

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

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

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

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

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

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

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

分析过程中自然提出了"这是否为 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 的关系。

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

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

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

完整的内存偏移链:

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

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

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

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

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

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

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

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

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

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

可疑函数的真实用途:

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

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

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

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

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

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

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

相关代码在仓库955K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6T1M7Y4y4*7P5Y4A6Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0V1M7$3q4F1k6r3u0G2P5l9`.`.

属性
文件大小 198,496 字节(193.8 KB)
文件类型 AArch64 ELF 可执行文件(PIE),扩展名伪装为 .sh
符号状态 完全剥离(stripped)
编译器 Clang 14.0.7, Android NDK r450784d1
外部导入 76 个函数(libc + libdl + libm)
目标游戏 7 款 UE4 手游
函数地址 指令数 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
维度 初始误判(CFF混淆) 真实架构(DSVM)
12个调度器 多级混淆状态机 操作码分配网络
2580个"状态" 混淆状态值 合法操作码映射
80个case块 被混淆的真实块 操作码处理器
ldrh [table, idx*2] 混淆查找表 操作码→处理器跳转表
递归 bl 0x18288 混淆间接调用 递归下降解析器
调度器 地址 跳转表 语义域
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 字段
处理器 地址 功能
HandleBone 0x1b518 骨骼变换矩阵计算
HandleVector 0x1b3dc FVector (X,Y,Z) 解析
HandleFloat 0x1a6a4 IEEE 754 单精度浮点
HandleEnum 0x19f34 枚举值映射
HandleTransform 0x1b8e0 FTransform 组合
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

[内核课程]《Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

收藏
免费 4
打赏
分享
最新回复 (1)
雪    币: 6261
活跃值: (7995)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
只要是vm它都会将代码的语义隐藏,通过静态是无法分析的,代码混淆与vm的区别在于,混淆、代码的语义还是在的,只是变得难以阅读,而vm是使用自定义字节码重新定义程序的指令,然后解释器执行这些字节码。这样的设计就使得vm天生就把代码的语义隐藏了的。
3小时前
0
游客
登录 | 注册 方可回帖
返回