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 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-03-28 18:05:25 +01:00
commit abbd264e4c
7 changed files with 1020 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

7
Cargo.lock generated Normal file
View File

@@ -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"

8
Cargo.toml Normal file
View File

@@ -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]

21
LICENSE Normal file
View File

@@ -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.

13
README.md Normal file
View File

@@ -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.

967
doc/xex2_format.md Normal file
View File

@@ -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, <ordinal> // 0x38800000 | ordinal
+0x08: mtspr CTR, r11 // 0x7D6903A6
+0x0C: bctr // 0x4E800420
```
For user module imports, this is rewritten to:
```
+0x00: lis r11, <addr_hi> // 0x3D600000 | (addr >> 16)
+0x04: ori r11, r11, <addr_lo>// 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 |

3
src/main.rs Normal file
View File

@@ -0,0 +1,3 @@
fn main() {
println!("Hello, world!");
}