Files
Elpisdev_unsegaREBORN/src/main.c
T
2026-02-06 01:07:35 +03:00

1189 lines
43 KiB
C

#include "lib.h"
#include "bootid.h"
#include "crypto.h"
#include "aes.h"
#include "progress.h"
#include "exfat.h"
#include "ntfs.h"
#include "error.h"
#include "stream.h"
#include "common.h"
#define PAGE_SIZE 4096
#define BUFFER_SIZE (PAGE_SIZE * 256)
#define MAX_PATH_LENGTH 256
#define VERSION "2026020600"
typedef struct {
bool silent;
bool verbose;
bool extract_fs;
bool write_intermediate;
char* output_filename;
const char* output_dir;
const char* parent_file;
uint64_t total_files_extracted;
uint64_t total_bytes_extracted;
time_t start_time;
} AppContext;
typedef struct {
BootId bootid;
uint8_t key[16];
uint8_t iv[16];
char os_id[4];
char game_id[5];
char timestamp_str[20];
uint64_t data_offset;
uint64_t data_size;
bool is_apm3;
bool is_inner_apm3;
} DecryptInfo;
#define PRINT(ctx, ...) do { if (!(ctx)->silent) printf(__VA_ARGS__); } while(0)
#define VERBOSE(ctx, ...) do { if ((ctx)->verbose && !(ctx)->silent) printf(__VA_ARGS__); } while(0)
static inline void path_join(char* dest, size_t size, const char* dir, const char* name) {
if (dir) {
snprintf(dest, size, "%s%s%s", dir, PATH_SEPARATOR, name);
} else {
snprintf(dest, size, "%s", name);
}
}
static ErrorCode do_file(AppContext* app_ctx, const char* path);
static bool test_keys(FILE* file, uint64_t data_offset, const uint8_t* test_key,
const uint8_t* test_iv, const uint8_t* expected_header) {
uint8_t buffer[16];
uint8_t page_iv[16];
uint8_t decrypted[16];
long saved_pos = ftell(file);
if (fseek(file, (long)data_offset, SEEK_SET) != 0) {
fseek(file, saved_pos, SEEK_SET);
return false;
}
if (fread(buffer, 1, 16, file) != 16) {
fseek(file, saved_pos, SEEK_SET);
return false;
}
fseek(file, saved_pos, SEEK_SET);
iv_page(0, test_iv, page_iv);
memcpy(decrypted, buffer, 16);
AES_ctx ctx;
AES_init_ctx_iv(&ctx, test_key, page_iv);
AES_CBC_decrypt_buffer(&ctx, decrypted, 16);
return memcmp(decrypted, expected_header, 8) == 0;
}
static ErrorCode parse_bootid(AppContext* app_ctx, FILE* file, DecryptInfo* info, uint8_t* read_buffer) {
uint8_t bootid_bytes[96];
if (fread(bootid_bytes, 1, 96, file) != 96) {
FAIL(ERR_INVALID_BOOTID);
return ERR_INVALID_BOOTID;
}
uint8_t decrypted[96];
memcpy(decrypted, bootid_bytes, 96);
AES_ctx ctx;
AES_init_ctx_iv(&ctx, BOOTID_KEY, BOOTID_IV);
AES_CBC_decrypt_buffer(&ctx, decrypted, 96);
memcpy(&info->bootid, decrypted, sizeof(BootId));
if (info->bootid.container_type != CONTAINER_TYPE_OS &&
info->bootid.container_type != CONTAINER_TYPE_APP &&
info->bootid.container_type != CONTAINER_TYPE_OPTION) {
char msg[64];
snprintf(msg, sizeof(msg), "type %d", info->bootid.container_type);
FAIL_MSG(ERR_UNKNOWN_CONTAINER, msg);
return ERR_UNKNOWN_CONTAINER;
}
format_timestamp(&info->bootid.target_timestamp, info->timestamp_str, sizeof(info->timestamp_str));
memcpy(info->os_id, info->bootid.os_id, 3);
info->os_id[3] = '\0';
memcpy(info->game_id, info->bootid.game_id, 4);
info->game_id[4] = '\0';
info->is_apm3 = (info->bootid.container_type == CONTAINER_TYPE_OPTION) && IS_APM3_OPTION(info->bootid.game_id);
info->is_inner_apm3 = false;
info->data_offset = info->bootid.header_block_count * info->bootid.block_size;
info->data_size = (info->bootid.block_count - info->bootid.header_block_count) * info->bootid.block_size;
const char* id = (info->bootid.container_type == CONTAINER_TYPE_OS) ? info->os_id : info->game_id;
VERBOSE(app_ctx, " %s %s %s #%d AES:%s\n",
(info->bootid.container_type == CONTAINER_TYPE_OS) ? "OS" :
(info->bootid.container_type == CONTAINER_TYPE_APP) ? "APP" :
info->is_apm3 ? "APM3" : "OPT", id, info->timestamp_str, info->bootid.sequence_number,
aes_hw_supported() ? "HW" : "SW");
GameKeys keys;
bool got_keys = false;
const char* key_source = NULL;
if (info->bootid.container_type == CONTAINER_TYPE_OS || info->bootid.container_type == CONTAINER_TYPE_APP) {
got_keys = key_game(id, &keys);
if (got_keys) key_source = keys.external ? "ext" : "int";
} else if (info->is_apm3) {
memcpy(keys.key, OPTION_KEY, 16);
memcpy(keys.iv, OPTION_IV, 16);
keys.has_iv = true;
got_keys = true;
key_source = "apm3";
} else {
uint8_t derived_key[16], derived_iv[16];
if (key_derive(info->game_id, derived_key, derived_iv) &&
test_keys(file, info->data_offset, derived_key, derived_iv, NTFS_HEADER)) {
memcpy(keys.key, derived_key, 16);
memcpy(keys.iv, derived_iv, 16);
keys.has_iv = true;
got_keys = true;
info->is_inner_apm3 = true;
key_source = "derived";
} else {
memcpy(keys.key, OPTION_KEY, 16);
keys.has_iv = false;
got_keys = true;
key_source = "optkey";
}
}
if (!got_keys) {
FAIL_MSG(ERR_KEY_NOT_FOUND, id);
return ERR_KEY_NOT_FOUND;
}
VERBOSE(app_ctx, " key:%s\n", key_source);
memcpy(info->key, keys.key, 16);
bool has_iv = !info->bootid.use_custom_iv && keys.has_iv;
if (has_iv) {
memcpy(info->iv, keys.iv, 16);
VERBOSE(app_ctx, " iv:key\n");
} else {
if (fseek(file, (long)info->data_offset, SEEK_SET) != 0) {
FAIL(ERR_FILE_SEEK);
return ERR_FILE_SEEK;
}
if (fread(read_buffer, 1, PAGE_SIZE, file) != PAGE_SIZE) {
FAIL(ERR_FILE_READ);
return ERR_FILE_READ;
}
const uint8_t* header = (info->bootid.container_type == CONTAINER_TYPE_OPTION && !info->is_apm3 && !info->is_inner_apm3) ? EXFAT_HEADER : NTFS_HEADER;
if (!iv_file(info->key, header, read_buffer, info->iv)) {
FAIL(ERR_IV_CALCULATION);
return ERR_IV_CALCULATION;
}
VERBOSE(app_ctx, " iv:calc\n");
}
return ERR_OK;
}
static void format_basename(const DecryptInfo* info, char* out, size_t size) {
if (info->bootid.container_type == CONTAINER_TYPE_OS) {
snprintf(out, size, "%s_%04d%02d%02d_%s_%d",
info->os_id, info->bootid.os_version.major, info->bootid.os_version.minor,
info->bootid.os_version.release, info->timestamp_str, info->bootid.sequence_number);
} else if (info->bootid.container_type == CONTAINER_TYPE_APP) {
if (info->bootid.sequence_number > 0) {
snprintf(out, size, "%s_%d%02d%02d_%s_%d_%d%02d%02d",
info->game_id, info->bootid.target_version.version.major, info->bootid.target_version.version.minor,
info->bootid.target_version.version.release, info->timestamp_str, info->bootid.sequence_number,
info->bootid.source_version.major, info->bootid.source_version.minor, info->bootid.source_version.release);
} else {
snprintf(out, size, "%s_%d%02d%02d_%s_%d",
info->game_id, info->bootid.target_version.version.major, info->bootid.target_version.version.minor,
info->bootid.target_version.version.release, info->timestamp_str, info->bootid.sequence_number);
}
} else if (info->bootid.container_type == CONTAINER_TYPE_OPTION) {
char option_str[5];
memcpy(option_str, info->bootid.target_version.option, 4);
option_str[4] = '\0';
snprintf(out, size, "%s_%s_%s_%d",
info->game_id, option_str, info->timestamp_str, info->bootid.sequence_number);
}
}
static const char* fmt_size(uint64_t bytes, char* buffer, size_t buffer_size) {
uint64_t unit, frac;
const char* suffix;
if (bytes >= 1024ULL * 1024 * 1024) {
unit = 1024ULL * 1024 * 1024; suffix = " GB";
} else if (bytes >= 1024ULL * 1024) {
unit = 1024ULL * 1024; suffix = " MB";
} else if (bytes >= 1024) {
unit = 1024; suffix = " KB";
} else {
snprintf(buffer, buffer_size, "%llu bytes", (unsigned long long)bytes);
return buffer;
}
uint64_t whole = bytes / unit;
frac = (bytes % unit) * 100 / unit;
snprintf(buffer, buffer_size, "%llu.%02llu%s", (unsigned long long)whole, (unsigned long long)frac, suffix);
return buffer;
}
static ErrorCode do_stream(AppContext* app_ctx, const char* path);
static bool do_inner_opt(AppContext* app_ctx, const char* opt_path, const char* output_dir) {
FILE* file = FOPEN(opt_path, "rb");
if (!file) return false;
uint8_t bootid_enc[96];
if (fread(bootid_enc, 1, 96, file) != 96) {
fclose(file);
return false;
}
uint8_t bootid_dec[96];
memcpy(bootid_dec, bootid_enc, 96);
AES_ctx bootid_ctx;
AES_init_ctx_iv(&bootid_ctx, BOOTID_KEY, BOOTID_IV);
AES_CBC_decrypt_buffer(&bootid_ctx, bootid_dec, 96);
BootId bootid;
memcpy(&bootid, bootid_dec, sizeof(BootId));
if (bootid.container_type != CONTAINER_TYPE_OPTION) {
fclose(file);
return false;
}
char inner_game_id[5];
memcpy(inner_game_id, bootid.game_id, 4);
inner_game_id[4] = '\0';
uint64_t data_offset = bootid.header_block_count * bootid.block_size;
uint64_t data_size = (bootid.block_count - bootid.header_block_count) * bootid.block_size;
uint8_t first_page[PAGE_SIZE];
if (fseek(file, (long)data_offset, SEEK_SET) != 0 ||
fread(first_page, 1, PAGE_SIZE, file) != PAGE_SIZE) {
fclose(file);
return false;
}
uint8_t derived_key[16], derived_iv[16];
uint8_t inner_iv[16];
bool is_inner_apm3 = false;
if (key_derive(inner_game_id, derived_key, derived_iv)) {
uint8_t page_iv[16];
uint8_t test_decrypt[16];
iv_page(0, derived_iv, page_iv);
memcpy(test_decrypt, first_page, 16);
AES_ctx test_ctx;
AES_init_ctx_iv(&test_ctx, derived_key, page_iv);
AES_CBC_decrypt_buffer(&test_ctx, test_decrypt, 16);
if (memcmp(test_decrypt, NTFS_HEADER, 8) == 0) {
is_inner_apm3 = true;
memcpy(inner_iv, derived_iv, 16);
}
}
if (!is_inner_apm3) {
memcpy(derived_key, OPTION_KEY, 16);
if (!iv_file(derived_key, EXFAT_HEADER, first_page, inner_iv)) {
fclose(file);
return false;
}
}
DecryptStream* stream = malloc(sizeof(DecryptStream));
if (!stream) {
fclose(file);
return false;
}
if (!stream_init(stream, file, data_offset, data_size, derived_key, inner_iv)) {
free(stream);
fclose(file);
return false;
}
bool success = false;
if (is_inner_apm3) {
NTFSContext inner_ntfs = {0};
inner_ntfs.silent = app_ctx->silent;
inner_ntfs.verbose = app_ctx->verbose;
if (ntfs_init_stream(&inner_ntfs, stream, output_dir)) {
inner_ntfs.silent = true;
if (ntfs_extract_all(&inner_ntfs)) {
inner_ntfs.silent = app_ctx->silent;
bool is_orphan = false;
if (inner_ntfs.pending_vhd_count > 0) {
ntfs_extract_pending_vhds(&inner_ntfs, app_ctx->silent, app_ctx->verbose, &is_orphan);
}
if (is_orphan) {
VERBOSE(app_ctx, " orphan (use -p)\n");
RMDIR(output_dir);
} else {
VERBOSE(app_ctx, " inner ok\n");
app_ctx->total_files_extracted += inner_ntfs.files_extracted;
app_ctx->total_bytes_extracted += inner_ntfs.extracted_bytes;
success = true;
}
}
ntfs_close(&inner_ntfs);
}
} else {
ExfatContext inner_exfat;
if (exfat_init_stream(&inner_exfat, stream)) {
inner_exfat.silent = app_ctx->silent;
inner_exfat.verbose = app_ctx->verbose;
if (exfat_extract_all(&inner_exfat, output_dir)) {
VERBOSE(app_ctx, " inner ok\n");
app_ctx->total_files_extracted += inner_exfat.files_extracted;
app_ctx->total_bytes_extracted += inner_exfat.extracted_bytes;
success = true;
}
exfat_close(&inner_exfat);
}
}
free(stream);
fclose(file);
return success;
}
static void do_inner_opts(AppContext* app_ctx, const char* dir_path) {
char search_path[MAX_PATH_LENGTH];
snprintf(search_path, sizeof(search_path), "%s%s*.opt", dir_path, PATH_SEPARATOR);
lib_finddata_t find_data;
intptr_t hFind = _findfirst(search_path, &find_data);
if (hFind == -1) return;
do {
if (find_data.attrib & _A_SUBDIR) continue;
char opt_path[MAX_PATH_LENGTH];
path_join(opt_path, sizeof(opt_path), dir_path, find_data.name);
char opt_basename[MAX_PATH_LENGTH];
strncpy(opt_basename, find_data.name, sizeof(opt_basename) - 1);
opt_basename[sizeof(opt_basename) - 1] = '\0';
char* ext = strrchr(opt_basename, '.');
if (ext) *ext = '\0';
char inner_output_dir[MAX_PATH_LENGTH];
path_join(inner_output_dir, sizeof(inner_output_dir), dir_path, opt_basename);
VERBOSE(app_ctx, " opt:%s\n", find_data.name);
if (do_inner_opt(app_ctx, opt_path, inner_output_dir)) {
REMOVE(opt_path);
}
} while (_findnext(hFind, &find_data) == 0);
_findclose(hFind);
}
ErrorCode do_file(AppContext* app_ctx, const char* path) {
if (!app_ctx->write_intermediate && app_ctx->extract_fs) {
return do_stream(app_ctx, path);
}
ErrorCode result = ERR_EXTRACTION_FAILED;
uint8_t* read_buffer = NULL;
uint8_t* decrypted_buffer = NULL;
char* output_filename = NULL;
FILE* file = NULL;
FILE* output_file = NULL;
uint8_t page_iv[16];
read_buffer = malloc(BUFFER_SIZE);
decrypted_buffer = malloc(BUFFER_SIZE);
if (!read_buffer || !decrypted_buffer) {
FAIL(ERR_MEMORY);
result = ERR_MEMORY;
goto cleanup;
}
file = FOPEN(path, "rb");
if (!file) {
FAIL_MSG(ERR_FILE_OPEN, path);
result = ERR_FILE_OPEN;
goto cleanup;
}
DecryptInfo info;
result = parse_bootid(app_ctx, file, &info, read_buffer);
if (result != ERR_OK) goto cleanup;
output_filename = malloc(MAX_PATH_LENGTH);
if (!output_filename) {
FAIL(ERR_MEMORY);
result = ERR_MEMORY;
goto cleanup;
}
char basename[MAX_PATH_LENGTH];
format_basename(&info, basename, MAX_PATH_LENGTH);
const char* ext = (info.bootid.container_type == CONTAINER_TYPE_OPTION && !info.is_apm3 && !info.is_inner_apm3) ? ".exfat" : ".ntfs";
strncat(basename, ext, MAX_PATH_LENGTH - strlen(basename) - 1);
path_join(output_filename, MAX_PATH_LENGTH, app_ctx->output_dir, basename);
if (app_ctx->output_dir) create_directories(app_ctx->output_dir);
output_file = FOPEN(output_filename, "wb");
if (!output_file) {
FAIL_MSG(ERR_FILE_OPEN, output_filename);
result = ERR_FILE_OPEN;
goto cleanup;
}
if (info.data_size % 16 != 0) {
char msg[64];
snprintf(msg, sizeof(msg), "%llu bytes", (unsigned long long)info.data_size);
FAIL_MSG(ERR_AES_ALIGNMENT, msg);
result = ERR_AES_ALIGNMENT;
goto cleanup;
}
if (fseek(file, (long)info.data_offset, SEEK_SET) != 0) {
FAIL(ERR_FILE_SEEK);
result = ERR_FILE_SEEK;
goto cleanup;
}
AES_ctx page_ctx;
AES_init_ctx_iv(&page_ctx, info.key, info.iv);
Progress progress;
if (!app_ctx->silent) {
progress_init(&progress, info.data_size);
}
uint64_t total_bytes_read = 0;
uint64_t bytes_remaining = info.data_size;
while (bytes_remaining > 0) {
size_t chunk_size = (bytes_remaining > BUFFER_SIZE) ? BUFFER_SIZE : (size_t)bytes_remaining;
size_t read_size = fread(read_buffer, 1, chunk_size, file);
if (read_size != chunk_size) {
FAIL_MSG(ERR_FILE_READ, feof(file) ? "unexpected end of file" : "read error");
result = ERR_FILE_READ;
goto cleanup;
}
size_t offset = 0;
while (offset < read_size) {
size_t block_size = (read_size - offset > PAGE_SIZE) ? PAGE_SIZE : (read_size - offset);
uint64_t file_offset = total_bytes_read + offset;
iv_page(file_offset, info.iv, page_iv);
memcpy(decrypted_buffer + offset, read_buffer + offset, block_size);
AES_ctx_set_iv(&page_ctx, page_iv);
AES_CBC_decrypt_buffer(&page_ctx, decrypted_buffer + offset, block_size);
offset += block_size;
}
if (fwrite(decrypted_buffer, 1, read_size, output_file) != read_size) {
FAIL(ERR_FILE_WRITE);
result = ERR_FILE_WRITE;
goto cleanup;
}
total_bytes_read += read_size;
bytes_remaining -= read_size;
if (!app_ctx->silent) {
progress_update(&progress, total_bytes_read);
}
}
if (!app_ctx->silent) {
progress_finish(&progress);
}
PRINT(app_ctx, "ok: %s\n", output_filename);
if (app_ctx->extract_fs) {
if (app_ctx->output_filename) free(app_ctx->output_filename);
app_ctx->output_filename = output_filename;
output_filename = NULL;
}
result = ERR_OK;
cleanup:
if (file) fclose(file);
if (output_file) fclose(output_file);
free(read_buffer);
free(decrypted_buffer);
free(output_filename);
return result;
}
static ErrorCode do_stream(AppContext* app_ctx, const char* path) {
ErrorCode result = ERR_EXTRACTION_FAILED;
uint8_t* read_buffer = NULL;
FILE* file = NULL;
DecryptStream* stream = NULL;
read_buffer = malloc(PAGE_SIZE);
stream = malloc(sizeof(DecryptStream));
if (!read_buffer || !stream) {
FAIL(ERR_MEMORY);
result = ERR_MEMORY;
goto cleanup;
}
file = FOPEN(path, "rb");
if (!file) {
FAIL_MSG(ERR_FILE_OPEN, path);
result = ERR_FILE_OPEN;
goto cleanup;
}
DecryptInfo info;
result = parse_bootid(app_ctx, file, &info, read_buffer);
if (result != ERR_OK) goto cleanup;
if (!stream_init(stream, file, info.data_offset, info.data_size, info.key, info.iv)) {
FAIL(ERR_EXTRACTION_FAILED);
result = ERR_EXTRACTION_FAILED;
goto cleanup;
}
char output_dir[MAX_PATH_LENGTH];
char basename_no_ext[MAX_PATH_LENGTH];
format_basename(&info, basename_no_ext, sizeof(basename_no_ext));
path_join(output_dir, sizeof(output_dir), app_ctx->output_dir, basename_no_ext);
bool extraction_success = false;
if (info.bootid.container_type == CONTAINER_TYPE_OPTION && !info.is_apm3 && !info.is_inner_apm3) {
ExfatContext ctx;
if (exfat_init_stream(&ctx, stream)) {
ctx.silent = app_ctx->silent;
ctx.verbose = app_ctx->verbose;
VERBOSE(app_ctx, "exfat c=%u f=%u\n", ctx.bytes_per_cluster, ctx.fat_length_bytes);
if (exfat_extract_all(&ctx, output_dir)) {
extraction_success = true;
app_ctx->total_files_extracted += ctx.files_extracted;
app_ctx->total_bytes_extracted += ctx.extracted_bytes;
}
exfat_close(&ctx);
}
}
else if (info.is_apm3 || info.is_inner_apm3) {
char inner_game_id[5] = {0};
if (info.is_apm3) {
memcpy(inner_game_id, info.bootid.target_version.option, 4);
} else {
memcpy(inner_game_id, info.game_id, 4);
}
inner_game_id[4] = '\0';
VERBOSE(app_ctx, "apm3:%s\n", inner_game_id);
NTFSContext ctx = { 0 };
ctx.silent = app_ctx->silent;
ctx.verbose = app_ctx->verbose;
ctx.apm3_decrypt = false;
if (ntfs_init_stream(&ctx, stream, output_dir)) {
int vhd_type = ntfs_detect_vhd_type(&ctx);
if (vhd_type == VHD_TYPE_DIFFERENCING && !app_ctx->parent_file) {
PRINT(app_ctx, "diff VHD, use -p\n");
ntfs_close(&ctx);
result = ERR_OK;
goto cleanup;
}
VERBOSE(app_ctx, "MFT=%llu c=%u\n",
(unsigned long long)ctx.total_mft_records, ctx.bytes_per_cluster);
ctx.silent = true;
if (ntfs_extract_all(&ctx)) {
extraction_success = true;
ctx.silent = app_ctx->silent;
if (ctx.pending_vhd_count > 0) {
bool is_orphan = false;
ntfs_extract_pending_vhds(&ctx, app_ctx->silent, app_ctx->verbose, &is_orphan);
}
app_ctx->total_files_extracted += ctx.files_extracted;
app_ctx->total_bytes_extracted += ctx.extracted_bytes;
if (info.is_apm3 && ctx.pending_opt_count > 0) {
int pending_count = ctx.pending_opt_count;
for (int i = 0; i < pending_count; i++) {
const PendingOpt* opt = ntfs_get_pending_opt(&ctx, i);
if (!opt) continue;
VERBOSE(app_ctx, "opt:%s\n", opt->filename);
uint8_t bootid_enc[96];
RunSource run_src = {0};
run_src.ntfs_ctx = &ctx;
run_src.run_count = (opt->run_count < MAX_DATA_RUNS) ? opt->run_count : MAX_DATA_RUNS;
run_src.file_size = opt->file_size;
for (int r = 0; r < run_src.run_count; r++) {
run_src.runs[r].offset = opt->runs[r].offset;
run_src.runs[r].length = opt->runs[r].length;
}
if (!stream_read_raw(&ctx, run_src.runs, run_src.run_count,
run_src.file_size, 0, bootid_enc, 96)) {
fprintf(stderr, "optrd\n");
continue;
}
uint8_t bootid_dec[96];
memcpy(bootid_dec, bootid_enc, 96);
AES_ctx bootid_aes;
AES_init_ctx_iv(&bootid_aes, BOOTID_KEY, BOOTID_IV);
AES_CBC_decrypt_buffer(&bootid_aes, bootid_dec, 96);
BootId inner_bootid;
memcpy(&inner_bootid, bootid_dec, sizeof(BootId));
if (inner_bootid.container_type != CONTAINER_TYPE_OPTION) continue;
char inner_game_id[5];
memcpy(inner_game_id, inner_bootid.game_id, 4);
inner_game_id[4] = '\0';
uint64_t inner_data_offset = inner_bootid.header_block_count * inner_bootid.block_size;
uint64_t inner_data_size = (inner_bootid.block_count - inner_bootid.header_block_count) * inner_bootid.block_size;
uint8_t first_page[PAGE_SIZE];
if (!stream_read_raw(&ctx, run_src.runs, run_src.run_count,
run_src.file_size, inner_data_offset, first_page, PAGE_SIZE)) {
fprintf(stderr, "pgrd\n");
continue;
}
uint8_t derived_key[16], derived_iv[16];
uint8_t inner_iv[16];
bool is_inner_apm3 = false;
if (key_derive(inner_game_id, derived_key, derived_iv)) {
uint8_t page_iv[16];
uint8_t test_decrypt[16];
iv_page(0, derived_iv, page_iv);
memcpy(test_decrypt, first_page, 16);
AES_ctx test_ctx;
AES_init_ctx_iv(&test_ctx, derived_key, page_iv);
AES_CBC_decrypt_buffer(&test_ctx, test_decrypt, 16);
if (memcmp(test_decrypt, NTFS_HEADER, 8) == 0) {
is_inner_apm3 = true;
memcpy(inner_iv, derived_iv, 16);
}
}
if (!is_inner_apm3) {
memcpy(derived_key, OPTION_KEY, 16);
if (!iv_file(derived_key, EXFAT_HEADER, first_page, inner_iv)) continue;
}
char inner_output_dir[MAX_PATH_LENGTH];
char opt_basename[MAX_FILENAME_LENGTH];
strncpy(opt_basename, opt->filename, sizeof(opt_basename) - 1);
opt_basename[sizeof(opt_basename) - 1] = '\0';
char* ext = strrchr(opt_basename, '.');
if (ext) *ext = '\0';
path_join(inner_output_dir, sizeof(inner_output_dir), output_dir, opt_basename);
RunSource* inner_run_src = malloc(sizeof(RunSource));
if (!inner_run_src) continue;
memset(inner_run_src, 0, sizeof(RunSource));
inner_run_src->ntfs_ctx = &ctx;
inner_run_src->run_count = (opt->run_count < MAX_DATA_RUNS) ? opt->run_count : MAX_DATA_RUNS;
inner_run_src->file_size = opt->file_size;
for (int r = 0; r < inner_run_src->run_count; r++) {
inner_run_src->runs[r].offset = opt->runs[r].offset;
inner_run_src->runs[r].length = opt->runs[r].length;
}
DecryptStream* inner_stream = malloc(sizeof(DecryptStream));
if (!inner_stream) {
free(inner_run_src);
continue;
}
if (!stream_init_from_runs(inner_stream, inner_run_src, derived_key, inner_iv)) {
free(inner_run_src);
free(inner_stream);
continue;
}
inner_stream->data_offset = inner_data_offset;
inner_stream->data_size = inner_data_size;
if (is_inner_apm3) {
NTFSContext inner_ntfs = {0};
inner_ntfs.silent = app_ctx->silent;
inner_ntfs.verbose = app_ctx->verbose;
if (ntfs_init_stream(&inner_ntfs, inner_stream, inner_output_dir)) {
inner_ntfs.silent = true;
if (ntfs_extract_all(&inner_ntfs)) {
inner_ntfs.silent = app_ctx->silent;
bool is_orphan = false;
if (inner_ntfs.pending_vhd_count > 0) {
ntfs_extract_pending_vhds(&inner_ntfs, app_ctx->silent, app_ctx->verbose, &is_orphan);
}
if (is_orphan) {
VERBOSE(app_ctx, " orphan\n");
RMDIR(inner_output_dir);
} else {
VERBOSE(app_ctx, " inner ok\n");
app_ctx->total_files_extracted += inner_ntfs.files_extracted;
app_ctx->total_bytes_extracted += inner_ntfs.extracted_bytes;
}
}
ntfs_close(&inner_ntfs);
}
} else {
ExfatContext inner_exfat;
if (exfat_init_stream(&inner_exfat, inner_stream)) {
inner_exfat.silent = app_ctx->silent;
inner_exfat.verbose = app_ctx->verbose;
if (exfat_extract_all(&inner_exfat, inner_output_dir)) {
VERBOSE(app_ctx, " inner ok\n");
app_ctx->total_files_extracted += inner_exfat.files_extracted;
app_ctx->total_bytes_extracted += inner_exfat.extracted_bytes;
}
exfat_close(&inner_exfat);
}
}
free(inner_run_src);
free(inner_stream);
}
ntfs_close(&ctx);
} else {
ntfs_close(&ctx);
}
}
else {
fprintf(stderr, "ntfs\n");
ntfs_close(&ctx);
}
}
else {
fprintf(stderr, "ntfsi\n");
}
}
else {
NTFSContext ctx = { 0 };
ctx.silent = app_ctx->silent;
ctx.verbose = app_ctx->verbose;
if (ntfs_init_stream(&ctx, stream, output_dir)) {
int vhd_type = ntfs_detect_vhd_type(&ctx);
if (vhd_type == VHD_TYPE_DIFFERENCING && !app_ctx->parent_file) {
PRINT(app_ctx, "diff VHD, use -p\n");
ntfs_close(&ctx);
result = ERR_OK;
goto cleanup;
}
VERBOSE(app_ctx, "MFT=%llu c=%u\n",
(unsigned long long)ctx.total_mft_records, ctx.bytes_per_cluster);
ctx.silent = true;
if (ntfs_extract_all(&ctx)) {
extraction_success = true;
ctx.silent = app_ctx->silent;
if (ctx.pending_vhd_count > 0) {
bool is_orphan = false;
ntfs_extract_pending_vhds(&ctx, app_ctx->silent, app_ctx->verbose, &is_orphan);
}
app_ctx->total_files_extracted += ctx.files_extracted;
app_ctx->total_bytes_extracted += ctx.extracted_bytes;
}
else {
fprintf(stderr, "ntfs\n");
}
ntfs_close(&ctx);
}
else {
fprintf(stderr, "ntfsi\n");
}
}
if (extraction_success) {
result = ERR_OK;
}
cleanup:
if (file) fclose(file);
free(read_buffer);
free(stream);
return result;
}
#ifdef PLATFORM_WINDOWS
static bool env_has_prefix(const WCHAR* env, const WCHAR* prefix) {
while (*prefix) {
if (*env != *prefix) return false;
env++; prefix++;
}
return true;
}
static bool wchar_iequals(const WCHAR* a, const WCHAR* b) {
while (*a && *b) {
WCHAR ca = *a, cb = *b;
if (ca >= 'A' && ca <= 'Z') ca += 32;
if (cb >= 'A' && cb <= 'Z') cb += 32;
if (ca != cb) return false;
a++; b++;
}
return *a == *b;
}
static bool get_parent_process_name(WCHAR* out_name, size_t max_chars) {
out_name[0] = 0;
PROCESS_BASIC_INFORMATION pbi;
ULONG ret_len;
#define NtCurrentProcess() ((HANDLE)(intptr_t)-1)
NTSTATUS status = NtQueryInformationProcess(NtCurrentProcess(), ProcessBasicInformation, &pbi, sizeof(pbi), &ret_len);
if (!NT_SUCCESS(status)) return false;
uintptr_t parent_pid = pbi.InheritedFromUniqueProcessId;
if (parent_pid == 0) return false;
ULONG buf_size = 1024 * 1024;
uint8_t* buf = NULL;
#define STATUS_INFO_LENGTH_MISMATCH ((NTSTATUS)0xC0000004L)
for (int attempt = 0; attempt < 3; attempt++) {
buf = lib_malloc(buf_size);
if (!buf) return false;
status = NtQuerySystemInformation(SystemProcessInformation, buf, buf_size, &ret_len);
if (NT_SUCCESS(status)) break;
lib_free(buf);
buf = NULL;
if (status == STATUS_INFO_LENGTH_MISMATCH) {
buf_size = ret_len + 65536;
} else {
return false;
}
}
if (!buf) return false;
SYSTEM_PROCESS_INFORMATION* proc = (SYSTEM_PROCESS_INFORMATION*)buf;
bool found = false;
while (1) {
if (proc->UniqueProcessId == parent_pid && proc->ImageName.Buffer) {
WCHAR* name = proc->ImageName.Buffer;
WCHAR* last_slash = name;
for (WCHAR* p = name; *p; p++) {
if (*p == '\\' || *p == '/') last_slash = p + 1;
}
size_t i = 0;
while (last_slash[i] && i < max_chars - 1) {
out_name[i] = last_slash[i];
i++;
}
out_name[i] = 0;
found = true;
break;
}
if (proc->NextEntryOffset == 0) break;
proc = (SYSTEM_PROCESS_INFORMATION*)((uint8_t*)proc + proc->NextEntryOffset);
}
lib_free(buf);
return found;
}
static bool from_explorer(void) {
WCHAR parent_name[260];
if (get_parent_process_name(parent_name, 260)) {
static const WCHAR cmd[] = {'c','m','d','.','e','x','e',0};
static const WCHAR powershell[] = {'p','o','w','e','r','s','h','e','l','l','.','e','x','e',0};
static const WCHAR pwsh[] = {'p','w','s','h','.','e','x','e',0};
static const WCHAR wt[] = {'W','i','n','d','o','w','s','T','e','r','m','i','n','a','l','.','e','x','e',0};
static const WCHAR code[] = {'C','o','d','e','.','e','x','e',0};
static const WCHAR conhost[] = {'c','o','n','h','o','s','t','.','e','x','e',0};
if (wchar_iequals(parent_name, cmd)) return false;
if (wchar_iequals(parent_name, powershell)) return false;
if (wchar_iequals(parent_name, pwsh)) return false;
if (wchar_iequals(parent_name, wt)) return false;
if (wchar_iequals(parent_name, code)) return false;
if (wchar_iequals(parent_name, conhost)) return false;
static const WCHAR explorer[] = {'e','x','p','l','o','r','e','r','.','e','x','e',0};
if (wchar_iequals(parent_name, explorer)) return true;
}
PEB* peb = lib_get_peb();
RTL_USER_PROCESS_PARAMETERS* params = peb->ProcessParameters;
WCHAR* env = params->Environment;
if (!env) return true;
static const WCHAR prompt[] = {'P','R','O','M','P','T','=',0};
static const WCHAR wt_session[] = {'W','T','_','S','E','S','S','I','O','N','=',0};
static const WCHAR term_prog[] = {'T','E','R','M','_','P','R','O','G','R','A','M','=',0};
WCHAR* scan = env;
while (*scan) {
if (env_has_prefix(scan, prompt)) return false;
if (env_has_prefix(scan, wt_session)) return false;
if (env_has_prefix(scan, term_prog)) return false;
while (*scan) scan++;
scan++;
}
return true;
}
#else
static bool from_explorer(void) {
return false;
}
#endif
static void show_usage(void) {
puts("unsegaREBORN [flags] <files>\n-o dir -n -w -p parent -s -v -vn");
}
#ifdef PLATFORM_WINDOWS
__declspec(dllimport) NTSTATUS __stdcall NtDelayExecution(uint8_t Alertable, LARGE_INTEGER* DelayInterval);
#endif
static void wait_for_enter(void) {
#ifdef PLATFORM_WINDOWS
extern HANDLE lib_stdin_handle;
if (!lib_stdin_handle || lib_stdin_handle == INVALID_HANDLE_VALUE) return;
char buf[16];
IO_STATUS_BLOCK iosb = {0};
NtReadFile(lib_stdin_handle, NULL, NULL, NULL, &iosb, buf, 1, NULL, NULL);
#else
char c;
lib_fread(&c, 1, 1, stdin);
#endif
}
static void show_info(void) {
printf("unsegaREBORN %s\ndrag files or use flags\nkeys: keys.inc\nenter...", VERSION);
fflush(stdout);
wait_for_enter();
}
int lib_main(int argc, char** argv) {
AppContext app_ctx = {0};
app_ctx.extract_fs = true;
app_ctx.start_time = time(NULL);
int start_index = 1;
if (argc < 2) {
if (from_explorer()) {
show_info();
} else {
show_usage();
}
return 0;
}
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-vn") == 0) {
printf("unsegaREBORN %s\n", VERSION);
return 0;
}
if (strcmp(argv[i], "-h") == 0) {
show_usage();
return 0;
}
}
const char* input_files[256];
int input_file_count = 0;
for (int i = start_index; i < argc && input_file_count < 256; i++) {
const char* a = argv[i];
if (a[0] == '-') {
if (strcmp(a, "-n") == 0) app_ctx.extract_fs = false;
else if (strcmp(a, "-s") == 0) app_ctx.silent = true;
else if (strcmp(a, "-v") == 0) app_ctx.verbose = true;
else if (strcmp(a, "-w") == 0) app_ctx.write_intermediate = true;
else if (strcmp(a, "-o") == 0 && i + 1 < argc) app_ctx.output_dir = argv[++i];
else if (strcmp(a, "-p") == 0 && i + 1 < argc) app_ctx.parent_file = argv[++i];
continue;
}
input_files[input_file_count++] = a;
}
if (input_file_count == 0) { fprintf(stderr, "no files\n"); return 1; }
if (!key_any()) fprintf(stderr, "no keys\n");
bool any_failed = false;
for (int i = 0; i < input_file_count; ++i) {
const char* file_path = input_files[i];
if (do_file(&app_ctx, file_path) == ERR_OK) {
if (app_ctx.extract_fs && app_ctx.output_filename) {
char output_dir[MAX_PATH_LENGTH];
const char* basename = strrchr(app_ctx.output_filename, '/');
if (!basename) basename = strrchr(app_ctx.output_filename, '\\');
basename = basename ? basename + 1 : app_ctx.output_filename;
char basename_no_ext[MAX_PATH_LENGTH];
strncpy(basename_no_ext, basename, sizeof(basename_no_ext) - 1);
basename_no_ext[sizeof(basename_no_ext) - 1] = '\0';
char* ext = strrchr(basename_no_ext, '.');
if (ext) *ext = '\0';
if (app_ctx.output_dir) {
path_join(output_dir, sizeof(output_dir), app_ctx.output_dir, basename_no_ext);
} else {
strncpy(output_dir, app_ctx.output_filename, sizeof(output_dir) - 1);
output_dir[sizeof(output_dir) - 1] = '\0';
ext = strrchr(output_dir, '.');
if (ext) *ext = '\0';
}
bool extraction_success = false;
if (strstr(app_ctx.output_filename, ".exfat") != NULL) {
ExfatContext ctx;
if (exfat_init(&ctx, app_ctx.output_filename)) {
ctx.silent = app_ctx.silent;
ctx.verbose = app_ctx.verbose;
VERBOSE(&app_ctx, "exfat c=%u f=%u\n", ctx.bytes_per_cluster, ctx.fat_length_bytes);
if (exfat_extract_all(&ctx, output_dir)) {
extraction_success = true;
app_ctx.total_files_extracted += ctx.files_extracted;
app_ctx.total_bytes_extracted += ctx.extracted_bytes;
}
exfat_close(&ctx);
}
}
else if (strstr(app_ctx.output_filename, ".ntfs") != NULL) {
NTFSContext ctx = { 0 };
ctx.silent = app_ctx.silent;
ctx.verbose = app_ctx.verbose;
if (ntfs_init(&ctx, app_ctx.output_filename, output_dir)) {
int vhd_type = ntfs_detect_vhd_type(&ctx);
if (vhd_type == VHD_TYPE_DIFFERENCING && !app_ctx.parent_file) {
PRINT(&app_ctx, "\ndiff VHD, use -p\n");
ntfs_close(&ctx);
} else {
VERBOSE(&app_ctx, "ntfs MFT=%llu c=%u\n",
(unsigned long long)ctx.total_mft_records, ctx.bytes_per_cluster);
if (ntfs_extract_all(&ctx)) {
extraction_success = true;
app_ctx.total_files_extracted += ctx.files_extracted;
app_ctx.total_bytes_extracted += ctx.extracted_bytes;
char vhd_path[MAX_PATH_LENGTH];
int highest_vhd = -1;
for (int vhd_num = 0; vhd_num < 10; vhd_num++) {
snprintf(vhd_path, sizeof(vhd_path), "%s%sinternal_%d.vhd",
output_dir, PATH_SEPARATOR, vhd_num);
FILE* test = FOPEN(vhd_path, "rb");
if (!test) continue;
fclose(test);
highest_vhd = vhd_num;
}
if (highest_vhd >= 0) {
snprintf(vhd_path, sizeof(vhd_path), "%s%sinternal_%d.vhd",
output_dir, PATH_SEPARATOR, highest_vhd);
char vhd_output_dir[MAX_PATH_LENGTH];
snprintf(vhd_output_dir, sizeof(vhd_output_dir), "%s%scontents",
output_dir, PATH_SEPARATOR);
NTFSContext vhd_ctx = { 0 };
vhd_ctx.silent = app_ctx.silent;
vhd_ctx.verbose = app_ctx.verbose;
if (ntfs_init(&vhd_ctx, vhd_path, vhd_output_dir)) {
VERBOSE(&app_ctx, "vhd MFT=%llu c=%u\n",
(unsigned long long)vhd_ctx.total_mft_records, vhd_ctx.bytes_per_cluster);
if (ntfs_extract_all(&vhd_ctx)) {
app_ctx.total_files_extracted += vhd_ctx.files_extracted;
app_ctx.total_bytes_extracted += vhd_ctx.extracted_bytes;
}
ntfs_close(&vhd_ctx);
}
}
do_inner_opts(&app_ctx, output_dir);
}
ntfs_close(&ctx);
}
}
}
else {
fprintf(stderr, "fs:%s\n", app_ctx.output_filename);
}
free(app_ctx.output_filename);
app_ctx.output_filename = NULL;
}
}
else {
fprintf(stderr, "fail:%s\n", file_path);
any_failed = true;
}
}
if (!app_ctx.silent && app_ctx.total_files_extracted > 0) {
char size_buf[32];
fmt_size(app_ctx.total_bytes_extracted, size_buf, sizeof(size_buf));
printf("\n%llu files %s %ds\n",
(unsigned long long)app_ctx.total_files_extracted, size_buf,
(int)difftime(time(NULL), app_ctx.start_time));
}
if (app_ctx.output_filename) {
free(app_ctx.output_filename);
}
return any_failed ? 1 : 0;
}