Skip to content

Instantly share code, notes, and snippets.

@sargunv
Created January 8, 2026 04:29
Show Gist options
  • Select an option

  • Save sargunv/c8f83311083a13d04571ce4edc7e3abc to your computer and use it in GitHub Desktop.

Select an option

Save sargunv/c8f83311083a13d04571ce4edc7e3abc to your computer and use it in GitHub Desktop.
Identify an Xbox game using certificate metadata
#!/usr/bin/env python3
"""
Xbox XISO Game Identifier Extractor
Extracts game identification data directly from .xiso.iso files
without needing to extract them first.
Usage:
python xbox_xiso_id.py <xiso_file>
python xbox_xiso_id.py -j <xiso_file>
python xbox_xiso_id.py -j <xiso_file1> <xiso_file2> <xiso_file3>
"""
import argparse
import json
import struct
import sys
from pathlib import Path
from dataclasses import dataclass, asdict
from typing import Optional
import datetime
XISO_MAGIC = b"MICROSOFT*XBOX*MEDIA"
XBE_MAGIC = b"XBEH"
SECTOR_SIZE = 2048
@dataclass
class XboxGameInfo:
title_id_hex: str
title_id_formatted: str
publisher_code: str
game_number: int
title: str
region: str
region_flags: int
version: int
disc_number: int
build_timestamp: Optional[datetime.datetime]
xbe_path: str
def __str__(self) -> str:
lines = [
f"Title ID: {self.title_id_formatted} (0x{self.title_id_hex})",
f"Title: {self.title}",
f"Publisher: {self.publisher_code}",
f"Game Number: {self.game_number}",
f"Region: {self.region} (0x{self.region_flags:08X})",
f"Version: {self.version}",
f"Disc: {self.disc_number}",
f"Build Date: {self.build_timestamp}",
f"XBE Path: {self.xbe_path}",
]
return "\n".join(lines)
def read_u16_le(data: bytes, offset: int) -> int:
return struct.unpack_from("<H", data, offset)[0]
def read_u32_le(data: bytes, offset: int) -> int:
return struct.unpack_from("<I", data, offset)[0]
def decode_region(flags: int) -> str:
regions = []
if flags & 0x00000001:
regions.append("NA")
if flags & 0x00000002:
regions.append("JP")
if flags & 0x00000004:
regions.append("EU")
if flags & 0x80000000:
regions.append("DEBUG")
return "/".join(regions) if regions else "Unknown"
def decode_title_id(title_id: int) -> tuple[str, str, int]:
"""Decode Title ID into publisher code and game number."""
game_number = title_id & 0xFFFF
publisher_code_int = (title_id >> 16) & 0xFFFF
# Publisher code is 2 ASCII chars (big-endian in the high word)
pub_char1 = chr((publisher_code_int >> 8) & 0xFF)
pub_char2 = chr(publisher_code_int & 0xFF)
publisher_code = f"{pub_char1}{pub_char2}"
formatted = f"{publisher_code}-{game_number:03d}"
return formatted, publisher_code, game_number
def find_xbe_in_directory(f, dir_sector: int, dir_size: int, path: str = "") -> Optional[tuple[int, int, str]]:
"""
Search the XISO directory tree for an .xbe file.
Returns (sector, size, path) of the first .xbe found, preferring default.xbe.
"""
f.seek(dir_sector * SECTOR_SIZE)
dir_data = f.read(dir_size)
xbe_candidates = []
def parse_entry(offset: int) -> None:
if offset >= len(dir_data) - 14:
return
left_offset = read_u16_le(dir_data, offset)
right_offset = read_u16_le(dir_data, offset + 2)
file_sector = read_u32_le(dir_data, offset + 4)
file_size = read_u32_le(dir_data, offset + 8)
attributes = dir_data[offset + 12]
name_len = dir_data[offset + 13]
if name_len == 0 or offset + 14 + name_len > len(dir_data):
return
name = dir_data[offset + 14 : offset + 14 + name_len].decode("ascii", errors="replace")
full_path = f"{path}/{name}" if path else name
is_directory = (attributes & 0x10) != 0
if is_directory:
# Recursively search subdirectories
result = find_xbe_in_directory(f, file_sector, file_size, full_path)
if result:
xbe_candidates.append(result)
elif name.lower().endswith(".xbe"):
xbe_candidates.append((file_sector, file_size, full_path))
# Traverse B-tree
if left_offset != 0 and left_offset != 0xFFFF:
parse_entry(left_offset * 4)
if right_offset != 0 and right_offset != 0xFFFF:
parse_entry(right_offset * 4)
# Start parsing from the root of this directory
if len(dir_data) >= 14:
parse_entry(0)
if not xbe_candidates:
return None
# Prefer default.xbe
for sector, size, filepath in xbe_candidates:
if filepath.lower() == "default.xbe":
return (sector, size, filepath)
# Otherwise return the first one found
return xbe_candidates[0]
def extract_xbe_info(f, xbe_sector: int, xbe_path: str) -> XboxGameInfo:
"""Extract game info from an XBE file at the given sector."""
xbe_offset = xbe_sector * SECTOR_SIZE
# Read XBE header (first 0x200 bytes should be enough for certificate offset)
f.seek(xbe_offset)
header = f.read(0x200)
if header[:4] != XBE_MAGIC:
raise ValueError(f"Invalid XBE magic at sector {xbe_sector}")
# Certificate offset is at 0x118 (virtual address)
# Base address is at 0x104
base_addr = read_u32_le(header, 0x104)
cert_addr = read_u32_le(header, 0x118)
# Convert virtual address to file offset
cert_file_offset = xbe_offset + (cert_addr - base_addr)
# Read certificate (0x1D0 = 464 bytes is typical size)
f.seek(cert_file_offset)
cert = f.read(0x1D0)
# Parse certificate fields
# cert_size = read_u32_le(cert, 0x00)
timestamp = read_u32_le(cert, 0x04)
title_id = read_u32_le(cert, 0x08)
# Title name is UTF-16LE at offset 0x0C, 40 chars (80 bytes)
title_bytes = cert[0x0C : 0x0C + 80]
title = title_bytes.decode("utf-16-le", errors="replace").rstrip("\x00")
# XBE Certificate structure:
# 0x00: cert size (4), 0x04: timestamp (4), 0x08: title ID (4)
# 0x0C: title name (80), 0x5C: alt title IDs (64), 0x9C: allowed media (4)
# 0xA0: game region (4), 0xA4: game ratings (4), 0xA8: disc number (4), 0xAC: version (4)
region_flags = read_u32_le(cert, 0xA0)
disc_number = read_u32_le(cert, 0xA8)
version = read_u32_le(cert, 0xAC)
# Decode title ID
title_id_formatted, publisher_code, game_number = decode_title_id(title_id)
# Convert timestamp
try:
build_timestamp = datetime.datetime.fromtimestamp(timestamp)
except (ValueError, OSError):
build_timestamp = None
return XboxGameInfo(
title_id_hex=f"{title_id:08X}",
title_id_formatted=title_id_formatted,
publisher_code=publisher_code,
game_number=game_number,
title=title,
region=decode_region(region_flags),
region_flags=region_flags,
version=version,
disc_number=disc_number,
build_timestamp=build_timestamp,
xbe_path=xbe_path,
)
def extract_game_info(xiso_path: str) -> XboxGameInfo:
"""Extract game identification info from an XISO file."""
with open(xiso_path, "rb") as f:
# XISO header is at offset 0x10000 (sector 32)
f.seek(0x10000)
header = f.read(0x20)
if header[:20] != XISO_MAGIC:
raise ValueError("Not a valid XISO file (missing MICROSOFT*XBOX*MEDIA signature)")
# Root directory info
root_dir_sector = read_u32_le(header, 0x14)
root_dir_size = read_u32_le(header, 0x18)
# Find default.xbe or any .xbe file
xbe_info = find_xbe_in_directory(f, root_dir_sector, root_dir_size)
if not xbe_info:
raise ValueError("No .xbe file found in XISO")
xbe_sector, xbe_size, xbe_path = xbe_info
# Extract info from the XBE
return extract_xbe_info(f, xbe_sector, xbe_path)
def info_to_dict(info: XboxGameInfo, filename: str) -> dict:
"""Convert XboxGameInfo to a JSON-serializable dict."""
return {
"file": filename,
"title_id": info.title_id_hex,
"title_id_formatted": info.title_id_formatted,
"title": info.title,
"publisher": info.publisher_code,
"game_number": info.game_number,
"region": info.region,
"region_flags": info.region_flags,
"version": info.version,
"disc": info.disc_number,
"build_date": info.build_timestamp.isoformat() if info.build_timestamp else None,
"xbe_path": info.xbe_path,
}
def main():
parser = argparse.ArgumentParser(
description="Extract Xbox game identification from XISO files."
)
parser.add_argument("files", nargs="+", help="XISO file(s) to process")
parser.add_argument("-j", "--json", action="store_true", help="Output as JSON")
args = parser.parse_args()
results = []
for xiso_path in args.files:
path = Path(xiso_path)
try:
info = extract_game_info(xiso_path)
if args.json:
results.append(info_to_dict(info, path.name))
else:
print(f"\n{'=' * 60}")
print(f"File: {path.name}")
print('=' * 60)
print(info)
except Exception as e:
if args.json:
results.append({"file": path.name, "error": str(e)})
else:
print(f"\n{'=' * 60}")
print(f"File: {path.name}")
print('=' * 60)
print(f"Error: {e}")
if args.json:
print(json.dumps(results, indent=2))
else:
print()
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment