Any sufficiently advanced technology is indistinguishable from magic. | |
Arthur C. Clarke |
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. [1] On practically every recent installation of Linux/i386 the following code will emit three magic letters instead of just dumping core.
Source: pre/i386-redhat8.0-linux/magic_elf/magic_elf.c
#include <unistd.h>
int main() { write(1, (void*)0x8048001, 3); return 0; } |
Command: pre/i386-redhat8.0-linux/magic_elf/cc.sh
#!/bin/bash
/usr/bin/gcc \
-Wall -O1 -I . -I out/i386-redhat8.0-linux -D NDEBUG \
-o tmp/i386-redhat8.0-linux/magic_elf/magic_elf \
pre/i386-redhat8.0-linux/magic_elf/magic_elf.c \
&& tmp/i386-redhat8.0-linux/magic_elf/magic_elf |
Output: out/i386-redhat8.0-linux/magic_elf/magic_elf
ELF |
The three letters are part of the signature of ELF files. Executables created by ld(1) are always mapped into the same memory region. That's why the program can find its own header at a predictable virtual address.
RTFM. [2] Just read all of Executable and linkable format (i).
0x8048000 is not a natural constant, but happens to be the default base address of ELF executables produced by ld(1) on i386. Option -Ttext ORG and --section-start SECTIONNAME=ORG of GNU ld should allow to change it, but I didn't get it working. Anyway, the layout of executables produced by ld(1) is straight forward.
One ELF header - Elf32_Ehdr
Program headers - Elf32_Phdr
Program interpreter (not if statically linked)
Code
Data
Section headers - Elf32_Shdr
Everything from the start of the file to the last byte of code is mapped into one segment (colloquially named "code" or "text") that begins at the base address. There is a whole chapter called Segments describing tools to view all these details. In the meantime I will show fancy ways to get by without.
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(1).
Command: pre/i386-redhat8.0-linux/magic_elf/strings.sh
#!/bin/bash
# without "-a -n 3" we don't get any output
/usr/bin/strings -a -n 3 \
tmp/i386-redhat8.0-linux/magic_elf/magic_elf \
| /bin/grep -n ELF |
Output: out/i386-redhat8.0-linux/magic_elf/strings
1:ELF |
The leading 1: is written by grep(1) 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(1) or khexedit(1) might be useful.
The traditional tool for dumps is called od(1), the abbreviation for "octal dump". The classic version does provide hexadecimal output, but unfortunately not for single bytes. Option -x outputs "words", which are defined to be two bytes. This does not matter on big-endian machines like the sparc, but on i386 and alpha it is quite confusing.
On both Linux and SunOS od features option -tx1 to get byte-wise hexadecimals. FreeBSD's od has no equivalent. Another interesting option is -N count which reads no more than count bytes of input. Again it is not available on FreeBSD.
Command: pre/i386-redhat8.0-linux/magic_elf/od/Linux.sh
#!/bin/bash
src=tmp/i386-redhat8.0-linux/magic_elf/magic_elf
/usr/bin/od -N 16 -c ${src} | /bin/sed 1q
/usr/bin/od -N 16 -x ${src} | /bin/sed 1q
/usr/bin/od -N 16 -tx1 ${src} | /bin/sed 1q
/usr/bin/od -N 16 -ta ${src} | /bin/sed 1q |
Output: out/i386-redhat8.0-linux/magic_elf/od
0000000 177 E L F 001 001 001 \0 \0 \0 \0 \0 \0 \0 \0 \0
0000000 457f 464c 0101 0001 0000 0000 0000 0000
0000000 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
0000000 del E L F soh soh soh nul nul nul nul nul nul nul nul nul |
hexdump features user defined formats. It is a typical ingredient of Linux and FreeBSD installations, but not shipped with SunOS.
Data: src/format.hex
"%04.4_ax " 8/1 "%02x " " " 8/1 "%02x "
" " 16/1 "%_p" "\n" |
Source: pre/i386-redhat8.0-linux/magic_elf/hexdump.sh
#!/bin/bash
/usr/bin/hexdump -n 80 -f ./src/format.hex \
< tmp/i386-redhat8.0-linux/magic_elf/magic_elf |
Output: out/i386-redhat8.0-linux/magic_elf/hexdump
0000 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 .ELF............
0010 02 00 03 00 01 00 00 00 78 82 04 08 34 00 00 00 ........x...4...
0020 b8 20 00 00 00 00 00 00 34 00 20 00 06 00 28 00 ¸ ......4. ...(.
0030 22 00 1f 00 06 00 00 00 34 00 00 00 34 80 04 08 ".......4...4...
0040 34 80 04 08 c0 00 00 00 c0 00 00 00 05 00 00 00 4...À...À....... |
xxd is part of vim. [3] And though that can't be called installation core it comes pretty close on Linux distributions.
Source: pre/i386-redhat8.0-linux/magic_elf/xxd.sh
#!/bin/bash
/usr/bin/xxd -l 80 \
< tmp/i386-redhat8.0-linux/magic_elf/magic_elf |
Output: out/i386-redhat8.0-linux/magic_elf/xxd
0000000: 7f45 4c46 0101 0100 0000 0000 0000 0000 .ELF............
0000010: 0200 0300 0100 0000 7882 0408 3400 0000 ........x...4...
0000020: b820 0000 0000 0000 3400 2000 0600 2800 . ......4. ...(.
0000030: 2200 1f00 0600 0000 3400 0000 3480 0408 ".......4...4...
0000040: 3480 0408 c000 0000 c000 0000 0500 0000 4...À...À....... |
Anyway, at this point we can guess that file offset 1 and 0x8048000 + 1 are not coincidental. A test program might help.
Source: pre/i386-redhat8.0-linux/magic_elf/addr_of_main.c
#include <stdio.h>
int main()
{
printf("# sizeof(unsigned long)=%u\n", (unsigned)sizeof(unsigned long));
printf("# 8048000=%#02x\n", *(unsigned char*)0x8048000);
printf("# 8048001=%.3s\n", (char*)0x8048001);
/* output of %p can either be hex or decimal, so comment it out */
printf("# addr_main_p=%p\n\n", main);
/* suffix "_x" is required by post-processing (etc/calc.pl) */
printf("addr_main_x=%lx\n", (unsigned long)main);
printf("ofs_main_x=%lu\n", (unsigned long)main - 0x8048000);
printf("ofs_main=%lu\n", (unsigned long)main - 0x8048000);
return 0;
} |
Output: out/i386-redhat8.0-linux/magic_elf/addr_of_main
# sizeof(unsigned long)=4
# 8048000=0x7f
# 8048001=ELF
# addr_main_p=0x8048328
addr_main_x=8048328
ofs_main_x=808
ofs_main=808 |
Note that the output of %p is not standardized. Some platforms print a leading 0x, some don't. Even %#p does not guarantee a leading 0x. Anyway, output looks good. The byte at address 0x8048000 + 0 is equal to that at file offset 0. And 0x8048328 is a plausible address of function main.
Source: pre/i386-redhat8.0-linux/magic_elf/other_perl.pl
#!/usr/bin/perl
syscall 4, 1, 0x8048001, 3 |
Output: out/i386-redhat8.0-linux/magic_elf/other_perl
ELF |
Source: pre/i386-redhat8.0-linux/magic_elf/other_exe.sh
#!/bin/bash
/bin/dd if=/proc/self/exe bs=1 skip=1 count=3 2>/dev/null |
Output: out/i386-redhat8.0-linux/magic_elf/other_exe
ELF |
The classic tool for conversion from hex to decimal is bc, a front end to dc. dc itself lacks standardized commands for nice output. Command n is a rather recent GNU extension and not available on bc-1.04-39 shipped with SuSE Linux 6.0.
Command: pre/i386-redhat8.0-linux/magic_elf/other_mem.sh
#!/bin/bash
skip=$( echo "ibase=16; 8048001" | /usr/bin/bc )
/bin/dd if=/proc/self/mem bs=1 skip=${skip} count=3 2>/dev/null |
Output: out/i386-redhat8.0-linux/magic_elf/other_mem
ELF |
[1] | On this platform it's actually /bin/tcsh. This is the result of a systematic search at A kingdom for a shell. |
[2] | |
[3] |