forked from Mirror/GodMode9
Improved FIRM detection / verification routines
This commit is contained in:
parent
d2e16c9de5
commit
1ad48969ca
@ -36,3 +36,9 @@ void sha_quick(void* res, const void* src, u32 size, u32 mode) {
|
|||||||
sha_update(src, size);
|
sha_update(src, size);
|
||||||
sha_get(res);
|
sha_get(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int sha_cmp(const void* sha, const void* src, u32 size, u32 mode) {
|
||||||
|
u8 res[0x20];
|
||||||
|
sha_quick(res, src, size, mode);
|
||||||
|
return memcmp(sha, res, 0x20);
|
||||||
|
}
|
||||||
|
@ -26,3 +26,4 @@ void sha_init(u32 mode);
|
|||||||
void sha_update(const void* src, u32 size);
|
void sha_update(const void* src, u32 size);
|
||||||
void sha_get(void* res);
|
void sha_get(void* res);
|
||||||
void sha_quick(void* res, const void* src, u32 size, u32 mode);
|
void sha_quick(void* res, const void* src, u32 size, u32 mode);
|
||||||
|
int sha_cmp(const void* sha, const void* src, u32 size, u32 mode);
|
||||||
|
@ -6,8 +6,8 @@
|
|||||||
|
|
||||||
u32 IdentifyFileType(const char* path) {
|
u32 IdentifyFileType(const char* path) {
|
||||||
const u8 romfs_magic[] = { ROMFS_MAGIC };
|
const u8 romfs_magic[] = { ROMFS_MAGIC };
|
||||||
const u8 firm_magic[] = { FIRM_MAGIC };
|
|
||||||
u8 header[0x200] __attribute__((aligned(32))); // minimum required size
|
u8 header[0x200] __attribute__((aligned(32))); // minimum required size
|
||||||
|
void* data = (void*) header;
|
||||||
size_t fsize = FileGetSize(path);
|
size_t fsize = FileGetSize(path);
|
||||||
char* fname = strrchr(path, '/');
|
char* fname = strrchr(path, '/');
|
||||||
char* ext = (fname) ? strrchr(++fname, '.') : NULL;
|
char* ext = (fname) ? strrchr(++fname, '.') : NULL;
|
||||||
@ -20,42 +20,42 @@ u32 IdentifyFileType(const char* path) {
|
|||||||
return IMG_NAND; // NAND image
|
return IMG_NAND; // NAND image
|
||||||
} else if (ValidateFatHeader(header) == 0) {
|
} else if (ValidateFatHeader(header) == 0) {
|
||||||
return IMG_FAT; // FAT image file
|
return IMG_FAT; // FAT image file
|
||||||
} else if (ValidateMbrHeader((MbrHeader*) (void*) header) == 0) {
|
} else if (ValidateMbrHeader((MbrHeader*) data) == 0) {
|
||||||
MbrHeader* mbr = (MbrHeader*) (void*) header;
|
MbrHeader* mbr = (MbrHeader*) data;
|
||||||
MbrPartitionInfo* partition0 = mbr->partitions;
|
MbrPartitionInfo* partition0 = mbr->partitions;
|
||||||
if ((partition0->sector + partition0->count) <= (fsize / 0x200)) // size check
|
if ((partition0->sector + partition0->count) <= (fsize / 0x200)) // size check
|
||||||
return IMG_FAT; // possibly an MBR -> also treat as FAT image
|
return IMG_FAT; // possibly an MBR -> also treat as FAT image
|
||||||
} else if (ValidateCiaHeader((CiaHeader*) (void*) header) == 0) {
|
} else if (ValidateCiaHeader((CiaHeader*) data) == 0) {
|
||||||
// this only works because these functions ignore CIA content index
|
// this only works because these functions ignore CIA content index
|
||||||
CiaInfo info;
|
CiaInfo info;
|
||||||
GetCiaInfo(&info, (CiaHeader*) header);
|
GetCiaInfo(&info, (CiaHeader*) header);
|
||||||
if (fsize >= info.size_cia)
|
if (fsize >= info.size_cia)
|
||||||
return GAME_CIA; // CIA file
|
return GAME_CIA; // CIA file
|
||||||
} else if (ValidateNcsdHeader((NcsdHeader*) (void*) header) == 0) {
|
} else if (ValidateNcsdHeader((NcsdHeader*) data) == 0) {
|
||||||
NcsdHeader* ncsd = (NcsdHeader*) (void*) header;
|
NcsdHeader* ncsd = (NcsdHeader*) data;
|
||||||
if (fsize >= GetNcsdTrimmedSize(ncsd))
|
if (fsize >= GetNcsdTrimmedSize(ncsd))
|
||||||
return GAME_NCSD; // NCSD (".3DS") file
|
return GAME_NCSD; // NCSD (".3DS") file
|
||||||
} else if (ValidateNcchHeader((NcchHeader*) (void*) header) == 0) {
|
} else if (ValidateNcchHeader((NcchHeader*) data) == 0) {
|
||||||
NcchHeader* ncch = (NcchHeader*) (void*) header;
|
NcchHeader* ncch = (NcchHeader*) data;
|
||||||
u32 type = GAME_NCCH | (NCCH_IS_CXI(ncch) ? FLAG_CXI : 0);
|
u32 type = GAME_NCCH | (NCCH_IS_CXI(ncch) ? FLAG_CXI : 0);
|
||||||
if (fsize >= (ncch->size * NCCH_MEDIA_UNIT))
|
if (fsize >= (ncch->size * NCCH_MEDIA_UNIT))
|
||||||
return type; // NCCH (".APP") file
|
return type; // NCCH (".APP") file
|
||||||
} else if (ValidateExeFsHeader((ExeFsHeader*) (void*) header, fsize) == 0) {
|
} else if (ValidateExeFsHeader((ExeFsHeader*) data, fsize) == 0) {
|
||||||
return GAME_EXEFS; // ExeFS file (false positives possible)
|
return GAME_EXEFS; // ExeFS file (false positives possible)
|
||||||
} else if (memcmp(header, romfs_magic, sizeof(romfs_magic)) == 0) {
|
} else if (memcmp(header, romfs_magic, sizeof(romfs_magic)) == 0) {
|
||||||
return GAME_ROMFS; // RomFS file (check could be better)
|
return GAME_ROMFS; // RomFS file (check could be better)
|
||||||
} else if (strncmp(TMD_ISSUER, (char*) (header + 0x140), 0x40) == 0) {
|
} else if (strncmp(TMD_ISSUER, (char*) (header + 0x140), 0x40) == 0) {
|
||||||
if (fsize >= TMD_SIZE_N(getbe16(header + 0x1DE)))
|
if (fsize >= TMD_SIZE_N(getbe16(header + 0x1DE)))
|
||||||
return GAME_TMD; // TMD file
|
return GAME_TMD; // TMD file
|
||||||
} else if (memcmp(header, firm_magic, sizeof(firm_magic)) == 0) {
|
} else if (ValidateFirmHeader((FirmHeader*) data, fsize) == 0) {
|
||||||
return SYS_FIRM; // FIRM file
|
return SYS_FIRM; // FIRM file
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ((fsize > sizeof(BossHeader)) &&
|
if ((fsize > sizeof(BossHeader)) &&
|
||||||
(ValidateBossHeader((BossHeader*) (void*) header, fsize) == 0)) {
|
(ValidateBossHeader((BossHeader*) data, fsize) == 0)) {
|
||||||
return GAME_BOSS; // BOSS (SpotPass) file
|
return GAME_BOSS; // BOSS (SpotPass) file
|
||||||
} else if ((fsize > sizeof(NcchInfoHeader)) &&
|
} else if ((fsize > sizeof(NcchInfoHeader)) &&
|
||||||
(GetNcchInfoVersion((NcchInfoHeader*) (void*) header)) &&
|
(GetNcchInfoVersion((NcchInfoHeader*) data)) &&
|
||||||
fname && (strncasecmp(fname, NCCHINFO_NAME, 32) == 0)) {
|
fname && (strncasecmp(fname, NCCHINFO_NAME, 32) == 0)) {
|
||||||
return BIN_NCCHNFO; // ncchinfo.bin file
|
return BIN_NCCHNFO; // ncchinfo.bin file
|
||||||
#if PAYLOAD_MAX_SIZE <= TEMP_BUFFER_SIZE
|
#if PAYLOAD_MAX_SIZE <= TEMP_BUFFER_SIZE
|
||||||
|
@ -8,9 +8,23 @@
|
|||||||
// 0 -> pre 9.5 / 1 -> 9.5 / 2 -> post 9.5
|
// 0 -> pre 9.5 / 1 -> 9.5 / 2 -> post 9.5
|
||||||
#define A9L_CRYPTO_TYPE(hdr) ((hdr->k9l[3] == 0xFF) ? 0 : (hdr->k9l[3] == '1') ? 1 : 2)
|
#define A9L_CRYPTO_TYPE(hdr) ((hdr->k9l[3] == 0xFF) ? 0 : (hdr->k9l[3] == '1') ? 1 : 2)
|
||||||
|
|
||||||
u32 ValidateFirmHeader(FirmHeader* header) {
|
u32 ValidateFirmHeader(FirmHeader* header, u32 data_size) {
|
||||||
u8 magic[] = { FIRM_MAGIC };
|
u8 magic[] = { FIRM_MAGIC };
|
||||||
return memcmp(header->magic, magic, sizeof(magic)); // duh
|
if (memcmp(header->magic, magic, sizeof(magic)) != 0)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
u32 firm_size = sizeof(FirmHeader);
|
||||||
|
for (u32 i = 0; i < 4; i++) {
|
||||||
|
FirmSectionHeader* section = header->sections + i;
|
||||||
|
if (!section->size) continue;
|
||||||
|
if (section->offset < firm_size) return 1;
|
||||||
|
firm_size = section->offset + section->size;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((firm_size > FIRM_MAX_SIZE) || (data_size && (firm_size > data_size)))
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
u32 ValidateFirmA9LHeader(FirmA9LHeader* header) {
|
u32 ValidateFirmA9LHeader(FirmA9LHeader* header) {
|
||||||
@ -18,9 +32,7 @@ u32 ValidateFirmA9LHeader(FirmA9LHeader* header) {
|
|||||||
0x0A, 0x85, 0x20, 0x14, 0x8F, 0x7E, 0xB7, 0x21, 0xBF, 0xC6, 0xC8, 0x82, 0xDF, 0x37, 0x06, 0x3C,
|
0x0A, 0x85, 0x20, 0x14, 0x8F, 0x7E, 0xB7, 0x21, 0xBF, 0xC6, 0xC8, 0x82, 0xDF, 0x37, 0x06, 0x3C,
|
||||||
0x0E, 0x05, 0x1D, 0x1E, 0xF3, 0x41, 0xE9, 0x80, 0x1E, 0xC9, 0x97, 0x82, 0xA0, 0x84, 0x43, 0x08
|
0x0E, 0x05, 0x1D, 0x1E, 0xF3, 0x41, 0xE9, 0x80, 0x1E, 0xC9, 0x97, 0x82, 0xA0, 0x84, 0x43, 0x08
|
||||||
};
|
};
|
||||||
u8 hash[0x20];
|
return sha_cmp(enckeyX0x15hash, header->keyX0x15, 0x10, SHA256_MODE);
|
||||||
sha_quick(hash, header->keyX0x15, 0x10, SHA256_MODE);
|
|
||||||
return memcmp(hash, enckeyX0x15hash, 0x20);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FirmSectionHeader* FindFirmArm9Section(FirmHeader* firm) {
|
FirmSectionHeader* FindFirmArm9Section(FirmHeader* firm) {
|
||||||
@ -194,7 +206,7 @@ u32 DecryptFirmSequential(u8* data, u32 offset, u32 size) {
|
|||||||
// fetch firm header from data
|
// fetch firm header from data
|
||||||
if ((offset == 0) && (size >= sizeof(FirmHeader))) {
|
if ((offset == 0) && (size >= sizeof(FirmHeader))) {
|
||||||
memcpy(&firm, data, sizeof(FirmHeader));
|
memcpy(&firm, data, sizeof(FirmHeader));
|
||||||
firmptr = (ValidateFirmHeader(&firm) == 0) ? &firm : NULL;
|
firmptr = (ValidateFirmHeader(&firm, 0) == 0) ? &firm : NULL;
|
||||||
arm9s = (firmptr) ? FindFirmArm9Section(firmptr) : NULL;
|
arm9s = (firmptr) ? FindFirmArm9Section(firmptr) : NULL;
|
||||||
a9lptr = NULL;
|
a9lptr = NULL;
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
#define SECTOR_NAME "sector0x96.bin"
|
#define SECTOR_NAME "sector0x96.bin"
|
||||||
#define SECRET_NAME "secret_sector.bin"
|
#define SECRET_NAME "secret_sector.bin"
|
||||||
|
|
||||||
|
#define FIRM_MAX_SIZE 0x400000 // 4MB, due to FIRM partition size
|
||||||
#define ARM9BIN_OFFSET 0x800
|
#define ARM9BIN_OFFSET 0x800
|
||||||
|
|
||||||
// see: https://www.3dbrew.org/wiki/FIRM#Firmware_Section_Headers
|
// see: https://www.3dbrew.org/wiki/FIRM#Firmware_Section_Headers
|
||||||
@ -42,7 +43,7 @@ typedef struct {
|
|||||||
u8 padding[0x1A0];
|
u8 padding[0x1A0];
|
||||||
} __attribute__((packed, aligned(16))) FirmA9LHeader;
|
} __attribute__((packed, aligned(16))) FirmA9LHeader;
|
||||||
|
|
||||||
u32 ValidateFirmHeader(FirmHeader* header);
|
u32 ValidateFirmHeader(FirmHeader* header, u32 data_size);
|
||||||
u32 ValidateFirmA9LHeader(FirmA9LHeader* header);
|
u32 ValidateFirmA9LHeader(FirmA9LHeader* header);
|
||||||
|
|
||||||
FirmSectionHeader* FindFirmArm9Section(FirmHeader* firm);
|
FirmSectionHeader* FindFirmArm9Section(FirmHeader* firm);
|
||||||
|
@ -471,12 +471,15 @@ u32 VerifyFirmFile(const char* path) {
|
|||||||
FIL file;
|
FIL file;
|
||||||
UINT btr;
|
UINT btr;
|
||||||
|
|
||||||
|
char pathstr[32 + 1];
|
||||||
|
TruncateString(pathstr, path, 32, 8);
|
||||||
|
|
||||||
// open file, get FIRM header
|
// open file, get FIRM header
|
||||||
if (fvx_open(&file, path, FA_READ | FA_OPEN_EXISTING) != FR_OK)
|
if (fvx_open(&file, path, FA_READ | FA_OPEN_EXISTING) != FR_OK)
|
||||||
return 1;
|
return 1;
|
||||||
fvx_lseek(&file, 0);
|
fvx_lseek(&file, 0);
|
||||||
if ((fvx_read(&file, &header, sizeof(FirmHeader), &btr) != FR_OK) ||
|
if ((fvx_read(&file, &header, sizeof(FirmHeader), &btr) != FR_OK) ||
|
||||||
(ValidateFirmHeader(&header) != 0)) {
|
(ValidateFirmHeader(&header, fvx_size(&file)) != 0)) {
|
||||||
fvx_close(&file);
|
fvx_close(&file);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@ -496,8 +499,6 @@ u32 VerifyFirmFile(const char* path) {
|
|||||||
u8 hash[0x20];
|
u8 hash[0x20];
|
||||||
sha_get(hash);
|
sha_get(hash);
|
||||||
if (memcmp(hash, section->hash, 0x20) != 0) {
|
if (memcmp(hash, section->hash, 0x20) != 0) {
|
||||||
char pathstr[32 + 1];
|
|
||||||
TruncateString(pathstr, path, 32, 8);
|
|
||||||
ShowPrompt(false, "%s\nSection %u hash mismatch", pathstr, i);
|
ShowPrompt(false, "%s\nSection %u hash mismatch", pathstr, i);
|
||||||
fvx_close(&file);
|
fvx_close(&file);
|
||||||
return 1;
|
return 1;
|
||||||
@ -505,6 +506,25 @@ u32 VerifyFirmFile(const char* path) {
|
|||||||
}
|
}
|
||||||
fvx_close(&file);
|
fvx_close(&file);
|
||||||
|
|
||||||
|
// check arm11 / arm9 entrypoints
|
||||||
|
int section_arm11 = -1;
|
||||||
|
int section_arm9 = -1;
|
||||||
|
for (u32 i = 0; i < 4; i++) {
|
||||||
|
FirmSectionHeader* section = header.sections + i;
|
||||||
|
if ((header.entry_arm11 >= section->address) &&
|
||||||
|
(header.entry_arm11 < section->address + section->size))
|
||||||
|
section_arm11 = i;
|
||||||
|
if ((header.entry_arm9 >= section->address) &&
|
||||||
|
(header.entry_arm9 < section->address + section->size))
|
||||||
|
section_arm9 = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
// sections for arm11 / arm9 entrypoints not found?
|
||||||
|
if ((section_arm11 < 0) || (section_arm9 < 0)) {
|
||||||
|
ShowPrompt(false, "%s\nARM11/ARM9 entrypoint not found", pathstr);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -625,7 +645,7 @@ u32 CheckEncryptedFirmFile(const char* path) {
|
|||||||
return 1;
|
return 1;
|
||||||
fvx_lseek(&file, 0);
|
fvx_lseek(&file, 0);
|
||||||
if ((fvx_read(&file, &header, sizeof(FirmHeader), &btr) != FR_OK) ||
|
if ((fvx_read(&file, &header, sizeof(FirmHeader), &btr) != FR_OK) ||
|
||||||
(ValidateFirmHeader(&header) != 0)) {
|
(ValidateFirmHeader(&header, fvx_size(&file)) != 0)) {
|
||||||
fvx_close(&file);
|
fvx_close(&file);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@ -714,7 +734,7 @@ u32 CryptNcchNcsdBossFirmFile(const char* orig, const char* dest, u32 mode, u16
|
|||||||
u32 ret = 0;
|
u32 ret = 0;
|
||||||
if (!ShowProgress(offset, fsize, dest)) ret = 1;
|
if (!ShowProgress(offset, fsize, dest)) ret = 1;
|
||||||
if (mode & (GAME_NCCH|GAME_NCSD|GAME_BOSS|SYS_FIRM)) { // for NCCH / NCSD / BOSS / FIRM files
|
if (mode & (GAME_NCCH|GAME_NCSD|GAME_BOSS|SYS_FIRM)) { // for NCCH / NCSD / BOSS / FIRM files
|
||||||
for (u32 i = 0; (i < size) && (ret == 0); i += MAIN_BUFFER_SIZE) {
|
for (u64 i = 0; (i < size) && (ret == 0); i += MAIN_BUFFER_SIZE) {
|
||||||
u32 read_bytes = min(MAIN_BUFFER_SIZE, (size - i));
|
u32 read_bytes = min(MAIN_BUFFER_SIZE, (size - i));
|
||||||
UINT bytes_read, bytes_written;
|
UINT bytes_read, bytes_written;
|
||||||
if (fvx_read(ofp, MAIN_BUFFER, read_bytes, &bytes_read) != FR_OK) ret = 1;
|
if (fvx_read(ofp, MAIN_BUFFER, read_bytes, &bytes_read) != FR_OK) ret = 1;
|
||||||
@ -745,7 +765,7 @@ u32 CryptNcchNcsdBossFirmFile(const char* orig, const char* dest, u32 mode, u16
|
|||||||
GetTmdCtr(ctr, chunk);
|
GetTmdCtr(ctr, chunk);
|
||||||
fvx_lseek(ofp, offset);
|
fvx_lseek(ofp, offset);
|
||||||
sha_init(SHA256_MODE);
|
sha_init(SHA256_MODE);
|
||||||
for (u32 i = 0; (i < size) && (ret == 0); i += MAIN_BUFFER_SIZE) {
|
for (u64 i = 0; (i < size) && (ret == 0); i += MAIN_BUFFER_SIZE) {
|
||||||
u32 read_bytes = min(MAIN_BUFFER_SIZE, (size - i));
|
u32 read_bytes = min(MAIN_BUFFER_SIZE, (size - i));
|
||||||
if (fvx_read(ofp, MAIN_BUFFER, read_bytes, &bytes_read) != FR_OK) ret = 1;
|
if (fvx_read(ofp, MAIN_BUFFER, read_bytes, &bytes_read) != FR_OK) ret = 1;
|
||||||
if (cia_crypto && (DecryptCiaContentSequential(MAIN_BUFFER, read_bytes, ctr, titlekey) != 0)) ret = 1;
|
if (cia_crypto && (DecryptCiaContentSequential(MAIN_BUFFER, read_bytes, ctr, titlekey) != 0)) ret = 1;
|
||||||
@ -818,7 +838,7 @@ u32 DecryptFirmFile(const char* orig, const char* dest) {
|
|||||||
return 1;
|
return 1;
|
||||||
fvx_lseek(&file, 0);
|
fvx_lseek(&file, 0);
|
||||||
if ((fvx_read(&file, &firm, sizeof(FirmHeader), &btr) != FR_OK) ||
|
if ((fvx_read(&file, &firm, sizeof(FirmHeader), &btr) != FR_OK) ||
|
||||||
(ValidateFirmHeader(&firm) != 0)) {
|
(ValidateFirmHeader(&firm, fvx_size(&file)) != 0)) {
|
||||||
fvx_close(&file);
|
fvx_close(&file);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
@ -77,7 +77,7 @@ u32 ValidateNandDump(const char* path) {
|
|||||||
for (u32 i = 0; i < sizeof(firm_sectors) / sizeof(u32); i++) {
|
for (u32 i = 0; i < sizeof(firm_sectors) / sizeof(u32); i++) {
|
||||||
u32 keyslot = 0x06;
|
u32 keyslot = 0x06;
|
||||||
if ((ReadNandFile(&file, &firm, firm_sectors[i], 1, keyslot) != 0) ||
|
if ((ReadNandFile(&file, &firm, firm_sectors[i], 1, keyslot) != 0) ||
|
||||||
(ValidateFirmHeader(&firm) != 0) ||
|
(ValidateFirmHeader(&firm, 0) != 0) ||
|
||||||
(getbe32(firm.dec_magic) != 0)) { // decrypted firms are not allowed
|
(getbe32(firm.dec_magic) != 0)) { // decrypted firms are not allowed
|
||||||
ShowPrompt(false, "%s\nError: FIRM%u header is corrupt", pathstr, i);
|
ShowPrompt(false, "%s\nError: FIRM%u header is corrupt", pathstr, i);
|
||||||
fvx_close(&file);
|
fvx_close(&file);
|
||||||
|
@ -490,7 +490,7 @@ bool OpenVGameDir(VirtualDir* vdir, VirtualFile* ventry) {
|
|||||||
// build directories where required
|
// build directories where required
|
||||||
if ((vdir->flags & VFLAG_FIRM) && (offset_firm != vdir->offset)) {
|
if ((vdir->flags & VFLAG_FIRM) && (offset_firm != vdir->offset)) {
|
||||||
if ((ReadImageBytes((u8*) firm, 0, sizeof(FirmHeader)) != 0) ||
|
if ((ReadImageBytes((u8*) firm, 0, sizeof(FirmHeader)) != 0) ||
|
||||||
(ValidateFirmHeader(firm) != 0)) return false;
|
(ValidateFirmHeader(firm, 0) != 0)) return false;
|
||||||
offset_firm = vdir->offset;
|
offset_firm = vdir->offset;
|
||||||
FirmSectionHeader* arm9s = FindFirmArm9Section(firm);
|
FirmSectionHeader* arm9s = FindFirmArm9Section(firm);
|
||||||
if (arm9s && (ReadImageBytes((u8*) a9l, arm9s->offset, sizeof(FirmA9LHeader)) == 0) &&
|
if (arm9s && (ReadImageBytes((u8*) a9l, arm9s->offset, sizeof(FirmA9LHeader)) == 0) &&
|
||||||
|
Loading…
x
Reference in New Issue
Block a user