19 KiB
Firefly Static Parser Architectural Specification & Cryptographic Deep Dive
This technical specification provides an exhaustive, code-level analysis of Firefly-Static-Parser (by AzenKain), an offline, static reverse-engineering framework and IL2CPP metadata extractor implemented in Rust. Unlike in-process dynamic dumpers (such as honkai-dumper), Firefly-Static-Parser operates completely offline without launching, injecting, or hooking into the game client process.
It achieves this by completely reverse-engineering the proprietary metadata container format (MHY\0), the embedded layout tables in GameAssembly.dll, dynamic semantic XOR key derivation, and non-linear mathematical scrambling algorithms used in Honkai: Star Rail.
1. Executive Summary & Architectural Paradigm
1.1 The Challenge of Custom IL2CPP Obfuscation
Standard Unity IL2CPP uses known, public data structures:
global-metadata.datbegins with magic0xFAB11BAF(version 29+).- Metadata definitions (TypeDef, MethodDef, FieldDef) are stored sequentially in unencrypted flat tables.
In Honkai: Star Rail and similar protected titles:
- Container Replaced:
global-metadata.datbegins with custom magicMHY\0(0x0059484D). - Split Metadata: Metadata ranges and image mappings are split between
global-metadata.datandstartup-metadata.dat. - Embedded PE Headers: The true table offsets, sizes, and field layout descriptors are embedded inside
GameAssembly.dlland encrypted with arithmetic displacements and bitwise XOR masks. - Scrambled Field/Method Entries: Entries are not sequential; attributes, indices, and names are encrypted with non-linear hash/PRNG functions unique to each table.
- Seeded Stream Cipher on Strings: Type names and string literals are packed into bitfields and encrypted with a QWORD running stream cipher.
1.2 The Two Dumping Paradigms
+----------------------------------------------------------------------------------------------------+
| IN-PROCESS vs. OFFLINE PARSER |
+----------------------------------------------------------------------------------------------------+
| |
| [ In-Process Dumper: honkai-dumper ] |
| Injected DLL -> Waits 10s -> Lets game unpacker decrypt metadata -> Scans UnityPlayer.dll |
| -> Calls runtime C-API pointers in RAM -> Dumps memory objects |
| * Advantage: Simple implementation, relies on game's own decryption routines. |
| * Disadvantage: Requires running game process, triggers anti-cheat/integrity monitors. |
| |
| [ Offline Static Parser: Firefly-Static-Parser ] |
| Direct disk analysis: GameAssembly.dll + global-metadata.dat + startup-metadata.dat |
| 1. Parses PE64 headers -> Scans byte patterns for metadata initializers |
| 2. Solves arithmetic/XOR masks -> Resolves table offsets & payload boundaries |
| 3. Dynamic Semantic Key Discovery -> Derives XOR keys via CLR invariants (<Module>, ValueType) |
| 4. Decrypts QWORD string cipher & PRNG-scrambled method/field definitions |
| 5. Reconstructs TypeDef AST -> Emits dump.cs & methods.json |
| * Advantage: Zero process injection, zero anti-cheat risk, pure deterministic static analysis. |
| * Disadvantage: High mathematical and reverse-engineering complexity. |
| |
+----------------------------------------------------------------------------------------------------+
2. Codebase Architecture & File Organization
The project is structured in Rust (2024 Edition) with a modular pipeline separating PE analysis, metadata loading, mathematical key derivation, and AST emission.
Firefly-Static-Parser/
├── Cargo.toml # anyhow, serde, serde_json, windows-sys, iced-x86, il2cpp_dumper
├── src/
│ ├── main.rs # CLI entry point -> app::run()
│ ├── app.rs # Input routing (Standard vs MHY custom metadata branch)
│ ├── args.rs # CLI arguments & default path resolvers
│ ├── config.rs # Dumper configuration flags
│ ├── pe_image.rs # Zero-dependency PE64 section parser, RVA mapper, pattern scanner
│ ├── static_layout.rs # Signature scanner, PE layout solver, arithmetic/XOR mask decoders
│ ├── metadata.rs # Magic validator (0xFAB11BAF vs b"MHY\0")
│ ├── obfuscated.rs # Fallback in-process loader via GameAssembly.dll custom loader RVA
│ ├── il2cpp_pe.rs # Standard IL2CPP symbol/code registration loader
│ ├── output.rs # Standard dumper output generator
│ └── layout_model/ # The custom MHY layout reconstruction engine
│ ├── mod.rs # LayoutImage, LayoutTypeDef, LayoutMethod, LayoutField AST models
│ ├── output.rs # Static dump.cs & methods.json serialization
│ └── parser/
│ ├── mod.rs # Dynamic Semantic Key Discovery & PRNG scramble formulas
│ ├── loader.rs # Dynamic scanner for il2cpp_type_table & method_pointer_table
│ └── reader.rs # QWORD string stream cipher, method/field decoders, generic solver
3. PE Layout & Global Discovery Engine (static_layout.rs)
To parse the custom metadata container, the parser must first extract layout descriptors embedded within GameAssembly.dll.
3.1 Pattern Scanning for Initializers
Firefly-Static-Parser scans the .text section of GameAssembly.dll for two critical initialization sequences:
-
static_metadata_initializer_pattern: The routine that initializes the IL2CPP static metadata globals. From this function, RIP-relativeleainstructions are decoded to extract:metadata_registration_rvadescriptor_rvametadata_cache_rvaembedded_header_rva
-
metadata_payload_initializer_pattern: The routine responsible for loading the external data files. It validates string references:rva_has_c_string(image, global_name_rva, b"global-metadata.dat") rva_has_c_string(image, startup_name_rva, b"startup-metadata.dat")Extracts
payload_offset(typically0x00or file header length).
3.2 Decryption of Embedded Header Offsets
The table offsets in the embedded header are obfuscated using combinations of 32-bit XOR keys, bitwise arithmetic shifts (sar_u32), and wrapping modular additions.
// Discovered from embedded_header_rva in GameAssembly.dll
let descriptor_count_80 = image.read_u32_rva(descriptor_rva + 0x80)? ^ 0x69BD_97F1;
let header_size_1a8 = image.read_u32_rva(header + 0x1A8)? ^ 0x729B_1A9E;
let header_size_1f8 = image.read_u32_rva(header + 0x1F8)? ^ 0x1608_C2C8;
let header_size_74 = image.read_u32_rva(header + 0x74)? ^ 0x080E_F250;
let header_count_134 = (image.read_u32_rva(header + 0x134)? ^ 0x1021_0728) / 40;
let header_count_178 = sar_u32(image.read_u32_rva(header + 0x178)?, 4) ^ 0x05CC_1AE1;
let header_count_bc = sar_u32(image.read_u32_rva(header + 0xBC)?, 3) ^ 0x079F_C2EC;
Complete Table Offsets Mapping:
| Offset In Header | Operation | Key / Operand | Output Field in StaticLayout |
|---|---|---|---|
+0x164 |
XOR |
0x7F5C_5934 |
startup_section_164_offset |
+0x150 |
wrapping_add |
0xD882_615E |
startup_image_table_offset |
+0x158 |
wrapping_add |
0xE03E_EAC1 |
startup_assembly_table_offset |
+0xD0 |
wrapping_add |
0xDFCF_C6B0 |
startup_assembly_name_table_offset |
+0x84 |
XOR |
0x6853_1D3F |
global_type_table_offset |
+0x20 |
wrapping_add |
0xB2FC_E189 |
global_field_table_offset |
+0x40 |
XOR |
0x3C68_5CDB |
global_property_table_offset |
+0x180 |
wrapping_add |
0xE4DB_E763 |
global_type_field_offset_map_offset |
+0x9C |
wrapping_add |
0xBC7E_C9D7 |
global_field_offset_group_table_offset |
+0x148 |
XOR |
0x329E_1172 |
global_field_offset_table_offset |
+0x14C |
wrapping_add |
0xF3A0_4294 |
global_method_table_offset |
+0x30 |
wrapping_add |
0xDCF5_FDBE |
global_parameter_table_offset |
+0x12C |
XOR |
0x26F0_FC20 |
global_nested_type_table_offset |
+0x1C8 |
XOR |
0x042F_9275 |
global_interface_type_table_offset |
+0xF0 |
wrapping_add |
0x9D48_920F |
global_generic_container_table_offset |
+0x140 |
wrapping_add |
0xDAAC_7309 |
global_generic_parameter_table_offset |
+0x38 |
wrapping_add |
0xF778_F1AB |
global_metadata_usage_type_table_offset |
+0x1D0 |
wrapping_add |
0xC9A6_64A5 |
global_metadata_usage_range_offset |
+0x190 |
wrapping_add |
0xED79_1073 |
global_metadata_usage_pair_offset |
+0x1F0 |
XOR |
0x56C7_D20D |
global_string_literal_table_offset |
+0x08 |
wrapping_add |
0xF711_4E59 |
global_string_literal_data_offset |
+0x1FC |
XOR |
0x6238_CDB0 |
global_field_default_value_table_offset |
+0x3C |
wrapping_add |
0x978B_E7A5 |
global_field_default_value_data_offset |
+0x1DC |
XOR / 12 |
0x3E0C_72F0 |
field_default_value_count |
+0x1B4 |
wrapping_add |
0x8D43_A4EE |
global_string_data_offset |
4. Dynamic Semantic Key Derivation (layout_model/parser/mod.rs)
A major breakthrough in Firefly-Static-Parser is how it discovers runtime XOR keys and type sentinels without hardcoding version-specific constants.
4.1 The Semantic Invariant Technique
In every valid .NET/IL2CPP metadata image, standard CLI core types always exhibit known semantic invariants:
- TypeDef 0 is
<Module>:- Has no base type
\rightarrowraw base value givestype_base_none. - Has known attribute flags
\rightarrowraw_flags ^ 0x700revealstype_attribute_key.
- Has no base type
System.ValueType:- Inherits directly from
System.Object. Therefore, the raw base type ofSystem.ValueTyperevealstype_base_object.
- Inherits directly from
System.Enum:- Inherits directly from
System.ValueType. Its raw base type revealstype_base_value_type.
- Inherits directly from
System.AttributeTargets:- Is an Enum. Its raw base type reveals
type_base_enum.
- Is an Enum. Its raw base type reveals
- Parent Type Index Formula:
\text{type\_parent\_key} = \text{type\_base\_object} - 1\text{ParentIndex} = \text{raw\_base\_type} - \text{type\_parent\_key}
4.2 Code Implementation
fn discover_type_layout_keys(layout: &mut StaticLayout, global_data: &[u8]) -> Result<()> {
// 1. Module Sentinel & Attribute Key
let type_table_offset = layout.payload_offset as usize + layout.global_type_table_offset as usize;
let module_name = decode_type_name_at(global_data, layout, type_table_offset)?.1;
assert_eq!(module_name, "<Module>");
layout.keys.type_base_none = read_u32(global_data, type_table_offset + 0x04)?;
layout.keys.type_attribute_key = read_u32(global_data, type_table_offset + 0x14)? ^ 0x700;
// 2. Scan Type Definitions for Standard BCL Anchors
for type_index in 0..scan_count {
let entry_offset = type_entry_offset_raw(layout, type_index).unwrap();
let raw_base = read_u32(global_data, entry_offset + 0x04)?;
let (namespace, name) = decode_type_name_at(global_data, layout, entry_offset)?;
match (namespace.as_str(), name.as_str()) {
("System", "ValueType") => layout.keys.type_base_object = raw_base,
("System", "Enum") => layout.keys.type_base_value_type = raw_base,
("System", "AttributeTargets") => layout.keys.type_base_enum = raw_base,
_ => {}
}
}
layout.keys.type_parent_key = layout.keys.type_base_object.wrapping_sub(1);
Ok(())
}
5. Decryption Algorithms & Mathematical Models
5.1 The QWORD String Stream Cipher (decode_string_raw)
In the MHY\0 container, strings are not null-terminated C-strings. String pointers are 32-bit composite integers encoding both length and offset.
1. Composite Index Bitfield Decoding:
let signed_index = index as i32;
let (length, string_offset_mask) = if signed_index < 0 {
// Long String Format (up to 255 bytes, 23-bit offset)
(((index >> 23) & 0xFF) as usize, 0x007F_FFFF)
} else {
// Short String Format (up to 63 bytes, 25-bit offset)
(((index >> 25) & 0x3F) as usize, 0x01FF_FFFF)
};
let string_offset = (index & string_offset_mask) as usize;
2. Seeded Stream Decryption:
Strings are encrypted in 8-byte QWORD blocks. The key is dynamically computed per string offset:
K_0 = \text{seed\_add} + (\text{seed\_mul} \times \text{string\_offset})
For each 8-byte chunk i \in [0, \lceil \text{length} / 8 \rceil):
P_i = C_i \oplus K_i
K_{i+1} = K_i + \text{increment}
let mut key = string_payload_key(layout, string_offset);
let mut out = Vec::with_capacity(qword_count * 8);
for chunk_index in 0..qword_count {
let encrypted = read_u64(global_data, data_offset + chunk_index * 8)?;
out.extend_from_slice(&(encrypted ^ key).to_le_bytes());
key = key.wrapping_add(string_payload_increment(layout));
}
out.truncate(length);
5.2 Non-Linear PRNG Scrambling Functions
Every table entry in the metadata layout is obfuscated by non-linear integer hashing functions:
1. Image Key (image_key):
K_{\text{img}}(i) = ((((i \times \mathtt{0xE07C}) \oplus \mathtt{0x7538159E}) \times \mathtt{0x120D0703}) \oplus \mathtt{0x6032C9D3}) + \mathtt{0x2EBB0085}
2. Method Key (method_key):
v_1 = (i \times \mathtt{0x31E1}) \oplus \mathtt{0x33914937}
v_2 = ((v_1 \times \mathtt{0x2C03F17D}) \gg 23) \times \mathtt{0x540CC9F4} \gg 21
K_{\text{method}}(i) = (v_2 \text{ as u32}) + \mathtt{0x71BC7861}
3. Parameter Key (parameter_key):
v = ((i \times \mathtt{0x072E1D74B12B} + \mathtt{0x01911D05AFF5}) \gg 11) \text{ as u32}
K_{\text{param}}(i) = (v \times \mathtt{0x58B870A2}) + \mathtt{0x83CF7B44}
4. Field Key (field_key):
K_{\text{field}}(\text{start}, j) = \mathtt{0xAD416BB9} - (\text{start} \times \mathtt{0x2C5DCB00}) + (j \times \mathtt{0xD3A23500})
5. Generic Container Key (generic_container_key):
v_1 = (i \times \mathtt{0x3D6913E0AF40} + \mathtt{0x0A64CAD60FA052C0}) \gg 23
v_2 = ((v_1 \times \mathtt{0x770E3FE8}) \gg 11) \times \mathtt{0x2C9A0EA3} \gg 23
K_{\text{gen\_container}}(i) = v_2 \text{ as u32}
6. Type System Reconstruction & Output Generation
6.1 Dynamic Table Resolution in GameAssembly.dll
In addition to metadata parsing, method RVAs require locating the compiled function pointers. In src/layout_model/parser/loader.rs:
il2cpp_type_table: ScansGameAssembly.dllfor continuous 16-byte descriptors wherekind\in (0, 0x50)andbyref\in \{0, 1, 0x80, 0x81\}.method_pointer_table: Scans for continuous runs of 64-bit pointers residing within the.textsegment (ImageBase + 0x1000 \le VA < ImageBase + Size).generic_inst_table: Scans for runs ofargc(0 < argc < 30) with 32-bit zero padding and valid pointer prefixes (0x01,0x00,0x7F).
6.2 Output Formats
1. Reconstructed C# Source (dump.cs)
Outputs complete class declarations with namespaces, type index comments, attributes, and field offsets:
// FirelfyShelter static asm parse
// Image 0: Assembly-CSharp.dll
// Namespace: RPG.Client
// TypeDefIndex: 1420
[Serializable]
public class AutoScrollRect : ScrollRect
{
// Fields
private Vector3[] KOIAJFFGPGF; // 0x180
// Methods
// RVA: 0x1febdf0 VA: 0x181febdf0
public void .ctor() { }
// RVA: 0x1feb700 VA: 0x181feb700
private void MNOGNALMGPL(AnimatorButton target) { }
// RVA: 0x1feb9c0 VA: 0x181feb9c0
public void SnapTo(GameObject target) { }
}
2. JSON Method Map (methods.json)
Outputs canonical method signatures mapped to hexadecimal RVAs:
{
"RPG.Client.AutoScrollRect::.ctor()": "0x1febdf0",
"RPG.Client.AutoScrollRect::MNOGNALMGPL(RPG.Client.AnimatorButton)": "0x1feb700",
"RPG.Client.AutoScrollRect::POFLDHKMLAF()": "0x1feb7a0",
"RPG.Client.AutoScrollRect::SetItemSelectCallback()": "0x1feb7f0",
"RPG.Client.AutoScrollRect::SnapTo(UnityEngine.GameObject)": "0x1feb9c0"
}
7. Comparative Analysis Matrix
| Feature | Firefly-Static-Parser |
honkai-dumper |
Standard Il2CppDumper |
|---|---|---|---|
| Execution Mode | Offline CLI | In-Process DLL | Offline CLI |
| Target Files | GameAssembly.dllglobal-metadata.datstartup-metadata.dat |
Running StarRail.exe process |
GameAssembly.dllglobal-metadata.dat |
MHY\0 Container Decryption |
Native Built-in (Algebraic & QWORD stream cipher) | Delegated to game process | Unsupported (Fails on magic mismatch) |
| Anti-Cheat Exposure | Zero | High (Process injection) | Zero |
| Key Extraction | Dynamic Semantic Discovery (via <Module>, ValueType) |
Memory query | Static pattern scanning |
| Method RVA Output | Canonical typed signatures in methods.json |
Flattened overloaded names in methods.json |
Script files for IDA/Ghidra |
| Portability | Multi-platform (Linux/Windows/macOS) | Windows-only (x86_64) | Multi-platform |
8. Integration & Protocol Engineering Synergy
8.1 Feedstock for FireflyProtoc
In the Star Rail reverse-engineering toolchain:
Firefly-Static-Parserextracts the exact class structures and obfuscated field names (e.g.,KOIAJFFGPGF\rightarrowWire tag1at offset0x180).- Engineers cross-reference these offsets with network serialization routines.
- The resulting proto definitions with wire masks (
[get: (id ^ 1) + 2]) and metadata (meta uint32 cmd_id = 143;) are compiled usingFireflyProtoc. - The generated high-performance C# or Go models are directly consumed by
March7thHoneyorGaC-hkrpg-go.
8.2 Compliance with Repository Rules (AGENTS.md)
Firefly-Static-Parseroperates purely as an offline analysis tool.- It contains no hardcoded user paths, machine IP addresses, or environment-dependent paths.
- It eliminates the need for prohibited live heartbeat scanning scripts in game client directories (
scripts/), providing a 100% compliant and safe research workflow.