Wir präsentieren Ihnen die Technik zum Erstellen von Assembler-Programmen mit überlappenden Anweisungen, um den kompilierten Bytecode vor dem Zerlegen zu schützen. Diese Technik kann sowohl statischen als auch dynamischen Bytecode-Analysen standhalten. Die Idee besteht darin, einen Bytestrom auszuwählen, der, wenn er aus zwei verschiedenen Offsets zerlegt wird, zu zwei verschiedenen Befehlsketten führt, dh zu zwei verschiedenen Arten der Ausführung des Programms. Dazu nehmen wir Multibyte-Assembler-Anweisungen und verstecken den geschützten Code in den variablen Teilen des Bytecodes dieser Anweisungen. Um den Disassembler zu täuschen, indem man ihn auf eine falsche Spur bringt (gemäß einer maskierenden Anweisungskette), und um eine verborgene Anweisungskette vor seinen Augen zu schützen.

Drei Voraussetzungen für eine effektive „Überlappung“
Um den Disassembler zu täuschen, muss der überlappende Code die folgenden drei Bedingungen erfüllen: 1) Anweisungen von der Maskierungskette und der verborgenen Kette müssen sich immer überschneiden, d. H. sollten nicht relativ zueinander ausgerichtet sein (ihr erstes und letztes Byte sollten nicht zusammenfallen). Andernfalls ist ein Teil des versteckten Codes in der Maskierungskette sichtbar. 2) Beide Ketten sollten aus plausiblen Montageanleitungen bestehen. Andernfalls wird die Maskierung bereits in der Phase der statischen Analyse erkannt (nachdem der Disassembler auf ungeeigneten Code für die Ausführung gestoßen ist, korrigiert er den Befehlszeiger und legt die Maskierung frei). 3) Alle Anweisungen beider Ketten sollten nicht nur plausibel sein, sondern auch korrekt ausgeführt werden (um dies zu verhindern, stürzte das Programm ab, wenn Sie versuchen, sie auszuführen). Andernfalls ziehen die Fehler während der dynamischen Analyse die Aufmerksamkeit des Gegenteils auf sich und die Maske wird aufgedeckt.
Beschreibung der Technik der "überlappenden" Assembler-Anweisungen
Um den Prozess der Erstellung überlappenden Codes so flexibel wie möglich zu gestalten, müssen nur solche Multibyte-Anweisungen ausgewählt werden, für die so viele Bytes wie möglich einen beliebigen Wert annehmen können. Diese Multibyte-Befehle bilden eine Maskierungsbefehlskette.
Um das Ziel zu erreichen, überlappenden Code zu erstellen, der die obigen drei Bedingungen erfüllt, betrachten wir jede Maskierungsanweisung als eine Folge von Bytes der Form: XX YY ZZ.
Hier ist XX das Befehlspräfix (Befehlscode und andere statische Bytes - die nicht geändert werden können).
YY sind Bytes, die beliebig geändert werden können (in der Regel speichern diese Bytes den direkten numerischen Wert, der an den Befehl übergeben wird, oder die Adresse des im Speicher gespeicherten Operanden). Es sollten so viele YY-Bytes wie möglich vorhanden sein, damit mehr versteckte Anweisungen in sie passen.
ZZ - Dies sind auch Bytes, die beliebig geändert werden können. Der einzige Unterschied besteht darin, dass die Kombination von ZZ-Bytes mit den nachfolgenden Bytes XX (ZZ XX) eine gültige Anweisung bilden sollte, die die drei am Anfang des Artikels formulierten Bedingungen erfüllt. Im Idealfall sollte ZZ nur ein Byte belegen, damit auf YY (dies ist im Wesentlichen der wichtigste Teil - unser versteckter Code wird hier platziert) so viele Bytes wie möglich vorhanden sein sollten. Die letzte versteckte Anweisung sollte in ZZ enden - und einen Konvergenzpunkt für die beiden Ausführungsketten erstellen.
Anleitung zum Kleben
Die Kombination ZZ XX - wir nennen die Klebeanweisung. Eine Klebeanweisung wird zum einen benötigt, um versteckte Anweisungen zu verbinden, die sich in benachbarten Maskierungsanweisungen befinden, und zum anderen, um die erste notwendige Bedingung zu erfüllen, die am Anfang des Artikels angegeben ist: Die Anweisungen beider Ketten sollten sich immer überschneiden (daher die Klebeanweisung immer befindet sich am Schnittpunkt zweier Maskierungsanweisungen).
Der Klebebefehl wird in einer verborgenen Befehlskette ausgeführt und muss daher so ausgewählt werden, dass dem verborgenen Code so wenig Einschränkungen wie möglich auferlegt werden. Angenommen, wenn es ausgeführt wird, werden die Allzweckregister und das EFLAGS-Register geändert, dann kann der verborgene Code die entsprechenden Register und bedingten Befehle nicht effektiv verwenden (wenn dem Klebebefehl beispielsweise der Vergleichsoperator vorausgeht und der Klebebefehl selbst den Wert des EFLAGS-Registers ändert, dann der bedingte Übergang). was nach der Klebeanweisung steht, funktioniert nicht richtig).
Die obige Beschreibung der Überlappungstechnik ist in der folgenden Abbildung dargestellt. Beginnt die Ausführung mit den Startbytes (XX), wird eine Maskierungsbefehlskette aktiviert. Und wenn von Bytes YY, wird eine versteckte Befehlskette aktiviert.

Assembler-Anweisungen, die für die Rolle der "Maskierungsanweisungen" geeignet sind
Die längste der Anweisungen, die auf den ersten Blick am besten zu uns passt, ist eine 10-Byte-Version von MOV, bei der der durch das Register und die 32-Bit-Adresse angegebene Offset als erster Operand und die 32-Bit-Nummer als zweiter Operand übertragen werden. Diese Anweisung enthält die meisten Bytes, die beliebig geändert werden können (bis zu 8 Teile).

Obwohl dieser Befehl plausibel erscheint (theoretisch kann er korrekt ausgeführt werden), passt er immer noch nicht zu uns, da sein erster Operand in der Regel eine unzugängliche Adresse angibt und daher beim Versuch, einen solchen MOV auszuführen, das Programm wird zusammenbrechen. T.O. Diese 10-Byte-MOV erfüllt nicht die dritte notwendige Bedingung: Alle Anweisungen beider Ketten müssen korrekt ausgeführt werden.
Daher werden wir für die Rolle der Maskierungsanweisungen nur diejenigen Bewerber auswählen, bei denen kein Risiko eines Zusammenbruchs des Programms besteht. Diese Bedingung schränkt den Bereich von Anweisungen, die zum Erstellen von überlappendem Code geeignet sind, erheblich ein, es gibt jedoch noch geeignete Anweisungen. Unten sind vier davon. Jeder dieser vier Befehle enthält fünf Bytes, die beliebig geändert werden können, ohne dass das Risiko eines Programmabsturzes besteht.
- LEA. Dieser Befehl berechnet die durch den Ausdruck im zweiten Operanden angegebene Speicheradresse und speichert das Ergebnis im ersten Operanden. Da wir auf den Speicher verweisen können, ohne tatsächlich darauf zuzugreifen (und dementsprechend ohne das Risiko eines Programmabsturzes), können die letzten fünf Bytes dieser Anweisung beliebige Werte annehmen.

- CMOVcc. Dieser Befehl führt die MOV-Operation aus, wenn die Bedingung "cc" erfüllt ist. Damit diese Anweisung die dritte Anforderung erfüllt, muss die Bedingung so ausgewählt werden, dass sie unter allen Umständen den Wert FALSE hat. Andernfalls versucht diese Anweisung möglicherweise, auf eine unzugängliche Speicheradresse usw. zuzugreifen. Programm herunterfahren.

- SETcc Es funktioniert nach dem gleichen Prinzip wie CMOVcc: setzt das Byte auf eins, wenn die Bedingung "cc" erfüllt ist. Diese Anweisung hat das gleiche Problem wie CMOVcc: Der Zugriff auf eine ungültige Adresse führt zum Absturz des Programms. Daher muss die Wahl des "cc" -Zustands sehr sorgfältig angegangen werden.

- NOP. NOPs können unterschiedlich lang sein (von 2 bis 15 Byte), je nachdem, welche Operanden in ihnen angegeben sind. In diesem Fall besteht kein Risiko eines Absturzes des Programms (aufgrund des Zugriffs auf eine ungültige Speicheradresse). Weil NOPs nur den Befehlszähler erhöhen (sie führen keine Operationen an Operanden aus). Daher können die NOP-Bytes, in denen die Operanden angegeben sind, einen beliebigen Wert annehmen. Für unsere Zwecke ist ein 9-Byte-NOP am besten geeignet.

Als Referenz finden Sie hier einige andere NOP-Optionen.

Assembler-Anweisungen, die für die Rolle der „Klebe-Anweisungen“ geeignet sind
Die Liste der Anweisungen, die für die Rolle einer Klebeanweisung geeignet sind, ist für jede spezifische Maskierungsanweisung eindeutig. Unten finden Sie eine Liste (generiert mit dem in der folgenden Abbildung gezeigten Algorithmus) am Beispiel des 9-Byte-NOP.

Bei der Erstellung dieser Liste haben wir nur die Optionen berücksichtigt, bei denen ZZ 1 Byte benötigt (andernfalls bleibt nur wenig Platz für versteckten Code). Hier ist eine Liste geeigneter Haftanweisungen für ein 9-Byte-NOP.

Unter dieser Liste von Anweisungen gibt es keine, die frei von Nebenwirkungen wäre. Jeder von ihnen ändert entweder EFLAGS oder Allzweckregister oder beides gleichzeitig. Diese Liste ist in 4 Kategorien unterteilt, je nachdem, welche Nebenwirkung die Anweisung hat.
Die erste Kategorie enthält Anweisungen, die das EFLAGS-Register ändern, jedoch keine Allzweckregister. Anweisungen aus dieser Kategorie können verwendet werden, wenn keine bedingten Sprünge oder Anweisungen in der Kette versteckter Anweisungen vorhanden sind, die auf der Auswertung von Informationen aus dem EFLAGS-Register basieren. In diesem Fall gibt es in diesem Fall (für einen 9-Byte-NOP) nur zwei Anweisungen: TEST und CMP.

Das Folgende ist ein einfaches Beispiel für versteckten Code, der TEST als Klebeanweisung verwendet. In diesem Beispiel wird ein Systemaufruf beendet, der für jede Linux-Version den Wert 1 zurückgibt. Um die TEST-Anweisung für unsere Anforderungen korrekt zu bilden, müssen wir das letzte Byte des ersten NOP auf 0xA9 setzen. Dieses Byte wird in Verbindung mit den ersten vier Bytes des nächsten NOP (66 0F 1F 84) zu einem TEST EAX-Befehl 0x841F0F66. Die folgenden beiden Abbildungen zeigen den entsprechenden Assembler-Code (zum Maskieren der Kette und der versteckten Kette). Die versteckte Kette wird aktiviert, wenn die Steuerung auf das 4. Byte des ersten NOP übertragen wird.


Die zweite Kategorie enthält Anweisungen, die die Werte allgemeiner Register oder des verfügbaren Speichers (z. B. Stapel) ändern, das EFLAGS-Register jedoch nicht ändern. Bei der Ausführung eines PUSH-Befehls oder einer MOV-Variante, bei der als zweiter Operand ein Sofortwert angegeben wird, bleibt das EFLAGS-Register unverändert. T.O. Klebebefehle der zweiten Kategorie können sogar zwischen dem Vergleichsbefehl (z. B. TEST) und dem Befehl, der das EFLAGS-Register auswertet, platziert werden. Anweisungen in dieser Kategorie beschränken jedoch die Verwendung des Registers, das in den entsprechenden Klebeanweisungen enthalten ist. Wenn beispielsweise MOV EBP, 0x841F0F66 als Klebebefehl verwendet wird, sind die Möglichkeiten zur Verwendung des EBP-Registers (aus dem Rest des verborgenen Codes) erheblich eingeschränkt.
Die dritte Kategorie enthält Anweisungen, die das EFLAGS-Register ändern, und die Allzweckregister (oder den Speicher) ändern sich. Diese Anweisungen haben keine offensichtlichen Vorteile gegenüber Anweisungen aus den ersten beiden Kategorien. Sie können jedoch auch verwendet werden, da sie den drei am Anfang des Artikels formulierten Bedingungen nicht widersprechen. Die vierte Kategorie enthält Anweisungen, für deren Implementierung nicht garantiert werden kann, dass das Programm nicht abstürzt. Es besteht die Gefahr eines illegalen Zugriffs auf den Speicher. Es ist äußerst unerwünscht, sie zu verwenden, weil Sie erfüllen nicht die dritte Bedingung.
Assembler-Anweisungen, die in einer versteckten Kette verwendet werden können
In unserem Fall (wenn 9-Byte-NOPs als Maskierungsanweisungen verwendet werden) sollte die Länge jeder Anweisung aus der verborgenen Kette vier Bytes nicht überschreiten (diese Einschränkung gilt nicht für Sticky-Anweisungen, die 5 Bytes belegen). Dies ist jedoch keine sehr kritische Einschränkung, da die meisten Befehle, die länger als vier Bytes sind, in mehrere kürzere Befehle zerlegt werden können. Das folgende Beispiel zeigt einen 5-Byte-MOV, der zu groß ist, um in eine versteckte Kette zu passen.

Diese Fünf-Byte-MOV kann jedoch in drei Befehle zerlegt werden, deren Länge vier Bytes nicht überschreitet.

Verbessern der Maskierung durch Verteilen von Maskierungs-NOPs im gesamten Programm
Eine große Anzahl aufeinanderfolgender NOPs erscheint aus umgekehrter Sicht sehr verdächtig. Ein erfahrener Umkehrer, der sein Interesse auf diese verdächtigen NOPs konzentriert, kann dem darin verborgenen Code auf den Grund gehen. Um diese Exposition zu vermeiden, können maskierte NOPs im gesamten Programm verteilt werden.
Die korrekte Ausführungskette des versteckten Codes kann in diesem Fall durch Doppelbyte-Anweisungen für einen bedingungslosen Sprung unterstützt werden. In diesem Fall belegen die letzten zwei Bytes jedes NOP einen 2-Byte-JMP.
Mit diesem Trick können Sie eine lange Folge von NOPs in mehrere kurze aufteilen (oder sogar jeweils einen NOP verwenden). Im letzten NOP einer solch kurzen Sequenz können nur 3 Bytes der Nutzlast zugewiesen werden (das 4. Byte wird von der bedingungslosen Sprunganweisung übernommen). T.O. Hier gibt es eine zusätzliche Einschränkung für die Größe gültiger Anweisungen. Wie oben erwähnt, können lange Anweisungen jedoch in einer Kette kürzerer Anweisungen angeordnet werden. Unten finden Sie ein Beispiel für denselben 5-Byte-MOV, den wir bereits so ausgelegt haben, dass er in die 4-Byte-Grenze passt. Jetzt zerlegen wir diesen MOV jedoch so, dass er in die 3-Byte-Grenze passt.

Nachdem wir alle langen Anweisungen nach demselben Prinzip in kürzere zerlegt haben, können wir, um mehr zu maskieren, im Allgemeinen nur einzelne NOPs verwenden, die über das Programm verteilt sind. Zwei-Byte-JMP-Befehle können um 127 Bytes vorwärts und rückwärts springen, was bedeutet, dass zwei aufeinanderfolgende NOPs (aufeinanderfolgend in Bezug auf eine Kette versteckter Befehle) innerhalb von 127 Bytes liegen müssen.
Dieser Trick hat einen weiteren signifikanten Vorteil (zusätzlich zur verbesserten Maskierung): Mit ihm können Sie versteckten Code in die vorhandenen NOPs der kompilierten Binärdatei einfügen (d. H. Nach dem Kompilieren eine Nutzlast in die Binärdatei einfügen). In diesem Fall ist es nicht erforderlich, dass diese verwaisten NOPs 9 Byte groß sind. Wenn beispielsweise mehrere Einzelbyte-NOPs in einer Reihe in der Binärdatei vorhanden sind, können sie in Mehrbyte-NOPs konvertiert werden, ohne die Funktionalität des Programms zu beeinträchtigen. Nachfolgend finden Sie ein Beispiel für eine Technik zum Verteilen von NOPs (dieser Code entspricht funktional dem oben diskutierten Beispiel).

Ein solcher versteckter Code, der in NOP versteckt ist und über das Programm verteilt ist, ist bereits viel schwieriger zu erkennen.
Ein aufmerksamer Leser muss bemerkt haben, dass der erste NOP kein letztes Byte hat. Es gibt jedoch keinen Grund zur Sorge. Weil diesem nicht beanspruchten Byte ein bedingungsloser Sprung vorausgeht. T.O. Die Kontrolle wird niemals auf ihn übertragen. Also ist alles in Ordnung.
Hier ist eine Technik zum Erstellen von überlappendem Code. Verwendung für die Gesundheit. Verstecken Sie Ihren wertvollen Code vor neugierigen Blicken. Aber nehmen Sie einfach eine andere Anweisung an, keine 9-Byte-NOP. Weil die Umkehrer wahrscheinlich auch diesen Artikel lesen werden.