Self Protecting Global Offset Table (GOT)

http://em386.blogspot.com
24 August 2008
Draft Version 1.4

Introduction

This paper describes a simple technique for Executable Linkable Format (ELF) objects to protect themselves from a potential exploitation technique of unknown vulnerabilities by marking specific pages read-only from user space virtual memory. This technique requires no kernel patching or third party programs. GOT protection already exists in the form of PT_RELRO marked program headers and can be achieved using the GNU Linker (ld) flag -z and the keyword 'relro'. However sometimes these features may not be available on your target platform, and sometimes it may be worth exploring other ways to achieve them. That is the purpose of this paper, to explore another way to implement a read-only Global Offset Table.

It is assumed that the reader possesses some knowledge of C, ELF, binutils and the Linux operating system. All research was performed on a modern Ubuntu Linux 8.04 (Hardy Heron) operating system. Also it should be noted that this article mentions two different 'linkers'. One is the GNU linker (ld) which is generates ELF objects at compile time. The other is the dynamic linker (ld-linux) which is used at runtime.

Constructor Functions (ctors)

A constructor function in basic terms is a function that runs prior to another function. The term 'constructors' is usually associated with C++ programs. Classes usually execute constructors upon the creation of a new instance. However generic constructors are entirely possible to implement in C using a GCC attribute:

__attribute__((constructor)) void foo_func()

The function foo_func() will execute before main()

We can test this with a simple program:

# cat test.c
__attribute__((constructor)) void foo_func()
{
        printf("Hello from foo_func\n");
}

int main()
{
        printf("Hello from main\n");

return 0;
}

# gcc -o test test.c
# ./test 
Hello from foo_func
Hello from main

You can read more about GCC attributes here http://www.ohse.de/uwe/articles/gcc-attributes.html. The technique covered in this paper occurs in a constructor function that is executed before main(). This allows our protection code to be added to preexisting code bases without any major modifications.

Despite our protection functions being executed before main() there are certain things in memory we simply cannot alter. If our protections become to invasive we run into runtime errors, which creates stability issues. Just like full kernel level protections such as PAX and Exec-Shield, we want to avoid disrupting program runtimes at all costs.

Runtime GOT PLT Protection

The GOT (Global Offset Table) is used by dynamically linked ELF objects. The ELF 1.2 specification states the following about the ELF relocation process:

"Relocation is the process of connecting symbolic references with symbolic definitions. For example, when a program calls a function, the associated call instruction must transfer control to the proper destination address at execution. In other words, relocatable files must have information that describes how to modify their section contents, thus allowing executable and shared object files to hold the right information for a process's program image. Relocation entries are these data."

If you want to read more about dynamic ELF relocation I have written a decent overview on my blog.

By default on modern Linux systems relocation is done the lazy way (RTLD_LAZY) by the dynamic linker (ld-linux). What this means is a function's location is only looked up when that function is first called. Every function in a loaded shared library that the program calls has a GOT entry. The default location in the GOT will point to the dynamic linker itself until the function is called, then the GOT is rewritten with the actual location of the function. This is mainly for performance reasons. However the dynamic linker can be forced to lookup the absolute location of all functions when the program is first executed. The way to force this is using the environment variable LD_BIND_NOW. Setting it to a non NULL value will force the dynamic linker to lookup all function locations immediately after all shared libraries are loaded and fill in the GOT with the correct values.

The GOT is an easy target for exploit writers. When an arbitrary 4 byte overwrite is possible anywhere in memory an attacker can overwrite a pointer in the GOT for a function that has not yet been executed, such as exit(). When the program finally calls exit() it's PLT entry will reference it's offset in the GOT and use its value as the location of the exit() function. This is usually overwritten with an attacker supplied function such as system() or an indirect execution of shellcode.

The first implementation of self GOT protection comes in the form of a special constructor function that checks for the LD_BIND_NOW environment variable, locates it's own GOT in memory and marks the memory segment read-only. My first implementation did not work too well for various reasons. For one I was not controlling the location of the GOT, I was simply allowing GCC to compile my test program and place its ELF object data to their default location. This presented several problems. For one, calling mprotect() on non page-aligned locations was failing. It was simply not feasible to mprotect() the ELF base and beyond just to protect the GOT. There are plenty of other memory segments adjacent to the GOT that MUST be writable such as the .bss segment. So the first problem becomes quite clear. If we want to mprotect() the GOT, or any other sensitive memory segments, then we have to group them near a page aligned boundary separate from other segments that must remain writable. Of course like anything else, there is the easy way to do this and the hard way. The hard way consists of rewriting the binary, which is necessary for closed source applications, this has the potential to be messy which means we may interfere with our application. The easy way is to use a GNU linker script to tell the linker how to setup our sections at compile time. This is of course the preferred method.

The GNU linker (ld) has its own interpreted scripting language. This language is used to write linker files that tell the linker how to create the final object file. To view the default linker script type the following:

# ld --verbose
GNU ld (GNU Binutils for Ubuntu) 2.18.0.20080103
  Supported emulations:
   elf_i386
   i386linux
   elf_x86_64
using internal linker script:
==================================================
/* Script for -z combreloc: combine and sort reloc sections */
OUTPUT_FORMAT("elf32-i386", "elf32-i386",
              "elf32-i386")
OUTPUT_ARCH(i386)
...(output cropped)...

For additional linker scripts, most Linux distributions store them in /usr/lib/ldscripts. The default script is around 197 lines long. Most open source projects use the default linker script, as it generates a binary with a common layout. In order to make our self GOT protection technique work we are going to have to create a custom linker script. One which places sensitive areas of memory where we want them to be. Preferably on page aligned boundaries so that mprotect() can be used.

Let's take a quick look at the typical layout of ELF object data in an example program:

# cat test-prog.c
#include < stdio.h>
#include < stdlib.h>
#include < string.h>

char global_var[256];

int main(int argc, char *argv[])
{
    char *ptr = NULL;
    char buffer[128];

    ptr = buffer;

    if(argc == 3)
    {
        strcpy(ptr, argv[1]);   
        printf("Copied buffer: [%s]\n", buffer);
        strcpy(ptr, argv[1]);

        strncpy(global_var, argv[2], 255);  
        printf("Copied into global_var: [%s]\n", global_var);
    }

return 0;
}

# gcc -o test-prog test-prog.c
# readelf -Sl test-prog
There are 36 section headers, starting at offset 0xe64:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .interp           PROGBITS        08048114 000114 000013 00   A  0   0  1
  [ 2] .note.ABI-tag     NOTE            08048128 000128 000020 00   A  0   0  4
  [ 3] .hash             HASH            08048148 000148 000034 04   A  5   0  4
  [ 4] .gnu.hash         GNU_HASH        0804817c 00017c 000020 04   A  5   0  4
  [ 5] .dynsym           DYNSYM          0804819c 00019c 000080 10   A  6   1  4
  [ 6] .dynstr           STRTAB          0804821c 00021c 000076 00   A  0   0  1
  [ 7] .gnu.version      VERSYM          08048292 000292 000010 02   A  5   0  2
  [ 8] .gnu.version_r    VERNEED         080482a4 0002a4 000030 00   A  6   1  4
  [ 9] .rel.dyn          REL             080482d4 0002d4 000008 08   A  5   0  4
  [10] .rel.plt          REL             080482dc 0002dc 000030 08   A  5  12  4
  [11] .init             PROGBITS        0804830c 00030c 000030 00  AX  0   0  4
  [12] .plt              PROGBITS        0804833c 00033c 000070 04  AX  0   0  4
  [13] .text             PROGBITS        080483b0 0003b0 00022c 00  AX  0   0 16
  [14] .fini             PROGBITS        080485dc 0005dc 00001c 00  AX  0   0  4
  [15] .rodata           PROGBITS        080485f8 0005f8 00003b 00   A  0   0  4
  [16] .eh_frame         PROGBITS        08048634 000634 000004 00   A  0   0  4
  [17] .ctors            PROGBITS        08049638 000638 000008 00  WA  0   0  4
  [18] .dtors            PROGBITS        08049640 000640 000008 00  WA  0   0  4
  [19] .jcr              PROGBITS        08049648 000648 000004 00  WA  0   0  4
  [20] .dynamic          DYNAMIC         0804964c 00064c 0000d0 08  WA  6   0  4
  [21] .got              PROGBITS        0804971c 00071c 000004 04  WA  0   0  4
  [22] .got.plt          PROGBITS        08049720 000720 000024 04  WA  0   0  4
  [23] .data             PROGBITS        08049744 000744 00000c 00  WA  0   0  4
  [24] .bss              NOBITS          08049760 000750 000120 00  WA  0   0 32
  [25] .comment          PROGBITS        00000000 000750 000126 00      0   0  1
  [26] .debug_aranges    PROGBITS        00000000 000878 000050 00      0   0  8
  [27] .debug_pubnames   PROGBITS        00000000 0008c8 000025 00      0   0  1
  [28] .debug_info       PROGBITS        00000000 0008ed 0001a7 00      0   0  1
  [29] .debug_abbrev     PROGBITS        00000000 000a94 00006f 00      0   0  1
  [30] .debug_line       PROGBITS        00000000 000b03 000129 00      0   0  1
  [31] .debug_str        PROGBITS        00000000 000c2c 0000bb 01  MS  0   0  1
  [32] .debug_ranges     PROGBITS        00000000 000ce8 000040 00      0   0  8
  [33] .shstrtab         STRTAB          00000000 000d28 000139 00      0   0  1
  [34] .symtab           SYMTAB          00000000 001404 0004e0 10     35  55  4
  [35] .strtab           STRTAB          00000000 0018e4 000250 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

Elf file type is EXEC (Executable file)
Entry point 0x80483b0
There are 7 program headers, starting at offset 52

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4
  INTERP         0x000114 0x08048114 0x08048114 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /lib/ld-linux.so.2]
  LOAD           0x000000 0x08048000 0x08048000 0x00638 0x00638 R E 0x1000
  LOAD           0x000638 0x08049638 0x08049638 0x00118 0x00248 RW  0x1000
  DYNAMIC        0x00064c 0x0804964c 0x0804964c 0x000d0 0x000d0 RW  0x4
  NOTE           0x000128 0x08048128 0x08048128 0x00020 0x00020 R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .interp 
   02     .interp .note.ABI-tag .hash .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame 
   03     .ctors .dtors .jcr .dynamic .got .got.plt .data .bss 
   04     .dynamic 
   05     .note.ABI-tag 
   06     

The section header has its own permission markings such as W for write, A for allocate, X for execute and more. However these are inconsequential to the program once its in memory. The ELF program header and its correlation to the section header is what we need to look at. The process of our protection technique is to make sure the dynamic linker has resolved all function locations at process creation and then mark the GOT read only. As stated before this is not possible yet because the GOT (.got.plt in our example) appears to be on the same page as the .bss and other important sections that must be writable. So we must use our linker script to control where the GOT is placed. Use the following command to pipe the ld default script to a text file.

# ld --verbose > ld.txt

Now we want to edit this file. Make sure to strip all contents above the '===' dashed line, these are just comments. You also want to remove the trailing '===' line from the file. Now around line 136 you should see the '.got' and '.got.plt' sections. We want to change a few lines here that tells the GNU linker where to place the sections. The following patch accomplishes this by telling the linker to place the .got and .got.plt at least one MAXPAGESIZE beyond the .bss section.

--- ../testing/ld.txt   2008-05-02 10:20:21.000000000 -0400
+++ ../testing/new.txt  2008-05-02 14:26:29.000000000 -0400
@@ -133,9 +133,6 @@
   .jcr            : { KEEP (*(.jcr)) }
   .data.rel.ro : { *(.data.rel.ro.local* .gnu.linkonce.d.rel.ro.local.*) *(.data.rel.ro* .gnu.linkonce.d.rel.ro.*) }
   .dynamic        : { *(.dynamic) }
-  .got            : { *(.got) }
-  . = DATA_SEGMENT_RELRO_END (12, .);
-  .got.plt        : { *(.got.plt) }
   .data           :
   {
     *(.data .data.* .gnu.linkonce.d.*)
@@ -159,6 +156,9 @@
   }
   . = ALIGN(32 / 8);
   . = ALIGN(32 / 8);
+  .got ALIGN(CONSTANT(MAXPAGESIZE))           : { *(.got) }    /* SPG Align the .got at least one page beyond .bss */
+  . = DATA_SEGMENT_RELRO_END (12, .);
+  .got.plt        : { *(.got.plt) }
   _end = .; PROVIDE (end = .);
   . = DATA_SEGMENT_END (.);
   /* Stabs debugging sections.  */

Notice the extra 'ALIGN(CONSTANT(MAXPAGESIZE))' expression, this pads our sections at least one page beyond the end of the .bss section. Now we compile, make sure the sections are placed correctly and test the program. You can tell the GNU linker to use a different linker script by using the gcc command line flags '-Wl,-T new.txt' where new.txt is your modified linker script.

# gcc -o test-prog test-prog.c -DSPG_DEBUG -Wl,-T new.txt 
# readelf -lS test-prog
There are 36 section headers, starting at offset 0x273c:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .interp           PROGBITS        08048134 000134 000013 00   A  0   0  1
  [ 2] .note.ABI-tag     NOTE            08048148 000148 000020 00   A  0   0  4
  [ 3] .hash             HASH            08048168 000168 000034 04   A  5   0  4
  [ 4] .gnu.hash         GNU_HASH        0804819c 00019c 000020 04   A  5   0  4
  [ 5] .dynsym           DYNSYM          080481bc 0001bc 000080 10   A  6   1  4
  [ 6] .dynstr           STRTAB          0804823c 00023c 000076 00   A  0   0  1
  [ 7] .gnu.version      VERSYM          080482b2 0002b2 000010 02   A  5   0  2
  [ 8] .gnu.version_r    VERNEED         080482c4 0002c4 000030 00   A  6   1  4
  [ 9] .rel.dyn          REL             080482f4 0002f4 000008 08   A  5   0  4
  [10] .rel.plt          REL             080482fc 0002fc 000030 08   A  5  12  4
  [11] .init             PROGBITS        0804832c 00032c 000030 00  AX  0   0  4
  [12] .plt              PROGBITS        0804835c 00035c 000070 04  AX  0   0  4
  [13] .text             PROGBITS        080483d0 0003d0 00022c 00  AX  0   0 16
  [14] .fini             PROGBITS        080485fc 0005fc 00001c 00  AX  0   0  4
  [15] .rodata           PROGBITS        08048618 000618 00003b 00   A  0   0  4
  [16] .eh_frame         PROGBITS        08048654 000654 000004 00   A  0   0  4
  [17] .ctors            PROGBITS        08049000 001000 000008 00  WA  0   0  4
  [18] .dtors            PROGBITS        08049008 001008 000008 00  WA  0   0  4
  [19] .jcr              PROGBITS        08049010 001010 000004 00  WA  0   0  4
  [20] .dynamic          DYNAMIC         08049014 001014 0000d0 08  WA  6   0  4
  [21] .data             PROGBITS        080490e4 0010e4 00000c 00  WA  0   0  4
  [22] .bss              NOBITS          08049100 0010f0 000120 00  WA  0   0 32
  [23] .got              PROGBITS        0804a000 002000 000004 04  WA  0   0  4
  [24] .got.plt          PROGBITS        0804a004 002004 000024 04  WA  0   0  4
  [25] .comment          PROGBITS        00000000 002028 000126 00      0   0  1
  [26] .debug_aranges    PROGBITS        00000000 002150 000050 00      0   0  8
  [27] .debug_pubnames   PROGBITS        00000000 0021a0 000025 00      0   0  1
  [28] .debug_info       PROGBITS        00000000 0021c5 0001a7 00      0   0  1
  [29] .debug_abbrev     PROGBITS        00000000 00236c 00006f 00      0   0  1
  [30] .debug_line       PROGBITS        00000000 0023db 000129 00      0   0  1
  [31] .debug_str        PROGBITS        00000000 002504 0000bb 01  MS  0   0  1
  [32] .debug_ranges     PROGBITS        00000000 0025c0 000040 00      0   0  8
  [33] .shstrtab         STRTAB          00000000 002600 000139 00      0   0  1
  [34] .symtab           SYMTAB          00000000 002cdc 0004e0 10     35  55  4
  [35] .strtab           STRTAB          00000000 0031bc 000258 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

Elf file type is EXEC (Executable file)
Entry point 0x80483d0
There are 8 program headers, starting at offset 52

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x08048034 0x08048034 0x00100 0x00100 R E 0x4
  INTERP         0x000134 0x08048134 0x08048134 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /lib/ld-linux.so.2]
  LOAD           0x000000 0x08048000 0x08048000 0x00658 0x00658 R E 0x1000
  LOAD           0x001000 0x08049000 0x08049000 0x000f0 0x00220 RW  0x1000
  LOAD           0x002000 0x0804a000 0x0804a000 0x00028 0x00028 RW  0x1000
  DYNAMIC        0x001014 0x08049014 0x08049014 0x000d0 0x000d0 RW  0x4
  NOTE           0x000148 0x08048148 0x08048148 0x00020 0x00020 R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .interp 
   02     .interp .note.ABI-tag .hash .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame 
   03     .ctors .dtors .jcr .dynamic .data .bss 
   04     .got .got.plt 
   05     .dynamic 
   06     .note.ABI-tag 
   07    
 
# ./test-prog Hello World
Copied buffer: [Hello]
Copied into global_var: [World]

Notice the .got and .got.plt sections are placed appropriately. However we still have a problem here, they are still marked RW. This is where we need to get creative with our constructor function. Our constructor function has to be able to locate its own GOT in memory and mprotect() it read-only. The simplest way is by locating its own ELF header in memory, locating the program header for the PT_DYNAMIC segment and using it to find the dynamic DT_PLTGOT entry. This entry gives us our .got.plt location. Using a simple (but hackish) AND-bitmask we can grab the address the page the .got.plt starts on and mprotect() it accordingly. My constructor function plus the example code is below and commented in detail.

# cat got-spg.c
#include < stdio.h>
#include < stdlib.h>
#include < string.h>
#include < unistd.h>
#include < elf.h>
#include < errno.h>
#include < sys/mman.h>

/* gcc -o got-spg got-spg.c -DSPG_DEBUG -Wl,-T new.txt */

#define ERR  -1

__attribute__((constructor)) void __self_protect_got()
{

int i;
int ret;
int loc = 0;

#ifdef SPG_DEBUG
        fprintf(stdout, "\nSelf Protecting GOT\n\n");
#endif

/* Check if ld has the right env settings - RTLD_LAZY means we cant touch the GOT */
char *lazy = getenv("LD_BIND_NOW");

        if(lazy == NULL)
        {
                #ifdef SPG_DEBUG
                        fprintf(stdout, "\nSkipping self protection...\nPlease set LD_BIND_NOW=1 and retry\n\n");
                #endif

                return;
        }

/* Get the page size, 4096 on x86 */
int     pagesize = sysconf(_SC_PAGE_SIZE);

        if(pagesize == ERR)
        {
                #ifdef SPG_DEBUG
                        fprintf(stdout, "sysconf() failed, assuming 4096 byte page size (x86 default)\n");
                #endif

                pagesize = 4096;
        }
        else
        {
                #ifdef SPG_DEBUG
                        fprintf(stdout, "Pagesize = %d\n", pagesize);
                #endif
        }

/* Locate the ELF base address. This is the only hard coded address in the whole function */
char *base_ptr = (char *) 0x8048000;

    /* Randomizing the ELF base address means we may have to search for it */
    while(*base_ptr != 'E' && *base_ptr+1 != 'L' && *base_ptr+2 != 'F')
    {
        *base_ptr++;
    }

    *base_ptr--;

Elf32_Ehdr *elf_header = (Elf32_Ehdr *) base_ptr; /* Elf Header */
Elf32_Phdr *phdr;               /* ELF program header */
Elf32_Phdr *dyn_phdr = NULL;    /* Program header marked 'dynamic' */
Elf32_Dyn *dyn;                 /* Specific dynamic segment entry for .got.plt */

        /* Iterate the program header for the dynamic segment */
        for(i=0;i < elf_header->e_phnum; i++)
        {
                phdr = (Elf32_Phdr *)(base_ptr + elf_header->e_phoff + (i * elf_header->e_phentsize));

                if(phdr->p_type == PT_DYNAMIC)
                {
                        dyn_phdr = phdr;
                        break;
                }
        }

        if(dyn_phdr == NULL)
        {
                #ifdef SPG_DEBUG
                        fprintf(stdout, "Could not locate the dynamic segment\n\n");
                #endif

                return;
        }

        /* Iterate through the dynamic segment for an entry of type DT_PLTGOT */
        for(i=0;i < dyn_phdr->p_filesz / sizeof(Elf32_Dyn); i++)
        {
                dyn = (Elf32_Dyn *)(base_ptr + dyn_phdr->p_offset + (i * sizeof(Elf32_Dyn)));

                /* Once its found, save the location of our .got.plt */
                if(dyn->d_tag == DT_PLTGOT)
                {
                        #ifdef SPG_DEBUG
                                fprintf(stdout, "GOTPLT Located @ %x\n", dyn->d_un.d_val);
                        #endif

                        loc = dyn->d_un.d_val;
                        break;
                }
        }

        /* We can't do anything without an address for the GOT */
        if(loc == 0)
        {
                #ifdef SPG_DEBUG
                        fprintf(stdout, "GOTPLT not located\n\n");
                #endif

                return;
        }

        /* AND-bitmask would turn 0x804b004 into 0x804b000, which is our page aligned addr */
        loc = (loc & 0xffff000);

        #ifdef SPG_DEBUG
                fprintf(stdout, "Protecting page found at %x\n", loc);
        #endif

        /* Set the entire page READ-ONLY */
        ret = mprotect((void *) loc, pagesize, PROT_READ);

        if(ret != ERR)
        {
                #ifdef SPG_DEBUG
                        fprintf(stdout, "GOT segment now protected for process [%d]...\n\n", getpid());
                #endif
        }
        else
        {
                #ifdef SPG_DEBUG
                        perror("mprotect");
                        fprintf(stdout, "\n\n");
                #endif
        }

return;
}

/* Main Program */

char global_var[256];

int main(int argc, char *argv[])
{
        char *ptr;
        char buffer[128];

        ptr = buffer;

    if(argc == 3)
    {
        strcpy(ptr, argv[1]);   /* A segfault would happen here if strcpy
                                was not already present in the GOT */
        printf("Copied buffer: [%s]\n", buffer);
        strcpy(ptr, argv[1]);

        strncpy(global_var, argv[2], 255);  /* Simply to test global data is still OK */
        printf("Copied into global_var: [%s]\n", global_var);
    }

        /* This is so we can grab the PID and print /proc/(pid)/maps to check if it worked */
        sleep(35);

return 0;
}

# gcc -o got-spg got-spg.c -DSPG_DEBUG -Wl,-T new.txt 
# ./got-spg Hello World

Self Protecting GOT
Pagesize = 4096
GOTPLT Located @ 804a004
Protecting page found at 804a000
GOT segment now protected for process [16722]...

Copied buffer: [Hello]
Copied into global_var: [World]

...(in another terminal)...

# cat /proc/16722/maps
08048000-08049000 r-xp 00000000 07:00 147464     /testing/got-spg
08049000-0804a000 rw-p 00001000 07:00 147464     /testing/SPG/got-spg
0804a000-0804b000 r--p 00002000 07:00 147464     /testing/SPG/got-spg
b7e0f000-b7e10000 rw-p b7e0f000 00:00 0 
b7e10000-b7f59000 r-xp 00000000 08:02 6214306    /lib/tls/i686/cmov/libc-2.7.so
b7f59000-b7f5a000 r--p 00149000 08:02 6214306    /lib/tls/i686/cmov/libc-2.7.so
b7f5a000-b7f5c000 rw-p 0014a000 08:02 6214306    /lib/tls/i686/cmov/libc-2.7.so
b7f5c000-b7f5f000 rw-p b7f5c000 00:00 0 
b7f73000-b7f76000 rw-p b7f73000 00:00 0 
b7f76000-b7f77000 r-xp b7f76000 00:00 0          [vdso]
b7f77000-b7f91000 r-xp 00000000 08:02 6213883    /lib/ld-2.7.so
b7f91000-b7f93000 rw-p 00019000 08:02 6213883    /lib/ld-2.7.so
bf961000-bf976000 rw-p bffeb000 00:00 0          [stack]

Our process map above shows the page we marked read-only is being enforced properly. If an attacker tried to write a pointer value to the GOT during an exploitation attempt it would fail.

Other Protections

So now we have protected our own .got.plt from being written to by attackers. But what other parts of memory can we protect? The .dtors section seems like a good target. And all we need to do is move its location in the linker script below .got and .got.plt. What about .rodata? .jcr? ctors? data.rel.ro? Below is a patch to the default linker script to move these sections.

--- ld.txt      2008-05-02 10:20:21.000000000 -0400
+++ new.txt     2008-05-03 00:57:52.000000000 -0400
@@ -101,6 +101,33 @@
     KEEP (*(SORT(.fini_array.*)))
     PROVIDE_HIDDEN (__fini_array_end = .);
   }
+  .dynamic        : { *(.dynamic) }
+  .data           :
+  {
+    *(.data .data.* .gnu.linkonce.d.*)
+    KEEP (*(.gnu.linkonce.d.*personality*))
+    SORT(CONSTRUCTORS)
+  }
+  .data1          : { *(.data1) }
+  _edata = .; PROVIDE (edata = .);
+  __bss_start = .;
+  .bss            :
+  {
+   *(.dynbss)
+   *(.bss .bss.* .gnu.linkonce.b.*)
+   *(COMMON)
+   /* Align here to ensure that the .bss section occupies space up to
+      _end.  Align after .bss to ensure correct alignment even if the
+      .bss section disappears because there are no input sections.
+      FIXME: Why do we need it? When there is no .bss section, we don't
+      pad the .data section.  */
+   . = ALIGN(. != 0 ? 32 / 8 : 1);
+  }
+  . = ALIGN(32 / 8);
+  . = ALIGN(32 / 8);
+  .got ALIGN(CONSTANT(MAXPAGESIZE))           : { *(.got) }
+  . = DATA_SEGMENT_RELRO_END (12, .);
+  .got.plt        : { *(.got.plt) }
   .ctors          :
   {
     /* gcc uses crtbegin.o to find the start of
@@ -132,33 +159,6 @@
   }
   .jcr            : { KEEP (*(.jcr)) }
   .data.rel.ro : { *(.data.rel.ro.local* .gnu.linkonce.d.rel.ro.local.*) *(.data.rel.ro* .gnu.linkonce.d.rel.ro.*) }
-  .dynamic        : { *(.dynamic) }
-  .got            : { *(.got) }
-  . = DATA_SEGMENT_RELRO_END (12, .);
-  .got.plt        : { *(.got.plt) }
-  .data           :
-  {
-    *(.data .data.* .gnu.linkonce.d.*)
-    KEEP (*(.gnu.linkonce.d.*personality*))
-    SORT(CONSTRUCTORS)
-  }
-  .data1          : { *(.data1) }
-  _edata = .; PROVIDE (edata = .);
-  __bss_start = .;
-  .bss            :
-  {
-   *(.dynbss)
-   *(.bss .bss.* .gnu.linkonce.b.*)
-   *(COMMON)
-   /* Align here to ensure that the .bss section occupies space up to
-      _end.  Align after .bss to ensure correct alignment even if the
-      .bss section disappears because there are no input sections.
-      FIXME: Why do we need it? When there is no .bss section, we don't
-      pad the .data section.  */
-   . = ALIGN(. != 0 ? 32 / 8 : 1);
-  }
-  . = ALIGN(32 / 8);
-  . = ALIGN(32 / 8);
   _end = .; PROVIDE (end = .);
   . = DATA_SEGMENT_END (.);
   /* Stabs debugging sections.  */


Conclusion

We can easily turn our constructor function into a simple library that can be linked into any program. I tested the GOT protection technique on Snort IDS version 2.8.1. Below is the output of its ELF section header and its memory mapping.

# readelf -lS /usr/src/snort-2.8.1/src/snort
...
  [17] .dynamic          DYNAMIC         080e9000 0a1000 0000f8 08  WA  6   0  4
  [18] .data             PROGBITS        080e9100 0a1100 582668 00  WA  0   0 32
  [19] .bss              NOBITS          0866b780 623768 5c68bc 00  WA  0   0 32
  [20] .got              PROGBITS        08c33000 624000 000004 04  WA  0   0  4
  [21] .got.plt          PROGBITS        08c33004 624004 000280 04  WA  0   0  4
  [22] .ctors            PROGBITS        08c33284 624284 00000c 00  WA  0   0  4
  [23] .dtors            PROGBITS        08c33290 624290 000008 00  WA  0   0  4
  [24] .jcr              PROGBITS        08c33298 624298 000004 00  WA  0   0  4
...
# cat /proc/`pidof snort`/maps
08048000-080e9000 r-xp 00000000 08:02 4891501    /usr/src/snort-2.8.1/src/snort
080e9000-0866c000 rw-p 000a1000 08:02 4891501    /usr/src/snort-2.8.1/src/snort
0866c000-08c33000 rw-p 0866c000 00:00 0 
08c33000-08c34000 r--p 00624000 08:02 4891501    /usr/src/snort-2.8.1/src/snort
08c34000-08df5000 rw-p 08c34000 00:00 0          [heap]

When run with LD_BIND_NOW environment variable and our constructor function, Snort worked without any runtime issues. The interesting part about the technique is that it was done without any special dynamic linker support and does not use or depend on its '-z relro' option. Therefore the concept is technically 'portable' to another ELF based system that uses a different tool set without native protection mechanisms.

References

Fedora Security Enhancements http://people.redhat.com/drepper/nonselsec.pdf
GCC Attributes http://www.ohse.de/uwe/articles/gcc-attributes.html
Dynamic Linker Scripts http://sourceware.org/binutils/docs-2.18/ld/
RELRO Binutils http://sources.redhat.com/ml/binutils/2004-01/msg00070.html



http://em386.blogspot.com
Struct Software