Schwierigkeiten beim Verständnis der Logik in der zerlegten binären Bombenphase 3

8

Ich habe das folgende Montageprogramm aus dem Binärbombenlabor. Ziel ist es, das Schlüsselwort zu ermitteln, das zum Ausführen der Binärdatei erforderlich ist, ohne die explode_bombFunktion auszulösen . Ich habe meine Analyse der Baugruppe für dieses Programm kommentiert, aber ich habe Probleme, alles zusammenzusetzen.

Ich glaube, ich habe alle Informationen, die ich brauche, aber ich kann die zugrunde liegende Logik immer noch nicht erkennen und stecke daher fest. Ich würde mich über jede Hilfe sehr freuen!

Das folgende ist das zerlegte Programm selbst:

0x08048c3c <+0>:     push   %edi
   0x08048c3d <+1>:     push   %esi
   0x08048c3e <+2>:     sub    $0x14,%esp
   0x08048c41 <+5>:     movl   $0x804a388,(%esp)
   0x08048c48 <+12>:    call   0x80490ab <string_length>
   0x08048c4d <+17>:    add    $0x1,%eax
   0x08048c50 <+20>:    mov    %eax,(%esp)
   0x08048c53 <+23>:    call   0x8048800 <malloc@plt>
   0x08048c58 <+28>:    mov    $0x804a388,%esi
   0x08048c5d <+33>:    mov    $0x13,%ecx
   0x08048c62 <+38>:    mov    %eax,%edi
   0x08048c64 <+40>:    rep movsl %ds:(%esi),%es:(%edi)
   0x08048c66 <+42>:    movzwl (%esi),%edx
   0x08048c69 <+45>:    mov    %dx,(%edi)
   0x08048c6c <+48>:    movzbl 0x11(%eax),%edx
   0x08048c70 <+52>:    mov    %dl,0x10(%eax)
   0x08048c73 <+55>:    mov    %eax,0x4(%esp)
   0x08048c77 <+59>:    mov    0x20(%esp),%eax
   0x08048c7b <+63>:    mov    %eax,(%esp)
   0x08048c7e <+66>:    call   0x80490ca <strings_not_equal>
   0x08048c83 <+71>:    test   %eax,%eax
   0x08048c85 <+73>:    je     0x8048c8c <phase_3+80>
   0x08048c87 <+75>:    call   0x8049363 <explode_bomb>
   0x08048c8c <+80>:    add    $0x14,%esp
   0x08048c8f <+83>:    pop    %esi
   0x08048c90 <+84>:    pop    %edi
   0x08048c91 <+85>:    ret  

Der folgende Block enthält meine Analyse

  5 <phase_3>
  6 0x08048c3c <+0>:     push   %edi // push value in edi to stack
  7 0x08048c3d <+1>:     push   %esi // push value of esi to stack
  8 0x08048c3e <+2>:     sub    $0x14,%esp // grow stack by 0x14 (move stack ptr -0x14 bytes)
  9 
 10 0x08048c41 <+5>:     movl   $0x804a388,(%esp) // put 0x804a388 into loc esp points to
 11 
 12 0x08048c48 <+12>:    call   0x80490ab <string_length> // check string length, store in eax
 13 0x08048c4d <+17>:    add    $0x1,%eax // increment val in eax by 0x1 (str len + 1) 
 14 // at this point, eax = str_len + 1  = 77 + 1 = 78
 15 
 16 0x08048c50 <+20>:    mov    %eax,(%esp) // get val in eax and put in loc on stack
 17 //**** at this point, 0x804a388 should have a value of 78? ****
 18 
 19 0x08048c53 <+23>:    call   0x8048800 <malloc@plt> // malloc --> base ptr in eax
 20 
 21 0x08048c58 <+28>:    mov    $0x804a388,%esi // 0x804a388 in esi 
 22 0x08048c5d <+33>:    mov    $0x13,%ecx // put 0x13 in ecx (counter register)
 23 0x08048c62 <+38>:    mov    %eax,%edi // put val in eax into edi
 24 0x08048c64 <+40>:    rep movsl %ds:(%esi),%es:(%edi) // repeat 0x13 (19) times
 25 // **** populate malloced memory with first 19 (edit: 76) chars of string at 0x804a388 (this string is 77 characters long)? ****
 26 
 27 0x08048c66 <+42>:    movzwl (%esi),%edx // put val in loc esi points to into edx
***** // at this point, edx should contain the string at 0x804a388?
 28 
 29 0x08048c69 <+45>:    mov    %dx,(%edi) // put val in dx to loc edi points to
***** // not sure what effect this has or what is in edi at this point
 30 0x08048c6c <+48>:    movzbl 0x11(%eax),%edx // edx = [eax + 0x11]
 31 0x08048c70 <+52>:    mov    %dl,0x10(%eax) // [eax + 0x10] = dl
 32 0x08048c73 <+55>:    mov    %eax,0x4(%esp) // [esp + 0x4] = eax
 33 0x08048c77 <+59>:    mov    0x20(%esp),%eax // eax = [esp + 0x20]
 34 0x08048c7b <+63>:    mov    %eax,(%esp) // put val in eax into loc esp points to
***** // not sure what effect these movs have
 35 
 36 // edi --> first arg
 37 // esi --> second arg
 38 // compare value in esi to edi
 39 0x08048c7e <+66>:    call   0x80490ca <strings_not_equal> // store result in eax
 40 0x08048c83 <+71>:    test   %eax,%eax 
 41 0x08048c85 <+73>:    je     0x8048c8c <phase_3+80>
 42 0x08048c87 <+75>:    call   0x8049363 <explode_bomb>
 43 0x08048c8c <+80>:    add    $0x14,%esp
 44 0x08048c8f <+83>:    pop    %esi
 45 0x08048c90 <+84>:    pop    %edi
 46 0x08048c91 <+85>:    ret 

Aktualisieren:

Wenn ich die Register überprüfe, bevor strings_not_equal aufgerufen wird, erhalte ich Folgendes:

eax            0x804d8aa        134535338
ecx            0x0      0
edx            0x76     118
ebx            0xffffd354       -11436
esp            0xffffd280       0xffffd280
ebp            0xffffd2b8       0xffffd2b8
esi            0x804a3d4        134521812
edi            0x804f744        134543172
eip            0x8048c7b        0x8048c7b <phase_3+63>
eflags         0x282    [ SF IF ]
cs             0x23     35
ss             0x2b     43
ds             0x2b     43
es             0x2b     43
fs             0x0      0
gs             0x63     99

und ich erhalte den folgenden zerlegten Pseudocode mit Hopper:

Geben Sie hier die Bildbeschreibung ein

Ich habe sogar versucht, sowohl die in eax gefundene Nummer als auch die zuvor als Schlüsselwort angegebene Zeichenfolge zu verwenden, aber keine davon hat funktioniert.

scy8
quelle
3
Ich freue mich, dass Sie eine gut kommentierte Demontage und detaillierte Hinweise dazu veröffentlichen, was der Code Ihrer Meinung nach bewirkt. Wenn nur mehr Leute sich so viel Mühe geben würden, wenn sie Fragen zum Bombenlabor stellen ...
fuz
2
rep movslkopiert 32-Bit-Langwörter von Adresse %esizu Adresse %ediund erhöht beide um jeweils 4, was einer Anzahl von gleich entspricht ecx. Betrachten Sie es als memcpy(edi, esi, ecx*4). Siehe felixcloutier.com/x86/movs:movsb:movsw:movsd:movsq ( movsdin Intel-Notation).
Nate Eldredge
2
Es sind also nicht 19 Bytes, sondern 19 * 4 = 76 Bytes.
Nate Eldredge
@NateEldredge ah Ich sehe, das macht Sinn, weil die Zeichenfolge am Speicherort 0x80490abeine Länge von 77 hat. Und vielen Dank für den Link!
scy8
@fuz danke, leider hat es mir noch nicht geholfen, alles zusammenzusetzen
scy8

Antworten:

3

Die Funktion erstellt eine geänderte Kopie eines Strings aus dem statischen Speicher in einen malloced Puffer.


Das sieht komisch aus. Die mallocGröße ist abhängig von strlen+1, aber die memcpyGröße ist eine Kompilierungszeitkonstante? Ihre Dekompilierung zeigt anscheinend, dass die Adresse ein String-Literal war, also scheint das in Ordnung zu sein.

Wahrscheinlich ist diese verpasste Optimierung auf eine benutzerdefinierte string_length()Funktion zurückzuführen, die möglicherweise nur in einer anderen definiert wurde .c(und die Bombe wurde ohne Optimierung der Verbindungszeit für dateiübergreifendes Inlining kompiliert). Es size_t len = string_length("some string literal");handelt sich also nicht um eine Kompilierungszeitkonstante, und der Compiler hat einen Aufruf an sie gesendet, anstatt die bekannte konstante Länge der Zeichenfolge verwenden zu können.

Aber wahrscheinlich haben sie strcpyin der Quelle verwendet und der Compiler hat das als rep movs. Da es anscheinend aus einem String-Literal kopiert wird, ist die Länge eine Konstante für die Kompilierungszeit und kann den Teil der Arbeit, der strcpynormalerweise zu erledigen ist, optimieren . Normalerweise , wenn Sie bereits über die Länge berechnet haben ist es besser , zu verwenden , memcpyanstatt, strcpyberechnen sie im Fluge wieder, aber in diesem Fall ist es half tatsächlich der Compiler macht besseren Code für den Teil , als wenn sie den Rückgabewert übergeben haben , string_lengthum ein memcpy, wieder weil string_lengthnicht inline und weg optimieren konnte.


   <+0>:     push   %edi // push value in edi to stack
   <+1>:     push   %esi // push value of esi to stack
   <+2>:     sub    $0x14,%esp // grow stack by 0x14 (move stack ptr -0x14 bytes)

Solche Kommentare sind überflüssig. die Anweisung selbst sagt das schon. Dadurch werden zwei aufruferhaltene Register gespeichert, damit die Funktion sie intern verwenden und später wiederherstellen kann.

Ihr Kommentar zu subist besser; Ja, wachsen Sie den Stapel ist die übergeordnete semantische Bedeutung hier. Diese Funktion reserviert etwas Platz für Einheimische (und für Funktionsargumente, mit denen movanstelle von pushed gespeichert werden soll ).


Die rep movsdKopien 0x13 * 4 Bytes erhöhen ESI und EDI, um über das Ende des kopierten Bereichs hinaus zu zeigen. Eine andere movsdAnweisung würde also weitere 4 Bytes kopieren, die an die vorherige Kopie angrenzen.

Der Code kopiert tatsächlich eine weitere 2, verwendet jedoch anstelle der Verwendung movsweine movzwWortladung und einen movSpeicher. Dadurch werden insgesamt 78 Bytes kopiert.

  ...
      # at this point EAX = malloc return value which I'll call buf
<+28>:    mov    $0x804a388,%esi            # copy src = a string literal in .rodata?
<+33>:    mov    $0x13,%ecx
<+38>:    mov    %eax,%edi                  # copy dst = buf
<+40>:    rep movsl %ds:(%esi),%es:(%edi)   # memcpy 76 bytes and advance ESI, EDI

<+42>:    movzwl (%esi),%edx
<+45>:    mov    %dx,(%edi)        # copy another 2 bytes (not moving ESI or EDI)
 # final effect: 78-byte memcpy

Auf einigen (aber nicht allen) CPUs wäre es effizient gewesen, nur rep movsboder rep movswmit angemessener Anzahl zu verwenden, aber das hat der Compiler in diesem Fall nicht gewählt. movzxaka AT & T movzist ein guter Weg, um enge Lasten ohne Teilregistrierungsstrafen zu erledigen. Das ist der Grund, warum Compiler dies tun, damit sie ein vollständiges Register schreiben können, obwohl sie nur die niedrigen 8 oder 16 Bits dieser Registrierung mit einer Speicheranweisung lesen werden.

Nach dieser Kopie eines String-Literal in buf haben wir ein Byte laden / speichern, mit dem ein Zeichen kopiert wird buf. Denken Sie daran, dass EAX an dieser Stelle immer noch auf bufden mallocRückgabewert zeigt. Es wird also eine modifizierte Kopie des String-Literal erstellt.

<+48>:    movzbl 0x11(%eax),%edx
<+52>:    mov    %dl,0x10(%eax)             # buf[16] = buf[17]

Wenn die Quelle die konstante Ausbreitung nicht besiegt hätte, hätte der Compiler bei einer ausreichend hohen Optimierungsstufe möglicherweise nur die letzte Zeichenfolge .rodatadort abgelegt, wo Sie sie finden könnten, und diese Bombenphase trivialisiert. : P.

Anschließend werden Zeiger als Stapelargumente für den Zeichenfolgenvergleich gespeichert.

<+55>:    mov    %eax,0x4(%esp)               # 2nd arg slot = EAX = buf
<+59>:    mov    0x20(%esp),%eax              #  function arg = user input?
<+63>:    mov    %eax,(%esp)                  # first arg slot = our incoming stack arg
<+66>:    call   0x80490ca <strings_not_equal>

Wie man "schummelt": Betrachten Sie das Laufzeitergebnis mit GDB

In einigen Bombenlabors können Sie die Bombe nur online auf einem Testserver ausführen, auf dem Explosionen aufgezeichnet werden. Sie konnten es nicht unter GDB ausführen, sondern nur statische Demontage (wie objdump -drwC -Mintel) verwenden. So konnte der Testserver aufzeichnen, wie viele fehlgeschlagene Versuche Sie hatten. zB wie CS 3330 bei cs.virginia.edu , das ich bei Google gefunden habe, wo volle Gutschrift weniger als 20 Explosionen erfordert.

Die Verwendung von GDB zur Untersuchung von Speicher / Registern auf halbem Weg durch eine Funktion macht dies erheblich einfacher als nur die Arbeit mit statischen Analysen. Diese Funktion wird trivialisiert, wenn die einzelne Eingabe nur ganz am Ende überprüft wird. zB schau dir nur an, an welches andere Argument weitergegeben wird strings_not_equal. (Vor allem, wenn Sie GDBs jumpoder set $pc = ...Befehle verwenden, um die Bombenexplosionsprüfungen zu überspringen.)

Setzen Sie einen Haltepunkt oder Einzelschritt auf kurz vor dem Aufruf von strings_not_equal. Verwenden Sie p (char*)$eaxdiese Option, um EAX als zu behandeln char*und Ihnen die (0-terminierte) C-Zeichenfolge ab dieser Adresse anzuzeigen. Zu diesem Zeitpunkt enthält EAX die Adresse des Puffers, wie Sie vom Speicher bis zum Stapel sehen können.

Kopieren Sie das String-Ergebnis und fügen Sie es ein.

Andere Phasen mit mehreren numerischen Eingaben sind normalerweise nicht so einfach mit einem Debugger zu erstellen und erfordern zumindest einige mathematische Berechnungen. Phasen mit verknüpften Listen, bei denen Sie eine Folge von Zahlen in der richtigen Reihenfolge für die Listenüberquerung benötigen, werden jedoch ebenfalls trivial, wenn Sie wissen, wie Sie mit einem Debugger Register festlegen, damit Vergleiche erfolgreich sind, wenn Sie zu ihnen gelangen.

Peter Cordes
quelle
Ich danke dir sehr! Ich verstehe endlich.
mlz7
Dies räumte auch meine Verwirrung auf! Vielen Dank!
scy8
2

rep movslkopiert 32-Bit-Langwörter von Adresse %esizu Adresse %ediund erhöht beide um jeweils 4, was einer Anzahl von gleich entspricht %ecx. Betrachten Sie es als memcpy(edi, esi, ecx*4).

Siehe https://felixcloutier.com/x86/movs:movsb:movsw:movsd:movsq (es ist movsd in Intel-Notation).

Das kopiert also 19*4=76Bytes.

Nate Eldredge
quelle
danke, ich hatte auch einige andere Dinge, über die ich etwas verwirrt war (unten aufgeführt), wenn Sie mir möglicherweise helfen könnten, sie besser zu verstehen
scy8