RELRO: RELocation Read-Only

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.

  • Because the PLT contains code that is called by the program directly, it needs to be allocated at a known offset from the .text segment.
  • Because the GOT contains data used by different parts of the program directly, it needs to be allocated at a known static address in memory.
  • Because the GOT is “lazy binded,” it needs to be writable.

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

你可能感兴趣的:(RELRO: RELocation Read-Only)