
Spielen mit dem VMProtect Software-Schutz. Automatische Deobfuskation reiner Funktionen mittels symbolischer Ausführung und LLVM.
Ein experimenteller dynamischer Ansatz zur Devirtualisierung reiner Funktionen, die durch VMProtect 3.x geschützt sind.
Ich teile einige Notizen über einen dynamischen Ansatz zur Devirtualisierung reiner Funktionen, die durch VMProtect geschützt sind. Dieser Ansatz hat sehr gute Ergebnisse gezeigt, wenn die virtualisierte Funktion nur einen Basisblock enthält (unabhängig von seiner Größe). Dies ist ein häufiges Szenario, wenn Binärdateien arithmetische Operationen schützen. Allerdings ist dieser Ansatz etwas experimenteller, wenn die Zielfunktion mehr als einen Basisblock enthält. Dennoch gelang es uns, Binärcode aus Beispielen, die 2 Basisblöcke enthielten, zu devirtualisieren und zu rekonstruieren, was darauf hindeutet, dass es möglich ist, kleine Funktionen vollständig dynamisch zu devirtualisieren.
VMProtect ist ein Softwareschutz, der Code schützt, indem er ihn durch eine virtuelle Maschine mit nicht standardmäßiger Architektur ausführt. Dieser Schutz ist ein großartiges Spielfeld für ASM-Liebhaber [0, 1, 2, 3, 4, 5, 6, 11]. Es gibt bereits zahlreiche Werkzeuge, die diesen Schutz angreifen [7, 8, 9, 12, 13]. Im Jahr 2016 haben wir uns die Tigress Softwareschutzlösung angesehen und es geschafft, ihre Virtualisierung mithilfe symbolischer Ausführung und LLVM zu besiegen. Dieser Ansatz wurde auf der DIMVA 2018 vorgestellt [10] und ich wollte ihn an VMProtect testen. Beachten Sie, dass es keine magische Lösung gibt, die bei jeder Binärdatei funktioniert; es gibt immer Kompromisse je nach Ziel und Zielsetzung. Dieser bescheidene Beitrag soll ein Beispiel für einen dynamischen Angriff gegen reine Funktionen liefern, die durch VMProtect virtualisiert werden. Der Hauptvorteil eines dynamischen Angriffs besteht darin, dass er von Natur aus einige statische Schutzmechanismen von VMProtect wie selbstmodifizierenden Code, Schlüssel- und Operandenverschlüsselung usw. umgeht.
Wir betrachten eine reine Funktion als eine Funktion mit einer endlichen Anzahl von Pfaden, die keine Nebeneffekte hat. Es können mehrere Eingaben vorhanden sein, aber nur eine Ausgabe. Nachfolgend ein Beispiel einer reinen Funktion:```cpp int secret(int x, int y) { int r = x ^ y; return r; }
# Der Ansatz
Wir stützen uns auf die grundlegende Erkenntnis, dass eine verschleierte Ablaufspur T' (aus dem verschleierten Code P') die ursprünglichen Anweisungen des Originalcodes P (die Ablaufspur T, die T' im Originalcode entspricht) und Anweisungen der virtuellen Maschine VM kombiniert, sodass T' = T + VM(T). Wenn wir in der Lage sind, zwischen diesen beiden Untersequenzen von Anweisungen T und VM(T) zu unterscheiden, können wir einen Pfad des Originalprogramms P aus einer Ablaufspur T' rekonstruieren. Durch Wiederholung dieses Vorgangs, um alle Pfade des virtualisierten Programms abzudecken, können wir das Originalprogramm P rekonstruieren. In unserem praktischen Beispiel hat der Originalcode eine endliche Anzahl ausführbarer Pfade, was in vielen Situationen des Schutzes geistigen Eigentums der Fall ist. Dazu gehen wir wie folgt vor:
1. Identifizieren der virtualisierten Funktion und ihrer Argumente
2. Erzeugen einer VMProtect-Ablaufspur des Ziels
3. Abspielen der VMP-Ablaufspur und Konstruktion symbolischer Ausdrücke, um die Beziehung zwischen Eingaben und Ausgabe zu erhalten
4. Anwenden von Optimierungen auf symbolische Ausdrücke, um nach Möglichkeit Anweisungen der VM zu vermeiden
5. Heben unserer symbolischen Darstellung auf LLVM-IR, um eine neue ungeschützte Version des Ziels zu erstellen
## Beispiel 1: Eine einfache bitweise Operation
Nehmen wir als erstes Beispiel die folgende Funktion: Sie nimmt zwei Eingaben entgegen und gibt `x ^ y` zurück, die durch VMProtect geschützt ist.```cpp
int secret(int x, int y) {
VMProtectBegin("secret");
int r = x ^ y;
VMProtectEnd();
return r;
}
Wir beginnen damit, zu identifizieren, wo Funktionen VMProtect verwenden und wie viele Argumente sie haben. Für unser Beispiel könnten wir etwas wie unten haben:
Allein durch das Lesen des Codes wissen wir, dass die Funktion an der Adresse 0x4011c0 beginnt, zwei 32-Bit-Argumente (edi und esi) hat und bei 0x4011ef zurückkehrt. Das ist alles Reverse-Engineering, was wir brauchen. Die nächsten Teile werden automatisch erfolgen. Jetzt müssen wir eine Trace-Ausführung dieser virtualisierten Funktion generieren. Dazu verwenden wir ein Pintool. Es benötigt nur eine Start- und eine Endadresse (für unser Beispiel 0x4011c0 und 0x4011ef), die den Bereich der Instrumentierung darstellt. Beachten Sie, dass jede Art von DBI oder Emulator diese Aufgabe übernehmen könnte.```
$ ./pin/pin -t ./pin/source/tools/VMP_Trace/obj-intel64/VMP_Trace.so -start 4198848 -end 4198895 -- ./vmp_binaries/binaries/sample2.vmp.bin 1 2 &> ./vmp_traces/sample2.vmp.trace
Sie können das Ergebnis [hier](https://github.com/jonathansalwan/vmprotect-devirtualization/blob/main/vmp_traces/sample2.vmp.trace) sehen. Das Trace-Format verwendet drei Arten von Operationen: `mr`, `r` und `i`.
`mr` ist ein Speicherlesezugriff, der von der Instruktion `i` durchgeführt wird, und `r` sind die CPU-Register. Zum Beispiel:```
mr:0x7ffda459d718:8:0x227db4f8
r:0x40200a:0x0:0x7ffda459f571:0x2:0x40200a:0x0:0x0:0x7ffda459d688:0x0:0x0:0x7feee9b80ac0:0x7feee9b8000f:0xad1c3e:0x0:0x0:0x0
i:0x89173e:8:488BB42490000000
Wir haben einen Speicherzugriff, der eine 8 Byte große Konstante 0x227db4f8 von der Adresse 0x7ffda459d718 lädt.
Die Anweisung wird an der Adresse 0x89173e ausgeführt und ihr 8 Byte langer Opcode ist 488BB42490000000, was ein
mov rsi, qword ptr [rsp + 0x90] ist.
Der Registerzustand vor der Ausführung ist der folgende:```python
(1) RAX = 0x40200a (9) R8 = 0
(2) RBX = 0 (10) R9 = 0
(3) RCX = 0x7ffda459f571 (11) R10 = 0x7feee9b80ac0
(4) RDX = 0x2 (12) R11 = 0x7feee9b8000f
(5) RDI = 0x40200a (13) R12 = 0xad1c3e
(6) RSI = 0 (14) R13 = 0
(7) RBP = 0 (15) R14 = 0
(8) RSP = 0x7ffda459d688 (16) R15 = 0
Sobald die VMP-Ablaufverfolgung generiert wurde, spielen wir sie mit dem Skript [attack_vmp.py](https://github.com/jonathansalwan/vmprotect-devirtualization/blob/main/attack_vmp.py) erneut ab. Dieses Skript verwendet [Triton](https://github.com/jonathansalwan/Triton), um das Pfadprädikat der Ablaufverfolgung zu erstellen. Beachten Sie, dass alle Ausdrücke, die symbolische Variablen (Eingaben der Funktion) betreffen, symbolisch bleiben, während alle nicht damit zusammenhängenden Eingabeausdrücke konkretisiert werden. Mit anderen Worten: Unsere symbolischen Ausdrücke enthalten keine Operationen, die sich auf die virtuelle Maschine beziehen (die Maschinerie selbst hängt nicht vom Benutzer ab), sondern nur Operationen, die sich auf das ursprüngliche Programm beziehen.