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
binprotect — x64 PE bin2bin-Obfuskator, der der Binärdatei keinen Abschnitt hinzufügt | Kitploit
Tools/GitHubGitHub/noahware/binprotect
Statische AnalyseDynamische Analyse (Sandboxing)Reverse EngineeringMalware-AnalyseBinäranalyse
GitHubnoahware/binprotect

binprotect

x64 PE bin2bin-Obfuskator, der der Binärdatei keinen Abschnitt hinzufügt

Repository anzeigen
304364vor 21 TagenVon Kitploit geprüft

Beliebteste

Alle anzeigen →

Entdecken Sie die meistgenutzten Tools unserer Community.

Alle Tools erkunden

Durchsuchen Sie unsere Tool-Sammlung

Alle Tools anzeigen →
Teilen

Bauanweisungen

Siehe 4. Bauen für Bauanweisungen.

Inhalt

  1. Einleitung
  2. Binär-Rewriter
    • 2.1. Relative Adressverfolgung
    • 2.2. Disassemblierung
      • 2.2.1. Basisblock-Aufteilung
      • 2.2.2. Indirekter Kontrollfluss
        • 2.2.2.1. Sprungtabellen
          • 2.2.2.1.1. Begrenzte Sprungtabellen
          • 2.2.2.1.2. Unbegrenzte Sprungtabellen
          • 2.2.2.1.3. Verschiedene Arten von Sprungtabellen
        • 2.2.2.2. Randfälle
      • 2.2.3. Funktionen
      • 2.2.4. Behandlung von 'Noreturn'-Aufrufen
    • 2.3. Ausnahmebehandlung
      • 2.3.1. Entladungsunterstützung
        • 2.3.1.1. Implementierung
      • 2.3.2. Ausnahmeinfo-Parsing
        • 2.3.2.1. SEH/C_SCOPE_TABLE
        • 2.3.2.2. FuncInfo3 und FuncInfo4
      • 2.3.3. RTTI- und ThrowInfo-Parsing
        • 2.3.3.1. RTTI
        • 2.3.3.2. ThrowInfo
  3. Verschleierung
    • 3.1. Virtuelle Maschine
      • 3.1.1. Entladungsunterstützung
    • 3.2. Opaque-Prädikatblöcke
    • 3.3. Kontrollflussabflachung
    • 3.4. Lineare Substitution
    • 3.5. Gemischte boolesche Arithmetik
  4. Bauen
  5. Verwendung
  6. Akronyme
  7. Danksagungen

1. Einleitung

Dieser Verschleierer ist bin2bin, das heißt, er nimmt eine bereits kompilierte ausführbare Binärdatei und reproduziert sie mit den angewendeten Verschleierungspässen. Dies kann verwendet werden, um eine Anwendung zu schützen, ohne Zugriff auf den ursprünglichen Quellcode zu haben. Derzeit werden nur x64 PE (portable executable) Dateien unterstützt, aber es gibt Pläne, in Zukunft Unterstützung für andere Binärformate (z. B. ELF) hinzuzufügen.

Derzeit fügen alle bekannten bin2bin-Verschleierer einen Abschnitt am Ende der Binärdatei ein, um den verschleierten Code oder die verschleierten Daten darin zu platzieren. Dadurch bleibt das ursprüngliche Layout der Binärdatei erhalten, da der Inhalt der vorhandenen Abschnitte nicht geändert werden muss. Dies ist viel einfacher zu verwalten, da die meisten RVAs (relative Adressen) gültig bleiben.

Dieses Projekt verfolgt einen einzigartigen Ansatz für bin2bin, bei dem jeglicher verschleierter Code oder verschleierte Daten innerhalb der ursprünglichen Abschnitte der Binärdatei eingefügt werden. Dies erfordert die Verfolgung jeder einzelnen RVA in der Anwendung. Die Vorteile dieses Ansatzes sind:

  • Weniger auffällig für Malware-Analysen, da keine zusätzlichen ausführbaren Abschnitte zur Binärdatei hinzugefügt werden. Hinweis: Dies wurde rein zu Bildungs- und Forschungszwecken getestet.
  • Reduzierte Größe der Ausgabebinärdatei. Dies liegt daran, dass der ursprüngliche, nicht verschleierte Code aus der Binärdatei gelöscht werden kann, da die Abschnitte in der Größe angepasst werden können.
  • Schwierigere Analyse, da ein Reverse Engineer verschleierten und nicht verschleierten Code nicht allein anhand der Abschnitte trennen kann, in denen sie sich befinden; jede Routine müsste überprüft werden, um festzustellen, ob sie verschleiert ist.

Dieses Dokument beschreibt sowohl das Umschreiben der ausführbaren Binärdatei als auch die implementierten Verschleierungstechniken. Die folgenden Verschleierungstechniken wurden implementiert:

  • Virtuelle Maschine.
  • Opaque-Prädikate.
  • Kontrollflussabflachung.
  • Lineare Substitution.
  • Gemischte boolesche Arithmetik.

Darüber hinaus unterstützt dieses Projekt auch Ausnahmen (C++-Ausnahmen und SEH) und kann Funktionen verschleiern, die Ausnahmebehandlung besitzen.

Zur Unterstützung der Disassemblierung und Entdeckung von Code in der Binärdatei werden optional Symboldateien (sowohl PDB als auch MAP) akzeptiert. Die Bereitstellung von Symboldateien ist nicht erforderlich, hilft aber bei der Disassemblierung komplexer Binärdateien. Einige Funktionen wie Ausnahmebehandlung und Kontrollflussabflachung erfordern die Bereitstellung einer Symboldatei.

2. Binär-Rewriter

Ein Binär-Rewriter nimmt eine ausführbare Binärdatei und ändert den darin enthaltenen Code oder die Daten, um eine Ausgabebinärdatei mit den angewendeten Änderungen zu erzeugen.

2.1. Relative Adressverfolgung

Da der verschleierte Code direkt in die ursprünglichen Abschnitte der Binärdatei eingefügt wird, müssen die relativen Adressen im Programm verfolgt werden, damit alle Verweise auf sie angepasst werden können. Dadurch wird sichergestellt, dass die Verweise nach dem Einfügen von Code und Daten immer noch auf dieselbe Stelle zeigen. Andernfalls würde auf Daten oder Code an der falschen Stelle zugegriffen, was das Verhalten der Ausgabebinärdatei verändert und schwere Instabilitäten verursacht.

Immer wenn ein Verweis auf eine relative Adresse gefunden wird (z. B. Anweisungen mit rip-relativen Operanden oder PE-Datenverzeichnisse), wird er zu einer Tracking-Liste hinzugefügt, die am Ende des Umschreibens aktualisiert wird. Die RVA, an der der Verweis auftritt, wird verfolgt (um zu wissen, wo der Verweis aktualisiert werden muss), ebenso wie die RVA, auf die verwiesen wird (um zu wissen, mit welcher RVA der Verweis aktualisiert werden muss).

Wenn der Disassembler eine rip-relative Anweisung findet, fügt er sie zu einer Liste von Verweisen hinzu, die am Ende der Verschleierung aktualisiert werden müssen. Dadurch wird sichergestellt, dass alle diese Anweisungen immer noch auf die Stelle zeigen, die sie ursprünglich hatten. Andere relative Anweisungsfälle wie Sprungtabellen werden ebenfalls als zu aktualisierende Verweise hinzugefügt.

Alle verfolgten RVAs müssen angepasst werden, wenn Bytes in die Binärdatei eingefügt oder daraus entfernt werden. Hier ist zum Beispiel der Byte-Einfüge-Handler:```cpp
void binwrite::binary_t::insert(const rva_t rva, const std::span data, const bool inclusive)
{
buffer_.insert_range(buffer_.begin() + rva.value(), data);

root@kitploit:~
update_rvas(rva, static_cast<rva_t::size_type>(data.size()), inclusive);  

}

root@kitploit:~
`update_rvas` ist der Ort, an dem jede verfolgte RVA aktualisiert wird, um die im Binärfile aufgetretene Änderung widerzuspiegeln. Hier ist ein Diagramm dieses Vorgangs:

![](https://assets.kitploit.com/production/public/readmes/8091/12aaec3746c267c932ab3804b13df50c08a463c403417bf6ba4a52f39d12f48a.png)

Abbildung 1. Relative Adressverfolgung.

Die eingefügten Daten (blau) verschieben die aktuellen Daten (grau). Die referenzierte RVA der Anweisung (orange) wird aktualisiert, um auf denselben Speicher zu verweisen, unter Berücksichtigung der eingefügten Daten (blau).

## 2.2. Disassemblierung

Alle potenziellen Codeeinträge (Exporte, Einstiegspunkt, Umleitungen, die auf den Codeschnitt zeigen, usw.) werden zu einer Disassemblierungs-Warteschlange hinzugefügt. Wenn eine Symboldatei vorhanden ist, werden alle durch die Symboldatei beschriebenen Funktionen ebenfalls zur Disassemblierungs-Warteschlange hinzugefügt. Jeder Eintrag in der Warteschlange wird als einzelner grundlegender Block behandelt.

Ein grundlegender Block ist eine Gruppe von Befehlen ohne Verzweigungen; das bedeutet, dass er an Kontrollflussanweisungen (z.B. Sprung, Rückkehr, Interrupt) beendet wird. Grundlegende Blöcke werden nicht an Aufrufen beendet, da diese in den meisten Fällen zurückkehren. Einige Funktionen kehren nicht zurück (z.B. _CxxThrowException) und werden im Folgenden als 'noreturn'-Aufrufe bezeichnet.

Wenn ein grundlegender Block aus der Disassemblierungs-Warteschlange verarbeitet wird, wird jeder Befehl von oben beginnend disassembliert, bis einer der folgenden Fälle eintritt:

- Ein weiterer bereits analysierter grundlegender Block wird erreicht, was zu einer Überlappung führt. Siehe „Grundlegende Blöcke aufteilen“.  
- Eine terminierende Anweisung wird gefunden (Sprung, Rückkehr, Interrupt).  
- Die Befehlsdisassemblierung ist fehlgeschlagen.  
- Code-Padding wurde gefunden.

Nachfolgend ist ein Diagramm der Disassemblierung und des Eintrags in die Disassemblierungs-Warteschlange dargestellt (Code-Padding-Prüfung im Diagramm ausgelassen). Dies wird wiederholt, bis die Disassemblierungs-Warteschlange leer ist.

![](https://assets.kitploit.com/production/public/readmes/8091/7fe76b27e9f1f6ef620bc3aca182b85f9075c1dd31441e565099c05cfa396098.png)

Abbildung 2. Disassemblierungsverarbeitung.

### 2.2.1 Aufteilung grundlegender Blöcke

Wenn zwei grundlegende Blöcke überlappen, muss einer von ihnen aufgeteilt werden. Dadurch wird verhindert, dass zwei Blöcke dieselben Anweisungen beschreiben. Zum Beispiel:```asm  
wcslen proc  
    or      rax, 0FFFFFFFFFFFFFFFFh  
loc_140001078:  
    inc     rax  
    cmp     word ptr [rcx+rax*2], 0  
    jnz     short loc_140001078  
    retn  
wcslen endp  

Dies ist eine Implementierung von wcslen, die die Länge einer Breitzeichenkette ermittelt. Wenn die erste Anweisung 'or rax, FFFFFFFFFFFFFFFF' als Beginn eines Basisblocks disassembliert wird, wird sie weiter disassemblieren bis zum 'retn'.

Das 'jnz loc_140001078' springt zurück, um eine Schleife zu bilden. Dies wird als Referenz hinzugefügt, und das Ziel des jnz wird zusammen mit dem Fallthrough-Zweig (der nächsten Anweisung) zur Disassembly-Warteschlange hinzugefügt. Die Anweisung springt in die Mitte des bereits analysierten Blocks, sodass sie nicht einfach einen neuen Block bilden und erneut bis zum 'retn' disassemblieren kann, da dies eine doppelte Darstellung ergeben würde.

Das 'jnz' (bedingter Sprung) würde ebenfalls den Fallthrough-Zweig nehmen, um auch einen neuen Basisblock zu erstellen. Nun gäbe es 4 Basisblöcke, die so aussehen:

Block A:```asm
or rax, 0FFFFFFFFFFFFFFFFh
loc_140001078:
inc rax
cmp word ptr [rcx+rax*2], 0
jnz short loc_140001078

root@kitploit:~
Block B:```asm  
loc_140001078:  
    inc     rax  
    cmp     word ptr [rcx+rax*2], 0  
    jnz     short loc_140001078  

Block C:```asm
retn

root@kitploit:~
Block D:```asm  
    retn  

Dies ist eine Fehldarstellung der Basisblöcke, da sie dieselben Anweisungen über 4 Blöcke dupliziert. Dies kann behoben werden, indem alle Basisblöcke, die sich mit der aktuellen Disassemblierung überschneiden, aufgeteilt werden. Es wird keine duplizierten Anweisungen mehr geben, da statt einer duplizierten Darstellung der Anweisungen an jeder Überschneidung die vorhandenen Anweisungen in den neuen Basisblock übertragen werden. Die korrekte Darstellung unter Verwendung der Aufteilung ist wie folgt:

Block A:```asm
or rax, 0FFFFFFFFFFFFFFFFh

root@kitploit:~
Block B:```asm  
    inc     rax  
    cmp     word ptr [rcx+rax*2], 0  
    jnz     short loc_140001078  

Block C:```asm
retn

root@kitploit:~
### 2.2.2. Indirekter Kontrollfluss

#### 2.2.2.1. Sprunghülltabellen

Sprunghülltabellen werden verwendet, um die Adressen der Handler für switch-Anweisungen zu speichern. Anstatt viele if-Anweisungen/bedingte Sprünge für jeden Fall in einer switch-Anweisung zu haben, wird eine Tabelle mit den Adressen der Handler der case-Anweisungen geführt. Hier ist ein Beispiel (LLVM/CLANG Binary):```cpp  
std::int32_t sub_140004080(const std::int32_t a1)  
{  
  std::int32_t result;

  switch ( a1 )  
  {  
    case 0:  
      result = 9;  
      break;  
    case 1:  
      result = 4;  
      break;  
    case 2:  
      result = 3;  
      break;  
    case 3:  
      result = 1;  
      break;  
    default:  
      result = 0;  
      break;  
  }

  return result;  
}  

Diese Switch-Anweisung wird in den folgenden Assemblercode kompiliert:```asm
; ecx = a1
cmp ecx, 3 ; check if above bounds, must be default case
ja short def_140004097 ; goto default if a1 above 3
mov ecx, ecx
mov eax, ecx
lea rcx, jpt_140004097
movsxd rax, ds:(jpt_140004097 - 140004100h)[rcx+rax*4] ; select correct index of jump table for a1
add rax, rcx
jmp rax ; goto index specified - the case handler

jpt_140004097:
dd offset loc_140004099 - 140004100h ; address of handler for first case
dd offset loc_1400040D5 - 140004100h ; address of handler for second case
dd offset loc_1400040BD - 140004100h ; address of handler for third case
dd offset loc_1400040C9 - 140004100h ; address of handler for fourth case

root@kitploit:~
Der Wert 'a1' wird mit dem Maximalwert der Fälle verglichen. Liegt er darüber, wird direkt zum Standard-Handler gesprungen. Liegt a1 innerhalb des Fallbereichs, wird auf den entsprechenden Eintrag in der Sprungtabelle zugegriffen und zur berechneten Handler-Adresse gesprungen.

Die Sprungtabelleneinträge sowie die Verweise auf die Sprungtabelle werden nachverfolgt, damit sie intakt bleiben.

##### 2.2.2.1.1. Begrenzte Sprungtabellen

Wenn eine Sprungtabelle nicht alle Wertebereiche beschreibt, die eine switch-Anweisung verwenden kann, wird eine begrenzte Tabelle zur Überprüfung der Grenzen verwendet. Dadurch wird der switch bei Werten außerhalb der Grenzen zur default-Anweisung umgeleitet. Um die Anzahl der case-Anweisungen zu ermitteln, wird die Vergleichsanweisung überprüft. Beispielsweise zeigt die Anweisung 'cmp ecx, 3', dass die Sprungtabelle 3 Einträge enthält.

##### 2.2.2.1.2. Unbegrenzte Sprungtabellen

Wenn eine Sprungtabelle alle möglichen Werte der case-Anweisungen abdeckt (z.B. vom UINT8-Minimum bis zum UINT8-Maximum für den Typ UINT8), wird eine unbegrenzte Tabelle ohne Grenzprüfungen verwendet. Der Compiler weiß dann, dass die Sprungtabelle alle möglichen Werte abdeckt. Es gibt keine Vergleichsanweisung, die die Anzahl der Sprungtabelleneinträge andeutet, daher müssen die Einträge per Brute-Force ermittelt werden. Die Basis der Tabelle wird schrittweise auf gültige RVAs in einem Codeabschnitt geprüft, und jeder gültige Eintrag wird nachverfolgt. Dies ist nicht so sicher, da auch andere Daten/Anweisungen als Sprungtabelleneinträge interpretiert werden könnten – weshalb die Prüfung auf begrenzte Sprungtabellen bevorzugt wird, wenn möglich.

##### 2.2.2.1.3. Verschiedene Arten von Sprungtabellen

Der binäre Umschreiber unterstützt Sprungtabellen auf MSVC (einschließlich mehrstufiger Tabellen), LLVM/CLANG und GCC erstellten Binärdateien.

MSVC-Sprungtabellen gibt es in zwei Formen: normal und mehrstufig. Die normalen Sprungtabellen von MSVC sind ein Array von RVAs. Jede RVA zeigt auf den Handler der case-Anweisung.

Die mehrstufigen Tabellen von MSVC werden für switch-Anweisungen mit einer großen Anzahl von case-Anweisungen verwendet, die sich Handler teilen. Die mehrstufige Version hat zwei Tabellen: eine für das Array der Handler-RVAs und eine, um die case-Werte mit den Indizes der ersten Tabelle abzugleichen. Dies verhindert die Wiederholung der RVAs in der ersten Tabelle, da jeder case-Wert nur den 1-Byte-Index statt einer 4-Byte-RVA beschreiben muss. Hier ist ein Beispiel für eine mehrstufige Sprungtabelle:```asm  
lea     rdx, cs:140000000h  
movsxd  rax, edi ; load value  
movzx   eax, ds:(byte_1400023D8 - 140000000h)[rdx+rax] ; get handler index by value  
mov     ecx, ds:(jpt_14000209D - 140000000h)[rdx+rax*4] ; get RVA of handler by handler index  
add     rcx, rdx  
jmp     rcx

jpt_14000209D dd offset loc_14000209F - 140000000h  
dd offset loc_1400020AB - 140000000h  
dd offset loc_1400020B7 - 140000000h  
dd offset loc_1400020CF - 140000000h  
dd offset loc_1400020E7 - 140000000h  
dd offset loc_1400020F3 - 140000000h  
dd offset loc_1400020FF - 140000000h  
dd offset loc_14000210B - 140000000h  
dd offset loc_140002117 - 140000000h  
dd offset loc_140002123 - 140000000h  
dd offset loc_1400020C3 - 140000000h  
dd offset loc_14000213B - 140000000h  
dd offset loc_140002147 - 140000000h  
dd offset loc_140002153 - 140000000h  
dd offset loc_14000216B - 140000000h  
dd offset loc_140002177 - 140000000h  
dd offset loc_140002183 - 140000000h  
dd offset loc_14000219B - 140000000h  
dd offset loc_1400021A7 - 140000000h  
dd offset loc_1400021B3 - 140000000h  
dd offset loc_1400021BF - 140000000h  
dd offset loc_1400021CB - 140000000h  
dd offset loc_1400021D7 - 140000000h  
dd offset loc_1400021E3 - 140000000h  
dd offset loc_1400021EF - 140000000h  
dd offset loc_1400021FB - 140000000h  
dd offset loc_140002207 - 140000000h  
dd offset loc_140002213 - 140000000h  
dd offset loc_14000221F - 140000000h  
dd offset loc_14000222B - 140000000h  
dd offset loc_140002237 - 140000000h  
dd offset loc_140002243 - 140000000h  
dd offset loc_14000224F - 140000000h  
dd offset loc_14000225B - 140000000h  
dd offset loc_140002267 - 140000000h  
dd offset loc_140002273 - 140000000h  
dd offset loc_14000227F - 140000000h  
dd offset loc_14000228B - 140000000h  
dd offset loc_140002294 - 140000000h  
; ... more handler addresses

byte_1400023D8:  
db 0, 2Bh, 1, 2, 2Bh, 3, 2Bh, 4, 2Bh, 5, 6, 7, 8, 9, 0Ah, 0Bh, 0Ch, 0Dh  
db 2Bh, 0Eh, 0Fh, 10h, 2Bh, 11h, 12h, 13h, 14h, 15h, 2Bh, 16h, 2Bh, 17h  
db 18h, 19h, 1Ah, 1Bh, 1Ch, 2Bh, 1Dh, 1Eh, 1Fh, 20h, 21h, 22h, 2Bh, 23h  
db 24h, 25h, 26h, 2Bh, 2Bh, 2Bh, 2Bh, 2Bh, 2Bh, 2Bh, 2Bh, 2Bh, 2Bh, 2Bh  
; ... more indexes to handler table  

LLVM verwendet eine Tabelle, die Offsets relativ zur Basis der Tabelle enthält. Durch Addition der Adresse der Basis der Tabelle mit dem im Eintrag beschriebenen Offset kann die Handler-Adresse berechnet werden.

GCCs Sprungtabelle ist ein Array von DIR64-Relokationen. Jede Relokation zeigt auf die Adresse einer Case-Anweisung. Relokationen werden bereits verfolgt, sodass diese Sprungtabelleneinträge bereits repariert sind. Zur Laufzeit werden diese Relokationseinträge um die Basisadresse versetzt, sodass jeder Eintrag dereferenziert werden kann, um die Adresse der Case-Anweisung zu erhalten.

2.2.2.2. Randfälle

Es gibt einige andere Formen von indirektem Kontrollfluss, die unterstützt werden müssen. Zum Beispiel wird bei CLANG-Binärdateien, die FuncInfo3 C++-Ausnahmen verwenden, die Fortsetzungsadresse in das Register rax geladen und dann an den Aufrufer zurückgegeben. Der Aufrufer springt zur Fortsetzungsadresse.```asm
lea rax, [rip+X]
retn

root@kitploit:~
Selbst wenn die Adresse, auf die das lea zeigt, in einem Codeabschnitt liegt, gibt es keine Garantie, dass es sich tatsächlich um Code handelt. Sprungtabellen und Zeichenketten können aus Gründen der Cache-Lokalität in den Codeabschnitten einer Binärdatei platziert werden. Die korrekten Codepfade müssen entdeckt und disassembliert werden, um die darin enthaltenen RVA-Referenzen verfolgen (sowie verschleiern) zu können. Daher ist es entscheidend, dass diese Fälle identifiziert werden können. Dies sind 'riskante' Referenzen.

Um zu beheben, dass Sprungtabellen fälschlicherweise zur Disassemblierungswarteschlange hinzugefügt werden, prüft der Disassembler zuerst, ob es sich um eine Sprungtabelle handelt, bevor diese riskanten Referenzen zur Warteschlange hinzugefügt werden.

Um zu beheben, dass Zeichenketten per lea geladen und fälschlicherweise zur Disassemblierungswarteschlange hinzugefügt werden, versucht der Disassembler, sie als einen Basisblock mit zusätzlichen Plausibilitätsprüfungen zu disassemblieren (z. B. muss eine Abschlussanweisung vorhanden sein, wenn es sich um eine dieser riskanten Referenzen handelt). Wenn eine dieser Plausibilitätsprüfungen bei einem riskanten Referenzblock fehlschlägt, wird der gesamte Block ignoriert und als Daten angenommen.

Wenn eine Symboldatei bereitgestellt wird, wird kein Datensymbol als riskante Referenz hinzugefügt.

### 2.2.3. Functions

Einige Verschleierungspässe erfordern Wissen darüber, welche Basisblöcke zu welchen Funktionen gehören. Aus diesem Grund werden alle Basisblöcke den entsprechenden Funktionen zugeordnet, die sie besitzen. Symboldateien werden analysiert, um alle Funktionsadressen zu finden und in eine Liste aufzunehmen.

Für jede Funktion werden die folgenden Schritte ausgeführt:

- Holen Sie sich den Einstiegsbasisblock der Funktion (den Basisblock an der Startadresse der Funktion).  
- Weisen Sie diesen Einstiegsblock der Funktion zu.  
- Finden Sie alle Ausgänge des Basisblocks (Fallthrough-Verzweigungen, Zielverzweigungen).  
- Weisen Sie für jeden Ausgangsbasisblock diesen der Funktion zu, wenn er nicht der Einstiegsblock einer anderen Funktion ist (Basisblock-RVA != jede Funktions-RVA). Diese letzten beiden Schritte werden für jeden entdeckten Basisblock wiederholt.  
- Alle Sprungtabellen in den entdeckten Blöcken werden analysiert und ihre Zielbasisblöcke werden der Funktion zugewiesen.

### 2.2.4. 'Noreturn'-Aufrufbehandlung

Noreturn-Funktionen kehren nicht zurück. Wenn ein noreturn-Aufruf erfolgt, wird der Basisblock weiterhin disassembliert, da Aufrufe Basisblöcke nicht beenden. Es wird nicht erwartet, dass die Binärdatei nach dem Aufruf ausgeführt wird, daher hat der Compiler keinen geeigneten Code eingefügt, der den Basisblock beendet. Die zuvor genannten Disassemblierungsprüfungen werden diese Fälle wahrscheinlich abfangen und den Basisblock beenden.```asm  
sub_140006310 proc  
    sub     rsp, 38h  
    mov     rax, cs:__security_cookie  
    xor     rax, rsp  
    mov     [rsp+38h+var_8], rax  
    mov     [rsp+38h+pExceptionObject], 2Ah  
    lea     rdx, __TI1H  
    lea     rcx, [rsp+38h+pExceptionObject]  
    call    _CxxThrowException  
    db 0CCh  
sub_140006310 endp  
algn_14000633D:  
    align 20h  

Zum Beispiel hat der Compiler bei diesem noreturn _CxxThrowException-Aufruf Padding in Form von INT3-Anweisungen eingefügt, die sowohl als Padding als auch als terminierende Anweisung erkannt würden. Es gibt noch einige andere Fälle, wie etwa UD2-Anweisungen, die nach einem noreturn-Aufruf eingefügt werden und ebenfalls behandelt werden.

Dies ist keine unfehlbare Methode, dies zu erkennen, wie das CodeDefender-Team ebenfalls diskutiert, aber es sind Gegenmaßnahmen vorhanden, um eine zu weit fortgeschrittene Disassemblierung zu korrigieren. Wenn innerhalb eines Basisblocks Sprungtabelleneinträge gefunden werden, wird der Basisblock aufgeteilt, sodass die Sprungtabelle Priorität erhält. Dadurch wird verhindert, dass Sprungtabelleneinträge nach einem noreturn-Aufruf als Anweisungen disassembliert werden. Wenn die nächsten Anweisungen gültiger Code sind, würde der Basisblock ohnehin aufgeteilt werden, wenn der Basisblock der nächsten Adresse disassembliert/verarbeitet wird.

2.3. Exceptions-Unterstützung

2.3.1. Unwind-Unterstützung

Der Obfuscator nutzt in seinen Obfuscation-Pässen Stack-Allokationen. Dies ermöglicht es ihm, den Wert von verwendeten Registern zu speichern (z. B. push rax) und nach Abschluss des Obfuscation-Passes wiederherzustellen, sodass Register nicht überschrieben werden. Ein Frame-Pointer ist ein Register, das auf eine bestimmte Position im Stack zeigt.

Alle Stack-Allokationen sollten durch die Unwind-Codes einer Funktion beschrieben werden (sofern kein Frame-Pointer vorhanden ist). Dadurch kann der OS-Exception-Handler den Stack bis zur Rücksprungadresse zurückverfolgen und nach dem Exception-Handler der Anwendung suchen. Diese Stack-Allokationen müssen sich im Prolog (Anfang der Funktion) befinden, da es eine Begrenzung gibt, wie weit sie vom Funktionsanfang entfernt sein dürfen. Werden Stack-Allokationen außerhalb des Prologs vorgenommen, können die Unwind-Codes sie nicht beschreiben und das OS könnte den Stack nicht zurückverfolgen, was die Exceptions-Unterstützung beeinträchtigen würde.

Wird ein Frame-Pointer verwendet, muss das OS den Stack nicht vom rsp-Register aus zurückverfolgen können, sondern kann stattdessen die Rückverfolgung vom Frame-Pointer aus durchführen. Das bedeutet, dass der Obfuscator Stack-Allokationen außerhalb des Prologs vornehmen kann, ohne diese in Unwind-Codes beschreiben zu müssen.

2.3.1.1 Implementierung

Der Rewriter fügt in jede Laufzeitfunktion, die noch keinen besitzt, ein Frame-Pointer-Register ein. Dadurch kann das OS den Stack auch dann zurückverfolgen, nachdem der Obfuscator Stack-Allokationen außerhalb des Prologs vorgenommen hat.

Nachfolgend ein Beispiel der Änderungen, die am Prolog und Epilog der Funktion vorgenommen werden:

Ursprünglicher Funktionsprolog:```asm
DriverEntry proc
sub rsp, 38h

root@kitploit:~
Modifizierter Funktions-Prolog:```asm  
DriverEntry proc  
    push    rbp  
    push    rbp  
    sub     rsp, 38h  
    lea     rbp, [rsp]  
    lea     rbp, [rsp]  

Originaler Funktionsepilog:```asm
add rsp, 38h
ren
DriverEntry endp

root@kitploit:~
Modifizierter Funktions-Epilog:```asm  
    add     rsp, 38h  
    pop     rbp  
    pop     rbp  
    retn
DriverEntry endp  

Abbildung 3. Stack-Layout vor dem Einfügen des Frame-Pointers.

Das nichtflüchtige Register rbp wird vom Rewriter als Frame-Pointer verwendet. Nichtflüchtige Register müssen erhalten bleiben, daher wird der Wert von rbp im Prolog auf den Stack gelegt und durch Unwind-Codes beschrieben (damit der OS-Unwinder den ursprünglichen Wert von rbp wiederherstellen kann). Das Register rbp wird 2-mal gepusht, um den Stack auf 16 Byte auszurichten; der zweite Push dient ausschließlich der Ausrichtung.

Da der Wert von rbp im Prolog zweimal gepusht wird, muss er am Ende der Funktion auch zweimal gepoppt werden, um den Stack-Pointer wieder auf seinen ursprünglichen Wert zu setzen. Nur so befindet sich die Rücksprungadresse für die Return-Instruktion an [rsp].

Für diese Push-Instruktionen werden die entsprechenden Unwind-Codes eingefügt, damit das OS weiß, dass im Prolog weitere Stack-Allokationen vorhanden sind (von denen der Frame-Pointer entrollt werden kann). Deshalb gibt es im Epilog 2 Pop-Instruktionen.

Um diese Exit-Basisblöcke/Epiloge zu finden, werden alle Basisblöcke einer Funktion nach einem 'ret' oder einem Sprung, der aus der aktuellen Funktion herausführt, durchsucht. Indirekte Sprünge (z. B. jmp rcx) gelten ebenfalls als Sprünge aus der aktuellen Funktion heraus, mit Ausnahme von Sprungtabellen. Nachdem alle Exit-Basisblöcke gruppiert wurden, können die 2 Pop-Instruktionen in sie eingefügt werden, um sicherzustellen, dass die Auswirkungen der Push-Instruktionen im Prolog beim Verlassen der Funktion rückgängig gemacht werden.

Das 'lea rbp, [rsp]' am Ende des Prologs dient dazu, dem OS eine konkrete Position des Stacks mitzuteilen, die es dann entrollen kann, anstatt es von rsp aus zu entrollen. Die entsprechenden Unwind-Info- und Unwind-Codes werden für diese Frame-Pointer-Setz-Instruktion eingefügt.

Abbildung 4. Falsches Stack-Layout nach Einfügen des Frame-Pointers.

Eine weitere Überlegung betrifft Stack-Argumente, die sich vor dem Stack-Pointer befinden. Wenn innerhalb einer Funktion auf den Stack nach der lokalen Allokation (an der gleichen oder einer späteren Stelle als dem Slot der Rücksprungadresse) zugegriffen wird, müssen diese Referenzen aktualisiert werden. Da die Push-Instruktionen den Stack um 16 Byte verschieben, müssen alle darauf folgenden referenzierten Daten ebenfalls um 16 Byte verschoben werden. Das obige Diagramm zeigt, wie die Stack-Referenzen falsch ausgerichtet werden und korrigiert werden müssen.

Zum Beispiel würde 'mov rax, [rsp+0x90]' um 0x10 (dezimal: 16) erhöht werden, sodass es nach Ausführung der Push-Instruktionen immer noch auf denselben Stack-Slot zugreift. Die korrigierte Instruktion wäre: 'mov rax, [rsp+0xA0]'.

Wenn eine Instruktion auf einen Bereich jenseits der lokalen Stack-Allokation zugreift, wird sie um 16 angepasst, sodass sie die Push-Instruktionen auf dem Stack überspringt. Manchmal wird der Stack-Pointer in andere Register verschoben und über ein anderes Register angesprochen; in diesem Fall wird dieses Register überwacht und in gleicher Weise wie der Stack-Pointer angepasst.

Ein weiterer Fall sind Catch-Handler für Ausnahmen: Sie erhalten in rdx die Adresse des EstablisherFrame (entspricht dem Wert unseres Frame-Pointer-Registers zum Zeitpunkt der Ausnahme). Die Catch-Handler greifen über rdx auf den Stack der Ausnahmefunktion zu; daher muss auch rdx verfolgt und angepasst werden.

Unten ist das Diagramm des Stack-Layouts nach der Korrektur durch die Stack-Referenz-Anpassungen.

Abbildung 5. Korrigiertes Stack-Layout nach Einfügen des Frame-Pointers.

Die Unterstützung von Ausnahmen erfordert, dass dem Obfusaktor eine Symbol-Datei zur Verfügung gestellt wird, da möglichst viele Informationen über die Symbole der Binärdatei benötigt werden.

2.3.2. Ausnahmeinformations-Parsing

Der Rewriter parst die Unwind-Info einer Binärdatei, um Informationen zu Ausnahmehandlern zu finden. Alle dabei gefundenen RVAs werden ebenfalls verfolgt. Die unterstützten Arten von Ausnahmehandler-Informationen sind:

  • SEH/C_SCOPE_TABLE (C-artige Ausnahmen).
  • C++-Ausnahmen der Typen FuncInfo3 und FuncInfo4.

2.3.2.1. SEH/C_SCOPE_TABLE

Das Format hierfür ist ein Array der folgenden Tabelleneinträge:```cpp
struct c_scope_table_entry_t
{
std::uint32_t begin_rva; // where exception-throwing range begins
std::uint32_t end_rva; // where exception-throwing range ends
std::uint32_t handler_rva; // the handler type/rva (normally 1)
std::uint32_t target_rva; // the catch handler rva
};

struct c_scope_table_t
{
std::uint32_t entry_count;
c_scope_table_entry_t table[1];
};

root@kitploit:~
Die begin/end-RVAs beschreiben, welcher Codebereich eine Ausnahme auslösen kann. Die Ziel-RVA beschreibt den Catch-Handler, der die Ausnahme verarbeitet, wenn sie auftritt.

#### 2.3.2.2. FuncInfo3 und FuncInfo4

Verwendet für C++-Ausnahmen. Der Hauptunterschied zwischen FH3 und FH4 besteht darin, dass FH4 ein komprimiertes Format verwendet, um Speicher zu sparen. Sie teilen sich die folgenden Deskriptoren:

- Unwind-Map – Liste von C++-Objekten, die zerstört werden müssen, sowie der Offset des Objekts vom Frame.  
- Try-Block-Map – Liste von Catch-Handlern und den Typen, die jeder abfangen kann (z. B. std::runtime_error).  
- IP2State-Map – beschreibt den Zustand von Objekten in Abhängigkeit vom aktuellen Befehlzeiger bzw. dem Offset in der Funktion.

FH3-Spezifika:

- Die Fortsetzungsadresse in der Try-Block-Map wird im Code aufbewahrt und vom Catch-Handler in rax zurückgegeben (z. B. lea rax, continuation_address).

FH4-Spezifika:

- Speichert Map-Informationen in einem komprimierten Integer-Format, um Platz zu sparen.  
- Die Try-Block-Map enthält die Fortsetzungsadresse, die in der FH4-Informationsstruktur codiert ist.

Für C++-Ausnahmen werden nur MSVC und CLANG/LLVM unterstützt. GCC wird für C++-Ausnahmen nicht unterstützt, da es ein anderes Format als FH3/FH4 verwendet.

### 2.3.3. RTTI- und ThrowInfo-Analyse

Runtime Type Information (RTTI) und Throw-Info sind Strukturen, die verwendet werden, um C++-Typen zur Laufzeit zu untersuchen, einschließlich zum Auslösen von Ausnahmen. Diese Strukturen enthalten viele RVAs und müssen daher aus Stabilitätsgründen verfolgt werden.

#### 2.3.3.1. RTTI

Die 'Try-Block'-Map in den C++-Ausnahme-Deskriptoren enthält Typinformationen, um zu wissen, ob sie den ausgelösten Typ abfangen. Diese Typinformationen werden RTTI genannt und beschreiben auch andere Dinge über den Typ, wie zum Beispiel:

- Virtuelle Funktionstabellen.  
- Typname.  
- Erbende Klassen.

Für Klassen ohne virtuelle Funktionen wird lediglich ein Typ-Deskriptor generiert:```cpp  
struct type_descriptor_t  
{  
	std::uint64_t vftable_address; // this is a DIR64 relocation  
	std::uint64_t unk;  
	char name[1];  
};  

Dies wird gefunden, indem Datenabschnitte nach der DIR64-Relocation am Member-Feld 'vftable_address' durchsucht werden, das überprüft wird, ob es sich um eine echte virtuelle Funktionstabelle handelt.

Für Klassen mit virtuellen Funktionen werden ein complete object locator und ein class hierarchy descriptor generiert. Der class hierarchy descriptor enthält ein Array von Basisklassen.```cpp
struct complete_object_locator_t
{
std::uint32_t signature;
std::uint32_t offset;
std::uint32_t constructor_offset;
std::uint32_t type_rva;
std::uint32_t hierarchy_rva;
std::uint32_t self_rva;
};

struct hierarchy_descriptor_t
{
std::uint32_t signature;
std::uint32_t attributes;
std::uint32_t base_class_count;
std::uint32_t base_class_list_rva;
};

struct base_class_array_t
{
std::uint32_t class_rvas[1];
};

struct base_class_descriptor_t
{
std::uint32_t type_rva;
std::uint32_t element_count;
std::uint32_t member_displacement;
std::uint32_t unk;
std::uint32_t unk1;
std::uint32_t attributes;
std::uint32_t hierarchy_rva;
};

root@kitploit:~
Um diese zu finden, werden Datensektionen nach DIR64-Relokationen durchsucht, die auf einen vollständigen Objektlocator verweisen. Es werden Überprüfungen am Ziel der DIR64-Relokation durchgeführt, um sicherzustellen, dass es auf einen vollständigen Objektlocator zeigt (z. B. ob self_rva auf die RVA der Basisklasse verweist, ob die Typdeskriptoren und Hierarchiedeskriptoren korrekt parsen).

#### 2.3.3.2. ThrowInfo

ThrowInfo wird verwendet, um zu beschreiben, wie das Exception-Objekt zerstört wird, nachdem es verarbeitet wurde, sowie den ausgelösten Typ. Die geerbten Klassen des ausgelösten Exception-Typs werden im catchable type array (enthält RTTI-Referenzen) beschrieben, um sicherzustellen, dass der Exception-Handler sie mit den catch-Anweisungen abgleichen kann.

Das ThrowInfo wird in Datensektionen gescannt, indem der Inhalt des catchable type array mit den zuvor entdeckten RTTI-Informationen überprüft wird. Alle RVAs der catchable types und des ThrowInfo werden zur Tracking-Liste hinzugefügt.```cpp  
struct throw_info_t  
{  
	std::uint32_t attributes;  
	std::uint32_t pmfn_unwind; // address of exception object destructor  
	std::uint32_t forward_compat;  
	std::uint32_t catchable_type_array;  
};

struct catchable_type_array_t  
{  
	std::uint32_t count;  
	std::uint32_t type_rvas[1];  
};

struct catchable_type_t  
{  
	std::uint32_t attributes;  
	std::uint32_t rva_type;  
	std::uint32_t mdisp;  
	std::uint32_t pdisp;  
	std::uint32_t vdisp;  
	std::uint32_t size_of_thrown_object;  
	std::uint32_t optional_copy_constructor_rva;  
};  

3. Verschleierung

3.1. Virtuelle Maschine

Diese Technik nimmt Instruktionen der x86-64-Architektur und übersetzt sie in eine virtuelle CPU-Architektur. Dies ist wesentlich schwieriger zu analysieren, da ein Reverse Engineer zunächst die virtuelle CPU-Architektur verstehen müsste, bevor er analysieren kann, was die ursprünglichen Instruktionen tun.

Diese Implementierung verwendet einen generischen Ansatz zur Generierung von Virtual-Machine-Handlern, sodass ein breites Spektrum an Instruktionen verschleiert werden kann, ohne für jede x86-64-Instruktion fest codierte Handler zu benötigen.



Abbildung 6. Architektur der virtuellen Maschine.

Die ursprüngliche Sequenz von Instruktionen, die virtualisiert werden, wird durch einen Aufruf des Einstiegsblocks der virtuellen Maschine ersetzt.

Beim Eintritt in die virtuelle Maschine werden alle Allzweckregister (außer rsp) auf den Stack gelegt. Das rflags-Register wird ebenfalls auf den Stack gelegt. Diese Stack-Slots werden als virtuelle Register verwendet, die jeweils ihrem ursprünglichen Register entsprechen (der Slot für rax würde also anstelle von rax verwendet). Die Reihenfolge dieser virtuellen Register im Stack ist randomisiert, sodass sich das Registerlayout jedes Virtual-Machine-Handlers ändert.

Die Push-Instruktionen, um die Allzweckregister im virtuellen Stack-Layout zu platzieren, sind ebenfalls randomisiert und können ein 'sub rsp, 8; mov [rsp] reg' oder ein 'push reg' sein. Dies geschieht, um es schwieriger zu machen, Signaturen für den Einstieg in die virtuelle Maschine zu erstellen.

Hardware-Register sind die Allzweckregister der x86-64-Architektur. Nachdem die Hardware-Register in ihrem virtuellen CPU-Zustand auf dem Stack gespeichert wurden, können sie frei überschrieben werden. Der Zustand der virtuellen Maschine verfolgt eine Liste der derzeit verfügbaren Hardware-Register, die von den Virtual-Machine-Stubs verwendet werden können. Sobald der Stub abgeschlossen ist, werden diese Hardware-Register wieder zur Liste hinzugefügt, sodass sie wiederverwendet werden können.

Nun ist die Anwendung in die virtuelle Maschine eingetreten und es ist an der Zeit, die Ausführung an die Handler für die Zielinstruktionen zu übergeben. Zielinstruktionen sind die x86-64-Instruktionen, die in diese CPU-Architektur virtualisiert werden.

Zuerst müssen die Operanden für die Zielinstruktion auf den Stack geladen werden. Die Werte der Operanden werden in ein freies Hardware-Register geladen. Die Werte der Operanden werden dann verschleiert und auf den Stack gelegt. Dies geschieht vom vorhergehenden Block zum Instruktionshandler (vorheriger Handler oder der Einstiegsblock der virtuellen Maschine, falls dies der erste Handler ist).

Die auf die Operandenwerte angewandte Verschleierung ist die folgende:

  • Zufällige 16-Bit-XOR-Operation auf den Operandenwert.
  • Einerkomplement-Negationsoperation auf den Operandenwert.

Bei unmittelbaren Operanden kann diese Verschleierung zum Zeitpunkt der Verschleierung durchgeführt werden, da der Wert bekannt ist, sodass die Berechnung nicht zur Laufzeit erfolgt und daher schwerer zu analysieren ist.

Versteckte Operanden, die bestimmte Register erfordern (z. B. rsi und rdi für 'rep movsb'), werden in diese bestimmten Register geladen, anstatt in ein zufälliges Hardware-Register.

Im Instruktionshandler-Block werden die Operanden vom Stack geholt und entschleiert. Die umgekehrte Operation wird ausgeführt, um die ursprünglichen Werte der Operanden zu erhalten.

Wenn die ursprüngliche Instruktion das rflags-Register liest, wird rflags vor der Ausführung der Instruktion aus dem Stack-Kontext geladen.

Wenn die ursprüngliche Instruktion in das rflags-Register schreibt, wird rflags vor der Ausführung der Instruktion aus dem Stack-Kontext geladen. Nach der Ausführung der ursprünglichen Instruktion wird das aktualisierte rflags zurück in den Stack-Kontext geschrieben.

Dies stellt sicher, dass virtualisierte Instruktionen das exakt gleiche Flag-Verhalten wie die ursprünglichen Instruktionen haben.

Innerhalb des Instruktionshandler-Blocks wird die ursprüngliche x86-64-Instruktion so codiert, dass sie entschleierte Operanden verwendet. Sobald die ursprüngliche Instruktion ausgeführt wird, werden die Ergebnisoperanden verschleiert und auf den Stack gelegt.

Wenn dies der letzte Instruktionshandler ist, wird der nächste Basisblock der Ausstiegsblock der virtuellen Maschine sein. Andernfalls wird der nächste Block vom nächsten Handler stammen.

Der nächste Basisblock holt die verschleierten Ergebnisoperanden vom Stack und wendet denselben Entschleierungsprozess an. Die Ergebniswerte werden an ihr ursprüngliches Ziel geschrieben. Dies könnte ein virtuelles Register im Stack-Kontext oder eine bestimmte Speicherstelle sein, die durch die ursprüngliche Instruktion beschrieben wird.

Dieser Prozess wiederholt sich, bis eine Instruktion im Basisblock nicht virtualisiert werden kann (z. B. eine Instruktion, die das rsp-Register verwendet).

Der Kontext der virtuellen Maschine muss dann entladen werden, um zum nicht-virtualisierten Code zurückzukehren. Alle virtuellen Register werden vom Stack in ihre entsprechenden Allzweckregister geholt. Das modifizierte rflags-Register wird ebenfalls durch Abholen vom Stack wiederhergestellt. Nun kehrt der Ausstiegsblock der virtuellen Maschine zum Aufrufer zurück.

3.3.1 Unwind-Unterstützung

Wenn eine virtualisierte Instruktion eine Ausnahme auslöst, muss das Betriebssystem in der Lage sein, aus dem Kontext der virtuellen Maschine zu entwinden, um einen geeigneten Ausnahmehandler in den Aufrufern zu finden.

Um dies zu ermöglichen, müssen dem Binärprogramm Unwind-Informationen hinzugefügt werden, damit das Betriebssystem das Stack-Layout der Virtual-Machine-Funktion kennt.

Ein Frame-Pointer wird in rbp geladen, da die Virtual-Machine-Handler Stack-Allokationen außerhalb des Prologs nutzen. Dies bedeutet, dass rbp nicht als 'verfügbares' Hardware-Register von den Virtual-Machine-Handlern verwendet werden darf.

Alle verwendeten Hardware-Register der virtuellen Maschine werden auf den Stack-Kontext gelegt, sodass die entsprechenden Unwind-Codes für diese Push-Vorgänge eingefügt werden.

Die Laufzeitfunktion für die Virtual-Machine-Funktion wird dann in das Ausnahmenverzeichnis eingefügt. Die virtuelle Maschine ist nun abwickelbar.

3.2. Undurchsichtige Prädikatsblöcke

Diese Technik erstellt Verzweigungen zu gefälschten Basisblöcken mit inkorrektem Datenfluss, um einen Reverse Engineer zu verwirren.

Undurchsichtige Prädikate sind Anweisungen, die nur zu true oder false ausgewertet werden.

Basisblöcke werden dupliziert und in eine if-Anweisung mit undurchsichtigem Prädikat eingewickelt. Einer der Blöcke wird einen verzerrten Datenfluss haben, sodass er ähnlich, aber inkorrekt ist.```cpp
if (opaque_statement_always_true)
{
… original basic block
}
else
{
… incorrect but similar basic block
}

root@kitploit:~
Um einen Reverse Engineer weiter in die Irre zu führen, werden alle Instruktionsoperanden gesammelt und randomisiert. Jede Anweisung wird mit zufälligen Operanden aus der gesammelten Liste neu kompiliert. Dadurch wird sichergestellt, dass sich das Verhalten des duplizierten Blocks vom Original unterscheidet.

Die Position der Blöcke wird ebenfalls zufällig gemischt, sodass die physische Position im Speicher nicht verrät, welcher der korrekte Zweig ist.

Die für die Zweigauswahl erforderliche Bedingung wird ebenfalls zufällig gewählt. Beispielsweise führt in einer Iteration der Durchfall-Zweig des bedingten Sprungs zum korrekten Block. In einer anderen Iteration führt der Zielzweig des bedingten Sprungs zum korrekten Block. Dies erschwert das Finden des korrekten Zweigs.

Der Ausdruck des opaken Prädikats selbst ist ebenfalls sehr wichtig, denn wenn er leicht auszuwerten wäre, wäre er nicht effektiv. Aus diesem Grund wurde der [Große Fermatsche Satz](https://en.wikipedia.org/wiki/Fermat's_Last_Theorem) als opaker Ausdruck gewählt. Der Große Fermatsche Satz “besagt, dass es keine drei positiven ganzen Zahlen a, b und c gibt, die die Gleichung a^n + b^n = c^n für einen ganzzahligen Wert von n größer als 2 erfüllen” (wobei ^ Potenz bedeutet). Es wurde bewiesen, dass dieser Ausdruck unter den gegebenen Bedingungen immer falsch ist. Die drei Zahlen a, b und c werden ausgewählt und mit einer zufällig gewählten Potenz von 3 bis 7 potenziert. Der Ausdruck wird in einem neu erstellten Basisblock ausgeführt und dann wird der bedingte Sprung zum korrekten Block genommen.

Wenn die Parameter a, b, c und n bekannt wären, könnte ein Angreifer den Ausdruck lösen und den korrekten Zweig finden. Um dies zu erschweren, werden die Parameter aus Werten gewählt, die zur Laufzeit bestimmt werden: dem Stack-Pointer (rsp) und dem Instruktionszeiger (ASLR bedeutet, dass der rip zur Laufzeit neu positioniert wird). Zum Beispiel:```cpp  
if ((rsp^n) + (rip^n) != (c^n))  
{  
     … incorrect but similar basic block  
}  
else  
{  
    … original basic block  
}  

3.3. Kontrollflussabflachung

Der Kontrollfluss sind die Ausführungspfade, die ein Programm nimmt (z.B. 'if-Anweisungen'). Eine Änderung des Kontrollflusses tritt auf, wenn ein Basisblock zu einem anderen springt. Diese Kontrollflussänderungen können in einem einzigen Dispatcher-Stub zusammengefasst und neu angeordnet werden, um das Verständnis zu erschweren. Der Dispatcher-Stub ist dann dafür verantwortlich, den Kontrollfluss zum nächsten Basisblock zu ändern, anstatt dass dies direkt geschieht.

Abbildung 7. Kontrollflussabflachung.

Alle Basisblöcke einer Funktion (außer dem Prolog) werden in einer Liste zusammengefasst und alle ihre Verzweigungen (bedingt, unbedingt) werden gesammelt. Jeder Basisblock erhält eine eindeutige ID/Kennung. Ein Dispatcher-Stub nimmt alle möglichen Verzweigungen und erstellt eine switch-Anweisung mit allen IDs als Fälle. Die case-Anweisungen springen zum Zielbasisblock. Alle Sprünge zu den ursprünglichen Basisblöcken/zum ursprünglichen Kontrollfluss werden durch einen Sprung zur switch-Anweisung mit der korrekten ID (der ID des Zielblocks) ersetzt.

Das physische Layout der Basisblöcke wird durcheinandergewürfelt, sodass ihre Speicherposition keine Hinweise auf den ursprünglichen Kontrollfluss gibt.

3.4. Lineare Substitution

Diese Technik nimmt alle in einer Anweisung codierten Zahlen (Speicheroperanden-Verschiebungen, unmittelbare Operanden) und verbirgt ihren wahren Wert, indem sie zur Laufzeit berechnet werden.

Zum Zeitpunkt der Verschleierung wird ein Zufallszahlenwert mit derselben Bitbreite wie die ursprüngliche Zahl generiert. Dieser wird zur ursprünglichen Zahl addiert und als unmittelbarer Operand in ein ungenutztes Register geladen.

Zur Laufzeit wird die Zufallszahl vom Register subtrahiert, was die ursprüngliche Zahl ergibt.

Wenn R die Zufallszahl und N die ursprüngliche Zahl ist, ist der Ausdruck effektiv ((R+N) - R).

Um Speicheroperanden neu zu codieren, wird die Registereinfügung am Basisoperanden durchgeführt. Wenn bereits ein Basisoperand vorhanden ist, wird dessen Wert zusätzlich addiert.

Bei der Substitution von Stack-Speicheroperanden wird eine Verschiebung zum Wert hinzugefügt, um die durch die Pushs verursachte Stack-Verschiebung zu berücksichtigen. Die Pushs dienen zum Sichern des rflags-Registers sowie des ungenutzten Registers.

Hier ist ein Beispiel für die Substitution des Speicheroperanden in 'mov [rsp+24h], 0':```asm
push r11
pushfq ; save flags for now
mov r11, rsp
add r11, 1D4D9F71h
sub r11, 1D4D9F3Dh
popfq ; restore flags, the original instruction will now execute and populate flags with the correct values
mov dword ptr [r11], 0 ; original instruction substituted (r11 == rsp+24h)
pop r11

root@kitploit:~
Hier ist ein weiteres Beispiel für die Substitution des unmittelbaren Operanden in 'add rbx, 8':```asm  
push    r10  
pushfq  
mov     r10, 0FFFFFFFFE3F78112h  
sub     r10, 0FFFFFFFFE3F7810Ah  
popfq  
add     rbx, r10  
pop     r10  

3.5. Gemischte boolesche Arithmetik

Diese Technik nimmt reguläre arithmetische Ausdrücke (z. B. x+y) und wandelt sie in komplexere Ausdrücke um, die dasselbe Ergebnis liefern. Dies geschieht, indem Ausdrücke durch gleiche lineare Identitäten ersetzt werden.

Zum Beispiel hat (x+y) die gleiche Identität ((x & y) + (x | y)). Wenn der Obfuskator (x+y) verarbeitet, ersetzt er es durch eine dieser Identitäten, die schwerer zu reverse-engineeren sind.

Es gibt eine Liste von Identitäten für die folgenden arithmetischen Anweisungen: 'add', 'sub', 'and', 'or', 'xor'. Das bedeutet, dass für jede dieser Anweisungen eine komplexere, zufällige Ersetzung gewählt wird, um sie zu ersetzen. Die zufällige Auswahl der Identitäten aus einer Liste stellt sicher, dass jede Obfuskationsausgabe unterschiedlich ist.

Um die Deobfuskation zu erschweren, wird dies rekursiv angewendet. Dies führt dazu, dass jede ersetzte Identität mehrfach neu ersetzt wird, was die Komplexität exponentiell wachsen lässt. Unten ist ein Beispiel für die Transformation von (x+y) nach 2 Durchläufen dieser Technik:

Abbildung 8. Gemischte boolesche Arithmetik.

Ein zu berücksichtigender Aspekt ist das Ergebnis der Flag-Berechnung der Anweisung. Die ursprüngliche Anweisung hätte ein spezifisches Flag-Verhalten auf das Flags-Register angewendet, das aufgrund der Aufteilung des Ausdrucks in verschiedene Operationen/Anweisungen nicht mehr dasselbe ist. Um dies zu lösen, emuliert der Obfuskator das Flag-Verhalten, indem er einen Stub einfügt, der die korrekten Flags berechnet und anwendet.

Für 'and', 'or', 'xor' ist das Flag-Verhalten dasselbe wie bei der 'test'-Anweisung; durch das Einfügen einer test-Anweisung mit denselben Operanden werden die Flags für diese drei ersetzten Anweisungen korrekt emuliert.

Für 'sub' hat die 'cmp'-Anweisung dasselbe Flag-Verhalten und kann mit denselben Operanden zur Flag-Berechnung eingefügt werden.

Für 'add' können SF, ZF, PF durch 'test' berechnet werden, aber CF, AF und OF müssen manuell berechnet werden. Dann wird ein Stub eingefügt, der CF, AF und OF für die add-Anweisung manuell berechnet.

Diese Flag-Emulations-Stubs geben Hinweise auf die ursprüngliche Anweisung, daher werden sie nur verwendet, wenn es unbedingt notwendig ist. Der gesamte Basisblock wird nach Anweisungen durchsucht, die Flags lesen und Flags schreiben. Die Bedingungen für das Hinzufügen der Flag-Emulation sind wie folgt:

  • Die MBA-Anweisung ist die letzte Flag-schreibende Anweisung vor einer Flag-lesenden Anweisung.
  • Die MBA-Anweisung ist die letzte Flag-schreibende Anweisung im Basisblock.

Dies stellt sicher, dass die Flags immer dann aktuell gehalten werden, wenn das Ende eines Basisblocks erreicht wird oder die Flags gelesen werden. Dies verhindert, dass Bedingungen (z. B. if-Anweisungen) die falschen Verzweigungen nehmen.

4. Bauen

Nachfolgend finden Sie Beispielbefehle zum Erstellen des Projekts mit CMake. Beginnen Sie die Ausführung dieser Befehle aus dem Stammverzeichnis des Projekts.```
cmake -B build cmake --build build

root@kitploit:~
Unter Windows-Systemen mit installiertem Visual Studio kann der letzte Befehl übersprungen werden, da das Projekt über die generierten Visual Studio-Projektmappendateien (.sln) erstellt werden kann. Die Visual Studio-Projektmappendateien befinden sich im Ordner 'build/'.

# 5. Verwendung

Der Obfuscator verfügt über ein kommandozeilenbasiertes Konfigurationssystem. Folgendes kann über die Befehlszeilenargumente konfiguriert werden:

- Welche Obfuscation-Pässe verwendet werden sollen.
- Pfad für die Eingabebinärdatei.
- Pfad für die Eingabesymboledatei (optional).
- Pfad für die Ausgabebinärdatei (optional).

Nachfolgend finden Sie eine Übersicht über die Befehlszeilenargumente:

Usage: binprotect binary-path symbol-path [--out-binary-path VAR] [--control-flow-flattening VAR] [--virtual-machine VAR] [--opaque-predicates VAR] [--linear-substitution VAR] [--mixed-boolean-arithmetic VAR]

Positional arguments:  
  binary-path                                               Dateipfad der Eingabebinärdatei [erforderlich]  
  symbol-path                                               Dateipfad der Symbole der Eingabebinärdatei [optional]

  --out, --out-path, --out-binary-path                      gewünschter Dateipfad der Ausgabebinärdatei  
  --cff, --control-flow-flattening                          Aktivieren des Control-Flow-Flattening-Passes [Standard: 1]  
  --vm, --virtual-machine                                   Aktivieren des Virtual-Machine-Passes [Standard: 1]  
  --opa, --opaque, --opaque-predicate, --opaque-predicates  Aktivieren des Opaque-Predicate-Passes [Standard: 1]  
  --lin, --linear-substitution                              Aktivieren des Linear-Substitution-Passes [Standard: 1]  
  --mba, --mixed-boolean-arithmetic                         Anzahl der Mixed-Boolean-Arithmetic-Pässe [Standard: 2]

# 6. Akronyme

- bin2bin - binary to binary.
- RVA - relative address.  
- MBA - mixed boolean arithmetic.  
- [SEH - structured exception handling](https://learn.microsoft.com/en-us/cpp/cpp/structured-exception-handling-c-cpp).
- FH3 - FuncInfo3.
- FH4 - FuncInfo4.
- [MSVC - Microsoft Visual C++](https://en.wikipedia.org/wiki/Microsoft_Visual_C%2B%2B).  
- [LLVM - low level virtual machine](https://en.wikipedia.org/wiki/LLVM).  
- [CLANG - C/C++ language frontend for LLVM](https://clang.llvm.org/).  
- [GCC - GNU compiler collection](https://en.wikipedia.org/wiki/GNU_Compiler_Collection).
- SF - sign flag.  
- ZF - zero flag.  
- PF - parity flag.  
- CF - carry flag.  
- AF - auxiliary carry flag.  
- OF - overflow flag.

# 7. Danksagungen

Die folgenden Personen haben während der Entwicklung des Projekts wertvolle Ratschläge gegeben:

- Aita.  
- Papstuc.  
- Eriktion.  
- IDontCode.  
- Abdulla.  
- Brit.  
- Phage.
Tool herunterladen