mirror of
https://github.com/SpeedBreakerProject/speedbreaker.git
synced 2026-10-18 10:01:20 +02:00
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
221 lines
7.6 KiB
C++
221 lines
7.6 KiB
C++
// xexsurvey: Phase 1 static survey of a retail XEX using XenonRecomp's loader.
|
|
//
|
|
// xexsurvey <default.xex> <out-dir>
|
|
//
|
|
// Writes <out-dir>/image.bin (the decrypted, decompressed image, loadable in
|
|
// Ghidra as raw big-endian PowerPC at the printed base) and prints sections,
|
|
// the entry point, every kernel/XAM import, and the addresses of the
|
|
// compiler helpers XenonRecomp's TOML config needs.
|
|
|
|
#include <xex.h>
|
|
#include <image.h>
|
|
#include <file.h>
|
|
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <map>
|
|
#include <set>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
|
|
extern std::unordered_map<size_t, const char*> XamExports;
|
|
extern std::unordered_map<size_t, const char*> XboxKernelExports;
|
|
|
|
static uint32_t ReadBE32(const uint8_t* p)
|
|
{
|
|
return (uint32_t(p[0]) << 24) | (uint32_t(p[1]) << 16) | (uint32_t(p[2]) << 8) | p[3];
|
|
}
|
|
|
|
// Optional-header lookup on the raw file (headers are never encrypted).
|
|
static const uint8_t* FindOptHeader(const std::vector<uint8_t>& file, uint32_t key)
|
|
{
|
|
uint32_t count = ReadBE32(&file[0x14]);
|
|
for (uint32_t i = 0; i < count; i++)
|
|
{
|
|
const uint8_t* entry = &file[0x18 + i * 8];
|
|
if (ReadBE32(entry) == key)
|
|
{
|
|
uint32_t value = ReadBE32(entry + 4);
|
|
return (key & 0xFF) == 0 || (key & 0xFF) == 1 ? entry + 4 : &file[value];
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
struct Pattern
|
|
{
|
|
const char* key;
|
|
std::vector<uint8_t> bytes;
|
|
};
|
|
|
|
int main(int argc, char** argv)
|
|
{
|
|
if (argc < 3)
|
|
{
|
|
fprintf(stderr, "usage: xexsurvey <default.xex> <out-dir>\n");
|
|
return 1;
|
|
}
|
|
|
|
std::vector<uint8_t> file = LoadFile(argv[1]);
|
|
if (file.empty())
|
|
{
|
|
fprintf(stderr, "cannot read %s\n", argv[1]);
|
|
return 1;
|
|
}
|
|
|
|
Image image = Xex2LoadImage(file.data(), file.size());
|
|
|
|
printf("## Image\n\n");
|
|
printf("base 0x%08zX, size 0x%08X, entry 0x%08zX\n\n", image.base, image.size, image.entry_point);
|
|
printf("| Section | Start | End | Size | Kind |\n|---|---|---|---|---|\n");
|
|
for (auto& s : image.sections)
|
|
{
|
|
printf("| %s | 0x%08zX | 0x%08zX | 0x%X | %s |\n", s.name.c_str(), s.base, s.base + s.size, s.size,
|
|
(s.flags & SectionFlags_Code) ? "code" : "data");
|
|
}
|
|
|
|
std::string outPath = std::string(argv[2]) + "/image.bin";
|
|
if (FILE* f = fopen(outPath.c_str(), "wb"))
|
|
{
|
|
fwrite(image.data.get(), 1, image.size, f);
|
|
fclose(f);
|
|
printf("\nwrote %s (load as PowerPC:BE:64:A2ALT-32addr or VLE-less PPC BE at base 0x%08zX)\n", outPath.c_str(), image.base);
|
|
}
|
|
|
|
// Imports: walk the import-library header on the raw file; read each
|
|
// thunk from the loaded image. The loader overwrites function thunks with
|
|
// a nop/blr stub and names them in the symbol table; variable records keep
|
|
// their ordinal.
|
|
printf("\n## Imports\n\n");
|
|
const uint8_t* importHeader = FindOptHeader(file, 0x000103FF);
|
|
std::map<std::string, std::set<std::string>> byLibrary;
|
|
std::map<std::string, std::set<std::string>> variablesByLibrary;
|
|
if (importHeader != nullptr)
|
|
{
|
|
uint32_t stringTableSize = ReadBE32(importHeader + 4);
|
|
uint32_t numLibraries = ReadBE32(importHeader + 8);
|
|
std::vector<std::string> names;
|
|
const char* strings = reinterpret_cast<const char*>(importHeader + 12);
|
|
size_t off = 0;
|
|
for (uint32_t i = 0; i < numLibraries; i++)
|
|
{
|
|
names.emplace_back(strings + off);
|
|
off += ((names.back().size() + 1) + 3) & ~size_t(3);
|
|
}
|
|
|
|
const uint8_t* lib = importHeader + 12 + stringTableSize;
|
|
for (uint32_t i = 0; i < numLibraries; i++)
|
|
{
|
|
uint32_t libSize = ReadBE32(lib);
|
|
uint16_t nameIndex = (lib[0x24] << 8) | lib[0x25];
|
|
uint16_t count = (lib[0x26] << 8) | lib[0x27];
|
|
const std::string& libName = names[nameIndex];
|
|
const auto* table = libName == "xam.xex" ? &XamExports
|
|
: libName == "xboxkrnl.exe" ? &XboxKernelExports : nullptr;
|
|
|
|
for (uint16_t im = 0; im < count; im++)
|
|
{
|
|
uint32_t thunkAddr = ReadBE32(lib + 0x28 + im * 4);
|
|
auto sym = image.symbols.find(thunkAddr);
|
|
if (sym != image.symbols.end() && sym->address == thunkAddr)
|
|
{
|
|
byLibrary[libName].insert(sym->name);
|
|
continue;
|
|
}
|
|
|
|
auto* thunk = static_cast<const uint8_t*>(image.Find(thunkAddr));
|
|
if (thunk == nullptr)
|
|
continue;
|
|
|
|
// Loader byteswapped this record to host order.
|
|
uint32_t data;
|
|
memcpy(&data, thunk, 4);
|
|
uint32_t ordinal = data & 0xFFFF;
|
|
uint32_t type = data >> 24;
|
|
const char* name = nullptr;
|
|
if (table != nullptr)
|
|
{
|
|
auto it = table->find(ordinal);
|
|
if (it != table->end())
|
|
name = it->second;
|
|
}
|
|
|
|
char buf[64];
|
|
if (name == nullptr)
|
|
{
|
|
snprintf(buf, sizeof(buf), "ordinal_%u", ordinal);
|
|
name = buf;
|
|
}
|
|
|
|
if (type == 0)
|
|
variablesByLibrary[libName].insert(name);
|
|
else
|
|
byLibrary[libName].insert(name);
|
|
}
|
|
lib += libSize;
|
|
}
|
|
}
|
|
|
|
size_t total = 0;
|
|
for (auto& [lib, fns] : byLibrary)
|
|
{
|
|
// A variable record that pairs a named function is just its IAT slot.
|
|
std::set<std::string> dataOnly;
|
|
for (auto& v : variablesByLibrary[lib])
|
|
if (!fns.count(v))
|
|
dataOnly.insert(v);
|
|
|
|
printf("### %s: %zu functions, %zu data imports\n\n", lib.c_str(), fns.size(), dataOnly.size());
|
|
for (auto& fn : fns)
|
|
printf("- %s\n", fn.c_str());
|
|
for (auto& v : dataOnly)
|
|
printf("- %s (data)\n", v.c_str());
|
|
printf("\n");
|
|
total += fns.size() + dataOnly.size();
|
|
}
|
|
printf("total distinct imports: %zu\n", total);
|
|
|
|
// Compiler helpers: byte patterns from the XenonRecomp README.
|
|
const Pattern patterns[] = {
|
|
{ "restgprlr_14_address", { 0xE9, 0xC1, 0xFF, 0x68 } },
|
|
{ "savegprlr_14_address", { 0xF9, 0xC1, 0xFF, 0x68 } },
|
|
{ "restfpr_14_address", { 0xC9, 0xCC, 0xFF, 0x70 } },
|
|
{ "savefpr_14_address", { 0xD9, 0xCC, 0xFF, 0x70 } },
|
|
{ "restvmx_14_address", { 0x39, 0x60, 0xFE, 0xE0, 0x7D, 0xCB, 0x60, 0xCE } },
|
|
{ "savevmx_14_address", { 0x39, 0x60, 0xFE, 0xE0, 0x7D, 0xCB, 0x61, 0xCE } },
|
|
{ "restvmx_64_address", { 0x39, 0x60, 0xFC, 0x00, 0x10, 0x0B, 0x60, 0xCB } },
|
|
{ "savevmx_64_address", { 0x39, 0x60, 0xFC, 0x00, 0x10, 0x0B, 0x61, 0xCB } },
|
|
};
|
|
|
|
printf("\n## Compiler helpers\n\n");
|
|
for (auto& p : patterns)
|
|
{
|
|
std::vector<size_t> hits;
|
|
for (auto& s : image.sections)
|
|
{
|
|
if (!(s.flags & SectionFlags_Code))
|
|
continue;
|
|
for (size_t i = 0; i + p.bytes.size() <= s.size; i += 4)
|
|
{
|
|
if (memcmp(s.data + i, p.bytes.data(), p.bytes.size()) == 0)
|
|
hits.push_back(s.base + i);
|
|
}
|
|
}
|
|
|
|
printf("%s =", p.key);
|
|
for (size_t h : hits)
|
|
printf(" 0x%08zX", h);
|
|
printf("%s\n", hits.empty() ? " (not found)" : hits.size() > 1 ? " <-- multiple hits, check by hand" : "");
|
|
}
|
|
|
|
printf("\n## Import stubs\n\n");
|
|
for (auto& sym : image.symbols)
|
|
{
|
|
if (sym.name.rfind("__imp__", 0) == 0)
|
|
printf("0x%08zX %s\n", sym.address, sym.name.c_str());
|
|
}
|
|
|
|
return 0;
|
|
}
|