日二节奏跳过自检

Table Of Contents

  1. 1. 概述
  2. 2. State Machine 架构
    1. 2.1. 整体结构
    2. 2.2. State Machine Base Class
  3. 3. 分析方法
    1. 3.1. Step 1:字符串
    2. 3.2. Step 2:xrefs 分析
    3. 3.3. Step 3:Constructor 追踪
    4. 3.4. Step 4:Copyright 定位
  4. 4. Patch 分析
    1. 4.1. Patch 1:PowerOn State Count
    2. 4.2. Patch 2:PowerOnTest Factory Registration NOP
    3. 4.3. Patch 3:Initialize State Count
    4. 4.4. Patch 4:Initialize Factory Registrations NOP
    5. 4.5. Patch 5:Copyright Bypass
  5. 5. 验证与测试
    1. 5.1. 静态验证
    2. 5.2. 动态测试
  6. 6. 技术附录
    1. 6.1. A. Patch 数据
    2. 6.2. B. State Machine Vtable 结构
    3. 6.3. C. Factory Registration 机制
    4. 6.4. D. Patch 应用代码
    5. 6.5. E. 版本可移植性
    6. 6.6. F. 已知局限性
  7. 7. 总结
  8. 8. 鸣谢

概述

所有分析基于 X-VERSE-X 的 chusanApp.exebase address 0x400000

仅供学习参考

State Machine 架构

整体结构

CHUNITHM 用了一套层级式的 state machine 来管理启动流程

Root StateMachine
├─ State 0: PowerOn
  ├─ State 0: PowerOnLoad (XML asset loading)
  └─ State 1: PowerOnTest (hardware diagnostics)
└─ State 1: Initialize  
   ├─ State 0: InitializeMecha (device tests)
   └─ State 1: InitializeCollab (network + dist server)

State Machine Base Class

函数StateMachineBase_ctor @ 0xA33E10

反编译签名长这样

StateMachineBase* __thiscall StateMachineBase_ctor(
    StateMachineBase* this,
    int parentSM,
    int stateCount  // ← the key parameter
)

核心逻辑反编译结果

this->field_5 = parentSM;
this->field_6 = stateCount;  // Stored at this+0x18
// ... initialization of factory registry ...

从 xref 推断出的 state transition 逻辑

bool StateMachine::IsComplete() {
    return (this->currentState >= this->stateCount);
}

currentState >= stateCount这个 SM 就会向它的 parent 发送完成信号然后把控制权交出去 整个 bypass 的核心就建立在这个简单的判断上


分析方法

Step 1字符串

第一步是在 IDA Pro 里搜索已知的 state 名称字符串目的是找到 state machine 的 constructor 和 factory registration 的位置搜出来的结果

0x18E9B18: "PowerOnTest"
0x18E9BBC: "InitializeMecha"  
0x18E9BD0: "InitializeCollab"
0x1C3A200: ".?AV?$FactoryConstructor@VPowerOnTest@projGame@@..."
0x1C3A328: ".?AV?$FactoryConstructor@VInitializeMecha@projGame@@..."
0x1C3A398: ".?AV?$FactoryConstructor@VInitializeCollab@projGame@@..."

RTTI 字符串暴露了 FactoryConstructor<StateClass> 的 template 实例化这些都是在 parent SM 构建时注册进去的

Step 2xrefs 分析

对字符串 "PowerOnTest" 做 xref0xD9D5B3 附近找到了用法

d9d5b3  push    1              ; state index
d9d5b5  mov     ecx, esi       ; this = PowerOn SM
d9d5b7  call    sub_44C05A     ; factory registration function

push <index>; mov ecx, <sm>; call <register_func> 这个 pattern 就是 factory registration 的特征 —— 每个 child state 在构建期间都会把自己注册到 parent SM 里

Step 3Constructor 追踪

0xD9D5B3 往上追找到了 parent 函数 PowerOnSM_ctor @ 0xD9D4400xD9D46A 处的 constructor prologue

d9d46a  push    2              ; ← stateCount = 2
d9d46c  push    [ebp+arg_0]    ; parentSM
d9d46f  call    j_StateMachineBase_ctor

反编译出来是这样

int __thiscall PowerOnSM_ctor(int this, int a2, int a3) {
    j_StateMachineBase_ctor(a2, 2);  // ← stateCount hardcoded as immediate
    *this = &earth::StateMachine<projGame::PowerOn>::`vftable';
    // ... vtable setup, member initialization ...
    return this;
}

state count 是以 immediate value 的形式 push 进去的push 2直接改这一个字节就能在不碰任何逻辑的情况下改变 SM 的终止条件 这是整个方案的基础发现

Step 4Copyright 定位

其实这个自己并没有找到而是参照了 tangent90 大佬的 X-VERSE 的跳过版权 patch然后 sigscan 了一下来找到了 X-VERSE-X 对应的上一层 xref 和上下文

反编译上下文

char __thiscall InitializeCollab_Execute(_DWORD* this) {
    // ... device checks ...
    if (!IsCopyrightWarningAcknowledged_thunk()) {
        return 0;  // Block progression
    }
    // ... proceed with initialization ...
    this[5] = 2;  // Transition to next state
    return 1;
}

顺着 thunk 找到目标函数 @ 0x7D4CA0

7d4ca0  xor     al, al    ; return 0
7d4ca2  retn
char IsCopyrightWarningAcknowledged() {
    return 0;  // Always returns "not acknowledged"
}

这是个永远返回 false 的 stub强制让对话框显示出来 直接 patch 它就行


Patch 分析

Patch 1PowerOn State Count

Patch0xD9D46BPowerOnSM_ctor原始字节 6A 02push 2改成 6A 01push 1

; BEFORE:
d9d46a  push    2              ; stateCount = 2 (PowerOnLoad, PowerOnTest)
d9d46c  push    [ebp+arg_0]
d9d46f  call    j_StateMachineBase_ctor

; AFTER:
d9d46a  push    1              ; stateCount = 1 (PowerOnLoad only)
d9d46c  push    [ebp+arg_0]
d9d46f  call    j_StateMachineBase_ctor

PowerOnLoadstate 0执行完后currentState 自增到 1条件 1 >= 1 成立SM 发出完成信号PowerOnTeststate 1永远不会执行 state transition 逻辑检查的是 >= 而不是 ==所以把 count 设成 1state 1 就直接不可达了


Patch 2PowerOnTest Factory Registration NOP

Patch0xD9D5B3PowerOn_ctor原始 9 字节 6A 01 8B CE E8 9E EA 6A FF 全替换成 NOP

; BEFORE:
d9d5b3  push    1              ; state index = 1
d9d5b5  mov     ecx, esi       ; this = PowerOn SM
d9d5b7  call    sub_44C05A     ; RegisterStateFactory(1, PowerOnTest)
d9d5bc  mov     eax, esi

; AFTER:
d9d5b3  nop                    ; 9 bytes of NOP
d9d5bc  mov     eax, esi

这个 patch 和 Patch 1 是冗余的但作为 defense-in-depth 存在 —— 万一 state count 检查失效缺失的 factory 也能阻止执行


Patch 3Initialize State Count

Patch0xD9DF5BInitializeSM_ctor6A 02 改成 6A 00

; BEFORE:
d9df5a  push    2              ; stateCount = 2 (InitializeMecha, InitializeCollab)
d9df5c  push    [ebp+arg_0]
d9df5f  call    j_StateMachineBase_ctor

; AFTER:
d9df5a  push    0              ; stateCount = 0 (no states)
d9df5c  push    [ebp+arg_0]
d9df5f  call    j_StateMachineBase_ctor

stateCount = 0 的时候初始 currentState = 0条件 0 >= 0 立刻成立SM 不执行任何 state 直接发出完成信号 InitializeMecha 和 InitializeCollab 全部跳过这里用 0 而不是 1是因为设成 0 连 state 0 都不进连第一个 state 的初始化开销都省了


Patch 4Initialize Factory Registrations NOP

Patch0xD9E0A8Initialize_ctor连续 18 字节全 NOP

; BEFORE:
d9e0a8  push    0              ; state index = 0
d9e0aa  mov     ecx, esi
d9e0ac  call    j_InitializeMecha_factoryReg    ; 9 bytes total
d9e0b1  push    1              ; state index = 1
d9e0b3  mov     ecx, esi
d9e0b5  call    j_InitializeCollab_factoryReg   ; 9 bytes total
d9e0ba  mov     eax, esi

; AFTER:
d9e0a8  nop                    ; 18 bytes of NOP
d9e0ba  mov     eax, esi

两个 factory registration 恰好相邻所以一次性 NOP 掉 18 字节就能同时搞定 InitializeMecha 和 InitializeCollab同样是和 Patch 3 冗余的 defense-in-depth


Patch 5Copyright Bypass

Patch0x7D4CA0函数 IsCopyrightWarningAcknowledged32 C0xor al, al改成 B0 01mov al, 1就 2 字节

; BEFORE:
7d4ca0  xor     al, al         ; AL = 0 (false)
7d4ca2  retn

; AFTER:
7d4ca0  mov     al, 1          ; AL = 1 (true)
7d4ca2  retn

验证与测试

静态验证

指令长度合计

Patch 1:  1 字节  (0xD9D46B: 0201)
Patch 2:  9 字节  (0xD9D5B3: push+mov+call → NOP×9)
Patch 3:  1 字节  (0xD9DF5B: 0200)
Patch 4: 18 字节  (0xD9E0A8: 2×(push+mov+call) → NOP×18)
Patch 5:  2 字节  (0x7D4CA0: xor al,al → mov al,1)

所有被 NOP 掉的区域都包含完整的 call 指令因为整条指令都被替换了不需要另外修 relative offset所有 patch 也都是自然对齐的没有指令跨越 patch 边界的情况

动态测试

结果PowerOn SM 在 PowerOnLoadstate 0完成后直接结束Initialize SM 立刻完成没有硬件测试对话框没有版权对话框游戏直接跳到主菜单没有观察到任何 crashstate machine 逻辑对 stateCount = 0stateCount = 1 的情况都能优雅地处理


技术附录

A. Patch 数据

Patch 1 @ 0xD9D46B:
  Original: 02
  Patched:  01

Patch 2 @ 0xD9D5B3:
  Original: 6A 01 8B CE E8 9E EA 6A FF
  Patched:  90 90 90 90 90 90 90 90 90

Patch 3 @ 0xD9DF5B:
  Original: 02
  Patched:  00

Patch 4 @ 0xD9E0A8:
  Original: 6A 00 8B CE E8 DC C5 6A FF 6A 01 8B CE E8 B3 BB 67 FF
  Patched:  90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90

Patch 5 @ 0x7D4CA0:
  Original: 32 C0
  Patched:  B0 01

B. State Machine Vtable 结构

PowerOn SM vtable @ 0x18D3xxx

struct StateMachine_PowerOn_vtable {
    void* dtor;
    void* Execute;
    void* OnEnter;
    void* OnExit;
    void* GetStateName;
    // ... additional virtual methods ...
};

State 执行流

while (!sm->IsComplete()) {
    sm->vtable->Execute(sm);
    sm->currentState++;
}

C. Factory Registration 机制

sub_44C05Athunk to sub_D9D190负责把一个 state factory 注册到 parent SM 的 factory registry 里推断出来的签名

void __thiscall RegisterStateFactory(
    StateMachine* parentSM,
    int stateIndex,
    FactoryConstructor* factory
);

效果是在 parentSM->factoryRegistry[stateIndex] 里添加一个条目用于按需实例化 child state

D. Patch 应用代码

void ApplyEarlyPatches() {
    DWORD oldProtect;
    
    // Patch 1: PowerOn state count 2 → 1
    VirtualProtect((void*)0xD9D46B, 1, PAGE_EXECUTE_READWRITE, &oldProtect);
    *(BYTE*)0xD9D46B = 0x01;
    VirtualProtect((void*)0xD9D46B, 1, oldProtect, &oldProtect);
    
    // Patch 2: NOP PowerOnTest factory registration
    VirtualProtect((void*)0xD9D5B3, 9, PAGE_EXECUTE_READWRITE, &oldProtect);
    memset((void*)0xD9D5B3, 0x90, 9);
    VirtualProtect((void*)0xD9D5B3, 9, oldProtect, &oldProtect);
    
    // Patch 3: Initialize state count 2 → 0
    VirtualProtect((void*)0xD9DF5B, 1, PAGE_EXECUTE_READWRITE, &oldProtect);
    *(BYTE*)0xD9DF5B = 0x00;
    VirtualProtect((void*)0xD9DF5B, 1, oldProtect, &oldProtect);
    
    // Patch 4: NOP both Initialize factory registrations
    VirtualProtect((void*)0xD9E0A8, 18, PAGE_EXECUTE_READWRITE, &oldProtect);
    memset((void*)0xD9E0A8, 0x90, 18);
    VirtualProtect((void*)0xD9E0A8, 18, oldProtect, &oldProtect);
    
    // Patch 5: Copyright check xor al,al → mov al,1
    VirtualProtect((void*)0x7D4CA0, 2, PAGE_EXECUTE_READWRITE, &oldProtect);
    *(BYTE*)0x7D4CA0 = 0xB0;
    *(BYTE*)0x7D4CA1 = 0x01;
    VirtualProtect((void*)0x7D4CA0, 2, oldProtect, &oldProtect);
}

DllMainDLL_PROCESS_ATTACH 里调用在任何游戏代码执行之前VirtualProtect 临时把内存页标记为可写操作完再还原原来的保护属性符合 DEP 的要求

E. 版本可移植性

对于未来版本有三种移植思路基于字符串的 AOB scan基于 pattern 的 scan以及 signature 验证

DWORD FindPowerOnCtor() {
    // Search for "PowerOn" string
    DWORD strAddr = FindString("PowerOn");
    // Find xrefs to string
    DWORD* xrefs = GetXrefs(strAddr);
    // Locate constructor via xref analysis
    return LocateConstructor(xrefs);
}
// Pattern: push 2; push [ebp+arg]; call <ctor>
BYTE pattern[] = {0x6A, 0x02, 0xFF, 0x75, '?', 0xE8, '?', '?', '?', '?'};
DWORD addr = AOBScan(pattern, sizeof(pattern));
bool ValidatePowerOnCtor(DWORD addr) {
    // Verify vtable assignment follows constructor call
    if (*(BYTE*)(addr + 5) != 0x8B) return false;  // mov ecx, esi
    if (*(BYTE*)(addr + 10) != 0xC7) return false; // mov [esi], vtable
    return true;
}

F. 已知局限性

所有 patch 用的都是硬编码的绝对地址游戏更新后需要重新分析Patch 假设启动流程正常进行如果游戏在运行时尝试重新进入启动 state比如按 TEST行为是 undefined 的

此外跳过的硬件测试可能会掩盖真实的硬件问题

另外NOP 掉 factory registration 会阻止 state 实例化但不会阻止 constructor 里其他初始化代码的执行如果 constructor 在 factory registration 之外还有 side effect那些 side effect 仍然会发生


总结

修改 control flow 的 metadatastate count比 patch control flow 逻辑本身jumpcall更可靠State machine 自己的 completion check 反而成了 bypass 的工具

鸣谢

  • tangent90 协助我了很多前备知识
  • CaochengQWQ 协助了很多测试和游戏知识以及出给我了他的 chupico 手台
  • 所有的群 u 们