This article describes ELF relocation sections, how to abuse them for arbitrary code execution, and how to protect them at runtime.
ELF Relocation Sections
A dynamically linked ELF binary uses a look-up table called Global Offset Table (GOT) to dynamically resolve functions that are located in shared libraries.
When you call a function that is located in a shared library, it looks like the following. This is a high-level view of what is going on, there are lots of things the linker is doing that we won’t go into.
First, the call is actually pointing to the Procedure Linkage Table (PLT), which exists in the .plt section of the binary.
objdump -M intel -d YOUR_BINARY
80484da: e8 95 fe ff ff call 8048374 <printf@plt>
The .plt section contains x86 instructions that point directly to the GOT, which lives in the .got.plt section.
objdump -M intel -d YOUR_BINARY
08048374 <printf@plt>: 8048374: ff 25 54 97 04 08 jmp DWORD PTR ds:0x8049754 804837a: 68 20 00 00 00 push 0x20 804837f: e9 a0 ff ff ff jmp 8048324 <_init+0x30>
The .got.plt section contains binary data. The GOT contain pointers back to the PLT or to the location of the dynamically linked function.
objdump -s YOUR_BINARY
Contents of section .got.plt: 8049738 6c960408 00000000 00000000 3a830408 l...........:... 8049748 4a830408 5a830408 6a830408 7a830408 J...Z...j...z... 8049758 8a830408 9a830408 ........
By default, the GOT is populated dynamically while the program is running. The first time a function is called, the GOT contains a pointer back to the PLT, where the linker is called to find the actual location of the function in question (this is the part we’re not going into detail about). The location found is then written to the GOT. The second time a function is called, the GOT contains the known location of the function. This is called “lazy binding.”
lazy When generating an executable or shared library, mark it to tell the dynamic linker to defer function call resolution to the point when the function is called (lazy binding), rather than at load time. Lazy binding is the default. [1]
There are a couple of design constraints for the GOT and the PLT.
GOT Overwrite
Since we know that the GOT lives in a predefined place and is writable, all that is needed is a bug that lets an attacker write four bytes anywhere. We’ll use the following vulnerable program to simulate that. Note that we are operating on the same binary we examined above.
Here we have an intentionality vulnerable program that is hopefully believable enough to make this demo realistic. Understanding this program is not necessary for understanding the exploitation and mitigation techniques being demonstrated, it is only included for completeness.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
// Include standard I/O declarations
#include <stdio.h>
// Include string declarations
#include <string.h>
// Program entry point
intmain(intargc,char** argv) {
// Terminate if program is not run with three parameters.
if(argc != 4) {
// Print out the proper use of the program
puts("./a.out <size> <offset> <string>");
// Return failure
return-1;
}
// Convert size to an integer
intsize =atoi(argv[1]);
// Convert offset to an integer
intoffset =atoi(argv[2]);
// Place string into its own string on the stack
char* str = argv[3];
// Declare a 256 byte buffer on the stack
charbuffer[256];
// Print the location of the buffer for calculating the offset.
printf("Buffer:\t\t%8x\n", &buffer);
// Fill the buffer with the letter 'A'.
memset(buffer, 65, 256 - 1);
// Null-terminate the buffer.
buffer[255] = 0;
// Attempt to copy the specified string into the specified location.
strncpy(buffer + offset, str, size);
// Print out the buffer.
printf("%s", buffer);
// Return success
return0;
}
|
gcc -g -O0 -Wl,-z,norelro -fno-stack-protector -o YOUR_BINARY YOUR_SOURCE_CODE
First, some reconnaissance. We know from the examination above, that our GOT entry for printf lives at0x08049754. We know from tesing the program, that our buffer will live on the stack at0xbffff284.
(gdb) x 0x08049754 0x8049754 <_GLOBAL_OFFSET_TABLE_+28>: 0x0804837a (gdb) p -(0xbffff284 - 0x08049754) % 0x80000000 $1 = 1208263888 (gdb) r 4 1208263888 \$\$\$\$ Starting program: /home/hake/code/relro/c 4 1208263888 \$\$\$\$ Buffer: bffff284 Program received signal SIGSEGV, Segmentation fault. 0x08048374 in printf@plt () (gdb) x 0x08049754 0x8049754 <_GLOBAL_OFFSET_TABLE_+28>: 0x24242424
Here, we see that we can overwrite the GOT entry for printf with our string. The program crashes because it’s trying to jump to memory that is not mapped.
RELRO: RELocation Read-Only
To prevent the above exploitation technique, we can tell the linker to resolve all dynamically linked functions at the beginning of execution and make the GOT read-only. Note that we are operating on a different binary below compiled from the same source code.
now When generating an executable or shared library, mark it to tell the dynamic linker to resolve all symbols when the program is started, or when the shared library is linked to using dlopen, instead of deferring function call resolution to the point when the function is first called. [1]
This exploitation mitigation technique is known as RELRO which stands for RELocation Read-Only. The idea is simple, make the relocation sections that are used to resolve dynamically loaded functions read-only. This way, they cannot overwrite them and we cannot take control of execution like we did above.
You can turn on Full RELRO with the gcc compiler option:-Wl,-z,relro,-z,now. This gets passed to the linker as-z relro -z now. On most modern Linux distributions a variant of RELRO known as Partial RELRO is used by default. Partial RELRO uses the-z relrooption, but not the-z nowoption.
gcc -g -O0 -Wl,-z,relro,-z,now -fno-stack-protector -o YOUR_BINARY YOUR_SOURCE_CODE
80484fa: e8 95 fe ff ff call 8048394 <printf@plt> 08048394 <printf@plt>: 8048394: ff 25 f0 9f 04 08 jmp DWORD PTR ds:0x8049ff0 804839a: 68 20 00 00 00 push 0x20 804839f: e9 a0 ff ff ff jmp 8048344 <_init+0x30> Contents of section .got: 8049fd4 fc9e0408 00000000 00000000 5a830408 ............Z... 8049fe4 6a830408 7a830408 8a830408 9a830408 j...z........... 8049ff4 aa830408 ba830408 00000000 ............
The GOT entry for printf lives at0x08049ff0. The buffer will live on the stack at0xbffff284.
(gdb) x 0x08049ff0 0x8049ff0 <_GLOBAL_OFFSET_TABLE_+28>: 0x0804839a (gdb) p -(0xbffff284 - 0x08049ff0) % 0x80000000 $1 = 1208266092 (gdb) r 4 1208266092 \$\$\$\$ Starting program: /home/hake/code/relro/r 4 1208266092 \$\$\$\$ Buffer: bffff284 Program received signal SIGSEGV, Segmentation fault. strncpy (s1=0x8049ff0 "ph\027", s2=0xbffff5fc "$$$", n=4) at strncpy.c:43 43 strncpy.c: No such file or directory. in strncpy.c (gdb) x 0x08049ff0 0x8049ff0 <_GLOBAL_OFFSET_TABLE_+28>: 0x00176870
Here, we see that we cannot overwrite the GOT entry for printf with our string. The program crashes because it trying to write to a memory segment that is read-only.
Technical note: All other memory corruption exploitation mitigation techniques were turned off for this demonstration.
Technical note: RELRO automatically applies all the specified protections to following segments:.ctors,.dtors,.jcr,.dynamicand.got.
Some Handy Commands
Display Dynamic Relocation Entries
objdump -R YOUR_BINARY
Show Program Header Table
readelf -l YOUR_BINARY
Show Section Header Table
readelf -S YOUR_BINARY
Display Relocations
readelf -r YOUR_BINARY
Thanks
Jon Oberheide
Citations
[1] Manual page ld(1)
Related Resources
RELRO – A (not so well known) Memory Corruption Mitigation Technique
How to Hijack the Global Offset Table with pointers
Chapter 9. Dynamic Linking: Global Offset Tables
The ELF format – How programs look from the inside
Resolving ELF Relocation Name / Symbols