// Erzeugt und verwaltet die MBMNET-Patches. // // Gepatcht werden zwei Systemmodule: // Freunde (0004013000003202): https://nasc.nintendowifi.net -> http:// // Konto (0004013000003802): https://account.nintendo.net -> http:// // // Ablauf je Modul: // 1. .code lesen (direkt aus dem NAND, sonst aus einem Dump auf der SD) // 2. falls komprimiert: entpacken (Luma patcht die entpackte Version) // 3. alle Nintendo-Adressen suchen und durch die MBMNET-Adresse ersetzen // 4. als IPS-Patch nach /luma/titles//code.ips schreiben // // Am NAND wird nie etwas verändert. Luma3DS wendet die Patches nur beim Laden an. #include <3ds.h> #include #include #include #include #include #include "blz.h" #include "patcher.h" #include "lumaconfig.h" #include "olvpatch.h" #include "nnid.h" #define SYSMODULE_DIR "sdmc:/luma/sysmodules" #define MAX_HITS 8 // Boot-Splash: Luma zeigt beim Einschalten "MBMNET aktiv" (Bild in splash_data.c) #define SPLASH_FILE "sdmc:/luma/splash.bin" #define SPLASH_BACKUP "sdmc:/luma/splash.bin.vor-mbmnet" #define SPLASH_SIZE (400 * 240 * 3) // Zwischenspeicher: fertige Patches und der Splash. Damit ist Umschalten nur noch Umbenennen und // zwei kleine Dateien schreiben, statt die Module aus dem NAND zu lesen, zu entpacken und zu durchsuchen. // Ein Patch gilt nur, solange Modul (Größe + Ende der .code), Server und Regeln gleich sind. #define CACHE_DIR APP_DIR "/cache" #define SPLASH_CACHE CACHE_DIR "/splash-" APP_VERSION ".bin" #define RULES_VERSION "1" extern const u8 splash_rle[]; extern const u32 splash_rle_size; // Eine Ersetzungsregel. Es gibt zwei Arten: // PREFIX: Text beginnt mit needle -> needle wird zu "http://", der Rest bleibt // ("https://nasc.nintendowifi.net/ac" -> "http:///ac") // EXACT: Text ist genau needle -> wird zu "http://" + suffix // (für Vorlagen mit Platzhaltern, deren Werte der 3DS dann ins Leere schreibt) typedef enum { RULE_PREFIX, RULE_EXACT } RuleType; typedef struct { RuleType type; const char *needle; const char *suffix; // nur EXACT } Rule; #define MAX_RULES 2 typedef struct { u64 tid; const char *dir; // Luma-Ordner für dieses Modul const char *label; // Name für Meldungen Rule rules[MAX_RULES]; bool optional; // Anwendung statt Systemmodul: nur patchen, wenn installiert und Adresse gefunden bool soft; // Systemmodul, aber verzichtbar: Adresse nicht gefunden = ohne diesen Patch weiter } Target; #define TITLES_DIR "sdmc:/luma/titles" // Umgelenkt wird die normale Nintendo-Umgebung: Alles, was der 3DS sonst zu // nasc.nintendowifi.net und account.nintendo.net schickt, geht an MBMNET. static const Target targets[] = { { 0x0004013000003202ULL, SYSMODULE_DIR, "Freunde", { { RULE_PREFIX, "https://nasc.nintendowifi.net" }, } }, { 0x0004013000003802ULL, SYSMODULE_DIR, "Konto", { // Aktuelle Firmware: die beiden %s sind Zusätze für Nintendos Test-Server (sonst leer), // %u ist die API-Version (immer 1). { RULE_EXACT, "https://%s%saccount.nintendo.net/v%u/api/", "/v1/api/" }, { RULE_PREFIX, "https://account.nintendo.net" }, } }, // SpotPass (BOSS): holt vor jedem Abruf die Policy-Liste von nppl.c.app.nintendowifi.net (abgeschaltet). // Die App stellt die Adresse zwar zur Laufzeit um (boss:P SetPolicyListUrl), nach einem Neustart gilt // aber wieder Nintendos - dann scheitern alle Abrufe im Hintergrund, bis man die App öffnet. Der Patch // macht die MBMNET-Adresse dauerhaft. Verzichtbar: Fehlt die Adresse im Modul, läuft alles andere weiter. { 0x0004013000003402ULL, SYSMODULE_DIR, "SpotPass", { { RULE_PREFIX, "https://nppl.c.app.nintendowifi.net" }, }, false, true }, // Miiverse (JPN, USA, EUR): fragt zuerst den Discovery-Server, wohin es geht, und lädt dann seine Seiten // per HTTPS. Gebaut wird der Patch in olvpatch.c (die Regel hier dient nur der Diagnose). Nur die // installierte Region wird gepatcht. Luma lädt Patches für Anwendungen aus /luma/titles//code.ips (wie Nimbus). { 0x000400300000BC02ULL, TITLES_DIR, "Miiverse", { { RULE_PREFIX, "https://discovery.olv.nintendo.net" } }, true }, { 0x000400300000BD02ULL, TITLES_DIR, "Miiverse", { { RULE_PREFIX, "https://discovery.olv.nintendo.net" } }, true }, { 0x000400300000BE02ULL, TITLES_DIR, "Miiverse", { { RULE_PREFIX, "https://discovery.olv.nintendo.net" } }, true }, }; #define TARGET_COUNT (sizeof(targets) / sizeof(targets[0])) typedef struct { u32 offset; u32 size; // Anzahl Bytes, die geschrieben werden u8 data[160]; } Hit; typedef struct { Hit hits[MAX_HITS]; int count; } Patch; // --- Dateien --------------------------------------------------------------------- static bool file_exists(const char *path) { struct stat st; return stat(path, &st) == 0; } // Luma3DS lädt IPS-Patches für Systemmodule aus /luma/sysmodules/.ips // (nur wenn "Enable loading external FIRMs and system modules" an ist). // Daneben legen wir einen Merker .mbmnet und ggf. die Sicherung eines fremden Patches ab. static void path_in(char *out, size_t len, const Target *t, const char *file) { // Anwendungen: /luma/titles//code.ips (+ mbmnet.txt, code.ips.vor-mbmnet) if (strcmp(t->dir, TITLES_DIR) == 0) { snprintf(out, len, "%s/%016llX/%s", t->dir, t->tid, file); return; } const char *ext = strcmp(file, "code.ips") == 0 ? ".ips" : strcmp(file, "mbmnet.txt") == 0 ? ".mbmnet" : ".ips.vor-mbmnet"; snprintf(out, len, "%s/%016llX%s", t->dir, t->tid, ext); } // Bis v2.2.0 lagen die Patches unter /luma/titles//code.ips - dort wendet Luma sie für // Systemmodule aber nicht an. Alte Dateien entfernen und gesicherte fremde Patches zurückholen. bool patch_migrate(void) { bool migrated = false; for (size_t i = 0; i < TARGET_COUNT; i++) { if (targets[i].optional) continue; // Anwendungen gehören nach /luma/titles char dir[64], ips[96], marker[96], backup[96]; snprintf(dir, sizeof(dir), "sdmc:/luma/titles/%016llX", targets[i].tid); snprintf(ips, sizeof(ips), "%s/code.ips", dir); snprintf(marker, sizeof(marker), "%s/mbmnet.txt", dir); snprintf(backup, sizeof(backup), "%s/code.ips.vor-mbmnet", dir); if (!file_exists(marker)) continue; remove(ips); remove(marker); if (file_exists(backup)) rename(backup, ips); migrated = true; } return migrated; } static void mkdirs(const char *path) { char tmp[256]; snprintf(tmp, sizeof(tmp), "%s", path); for (char *p = tmp + strlen("sdmc:/"); *p; p++) { if (*p == '/') { *p = 0; mkdir(tmp, 0777); *p = '/'; } } mkdir(tmp, 0777); } PatchState patch_state(void) { char path[128]; bool foreign = false; for (size_t i = 0; i < TARGET_COUNT; i++) { path_in(path, sizeof(path), &targets[i], "mbmnet.txt"); if (file_exists(path)) return STATE_MBMNET; path_in(path, sizeof(path), &targets[i], "code.ips"); if (!targets[i].optional && file_exists(path)) foreign = true; } return foreign ? STATE_FOREIGN : STATE_ORIGINAL; } // --- .code lesen ------------------------------------------------------------ static Result read_from_nand(u64 tid, u8 **out, u32 *outSize) { u32 archPath[4] = { (u32)tid, (u32)(tid >> 32), MEDIATYPE_NAND, 0 }; // ExeFS-Datei ".code" static const u32 filePath[5] = { 0, 0, 2, 0x646F632E, 0x00000065 }; FS_Path arch = { PATH_BINARY, sizeof(archPath), archPath }; FS_Path file = { PATH_BINARY, sizeof(filePath), filePath }; Handle h; Result rc = FSUSER_OpenFileDirectly(&h, ARCHIVE_SAVEDATA_AND_CONTENT, arch, file, FS_OPEN_READ, 0); if (R_FAILED(rc)) return rc; u64 size = 0; rc = FSFILE_GetSize(h, &size); if (R_SUCCEEDED(rc)) { u8 *buf = malloc(size); u32 read = 0; if (!buf) rc = -1; else rc = FSFILE_Read(h, &read, 0, buf, size); if (R_SUCCEEDED(rc)) { *out = buf; *outSize = read; } else free(buf); } FSFILE_Close(h); return rc; } // Ersatzweise ein Dump von der SD: /3ds/mbmnet/.bin static bool read_from_sd(u64 tid, u8 **out, u32 *outSize) { char path[64]; snprintf(path, sizeof(path), APP_DIR "/%016llX.bin", tid); FILE *f = fopen(path, "rb"); if (!f) return false; fseek(f, 0, SEEK_END); long size = ftell(f); fseek(f, 0, SEEK_SET); u8 *buf = size > 0 ? malloc(size) : NULL; bool ok = buf && fread(buf, 1, size, f) == (size_t)size; fclose(f); if (!ok) { free(buf); return false; } *out = buf; *outSize = size; return true; } static bool load_code(const Target *t, u8 **out, u32 *outSize, char *msg, size_t msglen) { u8 *buf = NULL; u32 size = 0; Result rc = read_from_nand(t->tid, &buf, &size); if (R_FAILED(rc) && t->optional) return false; // nicht installiert (andere Region) if (R_FAILED(rc) && !read_from_sd(t->tid, &buf, &size)) { snprintf(msg, msglen, "%s-Modul nicht lesbar (Fehler %08lX).\n" "Lege einen Dump als /3ds/mbmnet/%016llX.bin\nab (siehe Anleitung).", t->label, (unsigned long)rc, t->tid); return false; } if (blz_looks_compressed(buf, size)) { u32 full = blz_decompressed_size(buf, size); u8 *big = realloc(buf, full); if (!big) { free(buf); snprintf(msg, msglen, "Zu wenig Speicher."); return false; } buf = big; if (!blz_decompress(buf, size)) { free(buf); snprintf(msg, msglen, "%s-Modul konnte nicht entpackt werden.", t->label); return false; } size = full; } *out = buf; *outSize = size; return true; } // --- Zwischenspeicher ------------------------------------------------------------ // Fingerabdruck des Moduls ohne es ganz zu lesen: Größe und die letzten 16 Bytes der .code // (bei komprimierten Modulen stehen dort die Entpack-Angaben). Leer, wenn nicht lesbar. static void fingerprint(u64 tid, char *out, size_t len) { u32 archPath[4] = { (u32)tid, (u32)(tid >> 32), MEDIATYPE_NAND, 0 }; static const u32 filePath[5] = { 0, 0, 2, 0x646F632E, 0x00000065 }; out[0] = 0; Handle h; if (R_FAILED(FSUSER_OpenFileDirectly(&h, ARCHIVE_SAVEDATA_AND_CONTENT, (FS_Path){ PATH_BINARY, sizeof(archPath), archPath }, (FS_Path){ PATH_BINARY, sizeof(filePath), filePath }, FS_OPEN_READ, 0))) return; u64 size = 0; static u8 block[0x400]; u32 got = 0; // Nur ab einer 0x200-Grenze lesen: Ein Lesen mitten im Block (Ende - 16) scheitert bei diesen Dateien u64 start = size; if (R_SUCCEEDED(FSFILE_GetSize(h, &size)) && size >= 16) { start = (size - 16) & ~0x1FFULL; if (R_SUCCEEDED(FSFILE_Read(h, &got, start, block, (u32)(size - start))) && got == size - start) { const u8 *tail = block + got - 16; int n = snprintf(out, len, "%llX-", size); for (int i = 0; i < 16 && n + 2 < (int)len; i++) n += snprintf(out + n, len - n, "%02X", tail[i]); } } FSFILE_Close(h); } static void cache_key(const Target *t, const char *server, char *out, size_t len) { char fp[64]; fingerprint(t->tid, fp, sizeof(fp)); if (fp[0]) snprintf(out, len, "%s|%s|" RULES_VERSION, fp, server); else out[0] = 0; } static u32 cache_load(const Target *t, const char *key, u8 *data, u32 cap) { char path[96], stored[160] = ""; if (!key[0]) return 0; snprintf(path, sizeof(path), CACHE_DIR "/%016llX.key", t->tid); FILE *f = fopen(path, "r"); if (!f) return 0; if (!fgets(stored, sizeof(stored), f)) stored[0] = 0; fclose(f); stored[strcspn(stored, "\r\n")] = 0; if (strcmp(stored, key)) return 0; snprintf(path, sizeof(path), CACHE_DIR "/%016llX.ips", t->tid); f = fopen(path, "rb"); u32 n = f ? fread(data, 1, cap, f) : 0; if (f) fclose(f); return n > 8 && !memcmp(data, "PATCH", 5) && !memcmp(data + n - 3, "EOF", 3) ? n : 0; } static void cache_store(const Target *t, const char *key, const u8 *data, u32 n) { char path[96]; if (!key[0]) return; mkdir(APP_DIR, 0777); mkdir(CACHE_DIR, 0777); snprintf(path, sizeof(path), CACHE_DIR "/%016llX.ips", t->tid); FILE *f = fopen(path, "wb"); bool ok = f && fwrite(data, 1, n, f) == n; if (f) fclose(f); snprintf(path, sizeof(path), CACHE_DIR "/%016llX.key", t->tid); f = ok ? fopen(path, "w") : NULL; if (f) { fprintf(f, "%s\n", key); fclose(f); } } // --- Patch berechnen und schreiben ---------------------------------------------- static bool build_patch(const Target *t, const char *server, Patch *p, char *msg, size_t msglen) { u8 *code; u32 size; if (!load_code(t, &code, &size, msg, msglen)) return false; p->count = 0; for (u32 i = 0; i < size && p->count < MAX_HITS; i++) { for (int r = 0; r < MAX_RULES && t->rules[r].needle; r++) { const Rule *rule = &t->rules[r]; size_t needleLen = strlen(rule->needle); if (i + needleLen >= size || memcmp(code + i, rule->needle, needleLen) != 0) continue; u32 len = strnlen((const char *)code + i, size - i); Hit *h = &p->hits[p->count]; memset(h, 0, sizeof(*h)); if (rule->type == RULE_EXACT && len != needleLen) continue; // Vorlage muss genau passen { char repl[128]; if (rule->type == RULE_EXACT) snprintf(repl, sizeof(repl), "http://%s%s", server, rule->suffix); else snprintf(repl, sizeof(repl), "http://%s%.*s", server, (int)(len - needleLen), (const char *)code + i + needleLen); if (len + 1 > sizeof(h->data) || strlen(repl) > len) { free(code); snprintf(msg, msglen, "Server-Adresse passt nicht in das %s-Modul\n(zu lang).", t->label); return false; } // Ersatz + Nullbytes bis zur Originallänge (inkl. Terminator) h->offset = i; h->size = len + 1; memcpy(h->data, repl, strlen(repl)); } p->count++; i += len; break; } } free(code); if (p->count == 0 && (t->optional || t->soft)) return false; if (p->count == 0) { char diag[64]; patch_diagnose(diag, sizeof(diag)); snprintf(msg, msglen, "%s-Modul: Nintendo-Adresse nicht gefunden.\n" "Diagnose gespeichert: /3ds/mbmnet/diagnose.txt\nBitte an den Entwickler schicken.", t->label); return false; } return true; } // Baut den IPS-Patch im Speicher. Gibt die Länge zurück. static u32 build_ips(const Patch *p, u8 *out, u32 cap) { u32 n = 0; memcpy(out, "PATCH", 5); n = 5; for (int i = 0; i < p->count && n + 5 + p->hits[i].size + 3 <= cap; i++) { const Hit *h = &p->hits[i]; u8 rec[5] = { h->offset >> 16, h->offset >> 8, h->offset, h->size >> 8, h->size }; memcpy(out + n, rec, 5); memcpy(out + n + 5, h->data, h->size); n += 5 + h->size; } memcpy(out + n, "EOF", 3); return n + 3; } // Schreibt den Patch. *changed wird true, wenn sich die Datei dadurch geändert hat // (nur dann muss der 3DS neu starten, denn Luma patcht beim Laden der Module). #define IPS_CAP 2048 // Miiverse-Patch mit CA: ca. 1100 Bytes static bool write_ips(const char *path, const u8 *data, u32 n, bool *changed) { static u8 old[IPS_CAP]; FILE *f = fopen(path, "rb"); size_t oldLen = f ? fread(old, 1, sizeof(old), f) : 0; if (f) fclose(f); if (oldLen == n && memcmp(old, data, n) == 0) return true; // schon genau so vorhanden f = fopen(path, "wb"); bool ok = f && fwrite(data, 1, n, f) == n; if (f) fclose(f); if (ok) *changed = true; return ok; } // Miiverse-Patch (olvpatch.c): eigene Code-Änderungen + MBMNET-CA statt einfacher Text-Regeln. // 0 = nicht installiert, andere Version oder CA nicht vom Server zu holen (dann bleibt ein alter Patch stehen). static u32 build_miiverse(const Target *t, const char *server, u8 *out, u32 cap) { static u8 der[1024]; u32 derLen = 0; char fp[64]; fingerprint(t->tid, fp, sizeof(fp)); // leer = kein Zwischenspeicher, gebaut wird trotzdem if (!olvpatch_fetch_ca(server, der, &derLen)) return 0; // aus dem Zwischenspeicher (aktualisiert das Auffrischen) u32 sum = 2166136261u; // FNV-1a über die CA: neue CA -> neuer Patch for (u32 i = 0; i < derLen; i++) sum = (sum ^ der[i]) * 16777619u; char key[160]; if (fp[0]) snprintf(key, sizeof(key), "%s|%s|olv1|%08lX", fp, server, (unsigned long)sum); else key[0] = 0; u32 n = cache_load(t, key, out, cap); if (n) return n; u8 *code; u32 size; char ignore[8]; if (!load_code(t, &code, &size, ignore, sizeof(ignore))) return 0; bool supported = olvpatch_supported(code, size); free(code); if (!supported) return 0; n = olvpatch_build(server, der, derLen, out, cap); if (n) cache_store(t, key, out, n); return n; } // --- Fremde Patches, die MBMNET stören --------------------------------------------- // Pretendos Patch für das http-Modul fragt bei jeder Verbindung den Freunde-Dienst nach dem // aktiven Konto. Stellt der Freunde-Dienst selbst die Anfrage, warten beide aufeinander und // keine Anfrage kommt bei MBMNET an. Er wird beim Aktivieren beiseitegelegt und beim // Zurücksetzen zurückgeholt. static const u64 conflicting[] = { 0x0004013000002902ULL, // http }; static int park_conflicting(void) { int parked = 0; for (size_t i = 0; i < sizeof(conflicting) / sizeof(conflicting[0]); i++) { char ips[96], backup[112]; snprintf(ips, sizeof(ips), SYSMODULE_DIR "/%016llX.ips", conflicting[i]); snprintf(backup, sizeof(backup), "%s.vor-mbmnet", ips); // Liegt hier ein Patch, ist er fremd (MBMNET legt hier keinen an): immer den aktuellen sichern if (file_exists(ips)) { remove(backup); if (rename(ips, backup) == 0) parked++; } } return parked; } static void restore_conflicting(void) { for (size_t i = 0; i < sizeof(conflicting) / sizeof(conflicting[0]); i++) { char ips[96], backup[112]; snprintf(ips, sizeof(ips), SYSMODULE_DIR "/%016llX.ips", conflicting[i]); snprintf(backup, sizeof(backup), "%s.vor-mbmnet", ips); if (file_exists(backup) && !file_exists(ips)) rename(backup, ips); } } // --- Boot-Splash -------------------------------------------------------------------- static bool install_splash(bool alreadyOurs) { // Eigenen Splash des Nutzers sichern (nur beim ersten Aktivieren) if (!alreadyOurs && file_exists(SPLASH_FILE) && !file_exists(SPLASH_BACKUP)) rename(SPLASH_FILE, SPLASH_BACKUP); if (alreadyOurs && file_exists(SPLASH_FILE)) return true; // Beim Deaktivieren geparkt: nur zurückschieben if (!file_exists(SPLASH_FILE) && file_exists(SPLASH_CACHE) && rename(SPLASH_CACHE, SPLASH_FILE) == 0) return true; u8 *buf = malloc(SPLASH_SIZE); if (!buf) return false; u32 out = 0; for (u32 i = 0; i + 3 < splash_rle_size && out < SPLASH_SIZE; i += 4) { for (u8 n = splash_rle[i]; n > 0 && out + 3 <= SPLASH_SIZE; n--) { memcpy(buf + out, &splash_rle[i + 1], 3); out += 3; } } FILE *f = out == SPLASH_SIZE ? fopen(SPLASH_FILE, "wb") : NULL; bool ok = f && fwrite(buf, 1, SPLASH_SIZE, f) == SPLASH_SIZE; if (f) fclose(f); free(buf); return ok; } static void remove_splash(void) { // Parken statt löschen, damit das nächste Aktivieren ihn nicht neu schreiben muss mkdir(APP_DIR, 0777); mkdir(CACHE_DIR, 0777); remove(SPLASH_CACHE); if (rename(SPLASH_FILE, SPLASH_CACHE) != 0) remove(SPLASH_FILE); if (file_exists(SPLASH_BACKUP)) rename(SPLASH_BACKUP, SPLASH_FILE); } // --- Aktivieren / Deaktivieren ----------------------------------------------- void (*patch_progress)(const char *step); static void progress(const char *what, const Target *t) { if (!patch_progress) return; char line[64]; snprintf(line, sizeof(line), "%s %s-Modul", what, t->label); patch_progress(line); } bool patch_enable(const char *server, bool *changed, char *msg, size_t msglen) { *changed = false; if (!server[0]) { snprintf(msg, msglen, "Bitte zuerst die Server-Adresse eintragen."); return false; } bool alreadyOurs = patch_state() == STATE_MBMNET; // Erst alle Patches bereitlegen (aus dem Zwischenspeicher oder neu berechnet): // Klappt eines nicht, wird gar nichts geschrieben. static u8 ips[TARGET_COUNT][IPS_CAP]; u32 ipsLen[TARGET_COUNT]; for (size_t i = 0; i < TARGET_COUNT; i++) { progress("Patches:", &targets[i]); if (targets[i].optional) { // Miiverse ipsLen[i] = build_miiverse(&targets[i], server, ips[i], IPS_CAP); continue; } char key[160]; cache_key(&targets[i], server, key, sizeof(key)); ipsLen[i] = cache_load(&targets[i], key, ips[i], IPS_CAP); if (ipsLen[i]) continue; static Patch patch; progress("Patches: lese", &targets[i]); if (!build_patch(&targets[i], server, &patch, msg, msglen)) { if (targets[i].optional || targets[i].soft) { ipsLen[i] = 0; msg[0] = 0; continue; } // auslassen return false; } ipsLen[i] = build_ips(&patch, ips[i], IPS_CAP); cache_store(&targets[i], key, ips[i], ipsLen[i]); } bool backedUp = false, appPatched = false; for (size_t i = 0; i < TARGET_COUNT; i++) { const Target *t = &targets[i]; if (!ipsLen[i]) continue; char path[128], marker[128], backup[128], dir[96]; if (t->optional) { snprintf(dir, sizeof(dir), "%s/%016llX", t->dir, t->tid); mkdirs(dir); appPatched = true; } path_in(path, sizeof(path), t, "code.ips"); path_in(marker, sizeof(marker), t, "mbmnet.txt"); path_in(backup, sizeof(backup), t, "code.ips.vor-mbmnet"); mkdirs(t->dir); // Fremden Patch (z. B. Pretendo) sichern, nicht überschreiben. Immer den aktuellen: Eine ältere Sicherung // (z. B. von vor einem Nimbus-Update) würde sonst beim Zurückschalten veraltete Patches herstellen // (bei Pretendo Fehler 002-0122). if (file_exists(path) && !file_exists(marker)) { remove(backup); rename(path, backup); backedUp = true; } if (!write_ips(path, ips[i], ipsLen[i], changed)) { snprintf(msg, msglen, "Patch konnte nicht auf die SD-Karte geschrieben werden."); return false; } FILE *m = fopen(marker, "w"); if (m) { fprintf(m, "MBMNET-Patch aktiv\nServer: %s\n", server); fclose(m); } } if (park_conflicting() > 0) *changed = true; // Neustart nötig, damit das http-Modul neu lädt bool splash = install_splash(alreadyOurs) && lumaconfig_enable_splash(); lumaconfig_enable_modules(); if (appPatched && lumaconfig_enable_game_patching()) *changed = true; snprintf(msg, msglen, "Patches geschrieben.%s%s", backedUp ? "\nAlter Patch wurde gesichert." : "", splash ? "\nLuma zeigt beim Start \"MBMNET aktiv\"." : ""); return true; } // --- Diagnose --------------------------------------------------------------------- static bool has_ci(const char *s, u32 len, const char *word) { size_t wl = strlen(word); for (u32 i = 0; i + wl <= len; i++) { size_t k = 0; while (k < wl && (s[i + k] | 0x20) == word[k]) k++; if (k == wl) return true; } return false; } static bool interesting(const char *s, u32 len) { return has_ci(s, len, "nintendo") || has_ci(s, len, "http") || has_ci(s, len, "nasc") || has_ci(s, len, "account") || has_ci(s, len, "%s") || has_ci(s, len, ".net"); } static void dump_strings(FILE *f, const u8 *code, u32 size) { int lines = 0, total = 0; char buf[256]; // ASCII-Texte for (u32 i = 0; i < size;) { u32 j = i; while (j < size && code[j] >= 0x20 && code[j] < 0x7F) j++; u32 len = j - i; if (len >= 6) total++; if (len >= 4 && interesting((const char *)code + i, len) && lines < 400) { fprintf(f, " %06lX %.*s\n", (unsigned long)i, (int)(len > 200 ? 200 : len), (const char *)code + i); lines++; } i = j + 1; } // UTF-16-Texte (ein Zeichen + Nullbyte) for (u32 i = 0; i + 1 < size;) { u32 j = i, n = 0; while (j + 1 < size && code[j] >= 0x20 && code[j] < 0x7F && code[j + 1] == 0 && n < sizeof(buf) - 1) { buf[n++] = code[j]; j += 2; } if (n >= 6 && interesting(buf, n) && lines < 500) { fprintf(f, " %06lX [UTF-16] %.*s\n", (unsigned long)i, (int)n, buf); lines++; } i = (j > i) ? j + 2 : i + 1; } fprintf(f, " (%d Texte insgesamt, %d passende gezeigt)\n", total, lines); } // Sucht den laufenden Prozess eines Titels (Luma liefert die Titel-ID über Info-Typ 0x10001) bool patch_find_process(u64 tid, u32 *pidOut) { static u32 pids[0x60]; s32 n = 0; if (R_FAILED(svcGetProcessList(&n, pids, 0x60))) return false; for (s32 i = 0; i < n; i++) { Handle h; if (R_FAILED(svcOpenProcess(&h, pids[i]))) continue; s64 t = 0; Result r = svcGetProcessInfo(&t, h, 0x10001); svcCloseHandle(h); if (R_SUCCEEDED(r) && (u64)t == tid) { *pidOut = pids[i]; return true; } } return false; } // Liest aus dem laufenden Modul, welche Adresse dort wirklich im Speicher steht. // So sieht man, ob Luma den Patch beim Laden angewendet hat. static void dump_runtime(FILE *f, const char *label, u64 tid, u32 offset) { u32 pid = 0; if (!patch_find_process(tid, &pid)) { fprintf(f, " %s: Prozess nicht gefunden\n", label); return; } Handle dbg; Result r = svcDebugActiveProcess(&dbg, pid); if (R_FAILED(r)) { fprintf(f, " %s: kein Zugriff (%08lX)\n", label, (unsigned long)r); return; } DebugEventInfo info; while (R_SUCCEEDED(svcGetProcessDebugEvent(&info, dbg))) if (info.flags & 1) svcContinueDebugEvent(dbg, 0); char buf[64] = { 0 }; r = svcReadProcessMemory(buf, dbg, 0x100000 + offset, 48); svcCloseHandle(dbg); if (R_FAILED(r)) { fprintf(f, " %s: Lesen fehlgeschlagen (%08lX)\n", label, (unsigned long)r); return; } for (int i = 0; i < 48 && buf[i]; i++) if (buf[i] < 0x20 || buf[i] > 0x7E) buf[i] = '.'; fprintf(f, " %s (PID %lu) bei %06lX: %s\n", label, (unsigned long)pid, (unsigned long)offset, buf); } bool patch_diagnose(char *msg, size_t msglen) { mkdirs(APP_DIR); FILE *f = fopen(DIAG_FILE, "w"); if (!f) { snprintf(msg, msglen, "Diagnose konnte nicht gespeichert werden."); return false; } fprintf(f, "MBMNET-Diagnose (App v%s)\n\n", APP_VERSION); for (size_t i = 0; i < TARGET_COUNT; i++) { const Target *t = &targets[i]; u8 *raw = NULL; u32 rawSize = 0; Result rc = read_from_nand(t->tid, &raw, &rawSize); bool fromSd = false; if (R_FAILED(rc)) fromSd = read_from_sd(t->tid, &raw, &rawSize); fprintf(f, "== %s-Modul %016llX\n", t->label, t->tid); if (!raw) { fprintf(f, " nicht lesbar (Fehler %08lX)\n\n", (unsigned long)rc); continue; } bool compressed = blz_looks_compressed(raw, rawSize); fprintf(f, " Quelle: %s, Größe %lu, komprimiert: %s\n", fromSd ? "SD-Dump" : "NAND", (unsigned long)rawSize, compressed ? "ja" : "nein"); free(raw); char err[160]; u8 *code; u32 size; if (!load_code(t, &code, &size, err, sizeof(err))) { fprintf(f, " Fehler: %s\n\n", err); continue; } fprintf(f, " entpackt: %lu Bytes\n", (unsigned long)size); dump_strings(f, code, size); fprintf(f, "\n"); free(code); } // Installierte Patches: liegen sie auf der SD, und was steht drin? fprintf(f, "== Installierte Patches\n"); for (size_t i = 0; i < TARGET_COUNT; i++) { char ips[128]; path_in(ips, sizeof(ips), &targets[i], "code.ips"); FILE *pf = fopen(ips, "rb"); if (!pf) { fprintf(f, " %s: kein Patch (%s)\n", targets[i].label, ips + 5); continue; } static u8 buf[4096]; size_t n = fread(buf, 1, sizeof(buf), pf); fclose(pf); fprintf(f, " %s: %s, %lu Bytes\n", targets[i].label, ips + 5, (unsigned long)n); for (size_t o = 5; o + 5 <= n && memcmp(buf + o, "EOF", 3) != 0;) { u32 off = (buf[o] << 16) | (buf[o + 1] << 8) | buf[o + 2]; u32 len = (buf[o + 3] << 8) | buf[o + 4]; if (o + 5 + len > n) break; fprintf(f, " bei %06lX: %.*s\n", (unsigned long)off, (int)strnlen((char *)buf + o + 5, len), (char *)buf + o + 5); o += 5 + len; } } fprintf(f, "\n"); // Alles in /luma/sysmodules (auch fremde Patches, z. B. von Pretendo) fprintf(f, "== Inhalt von /luma/sysmodules\n"); DIR *dir = opendir(SYSMODULE_DIR); if (dir) { struct dirent *e; while ((e = readdir(dir))) { char full[300]; struct stat est; snprintf(full, sizeof(full), "%s/%s", SYSMODULE_DIR, e->d_name); if (stat(full, &est) == 0) fprintf(f, " %-40s %8ld Bytes\n", e->d_name, (long)est.st_size); } closedir(dir); } else { fprintf(f, " (Ordner fehlt)\n"); } fprintf(f, "\n"); // Konto-Plätze des Kontodienstes (act) fprintf(f, "== Konto-Plätze\n"); nnid_dump(f); fprintf(f, "\n"); // Was steht im ARBEITSSPEICHER der laufenden Module? (zeigt, ob Luma die Patches angewendet hat) fprintf(f, "== Laufende Module (Arbeitsspeicher)\n"); dump_runtime(f, "Freunde", 0x0004013000003202ULL, 0x61279); dump_runtime(f, "Konto ", 0x0004013000003802ULL, 0xE564); fprintf(f, "\n"); // Verbindungstest: so, wie die Systemmodule eine Verbindung aufbauen (mit Nintendo-Zertifikaten) fprintf(f, "== Verbindungstest (wie Freunde-/Konto-Modul)\n"); httpcContext c; Result r = httpcOpenContext(&c, HTTPC_METHOD_GET, "http://mbmnetapi.mbm.wtf:81/app/version", 1); fprintf(f, " Verbindung öffnen (http) %08lX\n", (unsigned long)r); if (R_SUCCEEDED(r)) { fprintf(f, " Nintendo-Root-Zertifikat %08lX\n", (unsigned long)httpcAddDefaultCert(&c, SSLC_DefaultRootCert_Nintendo_CA_G3)); fprintf(f, " Client-Zertifikat %08lX\n", (unsigned long)httpcSetClientCertDefault(&c, SSLC_DefaultClientCert_ClCertA)); r = httpcBeginRequest(&c); u32 status = 0; if (R_SUCCEEDED(r)) httpcGetResponseStatusCodeTimeout(&c, &status, 10ULL * 1000 * 1000 * 1000); fprintf(f, " Anfrage senden %08lX HTTP %lu\n", (unsigned long)r, (unsigned long)status); httpcCloseContext(&c); } fprintf(f, "\n"); // Boot-Splash und Luma-Einstellungen fprintf(f, "== Boot-Splash / Luma-Einstellungen\n"); struct stat st; if (stat(SPLASH_FILE, &st) == 0) fprintf(f, " /luma/splash.bin: %ld Bytes%s\n", (long)st.st_size, st.st_size == SPLASH_SIZE ? " (richtig)" : " (FALSCHE GRÖSSE, Luma erwartet 288000)"); else fprintf(f, " /luma/splash.bin: fehlt\n"); FILE *cfg = fopen("sdmc:/luma/config.ini", "r"); if (cfg) { char line[160]; while (fgets(line, sizeof(line), cfg)) { if (strstr(line, "splash_position") || strstr(line, "splash_duration") || strstr(line, "config_version") || strstr(line, "enable_external_firm_and_modules =")) fprintf(f, " config.ini: %s", line); } fclose(cfg); } else { fprintf(f, " /luma/config.ini: fehlt (ältere Luma-Version? Splash dann im Luma-Menü einschalten)\n"); } fprintf(f, "\n"); // Kontospeicher: Darf die App Systemspeicher öffnen? Scheitert nur der eines laufenden Dienstes // (act, frd), ist er belegt; scheitern alle, fehlt eine Berechtigung. fprintf(f, "== Systemspeicher öffnen (für \"Anmeldung merken\")\n"); static const struct { u32 id; const char *name; } saves[] = { { 0x00010038, "act (Kontodienst, läuft)" }, { 0x00010032, "frd (Freunde-Dienst, läuft)" }, { 0x00010017, "cfg (Systemeinstellungen-Modul, läuft)" }, { 0x00020082, "HOME-Menü (System-App)" }, { 0x00020086, "Systemeinstellungen (System-App, läuft nicht)" }, }; for (size_t i = 0; i < sizeof(saves) / sizeof(saves[0]); i++) { u32 path[2] = { MEDIATYPE_NAND, saves[i].id }; FS_Archive arch; Result r = FSUSER_OpenArchive(&arch, ARCHIVE_SYSTEM_SAVEDATA, (FS_Path){ PATH_BINARY, sizeof(path), path }); int files = -1; if (R_SUCCEEDED(r)) { Handle dir; if (R_SUCCEEDED(FSUSER_OpenDirectory(&dir, arch, fsMakePath(PATH_ASCII, "/")))) { FS_DirectoryEntry e[16]; u32 n = 0; files = R_SUCCEEDED(FSDIR_Read(dir, &n, 16, e)) ? (int)n : -2; FSDIR_Close(dir); } FSUSER_CloseArchive(arch); } fprintf(f, " %08lX %-44s %08lX%s", (unsigned long)saves[i].id, saves[i].name, (unsigned long)r, R_SUCCEEDED(r) ? " OK" : ""); if (files >= 0) fprintf(f, ", %d Einträge", files); fprintf(f, "\n"); } fprintf(f, " Als %s gestartet\n", envIsHomebrew() ? "3dsx (Homebrew Launcher)" : "CIA"); fprintf(f, "\n"); fclose(f); snprintf(msg, msglen, "Diagnose gespeichert:\n/3ds/mbmnet/diagnose.txt\nBitte an den Entwickler schicken."); return true; } bool patch_disable(char *msg, size_t msglen) { PatchState st = patch_state(); if (st != STATE_MBMNET) { snprintf(msg, msglen, st == STATE_FOREIGN ? "MBMNET ist nicht aktiv.\nDer vorhandene fremde Patch bleibt unverändert." : "Schon im Originalzustand."); return false; } remove_splash(); restore_conflicting(); lumaconfig_restore_splash(); lumaconfig_restore_modules(); lumaconfig_restore_game_patching(); bool restored = false; for (size_t i = 0; i < TARGET_COUNT; i++) { const Target *t = &targets[i]; char ips[128], marker[128], backup[128]; path_in(ips, sizeof(ips), t, "code.ips"); path_in(marker, sizeof(marker), t, "mbmnet.txt"); path_in(backup, sizeof(backup), t, "code.ips.vor-mbmnet"); if (!file_exists(marker)) continue; remove(ips); remove(marker); if (file_exists(backup)) { rename(backup, ips); restored = true; } } snprintf(msg, msglen, restored ? "Vorheriger Patch wiederhergestellt." : "Patches entfernt."); return true; }