diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..7ee6370 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,69 @@ +cmake_minimum_required(VERSION 3.16) +project(unsegaREBORN C) + +set(CMAKE_C_STANDARD 11) + +option(BUILD_STATIC "Build a static executable" OFF) + +if(BUILD_STATIC) + set(OPENSSL_USE_STATIC_LIBS TRUE) + set(OPENSSL_MSVC_STATIC_RT TRUE) + if(WIN32) + set(CMAKE_FIND_LIBRARY_SUFFIXES .lib .a) + else() + set(CMAKE_FIND_LIBRARY_SUFFIXES .a) + endif() +endif() + +find_package(OpenSSL REQUIRED) + +add_library(unsega STATIC + src/crypto.c + src/keys.c + include/crypto.h + src/exfat.c + include/exfat.h + src/ntfs.c + include/ntfs.h + src/bootid.c + include/bootid.h + include/common.h +) + +target_include_directories(unsega PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) +target_link_libraries(unsega PUBLIC OpenSSL::Crypto) + +if (MSVC) + target_compile_definitions(unsega PUBLIC _CRT_SECURE_NO_WARNINGS) + if (MSVC_VERSION LESS 1900) + target_compile_definitions(unsega PUBLIC snprintf=_snprintf) + endif() +elseif(UNIX) + target_compile_definitions(unsega PUBLIC _FILE_OFFSET_BITS=64) +endif() + +add_executable(unsegareborn src/main.c) +target_link_libraries(unsegareborn PRIVATE unsega) + +if(BUILD_STATIC) + if(MSVC) + set_property(TARGET unsegareborn PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") + set_property(TARGET unsega PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") + + target_link_libraries(unsegareborn PRIVATE + ws2_32 + crypt32 + bcrypt + Advapi32 + ) + + target_link_options(unsegareborn PRIVATE /INCREMENTAL:NO) + else() + target_link_options(unsegareborn PRIVATE -static) + if(UNIX AND NOT APPLE) + target_link_libraries(unsegareborn PRIVATE dl pthread) + endif() + endif() +endif() + +install(TARGETS unsegareborn RUNTIME DESTINATION bin) diff --git a/README.md b/README.md new file mode 100644 index 0000000..67bb384 --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# unsegaREBORN + +An all-in-one tool that unpacks (and optionally extracts) arcade SEGA images. + +--- + +## Features + +* Decrypts update/app containers +* Extracts the embedded + * NTFS archives (with nested VHDs) + * exFAT archives (cluster-by-cluster walker) +* Cross-platform (Windows / Linux / macOS) + Uses plain C11 + OpenSSL, no fancy dependencies. +* Can be built fully static (`--static`) if you need a drop-in binary. + +--- + +## Building + +### Unix-like (Linux / macOS) + +```bash +./build.sh # release build +./build.sh --debug # debug symbols, no optimisation +./build.sh --static # link everything statically + +### Windows (MSVC or MinGW) + +build.bat :: release build +build.bat --debug :: debug build +build.bat --static :: static CRT + OpenSSL + + +## Usage + +unsegareborn [-no] [image2 …] + + -no just decrypt, do NOT auto-extract the embedded file system + +The program writes a decrypted .ntfs or .exfat file next to the input +and, unless -no is given, immediately unpacks its contents into a folder +with the same stem. + +You can also just drag and drop the image(s) on the program. ("-no" flag is disabled by default) + +## Where do the keys come from? + +They were all collected from leaks over time. +If you have additional keys and want to contribute, feel free to open a PR. +If you are a gatekeeper, you can just use the built in custom key function. + +## License + +UNLICENSE + +## Caveats / TODO + +No APM3 support. Maybe will be added when I feel like it. +Child VHD won't be processed for app files. If the output VHD number is not zero (e.g. internal_1, internal_2) you are on your own. \ No newline at end of file diff --git a/UNLICENSE b/UNLICENSE new file mode 100644 index 0000000..bfdcf39 --- /dev/null +++ b/UNLICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +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 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. + +For more information, please refer to \ No newline at end of file diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..1b8a82a --- /dev/null +++ b/build.bat @@ -0,0 +1,36 @@ +@echo off +REM Build script for unsegaREBORN on Windows + +set BUILD_TYPE=Release +set STATIC_BUILD=OFF + +:parse_args +if "%1"=="" goto build +if "%1"=="--static" ( + set STATIC_BUILD=ON + echo Building static executable... +) +if "%1"=="--debug" ( + set BUILD_TYPE=Debug + echo Building debug version... +) +shift +goto parse_args + +:build +if not exist build mkdir build +cd build + +echo Configuring with CMake... +cmake -DBUILD_STATIC=%STATIC_BUILD% -DCMAKE_BUILD_TYPE=%BUILD_TYPE% .. + +echo Building... +cmake --build . --config %BUILD_TYPE% + +echo. +echo Build complete! +echo Executable: build\%BUILD_TYPE%\unsegareborn.exe +if "%STATIC_BUILD%"=="ON" ( + echo Built as static executable - no external DLLs required +) +cd .. \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..cbf065d --- /dev/null +++ b/build.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# Build script for unsegaREBORN on Linux/macOS + +BUILD_TYPE=Release +STATIC_BUILD=OFF + +while [[ $# -gt 0 ]]; do + case $1 in + --static) + STATIC_BUILD=ON + echo "Building static executable..." + shift + ;; + --debug) + BUILD_TYPE=Debug + echo "Building debug version..." + shift + ;; + *) + echo "Unknown option: $1" + echo "Usage: $0 [--static] [--debug]" + exit 1 + ;; + esac +done + +mkdir -p build +cd build + +echo "Configuring with CMake..." +cmake -DBUILD_STATIC=$STATIC_BUILD -DCMAKE_BUILD_TYPE=$BUILD_TYPE .. + +echo "Building..." +cmake --build . --config $BUILD_TYPE + +cd .. + +echo "" +echo "Build complete!" +echo "Executable: build/unsegareborn" +if [ "$STATIC_BUILD" = "ON" ]; then + echo "Built as static executable - no external dependencies required" +fi \ No newline at end of file diff --git a/include/bootid.h b/include/bootid.h new file mode 100644 index 0000000..5bd875f --- /dev/null +++ b/include/bootid.h @@ -0,0 +1,74 @@ +#ifndef BOOTID_H +#define BOOTID_H + +#include +#include + +static const uint8_t BOOTID_KEY[16] = { + 0x09, 0xCA, 0x5E, 0xFD, 0x30, 0xC9, 0xAA, 0xEF, + 0x38, 0x04, 0xD0, 0xA7, 0xE3, 0xFA, 0x71, 0x20 +}; + +static const uint8_t BOOTID_IV[16] = { + 0xB1, 0x55, 0xC2, 0x2C, 0x2E, 0x7F, 0x04, 0x91, + 0xFA, 0x7F, 0x0F, 0xDC, 0x21, 0x7A, 0xFF, 0x90 +}; + +enum ContainerType { + CONTAINER_TYPE_OS = 0x00, + CONTAINER_TYPE_APP = 0x01, + CONTAINER_TYPE_OPTION = 0x02 +}; + +#pragma pack(push, 1) + +typedef struct { + uint16_t year; + uint8_t month; + uint8_t day; + uint8_t hour; + uint8_t minute; + uint8_t second; + uint8_t unk1; +} Timestamp; + +typedef struct { + uint8_t release; + uint8_t minor; + uint16_t major; +} Version; + +typedef union { + Version version; + uint8_t option[4]; +} GameVersion; + +typedef struct { + uint32_t crc32; + uint32_t length; + uint8_t signature[4]; + uint8_t unk1; + uint8_t container_type; + uint8_t sequence_number; + bool use_custom_iv; + uint8_t game_id[4]; + Timestamp target_timestamp; + GameVersion target_version; + uint64_t block_count; + uint64_t block_size; + uint64_t header_block_count; + uint64_t unk2; + uint8_t os_id[3]; + uint8_t os_generation; + Timestamp source_timestamp; + Version source_version; + Version os_version; + uint8_t padding[8]; + uint8_t extra_padding[4]; +} BootId; + +#pragma pack(pop) + +void format_timestamp(const Timestamp* ts, char* buffer, size_t buffer_size); + +#endif // BOOTID_H \ No newline at end of file diff --git a/include/common.h b/include/common.h new file mode 100644 index 0000000..42d9637 --- /dev/null +++ b/include/common.h @@ -0,0 +1,50 @@ +#ifndef COMMON_H +#define COMMON_H + +#include +#include + +#ifdef _WIN32 + #include + #define MKDIR(path) _mkdir(path) + #define PATH_SEPARATOR "\\" + #define STRDUP _strdup + #define FSEEKO _fseeki64 + #define FTELLO _ftelli64 +#else + #include + #include + #define MKDIR(path) mkdir((path), 0755) + #define PATH_SEPARATOR "/" + #define STRDUP strdup + #define FSEEKO fseeko + #define FTELLO ftello +#endif + +#define MAX_PATH_LENGTH 256 +#define MAX_FILENAME_LENGTH 256 + +#ifndef min + #define min(a,b) ((a) < (b) ? (a) : (b)) +#endif +#ifndef max + #define max(a,b) ((a) > (b) ? (a) : (b)) +#endif + +#define SNPRINTF snprintf + +#ifdef _WIN32 + #ifdef _MSC_VER + #define STRCPY_S(dst, size, src) strcpy_s(dst, size, src) + #define STRCAT_S(dst, size, src) strcat_s(dst, size, src) + #else + #define STRCPY_S(dst, size, src) do { strncpy(dst, src, (size)-1); (dst)[(size)-1] = '\0'; } while(0) + #define STRCAT_S(dst, size, src) strncat(dst, src, (size) - strlen(dst) - 1) + #endif +#else + #define STRCPY_S(dst, size, src) do { strncpy(dst, src, (size)-1); (dst)[(size)-1] = '\0'; } while(0) + #define STRCAT_S(dst, size, src) strncat(dst, src, (size) - strlen(dst) - 1) +#endif + +#endif /* COMMON_H */ + diff --git a/include/crypto.h b/include/crypto.h new file mode 100644 index 0000000..af60aa9 --- /dev/null +++ b/include/crypto.h @@ -0,0 +1,54 @@ +#ifndef CRYPTO_H +#define CRYPTO_H + +#include +#include + +static const uint8_t NTFS_HEADER[16] = { + 0xeb, 0x52, 0x90, 0x4e, 0x54, 0x46, 0x53, 0x20, + 0x20, 0x20, 0x20, 0x00, 0x10, 0x01, 0x00, 0x00 +}; + +static const uint8_t EXFAT_HEADER[16] = { + 0xeb, 0x76, 0x90, 0x45, 0x58, 0x46, 0x41, 0x54, + 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +static const uint8_t OPTION_KEY[16] = { + 0x5c, 0x84, 0xa9, 0xe7, 0x26, 0xea, 0xa5, 0xdd, + 0x35, 0x1f, 0x2b, 0x07, 0x50, 0xc2, 0x36, 0x97 +}; + +static const uint8_t OPTION_IV[16] = { + 0xc0, 0x63, 0xbf, 0x6f, 0x56, 0x2d, 0x08, 0x4d, + 0x79, 0x63, 0xc9, 0x87, 0xf5, 0x28, 0x17, 0x61 +}; + +typedef struct { + uint8_t key[16]; + uint8_t iv[16]; + bool has_iv; +} GameKeys; + +typedef struct { + const char* game_id; + uint8_t key[16]; + uint8_t iv[16]; + bool has_iv; +} GameKeyEntry; + +extern const GameKeyEntry game_keys[]; +extern const size_t game_keys_count; + +void calculate_page_iv(uint64_t file_offset, const uint8_t* file_iv, uint8_t* page_iv); + +bool calculate_file_iv( + const uint8_t key[16], + const uint8_t expected_header[16], + const uint8_t* first_page, + uint8_t out_iv[16] +); + +bool get_game_keys(const char* game_id, GameKeys* out_keys); + +#endif // CRYPTO_H \ No newline at end of file diff --git a/include/exfat.h b/include/exfat.h new file mode 100644 index 0000000..524c804 --- /dev/null +++ b/include/exfat.h @@ -0,0 +1,103 @@ +#ifndef EXFAT_H +#define EXFAT_H + +#include +#include +#include +#include "common.h" + +#define EXFAT_ENTRY_SIZE 32 + +#define EXFAT_ENTRY_EOD 0x00 +#define EXFAT_ENTRY_BITMAP 0x81 +#define EXFAT_ENTRY_FILE 0x85 +#define EXFAT_ENTRY_STREAM 0xC0 +#define EXFAT_ENTRY_FILENAME 0xC1 + +#pragma pack(push, 1) + +typedef struct { + uint8_t jump_boot[3]; + uint8_t fs_name[8]; + uint8_t must_be_zero[53]; + uint64_t partition_offset; + uint64_t volume_length; + uint32_t fat_offset; + uint32_t fat_length; + uint32_t cluster_heap_offset; + uint32_t cluster_count; + uint32_t first_cluster_of_root_dir; + uint32_t volume_serial_number; + uint16_t fs_revision; + uint16_t volume_flags; + uint8_t bytes_per_sector_shift; + uint8_t sectors_per_cluster_shift; + uint8_t number_of_fats; + uint8_t drive_select; + uint8_t percent_in_use; + uint8_t reserved[7]; + uint8_t boot_code[390]; + uint16_t boot_signature; +} ExfatBootSector; + +typedef struct { + uint8_t entry_type; + uint8_t secondary_count; + uint16_t set_checksum; + uint16_t file_attributes; + uint16_t reserved1; + uint32_t create_timestamp; + uint32_t last_modified_timestamp; + uint32_t last_access_timestamp; + uint8_t create_10ms; + uint8_t last_modified_10ms; + uint8_t create_utc_offset; + uint8_t last_modified_utc_offset; + uint8_t last_access_utc_offset; + uint8_t reserved2[7]; +} ExfatFileEntry; + +typedef struct { + uint8_t entry_type; + uint8_t flags; + uint8_t reserved1; + uint8_t name_length; + uint16_t name_hash; + uint16_t reserved2; + uint64_t valid_data_length; + uint32_t reserved3; + uint32_t first_cluster; + uint64_t data_length; +} ExfatStreamEntry; + +typedef struct { + uint8_t entry_type; + uint8_t flags; + uint16_t file_name[15]; +} ExfatFileNameEntry; + +#pragma pack(pop) + +typedef struct { + char name[MAX_PATH_LENGTH]; + uint32_t first_cluster; + uint64_t data_length; + bool is_directory; +} ExfatFileInfo; + +typedef struct { + FILE* fp; + ExfatBootSector boot_sector; + uint32_t bytes_per_sector; + uint32_t bytes_per_cluster; + uint32_t cluster_heap_offset_bytes; + uint32_t fat_offset_bytes; + uint32_t fat_length_bytes; + uint32_t* fat; +} ExfatContext; + +bool exfat_init(ExfatContext* ctx, const char* filename); +bool exfat_extract_all(ExfatContext* ctx, const char* output_dir); +void exfat_close(ExfatContext* ctx); + +#endif // EXFAT_H diff --git a/include/ntfs.h b/include/ntfs.h new file mode 100644 index 0000000..fdaf64c --- /dev/null +++ b/include/ntfs.h @@ -0,0 +1,215 @@ +#ifndef NTFS_H +#define NTFS_H + +#include +#include +#include +#include +#include +#include +#include "common.h" + +#define VHD_FOOTER_SIZE 512 +#define VHD_SECTOR_SIZE 512 +#define VHD_BAT_ENTRY_RESERVED 0xFFFFFFFF +#define NTFS_RECORD_SIZE 1024 +#define MFT_RECORD_MAGIC "FILE" +#define VHD_COOKIE "conectix" +#define VHD_DYNAMIC_COOKIE "cxsparse" +#define VHD_TYPE_FIXED 2 +#define VHD_TYPE_DYNAMIC 3 +#define FILE_NAME_ATTR 0x30 +#define DATA_ATTR 0x80 +#define INDEX_ROOT_ATTR 0x90 +#define INDEX_ALLOCATION_ATTR 0xA0 +#define NTFS_SIGNATURE "NTFS " +#define NTFS_PARTITION_TYPE 0x07 +#define MFT_RECORD_IN_USE 0x0001 +#define MFT_RECORD_IS_DIRECTORY 0x0002 + +typedef struct { + uint64_t ref_number; + char path[MAX_PATH_LENGTH]; +} DirectoryInfo; + +typedef struct { + DirectoryInfo* directories; + size_t capacity; + size_t count; +} DirectoryCache; + +#pragma pack(push, 1) + +typedef struct { + char cookie[8]; + uint32_t features; + uint32_t version; + uint64_t data_offset; + uint32_t timestamp; + uint32_t creator_app; + uint32_t creator_ver; + uint32_t creator_os; + uint64_t original_size; + uint64_t current_size; + uint16_t cylinder; + uint8_t heads; + uint8_t sectors; + uint32_t disk_type; + uint32_t checksum; + uint8_t unique_id[16]; + uint8_t saved_state; + uint8_t reserved[427]; +} VHDFooter; + +typedef struct { + char cookie[8]; + uint64_t data_offset; + uint64_t bat_offset; + uint32_t head_vers; + uint32_t max_bat_entries; + uint32_t block_size; + uint32_t checksum; + uint8_t parent_id[16]; + uint32_t parent_timestamp; + uint32_t reserved1; + uint16_t parent_name[256]; + uint8_t parent_loc[8][512]; + uint8_t reserved2[256]; +} VHDDynamicHeader; + +typedef struct { + uint8_t jump[3]; + uint8_t signature[8]; + uint16_t bytes_per_sector; + uint8_t sectors_per_cluster; + uint16_t reserved_sectors; + uint8_t always_zero1[3]; + uint16_t not_used1; + uint8_t media_descriptor; + uint16_t always_zero2; + uint16_t sectors_per_track; + uint16_t number_of_heads; + uint32_t hidden_sectors; + uint32_t not_used2; + uint32_t not_used3; + uint64_t total_sectors; + uint64_t mft_cluster_number; + uint64_t mft_mirror_cluster_number; + int8_t clusters_per_mft_record; + uint8_t not_used4[3]; + int8_t clusters_per_index_record; + uint8_t not_used5[3]; + uint64_t volume_serial_number; + uint32_t checksum; +} NTFSBootSector; + +typedef struct { + char magic[4]; + uint16_t usa_offset; + uint16_t usa_count; + uint64_t lsn; + uint16_t sequence_number; + uint16_t link_count; + uint16_t attrs_offset; + uint16_t flags; + uint32_t bytes_used; + uint32_t bytes_allocated; + uint64_t base_ref; + uint16_t next_attr_id; + uint16_t record_number; + uint16_t usa_value; +} MFTRecordHeader; + +typedef struct { + uint32_t type; + uint32_t length; + uint8_t non_resident; + uint8_t name_length; + uint16_t name_offset; + uint16_t flags; + uint16_t attribute_id; + union { + struct { + uint32_t value_length; + uint16_t value_offset; + uint16_t flags; + } resident; + struct { + uint64_t lowest_vcn; + uint64_t highest_vcn; + uint16_t mapping_pairs_offset; + uint16_t compression_unit; + uint32_t padding; + uint64_t allocated_size; + uint64_t data_size; + uint64_t initialized_size; + uint64_t compressed_size; + } non_resident; + } data; +} AttributeHeader; + +typedef struct { + uint64_t parent_directory; + uint64_t creation_time; + uint64_t modification_time; + uint64_t mft_modification_time; + uint64_t access_time; + uint64_t allocated_size; + uint64_t real_size; + uint32_t flags; + uint32_t reparse_value; + uint8_t name_length; + uint8_t namespace; + uint16_t name[256]; +} FileNameAttribute; + +typedef struct { + uint64_t offset; + uint64_t length; +} DataRun; + +#pragma pack(pop) + +typedef struct { + FILE* fp; + VHDFooter footer; + VHDDynamicHeader dyn_header; + uint32_t* bat; + uint32_t sector_bitmap_size; + uint8_t* sector_bitmap; + uint8_t* block_buffer; +} VHDContext; + +typedef struct { + char name[MAX_FILENAME_LENGTH]; + uint64_t parent_ref; + bool is_directory; + bool valid; +} FileInfo; + +typedef struct { + FILE* fp; +} RawNTFSContext; + +typedef struct { + union { + VHDContext vhd; + RawNTFSContext raw; + }; + bool is_vhd; + NTFSBootSector boot; + uint32_t bytes_per_cluster; + uint64_t mft_offset; + uint32_t mft_record_size; + uint64_t mft_data_size; + uint64_t total_mft_records; + char base_path[MAX_PATH_LENGTH]; + DirectoryCache dir_cache; + uint64_t data_start_offset; +} NTFSContext; + +bool ntfs_init(NTFSContext* ctx, const char* vhd_path, const char* extract_path); +bool ntfs_extract_all(NTFSContext* ctx); +void ntfs_close(NTFSContext* ctx); + +#endif // NTFS_H \ No newline at end of file diff --git a/src/bootid.c b/src/bootid.c new file mode 100644 index 0000000..0abec04 --- /dev/null +++ b/src/bootid.c @@ -0,0 +1,7 @@ +#include "bootid.h" +#include + +void format_timestamp(const Timestamp* ts, char* buffer, size_t buffer_size) { + snprintf(buffer, buffer_size, "%04d%02d%02d%02d%02d%02d", + ts->year, ts->month, ts->day, ts->hour, ts->minute, ts->second); +} \ No newline at end of file diff --git a/src/crypto.c b/src/crypto.c new file mode 100644 index 0000000..b07c82b --- /dev/null +++ b/src/crypto.c @@ -0,0 +1,116 @@ +#include "crypto.h" +#include "common.h" +#include +#include +#include +#include +#include + +void calculate_page_iv(uint64_t file_offset, const uint8_t* file_iv, uint8_t* page_iv) { + for (int i = 0; i < 16; i++) { + page_iv[i] = file_iv[i] ^ ((file_offset >> (8 * (i % 8))) & 0xFF); + } +} + +bool calculate_file_iv( + const uint8_t key[16], + const uint8_t expected_header[16], + const uint8_t* first_page, + uint8_t out_iv[16] +) { + uint8_t iv[16]; + uint8_t header[16]; + memcpy(header, first_page, 16); + + calculate_page_iv(0, expected_header, iv); + + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + if (!ctx) + return false; + + int len; + int plaintext_len; + + if (1 != EVP_DecryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, key, iv)) { + EVP_CIPHER_CTX_free(ctx); + return false; + } + + EVP_CIPHER_CTX_set_padding(ctx, 0); + + uint8_t decrypted[16]; + + if (1 != EVP_DecryptUpdate(ctx, decrypted, &len, header, 16)) { + EVP_CIPHER_CTX_free(ctx); + return false; + } + plaintext_len = len; + + if (1 != EVP_DecryptFinal_ex(ctx, decrypted + len, &len)) { + EVP_CIPHER_CTX_free(ctx); + return false; + } + plaintext_len += len; + + EVP_CIPHER_CTX_free(ctx); + + if (plaintext_len != 16) + return false; + + memcpy(out_iv, decrypted, 16); + + return true; +} + +bool get_game_keys(const char* game_id, GameKeys* out_keys) { + const GameKeyEntry* entry = game_keys; + while (entry->game_id != NULL) { + if (strcmp(entry->game_id, game_id) == 0) { + memcpy(out_keys->key, entry->key, 16); + if (entry->has_iv) { + memcpy(out_keys->iv, entry->iv, 16); + out_keys->has_iv = true; + } + else { + memset(out_keys->iv, 0, 16); + out_keys->has_iv = false; + } + return true; + } + entry++; + } + + char filename[256]; + SNPRINTF(filename, sizeof(filename), "%s.bin", game_id); + + FILE* file = fopen(filename, "rb"); + if (!file) { + return false; + } + + uint8_t buffer[32]; + size_t read_size = fread(buffer, 1, sizeof(buffer), file); + fclose(file); + + if (read_size == 16) { + memcpy(out_keys->key, buffer, 16); + memset(out_keys->iv, 0, 16); + out_keys->has_iv = false; + return true; + } + else if (read_size == 32) { + memcpy(out_keys->key, buffer, 16); + memcpy(out_keys->iv, buffer + 16, 16); + + if (memcmp(out_keys->iv, NTFS_HEADER, 16) == 0 || memcmp(out_keys->iv, EXFAT_HEADER, 16) == 0) { + out_keys->has_iv = false; + } + else { + out_keys->has_iv = true; + } + return true; + } + else { + return false; + } +} \ No newline at end of file diff --git a/src/exfat.c b/src/exfat.c new file mode 100644 index 0000000..4fab975 --- /dev/null +++ b/src/exfat.c @@ -0,0 +1,300 @@ +#include "exfat.h" +#include +#include +#include +#include +#include +#include + +static bool create_directories(const char* path) { + char temp[MAX_PATH_LENGTH]; + char* p = NULL; + size_t len; + bool result = true; + + strncpy(temp, path, MAX_PATH_LENGTH - 1); + temp[MAX_PATH_LENGTH - 1] = '\0'; + len = strlen(temp); + + if (len > 0 && (temp[len - 1] == '/' || temp[len - 1] == '\\')) { + temp[len - 1] = '\0'; + } + + for (p = temp + 1; *p; p++) { + if (*p == '/' || *p == '\\') { + *p = '\0'; + if (MKDIR(temp) != 0 && errno != EEXIST) { + result = false; + break; + } + *p = PATH_SEPARATOR[0]; + } + } + + if (result && MKDIR(temp) != 0 && errno != EEXIST) { + result = false; + } + + return result; +} + +static uint32_t get_cluster_offset(ExfatContext* ctx, uint32_t cluster) { + return ctx->cluster_heap_offset_bytes + ((cluster - 2) * ctx->bytes_per_cluster); +} + +static bool read_cluster(ExfatContext* ctx, uint32_t cluster, void* buffer) { + uint32_t offset = get_cluster_offset(ctx, cluster); + if (fseek(ctx->fp, offset, SEEK_SET) != 0) { + return false; + } + return fread(buffer, 1, ctx->bytes_per_cluster, ctx->fp) == ctx->bytes_per_cluster; +} + +static uint32_t get_next_cluster(ExfatContext* ctx, uint32_t cluster) { + uint32_t next = ctx->fat[cluster]; + if (next >= 0xFFFFFFF8) { + return 0; // end-of-chain + } + if (next == 0) + { + return cluster + 1; // if cluster in FAT is 0 it means the cluster heap is continous (exfat only) + } + return next; +} + +static bool is_safe_path(const char* path) { + if (!path) return false; + + if (strstr(path, "..") != NULL) return false; + if (strstr(path, "./") != NULL) return false; + if (strstr(path, ".\\") != NULL) return false; + + #ifndef _WIN32 + if (path[0] == '/') return false; + #endif + + return true; +} + +static bool combine_path(char* dest, size_t dest_size, const char* dir, const char* name) { + if (!dest || dest_size == 0 || !dir || !name) { + return false; + } + + if (!is_safe_path(name)) { + return false; + } + + size_t dir_len = strlen(dir); + size_t name_len = strlen(name); + size_t sep_len = (dir_len > 0 && dir[dir_len - 1] != '/' && dir[dir_len - 1] != '\\') ? 1 : 0; + + if (dir_len + sep_len + name_len + 1 > dest_size) { + return false; + } + + STRCPY_S(dest, dest_size, dir); + if (sep_len) { + STRCAT_S(dest, dest_size, PATH_SEPARATOR); + } + STRCAT_S(dest, dest_size, name); + return true; +} + +static bool extract_file(ExfatContext* ctx, ExfatFileInfo* file, const char* output_path) { + FILE* out = fopen(output_path, "wb"); + if (!out) { + return false; + } + + uint32_t current_cluster = file->first_cluster; + uint64_t remaining = file->data_length; + uint8_t* buffer = malloc(ctx->bytes_per_cluster); + if (!buffer) { + fclose(out); + return false; + } + + bool success = true; + while (remaining > 0 && current_cluster != 0 && success) { + if (!read_cluster(ctx, current_cluster, buffer)) { + success = false; + break; + } + + size_t write_size = (remaining > ctx->bytes_per_cluster) ? ctx->bytes_per_cluster : (size_t)remaining; + if (fwrite(buffer, 1, write_size, out) != write_size) { + success = false; + break; + } + + remaining -= write_size; + current_cluster = get_next_cluster(ctx, current_cluster); + } + + free(buffer); + fclose(out); + return success; +} + +static bool process_directory(ExfatContext* ctx, uint32_t start_cluster, const char* output_dir) { + uint8_t* cluster_buffer = malloc(ctx->bytes_per_cluster); + if (!cluster_buffer) { + return false; + } + + uint32_t current_cluster = start_cluster; + bool finished = false; + + while (!finished && current_cluster != 0) { + if (!read_cluster(ctx, current_cluster, cluster_buffer)) { + free(cluster_buffer); + return false; + } + + uint32_t entries_per_cluster = ctx->bytes_per_cluster / EXFAT_ENTRY_SIZE; + uint8_t* entry_ptr = cluster_buffer; + for (uint32_t i = 0; i < entries_per_cluster; ) { + uint8_t entry_type = *entry_ptr; + if (entry_type == EXFAT_ENTRY_EOD) { + finished = true; + break; + } + + if (entry_type == EXFAT_ENTRY_FILE) { + ExfatFileEntry* file_entry = (ExfatFileEntry*)entry_ptr; + ExfatStreamEntry* stream_entry = (ExfatStreamEntry*)(entry_ptr + EXFAT_ENTRY_SIZE); + if (stream_entry->entry_type != EXFAT_ENTRY_STREAM) { + i++; + entry_ptr += EXFAT_ENTRY_SIZE; + continue; + } + int total_name_chars = stream_entry->name_length; + int num_name_entries = (total_name_chars + 14) / 15; + + char full_name[MAX_FILENAME_LENGTH]; + wchar_t full_name_unicode[MAX_FILENAME_LENGTH]; + int pos = 0; + uint8_t* name_entry_ptr = entry_ptr + EXFAT_ENTRY_SIZE * 2; + for (int k = 0; k < num_name_entries; k++) { + ExfatFileNameEntry* name_entry = (ExfatFileNameEntry*)(name_entry_ptr + k * EXFAT_ENTRY_SIZE); + int chars_in_this_entry = (total_name_chars - k * 15 < 15) ? (total_name_chars - k * 15) : 15; + for (int j = 0; j < chars_in_this_entry; j++) { + if (pos < MAX_FILENAME_LENGTH - 1) { + // shouldn't cut off the wide char + full_name_unicode[pos++] = (wchar_t)name_entry->file_name[j]; + } + } + } + full_name_unicode[pos] = L'\0'; + + // convert UTF-16 filename to multibyte +#ifdef _WIN32 + _locale_t locale = _create_locale(LC_ALL, ""); + _wcstombs_l(full_name, full_name_unicode, MAX_FILENAME_LENGTH, locale); + _free_locale(locale); +#else + wcstombs(full_name, full_name_unicode, MAX_FILENAME_LENGTH); +#endif + + ExfatFileInfo file_info; + memset(&file_info, 0, sizeof(file_info)); + strncpy(file_info.name, full_name, MAX_PATH_LENGTH - 1); + file_info.name[MAX_PATH_LENGTH - 1] = '\0'; + file_info.first_cluster = stream_entry->first_cluster; + file_info.data_length = stream_entry->data_length; + file_info.is_directory = ((file_entry->file_attributes & 0x10) != 0); + + char full_path[MAX_PATH_LENGTH]; + if (!combine_path(full_path, sizeof(full_path), output_dir, file_info.name)) { + fprintf(stderr, "Warning: Invalid or too long path, skipping: %s/%s\n", output_dir, file_info.name); + continue; + } + + if (file_info.is_directory) { + if (create_directories(full_path)) { + process_directory(ctx, file_info.first_cluster, full_path); + } + } + else { + extract_file(ctx, &file_info, full_path); + } + + int total_entries = 2 + num_name_entries; + i += total_entries; + entry_ptr += EXFAT_ENTRY_SIZE * total_entries; + continue; + } + else { + i++; + entry_ptr += EXFAT_ENTRY_SIZE; + } + } + + if (!finished) { + current_cluster = get_next_cluster(ctx, current_cluster); + } + } + + free(cluster_buffer); + return true; +} + +bool exfat_init(ExfatContext* ctx, const char* filename) { + memset(ctx, 0, sizeof(ExfatContext)); + + ctx->fp = fopen(filename, "rb"); + if (!ctx->fp) { + return false; + } + + if (fread(&ctx->boot_sector, sizeof(ExfatBootSector), 1, ctx->fp) != 1) { + fclose(ctx->fp); + return false; + } + + ctx->bytes_per_sector = (1 << ctx->boot_sector.bytes_per_sector_shift); + ctx->bytes_per_cluster = ctx->bytes_per_sector * (1 << ctx->boot_sector.sectors_per_cluster_shift); + ctx->cluster_heap_offset_bytes = ctx->boot_sector.cluster_heap_offset * ctx->bytes_per_sector; + + ctx->fat_offset_bytes = ctx->boot_sector.fat_offset * ctx->bytes_per_sector; + ctx->fat_length_bytes = ctx->boot_sector.fat_length * ctx->bytes_per_sector; + + ctx->fat = malloc(ctx->fat_length_bytes); + if (!ctx->fat) { + fclose(ctx->fp); + return false; + } + + if (fseek(ctx->fp, ctx->fat_offset_bytes, SEEK_SET) != 0) { + free(ctx->fat); + fclose(ctx->fp); + return false; + } + + if (fread(ctx->fat, 1, ctx->fat_length_bytes, ctx->fp) != ctx->fat_length_bytes) { + free(ctx->fat); + fclose(ctx->fp); + return false; + } + + return true; +} + +bool exfat_extract_all(ExfatContext* ctx, const char* output_dir) { + if (!create_directories(output_dir)) { + return false; + } + return process_directory(ctx, ctx->boot_sector.first_cluster_of_root_dir, output_dir); +} + +void exfat_close(ExfatContext* ctx) { + if (ctx->fp) { + fclose(ctx->fp); + ctx->fp = NULL; + } + if (ctx->fat) { + free(ctx->fat); + ctx->fat = NULL; + } +} diff --git a/src/keys.c b/src/keys.c new file mode 100644 index 0000000..2c2d6e0 --- /dev/null +++ b/src/keys.c @@ -0,0 +1,224 @@ +#include "crypto.h" + +const GameKeyEntry game_keys[] = { + // Nu Firmware / Hardware Test + {"SBZS", {0x2e, 0xcb, 0xcf, 0xf6, 0x5c, 0xe0, 0xab, 0xec, 0xc1, 0x05, 0x47, 0xf8, 0xac, 0x83, 0x51, 0xd8}, {0xf2, 0xac, 0x6c, 0x28, 0x17, 0xd0, 0x57, 0x4b, 0xba, 0x11, 0x3d, 0x49, 0x7e, 0x31, 0x9f, 0x3e}, true}, + + // KEY CHIP NU FACTORY / KEY CHIP NUSX FACTORY + {"SBZT", {0x9a, 0xb9, 0xce, 0x55, 0xed, 0x9c, 0x19, 0x4a, 0x71, 0x5a, 0x73, 0xa7, 0x69, 0x9f, 0x79, 0x5b}, {0x85, 0x52, 0xde, 0x88, 0xfe, 0xdd, 0xa6, 0xe8, 0x59, 0x36, 0x9f, 0xb0, 0x00, 0xf4, 0x4d, 0x5b}, true}, + + // Nu Firmware / Hardware Test + {"SBZU", {0xeb, 0x12, 0x28, 0x25, 0x4c, 0xdd, 0x30, 0x77, 0xeb, 0x3e, 0x44, 0x1c, 0x02, 0x27, 0xbf, 0x40}, {0x3f, 0x9b, 0x46, 0x76, 0x11, 0x8c, 0xee, 0x12, 0x9f, 0xe2, 0xf1, 0xcb, 0x27, 0x47, 0xbc, 0xa5}, true}, + + // Project DIVA Arcade Future Tone + {"SBZV", {0x32, 0x74, 0xa3, 0x99, 0x59, 0x4d, 0x84, 0x77, 0x96, 0x25, 0x94, 0x0b, 0x69, 0xc0, 0x2d, 0x3f}, {0x67, 0x5b, 0xa6, 0x6d, 0x29, 0xc8, 0x79, 0x23, 0xf5, 0xf1, 0x54, 0xc4, 0x06, 0xaf, 0xee, 0x42}, true}, + + // Wonderland Wars + {"SDAP", {0x41, 0xb5, 0x02, 0x7c, 0x5e, 0x99, 0xd9, 0x4a, 0xa9, 0x33, 0x5d, 0x6d, 0x71, 0x83, 0x8e, 0xcf}, {0x41, 0xb5, 0x02, 0x7c, 0x5e, 0x99, 0xd9, 0x4a, 0xa9, 0x33, 0x5d, 0x6d, 0x71, 0x83, 0x8e, 0xcf}, true}, + + // Herobank Arcade + {"SDAQ", {0xc2, 0x8f, 0x22, 0xbc, 0x1b, 0x33, 0x9a, 0xe6, 0x41, 0x80, 0x73, 0x98, 0x86, 0xdc, 0x83, 0xd6}, {0x0a, 0x29, 0xfd, 0x14, 0x5d, 0x72, 0xbf, 0x8d, 0xed, 0xd4, 0x36, 0x02, 0x5d, 0xf0, 0xa9, 0xfc}, true}, + + // Uranai Collection: Torotte + {"SDAV", {0xee, 0xd9, 0x55, 0x13, 0x26, 0x6a, 0x49, 0x9a, 0x55, 0xe2, 0x65, 0xb0, 0x49, 0x16, 0x9c, 0x44}, {0x84, 0xc0, 0xe5, 0x93, 0x1d, 0x91, 0xa6, 0xa4, 0x77, 0xd6, 0x2c, 0x27, 0x15, 0x46, 0x05, 0x6e}, true}, + + // Shin Kouchuu Ouja Mushiking + {"SDBE", {0x70, 0x53, 0xfb, 0x94, 0x45, 0x72, 0xe5, 0xb6, 0x31, 0xa6, 0x65, 0xce, 0xf4, 0xb5, 0xbc, 0xdd}, {0xae, 0x4d, 0x7e, 0x88, 0x40, 0x02, 0xc7, 0x9e, 0xb3, 0x57, 0x11, 0x55, 0x4d, 0x61, 0x30, 0x57}, true}, + + // E-DEL Sand + {"SDBN", {0xc1, 0xf1, 0x4a, 0xe2, 0xe8, 0x5b, 0x09, 0x5e, 0x31, 0x3c, 0x8b, 0xae, 0xc1, 0x25, 0x80, 0x5e}, {0x3c, 0x53, 0x8e, 0xea, 0x66, 0x25, 0x1a, 0xcd, 0x54, 0x04, 0xb9, 0x3f, 0x89, 0x76, 0xa7, 0xf7}, true}, + + // CHUNITHM + {"SDBT", {0xa6, 0xa8, 0x70, 0x67, 0x1f, 0xd4, 0x32, 0xec, 0x63, 0x7a, 0xdf, 0x7a, 0x82, 0x2f, 0x97, 0xda}, {0x2c, 0x27, 0x7f, 0x31, 0xcd, 0x55, 0x0c, 0xfa, 0x2c, 0x99, 0x3b, 0x4d, 0xd5, 0x6b, 0x85, 0xae}, true}, + + // Sonic Dash Extreme + {"SDBX", {0x3d, 0xc1, 0x9c, 0x2d, 0x0c, 0x20, 0xac, 0x19, 0x9d, 0x5f, 0xa4, 0x6e, 0x7f, 0x63, 0x35, 0xa6}, {0xd8, 0xf0, 0x29, 0xec, 0x90, 0xfe, 0x55, 0xbe, 0x67, 0x58, 0x4f, 0x74, 0x2c, 0x55, 0xef, 0x8b}, true}, + + // Kancolle Arcade + {"SDBZ", {0x52, 0x1b, 0xde, 0x44, 0x60, 0xf4, 0x18, 0x4e, 0xdd, 0x87, 0x91, 0x36, 0xad, 0xee, 0xa5, 0xee}, {0x1b, 0x83, 0x24, 0x03, 0x2d, 0xb6, 0x9d, 0x7b, 0x09, 0x54, 0x79, 0x4a, 0xa2, 0x29, 0xfe, 0x68}, true}, + + // crossbeats REV. + {"SDCA", {0x16, 0x49, 0x49, 0x0a, 0x03, 0xd6, 0xc2, 0xae, 0xc1, 0xc4, 0x96, 0x98, 0x2c, 0xb0, 0x40, 0x5c}, {0x46, 0x80, 0x71, 0x1c, 0x7e, 0x67, 0xa2, 0x6f, 0x92, 0x30, 0xd5, 0xaf, 0x74, 0xb5, 0xdc, 0xfb}, true}, + + // nailpuri + {"SDCD", {0x43, 0xb3, 0x85, 0x02, 0xd8, 0xf6, 0xd3, 0xc7, 0xb0, 0x2b, 0x95, 0xfc, 0x28, 0xdb, 0x53, 0x08}, {0x6d, 0xfc, 0xb9, 0x4b, 0xf7, 0x4f, 0x15, 0x2b, 0x55, 0xf3, 0xe0, 0xc7, 0xf3, 0x5b, 0x44, 0xb5}, true}, + + // Luigi's Mansion Arcade + {"SDCF", {0xdf, 0x98, 0x68, 0x83, 0xda, 0x83, 0x75, 0x38, 0xe3, 0x7b, 0x95, 0x9a, 0x3e, 0x41, 0x17, 0xcd}, {0xda, 0xbf, 0x53, 0x97, 0x38, 0x85, 0x2f, 0x17, 0x71, 0x48, 0x11, 0xaf, 0x70, 0x43, 0x5a, 0x83}, true}, + + // Mario & Sonic at Rio Olympic Games + {"SDCH", {0xe2, 0xda, 0x76, 0x9e, 0x94, 0xf1, 0xd3, 0xac, 0xa1, 0x93, 0x0c, 0xdb, 0xe0, 0x70, 0x8c, 0x9f}, {0xc7, 0xdc, 0xce, 0x20, 0x3c, 0x84, 0xab, 0x04, 0x77, 0x23, 0x6d, 0x69, 0x75, 0x70, 0xda, 0xdc}, true}, + + // KEY CHIP NUSX EDB SOC + {"SDCR", {0x49, 0x61, 0xa5, 0x1f, 0xd3, 0x6f, 0x14, 0xe7, 0x26, 0x64, 0xf5, 0x23, 0x73, 0x05, 0x21, 0x60}, {0x25, 0xd7, 0xd1, 0x34, 0x1a, 0x28, 0x2c, 0x5e, 0x0a, 0x34, 0xc6, 0x45, 0x62, 0xc0, 0x23, 0xec}, true}, + + // Celevie + {"SDCT", {0xd6, 0xae, 0x51, 0xf1, 0x0e, 0xc7, 0x6d, 0xa9, 0x3c, 0x98, 0x18, 0x00, 0xfc, 0x3a, 0xd3, 0xcb}, {0xfb, 0x8e, 0x43, 0xe2, 0x80, 0xd3, 0x30, 0xd0, 0x65, 0x81, 0x73, 0x2f, 0x2e, 0x11, 0xa6, 0xdc}, true}, + + // CYTUS Ω + {"SDCX", {0x79, 0x50, 0x4c, 0xcc, 0x50, 0x9b, 0x67, 0xd1, 0xf7, 0xa3, 0xf5, 0x93, 0xe6, 0xf9, 0xd9, 0xd6}, {0x15, 0x51, 0xea, 0x89, 0x26, 0xf2, 0xae, 0xe2, 0x33, 0xee, 0xc3, 0x09, 0xde, 0x3e, 0x5f, 0x3c}, true}, + + // KEY CHIP NUSX1.1 TDW + {"SDDB", {0x87, 0x56, 0x79, 0xb2, 0xcd, 0x16, 0x37, 0x96, 0x2b, 0x0d, 0xb2, 0x5c, 0x51, 0xfb, 0x21, 0xa6}, {0x8e, 0xf4, 0x47, 0x22, 0xa0, 0x56, 0x6e, 0x8f, 0x57, 0x23, 0x56, 0x24, 0x56, 0x87, 0xfb, 0xe5}, true}, + + // Sangokushi Taisen + {"SDDD", {0x56, 0x4e, 0x96, 0x78, 0x73, 0xde, 0x6c, 0xbc, 0xd2, 0x2e, 0xfe, 0xca, 0x69, 0x52, 0xe9, 0xdc}, {0x4e, 0x3d, 0xd4, 0x65, 0xcf, 0x09, 0xcd, 0x82, 0xb2, 0x59, 0xf7, 0xbe, 0xd5, 0xfc, 0x2d, 0x6d}, true}, + + // Initial D Arcade Stage Zero + {"SDDF", {0x65, 0x05, 0x85, 0x73, 0xa0, 0xcb, 0x81, 0x74, 0x9e, 0x69, 0x4a, 0xe1, 0x64, 0xc6, 0x1b, 0x04}, {0x98, 0x1c, 0x4f, 0x45, 0xe3, 0xc6, 0x95, 0x8f, 0x05, 0x4e, 0x5d, 0x00, 0x91, 0x6b, 0xdf, 0x2b}, true}, + + // KEY CHIP NU1.1 ESC + {"SDDJ", {0x63, 0x0f, 0xe5, 0x22, 0x76, 0x53, 0x7b, 0xd7, 0xfb, 0x26, 0x7a, 0xdf, 0x17, 0x5f, 0x4e, 0x99}, {0xdc, 0x57, 0x55, 0xbe, 0x57, 0xde, 0xd2, 0xcd, 0xb3, 0x44, 0x33, 0xbb, 0xba, 0x22, 0x04, 0xff}, true}, + + // APM3 Sample Program 2 + {"SDDL", {0x99, 0x24, 0x58, 0x29, 0x5f, 0xd0, 0x6d, 0x6a, 0x8a, 0xf0, 0xdf, 0xb3, 0xf6, 0x85, 0x4c, 0x19}, {0x84, 0x84, 0x90, 0x6d, 0x4c, 0xd5, 0xfd, 0x22, 0x5e, 0x03, 0x28, 0x43, 0xed, 0x37, 0x49, 0x5d}, true}, + + // ALLS MX Factory Dummy + {"SDDM", {0x01, 0x27, 0x95, 0x82, 0x10, 0xf6, 0xae, 0x9b, 0xde, 0xb8, 0x97, 0x50, 0x18, 0xb5, 0xaf, 0x24}, {0x18, 0x17, 0x16, 0xba, 0xdc, 0xcf, 0xf4, 0xbc, 0x2b, 0x1e, 0x29, 0xae, 0x02, 0xa1, 0xbb, 0xbb}, true}, + + // Demo ID + {"SDDN", {0x41, 0xdd, 0x8e, 0x66, 0x29, 0x01, 0x17, 0xac, 0x67, 0xd3, 0x11, 0xa2, 0xf0, 0xa6, 0x41, 0x6e}, {0x73, 0xe1, 0x8e, 0x84, 0x18, 0xf6, 0xce, 0xef, 0xb1, 0x1e, 0x27, 0x67, 0xfd, 0xea, 0x19, 0x0c}, true}, + + // Soul Reverse + {"SDDP", {0xcf, 0x6d, 0x64, 0x42, 0x7e, 0xec, 0xa4, 0x76, 0x74, 0xe1, 0x7b, 0xcd, 0x46, 0xd1, 0xea, 0x8c}, {0xce, 0x51, 0x74, 0x09, 0x3d, 0x26, 0xca, 0x2a, 0x31, 0xb5, 0x85, 0x41, 0xe8, 0x5a, 0xc2, 0x76}, true}, + + // SEGA World Driver Championship + {"SDDS", {0x16, 0x1b, 0xec, 0x6d, 0x90, 0x98, 0x9d, 0x0e, 0x26, 0xd7, 0x91, 0x17, 0x06, 0x07, 0xa4, 0x40}, {0x81, 0xdc, 0x26, 0xa2, 0x70, 0x28, 0xe2, 0x09, 0x23, 0x32, 0x03, 0x8a, 0xa1, 0xbf, 0xfc, 0x47}, true}, + + // O.N.G.E.K.I. + {"SDDT", {0x3f, 0x76, 0x58, 0x72, 0x8b, 0x95, 0x17, 0xd3, 0x31, 0x4e, 0x68, 0x4f, 0xa2, 0xe2, 0xa0, 0x45}, {0x41, 0x57, 0x88, 0x33, 0xc5, 0x47, 0xaa, 0xff, 0x04, 0xdb, 0x59, 0x7a, 0x6e, 0x9e, 0xb7, 0x84}, true}, + + // Let's dzuri GO! + {"SDDU", {0x64, 0x9a, 0xe9, 0x98, 0x26, 0x25, 0xf9, 0x0c, 0x55, 0xaf, 0x86, 0x71, 0x3c, 0x55, 0xd3, 0xfd}, {0x18, 0x71, 0x16, 0xfc, 0x46, 0x47, 0xa7, 0xd3, 0xb6, 0xf2, 0x30, 0x3a, 0x34, 0xf0, 0xa2, 0xfe}, true}, + + // ALLS X / X2 Research & Development + {"SDDW", {0x11, 0x85, 0x65, 0xd3, 0x44, 0xf3, 0xe1, 0x4c, 0xa6, 0x92, 0x99, 0xee, 0xac, 0x04, 0x9b, 0xb9}, {0x9d, 0x6d, 0x39, 0x2e, 0xc3, 0x5e, 0xd9, 0x4e, 0xf9, 0xfe, 0x0a, 0x5b, 0xe0, 0x57, 0x39, 0x81}, true}, + + // KEY CHIP ALLS X FACTORY + {"SDDX", {0x42, 0x8b, 0xff, 0x0f, 0x9e, 0x7a, 0xaf, 0xc1, 0x69, 0xa7, 0xa7, 0x57, 0x51, 0xff, 0xda, 0x98}, {0xf8, 0x25, 0x05, 0x94, 0xf4, 0x25, 0x33, 0x2c, 0x6d, 0x34, 0x9d, 0x7e, 0xa0, 0xe8, 0x66, 0x69}, true}, + + // Shin Kouchuu Ouja Mushiking TWN + {"SDEA", {0x9f, 0x9c, 0xf1, 0x48, 0xac, 0x3c, 0x50, 0xaa, 0xf9, 0x25, 0xaf, 0x1d, 0xfb, 0x27, 0xf5, 0x8b}, {0x4d, 0x8e, 0xbb, 0xd9, 0x71, 0x89, 0x6b, 0x8a, 0x4a, 0x3d, 0xd8, 0x4a, 0x23, 0xb3, 0x29, 0xfc}, true}, + + // WCCF FOOTISTA + {"SDEB", {0xd5, 0x11, 0xed, 0x69, 0x04, 0x15, 0xf6, 0x35, 0x98, 0x43, 0xa1, 0x34, 0xfd, 0x47, 0x83, 0x6a}, {0xac, 0x13, 0x9b, 0x38, 0x2a, 0xcd, 0xd1, 0x12, 0xe3, 0x15, 0x64, 0xea, 0x7f, 0x38, 0x18, 0x6c}, true}, + + // Chrono Regalia + {"SDEC", {0xf2, 0x72, 0xe5, 0x01, 0x68, 0x63, 0xaf, 0x2b, 0xa0, 0x33, 0x7f, 0x50, 0xde, 0x68, 0x6f, 0x6e}, {0x53, 0x27, 0xe1, 0x32, 0x63, 0x1e, 0x7f, 0x71, 0xb6, 0x1b, 0xe7, 0xcc, 0x0d, 0xf3, 0x82, 0xce}, true}, + + // CARD MAKER + {"SDED", {0x21, 0xfc, 0xec, 0x77, 0x9a, 0x16, 0x76, 0x9f, 0x52, 0x77, 0xa3, 0x6f, 0xb5, 0x42, 0x99, 0x2c}, {0x22, 0xb5, 0x02, 0x39, 0xf1, 0xb4, 0x0c, 0xcc, 0x3e, 0x55, 0xa2, 0xd6, 0x9c, 0x69, 0xb1, 0x60}, true}, + + // House of the Dead: Scarlet Dawn + {"SDEE", {0x19, 0x1e, 0xb7, 0x44, 0x06, 0x72, 0xda, 0xb0, 0x8d, 0xdb, 0xb7, 0x19, 0x5e, 0xfb, 0x35, 0x6f}, {0xc2, 0x78, 0xb5, 0x38, 0x6d, 0xc3, 0x8b, 0xd7, 0x6d, 0x71, 0xdb, 0xcd, 0x82, 0x69, 0x54, 0xcf}, true}, + + // FiZ + { "SDEG", {0x72, 0x18, 0x53, 0xdb, 0xe2, 0xd3, 0x0b, 0xaf, 0xe2, 0x4f, 0x0e, 0xdb, 0xd2, 0x10, 0xde, 0xeb}, {0x4d, 0xfb, 0x0b, 0xce, 0xc8, 0x61, 0x59, 0xaa, 0xb2, 0x97, 0x16, 0x6b, 0xcd, 0x50, 0x9e, 0x6f}, true }, + + // Fate/Grand Order Arcade + { "SDEJ", {0x9d, 0xe1, 0xea, 0x6a, 0xe3, 0x8d, 0x90, 0x11, 0xf5, 0x5d, 0x8e, 0xe8, 0x64, 0x39, 0x5d, 0x24}, {0xf6, 0x0c, 0xde, 0x21, 0x98, 0x28, 0x76, 0xd1, 0x2d, 0x17, 0x66, 0x2a, 0x48, 0xd9, 0x08, 0x36}, true }, + + // ALL.Net P.ras multi Ver.3 + { "SDEM", {0x70, 0x06, 0x17, 0xf2, 0x93, 0x69, 0x6c, 0x07, 0xfb, 0x9f, 0x35, 0x6d, 0x3b, 0x99, 0x24, 0x0d}, {0x66, 0x7d, 0x02, 0x6d, 0x6c, 0xdf, 0x32, 0x9f, 0xf3, 0x51, 0xdb, 0xaf, 0x70, 0x98, 0xe8, 0x1d}, true }, + + // StarHorse4 (Server) / MESTA Medal Station + { "SDEP", {0xfa, 0x2b, 0x7c, 0xa5, 0x3a, 0x82, 0x3c, 0x15, 0x2d, 0x94, 0x09, 0x72, 0xcb, 0xf5, 0x32, 0xf5}, {0xf4, 0xaf, 0x35, 0x12, 0x0c, 0x48, 0x61, 0x77, 0x04, 0xbb, 0x5b, 0x84, 0x71, 0x79, 0x7a, 0x62}, true }, + + // Initial D Arcade Stage Zero (CHN) + { "SDER", {0x7d, 0x73, 0x36, 0x7e, 0xbb, 0x21, 0x8e, 0xc8, 0x29, 0x30, 0xd5, 0x8d, 0xc6, 0xd7, 0x95, 0x0b}, {0x97, 0x88, 0xc3, 0xec, 0xa2, 0xdb, 0x6b, 0xa9, 0x2b, 0xac, 0x4f, 0x6f, 0x7b, 0x70, 0x63, 0x08}, true }, + + // House of the Dead: Scarlet Dawn (EXP) + { "SDET", {0x46, 0x43, 0xe7, 0xb2, 0xc3, 0x00, 0x6e, 0x02, 0x64, 0x16, 0x3e, 0xdc, 0x85, 0x45, 0xfb, 0x72}, {0x61, 0x2b, 0xca, 0x81, 0xea, 0x29, 0x58, 0xff, 0xba, 0xc3, 0x6f, 0x78, 0x0f, 0x1e, 0xd6, 0x88}, true }, + + // KEY CHIP ALLS X HDZ + { "SDEU", {0x23, 0xb3, 0xe9, 0xbb, 0x47, 0xe3, 0xac, 0x99, 0x98, 0xf6, 0xe6, 0xc1, 0xad, 0xc4, 0xae, 0x33}, {0xa9, 0x64, 0x71, 0x4c, 0xea, 0x60, 0x68, 0x84, 0x07, 0xbf, 0x55, 0x4b, 0xd1, 0xc2, 0x7e, 0xc2}, true }, + + // House of the Dead: Scarlet Dawn (CHN) + { "SDEV", {0x3c, 0x1f, 0x01, 0x8d, 0x88, 0x92, 0x6d, 0x98, 0x16, 0x3b, 0x07, 0xa1, 0x56, 0x3a, 0x48, 0x18}, {0xca, 0x73, 0x73, 0xc9, 0xc7, 0xdf, 0xeb, 0xac, 0x0f, 0xc2, 0x42, 0x54, 0xc0, 0x30, 0xe4, 0xad}, true }, + + // maimai DX + { "SDEZ", {0xd1, 0x36, 0xeb, 0xa0, 0x5d, 0x40, 0xe8, 0x26, 0x82, 0xe6, 0xaa, 0xd8, 0xd9, 0xe8, 0x68, 0x8c}, {0xc4, 0x84, 0xde, 0xea, 0xa0, 0x24, 0x9e, 0xf4, 0x66, 0x95, 0xf6, 0x36, 0x94, 0xb7, 0x37, 0x2f}, true }, + + // KEY CHIP ALLS X REC + { "SDFA", {0x8e, 0x81, 0x6b, 0x43, 0x62, 0xdb, 0x24, 0xa2, 0x30, 0x87, 0x78, 0x85, 0x86, 0x4d, 0x20, 0x6d}, {0x8e, 0x5a, 0x0b, 0xa6, 0xa0, 0xa1, 0x15, 0x0d, 0x47, 0xd1, 0x2b, 0xdb, 0x64, 0xde, 0xbb, 0xa7}, true }, + + // WACCA + { "SDFE", {0xf6, 0x17, 0x19, 0xc3, 0x71, 0xe5, 0xbc, 0xa6, 0x78, 0x8c, 0x13, 0x9a, 0x53, 0x09, 0x16, 0x17}, {0x67, 0xd4, 0x31, 0x73, 0xe3, 0x43, 0x81, 0x3f, 0xa2, 0x09, 0x7f, 0xd3, 0x29, 0x92, 0xa8, 0xe2}, true }, + + // SANDRA + { "SDFG", {0x33, 0x98, 0xfb, 0x86, 0xbf, 0xe6, 0x30, 0xa1, 0x49, 0x79, 0x41, 0x18, 0x79, 0x86, 0x1a, 0xc7}, {0xa7, 0x94, 0xc4, 0x9c, 0x2c, 0x76, 0x39, 0xcd, 0x80, 0x57, 0x18, 0x07, 0xc1, 0x72, 0x46, 0xff}, true }, + + // Kemono Friends 3: Planet Tours + { "SDFL", {0x24, 0x49, 0xb4, 0x80, 0x67, 0xb9, 0x17, 0x6a, 0x6e, 0x0f, 0x95, 0x63, 0x48, 0x1e, 0x97, 0xf4}, {0x61, 0x6f, 0x87, 0x10, 0x45, 0x46, 0x32, 0xeb, 0x4f, 0xb1, 0xd8, 0x9d, 0x8c, 0x19, 0xc1, 0x9a}, true }, + + // KEY CHIP ALLS X CASJ + { "SDFN", {0x29, 0xf6, 0x2e, 0x22, 0xc6, 0xa9, 0xfd, 0x8b, 0xe3, 0x27, 0x63, 0x1c, 0x68, 0x54, 0x64, 0x05}, {0x2a, 0x86, 0x09, 0x76, 0xe6, 0xd9, 0x85, 0x13, 0x82, 0x5f, 0x29, 0x1e, 0x56, 0xcf, 0xb5, 0xee}, true }, + + // KEY CHIP NUSX FUTURE + { "SDFP", {0x57, 0x0b, 0x87, 0x26, 0x3a, 0x7c, 0xa0, 0xaa, 0x4c, 0x13, 0x88, 0xe2, 0x04, 0xee, 0x6d, 0x4b}, {0x76, 0x40, 0x88, 0x60, 0x11, 0xa2, 0x30, 0x0a, 0x91, 0xfa, 0xd9, 0xf3, 0x6a, 0x8c, 0x47, 0x75}, true }, + + // StarHorse4 + { "SDFT", {0x92, 0xa2, 0x5f, 0x38, 0x8c, 0x50, 0x73, 0x7e, 0x39, 0xc3, 0xc2, 0xf0, 0x06, 0x64, 0x5f, 0x31}, {0xa9, 0x7e, 0x72, 0xf9, 0x90, 0x41, 0x74, 0x88, 0xcb, 0x4c, 0x67, 0xf8, 0xf0, 0xc3, 0xfb, 0x25}, true }, + + // Mario & Sonic at TOKYO Olympic + { "SDFV", {0xfe, 0x82, 0xdb, 0x9a, 0x60, 0x29, 0x5d, 0x82, 0x9b, 0x95, 0xf0, 0x3c, 0x22, 0x76, 0x01, 0x8b}, {0x34, 0xd8, 0x27, 0x72, 0xae, 0x18, 0x17, 0x4f, 0x0a, 0x18, 0x1d, 0xc5, 0x33, 0x99, 0xea, 0x9c}, true }, + + // maimai DX (EXP) + { "SDGA", {0x0a, 0x66, 0x10, 0xa6, 0x2e, 0xf6, 0x70, 0xc6, 0x5b, 0x7e, 0x7b, 0x17, 0x50, 0xff, 0xb7, 0xa1}, {0x17, 0xa2, 0xa2, 0x29, 0x15, 0xf8, 0x1c, 0x58, 0x96, 0xed, 0xbb, 0xa4, 0xc4, 0x12, 0x58, 0x5e}, true }, + + // maimai DX (CHN) + { "SDGB", {0x7c, 0xa4, 0xe6, 0xb6, 0xf3, 0xd6, 0xe8, 0xb2, 0x64, 0x72, 0x97, 0x38, 0x87, 0xd7, 0xfa, 0x3a}, {0x53, 0xfe, 0x71, 0x35, 0x76, 0x2d, 0xe3, 0xf9, 0x7e, 0x7f, 0xe7, 0x6b, 0x0f, 0xef, 0x3f, 0x27}, true }, + + // Puyo Puyo e-Sports Arcade + { "SDGH", {0xb3, 0xe3, 0x0e, 0x7e, 0xab, 0xac, 0x37, 0x67, 0xad, 0xe1, 0x3c, 0x69, 0xc9, 0xb2, 0xf2, 0x2b}, {0x03, 0xde, 0xae, 0xa3, 0x74, 0x2d, 0x69, 0x67, 0x5b, 0x36, 0xcd, 0xdc, 0x8b, 0x15, 0xac, 0x91}, true }, + + // WCCF FOOTISTA (EXP) + { "SDGK", {0x9d, 0xc4, 0xa1, 0x7f, 0xc3, 0x9f, 0xca, 0x5a, 0x8a, 0x35, 0x89, 0x84, 0x80, 0x1c, 0xaa, 0xa7}, {0xe0, 0x44, 0x5b, 0x11, 0xdc, 0xfa, 0x0d, 0xae, 0x56, 0xc8, 0x5e, 0x87, 0x87, 0xe1, 0x1d, 0x9b}, true }, + + // KEY CHIP ALLS X HDZ CASJ + { "SDGP", {0xc8, 0x7a, 0xb3, 0x12, 0x47, 0xe7, 0xb6, 0xff, 0x95, 0xfd, 0xd7, 0x9f, 0xb9, 0x1f, 0x9f, 0x37}, {0x24, 0x67, 0xab, 0x3c, 0x03, 0x1e, 0x3d, 0xc0, 0x56, 0x8b, 0x70, 0x77, 0xef, 0xd2, 0x7c, 0x36}, true }, + + // ROKUMEN + { "SDGQ", {0xc5, 0x35, 0x6d, 0xae, 0x7b, 0x06, 0x6b, 0xce, 0x88, 0x98, 0x4a, 0xec, 0x36, 0xde, 0xb6, 0x2d}, {0x4e, 0x9f, 0x29, 0x82, 0x46, 0x0e, 0x2f, 0xd9, 0x07, 0xbd, 0xe1, 0x57, 0x09, 0xed, 0xfb, 0xa7}, true }, + + // CHUNITHM (EXP) + { "SDGS", {0xa5, 0x15, 0x0c, 0xc5, 0x06, 0x5d, 0x2c, 0x59, 0xee, 0x2f, 0x8f, 0x33, 0x2c, 0xbd, 0x29, 0xd5}, {0x84, 0x01, 0x4d, 0x26, 0x69, 0x6f, 0x29, 0x0a, 0xd7, 0xea, 0xd7, 0x0c, 0x75, 0x49, 0xbd, 0x81}, true }, + + // Initial D THE ARCADE + { "SDGT", {0x9d, 0x0b, 0xba, 0x20, 0xd1, 0xe8, 0x4f, 0x24, 0x59, 0x39, 0x9f, 0x53, 0x83, 0xbe, 0xee, 0x72}, {0x5d, 0x34, 0x00, 0x13, 0xfd, 0xfb, 0x24, 0x64, 0xd2, 0x53, 0x09, 0x36, 0x02, 0xfe, 0x4b, 0x64}, true }, + + // Pokemon COROGARENA + { "SDGV", {0x57, 0x3f, 0x5c, 0x8c, 0xc4, 0x4f, 0x10, 0xf3, 0x1e, 0xc7, 0x49, 0xb6, 0x95, 0xeb, 0xe8, 0x86}, {0x6b, 0xfc, 0xa8, 0x6f, 0x9d, 0x20, 0x8a, 0x79, 0x44, 0xcd, 0xfc, 0x25, 0xea, 0x3c, 0xd2, 0x20}, true }, + + // Eiketsu Taisen: Sanzensekai no Hadou + { "SDGY", {0xc0, 0x4b, 0x66, 0x3a, 0x59, 0x05, 0x5a, 0xcb, 0xdf, 0xeb, 0xc6, 0xd3, 0xdf, 0x0e, 0x6a, 0x04}, {0x76, 0xfc, 0x5f, 0x1d, 0x88, 0x60, 0x51, 0x07, 0x94, 0x7d, 0x0c, 0x1f, 0xf3, 0x47, 0x02, 0x2d}, true }, + + // Hori a Tale + { "SDGZ", {0x9a, 0xd7, 0x4e, 0xfb, 0x20, 0x8d, 0x6e, 0xe4, 0xfe, 0x5e, 0xe7, 0x70, 0x33, 0x17, 0x12, 0xcf}, {0x45, 0xf6, 0xe5, 0x3f, 0x0e, 0xb8, 0xfa, 0xe6, 0x66, 0x5b, 0x45, 0x44, 0x4a, 0x61, 0xe2, 0x66}, true }, + + // CHUNITHM NEW!! + { "SDHD", {0x3a, 0xbd, 0x00, 0xd7, 0xa8, 0x20, 0xce, 0x86, 0x2e, 0xaf, 0x47, 0x4b, 0xf6, 0xc8, 0xf3, 0x3e}, {0x0f, 0x1e, 0x7e, 0xea, 0x78, 0xda, 0x7e, 0x03, 0x7e, 0x05, 0x52, 0xc2, 0x84, 0x3e, 0x1b, 0x6a}, true }, + + // Gutsdzuri GO! + { "SDHH", {0xfc, 0x6f, 0x88, 0x7f, 0x37, 0x17, 0xc5, 0xd6, 0x71, 0x31, 0x13, 0xb9, 0x2f, 0xa3, 0xfb, 0x27}, {0xad, 0x76, 0x60, 0x64, 0x60, 0xdb, 0xe1, 0xe9, 0x1e, 0x41, 0xbe, 0xf7, 0xab, 0x0c, 0x15, 0x35}, true }, + + // CHUNITHM (CHN) + { "SDHJ", {0x98, 0x5e, 0xa6, 0x6e, 0xcb, 0x5b, 0x1f, 0x20, 0x8c, 0x90, 0xe2, 0xb8, 0x98, 0xf0, 0xb0, 0x73}, {0x16, 0x4a, 0x65, 0x42, 0x2e, 0x7f, 0x01, 0xb7, 0xf1, 0xb0, 0x84, 0x9f, 0xc7, 0x73, 0x7c, 0xdb}, true }, + + // UFO CATCHER LINK STATION + { "SDHK", {0xbc, 0x92, 0xd6, 0x3c, 0x2a, 0x09, 0x9c, 0xa2, 0x31, 0x5a, 0x48, 0x3c, 0x30, 0x41, 0xfd, 0xd7}, {0xb1, 0x4d, 0x84, 0x49, 0xb6, 0xd4, 0x32, 0x5d, 0x83, 0xa2, 0x77, 0x4b, 0x13, 0xdd, 0x21, 0xff}, true }, + + // WACCA (CHN) + { "SDHN", {0x89, 0x21, 0x23, 0xa2, 0x6d, 0x7c, 0x03, 0xd4, 0x9e, 0xdd, 0x12, 0xa8, 0x0e, 0xe0, 0xc5, 0x8f}, {0x76, 0xaa, 0x15, 0xa6, 0x86, 0x8b, 0x8d, 0xbd, 0xf7, 0x20, 0x79, 0x06, 0x35, 0x4d, 0x51, 0x69}, true }, + + // meityromantic + { "SDHR", {0x1f, 0xb8, 0x97, 0xca, 0xb9, 0x7c, 0x81, 0x70, 0xa6, 0xac, 0x0a, 0x21, 0x68, 0x5c, 0x58, 0xd9}, {0xf9, 0xb6, 0x0f, 0x65, 0xb0, 0x1e, 0x8e, 0x83, 0x6a, 0x4b, 0xc2, 0x0f, 0x7d, 0x39, 0xfa, 0xf5}, true }, + + // ALLS System + { "ACA", {0xe4, 0x28, 0x1b, 0xcf, 0x48, 0xc4, 0xd2, 0x8e, 0xb0, 0x57, 0x72, 0xce, 0x6f, 0x98, 0x58, 0x7a}, {0x6c, 0xee, 0x7f, 0x5a, 0x2c, 0x4b, 0x5f, 0x1e, 0x93, 0xc5, 0x94, 0x91, 0x14, 0xff, 0x0b, 0x74}, true }, + + // read from {game_id}.bin for unknown keys + { NULL, {0}, {0}, false } +}; + +const size_t game_keys_count = sizeof(game_keys) / sizeof(game_keys[0]); \ No newline at end of file diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..2c6eeaf --- /dev/null +++ b/src/main.c @@ -0,0 +1,500 @@ +#include +#include +#include +#include +#include +#include +#include "bootid.h" +#include "crypto.h" +#include "exfat.h" +#include "ntfs.h" +#include +#include + +#define PAGE_SIZE 4096 +#define BUFFER_SIZE (PAGE_SIZE * 256) +#define MAX_PATH_LENGTH 256 + +char* g_output_filename = NULL; + +int process_file(const char* path, bool extract_fs) { + uint8_t* bootid_bytes = malloc(96); + uint8_t* read_buffer = malloc(BUFFER_SIZE); + uint8_t* decrypted_buffer = malloc(BUFFER_SIZE); + uint8_t iv[16]; + uint8_t page_iv[16]; + uint8_t key[16]; + + if (!bootid_bytes || !read_buffer || !decrypted_buffer) { + printf("Memory allocation failed\n"); + free(bootid_bytes); + free(read_buffer); + free(decrypted_buffer); + return 1; + } + + FILE* file = fopen(path, "rb"); + if (!file) { + perror(path); + free(bootid_bytes); + free(read_buffer); + free(decrypted_buffer); + return 1; + } + + if (fread(bootid_bytes, 1, 96, file) != 96) { + printf("Could not read BootId from %s\n", path); + fclose(file); + free(bootid_bytes); + free(read_buffer); + free(decrypted_buffer); + return 1; + } + + uint8_t decrypted_bootid_bytes[96]; + int out_len = 0; + + EVP_CIPHER_CTX* bootid_ctx = EVP_CIPHER_CTX_new(); + if (!bootid_ctx) { + printf("Could not create cipher context\n"); + fclose(file); + free(bootid_bytes); + free(read_buffer); + free(decrypted_buffer); + return 1; + } + + EVP_DecryptInit_ex(bootid_ctx, EVP_aes_128_cbc(), NULL, BOOTID_KEY, BOOTID_IV); + EVP_CIPHER_CTX_set_padding(bootid_ctx, 0); + + if (!EVP_DecryptUpdate(bootid_ctx, decrypted_bootid_bytes, &out_len, bootid_bytes, 96)) { + printf("Could not decrypt BootId in %s\n", path); + EVP_CIPHER_CTX_free(bootid_ctx); + fclose(file); + free(bootid_bytes); + free(read_buffer); + free(decrypted_buffer); + return 1; + } + + int final_len = 0; + if (!EVP_DecryptFinal_ex(bootid_ctx, decrypted_bootid_bytes + out_len, &final_len)) { + printf("Could not finalize decryption\n"); + EVP_CIPHER_CTX_free(bootid_ctx); + fclose(file); + free(bootid_bytes); + free(read_buffer); + free(decrypted_buffer); + return 1; + } + EVP_CIPHER_CTX_free(bootid_ctx); + + BootId bootid; + memcpy(&bootid, decrypted_bootid_bytes, sizeof(BootId)); + + if (bootid.container_type != CONTAINER_TYPE_OS && + bootid.container_type != CONTAINER_TYPE_APP && + bootid.container_type != CONTAINER_TYPE_OPTION) { + printf("Unknown container type %d\n", bootid.container_type); + fclose(file); + free(bootid_bytes); + free(read_buffer); + free(decrypted_buffer); + return 1; + } + + char target_timestamp_str[20]; + format_timestamp(&bootid.target_timestamp, target_timestamp_str, sizeof(target_timestamp_str)); + + char os_id[4]; + char game_id[5]; + memcpy(os_id, bootid.os_id, 3); + os_id[3] = '\0'; + memcpy(game_id, bootid.game_id, 4); + game_id[4] = '\0'; + + const char* id = (bootid.container_type == CONTAINER_TYPE_OS) ? os_id : game_id; + + GameKeys keys; + bool got_keys = false; + if (bootid.container_type == CONTAINER_TYPE_OS || bootid.container_type == CONTAINER_TYPE_APP) { + got_keys = get_game_keys(id, &keys); + } + else { + memcpy(keys.key, OPTION_KEY, 16); + memcpy(keys.iv, OPTION_IV, 16); + keys.has_iv = true; + got_keys = true; + } + + if (!got_keys) { + printf("Decryption key invalid or not found.\n"); + fclose(file); + free(bootid_bytes); + free(read_buffer); + free(decrypted_buffer); + return 1; + } + + uint64_t data_offset = bootid.header_block_count * bootid.block_size; + + bool has_iv = false; + memcpy(key, keys.key, 16); + + if (bootid.use_custom_iv) { + has_iv = false; + } + else { + has_iv = keys.has_iv; + if (has_iv) { + memcpy(iv, keys.iv, 16); + } + } + + if (!has_iv) { + if (fseek(file, data_offset, SEEK_SET) != 0) { + perror("fseek"); + fclose(file); + free(bootid_bytes); + free(read_buffer); + free(decrypted_buffer); + return 1; + } + + size_t read_size = fread(read_buffer, 1, PAGE_SIZE, file); + if (read_size != PAGE_SIZE) { + perror("fread"); + fclose(file); + free(bootid_bytes); + free(read_buffer); + free(decrypted_buffer); + return 1; + } + + if (bootid.container_type == CONTAINER_TYPE_OPTION) { + if (!calculate_file_iv(key, EXFAT_HEADER, read_buffer, iv)) { + printf("Could not calculate file IV\n"); + fclose(file); + free(bootid_bytes); + free(read_buffer); + free(decrypted_buffer); + return 1; + } + } + else { + if (!calculate_file_iv(key, NTFS_HEADER, read_buffer, iv)) { + printf("Could not calculate file IV\n"); + fclose(file); + free(bootid_bytes); + free(read_buffer); + free(decrypted_buffer); + return 1; + } + } + has_iv = true; + } + + char* output_filename = malloc(MAX_PATH_LENGTH); + if (!output_filename) { + printf("Memory allocation failed\n"); + fclose(file); + free(bootid_bytes); + free(read_buffer); + free(decrypted_buffer); + return 1; + } + + if (bootid.container_type == CONTAINER_TYPE_OS) { + snprintf(output_filename, MAX_PATH_LENGTH, "%s_%04d%02d%02d_%s_%d.ntfs", + os_id, + bootid.os_version.major, + bootid.os_version.minor, + bootid.os_version.release, + target_timestamp_str, + bootid.sequence_number); + } + else if (bootid.container_type == CONTAINER_TYPE_APP) { + if (bootid.sequence_number > 0) { + snprintf(output_filename, MAX_PATH_LENGTH, "%s_%d%02d%02d_%s_%d_%d%02d%02d.ntfs", + game_id, + bootid.target_version.version.major, + bootid.target_version.version.minor, + bootid.target_version.version.release, + target_timestamp_str, + bootid.sequence_number, + bootid.source_version.major, + bootid.source_version.minor, + bootid.source_version.release); + } + else { + snprintf(output_filename, MAX_PATH_LENGTH, "%s_%d%02d%02d_%s_%d.ntfs", + game_id, + bootid.target_version.version.major, + bootid.target_version.version.minor, + bootid.target_version.version.release, + target_timestamp_str, + bootid.sequence_number); + } + } + else if (bootid.container_type == CONTAINER_TYPE_OPTION) { + char option_str[5]; + memcpy(option_str, bootid.target_version.option, 4); + option_str[4] = '\0'; + snprintf(output_filename, MAX_PATH_LENGTH, "%s_%s_%s_%d.exfat", + game_id, + option_str, + target_timestamp_str, + bootid.sequence_number); + } + + FILE* output_file = fopen(output_filename, "wb"); + if (!output_file) { + perror(output_filename); + free(output_filename); + fclose(file); + free(bootid_bytes); + free(read_buffer); + free(decrypted_buffer); + return 1; + } + + uint64_t output_size = (bootid.block_count - bootid.header_block_count) * bootid.block_size; + + if (fseek(file, data_offset, SEEK_SET) != 0) { + perror("fseek"); + fclose(file); + fclose(output_file); + free(bootid_bytes); + free(read_buffer); + free(decrypted_buffer); + free(output_filename); + return 1; + } + + EVP_CIPHER_CTX* page_ctx = EVP_CIPHER_CTX_new(); + if (!page_ctx) { + printf("Could not create cipher context\n"); + fclose(file); + fclose(output_file); + free(bootid_bytes); + free(read_buffer); + free(decrypted_buffer); + free(output_filename); + return 1; + } + EVP_DecryptInit_ex(page_ctx, EVP_aes_128_cbc(), NULL, key, NULL); + EVP_CIPHER_CTX_set_padding(page_ctx, 0); + + printf("\nDecrypting file...\n"); + time_t last_update_time = time(NULL); + int last_percentage = -1; + + uint64_t total_bytes_read = 0; + uint64_t bytes_remaining = output_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) { + if (feof(file)) { + printf("\nUnexpected end of file\n"); + } else { + perror("\nread_chunk"); + } + break; + } + + 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; + calculate_page_iv(file_offset, iv, page_iv); // Generate unique IV for each 4KB page + + EVP_DecryptInit_ex(page_ctx, NULL, NULL, NULL, page_iv); + + int out_len1 = 0, out_len2 = 0; + if (!EVP_DecryptUpdate(page_ctx, decrypted_buffer + offset, &out_len1, read_buffer + offset, block_size)) { + printf("\nCould not decrypt data\n"); + break; + } + if (!EVP_DecryptFinal_ex(page_ctx, decrypted_buffer + offset + out_len1, &out_len2)) { + printf("\nCould not finalize decryption\n"); + break; + } + if (out_len1 + out_len2 != (int)block_size) { + printf("\nDecrypted data size mismatch\n"); + break; + } + offset += block_size; + } + + if (fwrite(decrypted_buffer, 1, read_size, output_file) != read_size) { + perror("\nfwrite"); + break; + } + + total_bytes_read += read_size; + bytes_remaining -= read_size; + + time_t current_time = time(NULL); + if (current_time != last_update_time) { + int percentage = (int)((total_bytes_read * 100) / output_size); + if (percentage != last_percentage) { + printf("\rProgress: %d%% ", percentage); // Extra spaces to clear line + fflush(stdout); + last_percentage = percentage; + } + last_update_time = current_time; + } + } + + printf("\rProgress: 100%% \n"); + + EVP_CIPHER_CTX_free(page_ctx); + + fclose(file); + fclose(output_file); + + printf("Decryption finalized: %s\n", output_filename); + + if (extract_fs) { + if (g_output_filename) { + free(g_output_filename); + } + g_output_filename = output_filename; + } + else { + free(output_filename); + } + + free(bootid_bytes); + free(read_buffer); + free(decrypted_buffer); + + return 0; +} + +int main(int argc, char* argv[]) { + bool extract_fs = true; + int start_index = 1; + + if (argc < 2) { + printf("usage: unsegaREBORN [-no] [ ...]\n"); + printf(" -no Do not extract filesystem archives after decryption\n"); + return 0; + } + + if (strcmp(argv[1], "-no") == 0) { + extract_fs = false; + start_index = 2; + + if (argc < 3) { + printf("No input files specified\n"); + return 1; + } + } + + for (int i = start_index; i < argc; ++i) { + const char* file_path = argv[i]; + printf("Processing file: %s\n", file_path); + + if (process_file(file_path, extract_fs) == 0) { + if (extract_fs && g_output_filename) { + char output_dir[MAX_PATH_LENGTH]; + strncpy(output_dir, g_output_filename, sizeof(output_dir) - 1); + output_dir[sizeof(output_dir) - 1] = '\0'; + + char* ext = strrchr(output_dir, '.'); + if (ext) *ext = '\0'; + + if (strstr(g_output_filename, ".exfat") != NULL) { + ExfatContext ctx; + if (exfat_init(&ctx, g_output_filename)) { + if (exfat_extract_all(&ctx, output_dir)) { + printf("\nExFAT extraction completed successfully\n"); + } + else { + printf("\nFailed to extract ExFAT archive\n"); + } + exfat_close(&ctx); + } + else { + printf("\nFailed to initialize ExFAT context\n"); + } + } + else if (strstr(g_output_filename, ".ntfs") != NULL) { + NTFSContext ctx = { 0 }; + if (ntfs_init(&ctx, g_output_filename, output_dir)) { + printf("\nExtracting NTFS archive...\n"); + + if (ntfs_extract_all(&ctx)) { + printf("\nNTFS extraction completed successfully\n"); + + char vhd_path[MAX_PATH_LENGTH]; + bool found_child = false; + + 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); + + if (vhd_num > 0) { + printf("\nChild internal VHD identified, finalizing process.\n"); + found_child = true; + break; + } + + 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 }; + if (ntfs_init(&vhd_ctx, vhd_path, vhd_output_dir)) { + printf("\nExtracting from internal VHD...\n"); + if (ntfs_extract_all(&vhd_ctx)) { + printf("\nInternal VHD extraction completed successfully\n"); + } + else { + printf("\nFailed to extract VHD contents\n"); + } + ntfs_close(&vhd_ctx); + } + else { + printf("\nFailed to open internal VHD\n"); + } + break; + } + } + else { + printf("\nFailed to extract NTFS archive\n"); + } + ntfs_close(&ctx); + } + else { + printf("\nFailed to initialize NTFS context\n"); + } + } + else { + printf("\nUnknown filesystem type for file %s\n", g_output_filename); + } + + free(g_output_filename); + g_output_filename = NULL; + } + } + else { + printf("Failed to process %s\n", file_path); + } + } + + if (g_output_filename) { + free(g_output_filename); + } + + return 0; +} \ No newline at end of file diff --git a/src/ntfs.c b/src/ntfs.c new file mode 100644 index 0000000..7e6d0b2 --- /dev/null +++ b/src/ntfs.c @@ -0,0 +1,884 @@ +#include "ntfs.h" +#include +#include +#include +#define BUFFER_SIZE 65536 + +static bool vhd_read(VHDContext* ctx, void* buffer, uint64_t offset, size_t size); + +static uint16_t swap16(uint16_t value) { + return ((value & 0xFF00) >> 8) | ((value & 0x00FF) << 8); +} + +static uint32_t swap32(uint32_t value) { + return ((value & 0xFF000000) >> 24) | + ((value & 0x00FF0000) >> 8) | + ((value & 0x0000FF00) << 8) | + ((value & 0x000000FF) << 24); +} + +static uint64_t swap64(uint64_t value) { + return ((value & 0xFF00000000000000ULL) >> 56) | + ((value & 0x00FF000000000000ULL) >> 40) | + ((value & 0x0000FF0000000000ULL) >> 24) | + ((value & 0x000000FF00000000ULL) >> 8) | + ((value & 0x00000000FF000000ULL) << 8) | + ((value & 0x0000000000FF0000ULL) << 24) | + ((value & 0x000000000000FF00ULL) << 40) | + ((value & 0x00000000000000FFULL) << 56); +} + +static bool is_safe_path(const char* path) { + if (!path) return false; + + if (strstr(path, "..") != NULL) return false; + if (strstr(path, "./") != NULL) return false; + if (strstr(path, ".\\") != NULL) return false; + + #ifndef _WIN32 + if (path[0] == '/') return false; + #endif + + return true; +} + +static bool create_directories(const char* path) { + if (!is_safe_path(path)) { + return false; + } + char* tmp = STRDUP(path); + if (!tmp) return false; + + bool success = true; + char* p = tmp; + + if (strlen(p) > 2) { + if (p[1] == ':') p += 2; + if (*p == '\\' || *p == '/') p++; + } + + while ((p = strchr(p, PATH_SEPARATOR[0])) != NULL) { + *p = '\0'; + if (strlen(tmp) > 0) { + if (MKDIR(tmp) != 0 && errno != EEXIST) { + success = false; + break; + } + } + *p = PATH_SEPARATOR[0]; + p++; + } + + if (success && strlen(tmp) > 0) { + if (MKDIR(tmp) != 0 && errno != EEXIST) { + success = false; + } + } + + free(tmp); + return success; +} + +static void convert_name_to_ascii(const uint16_t* utf16_name, int name_length, char* ascii_name, size_t ascii_buffer_size) { + if (!ascii_name || ascii_buffer_size == 0) return; + + int real_name_length = min(name_length, (int)(ascii_buffer_size - 1)); + real_name_length = min(real_name_length, MAX_FILENAME_LENGTH - 1); + +#ifdef _WIN32 + _locale_t locale = _create_locale(LC_ALL, ""); + size_t converted = _wcstombs_l(ascii_name, (const wchar_t*)utf16_name, real_name_length, locale); + _free_locale(locale); + if (converted == (size_t)-1) { + ascii_name[0] = '\0'; + return; + } +#else + wchar_t temp[MAX_FILENAME_LENGTH]; + for (int i = 0; i < real_name_length && i < MAX_FILENAME_LENGTH - 1; i++) { + temp[i] = (wchar_t)utf16_name[i]; + } + temp[real_name_length] = L'\0'; + size_t converted = wcstombs(ascii_name, temp, ascii_buffer_size - 1); + if (converted == (size_t)-1) { + ascii_name[0] = '\0'; + return; + } +#endif + ascii_name[min(converted, ascii_buffer_size - 1)] = '\0'; +} + +static bool ntfs_read(NTFSContext* ctx, void* buffer, uint64_t offset, size_t size) { + if (ctx->is_vhd) { + return vhd_read(&ctx->vhd, buffer, offset, size); + } + if (FSEEKO(ctx->raw.fp, offset, SEEK_SET) != 0) { + return false; + } + return fread(buffer, 1, size, ctx->raw.fp) == size; +} + +static bool read_file_info(NTFSContext* ctx, uint64_t ref_number, FileInfo* info) { + memset(info, 0, sizeof(FileInfo)); + + uint64_t mft_offset = ctx->mft_offset + (ref_number * ctx->mft_record_size); + uint8_t* record_buffer = malloc(ctx->mft_record_size); + if (!record_buffer) { + return false; + } + + bool success = false; + if (ntfs_read(ctx, record_buffer, mft_offset, ctx->mft_record_size)) { + const MFTRecordHeader* record = (const MFTRecordHeader*)record_buffer; + + if (memcmp(record->magic, "FILE", 4) == 0 && (record->flags & MFT_RECORD_IN_USE)) { + info->is_directory = (record->flags & MFT_RECORD_IS_DIRECTORY) != 0; + + const uint8_t* attr = (const uint8_t*)record + record->attrs_offset; + while (attr < (const uint8_t*)record + record->bytes_used) { + const AttributeHeader* header = (const AttributeHeader*)attr; + + if (header->type == 0xFFFFFFFF || header->length == 0) { + break; + } + + if (header->type == FILE_NAME_ATTR && !header->non_resident) { + const FileNameAttribute* fname = + (const FileNameAttribute*)(attr + header->data.resident.value_offset); + + if (fname->namespace != 2) { + convert_name_to_ascii(fname->name, fname->name_length, info->name, sizeof(info->name)); + info->parent_ref = fname->parent_directory & 0xFFFFFFFFFFFF; + info->valid = true; + success = true; + break; + } + } + + attr += header->length; + } + } + } + + free(record_buffer); + return success; +} + +static bool init_directory_cache(DirectoryCache* cache) { + cache->capacity = 1024; + cache->count = 0; + cache->directories = malloc(cache->capacity * sizeof(DirectoryInfo)); + if (!cache->directories) return false; + + cache->directories[0].ref_number = 5; + cache->directories[0].path[0] = '\0'; + cache->count = 1; + return true; +} + +static void free_directory_cache(DirectoryCache* cache) { + free(cache->directories); + cache->directories = NULL; + cache->capacity = 0; + cache->count = 0; +} + +static bool add_directory_to_cache(DirectoryCache* cache, uint64_t ref_number, const char* path) { + for (size_t i = 0; i < cache->count; i++) { + if (cache->directories[i].ref_number == ref_number) { + strncpy(cache->directories[i].path, path, MAX_PATH_LENGTH - 1); + cache->directories[i].path[MAX_PATH_LENGTH - 1] = '\0'; + return true; + } + } + + if (cache->count >= cache->capacity) { + size_t new_capacity = cache->capacity * 2; + DirectoryInfo* new_dirs = realloc(cache->directories, + new_capacity * sizeof(DirectoryInfo)); + if (!new_dirs) return false; + cache->directories = new_dirs; + cache->capacity = new_capacity; + } + + if (cache->count < cache->capacity) { + cache->directories[cache->count].ref_number = ref_number; + strncpy(cache->directories[cache->count].path, path, MAX_PATH_LENGTH - 1); + cache->directories[cache->count].path[MAX_PATH_LENGTH - 1] = '\0'; + cache->count++; + return true; + } + + return false; +} + +static const char* get_cached_path(DirectoryCache* cache, uint64_t ref_number) { + for (size_t i = 0; i < cache->count; i++) { + if (cache->directories[i].ref_number == ref_number) { + return cache->directories[i].path; + } + } + return NULL; +} + +static bool build_path_recursively(NTFSContext* ctx, uint64_t ref_number, char* buffer, size_t buffer_size) { + if (ref_number == 5) { + buffer[0] = '\0'; + return true; + } + + const char* cached_path = get_cached_path(&ctx->dir_cache, ref_number); + if (cached_path) { + strncpy(buffer, cached_path, buffer_size - 1); + buffer[buffer_size - 1] = '\0'; + return true; + } + + FileInfo info; + if (!read_file_info(ctx, ref_number, &info) || !info.valid) { + return false; + } + + if (info.name[0] == '$') { + return false; + } + + char parent_path[MAX_PATH_LENGTH]; + if (!build_path_recursively(ctx, info.parent_ref, parent_path, sizeof(parent_path))) { + return false; + } + + if (parent_path[0] == '\0') { + strncpy(buffer, info.name, buffer_size - 1); + } + else { + snprintf(buffer, buffer_size, "%s%s%s", parent_path, PATH_SEPARATOR, info.name); + } + buffer[buffer_size - 1] = '\0'; + + if (info.is_directory) { + add_directory_to_cache(&ctx->dir_cache, ref_number, buffer); + } + + return true; +} + +static void get_full_path(NTFSContext* ctx, uint64_t parent_ref, const char* name, + char* out_path, size_t out_size) { + char parent_path[MAX_PATH_LENGTH]; + + if (!build_path_recursively(ctx, parent_ref, parent_path, sizeof(parent_path))) { + snprintf(out_path, out_size, "%s%s%s", + ctx->base_path, + PATH_SEPARATOR, + name); + return; + } + + if (parent_path[0] == '\0') { + snprintf(out_path, out_size, "%s%s%s", + ctx->base_path, + PATH_SEPARATOR, + name); + } + else { + snprintf(out_path, out_size, "%s%s%s%s%s", + ctx->base_path, + PATH_SEPARATOR, + parent_path, + PATH_SEPARATOR, + name); + } +} + +static bool extract_data_from_runs(NTFSContext* ctx, const DataRun* runs, int run_count, + uint64_t data_size, FILE* out_file) { + uint8_t* temp_buffer = malloc(BUFFER_SIZE); + if (!temp_buffer) return false; + + uint64_t total_written = 0; + bool success = true; + + for (int i = 0; i < run_count && total_written < data_size; i++) { + uint64_t cluster_offset = ctx->data_start_offset + + (runs[i].offset * ctx->bytes_per_cluster); + uint64_t length = runs[i].length * ctx->bytes_per_cluster; + + if (length > data_size - total_written) { + length = data_size - total_written; + } + + uint64_t remaining = length; + while (remaining > 0 && success) { + size_t to_read = (remaining > BUFFER_SIZE) ? BUFFER_SIZE : (size_t)remaining; + + if (!ntfs_read(ctx, temp_buffer, cluster_offset, to_read)) { + success = false; + break; + } + + if (fwrite(temp_buffer, 1, to_read, out_file) != to_read) { + success = false; + break; + } + + cluster_offset += to_read; + remaining -= to_read; + total_written += to_read; + } + } + + free(temp_buffer); + return success; +} + +static int parse_data_runs(const uint8_t* run_list, DataRun* runs, int max_runs) { + int count = 0; + uint64_t offset_base = 0; + const uint8_t* p = run_list; + + while (*p != 0 && count < max_runs) { + uint8_t header = *p++; + int length_size = header & 0xF; + int offset_size = header >> 4; + + if (length_size == 0) break; + + uint64_t length = 0; + for (int i = 0; i < length_size; i++) { + length |= ((uint64_t)*p++) << (i * 8); + } + + int64_t offset = 0; + if (offset_size > 0) { + for (int i = 0; i < offset_size; i++) { + offset |= ((uint64_t)*p++) << (i * 8); + } + if (offset & ((uint64_t)1 << ((offset_size * 8) - 1))) { + offset |= ~((uint64_t)(1ULL << (offset_size * 8)) - 1); + } + } + + offset_base += offset; + runs[count].offset = offset_base; + runs[count].length = length; + count++; + } + + return count; +} + +static bool extract_file(NTFSContext* ctx, const MFTRecordHeader* record, + const char* full_path) { + char parent_path[MAX_PATH_LENGTH]; + strncpy(parent_path, full_path, sizeof(parent_path) - 1); + parent_path[sizeof(parent_path) - 1] = '\0'; + + char* last_separator = strrchr(parent_path, PATH_SEPARATOR[0]); + if (last_separator) { + *last_separator = '\0'; + create_directories(parent_path); + } + + FILE* out_file = fopen(full_path, "wb"); + if (!out_file) { + printf("Failed to create file: %s\n", full_path); + return false; + } + + bool success = false; + const uint8_t* attr = (const uint8_t*)record + record->attrs_offset; + + while (attr < (const uint8_t*)record + record->bytes_used) { + const AttributeHeader* header = (const AttributeHeader*)attr; + + if (header->type == 0xFFFFFFFF || header->length == 0) { + break; + } + + if (header->type == DATA_ATTR && header->name_length == 0) { + if (header->non_resident) { + DataRun runs[256]; + const uint8_t* run_list = attr + header->data.non_resident.mapping_pairs_offset; + int run_count = parse_data_runs(run_list, runs, 256); + + success = extract_data_from_runs(ctx, runs, run_count, + header->data.non_resident.data_size, + out_file); + } + else { + const uint8_t* data = attr + header->data.resident.value_offset; + success = (fwrite(data, 1, header->data.resident.value_length, out_file) == + header->data.resident.value_length); + } + break; + } + + attr += header->length; + } + + fclose(out_file); + if (!success) { + remove(full_path); + } + return success; +} + +static bool process_mft_record(NTFSContext* ctx, const uint8_t* record_data) { + const MFTRecordHeader* record = (const MFTRecordHeader*)record_data; + + if (memcmp(record->magic, "FILE", 4) != 0 || !(record->flags & MFT_RECORD_IN_USE)) { + return true; + } + + char filename[MAX_FILENAME_LENGTH]; + uint64_t parent_ref = 0; + bool got_filename = false; + bool is_directory = (record->flags & MFT_RECORD_IS_DIRECTORY) != 0; + uint64_t record_num = record->record_number & 0xFFFFFFFFFFFF; + + const uint8_t* attr = (const uint8_t*)record + record->attrs_offset; + while (attr < (const uint8_t*)record + record->bytes_used) { + const AttributeHeader* header = (const AttributeHeader*)attr; + + if (header->type == 0xFFFFFFFF || header->length == 0) { + break; + } + + if (header->type == FILE_NAME_ATTR && !header->non_resident) { + const FileNameAttribute* fname = + (const FileNameAttribute*)(attr + header->data.resident.value_offset); + + if (fname->namespace != 2) { + convert_name_to_ascii(fname->name, fname->name_length, filename, sizeof(filename)); + parent_ref = fname->parent_directory & 0xFFFFFFFFFFFF; + got_filename = true; + break; + } + } + + attr += header->length; + } + + if (!got_filename || filename[0] == '$') { + return true; + } + + if (!is_safe_path(filename)) { + return true; + } + + char full_path[MAX_PATH_LENGTH]; + get_full_path(ctx, parent_ref, filename, full_path, sizeof(full_path)); + + if (is_directory) { + if (!create_directories(full_path)) { + printf("Failed to create directory: %s\n", full_path); + return false; + } + + const char* relative_path = full_path + strlen(ctx->base_path); + while (*relative_path == PATH_SEPARATOR[0]) relative_path++; + + if (!add_directory_to_cache(&ctx->dir_cache, record_num, relative_path)) { + printf("Failed to cache directory: %s\n", filename); + return false; + } + return true; + } + + return extract_file(ctx, record, full_path); +} + +static bool vhd_read(VHDContext* ctx, void* buffer, uint64_t offset, size_t size) { + if (ctx->footer.disk_type == VHD_TYPE_FIXED) { + if (FSEEKO(ctx->fp, offset, SEEK_SET) != 0) { + return false; + } + return fread(buffer, 1, size, ctx->fp) == size; + } + else if (ctx->footer.disk_type == VHD_TYPE_DYNAMIC) { + uint8_t* buf = (uint8_t*)buffer; + uint64_t block_size = ctx->dyn_header.block_size; + + while (size > 0) { + uint32_t block_idx = (uint32_t)(offset / block_size); + uint32_t block_offset = (uint32_t)(offset % block_size); + + if (block_idx >= ctx->dyn_header.max_bat_entries) { + return false; + } + + uint32_t bat_entry = ctx->bat[block_idx]; + if (bat_entry == VHD_BAT_ENTRY_RESERVED) { + size_t chunk = (size < (block_size - block_offset)) ? + size : (size_t)(block_size - block_offset); + memset(buf, 0, chunk); + buf += chunk; + offset += chunk; + size -= chunk; + } + else { + uint64_t sector_offset = ((uint64_t)bat_entry) * VHD_SECTOR_SIZE; + + if (FSEEKO(ctx->fp, sector_offset, SEEK_SET) != 0) { + return false; + } + + if (fread(ctx->sector_bitmap, 1, ctx->sector_bitmap_size, ctx->fp) + != ctx->sector_bitmap_size) { + return false; + } + + if (fread(ctx->block_buffer, 1, block_size, ctx->fp) != block_size) { + return false; + } + + size_t chunk = (size < (block_size - block_offset)) ? + size : (size_t)(block_size - block_offset); + memcpy(buf, ctx->block_buffer + block_offset, chunk); + buf += chunk; + offset += chunk; + size -= chunk; + } + } + return true; + } + return false; +} + +static bool vhd_init(VHDContext* ctx, const char* filename) { + memset(ctx, 0, sizeof(VHDContext)); + + ctx->fp = fopen(filename, "rb"); + if (!ctx->fp) { + printf("Failed to open file: %s\n", filename); + return false; + } + + if (FSEEKO(ctx->fp, -((int64_t)VHD_FOOTER_SIZE), SEEK_END) != 0) { + printf("Failed to seek to VHD footer\n"); + fclose(ctx->fp); + return false; + } + + if (fread(&ctx->footer, 1, sizeof(VHDFooter), ctx->fp) != sizeof(VHDFooter)) { + printf("Failed to read VHD footer\n"); + fclose(ctx->fp); + return false; + } + + if (memcmp(ctx->footer.cookie, VHD_COOKIE, strlen(VHD_COOKIE)) != 0) { + printf("Invalid VHD signature\n"); + fclose(ctx->fp); + return false; + } + + ctx->footer.features = swap32(ctx->footer.features); + ctx->footer.version = swap32(ctx->footer.version); + ctx->footer.data_offset = swap64(ctx->footer.data_offset); + ctx->footer.timestamp = swap32(ctx->footer.timestamp); + ctx->footer.creator_app = swap32(ctx->footer.creator_app); + ctx->footer.creator_ver = swap32(ctx->footer.creator_ver); + ctx->footer.creator_os = swap32(ctx->footer.creator_os); + ctx->footer.original_size = swap64(ctx->footer.original_size); + ctx->footer.current_size = swap64(ctx->footer.current_size); + ctx->footer.cylinder = swap16(ctx->footer.cylinder); + ctx->footer.disk_type = swap32(ctx->footer.disk_type); + ctx->footer.checksum = swap32(ctx->footer.checksum); + + if (ctx->footer.disk_type == VHD_TYPE_DYNAMIC) { + if (FSEEKO(ctx->fp, ctx->footer.data_offset, SEEK_SET) != 0) { + printf("Failed to seek to dynamic header\n"); + fclose(ctx->fp); + return false; + } + + if (fread(&ctx->dyn_header, 1, sizeof(VHDDynamicHeader), ctx->fp) != sizeof(VHDDynamicHeader)) { + printf("Failed to read dynamic header\n"); + fclose(ctx->fp); + return false; + } + + if (memcmp(ctx->dyn_header.cookie, VHD_DYNAMIC_COOKIE, strlen(VHD_DYNAMIC_COOKIE)) != 0) { + printf("Invalid dynamic disk header signature\n"); + fclose(ctx->fp); + return false; + } + + ctx->dyn_header.data_offset = swap64(ctx->dyn_header.data_offset); + ctx->dyn_header.bat_offset = swap64(ctx->dyn_header.bat_offset); + ctx->dyn_header.head_vers = swap32(ctx->dyn_header.head_vers); + ctx->dyn_header.max_bat_entries = swap32(ctx->dyn_header.max_bat_entries); + ctx->dyn_header.block_size = swap32(ctx->dyn_header.block_size); + + size_t bat_size = (size_t)ctx->dyn_header.max_bat_entries * sizeof(uint32_t); + + if (bat_size == 0 || bat_size > (1ULL << 30)) { + printf("Invalid BAT size\n"); + fclose(ctx->fp); + return false; + } + + ctx->bat = malloc(bat_size); + if (!ctx->bat) { + printf("Failed to allocate BAT memory\n"); + fclose(ctx->fp); + return false; + } + + if (FSEEKO(ctx->fp, ctx->dyn_header.bat_offset, SEEK_SET) != 0) { + printf("Failed to seek to BAT\n"); + free(ctx->bat); + fclose(ctx->fp); + return false; + } + + if (fread(ctx->bat, 1, bat_size, ctx->fp) != bat_size) { + printf("Failed to read BAT\n"); + free(ctx->bat); + fclose(ctx->fp); + return false; + } + + for (uint32_t i = 0; i < ctx->dyn_header.max_bat_entries; i++) { + ctx->bat[i] = swap32(ctx->bat[i]); + } + + ctx->sector_bitmap_size = (ctx->dyn_header.block_size / VHD_SECTOR_SIZE + 7) / 8; + ctx->sector_bitmap = malloc(ctx->sector_bitmap_size); + ctx->block_buffer = malloc(ctx->dyn_header.block_size); + + if (!ctx->sector_bitmap || !ctx->block_buffer) { + printf("Failed to allocate dynamic disk buffers\n"); + free(ctx->bat); + free(ctx->sector_bitmap); + free(ctx->block_buffer); + fclose(ctx->fp); + return false; + } + } + + return true; +} + +bool ntfs_init(NTFSContext* ctx, const char* path, const char* extract_path) { + memset(ctx, 0, sizeof(NTFSContext)); + strncpy(ctx->base_path, extract_path, sizeof(ctx->base_path) - 1); + + if (!init_directory_cache(&ctx->dir_cache)) { + return false; + } + + FILE* fp = fopen(path, "rb"); + if (!fp) { + free_directory_cache(&ctx->dir_cache); + return false; + } + + if (FSEEKO(fp, -512, SEEK_END) == 0) { + char signature[9] = { 0 }; + if (fread(signature, 1, 8, fp) == 8 && memcmp(signature, VHD_COOKIE, 8) == 0) { + fclose(fp); + ctx->is_vhd = true; + if (!vhd_init(&ctx->vhd, path)) { + free_directory_cache(&ctx->dir_cache); + return false; + } + } + else { + rewind(fp); + ctx->is_vhd = false; + ctx->raw.fp = fp; + } + } + + uint64_t ntfs_offset = 0; + bool found_ntfs = false; + + if (ctx->is_vhd) { + uint8_t sector[VHD_SECTOR_SIZE]; + if (ntfs_read(ctx, sector, 0, VHD_SECTOR_SIZE)) { + if (sector[0x1FE] == 0x55 && sector[0x1FF] == 0xAA) { + for (int i = 0; i < 4; i++) { + const uint8_t* part = sector + 0x1BE + (i * 16); + if (part[4] == NTFS_PARTITION_TYPE) { + uint32_t start_sector = + (uint32_t)part[8] | + ((uint32_t)part[9] << 8) | + ((uint32_t)part[10] << 16) | + ((uint32_t)part[11] << 24); + ntfs_offset = (uint64_t)start_sector * VHD_SECTOR_SIZE; + + if (ntfs_read(ctx, sector, ntfs_offset, VHD_SECTOR_SIZE) && + memcmp(sector + 3, NTFS_SIGNATURE, 8) == 0) { + found_ntfs = true; + break; + } + } + } + } + } + + if (!found_ntfs) { + const uint64_t offsets[] = { 0, 0x100000, 0x200000, 0x400000, 0x800000, 0 }; + for (int i = 0; offsets[i]; i++) { + if (ntfs_read(ctx, sector, offsets[i], VHD_SECTOR_SIZE) && + memcmp(sector + 3, NTFS_SIGNATURE, 8) == 0) { + ntfs_offset = offsets[i]; + found_ntfs = true; + break; + } + } + } + } + else { + uint8_t boot[512]; + if (ntfs_read(ctx, boot, 0, sizeof(boot)) && + boot[0] == 0xEB && boot[1] == 0x52 && boot[2] == 0x90 && + memcmp(boot + 3, NTFS_SIGNATURE, 8) == 0) { + found_ntfs = true; + } + } + + if (!found_ntfs) { + printf("No NTFS filesystem found\n"); + ntfs_close(ctx); + return false; + } + + ctx->data_start_offset = ntfs_offset; + + if (!ntfs_read(ctx, &ctx->boot, ntfs_offset, sizeof(NTFSBootSector))) { + printf("Failed to read NTFS boot sector\n"); + ntfs_close(ctx); + return false; + } + + ctx->bytes_per_cluster = (uint32_t)ctx->boot.bytes_per_sector * ctx->boot.sectors_per_cluster; + ctx->mft_offset = ntfs_offset + (ctx->boot.mft_cluster_number * ctx->bytes_per_cluster); + + if (ctx->boot.clusters_per_mft_record > 0) { + ctx->mft_record_size = ctx->boot.clusters_per_mft_record * ctx->bytes_per_cluster; + } + else { + ctx->mft_record_size = 1U << (-ctx->boot.clusters_per_mft_record); + } + + uint8_t* mft_record = malloc(ctx->mft_record_size); + if (!mft_record) { + printf("Failed to allocate memory for MFT record\n"); + ntfs_close(ctx); + return false; + } + + if (!ntfs_read(ctx, mft_record, ctx->mft_offset, ctx->mft_record_size)) { + printf("Failed to read MFT record 0\n"); + free(mft_record); + ntfs_close(ctx); + return false; + } + + const MFTRecordHeader* record = (const MFTRecordHeader*)mft_record; + if (memcmp(record->magic, "FILE", 4) != 0) { + printf("Invalid MFT record signature\n"); + free(mft_record); + ntfs_close(ctx); + return false; + } + + const uint8_t* attr = mft_record + record->attrs_offset; + while (attr < mft_record + record->bytes_used) { + const AttributeHeader* header = (const AttributeHeader*)attr; + if (header->type == 0xFFFFFFFF || header->length == 0) break; + + if (header->type == DATA_ATTR && header->name_length == 0) { + if (header->non_resident) { + uint64_t mft_data_size = header->data.non_resident.data_size; + ctx->mft_data_size = mft_data_size; + ctx->total_mft_records = mft_data_size / ctx->mft_record_size; + } + break; + } + attr += header->length; + } + + free(mft_record); + return true; +} + +bool ntfs_extract_all(NTFSContext* ctx) { + if (!create_directories(ctx->base_path)) { + printf("Failed to create output directory\n"); + return false; + } + + uint8_t* record_buffer = malloc(ctx->mft_record_size); + if (!record_buffer) { + printf("Failed to allocate MFT record buffer\n"); + return false; + } + + printf("Extraction in progress...\n"); + uint64_t current_offset = ctx->mft_offset; + uint64_t total_records = ctx->total_mft_records; + uint64_t processed_records = 0; + uint64_t extracted_records = 0; + + time_t last_update_time = time(NULL); + int last_percentage = -1; + + for (uint64_t i = 0; i < total_records; i++) { + if (!ntfs_read(ctx, record_buffer, current_offset, ctx->mft_record_size)) { + printf("Failed to read MFT record at offset 0x%llX\n", + (unsigned long long)current_offset); + break; + } + + const MFTRecordHeader* record = (const MFTRecordHeader*)record_buffer; + if (memcmp(record->magic, "FILE", 4) == 0) { + processed_records++; + if (process_mft_record(ctx, record_buffer)) { + extracted_records++; + } + } + + current_offset += ctx->mft_record_size; + + time_t current_time = time(NULL); + if (current_time != last_update_time) { + int percentage = (int)((i + 1) * 100 / total_records); + if (percentage != last_percentage) { + printf("\rProgress: %d%% ", percentage); + fflush(stdout); + last_percentage = percentage; + } + last_update_time = current_time; + } + } + + printf("\rProgress: 100%% \n"); + + printf("Extraction completed.\n"); + + free(record_buffer); + return true; +} + +void ntfs_close(NTFSContext* ctx) { + if (ctx->is_vhd) { + if (ctx->vhd.fp) { + fclose(ctx->vhd.fp); + free(ctx->vhd.bat); + free(ctx->vhd.sector_bitmap); + free(ctx->vhd.block_buffer); + } + } + else { + if (ctx->raw.fp) { + fclose(ctx->raw.fp); + } + } + free_directory_cache(&ctx->dir_cache); + memset(ctx, 0, sizeof(NTFSContext)); +} \ No newline at end of file