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