Add binary patch to make unalunch work properly with HNAA tmd

This commit is contained in:
Edoardo Lolletti 2025-08-18 00:03:56 +02:00
parent 28dee555e1
commit c4f861b4aa
6 changed files with 163 additions and 65 deletions

View File

@ -48,6 +48,105 @@ protection since it won't interfere with the system. As a bonus, if you
sell/trade you console in the future and the new owner uses the official sell/trade you console in the future and the new owner uses the official
installer, they'll be protected from bricks. installer, they'll be protected from bricks.
## Patches applied to unlaunch
The installer ships with 2 binary patches, one is an "ahestetic" one to enable
the dsi H&S screen and sound.
The other is a mandatory one, required to make unlaunch properly use the tmd that
was installed in the HNAA folder, otherwise it would attempt to save its settings
to random fat blocks, since it assumes that unlaunch is installed in the blocks right
after the title.tmd associated with the launcher read from HWINFO_S.
More specifically, the sound and splash patch modifies the arm7 instruction of unlaunch at
address 0x1308 (in the then relocated code it is run at 0x23fe038). The patched instruction
is a `bl` to the function patching a second binary blob of the launcher, and is replaced with
a nop.
The other patch, modifies the code responsible for reading the launcher title id from HWINFO.
The original code is as follow
```
@ this loads in r0 the buffer containing the contents read
@ from HWINFO_S, offsetted at the position of the title id
FC 05 9F E5 ldr r0, [pc, #0x5fc]
FC 15 9F E5 ldr r1, [pc, #0x5fc]
@ This reads the title id and then stores it to the address
@ pointed by r1, which is then used across the whole program
00 00 90 E5 ldr r0, [r0]
00 00 81 E5 str r0, [r1]
F4 05 9F E5 ldr r0, [pc, #0x5f4]
9d 01 00 EB bl #0x602a3c4
C5 FD FF EB bl #0x602946c
EC 05 9F E5 ldr r0, [pc, #0x5ec]
00 00 90 E5 ldr r0, [r0]
E8 15 9F E5 ldr r1, [pc, #0x5e8]
66 00 00 EB bl #0x6029f00
E4 05 9F E5 ldr r0, [pc, #0x5e4]
E4 15 9F E5 ldr r1, [pc, #0x5e4]
14 20 A0 E3 mov r2, #0x14
0C 09 00 EB bl #0x602c1a8
DC 05 9F E5 ldr r0, [pc, #0x5dc]
91 01 00 EB bl #0x602a3c4
BA FD FF EB bl #0x602946c
D4 05 9F E5 ldr r0, [pc, #0x5d4]
00 00 90 E5 ldr r0, [r0]
D0 15 9F E5 ldr r1, [pc, #0x5d0]
02 2C A0 E3 mov r2, #0x200
5B 00 00 EB bl #0x6029f04
C8 15 9F E5 ldr r1, [pc, #0x5c8]
E4 01 91 E5 ldr r0, [r1, #0x1e4]
72 08 00 EB bl #0x602bf6c
C0 15 9F E5 ldr r1, [pc, #0x5c0]
19 00 00 EB bl #0x6029e10
00 00 A0 E3 mov r0, #0
01 00 C1 E4 strb r0, [r1], #1
B4 05 9F E5 ldr r0, [pc, #0x5b4]
93 01 D0 E5 ldrb r0, [r0, #0x193]
01 00 C1 E4 strb r0, [r1], #1
FF 9F BD E8 pop {r0, r1, r2, r3, r4, r5, r6, r7, r8, sb, sl, fp, ip, pc}
```
The patched code is
```
@ Instead of reading the title id from the HWINFO_S buffer,
@ read the hardcoded value placed at the bottom.
@ all the relative addresses have been incremented by 4 to account
@ for the shift of the instructions (same for the relative bl instructions)
7C 00 9F E5 ldr r0, [pc, #0x7c]
FC 15 9F E5 ldr r1, [pc, #0x5fc]
00 00 81 E5 str r0, [r1]
F8 05 9F E5 ldr r0, [pc, #0x5f8]
9D 01 00 EB bl #0x602a3c4
C6 FD FF EB bl #0x602946c
F0 05 9F E5 ldr r0, [pc, #0x5f0]
00 00 90 E5 ldr r0, [r0]
EC 15 9F E5 ldr r1, [pc, #0x5ec]
67 00 00 EB bl #0x6029f00
E8 05 9F E5 ldr r0, [pc, #0x5e8]
E8 15 9F E5 ldr r1, [pc, #0x5e8]
14 20 A0 E3 mov r2, #0x14
0D 09 00 EB bl #0x602c1a8
E0 05 9F E5 ldr r0, [pc, #0x5e0]
92 01 00 EB bl #0x602a3c4
BB FD FF EB bl #0x602946c
D8 05 9F E5 ldr r0, [pc, #0x5d8]
00 00 90 E5 ldr r0, [r0]
D4 15 9F E5 ldr r1, [pc, #0x5d4]
02 2C A0 E3 mov r2, #0x200
5C 00 00 EB bl #0x6029f04
CC 15 9F E5 ldr r1, [pc, #0x5cc]
E8 01 91 E5 ldr r0, [r1, #0x1e8]
73 08 00 EB bl #0x602bf6c
C4 15 9F E5 ldr r1, [pc, #0x5c4]
1A 00 00 EB bl #0x6029e10
00 00 A0 E3 mov r0, #0
01 00 C1 E4 strb r0, [r1], #1
B8 05 9F E5 ldr r0, [pc, #0x5b8]
97 01 D0 E5 ldrb r0, [r0, #0x197]
01 00 C1 E4 strb r0, [r1], #1
FF 9F BD E8 pop {r0, r1, r2, r3, r4, r5, r6, r7, r8, sb, sl, fp, ip, pc}
41 41 4E 48 .word 0x484e4141
```
The patched code is found at address `0x60a7` in the decompressed binary, and is loaded into
ram at address `0x6029d38`.
The lzss compressed arm7 payload is found at offset `0x8580` for unlaunch 2.0 with a length of `0x67FD`.
## Credits ## Credits
- [DevkitPro](https://devkitpro.org/): devkitARM and libnds - [DevkitPro](https://devkitpro.org/): devkitARM and libnds
- [Martin Korth (nocash)](https://problemkaputt.de): - [Martin Korth (nocash)](https://problemkaputt.de):

View File

@ -90,7 +90,7 @@ static void setupScreens()
static int mainMenu(const consoleInfo& info, int cursor) static int mainMenu(const consoleInfo& info, int cursor)
{ {
const auto tidPatchesSupported = (foundUnlaunchInstallerVersion == v1_9 || foundUnlaunchInstallerVersion == v2_0) && isLauncherVersionSupported; const auto tidPatchesSupported = (foundUnlaunchInstallerVersion == v2_0) && isLauncherVersionSupported;
//top screen //top screen
clearScreen(&topScreen); clearScreen(&topScreen);
@ -352,12 +352,13 @@ void loadUnlaunchInstaller() {
} }
void loadUnlaunchInstallerPatch() { void loadUnlaunchInstallerPatch() {
if (fileExists("sd:/unlaunch-patch.bin")) { if (fileExists("sd:/sound-and-splash-patch.bin")) {
splashSoundBinaryPatchPath = "sd:/unlaunch-patch.bin"; splashSoundBinaryPatchPath = "sd:/sound-and-splash-patch.bin";
} else if(fileExists("nitro:/sound-and-splash-patch.bin")) {
splashSoundBinaryPatchPath = "nitro:/sound-and-splash-patch.bin";
} }
else if(fileExists("nitro:/unlaunch-patch.bin")) if(!fileExists("nitro:/force-hnaa-patch.bin")) {
{ throw std::runtime_error(std::format("Failed to find hnaa patch ({})", "nitro:/force-hnaa-patch.bin"));
splashSoundBinaryPatchPath = "nitro:/unlaunch-patch.bin";
} }
} }
@ -657,7 +658,7 @@ void doMainMenu(consoleInfo& info) {
{ {
break; break;
} }
if(advancedOptionsUnlocked && (foundUnlaunchInstallerVersion == v1_9 || foundUnlaunchInstallerVersion == v2_0)) { if(advancedOptionsUnlocked && (foundUnlaunchInstallerVersion == v2_0)) {
disableAllPatches = !disableAllPatches; disableAllPatches = !disableAllPatches;
} }
break; break;
@ -700,11 +701,12 @@ int main(int argc, char **argv)
} }
loadUnlaunchInstaller(); loadUnlaunchInstaller();
loadUnlaunchInstallerPatch();
consoleInfo info;
try { try {
loadUnlaunchInstallerPatch();
consoleInfo info;
retrieveInstalledLauncherInfo(info); retrieveInstalledLauncherInfo(info);
checkNocashFooter(info); checkNocashFooter(info);

View File

@ -30,19 +30,19 @@ constexpr std::array knownUnlaunchHashes{
/*"0525b28cc59b6f7fc00ad592aebadd7257bf7efb"_sha1, // v1.5: 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*/ /*"9470a51fde188235052b119f6bfabf6689cb2343"_sha1, // v1.6: blacklisted, doesn't like this install method*/
/*"672c11eb535b97b0d32ff580d314a2ad6411d5fe"_sha1, // v1.7: blacklisted, doesn't like this install method*/ /*"672c11eb535b97b0d32ff580d314a2ad6411d5fe"_sha1, // v1.7: blacklisted, doesn't like this install method*/
"b76c2b1722e769c6c0b4b3d4bc73250e41993229"_sha1, // v1.8 /*"b76c2b1722e769c6c0b4b3d4bc73250e41993229"_sha1, // v1.8: blacklisted, the HNAA patch is only done for 2.0 */
"f3eb41cba136a3477523155f8b05df14917c55f4"_sha1, // v1.9 /*"f3eb41cba136a3477523155f8b05df14917c55f4"_sha1, // v1.9: blacklisted, the HNAA patch is only done for 2.0 */
"15f4a36251d1408d71114019b2825fe8f5b4c8cc"_sha1, // v2.0 "15f4a36251d1408d71114019b2825fe8f5b4c8cc"_sha1, // v2.0
}; };
constexpr std::array gifOffsets{ constexpr std::array gifOffsets{
std::make_pair(0x48d4, 0x8540), /* 1.8 */ /* std::make_pair(0x48d4, 0x8540),*/ /* 1.8 */
std::make_pair(0x48c8, 0x8534), /* 1.9 */ /* std::make_pair(0x48c8, 0x8534),*/ /* 1.9 */
std::make_pair(0x48f0, 0x855c), /* 2.0 */ std::make_pair(0x48f0, 0x855c), /* 2.0 */
}; };
constexpr std::array blockAllPatchesOffset{ constexpr std::array blockAllPatchesOffset{
0xae74, /* 1.9 */ /* 0xae74, */ /* 1.9 */
0xae91, /* 2.0 */ 0xae91, /* 2.0 */
}; };
@ -476,55 +476,56 @@ static bool patchCustomBackground(const char* customBackgroundPath)
return true; 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, const char* customBackgroundPath) static bool patchUnlaunchInstaller(bool disableAllPatches, const char* splashSoundBinaryPatchPath, const char* customBackgroundPath)
{ {
tonccpy(unlaunchInstallerBuffer, ogUnlaunchInstallerBuffer, sizeof(unlaunchInstallerBuffer)); tonccpy(unlaunchInstallerBuffer, ogUnlaunchInstallerBuffer, sizeof(unlaunchInstallerBuffer));
if(disableAllPatches) if (splashSoundBinaryPatchPath)
{ {
if(installerVersion == v1_8) iprintf("Applying splash and sound patch\n");
{ if(!applyBinaryPatch(splashSoundBinaryPatchPath))
messageBox("\x1B[31mError:\x1B[33m Unlaunch 1.8 can't be patched\n"); {
return false; return false;
} }
// change launcher TID from ANH to SAN so that unlaunch doesn't realize it's booting the launcher } else {
auto patchOffset = blockAllPatchesOffset[installerVersion - 1]; if(disableAllPatches) {
const char newID[]{'S','A','N'}; // change launcher TID from ANH to SAN so that unlaunch doesn't realize it's booting the launcher
memcpy((unlaunchInstallerBuffer + 520) + patchOffset, newID, 3); auto patchOffset = blockAllPatchesOffset[installerVersion];
} const char newID[]{'S','A','N'};
else if (splashSoundBinaryPatchPath) memcpy((unlaunchInstallerBuffer + 520) + patchOffset, newID, 3);
{ }
if(installerVersion != v2_0) iprintf("Applying HNAA patch\n");
{ if(!applyBinaryPatch("nitro:/force-hnaa-patch.bin"))
messageBox("\x1B[31mError:\x1B[33m The splash and sound patch is\n" {
"only for unlaunch 2.0\n"); return false;
return false; }
} }
static constexpr auto patchOffset = 0x8580;
static constexpr auto patchSectionSize = 0x67FD;
auto* patch = fopen(splashSoundBinaryPatchPath, "rb");
if(!patch)
{
messageBox("\x1B[31mError:\x1B[33m Failed to open the splash and\n"
"sound patch is.\n");
return false;
}
auto patchSize = getFileSize(patch);
if(patchSize > patchSectionSize)
{
messageBox("\x1B[31mError:\x1B[33m Splash and sound patch is too\n"
"big.\n");
fclose(patch);
return false;
}
if (fread((unlaunchInstallerBuffer + 520) + patchOffset, 1, patchSize, patch) != patchSize)
{
messageBox("\x1B[31mError:\x1B[33m Failed to read splash and sound\n"
"patch.\n");
fclose(patch);
return false;
}
fclose(patch);
}
if(!patchCustomBackground(customBackgroundPath)) if(!patchCustomBackground(customBackgroundPath))
{ {
return false; return false;
@ -543,8 +544,6 @@ UNLAUNCH_VERSION loadUnlaunchInstaller(std::string_view path)
} }
std::array unlaunchVersionStrings{ std::array unlaunchVersionStrings{
"v1.8",
"v1.9",
"v2.0", "v2.0",
"INVALID", "INVALID",
}; };

View File

@ -5,8 +5,6 @@
#include "consoleInfo.h" #include "consoleInfo.h"
typedef enum UNLAUNCH_VERSION { typedef enum UNLAUNCH_VERSION {
v1_8,
v1_9,
v2_0, v2_0,
INVALID, INVALID,
} UNLAUNCH_VERSION; } UNLAUNCH_VERSION;

Binary file not shown.