The magic of the Elf

 

Any sufficiently advanced technology is indistinguishable from magic.

 Arthur C. Clarke

Building executables from C source code is a complex task. An innocent looking call of gcc will invoke a pre-processor, a multi-pass compiler, an assembler and finally a linker. Using all these tools to plant virus code into another executable makes the result either prohibitively large, or very dependent on the completeness of the target installation.

Real viruses approach the problem from the other end. They are aggressively optimized for code size and do only what's absolutely necessary. Basically they just copy one chunk of code and patch a few addresses at hard coded offsets.

However, this has drastic effects:

There are ways to circumvent these limitations. But they are complicated and make the virus more likely to fail.

For the first example I'll present the simplest piece of code that still gives sufficient feedback. Our aim is to implant it into /bin/sh. On practically every recent installation of Linux/i386 the following code will emit three magic letters instead of just dumping core.

In the language of mortals

Source.

Command.

Output.

How it works

Digested answer

The three letters are part of the signature of ELF files. Executables created by ld are always mapped into the same memory region. That's why the program can find its own header at a predictable virtual address.

Short answer

RTFM.

The raw details are in /usr/include/elf.h. The canonical document describing the ELF file format for Intel-386 architectures can be found at ftp://tsx.mit.edu/pub/linux/packages/GCC/ELF.doc.tar.gz. A flat-text version is http://www.muppetlabs.com/~breadbox/software/ELF.txt. And finally http://www.muppetlabs.com/~breadbox/software/tiny/teensy.html humorously describes how far you can bend the rules to reach minimal size.

Sort of an answer

0x8048000 is not a natural constant, but happens to be the default base address of ELF executables produced by ld. As of version 2.11 of binutils it should be possible to change that with options -Ttext ORG and --section-start SECTIONNAME=ORG, but I didn't get it working. Anyway, the layout of executables produced by ld is straight forward.

  1. One ELF header - Elf32_Ehdr

  2. Program headers - Elf32_Phdr

  3. Program interpreter (not if statically linked)

  4. Code

  5. Data

  6. Section headers - Elf32_Shdr

Everything from the start of the file to the last byte of code is loaded into one segment (named "code" or "text") that begins at the base address. There is a whole section called readelf describing a command to view all these details. In the meantime I will show fancy ways to get by without.

Showing off some tools

What would you do if you knew nothing about ELF and just asked yourself how that example works? How can you go sure that the executable file really contains those three letters?

A good start for finding text in binary files is strings.

Command.

Output.

The leading 1: is written by grep and tells that our three-letter word is the first found string. This gives some help where we can find it in a hex dump. It is difficult to search strings in such a dump because of line breaks. Interactive tools like hexedit might be useful.

Command.

Output.

At this point we can guess that file offset 1 and 0x8048000 + 1 are not coincidental. A test program might help.

Source - addr_of_main.c.

Output.

Looks good. The byte at address 0x8048000 + 0 is equal to that at file offset 0. And the address of function main is plausible.

Command.

Output.

Both programs have main at the same file offset. Unfortunately a brief look through /bin proves this to be pure chance. The really bad news is the generated code, however. Instead of a real system call for write we see a strange negative address. Let's have another try.

Command.

Output.

That strange negative address resolves to a function in a shared library. Not shown is a pathetic attempt to single-step to the actual code of write.

In doubt use force

We can now search for a fine manual explaining how to debug shared libraries. Or just compile the bugger static.

Command.

Output.

Seems we found an easy way to fill up the hard disk. Anyway, what has gdb to say about it?

Output.

The name of the function changed for no apparent reason. But it is reachable for disassembly now.

Output.

In the language of evil

The code generated by gcc is not suitable for a virus. So here comes hand crafted code optimized for size (twenty three is the perfect number of bytes). I prefer nasm to GNU as.

Source.

Command.

Output.

Output is good. But how do we get the resulting machine code? We can't just add a call to printf(3) to the assembly code. Above example is not linked with glibc; it does not even have a function called main.

Entry point

On the other hand things became a lot easier. There is no initialization code that gets executed before _start, so the address of _start is really the ELF entry point of the executable. A look into /usr/include/elf.h shows that Elf32_Ehdr::e_entry is at file offset 24.

Command.

Output.

The entry point is specified as a virtual address in memory. By subtracting the base address we get the file offset:

0x08048080 - 0x8048000 = 0x80

Evil magic revealed

Command.

Output.

Dressing up binary code

There is still one thing left: Dressing up the hex dump as C source. A filter written in Perl will do.

Filter.

Output.

The __attribute__ clause is explained in A section called .text. It is not required at this point.

Calling the string constant main is not a mistake. Above output is a complete and valid C program.

Command.

Output.

Other roads to ELF

Source.

Output.

Command.

Output.

Command.

Output.