Skip to content
KitploitKITPLOIT
ToolsBlog
Einreichen
ToolsBlog
Einreichen

Hacking-, PenTest- und Cybersicherheits-Tools für Ihr Sicherheitsarsenal!

Kitploit ist ein Verzeichnis von Hacking-, Cybersicherheits- und Pentesting-Tools. Entdecken Sie die neuesten Projekt-Updates, um Schwachstellen zu finden, Systeme zu analysieren, Tests zu automatisieren und Ihre Sicherheit zu stärken.

··Feeds·Kontakt·Datenschutz·© 2026 Kitploit

Tool-Verzeichnis

Kategorien

Alle Kategorien anzeigen
Loading categories
VMProtect-devirtualization — Spielen mit dem VMProtect Software-Schutz. Automatische Deobfuskation reiner Funktionen mittels symbolischer Ausführung und LLVM. | Kitploit
Tools/GitHubGitHub/jonathansalwan/vmprotect-devirtualization
Statische AnalyseDynamische Analyse (Sandboxing)Reverse EngineeringFuzzingBinäranalyse
GitHubjonathansalwan/vmprotect-devirtualization

VMProtect-devirtualization

Spielen mit dem VMProtect Software-Schutz. Automatische Deobfuskation reiner Funktionen mittels symbolischer Ausführung und LLVM.

Repository anzeigen

Beliebteste

Alle anzeigen →

Entdecken Sie die meistgenutzten Tools unserer Community.

Alle Tools erkunden

Durchsuchen Sie unsere Tool-Sammlung

Alle Tools anzeigen →
Teilen
1.5k208vor 4 JahrenVon Kitploit geprüft

VMProtect Devirtualisierung

Ein experimenteller dynamischer Ansatz zur Devirtualisierung reiner Funktionen, die durch VMProtect 3.x geschützt sind.

 

 

  • TL;DR
  • Einführung
  • Der Ansatz
    • Beispiel 1: Eine einfache geschützte bitweise Operation
    • Beispiel 2: Eine geschützte MBA-Operation
    • Beispiel 3: Mehr als ein Basisblock
  • Fazit und Einschränkungen
  • Referenzen

 

 

TL;DR

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.

Einführung

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; }

root@kitploit:~
# 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

root@kitploit:~
Sie können das Ergebnis [hier](https://github.com/jonathansalwan/vmprotect-devirtualization/blob/HEAD/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

root@kitploit:~
Sobald die VMP-Ablaufverfolgung generiert wurde, spielen wir sie mit dem Skript [attack_vmp.py](https://github.com/jonathansalwan/vmprotect-devirtualization/blob/HEAD/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.

Zum Beispiel folgt unten ein Beispiel für eine Konkretisierung. Links haben wir einen AST, der Unterausdrücke enthält, die keine symbolische Variable involvieren (`1 + 2` und `6 ^ 3`). Diese Zweige werden also konkretisiert und durch die Konstanten `3` und `5` ersetzt, was zum AST rechts führt. **So devirtualisieren wir Code.**

<p align="center">
  <img src="https://assets.kitploit.com/production/public/readmes/8096/857ed50cfe9cb2347f816ece1d8dc4c13174c971dcb65ebc5621be14269a5ca8.png">
</p>

**Ein Hinweis zum rückwärtsgerichteten Slicing auf Formelebene**: Wie bei der symbolischen Ausführung üblich, wird die symbolische Darstellung zunächst vorwärts entlang des Pfades berechnet. Anschließend werden alle logischen Operationen und Definitionen, die weder das Endergebnis noch den verfolgten Pfad beeinflussen, aus dem symbolischen Ausdruck entfernt (Formel-Slicing, auch Formel-Pruning genannt). Dies führt auf der Formel zu einer Analyse, die dem rückwärtsgerichteten Slicing des Codes aus der Programmausgabe entspricht. Somit haben wir bei der Rückkehr der `secret`-Funktion einen Ausdruck für die Beziehung zwischen den Eingaben und der Ausgabe ohne die Anweisungen von VMProtect.

Das Skript `./attack_vmp.py` nimmt als Parameter die Ablaufverfolgungsdatei und die Größe der symbolischen Variablen entgegen. Erinnern Sie sich, es waren `edi` und `esi`, also sind sie 4 Bytes lang. Das Ergebnis des Skripts ist das Folgende:```
$ ./attack_vmp.py --trace1 ./vmp_traces/sample2.vmp.trace --symsize 4
[+] Replaying the VMP trace
[+] Symbolize inputs
[+] Instruction executed: 12462
[+] Emulation done
[+] Return value: 0x3
[+] Devirt expr: (bvor (bvnot (bvor (bvnot (bvnot x)) (bvnot y))) (bvnot (bvor (bvnot x) (bvnot (bvand (bvnot y) (bvnot y))))))
[+] Synth expr: (bvxor x y)

[+] LLVM IR ==============================

; ModuleID = 'tritonModule'
source_filename = "tritonModule"

define i32 @__triton(i32 %SymVar_0, i32 %SymVar_1) {
entry:
  %0 = xor i32 %SymVar_0, %SymVar_1
  ret i32 %0
}

[+] EOF LLVM IR ==============================

Wie wir sehen können, ist der devirtualisierte Ausdruck, der von der secret-Funktion zurückgegeben wird, recht prägnant und enthält keine Anweisungen von der virtuellen Maschine.```smt (bvor (bvnot (bvor (bvnot (bvnot x)) (bvnot y) ) ) (bvnot (bvor (bvnot x) (bvnot (bvand (bvnot y) (bvnot y) ) ) ) ) )

root@kitploit:~
Allerdings gelang es uns nicht, den ursprünglichen Ausdruck wiederherzustellen, der eine einfache `XOR`-Operation war. Es scheint, dass das `XOR` in bitweise Operationen übersetzt wurde. Glücklicherweise haben wir kürzlich neue Funktionen im Triton-Projekt veröffentlicht, nämlich einen [Synthesizer](https://github.com/JonathanSalwan/Triton/issues/1074) und einen Lifter zu [LLVM-IR](https://github.com/JonathanSalwan/Triton/issues/1078). Somit können wir den Ausdruck synthetisieren, der den Ausdruck `(bvxor x y)` ergibt. Das ist ein guter Erfolg, und jetzt können wir weitergehen, indem wir diesen Ausdruck nach LLVM-IR liften und dann einen neuen devirtualisierten Binärcode kompilieren.

## Beispiel 2: Eine geschützte MBA-Operation

Ok, schauen wir uns nun ein weiteres Beispiel an, das versucht, eine MBA-Operation zu verbergen. Der ursprüngliche Quellcode ist der folgende:```cpp
// This function is an MBA that computes: (x ^ 92) + y
// We will protect this MBA with VMProtect and see if we can recover "(x ^ 92) + y"
char secret(char x, char y) {
  VMProtectBegin("secret");
  int a = 229 * x + 247;
  int b = 237 * a + 214 + ((38 * a + 85) & 254);
  int c = (b + ((-(2 * b) + 255) & 254)) * 3 + 77;
  int d = ((86 * c + 36) & 70) * 75 + 231 * c + 118;
  int e = ((58 * d + 175) & 244) + 99 * d + 46;
  int f = (e & 148);
  int g = (f - (e & 255) + f) * 103 + 13;
  int r = (237 * (45 * g + (174 * g | 34) * 229 + 194 - 247) & 255) + y;
  VMProtectEnd();
  return r;
}

Wie beim ersten Beispiel müssen wir ermitteln, wo diese Funktion beginnt und endet, und eine VMP trace erstellen.``` $ ./pin/pin -t ./pin/source/tools/VMP_Trace/obj-intel64/VMP_Trace.so -start 4198857 -end 4199140 -- ./vmp_binaries/binaries/sample3.vmp.bin 1 2 &> ./vmp_traces/sample3.vmp.trace

root@kitploit:~
Sobald die [VMP-Spur](https://github.com/jonathansalwan/vmprotect-devirtualization/blob/HEAD/vmp_traces/sample3.vmp.trace) erzeugt wurde, führen wir das Skript `./attack_vmp.py` aus.```
$ ./attack_vmp.py --trace1 ./vmp_traces/sample3.vmp.trace --symsize 1
[+] Replaying the VMP trace
[+] Symbolize inputs
[+] A potential symbolic jump found on CF flag: 0x821dac: popfq - Model: {0: x:32 = 0xa3, 1: y:32 = 0xff}
[+] A potential symbolic jump found on CF flag: 0x87f437: popfq - Model: {0: x:32 = 0xa3, 1: y:32 = 0xff}
[+] Instruction executed: 25085
[+] Emulation done
[+] Return value: 0x5f
[+] Devirt expr: In: (bvadd (bvadd (bvshl (bvadd (_ bv1 32) (bvnot (bvlshr (concat (_ bv0 8) (_ bv0 8) ((_ extract 15 8)  ...
[+] Synth expr: In: (bvadd (bvadd (bvshl (bvadd (_ bv1 32) (bvnot (bvlshr (concat (_ bv0 8) (_ bv0 8) ((_ extract 15 8)  ...

[+] LLVM IR ==============================

; ModuleID = 'tritonModule'
source_filename = "tritonModule"

define i32 @__triton(i8 %SymVar_0, i8 %SymVar_1) {
entry:
  %0 = xor i8 %SymVar_0, 92
  %1 = and i8 %SymVar_0, 0
  %2 = zext i8 %1 to i32
  %3 = or i32 0, %2
  %4 = shl i32 %3, 8
  %5 = zext i8 %0 to i32
  %6 = or i32 %4, %5
  %7 = and i8 %SymVar_1, 0
  %8 = zext i8 %7 to i32
  %9 = or i32 0, %8
  %10 = shl i32 %9, 8
  %11 = zext i8 %SymVar_1 to i32
  %12 = or i32 %10, %11
  %13 = zext i8 %7 to i32
  %14 = or i32 0, %13
  %15 = shl i32 %14, 8
  %16 = zext i8 %SymVar_1 to i32
  %17 = or i32 %15, %16
  %18 = lshr i32 %17, 7
  %19 = xor i32 %18, -1
  %20 = add i32 1, %19
  %21 = shl i32 %20, 8
  %22 = add i32 %21, %12
  %23 = add i32 %22, %6
  ret i32 %23
}

[+] EOF LLVM IR ==============================

Das Ergebnis ist aus mehreren Gründen recht interessant. Erstens ist es uns erfolgreich gelungen, Anweisungen aus der virtuellen Maschine so weit wie möglich zu vermeiden, da wir von 25085 ausgeführten Anweisungen auf 25 LLVM-Anweisungen reduziert haben. Allerdings haben wir keine gute synthetisierte Version der Ausgabe erhalten (ja, ich weiß, wir gehen weiter als nur Devirtualisierung). Der Vorteil der Übertragung unserer symbolischen Ausdrücke auf LLVM-IR besteht darin, dass wir die Optimierungspipeline von LLVM voll ausnutzen können. Machen wir Folgendes:```llvm $ opt -S -O3 ./devirt/sample3.ll ; ModuleID = 'devirt/sample3.ll' source_filename = "tritonModule"

; Function Attrs: mustprogress nofree norecurse nosync nounwind readnone willreturn define i32 @__triton(i8 %SymVar_0, i8 %SymVar_1) local_unnamed_addr #0 { entry: %0 = xor i8 %SymVar_0, 92 %1 = zext i8 %0 to i32 %2 = zext i8 %SymVar_1 to i32 %3 = shl nuw nsw i32 %2, 1 %4 = and i32 %3, 256 %5 = add nuw nsw i32 %1, %2 %6 = sub nsw i32 %5, %4 ret i32 %6 }

root@kitploit:~
Unter Verwendung von LLVM-Optimierungen gelang es uns, Rauschen aus unserer devirtualisierten Ausgabe zu entfernen und so die MBA zu brechen.
Wir sehen die `XOR`-Operation mit ihrer Konstanten (`%0 = xor i8 %SymVar_0, 92`) und das `+ y` (`%6 = add nsw i32 %5, %1`).
Anweisungen dazwischen beschäftigen sich lediglich mit dem Vorzeichen. Um dieses Beispiel zusammenzufassen: Wir haben die `secret`-Funktion mit Hilfe des `attack_vmp.py`-Skripts vollständig devirtualisiert und dann die MBA mit LLVM-Optimierungen vollständig gebrochen.

## Beispiel 3: Mehr als ein Basisblock

Wir haben sehr gute Ergebnisse erzielt, wenn die `secret`-Funktion unabhängig von ihrer Größe nur einen Basisblock enthält. An diesem Punkt
sind wir in der Lage, einen Pfad zu devirtualisieren. Um das gesamte Funktionsverhalten zu rekonstruieren, müssen wir nacheinander erreichbare
Pfade devirtualisieren. Dazu müssen wir eine Pfadabdeckung auf benutzerabhängigen Verzweigungen durchführen. Als Ergebnis erhalten wir
einen Pfadbaum, der die verschiedenen Pfade der ursprünglichen Funktion darstellt. Ein Pfadbaum wird durch Einführung von
If-Then-Else-Konstruktionen aus zwei Spuren T1 und T2 mit demselben Präfix, gefolgt von einer Bedingung C in T1 und einem not(C)
in T2, erhalten. Sobald ein Pfadbaum erstellt ist, können wir LLVM einen CFG generieren lassen.

<p align="center">
  <img src="https://assets.kitploit.com/production/public/readmes/8096/fc5d1bf9a1544b9b44ef164247f3fc38d04bf45d7c2d7a3c0b21ea3d6636cd39.png">
</p>

Beim Tigress-Software-Schutz wurden virtuelle Sprünge mit echten `jcc`-Anweisungen implementiert, die es uns ermöglichten,
die Sprungbedingung schnell zu identifizieren. Die Dinge werden jedoch komplexer, wenn virtuelle Sprünge bei
VMProtect involviert sind, da es keine `jcc`-Anweisungen zum Springen zu einem anderen virtuellen Block verwendet. Wir mussten
Marker auf einer dynamischen Spur definieren, um die an einer benutzerabhängigen Verzweigung beteiligte Bedingung zu lokalisieren. Dies ist der experimentelle Teil
dieses Angriffs, da Marker nicht sehr genau sind, aber für unsere Beispiele funktionierten.

Ok, betrachten wir das folgende Beispiel:```cpp
int secret(int x, int y) {
  VMProtectBegin("secret");
  int r = 0;
  if (x + y == 1001)
    r = x + 1;
  else
    r = y - 1;
  VMProtectEnd();
  return r;
}

Wie bei den ersten Beispielen müssen wir die Spur generieren und analysieren.``` $./pin/pin -t ./pin/source/tools/VMP_Trace/obj-intel64/VMP_Trace.so -start 4198848 -end 4198928 -- ./vmp_binaries/binaries/sample5.vmp.bin 1 2 &> ./vmp_traces/sample5.vmp.trace.1

$ ./attack_vmp.py --trace1 ./vmp_traces/sample5.vmp.trace.1 --symsize 4 [+] Replaying the VMP trace [+] Symbolize inputs [+] A potential symbolic jump found of AF flag: 0x80d905: cmp r11b, dl - Model: {0: x:32 = 0x0, 1: y:32 = 0x3e9} [+] Instruction executed: 16164 [+] Emulation done [+] Return value: 0x4 [+] Devirt expr: (bvnot (bvadd (bvand (bvnot y) (bvnot y)) (_ bv1 32))) [+] Synth expr: (bvadd y (_ bv4294967295 32))

[+] LLVM IR ==============================

; ModuleID = 'tritonModule' source_filename = "tritonModule"

define i32 @__triton(i32 %SymVar_1) { entry: %0 = add i32 %SymVar_1, -1 ret i32 %0 }

[+] EOF LLVM IR ==============================

root@kitploit:~
Das Skript teilt uns mit, dass möglicherweise ein potenzieller symbolischer Sprung am `AF`-Flag an der Adresse `0x80d905` gefunden wurde.
Es stellt auch ein neues Modell (unter Verwendung symbolischer Ausführung) bereit, das den anderen Pfad nehmen sollte. Lassen Sie uns also eine
zweite Ablaufverfolgung mit diesem Modell generieren (wenn Sie sich das Modell ansehen, ist es bezüglich unseres Quellcodes korrekt).```
$ ./pin/pin -t ./pin/source/tools/VMP_Trace/obj-intel64/VMP_Trace.so -start 4198848 -end 4198928 -- ./vmp_binaries/binaries/sample5.vmp.bin 0 1001 &> ./vmp_traces/sample5.vmp.trace.2

Sobald die zweite Spur generiert wurde, müssen wir diese beiden Spuren dem Skript attack_vmp.py übergeben, damit es sie zusammenführen und einen Pfadbaum erstellen kann. Wir haben zusätzliche Optionen, um zu definieren, wo sich die Bedingung befindet und auf welchem Flag (AF-Flag bei 0x80d905).``` $ ./attack_vmp.py --trace1 ./vmp_traces/sample5.vmp.trace.1 --symsize 4 --trace2 ././vmp_traces/sample5.vmp.trace.2 --vbraddr 0x80d905 --vbrflag af [+] Replaying the VMP trace [+] Symbolize inputs [+] A potential symbolic jump found of AF flag: 0x80d905: cmp r11b, dl - Model: {0: x:32 = 0x0, 1: y:32 = 0x3e9} [+] Instruction executed: 16164 [+] Emulation done [+] A second trace has been provided [+] Replaying the VMP trace [+] Symbolize inputs [+] Instruction executed: 15758 [+] Emulation done [+] Merging expressions from trace1 and trace2 [+] Return value: 0x3e9 [+] Devirt expr: In: (ite (= (ite (= (_ bv16 8) (bvand (_ bv16 8) (bvxor (bvsub (_ bv80 8) ((_ extract 7 0) (bvadd (bvlsh ... [+] Synth expr: In: (ite (= (ite (= (_ bv16 8) (bvand (_ bv16 8) (bvxor (bvsub (_ bv80 8) ((_ extract 7 0) (bvadd (bvlsh ...

[+] LLVM IR ==============================

; ModuleID = 'tritonModule' source_filename = "tritonModule"

define i32 @__triton(i32 %SymVar_0, i32 %SymVar_1) { entry: %0 = add i32 %SymVar_1, -1 %1 = add i32 %SymVar_0, 1 %2 = add i32 %SymVar_1, %SymVar_0 %3 = xor i32 %2, -1 %4 = xor i32 %2, -1 %5 = and i32 %4, %3 %6 = xor i32 %5, 1001 %7 = add i32 %5, 1001 %8 = xor i32 %5, 1001 %9 = xor i32 %8, %7 %10 = and i32 %9, %6 [... skip ...] %469 = add i64 %468, 140737488347280 %470 = trunc i64 %469 to i8 %471 = xor i8 80, %470 %472 = sub i8 80, %470 %473 = xor i8 %472, %471 %474 = and i8 16, %473 %475 = icmp eq i8 16, %474 %476 = select i1 %475, i1 true, i1 false %477 = icmp eq i1 %476, false %478 = select i1 %477, i32 %1, i32 %0 ret i32 %478 }

[+] EOF LLVM IR ==============================

root@kitploit:~
Bei diesem Schritt haben wir die beiden Traces entvirtualisiert und zu `if-then-else`-Ausdrücken zusammengeführt.
Nach dem Anheben des Ausdrucks auf LLVM-IR erhalten wir einen CFG mit nur 480 LLVM-Instruktionen, was bereits ein guter Gewinn im Vergleich zu den Tausenden von Instruktionen ist, die von der virtuellen Maschine ausgeführt werden.
Aber wir können noch besser werden, wenn wir LLVM-Optimierungen nutzen:```llvm
$ opt -S -O3 ./devirt/sample5.ll
; ModuleID = './devirt/sample5.ll'
source_filename = "tritonModule"

; Function Attrs: mustprogress nofree norecurse nosync nounwind readnone willreturn
define i32 @__triton(i32 %SymVar_0, i32 %SymVar_1) local_unnamed_addr #0 {
entry:
  %0 = add i32 %SymVar_0, 1
  %1 = add i32 %SymVar_1, -1
  %2 = add i32 %SymVar_1, %SymVar_0
  %.not = icmp eq i32 %2, 1001
  %3 = select i1 %.not, i32 %0, i32 %1
  ret i32 %3
}

attributes #0 = { mustprogress nofree norecurse nosync nounwind readnone willreturn }

Woot, wir haben das ursprüngliche Verhalten der secret-Funktion wiederhergestellt!

Fazit und Grenzen

Während der Ansatz für Funktionen mit einem Pfad sehr gute Ergebnisse zeigte, besteht die Hauptbeschränkung der Methode darin, dass sie hauptsächlich auf Programme mit einer geringen Anzahl von Pfaden ausgelegt ist, aufgrund der Art und Weise, wie VMProtect virtuelle Sprünge durchführt. Bei einer zu hohen Anzahl von Pfaden können Teile des ursprünglichen Codes verloren gehen, was zu einer unvollständigen Wiederherstellung führt. Beachten Sie, dass wir ausführbare Pfade und nicht syntaktische Pfade im CFG betrachten. Hash- und andere kryptografische Funktionen haben oft nur sehr wenige Pfade – bei timing-angriffsresistenten Implementierungen nur einen einzigen Pfad.

Auch unsere aktuelle Implementierung ist auf Programme ohne benutzerabhängige Speicherzugriffe beschränkt. Diese Einschränkung kann teilweise durch eine symbolischere Behandlung von Speicherzugriffen im DSE behoben werden.

Beachten Sie auch, dass begrenzte Schleifen und nicht-rekursive Funktionsaufrufe zwar behandelt werden, aber derzeit als inlining oder entrollter Code wiederhergestellt werden, was zu einer potenziellen Aufblähung der Größe des devirtualisierten Codes führt. Es wäre interessant, einen Nachbearbeitungsschritt zu haben, der versucht, diese Abstraktionen auf hoher Ebene wiederherzustellen.

Abschließend möchte ich darauf hinweisen, dass ich nicht beabsichtige, eine Art magische Methode bereitzustellen, dies sind nur einige Anmerkungen zu einem dynamischen Angriff gegen sehr spezifische Fälle, die durch VMProtect geschützt sind =).

Wenn Sie einen tieferen Einblick nehmen möchten, schauen Sie sich diese Ressourcen an:

  • Das Pintool zum Erzeugen eines Traces
  • Skript zum Analysieren eines VMP-Traces
  • Quellcode der Beispiele
  • Original- und geschützte Binärdateien
  • VMP-Traces
  • Devirtualisierte Ergebnisse

Last but not least, besonderer Dank an meinen Kumpel @0vercl0k für das Korrekturlesen und die Bearbeitungen 🚀

Referenzen```

[00] https://www.usenix.org/legacy/event/woot09/tech/full_papers/rolles.pdf [01] https://secret.club/2021/09/08/vmprotect-llvm-lifting-1.html [02] https://secret.club/2021/09/08/vmprotect-llvm-lifting-2.html [03] https://secret.club/2021/09/08/vmprotect-llvm-lifting-3.html [04] https://back.engineering/17/05/2021/ [05] https://back.engineering/21/06/2021/ [06] https://www.mitchellzakocs.com/blog/vmprotect3 [07] https://github.com/can1357/NoVmp [08] https://github.com/archercreat/vmpfix [09] https://github.com/void-stack/VMUnprotect [10] https://github.com/JonathanSalwan/Triton/blob/master/publications/DIMVA2018-slide-deobfuscation-salwan-bardin-potet.pdf [11] https://whereisr0da.github.io/blog/posts/2021-02-16-vmp-3/ [12] https://github.com/pgarba/UniTaint [13] https://github.com/mrexodia/VMProtectTest

root@kitploit:~
Tool herunterladen