21 min read

In this article by Ryan O’Neill, author of the book Learning Linux Binary Analysis, ELF will be discussed. In order to reverse-engineer Linux binaries, we must understand the binary format itself. ELF has become the standard binary format for UNIX and UNIX-Flavor OS’s. Binary formats such as ELF are not generally a quick study, and to learn ELF requires some degree of application of the different components that you learn as you go. Programming things that perform tasks such as binary parsing will require learning some ELF, and in the act of programming such things, you will in-turn learn ELF better and more proficiently as you go along.

ELF is often thought to be a dry and complicated topic to learn, and if one were to simply read through the ELF specs without applying them through the spirit of creativity, then indeed it would be. ELF is really quite an incredible composition of computer science at work, with program layout, program loading, dynamic linking, symbol table lookups, and many other tightly orchestrated components.

(For more resources related to this topic, see here.)

ELF section headers

Now that we’ve looked at what program headers are, it is time to look at section headers. I really want to point out here the distinction between the two; I often hear people calling sections “segments” and vice versa. A section is not a segment. Segments are necessary for program execution, and within segments are contained different types of code and data which are separated within sections and these sections always exist, and usually they are addressable through something called section-headers. Section-headers are what make sections accessible, but if the section-headers are stripped (Missing from the binary), it doesn’t mean that the sections are not there. Sections are just data or code. This data or code is organized across the binary in different sections. The sections themselves exist within the boundaries of the text and data segment.

Each section contains either code or data of some type. The data could range from program data, such as global variables, or dynamic linking information that is necessary for the linker. Now, as mentioned earlier, every ELF object has sections, but not all ELF objects have section headers. Usually this is because the executable has been tampered with (Such as the section headers having been stripped so that debugging is harder). All of GNU’s binutils, such as objcopy, objdump, and other tools such as gdb, rely on the section-headers to locate symbol information that is stored in the sections specific to containing symbol data. Without section-headers, tools such as gdb and objdump are nearly useless. Section-headers are convenient to have for granular inspection over what parts or sections of an ELF object we are viewing. In fact, section-headers make reverse engineering a lot easier, since they provide us with the ability to use certain tools that require them. If, for instance, the section-header table is stripped, then we can’t access a section such as .dynsym, which contains imported/exported symbols describing function names and offsets/addresses.

Even if a section-header table has been stripped from an executable, a moderate reverse engineer can actually reconstruct a section-header table (and even part of a symbol table) by getting information from certain program headers, since these will always exist in a program or shared library. We discussed the dynamic segment earlier and the different DT_TAGs that contain information about the symbol table and relocation entries.

This is what a 32 bit ELF section-header looks like:

typedef struct {

    uint32_t   sh_name; // offset into shdr string table for shdr name

    uint32_t   sh_type; // shdr type I.E SHT_PROGBITS

    uint32_t   sh_flags; // shdr flags I.E SHT_WRITE|SHT_ALLOC

    Elf32_Addr sh_addr;  // address of where section begins

    Elf32_Off  sh_offset; // offset of shdr from beginning of file

    uint32_t   sh_size;   // size that section takes up on disk

    uint32_t   sh_link;   // points to another section

    uint32_t   sh_info;   // interpretation depends on section type

    uint32_t   sh_addralign; // alignment for address of section

    uint32_t   sh_entsize;  // size of each certain entries that may be in section

} Elf32_Shdr;

Let’s take a look at some of the most important section types, once again allowing room to study the ELF(5) man pages and the official ELF specification for more detailed information about the sections.

.text

The .text section is a code section that contains program code instructions. In an executable program where there are also phdr, this section would be within the range of the text segment. Because it contains program code, it is of the section type SHT_PROGBITS.

.rodata

The rodata section contains read-only data, such as strings, from a line of C code:

printf("Hello World!n");

These are stored in this section. This section is read-only, and therefore must exist in a read-only segment of an executable. So, you would find .rodata within the range of the text segment (Not the data segment). Because this section is read-only, it is of the type SHT_PROGBITS.

.plt

The Procedure linkage table (PLT) contains code that is necessary for the dynamic linker to call functions that are imported from shared libraries. It resides in the text segment and contains code, so it is marked as type SHT_PROGBITS.

.data

The data section, not to be confused with the data segment, will exist within the data segment and contain data such as initialized global variables. It contains program variable data, so it is marked as SHT_PROGBITS.

.bss

The bss section contains uninitialized global data as part of the data segment, and therefore takes up no space on the disk other than 4 bytes, which represents the section itself. The data is initialized to zero at program-load time, and the data can be assigned values during program execution. The bss section is marked as SHT_NOBITS, since it contains no actual data.

.got

The Global offset table (GOT) section contains the global offset table. This works together with the PLT to provide access to imported shared library functions, and is modified by the dynamic linker at runtime. This section has to do with program execution and is therefore marked as SHT_PROGBITS.

.dynsym

The dynsym section contains dynamic symbol information imported from shared libraries. It is contained within the text segment and is marked as type SHT_DYNSYM.

.dynstr

The dynstr section contains the string table for dynamic symbols; this has the name of each symbol in a series of null terminated strings.

.rel.*

Relocation sections contain information about how the parts of an ELF object or process image need to be fixed up or modified at linking or runtime.

.hash

The hash section, sometimes called .gnu.hash, contains a hash table for symbol lookup. The following hash algorithm is used for symbol name lookups in Linux ELF:

uint32_t

dl_new_hash (const char *s)

{

        uint32_t h = 5381;

 

        for (unsigned char c = *s; c != ''; c = *++s)

                h = h * 33 + c;

 

        return h;

}

.symtab

The symtab section contains symbol information of the type ElfN_Sym. The symtab section is marked as type SHT_SYMTAB as it contains symbol information.

.strtab

This section contains the symbol string table that is referenced by the st_name entries within the ElfN_Sym structs of .symtab, and is marked as type SHT_STRTAB since it contains a string table.

.shstrtab

The shstrtab section contains the section header string table, which is a set of null terminated strings containing the names of each section, such as .text, .data, and so on. This section is pointed to by the ELF file header entry called e_shstrndx, which holds the offset of .shstrtab. This section is marked as SHT_STRTAB since it contains a string table.

.ctors and .dtors

The .ctors (constructors) and .dtors (destructors) sections contain code for initialization and finalization, which is to be executed before and after the actual main() body of program code, and then after the main program code.

The __constructor__ function attribute is often used by hackers and virus writers to implement a function that performs an anti-debugging trick, such as calling PTRACE_TRACEME, so that the process traces itself and no debuggers can attach themselves to it. This way, the anti-debugging mechanism gets executed before the program enters main().

There are many other section names and types, but we have covered most of the primary ones found in a dynamically linked executable. One can now visualize how an executable is laid out with both phdrs and shdrs:

ELF Relocations

From the ELF(5) man pages:

Relocation is the process of connecting symbolic references with symbolic definitions.  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.

The process of relocation relies on symbols, which is why we covered symbols first. An example of relocation might be a couple of relocatable objects (ET_REL) being linked together to create an executable. obj1.o wants to call a function, foo(), located in obj2.o. Both obj1.o and obj2.o are being linked to create a fully working executable; they are currently Position independent code (PIC), but once relocated to form an executable, they will no longer be position independent since symbolic references will be resolved into symbolic definitions. The term “relocated” means exactly that: a piece of code or data is being relocated from a simple offset in an object file to some memory address location in an executable, and anything that references that relocated code or data must also be adjusted. Let’s take a quick look at a 32 bit relocation entry:

typedef struct {

    Elf32_Addr r_offset;

    uint32_t   r_info;

} Elf32_Rel;

And some relocation entries require an addend:

typedef struct {

    Elf32_Addr r_offset;

    uint32_t   r_info;

    int32_t    r_addend;

} Elf32_Rela;

Following is the description of the preceding snippet:

  • r_offset: This points to the location (offset or address) that requires the relocation action (which is going to be some type of modification)
  • r_info: This gives both the symbol table index with respect to which the relocation must be made, and the type of relocation to apply
  • r_addend: This specifies a constant addend used to compute the value stored in the relocatable field

Let’s take a look at the source code for obj1.o:

_start()

{

  foo();

}

We see that it calls the function foo(), however foo() is not located within the source code or the compiled object file, so there will be a relocation entry necessary for symbolic reference:

ryan@alchemy:~$ objdump -d obj1.o

obj1.o:     file format elf32-i386

Disassembly of section .text:

00000000 <func>:

   0:  55                     push   %ebp

   1:  89 e5                  mov    %esp,%ebp

   3:  83 ec 08               sub    $0x8,%esp

   6:  e8 fc ff ff ff         call   7 <func+0x7>

   b:  c9                     leave 

   c:  c3                     ret  

As we can see, the call to foo() is highlighted and simply calls to nowhere; 7 is the offset of itself. So, when obj1.o, which calls foo() (located in obj2.o), is linked with obj2.o to make an executable, a relocation entry is there to point at offset 7, which is the data that needs to be modified, changing it to the offset of the actual function, foo(), once the linker knows its location in the executable during link time:

ryan@alchemy:~$ readelf -r obj1.o

Relocation section '.rel.text' at offset 0x394 contains 1 entries:

 Offset     Info    Type            Sym.Value  Sym. Name

00000007  00000902 R_386_PC32        00000000   foo

As we can see, a relocation field at offset 7 is specified by the relocation entry’s r_offset field.

R_386_PC32 is the relocation type; to understand all of these types, read the ELF specs as we will only be covering some. Each relocation type requires a different computation on the relocation target being modified. R_386_PC32 says to modify the target with S + A – P. The following list explains all these terms:

  • S is the value of the symbol whose index resides in the relocation entry
  • A is the addend found in the relocation entry
  • P is the place (section offset or address) where the storage unit is being relocated (computed using r_offset)

If we look at the final output of our executable after compiling obj1.o and obj2.o, as shown in the following code snippet:

ryan@alchemy:~$ gcc -nostdlib obj1.o obj2.o -o relocated

ryan@alchemy:~$ objdump -d relocated

test:     file format elf32-i386

Disassembly of section .text:

080480d8 <func>:

 80480d8:  55                     push   %ebp

 80480d9:  89 e5                  mov    %esp,%ebp

 80480db:  83 ec 08               sub    $0x8,%esp

 80480de:  e8 05 00 00 00         call   80480e8 <foo>

 80480e3:  c9                     leave 

 80480e4:  c3                     ret   

 80480e5:  90                     nop

 80480e6:  90                     nop

 80480e7:  90                     nop

080480e8 <foo>:

 80480e8:  55                     push   %ebp

 80480e9:  89 e5                  mov    %esp,%ebp

 80480eb:  5d                     pop    %ebp

 80480ec:  c3                     ret

We can see that the call instruction (the relocation target) at 0x80480de has been modified with the 32 bit offset value of 5, which points to foo(). The value 5 is the result of the R386_PC_32 relocation action:

S + A – P: 0x80480e8 + 0xfffffffc – 0x80480df = 5

0xfffffffc is the same as -4 if a signed integer, so the calculation can also be seen as:

0x80480e8 + (0x80480df + sizeof(uint32_t))

To calculate an offset into a virtual address, use the following computation:

address_of_call + offset + 5 (Where 5 is the length of the call instruction)

Which in this case is 0x80480de + 5 + 5 = 0x80480e8.

An address may also be computed into an offset with the following computation:

address – address_of_call – 4 (Where 4 is the length of a call instruction – 1)

Relocatable code injection based binary patching

Relocatable code injection is a technique that hackers, virus writers, or anyone who wants to modify the code in a binary may utilize as a way to sort of re-link a binary after it has already been compiled. That is, you can inject an object file into an executable, update the executables symbol table, and perform the necessary relocations on the injected object code so that it becomes a part of the executable. A complicated virus might use this rather than just appending code at the end of an executable or finding existing padding. This technique requires extending the text segment to create enough padding room to load the object file. The real trick though is handling the relocations and applying them properly.

I designed a custom reverse engineering tool for ELF that is named Quenya. Quenya has many features and capabilities, and one of them is to inject object code into an executable. Why do this? Well, one reason would be to inject a malicious function into an executable, and then hijack a legitimate function and replace it with the malicious one. From a security point of view, one could do hot-patching and apply a legitimate patch to a binary rather than doing something malicious. Let’s pretend we are an attacker and we want to infect a program that calls puts() to print “Hello World”, and our goal is to hijack puts() so that it calls evil_puts(). First, we would need to write a quick PIC object that can write a string to standard output:

#include <sys/syscall.h>

int _write (int fd, void *buf, int count)

{

  long ret;

 

  __asm__ __volatile__ ("pushl %%ebxnt"

                        "movl %%esi,%%ebxnt"

                        "int $0x80nt" "popl %%ebx":"=a" (ret)

                        :"0" (SYS_write), "S" ((long) fd),

                        "c" ((long) buf), "d" ((long) count));

  if (ret >= 0) {

    return (int) ret;

  }

  return -1;

}

int evil_puts(void)

{

        _write(1, "HAHA puts() has been hijacked!n", 31);

}

Now, we compile evil_puts.c into evil_puts.o, and inject it into our program, hello_world:

ryan@alchemy:~/quenya$ ./hello_world

Hello World

This program calls the following:

puts(“Hello Worldn”);

We now use Quenya to inject and relocate our evil_puts.o file into hello_world:

[Quenya v0.1@alchemy] reloc evil_puts.o hello_world

0x08048624  addr: 0x8048612

0x080485c4 _write addr: 0x804861e

0x080485c4  addr: 0x804868f

0x080485c4  addr: 0x80486b7

Injection/Relocation succeeded

As we can see, the function write() from our evil_puts.o has been relocated and assigned an address at 0x804861e in the executable, hello_world. The next command, hijack, overwrites the global offset table entry for puts() with the address of evil_puts():

[Quenya v0.1@alchemy] hijack binary hello_world evil_puts puts

Attempting to hijack function: puts

Modifying GOT entry for puts

Succesfully hijacked function: puts

Commiting changes into executable file

[Quenya v0.1@alchemy] quit

And Whammi!

ryan@alchemy:~/quenya$ ./hello_world

HAHA puts() has been hijacked!

We have successfully relocated an object file into an executable and modified the executable’s control flow so that it executes the code that we injected. If we use readelf -s on hello_world, we can actually now see a symbol called evil_puts(). For the readers interest, I have included a small snippet of code that contains the ELF relocation mechanics in Quenya; it may be a little bit obscure without knowledge of the rest of the code base, but it is also somewhat straightforward if you’ve paid attention to what we learned about relocations. It is just a snippet and does not show any of the other important aspects such as modifying the executables symbol table:

case SHT_RELA:

for (j = 0; j < obj.shdr[i].sh_size / sizeof(Elf32_Rela); j++, rela++)

{

  rela = (Elf32_Rela *)(obj.mem + obj.shdr[i].sh_offset);


      /* symbol table */                         

  symtab = (Elf32_Sym *)obj.section[obj.shdr[i].sh_link];

       
      /* symbol we are applying relocation to */

      symbol = &symtab[ELF32_R_SYM(rela->r_info)];

 

     /* section to modify */

      TargetSection = &obj.shdr[obj.shdr[i].sh_info];

      TargetIndex = obj.shdr[i].sh_info;
 

     /* target location */

      TargetAddr = TargetSection->sh_addr + rela->r_offset;

       
     /* pointer to relocation target */

      RelocPtr = (Elf32_Addr *)(obj.section[TargetIndex] + rela->r_offset);

       
     /* relocation value */

      RelVal = symbol->st_value;

      RelVal += obj.shdr[symbol->st_shndx].sh_addr;

      switch (ELF32_R_TYPE(rela->r_info))

      {

        /* R_386_PC32      2    word32  S + A - P */

        case R_386_PC32:

              *RelocPtr += RelVal;

                  *RelocPtr += rela->r_addend;

                  *RelocPtr -= TargetAddr;

                  break; 

            /* R_386_32        1    word32  S + A */

           case R_386_32:

               *RelocPtr += RelVal;

                  *RelocPtr += rela->r_addend;

                  break;

      }

 }

As shown in the preceding code, the relocation target that RelocPtr points to is modified according to the relocation action requested by the relocation type (such as R_386_32).

Although relocatable code binary injection is a good example of the idea behind relocations, it is not a perfect example of how a linker actually performs it with multiple object files. Nevertheless, it still retains the general idea and application of a relocation action. Later on, we will talk about shared library (ET_DYN) injection, which brings us now to the topic of dynamic linking.

Summary

In this article we discussed different types of ELF section headers and ELF relocations.

Resources for Article:

 


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here