Procedure Linkage Table (PLT) and Global Offset Table (GOT)

Understanding how the PLT and GOT work with dynamic linking.

External functions from shared libraries are used everywhere. Even the basic “hello world” programs depend upon stdio and stdlib from libc. Shared libraries are useful because they can be updated without needing to recompile the binaries they are dynamically linked to.

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

int main(int argc, char **argv)
{
    printf("Hello world\n");

    exit(0);
    return 1;
}

Per its man page, the ldd command prints the shared libraries required by each program.

$ ldd a.out 
	linux-vdso.so.1 (0x00007ffff7fd0000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffff7dcb000)
	/lib64/ld-linux-x86-64.so.2 (0x00007ffff7fd2000)

Due to address space layout randomization (ASLR), the base address of libc changes upon each execution. Therefore, the binary making use of libc must have a way to get the correct address of libc’s external functions when calling them. This is where the PLT and GOT come into play. Looking at the disassembly of the “hello world” binary shows that the printf function was optimized and replaced with a puts function. In addition, notice that the call is to puts@plt, meaning that it is referencing the PLT.

gef➤  disas main
Dump of assembler code for function main:
   ...
   0x000000000000115b <+22>:	call   0x1030 <puts@plt>
   ...

Going to puts@plt in the disassembly shows the entry in the PLT. Notably, it immediately jumps to an address stored at another location, 4018 <puts@GLIBC_2.2.5>

gef➤  disas 0x1030
Dump of assembler code for function puts@plt:
   0x0000000000001030 <+0>:	jmp    QWORD PTR [rip+0x2fe2]        # 0x4018 <[email protected]>
   0x0000000000001036 <+6>:	push   0x0
   0x000000000000103b <+11>:	jmp    0x1020

Going to [email protected] in the disassembly shows the entry in the GOT. However, the GOT entry does not contain the address of the external puts function from libc until it is resolved. During the first invocation of the puts function at runtime, the GOT will return to the PLT and have the PLT call the omniscient _dl_runtime_resolve function. After this address is resolved, the GOT entry will be updated and all future invocations to the GOT will return the resolved address to the external puts function within libc.

Exploits

Arbitrary Read

If a binary does not have the position independent executable (PIE) mitigation enabled, then the addresses of the binary’s global offset table will always be fixed. Therefore, an arbitrary read can read an entry of the GOT, which is an address in libc. Now that an address from libc has been leaked, other libc locations such as the base address can be calculated given the offset.

Arbitrary Write

Assuming that an arbitrary write exists, the address of a function within the GOT can be overwritten. Therefore, any time that the function is called afterwards will instead call the GOT’s overwritten address which can point to any malicious function. For example, if a buffer overflow or arbitrary write exists in a function but never returns to the return pointer due to conditions such as a loop or system call, overwriting a GOT entry can instead be used to redirect code execution.

Resources