Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
771d401fb1 | ||
|
|
a951027aeb |
65
Cargo.lock
generated
65
Cargo.lock
generated
@@ -194,6 +194,12 @@ version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.183"
|
||||
@@ -206,6 +212,12 @@ version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b29dffab797218e12e4df08ef5d15ab9efca2504038b1b32b9b32fc844b39c9"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
@@ -230,6 +242,49 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
@@ -288,10 +343,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "xex2tractor"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"cbc",
|
||||
"clap",
|
||||
"lzxd",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "xex2tractor"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
edition = "2024"
|
||||
description = "A tool for extracting and inspecting Xbox 360 XEX2 executable files"
|
||||
license = "MIT"
|
||||
@@ -10,3 +10,5 @@ aes = "0.8.4"
|
||||
cbc = "0.1.2"
|
||||
clap = { version = "4.6.0", features = ["derive"] }
|
||||
lzxd = "0.2.6"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
39
README.md
39
README.md
@@ -6,7 +6,7 @@ A tool for extracting and inspecting Xbox 360 XEX2 executable files, written in
|
||||
|
||||
### Inspect
|
||||
|
||||
Display XEX2 file information (headers, security info, etc.):
|
||||
Display XEX2 file information (headers, security info, resolved imports, etc.):
|
||||
|
||||
```sh
|
||||
xex2tractor inspect <file.xex>
|
||||
@@ -51,6 +51,18 @@ Load Address: 0x82000000
|
||||
Region: 0xFFFFFFFF [ALL REGIONS]
|
||||
Allowed Media Types: 0x00000004 [DVD_CD]
|
||||
...
|
||||
|
||||
=== Resolved Imports (398 total) ===
|
||||
|
||||
xam.xex (104 imports):
|
||||
0x82DAA7D8 Variable 0x0041 XamLoaderGetLaunchDataSize
|
||||
0x82DAA7E0 Thunk 0x0041 XamLoaderGetLaunchDataSize
|
||||
...
|
||||
|
||||
xboxkrnl.exe (294 imports):
|
||||
0x82DAA600 Variable 0x0001 DbgBreakPoint
|
||||
0x82DAA608 Thunk 0x0001 DbgBreakPoint
|
||||
...
|
||||
```
|
||||
|
||||
### Extract
|
||||
@@ -63,6 +75,19 @@ xex2tractor extract <file.xex> [output.exe]
|
||||
|
||||
If no output path is given, defaults to the input filename with `.exe` extension.
|
||||
|
||||
#### Import Resolution
|
||||
|
||||
Pass `-r` / `--resolve-imports` to write Xenia-style thunk stubs and variable
|
||||
slot values into the extracted PE image, making it suitable for further
|
||||
static analysis with tools that understand resolved Xbox 360 imports:
|
||||
|
||||
```sh
|
||||
xex2tractor extract -r <file.xex> [output.exe]
|
||||
```
|
||||
|
||||
Without `-r`, the extracted PE is byte-for-byte identical to the decrypted and
|
||||
decompressed image as it appears in the XEX2 file.
|
||||
|
||||
#### Example
|
||||
|
||||
```sh
|
||||
@@ -70,12 +95,21 @@ $ xex2tractor extract default.xex default.exe
|
||||
Encryption: Normal (AES-128-CBC)
|
||||
Compression: Normal (LZX)
|
||||
Extracted PE image (9568256 bytes) -> default.exe
|
||||
|
||||
$ xex2tractor extract -r default.xex resolved.exe
|
||||
Encryption: Normal (AES-128-CBC)
|
||||
Compression: Normal (LZX)
|
||||
Resolved 398 imports (204 variables, 194 thunks)
|
||||
xam.xex: 104
|
||||
xboxkrnl.exe: 294
|
||||
Extracted PE image (9568256 bytes) -> resolved.exe
|
||||
```
|
||||
|
||||
Supports:
|
||||
- AES-128-CBC decryption (retail, devkit, and XEX1 master keys)
|
||||
- No compression, basic (zero-fill), and normal (LZX) decompression
|
||||
- PE header verification (MZ signature, PE signature, POWERPCBE machine type)
|
||||
- Xenia-style import resolution (variable slots and thunk stubs)
|
||||
|
||||
## Building
|
||||
|
||||
@@ -93,7 +127,8 @@ cargo test
|
||||
|
||||
## Documentation
|
||||
|
||||
See [doc/xex2_format.md](doc/xex2_format.md) for the XEX2 file format specification.
|
||||
- [doc/xex2_format.md](doc/xex2_format.md) — XEX2 file format specification
|
||||
- [doc/xbox360_exports.json](doc/xbox360_exports.json) — Xbox 360 system export database (2,913 exports across xboxkrnl.exe, xam.xex, xbdm.xex)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
33562
doc/xbox360_exports.json
Normal file
33562
doc/xbox360_exports.json
Normal file
File diff suppressed because it is too large
Load Diff
112
src/exports.rs
112
src/exports.rs
@@ -1,13 +1,15 @@
|
||||
/// Xbox 360 system export database.
|
||||
///
|
||||
/// Parses the embedded `doc/xbox360_exports.md` at first access and provides
|
||||
/// Parses the embedded `doc/xbox360_exports.json` at first access and provides
|
||||
/// ordinal-to-name lookups for xboxkrnl.exe, xam.xex, and xbdm.xex.
|
||||
use std::collections::HashMap;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
static EXPORT_DB: OnceLock<ExportDatabase> = OnceLock::new();
|
||||
|
||||
const EXPORTS_MD: &str = include_str!("../doc/xbox360_exports.md");
|
||||
const EXPORTS_JSON: &str = include_str!("../doc/xbox360_exports.json");
|
||||
|
||||
/// Information about a single Xbox 360 system export.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -46,77 +48,52 @@ pub fn lookup(library: &str, ordinal: u16) -> Option<&'static ExportInfo> {
|
||||
}
|
||||
|
||||
fn get_db() -> &'static ExportDatabase {
|
||||
EXPORT_DB.get_or_init(|| parse_exports_md(EXPORTS_MD))
|
||||
EXPORT_DB.get_or_init(|| parse_exports_json(EXPORTS_JSON))
|
||||
}
|
||||
|
||||
/// Parses the markdown export database into a lookup structure.
|
||||
///
|
||||
/// Expected format per module section:
|
||||
/// ```text
|
||||
/// ## module_name (filename.exe)
|
||||
/// ...
|
||||
/// | 0xNNN | FunctionName | function | status | ... |
|
||||
/// ```
|
||||
fn parse_exports_md(md: &str) -> ExportDatabase {
|
||||
#[derive(Deserialize)]
|
||||
struct JsonRoot {
|
||||
modules: HashMap<String, JsonModule>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct JsonModule {
|
||||
file: String,
|
||||
exports: Vec<JsonExport>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct JsonExport {
|
||||
ordinal: u32,
|
||||
name: String,
|
||||
#[serde(rename = "type")]
|
||||
export_type: String,
|
||||
}
|
||||
|
||||
fn parse_exports_json(json: &str) -> ExportDatabase {
|
||||
let root: JsonRoot = serde_json::from_str(json).expect("invalid exports JSON");
|
||||
let mut modules: HashMap<String, HashMap<u16, ExportInfo>> = HashMap::new();
|
||||
let mut current_file: Option<String> = None;
|
||||
|
||||
for line in md.lines() {
|
||||
// Detect section headers: "## xboxkrnl (xboxkrnl.exe)"
|
||||
if let Some(rest) = line.strip_prefix("## ") {
|
||||
if let (Some(paren_start), Some(paren_end)) =
|
||||
(rest.find('('), rest.find(')'))
|
||||
{
|
||||
let filename = rest[paren_start + 1..paren_end].trim().to_ascii_lowercase();
|
||||
current_file = Some(filename);
|
||||
} else {
|
||||
current_file = None;
|
||||
}
|
||||
continue;
|
||||
for module in root.modules.values() {
|
||||
let file_key = module.file.to_ascii_lowercase();
|
||||
let entries = modules.entry(file_key).or_default();
|
||||
|
||||
for export in &module.exports {
|
||||
let ordinal = export.ordinal as u16;
|
||||
entries.insert(
|
||||
ordinal,
|
||||
ExportInfo {
|
||||
ordinal,
|
||||
name: export.name.clone(),
|
||||
is_function: export.export_type == "function",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Parse table rows: "| 0xNNN | Name | function/variable | ... |"
|
||||
let Some(ref file) = current_file else {
|
||||
continue;
|
||||
};
|
||||
if !line.starts_with("| 0x") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let cols: Vec<&str> = line.split('|').collect();
|
||||
// cols[0] is empty (before first |), cols[1] = ordinal, cols[2] = name, cols[3] = type
|
||||
if cols.len() < 4 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let ordinal_str = cols[1].trim();
|
||||
let name = cols[2].trim();
|
||||
let type_str = cols[3].trim();
|
||||
|
||||
let Some(ordinal) = parse_hex_ordinal(ordinal_str) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let entry = ExportInfo {
|
||||
ordinal,
|
||||
name: name.to_string(),
|
||||
is_function: type_str == "function",
|
||||
};
|
||||
|
||||
modules
|
||||
.entry(file.clone())
|
||||
.or_default()
|
||||
.insert(ordinal, entry);
|
||||
}
|
||||
|
||||
ExportDatabase { modules }
|
||||
}
|
||||
|
||||
fn parse_hex_ordinal(s: &str) -> Option<u16> {
|
||||
let hex = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X"))?;
|
||||
u16::from_str_radix(hex, 16).ok()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -182,13 +159,4 @@ mod tests {
|
||||
fn test_lookup_unknown_library() {
|
||||
assert!(lookup("nonexistent.dll", 0x001).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_hex_ordinal() {
|
||||
assert_eq!(parse_hex_ordinal("0x001"), Some(1));
|
||||
assert_eq!(parse_hex_ordinal("0x3A3"), Some(0x3A3));
|
||||
assert_eq!(parse_hex_ordinal("0xFFFF"), Some(0xFFFF));
|
||||
assert_eq!(parse_hex_ordinal("invalid"), None);
|
||||
assert_eq!(parse_hex_ordinal(""), None);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user