MBMNET-App v2.8.2: Quellcode (GPLv3)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nico Meister
2026-10-18 17:05:44 +02:00
co-authored by Claude Opus 5.5
commit d264cd93cd
42 changed files with 7054 additions and 0 deletions
+885
View File
@@ -0,0 +1,885 @@
// Erzeugt und verwaltet die MBMNET-Patches.
//
// Gepatcht werden zwei Systemmodule:
// Freunde (0004013000003202): https://nasc.nintendowifi.net -> http://<server>
// Konto (0004013000003802): https://account.nintendo.net -> http://<server>
//
// 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/<TID>/code.ips schreiben
//
// Am NAND wird nie etwas verändert. Luma3DS wendet die Patches nur beim Laden an.
#include <3ds.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <dirent.h>
#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://<server>", der Rest bleibt
// ("https://nasc.nintendowifi.net/ac" -> "http://<server>/ac")
// EXACT: Text ist genau needle -> wird zu "http://<server>" + 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
} 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" },
} },
// 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/<ID>/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/<Titel-ID>.ips
// (nur wenn "Enable loading external FIRMs and system modules" an ist).
// Daneben legen wir einen Merker <ID>.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/<ID>/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/<ID>/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/<TID>.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) 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];
static u32 derLen;
static int caState; // 0 = noch nicht geholt, 1 = da, -1 = nicht zu holen
char fp[64];
fingerprint(t->tid, fp, sizeof(fp)); // leer = kein Zwischenspeicher, gebaut wird trotzdem
if (caState == 0) caState = olvpatch_fetch_ca(server, der, &derLen) ? 1 : -1;
if (caState < 0) return 0;
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 -----------------------------------------------
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++) {
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;
if (!build_patch(&targets[i], server, &patch, msg, msglen)) {
if (targets[i].optional) { ipsLen[i] = 0; continue; } // nicht installiert: 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;
}