diff --git a/README.md b/README.md index 18a62ca..cff9195 100644 --- a/README.md +++ b/README.md @@ -96,4 +96,5 @@ The output will be found in `build/MinSizeRel/sphaira.nro` - GBATemp - hb-appstore - haze +- nxdumptool (for gamecard bin dumping and rsa verify code) - Everyone who has contributed to this project! diff --git a/sphaira/include/ui/menus/gc_menu.hpp b/sphaira/include/ui/menus/gc_menu.hpp index 362d3b9..3194bb2 100644 --- a/sphaira/include/ui/menus/gc_menu.hpp +++ b/sphaira/include/ui/menus/gc_menu.hpp @@ -15,6 +15,123 @@ typedef enum { FsGameCardStoragePartition_Secure = 1, } FsGameCardStoragePartition; +//////////////////////////////////////////////// +// The below structs are taken from nxdumptool./ +//////////////////////////////////////////////// + +/// Located at offset 0x7000 in the gamecard image. +typedef struct { + u8 signature[0x100]; ///< RSA-2048-PKCS#1 v1.5 with SHA-256 signature over the rest of the data. + u32 magic; ///< "CERT". + u32 version; + u8 kek_index; + u8 reserved[0x7]; + u8 t1_card_device_id[0x10]; + u8 iv[0x10]; + u8 hw_key[0x10]; ///< Encrypted. + u8 data[0xC0]; ///< Encrypted. +} FsGameCardCertificate; + +static_assert(sizeof(FsGameCardCertificate) == 0x200); + +typedef struct { + u8 maker_code; ///< FsCardId1MakerCode. + u8 memory_capacity; ///< Matches GameCardRomSize. + u8 reserved; ///< Known values: 0x00, 0x01, 0x02, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0C, 0x0D, 0x0E, 0x80. + u8 memory_type; ///< FsCardId1MemoryType. +} FsCardId1; + +static_assert(sizeof(FsCardId1) == 0x4); + +typedef struct { + u8 card_security_number; ///< FsCardId2CardSecurityNumber. + u8 card_type; ///< FsCardId2CardType. + u8 reserved[0x2]; ///< Usually filled with zeroes. +} FsCardId2; + +static_assert(sizeof(FsCardId2) == 0x4); + +typedef struct { + u8 reserved[0x4]; ///< Usually filled with zeroes. +} FsCardId3; + +static_assert(sizeof(FsCardId3) == 0x4); + +/// Returned by fsDeviceOperatorGetGameCardIdSet. +typedef struct { + FsCardId1 id1; ///< Specifies maker code, memory capacity and memory type. + FsCardId2 id2; ///< Specifies card security number and card type. + FsCardId3 id3; ///< Always zero (so far). +} FsGameCardIdSet; + +/// Encrypted using AES-128-ECB with the common titlekek generator key (stored in the .rodata segment from the Lotus firmware). +typedef struct { + union { + u8 value[0x10]; + struct { + u8 package_id[0x8]; ///< Matches package_id from GameCardHeader. + u8 reserved[0x8]; ///< Just zeroes. + }; + }; +} GameCardKeySource; + +static_assert(sizeof(GameCardKeySource) == 0x10); + +/// Plaintext area. Dumped from FS program memory. +typedef struct { + GameCardKeySource key_source; + u8 encrypted_titlekey[0x10]; ///< Encrypted using AES-128-CCM with the decrypted key_source and the nonce from this section. + u8 mac[0x10]; ///< Used to verify the validity of the decrypted titlekey. + u8 nonce[0xC]; ///< Used as the IV to decrypt encrypted_titlekey using AES-128-CCM. + u8 reserved[0x1C4]; +} GameCardInitialData; + +static_assert(sizeof(GameCardInitialData) == 0x200); + +typedef struct { + u8 maker_code; ///< GameCardUidMakerCode. + u8 version; ///< TODO: determine whether this matches GameCardVersion or not. + u8 card_type; ///< GameCardUidCardType. + u8 unique_data[0x9]; + u32 random; + u8 platform_flag; + u8 reserved[0xB]; + FsCardId1 card_id_1_mirror; ///< This field mirrors bit 5 of FsCardId1MemoryType. + u8 mac[0x20]; +} GameCardUid; + +static_assert(sizeof(GameCardUid) == 0x40); + +/// Plaintext area. Dumped from FS program memory. +/// Overall structure may change with each new LAFW version. +typedef struct { + u32 asic_security_mode; ///< Determines how the Lotus ASIC initialised the gamecard security mode. Usually 0xFFFFFFF9. + u32 asic_status; ///< Bitmask of the internal gamecard interface status. Usually 0x20000000. + FsCardId1 card_id1; + FsCardId2 card_id2; + GameCardUid card_uid; + u8 reserved[0x190]; + u8 mac[0x20]; ///< Changes with each gamecard (re)insertion. +} GameCardSpecificData; + +static_assert(sizeof(GameCardSpecificData) == 0x200); + +/// Plaintext area. Dumped from FS program memory. +/// This struct is returned by Lotus command "ChangeToSecureMode" (0xF). This means it is only available *after* the gamecard secure area has been mounted. +/// A copy of the gamecard header without the RSA-2048 signature and a plaintext GameCardInfo precedes this struct in FS program memory. +typedef struct { + GameCardSpecificData specific_data; + FsGameCardCertificate certificate; + u8 reserved[0x200]; + GameCardInitialData initial_data; +} GameCardSecurityInformation; + +static_assert(sizeof(GameCardSecurityInformation) == 0x800); + +/////////////////// +// nxdumptool fin./ +/////////////////// + struct GcCollection : yati::container::CollectionEntry { GcCollection(const char* _name, s64 _size, u8 _type, u8 _id_offset) { name = _name; @@ -61,7 +178,7 @@ struct Menu final : MenuBase { private: Result GcPoll(bool* inserted); - Result GcOnEvent(); + Result GcOnEvent(bool force = false); // GameCard FS api. Result GcMount(); @@ -74,6 +191,9 @@ private: void GcUnmountPartition(); Result GcStorageReadInternal(void* buf, s64 off, s64 size, u64* bytes_read); + // taken from nxdumptool. + Result GcGetSecurityInfo(GameCardSecurityInformation& out); + Result LoadControlData(ApplicationEntry& e); Result UpdateStorageSize(); void FreeImage(); @@ -104,9 +224,13 @@ private: s64 m_parition_secure_size{}; s64 m_storage_trimmed_size{}; s64 m_storage_total_size{}; + u64 m_package_id{}; u8 m_initial_data_hash[SHA256_HASH_SIZE]{}; FsGameCardStoragePartition m_partition{FsGameCardStoragePartition_None}; bool m_storage_mounted{}; + + // set when the gc should be re-mounted, cleared when handled. + bool m_dirty{}; }; } // namespace sphaira::ui::menu::gc diff --git a/sphaira/source/ui/menus/gc_menu.cpp b/sphaira/source/ui/menus/gc_menu.cpp index d182392..846dc57 100644 --- a/sphaira/source/ui/menus/gc_menu.cpp +++ b/sphaira/source/ui/menus/gc_menu.cpp @@ -21,6 +21,7 @@ namespace sphaira::ui::menu::gc { namespace { constexpr u32 XCI_MAGIC = std::byteswap(0x48454144); +constexpr u32 REMOUNT_ATTEMPT_MAX = 8; // same as nxdumptool. enum DumpFileType { DumpFileType_XCI, @@ -88,6 +89,17 @@ void utilsReplaceIllegalCharacters(char *str, bool ascii_only) *ptr2 = '\0'; } +struct DebugEventInfo { + u32 event_type; + u32 flags; + u64 thread_id; + u64 title_id; + u64 process_id; + char process_name[12]; + u32 mmu_flags; + u8 _0x30[0x10]; +}; + auto GetDumpTypeStr(u8 type) -> const char* { switch (type) { case DumpFileType_XCI: return ".xci"; @@ -122,10 +134,12 @@ auto BuildXciBasePath(std::span entries) -> fs::FsPath { return path; } +#if 0 // builds path suiteable for usb transfer. auto BuildFilePath(DumpFileType type, std::span entries) -> fs::FsPath { return BuildXciBasePath(entries) + GetDumpTypeStr(type); } +#endif // builds path suiteable for file dumps. auto BuildFullDumpPath(DumpFileType type, std::span entries) -> fs::FsPath { @@ -408,18 +422,19 @@ Menu::Menu() : MenuBase{"GameCard"_i18n} { } Menu::~Menu() { - GcUmountStorage(); GcUnmount(); eventClose(std::addressof(m_event)); fsEventNotifierClose(std::addressof(m_event_notifier)); fsDeviceOperatorClose(std::addressof(m_dev_op)); + nsExit(); } void Menu::Update(Controller* controller, TouchInfo* touch) { // poll for the gamecard first before handling inputs as the gamecard // may have been removed, thus pressing A would fail. - if (R_SUCCEEDED(eventWait(std::addressof(m_event), 0))) { - GcOnEvent(); + if (m_dirty || R_SUCCEEDED(eventWait(std::addressof(m_event), 0))) { + GcOnEvent(m_dirty); + m_dirty = false; } MenuBase::Update(controller, touch); @@ -499,9 +514,9 @@ void Menu::OnFocusGained() { Result Menu::GcMount() { GcUnmount(); - // after storage has been mounted, it will take 2 attempts to mount + // after storage has been mounted, it will take X attempts to mount // the fs, same as mounting storage. - for (int i = 0; i < 2; i++) { + for (u32 i = 0; i < REMOUNT_ATTEMPT_MAX; i++) { R_TRY(fsDeviceOperatorGetGameCardHandle(std::addressof(m_dev_op), std::addressof(m_handle))); m_fs = std::make_unique(std::addressof(m_handle), FsGameCardPartition_Secure, false); if (R_SUCCEEDED(m_fs->GetFsOpenResult())) { @@ -654,29 +669,20 @@ Result Menu::GcMount() { auto options = std::make_shared("Select content to dump"_i18n, Sidebar::Side::RIGHT); ON_SCOPE_EXIT(App::Push(options)); - options->Add(std::make_shared("Dump XCI"_i18n, [this](){ - DumpGames(DumpFileFlag_XCI); - }, true)); - options->Add(std::make_shared("Dump All"_i18n, [this](){ - DumpGames(DumpFileFlag_All); - }, true)); - options->Add(std::make_shared("Dump All Bins"_i18n, [this](){ - DumpGames(DumpFileFlag_AllBin); - }, true)); - options->Add(std::make_shared("Dump Card ID Set"_i18n, [this](){ - DumpGames(DumpFileFlag_Set); - }, true)); - // todo: - // options->Add(std::make_shared("Dump Card UID"_i18n, [this](){ - // DumpGames(DumpFileFlag_UID); - // }, true)); - options->Add(std::make_shared("Dump Certificate"_i18n, [this](){ - DumpGames(DumpFileFlag_Cert); - }, true)); - // todo: - // options->Add(std::make_shared("Dump Initial Data"_i18n, [this](){ - // DumpGames(DumpFileFlag_Initial); - // }, true)); + const auto add = [&](const std::string& name, u32 flags){ + options->Add(std::make_shared(name, [this, flags](){ + DumpGames(flags); + m_dirty = true; + }, true)); + }; + + add("Dump XCI"_i18n, DumpFileFlag_XCI); + add("Dump All"_i18n, DumpFileFlag_All); + add("Dump All Bins"_i18n, DumpFileFlag_AllBin); + add("Dump Card ID Set"_i18n, DumpFileFlag_Set); + add("Dump Card UID"_i18n, DumpFileFlag_UID); + add("Dump Certificate"_i18n, DumpFileFlag_Cert); + add("Dump Initial Data"_i18n, DumpFileFlag_Initial); } } }}); @@ -700,6 +706,8 @@ Result Menu::GcMount() { } void Menu::GcUnmount() { + GcUmountStorage(); + m_fs.reset(); m_entries.clear(); m_entry_index = 0; @@ -722,6 +730,7 @@ Result Menu::GcMountStorage() { u32 trim_size; std::memcpy(&magic, header + 0x100, sizeof(magic)); std::memcpy(&trim_size, header + 0x118, sizeof(trim_size)); + std::memcpy(&m_package_id, header + 0x110, sizeof(m_package_id)); std::memcpy(m_initial_data_hash, header + 0x160, sizeof(m_initial_data_hash)); R_UNLESS(magic == XCI_MAGIC, 0x1); @@ -754,7 +763,7 @@ Result Menu::GcMountPartition(FsGameCardStoragePartition partition) { // the 2nd attempt will succeeded, but qlaunch will fail to mount // the gamecard as it will only attempt to mount once. Result rc; - for (int i = 0; i < 2; i++) { + for (u32 i = 0; i < REMOUNT_ATTEMPT_MAX; i++) { R_TRY(fsDeviceOperatorGetGameCardHandle(&m_dev_op, &m_handle)); if (R_SUCCEEDED(rc = fsOpenGameCardStorage(&m_storage, &m_handle, partition))){ break; @@ -843,11 +852,11 @@ Result Menu::GcPoll(bool* inserted) { R_SUCCEED(); } -Result Menu::GcOnEvent() { +Result Menu::GcOnEvent(bool force) { bool inserted{}; R_TRY(GcPoll(&inserted)); - if (m_mounted != inserted) { + if (force || m_mounted != inserted) { log_write("gc state changed\n"); m_mounted = inserted; if (m_mounted) { @@ -946,6 +955,17 @@ void Menu::OnChangeIndex(s64 new_index) { Result Menu::DumpGames(u32 flags) { R_TRY(GcMountStorage()); + u32 location_flags = dump::DumpLocationFlag_All; + + // if we need to dump any of the bins, read fs memory until we find + // what we are looking for. + // the below code, along with the structs is taken from nxdumptool. + GameCardSecurityInformation security_info; + if ((flags &~ DumpFileFlag_XCI)) { + location_flags &= ~dump::DumpLocationFlag_UsbS2S; + R_TRY(GcGetSecurityInfo(security_info)); + } + auto source = std::make_shared(); source->menu = this; source->application_name = m_entries[m_entry_index].lang_entry.name; @@ -963,34 +983,103 @@ Result Menu::DumpGames(u32 flags) { } if (flags & DumpFileFlag_Set) { - source->id_set.resize(0xC); + source->id_set.resize(sizeof(FsGameCardIdSet)); R_TRY(fsDeviceOperatorGetGameCardIdSet(&m_dev_op, source->id_set.data(), source->id_set.size(), source->id_set.size())); paths.emplace_back(BuildFullDumpPath(DumpFileType_Set, m_entries)); } - // todo: if (flags & DumpFileFlag_UID) { - // paths.emplace_back(BuildFullDumpPath(DumpFileType_UID, m_entries)); + source->uid.resize(sizeof(security_info.specific_data.card_uid)); + std::memcpy(source->uid.data(), &security_info.specific_data.card_uid, source->uid.size()); + paths.emplace_back(BuildFullDumpPath(DumpFileType_UID, m_entries)); } if (flags & DumpFileFlag_Cert) { - s64 size; - source->cert.resize(0x200); - R_TRY(fsDeviceOperatorGetGameCardDeviceCertificate(&m_dev_op, &m_handle, source->cert.data(), source->cert.size(), &size, source->cert.size())); + source->cert.resize(sizeof(security_info.certificate)); + std::memcpy(source->cert.data(), &security_info.certificate, source->cert.size()); paths.emplace_back(BuildFullDumpPath(DumpFileType_Cert, m_entries)); } - // todo: if (flags & DumpFileFlag_Initial) { - // paths.emplace_back(BuildFullDumpPath(DumpFileType_Initial, m_entries)); + source->initial.resize(sizeof(security_info.initial_data)); + std::memcpy(source->initial.data(), &security_info.initial_data, source->initial.size()); + paths.emplace_back(BuildFullDumpPath(DumpFileType_Initial, m_entries)); } - dump::Dump(source, paths, [this](Result rc){ - GcUmountStorage(); - GcUnmount(); - }); + dump::Dump(source, paths, [](Result){}, location_flags); R_SUCCEED(); } +Result Menu::GcGetSecurityInfo(GameCardSecurityInformation& out) { + R_TRY(GcMountPartition(FsGameCardStoragePartition_Secure)); + + constexpr u64 title_id = 0x0100000000000000; // FS + Handle handle{}; + DebugEventInfo event_info{}; + u64 pids[0x50]{}; + s32 process_count{}; + + R_TRY(svcGetProcessList(&process_count, pids, std::size(pids))); + for (s32 i = 0; i < (process_count - 1); i++) { + if (R_SUCCEEDED(svcDebugActiveProcess(&handle, pids[i]))) { + ON_SCOPE_EXIT(svcCloseHandle(handle)); + + if (R_FAILED(svcGetDebugEvent(&event_info, handle)) || title_id != event_info.title_id) { + continue; + } + + const auto package_id = m_package_id; + static u64 addr{}; + MemoryInfo mem_info{}; + u32 page_info{}; + std::vector data{}; + + for (;;) { + R_TRY(svcQueryDebugProcessMemory(&mem_info, &page_info, handle, addr)); + + // if addr=0 then we hit the reserved memory section + addr = mem_info.addr + mem_info.size; + if (!addr) { + break; + } + + // skip memory that we don't want + if (mem_info.attr || !mem_info.size || (mem_info.perm & Perm_Rw) != Perm_Rw || (mem_info.type & MemState_Type) != MemType_CodeMutable) { + continue; + } + + data.resize(mem_info.size); + R_TRY(svcReadDebugProcessMemory(data.data(), handle, mem_info.addr, data.size())); + + for (s64 i = 0; i < data.size(); i += 8) { + if (i + sizeof(out.initial_data) >= data.size()) { + break; + } + + if (!std::memcmp(&package_id, data.data() + i, sizeof(m_package_id))) [[unlikely]] { + log_write("[GC] found the package id\n"); + u8 hash[SHA256_HASH_SIZE]; + sha256CalculateHash(hash, data.data() + i, 0x200); + + if (!std::memcmp(hash, m_initial_data_hash, sizeof(hash))) { + // successive calls will jump to the addr as the location will not change. + addr = mem_info.addr; + log_write("[GC] found the security info\n"); + log_write("\tperm: 0x%X\n", mem_info.perm); + log_write("\ttype: 0x%X\n", mem_info.type & MemState_Type); + log_write("\taddr: 0x%016lX\n", mem_info.addr); + log_write("\toff: 0x%016lX\n", mem_info.addr + i); + std::memcpy(&out, data.data() + i - offsetof(GameCardSecurityInformation, initial_data), sizeof(out)); + R_SUCCEED(); + } + } + } + } + } + } + + R_THROW(0x1); +} + } // namespace sphaira::ui::menu::gc