Laufzeit-Patching: der Angriff, den Ihre Integritätsprüfung nie sieht
Eine Anti-Tamper-Prüfung, die die DLL auf der Festplatte hasht, erkennt eine gepatchte Datei — aber keine zur Laufzeit im Speicher umgeschriebene Methode. Bibliotheken wie Harmony leiten Ihre Methoden um, ohne die Datei zu berühren, und genau so funktionieren moderne Lizenz-Umgehungen und Cheats. Hier steht, wie In-Memory-Patching funktioniert und was es wirklich erkennt.
Die meisten .NET-Anti-Tamper tun eine Sache: Beim Start hashen sie die Assembly und vergleichen den Hash mit einem bekannten guten Wert, sodass die App, falls jemand die DLL bearbeitet hat, um eine Lizenzprüfung zu löschen, es bemerkt und sich weigert zu laufen. Das ist eine echte und nützliche Verteidigung — gegen Datei-Manipulation. Aber es gibt eine ganze Klasse von Angriffen, die sie nicht sehen kann, weil sie die Datei nie berührt. Die Methode wird im Speicher umgeschrieben, nachdem der Runtime sie bereits geladen und per JIT kompiliert hat. Die Bytes auf der Festplatte sind identisch; das Verhalten nicht.
Die Methode, die die Hash-Prüfung nie inspiziert
Eine .NET-Methode durchläuft zwei Formen. Auf der Festplatte ist sie IL in der Assembly — das deckt ein Datei-Hash ab. Zur Laufzeit kompiliert der JIT dieses IL in nativen Maschinencode im Speicher des Prozesses, und dieser native Code ist das, was die CPU tatsächlich ausführt. In-Memory-Patching zielt auf die zweite Form. Es lässt die Methode normal laden und per JIT kompilieren — jede Datei-Prüfung beim Laden besteht — und schreibt dann die kompilierte Methode um oder lenkt ihren Einstiegspunkt um, sodass Aufrufe woanders landen.
Das beliebteste managed Werkzeug dafür ist Harmony. Es ist eine legitime und ausgezeichnete Bibliothek (Spiel-Mods hängen davon ab), aber dieselbe Fähigkeit, die einem Mod erlaubt, das Spielverhalten zu ändern, erlaubt einem Angreifer, eine Lizenzprüfung zu neutralisieren. Ein Harmony-Patch Ihres Gates sieht so aus:
// Attacker's patch assembly, loaded into your process (via a mod loader,
// an injected AppDomain, or a wrapper exe):
[HarmonyPatch(typeof(LicenseGate), nameof(LicenseGate.IsLicensed))]
static class Bypass
{
// A "prefix" runs before your method and can skip it entirely.
static bool Prefix(ref bool __result)
{
__result = true; // force IsLicensed() to return true
return false; // skip the original method body
}
}
var harmony = new Harmony("bypass");
harmony.PatchAll(); // detours LicenseGate.IsLicensed in memory
Nach PatchAll() gibt jeder Aufruf von LicenseGate.IsLicensed() true zurück, ohne dass Ihr Code läuft. Harmony tat dies, indem es die JIT-kompilierte Methode mit einem Umweg zum Prefix umschrieb — keine Datei wurde geändert. Ihre Authenticode-Signatur ist weiterhin gültig. Ihr SHA-256 der DLL stimmt weiterhin. Die Integritätsprüfung beim Laden besteht, und das Gate steht weit offen.
Erkennen Sie das Werkzeug, nicht die Datei
Wenn Sie die Änderung nicht durch Hashen der Datei fangen können, fangen Sie die Bedingungen, die der Angriff braucht. Umschreiben im Speicher in .NET hängt davon ab, dass eines von zwei Dingen in Ihrem Prozess vorhanden ist, und beide sind beim Start mit gewöhnlichem managed Code beobachtbar.
Ein CLR-Profiler. Der Runtime stellt eine Profiling-/Instrumentierungs-API bereit, die IL umschreiben kann, während es per JIT kompiliert wird — die sanktionierte, mächtige Art, managed Methoden umzuleiten. Der Runtime aktiviert sie über Umgebungsvariablen, also ist deren Anwesenheit ein starkes Signal, dass etwas positioniert ist, Ihre Methoden umzuschreiben:
static bool ProfilerAttached() =>
Environment.GetEnvironmentVariable("CORECLR_ENABLE_PROFILING") == "1" ||
Environment.GetEnvironmentVariable("COR_ENABLE_PROFILING") == "1";
Ein im Prozess geladenes Patching-Framework. Harmony muss in Ihrem Prozess sein, um ihn zu patchen, und es wird als eine bestimmte Assembly (0Harmony) ausgeliefert. Sie müssen nicht jede Assembly aufzählen — fragen Sie den Runtime, ob sein Einstiegstyp sich auflöst:
static bool HarmonyLoaded() =>
Type.GetType("HarmonyLib.Harmony, 0Harmony", throwOnError: false) != null
|| Type.GetType("Harmony.HarmonyInstance, 0Harmony", throwOnError: false) != null; // legacy 1.x
Führen Sie beide beim Start aus (im Modul-Initialisierer, damit sie vor Ihrem eigenen Code feuern) und reagieren Sie so, wie Sie auf jedes andere Manipulationssignal reagieren — werfen, beenden oder an Ihren eigenen Handler übergeben. Keine Prüfung ist teuer, und entscheidend sind sie managed und plattformübergreifend, also funktioniert dieselbe Wache unter Windows, Linux, macOS und dem WASM-Runtime.
Was das bringt und was nicht
Seien Sie ehrlich über das Bedrohungsmodell, denn eine Verteidigung zu überverkaufen ist, wie man ein falsches Sicherheitsgefühl bekommt. Diese Prüfungen erhöhen die Kosten einer gelegentlichen Umweg-Umgehung — der Angreifer, der Harmony greift und einen zehnzeiligen Prefix schreibt, muss nun auch einen Detektor besiegen, bevor sein Patch überhaupt läuft. Das stoppt die opportunistische Mehrheit. Sie besiegen keinen entschlossenen Angreifer: Wer die Umgebung kontrolliert, kann die Patcher-Assembly umbenennen, zuerst Ihren Detektor patchen oder einen Profiler anhängen, nach dem die Prüfung nicht sucht. Es gibt keine managed-Code-Prüfung, die ein hinreichend motivierter Angreifer mit voller Kontrolle über die Maschine nicht irgendwann entfernen kann — dieselbe Einschränkung, die für jeden clientseitigen Schutz gilt.
Die richtige Haltung ist Schichtung. Datei-Integritäts-Anti-Tamper fängt den Angriff mit gepatchtem Binary; die Profiler- und Patcher-Erkennung fängt den In-Memory-Umweg-Angriff; und das, was beide schützen — Ihr Lizenz-Gate — sollte selbst schwer zu finden und schwer zu lesen sein, damit ein Angreifer die zu patchende Methode gar nicht erst leicht lokalisieren kann. Ein Gate, vergraben in kontrollfluss-abgeflachtem und virtualisiertem Code, das einen Wert zurückgibt, der in echte Logik einfließt, statt eines sauberen bool IsLicensed(), ist weit schwerer sauber umzuleiten als eine einzelne gut benannte Methode. Die Erkennung sagt Ihnen, dass ein Angriffswerkzeug vorhanden ist; die Obfuskierung macht das Ziel, auf das es zielt, teuer zu erlangen.
Alles zusammengenommen
Wenn Ihre Anti-Tamper-Geschichte lautet „Ich hashe die DLL beim Start”, verteidigen Sie eine Tür und lassen eine andere offen. Der In-Memory-Patch ist die Tür, durch die moderne Lizenz-Umgehungen und Cheats tatsächlich gehen, gerade weil er die Datei — und ihre Signatur — makellos lässt. Fügen Sie beim Start die Erkennung der zwei Dinge hinzu, die ein In-Memory-Patch braucht (einen Profiler-Hook, einen geladenen Patcher), reagieren Sie zu Ihren Bedingungen, und kombinieren Sie das mit Obfuskierung, die das gepatchte Ziel schwer auffindbar macht. Nebula injiziert sowohl die Datei-Integritätsprüfung als auch die Profiler-/Patcher-Erkennung für Sie, sodass der geschützte Build die Tür bewacht, die der Hash nicht sehen kann — ohne dass Sie den Erkennungscode von Hand schreiben.
Nebula.NET testen
Härten Sie Ihren .NET-Code in wenigen Minuten — starten Sie mit der kostenlosen Edition.