From db8bbdb22425b10471004d881aa90c617183d251 Mon Sep 17 00:00:00 2001 From: d0k3 Date: Thu, 8 Dec 2016 22:06:47 +0100 Subject: [PATCH] Allow (batch) verification of NCCH / NCSD / CIA --- source/crypto/aes.c | 2 +- source/game/cia.c | 39 +++--- source/game/cia.h | 3 +- source/game/gameio.c | 276 +++++++++++++++++++++++++++++++++++++++++++ source/game/gameio.h | 12 ++ source/game/ncsd.c | 4 +- source/godmode.c | 55 +++++++-- source/ui.c | 5 +- 8 files changed, 356 insertions(+), 40 deletions(-) create mode 100644 source/game/gameio.c create mode 100644 source/game/gameio.h diff --git a/source/crypto/aes.c b/source/crypto/aes.c index 7c2a8bd..4a19975 100644 --- a/source/crypto/aes.c +++ b/source/crypto/aes.c @@ -163,7 +163,7 @@ void cbc_decrypt(void *inbuf, void *outbuf, size_t size, uint32_t mode, uint8_t set_ctr(ctr); blocks = (blocks_left >= 0xFFFF) ? 0xFFFF : blocks_left; for (i=0; iindex, 2); + return 0; +} + u32 BuildCiaCert(u8* ciacert) { const u8 cert_hash_expected[0x20] = { 0xC7, 0x2E, 0x1C, 0xA5, 0x61, 0xDC, 0x9B, 0xC8, 0x05, 0x58, 0x58, 0x9C, 0x63, 0x08, 0x1C, 0x8A, @@ -165,30 +171,13 @@ u32 BuildFakeTmd(TitleMetaData* tmd, u8* title_id, u32 n_contents) { return 0; } -u32 LoadCiaStub(CiaStub* stub, const char* path) { - FIL file; - UINT bytes_read; - CiaInfo info; - - if (f_open(&file, path, FA_READ | FA_OPEN_EXISTING) != FR_OK) - return 1; - - // first 0x20 byte of CIA header - f_lseek(&file, 0); - if ((f_read(&file, stub, 0x20, &bytes_read) != FR_OK) || (bytes_read != 0x20) || - (ValidateCiaHeader(&(stub->header)) != 0)) { - f_close(&file); - return 1; - } - GetCiaInfo(&info, &(stub->header)); - - // everything up till content offset - f_lseek(&file, 0); - if ((f_read(&file, stub, info.offset_content, &bytes_read) != FR_OK) || (bytes_read != info.offset_content)) { - f_close(&file); - return 1; - } - - f_close(&file); +u32 DecryptCiaContent(u8* data, u32 size, u8* ctr, const u8* titlekey) { + // WARNING: size and offset of data have to be a multiple of 16 + u8 tik[16] __attribute__((aligned(32))); + u32 mode = AES_CNT_TITLEKEY_DECRYPT_MODE; + memcpy(tik, titlekey, 16); + setup_aeskey(0x11, tik); + use_aeskey(0x11); + cbc_decrypt(data, data, size / 16, mode, ctr); return 0; } diff --git a/source/game/cia.h b/source/game/cia.h index a20d55e..a34d4c8 100644 --- a/source/game/cia.h +++ b/source/game/cia.h @@ -147,6 +147,7 @@ u32 ValidateCiaHeader(CiaHeader* header); u32 GetCiaInfo(CiaInfo* info, CiaHeader* header); u32 GetCiaContentInfo(CiaContentInfo* contents, TitleMetaData* tmd); u32 GetTitleKey(u8* titlekey, Ticket* ticket); +u32 GetCiaCtr(u8* ctr, TmdContentChunk* chunk); u32 BuildCiaCert(u8* ciacert); u32 BuildFakeTicket(Ticket* ticket, u8* title_id); @@ -155,4 +156,4 @@ u32 BuildFakeTmd(TitleMetaData* tmd, u8* title_id, u32 n_contents); /*u32 BuildCiaMeta(Ncch ncch); u32 InsertCiaContent(const char* path, const char* content, bool encrypt);*/ -u32 LoadCiaStub(CiaStub* stub, const char* path); +u32 DecryptCiaContent(u8* data, u32 size, u8* ctr, const u8* titlekey); diff --git a/source/game/gameio.c b/source/game/gameio.c new file mode 100644 index 0000000..deda249 --- /dev/null +++ b/source/game/gameio.c @@ -0,0 +1,276 @@ +#include "gameio.h" +#include "game.h" +#include "ui.h" +#include "filetype.h" +#include "sddata.h" +#include "aes.h" +#include "sha.h" +#include "ff.h" + +u32 GetNcchFileHeaders(NcchHeader* ncch, ExeFsHeader* exefs, FIL* file) { + u32 offset_ncch = f_tell(file); + UINT btr; + + if ((fx_read(file, ncch, sizeof(NcchHeader), &btr) != FR_OK) || + (ValidateNcchHeader(ncch) != 0)) + return 1; + + if (exefs && ncch->size_exefs) { + u32 offset_exefs = offset_ncch + (ncch->offset_exefs * NCCH_MEDIA_UNIT); + f_lseek(file, offset_exefs); + if ((fx_read(file, exefs, sizeof(ExeFsHeader), &btr) != FR_OK) || + (DecryptNcch((u8*) exefs, ncch->offset_exefs * NCCH_MEDIA_UNIT, sizeof(ExeFsHeader), ncch, NULL) != 0) || + (ValidateExeFsHeader(exefs, ncch->size_exefs * NCCH_MEDIA_UNIT) != 0)) + return 1; + } + + return 0; +} + +u32 CheckNcchFileHash(u8* expected, FIL* file, u32 size_data, u32 offset_ncch, NcchHeader* ncch, ExeFsHeader* exefs) { + u32 offset_data = f_tell(file) - offset_ncch; + u8 hash[32]; + + sha_init(SHA256_MODE); + for (u32 i = 0; i < size_data; i += MAIN_BUFFER_SIZE) { + u32 read_bytes = min(MAIN_BUFFER_SIZE, (size_data - i)); + UINT bytes_read; + fx_read(file, MAIN_BUFFER, read_bytes, &bytes_read); + DecryptNcch(MAIN_BUFFER, offset_data + i, read_bytes, ncch, exefs); + sha_update(MAIN_BUFFER, read_bytes); + } + sha_get(hash); + + return (memcmp(hash, expected, 32) == 0) ? 0 : 1; +} + +u32 VerifyNcchFile(const char* path, u32 offset, u32 size) { + NcchHeader ncch; + ExeFsHeader exefs; + FIL file; + + char pathstr[32 + 1]; + TruncateString(pathstr, path, 32, 8); + + // open file, get NCCH, ExeFS header + if (fx_open(&file, path, FA_READ | FA_OPEN_EXISTING) != FR_OK) + return 1; + f_lseek(&file, offset); + + if (GetNcchFileHeaders(&ncch, &exefs, &file) != 0) { + if (!offset) ShowPrompt(false, "%s\nError: Not a NCCH file", pathstr); + fx_close(&file); + return 1; + } + + // size checks + if (!size) size = f_size(&file) - offset; + if ((f_size(&file) < offset) || (size < ncch.size * NCCH_MEDIA_UNIT)) { + if (!offset) ShowPrompt(false, "%s\nError: File is too small", pathstr); + fx_close(&file); + return 1; + } + + // check / setup crypto + if (SetupNcchCrypto(&ncch) != 0) { + if (!offset) ShowPrompt(false, "%s\nError: Crypto not set up", pathstr); + fx_close(&file); + return 1; + } + + u32 ver_exthdr = 0; + u32 ver_exefs = 0; + u32 ver_romfs = 0; + + // base hash check for extheader + if (ncch.size_exthdr > 0) { + f_lseek(&file, offset + NCCH_EXTHDR_OFFSET); + ver_exthdr = CheckNcchFileHash(ncch.hash_exthdr, &file, 0x400, offset, &ncch, &exefs); + } + + // base hash check for exefs + if (ncch.size_exefs > 0) { + f_lseek(&file, offset + (ncch.offset_exefs * NCCH_MEDIA_UNIT)); + ver_exefs = CheckNcchFileHash(ncch.hash_exefs, &file, ncch.size_exefs_hash * NCCH_MEDIA_UNIT, offset, &ncch, &exefs); + } + + // base hash check for romfs + if (ncch.size_romfs > 0) { + f_lseek(&file, offset + (ncch.offset_romfs * NCCH_MEDIA_UNIT)); + ver_romfs = CheckNcchFileHash(ncch.hash_romfs, &file, ncch.size_romfs_hash * NCCH_MEDIA_UNIT, offset, &ncch, &exefs); + } + + // thorough exefs verification + if (ncch.size_exefs > 0) { + for (u32 i = 0; !ver_exefs && (i < 10); i++) { + ExeFsFileHeader* exefile = exefs.files + i; + u8* hash = exefs.hashes[9 - i]; + if (!exefile->size) continue; + f_lseek(&file, offset + (ncch.offset_exefs * NCCH_MEDIA_UNIT) + 0x200 + exefile->offset); + ver_exefs = CheckNcchFileHash(hash, &file, exefile->size, offset, &ncch, &exefs); + } + } + + if (!offset && (ver_exthdr|ver_exefs|ver_romfs)) { // verification summary + ShowPrompt(false, "%s\nNCCH verification failed:\nExtHdr/ExeFS/RomFS: %s/%s/%s", pathstr, + (!ncch.size_exthdr) ? "-" : (ver_exthdr == 0) ? "ok" : "fail", + (!ncch.size_exefs) ? "-" : (ver_exefs == 0) ? "ok" : "fail", + (!ncch.size_romfs) ? "-" : (ver_romfs == 0) ? "ok" : "fail"); + } + + fx_close(&file); + return ver_exthdr|ver_exefs|ver_romfs; +} + +u32 LoadNcsdHeader(NcsdHeader* ncsd, const char* path) { + FIL file; + UINT btr; + + // open file, get NCSD header + if (fx_open(&file, path, FA_READ | FA_OPEN_EXISTING) != FR_OK) + return 1; + f_lseek(&file, 0); + if ((fx_read(&file, ncsd, sizeof(NcsdHeader), &btr) != FR_OK) || + (ValidateNcsdHeader(ncsd) != 0)) { + fx_close(&file); + return 1; + } + fx_close(&file); + + return 0; +} + +u32 VerifyNcsdFile(const char* path) { + NcsdHeader ncsd; + + // path string + char pathstr[32 + 1]; + TruncateString(pathstr, path, 32, 8); + + // load NCSD header + if (LoadNcsdHeader(&ncsd, path) != 0) { + ShowPrompt(false, "%s\nError: Not a NCSD file", pathstr); + return 1; + } + + // validate NCSD contents + for (u32 i = 0; i < 8; i++) { + NcchPartition* partition = ncsd.partitions + i; + u32 offset = partition->offset * NCSD_MEDIA_UNIT; + u32 size = partition->size * NCSD_MEDIA_UNIT; + if (!size) continue; + if (VerifyNcchFile(path, offset, size) != 0) { + ShowPrompt(false, "%s\nContent%lu (%08lX@%08lX):\nVerification failed", + pathstr, i, size, offset, i); + return 1; + } + } + + return 0; +} + +u32 LoadCiaStub(CiaStub* stub, const char* path) { + FIL file; + UINT btr; + CiaInfo info; + + if (fx_open(&file, path, FA_READ | FA_OPEN_EXISTING) != FR_OK) + return 1; + + // first 0x20 byte of CIA header + f_lseek(&file, 0); + if ((fx_read(&file, stub, 0x20, &btr) != FR_OK) || (btr != 0x20) || + (ValidateCiaHeader(&(stub->header)) != 0)) { + fx_close(&file); + return 1; + } + GetCiaInfo(&info, &(stub->header)); + + // everything up till content offset + f_lseek(&file, 0); + if ((fx_read(&file, stub, info.offset_content, &btr) != FR_OK) || (btr != info.offset_content)) { + fx_close(&file); + return 1; + } + + fx_close(&file); + return 0; +} + +u32 VerifyCiaContent(const char* path, u64 offset, TmdContentChunk* chunk, const u8* titlekey) { + u8 hash[32]; + u8 ctr[16]; + FIL file; + + u8* expected = chunk->hash; + u64 size = getbe64(chunk->size); + bool encrypted = getbe16(chunk->type) & 0x1; + + if (fx_open(&file, path, FA_READ | FA_OPEN_EXISTING) != FR_OK) + return 1; + if (offset + size > f_size(&file)) { + fx_close(&file); + return 1; + } + f_lseek(&file, offset); + + GetCiaCtr(ctr, chunk); + sha_init(SHA256_MODE); + ShowProgress(0, 0, path); + for (u32 i = 0; i < size; i += MAIN_BUFFER_SIZE) { + u32 read_bytes = min(MAIN_BUFFER_SIZE, (size - i)); + UINT bytes_read; + fx_read(&file, MAIN_BUFFER, read_bytes, &bytes_read); + if (encrypted) DecryptCiaContent(MAIN_BUFFER, read_bytes, ctr, titlekey); + sha_update(MAIN_BUFFER, read_bytes); + if (!ShowProgress(i + read_bytes, size, path)) break; + } + sha_get(hash); + fx_close(&file); + + return memcmp(hash, expected, 32); +} + +u32 VerifyCiaFile(const char* path) { + CiaStub* cia = (CiaStub*) TEMP_BUFFER; + CiaInfo info; + u8 titlekey[16]; + + // path string + char pathstr[32 + 1]; + TruncateString(pathstr, path, 32, 8); + + // load CIA stub + if ((LoadCiaStub(cia, path) != 0) || + (GetCiaInfo(&info, &(cia->header)) != 0) || + (GetTitleKey(titlekey, &(cia->ticket)) != 0)) { + ShowPrompt(false, "%s\nError: Probably not a CIA file", pathstr); + return 1; + } + + // verify contents + u32 content_count = getbe16(cia->tmd.content_count); + u64 next_offset = info.offset_content; + for (u32 i = 0; (i < content_count) && (i < CIA_MAX_CONTENTS); i++) { + TmdContentChunk* chunk = &(cia->content_list[i]); + if (VerifyCiaContent(path, next_offset, chunk, titlekey) != 0) { + ShowPrompt(false, "%s\nID %08lX (%08llX@%08llX)\nVerification failed", + pathstr, getbe32(chunk->id), getbe64(chunk->size), next_offset, i); + return 1; + } + next_offset += getbe64(chunk->size); + } + + return 0; +} + +u32 VerifyGameFile(const char* path) { + u32 filetype = IdentifyFileType(path); + if (filetype == GAME_CIA) + return VerifyCiaFile(path); + else if (filetype == GAME_NCSD) + return VerifyNcsdFile(path); + else if (filetype == GAME_NCCH) + return VerifyNcchFile(path, 0, 0); + else return 1; +} diff --git a/source/game/gameio.h b/source/game/gameio.h new file mode 100644 index 0000000..c1bda12 --- /dev/null +++ b/source/game/gameio.h @@ -0,0 +1,12 @@ +#pragma once + +#include "common.h" + +u32 VerifyNcchFile(const char* path, u32 offset, u32 size); +u32 VerifyNcsdFile(const char* path); +u32 VerifiyCiaFile(const char* path); +u32 VerifyGameFile(const char* path); + +u32 DecryptNcchFile(const char* path, u32 offset, u32 size); +u32 DecryptNcsdFile(const char* path); +u32 DecryptCiaFile(const char* path, bool deep); diff --git a/source/game/ncsd.c b/source/game/ncsd.c index a8fd2ac..ad7797f 100644 --- a/source/game/ncsd.c +++ b/source/game/ncsd.c @@ -9,8 +9,8 @@ u32 ValidateNcsdHeader(NcsdHeader* header) { u32 data_units = 0; for (u32 i = 0; i < 8; i++) { NcchPartition* partition = header->partitions + i; - if ((partition->offset == 0) && (partition->size == 0)) - continue; + if ((i == 0) && !partition->size) return 1; // first content must be there + else if (!partition->size) continue; if (partition->offset < data_units) return 1; // overlapping partitions, failed data_units = partition->offset + partition->size; diff --git a/source/godmode.c b/source/godmode.c index 4fa3058..2d318c0 100644 --- a/source/godmode.c +++ b/source/godmode.c @@ -2,6 +2,7 @@ #include "ui.h" #include "hid.h" #include "fs.h" +#include "gameio.h" #include "platform.h" #include "nand.h" #include "virtual.h" @@ -660,35 +661,44 @@ u32 GodMode() { } } else if ((pad_state & BUTTON_A) && (curr_entry->type == T_FILE)) { // process a file char pathstr[32 + 1]; - const char* optionstr[5]; - u32 n_opt = 0; + const char* optionstr[8]; + int n_opt = 0; u32 filetype = IdentifyFileType(curr_entry->path); u32 drvtype = DriveType(curr_entry->path); int mountable = (filetype && (drvtype & DRV_FAT) && !(drvtype & (DRV_IMAGE|DRV_RAMDRIVE))) ? - (int) ++n_opt : -1; - int hexviewer = (int) ++n_opt; - int calcsha = (int) ++n_opt; + ++n_opt : -1; + int hexviewer = ++n_opt; + int calcsha = ++n_opt; + int verificable = ((filetype == GAME_CIA) || (filetype == GAME_NCSD) || + (filetype == GAME_NCCH)) ? ++n_opt : -1; int injectable = ((clipboard->n_entries == 1) && (clipboard->entry[0].type == T_FILE) && (drvtype & DRV_FAT) && (strncmp(clipboard->entry[0].path, curr_entry->path, 256) != 0)) ? (int) ++n_opt : -1; - int searchdrv = (curr_drvtype & DRV_SEARCH) ? (int) ++n_opt : -1; + int searchdrv = (curr_drvtype & DRV_SEARCH) ? ++n_opt : -1; + + u32 n_marked = 0; + if (curr_entry->marked) { + for (u32 i = 0; i < current_dir->n_entries; i++) + if (current_dir->entry[i].marked) n_marked++; + } TruncateString(pathstr, curr_entry->path, 32, 8); optionstr[hexviewer-1] = "Show in Hexeditor"; optionstr[calcsha-1] = "Calculate SHA-256"; - if (injectable) optionstr[injectable-1] = "Inject data @offset"; - if (mountable) optionstr[mountable-1] = + if (verificable > 0) optionstr[verificable-1] = "Verify game image"; + if (injectable > 0) optionstr[injectable-1] = "Inject data @offset"; + if (mountable > 0) optionstr[mountable-1] = (filetype == IMG_NAND) ? "Mount as NAND image" : (filetype == IMG_FAT) ? "Mount as FAT image" : (filetype == GAME_CIA) ? "Mount as CIA image" : (filetype == GAME_NCSD) ? "Mount as NCSD image" : (filetype == GAME_NCCH) ? "Mount as NCCH image" : "???"; - if (searchdrv) optionstr[searchdrv-1] = "Open containing folder"; + if (searchdrv > 0) optionstr[searchdrv-1] = "Open containing folder"; int user_select = ShowSelectPrompt(n_opt, optionstr, pathstr); if (user_select == hexviewer) { // -> show in hex viewer @@ -696,6 +706,33 @@ u32 GodMode() { } else if (user_select == calcsha) { // -> calculate SHA-256 Sha256Calculator(curr_entry->path); GetDirContents(current_dir, current_path); + } else if (user_select == verificable) { // -> verify game file + if ((n_marked > 1) && ShowPrompt(true, "Try to verify all %lu selected files?", n_marked)) { + u32 n_success = 0; + u32 n_other = 0; + u32 n_processed = 0; + for (u32 i = 0; i < current_dir->n_entries; i++) { + const char* path = current_dir->entry[i].path; + if (!current_dir->entry[i].marked) + continue; + if (IdentifyFileType(path) != filetype) { + n_other++; + continue; + } + if ((filetype != GAME_CIA) && !ShowProgress(n_processed++, n_marked, path)) break; + if (VerifyGameFile(path) == 0) n_success++; + else if (filetype != GAME_CIA) ShowProgress(0, 0, path); // redraw progress bar + current_dir->entry[i].marked = false; + } + if (filetype != GAME_CIA) ShowProgress(1, 1, ""); // CIA verification has progress bar handling + if (n_other) ShowPrompt(false, "%lu/%lu files verified ok\n%lu/%lu not of same type", + n_success, n_marked, n_other, n_marked); + else ShowPrompt(false, "%lu/%lu files verified ok", n_success, n_marked); + } else { + ShowString("%s\nVerifying file, please wait...", pathstr); + u32 result = VerifyGameFile(curr_entry->path); + ShowPrompt(false, "%s\nVerification %s", pathstr, (result == 0) ? "success" : "failed"); + } } else if (user_select == injectable) { // -> inject data from clipboard char origstr[18 + 1]; TruncateString(origstr, clipboard->entry[0].name, 18, 10); diff --git a/source/ui.c b/source/ui.c index 7b3e2ca..a391e66 100644 --- a/source/ui.c +++ b/source/ui.c @@ -536,8 +536,9 @@ bool ShowProgress(u64 current, u64 total, const char* opstr) } DrawRectangle(TOP_SCREEN, bar_pos_x + 2, bar_pos_y + 2, prog_width, bar_height - 4, COLOR_STD_FONT); - TruncateString(tempstr, opstr, (bar_width / FONT_WIDTH_EXT) - 7, 8); - snprintf(progstr, 64, "%s (%lu%%)", tempstr, prog_percent); + TruncateString(progstr, opstr, (bar_width / FONT_WIDTH_EXT) - 7, 8); + snprintf(tempstr, 64, "%s (%lu%%)", progstr, prog_percent); + ResizeString(progstr, tempstr, bar_width / FONT_WIDTH_EXT, 8, false); DrawString(TOP_SCREEN, progstr, bar_pos_x, text_pos_y, COLOR_STD_FONT, COLOR_STD_BG); DrawString(TOP_SCREEN, "(hold B to cancel)", bar_pos_x + 2, text_pos_y + 14, COLOR_STD_FONT, COLOR_STD_BG);