#include "message.h" #include "sha1digest.h" #include "storage.h" #include "tonccpy.h" #include "unlaunch.h" #include #include #include #include #include #include #include static char unlaunchInstallerBuffer[0x30000]; static char ogUnlaunchInstallerBuffer[0x30000]; static const char* hnaaTmdPath = "nand:/title/00030017/484e4141/content/title.tmd"; static const char* hnaaBackupTmdPath = "nand:/title/00030017/484e4141/content/title.tmd.bak"; UNLAUNCH_VERSION installerVersion{INVALID}; size_t unlaunchInstallerSize{}; constexpr std::array knownUnlaunchHashes{ /*"9e6a8d95062533dfc422362f99ff3e24e7de9920"_sha1, // v0.8: blacklisted, doesn't like this install method*/ /*"fb0d0ffebda67b786f608bf5cbcb2efee6ab42bb"_sha1, // v0.9: blacklisted, doesn't like this install method*/ /*"d710ff585e321082b33456dd4e0568200c9adcc7"_sha1, // v1.0: blacklisted, doesn't like this install method*/ /*"4f3e455e0a752d35a219a3ff10ba14a6c98bff13"_sha1, // v1.1: blacklisted, doesn't like this install method*/ /*"25db1a47ba84748f911d9f4357bfc417533121c7"_sha1, // v1.2: blacklisted, doesn't like this install method*/ /*"068f1d56da02bb4f93fb76d7874e14010c7e7a3d"_sha1, // v1.3: blacklisted, doesn't like this install method*/ /*"43197370de74d302ef7c4420059c3ca7d50c4f3d"_sha1, // v1.4: blacklisted, doesn't like this install method*/ /*"0525b28cc59b6f7fc00ad592aebadd7257bf7efb"_sha1, // v1.5: blacklisted, doesn't like this install method*/ /*"9470a51fde188235052b119f6bfabf6689cb2343"_sha1, // v1.6: blacklisted, doesn't like this install method*/ /*"672c11eb535b97b0d32ff580d314a2ad6411d5fe"_sha1, // v1.7: blacklisted, doesn't like this install method*/ /*"b76c2b1722e769c6c0b4b3d4bc73250e41993229"_sha1, // v1.8: blacklisted, the HNAA patch is only done for 2.0 */ /*"f3eb41cba136a3477523155f8b05df14917c55f4"_sha1, // v1.9: blacklisted, the HNAA patch is only done for 2.0 */ "15f4a36251d1408d71114019b2825fe8f5b4c8cc"_sha1, // v2.0 }; constexpr std::array gifOffsets{ /* std::make_pair(0x48d4, 0x8540),*/ /* 1.8 */ /* std::make_pair(0x48c8, 0x8534),*/ /* 1.9 */ std::make_pair(0x48f0, 0x855c), /* 2.0 */ }; constexpr std::array blockAllPatchesOffset{ /* 0xae74, */ /* 1.9 */ 0xae91, /* 2.0 */ }; static bool writeUnlaunchToHNAAFolder(); bool isValidUnlaunchInstallerSize(size_t size) { return size == 163320 /*1.8*/ || size == 196088 /*1.9, 2.0*/; } static bool removeHnaaLauncher() { auto* errString = [] -> const char* { if(fileExists(hnaaTmdPath)) { if(!toggleFileReadOnly(hnaaTmdPath, false)) { return "\x1B[31mError:\x1B[33m Failed to mark unlaunch's title.tmd as writable\nLeaving as is\n"; } if(!removeIfExists(hnaaTmdPath)) { return "\x1B[31mError:\x1B[33m Failed to delete ulnaunch's title.tmd\n"; } } if(!removeIfExists("nand:/title/00030017/484e4141/content")) { return "\x1B[31mError:\x1B[33m Failed to delete ulnaunch's content folder\n"; } if(!removeIfExists("nand:/title/00030017/484e4141")) { return "\x1B[31mError:\x1B[33m Failed to delete ulnaunch's 484e4141 folder\n"; } return nullptr; }(); if(errString) { messageBox(errString); return false; } return true; } static bool restoreMainTmd(const consoleInfo& info, bool removeHNAABackup) { std::shared_ptr launcherTmdSptr{fopen(info.launcherTmdPath.data(), "r+b"), fclose}; if(!launcherTmdSptr) { messageBox("\x1B[31mError:\x1B[33m Failed to open default launcher's title.tmd\n"); return false; } FILE* launcherTmd = launcherTmdSptr.get(); // If the tmd is patched, assume the HNAA backup is already set in place. if(info.tmdPatched) { fseek(launcherTmd, 0x190, SEEK_SET); // Set back the title.tmd's title id from GNXX to HNXX char c = 0x48; fwrite(&c, 1, 1, launcherTmd); fflush(launcherTmd); } else if(!info.tmdGood || info.tmdInvalid) { // The tmd isn't good, it either has the wrong size, or the hash didn't match // and it wasn't patched with the new method // Install the hnaa backup if not found and then truncate the tmd to 520b // before restoring it if(!info.UnlaunchHNAAtmdFound && !removeHNAABackup) { auto choiceString = [&]{ if(installerVersion != INVALID) return "Unlaunch was installed with the\n" "legacy method.\n" "Before uninstalling it, a\n" "failsafe installation will be\n" "created.\n" "Proceed?"; return "Unlaunch was installed with the\n" "legacy method\n" "But a failsafe installation\n" "cannot be created since no valid\n" "unlaunch installer was provided.\n" "Proceed anyways?"; }(); if(choiceBox(choiceString) == NO) { return false; } if(installerVersion != INVALID) { if(!writeUnlaunchToHNAAFolder()) { if(choiceBox("Failsafe installation couldn't\n" "be copmleted.\n" "Proceed anyways?") == NO) { return false; } } } } if (ftruncate(fileno(launcherTmd), 520) != 0) { messageBox("\x1B[31mError:\x1B[33m Failed to remove unlaunch\n"); return false; } } Sha1Digest digest; calculateFileSha1(launcherTmd, &digest); // the tmd still doesn't match, write a known good one if(digest != info.recoveryTmdDataSha) { fseek(launcherTmd, 0, SEEK_SET); auto written = fwrite(info.recoveryTmdData.data(), info.recoveryTmdData.size(), 1, launcherTmd); if(written != 1) { messageBox("\x1B[31mError:\x1B[33m Failed to remove unlaunch\n"); return false; } } if(removeHNAABackup && info.UnlaunchHNAAtmdFound) { return removeHnaaLauncher(); } return true; } static bool patchMainTmd(const char* path) { FILE* launcherTmd = fopen(path, "r+b"); if(!launcherTmd) { messageBox("\x1B[31mError:\x1B[33m Failed to open default launcher's title.tmd\n"); return false; } // Patches the title.tmd's title id from HNXX to GNXX fseek(launcherTmd, 0x190, SEEK_SET); char c; fread(&c, 1, 1, launcherTmd); //if byte is not already set, it's clean if(c == 0x48) { fseek(launcherTmd, -1, SEEK_CUR); c = 0x47; fwrite(&c, 1, 1, launcherTmd); } else if(c != 0x47) { messageBox("\x1B[31mError:\x1B[33m Default launcher's title.tmd was tamprered with, aborting\n"); fclose(launcherTmd); return false; } fclose(launcherTmd); return true; } static bool restoreProtoTmd(const char* path) { if (!fileExists(hnaaBackupTmdPath)) { messageBox("\x1B[31mError:\x1B[33m No original tmd found!\nCan't uninstall unlaunch.\n"); return false; } removeIfExists(path); rename(hnaaBackupTmdPath, path); toggleFileReadOnly(path, false); return true; } bool uninstallUnlaunch(const consoleInfo& info, bool removeHNAABackup) { // TODO: handle retailLauncherTmdPresentAndToBePatched = false on retail consoles if (info.isRetail) { if(!toggleFileReadOnly(info.launcherTmdPath.data(), false)) { messageBox(std::format("\x1B[31mError:\x1B[33m Failed to make {} writable\n", info.launcherTmdPath).data()); return false; } if(!toggleFileReadOnly(info.launcherAppPath.data(), false)) { messageBox(std::format("\x1B[31mError:\x1B[33m Failed to make {} writable\n", info.launcherAppPath).data()); return false; } if (!restoreMainTmd(info, removeHNAABackup)) { return false; } } else { if (!toggleFileReadOnly(hnaaTmdPath, false) || !restoreProtoTmd(hnaaTmdPath)) { return false; } } return true; } static bool writeUnlaunchTmd(const char* path) { static constexpr auto unlaunchShaOffset = 0x4000; Sha1Digest expectedDigest, actualDigest; swiSHA1Calc(expectedDigest.data(), unlaunchInstallerBuffer + unlaunchShaOffset, (unlaunchInstallerSize + 520) - unlaunchShaOffset); if(calculateFileSha1PathOffset(path, actualDigest.data(), unlaunchShaOffset) && expectedDigest == actualDigest) { // the tmd hasn't changed, no need to do anything return true; } FILE* targetTmd = fopen(path, "wb"); if (!targetTmd) { messageBox("\x1B[31mError:\x1B[33m Failed to open target unlaunch tmd\n"); return false; } if(!writeToFile(targetTmd, unlaunchInstallerBuffer, unlaunchInstallerSize + 520)) { fclose(targetTmd); removeIfExists(path); messageBox("\x1B[31mError:\x1B[33m Failed write unlaunch to tmd\n"); return false; } fclose(targetTmd); if(!calculateFileSha1PathOffset(path, actualDigest.data(), unlaunchShaOffset) || expectedDigest != actualDigest) { removeIfExists(path); messageBox("\x1B[31mError:\x1B[33m Unlaunch tmd was not properly written\n"); return false; } return true; } static bool writeUnlaunchToHNAAFolder() { //Create HNAA launcher folder if (!safeCreateDir("nand:/title/00030017") || !safeCreateDir("nand:/title/00030017/484e4141") || !safeCreateDir("nand:/title/00030017/484e4141/content")) { return false; } // We have to remove write protect otherwise reinstalling will fail. if (fileExists(hnaaTmdPath) && !toggleFileReadOnly(hnaaTmdPath, false)) { messageBox("\x1B[31mError:\x1B[33m Can't remove launcher tmd write protect\n"); return false; } if (!writeUnlaunchTmd(hnaaTmdPath)) { removeHnaaLauncher(); return false; } //Mark the tmd as readonly if(!toggleFileReadOnly(hnaaTmdPath, true)) { messageBox("\x1B[31mError:\x1B[33m Failed to mark unlaunch's title.tmd as read only\n"); removeHnaaLauncher(); return false; } return true; } static bool installUnlaunchRetailConsole(const consoleInfo& info) { if(!writeUnlaunchToHNAAFolder()) return false; //Finally patch the default launcher tmd to be invalid //If there isn't a title.tmd matching the language region in the hwinfo // nothing else has to be done, could be a language patch, or a dev system, the user will know what they have done if (!info.tmdFound) return true; // Set tmd as writable in case unlaunch was already installed through the old method if(!toggleFileReadOnly(info.launcherTmdPath.data(), false) || !patchMainTmd(info.launcherTmdPath.data())) { removeHnaaLauncher(); return false; } if (!toggleFileReadOnly(info.launcherTmdPath.data(), true) || !toggleFileReadOnly(info.launcherAppPath.data(), true)) { messageBox("\x1B[31mError:\x1B[33m Failed to mark default launcher's title.tmd\nas read only, install might be unstable\n"); } return true; } static bool installUnlaunchProtoConsole(void) { if(choiceBox("Your DSi has a non-standard\nregion.\n" "\x1B[31mInstalling unlaunch may be\n" "unsafe.\x1B[33m" "\nCancelling is recommended!" "\n\nContinue anyways?") == NO) return false; // Prototypes DSis are always HNAA. We can't use code that will nuke their launcher. // Also some justification for adding proto support: they're really common. // "Real" protos (X3, X4, etc) are hard to find but there are tons of release // version DSis that are running prototype firmware. // Likely factory rejects that never had production firmware flashed. // We have to remove write protect otherwise reinstalling will fail. if (fileExists(hnaaTmdPath) && !toggleFileReadOnly(hnaaTmdPath, false)) { messageBox("\x1B[31mError:\x1B[33m Can't remove launcher tmd write protect\n"); return false; } bool hnaaBackupExists = fileExists(hnaaBackupTmdPath); // Back up the TMD since we'll be writing to it directly. if (!hnaaBackupExists) { rename(hnaaTmdPath, hnaaBackupTmdPath); // Mark backup tmd as readonly, just to be sure toggleFileReadOnly("nand:/title/00030017/484e4141/content/title.tmd.bak", true); } if(!writeUnlaunchTmd(hnaaTmdPath)) { copyFile("nand:/title/00030017/484e4141/content/title.tmd.bak", hnaaTmdPath); return false; } // Mark the tmd as readonly if (!toggleFileReadOnly(hnaaTmdPath, true)) { // There is nothing that can be done at this point. messageBox("\x1B[31mError:\x1B[33m Failed to mark tmd as read only\n"); } return true; } static bool readUnlaunchInstaller(std::string_view path) { FILE* unlaunchInstaller = fopen(path.data(), "rb"); if (!unlaunchInstaller) { messageBox("\x1B[31mError:\x1B[33m Failed to open unlaunch installer\n"); return false; } unlaunchInstallerSize = getFileSize(unlaunchInstaller); if(!isValidUnlaunchInstallerSize(unlaunchInstallerSize)) { messageBox("\x1B[31mError:\x1B[33m Unlaunch installer wrong file size\n"); return false; } int toRead = unlaunchInstallerSize; auto* buff = unlaunchInstallerBuffer; // Pad the installer with 520 bytes, those being the size of a valid tmd buff += 520; size_t n = 0; while (toRead != 0 && (n = fread(buff, sizeof(uint8_t), toRead, unlaunchInstaller)) > 0) { toRead -= n; buff += n; } if (toRead != 0 || ferror(unlaunchInstaller)) { fclose(unlaunchInstaller); messageBox("\x1B[31mError:\x1B[33m Failed read unlaunch installer\n"); return false; } fclose(unlaunchInstaller); return true; } static bool verifyUnlaunchInstaller(void) { Sha1Digest digest; swiSHA1Calc(digest.data(), unlaunchInstallerBuffer + 520, unlaunchInstallerSize); auto it = std::ranges::find(knownUnlaunchHashes, digest); if(it == knownUnlaunchHashes.end()) { messageBox("\x1B[31mError:\x1B[33m Provided unlaunch installer has an unknown hash\n"); return false; } auto idx = std::distance(knownUnlaunchHashes.begin(), it); installerVersion = static_cast(idx); return true; } static bool patchCustomBackground(std::span customBackground) { auto size = customBackground.size(); if(size == 0) { return true; } if(size < 7 || size > MAX_GIF_SIZE) { messageBox("\x1B[31mError:\x1B[33m Gif file too big.\n"); return false; } const u32 gifSignatureStart = 0x38464947; const u32 gifSignatureEnd = 0x3B000044; auto [gifOffsetStart, gifOffsetEnd] = gifOffsets[installerVersion]; auto* gifStart = reinterpret_cast((unlaunchInstallerBuffer + 520) + gifOffsetStart); auto* gifEnd = reinterpret_cast((unlaunchInstallerBuffer + 520) + gifOffsetEnd); if(*gifStart != gifSignatureStart || *gifEnd != gifSignatureEnd) { messageBox("\x1B[31mError:\x1B[33m Gif offsets not matching.\n"); return false; } std::memcpy(gifStart, customBackground.data(), size); return true; } static bool applyBinaryPatch(const char* path) { static constexpr auto lzssCompressedBinaryOffset = 0x8580; static constexpr auto lzssCompressedBinarySize = 0x67FD; auto* patch = fopen(path, "rb"); if(!patch) { messageBox("\x1B[31mError:\x1B[33m Failed to open the patch.\n"); return false; } auto patchSize = getFileSize(patch); if(patchSize > lzssCompressedBinarySize) { messageBox("\x1B[31mError:\x1B[33m Patch is too big.\n"); fclose(patch); return false; } if (fread((unlaunchInstallerBuffer + 520) + lzssCompressedBinaryOffset, 1, patchSize, patch) != patchSize) { messageBox("\x1B[31mError:\x1B[33m Failed to read patch.\n"); fclose(patch); return false; } fclose(patch); return true; } static bool patchUnlaunchInstaller(bool disableAllPatches, const char* splashSoundBinaryPatchPath, std::span customBackground) { tonccpy(unlaunchInstallerBuffer, ogUnlaunchInstallerBuffer, sizeof(unlaunchInstallerBuffer)); if (splashSoundBinaryPatchPath) { iprintf("Applying splash and sound patch\n"); if(!applyBinaryPatch(splashSoundBinaryPatchPath)) { return false; } } else { if(disableAllPatches) { // change launcher TID from ANH to SAN so that unlaunch doesn't realize it's booting the launcher auto patchOffset = blockAllPatchesOffset[installerVersion]; const char newID[]{'S','A','N'}; memcpy((unlaunchInstallerBuffer + 520) + patchOffset, newID, 3); } iprintf("Applying HNAA patch\n"); if(!applyBinaryPatch("nitro:/force-hnaa-patch.bin")) { return false; } } if(!patchCustomBackground(customBackground)) { return false; } return true; } UNLAUNCH_VERSION loadUnlaunchInstaller(std::string_view path) { if(readUnlaunchInstaller(path) && verifyUnlaunchInstaller()) { tonccpy(ogUnlaunchInstallerBuffer, unlaunchInstallerBuffer, sizeof(unlaunchInstallerBuffer)); return installerVersion; } return INVALID; } std::array unlaunchVersionStrings{ "v2.0", "INVALID", }; static_assert(unlaunchVersionStrings.size() == (INVALID + 1)); const char* getUnlaunchVersionString(UNLAUNCH_VERSION version) { return unlaunchVersionStrings[version]; } bool installUnlaunch(const consoleInfo& info, bool disableAllPatches, const char* splashSoundBinaryPatchPath, std::span customBackground) { if (installerVersion == INVALID || !patchUnlaunchInstaller(disableAllPatches, splashSoundBinaryPatchPath, customBackground)) return false; // Treat protos differently if (!info.isRetail) { return installUnlaunchProtoConsole(); } // Do things normally for production units return installUnlaunchRetailConsole(info); }