From .rodata to .rwdata – introduction to memory mapping and LD scripts

A few days ago a colleague of mine, which had just started to learn C, was wondering about the following piece of code:

char *foo = "AAAAAA";
foo[0] = 'B';

This is described as a valid code, according to the tutorial he followed, yet when running it a segmentation fault occurs:

guy@localhost ~/b/string_elf> gcc sample_1.c
guy@localhost ~/b/string_elf> ./a.out 
fish: “./a.out” terminated by signal SIGSEGV (Address boundary error)

Why there is a segmentation fault?

We can easily find the instruction that caused the segmentation fault by debugging the process under GDB:

guy@localhost ~/b/string_elf> gdb a.out 
...

(gdb) set disassemble-next-line on
(gdb) r
Starting program: /home/guy/blog/string_elf/a.out 

Program received signal SIGSEGV, Segmentation fault.
0x00000000004004dd in main ()
=> 0x00000000004004dd <main+16>:	c6 00 42	movb   $0x42,(%rax)

It is easy to see that writing to address of $rax caused the crash – the instruction

movb   $0x42,(%rax)

was the last instruction that was run before the segmentation fault.

We can see that $rax contains the initial value of the string by using the examine command:

(gdb) x/6c $rax
0x400580:	65 'A'	65 'A'	65 'A'	65 'A'	65 'A'	65 'A'

In order to display the memory mapping of the process, we can use the file /proc/PID/maps. The info proc gdb command returns the PID:

(gdb) info proc
process 8596
cmdline = '/home/guy/blog/string_elf/a.out'
cwd = '/home/guy/blog/string_elf'
exe = '/home/guy/blog/string_elf/a.out'

(gdb) shell cat /proc/8596/maps
00400000-00401000 r-xp 00000000 fd:02 134817229 ...
00600000-00601000 r--p 00000000 fd:02 134817229 ...
00601000-00602000 rw-p 00001000 fd:02 134817229 ...
....

As seen from the memory mapping, the address that is stored in $rax, 0x400580, which resides in range 0x00400000-0x00401000, is marked as non-writable.

00400000-00401000 r-xp

Writing to it would obviously cause a segmentation fault, as the page is marked as non-writable.

An interesting question popped up:

how can we cause the string to be writable?

Before we continue any further, we need to make a quick detour and introduce the ELF file format.

A brief introduction to the ELF file format

The ELF file format, among other things, consists of sections that describe the logical memory layout of the binary.

Some typical sections one may find in an ELF file are:

  1. .text – which stores the instructions that consist of the program itself. It is marked as executable and read-only (r-x).
  2. .data – which is used to store static and global variables (non-static variables are stored on the stack). It is marked as read-write and non-executable (rw-).
  3. .bss – which stores non-initialized variables. It is marked as read-write and non-executable (rw-).
  4. .rodata – which stores constant data. One should expect string literals, and other constant values to reside there. It is marked as read-only (although usually resides in a read and executable segment).
Source: Wikipedia

The section header table stores information on the various sections – including their permissions, virtual address memory range, etc.

One can view the different sections using the readelf tool:

guy@localhost ~/b/string_elf> readelf -S a.out 
There are 31 section headers, starting at offset 0x1908:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
...
  [14] .text             PROGBITS         00000000004003e0  000003e0
...
  [16] .rodata           PROGBITS         0000000000400570  00000570
       000000000000003b  0000000000000000   A       0     0     8
...
  [25] .data             PROGBITS         0000000000601020  00001020
       0000000000000004  0000000000000000  WA       0     0     1
  [26] .bss              NOBITS           0000000000601024  00001024
       0000000000000004  0000000000000000  WA       0     0     1
...

objdump is another great tool. The VMA column stands for Virtual Memory Address and describes the base virtual address of the section.

guy@localhost ~/b/string_elf> objdump -h a.out 

a.out:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
 13 .text         00000182  00000000004003e0  00000000004003e0  000003e0  2**4
...
 15 .rodata       0000003b  0000000000400570  0000000000400570  00000570  2**3
...
 24 .data         00000004  0000000000601020  0000000000601020  00001020  2**0
                  CONTENTS, ALLOC, LOAD, DATA
 25 .bss          00000004  0000000000601024  0000000000601024  00001024  2**0
...

In addition to sections, the elf file format consists of a Program Header table which describes how different sections are grouped into segments in memory. The elf loader creates the memory mapping of the process according to those segments:

When an ELF file format is being loaded into memory, via a call to execve(), the kernel only examines the segments in order to set the memory mapping of the process. The kernel does not care about individual sections.

Here is a snippet of the load_elf_binary function (kernel 3.18) – it can be seen that the kernel only considers the program headers (segments) and calls elf_map (which in turn calls vm_mmap) which mmaps each segment to its VMA with its given flags:

static int load_elf_binary(struct linux_binprm *bprm) {
    ...
    /* iterate over the program headers */
    for(i = 0, elf_ppnt = elf_phdata;
        i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {
            int elf_prot = 0, elf_flags;
	    unsigned long k, vaddr;

    ...
    /* store the virtual address of the segment */
        vaddr = elf_ppnt->p_vaddr;
    ...
    /* mmap the segment to its virtual address with the permissions that are specified in the program header table */
        error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,		 
            elf_prot, elf_flags, 0);
    ...	
}	

Let’s find the section in which $rax resides in via GDB (since we already covered readelf and objdump):

(gdb) maintenance info sections
Exec file:
    `/home/guy/blog/string_elf/a.out', file type elf64-x86-64.
    ...
   
    0x00400570->0x004005ab at 0x00000570: .rodata ALLOC LOAD READONLY DATA HAS_CONTENTS
    ...

We can then see, that the string was put at the “.rodata” section which is marked as READONLY. Writing to it would obviously cause a segmentation fault.

A run-time patch: mprotect

mprotect is a syscall that sets protection on a region of memory:

#include <sys/mman.h>

int mprotect(void *addr, size_t len, int prot);

It sets permissions of the memory region starting at addr and ending in addr+len with the following permissions (which are passed via prot):

PROT_NONE  The memory cannot be accessed at all.

PROT_READ  The memory can be read.

PROT_WRITE The memory can be modified.

PROT_EXEC  The memory can be executed.

This means that we can call mprotect with the address of the page containing the string and set it to:

PROT_WRITE | PROT_READ | PROT_EXEC

(which is equal to 7).

Why would we mark it as executable? Because “.text” maps to this page as well! We can see that by examining the section-to-segment mapping using readelf:

guy@localhost ~/b/string_elf> readelf -l a.out

Elf file type is EXEC (Executable file)
Entry point 0x4003e0
There are 9 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x00000000000001f8 0x00000000000001f8  R E    8
  INTERP         0x0000000000000238 0x0000000000400238 0x0000000000400238
                 0x000000000000001c 0x000000000000001c  R      1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x00000000000006b4 0x00000000000006b4  R E    200000
...

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .interp 
   02     .interp .note.ABI-tag ... .plt.got .text .fini .rodata ... 
   03     .init_array .fini_array ...
   04     .dynamic 
   05     .note.ABI-tag .note.gnu.build-id 
   06     .eh_frame_hdr 
   07     
   08     .init_array .fini_array .jcr .dynamic .got 

We can find the page address by aligning $rax to a page size:

(gdb) $rax - $rax%4096

Here is the continuation of the GDB session, in which a breakpoint was set before the write;

A call to mprotect was then performed, changing the page which $rax resides in to be writable:

(gdb) p mprotect($rax - $rax%4096, 4096, 7)

And success! The binary exited successfully!

(gdb) b
Breakpoint 1 at 0x4004dd
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y

Starting program: /home/guy/blog/string_elf/a.out 

Breakpoint 1, 0x00000000004004dd in main ()
=> 0x00000000004004dd <main+16>:	c6 00 42	movb   $0x42,(%rax)
(gdb) p mprotect($rax - $rax%4096, 4096, 7)
$1 = 0
(gdb) si
0x00000000004004e0 in main ()
=> 0x00000000004004e0 <main+19>:	5d	pop    %rbp
(gdb) x/6c $rax
0x400580:	66 'B'	65 'A'	65 'A'	65 'A'	65 'A'	65 'A'

While this method works, it is not ideal and suffers from a lot of problems:

  1. This is a run-time solution, meaning that additional instructions are required to be run.
  2. We have to know the size of the buffer we want to change in advance – this method will fail when the size of .rodata is over one page.
  3. Since multiple sections can reside in a single page, a call to mprotect would possibly change permissions of other sections.

We can do better: LD scripts

If we could change the segment in which “.rodata” resides in to a writable segment it would have been perfect! That is where LD scripts come into play.

The segment to section mapping, among other things, is determined during the linkage of the program.

GCC, the compiler, creates an object file, which already contains some sections inside – such as the “.text”, “.data”, and “.bss”. According to the GCC documentation, every output must contain, at least, a text section.

The linker, LD, takes a bunch of object files and combines them into an ELF file. Commands are passed to LD using an ld script.

source: wikipedia

The main purpose of the linker script is to describe how the sections in the input files should be mapped into the output file and to control the memory layout of the output file. The linker always uses a linker script.

Using the default linker script

The default linker script can be obtained by running

ld --verbose

The output is quite long so I put it into the following gist.

I’ve removed some parts in order to create a simplified view of the default linker script:

SECTIONS {
   /* Read-only sections, merged into text segment: */
   . = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS;
   .text : {*(.text) }
   .rodata : {*(.rodata .rodata.*) }

   /* Read-write sections */
   . = DATA_SEGMENT_ALIGN (...);
   .data : {*(.data .data.*) }
}

The SECTION block describes how section from the object files, that are given as input to the linker, will map to section in the output ELF file.

For example, the line:

.text : {*(.text) }

Defines an output section named “.text” (left-hand side), which contains the “.text” section from all the input files – using a wildcard:

{*(.text) }

The “.” symbol is used to describe the current memory location and is called the location counter. Output sections are mapped to the location counter and the location counter is incremented by the size of the output section.

The line

. = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS;

Sets the location counter to the start of the text segment, at VMA 0x400000. Since the text segment is marked as readable and executable, sections that are put there will be non-writable – which in this case are “.text” and “.rodata”.

It can later be seen that the location counter is set to the data segment, and the data section is put there.

An easy hack would be to put the .rodata section after updating the location to the data segment. Let’s create a segment called “.rwdata” which would replace “.rodata”:

.rwdata : {*(.rodata .rodata.*) }

And we will change the linker script as follows – we will remove that “.rodata” section, and insert our “.rwdata” inside the data section:

SECTIONS {
   /* Read-only sections, merged into text segment: */
   . = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS;
   .text : {*(.text) }

   /* Read-write sections */
   . = DATA_SEGMENT_ALIGN (...);
   .data : {*(.data .data.*) }

   /* ---- our evil hack ---- /*
   .rwdata : {*(.rodata .rodata.*) }
}

The full linker script can be found here.

GCC can take a non-default linker script using the -T option.

Let’s try and compile the following code using our modified linker script:

#include <stdio.h>

int main(void) {
    char *foo = "AAAAAA";

    printf("printing string foo %s\n", foo);
    foo[0] = 'B';
    printf("printing string foo %s\n", foo);
    
    return 0;
}

I’ve written a Makefile that invokes GCC using the modified linker script:

guy@localhost ~/b/ld_script_elf_blog_post> make
mkdir -p build
gcc -T rwdata.ld sample.c -o build/sample  	

And when running:

guy@localhost ~/b/ld_script_elf_blog_post> ./build/sample 
printing string foo AAAAAA
printing string foo BAAAAA

It works!

Listing the section-to-segment mapping of the ELF, we see a new “.rwdata” section and no “.rodata” section:

guy@localhost ~/b/ld_script_elf_blog_post> readelf -l build/sample 

...
             
 Section to Segment mapping:
  Segment Sections...
   ...
   02     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .eh_frame_hdr .eh_frame 
   03     .init_array .fini_array .jcr .dynamic .got .got.plt .data .rwdata .bss 
   ...

Conclusion

LD scripts can be very useful when a tight control over the memory mapping is needed, which is something that is sometimes needed when programming for an embedded target (see the following example), or for other esoteric needs – for example, making the “.text” section writable for a self-modifying binary.

I hope you found this post interesting. This is my first-ever post, so any comments would be much appreciated 🙂