Ein Reverse-Engineering-Beispiel mit GDB und Iaito/Radare2

In den letzten Wochen habe ich Reverse Engineering gelernt und einen Blogbeitrag geschrieben, um meine allererste Erfahrung zu teilen. Inzwischen arbeite ich mich etwas weiter vor, löse eine CTF-Herausforderung mit einer Kombination aus statischer Analyse und dynamischer Analyse, und ich fand, dass sich das hervorragend eignet, um Reverse Engineering noch etwas weiter zu erklären.

Ich hatte zuvor vor, einen Artikel über dynamische Analyse mit GDB zu schreiben. Allerdings stellte ich fest, dass diese Herausforderung ebenfalls ein sehr gutes Beispiel ist, um GDB zu erklären. Um diese Aufgabe zu lösen, werde ich ausserdem noch ein weiteres Werkzeug verwenden, iaito, die grafische Oberfläche von radare2.

Die Herausforderung

cIMG ist gewissermassen ein Bildformat, das dazu dient, ein Bild aus Zeichen in unterschiedlichen Farben im Terminal darzustellen. Es besteht aus:

  1. der magischen Zahl, nämlich cIMG;
  2. der Versionsnummer;
  3. Breite und Höhe;
  4. den Daten, wobei jedes Pixel R, G, B und das ASCII-Zeichen enthält.

Bei dieser Herausforderung geht es darum, einem ausführbaren Programm eine cIMG-Datei zu übergeben, das die Flag nur dann ausgibt, wenn die cIMG bestimmte Bedingungen erfüllt.

Der Quelltext

Den Quelltext der Herausforderung erhalten Sie hier. Dieser Quelltext scheint unter der BSD-2-Klausel-Lizenz zu stehen.

Kompilieren Sie ihn mit GCC:

$ gcc -O3 challenge.c -o challenge

Öffnen Sie Python und erstellen Sie eine Beispiel-cIMG-Datei:

$ python3
Python 3.13.5 (Jun 25 2025, 18:55:22) [GCC 14.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> with open("example.cimg", "wb") as file:
...     file.write(b"cIMG\x02\0\x0a\x0a")
...     file.write(b"\x50"*(10*10*4))
...     
8
400

Verwenden Sie das Binärprogramm der Herausforderung, um die Beispiel-cIMG-Datei zu öffnen:

user@learnaarch64asm:~$ ./challenge ./example.cimg 
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP

Diese Datei gibt etwas aus, aber nicht die Flag.

Statische Analyse

Wir müssen eine cIMG-Datei erstellen, die die Flag ausgibt. Lassen Sie uns zunächst eine statische Analyse durchführen.

Öffnen Sie die Datei in iaito. Führen Sie eine aaa-Analyse aus und öffnen Sie den Graphen der Funktion main. Sie erhalten:

den Graphen der Funktion main

Wir finden das Einlesen des Dateikopfs bei 0x00000cac, die Prüfung der magischen Zahl bei 0x00000cc4, die Versionsprüfung bei 0x00000cd0 sowie das Einlesen von Breite und Höhe bei 0x00000ce0. Danach wird malloc verwendet, um Speicher für die Bilddaten zu reservieren, und die Daten werden in diesen Speicher eingelesen.

Die magische Zahl ist cIMG, was in Hex 63 49 4d 47 entspricht; die Version ist eine 32-Bit-Little-Endian-Ganzzahl; sowohl Höhe als auch Breite sind 8-Bit-Ganzzahlen.

Bedingung für die Flag

Gehen wir zum Ende der Funktion. Offensichtlich ist sym.win die Funktion, die uns die Flag gibt. Sie wird bei 0x00000e8c aufgerufen, und dieser Aufruf erfolgt nach einer Prüfung bei 0x00000e58. Diese Prüfung verlangt, dass w19 nicht null ist.

Manipulation des Kontrollflusses

Es ist Zeit, GDB zu starten.

$ gdb --args ./challenge ./example.cimg 
GNU gdb (Debian 16.3-1) 16.3
Copyright (C) 2024 Free Software Foundation, Inc.
License GPLv3+ or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "aarch64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./challenge...
(No debugging symbols found in ./challenge)

Setzen Sie einen Breakpoint auf die Funktion main und starten Sie das Programm:

(gdb) break *main
Breakpoint 1 at 0xc44
(gdb) run
Starting program: /home/user/challenge ./example.cimg
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1".

Breakpoint 1, 0x0000aaaaaaaa0c44 in main ()

Wir merken uns, dass GDBs Datenadressen einen Versatz von 0xaaaaaaaa0000 haben.

Setzen Sie einen Breakpoint direkt vor der Prüfung der Flag-Bedingung und fahren Sie fort:

(gdb) break *0x0000aaaaaaaa0e58
Breakpoint 2 at 0xaaaaaaaa0e58
(gdb) continue
Continuing.
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP

Breakpoint 2, 0x0000aaaaaaaa0e58 in main ()

Ändern Sie den Inhalt des Registers w19 und fahren Sie fort:

(gdb) set $w19 = 1
(gdb) continue

Dann wird das ausführbare Programm uns die Flag geben. Damit ist die Aufgabe beendet, und haben Sie einen schönen Tag :-p

Nur ein Scherz. Wenn wir die Herausforderung tatsächlich so abschliessen würden, wäre der Reiz von RE weg. Gehen wir tiefer hinein.

Datenprüfung

Gehen Sie zurück zu iaito. Klicken Sie im Graphen auf w19. Dadurch wird dieses Register in allen Instruktionen des Graphen hervorgehoben. Scrollen Sie im Graphen zurück, um einen Hinweis zu finden.

Wir können einige Aufrufe von sym.imp.memcmp im Graphen sehen.

memcmp ist eine Standardfunktion der C-Bibliothek, die in <string.h> deklariert ist und zwei Speicherblöcke byteweise vergleicht:

int memcmp(const void *ptr1, const void *ptr2, size_t n);

ptr1 und ptr2 zeigen jeweils auf die beiden Blöcke, und n gibt die Anzahl der zu vergleichenden Bytes an. Wenn die beiden Blöcke identisch sind, gibt die Funktion 0 zurück; andernfalls wird eine von null verschiedene Zahl zurückgegeben.

Es gibt vier memcmp-Aufrufe. Auf jeden Aufruf folgt cmp w0, 0. Ausser beim ersten Aufruf gibt es ausserdem nach cmp noch cset w0, eq und and w19, w19, w0. Daraus können wir schliessen, dass w19 gelöscht wird, wenn memcmp einen Unterschied findet, da w0 dann null sein wird. Deshalb vermuten wir, dass wir die vier Paare von Speicherblöcken gleich machen müssen.

x0 und x1 sind die beiden zu vergleichenden Adressen. x2 enthält die Grösse; mov x2, 0x18 sagt uns, dass die Grösse 24 Bytes beträgt.

Öffnen Sie GDB erneut und untersuchen Sie den Speicher vor dem ersten memcmp:

(gdb) break *0x0000aaaaaaaa0e78
Breakpoint 1 at 0xaaaaaaaa0e78
(gdb) run
Starting program: /home/user/challenge ./example.cimg
[Thread debugging using libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1].
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP
PPPPPPPPPP

Breakpoint 1, 0x0000aaaaaaaa0e78 in main ()
(gdb) info registers x0 x1
x0             0xaaaaaaac12a0      187649984565920
x1             0xaaaaaaac00c0      187649984561344
(gdb) x/24xb $x0
0xaaaaaaac12a0: 0x1b    0x5b    0x33    0x38    0x3b    0x32    0x3b    0x30
0xaaaaaaac12a8: 0x38    0x30    0x3b    0x30    0x38    0x30    0x3b    0x30
0xaaaaaaac12b0: 0x38    0x30    0x6d    0x50    0x1b    0x5b    0x30    0x6d
(gdb) x/24xb $x1
0xaaaaaaac00c0 <desired_output>:        0x1b    0x5b    0x33    0x38    0x3b   0x32     0x3b    0x32
0xaaaaaaac00c8 <desired_output+8>:      0x33    0x31    0x3b    0x30    0x31   0x37     0x3b    0x31
0xaaaaaaac00d0 <desired_output+16>:     0x33    0x30    0x6d    0x63    0x1b   0x5b     0x30    0x6d

Zufällige Bytes. Ich kann nicht herausfinden, was sie bedeuten.

Gehen Sie zurück zu iaito. Der erste Speicherblock wird aus x22 geladen, und x22 wird aus der Adresse var_48h geladen. afv zeigt uns, dass sie 0x48 von sp entfernt liegt:

[0x00000e70]> afv
arg signed int argc @ x0
arg char ** s @ x1
var int64_t var_50h @ sp+0x0
var int64_t var_50h_2 @ sp+0x8
var int64_t var_10h @ sp+0x10
var int64_t var_10h_2 @ sp+0x18
var int64_t var_20h @ sp+0x20
var int64_t var_20h_2 @ sp+0x28
var void * buf @ sp+0x38
var int64_t var_3ch @ sp+0x3c
var int64_t var_3eh @ sp+0x3e
var int64_t var_3fh @ sp+0x3f
var int64_t var_40h @ sp+0x40
var int64_t var_48h @ sp+0x48

Starten Sie GDB neu und überspringen Sie den Funktionsprolog, nachdem main aufgerufen wurde:

(gdb) break *main
Breakpoint 1 at 0xaaaaaaaa0c44
(gdb) run
Starting program: /home/user/challenge ./example.cimg
[Thread debugging using libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1].

Breakpoint 1, 0x0000aaaaaaaa0c44 in main ()
(gdb) si
0x0000aaaaaaaa0c48 in main ()
(gdb) si
0x0000aaaaaaaa0c4c in main ()

Prüfen Sie den Wert des Stackpointers und setzen Sie einen Watchpoint:

(gdb) info registers sp
sp             0xfffffffff290      0xfffffffff290
(gdb) watch *(long long *)0xfffffffff2d8
Hardware watchpoint 2: *(long long *)0xfffffffff2d8

Fahren Sie fort:

(gdb) continue
Continuing.

Hardware watchpoint 2: *(long long *)0xfffffffff2d8

Old value = 281474840286680
New value = 0
0x0000aaaaaaaa0c5c in main ()

Lokalisieren Sie diese Adresse in iaito. Wir können sehen, dass bei 0x00000c58 (der vorherigen Instruktion zu 0x00000c5c) var_48h durch ein str auf null gesetzt wird.

Fahren Sie fort, um herauszufinden, wo die Variable gesetzt wird:

(gdb) continue
Continuing.

Hardware watchpoint 2: *(long long *)0xfffffffff2d8

Old value = 0
New value = 187649984565920
0x0000aaaaaaaa1358 in initialize_framebuffer ()

Wir sehen eine neue Funktion, die wir bisher noch nicht untersucht haben: initialize_framebuffer.

Gehen Sie in iaito zu dieser Funktion:

den Graphen der Funktion initialize\_framebuffer

Wir können sehen, dass eine von malloc reservierte Speicheradresse durch die Instruktion bei 0x00001354 in der Variable gespeichert wird. Sie wird jedoch als [x22, 0x10] dargestellt. Prüfen wir den Inhalt von x22:

(gdb) info registers $x22
x22            0xfffffffff2c8      281474976707272

0xfffffffff2c8 + 0x10 = 0xfffffffff2d8. Genau die Adresse von var_48h.

Ausserdem ist die an malloc übergebene Grösse interessant. Sie wird berechnet, indem die Werte an den Offsets 6 und 7 miteinander multipliziert werden, dann das Ergebnis mit w1 multipliziert wird, welches 0x18 (24) ist, und dann x0 addiert wird, welches 1 ist.

Lassen Sie uns diese Werte an den Offsets ebenfalls untersuchen:

(gdb) x/1xb 0xfffffffff2ce
0xfffffffff2ce: 0x0a
(gdb) x/1xb 0xfffffffff2cf
0xfffffffff2cf: 0x0a

Beide sind 10. Sie scheinen die von uns angegebene Bildbreite und Bildhöhe zu sein. Prüfen wir das mit Hardware-Watchpoints. Da AArch64 eine Ausrichtung verlangt, setzen wir den Hardware-Watchpoint bei 0xfffffffff2cc:

(gdb) watch *(unsigned int *)0xfffffffff2cc
Hardware watchpoint 1: *(unsigned int *)0xfffffffff2cc
(gdb) run
Starting program: /home/user/challenge ./example.cimg

Überspringen Sie die Auslöser der GNU C Library:

Hardware watchpoint 1: *(unsigned int *)0xfffffffff2cc

Old value = 0
New value = 65535
0x0000fffff7fccd08 in ?? () from /lib/ld-linux-aarch64.so.1
(gdb) continue
Continuing.

Hardware watchpoint 1: *(unsigned int *)0xfffffffff2cc

Old value = 65535
New value = 0
0x0000fffff7fd5668 in ?? () from /lib/ld-linux-aarch64.so.1
(gdb) continue
Continuing.
[Thread debugging using libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1"]

Hardware watchpoint 1: *(unsigned int *)0xfffffffff2cc

Old value = 0
New value = 65535
0x0000fffff7fcc3a4 in ?? () from /lib/ld-linux-aarch64.so.1
(gdb) continue
Continuing.

Hier sehen wir, dass der Speicher bei 0x00000c54 auf null gesetzt wird:

Hardware watchpoint 1: *(unsigned int *)0xfffffffff2cc

Old value = 65535
New value = 0
0x0000aaaaaaaa0c58 in main ()

Wir sehen ein stp xzr, xzr, [buf]. Aus dem obigen afv wissen wir, dass buf 0x38 vom Stackpointer entfernt liegt.

Bei 0x00000cb8 finden wir ein ldr aus buf, gefolgt von einer Prüfung der magischen Zahl.

Bei 0x00000c9c finden wir etwas Interessantes:

0x00000c9c      add      x21,    sp,     0x38
0x00000ca0      mov      x2,     8                          ; size_t nbyte
0x00000ca4      mov      x1,     x21                        ; void *buf
0x00000ca8      mov      w0,     0
0x00000cac      bl       sym.imp.read                       ; ssize_t read(int fildes, void *buf, size_t nbyte)
; ssize_t read(0, 0x0000000000000000, 0x00000000)

Damit haben wir bestätigt, dass die beiden Werte an diesen Offsets Breite und Höhe sind.

Gehen Sie in iaito und GDB zurück zur Funktion initialize_framebuffer. Setzen Sie einen Watchpoint und untersuchen Sie den Speicher:

(gdb) break *0xaaaaaaaa1354
Breakpoint 1 at 0xaaaaaaaa1354
(gdb) run
Starting program: /home/user/challenge ./example.cimg
[Thread debugging using libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1].

Breakpoint 1, 0x0000aaaaaaaa1354 in initialize_framebuffer ()
(gdb) info registers x0
x0             0xaaaaaaac12a0      187649984565920
(gdb) watch *(char *)0xaaaaaaac12a0
Hardware watchpoint 2: *(char *)0xaaaaaaac12a0
(gdb) continue
Continuing.

Hardware watchpoint 2: *(char *)0xaaaaaaac12a0

Old value = 0 '\000'
New value = 27 '\033'
0x0000aaaaaaaa13bc in initialize_framebuffer ()
(gdb) x/24xb 0xaaaaaaac12a0
0xaaaaaaac12a0: 0x1b    0x5b    0x33    0x38    0x3b    0x32    0x3b    0x32
0xaaaaaaac12a8: 0x35    0x35    0x3b    0x32    0x35    0x35    0x3b    0x32
0xaaaaaaac12b0: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00

Vergleichen Sie das mit den obigen Daten:

(gdb) x/24xb $x0
0xaaaaaaac12a0: 0x1b    0x5b    0x33    0x38    0x3b    0x32    0x3b    0x30
0xaaaaaaac12a8: 0x38    0x30    0x3b    0x30    0x38    0x30    0x3b    0x30
0xaaaaaaac12b0: 0x38    0x30    0x6d    0x50    0x1b    0x5b    0x30    0x6d

0xaaaaaaac12a7 ist das erste Byte, das sich unterscheidet. Wir können dafür einen Watchpoint setzen:

(gdb) watch *(char *)0xaaaaaaac12a7
Hardware watchpoint 3: *(char *)0xaaaaaaac12a7
(gdb) continue
Continuing.

Hardware watchpoint 3: *(char *)0xaaaaaaac12a7

Old value = 50 '2'
New value = 48 '0'
0x0000aaaaaaaa128c in display ()

Eine neue Funktion, die es zu untersuchen gilt. Öffnen Sie sie in iaito.

den Graphen der Funktion initialize\_framebuffer

Wir können sehen, dass die Datenänderung bei 0x00001288 in einer stp-Instruktion stattfindet. Die str-Instruktion bei 0x0000128c ist ebenfalls interessant; untersuchen wir hier den Speicher:

(gdb) x/24xb 0xaaaaaaac12a0
0xaaaaaaac12a0: 0x1b    0x5b    0x33    0x38    0x3b    0x32    0x3b    0x30
0xaaaaaaac12a8: 0x38    0x30    0x3b    0x30    0x38    0x30    0x3b    0x30
0xaaaaaaac12b0: 0x35    0x35    0x6d    0x20    0x1b    0x5b    0x30    0x6d
(gdb) si
0x0000aaaaaaaa1290 in display ()
(gdb) x/24xb 0xaaaaaaac12a0
0xaaaaaaac12a0: 0x1b    0x5b    0x33    0x38    0x3b    0x32    0x3b    0x30
0xaaaaaaac12a8: 0x38    0x30    0x3b    0x30    0x38    0x30    0x3b    0x30
0xaaaaaaac12b0: 0x38    0x30    0x6d    0x50    0x1b    0x5b    0x30    0x6d

Der 8-Byte-Block ab 0xaaaaaaac12b0 wurde durch die str-Instruktion geändert. Das Register erzählt die Geschichte:

(gdb) info registers x2
x2             0xaaaaaaac12a0      187649984565920

Wir müssen uns um x2 nicht zu sehr kümmern. Konzentrieren wir uns auf x4, x5 und x1. Bei 0x00001268 und 0x00001270 sehen wir ein sehr wichtiges Register, x25. Es enthält die Adresse des Speichers, der die Daten enthält, die in den durch x2 adressierten Speicher geschrieben werden. Untersuchen wir diesen Speicher:

(gdb) x/24xb $x25
0xfffffffff270: 0x1b    0x5b    0x33    0x38    0x3b    0x32    0x3b    0x30
0xfffffffff278: 0x38    0x30    0x3b    0x30    0x38    0x30    0x3b    0x30
0xfffffffff280: 0x38    0x30    0x6d    0x50    0x1b    0x5b    0x30    0x6d

Bei 0x00001238 wird die Adresse in x25 nach x0 kopiert. Danach werden die Datenstücke in die folgenden Register kopiert. Dann erfolgt ein Aufruf von snprintf. snprintf schreibt formatierten Output in ein Zeichenfeld:

int snprintf(char *str, size_t size, const char *format, ...);

Es schreibt also eine formatierte Zeichenkette in den Speicher bei x25. Starten wir GDB neu und untersuchen den von x2 adressierten Speicher:

(gdb) break *0xaaaaaaaa1258
Breakpoint 1 at 0xaaaaaaaa1258
(gdb) run
Starting program: /home/user/challenge ./example.cimg
[Thread debugging using libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1].

Breakpoint 1, 0x0000aaaaaaaa1258 in display ()
(gdb) x/s $x2
0xaaaaaaaa1510: "\033[38;2;%03d;%03d;%03dm%c\033[0m"
(gdb) x/29xb $x2
0xaaaaaaaa1510: 0x1b    0x5b    0x33    0x38    0x3b    0x32    0x3b    0x25
0xaaaaaaaa1518: 0x30    0x33    0x64    0x3b    0x25    0x30    0x33    0x64
0xaaaaaaaa1520: 0x3b    0x25    0x30    0x33    0x64    0x6d    0x25    0x63
0xaaaaaaaa1528: 0x1b    0x5b    0x30    0x6d    0x00

Das ist die Formatzeichenkette. Wenn Sie mit der von x/s zurückgegebenen Zeichenkette nicht zufrieden sind, können Sie Python verwenden, um die Zeichenkette Byte für Byte auszugeben:

>>> print(b"\x1b\x5b\x33\x38\x3b\x32\x3b\x25\x30\x33\x64\x3b\x25\x30\x33\x64\x3\
b\x25\x30\x33\x64\x6d\x25\x63\x1b\x5b\x30\x6d\x00")
b'\x1b[38;2;%03d;%03d;%03dm%c\x1b[0m\x00'
>>> blob = b'\x1b[38;2;%03d;%03d;%03dm%c\x1b[0m\x00'
>>> for i1 in range(len(blob)):
...     blob[i1:i1+1]
...     
b'\x1b'
b'['
b'3'
b'8'
b';'
b'2'
b';'
b'%'
b'0'
b'3'
b'd'
b';'
b'%'
b'0'
b'3'
b'd'
b';'
b'%'
b'0'
b'3'
b'd'
b'm'
b'%'
b'c'
b'\x1b'
b'['
b'0'
b'm'
b'\x00'

Diese Formatzeichenkette hat vier Platzhalter. Die ersten drei sind %03d, die eine Ganzzahl als Dezimalzahl mit mindestens drei Ziffern formatieren (zum Beispiel wird aus 7 007, und aus 15 wird 015). Der letzte ist %c, der nur ein einzelnes Zeichen akzeptiert.

Nun haben wir das folgende Byte-Muster:

0x1b    0x5b    0x33    0x38    0x3b    0x32    0x3b    [N1]
[N1]    [N1]    0x3b    [N2]    [N2]    [N2]    0x3b    [N3]
[N3]    [N3]    0x6d    [CH]    0x1b    0x5b    0x30    0x6d

Wir können nun untersuchen, was die Platzhalter füllt:

(gdb) info registers x3 x4 x5 x6
x3             0x50                80
x4             0x50                80
x5             0x50                80
x6             0x50                80

Alle sind 0x50, was offenbar die Daten sind, die wir in der Beispiel-cIMG verwenden. Erstellen wir die cIMG nun erneut, um das zu verdeutlichen:

>>> with open("example.cimg", "wb") as file:
...     file.write(b"cIMG\x02\0\x0a\x0a\x12\x34\x56\x78")
...     file.write(b"\x50"*(10*10*4-4))
...     
12
396
(gdb) break *0xaaaaaaaa1258
Breakpoint 1 at 0xaaaaaaaa1258
(gdb) run
Starting program: /home/user/challenge ./example.cimg
[Thread debugging using libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1].

Breakpoint 1, 0x0000aaaaaaaa1258 in display ()
(gdb) info registers x3 x4 x5 x6
x3             0x12                18
x4             0x34                52
x5             0x56                86
x6             0x78                120

Damit entsprechen N1, N2 und N3 im obigen Hex-Muster R, G und B, und CH ist einfach das Zeichen, das wir einsetzen wollen.

Nun können wir zu GDB zurückkehren, vor jedem memcmp einen Breakpoint setzen, mit x/24xb $x1 die erwarteten Framebuffer ermitteln und daraus die erwarteten cIMG-Daten rekonstruieren. Anschliessend füllen wir die Daten mit einem Hex-Editor ein.

Grössenprüfung

Nach dem Einfügen der erwarteten Daten gibt das Programm jedoch immer noch nicht die Flag aus. Nachdem wir mit GDB den Speicher untersucht haben, sehen wir, dass die memcmp-Aufrufe erwartungsgemäss Null zurückgeben. Es muss also noch andere Bedingungen geben, die nicht erfüllt sind.

Gehen Sie in iaito zurück und scrollen Sie vom ersten memcmp aus rückwärts. Wir sehen ein cmp bei 0x00000d60, das var_40h mit 4 vergleicht. Bei 0x00001348 in initialize_framebuffer finden wir ein str, das das Produkt aus Breite und Höhe speichert.

Das cmp bei 0x00000d60 bestimmt dann den Wert von w0. Wenn var_40h gleich 4 ist, wird w0 zu 1; andernfalls wird es 0.

Bei 0x00000d78 wird w0 mit 0 verglichen, was das Verhalten der Instruktion ccmp bei 0x00000d84 beeinflusst. Der Inhalt von [x22, 0x13] wird in das Register w0 geladen, und [x1, 0x13] wird in w2 geladen. Aus memcmp können wir leicht erkennen, dass x22 die tatsächliche Adresse des Framebuffers ist und x1 die erwartete Adresse des Framebuffers. Da wir die Daten bereits so angepasst haben, dass sie mit den erwarteten Daten übereinstimmen, und der Offset 19 ein ASCII-Zeichen ist, sollten w0 und w2 gleich und ungleich null sein.

Wir können daraus schliessen, dass, wenn var_40h gleich 4 ist, w2 mit w0 verglichen wird, wodurch w19 auf 1 gesetzt wird. Andernfalls wird w2 mit 0 verglichen, wodurch w19 auf null gesetzt wird.

Nach dem ersten memcmp wird w19 mit 0 verglichen, da w0 bei 0x00000e7c gleich null ist. Dann setzt cset w19 auf 1, wenn ungleich, andernfalls auf 0.

Ändern Sie nun die cIMG mit einem Hex-Editor und setzen Sie Höhe und Breite auf Werte, deren Produkt 4 ergibt. Beispielsweise können wir 0202 verwenden. Nun sollte das ausführbare Programm die Flag ausgeben.

Fazit

Das obige Verfahren umfasst:

  1. statische Analyse mit radare2 und iaito;

  2. dynamische Analyse mit GDB, einschliesslich:

    • Manipulation des Kontrollflusses;
    • Speicherinspektion;
    • Ganzzahlinspektion.

Das ist ein sehr typisches Vorgehen beim Reverse Engineering.

Übrigens gibt es möglicherweise auch andere Ansätze, um dieses ausführbare Programm zu rekonstruieren. Doch unabhängig davon, welcher Ansatz verwendet wird, umfasst er normalerweise sowohl statische Analyse als auch dynamische Analyse.

Erstellt mit Hugo
Theme Stack gestaltet von Jimmy