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.