Författad av: swestres
Introduktion
ELF står för Executable and Linkable Format och är det filformat som används (med vissa undantag) på Linuxsystem för körbara filer som inte behöver någon annan maskin än CPUn för att köras, till skillnad från Javaprogram och diverse script. De senaste dagarna har jag kollat upp formatet och hur det är implementerat för IA-32 och för att sammanfatta det jag snappat upp så skriver jag denna artikel. Egoistiska skäl.
Som namnet antyder används ELF inte bara för exekverbara filer, utan även för programbibliotek. Denna artikel kommer uteslutande att handla om hur ELF-formatet används för exekverbara filer. Syftet är, förutom de egoistiska skäl som tidigare angavs, att läsaren ska få en ökad kunskap om hur program laddas i Linux och hur programfiler är uppbyggda. Denna kunskap kan sedan utnyttjas för att skriva program som:
- Skriver program
- Modifierar program
- Utnyttjar den miljö ett program tilldelas av ELF loadern
ELF-layout
Alla ELF-filer börjar med en ELF-header. Denna finns definierad i /usr/include/elf.h, i kärnan under include/linux/elf.h och ser ut såhär:
CODE
#define EI_NIDENT 16
typedef struct elf32_hdr{
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry; /* Entry point */
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
} Elf32_Ehdr;
typedef struct elf32_hdr{
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry; /* Entry point */
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
} Elf32_Ehdr;
Datatyperna ovan är alla 4 bytes, utom unsigned char som är 1 byte och Elf32_Half som är 2 byte. Alla är unsigned. Vi behöver inte bry oss om deras alignment.
Förutom ELF headern består en ELF fil av två tabeller (program header table och section header table)och ett antal segment som innehåller kod eller data. I en exekverbar ELF-fil behöver man inte ha section header table och vi kommer därför inte gå in mer på den. Fält som hänvisar till denna tabell sätts till 0. Sections i en körbar ELF-fil kan ange namn på segment och liknande.
- e_ident; de fyra första byten är strängen 0x7f,'ELF'. Detta används för att identifiera filen som en ELF fil. Den 5'e byten anger vilken objektklass filen är, den bör vara satt till 1, ELFCLASS32. Den 6'e byten anger vilken byte order som används, denna bör också vara satt till 1, ELFDATA2LSB. Byte nummer sju av e_ident anger vilken version ELF headern har. Standarden definierar bara en version, EV_CURRENT, 1. Resten av e_ident är utfyllnad och bör vara satt till 0.
- e_type anger vilken typ av ELF fil det är, i vårat fall sätter vi den till ET_EXEC, 2, som anger en exekverbar fil.
- e_machine anger vilken processorarkitektur som används. Sätt till EM_386, 3, för IA-32 processorer.
- e_version anger filformatsversionen. Finns bara en, EV_CURRENT, 1.
- e_entry anger den virtuella adress där exekveringen av filen startar. Vilket värde detta fält har är beroende på var vi laddar det segment som innehåller vår entry, och var i detta segment vår entry är. Mer om detta senare.
- e_phoff anger var vårt program header table finns relativt till början av filen
- e_shoff anger var vår section header table finns relativt till början av filen
- e_flags är reserverat för processorspecifika inställningar, för IA-32 sätts denna till 0
- e_ehsize håller storleken på vår ELF header i bytes
- e_phentsize håller storleken på ett fält i vår program header table. Alla fält är lika stora.
- e_phnum håller antalet fält i vår program header table.
- e_shentsize håller storleken på ett fält i vår section header table. Alla fält är lika stora.
- e_shnum håller antalet fält i vår section header table.
- e_shstrndx anger var ett fält i section header table som kallas section name string table finns, sätts till SHN_UNDEF, 0, om vi saknar det.
CODE
typedef struct elf32_phdr{
Elf32_Word p_type;
Elf32_Off p_offset;
Elf32_Addr p_vaddr;
Elf32_Addr p_paddr;
Elf32_Word p_filesz;
Elf32_Word p_memsz;
Elf32_Word p_flags;
Elf32_Word p_align;
} Elf32_Phdr;
Elf32_Word p_type;
Elf32_Off p_offset;
Elf32_Addr p_vaddr;
Elf32_Addr p_paddr;
Elf32_Word p_filesz;
Elf32_Word p_memsz;
Elf32_Word p_flags;
Elf32_Word p_align;
} Elf32_Phdr;
- p_type anger vilken typ av segment som specifiseras. Det finns ett par stycken men det enda vi behöver bry oss om är PT_LOAD, 1 som anger ett segment som kan laddas till minnet.
- p_offset anger offset till vårt segment från början av filen
- p_vaddr anger den virtuella adressen dit vårt segment laddas
- p_paddr anger den fysiska adressen dit vårt segment laddas, ignoreras. Sätts till samma som p_vaddr
- p_filesz anger storlek på segmentet i ELF-filen
- p_memsz anger storlek på segmentet i minnet
- p_flags anger om segmentet är läsbart (PF_R, 4), skrivbart (PF_W, 2) och/eller körbart (PF_X, 1).
- p_align anger vilket alignment i minnet som sektionen ska ha, kan vara 0 enligt ELF standarden. Standard i linux är 0x1000 (en page size).
CODE
+----------+
|Elf32_Ehdr|
+----------+
|Elf32_Phdr|
+----------+
| Seg 0 |
+----------+
|Elf32_Ehdr|
+----------+
|Elf32_Phdr|
+----------+
| Seg 0 |
+----------+
Vilket ger oss 84 byte headers och X antal byte programkod, helt OK. Vill man göra mindre program kan man utnyttja oanvända fält i ELF headern och väva ihop ELF headern med program header table. Hur man gör detta finns beskrivet i dokumentet "A Whirlwind Tutorial on Creating Really Teensy ELF Executables for Linux" som finns att hitta lite varstans på Internet.
Så var det det här med virtuella adresser. När en körbar ELF-fil laddas till minnet läggs den till den virtuella adressen 0x08048000 i Linuxsystem.
Nog med prat, låt mig presentera ett program som tar och bygger ett fungerande program:
CODE
// elf_build.c - ELF executable file binder, takes raw binary instructions from
// a file and puts them in an ELF segment. For IA-32 systems.
// Copyright (C) 2007 swestres, no rights reserved
#include <elf.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define VADDR_BASE 0x08048000; // Virtual base address of the image
char *read_code(char *file, int *fsize);
void setup_program_header(Elf32_Phdr *hdr, int codesize);
void setup_elf_header(Elf32_Ehdr *hdr);
int main(int argc, char *argv[])
{
Elf32_Ehdr e_hdr; //ELF header
Elf32_Phdr p_hdr; //Program header
char *imgdata;
int fsize;
FILE *fp;
if (argc != 3) {
printf("Swestres' ELF executable file builder\n");
printf("usage:\n");
printf("%s <infile> <outfile>\n", argv[0]);
return 0;
}
imgdata = read_code(argv[1], &fsize);
if (!imgdata) {
printf("couldn't read input file\n");
return -1;
}
setup_program_header(&p_hdr, fsize);
setup_elf_header(&e_hdr);
fp = fopen(argv[2], "wb");
if (!fp) {
printf("couldn't open output file\n");
return -1;
}
fwrite(&e_hdr, sizeof(e_hdr), 1, fp);
fwrite(&p_hdr, sizeof(p_hdr), 1, fp);
fwrite(imgdata, fsize, 1, fp);
fclose(fp);
return 0;
}
char *read_code(char *file, int *fsize)
{
FILE *fp;
char *imgdata;
fp = fopen(file, "rb");
if (!fp) {
return NULL;
}
fseek(fp, 0, SEEK_END);
*fsize = ftell(fp);
fseek(fp, 0, SEEK_SET);
imgdata = malloc(*fsize);
if (!imgdata) {
return NULL;
}
if (fread(imgdata, *fsize, 1, fp) == 0) {
free(imgdata);
return NULL;
}
fclose(fp);
return imgdata;
}
void setup_program_header(Elf32_Phdr *hdr, int codesize)
{
int imgsize = sizeof(Elf32_Ehdr) + sizeof(Elf32_Phdr) + codesize;
hdr->p_type = PT_LOAD;
hdr->p_offset = 0;
hdr->p_vaddr = VADDR_BASE; // Virtual address
hdr->p_paddr = 0; // physical address, ignored
hdr->p_filesz = imgsize; //file image size of segment
hdr->p_memsz = imgsize; //memory image size of segment
hdr->p_flags = PF_R | PF_W | PF_X; //rwx
hdr->p_align = 0x1000; // memory alignment
}
void setup_elf_header(Elf32_Ehdr *hdr)
{
memset(hdr->e_ident, 0, EI_NIDENT);
hdr->e_ident[EI_MAG0] = ELFMAG0;
hdr->e_ident[EI_MAG1] = ELFMAG1;
hdr->e_ident[EI_MAG2] = ELFMAG2;
hdr->e_ident[EI_MAG3] = ELFMAG3;
hdr->e_ident[EI_CLASS] = ELFCLASS32;
hdr->e_ident[EI_DATA] = ELFDATA2LSB;
hdr->e_ident[EI_VERSION] = EV_CURRENT;
hdr->e_type = ET_EXEC;
hdr->e_machine = EM_386; //i386
hdr->e_version = EV_CURRENT;
hdr->e_entry = VADDR_BASE + sizeof(Elf32_Ehdr) +
sizeof(Elf32_Phdr); // Virtual address entry point
hdr->e_phoff = sizeof(Elf32_Ehdr); // Program header table offset
hdr->e_shoff = 0; //section header offset, aint got one of those
hdr->e_flags = 0; // No flags are defined for IA-32
hdr->e_ehsize = sizeof(Elf32_Ehdr); //ELF header size
hdr->e_phentsize = sizeof(Elf32_Phdr); // Program header table entry size
hdr->e_phnum = 1; // Number of program header table entries
hdr->e_shentsize = 0; // Section header table entry size
hdr->e_shnum = 0; //Number of section header table entries
hdr->e_shstrndx = SHN_UNDEF;
}
// a file and puts them in an ELF segment. For IA-32 systems.
// Copyright (C) 2007 swestres, no rights reserved
#include <elf.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define VADDR_BASE 0x08048000; // Virtual base address of the image
char *read_code(char *file, int *fsize);
void setup_program_header(Elf32_Phdr *hdr, int codesize);
void setup_elf_header(Elf32_Ehdr *hdr);
int main(int argc, char *argv[])
{
Elf32_Ehdr e_hdr; //ELF header
Elf32_Phdr p_hdr; //Program header
char *imgdata;
int fsize;
FILE *fp;
if (argc != 3) {
printf("Swestres' ELF executable file builder\n");
printf("usage:\n");
printf("%s <infile> <outfile>\n", argv[0]);
return 0;
}
imgdata = read_code(argv[1], &fsize);
if (!imgdata) {
printf("couldn't read input file\n");
return -1;
}
setup_program_header(&p_hdr, fsize);
setup_elf_header(&e_hdr);
fp = fopen(argv[2], "wb");
if (!fp) {
printf("couldn't open output file\n");
return -1;
}
fwrite(&e_hdr, sizeof(e_hdr), 1, fp);
fwrite(&p_hdr, sizeof(p_hdr), 1, fp);
fwrite(imgdata, fsize, 1, fp);
fclose(fp);
return 0;
}
char *read_code(char *file, int *fsize)
{
FILE *fp;
char *imgdata;
fp = fopen(file, "rb");
if (!fp) {
return NULL;
}
fseek(fp, 0, SEEK_END);
*fsize = ftell(fp);
fseek(fp, 0, SEEK_SET);
imgdata = malloc(*fsize);
if (!imgdata) {
return NULL;
}
if (fread(imgdata, *fsize, 1, fp) == 0) {
free(imgdata);
return NULL;
}
fclose(fp);
return imgdata;
}
void setup_program_header(Elf32_Phdr *hdr, int codesize)
{
int imgsize = sizeof(Elf32_Ehdr) + sizeof(Elf32_Phdr) + codesize;
hdr->p_type = PT_LOAD;
hdr->p_offset = 0;
hdr->p_vaddr = VADDR_BASE; // Virtual address
hdr->p_paddr = 0; // physical address, ignored
hdr->p_filesz = imgsize; //file image size of segment
hdr->p_memsz = imgsize; //memory image size of segment
hdr->p_flags = PF_R | PF_W | PF_X; //rwx
hdr->p_align = 0x1000; // memory alignment
}
void setup_elf_header(Elf32_Ehdr *hdr)
{
memset(hdr->e_ident, 0, EI_NIDENT);
hdr->e_ident[EI_MAG0] = ELFMAG0;
hdr->e_ident[EI_MAG1] = ELFMAG1;
hdr->e_ident[EI_MAG2] = ELFMAG2;
hdr->e_ident[EI_MAG3] = ELFMAG3;
hdr->e_ident[EI_CLASS] = ELFCLASS32;
hdr->e_ident[EI_DATA] = ELFDATA2LSB;
hdr->e_ident[EI_VERSION] = EV_CURRENT;
hdr->e_type = ET_EXEC;
hdr->e_machine = EM_386; //i386
hdr->e_version = EV_CURRENT;
hdr->e_entry = VADDR_BASE + sizeof(Elf32_Ehdr) +
sizeof(Elf32_Phdr); // Virtual address entry point
hdr->e_phoff = sizeof(Elf32_Ehdr); // Program header table offset
hdr->e_shoff = 0; //section header offset, aint got one of those
hdr->e_flags = 0; // No flags are defined for IA-32
hdr->e_ehsize = sizeof(Elf32_Ehdr); //ELF header size
hdr->e_phentsize = sizeof(Elf32_Phdr); // Program header table entry size
hdr->e_phnum = 1; // Number of program header table entries
hdr->e_shentsize = 0; // Section header table entry size
hdr->e_shnum = 0; //Number of section header table entries
hdr->e_shstrndx = SHN_UNDEF;
}
Hela avbildningen laddas till minnet, inte bara kodsegmentet. Men vi måste ju ha kod att köra också. Sagt och gjort, här är ett program för Linux som skriver ut en text till skärmen:
CODE
;; dummy.asm, 31 bytes dummy file
;; Copyright (C) swestres 2007, no rights reserved
;; compile with nasm -fbin dummy.asm
BITS 32
org 0x08048054 ; the virtual base address
mov al, 4
mov bl, 1
mov ecx, _str
mov edx, _strl
int 0x80
xor eax, eax
inc eax
xor ebx, ebx
int 0x80
;; Some strings
_str: db 'Muihihi',10
_strl equ $-_str
;; Copyright (C) swestres 2007, no rights reserved
;; compile with nasm -fbin dummy.asm
BITS 32
org 0x08048054 ; the virtual base address
mov al, 4
mov bl, 1
mov ecx, _str
mov edx, _strl
int 0x80
xor eax, eax
inc eax
xor ebx, ebx
int 0x80
;; Some strings
_str: db 'Muihihi',10
_strl equ $-_str
Lägg märke till att vi använder org för att få rätt adresser på våra labels.
Låt oss testa det:
QUOTE
$ gcc -o elfb elf_build.c && nasm -fbin dummy.asm && ./elfb dummy new_elf
$ chmod u+x new_elf && ./new_elf
Muihihi
$ chmod u+x new_elf && ./new_elf
Muihihi
Hur många program har du som är 115 byte eller mindre?
Dynamisk länkning
Dynamisk länkning av bibliotek kan göras under körning (vilket inte har någotatt göra med ELF) eller vid programinitiering. I Linux görs detta med hjälp av ett antal sektioner (.dynsym, dynamic symbol table. .plt alt. .rel.plt, Procedure Linkage Table, en jump table, osv). Eftersom jag valt att bortse från sektioner då de inte är nädvändiga för att skriva körbara ELF program så kommer denna artikel inte att hantera dynamisk länkning. Det kanske kommer senare. Under tiden får vi nöja oss med syscalls.
Programmiljön
När vår ELF laddas till minnet får den en viss mijö tilldelad.
Programstacken börjar på 0xc0000000 och växer neråt. Mellan esp och 0xc0000000 finns:
CODE
4B argc*4+4B upp till NULL upp till AT_NULL 0-16 B
int argc | char *argv[] | char *envp[] | Elf32_auxv_t auxv[] | padding |
var var
argv strängar | envp strängar
int argc | char *argv[] | char *envp[] | Elf32_auxv_t auxv[] | padding |
var var
argv strängar | envp strängar
argc anger argument count, argv är ett fält med pekare till argumentsträngarna längre upp på stacken, envp är ett fält med pekare till miljövariablerna längre upp på stacken. auxv är intressant, det är ett fält med ELF auxiliary vectors, var och en på åtta byte. Varje element är 8 byte och har ett fält som anger vilken typ det är (AT_* i elf.h) och ett fält som anger data. Antingen en adress till mer data, eller en integer med ett speciellt värde. Här kan man hitta real/effective uid/gid för processen, entry point, times() frekvens, etc. På 2.6 kärnor hittar man adressen till global system page som används för syscalls med SYSENTER här. De fält som måste finnas är AT_ENTRY, AT_PHENT, AT_PHDR, AT_PHNUM och AT_PAGESZ. Efter det kommer lite utfyllnad, och sen kommer argumentsträngar och miljövariabelsträngar.
Processorns register (förutom esp och eip) sätts till noll. Alla dynamiska symboler har laddats. eip sätts till ELF headerns e_entry. Inte nödvändigtvis i den ordningen.
Slutord
Det finns mycket kvar som inte tagits upp i denna artikel. Vill du veta mer rekommenderar jag följande informationskällor (förutom de som ev.
redan nämts):
- Wikipedia, bra introduktionsartikel under "Executable and Linkable Format"
- TIS ELF 1.2, den officiella ELF specifikationen.
- Linux källkod
- Phrack har en del intressanta artiklar om bl.a. ELF injection (0x38, art. 7)