Dynamischer Code
Mit der Codeverschlüsselung lassen sich bestimmte Quellmethoden einer Anwendung verschlüsseln und anschließend austauschen, je nachdem, auf welche Funktionen ein Benutzer Zugriff hat. Hat eine Anwendung zum Beispiel einfache und erweiterte Funktionen, können die verschlüsselten Quellmethoden der erweiterten Funktionen getrennt von der ausführbaren Hauptdatei der Anwendung gespeichert und nur dann zur Laufzeit geladen werden, wenn der Benutzer eine Lizenz hat, die ihm den Zugriff auf diese Funktionen gewährt. Mit dieser dynamischen Codeauswahl können Entwickler eine Demo- oder Testversion ihrer Anwendung mit eingeschränktem Funktionsumfang erstellen und weitere Funktionen freischalten, indem sie den Benutzern einen Lizenzschlüssel geben. Mit der Codeverschlüsselung schützen Entwickler ihr geistiges Eigentum und bieten ihren Benutzern zugleich flexible Lizenzierungsmöglichkeiten.
Dynamischer Code, den die Codeverschlüsselung bereitstellt, erlaubt es, zur Laufzeit zwischen verschiedenen Versionen einer Methode zu wechseln, abhängig von äußeren Bedingungen. Eine Anwendung kann zum Beispiel im Demomodus einen eingeschränkten Funktionsumfang haben, während mit einer gültigen Lizenzdatei erweiterte Funktionen freigeschaltet werden. Dazu wird der Code sowohl der eingeschränkten als auch der erweiterten Version der Funktionen verschlüsselt, und ein Mechanismus der dynamischen Codeauswahl wechselt zwischen ihnen, je nachdem, ob die Lizenzdatei vorhanden ist.
Demoanwendung
Bei der klassischen Umsetzung einer Demoanwendung ist der Code der Vollversion bereits in der Demoversion enthalten, und die Lizenzdatei erlaubt die Ausführung der erweiterten Funktionen. Dieser Ansatz hat jedoch einen erheblichen Nachteil: Der Code, der bei vorhandener Lizenzdatei läuft, ist nicht geschützt, und ein Angreifer könnte die Lizenzvalidierung umgehen, um die lizenzierten Funktionen freizuschalten.
Bei der dynamischen Codeauswahl ist der Code, der die erweiterten Funktionen freischaltet, dagegen verschlüsselt und lässt sich ohne die Lizenzdatei auf keine Weise ausführen. Die Lizenzdatei enthält den Schlüssel, mit dem der Code entschlüsselt und ausgeführt wird. Das schützt besser vor Reverse Engineering und unbefugtem Zugriff auf die erweiterten Funktionen. Die Lizenzdatei dient als eindeutiger Schlüssel, der bestimmte Codeabschnitte freischaltet. Das macht es Angreifern schwer, den Lizenzierungsmechanismus zu umgehen und Zugriff auf die erweiterten Funktionen zu erlangen.
Betrachten Sie das folgende Beispielprojekt, das auf GitHub verfügbar ist. Den Quellcode des Beispiels erhalten Sie mit Git, indem Sie auf der Befehlszeile den folgenden Befehl ausführen:
git clone https://github.com/babelfornet/dynamic-code-example.gitDas Beispiel zeigt, wie Sie mit dynamischem Code das Verhalten der Anwendung davon abhängig machen, ob bestimmte Dateien vorhanden sind.
[Obfuscation(Feature = "msil encryption:id=feature1;source=demo;internal=true;password=1234", Exclude = false)]
public void Feature1Demo()
{
Console.WriteLine("Feature1 Demo");
Feature2Demo();
}
[Obfuscation(Feature = "msil encryption:id=feature2;source=demo;internal=true;password=1234", Exclude = false)]
public static void Feature2Demo()
{
Console.WriteLine("Feature2 Demo");
}Das sind zwei Demomethoden, die so gekennzeichnet sind, dass sie zur Laufzeit über die dynamische Codeauswahl durch andere Versionen der Methoden ersetzt werden. Die Methoden Feature1Demo() und Feature2Demo() sind mit der Codeverschlüsselung verschlüsselt, mit der ID „feature1“ beziehungsweise „feature2“. Als Quelle des verschlüsselten Codes ist „demo“ angegeben, und die Eigenschaft internal ist auf true gesetzt, damit der verschlüsselte Code in der Assembly bleibt. Das Passwort für die Verschlüsselung lautet „1234“.
Wird eine Anwendung ohne gültige Lizenzdatei ausgeführt, gilt sie als Anwendung im Demomodus. In diesem Fall ruft die Assembly die beiden obigen Methoden auf, die so gekennzeichnet sind, dass dynamischer Code sie zur Laufzeit ersetzt. Diese Methoden setzen die Grundfunktionen der Anwendung um, die gegenüber der vollständig lizenzierten Version eingeschränkt sein können.
static void Main(string[] args)
{
var app = new App();
app.Feature1Demo();
}Betrachten Sie nun die folgenden beiden Methoden, die die Demomethoden ersetzen, wenn die Anwendung lizenziert ist. Beachten Sie, dass die Quelle auf „retail“ gesetzt ist. Wird eine Lizenzdatei gefunden, ersetzt die Methode Feature1Retail() mit der ID „feature1“ die Methode Feature1Demo(), und die Methode Feature2Retail() mit der ID „feature2“ ersetzt Feature2Demo().
[Obfuscation(Feature = "msil encryption:id=feature1;source=retail;internal=true;password=4567", Exclude = false)]
public static void Feature1Retail()
{
Console.WriteLine("Feature1 Retail");
Feature2Retail();
}
[Obfuscation(Feature = "msil encryption:id=feature2;source=retail;internal=true;password=4567", Exclude = false)]
public static void Feature2Retail()
{
Console.WriteLine("Feature2 Retail");
}Die beiden obigen Methoden werden zur Laufzeit von der Assembly, die im Demomodus läuft, nicht aufgerufen und sind mit einem anderen Passwort verschlüsselt als die Demomethoden. Dieses Passwort steht dem Anwendungscode nicht zur Verfügung, weil Babel die Information bei der Verschleierung entfernt, nachdem die Methoden verschlüsselt wurden.
Wir brauchen eine Möglichkeit, mit der Codeverschlüsselung dynamisch zwischen „demo“ und „retail“ zu wechseln. Da die obigen Methoden mit einem Passwort verschlüsselt wurden, müssen wir den Einstiegspunkt der Codeverschlüsselung von Babel definieren, über den das Passwort zur Laufzeit abgerufen wird.
[Obfuscation(Feature = "msil encryption get password", Exclude = false)]
internal static string GetPassword(string source)
{
Console.WriteLine($"> Get {source} password");
if (source == "demo")
return "1234";
string pwd = File.ReadAllText(source + ".txt");
Console.WriteLine($">> {source} password: {pwd}");
return pwd;
}Die BVM (Bytecode Virtual Machine) verwendet die Methode GetPassword(), um das Passwort abzurufen, das zum Entschlüsseln einer verschlüsselten Methode nötig ist, wenn diese während der Programmausführung aufgerufen wird.
Lautet der Name der Quelle „demo“, gibt die Methode das fest codierte Passwort „1234“ zurück, mit dem die „demo“-Methoden Feature1Demo() und Feature2Demo() verschlüsselt wurden. Andernfalls liest sie das Passwort aus einer Textdatei, die den Namen der Quelle und die Erweiterung „.txt“ trägt. Das Passwort wird dann an den Aufrufer zurückgegeben. Beachten Sie, dass das Demopasswort im Code gespeichert ist: Die Anwendung kann immer im Demomodus laufen, und es besteht kein Grund, dieses Passwort zu verbergen.
Der Wechsel zwischen den Quellen „demo“ und „retail“ erfolgt in der Methode GetSourceStream(), die die BVM aufruft, wenn eine der verschlüsselten Methoden aufgerufen wird.
[Obfuscation(Feature = "msil encryption get stream", Exclude = false)]
internal static object GetSourceStream(string source)
{
Console.WriteLine($"> Get {source} stream");
if (source == "demo")
{
string[] sources = { "retail", "special" };
foreach (var src in sources)
{
if (File.Exists($"{src}.txt"))
{
Console.WriteLine($">> Switch to {src}");
return src;
}
}
return null;
}
// External sources
if (File.Exists(source + ".eil"))
{
Console.WriteLine($">>> Read {source} stream");
return File.OpenRead(source + ".eil");
}
return null;
}Diese Methode spielt eine entscheidende Rolle dabei, abhängig von äußeren Bedingungen wie dem Vorhandensein einer Lizenzdatei dynamisch die passende Quelle verschlüsselter Methoden auszuwählen. Beim Start ruft die Anwendung nämlich Demomethoden auf, die sich mit dem Passwort entschlüsseln lassen, das die Methode GetPassword() zurückgibt.
Bei der dynamischen Codeauswahl ist die BVM (Binary Virtual Machine) dafür zuständig, den verschlüsselten Code auszuführen und abhängig von äußeren Bedingungen zwischen verschiedenen Versionen einer Methode zu wechseln. Bevor die BVM eine „demo“-Methode ausführt, ruft sie die Methode GetSourceStream() auf und übergibt „demo“ als Parameter. Da die „demo“-Methoden in der Assembly liegen, müssen wir keinen Datenstrom für die Quelle „demo“ zurückgeben. Wird jedoch die Datei „retail.txt“ gefunden, wollen wir zum Quellcode „retail“ wechseln. Dazu teilen wir der BVM mit, dass die Quelle „retail“ ausgeführt werden soll, indem wir aus der Methode GetSourceStream() die Zeichenfolge „retail“ zurückgeben.
Sobald die BVM die Zeichenfolge „retail“ von GetSourceStream() erhält, weiß sie, dass sie den „retail“-Code ausführen muss. Die BVM ruft dann GetPassword() mit dem Argument „retail“ auf. GetPassword() liest das Passwort für die Version „retail“ aus der Datei retail.txt und gibt es an die BVM zurück, die damit den „retail“-Code entschlüsselt. Ist der „retail“-Code entschlüsselt, wird er anstelle des „demo“-Codes ausgeführt. Auf diese Weise kann die Anwendung zur Laufzeit zwischen verschiedenen Versionen des verschlüsselten Codes wechseln, je nachdem, ob eine Lizenzdatei vorhanden ist, ohne dass der Code neu kompiliert oder neu bereitgestellt werden muss.
Dynamischer Code erlaubt es, mehrere interne und externe Quellen zu definieren. Sie können zum Beispiel eine weitere externe Quelle „special“ deklarieren, die anstelle des „retail“-Codes geladen wird, wenn die Datei special.txt mit dem Passwort zum Entschlüsseln des Codes vorhanden ist.
[Obfuscation(Feature = "msil encryption:id=feature1;source=special;internal=false;password=0000", Exclude = false)]
public static void Feature1Special()
{
Console.WriteLine("Feature1 Special");
Feature2Special();
}
[Obfuscation(Feature = "msil encryption:id=feature2;source=special;internal=false;password=0000", Exclude = false)]
public static void Feature2Special()
{
Console.WriteLine("Feature2 Special");
}Der Code zeigt zwei Methoden, Feature1Special() und Feature2Special(), die mit Obfuscation-Attributen gekennzeichnet sind, in denen die Eigenschaft „internal“ auf „false“ gesetzt ist. Das bedeutet, dass der verschleierte Code dieser Methoden in einer externen Datei mit der Erweiterung „.eil“ erzeugt und nicht in die Assembly selbst aufgenommen wird. Die externe „.eil“-Datei mit dem verschlüsselten Code kann zur Laufzeit geladen und mit dem passenden Passwort entschlüsselt werden, das in den Obfuscation-Attributen angegeben ist.
Um das Beispiel auszuführen, kompilieren Sie zunächst die Projektmappe in der Debug-Konfiguration. Öffnen Sie dann das Projekt obfuscate.babel in Babel Desktop und führen Sie es aus. Das Projekt legt keinen Ausgabepfad fest, daher schreibt Babel Desktop die verschleierte Assembly in einen Ordner BabelOut neben der Eingabe-Assembly. Kopieren Sie anschließend die Dateien DynamicCode.exe, DynamicCode.runtimeconfig.json und DynamicCode.deps.json in den Ordner BabelOut. Führen Sie danach in einem PowerShell-Fenster DynamicCode.exe im Ordner BabelOut aus. Sie sollten die folgende Ausgabe sehen:
> Get demo stream
> Get demo password
Feature1 Demo
Feature2 DemoUm die Quelle „retail“ zu testen, kopieren Sie die Datei retail.txt in den Ordner BabelOut und führen Sie DynamicCode.exe aus. Die Ausgabe sollte lauten:
> Get demo stream
>> Switch to retail
> Get retail password
>> retail password: 4567
Feature1 Retail
Feature2 RetailUm die Quelle „special“ zu testen, kopieren Sie die Datei special.txt in den Ordner BabelOut und führen Sie DynamicCode.exe aus. Die Ausgabe sollte lauten:
> Get demo stream
>> Switch to special
> Get special stream
>>> Read special stream
> Get special password
>> special password: 0000
Feature1 Special
Feature2 Special