From 87d4152d4e37b10b46a93063e1c404ba0d273fcd Mon Sep 17 00:00:00 2001 From: d0k3 Date: Mon, 10 Jun 2019 16:30:40 +0200 Subject: [PATCH] Add handling for SD / TWLN & DLC CMD CMACs Fixes #340 --- arm9/source/filesys/filetype.c | 3 + arm9/source/filesys/filetype.h | 51 ++++++------- arm9/source/game/cmd.c | 11 +++ arm9/source/game/cmd.h | 18 +++++ arm9/source/game/game.h | 2 + arm9/source/godmode.c | 35 ++++++--- arm9/source/utils/nandcmac.c | 130 +++++++++++++++++++++++++++++++-- arm9/source/utils/nandcmac.h | 5 +- 8 files changed, 211 insertions(+), 44 deletions(-) create mode 100644 arm9/source/game/cmd.c create mode 100644 arm9/source/game/cmd.h diff --git a/arm9/source/filesys/filetype.c b/arm9/source/filesys/filetype.c index 2d57af8..e0d87e1 100644 --- a/arm9/source/filesys/filetype.c +++ b/arm9/source/filesys/filetype.c @@ -118,6 +118,9 @@ u64 IdentifyFileType(const char* path) { } else if ((fsize > sizeof(ThreedsxHeader)) && (memcmp(data, threedsx_magic, sizeof(threedsx_magic)) == 0)) { return GAME_3DSX; // 3DSX (executable) file + } else if ((fsize > sizeof(CmdHeader)) && + CheckCmdSize((CmdHeader*) data, fsize) == 0) { + return GAME_CMD; // CMD file } else if ((fsize > sizeof(NcchInfoHeader)) && (GetNcchInfoVersion((NcchInfoHeader*) data)) && (strncasecmp(fname, NCCHINFO_NAME, 32) == 0)) { diff --git a/arm9/source/filesys/filetype.h b/arm9/source/filesys/filetype.h index eea5112..f04170a 100644 --- a/arm9/source/filesys/filetype.h +++ b/arm9/source/filesys/filetype.h @@ -8,31 +8,32 @@ #define GAME_NCSD (1ULL<<3) #define GAME_NCCH (1ULL<<4) #define GAME_TMD (1ULL<<5) -#define GAME_EXEFS (1ULL<<6) -#define GAME_ROMFS (1ULL<<7) -#define GAME_BOSS (1ULL<<8) -#define GAME_NUSCDN (1ULL<<9) -#define GAME_TICKET (1ULL<<10) -#define GAME_SMDH (1ULL<<11) -#define GAME_3DSX (1ULL<<12) -#define GAME_NDS (1ULL<<13) -#define GAME_GBA (1ULL<<14) -#define GAME_TAD (1ULL<<15) -#define SYS_FIRM (1ULL<<16) -#define SYS_DIFF (1ULL<<17) -#define SYS_DISA (1ULL<<18) // not yet used -#define SYS_AGBSAVE (1ULL<<19) -#define SYS_TICKDB (1ULL<<20) -#define BIN_NCCHNFO (1ULL<<21) -#define BIN_TIKDB (1ULL<<22) -#define BIN_KEYDB (1ULL<<23) -#define BIN_LEGKEY (1ULL<<24) -#define TXT_SCRIPT (1ULL<<25) -#define TXT_GENERIC (1ULL<<26) -#define GFX_PNG (1ULL<<27) -#define FONT_PBM (1ULL<<28) -#define NOIMG_NAND (1ULL<<29) -#define HDR_NAND (1ULL<<30) +#define GAME_CMD (1ULL<<6) +#define GAME_EXEFS (1ULL<<7) +#define GAME_ROMFS (1ULL<<8) +#define GAME_BOSS (1ULL<<9) +#define GAME_NUSCDN (1ULL<<10) +#define GAME_TICKET (1ULL<<11) +#define GAME_SMDH (1ULL<<12) +#define GAME_3DSX (1ULL<<13) +#define GAME_NDS (1ULL<<14) +#define GAME_GBA (1ULL<<15) +#define GAME_TAD (1ULL<<16) +#define SYS_FIRM (1ULL<<17) +#define SYS_DIFF (1ULL<<18) +#define SYS_DISA (1ULL<<19) // not yet used +#define SYS_AGBSAVE (1ULL<<20) +#define SYS_TICKDB (1ULL<<21) +#define BIN_NCCHNFO (1ULL<<22) +#define BIN_TIKDB (1ULL<<23) +#define BIN_KEYDB (1ULL<<24) +#define BIN_LEGKEY (1ULL<<25) +#define TXT_SCRIPT (1ULL<<26) +#define TXT_GENERIC (1ULL<<27) +#define GFX_PNG (1ULL<<28) +#define FONT_PBM (1ULL<<29) +#define NOIMG_NAND (1ULL<<30) +#define HDR_NAND (1ULL<<31) #define TYPE_BASE 0xFFFFFFFFULL // 32 bit reserved for base types // #define FLAG_FIRM (1ULL<<57) // <--- for CXIs containing FIRMs diff --git a/arm9/source/game/cmd.c b/arm9/source/game/cmd.c new file mode 100644 index 0000000..62f4684 --- /dev/null +++ b/arm9/source/game/cmd.c @@ -0,0 +1,11 @@ +#include "cmd.h" + + +u32 CheckCmdSize(CmdHeader* cmd, u64 fsize) { + u64 cmdsize = sizeof(CmdHeader) + + (cmd->n_entries * sizeof(u32)) + + (cmd->n_cmacs * sizeof(u32)) + + (cmd->n_entries * 0x10); + + return (fsize == cmdsize) ? 0 : 1; +} diff --git a/arm9/source/game/cmd.h b/arm9/source/game/cmd.h new file mode 100644 index 0000000..b286cbc --- /dev/null +++ b/arm9/source/game/cmd.h @@ -0,0 +1,18 @@ +#pragma once + +#include "common.h" + + +// from: http://3dbrew.org/wiki/Titles#Data_Structure +typedef struct { + u32 cmd_id; // same as filename id, .cmd + u32 n_entries; // matches highest content index + u32 n_cmacs; // number of cmacs in file (excluding the one @0x10) + u32 unknown; // usually 1 + u8 cmac[0x10]; // calculated from first 0x10 byte of data, no hashing + // followed by u32 list of content ids (sorted by index, 0xFFFFFFFF for unavailable) + // followed by u32 list of content ids (sorted by id?) + // followed by CMACs (may contain garbage) +} __attribute__((packed, aligned(4))) CmdHeader; + +u32 CheckCmdSize(CmdHeader* cmd, u64 fsize); diff --git a/arm9/source/game/game.h b/arm9/source/game/game.h index cad60d9..ccbe7dd 100644 --- a/arm9/source/game/game.h +++ b/arm9/source/game/game.h @@ -13,5 +13,7 @@ #include "gba.h" #include "tad.h" #include "3dsx.h" +#include "tmd.h" +#include "cmd.h" #include "ticketdb.h" #include "ncchinfo.h" diff --git a/arm9/source/godmode.c b/arm9/source/godmode.c index fb16713..3b0bf81 100644 --- a/arm9/source/godmode.c +++ b/arm9/source/godmode.c @@ -875,23 +875,34 @@ u32 Sha256Calculator(const char* path) { u32 CmacCalculator(const char* path) { char pathstr[32 + 1]; - u8 cmac[16]; TruncateString(pathstr, path, 32, 8); - if (CalculateFileCmac(path, cmac) != 0) { - ShowPrompt(false, "Calculating CMAC: failed!"); - return 1; - } else { - u8 cmac_file[16]; - bool identical = ((ReadFileCmac(path, cmac_file) == 0) && (memcmp(cmac, cmac_file, 16) == 0)); - if (ShowPrompt(!identical, "%s\n%016llX%016llX\n%s%s%s", - pathstr, getbe64(cmac + 0), getbe64(cmac + 8), - "CMAC verification: ", (identical) ? "passed!" : "failed!", - (!identical) ? "\n \nFix CMAC in file?" : "") && - !identical && (WriteFileCmac(path, cmac) != 0)) { + if (IdentifyFileType(path) != GAME_CMD) { + u8 cmac[16] __attribute__((aligned(4))); + if (CalculateFileCmac(path, cmac) != 0) { + ShowPrompt(false, "Calculating CMAC: failed!"); + return 1; + } else { + u8 cmac_file[16]; + bool identical = ((ReadFileCmac(path, cmac_file) == 0) && (memcmp(cmac, cmac_file, 16) == 0)); + if (ShowPrompt(!identical, "%s\n%016llX%016llX\n%s%s%s", + pathstr, getbe64(cmac + 0), getbe64(cmac + 8), + "CMAC verification: ", (identical) ? "passed!" : "failed!", + (!identical) ? "\n \nFix CMAC in file?" : "") && + !identical && (WriteFileCmac(path, cmac) != 0)) { + ShowPrompt(false, "Fixing CMAC: failed!"); + } + } + } else { // special handling for CMD files + bool correct = (CheckCmdCmac(path) == 0); + if (ShowPrompt(!correct, "%s\nXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n%s%s%s", + pathstr, "CMAC verification: ", (correct) ? "passed!" : "failed!", + (!correct) ? "\n \nFix CMAC in file?" : "") && + !correct && (FixCmdCmac(path) != 0)) { ShowPrompt(false, "Fixing CMAC: failed!"); } } + return 0; } diff --git a/arm9/source/utils/nandcmac.c b/arm9/source/utils/nandcmac.c index edc31c1..cc0292e 100644 --- a/arm9/source/utils/nandcmac.c +++ b/arm9/source/utils/nandcmac.c @@ -1,6 +1,7 @@ #include "nandcmac.h" #include "fsperm.h" #include "gba.h" +#include "cmd.h" #include "sha.h" #include "aes.h" #include "vff.h" @@ -21,6 +22,8 @@ #define CMAC_MOVABLE 9 #define CMAC_AGBSAVE 10 #define CMAC_AGBSAVE_SD 11 +#define CMAC_CMD_SD 12 +#define CMAC_CMD_TWLN 13 // unsupported // see: https://www.3dbrew.org/wiki/Savegames#AES_CMAC_header #define CMAC_SAVETYPE NULL, "CTR-EXT0", "CTR-EXT0", "CTR-SYS0", "CTR-NOR0", "CTR-SAV0", "CTR-SIGN", "CTR-9DB0", "CTR-9DB0", NULL, NULL, NULL @@ -97,6 +100,7 @@ u32 ReadWriteFileCmac(const char* path, u8* cmac, bool do_write) { if (!cmac_type) return 1; else if (cmac_type == CMAC_MOVABLE) offset = 0x130; else if ((cmac_type == CMAC_AGBSAVE) || (cmac_type == CMAC_AGBSAVE_SD)) offset = 0x010; + else if ((cmac_type == CMAC_CMD_SD) || (cmac_type == CMAC_CMD_TWLN)) return 1; // can't do that here else offset = 0x000; if (do_write && !CheckWritePermissions(path)) return 1; @@ -128,6 +132,9 @@ u32 CalculateFileCmac(const char* path, u8* cmac) { ext && (strncasecmp(ext, "sav", 4) == 0)) { // cmac_type = (CheckCmacHeader(path) == 0) ? CMAC_SAVEDATA_SD : (CheckAgbSaveHeader(path) == 0) ? CMAC_AGBSAVE_SD : 0; cmac_type = (CheckCmacHeader(path) == 0) ? CMAC_SAVEDATA_SD : 0; + } else if ((sscanf(path, "%c:/title/%08lx/%08lx/content/cmd/%08lx.cmd", &drv, &tid_high, &tid_low, &sid) == 4) && + ext && (strncasecmp(ext, "cmd", 4) == 0)) { + cmac_type = CMAC_CMD_SD; // this needs special handling, it's in here just for detection } } else if ((drv == '1') || (drv == '4') || (drv == '7')) { // data on CTRNAND u64 id0_high, id0_low; // ID0 @@ -140,6 +147,11 @@ u32 CalculateFileCmac(const char* path, u8* cmac) { cmac_type = CMAC_EXTDATA_SYS; } else if (sscanf(path, "%c:/data/%016llx%016llx/sysdata/%08lx/%08lx", &drv, &id0_high, &id0_low, &fid_low, &fid_high) == 5) cmac_type = CMAC_SAVEDATA_SYS; + } else if ((drv == '2') || (drv == '5') || (drv == '8')) { // data on TWLN + if ((sscanf(path, "%c:/title/%08lx/%08lx/content/cmd/%08lx.cmd", &drv, &tid_high, &tid_low, &sid) == 4) && + ext && (strncasecmp(ext, "cmd", 4) == 0)) { + cmac_type = CMAC_CMD_TWLN; // this is not supported (yet), it's in here just for detection + } } if (!cmac_type) { // path independent stuff @@ -157,6 +169,7 @@ u32 CalculateFileCmac(const char* path, u8* cmac) { // exit with cmac_type if (u8*) cmac is NULL // somewhat hacky, but can be used to check if file has a CMAC if (!cmac) return cmac_type; + else if ((cmac_type == CMAC_CMD_SD) || (cmac_type == CMAC_CMD_TWLN)) return 1; else if (!cmac_type) return 1; const u32 cmac_keyslot[] = { CMAC_KEYSLOT }; @@ -234,15 +247,26 @@ u32 CalculateFileCmac(const char* path, u8* cmac) { } u32 CheckFileCmac(const char* path) { - u8 fcmac[16]; - u8 ccmac[16]; - return ((ReadFileCmac(path, fcmac) == 0) && (CalculateFileCmac(path, ccmac) == 0) && - (memcmp(fcmac, ccmac, 16) == 0)) ? 0 : 1; + u32 cmac_type = CalculateFileCmac(path, NULL); + if ((cmac_type == CMAC_CMD_SD) || (cmac_type == CMAC_CMD_TWLN)) { + return CheckCmdCmac(path); + } else if (cmac_type) { + u8 fcmac[16]; + u8 ccmac[16]; + return ((ReadFileCmac(path, fcmac) == 0) && (CalculateFileCmac(path, ccmac) == 0) && + (memcmp(fcmac, ccmac, 16) == 0)) ? 0 : 1; + } else return 1; + } u32 FixFileCmac(const char* path) { - u8 ccmac[16]; - return ((CalculateFileCmac(path, ccmac) == 0) && (WriteFileCmac(path, ccmac) == 0)) ? 0 : 1; + u32 cmac_type = CalculateFileCmac(path, NULL); + if ((cmac_type == CMAC_CMD_SD) || (cmac_type == CMAC_CMD_TWLN)) { + return FixCmdCmac(path); + } else if (cmac_type) { + u8 ccmac[16]; + return ((CalculateFileCmac(path, ccmac) == 0) && (WriteFileCmac(path, ccmac) == 0)) ? 0 : 1; + } else return 1; } u32 FixAgbSaveCmac(void* data, u8* cmac, const char* sddrv) { @@ -281,6 +305,100 @@ u32 FixAgbSaveCmac(void* data, u8* cmac, const char* sddrv) { return 0; } +u32 CheckFixCmdCmac(const char* path, bool fix) { + u8 cmac[16] __attribute__((aligned(4))); + u32 keyslot = ((*path == 'A') || (*path == 'B')) ? 0x30 : 0x0B; + bool fixed = false; + + // setup the keyslot if required + if ((keyslot == 0x30) && (SetupSlot0x30(*path) != 0)) + return 1; + + // set up the temporary path for contents + u32 pos_name_content = 2 + 1 + 5 + 1 + 8 + 1 + 8 + 1 + 7 + 1; + char path_content[256]; // that will be more than enough + char* name_content = path_content + pos_name_content; + strncpy(path_content, path, 256); + if (strnlen(path_content, 256) < pos_name_content) + return 1; + + // hacky check for DLC conents + bool is_dlc = (strncasecmp(path + 2 + 1 + 5 + 1, "0004008c", 8) == 0); + + // cmd data + u64 cmd_size = fvx_qsize(path); + u8* cmd_data = malloc(cmd_size); + CmdHeader* cmd = (CmdHeader*) (void*) cmd_data; + + // check for out of memory + if (cmd_data == NULL) return 1; + + // read the full file to memory and check it (we may write it back later) + if ((fvx_qread(path, cmd_data, 0, cmd_size, NULL) != FR_OK) || + (CheckCmdSize(cmd, cmd_size) != 0)) { + free(cmd_data); + return 1; + } + + + // now, check the CMAC@0x10 + use_aeskey(keyslot); + aes_cmac(cmd_data, cmac, 1); + if (memcmp(cmd->cmac, cmac, 0x10) != 0) { + if (fix) { + fixed = true; + memcpy(cmd->cmac, cmac, 0x10); + } else { + free(cmd_data); + return 1; + } + } + + // further checking will be more complicated + // set up pointers to cmd data (pointer arithemtic is hard) + u32 n_entries = cmd->n_entries; + u32* cnt_id = (u32*) (cmd + 1); + u8* cnt_cmac = (u8*) (cnt_id + cmd->n_entries + cmd->n_cmacs); + + // check all ids and cmacs + for (u32 cnt_idx = 0; cnt_idx < n_entries; cnt_idx++, cnt_id++, cnt_cmac += 0x10) { + u8 hashdata[0x108] __attribute__((aligned(4))); + u8 shasum[32]; + if (*cnt_id == 0xFFFFFFFF) continue; // unavailable content + snprintf(name_content, 32, "%s%08lX.app", (is_dlc) ? "00000000/" : "", *cnt_id); + if (fvx_qread(path_content, hashdata, 0x100, 0x100, NULL) != FR_OK) { + free(cmd_data); + return 1; // failed to read content + } + memcpy(hashdata + 0x100, &cnt_idx, 4); + memcpy(hashdata + 0x104, cnt_id, 4); + // hash block complete, check it + sha_quick(shasum, hashdata, 0x108, SHA256_MODE); + use_aeskey(keyslot); + aes_cmac(shasum, cmac, 2); + if (memcmp(cnt_cmac, cmac, 0x10) != 0) { + if (fix) { + fixed = true; + memcpy(cnt_cmac, cmac, 0x10); + } else { + free(cmd_data); + return 1; // bad cmac + } + } + } + + // if fixing is enable, write back cmd file + if (fix && fixed && CheckWritePermissions(path) && + (fvx_qwrite(path, cmd_data, 0, cmd_size, NULL) != FR_OK)) { + free(cmd_data); + return 1; + } + + // if we end up here, everything is fine + free(cmd_data); + return 0; +} + u32 RecursiveFixFileCmacWorker(char* path) { FILINFO fno; DIR pdir; diff --git a/arm9/source/utils/nandcmac.h b/arm9/source/utils/nandcmac.h index 70a6521..e509c84 100644 --- a/arm9/source/utils/nandcmac.h +++ b/arm9/source/utils/nandcmac.h @@ -3,7 +3,9 @@ #include "common.h" #define ReadFileCmac(path, cmac) ReadWriteFileCmac(path, cmac, false) -#define WriteFileCmac(path, cmac) ReadWriteFileCmac(path, cmac, true) +#define WriteFileCmac(path, cmac) ReadWriteFileCmac(path, cmac, true) +#define CheckCmdCmac(path) CheckFixCmdCmac(path, false) +#define FixCmdCmac(path) CheckFixCmdCmac(path, true) u32 CheckCmacPath(const char* path); u32 ReadWriteFileCmac(const char* path, u8* cmac, bool do_write); @@ -11,4 +13,5 @@ u32 CalculateFileCmac(const char* path, u8* cmac); u32 CheckFileCmac(const char* path); u32 FixFileCmac(const char* path); u32 FixAgbSaveCmac(void* data, u8* cmac, const char* sddrv); +u32 CheckFixCmdCmac(const char* path, bool fix); u32 RecursiveFixFileCmac(const char* path);