From abbd264e4c51e857e1d77770528e59c55dd33a06 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Sat, 28 Mar 2026 18:05:25 +0100 Subject: [PATCH] Initial project setup Scaffold xex2tractor Rust project with cargo, add MIT license, README, and XEX2 file format documentation. Co-Authored-By: Claude Opus 4.6 --- .gitignore | 1 + Cargo.lock | 7 + Cargo.toml | 8 + LICENSE | 21 + README.md | 13 + doc/xex2_format.md | 967 +++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 3 + 7 files changed, 1020 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 doc/xex2_format.md create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..dc0426a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "xex2tractor" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..09e7515 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "xex2tractor" +version = "0.1.0" +edition = "2024" +description = "A tool for extracting and inspecting Xbox 360 XEX2 executable files" +license = "MIT" + +[dependencies] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..821d7cb --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Fabian + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..bf1f9a8 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# xex2tractor + +A tool for extracting and inspecting Xbox 360 XEX2 executable files, written in Rust. + +## Building + +```sh +cargo build --release +``` + +## License + +This project is licensed under the MIT License. See [LICENSE](LICENSE) for details. diff --git a/doc/xex2_format.md b/doc/xex2_format.md new file mode 100644 index 0000000..4b8c6b4 --- /dev/null +++ b/doc/xex2_format.md @@ -0,0 +1,967 @@ +# XEX2 File Format Documentation + +This document describes the XEX2 (Xbox 360 Executable) file format as implemented in the Xenia emulator. All multi-byte values in the XEX2 file are **big-endian** unless otherwise noted. The contained PE image uses big-endian values as well (PowerPC BE target). + +**Terminology:** +- **XEX offset**: byte offset from the start of the `.xex` file on disk +- **PE offset**: byte offset from the start of the decompressed/decrypted PE image (which begins at `xex2_header.header_size` in the raw file, but after decryption/decompression is loaded at the base address) +- **Memory address**: Xbox 360 virtual address (typically starting at the `load_address` from security info, e.g. `0x82000000`) + +--- + +## 1. Top-Level XEX2 File Layout + +``` ++==================================+ XEX offset 0x00 +| xex2_header | +| (magic, flags, header_size, | +| security_offset, opt headers) | ++----------------------------------+ XEX offset 0x18 +| Optional Headers Array | +| (header_count entries of | +| xex2_opt_header, 8 bytes each) | ++----------------------------------+ XEX offset = security_offset +| xex2_security_info | +| (RSA sig, AES key, pages, ...) | ++----------------------------------+ XEX offset varies +| Optional Header Data | +| (pointed to by opt headers) | ++==================================+ XEX offset = header_size +| Encrypted/Compressed PE Image | +| (the actual executable payload) | ++==================================+ XEX offset = end of file +``` + +--- + +## 2. Main XEX2 Header (`xex2_header`) + +Located at **XEX offset 0x00**. + +| Offset | Size | Field | Description | +|--------|------|-------|-------------| +| 0x00 | 4 | `magic` | Magic bytes: `XEX2` (0x58455832) | +| 0x04 | 4 | `module_flags` | Bitfield of `xex2_module_flags` (see below) | +| 0x08 | 4 | `header_size` | Total size of all headers in bytes. **The PE image data starts at this XEX offset.** | +| 0x0C | 4 | `reserved` | Reserved (typically 0) | +| 0x10 | 4 | `security_offset` | XEX offset to the `xex2_security_info` structure (from start of file) | +| 0x14 | 4 | `header_count` | Number of optional header entries following | +| 0x18 | 8 * N | `headers[N]` | Array of `xex2_opt_header` entries | + +### Module Flags (`xex2_module_flags`, bitmask) + +| Value | Name | Description | +|-------|------|-------------| +| 0x00000001 | `XEX_MODULE_TITLE` | Main game/app executable | +| 0x00000002 | `XEX_MODULE_EXPORTS_TO_TITLE` | Module exports functions to titles | +| 0x00000004 | `XEX_MODULE_SYSTEM_DEBUGGER` | System debugger module | +| 0x00000008 | `XEX_MODULE_DLL_MODULE` | DLL module | +| 0x00000010 | `XEX_MODULE_MODULE_PATCH` | Module patch | +| 0x00000020 | `XEX_MODULE_PATCH_FULL` | Full patch (replaces entire module) | +| 0x00000040 | `XEX_MODULE_PATCH_DELTA` | Delta patch (applies diffs) | +| 0x00000080 | `XEX_MODULE_USER_MODE` | User-mode module | + +--- + +## 3. Optional Header Entry (`xex2_opt_header`) + +Each entry is 8 bytes, located starting at **XEX offset 0x18**. + +| Offset | Size | Field | Description | +|--------|------|-------|-------------| +| 0x00 | 4 | `key` | Header key identifier (`xex2_header_keys` enum) | +| 0x04 | 4 | `value` / `offset` | Interpretation depends on low byte of `key` | + +### How the `value`/`offset` field is interpreted + +The **low byte** (`key & 0xFF`) determines the meaning: + +| Low byte | Meaning | +|----------|---------| +| `0x00` | The 4-byte `value` field **is** the data itself (inline uint32_t). | +| `0x01` | The `value` field **is** the data itself (stored in-place, pointer to the 4-byte field within the header). | +| Any other | `offset` is an XEX offset (from start of file) pointing to the actual header data structure. | + +### Optional Header Keys (`xex2_header_keys`) + +| Key Value | Name | Data Size/Type | Description | +|-----------|------|---------------|-------------| +| 0x000002FF | `XEX_HEADER_RESOURCE_INFO` | Variable | Embedded resource descriptors | +| 0x000003FF | `XEX_HEADER_FILE_FORMAT_INFO` | Variable | Encryption + compression info | +| 0x000005FF | `XEX_HEADER_DELTA_PATCH_DESCRIPTOR` | Variable | Delta patch descriptor | +| 0x00000405 | `XEX_HEADER_BASE_REFERENCE` | Variable | Base reference for patches | +| 0x00004304 | `XEX_HEADER_DISC_PROFILE_ID` | 4 bytes | Disc profile ID | +| 0x000080FF | `XEX_HEADER_BOUNDING_PATH` | Variable string | Bounding path | +| 0x00008105 | `XEX_HEADER_DEVICE_ID` | 20 bytes | Device ID | +| 0x00010001 | `XEX_HEADER_ORIGINAL_BASE_ADDRESS` | Inline u32 | Original PE base address | +| 0x00010100 | `XEX_HEADER_ENTRY_POINT` | Inline u32 | Program entry point (memory address) | +| 0x00010201 | `XEX_HEADER_IMAGE_BASE_ADDRESS` | Inline u32 | Load base address override | +| 0x000103FF | `XEX_HEADER_IMPORT_LIBRARIES` | Variable | Import library table | +| 0x00018002 | `XEX_HEADER_CHECKSUM_TIMESTAMP` | 8 bytes | Checksum + timestamp | +| 0x00018102 | `XEX_HEADER_ENABLED_FOR_CALLCAP` | 8 bytes | Callcap thunk addresses | +| 0x00018200 | `XEX_HEADER_ENABLED_FOR_FASTCAP` | Inline u32 | Fastcap enabled | +| 0x000183FF | `XEX_HEADER_ORIGINAL_PE_NAME` | Variable string | Original PE file name | +| 0x000200FF | `XEX_HEADER_STATIC_LIBRARIES` | Variable | Linked static library info | +| 0x00020104 | `XEX_HEADER_TLS_INFO` | 16 bytes | Thread-Local Storage info | +| 0x00020200 | `XEX_HEADER_DEFAULT_STACK_SIZE` | Inline u32 | Default stack size | +| 0x00020301 | `XEX_HEADER_DEFAULT_FILESYSTEM_CACHE_SIZE` | Inline u32 | FS cache size | +| 0x00020401 | `XEX_HEADER_DEFAULT_HEAP_SIZE` | Inline u32 | Default heap size | +| 0x00028002 | `XEX_HEADER_PAGE_HEAP_SIZE_AND_FLAGS` | 8 bytes | Page heap config | +| 0x00030000 | `XEX_HEADER_SYSTEM_FLAGS` | Inline u32 | System privilege flags | +| 0x00030100 | `XEX_HEADER_SYSTEM_FLAGS_32` | Inline u32 | Extended system flags (Kinect, etc.) | +| 0x00030200 | `XEX_HEADER_SYSTEM_FLAGS_64` | Inline u32 | 64-bit privilege flags | +| 0x00040006 | `XEX_HEADER_EXECUTION_INFO` | 24 bytes | Title ID, media ID, disc info | +| 0x00040201 | `XEX_HEADER_TITLE_WORKSPACE_SIZE` | Inline u32 | Title workspace size | +| 0x00040310 | `XEX_HEADER_GAME_RATINGS` | 64 bytes | Game content ratings | +| 0x00040404 | `XEX_HEADER_LAN_KEY` | 16 bytes | LAN encryption key | +| 0x000405FF | `XEX_HEADER_XBOX360_LOGO` | Variable | Xbox 360 logo bitmap | +| 0x000406FF | `XEX_HEADER_MULTIDISC_MEDIA_IDS` | Variable | Multi-disc media IDs | +| 0x000407FF | `XEX_HEADER_ALTERNATE_TITLE_IDS` | Variable | Alternate title IDs | +| 0x00040801 | `XEX_HEADER_ADDITIONAL_TITLE_MEMORY` | Inline u32 | Extra title memory | +| 0x00E10402 | `XEX_HEADER_EXPORTS_BY_NAME` | 8 bytes | PE export directory info | + +--- + +## 4. Security Info (`xex2_security_info`) + +Located at **XEX offset = `xex2_header.security_offset`** (from start of file). + +| Offset | Size | Field | Description | +|--------|------|-------|-------------| +| 0x000 | 4 | `header_size` | Size of this security info structure | +| 0x004 | 4 | `image_size` | Size of the decompressed PE image | +| 0x008 | 256 (0x100) | `rsa_signature` | RSA-2048 signature over the header | +| 0x108 | 4 | `unk_108` | Unknown (length field?) | +| 0x10C | 4 | `image_flags` | `xex2_image_flags` bitmask | +| 0x110 | 4 | `load_address` | Virtual memory address where the PE is loaded (e.g. 0x82000000) | +| 0x114 | 20 (0x14) | `section_digest` | SHA-1 digest of section data | +| 0x128 | 4 | `import_table_count` | Number of import table entries | +| 0x12C | 20 (0x14) | `import_table_digest` | SHA-1 digest of import table | +| 0x140 | 16 (0x10) | `xgd2_media_id` | XGD2 media identifier | +| 0x150 | 16 (0x10) | `aes_key` | **Encrypted AES-128 session key** (see Encryption section) | +| 0x160 | 4 | `export_table` | Memory address of the XEX export table (0 if none) | +| 0x164 | 20 (0x14) | `header_digest` | SHA-1 digest of header | +| 0x178 | 4 | `region` | Allowed regions (`xex2_region_flags`) | +| 0x17C | 4 | `allowed_media_types` | Allowed media types (`xex2_media_flags`) | +| 0x180 | 4 | `page_descriptor_count` | Number of page descriptors following | +| 0x184 | 24 * N | `page_descriptors[N]` | Array of `xex2_page_descriptor` entries | + +### Image Flags (`xex2_image_flags`, bitmask) + +| Value | Name | +|-------|------| +| 0x00000002 | Manufacturing utility | +| 0x00000004 | Manufacturing support tools | +| 0x00000008 | XGD2 media only | +| 0x00000100 | Cardea key | +| 0x00000200 | Xeika key | +| 0x00000400 | Usermode title | +| 0x00000800 | Usermode system | +| 0x10000000 | **4KB page size** (otherwise 64KB) | +| 0x20000000 | Region free | +| 0x40000000 | Revocation check optional | +| 0x80000000 | Revocation check required | + +### Region Flags (`xex2_region_flags`, bitmask) + +| Value | Region | +|-------|--------| +| 0x000000FF | NTSC/U (North America) | +| 0x0000FF00 | NTSC/J (Japan + Asia) | +| 0x00000100 | NTSC/J - Japan | +| 0x00000200 | NTSC/J - China | +| 0x00FF0000 | PAL (Europe) | +| 0x00010000 | PAL - Australia/New Zealand | +| 0xFF000000 | Other regions | +| 0xFFFFFFFF | All regions (region-free) | + +### Media Flags (`xex2_media_flags`, bitmask) + +| Value | Media Type | +|-------|------------| +| 0x00000001 | Hard disk | +| 0x00000002 | DVD X2 | +| 0x00000004 | DVD/CD | +| 0x00000008 | DVD-5 | +| 0x00000010 | DVD-9 | +| 0x00000020 | System flash | +| 0x00000080 | Memory unit | +| 0x00000100 | USB mass storage | +| 0x00000200 | Network | +| 0x00000400 | Direct from memory | +| 0x00000800 | RAM drive | +| 0x00001000 | SVOD | +| 0x01000000 | Insecure package | +| 0x02000000 | Savegame package | +| 0x04000000 | Locally signed package | +| 0x08000000 | LIVE signed package | +| 0x10000000 | Xbox package | + +--- + +## 5. Page Descriptors (`xex2_page_descriptor`) + +Each page descriptor is **24 bytes** and immediately follows `page_descriptor_count` in the security info (starting at **XEX offset = security_offset + 0x184**). + +``` ++----------------------------------+ +| Bits 31-28 | Bits 27-0 | 0x00 (4 bytes, combined bitfield) +| info (4b) | page_count (28b) | ++----------------------------------+ +| data_digest (20 bytes, SHA-1) | 0x04 ++----------------------------------+ +``` + +| Offset | Size | Field | Description | +|--------|------|-------|-------------| +| 0x00 | 4 | `value` | Combined bitfield (big-endian, must be byte-swapped before reading bits) | +| | | `.info` (bits 31-28) | Section type: 1=Code, 2=Data, 3=Read-only data | +| | | `.page_count` (bits 27-0) | Number of pages in this section | +| 0x04 | 20 | `data_digest` | SHA-1 hash of the page data | + +**Page size** is determined by `XEX_IMAGE_PAGE_SIZE_4KB` in image flags: +- If set: **4 KB** (0x1000) pages +- If not set: **64 KB** (0x10000) pages + +**Memory mapping**: Pages are mapped sequentially starting at `load_address`. For page descriptor `i`, the memory address is: +``` +address = load_address + (sum of all previous page_counts) * page_size +size = desc.page_count * page_size +``` + +Section types determine memory protection: +| Type | Value | Protection | +|------|-------|------------| +| `XEX_SECTION_CODE` | 1 | Read-only (or Read+Write if writable_code_segments) | +| `XEX_SECTION_DATA` | 2 | Read + Write | +| `XEX_SECTION_READONLY_DATA` | 3 | Read-only | + +--- + +## 6. Encryption + +### Overview + +XEX2 uses a **two-level AES-128-CBC** encryption scheme: + +1. The **session key** (per-XEX) is stored encrypted in `xex2_security_info.aes_key` (at security info offset 0x150). +2. This session key is itself encrypted with one of the well-known **master keys**. +3. The session key is then used to decrypt the PE image payload. + +### Master AES-128 Keys + +| Key | Value (hex) | Usage | +|-----|-------------|-------| +| **XEX2 Retail** | `20 B1 85 A5 9D 28 FD C3 40 58 3F BB 08 96 BF 91` | Production/retail XEX2 files | +| **XEX2 DevKit** | `00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00` | Development kit XEX2 files (null key) | +| **XEX1 Retail** | `A2 6C 10 F7 1F D9 35 E9 8B 99 92 2C E9 32 15 72` | Legacy XEX1 format | + +### Key Derivation Process + +``` +1. Read encrypted_session_key from xex2_security_info.aes_key[0x10] +2. Decrypt encrypted_session_key using master_key with AES-128-CBC (IV = 0) + → This yields the session_key[16] +3. Use session_key to decrypt the PE image payload with AES-128-CBC (IV = 0) +``` + +### AES-128-CBC Decryption Algorithm + +The decryption used is standard **AES-128 in CBC mode** with a **zero IV** (16 bytes of 0x00): + +``` +Input: session_key[16], ciphertext, length +State: IV[16] = {0, 0, ..., 0} + rk[] = rijndaelKeySetupDec(session_key, 128) // 128-bit key + +For each 16-byte block: + plaintext_block = rijndaelDecrypt(rk, Nr, ciphertext_block) + plaintext_block ^= IV // XOR with previous ciphertext (or IV for first block) + IV = ciphertext_block // Update IV to current ciphertext +``` + +**Implementation**: Uses the Rijndael reference implementation (`rijndael-alg-fst.c`), with `Nr` rounds returned by `rijndaelKeySetupDec()` (10 rounds for AES-128). + +### Key Trial Order + +The loader tries keys in this order, falling back on failure: +1. XEX2 Retail key +2. XEX2 DevKit key (all zeros) +3. XEX1 Retail key + +Success is determined by checking if the decrypted image begins with a valid PE signature (`MZ` / 0x5A4D). + +### Encryption Type (`xex2_encryption_type`) + +Stored in `xex2_opt_file_format_info.encryption_type`: + +| Value | Name | Description | +|-------|------|-------------| +| 0 | `XEX_ENCRYPTION_NONE` | PE image is not encrypted | +| 1 | `XEX_ENCRYPTION_NORMAL` | PE image is AES-128-CBC encrypted | + +--- + +## 7. Compression + +Compression type is stored in `xex2_opt_file_format_info.compression_type`. + +### File Format Info (`xex2_opt_file_format_info`) + +Pointed to by optional header key `0x000003FF` (`XEX_HEADER_FILE_FORMAT_INFO`). + +| Offset | Size | Field | Description | +|--------|------|-------|-------------| +| 0x00 | 4 | `info_size` | Total size of this structure | +| 0x04 | 2 | `encryption_type` | 0=None, 1=Normal (AES-128-CBC) | +| 0x06 | 2 | `compression_type` | 0=None, 1=Basic, 2=Normal, 3=Delta | +| 0x08 | ... | `compression_info` | Union: basic or normal compression info | + +### Compression Types + +| Value | Name | Description | +|-------|------|-------------| +| 0 | `XEX_COMPRESSION_NONE` | No compression; raw PE image data | +| 1 | `XEX_COMPRESSION_BASIC` | Block-based zero-fill compression | +| 2 | `XEX_COMPRESSION_NORMAL` | LZX (Lempel-Ziv extended) compression with SHA-1 block chaining | +| 3 | `XEX_COMPRESSION_DELTA` | Delta patch compression (for update patches) | + +--- + +### 7a. No Compression (`XEX_COMPRESSION_NONE`) + +The PE image starts at **XEX offset = `header_size`** and extends to end of file. The raw data length is `xex_file_size - header_size`. If encrypted, the entire payload is decrypted in-place with AES-128-CBC using the session key. + +--- + +### 7b. Basic Compression (`XEX_COMPRESSION_BASIC`) + +The compression info contains an array of block descriptors that describe alternating data and zero-filled regions. + +#### Basic Compression Block (`xex2_file_basic_compression_block`) + +Located at `xex2_opt_file_format_info` offset 0x08. The number of blocks is `(info_size - 8) / 8`. + +| Offset | Size | Field | Description | +|--------|------|-------|-------------| +| 0x00 | 4 | `data_size` | Bytes of real data to copy from the XEX payload | +| 0x04 | 4 | `zero_size` | Bytes of zeros to append after the data | + +**Decompression process** (after AES-128-CBC decryption if `encryption_type == NORMAL`): + +``` +source_ptr = XEX file + header_size // start of PE payload in XEX file +dest_ptr = base_address in memory // Xbox 360 virtual memory + +For each block[i]: + Copy block[i].data_size bytes from source_ptr to dest_ptr + Advance source_ptr by data_size + Zero-fill block[i].zero_size bytes at dest_ptr + data_size + Advance dest_ptr by (data_size + zero_size) +``` + +The total uncompressed size = sum of all `(data_size + zero_size)` across all blocks. + +**Note on encryption with basic compression**: When encryption is `NORMAL`, the AES-128-CBC decryption is performed **inline per block** — the CBC IV state carries across block boundaries (it is NOT reset per block). The same `session_key` and continuous CBC state are used. + +--- + +### 7c. Normal Compression (`XEX_COMPRESSION_NORMAL`) + +This is a two-stage process: de-blocking, then LZX decompression. + +#### Normal Compression Info (`xex2_file_normal_compression_info`) + +Located at `xex2_opt_file_format_info` offset 0x08: + +| Offset | Size | Field | Description | +|--------|------|-------|-------------| +| 0x00 | 4 | `window_size` | LZX decompression window size in bytes (must be power of 2) | +| 0x04 | 4 | `first_block.block_size` | Size of the first compressed block in bytes | +| 0x08 | 20 | `first_block.block_hash` | SHA-1 hash of the first block's data | + +#### Compressed Block Info (`xex2_compressed_block_info`) + +Each block in the compressed stream is described by a chained structure: + +| Offset | Size | Field | Description | +|--------|------|-------|-------------| +| 0x00 | 4 | `block_size` | Total size of *this* block in bytes (0 = end of chain) | +| 0x04 | 20 | `block_hash` | SHA-1 hash of *this* block's data | + +**Block chaining**: The `block_size` and `block_hash` of the *next* block are stored at the **beginning** of the *current* block's data. This creates a hash chain for integrity verification. + +#### Decompression Process + +``` +1. DECRYPT (if encrypted): + Decrypt the entire PE payload (XEX file + header_size, length = file_size - header_size) + using AES-128-CBC with session_key and zero IV. + +2. DE-BLOCK: + current_block_info = first_block from compression_info header + source_ptr = start of decrypted payload + dest_buffer = temporary buffer + + While current_block_info.block_size != 0: + a. Verify SHA-1(source_ptr, current_block_info.block_size) == current_block_info.block_hash + b. Read next_block_info from source_ptr: + next_block_size = bytes [0..3] (4 bytes) + next_block_hash = bytes [4..23] (20 bytes) + c. Skip past block header (4 + 20 = 24 bytes) + d. Read data chunks: + While true: + chunk_size = read 2 bytes (big-endian uint16) + If chunk_size == 0: break (end of block) + Copy chunk_size bytes to dest_buffer + e. Advance source_ptr to: previous source_ptr + current_block_info.block_size + f. current_block_info = next_block_info + +3. LZX DECOMPRESS: + Decompress dest_buffer using LZX algorithm: + - Input: de-blocked data + - Output size: image_size (from page descriptors sum) + - Window size: compression_info.normal.window_size + - Reset interval: 0 (no reset) + - Frame size: 0x8000 (32 KB) + + Output is written to memory at base_address. +``` + +#### LZX Algorithm Details + +- **Algorithm**: LZX (Lempel-Ziv Extended), the same algorithm used in Microsoft CAB files +- **Implementation**: mspack library (`lzxd.c`) +- **Window size**: Specified per-XEX in `window_size` field (typically a power of 2; common values include 0x20000 = 128KB) +- **Window bits**: `log2(window_size)` — computed via bit scan +- **Frame size**: Fixed at `0x8000` (32,768 bytes) +- **Reset interval**: 0 (no periodic state reset) + +--- + +### 7d. Delta Compression / Patching (`XEX_COMPRESSION_DELTA`) + +Used for XEX patches (XEXP files). The patch XEX has `XEX_MODULE_PATCH_DELTA` set in module_flags. + +#### Delta Patch Descriptor (`xex2_opt_delta_patch_descriptor`) + +Pointed to by optional header key `0x000005FF`: + +| Offset | Size | Field | Description | +|--------|------|-------|-------------| +| 0x00 | 4 | `size` | Size of the header patch data | +| 0x04 | 4 | `target_version_value` | Target version after patch (xex2_version bitfield) | +| 0x08 | 4 | `source_version_value` | Source version required (xex2_version bitfield) | +| 0x0C | 20 | `digest_source` | SHA-1 digest of source image | +| 0x20 | 16 | `image_key_source` | Key verification data | +| 0x30 | 4 | `size_of_target_headers` | Size of target XEX headers after patch | +| 0x34 | 4 | `delta_headers_source_offset` | Offset within source XEX headers to copy from | +| 0x38 | 4 | `delta_headers_source_size` | Size of source header data to copy | +| 0x3C | 4 | `delta_headers_target_offset` | Offset within target XEX headers to copy to | +| 0x40 | 4 | `delta_image_source_offset` | Offset within source PE image to copy from | +| 0x44 | 4 | `delta_image_source_size` | Size of source image data to copy | +| 0x48 | 4 | `delta_image_target_offset` | Offset within target PE image to copy to | +| 0x4C | ... | `info` | First `xex2_delta_patch` entry (inline) | + +#### Delta Patch Entry (`xex2_delta_patch`) + +| Offset | Size | Field | Description | +|--------|------|-------|-------------| +| 0x00 | 4 | `old_addr` | Offset in the **existing memory image** to read source data from | +| 0x04 | 4 | `new_addr` | Offset in the **existing memory image** to write patched data to | +| 0x08 | 2 | `uncompressed_len` | Size of decompressed output | +| 0x0A | 2 | `compressed_len` | Size of compressed patch data (special values below) | +| 0x0C | ... | `patch_data` | Compressed patch data (variable length) | + +**Special `compressed_len` values:** +| Value | Action | +|-------|--------| +| 0 | Zero-fill: `memset(dest + new_addr, 0, uncompressed_len)` | +| 1 | Copy: `memcpy(dest + new_addr, dest + old_addr, uncompressed_len)` | +| >= 2 | LZX delta decompress: decompress `patch_data` using `old_addr` data as window reference | + +#### Delta Patch Key Handling + +Delta patches use a three-level key scheme: +1. **Base module's session key** is decrypted using the master key (as normal) +2. The **patch's encrypted AES key** is then decrypted using the **base module's session key** (not the master key) +3. Verification: `AES_Decrypt(base_session_key, patch_descriptor.image_key_source)` must equal the **original** session key of the base module + +--- + +## 8. Import Libraries + +Located via optional header key `0x000103FF` (`XEX_HEADER_IMPORT_LIBRARIES`). + +### Import Libraries Container (`xex2_opt_import_libraries`) + +| Offset | Size | Field | Description | +|--------|------|-------|-------------| +| 0x00 | 4 | `size` | Total size of the import libraries structure | +| 0x04 | 4 | `string_table.size` | Size of the string table in bytes | +| 0x08 | 4 | `string_table.count` | Number of strings in the table | +| 0x0C | N | `string_table.data` | Null-terminated strings, 4-byte aligned with padding | + +Library entries follow immediately after the string table (at offset `string_table.size + 12`). + +### Import Library (`xex2_import_library`) + +Each library entry: + +| Offset | Size | Field | Description | +|--------|------|-------|-------------| +| 0x00 | 4 | `size` | Size of this library entry in bytes | +| 0x04 | 20 | `next_import_digest` | SHA-1 digest of next import entry | +| 0x18 | 4 | `id` | Library ID | +| 0x1C | 4 | `version_value` | Library version (xex2_version bitfield) | +| 0x20 | 4 | `version_min_value` | Minimum required version | +| 0x24 | 2 | `name_index` | Index (low byte) into the string table | +| 0x26 | 2 | `count` | Number of import records | +| 0x28 | 4 * N | `import_table[N]` | Array of import record **memory addresses** | + +### Import Record Format (in memory) + +Each entry in `import_table` is a **memory address** pointing to a location within the loaded PE image. At that memory address, the value is: + +``` +Bits 31-24 (byte 0): record_type + 0x00 = Variable import + 0x01 = Thunk (function) import +Bits 15-0 (bytes 2-3): ordinal number +``` + +**Variable imports** (record_type == 0): The memory slot is overwritten with: +- For kernel exports (implemented): the variable's address +- For kernel exports (not implemented): `0xD000BEEF | (ordinal & 0xFFF) << 16` +- For user module exports: the export address +- For unresolved imports: `0xF00DF00D` + +**Thunk imports** (record_type == 1): The 16-byte thunk in memory is originally: +``` ++0x00: li r3, 0 // 0x38600000 ++0x04: li r4, // 0x38800000 | ordinal ++0x08: mtspr CTR, r11 // 0x7D6903A6 ++0x0C: bctr // 0x4E800420 +``` + +For user module imports, this is rewritten to: +``` ++0x00: lis r11, // 0x3D600000 | (addr >> 16) ++0x04: ori r11, r11, // 0x616B0000 | (addr & 0xFFFF) ++0x08: mtspr CTR, r11 // (unchanged) ++0x0C: bctr // (unchanged) +``` + +Import records alternate: variable descriptor, then thunk address, then next variable descriptor, etc. + +--- + +## 9. Export Table (`xex2_export_table`) + +Located at the **memory address** specified in `xex2_security_info.export_table`. This is a virtual address, NOT a file offset. + +| Offset | Size | Field | Description | +|--------|------|-------|-------------| +| 0x00 | 12 | `magic[3]` | Magic identifier (3 uint32_t values) | +| 0x0C | 8 | `modulenumber[2]` | Module number (2 uint32_t values) | +| 0x14 | 12 | `version[3]` | Version info (3 uint32_t values) | +| 0x20 | 4 | `imagebaseaddr` | Image base address (must be shifted left 16 bits to get actual address) | +| 0x24 | 4 | `count` | Number of exports | +| 0x28 | 4 | `base` | Base ordinal number | +| 0x2C | 4 * N | `ordOffset[N]` | Array of ordinal offsets | + +**Resolving an export address:** +``` +function_address = ordOffset[ordinal - base] + (imagebaseaddr << 16) +``` + +### PE Export Directory (`X_IMAGE_EXPORT_DIRECTORY`) + +An alternative export mechanism via the PE header (optional header key `XEX_HEADER_EXPORTS_BY_NAME`). The `xex2_opt_data_directory` at that key contains: +- `offset`: RVA from PE base to the `X_IMAGE_EXPORT_DIRECTORY` +- `size`: Size of the export directory + +The export directory is standard PE format (little-endian within the Xbox PE): + +| Offset | Size | Field | +|--------|------|-------| +| 0x00 | 4 | Characteristics | +| 0x04 | 4 | TimeDateStamp | +| 0x08 | 2 | MajorVersion | +| 0x0A | 2 | MinorVersion | +| 0x0C | 4 | Name (RVA) | +| 0x10 | 4 | Base ordinal | +| 0x14 | 4 | NumberOfFunctions | +| 0x18 | 4 | NumberOfNames | +| 0x1C | 4 | AddressOfFunctions (RVA from export directory) | +| 0x20 | 4 | AddressOfNames (RVA from export directory) | +| 0x24 | 4 | AddressOfNameOrdinals (RVA from export directory) | + +--- + +## 10. Specific Optional Header Structures + +### Execution Info (`xex2_opt_execution_info`) — Key 0x00040006 + +24 bytes (0x18). All offsets relative to start of structure. + +| Offset | Size | Field | Description | +|--------|------|-------|-------------| +| 0x00 | 4 | `media_id` | Media identifier | +| 0x04 | 4 | `version_value` | Module version (xex2_version bitfield) | +| 0x08 | 4 | `base_version_value` | Base version | +| 0x0C | 4 | `title_id` | Title ID (e.g. 0x415607D1) | +| 0x10 | 1 | `platform` | Platform identifier | +| 0x11 | 1 | `executable_table` | Executable table index | +| 0x12 | 1 | `disc_number` | Current disc number | +| 0x13 | 1 | `disc_count` | Total disc count | +| 0x14 | 4 | `savegame_id` | Savegame identifier | + +### Version Bitfield (`xex2_version`) + +Packed into a 32-bit big-endian value: + +| Bits | Field | Width | +|------|-------|-------| +| 31-28 | `major` | 4 bits | +| 27-24 | `minor` | 4 bits | +| 23-8 | `build` | 16 bits | +| 7-0 | `qfe` | 8 bits | + +### TLS Info (`xex2_opt_tls_info`) — Key 0x00020104 + +16 bytes (0x10): + +| Offset | Size | Field | Description | +|--------|------|-------|-------------| +| 0x00 | 4 | `slot_count` | Number of TLS slots | +| 0x04 | 4 | `raw_data_address` | Memory address of TLS raw data | +| 0x08 | 4 | `data_size` | Total TLS data size | +| 0x0C | 4 | `raw_data_size` | Size of initialized TLS data | + +### Checksum / Timestamp (`xex2_opt_checksum_timedatestamp`) — Key 0x00018002 + +8 bytes: + +| Offset | Size | Field | Description | +|--------|------|-------|-------------| +| 0x00 | 4 | `checksum` | Module checksum | +| 0x04 | 4 | `timedatestamp` | Unix timestamp of build | + +### Resource Info (`xex2_opt_resource_info`) — Key 0x000002FF + +Variable size. Resource count = `(size - 4) / 16`. + +| Offset | Size | Field | Description | +|--------|------|-------|-------------| +| 0x00 | 4 | `size` | Total size of resource info | +| 0x04 | 16 * N | `resources[N]` | Array of `xex2_resource` | + +Each `xex2_resource` (16 bytes): + +| Offset | Size | Field | Description | +|--------|------|-------|-------------| +| 0x00 | 8 | `name` | Resource name (null-padded) | +| 0x08 | 4 | `address` | Memory address of resource | +| 0x0C | 4 | `size` | Size of resource in bytes | + +### Static Libraries (`xex2_opt_static_libraries`) — Key 0x000200FF + +| Offset | Size | Field | Description | +|--------|------|-------|-------------| +| 0x00 | 4 | `size` | Total size. Library count = (size - 4) / 16 | +| 0x04 | 16 * N | `libraries[N]` | Array of `xex2_opt_static_library` | + +Each `xex2_opt_static_library` (16 bytes / 0x10): + +| Offset | Size | Field | Description | +|--------|------|-------|-------------| +| 0x00 | 8 | `name` | Library name (null-padded) | +| 0x08 | 2 | `version_major` | Major version | +| 0x0A | 2 | `version_minor` | Minor version | +| 0x0C | 2 | `version_build` | Build number | +| 0x0E | 1 | `approval_type` | 0=Unapproved, 1=Possible, 2=Approved, 3=Expired | +| 0x0F | 1 | `version_qfe` | QFE version | + +### LAN Key (`xex2_opt_lan_key`) — Key 0x00040404 + +16 bytes: raw AES-128 key used for LAN multiplayer encryption. + +### Game Ratings (`xex2_game_ratings_t`) — Key 0x00040310 + +64 bytes (0x40) containing age ratings for various regional rating boards: + +| Offset | Size | Board | +|--------|------|-------| +| 0x00 | 1 | ESRB (North America) | +| 0x01 | 1 | PEGI (Europe) | +| 0x02 | 1 | PEGI Finland | +| 0x03 | 1 | PEGI Portugal | +| 0x04 | 1 | BBFC (UK/Ireland) | +| 0x05 | 1 | CERO (Japan) | +| 0x06 | 1 | USK (Germany) | +| 0x07 | 1 | OFLC Australia | +| 0x08 | 1 | OFLC New Zealand | +| 0x09 | 1 | KMRB (South Korea) | +| 0x0A | 1 | Brazil | +| 0x0B | 1 | FPB (South Africa) | +| 0x0C | 52 | Reserved / Unknown | + +Each rating value is 0xFF for "Unrated". + +### Callcap Imports (`xex2_opt_call_cap_imports`) — Key 0x00018102 + +8 bytes: + +| Offset | Size | Field | Description | +|--------|------|-------|-------------| +| 0x00 | 4 | `start_func_thunk_addr` | Memory address of start function thunk | +| 0x04 | 4 | `end_func_thunk_addr` | Memory address of end function thunk | + +### Data Directory (`xex2_opt_data_directory`) — Key 0x00E10402 + +8 bytes, used for PE exports-by-name: + +| Offset | Size | Field | Description | +|--------|------|-------|-------------| +| 0x00 | 4 | `offset` | RVA from PE image base | +| 0x04 | 4 | `size` | Size of the directory | + +### Bound Path (`xex2_opt_bound_path`) — Key 0x000080FF + +Variable length: + +| Offset | Size | Field | Description | +|--------|------|-------|-------------| +| 0x00 | 4 | `size` | Total size including this field | +| 0x04 | N | `path` | Null-terminated path string | + +### Original PE Name (`xex2_opt_original_pe_name`) — Key 0x000183FF + +Variable length: + +| Offset | Size | Field | Description | +|--------|------|-------|-------------| +| 0x00 | 4 | `size` | Total size including this field | +| 0x04 | N | `name` | Null-terminated original PE filename | + +--- + +## 11. PE Image (After Decryption/Decompression) + +After decryption and decompression, the PE image is loaded into Xbox 360 virtual memory at `load_address` (from security info, or overridden by `XEX_HEADER_IMAGE_BASE_ADDRESS`). + +### PE Headers (in memory at `load_address`) + +The PE image is a standard 32-bit PE executable for PowerPC Big-Endian: + +#### DOS Header (at memory base address) + +| Offset | Size | Field | Description | +|--------|------|-------|-------------| +| 0x00 | 2 | `e_magic` | `MZ` signature (0x5A4D) — note: stored as 0x905A4D with byte swap check | +| ... | ... | ... | Standard DOS header fields | +| 0x3C | 4 | `e_lfanew` | Offset to NT headers (from start of PE image) | + +#### NT Headers (at PE offset `e_lfanew`) + +| Offset | Size | Field | Description | +|--------|------|-------|-------------| +| 0x00 | 4 | `Signature` | `PE\0\0` (0x00004550) | +| 0x04 | 20 | `FileHeader` | COFF file header | +| 0x18 | 224 | `OptionalHeader` | PE32 optional header | + +#### File Header Validation + +| Field | Expected Value | Description | +|-------|---------------|-------------| +| `Machine` | 0x01F2 | `IMAGE_FILE_MACHINE_POWERPCBE` | +| `Characteristics` | bit 0x0100 set | `IMAGE_FILE_32BIT_MACHINE` | +| `SizeOfOptionalHeader` | 224 (0xE0) | Standard PE32 optional header size | + +#### Optional Header Validation + +| Field | Expected Value | +|-------|---------------| +| `Magic` | 0x10B (`IMAGE_NT_OPTIONAL_HDR32_MAGIC`) | +| `Subsystem` | 14 (`IMAGE_SUBSYSTEM_XBOX`) | + +#### Section Headers + +Located immediately after the optional header. Each section header is 40 bytes: + +| Offset | Size | Field | Description | +|--------|------|-------|-------------| +| 0x00 | 8 | `Name` | Section name (e.g. `.text`, `.rdata`, `.data`) | +| 0x08 | 4 | `VirtualSize` | Size in memory | +| 0x0C | 4 | `VirtualAddress` | RVA from PE image base | +| 0x10 | 4 | `SizeOfRawData` | Size of raw data | +| 0x14 | 4 | `PointerToRawData` | PE offset to raw data | +| 0x18 | 4 | `PointerToRelocations` | (not used) | +| 0x1C | 4 | `PointerToLinenumbers` | (not used) | +| 0x20 | 2 | `NumberOfRelocations` | (not used) | +| 0x22 | 2 | `NumberOfLinenumbers` | (not used) | +| 0x24 | 4 | `Characteristics` | Section flags | + +Section characteristics relevant to Xbox 360: + +| Value | Name | +|-------|------| +| 0x00000020 | Contains code | +| 0x00000040 | Contains initialized data | +| 0x00000080 | Contains uninitialized data | +| 0x20000000 | Memory execute | +| 0x40000000 | Memory read | +| 0x80000000 | Memory write | + +The in-memory address of a section is: `load_address + VirtualAddress` + +--- + +## 12. System Flags (`xex2_system_flags`) + +Inline u32 at optional header key `0x00030000`. Bitmask of system privileges: + +| Value | Name | Description | +|-------|------|-------------| +| 0x00000001 | NO_FORCED_REBOOT | | +| 0x00000002 | FOREGROUND_TASKS | | +| 0x00000004 | NO_ODD_MAPPING | | +| 0x00000008 | HANDLE_MCE_INPUT | | +| 0x00000010 | RESTRICTED_HUD_FEATURES | | +| 0x00000020 | HANDLE_GAMEPAD_DISCONNECT | | +| 0x00000040 | INSECURE_SOCKETS | | +| 0x00000080 | XBOX1_INTEROPERABILITY | | +| 0x00000100 | DASH_CONTEXT | | +| 0x00000200 | USES_GAME_VOICE_CHANNEL | | +| 0x00000400 | PAL50_INCOMPATIBLE | | +| 0x00000800 | INSECURE_UTILITY_DRIVE | | +| 0x00001000 | XAM_HOOKS | | +| 0x00002000 | ACCESS_PII | | +| 0x00004000 | CROSS_PLATFORM_SYSTEM_LINK | | +| 0x00008000 | MULTIDISC_SWAP | | +| 0x00010000 | MULTIDISC_INSECURE_MEDIA | | +| 0x00020000 | AP25_MEDIA | Anti-piracy 2.5 media check | +| 0x00040000 | NO_CONFIRM_EXIT | | +| 0x00080000 | ALLOW_BACKGROUND_DOWNLOAD | | +| 0x00100000 | CREATE_PERSISTABLE_RAMDRIVE | | +| 0x00200000 | INHERIT_PERSISTENT_RAMDRIVE | | +| 0x00400000 | ALLOW_HUD_VIBRATION | | +| 0x00800000 | ACCESS_UTILITY_PARTITIONS | | +| 0x01000000 | IPTV_INPUT_SUPPORTED | | +| 0x02000000 | PREFER_BIG_BUTTON_INPUT | | +| 0x04000000 | ALLOW_EXTENDED_SYSTEM_RESERVATION | | +| 0x08000000 | MULTIDISC_CROSS_TITLE | | +| 0x10000000 | INSTALL_INCOMPATIBLE | | +| 0x20000000 | ALLOW_AVATAR_GET_METADATA_BY_XUID | | +| 0x40000000 | ALLOW_CONTROLLER_SWAPPING | | +| 0x80000000 | DASH_EXTENSIBILITY_MODULE | | + +### Extended System Flags (32-bit) — Key 0x00030100 + +| Value | Name | +|-------|------| +| 0x00000001 | ALLOW_NETWORK_READ_CANCEL | +| 0x00000002 | UNINTERRUPTABLE_READS | +| 0x00000004 | REQUIRE_FULL_EXPERIENCE | +| 0x00000008 | GAME_VOICE_REQUIRED_UI | +| 0x00000010 | TITLE_SET_PRESENCE_STRING | +| 0x00000020 | CAMERA_ANGLE_CONTROL | +| 0x00000040 | SKELETAL_TRACKING_REQUIRED | +| 0x00000080 | SKELETAL_TRACKING_SUPPORTED | + +--- + +## 13. Complete Loading Sequence + +Here is the full loading process as implemented by Xenia: + +``` +1. READ HEADER + a. Read xex2_header from offset 0 + b. Verify magic == "XEX2" (0x58455832) + c. Copy entire header region (header_size bytes) into memory + +2. PARSE SECURITY INFO + a. Navigate to xex2_header.security_offset + b. Extract: RSA signature, encrypted AES key, load_address, image_flags, + export_table address, page_descriptors + c. Determine base_address: use XEX_HEADER_IMAGE_BASE_ADDRESS if present, + otherwise security_info.load_address + +3. DECRYPT & DECOMPRESS PE IMAGE + a. Determine encryption/compression from XEX_HEADER_FILE_FORMAT_INFO + b. Derive session key: + session_key = AES-128-CBC-Decrypt(master_key, security_info.aes_key) + Try retail key first, then devkit, then XEX1 key + c. Based on compression_type: + - NONE: decrypt payload directly to base_address + - BASIC: decrypt + zero-fill blocks to base_address + - NORMAL: decrypt → de-block → LZX decompress to base_address + d. For patches: store raw patch data for later application + +4. VERIFY PE IMAGE + a. Check for MZ signature (0x5A4D) at base_address + b. If not valid PE and not a patch, loading fails + +5. APPLY PATCHES (if applicable) + a. Patch XEX headers using LZX delta + b. Re-derive session keys for patched module + c. Decrypt and apply image delta patches block by block + d. Verify block hashes (SHA-1) at each step + +6. PARSE PE HEADERS (LoadContinue) + a. Verify DOS header (MZ), NT headers (PE\0\0) + b. Verify Machine == POWERPCBE, Subsystem == XBOX + c. Extract all PE sections (name, VA, size, flags) + +7. SETUP MEMORY PROTECTION + a. For each page_descriptor: + - CODE / READONLY_DATA → Read-only + - DATA → Read + Write + b. Track low_address (first code page) and high_address (last code page) + +8. RESOLVE IMPORTS + a. Parse XEX_HEADER_IMPORT_LIBRARIES + b. For each import library: + - Parse string table for library names + - Load dependent user modules if not already loaded + - For each import record: + * Variable (type 0): write resolved address to memory slot + * Thunk (type 1): declare function, optionally rewrite PPC branch code + +9. SETUP EXPORTS + a. If security_info.export_table != 0: XEX export table is in memory + b. If XEX_HEADER_EXPORTS_BY_NAME present: PE export directory is available +``` + +--- + +## 14. Integrity Verification (SHA-1) + +SHA-1 is used throughout the format for data integrity: + +| Location | What is Hashed | Hash Location | +|----------|---------------|---------------| +| Page descriptors | Each page's data in memory | `xex2_page_descriptor.data_digest` (20 bytes) | +| Security info | Section data | `xex2_security_info.section_digest` | +| Security info | Import table | `xex2_security_info.import_table_digest` | +| Security info | Header data | `xex2_security_info.header_digest` | +| Normal compression | Each compressed block | `xex2_compressed_block_info.block_hash` (chain) | +| Delta patches | Each patch block | `xex2_compressed_block_info.block_hash` | +| Import libraries | Next import entry | `xex2_import_library.next_import_digest` | +| Delta patches | Source image | `xex2_opt_delta_patch_descriptor.digest_source` | + +--- + +## 15. Key Source Files + +| File | Purpose | +|------|---------| +| `src/xenia/kernel/util/xex2_info.h` | All XEX2 structure definitions, enums, and flags | +| `src/xenia/cpu/xex_module.h` | XexModule class, SecurityInfoContext, ImportLibrary structures | +| `src/xenia/cpu/xex_module.cc` | Main loading logic, AES keys, decryption, decompression dispatch | +| `src/xenia/cpu/lzx.h` / `lzx.cc` | LZX decompression and delta patch application | +| `src/xenia/base/pe_image.h` | PE (DOS/NT/Section) header structures | +| `third_party/crypto/rijndael-alg-fst.c` | AES (Rijndael) cipher implementation | +| `third_party/mspack/lzxd.c` | LZX decompression engine | diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +}