
CTF pwn challenge writeup, der CVE-2021-4034 (pkexec) mittels Heap-Manipulation ausnutzt, mit Ghidra-basiertem Reverse Engineering und einer benutzerdefinierten shelly.so-Hilfsbinärdatei.
Dies ist eine CTF-Pwn-Challenge, die ich in C geschrieben habe und die vom Benutzer erfordert, die CVE-2021-4034-Schwachstelle auszunutzen. Die Spieler erhalten 2 Binärdateien im challenge-Verzeichnis in diesem Repository. Die chal-Binärdatei implementiert die CTF-Challenge und shelly.so ist eine Hilfsbinärdatei.
Zum Zeitpunkt der Erstellung dieses Writeups ist das Dockerfile noch nicht vollständig. Das Dockerfile ist erforderlich, um diese Challenge während eines Live-CTFs bereitzustellen, aber nicht lokal: Sie können diese Challenge dennoch lokal emulieren, indem Sie die Benutzerberechtigungen einrichten und die anfälligen Pakete wie folgt installieren:
libpolkit-gobject-1-0=0.105-26ubuntu1 libpolkit-agent-1-0=0.105-26ubuntu1 policykit-1=0.105-26ubuntu1.flag.txt-Datei, die root:root gehört, im aktuellen Verzeichnis.challenge-Ordner in das aktuelle Verzeichnis herunter.chal als unprivilegierter Benutzer aus.Das Ausführen der chal-Binärdatei gibt uns eine vage Vorstellung davon, was diese Binärdatei tut:```
WELCOME TO THE HUB CTRL+ALT+DELICIOUS
We're not just a sandwich hub. We are the beacon of flavors, serving a symphony in every byte
Spielt man mit der Eingabegröße in der `Add`-Funktion und den Indizes der `Cancel`-Funktionen herum, erhält man nichts Besonderes (kein Überlauf oder Segmentierungsfehler). Dennoch ein paar interessante Dinge:
- Es scheint, als ob die Bestellungen in einer Liste (vielleicht einer verketteten Liste) abgelegt werden und sie 0-indexiert sind?
- Es gibt diese `Bestellnummer: 0x7ffde93681f0`, die offenbar eine Speicherstelle auf dem Stack ausgibt?
- Außerdem erstellt das Programm ein Verzeichnis mit unserem Eingabenamen zusammen mit 3 ausführbaren Dateien, von denen eine die uns gegebene Hilfsbinärdatei ist:```sh
peasant@Tin-VM:~/Desktop$ ls
chal chal.c Dockerfile shelly.so solve.py tin
peasant@Tin-VM:~/Desktop$ ls -l tin/
total 28
-rwxrwx--- 1 peasant vboxsf 5 Feb 4 14:33 notes
-rwxrwx--- 1 peasant vboxsf 20 Feb 4 14:33 recipe
-rwxr-x--- 1 peasant vboxsf 16488 Feb 4 14:33 shelly.so
peasant@Tin-VM:~/Desktop$ cat tin/notes
0000
peasant@Tin-VM:~/Desktop$ cat tin/recipe
aaaa
bbbb
cccc
dddc
Die ausführbaren Dateien enthalten unsere Eingabe.
Sie können mit den anderen Funktionen herumspielen und hoffen, auf einige Fehler zu stoßen (was wahrscheinlich ist), aber ich komme direkt zur Sache und öffne das Programm in Ghidra.
Beim Vergleichen der Zeichenfolgen, die während der Programmausführung erscheinen, und der Zeichenfolgen, die in Ghidra vorhanden sind, können wir einige der FUN_*-Funktionen in vertraute Namen umbenennen:```C
undefined8 main(void)
{ int iVar1; size_t sVar2; undefined2 *puVar3; long in_FS_OFFSET; int opt; int local_1c; char *local_18; long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28); local_18 = "/recipe"; print("WELCOME TO THE HUB CTRL+ALT+DELICIOUS\n"); print( "We're not just a sandwich hub. We are the beacon of flavors, serving a symphony in every by te\n\n" ); while( true ) { print("1. ENTER THE HUB\n"); print("2. QUIT\n"); __isoc99_scanf(&DAT_001030c5,&opt); getc(stdin); if (opt != 1) break; printf("Order number: %p\n",&local_18); print("Enter your name: "); __isoc99_scanf(&DAT_0010334f,&DAT_00105120); sVar2 = strlen(&DAT_00105120); puVar3 = (undefined2 *)malloc(sVar2 + 2); DAT_00105100 = puVar3; *puVar3 = 0x2f2e; *(undefined *)(puVar3 + 1) = 0; strcpy((char *)(DAT_00105100 + 1),&DAT_00105120); iVar1 = FUN_001022f0(DAT_00105100,&DAT_00105060); if (iVar1 == -1) { mkdir((char *)DAT_00105100,0x1c0); } DAT_00105140 = 0; order_cnt = 0; for (local_1c = 0; local_1c < 10; local_1c = local_1c + 1) { *(undefined8 *)(&ptr_array + (long)local_1c * 8) = 0; } main_menu(); } print("Come again :)\n");
Das `printf("Order number: %p\n",&local_18);` **gibt einen Speicherort einer lokalen Variable auf dem Stack aus.**
Eine schnelle Überprüfung in gdb zeigt uns, dass die geleakte Adresse die Adresse des Zeigers auf die konstante Zeichenkette `/recipe` ist:```gdb
...
Order number: 0x7fffffffdfc0
...
gef➤ x/gx 0x7fffffffdfc0
0x7fffffffdfc0: 0x0000555555559020
gef➤ x/s 0x0000555555559020
0x555555559020: "/recipe"
Wir sehen einen mkdir-Aufruf, der ein Verzeichnis mit unserem eingegebenen Namen im aktuellen Verzeichnis erstellt.
Dies stimmt mit unserer Beobachtung überein, als wir das Programm ausgeführt haben. Dann initialisiert es einige Variablen, bevor es die Funktion main_menu aufruft, die etwa so aussieht:```C
while( true ) {
while( true ) {
while( true ) {
while( true ) {
while( true ) {
while( true ) {
print("1. ADD NEW ORDER\n");
print("2. EDIT ORDER\n");
print("3. SHOW ORDER\n");
print("4. CANCEL ORDER\n");
print("5. CHECKOUT\n");
print("6. DONE\n");
__isoc99_scanf(&DAT_001030c5,&local_40);
getc(stdin);
if (local_40 != 1) break;
add_order();
}
if (local_40 != 2) break;
edit_order();
}
if (local_40 != 3) break;
show_order();
}
if (local_40 != 4) break;
cancel_order();
}
if (local_40 != 5) break;
checkout();
}
if (local_40 == 6) break;
if (local_40 == 0x539) {
print(
"\nGORDON RAMSAY: Finally, a worthy opponent, our battle will be legendary! I BET YOU CAN 'T GUESS THE SECRET RECIPE.\n"
);
fgets(inp,0x20,stdin);
getrandom(random-bytes,0x10,0);
for (local_3c = 0; local_3c < 0x10; local_3c = local_3c + 1) {
if (inp[local_3c] != random-bytes[local_3c]) {
print("...Nuh Uh!...\n");
/* WARNING: Subroutine does not return */
exit(0);
}
print("...Ooh Yes.. sCruMpTioUs...");
}
print("Fine... I'll give you a taste.\n");
FUN_00101504();
}
Falls du mit REV nicht vertraut bist: Dies ist die Dekompilierung für die `switch`-Anweisung in C. Es gibt eine interessante Option alias `0x539`. Sie erlaubt es den Spielern, `0x10` zufällige Bytes zu erraten. Wenn alle Bytes gleich sind, wird `FUN_00101504();` aufgerufen, das wiederum `system("cat flag.txt");` aufruft. Ansonsten beendet sich das Programm. Allerdings ist das Erraten von 16 zufälligen Bytes gleichbedeutend mit dem Durchspielen aller 256**16 = 340282366920938463463374607431768211456 Möglichkeiten. Viel Glück dabei lol. Selbst wenn man alle Möglichkeiten durchspielt, ist das Programm immer noch unprivilegiert, sodass es die Flagge nicht lesen kann. Diese `cat flag.txt`-Anweisung ist absichtlich eingebaut, nicht nur um unerfahrene Spieler dazu zu verleiten, diese `0x539`-Menüoption zu wählen, sondern auch um zu verhindern, dass die Spieler einfach diese Funktion aufrufen, um die Flagge zu lesen – worauf wir später noch eingehen werden.
***
## Hinzufügen```C
int iVar1;
undefined8 *puVar2;
void *pvVar3;
undefined8 *ptr2;
if (order_cnt < 10) {
puVar2 = (undefined8 *)malloc(0x30);
print("Pick your bread: ");
readline(puVar2 + 1,8);
print("Select your spread: ");
readline(puVar2 + 2,8);
print("Choose your veg: ");
readline(puVar2 + 3,8);
print("Slam your meat & egg: ");
readline(puVar2 + 4,9);
iVar1 = order_cnt;
pvVar3 = malloc(0x30);
*(void **)(&ptr_array + (long)iVar1 * 8) = pvVar3;
*puVar2 = *(undefined8 *)(&ptr_array + (long)order_cnt * 8);
print("Any side notes for the cook? ");
readline(*puVar2,0x30);
puVar2[5] = 0;
if (DAT_00105140 != (undefined8 *)0x0) {
for (ptr2 = DAT_00105140; ptr2[5] != 0; ptr2 = (undefined8 *)ptr2[5]) {
}
ptr2[5] = puVar2;
puVar2 = DAT_00105140;
}
DAT_00105140 = puVar2;
order_cnt = order_cnt + 1;
}
...
Wenn einige der Variablen für Sie nicht leicht lesbar aussehen, liegt es daran, dass ich mir die Zeit genommen habe, sie in diese umzubenennen, was Sie beim Reverse Engineering tun sollten, um den Überblick zu behalten. Also, kommen wir zu den Hauptpunkten:
puVar2 ist ein malloc-Chunk, der 0x30 groß ist.puVar3-Zeiger, der 8 Byte lang ist und auf die notes des aktuellen Chunks zeigt.puVar2[5] = 0; unseren 1-Byte-Überlauf trotzdem mit NULL.DAT_00105140 == 0 ist, setzen wir sie einfach auf den neuen Chunk, also könnte dies ein head-Zeiger für die Liste sein?... print("Enter order index: "); __isoc99_scanf(&DAT_001030c5,&local_20); getc(stdin); if ((local_20 < 0) || (order_cnt <= local_20)) { print("Invalid index!\n"); } else { local_18 = head; for (local_1c = 0; local_1c != local_20; local_1c = local_1c + 1) { local_18 = (undefined8 *)local_18[5]; } print("Pick your bread: "); readline(local_18 + 1,8); print("Select your spread: "); readline(local_18 + 2,8); print("Choose your veg: "); readline(local_18 + 3,8);WE print("Slam your meat & egg: "); readline(local_18 + 4,9); print("Any side notes for the cook? "); readline(*local_18,0x30); } ...
Es gibt eine Überprüfung unseres Eingabeindex, daher können wir nicht an beliebigen Stellen bearbeiten.
**Allerdings ist der 9-Byte-Lesevorgang im 4. Feld, der 1 Byte in das 5. Feld überläuft, immer noch vorhanden!!! Wir können 1 Byte in den `nxt`-Zeiger überschreiben.**
***
## Anzeigen```C
...
if (local_18 != (undefined8 *)0x0) {
printf("%s, %s, %s, %s, %s\n",local_18 + 1,local_18 + 2,local_18 + 3,local_18 + 4,*local_18);
}
...
%s wird bis zum NULL-Zeichen ausgeben, wenn also unser 4. Feld 8 Byte lang ist, dann gibt das 4. %s das 4. Feld unseres Chunks plus den Zeigerwert nxt im 5. Feld aus.
=> Wir bekommen einen heap leak!!!
Hier gibt es nichts Interessantes.
strcpy(local_d8,dir_name); sVar2 = strlen(dir_name); strcpy(local_d8 + sVar2,"/recipe"); creat(local_d8,0x1c0); iVar1 = open(local_d8,2); if (iVar1 == -1) { print("Error opening file f.\n"); /* WARNING: Subroutine does not return / exit(0); } chmod(local_d8,0x1f8); strcpy(local_98,dir_name); sVar2 = strlen(dir_name); strcpy(local_98 + sVar2,"/notes"); printf("%s %s\n","/notes",local_98); creat(local_98,0x1c0); __fd = open(local_98,2); if (__fd == -1) { print("Error opening file f_notes.\n"); / WARNING: Subroutine does not return */ exit(0); } chmod(local_98,0x1f8);
Also erstellt es die Dateien `recipe` und `notes` im Verzeichnis mit unserem Eingabenamen. Die `chmod`-Anweisungen setzen diese Dateien in den ausführbaren Modus: `0x1f8` und `0x1c0` sind `0700` bzw. `0770` im Oktalsystem.```C
...
for (local_ec = 0; (local_e0 != (char **)0x0 && (local_ec < order_cnt)); local_ec = local_ec + 1)
{
sVar2 = strlen((char *)(local_e0 + 1));
write(iVar1,local_e0 + 1,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen((char *)(local_e0 + 2));
write(iVar1,local_e0 + 2,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen((char *)(local_e0 + 3));
write(iVar1,local_e0 + 3,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen((char *)(local_e0 + 1));
write(iVar1,local_e0 + 4,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen(*local_e0);
write(__fd,*local_e0,sVar2);
write(__fd,&DAT_00103127,1);
local_e0 = (char **)local_e0[5];
}
iVar1 = close(iVar1);
if (-1 < iVar1) {
iVar1 = close(__fd);
if (-1 < iVar1) {
local_58 = 0x2f706d742f207063;
local_50 = 0x732e796c6c656873;
local_48 = 0x206f;
local_40 = 0;
local_38 = 0;
local_30 = 0;
local_28 = 0;
local_20 = 0;
strcpy((char *)((long)&local_48 + 2),dir_name);
system((char *)&local_58);
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
}
...
Dieser riesige, unübersichtliche Codeblock schreibt im Wesentlichen den Inhalt unserer Chunks in diese Dateien und führt dann den Befehl cp /tmp/shelly.so dir_name aus, wobei dir_name der Name ist, den wir dem Programm am Anfang geben.
void FUN_00101e84(void)
{ long in_FS_OFFSET; char *local_18; long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28); puts("HOLD UP, LET HIM COOK."); local_18 = (char *)0x0; execve("/usr/bin/pkexec",&local_18,(char **)&ptr_array); if (local_10 != *(long )(in_FS_OFFSET + 0x28)) { / WARNING: Subroutine does not return */ __stack_chk_fail(); } return; }
Dies ist eine interessante Funktion. Sie ruft `pkexec` mit einem `NULL argv` und dem `notes`-Array-Zeiger als Umgebungsvariablen auf, was der anfällige Befehl in unserer CVE-2021-4034 ist. Deutet dies darauf hin, dass wir unsere `notes` gestalten und die Ausführung irgendwie auf diese Funktion umleiten müssen?
***
## shelly.so
Wenn wir dies in Ghidra öffnen, erfahren wir, dass es sich offensichtlich um eine `set-UID-root`-Bibliothek handelt, ähnlich denen, die im CVE-Exploit verwendet wurden.
***
# Zusammenfassung
Einige wichtige Punkte:
1. Wir haben zu Beginn des Programms einen Stack-Leck (Ordnungsnummer), der die Adresse des konstanten Strings `/recipe` enthält, der beim Erstellen der Datei `recipe` verwendet wird.
2. Das 0. Feld eines Chunks ist sein `notes`-Zeiger.
3. Das 5. Feld eines Chunks ist der `nxt`-Zeiger auf den nächsten Chunk in der verketteten Liste.
4. `Show` kann eine Heap-Adresse leaken, die das 5. Feld in einem Chunk ist.
5. `Edit` kann 1 Byte in das 5. Feld überschreiben. **DAS IST DER EINZIGE ARBITRARY WRITE BUG!!**
6. `FUN_00101e84` ruft `pkexec` mit NULL argv und den Umgebungsvariablen, die wir kontrollieren (`notes`-Array), auf.
***
# Exploitation
## Heap-Chunk-Layout
Eine schnelle Überprüfung in gdb kann uns den Offset zwischen verschiedenen Chunks in unserem Programm mitteilen:```gdb
...
Pick your bread: aaaa
Select your spread: a
Choose your veg: a
Slam your meat & egg: a
Any side notes for the cook? 0000
1. ADD NEW ORDER
2. EDIT ORDER
3. SHOW ORDER
4. CANCEL ORDER
5. CHECKOUT
6. DONE
1
Pick your bread: bbbb
Select your spread: b
Choose your veg: b
Slam your meat & egg: b
Any side notes for the cook? 1111
...
gef➤ search-pattern aaaa
[+] Searching 'aaaa' in memory
[+] In '[heap]'(0x55555555a000-0x55555557b000), permission=rw-
0x55555555aae8 - 0x55555555aaec → "aaaa"
gef➤ x/30gx 0x55555555aae8-0x18
0x55555555aad0: 0x0000000000000000 0x0000000000000041
0x55555555aae0: 0x000055555555ab20 0x0000000061616161
0x55555555aaf0: 0x0000000000000061 0x0000000000000061
0x55555555ab00: 0x0000000000000061 0x000055555555ab60
0x55555555ab10: 0x0000000000000000 0x0000000000000041
0x55555555ab20: 0x0000000030303030 0x0000000000000000
0x55555555ab30: 0x0000000000000000 0x0000000000000000
0x55555555ab40: 0x0000000000000000 0x0000000000000000
0x55555555ab50: 0x0000000000000000 0x0000000000000041
0x55555555ab60: 0x000055555555aba0 0x0000000062626262
0x55555555ab70: 0x0000000000000062 0x0000000000000062
0x55555555ab80: 0x0000000000000062 0x0000000000000000
0x55555555ab90: 0x0000000000000000 0x0000000000000041
0x55555555aba0: 0x0000000031313131 0x0000000000000000
0x55555555abb0: 0x0000000000000000 0x0000000000000000
Die Chunks haben also eine Größe von 0x40 (einschließlich 0x10 Bytes Metadaten) und sind (vom Beginn des aktuellen Chunks bis zum Beginn des nächsten Chunks) durch einen Offset von 0x40+0x40 = 0x80 getrennt. Dies liegt daran, dass die notes bei jeder Zuweisung eines Chunks allokiert werden und somit immer zwischen aufeinanderfolgenden Chunks liegen.
Bevor wir zum Exploit springen, wie missbrauchen wir die 1-Byte-Überlauffehler, um an eine beliebige Adresse zu schreiben? Wir können die folgende Strategie anwenden:
nxt-Zeiger des 2. Chunks (2. Punkt im Zusammenfassung-Abschnitt).
C.B = C-0x80.B x.x + 8 in das 5. Feld des aktuellen Chunks. Der resultierende nxt-Zeiger wird B + 8 sein.
A = 0x55555555aae0, dann wäre die geleakte Adresse B = 0x55555555ab60 und x = 0xe8.A bei mit überschreiben, wird der resultierende -Zeiger `0x55555555e8 != 0x55555555e8 sein, was nicht das ist, was wir wollten.Es gibt eine Möglichkeit, zu dieser Funktion zu springen, die ich nicht diskutieren werde. Aber bevor du das versuchst, wirf einen Blick auf die Dockerfile. Fällt dir etwas an flag.txt auf?```
...
chown root:root /home/ctf/flag.txt
...
USER peasant
CMD ["/home/ctf/start.sh"]
***ES GEHÖRT ROOT, WÄHREND DAS PROGRAMM VON EINEM UNPRIVILEGIERTEN BENUTZER AUSGEFÜHRT WIRD UND IHM GEHÖRT***.
Also selbst wenn du „cat flag.txt“ ausführst, wird es die Flagge nicht ausgeben, da das Programm keine Berechtigung hat, auf die Datei zuzugreifen. Das bedeutet, dass wir zu root eskalieren müssen, um die Flagge zu lesen.
## 2. Weg – CVE-2021-4034
Um CVE-2021-4034 auszunutzen, benötigen wir folgendes Setup:
1. Ein Verzeichnis mit dem Namen `GCONV_PATH=.`
2. In diesem Verzeichnis benötigen wir eine ausführbare Datei, die den Namen des Verzeichnisses trägt, in dem sich unsere `gconv-modules`-Konfigurationsdatei befinden wird. Nennen wir dies in unserer Challenge `recipe`.
3. Im Verzeichnis `recipe` benötigen wir eine Datei namens `gconv-modules` mit von uns kontrolliertem Inhalt und eine dynamische Bibliothek, die eine Shell startet (vielleicht ist dies die mitgelieferte Binärdatei `shelly.so`?).
4. Dann müssen wir in unserer `gconv-modules` den gleichen CHARSET wie in Schritt 5 unten und den Namen unserer dynamischen Bibliothek `shelly.so` angeben, wie folgt: `module UTF-8// SHELLY// shelly 2`.
5. Nach all dem rufen wir `pkexec` mit NULL argv und dem manipulierten Umgebungsvariablen-Array auf: `{"recipe", "PATH=GCONV_PATH=.", "CHARSET=SHELLY", "SHELL=shelly", NULL}`.
Nun nutzen wir diese Challenge aus, um das obige Setup zu erreichen.
***
### Ziel 1–2
Die Schritte 1–2 sind einfach: Wir müssen uns nur mit einem Benutzernamen `GCONV_PATH=.` anmelden, um dieses Verzeichnis zu erstellen. Dann führen wir ein `checkout` aus, um die ausführbare Datei `recipe` in diesem Verzeichnis zu erstellen. Dann kehren wir zum ersten Menü zurück.
***
### Ziel 3–4
Wir melden uns mit dem Benutzernamen `recipe` an, um dieses Verzeichnis zu erstellen. Jetzt *müssen* wir eine Datei namens `gconv-modules` erstellen, aber `checkout` erstellt nur die Dateien `recipe` und `notes`.
Was wäre, wenn wir den Speicherort für den String `/notes` überschreiben würden, um ihn zu `/gconv-modules` zu machen? Das könnte funktionieren, erfordert aber, dass dieser Speicher beschreibbar ist. Schauen wir es uns in gdb an:```gdb
gef➤ search-pattern /notes
[+] Searching '/notes' in memory
[+] In '/home/peasant/Desktop/chal'(0x555555559000-0x55555555a000), permission=rw-
0x555555559010 - 0x555555559016 → "/notes"
Und sie ist beschreibbar!
Wie führen wir das Schreiben durch?
/recipe liegt./recipe zu /notes 0x10 beträgt. Subtrahiere dies von der /recipe-Adresse aus Schritt 1, um die /notes-Adresse zu erhalten./notes mit /gconv-modules zu überschreiben.Was das Schreiben des korrekten Inhalts in die Datei gconv-modules betrifft, wissen wir, dass beim checkout der notes-Inhalt der Chunks in /notes geschrieben wird, was jetzt /gconv-modules ist. Um sicherzugehen, können wir einfach den String module UTF-8// SHELLY// shelly 2 jedem notes geben.
Wenn wir also checkout aufrufen, werden 2 Dateien recipe und gconv-modules erstellt und einige Zeilen mit module UTF-8// SHELLY// shelly 2 (wir verwenden shelly, weil das der Name der bereitgestellten .so-Datei mit set-uid-root ist) in die Datei gconv-modules geschrieben. Außerdem wird eine shelly.so in dasselbe Verzeichnis kopiert.
notes die entsprechenden Umgebungsvariablen sind. Das notes-Zeigerarray wird 4 Zeiger auf diese 4 notes und einen abschließenden NULL-Zeiger haben, was genau das ist, was wir wollten.RIP für die main-Funktion mit unserer geheimen Funktion FUN_00101e84 zu überschreiben.
RIP ist? Erinnern Sie sich an unsere geleakte Stack-Adresse am Anfang des Programms? Gehen Sie in gdb, um deren Offset zum RIP zu ermitteln, und addieren Sie dann diesen Offset zur tatsächlichen geleakten Adresse, um den RIP zu erhalten.main ist? Erinnern Sie sich an unsere geleakte Adresse von /recipe aus Schritt 2 von Ziel 3-4? Machen Sie hier dasselbe!Ein Lösungs-Skript mit Kommentaren ist in diesem Repository bereitgestellt. Bei Fragen können Sie mich gerne kontaktieren :)
0x55555555ab08xnxtB + 8 anstelle von C. Bearbeite Chunk B, sodass sein erstes (bread) Feld die target-Adresse enthält, in die wir schreiben möchten.B + 8 überschreibt. Der notes-Zeiger dieses 3. Chunks ist das target! Also wird alles, was wir in notes schreiben, in target geschrieben!