Files
GaCy-GO/docs/scratch/honkai_dumper_deep_dive.md
T

450 lines
24 KiB
Markdown
Raw Normal View History

# Honkai: Star Rail In-Process Dumper (`honkai-dumper`) Architectural Specification & Deep Dive
This technical specification provides an exhaustive, code-level analysis of **`honkai-dumper`** (by AzenKain / lanylow), an in-process, native Rust dynamic link library (`cdylib`) engineered to extract metadata, field layouts, and compiled method Relative Virtual Addresses (RVAs) from the live process memory of *Honkai: Star Rail* (`StarRail.exe`).
---
## 1. Executive Summary & Design Rationale
### 1.1 The Fundamental Problem of Offline IL2CPP Dumpers
In conventional Unity IL2CPP games, compiled assemblies exist as two distinct artifacts:
1. `GameAssembly.dll`: Native machine code (PE x86_64) resulting from C# $\rightarrow$ C++ $\rightarrow$ Clang compilation.
2. `global-metadata.dat`: Binary metadata file containing string literals, type definitions, method signatures, and field offset tables.
In standard environments, tools like **Il2CppDumper** or **Il2CppInspector** parse `global-metadata.dat` statically on disk. However, modern live-service games protected by commercial packers/DRM implement active anti-reversing countermeasures:
- **Encrypted Metadata**: `global-metadata.dat` is heavily obfuscated and encrypted on disk; static extractors fail immediately without proprietary decryption keys.
- **Stripped API Exports**: `GameAssembly.dll` strips all standard IL2CPP C-API exports (e.g., `il2cpp_domain_get`, `il2cpp_class_get_methods`), rendering standard dynamic import resolution (`GetProcAddress`) ineffective.
### 1.2 The In-Process Dumper Solution
`honkai-dumper` bypasses disk-level metadata encryption entirely by operating **inside the game process space** as an injected native module (`.dll`):
- It delays execution until the game engine has completed unpacking, decrypted metadata, and instantiated the internal runtime structures in memory.
- Instead of relying on exported symbols in `GameAssembly.dll`, it utilizes **signature pattern scanning** inside `UnityPlayer.dll` to locate the hidden global table of IL2CPP API function pointers.
- It walks the live memory graph (`Domain` $\rightarrow$ `Assembly` $\rightarrow$ `Image` $\rightarrow$ `Class` $\rightarrow$ `Method` / `Field`), emitting reconstructed C# header definitions (`dump.cs`) and method RVA address maps (`methods.json`) ready for IDA Pro, Ghidra, or Binary Ninja.
```
+----------------------------------------------------------------------------------------------------+
| Game Process Address Space |
+----------------------------------------------------------------------------------------------------+
| |
| [ UnityPlayer.dll ] [ GameAssembly.dll ] |
| +-------------------------------------+ +-------------------------------+ |
| | Pattern: 48 8B 05 .. 48 8D 0D .. | | Native Machine Code Functions | |
| | | | | (Compiled from C# Methods) | |
| | v | +---------------+---------------+ |
| | RIP-relative calculation | ^ |
| | | | | |
| | v | | Method Pointers |
| | Internal Function Pointer Table | | (VAs / RVAs) |
| | [0] = null | | |
| | [22] = il2cpp_assembly_get_image | | |
| | [35] = il2cpp_class_get_methods | | |
| | [63] = il2cpp_domain_get | | |
| +-------------------+-----------------+ | |
| | | |
+----------------------|----------------------------------------------------------|------------------+
| Resolves Function Pointers |
v |
+---------------------------------------------------------------------------------+------------------+
| honkai-dumper.dll (Injected Module) |
+----------------------------------------------------------------------------------------------------+
| 1. entry() [#[ctor]] -> Spawns worker thread |
| 2. thread::sleep(10s) -> Awaits runtime initialization & metadata decryption |
| 3. AllocConsole() -> Interactive logging |
| 4. Traverser: Domain -> Assemblies -> Images -> Classes -> Fields & Methods |
| 5. Serialization: |
| * dump.cs : Reconstructed C# class headers with field offsets and method RVAs |
| * methods.json : JSON dictionary mapping "Namespace.Class::Method" -> "0xRVA" |
+----------------------------------------------------------------------------------------------------+
```
---
## 2. Codebase Architecture & File Organization
The project is implemented in pure Rust (2021 Edition) configured as a Windows C dynamic library (`crate-type = ["cdylib"]`).
```
honkai-dumper/
├── Cargo.toml # cdylib target configuration, winapi, pelite, serde_json, ctor
├── src/
│ ├── lib.rs # Module root (base, il2cpp, outputs)
│ ├── base.rs # DLL entry point, thread creation, 10s synchronization delay
│ ├── il2cpp/
│ │ ├── mod.rs # Re-exports submodules
│ │ ├── types.rs # Raw C-ABI memory representations (MethodInfo, Il2CppClass, etc.)
│ │ ├── constants.rs # ECMA-335 CLI metadata attribute masks and flags
│ │ ├── module.rs # Win32 module loader & generic FunctionPtr<T> wrapper
│ │ ├── utils.rs # PE scanner & RIP-relative displacement pattern matcher
│ │ ├── functions.rs # Il2CppFunctions table index mapping (array offsets)
│ │ └── api.rs # High-level safe wrapper over runtime API pointers
│ └── outputs/
│ ├── mod.rs # Output format definitions
│ ├── methoddumper.rs # Method RVA JSON exporter (methods.json)
│ └── csdumper.rs # C# AST reconstruction exporter (dump.cs)
```
### 2.1 Dependencies & Their Technical Roles
| Crate | Version | Architectural Role |
| :--- | :--- | :--- |
| `ctor` | `0.2.8` | Emits `#[ctor]` constructor function into `.CRT$XCU` section for execution on DLL load. |
| `pelite` | `0.10.0` | High-performance, zero-allocation PE64 file format parser for section headers and RVA conversion. |
| `winapi` | `0.3.9` | Windows API bindings for `libloaderapi`, `consoleapi`, `psapi`, and `processthreadsapi`. |
| `serde_json` | `1.0.116` | Serialization engine for generating formatted `methods.json`. |
| `thiserror` | `1.0.59` | Type-safe error enumeration derivation. |
---
## 3. Initialization Lifecycle & Process Synchronization
### 3.1 Non-Blocking DLL Attachment Pattern
Standard Windows DLL injection executes the payload inside `DllMain` during `DLL_PROCESS_ATTACH`. Executing heavy workloads or blocking synchronization inside `DllMain` violates Windows loader lock contracts and causes deadlocks.
`honkai-dumper` isolates injection lifecycle via `src/base.rs`:
```rust
use ctor::ctor;
use winapi::um::consoleapi;
use std::{error::Error, thread, time::Duration};
use crate::outputs::{csdumper, methoddumper};
#[ctor]
fn entry() {
thread::spawn(|| {
init().unwrap_or_else(|e| {
panic!("honkai-dumper encountered an error: {}", e)
});
});
}
```
1. **`#[ctor]` Injection**: The `ctor` macro generates a C-runtime startup initializer function.
2. **`thread::spawn`**: Immediately offloads execution to an isolated OS thread, returning control to the Windows loader instantly and averting loader lock deadlocks.
3. **Engine Warmup Delay**:
```rust
fn init() -> Result<(), Box<dyn Error>> {
thread::sleep(Duration::from_secs(10));
unsafe { consoleapi::AllocConsole() };
// ...
}
```
A fixed sleep of 10 seconds is imposed. This provides the host game process sufficient wall-clock time to:
- Load and decrypt secondary DLLs (`GameAssembly.dll`).
- Run native unpackers and decrypt internal `global-metadata.dat` structures into RAM.
- Execute IL2CPP runtime initialization routines (`il2cpp_init`) and register class domains.
4. **Console Allocation**: Calls Win32 `AllocConsole()` to attach a dedicated Win32 terminal window to the host process for operator feedback.
---
## 4. The Signature Scanning Engine (`utils.rs` & `api.rs`)
### 4.1 Locating Stripped IL2CPP Function Pointers
Because `GameAssembly.dll` exports are stripped, `honkai-dumper` leverages the structural architecture of the Unity Engine: `UnityPlayer.dll` acts as the engine host and holds a dispatch table of internal IL2CPP functions passed or registered during engine startup.
### 4.2 The x86_64 Machine Code Pattern
In `src/il2cpp/utils.rs`, the tool scans `UnityPlayer.dll` on disk using the following byte pattern:
```
48 8B 05 ? ? ? ? 48 8D 0D ? ? ? ? FF D0
```
#### Disassembly & Instruction Breakdown:
```nasm
48 8B 05 xx xx xx xx ; mov rax, [rip + disp32_1] <-- Loads target function table pointer
48 8D 0D yy yy yy yy ; lea rcx, [rip + disp32_2] <-- Loads context argument into RCX
FF D0 ; call rax <-- Indirect call to IL2CPP initialization
```
### 4.3 RIP-Relative Displacement Calculation
x86_64 uses relative instruction pointer (RIP) addressing. The displacement stored in the machine instruction is relative to the address of the **subsequent** instruction.
`honkai-dumper` resolves this with mathematical precision:
$$\text{RVA}_{\text{target}} = \text{RVA}_{\text{instruction}} + \text{Len}_{\text{instruction}} + \text{disp32}$$
In `utils.rs`:
```rust
pub fn find_qword_addr(data: &[u8], base_addr: u64, pe: &PeFile) -> Option<usize> {
let pattern = pattern_to_bytes("48 8B 05 ? ? ? ? 48 8D 0D ? ? ? ? FF D0");
for i in 0..=data.len().saturating_sub(pattern.len()) {
if match_pattern(data, &pattern, i) {
let rva = file_offset_to_rva(i as u32, pe)?;
let va = base_addr + rva as u64;
let rel_offset = i + 3; // Offset to 4-byte displacement in `mov rax, [rip + disp32]`
let disp = i32::from_le_bytes(data[rel_offset..rel_offset + 4].try_into().unwrap());
let rip_after_mov = va + 7; // Length of `mov rax, [rip + disp32]` is 7 bytes
let qword_addr = rip_after_mov.wrapping_add(disp as u64);
return Some((qword_addr - base_addr) as usize);
}
}
None
}
```
1. **File Offset $\rightarrow$ RVA Conversion**: Iterates PE section headers via `pelite` to map raw file offset $i$ to section Virtual Address.
2. **Displacement Extraction**: Reads 32-bit signed little-endian integer (`i32`) from offset `i + 3`.
3. **Effective Address**: Adds displacement to $RIP_{\text{next}} = VA + 7$.
4. **Base Address Normalization**: Subtracts image base to return the target table RVA relative to `UnityPlayer.dll`.
---
## 5. IL2CPP Function Table Jump Indices (`functions.rs`)
Once the table RVA is resolved, `honkai-dumper` treats the memory address inside the running `UnityPlayer.dll` as an array of raw function pointers:
```rust
let functions = Il2CppFunctions::new(unity_player.handle as usize + qword);
```
### 5.1 Function Pointer Dereferencing Wrapper
To safely call untyped C function pointers, `src/il2cpp/module.rs` defines a zero-cost wrapper `FunctionPtr<T>` utilizing Rust's `Deref` trait and `PhantomData`:
```rust
pub struct FunctionPtr<T> {
pub ptr: *const c_void,
pd: PhantomData<T>
}
impl<T> Deref for FunctionPtr<T> {
type Target = T;
fn deref(&self) -> &T {
unsafe { &*(&self.ptr as *const *const _ as *const T) }
}
}
```
### 5.2 The Index Map
`src/il2cpp/functions.rs` maps array offsets using the `index!` macro:
```rust
macro_rules! index {
($start:expr, $index:expr) => {{
let addr = unsafe { *$start.offset($index) };
if addr.is_null() { None } else { Some(FunctionPtr::new(addr)) }
}};
}
```
The specific indices corresponding to the global HSR Unity release:
| Table Index | Function Signature | Description |
| :---: | :--- | :--- |
| **22** | `il2cpp_assembly_get_image(*const Il2CppAssembly) -> *const Il2CppImage` | Retrieves primary PE image from assembly |
| **31** | `il2cpp_class_get_fields(*const Il2CppClass, *const *const c_void) -> *const FieldInfo` | Iterates member fields of a class |
| **33** | `il2cpp_class_get_interfaces(*const Il2CppClass, *const *const c_void) -> *const Il2CppClass` | Iterates implemented interfaces |
| **35** | `il2cpp_class_get_methods(*const Il2CppClass, *const *const c_void) -> *const MethodInfo` | Iterates class methods |
| **37** | `il2cpp_class_get_name(*const Il2CppClass) -> *const c_char` | Class name string |
| **39** | `il2cpp_class_get_namespace(*const Il2CppClass) -> *const c_char` | Class namespace string |
| **40** | `il2cpp_class_get_parent(*const Il2CppClass) -> *const Il2CppClass` | Base inheritance class |
| **43** | `il2cpp_class_is_valuetype(*const Il2CppClass) -> bool` | Checks if type is struct / ValueType |
| **45** | `il2cpp_class_get_flags(*const Il2CppClass) -> i32` | CLI TypeDef attribute flags |
| **49** | `il2cpp_class_from_type(*const Il2CppType) -> *const Il2CppClass` | Resolves class definition from type ref |
| **53** | `il2cpp_class_is_enum(*const Il2CppClass) -> bool` | Checks if type is Enum |
| **63** | `il2cpp_domain_get() -> *const Il2CppDomain` | Returns root AppDomain pointer |
| **65** | `il2cpp_domain_get_assemblies(*const Il2CppDomain, *const usize) -> *const *const Il2CppAssembly` | Returns array of loaded assemblies |
| **72** | `il2cpp_field_get_flags(*const FieldInfo) -> i32` | CLI Field attribute flags |
| **73** | `il2cpp_field_get_name(*const FieldInfo) -> *const c_char` | Field identifier name |
| **75** | `il2cpp_field_get_offset(*const FieldInfo) -> usize` | Memory offset within instance/static buffer |
| **76** | `il2cpp_field_get_type(*const FieldInfo) -> *const Il2CppType` | Field type definition |
| **116** | `il2cpp_method_get_return_type(*const MethodInfo) -> *const Il2CppType` | Method return type |
| **117** | `il2cpp_method_get_name(*const MethodInfo) -> *const c_char` | Method identifier name |
| **123** | `il2cpp_method_get_param_count(*const MethodInfo) -> u32` | Parameter count |
| **124** | `il2cpp_method_get_param(*const MethodInfo, u32) -> *const Il2CppType` | Parameter type at index |
| **161** | `il2cpp_type_get_name(*const Il2CppType) -> *const c_char` | Full CLR type name |
| **162** | `il2cpp_type_is_byref(*const Il2CppType) -> bool` | Checks `ref` / pointer indirection |
| **163** | `il2cpp_type_get_attrs(*const Il2CppType) -> u32` | Param/Type attribute flags (`[In]`, `[Out]`) |
| **168** | `il2cpp_image_get_name(*const Il2CppImage) -> *const c_char` | Assembly module name (e.g. `Assembly-CSharp.dll`) |
| **169** | `il2cpp_image_get_class_count(*const Il2CppImage) -> usize` | Total number of classes in image |
| **170** | `il2cpp_image_get_class(*const Il2CppImage, usize) -> *const Il2CppClass` | Retrieves class definition by index |
> [!WARNING] Version Fragility
> Because these table indices represent direct slots inside an internal dispatch table in `UnityPlayer.dll`, **any major Unity version update or custom engine compilation will shift these indices**. If the game client updates its Unity minor/patch version, the array offsets must be re-verified against a disassembly of `UnityPlayer.dll`.
---
## 6. Runtime Metamodel & Memory Layout (`types.rs` & `api.rs`)
### 6.1 Native `MethodInfo` Structure Representation
In IL2CPP runtime architecture, every compiled managed method is represented by a C struct. In `src/il2cpp/types.rs`, `MethodInfo` is declared as:
```rust
#[repr(C)]
pub struct MethodInfo {
pub klass: *const Il2CppClass,
pub method_pointer: *const c_void,
_pad: [u8; 0x20],
pub flags: u16
}
```
#### Memory Layout Diagram:
```
Offset Field Type Description
+0x00 klass *const Il2CppClass Pointer to declaring Il2CppClass
+0x08 method_pointer *const c_void Absolute Virtual Address (VA) of native compiled code
+0x10 _pad [u8; 0x20] Internal runtime fields (invoker_method, name, etc.)
+0x30 flags u16 CLI Method Attributes (Public, Private, Static, etc.)
```
### 6.2 Address Boundary Verification
In JIT or native code hooking engines, pointers may point to trampolines, dynamically generated stubs, or external modules. `methoddumper.rs` enforces strict pointer sanity checking:
```rust
fn verify_pointer(il2cpp: &Il2CppApi, pointer: usize) -> bool {
(pointer > il2cpp.game_assembly.handle as usize) &&
(pointer < il2cpp.game_assembly.handle as usize + il2cpp.game_assembly.size)
}
```
Only method pointers strictly located within the memory boundary of `GameAssembly.dll` ($\text{Base} \le \text{Ptr} < \text{Base} + \text{SizeOfImage}$) are treated as genuine compiled game methods.
---
## 7. C# Header Reconstruction Engine (`csdumper.rs`)
`csdumper.rs` decompiles raw binary runtime structures into valid C# source definitions, decorating them with exact memory offsets and RVAs.
### 7.1 ECMA-335 Bitmask Decompilation
The engine decodes CLI attribute bitfields from `constants.rs`:
#### 1. Method Visibility & Modifiers
```rust
let flags = unsafe { (*method).flags } as i32;
let access = flags & METHOD_ATTRIBUTE_MEMBER_ACCESS_MASK;
let access_str = match access {
METHOD_ATTRIBUTE_PRIVATE => "private ",
METHOD_ATTRIBUTE_PUBLIC => "public ",
METHOD_ATTRIBUTE_FAMILY => "protected ",
METHOD_ATTRIBUTE_ASSEM | METHOD_ATTRIBUTE_FAM_AND_ASSEM => "internal ",
METHOD_ATTRIBUTE_FAM_OR_ASSEM => "protected internal ",
_ => ""
};
```
#### 2. Virtual Table & Polymorphism Layout
IL2CPP preserves method slot layout flags (`METHOD_ATTRIBUTE_VTABLE_LAYOUT_MASK`):
- `METHOD_ATTRIBUTE_ABSTRACT`: Emits `abstract`.
- `METHOD_ATTRIBUTE_FINAL` with `METHOD_ATTRIBUTE_REUSE_SLOT`: Emits `sealed override`.
- `METHOD_ATTRIBUTE_VIRTUAL` with `METHOD_ATTRIBUTE_NEW_SLOT`: Emits `virtual`.
- `METHOD_ATTRIBUTE_VIRTUAL` with `METHOD_ATTRIBUTE_REUSE_SLOT`: Emits `override`.
#### 3. ValueType Field Offset Normalization
In IL2CPP's object model:
- Reference types (`class`) carry a 16-byte (`0x10`) header (`Il2CppObject` containing `Il2CppClass*` and `MonitorData*`).
- Value types (`struct`) on the heap are boxed with the 16-byte header, but unboxed instance structs lack this header.
`csdumper.rs` handles this normalization:
```rust
if is_valuetype && !is_static && field_offset > 0 {
field_offset -= 0x10;
}
```
### 7.2 Type Name Normalization
To produce clean, idiomatic C# source, `api.rs` maps raw CLR type names to C# language aliases:
```rust
const PRIMITIVE_TYPES: &[(&str, &str)] = &[
("System.Void", "void"),
("System.Boolean", "bool"),
("System.Char", "char"),
("System.SByte", "sbyte"),
("System.Byte", "byte"),
("System.Int16", "short"),
("System.UInt16", "ushort"),
("System.Int32", "int"),
("System.UInt32", "uint"),
("System.Int64", "long"),
("System.UInt64", "ulong"),
("System.Single", "float"),
("System.Double", "double"),
("System.String", "string"),
("System.IntPtr", "IntPtr"),
("System.UIntPtr", "UIntPtr"),
("System.Object", "object"),
("&", "")
];
```
---
## 8. Method RVA Map Generation (`methoddumper.rs`)
### 8.1 Method Signature Flattening & Collision Resolution
In C#, method overloading allows multiple methods to share the same identifier with different parameter signatures. To generate a flat JSON dictionary without key collisions, `methoddumper.rs` implements a collision tracker:
```rust
let method_name = il2cpp.method_get_name(method_info)?;
let description = format!("{}{}::{}", class_namespace, class_name, method_name);
let unique_description = if name_map.contains_key(&description) {
let count = duplicates.entry(description.to_string()).or_insert(0);
*count += 1;
format!("{}_{}", description, count)
} else {
description
};
name_map.insert(unique_description, pointer - il2cpp.game_assembly.handle as usize);
```
### 8.2 JSON Output Schema (`methods.json`)
The resulting file is formatted as a key-value mapping of fully qualified method signatures to hexadecimal RVAs:
```json
{
"RPG.Client.AutoScrollRect::.ctor": "0x1febdf0",
"RPG.Client.AutoScrollRect::MNOGNALMGPL": "0x1feb700",
"RPG.Client.AutoScrollRect::POFLDHKMLAF": "0x1feb7a0",
"RPG.Client.AutoScrollRect::SetItemSelectCallback": "0x1feb7f0",
"RPG.Client.AutoScrollRect::SnapTo": "0x1feb9c0"
}
```
---
## 9. Comparative Technical Assessment: In-Process vs. Static Dumpers
| Dimension | `honkai-dumper` (In-Process) | `Il2CppDumper` (Static Disk) |
| :--- | :--- | :--- |
| **Execution Vector** | Native DLL injection into running process | Standalone CLI executable analyzing disk files |
| **Metadata Protection** | **Immune**: Reads memory after unpacker decrypts metadata | **Fails**: Blocked by custom encryption on `global-metadata.dat` |
| **Symbol Stripping** | **Immune**: Locates API table via signature scan in `UnityPlayer.dll` | Requires manual IDA script or pattern search if exports are stripped |
| **Anti-Cheat Risk** | **High**: Process injection can trigger game integrity checks/watchdogs | **Zero**: Entirely offline |
| **Build Stability** | Fragile: Dependent on fixed jump table index offsets in `functions.rs` | Robust: Parses standard metadata format specifications |
| **Output Formats** | `dump.cs` (Headers) and `methods.json` (RVAs) | `dump.cs`, `script.py` (IDA), `il2cpp.h`, `stringliteral.json` |
---
## 10. Integration & Cross-Application in Server & Protocol Engineering
### 10.1 Labeling Disassembly in IDA Pro / Ghidra
When reverse engineering stripped Star Rail binaries:
1. Open `GameAssembly.dll` in IDA Pro / Ghidra / Binary Ninja.
2. In IDA Pro, run an import script:
```python
import json
import ida_name, ida_bytes
with open("methods.json", "r") as f:
methods = json.load(f)
base = ida_nalt.get_imagebase()
for name, rva_str in methods.items():
rva = int(rva_str, 16)
ea = base + rva
# Sanitize name for IDA symbol constraints
clean_name = name.replace("::", "__").replace(".", "_")
ida_name.set_name(ea, clean_name, ida_name.SN_NOWARN)
```
3. Instantly transforms thousands of anonymous subroutines (`sub_181FEB700`) into identifiable game logic (`RPG_Client_AutoScrollRect__MNOGNALMGPL`).
### 10.2 Protocol & Packet Handler Discovery
In private server engineering (`LunarCore`, `C# NativeAOT Reference Server`, `GaC-hkrpg-go`):
- Network packet dispatchers often use obfuscated command handlers.
- By searching `dump.cs` for classes implementing network interfaces or containing `CmdId` constants, engineers can pinpoint the exact RVA of packet deserialization routines.
- Cross-referencing `methods.json` RVAs with dynamic breakpoints allows live tracing of packet encryption keys, seed exchanges, and payload masking expressions.
### 10.3 Compliance with Repository Rules (`AGENTS.md`)
- `honkai-dumper` is a one-shot diagnostic utility executed in an isolated thread (`thread::spawn`) on injection, writing results and terminating.
- In contrast to prohibited persistent runtime probes, it does **not** attach to per-frame update loops or main thread heartbeats, adhering to the principle of zero in-game performance degradation.
- No local machine paths, static IPs, or client versions are hardcoded in the codebase. All memory resolution is performed dynamically relative to `GetModuleInformation` bases.