feat(currency_wars): complete systems, settlement Rank SS, damage report, camera and recovery fixes

This commit is contained in:
CAtlas0784 committed 2026-10-31 20:47:36 +07:00
1 parent c008243f11
commit 0995055296
78 files changed
+33840 -9525

No files matched your search

@@ -0,0 +1,379 @@
# Static CmdId Dumper (`StaticCmdIdDumper`) Architectural Specification & Machine-Code Deep Dive
This technical specification provides an exhaustive, code-level analysis of **`StaticCmdIdDumper`** (by Hiro420), an ultra-minimalist, zero-dependency C# (.NET 8.0) static binary analysis utility. The tool is engineered to extract network packet Command IDs (`CmdId` / `Opcode`) directly from compiled IL2CPP game executables (such as `YuanShen.exe`, `StarRail.exe`, or `GameAssembly.dll`) on disk without launching the game process or relying on third-party disassemblers (such as Capstone or Iced).
---
## 1. Executive Summary & Architectural Purpose
### 1.1 The Protocol Command ID Problem
In modern anime titles developed on Unity IL2CPP (Genshin Impact, Honkai: Star Rail, Zenless Zone Zero), client-server communication relies on an enumerated command opcode (`CmdId`) identifying each Protobuf packet.
Inside the game client assemblies, each packet class contains a getter method returning its unique opcode:
```csharp
public class GetAvatarDataCsReq : IMessage {
public override ushort GetCmdId() {
return 384; // 0x0180
}
}
```
In compiled releases:
1. Method names are stripped or obfuscated (e.g., `GetCmdId()` $\rightarrow$ `KOOKJNLBAMH()`).
2. Tools like `Firefly-Static-Parser` or `honkai-dumper` can identify the **Relative Virtual Address (RVA)** of these getter methods.
3. However, obtaining the actual integer value of the opcode traditionally required:
- Attaching a live debugger / Frida script to invoke the method at runtime, or
- Opening a 100MB+ binary in IDA Pro / Ghidra and manually decompiling thousands of tiny stub functions.
### 1.2 The `StaticCmdIdDumper` Solution
`StaticCmdIdDumper` demonstrates an elegant, lightweight alternative:
- It exploits the deterministic machine-code pattern emitted by C++ compilers when compiling a constant 16-bit integer getter.
- It parses PE (Portable Executable) headers using pure, hand-written C# structs.
- It maps the getter method's RVA directly to disk file offset, reads **exactly 4 bytes**, and extracts the embedded immediate integer via a micro-disassembler.
```
+----------------------------------------------------------------------------------------------------+
| StaticCmdIdDumper Architecture |
+----------------------------------------------------------------------------------------------------+
| |
| [ Inputs ] |
| 1. Binary File (e.g. YuanShen.exe / GameAssembly.dll) |
| 2. list.txt: Mapping of <ObfuscatedName> <RVA> |
| Example: "KOOKJNLBAMH 0x0CCA3780" |
| |
| [ Pipeline: Zero External Dependencies ] |
| |
| +----------------------------------------------------------------------------------------------+ |
| | 1. PEHeader.cs & Misc.cs | |
| | - Parse DOS Header ('MZ') -> e_lfanew -> PE Signature ('PE\0\0') | |
| | - Parse OptionalHeader64 -> Extract Section Table | |
| | - Locate Target Section ('il2cpp' or '.text') | |
| +----------------------------------------------+-----------------------------------------------+ |
| | |
| v |
| +----------------------------------------------------------------------------------------------+ |
| | 2. File Offset Translation | |
| | FileOffset = Section.PointerToRawData + (MethodRVA - Section.VirtualAddress) | |
| +----------------------------------------------+-----------------------------------------------+ |
| | |
| v |
| +----------------------------------------------------------------------------------------------+ |
| | 3. Targeted 4-Byte Seek & Read | |
| | fs.Seek(FileOffset) -> fs.Read(buffer, 0, 4) | |
| +----------------------------------------------+-----------------------------------------------+ |
| | |
| v |
| +----------------------------------------------------------------------------------------------+ |
| | 4. Micro-Disassembler (Disassembler.cs) | |
| | Matches x86-64 pattern: 66 B8 [imm16] --> "mov ax, imm16" | |
| | Extracts Little-Endian uint16 CmdId | |
| +----------------------------------------------+-----------------------------------------------+ |
| | |
| v |
| [ Output ] |
| PacketIds.json: |
| { |
| "KOOKJNLBAMH": 1234, |
| "MGDFLHGODLH": 4567 |
| } |
| |
+----------------------------------------------------------------------------------------------------+
```
---
## 2. Codebase Organization & Engineering Philosophy
The repository consists of a single C# project targeting `.NET 8.0`:
```
StaticCmdIdDumper/
├── StaticCmdIdDumper.sln
└── StaticCmdIdDumper/
├── StaticCmdIdDumper.csproj # Zero external package references (<PackageReference>)
├── PEHeader.cs # Complete Win32/PE64 data structure definitions & binary readers
├── Misc.cs # Section search, header dumpers, and file parsing utilities
├── Disassembler.cs # 50-line custom x86-64 machine-code instruction decoder
└── Program.cs # CLI coordinator & JSON serializer
```
### 2.1 The Zero-Dependency Design
Modern .NET reverse-engineering tools typically pull in large third-party dependencies:
- `AsmResolver.PE` or `PeNet` for PE parsing.
- `Iced` or `Gee.External.Capstone` for disassembly.
`StaticCmdIdDumper` explicitly rejects external packages, implementing:
1. Hand-coded binary reader routines (`Read16LE`, `Read32LE`, `Read64LE`).
2. Hand-crafted PE structures mirroring Windows `winnt.h` (`IMAGE_DOS_HEADER`, `IMAGE_NT_HEADERS64`, `IMAGE_SECTION_HEADER`).
3. Direct byte pattern recognition for the specific instruction opcode emitted by MSVC / Clang.
---
## 3. Machine Code Mechanics: IL2CPP Opcode Getters
### 3.1 C++ to Native x86-64 Compilation
In Unity's IL2CPP output, a constant getter function in C++ is written as:
```cpp
uint16_t GetCmdId() {
return 1234; // 0x04D2
}
```
When compiled by MSVC or Clang for x86-64 with standard optimizations (`/O2` or `-O3`), the compiler emits:
```nasm
mov ax, 0x04D2
ret
```
### 3.2 Binary Instruction Encoding
The x86-64 instruction encoding for `mov ax, imm16` is:
$$\begin{array}{|c|c|c|c|}
\hline
\mathbf{0x66} & \mathbf{0xB8} & \mathbf{\text{Byte}_0} & \mathbf{\text{Byte}_1} \\
\hline
\text{Prefix (Operand Size)} & \text{Opcode (mov ax)} & \text{Imm16 (Low)} & \text{Imm16 (High)} \\
\hline
\end{array}$$
- **`0x66` (Operand-Size Override Prefix)**: The default operand size in 64-bit mode is 32-bit. The `0x66` prefix instructs the processor to decode the instruction for the 16-bit register `ax` instead of 32-bit `eax`.
- **`0xB8`**: Opcode for `mov reg16/32, imm16/32` (specifically targeting accumulator register `ax`).
- **`Byte 0` & `Byte 1`**: 16-bit little-endian immediate value representing the `CmdId`.
- **`0xC3`**: The subsequent byte (at offset +4) is the `ret` opcode.
### 3.3 The Custom Micro-Disassembler (`Disassembler.cs`)
Because the target functions follow this invariant structure, `Disassembler.cs` decodes the opcode in just a few lines:
```csharp
public static int Disassemble(byte[] code)
{
int ip = 0;
while (ip < code.Length)
{
byte opcode = code[ip];
switch (opcode)
{
case 0x66: // 16-bit operand override
{
if (ip + 1 < code.Length && code[ip + 1] == 0xB8)
{
if (ip + 3 < code.Length)
{
// Read 16-bit unsigned little-endian integer
ushort imm16 = BitConverter.ToUInt16(code, ip + 2);
return imm16;
}
}
ip += 1;
break;
}
default:
ip += 1;
break;
}
}
return 0;
}
```
---
## 4. PE Header Parsing & RVA-to-Offset Translation
### 4.1 PE64 Structural Traversal
In `PEHeader.cs` and `Misc.cs`, the program traverses the binary structure:
1. **DOS Header**:
- Reads `dosHeader.magic` (must equal `0x5A4D` / `"MZ"`).
- Reads `dosHeader.e_lfanew` at offset `0x3C`, which points to the NT PE header.
2. **PE Header & Optional Header**:
- Validates `dosHeader.pe.signature == 0x00004550` (`"PE\0\0"`).
- Determines architecture from `optionalHeader.magic`:
- `0x10B`: 32-bit PE (`x86`)
- `0x20B`: 64-bit PE (`x86_64`)
- Reads `numberOfSections` and `optionalHeaderSize`.
3. **Section Table Traversal (`ReadSections`)**:
- Iterates through `IMAGE_SECTION_HEADER` entries (40 bytes each):
```csharp
dosHeader.section_table[idx].name = ReadStr(reader, 8);
dosHeader.section_table[idx].virtualSize = Read32LE(reader);
dosHeader.section_table[idx].virtualAddr = Read32LE(reader);
dosHeader.section_table[idx].sizeOfRawData = Read32LE(reader);
dosHeader.section_table[idx].ptrToRawData = Read32LE(reader);
```
### 4.2 Locating the IL2CPP Section (`Misc.GetBaseAdd`)
In certain compiled anime games (notably Genshin Impact's `YuanShen.exe`), the linker places compiled game code into a custom named section:
```csharp
for (int idx = 0; idx < dosHeader.pe.numberOfSections; idx++)
{
if (sections![idx].name != "il2cpp")
{
continue;
}
return sections[idx];
}
```
### 4.3 RVA to Disk File Offset Translation
Memory-mapped virtual addresses (VAs and RVAs) differ from raw disk offsets due to section alignment. The conversion formula is:
$$\text{FileOffset} = \text{Section.PointerToRawData} + (\text{RVA}_{\text{target}} - \text{Section.VirtualAddress})$$
In `Program.cs`:
```csharp
ulong CmdIDTestAddr = Convert.ToUInt64(pair.Value.Substring(2), 16);
var fileoffset = il2cpp_section.ptrToRawData + (CmdIDTestAddr - il2cpp_section.virtualAddr);
byte[] buffer = new byte[4];
using (FileStream fs = new FileStream(args[0], FileMode.Open, FileAccess.Read))
{
fs.Seek((long)fileoffset, SeekOrigin.Begin);
fs.Read(buffer, 0, 4);
}
int cmdid = Disassembler.Disassemble(buffer);
packetIds[pair.Key] = cmdid;
```
---
## 5. Execution Workflow & Serialization
### 5.1 Input Format (`list.txt`)
The tool accepts a plain whitespace-delimited text file containing pairs of function identifiers and hexadecimal RVAs:
```text
KOOKJNLBAMH 0x0CCA3780
MGDFLHGODLH 0x0BE5AA60
CGKBCOJMLPC 0x082AB170
OOMIIJFHPFN 0x0D16A280
```
> [!NOTE] Origin of the Input List
> This list is directly obtained from the output of `Firefly-Static-Parser` or `honkai-dumper`. In `methods.json`, getter methods for packet IDs appear as:
> `"KOOKJNLBAMH": "0xCCA3780"`
### 5.2 Output Format (`PacketIds.json`)
The resulting dictionary is serialized to formatted JSON:
```json
{
"KOOKJNLBAMH": 101,
"MGDFLHGODLH": 102,
"CGKBCOJMLPC": 143,
"OOMIIJFHPFN": 172
}
```
---
## 6. The Complete 5-Stage Protocol Reverse-Engineering Toolchain
By studying `LunarCore`, `March7thHoney`, `FireflyProtoc`, `honkai-dumper`, `Firefly-Static-Parser`, and `StaticCmdIdDumper`, we can now construct the entire open-source reverse-engineering pipeline:
```
+----------------------------------------------------------------------------------------------------+
| THE UNIFIED PROTOCOL EXTRACTION PIPELINE |
+----------------------------------------------------------------------------------------------------+
| |
| [ Stage 1: Metadata Extraction ] |
| Tool: Firefly-Static-Parser (Offline) OR honkai-dumper (In-Process) |
| Input: GameAssembly.dll + global-metadata.dat (b"MHY\0") |
| Output: dump.cs (class schemas) + methods.json (method RVAs) |
| |
| [ Stage 2: Static Opcode Extraction ] |
| Tool: StaticCmdIdDumper |
| Input: Executable binary + method RVAs of getter functions |
| Process: Reads 4-byte machine code (66 B8 imm16) -> extracts uint16 CmdIds |
| Output: PacketIds.json (Map of ObfuscatedPacketName -> Integer CmdId) |
| |
| [ Stage 3: Protobuf Schema Annotation ] |
| Tool: Schema Annotator / String Literal Correlator |
| Action: Injects metadata annotations into .proto files: |
| meta uint32 cmd_id = 143; |
| uint32 id = 1 [get: (id ^ 1) + 2]; |
| |
| [ Stage 4: Specialized Protobuf Compilation ] |
| Tool: FireflyProtoc |
| Action: Compiles .proto via AST pipeline (HIR -> MIR -> LIR) |
| - ExpressionInverter: Automatically derives setter logic for wire masks |
| - MessageFactory: Emits O(1) compile-time jump table switch(cmdId) |
| Output: High-performance C# / Go protobuf stubs |
| |
| [ Stage 5: Server Protocol Routing ] |
| Tool: March7thHoney (.NET 10) / LunarCore (Java) / GaC-hkrpg-go (Go) |
| Action: Instant zero-reflection packet dispatching on KCP connections |
| |
+----------------------------------------------------------------------------------------------------+
```
---
## 7. Technical Critique & Improvements for `GaC-hkrpg-go`
While `StaticCmdIdDumper` is compact and effective, an industrial-grade implementation (e.g. ported to Go for `GaC-hkrpg-go`) requires resolving several architectural limitations:
### 7.1 Limitation 1: Hardcoded Section Name (`"il2cpp"`)
In `Misc.cs`:
```csharp
if (sections![idx].name != "il2cpp") continue;
```
- **Issue**: In *Honkai: Star Rail* and standard Unity builds, code is placed in the standard `.text` section, **not** a section named `il2cpp`.
- **Solution**: Implement dynamic RVA-to-Section resolution across **all** sections:
$$\text{Section} = \{ s \in \text{Sections} \mid s.\text{VirtualAddress} \le \text{RVA} < s.\text{VirtualAddress} + s.\text{VirtualSize} \}$$
### 7.2 Limitation 2: 32-Bit Register Emptiness (`mov eax, imm32`)
- **Issue**: If the method return type is `int` (32-bit signed integer) rather than `ushort`, compilers emit `mov eax, imm32` without the `0x66` prefix:
`B8 xx xx 00 00` (5 bytes: `0xB8` followed by 4-byte integer).
- **Solution**: Support both instruction patterns:
- `66 B8 [imm16]` $\rightarrow$ 16-bit opcode getter.
- `B8 [imm32]` $\rightarrow$ 32-bit opcode getter.
### 7.3 Limitation 3: Stream Seeking Overhead
- **Issue**: Opening and seeking `FileStream` individually for every single RVA creates heavy I/O overhead for thousands of packets.
- **Solution**: Memory-map the section or load the section into a contiguous `[]byte` slice in memory for instant $O(1)$ random access.
### 7.4 Go Port Blueprint for `GaC-hkrpg-go`
A native, high-performance Go implementation for extracting opcodes directly within the `GaC-hkrpg-go` toolset:
```go
package pe_dumper
import (
"debug/pe"
"encoding/binary"
"fmt"
"os"
)
func ExtractCmdIdFromRVA(peFile *pe.File, rva uint32, f *os.File) (uint16, error) {
// 1. Locate the containing section dynamically
for _, sec := range peFile.Sections {
if rva >= sec.VirtualAddress && rva < sec.VirtualAddress+sec.VirtualSize {
fileOffset := int64(sec.Offset + (rva - sec.VirtualAddress))
buf := make([]byte, 4)
if _, err := f.ReadAt(buf, fileOffset); err != nil {
return 0, err
}
// 2. Pattern: 66 B8 [imm16]
if buf[0] == 0x66 && buf[1] == 0xB8 {
return binary.LittleEndian.Uint16(buf[2:4]), nil
}
// 3. Pattern: B8 [imm32] (32-bit fallback)
if buf[0] == 0xB8 {
return binary.LittleEndian.Uint16(buf[1:3]), nil
}
return 0, fmt.Errorf("unknown instruction pattern: % X", buf)
}
}
return 0, fmt.Errorf("RVA 0x%X not found in any section", rva)
}
```
---
## 8. Compliance with Repository Rules (`AGENTS.md`)
- **Documentation & Protocol Integrity**: `StaticCmdIdDumper` extracts Command IDs purely from the machine code representation of the target client binary. It adheres strictly to the rule requiring handlers to resolve `CmdID` from verified client protocol tables without guessing or hardcoding multi-version aliases.
- **No Machine-Specific Hardcoding**: All file paths and offsets are read dynamically from command-line arguments and PE headers.
- **Zero Runtime Interference**: Operates entirely offline on the executable file, with zero memory injection, hooks, or heartbeat overhead.