Bei der Suche nach "Unicorn Engine" auf Habr war ich überrascht, dass dieses Tool noch nie in Artikeln vorgestellt wurde. Ich werde versuchen, diese Lücke zu füllen. Beginnen wir mit den Grundlagen und sehen uns ein Beispiel für die Verwendung des Emulators im wirklichen Leben an. Um das Rad nicht neu zu erfinden, habe ich beschlossen, dieses Handbuch einfach zu übersetzen. Bevor ich anfange, werde ich sagen, dass alle meine Kommentare oder Kommentare so aussehen werden .
Was ist ein Einhornmotor?
Die Entwickler selbst schreiben darüber Einhornmotor Einhorn-Motor wie folgt:
Unicorn ist ein leichter Prozessoremulator mit mehreren Plattformen und mehreren Architekturen.
Dies ist kein Standardemulator. Es emuliert nicht den Betrieb des gesamten Programms oder des gesamten Betriebssystems. Es werden keine Systembefehle unterstützt (z. B. Öffnen einer Datei, Ausgabe eines Zeichens an die Konsole usw.). Sie müssen das Markup des Speichers durchführen und die Daten selbst in den Speicher laden. Anschließend starten Sie einfach die Ausführung von einer bestimmten Adresse aus.
Wie ist es also nützlich?
- Bei der Analyse von Viren können Sie einzelne Funktionen aufrufen, ohne einen schädlichen Prozess zu erstellen.
- CTF lösen.
- Zum Fuzzing .
- Ein Plugin für gdb zur Vorhersage des zukünftigen Zustands, z. B. zukünftige Sprünge oder Registerwerte.
- Emulation eines funktionsreichen Codes.
Was brauchst du
- Installierte Unicorn Engine mit Python-Bindung.
- Disassembler
Beispiel
Nehmen Sie als Beispiel eine Aufgabe mit hxp CTF 2017 unter dem Namen Fibonacci . Die Binärdatei kann hier heruntergeladen werden .
Wenn Sie das Programm starten, wird unsere Flagge in der Konsole angezeigt, jedoch sehr langsam. Jedes nachfolgende Flag-Byte wird als langsamer und langsamer angesehen.
The flag is: hxp{F
Dies bedeutet, dass wir den Betrieb dieser Anwendung optimieren müssen, um das Flag in angemessener Zeit zu erhalten.
Mit IDA Pro ( ich persönlich habe radare2 + Cutter verwendet ) haben wir den Code in einen C-ähnlichen Pseudocode dekompiliert. Trotz der Tatsache, dass der Code nicht richtig dekompiliert wurde, können wir immer noch Informationen darüber erhalten, was im Inneren passiert.
Dekompilierter Code __int64 __fastcall main(__int64 a1, char **a2, char **a3) { void *v3;
unsigned int __fastcall fibonacci(int i, _DWORD *a2) { _DWORD *v2;
Hier ist der Assembler-Code der Haupt- und Fibonacci- Funktionen:
Haupt .text:0x4004E0 main proc near ; DATA XREF: start+1Do .text:0x4004E0 .text:0x4004E0 var_1C = dword ptr -1Ch .text:0x4004E0 .text:0x4004E0 push rbp .text:0x4004E1 push rbx .text:0x4004E2 xor esi, esi ; buf .text:0x4004E4 mov ebp, offset unk_4007E1 .text:0x4004E9 xor ebx, ebx .text:0x4004EB sub rsp, 18h .text:0x4004EF mov rdi, cs:stdout ; stream .text:0x4004F6 call _setbuf .text:0x4004FB mov edi, offset format ; "The flag is: " .text:0x400500 xor eax, eax .text:0x400502 call _printf .text:0x400507 mov r9d, 49h .text:0x40050D nop dword ptr [rax] .text:0x400510 .text:0x400510 loc_400510: ; CODE XREF: main+8Aj .text:0x400510 xor r8d, r8d .text:0x400513 jmp short loc_40051B .text:0x400513 ; --------------------------------------------------------------------------- .text:0x400515 align 8 .text:0x400518 .text:0x400518 loc_400518: ; CODE XREF: main+67j .text:0x400518 mov r9d, edi .text:0x40051B .text:0x40051B loc_40051B: ; CODE XREF: main+33j .text:0x40051B lea edi, [rbx+r8] .text:0x40051F lea rsi, [rsp+28h+var_1C] .text:0x400524 mov [rsp+28h+var_1C], 0 .text:0x40052C call fibonacci .text:0x400531 mov edi, [rsp+28h+var_1C] .text:0x400535 mov ecx, r8d .text:0x400538 add r8, 1 .text:0x40053C shl edi, cl .text:0x40053E mov eax, edi .text:0x400540 xor edi, r9d .text:0x400543 cmp r8, 8 .text:0x400547 jnz short loc_400518 .text:0x400549 add ebx, 8 .text:0x40054C cmp al, r9b .text:0x40054F mov rsi, cs:stdout ; fp .text:0x400556 jz short loc_400570 .text:0x400558 movsx edi, dil ; c .text:0x40055C add rbp, 1 .text:0x400560 call __IO_putc .text:0x400565 movzx r9d, byte ptr [rbp-1] .text:0x40056A jmp short loc_400510 .text:0x40056A ; --------------------------------------------------------------------------- .text:0x40056C align 10h .text:0x400570 .text:0x400570 loc_400570: ; CODE XREF: main+76j .text:0x400570 mov edi, 0Ah ; c .text:0x400575 call __IO_putc .text:0x40057A add rsp, 18h .text:0x40057E xor eax, eax .text:0x400580 pop rbx .text:0x400581 pop rbp .text:0x400582 retn .text:0x400582 main endp
Fibonacci .text:0x400670 fibonacci proc near ; CODE XREF: main+4Cp .text:0x400670 ; fibonacci+19p ... .text:0x400670 test edi, edi .text:0x400672 push r12 .text:0x400674 push rbp .text:0x400675 mov rbp, rsi .text:0x400678 push rbx .text:0x400679 jz short loc_4006F8 .text:0x40067B cmp edi, 1 .text:0x40067E mov ebx, edi .text:0x400680 jz loc_400710 .text:0x400686 lea edi, [rdi-2] .text:0x400689 call fibonacci .text:0x40068E lea edi, [rbx-1] .text:0x400691 mov r12d, eax .text:0x400694 mov rsi, rbp .text:0x400697 call fibonacci .text:0x40069C add eax, r12d .text:0x40069F mov edx, eax .text:0x4006A1 mov ebx, eax .text:0x4006A3 shr edx, 1 .text:0x4006A5 and edx, 55555555h .text:0x4006AB sub ebx, edx .text:0x4006AD mov ecx, ebx .text:0x4006AF mov edx, ebx .text:0x4006B1 shr ecx, 2 .text:0x4006B4 and ecx, 33333333h .text:0x4006BA mov esi, ecx .text:0x4006BC .text:0x4006BC loc_4006BC: ; CODE XREF: fibonacci+C2j .text:0x4006BC and edx, 33333333h .text:0x4006C2 lea ecx, [rsi+rdx] .text:0x4006C5 mov edx, ecx .text:0x4006C7 shr edx, 4 .text:0x4006CA add edx, ecx .text:0x4006CC mov esi, edx .text:0x4006CE and edx, 0F0F0F0Fh .text:0x4006D4 shr esi, 8 .text:0x4006D7 and esi, 0F0F0Fh .text:0x4006DD lea ecx, [rsi+rdx] .text:0x4006E0 mov edx, ecx .text:0x4006E2 shr edx, 10h .text:0x4006E5 add edx, ecx .text:0x4006E7 and edx, 1 .text:0x4006EA xor [rbp+0], edx .text:0x4006ED pop rbx .text:0x4006EE pop rbp .text:0x4006EF pop r12 .text:0x4006F1 retn .text:0x4006F1 ; --------------------------------------------------------------------------- .text:0x4006F2 align 8 .text:0x4006F8 .text:0x4006F8 loc_4006F8: ; CODE XREF: fibonacci+9j .text:0x4006F8 mov edx, 1 .text:0x4006FD xor [rbp+0], edx .text:0x400700 mov eax, 1 .text:0x400705 pop rbx .text:0x400706 pop rbp .text:0x400707 pop r12 .text:0x400709 retn .text:0x400709 ; --------------------------------------------------------------------------- .text:0x40070A align 10h .text:0x400710 .text:0x400710 loc_400710: ; CODE XREF: fibonacci+10j .text:0x400710 xor edi, edi .text:0x400712 call fibonacci .text:0x400717 mov edx, eax .text:0x400719 mov edi, eax .text:0x40071B shr edx, 1 .text:0x40071D and edx, 55555555h .text:0x400723 sub edi, edx .text:0x400725 mov esi, edi .text:0x400727 mov edx, edi .text:0x400729 shr esi, 2 .text:0x40072C and esi, 33333333h .text:0x400732 jmp short loc_4006BC .text:0x400732 fibonacci endp
In dieser Phase haben wir viele Möglichkeiten, dieses Problem zu lösen. Zum Beispiel können wir den Code mit einer der Programmiersprachen wiederherstellen und dort die Optimierung anwenden, aber der Prozess der Wiederherstellung des Codes ist eine sehr schwierige Aufgabe, bei der wir Fehler machen können. Nun, dann ist es im Allgemeinen wertlos, den Code zu vergleichen, um den Fehler zu finden. Wenn wir jedoch die Unicorn Engine verwenden, können wir die Phase der Code-Rekonstruktion überspringen und das oben beschriebene Problem vermeiden. Natürlich können wir diese Probleme vermeiden, indem wir frida verwenden oder Skripte für gdb schreiben, aber darum geht es nicht.
Bevor wir mit der Optimierung beginnen, führen wir die Emulation in der Unicorn Engine aus, ohne das Programm zu ändern. Und erst nach einem erfolgreichen Start können wir mit der Optimierung fortfahren.
Schritt 1: Lassen Sie die Virtualisierung kommen
Lassen Sie uns die Datei fibonacci.py erstellen und neben der Binärdatei speichern.
Beginnen wir mit dem Importieren der erforderlichen Bibliotheken:
from unicorn import * from unicorn.x86_const import * import struct
In der ersten Zeile werden die wichtigsten binären und grundlegenden Einhornkonstanten geladen. In der zweiten Zeile werden die Konstanten für die beiden x86- und x86_64-Architekturen geladen.
Fügen Sie als Nächstes einige notwendige Funktionen hinzu:
def read(name): with open(name) as f: return f.read() def u32(data): return struct.unpack("I", data)[0] def p32(num): return struct.pack("I", num)
Hier haben wir die Funktionen angekündigt, die wir später benötigen werden:
- read gibt einfach den Inhalt der Datei zurück,
- u32 verwendet eine 4-Byte-Zeichenfolge in LE-Codierung und konvertiert in int,
- p32 macht das Gegenteil - es nimmt eine Zahl und wandelt sie in eine 4-Byte-Zeichenfolge in LE-Codierung um.
Hinweis: Wenn Sie pwntools installiert haben , müssen Sie diese Funktionen nicht erstellen, sondern nur importieren:
from pwn import *
Beginnen wir also mit der Initialisierung unserer Unicorn Engine-Klasse für die x86_64-Architektur:
mu = Uc (UC_ARCH_X86, UC_MODE_64)
Hier rufen wir die Uc- Funktionen mit folgenden Parametern auf:
- Der erste Parameter ist die Hauptarchitektur. Konstanten beginnen mit UC_ARCH_ ;
- Der zweite Parameter ist die Spezifikation der Architektur. Konstanten beginnen mit UC_MODE_ .
Sie finden alle Konstanten im Cheatsheet .
Wie ich oben geschrieben habe, müssen wir den virtuellen Speicher manuell initialisieren, um die Unicorn Engine verwenden zu können. In diesem Beispiel müssen wir den Code und den Stapel irgendwo im Speicher ablegen.
Die Basisadresse (Basisadresse) der Binärdatei beginnt bei 0x400000. Setzen wir unseren Stack auf 0x0 und weisen ihm 1024 * 1024 Speicher zu. Höchstwahrscheinlich brauchen wir nicht so viel Platz, aber es tut immer noch nicht weh.
Wir können den Speicher markieren, indem wir die Methode mem_map aufrufen.
Fügen Sie diese Zeilen hinzu:
BASE = 0x400000 STACK_ADDR = 0x0 STACK_SIZE = 1024*1024 mu.mem_map(BASE, 1024*1024) mu.mem_map(STACK_ADDR, STACK_SIZE)
Jetzt müssen wir die Binärdatei auf dieselbe Weise wie der Bootloader in ihre Hauptadresse laden. Danach müssen wir RSP auf das Ende des Stapels setzen.
mu.mem_write(BASE, read("./fibonacci")) mu.reg_write(UC_X86_REG_RSP, STACK_ADDR + STACK_SIZE - 1)
Jetzt können wir die Emulation starten und den Code ausführen, aber wir müssen herausfinden, mit welcher Adresse die Arbeit beginnen soll und wann der Emulator anhalten soll.
Nehmen Sie die Adresse des ersten Befehls von main () , wir können die Emulation von 0x004004e0 starten. Das Ende wird als Aufruf von putc ("\ n") betrachtet , das sich bei 0x00400575 befindet, nachdem das gesamte Flag angezeigt wurde.
.text:0x400570 mov edi, 0Ah ; c .text:0x400575 call __IO_putc
Wir können anfangen zu emulieren:
mu.emu_start(0x004004e0,0x00400575)
Führen Sie nun das Skript aus:
a@x:~/Desktop/unicorn_engine_lessons$ python solve.py Traceback (most recent call last): File "solve.py", line 32, in <module> mu.emu_start(0x00000000004004E0, 0x0000000000400575) File "/usr/local/lib/python2.7/dist-packages/unicorn/unicorn.py", line 288, in emu_start raise UcError(status) unicorn.unicorn.UcError: Invalid memory read (UC_ERR_READ_UNMAPPED)
Ups, etwas ist schief gelaufen, aber wir wissen nicht einmal was. Kurz bevor wir mu.emu_start aufrufen , können wir hinzufügen:
def hook_code(mu, address, size, user_data): print('>>> Tracing instruction at 0x%x, instruction size = 0x%x' %(address, size)) mu.hook_add(UC_HOOK_CODE, hook_code)
Dieser Code fügt einen Hook hinzu. Wir deklarieren unsere eigene hook_code- Funktion, die vom Emulator vor jedem Befehl aufgerufen wird. Es werden folgende Parameter benötigt:
- unsere Kopie von Uc ,
- Anweisungsadresse
- Größenanweisungen
- Benutzerdaten (wir können diesen Wert mit einem optionalen Argument an hook_add () übergeben ).
Wenn wir nun das Skript ausführen, sollten wir die folgende Ausgabe sehen:
a@x:~/Desktop/unicorn_engine_lessons$ python solve.py >>> Tracing instruction at 0x4004e0, instruction size = 0x1 >>> Tracing instruction at 0x4004e1, instruction size = 0x1 >>> Tracing instruction at 0x4004e2, instruction size = 0x2 >>> Tracing instruction at 0x4004e4, instruction size = 0x5 >>> Tracing instruction at 0x4004e9, instruction size = 0x2 >>> Tracing instruction at 0x4004eb, instruction size = 0x4 >>> Tracing instruction at 0x4004ef, instruction size = 0x7 Traceback (most recent call last): File "solve.py", line 41, in <module> mu.emu_start(0x00000000004004E0, 0x0000000000400575) File "/usr/local/lib/python2.7/dist-packages/unicorn/unicorn.py", line 288, in emu_start raise UcError(status) unicorn.unicorn.UcError: Invalid memory read (UC_ERR_READ_UNMAPPED)
An der Adresse, an der der Fehler aufgetreten ist, können wir verstehen, dass unser Skript diesen Befehl nicht verarbeiten kann:
.text:0x4004EF mov rdi, cs:stdout ; stream
Diese Anweisung liest Daten von der Adresse 0x601038 (Sie können sie in IDA Pro sehen). Dies ist der Abschnitt .bss , den wir nicht markiert haben. Meine Lösung wäre, einfach alle problematischen Anweisungen zu überspringen, wenn dies die Programmlogik nicht beeinflusst.
Unten ist eine weitere problematische Anweisung:
.text:0x4004F6 call _setbuf
Wir können mit glibc keine Funktionen aufrufen, da glibc nicht im Speicher geladen ist. In jedem Fall benötigen wir diesen Befehl nicht, sodass wir ihn auch überspringen können.
Hier ist die vollständige Liste der zu überspringenden Befehle:
.text:0x4004EF mov rdi, cs:stdout ; stream .text:0x4004F6 call _setbuf .text:0x400502 call _printf .text:0x40054F mov rsi, cs:stdout ; fp
Um Befehle zu überspringen, müssen wir RIP mit der folgenden Anweisung neu schreiben:
mu.reg_write(UC_X86_REG_RIP, address+size)
Jetzt sollte hook_code ungefähr so aussehen:
instructions_skip_list = [0x004004ef,0x004004f6,0x00400502,0x0040054f] def hook_code(mu, address, size, user_data): print('>>> Tracing instruction at 0x%x, instruction size = 0x%x' %(address, size)) if address in instructions_skip_list: mu.reg_write(UC_X86_REG_RIP, address+size)
Wir müssen auch etwas mit Anweisungen tun, die das Flag in der Byte-für-Byte-Konsole anzeigen.
.text:0x400558 movsx edi, dil ; c .text:0x40055C add rbp, 1 .text:0x400560 call __IO_putc
__IO_putc verwendet als erstes Argument Bytes für die Ausgabe (dies ist das RDI- Register).
Wir können Daten direkt aus dem Register lesen, Daten an die Konsole ausgeben und diese Anweisungen überspringen. Der aktualisierte hook_code wird unten dargestellt:
instructions_skip_list = [0x004004ef,0x004004f6,0x00400502,0x0040054f] def hook_code(mu, address, size, user_data):
Wir können rennen und alles wird funktionieren, aber immer noch langsam.
Schritt 2: Geschwindigkeit erhöhen!
Lassen Sie uns darüber nachdenken, die Arbeitsgeschwindigkeit zu erhöhen. Warum ist dieses Programm so langsam?
Wenn wir uns den dekompilierten Code ansehen, werden wir sehen, dass main () mehrmals fibonacci () aufruft und fibonacci () eine rekursive Funktion ist. Schauen wir uns diese Funktion genauer an: Sie benötigt zwei Argumente und gibt sie zurück. Der erste Rückgabewert wird über das RAX- Register übergeben, der zweite über die Verknüpfung, die über das zweite Argument an die Funktion übergeben wurde. Wenn wir uns die Beziehung zwischen main () und fibonacci () genauer ansehen, werden wir feststellen , dass das zweite Argument nur zwei mögliche Werte annimmt: 0 oder 1. Wenn Sie dies immer noch nicht sehen, führen Sie gdb aus und setzen Sie einen Haltepunkt am Anfang der Funktion Fibonacci () .
Um die Funktionsweise des Algorithmus zu optimieren, können wir die dynamische Programmierung verwenden, um den Rückgabewert für eingehende Parameter zu speichern . Denken Sie selbst, das zweite Argument kann nur zwei mögliche Werte annehmen. Wir müssen uns also nur daran erinnern $ inline $ 2 * MAX \ _OF \ _FIRST \ _ARGUMENT $ inline $ Dampf
Für diejenigen, die nicht verstehenFibonacci ist eine rekursive Funktion, die den nächsten Wert als Summe der beiden vorherigen berechnet. Bei jedem Schritt geht sie tiefer. Jedes Mal, wenn sie von vorne anfängt, geht sie den gleichen Weg wie zuvor, plus eine neue Bedeutung.
Ein Beispiel:
Angenommen, Tiefe = 6, dann: 1 1 2 3 5 8 .
Und jetzt Tiefe = 8, dann: 1 1 2 3 5 8 13 21.
Wir konnten uns nur daran erinnern, dass die ersten 6 Mitglieder 1 1 2 3 5 8 sind , und wenn sie uns bitten, mehr zu zählen, als wir uns erinnern, nehmen wir das, woran wir uns erinnern, und zählen nur das, was fehlt.
Sobald RIP am Anfang von fibonacci () steht , können wir die Funktionsargumente erhalten. Wir wissen, dass eine Funktion ein Ergebnis zurückgibt, wenn sie eine Funktion beendet. Da wir nicht mit zwei Parametern gleichzeitig arbeiten können, benötigen wir einen Stapel, um die Parameter zurückzugeben. Wenn wir fibonacci () eingeben , müssen wir die Argumente auf den Stapel legen und sie beim Beenden aufnehmen. Um die gezählten Paare zu speichern, können wir ein Wörterbuch verwenden.
Wie verarbeite ich ein Wertepaar?
- Ganz am Anfang der Funktion können wir überprüfen, ob dieses Paar in den bereits bekannten Ergebnissen enthalten ist:
- Wenn ja, können wir dieses Paar zurückgeben. Wir müssen nur die Rückgabewerte in RAX und an die Adresse des Links schreiben, die sich im zweiten Argument befindet. Wir weisen auch eine RIP- Adresse zu, um die Funktion zu verlassen. Wir können RET in fibonacci () nicht verwenden, da diese Aufrufe verknüpft sind. Daher werden wir RET von main () übernehmen .
- Wenn dies nicht der Fall ist, fügen wir sie einfach dem Stapel hinzu.
- Vor dem Beenden der Funktion können wir das zurückgegebene Paar speichern. Wir kennen die Eingabeargumente, da wir sie von unserem Stapel lesen können.
Dieser Code wird hier vorgestellt. FIBONACCI_ENTRY = 0x00400670 FIBONACCI_END = [ 0x004006f1, 0x00400709] instructions_skip_list = [0x004004ef,0x004004f6,0x00400502,0x0040054f]
Hier ist das ganze Skript
Hurra, wir konnten die Anwendung endlich mit der Unicorn Engine optimieren. Gut gemacht!
Eine Notiz
Jetzt habe ich beschlossen, dir ein paar Hausaufgaben zu machen.
Hier finden Sie drei weitere Aufgaben, von denen jede einen Hinweis und eine vollständige Lösung enthält. Sie können beim Lösen von Problemen einen Blick auf das Cheatsheet werfen .
Eines der nervigsten Probleme besteht darin, sich den Namen der gewünschten Konstante zu merken. Dies ist einfach zu handhaben, wenn Sie in IPython Tab-Add-Ons verwenden. Wenn Sie IPython installiert haben, können Sie vom Einhornimport UC_ARCH_ schreiben. Drücken Sie die Tabulatortaste, und es werden alle Konstanten angezeigt , die auf die gleiche Weise beginnen.