Zoom Icon

Shared object injection part 1

From UIC

Shared object injection part 1

Contents


Shared object injection part 1
Author: Quake2
Email: quake2th@gmail.com
Website: Home page
Date: 06/11/2008 (dd/mm/yyyy)
Level: Major skills are required
Language: Italian Flag Italian.gif
Comments: "The god of man is a failure And all of our shadows are ashes against the grain" Agalloch - Ashes Against The Grain



Introduzione

Salve! Dopo molto tempo (ma proprio tanto) ho trovato il tempo (e la voglia) di scrivere qualcosa.

Recentemente per vari motivi mi sono trovato a dover giocare un po' con linux, quindi la prima cosa che mi è venuta in mente di fare è tentare di implementare le tecniche più o meno "standard" che si usano su windows, anche su questo sistema operativo.

In questa serie di tre articoli, separati per semplificare la lettura (e perché non ho tempo di scriverne uno enorme subito :)), vedremo come poter "hookare" una funzione importata da un eseguibile ELF (non rompete con a.out, non ho voglia di studiarmelo :)), tramite l'injection a runtime di uno shared object, in cui è contenuta la funzione che andrà a "sostituire" quella originale. Ora vi starete chiedendo dov'è la novità in tutto ciò, visto che i più arditi di voi staranno già pensando "beh basta fare LD_PRELOAD=libreria.so ./eseguibile"...beh è proprio questo il punto, col "metodo LD_PRELOAD" dovete fermare e far ripartire l'eseguibile, mentre con il metodo che vado a proporvi, a patto che si verifichino determinate condizioni (niente di trascendentale, non dovrete assicuravi di avere la versione del kernel compilata alle 18.23 UTC da Linus in persona, tranquilli), non sarà necessario far ripartire il processo.

Riguardo all'hooking delle funzioni importate la tecnica che andremo ad utilizzare si basa sulla modifica della PLT, tecnica già nota da molto tempo, che in questa sede verrà "rivisitata" per adattarla ai nostri scopi. Per quanto riguarda l'injection dello shared object invece, ci baseremo su una tecnica "nuova" (si fa per dire) che, come già accennato, richiede una particolare situazione: nel processo in cui andremo ad iniettare lo shared object, dovrà essere presente libdl; questo è necessario perché a partire dal kernel 2.6, su linux non esistono più syscall per caricare uno shared object (fino al 2.4 vi era la syscall "uselib", che attualmente sembra non funzionare a prescindere da ciò che gli si passa). Questa richiesta sembra una limitazione "pesante", tuttavia vi garantisco che non è così, al giorno d'oggi è difficile trovare programmi per linux "di un certo spessore" che non si ritrovino mappata libdl nel loro address space, dobbiamo tenere a mente che la nostra tecnica va usata per il reversing, sicuramente ci ritroveremo a reversare un programma commerciale complesso (visto che quelli semplici già esistono per linux e sono tutti opensource, che cavolo reversate? :)), possiamo quindi essere certi che ci sia libdl. In ogni caso, se dovesse mancare libdl, questa tecnica è ancora applicabile, vi basterà iniettare lo shared object (so da ora in poi) col solito LD_PRELOAD oppure iniettare libdl con LD_PRELOAD, tenendo a mente che LD_PRELOAD non funziona con eseguibili setuid.

Questo articolo è suddiviso in tre parti logiche. La prima e la seconda parte serviranno a perapare il terreno per la terza ed ultima parte, in cui si arriverà all'implementazione vera e propria del metodo.


Tools & Files



URL o FTP del programma



Essay

Prima di partire col tutorial vero e proprio, bisogna fare un attimo alcune precisazioni sui requisiti e su "come si leggono" i manuali dell' ELF. Assicuratevi di scaricare il manuale generale e quello per x86, leggendo quello generale vi accorgerete che alcuni paragrafi sono mancanti (è indicato nel testo con dei marker), le parti mancanti sono quelle dipendenti dalla piattaforma, dovete quindi integrare il manuale generale con quello specifico per la vostra piattaforma (che suppongo sia x86), sopra ho riportato i link per le piattaforme x86 e x64, tuttavia in questo articolo e nei successivi farò sempre riferimento a x86.

Per quanto riguarda i requisiti, do per scontato che sapete programmare in C e qualche conoscenza di base di assembler x86. Inoltre è necessario installare tutta la toolchain del gcc, altrimenti non sarete in grado di buildare niente, quindi assicuratevi di avere il gcc (una versione abbastanza recente, la mia è la 4.3.2). Non sono richieste particolari conoscenze sugli internals di linux, apparte la struttura del proc filesystem (ma vi dirò quello che serve, tranquilli :)). Svincolando il codice presentato dall'utilizzo del proc filesystem, diventa tutto portabile su altre architetture unix in cui sia presente un implementazione funzionante dell'interfaccia ptrace. Comunque come vedrete il proc filesystem è usato veramente per poche cose. Riguardo a GDB, non è strettamente necessario, ma alcuni esempi che farò lo useranno per analizzare la memoria, vedere le istruzioni, ecc..., quindi se lo avete pure voi meglio. NASM invece dovrete averlo per forza, mai e poi mai scriverò il codice in sintassi at&t con "as", quindi rassegnatevi ed usate NASM :).

Breve introduzione all'ELF

In questo capitolo vedremo brevemente che cos'è il formato ELF, quali sono le sue strutture principali e il funzionamento di alcune di esse, rimandando l'analisi del dynamic linking capitolo successivo. Il mio scopo in questo capitolo non sarà fare una traduzione pari pari dei manuali dell' ELF (sono molto semplici e fatti bene, leggeteveli :)), ma piuttosto introdurre brevemente il formato e descriverne le strutture principali riportando parti di codice che mostrano come leggerle, sarà quindi un approccio "da programmatore".

Cenni storici

Il formato ELF (acronimo di Executable and Linking Format) è uno dei tanti formati che permettono di descrivere la struttura logico/fisica di un file contenente codice compilato. Esempi di formati simili all' ELF sono il PE (Portable Executable) di Windows, il Mach-O di OSX, il COFF (ovviamente, essendo partito tutto da qua :), anche se la struttura dell' ELF non ha praticamente niente a che fare col COFF, ma storicamente il COFF è stato uno dei primissimi formato eseguibili), il formato a.out (stesso discorso del COFF), etc... .

Come si può intuire dal nome, l' ELF permette di descrivere sia degli eseguibili (programmi stand-alone) sia delle librerie (collezione di codice che viene utilizzato da un programma). Storicamente l' ELF fu introdotto per rimpiazzare i formati a.out e COFF nei sistemi UNIX, diventando uno standard per i formati eseguibili nel 1999. Attualmente l' ELF è utilizzato praticamente da tutte le piattaforme non Microsoft (dove si utilizza prevalentemente il formato PE), grazie al fatto che si tratta di un formato molto versatile, adattabile praticamente ad ogni scopo. Inoltre grazie al linker ld presente nei tool GNU, è possibile generare un ELF personalizzandolo secondo le proprie esigenze (infatti ld può essere usato tramite degli script che permettono di gestire la generazione dell'eseguibile/libreria). Alcuni esempi di piattaforme non unix che utilizzano l'ELF come formato sono: PSP, PS2, PS3, Wii, Dreamcast, BeOS, Haiku, AmigaOS, MorphOS, SymbianOS (un formato derivato dall'ELF). Questa versatilità è dovuta principalmente al fatto che le strutture fondamentali dell' ELF non sono legate ad una piattaforma specifica.

Struttura di un ELF

La struttura dell' ELF non si discosta molto da quella degli altri formati eseguibili, ovvero è composta da un header e da un insieme di sezioni che descrivono il contenuto del file. Nei paragrafi che seguieranno ci aiuteremo con il programma readelf per vedere alcuni esempi. L'eseguibile che userò sarà /bin/ls (se non lo avete, PREOCCUPATEVI :)).

Header

L'header contiene informazioni sull'architettura, endianness, numero di sezioni, numero di program headers (vedremo poi cosa sono), string table, ecc... . Usando le struct del C, così come fanno i manuali dell' ELF, l'header si trova all'inizio del file e può essere rappresentato così:

#define EI_NIDENT (16)

typedef struct
{
  unsigned char e_ident[EI_NIDENT];    /* Magic number and other info */
  Elf32_Half    e_type;            /* Object file type */
  Elf32_Half    e_machine;        /* Architecture */
  Elf32_Word    e_version;        /* Object file version */
  Elf32_Addr    e_entry;        /* Entry point virtual address */
  Elf32_Off     e_phoff;        /* Program header table file offset */
  Elf32_Off     e_shoff;        /* Section header table file offset */
  Elf32_Word    e_flags;        /* Processor-specific flags */
  Elf32_Half    e_ehsize;        /* ELF header size in bytes */
  Elf32_Half    e_phentsize;        /* Program header table entry size */
  Elf32_Half    e_phnum;        /* Program header table entry count */
  Elf32_Half    e_shentsize;        /* Section header table entry size */
  Elf32_Half    e_shnum;        /* Section header table entry count */
  Elf32_Half    e_shstrndx;        /* Section header string table index */
} Elf32_Ehdr;

dove il significato dei campi più importanti è il seguente:

  • e_ident: permette di identificare con certezza che si tratta di un ELF. Come vedete è un array di 16 byte, di cui i primi quattro sono una signature e corrispondono a: {0x7F, 'E', 'L', 'F'}, indicizzati rispettivamente dalle costanti (EI_MAG0, EI_MAG1, EI_MAG2, EI_MAG3). I byte successivi permettono di identificare in modo univoco la dimensione degli "oggetti" all'interno dell' ELF (fondamentalmente se abbiamo a che fare con un Elf32 o un Elf64), l'endianness e altri parametri che non sono importanti per i nostri scopi. Le costanti definite per indicizzare i byte dell'endianness e la dimensione degli oggetti sono rispettivamente (EI_CLASS,EI_DATA). Nel nostro caso, cioè architettura x86 a 32bit, avremo e_ident[EI_CLASS] = ELFCLASS32 e e_ident[EI_DATA] = ELFDATA2LSB, comunque nel codice che mostra come leggere l'header vedremo come accertarci della validità di un ELF partendo dal contenuto di e_ident.
  • e_entry: entry-point dell'eseguibile, cioè l'indirizzo (virtual address) della prima istruzione da eseguire. Nel caso di una libreria (so) può essere 0 oppure essere presente. Un esempio interessante di libreria dotata di entry-point è il loader ld.so su linux, che viene usato come libreria per caricare un ELF, ma può anche essere eseguito da riga di comando.
  • e_phoff: offset (dall'inizio del file) della tabella dei program header. Si tratta di un offset fisico quindi per posizionarsi all'inizio della tabella basta fare lseek(fd, hdr.e_phoff, SEEK_SET).
  • e_shoff: offset della tabella dei section header. Vale il discorso fatto in precedenza, trattandosi di un offset fisico.
  • e_phentsize: dimensione di una entry nella tabella dei program header.
  • e_shentsize: dimensione di una entry nella tabella dei section header.
  • e_phnum: numero di program header.
  • e_shnum: numero di section header.
  • e_shstrndx: indice nella tabella dei section header che dà la sezione in cui si trova la string table contenente i nomi delle sezioni.

come avrete notato, i campi dell'header (ma anche delle altre strutture) sono definiti usando dei tipi di dato custom, cioè Elf32_Word, Elf32_Half, ecc..., questo permette di identificare in modo univoco il tipo di quel campo, a prescindere dal tipo di architettura su cui ci troviamo. Mi spiego meglio, se ci troviamo su un architettura a 32bit avremo che Elf32_Word, Elf32_Addr e Elf32_Off saranno interi a 32bit senza segno, mentre se ci troviamo su un architettura a 64bit, avremo che Elf64_Word sarà un intero a 32bit senza segno, mentre Elf64_Addr e Elf64_Off saranno interi a 64bit senza segno. da questo si deduce che la dimensione di una word negli ELF è sempre 32bit, quindi i campi definiti tramite Elf32_Word, passando da un architettura a 32bit ad una a 64bit, non cambiano dimensione. Il tipo Elf32_Half rappresenta la "metà" di una word, quindi sarà sempre a 16bit. Per completezza, vi riporto i typedef dei vari tipi:

/* Type for a 16-bit quantity.  */
typedef uint16_t Elf32_Half;
typedef uint16_t Elf64_Half;

/* Types for signed and unsigned 32-bit quantities.  */
typedef uint32_t Elf32_Word;
typedef    int32_t  Elf32_Sword;
typedef uint32_t Elf64_Word;
typedef    int32_t  Elf64_Sword;

/* Types for signed and unsigned 64-bit quantities.  */
typedef uint64_t Elf32_Xword;
typedef    int64_t  Elf32_Sxword;
typedef uint64_t Elf64_Xword;
typedef    int64_t  Elf64_Sxword;

/* Type of addresses.  */
typedef uint32_t Elf32_Addr;
typedef uint64_t Elf64_Addr;

/* Type of file offsets.  */
typedef uint32_t Elf32_Off;
typedef uint64_t Elf64_Off;

/* Type for section indices, which are 16-bit quantities.  */
typedef uint16_t Elf32_Section;
typedef uint16_t Elf64_Section;

/* Type for version symbol information.  */
typedef Elf32_Half Elf32_Versym;
typedef Elf64_Half Elf64_Versym;

Vediamo l'output di readelf:

quake2@quake2-desktop:~$ readelf -h /bin/ls
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x8049b20
Start of program headers: 52 (bytes into file)
Start of section headers: 95096 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 9
Size of section headers: 40 (bytes)
Number of section headers: 28
Section header string table index: 27

come ci aspettavamo, è un eseguibile a 32bit (suppongo state lavorando su una versione a 32bit di linux...). Come potete vedere, in questo caso (ma non deve essere per forza così), l'array dei section header si trova alla fine del file, mentre l'array dei program header subito dopo l'header ELF.

Program header

Il program header è una struttura fondamentale dell' ELF, infatti ci dà informazioni su come vanno mappate una o più sezioni (che descriveremo in seguito) in memoria. Si tratta di una struttura fondamentale, un file ELF valido DEVE avere almeno un program header altrimenti il loader non è in grado di mapparlo in memoria. Teoricamente è sufficiente un program header per rendere l' ELF valido, tuttavia la quasi totalità degli ELF in circolazione hanno vari program header. Il motivo per cui spesso è presente più di un program header è che ogni program header ha un suo scopo particolare, come vedremo descrivendo i campi della struttura.

Prima di passare alla descrizione però, bisogna fare una precisazione importante. Un program header contiene a livello logico una o più sezioni, le sezioni hanno senso solo finché il file vive su disco, una volta mappato in memoria, le sezioni PERDONO COMPLETAMENTE di significato e l'unica struttura attendibile è il program header. Infatti teoricamente un ELF mappato in memoria perde la tabella dei section header. Questo non è assolutamente un problema, perché il program header ha tutte le informazioni necessarie per gestire un ELF una volta mappato in memoria.

Vediamo quindi la struttura del program header, tenendo presente che all'offset indicato da e_phoff nell'header dell' ELF abbiamo un array di queste strutture (il cui numero di elementi è dato da e_phnum):

typedef struct
{
  Elf32_Word    p_type;            /* Segment type */
  Elf32_Off    p_offset;        /* Segment file offset */
  Elf32_Addr    p_vaddr;        /* Segment virtual address */
  Elf32_Addr    p_paddr;        /* Segment physical address */
  Elf32_Word    p_filesz;        /* Segment size in file */
  Elf32_Word    p_memsz;        /* Segment size in memory */
  Elf32_Word    p_flags;        /* Segment flags */
  Elf32_Word    p_align;        /* Segment alignment */
} Elf32_Phdr;
  • p_type: il tipo di segmento con cui abbiamo a che fare, ne esistono diversi tipi, date un'occhiata al manuale dell' ELF per una descrizione di tutti i tipi esistenti, a noi interesserà solo PT_DYNAMIC (poi descriverò cos'è) e indirettamente PT_LOAD. Ovviamente ogni tipo di program header contiene informazioni diverse.
  • p_offset: offset SUL FILE dove si trova il segmento descritto dal program header, una volta mappato il file in memoria, questo campo perde significato.
  • p_vaddr: virtual address a cui si trova il segmento descritto dal program header quando viene mappato il file in memoria, bisogna fare una distinzione tra eseguibili e librerie: nel caso di un eseguibile, questo campo è un virtual address vero e proprio (poiché il base address di un eseguibile è fisso), nel caso di una libreria, questo campo conterrà un relative virtual address, a cui andrà sommato il base address della libreria.
  • p_paddr: questo campo su x86 e x64 non ha alcun significato, viene messo uguale a p_vaddr, su altre architetture dice a che indirizzo fisico si trova il segmento descritto dal program header, tuttavia questo campo non ha significato nella maggior parte delle architetture moderne.
  • p_filesz: dimensione del segmento SUL FILE, per vari motivi la dimensione del segmento in memoria può essere diversa.
  • p_memsz: dimensione del segmento in memoria, può essere diversa da p_filesz.
  • p_flags: i permessi dell'area di memoria descritta dal program header, date un'occhiata al manuale per maggiori informazioni, ma sono i soliti read, write, execute.
  • p_align: allineamento del segomento su file e in memoria, se è uguale a 0 o a 1, non c'è nessun allineamento, altrimenti si avrà p_vaddr % p_align = 0 e p_paddr % p_align = 0.

dovrebbe essere tutto abbastanza semplice, non si discota poi molto da quello che già dovreste conoscere nel caso di un PE. Come spero avrete intuito, l'equivalente delle sezioni di un PE sono i program header in un ELF, e NON le sezioni di un ELF. Sto ribadendo il più possibile questo fatto perché è importante, non dovete pensare che le sezioni di un ELF abbiano un significato particolare (nel caso di eseguibili/librerie), sono solo una divisione logica, infatti spesso gli ELF hanno anche 40 sezioni, alcune di 10 o meno byte, mentre spesso i program header sono MOLTI di meno (7 è un numero tipico). Tutto questo vale nel caso in cui abbiamo a che fare con eseguibili/librerie, ovviamente per i file oggetto (i .o) vale un discorso inverso, visto che questi non devono venir caricati in memoria. Vediamo l'output di readelf sul nostro /bin/ls:

quake2@quake2-desktop:~$ readelf -l /bin/ls

Elf file type is EXEC (Executable file)
Entry point 0x8049b20
There are 9 program headers, starting at offset 52

Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x08048034 0x08048034 0x00120 0x00120 R E 0x4
INTERP 0x000154 0x08048154 0x08048154 0x00013 0x00013 R 0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
LOAD 0x000000 0x08048000 0x08048000 0x16eb4 0x16eb4 R E 0x1000
LOAD 0x016ef0 0x0805fef0 0x0805fef0 0x003a0 0x0081c RW 0x1000
DYNAMIC 0x016f04 0x0805ff04 0x0805ff04 0x000e8 0x000e8 RW 0x4
NOTE 0x000168 0x08048168 0x08048168 0x00020 0x00020 R 0x4
GNU_EH_FRAME 0x016dec 0x0805edec 0x0805edec 0x0002c 0x0002c R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
GNU_RELRO 0x016ef0 0x0805fef0 0x0805fef0 0x00110 0x00110 R 0x1

Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .hash .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame
03 .ctors .dtors .jcr .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag
06 .eh_frame_hdr
07
08 .ctors .dtors .jcr .dynamic .got

osserviamo che questo ELF ha nove program header. Se vi state chiedendo cosa significa quel "Requesting program interpreter: /lib/ld-linux.so.2", è presto detto: un ELF per essere caricato deve venir linkato a runtime con le librerie che utilizza, quindi serve un programma "interprete" che collabori con il loader principale per generare un'immagine funzionante dell' ELF in memoria, ed è esattamente ciò che fa /lib/ld-linux.so.2.

Osservando la tabella alla fine, possiamo notare come il linker ha raggruppato le varie sezioni dell' ELF nei vari segmenti, soprattutto possiamo notare che una stessa sezione può far parte di più segmenti, questo per facilitare i compiti al linker e risparmiare risorse.

Section header

Il section header è una struttura secondaria dei file ELF eseguibili (primaria per i file .o), come ribadito più volte, i section header non sono necessari al funzionamento di un ELF quando abbiamo a che fare con eseguibili/librerie, mentre sono fondamentali quando abbiamo a che fare con file oggetto (.o), in cui descrivono il contenuto del file (nel caso dei file .o non è necessario invece il program header visto che questi file non vengono mappati in memoria, ma sono usati durante il processo di link statico per creare un eseguibile/libreria). Come nel caso del program header, il campo e_shoff dell'header ELF corrisponde all'offset, dall'inizio del file, in cui si trova un array di strutture section header la cui dimensione è data da e_shnum. Vediamo la struttura:

typedef struct
{
  Elf32_Word    sh_name;        /* Section name (string tbl index) */
  Elf32_Word    sh_type;        /* Section type */
  Elf32_Word    sh_flags;        /* Section flags */
  Elf32_Addr    sh_addr;        /* Section virtual addr at execution */
  Elf32_Off    sh_offset;        /* Section file offset */
  Elf32_Word    sh_size;        /* Section size in bytes */
  Elf32_Word    sh_link;        /* Link to another section */
  Elf32_Word    sh_info;        /* Additional section information */
  Elf32_Word    sh_addralign;        /* Section alignment */
  Elf32_Word    sh_entsize;        /* Entry size if section holds table */
} Elf32_Shdr;
  • sh_name: è un indice all'interno di una string table che da il nome della seizione, nel paragrafo successivo vedremo brevemente cosa sono le string table. Per ora pensatelo come l'indice in una zona di memoria fatta così: "\0nome1\0nome2\0nome3\0\0".
  • sh_type: il tipo della sezione, ce ne sono diversi, date un'occhiata al documento sull' ELF. Brevemente:
    • PROGBITS: la sezione contiene dei dati la cui interpretazione è a discrezione del programma (tipicamente codice, note, dati globali, ecc..., esempi sono le sezioni .text, .data, .plt, .init, .fini, ecc...), i dati sono presenti sul file.
    • NOBITS: come prima, ma questa volta i dati non occupano spazio sul file, un esempio tipico è la sezione .bss, che contiene dati NON initializzati, quindi non occupano spazio sul file.
    • SHT_DYNAMIC: la sezione contiene informazioni per il link dinamic (verrà spiegato più avanti).
    • SHT_SYMTAB: la sezione contiene una symbol table, verrà spiegata più avanti.
    • SHT_DYNSYM: la sezione contiene una symbol table in cui compaiono solo simboli utili al link dinamico.
    • SHT_STRTAB: la sezione contiene una string table, anche questa verrà spiegata più avanti.
    • SHT_REL: la sezione contiene informazioni per rilocare alcuni simboli nell' ELF.
    • SHT_RELA: come sopra, ma questo tipo di sezione NON è mai presente in eseguibili/librerie x86, che quindi usano rilocazioni solo di tipo REL e non RELA (vedremo la differenza).
  • sh_flags: flag della sezione, una bitmask fatta dai soliti write, execute, ecc... vedete il manuale.
  • sh_addr: virtual address a cui si troverà la sezione mappata in memoria (sarà un virtual address contenuto in un segmento ovviamente).
  • sh_offset: offset dall'inizio del file in cui si trova la sezione, ovviamente ha significato solo quando stiamo leggendo da file.
  • sh_size: dimensione totale della sezione, in byte. Questa rappresenta la dimensione fisica della sezione, per le sezioni non c'è una dimensione in memoria, perché la sezione in memoria occuperà, eventualmente insieme ad altre sezioni, un segmento (descritto dal suo program header).
  • sh_link: a seconda del tipo di sezione, questo campo contiene informazioni su un'altra sezione che in qualche modo è in relazione con quella che stiamo vedendo. Maggiori dettagli sul solito manuale, comunque nel caso si tratti di una sezione di tipo SHT_SYMTAB o SHT_DYNSYM, il campo sh_link contiene l'indice della sezione che contriene la string table associata alla symbol table.
  • sh_info: informazioni addizionali, dipende dal tipo di sezione.
  • sh_entsize: se la sezione contiene una tabella, questo campo ci darà la dimension di un singolo elemento della tabella. Quindi la dimensione di un elemento dell'array contenuto nella sezione.

Vediamo l'output di readelf sul solito eseguibile:

quake2@quake2-desktop:~$ readelf -S /bin/ls
There are 28 section headers, starting at offset 0x17378:

Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 08048154 000154 000013 00 A 0 0 1
[ 2] .note.ABI-tag NOTE 08048168 000168 000020 00 A 0 0 4
[ 3] .hash HASH 08048188 000188 000330 04 A 5 0 4
[ 4] .gnu.hash GNU_HASH 080484b8 0004b8 00005c 04 A 5 0 4
[ 5] .dynsym DYNSYM 08048514 000514 000690 10 A 6 1 4
[ 6] .dynstr STRTAB 08048ba4 000ba4 0004af 00 A 0 0 1
[ 7] .gnu.version VERSYM 08049054 001054 0000d2 02 A 5 0 2
[ 8] .gnu.version_r VERNEED 08049128 001128 0000d0 00 A 6 3 4
[ 9] .rel.dyn REL 080491f8 0011f8 000028 08 A 5 0 4
[10] .rel.plt REL 08049220 001220 0002e8 08 A 5 12 4
[11] .init PROGBITS 08049508 001508 000030 00 AX 0 0 4
[12] .plt PROGBITS 08049538 001538 0005e0 04 AX 0 0 4
[13] .text PROGBITS 08049b20 001b20 01145c 00 AX 0 0 16
[14] .fini PROGBITS 0805af7c 012f7c 00001c 00 AX 0 0 4
[15] .rodata PROGBITS 0805afa0 012fa0 003e4c 00 A 0 0 32
[16] .eh_frame_hdr PROGBITS 0805edec 016dec 00002c 00 A 0 0 4
[17] .eh_frame PROGBITS 0805ee18 016e18 00009c 00 A 0 0 4
[18] .ctors PROGBITS 0805fef0 016ef0 000008 00 WA 0 0 4
[19] .dtors PROGBITS 0805fef8 016ef8 000008 00 WA 0 0 4
[20] .jcr PROGBITS 0805ff00 016f00 000004 00 WA 0 0 4
[21] .dynamic DYNAMIC 0805ff04 016f04 0000e8 08 WA 6 0 4
[22] .got PROGBITS 0805ffec 016fec 000008 04 WA 0 0 4
[23] .got.plt PROGBITS 0805fff4 016ff4 000180 04 WA 0 0 4
[24] .data PROGBITS 08060180 017180 000110 00 WA 0 0 32
[25] .bss NOBITS 080602a0 017290 00046c 00 WA 0 0 32
[26] .gnu_debuglink PROGBITS 00000000 017290 000008 00 0 0 1
[27] .shstrtab STRTAB 00000000 017298 0000df 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)

come avevo detto in precedenza, un ELF può contenere svariati section header che, come abbiamo visto nel paragrafo dedicato al program header, verranno raggruppati in vari segmenti. Le sezioni che hanno il campo sh_addr uguale a 0, non verranno mappate in memoria, infatti se date un'occhiata al raggruppamento nei segmenti, noterete che le sezioni che hanno sh_addr nullo non compaiono. Di nuovo, ciò che viene mappato in memoria dipende SOLO ED ESCLUSIVAMENTE dai program header. Inoltre come vi avevo accennato, possiamo vedere che abbiamo solo rilocazioni di tipo REL, per le sezioni ".rel.dyn" e ".rel.plt". Notiamo anche il fatto che possiamo avere più di una string table, in questo caso due: una mappata in memoria (quella relativa alla sezione ".dynstr"), l'altra no (quella relativa a ".shstrtab"). Cerchiamo di capire perché: diamo un'occhiata all'header ELF, notiamo che il campo e_shstrndx vale 27, che è proprio l'indice della string table che non è mappata in memoria, questo significa che la string table contenente i nomi delle sezioni non è importante una volta caricato il file, quindi può essere scartata senza problemi. L'altra string table invece, ".dynstr", contiene i nomi dei simboli per il link dinamico, questa NON può essere scartata, perché serve al linker dinamico per risolvere i simboli importati da altri moduli, anche molto tempo dopo che il programma è stato caricato (lazy binding, vedremo in seguito cos'è). Potendo quindi separare le string table, otteniamo un risparmio di memoria che per quanto insignificante oggi comunque non fa mai male.

String table

Le string table non sono una vera e propria struttura, ma piuttosto sono delle sezioni all'interno del file, che contengono un array di stringhe. C'è ben poco da dire su di esse, l'unica nota è che TUTTI i campi nelle strutture di un ELF che hanno a che fare con un nome, in realtà contengono un indice dentro una string table. Quale string table? Ce lo dice il contesto: ad esempio l'ultimo campo dell'header è un indice nella tabella delle sezioni e ci dà la sezione che contiene la string table con il nome delle sezioni. In altri casi vedremo che quando serve, ci sarà modo di risalire alla string table necessaria. Oltre a questo, non c'è proprio nient'altro, per completezza vi posto un dump della string table col nome delle sezioni, così vi fate un idea di come è organizzata di solito:

quake2@quake2-desktop:~$ readelf -x 27 /bin/ls

Hex dump of section '.shstrtab':
0x00000000 002e7368 73747274 6162002e 696e7465 ..shstrtab..inte
0x00000010 7270002e 6e6f7465 2e414249 2d746167 rp..note.ABI-tag
0x00000020 002e676e 752e6861 7368002e 64796e73 ..gnu.hash..dyns
0x00000030 796d002e 64796e73 7472002e 676e752e ym..dynstr..gnu.
0x00000040 76657273 696f6e00 2e676e75 2e766572 version..gnu.ver
0x00000050 73696f6e 5f72002e 72656c2e 64796e00 sion_r..rel.dyn.
0x00000060 2e72656c 2e706c74 002e696e 6974002e .rel.plt..init..
0x00000070 74657874 002e6669 6e69002e 726f6461 text..fini..roda
0x00000080 7461002e 65685f66 72616d65 5f686472 ta..eh_frame_hdr
0x00000090 002e6568 5f667261 6d65002e 63746f72 ..eh_frame..ctor
0x000000a0 73002e64 746f7273 002e6a63 72002e64 s..dtors..jcr..d
0x000000b0 796e616d 6963002e 676f7400 2e676f74 ynamic..got..got
0x000000c0 2e706c74 002e6461 7461002e 62737300 .plt..data..bss.
0x000000d0 2e676e75 5f646562 75676c69 6e6b00 .gnu_debuglink.

sulla prima colonna c'è l'offset (dall'inizio della sezione), nelle altre colonne il valore in hex. Come vedete la tabella inizia e finisce con 0x00.

Un po' di codice...

I tempi sono maturi per vedere un po' di codice che lavora con gli ELF, visto che abbiamo messo abbastanza carne al fuoco. Per partire, vediamo un sorgente che apre un ELF e ci mostra l'header:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <elf.h>

int elf_is_valid(Elf32_Ehdr *elf_hdr)
{
    if( (elf_hdr->e_ident[EI_MAG0] != 0x7F) ||
        (elf_hdr->e_ident[EI_MAG1] != 'E') ||
        (elf_hdr->e_ident[EI_MAG2] != 'L') ||
        (elf_hdr->e_ident[EI_MAG3] != 'F') )
    {
         return 0;
    }

    if(elf_hdr->e_ident[EI_CLASS] != ELFCLASS32)
        return 0;

    if(elf_hdr->e_ident[EI_DATA] != ELFDATA2LSB)
        return 0;

    return 1;
}

static char *elf_types[] = {
    "ET_NONE",
    "ET_REL",
    "ET_EXEC",
    "ET_DYN",
    "ET_CORE",
    "ET_NUM"
};

char *get_elf_type(Elf32_Ehdr *elf_hdr)
{
    if(elf_hdr->e_type > 5)
        return NULL;

    return elf_types[elf_hdr->e_type];
}

int print_elf_header(Elf32_Ehdr *elf_hdr)
{
    char *sz_elf_type = NULL;

    if(!elf_hdr)
        return 0;

    printf("ELF header information\n");

    sz_elf_type = get_elf_type(elf_hdr);
    if(sz_elf_type)
        printf("- Type: %s\n", sz_elf_type);
    else
        printf("- Type: %04x\n", elf_hdr->e_type);

    printf("- Version: %d\n", elf_hdr->e_version);
    printf("- Entrypoint: 0x%08x\n", elf_hdr->e_entry);
    printf("- Program header table offset: 0x%08x\n", elf_hdr->e_phoff);
    printf("- Section header table offset: 0x%08x\n", elf_hdr->e_shoff);
    printf("- Flags: 0x%08x\n", elf_hdr->e_flags);
    printf("- ELF header size: %d\n", elf_hdr->e_ehsize);
    printf("- Program header size: %d\n", elf_hdr->e_phentsize);
    printf("- Program header entries: %d\n", elf_hdr->e_phnum);
    printf("- Section header size: %d\n", elf_hdr->e_shentsize);
    printf("- Section header entries: %d\n", elf_hdr->e_shnum);
    printf("- Section string table index: %d\n", elf_hdr->e_shstrndx);

    return 1;
}

int main(int argc, char *argv[])
{
    int fd_elf = -1;
    u_char *p_base = NULL;
    struct stat elf_stat;
    Elf32_Ehdr *p_ehdr = NULL;

    if(argc < 2)
    {
        printf("Usage: %s </path/to/file>\n", argv[0]);
        return 1;
    }

    fd_elf = open(argv[1], O_RDONLY);
    if(fd_elf == -1)
    {
        fprintf(stderr, "Could not open %s: %s\n", argv[1], strerror(errno));
        return 1;
    }

    if(fstat(fd_elf, &elf_stat) == -1)
    {
        fprintf(stderr, "Could not stat %s: %s\n", argv[1], strerror(errno));
        close(fd_elf);
        return 1;
    }

    p_base = (u_char *)calloc(sizeof(u_char), elf_stat.st_size);
    if(!p_base)
    {
        fprintf(stderr, "Not enough memory\n");
        close(fd_elf);
        return 1;
    }

    if(read(fd_elf, p_base, elf_stat.st_size) != elf_stat.st_size)
    {
        fprintf(stderr, "Error while reading file: %s\n", strerror(errno));
        free(p_base);
        close(fd_elf);
        return 1;
    }
   
    close(fd_elf);

    p_ehdr = (Elf32_Ehdr *)p_base;
    if(elf_is_valid(p_ehdr))
        print_elf_header(p_ehdr);
    else
        fprintf(stderr, "Invalid ELF file\n");

    free(p_base);
    return 0;
}

il codice è abbastanza semplice e non dovreste avere problemi a capirlo anche senza commenti. Come vedete basta aprire il file, leggerlo e semplicemente l'header si trova all'inizio, quindi basta dichiarare un puntatore Elf32_Ehdr che punta all'inizio del buffer che contiene il file. Date un'occhiata alla funzione elf_is_valid che controlla i campi che vi avevo detto in precedenza per stabilire se è un ELF valido oppure no.

Vediamo ora un sorgente che mostra la tabella dei program header e quella dei section header, che quindi mostrerà anche come si usano le string table (nel codice mostro solo le funzioni aggiuntive a quello precedente e il nuovo main, trovate il sorgente completo nell'allegato al tutorial):

static char *ptypes[] = {
        "PT_NULL",
        "PT_LOAD",
        "PT_DYNAMIC",
        "PT_INTERP",
        "PT_NOTE",
        "PT_SHLIB",
        "PT_PHDR"
};

int print_program_header(Elf32_Phdr *phdr, uint index)
{
    if(!phdr)
        return 0;

    printf("Program header %d\n", index);
    if(phdr->p_type <= 6)
        printf("- Type: %s\n", ptypes[phdr->p_type]);
    else
        printf("- Type: %08x\n", phdr->p_type);

    printf("- Offset: %08x\n", phdr->p_offset);
    printf("- Virtual Address: %08x\n", phdr->p_vaddr);
    printf("- Physical Address: %08x\n", phdr->p_paddr);
    printf("- File size: %d\n", phdr->p_filesz);
    printf("- Memory size: %d\n", phdr->p_memsz);
    printf("- Flags: %08x\n", phdr->p_flags);
    printf("- Alignment: %08x\n", phdr->p_align);
}

static char *stypes[] = {
        "SHT_NULL",
        "SHT_PROGBITS",
        "SHT_SYMTAB",
        "SHT_STRTAB",
        "SHT_RELA",
        "SHT_HASH",
        "SHT_DYNAMIC",
        "SHT_NOTE",
        "SHT_NOBITS",
        "SHT_REL",
        "SHT_SHLIB",
        "SHT_DYNSYM"
};

int print_section_header(Elf32_Shdr *shdr, uint index, char *strtable)
{
    if(!shdr)
        return 0;

    printf("Section header: %d\n", index);
    printf("- Name index: %d\n", shdr->sh_name);
   
    //come vedete, per prendere il nome basta utilizzare il campo sh_name come indice
    //all'interno della string table
    printf("- Name: %s\n", strtable + shdr->sh_name);
    if(shdr->sh_type <= 11)
        printf("- Type: %s\n", stypes[shdr->sh_type]);
    else
        printf("- Type: %04x\n", shdr->sh_type);
    printf("- Flags: %08x\n", shdr->sh_flags);
    printf("- Address: %08x\n", shdr->sh_addr);
    printf("- Offset: %08x\n", shdr->sh_offset);
    printf("- Size: %08x\n", shdr->sh_size);
    printf("- Link %08x\n", shdr->sh_link);
    printf("- Info: %08x\n", shdr->sh_info);
    printf("- Address alignment: %08x\n", shdr->sh_addralign);
    printf("- Entry size: %08x\n", shdr->sh_entsize);

}

int main(int argc, char *argv[])
{
    int fd_elf = -1;
    u_char *p_base = NULL;
    char *p_strtable = NULL;
    struct stat elf_stat;
    Elf32_Ehdr *p_ehdr = NULL;
    Elf32_Phdr *p_phdr = NULL;
    Elf32_Shdr *p_shdr = NULL;
    int i;

    if(argc < 2)
    {
        printf("Usage: %s </path/to/file>\n", argv[0]);
        return 1;
    }

    fd_elf = open(argv[1], O_RDONLY);
    if(fd_elf == -1)
    {
        fprintf(stderr, "Could not open %s: %s\n", argv[1], strerror(errno));
        return 1;
    }

    if(fstat(fd_elf, &elf_stat) == -1)
    {
        fprintf(stderr, "Could not stat %s: %s\n", argv[1], strerror(errno));
        close(fd_elf);
        return 1;
    }

    p_base = (u_char *)calloc(sizeof(u_char), elf_stat.st_size);
    if(!p_base)
    {
        fprintf(stderr, "Not enough memory\n");
        close(fd_elf);
        return 1;
    }

    if(read(fd_elf, p_base, elf_stat.st_size) != elf_stat.st_size)
    {
        fprintf(stderr, "Error while reading file: %s\n", strerror(errno));
        free(p_base);
        close(fd_elf);
        return 1;
    }
   
    close(fd_elf);

    p_ehdr = (Elf32_Ehdr *)p_base;
    if(elf_is_valid(p_ehdr))
    {
        print_elf_header(p_ehdr);

        printf("\n");
       
        //come detto nella descrizione dell'header ELF, i campi e_phoff e e_shoff
        //sono dei semplici offset fisici, quindi basta sommarli a p_base (che punta all'inizio
        //del buffer) per ottenere l'indirizzo rispettivamente della tabella dei program header
        //e di quella dei section header
        p_phdr = (Elf32_Phdr *)(p_base + p_ehdr->e_phoff);
        p_shdr = (Elf32_Shdr *)(p_base + p_ehdr->e_shoff);
       
        //siccome vogliamo stampare i nomi delle sezioni, abbiamo bisogno della string table
        //che li contiene, l'indice all'interno della tabella è dato dal campo e_shstrndx
        //dell'header ELF, inoltre abbiamo visto che sh_offset è l'offset dall'inizio del file
        //quindi basterà sommare questo valore (relativo alla sezione con la string table) a p_base
        //per ottenere un puntatore alla string table con i nomi delle sezioni
        p_strtable = (char *)(p_base + p_shdr[p_ehdr->e_shstrndx].sh_offset);

        for(i = 0; i < p_ehdr->e_phnum; i++)
        {
            print_program_header(&p_phdr[i], i);
        }

        for(i = 0; i < p_ehdr->e_shnum; i++)
        {
            print_section_header(&p_shdr[i], i, p_strtable);
        }
    }
    else
        printf("Invalid ELF file\n");

    free(p_base);
    return 0;
}

ho commentato i punti importanti, ma non è niente di complesso comunque, se volete qualche chiarimento su alcuni campi delle strutture mi raccomando fate riferimento ai manuali dell' ELF, sono chiarissimi e spiegano tutto (ma proprio tutto) quello che c'è da sapere.

Symbol table

La symbol table è un'altra struttura fondamentale degli ELF. Come dice il nome, si tratta di una tabella di simboli: ogni elemento della tabella contiene il nome del simbolo (ricordate un nome è in realtà un indice in una string table), la visibilità e il comportamento in fase di linking (ad esempio se il simbolo è locale ad un file oggetto, oppure è globale), il tipo di simbolo, ecc... . Il manuale dell' ELF dà una definizione molto semplice ed elegante: la tabella dei simboli di un file contiene informazioni necessarie a localizzare e rilocare i riferimenti simbolici. Cerchiamo di capire meglio con un anticipazione del dynamic linking: un eseguibile ha bisogno di utilizzare una funzione definita in una libreria (so), quindi creerà una entry nella symbol table, che conterrà il nome di questa funzione con valore del simbolo pari a 0 e una entry nella relocation table che conterrà informazioni aggiuntive su come ottenere l'indirizzo della funzione; la libreria avrà nella sua symbol table un simbolo col nome della funzione E il valore del simbolo (che NON è il nome, attenzione) sarà l'entry point della funzione. In questo modo il linker dinamico è in grado di "risolvere" un simbolo che fa riferimento ad una funzione esterna.

Vediamo la struttura di una entry nella symbol table:

typedef struct
{
  Elf32_Word    st_name;        /* Symbol name (string tbl index) */
  Elf32_Addr    st_value;        /* Symbol value */
  Elf32_Word    st_size;        /* Symbol size */
  unsigned char    st_info;        /* Symbol type and binding */
  unsigned char    st_other;        /* Symbol visibility */
  Elf32_Section    st_shndx;        /* Section index */
} Elf32_Sym;
  • st_name: indice all'interno di una string table che contiene il nome del simbolo (può essere 0). La string table da utilizzare dipende come sempre dal contesto in cui siamo.
  • st_value: valore del simbolo, dipende dal tipo di simbolo che stiamo considerando: può essere un offset, un virtual address, ecc... .
  • st_size: dimensione del simbolo, ha significato solo per alcuni tipi di simbolo. Ad esempio se un simbolo è di tipo OBJECT, questo campo ci dà la dimensione del dato associato (un array ad esempio).
  • st_info: è un byte in cui un nibble ci dà il tipo di simbolo, l'altro ci dà informazioni sul binding (locale, globale, weak) del simbolo. Nel file elf.h sono definite delle macro per semplificare la gestione di questo campo:
    • ELF32_ST_BIND(i): ritorna il tipo di bind del simbolo
    • ELF32_ST_TYPE(i): ritorna il tipo del simbolo
    • ELF32_ST_INFO(b,t): ritorna un byte composto da b e t, rispettivamente binding type e symbol type
  • st_other: secondo il manuale ELF questo campo attualmente non ha significato, tuttavia su linux viene utilizzato per marcare la visibilità di un simbolo (gli unici valori che ho visto sono DEFAULT e HIDDEN).
  • st_shndx: sezione in cui si trova il simbolo descritto da questa entry, è un indice all'interno della tabella dei section header.

dovrebbe essere tutto chiaro sul funzionamento della symbol table, comunque ci sono veramente tante cose da dire e siccome questo non è un articolo sul formato ELF (altrimenti ci perdiamo troppo per strada), vi consiglio caldamente di leggere il manuale. Vediamo l'output di readelf (l'output è troncato sia all'inizio che alla fine, altrimenti occuperebbe troppo spazio):

quake2@quake2-desktop:~$ readelf -W -s /bin/ls

Symbol table '.dynsym' contains 105 entries:
Num: Value Size Type Bind Vis Ndx Name
[...]
23: 00000000 198 FUNC GLOBAL DEFAULT UND strncpy@GLIBC_2.0 (2)
24: 00000000 35 FUNC GLOBAL DEFAULT UND freecon
25: 00000000 88 FUNC GLOBAL DEFAULT UND memset@GLIBC_2.0 (2)
26: 00000000 441 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.0 (2)
27: 00000000 68 FUNC GLOBAL DEFAULT UND mempcpy@GLIBC_2.1 (5)
28: 00000000 80 FUNC GLOBAL DEFAULT UND __memcpy_chk@GLIBC_2.3.4 (6)
29: 00000000 186 FUNC GLOBAL DEFAULT UND _obstack_begin@GLIBC_2.0 (2)
30: 00000000 19 FUNC GLOBAL DEFAULT UND _exit@GLIBC_2.0 (2)
31: 00000000 441 FUNC GLOBAL DEFAULT UND strrchr@GLIBC_2.0 (2)
32: 00000000 336 FUNC GLOBAL DEFAULT UND __assert_fail@GLIBC_2.0 (2)
33: 00000000 29 FUNC GLOBAL DEFAULT UND bindtextdomain@GLIBC_2.0 (2)
34: 00000000 597 FUNC GLOBAL DEFAULT UND mbrtowc@GLIBC_2.0 (2)
35: 00000000 62 FUNC GLOBAL DEFAULT UND gettimeofday@GLIBC_2.0 (2)
36: 00000000 64 FUNC GLOBAL DEFAULT UND __ctype_toupper_loc@GLIBC_2.3 (7)
37: 00000000 69 FUNC GLOBAL DEFAULT UND __lxstat64@GLIBC_2.2 (3)
38: 00000000 446 FUNC GLOBAL DEFAULT UND _obstack_newchunk@GLIBC_2.0 (2)
39: 00000000 102 FUNC GLOBAL DEFAULT UND __overflow@GLIBC_2.0 (2)
40: 00000000 73 FUNC GLOBAL DEFAULT UND dcgettext@GLIBC_2.0 (2)
41: 00000000 100 FUNC GLOBAL DEFAULT UND sigaction@GLIBC_2.0 (2)
42: 00000000 351 FUNC GLOBAL DEFAULT UND strverscmp@GLIBC_2.1 (5)
43: 00000000 152 FUNC GLOBAL DEFAULT UND opendir@GLIBC_2.0 (2)
44: 00000000 71 FUNC GLOBAL DEFAULT UND getopt_long@GLIBC_2.0 (2)
45: 00000000 64 FUNC GLOBAL DEFAULT UND ioctl@GLIBC_2.0 (2)
46: 00000000 64 FUNC GLOBAL DEFAULT UND __ctype_b_loc@GLIBC_2.3 (7)
47: 00000000 226 FUNC GLOBAL DEFAULT UND iswcntrl@GLIBC_2.0 (2)
48: 00000000 50 FUNC GLOBAL DEFAULT UND isatty@GLIBC_2.0 (2)
49: 00000000 539 FUNC GLOBAL DEFAULT UND fclose@GLIBC_2.1 (5)
50: 00000000 25 FUNC GLOBAL DEFAULT UND mbsinit@GLIBC_2.0 (2)
51: 00000000 54 FUNC GLOBAL DEFAULT UND _setjmp@GLIBC_2.0 (2)
52: 00000000 56 FUNC GLOBAL DEFAULT UND tcgetpgrp@GLIBC_2.0 (2)
53: 00000000 60 FUNC GLOBAL DEFAULT UND mktime@GLIBC_2.0 (2)
54: 00000000 222 FUNC GLOBAL DEFAULT UND readdir64@GLIBC_2.2 (3)
55: 00000000 70 FUNC GLOBAL DEFAULT UND memcpy@GLIBC_2.0 (2)
56: 00000000 76 FUNC GLOBAL DEFAULT UND strtoul@GLIBC_2.0 (2)
57: 00000000 175 FUNC GLOBAL DEFAULT UND strlen@GLIBC_2.0 (2)
58: 00000000 299 FUNC GLOBAL DEFAULT UND getpwuid@GLIBC_2.0 (2)
59: 00000000 186 FUNC GLOBAL DEFAULT UND acl_extended_file@ACL_1.0 (8)
60: 00000000 1931 FUNC GLOBAL DEFAULT UND setlocale@GLIBC_2.0 (2)
61: 00000000 37 FUNC GLOBAL DEFAULT UND strcpy@GLIBC_2.0 (2)
62: 00000000 148 FUNC GLOBAL DEFAULT UND raise@GLIBC_2.0 (2)
63: 00000000 178 FUNC GLOBAL DEFAULT UND fwrite_unlocked@GLIBC_2.1 (5)
64: 00000000 293 FUNC GLOBAL DEFAULT UND clock_gettime@GLIBC_2.2 (9)
65: 00000000 123 FUNC GLOBAL DEFAULT UND getfilecon
66: 00000000 98 FUNC GLOBAL DEFAULT UND closedir@GLIBC_2.0 (2)
67: 00000000 403 FUNC GLOBAL DEFAULT UND fwrite@GLIBC_2.0 (2)
68: 00000000 174 FUNC GLOBAL DEFAULT UND sigprocmask@GLIBC_2.0 (2)
69: 00000000 32 FUNC GLOBAL DEFAULT UND __stack_chk_fail@GLIBC_2.4 (10)
70: 00000000 42 FUNC GLOBAL DEFAULT UND __fpending@GLIBC_2.2 (3)
71: 00000000 123 FUNC GLOBAL DEFAULT UND lgetfilecon
72: 00000000 223 FUNC GLOBAL DEFAULT UND error@GLIBC_2.0 (2)
73: 00000000 299 FUNC GLOBAL DEFAULT UND getgrgid@GLIBC_2.0 (2)
74: 00000000 75 FUNC GLOBAL DEFAULT UND __strtoull_internal@GLIBC_2.0 (2)
75: 00000000 115 FUNC GLOBAL DEFAULT UND sigaddset@GLIBC_2.0 (2)
[...]

come vedete l'eseguibile /bin/ls contiene solo la symbol table per i simboli dinamici, cioè quelli che hanno bisogno di essere risolti dal linker dinamico. Il formato di questa symbol table tuttavia è assolutamente identico agli altri. Fate attenzione che molti simboli hanno "@GLIBC_2.0" oppure "@GLIBC_2.2" o ancora "@GLIBC_2.1" o altro, questo NON FA parte del nome del simbolo, ma è un'estensione GNU al formato, che permette di specificare quale versione deve essere utilizzata della libreria, è aggiunto da readelf leggendo le altre sezioni, date un'occhiata al sorgente di readelf se volete vedere come ottenere un risultato analogo, per i nostri scopi non è importante.

Relocation table

Si tratta di una serie di elementi che descrivono come vanno rilocate alcune parti di codice/dati. Cerchiamo di capire cos'è la rilocazione: alcune parti del codice fanno riferimento a variabili e/o funzioni, che si trovano in altri punti del codice, tramite l'utilizzo di un indirizzo assoluto. Ora finché si tratta di un eseguibile, non è un problema, dato che gli eseguibili sono caricati sempre alla stessa posizione, quindi non c'è bisogno di risolvere i riferimenti. Ma facciamo il caso di una libreria, la posizione a cui viene caricata cambia, quindi in tutti i punti in cui si fa riferimento ad un indirizzo assoluto, bisogna applicare una rilocazione (che consiste nel modificare quell'indirizzo in determinati modi, affinché sia effettivamente l'indirizzo dell'oggetto interessato). Esistono diversi tipi di rilocazioni e dipendono dall'architettura, noi ci limiteremo a quelle usate su x86. Come accennato all'inizio, ci sono due grosse categorie di rilocazioni, quelle di tipo REL e quelle di tipo RELA, l'unica differenza tra questi due tipi è che quelle di tipo REL hanno l'addendo implicito (che corrisponde al valore che andiamo a rilocare), mentre quelle di tipo RELA lo hanno esplicito nella struttura. Nel caso di un architettura x86, si hanno solo rilocazioni di tipo REL, per x64 invece abbiamo solo rilocazioni di tipo RELA, ma in questo articolo siamo interessati solo ad un'architettura di tipo x86. Vediamo quindi la struttura per le rilocazioni di tipo REL:

typedef struct
{
  Elf32_Addr    r_offset;        /* Address */
  Elf32_Word    r_info;            /* Relocation type and symbol index */
} Elf32_Rel;
  • r_offset: indirizzo a cui va applicata la rilocazione.
  • r_info: informazioni aggiuntive, in particolare il tipo di rilocazione e, a seconda del tipo, un eventuale indice in una symbol table

la struttura è molto semplice, ma contiene tutto ciò che è necessario sapere. Noi siamo interessati ad un particolare tipo di rilocazione, che verrà descritto in dettaglio nel capitolo successivo, per ora limitiamoci a dire che si tratta del tipo R_386_JMP_SLOT, che permette di descrivere una rilocazione per un simbolo importato da un'altra libreria.

Vediamo un esempio pratico di come funzionano le rilocazioni. Prendiamo una libreria qualsiasi (è difficile trovare rilocazioni nel codice dentro un eseguibile, apparte quelle per il link dinamico), ad esempio libhook.so (che sarà la libreria che faremo nella parte finale dell'articolo), e vediamo qualche rilocazione:

quake2@quake2-desktop:~/elfinj$ readelf -r libhook.so

Relocation section '.rel.dyn' at offset 0x3b4 contains 49 entries:
Offset Info Type Sym.Value Sym. Name
00000679 00000008 R_386_RELATIVE
0000069d 00000008 R_386_RELATIVE
000006b8 00000008 R_386_RELATIVE
000006c5 00000008 R_386_RELATIVE
000006ca 00000008 R_386_RELATIVE

prima di tutto vediamo il tipo di rilocazione, cioè R_386_RELATIVE, prendete quindi il manuale e andate alla descrizione di questo tipo:

R_386_RELATIVE The link editor creates this relocation type for dynamic link-
ing. Its offset member gives a location within a shared object
that contains a value representing a relative address. The
dynamic linker computes the corresponding virtual address
by adding the virtual address at which the shared object was
loaded to the relative address. Relocation entries for this type
must specify 0 for the symbol table index.

quindi il campo r_offset ci dice a che indirizzo applicare la rilocazione, Scegliamo quella con offset 0x000006b8, si tratta di codice:

6b5: c7 04 24 81 0a 00 00 movl $0xa81,(%esp)
6bc: e8 fc ff ff ff call 6bd <InitHookLib+0x2d>

l'istruzione che ci interessa è quella a 0x6b5, che scritta con una sintassi civile diventa "mov [esp], 0x00000A81". All'offset 0x6b8 abbiamo la dword (sto usando la terminologia intel) 0x00000A81, quindi se supponiamo che la nostra libreria sia caricata all'indirizzo 0xB77F0000, dovremmo sommare questo base address al valore da rilocare: il valore definitivo sarà quindi 0xB77F0A81. Tutto questo ovviamente lo fa il linker dinamico che si occcupa di preparare l'immagine in memoria della libreria. Per la piattaforma x86 sono definiti altri tipi di rilocazioni, la lista completa la trovate sul manuale, però più o meno dovreste esservi fatti un idea su come funzionano.

Dynamic section

L'ultima struttura di cui parleremo è la dynamic section. Si tratta di una sezione all'interno dell' ELF che contiene informazioni per il linker dinamico, in particolare come recuperare i simboli dinamici, i nomi delle librerie in cui cercarli, le rilocazioni per questi simboli, ecc... . Di solito la dynamic section si trova in una sezione a parte, il cui tipo è SHT_DYNAMIC, mentre una volta caricato l' ELF in memoria, possiamo risalire alla dynamic section prendendo il program header di tipo PT_DYNAMIC. Inutile dire che anche in questo caso si tratta di una tabella formata da varie entry, terminata da un entry nulla, vediamo il formato di un entry:

typedef struct
{
  Elf32_Sword    d_tag;            /* Dynamic entry type */
  union
    {
      Elf32_Word d_val;            /* Integer value */
      Elf32_Addr d_ptr;            /* Address value */
    } d_un;
} Elf32_Dyn;
  • d_tag: il tipo di entry, noi saremo interessati prevalentemente alle entry di tipo DT_PLTRELSZ, DT_SYMTAB, DT_STRTAB, DT_REL e DT_JMPREL, quindi fate maggiore attenzione a questi tipi vedendo il manuale. In particolare notate come abbiamo due entry che ci permettono di recuperare la symbol table e la string table per i simboli dinamici, infatti se ricordate vi avevo avvertito che la string table sarà sempre possibile recuperarla dal contesto in cui ci troviamo. La dynamic section termina con un entry di tipo DT_NULL.
  • d_val/d_ptr: il valore dell'entry, che potrà essere un intero senza segno a 32bit oppure un indirizzo (come si vede dalla union).

Vediamo la dynamic section di /bin/ls:

quake2@quake2-desktop:~$ readelf -d /bin/ls

Dynamic section at offset 0x16f04 contains 24 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [librt.so.1]
0x00000001 (NEEDED) Shared library: [libselinux.so.1]
0x00000001 (NEEDED) Shared library: [libacl.so.1]
0x00000001 (NEEDED) Shared library: [libc.so.6]
0x0000000c (INIT) 0x8049508
0x0000000d (FINI) 0x805af7c
0x00000004 (HASH) 0x8048188
0x6ffffef5 (GNU_HASH) 0x80484b8
0x00000005 (STRTAB) 0x8048ba4
0x00000006 (SYMTAB) 0x8048514
0x0000000a (STRSZ) 1199 (bytes)
0x0000000b (SYMENT) 16 (bytes)
0x00000015 (DEBUG) 0x0
0x00000003 (PLTGOT) 0x805fff4
0x00000002 (PLTRELSZ) 744 (bytes)
0x00000014 (PLTREL) REL
0x00000017 (JMPREL) 0x8049220
0x00000011 (REL) 0x80491f8
0x00000012 (RELSZ) 40 (bytes)
0x00000013 (RELENT) 8 (bytes)
0x6ffffffe (VERNEED) 0x8049128
0x6fffffff (VERNEEDNUM) 3
0x6ffffff0 (VERSYM) 0x8049054
0x00000000 (NULL) 0x0

questa dynamic section ci sta dicendo che l'eseguibile /bin/ls necessita delle librerie librt.so.1, libselinux.so.1, libacl.so.1 e libc.so.6 per essere caricato, inoltre potete vedere le informazioni sui simboli dinamici (SYMTAB) e la string table associata (STRTAB). In questo caso bisogna notare che sia le informazioni sulla symbol table che quelle sulla string table appaiono come indirizzi, questo perché la dynamic section serve anche dopo che è stato caricato un ELF e una volta caricato le sezioni perdono di significato, di conseguenza non avrebbe senso parlare di indici nella tabella dei section header. Gli altri tipi vedeteli sul manuale per chiarirvi un po' le idee. Come accennato la tabella termina con un entry di tipo DT_NULL.

Gli hash

Se scorriamo l'elenco delle sezioni, vedremo che alcune di esse sono di tipo SHT_HASH. Questo tipo sta indicando che la sezione contiene una hash table. Le hash table sono usate per fare una ricerca rapida nei nomi dei simboli. In questa serie di articoli non ci preoccuperemo delle hash table, se non per una proprietà particolare della struttura: il secondo campo della struttura che descrive un hash table (nchains), guardate la Figura 5-11 a pagina 94 del manuale generico, ci dice quanti simboli ci sono nella symbol table che stiamo considerando. Questo ci tornerà molto utile nella ricerca dei simboli dinamici, perché se fate caso alla sezione dynamic, non c'è nessun campo che vi dà la dimensione della symbol table (SYMENT è la dimensione di una singola entry), quindi come facciamo a sapere quando smettere di cercare? Semplice, il numero di simboli ce lo dice l'hash table. Questa è l'unica cosa che dovete sapere dell'hash table, il resto non ci interessa.

Ancora un po' di codice...

Siamo arrivati alla fine di questa breve analisi delle strutture fondamentali dell' ELF, possiamo quindi dedicarci a vedere un po' di codice che mostra come lavorare con le nuove strutture introdotte, è tutto molto simile a quello che è già stato fatto, inoltre le funzioni vanno semplicemente aggiunte al vecchio sorgente, quindi, apparte il main, non riporterò il sorgente già scritto in precedenza.

Partiamo col codice che mostra le symbol table:

static char *btypes[] = {
        "STB_LOCAL",
        "STB_GLOBAL",
        "STB_WEAK"
};

static char *symtypes[] = {
        "STT_NOTYPE",
        "STT_OBJECT",
        "STT_FUNC",
        "STT_SECTION",
        "STT_FILE"
};

void print_bind_type(u_char info)
{
    u_char bind = ELF32_ST_BIND(info);
    if(bind <= 2)
        printf("- Bind type: %s\n", btypes[bind]);
    else
        printf("- Bind type: %d\n", bind);
}

void print_sym_type(u_char info)
{
    u_char type = ELF32_ST_TYPE(info);

    if(type <= 4)
        printf("- Symbol type: %s\n", symtypes[type]);
    else
        printf("- Symbol type: %d\n", type);
}

int print_sym_table(u_char *filebase, Elf32_Shdr *section, char *strtable)
{
    Elf32_Sym *symbols;
    size_t sym_size = section->sh_entsize;
    size_t cur_size = 0;

    if(section->sh_type == SHT_SYMTAB)
        printf("Symbol table\n");
    else
        printf("Dynamic symbol table\n");

    if(sym_size != sizeof(Elf32_Sym))
    {
        printf("There's something evil with symbol table...\n");
        return 0;
    }

    symbols = (Elf32_Sym *)(filebase + section->sh_offset);
    symbols++;
    cur_size += sym_size;
    do
    {
        printf("- Name index: %d\n", symbols->st_name);
        printf("- Name: %s\n", strtable + symbols->st_name);
        printf("- Value: 0x%08x\n", symbols->st_value);
        printf("- Size: 0x%08x\n", symbols->st_size);

        print_bind_type(symbols->st_info);
        print_sym_type(symbols->st_info);

        printf("- Section index: %d\n", symbols->st_shndx);
        cur_size += sym_size;
        symbols++;
    } while(cur_size < section->sh_size);

    return 1;
}

int main(int argc, char *argv[])
{
    int fd_elf = -1;
    u_char *p_base = NULL;
    char *p_strtable = NULL;
    struct stat elf_stat;
    Elf32_Ehdr *p_ehdr = NULL;
    Elf32_Phdr *p_phdr = NULL;
    Elf32_Shdr *p_shdr = NULL;
    int i;

    if(argc < 2)
    {
        printf("Usage: %s </path/to/file>\n", argv[0]);
        return 1;
    }

    fd_elf = open(argv[1], O_RDONLY);
    if(fd_elf == -1)
    {
        fprintf(stderr, "Could not open %s: %s\n", argv[1], strerror(errno));
        return 1;
    }

    if(fstat(fd_elf, &elf_stat) == -1)
    {
        fprintf(stderr, "Could not stat %s: %s\n", argv[1], strerror(errno));
        close(fd_elf);
        return 1;
    }

    p_base = (u_char *)calloc(sizeof(u_char), elf_stat.st_size);
    if(!p_base)
    {
        fprintf(stderr, "Not enough memory\n");
        close(fd_elf);
        return 1;
    }

    if(read(fd_elf, p_base, elf_stat.st_size) != elf_stat.st_size)
    {
        fprintf(stderr, "Error while reading file: %s\n", strerror(errno));
        free(p_base);
        close(fd_elf);
        return 1;
    }
   
    close(fd_elf);

    p_ehdr = (Elf32_Ehdr *)p_base;
    if(elf_is_valid(p_ehdr))
    {
        print_elf_header(p_ehdr);

        printf("\n");
        p_phdr = (Elf32_Phdr *)(p_base + p_ehdr->e_phoff);
        p_shdr = (Elf32_Shdr *)(p_base + p_ehdr->e_shoff);
        p_strtable = (char *)(p_base + p_shdr[p_ehdr->e_shstrndx].sh_offset);

        for(i = 0; i < p_ehdr->e_phnum; i++)
        {
            print_program_header(&p_phdr[i], i);
        }

        for(i = 0; i < p_ehdr->e_shnum; i++)
        {
            print_section_header(&p_shdr[i], i, p_strtable);
            if(p_shdr[i].sh_type == SHT_SYMTAB || p_shdr[i].sh_type == SHT_DYNSYM)
            {
                printf("This section holds a symbol table...\n");
                               
                            //trattandosi di una symbol table, il campo sh_link del section header
                            //conterra' l'indice della sezione che contiene la string table con i nomi dei simboli
                print_sym_table(p_base, &p_shdr[i], (char *)(p_base + p_shdr[p_shdr[i].sh_link].sh_offset));
            }
        }
    }
    else
        printf("Invalid ELF file\n");

    free(p_base);
    return 0;
}

il codice come sempre è molto semplice e dovrebbe essere chiaro, ho messo qualche commento per spiegare alcune cose introdotte nella descrizione delle strutture. A questo punto per correttezza dovrei mettere il codice che mostra la dynamic section, ma spero che ormai abbiate capito come si fa, quindi scrivetelo voi per esercizio.

Loading di un ELF

In questo capitolo vedremo brevemente il ruolo della varie componenti di un ELF durante il suo caricamento. Il sistema di riferimento sarà linux, come durante tutto l'articolo, tuttavia molte delle cose che dirò si applicano anche agli altri sistemi UNIX che utilizzano il formato ELF (non OSX quindi, che è si un sistema UNIX, ma usa il formato Mach-O). Ci soffermeremo in particolare sulla PLT (Procedure Linkage Table) che è la struttura che ci interessa principalmente.

Il ruolo dei program header

I program header fondamentalmente descrivono come mappare in memoria alcune sezioni del file, con i permessi indicati sulle pagine che veranno mappate. Esistono alcuni program header che hanno un ruolo speciale, in particolare:

PT_INTERP: questo program header punta ad una zona di memoria che solitamente contiene una stringa, questa stringa è il path assoluto di un file (che può anche essere un link simbolico) ad un programma/libreria che "collaborerà" al caricamento del file. Vediamo cosa c'è su linux:

quake2@quake2-desktop:~$ readelf -l /bin/ls
[...]
INTERP 0x000154 0x08048154 0x08048154 0x00013 0x00013 R 0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
[...]

quindi il program header sta richiedendo la collaborazione del programma /lib/ld-linux.so.2, che altro non è che il loader degli ELF su linux, come possiamo vedere eseguendo "/lib/ld-linux.so.2" (è si uno shared object, ma avendo un entry point valido è anche eseguibile):

quake2@quake2-desktop:~$ /lib/ld-linux.so.2
Usage: ld.so [OPTION]... EXECUTABLE-FILE [ARGS-FOR-PROGRAM...]
You have invoked `ld.so', the helper program for shared library executables.
This program usually lives in the file `/lib/ld.so', and special directives
in executable files using ELF shared libraries tell the system's program
loader to load the helper program from this file. This helper program loads
the shared libraries needed by the program executable, prepares the program
to run, and runs it. You may invoke this helper program directly from the
command line to load and run an ELF executable file; this is like executing
that file itself, but always uses this helper program from the file you
specified, instead of the helper program file specified in the executable
file you run. This is mostly of use for maintainers to test new versions
of this helper program; chances are you did not intend to run this program.

--list list all dependencies and how they are resolved
--verify verify that given object really is a dynamically linked
object we can handle
--library-path PATH use given PATH instead of content of the environment
variable LD_LIBRARY_PATH
--inhibit-rpath LIST ignore RUNPATH and RPATH information in object names
in LIST

niente di più chiaro, ci viene detto che ld.so è un loader di file ELF che collabora col sistema per creare un'immagine funzionante dell' ELF in memoria.

PT_LOAD: come abbiamo già detto, i segmenti di tipo PT_LOAD di solito contengono codice/dati e sono mappati in memoria secondo le indicazioni date nel program header.

PT_DYNAMIC: questo è un segmento importantissimo, dice al dynamic linker (ld.so di prima) come gestire i simboli dinamici, inoltre gli dà le informazioni necessarie per localizzare la symbol table, la string table, le rilocazioni, ecc... . Di solito il suo contenuto è lo stesso della dynamic section.

Il ruolo della dynamic section

La dynamic section contiene tutte le informazioni fondamentali per eseguire il link dinamico dell'eseguibile/libreria una volta che questo è stato caricato in memoria. In particolare contiene le informazioni necessarie alla rilocazione del file. Alcune entry della dynamic section hanno un significato particolare:

  • DT_JMPREL: contiene l'indirizzo di una tabella in cui sono presenti delle rilocazioni associate esclusivamente con la PLT, in questo modo se è attivo il lazy binding, il linker dinamico ignorerà queste rilocazioni durante il caricamento dell'immagine. Se la dynamic section contiene questa entry, allora deve contenere anche altre due entry:
    • DT_PLTREL: dice il tipo di rilocazioni per la PLT, nel caso di x86 come abbiamo detto sono valide solo rilocazioi di tipo REL.
    • DT_PLTRELSZ: la dimensione totale in byte della tabella delle rilocazioni associate alla PLT.
  • DT_NEEDED: una stringa che contiene il nome di una libreria richiesta per poter creare correttamente l'immagine del file. Si tratta ovviamente di un indice, all'interno della string table il cui indirizzo è dato da un entry di tipo DT_STRTAB nella dynamic section.
  • DT_INIT: contiene l'indirizzo della funzione da chiamare durante l'inizializzazione, prima del main (se è un eseguibile) oppure prima di passare il controllo all'eseguibile (se è una libreria).
  • DT_FINI: contiene l'indirizzo di una funzione da chiamare subito dopo il main nel caso di un eseguibile, oppure subito prima di eliminare l'immagine di una libreria. L'ordine di chiamata delle funzioni di "finalizzazione" (passatemi il termine) è esattamente l'inverso di quello delle funzioni di inizializzazione.

La PLT

La Procedure Linkage Table è una "tabella" costituita da codice, che permette di gestire i simboli importati da librerie esterne. Supponiamo che in un certo punto del codice, chiamate una funzione che si trova in un'altra liberia:

int main()
{
    int res = funzione_esterna(3, 4);
    return 0;
}

ora ovviamente voi non sapete qual'è l'indirizzo della funzione che state chiamando, essendo esterna al voltro file, per risolvere questo problema, si introduce la PLT, quindi il codice generato effettivamente sarà (situazione iniziale, ovvero PRIMA che venga chiamata la funzione):

push 0x04
push 0x03
call funzione_esterna@PLT
add esp, 8

[...]

funzione_esterna@PLT (indirizzo 0xXXXXXX00):
funzione_esterna@PLT+0x00: jmp dword ptr [reloc_address] ; reloc_address è una locazione di memoria
funzione_esterna@PLT+0x06: push reloc_offset ;è l'offset nella relocation table relativo a questa rilocazione
funzione_esterna@PLT+0x0B: jmp resolve_function ; una funzione che si occupa di risolvere la rilocazione con l'offset
 ; passato nello stack, ci pensa il linker


;reloc_address è una zona di memoria, il contenuto viene mostrato come array di dword,
;delle altre adiacenti non ci interessa, mostro dei ..

reloc_address: XXXXXX06 ........

cerchiamo di capire cosa succede. Quando il programma arriva alla call, si ritroverà all'indirizzo funzione_esterna@PLT+0x00 dove c'è un jmp al valore contenuto in reloc_address. All'inizio, dentro reloc_address c'è l'indirizzo dell'istruzione successiva al jmp, ovvero il push reloc_index, quindi verrà messo sullo stack un offset nella relocation table e poi ci sarà il jmp alla funzione che si occuperà di risolverla. Questa funzione non fa altro che prendere dallo stack il valore pushato (l'offset nella relocation table) e usarlo come indice nella zona di memoria puntata dalla entry di tipo DT_JMPREL nella dynamic section, da cui preleva il tipo di rilocazione (R_386_JMP_SLOT), le informazioni sul simbolo e passa il controllo al linker dinamico, che risolverà il simbolo e SOSTITUIRA' il valore contenuto in reloc_address con il valore del simbolo, ovvero l'indirizzo della funzione. Quindi dopo la prima esecuzione di quel codice, la situazione sarà questa:

push 0x04
push 0x03
call funzione_esterna@PLT
add esp, 8

[...]

funzione_esterna@PLT:
funzione_esterna@PLT+0x00: jmp dword ptr [reloc_address]
funzione_esterna@PLT+0x06: push reloc_index
funzione_esterna@PLT+0x0B: jmp resolve_function

;reloc_address è una zona di memoria, il contenuto viene mostrato come array di dword, delle altre adiacenti non ci interessa, mostro dei ...
;L'indirizzo di funzione_esterna ora è diventato 0xBFF31337, un indirizzo del tutto inventato

reloc_address: BFF31337 ........

questa volta non verrà più risolto il simbolo, ma il jmp trasferirà l'esecuzione direttamente alla funzione interessata. Ho semplificato di molto il processo, altrimenti avrei dovuto introddure la Global offset table e altri concetti, che non sono necessari per i nostri scopi, vi consiglio caldamente di leggere il manuale, che fa una descrizione molto dettagliata della PLT. Comunque se non avete chiaro qualcosa, tranquilli che poi faremo un esempio pratico con un nostro programma, una nostra libreria e con gdb.

Prepariamo il terreno

Dopo la breve introduzione al formato ELF e al modo in cui viene gestito sui sistemi UNIX, e in particolare su linux, possiamo iniziare a preparare il terreno per affrontare l'argomento principale del tutorial, cioè hookare una funzione in un altro processo tramite l'injection a runtime di una libreria. L'introduzione dovrebbe avervi dato abbastanza concetti per capire il seguito del tutorial, ma so benissimo che è un'introduzione sbrigativa, ci sono troppe cose da dire sul formato ELF e rischieremmo di perderci per strada. Quindi diciamo che è obbligatoria la lettura dei manuali dell' ELF per capire veramente bene quello che accadrà da qui in poi.

L'ambiente di test

Il nostro "ambiente" di test sarà formato da tre ELF: una libreria con la funzione che verrà hookata, un eseguibile che usa la libreria, una seconda libreria che sarà iniettata nell'address space del processo e che si occuperà di hookare la funzione.

Vediamo prima di tutto la libreria, contenuta nei sorgenti libdummy.c e libdummy.h (riporto solo il file c):

#include "libdummy.h"

int dummy_add(int a, int b)
{
        return a+b;
}

compilatela e linkatela in questo modo:

gcc -fPIC -c libdummy.c
ld -shared -soname libdummy.so.1 -o libdummy.so.1.0 -lc libdummy.o

a questo punto eseguite ldconfig per creare i link simbolici e aggiornare la cache (volendo potete copiare la libreria in /usr/lib e lanciare ldconfig senza -n):

ldconfig -v -n .

ora bisogna creare il link simbolico libdummy.so (così da poter usare usare la libreria con il linker usando il parametro -ldummy):

ln -sf libdummy.so.1 libdummy.so

con la libreria abbiamo praticamente finito. Se volete potete usare strip per rimuovere i simboli di debug che tanto non ci servono.

Vediamo quindi l'eseguibile che utilizza la libreria (se volete spiegazioni sul sorgente dell'eseguibile o della libreria, avete grossi problemi :)):

#include <stdio.h>
#include "libdummy.h"

int main()
{
        int a,b;
        int res = 0;

        printf("Enter the first number: ");
        scanf("%d", &a);
        printf("Enter the second number: ");
        scanf("%d", &b);
        res = dummy_add(a,b);
        printf("Result is: %d\n", res);
        return 0;
}

compilate e linkate:

gcc -o dummyelf dummyelf.c -L. -ldummy

a questo punto se eseguite dummyelf dovrebbe funzionare tutto senza problemi (ricordatevi di impostare LD_LIBRARY_PATH: "export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH").

La seconda libreria non la faremo ora, me in un articolo successivo visto che prima bisogna parlare d'altro, per ora limitiamoci a questi due file. Il sorgente di tutto è nell'allegato al tutorial, quindi se avete problemi dategli un'occhiata.

PLT: un esempio

Nell'ultimo paragrafo dell'ultimo capitolo vediamo un esempio di funzionamento della PLT, in modo da fissare bene le idee. Come target useremo gli ELF appena buildati.

Quindi eseguite dummyelf con gdb e impostate breakpoint all'indirizzo in cui viene chiamata dummy_add:

quake2@quake2-desktop:~/elfinj$ gdb dummyelf
GNU gdb 6.8-debian
Copyright (C) 2008 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "i486-linux-gnu"...
(gdb) disassemble main
Dump of assembler code for function main:
[...]
0x08048507 <main+99>: call 0x80483cc <dummy_add@plt>
[...]
End of assembler dump.
(gdb) break *0x08048507
Breakpoint 1 at 0x8048507
(gdb)

a questo punto facciamo partire il programma e steppiamo un po' finché non arriviamo alla funzione dummy_add@plt:

(gdb) display/i $pc
(gdb) run
Starting program: /home/quake2/elfinj/dummyelf
Enter the first number: 23
Enter the second number: 23

Breakpoint 1, 0x08048507 in main ()
1: x/i $pc
0x8048507 <main+99>: call 0x80483cc <dummy_add@plt>
Current language: auto; currently asm
(gdb) stepi
0x080483cc in dummy_add@plt ()
1: x/i $pc
0x80483cc <dummy_add@plt>: jmp *0x804a00c
(gdb)

come vedete, abbiamo un "jmp *0x804a00c", vediamo quindi cosa c'è a questo indirizzo:

(gdb) print /x *0x804a00c
$1 = 0x80483d2
(gdb)

ovvero l'indirizzo 0x804a00c contiene un valore che corrisponde all'indirizzo dell'istruzione subito dopo il jmp, infatti disassembliamo nel punto in cui ci troviamo:

(gdb) disassemble
Dump of assembler code for function dummy_add@plt:
0x080483cc <dummy_add@plt+0>: jmp *0x804a00c
0x080483d2 <dummy_add@plt+6>: push $0x18
0x080483d7 <dummy_add@plt+11>: jmp 0x804838c <_init+48>
End of assembler dump.
(gdb)

come vedete dopo il jmp c'è il push dell'offset e poi di nuovo il jmp alla funzione che risolverà il simbolo. L'offset nel push è un byte offset nella relocation table e non un indice nella tabella, quindi la relocation da usare sarà data dall'entry all'indirizzo "p_reloc + 0x18", supponendo p_reloc base address della relocation table, come possiamo vedere da questo esempio ("0x8048334" è l'indirizzo a cui si trova la relocation table, entry DT_JMPREL della dynamic section):

(gdb) print /x *(0x8048334+0x18)
$4 = 0x804a00c
(gdb)

quello che farà il linker dinamico sarà sostituire il valore a quella locazione con l'indirizzo reale di dummy_add, quindi steppiamo finché non termina tutto, e poi esaminiamo di nuovo le stesse locazioni di memoria:

0x80483d7 <dummy_add@plt+11>: jmp 0x804838c <_init+48>
(gdb) step
Single stepping until exit from function dummy_add@plt,
which has no line number information.
0xb8095168 in dummy_add () from ./libdummy.so.1
1: x/i $pc
0xb8095168 <dummy_add>: push  %ebp
(gdb) print /x *0x804a00c
$1 = 0xb8095168
(gdb)

nella locazione "0x804a00c"" ora c'è l'indirizzo reale di dummy_add ovvero "0xb8095168".

Conclusioni

In questo primo articolo abbiamo visto le basi e abbiamo preparato ciò che ci servirà nel seguito. Sicuramente la descrizione del formato ELF è stata troppo superficiale, ma ripeto, non è questo lo scopo di questa serie di articoli. Il resto dovrebbe essere tutto abbastanza chiaro, è importante capire bene il funzionamento della PLT perché la tecnica di hooking che useremo si baserà sul modificare la PLT.

Nel prossimo articolo vedremo come utilizzare l'interfaccia ptrace per iniettare una libreria nell'address space di un altro processo. Particolare attenzione verrà data all'interfaccia ptrace che verrà spiegata molto in dettaglio.


Note Finali

Bene, finalmente eccoci alle note finali, la sezione che mi piace di più :), perché posso riempirla di tutto ciò che voglio senza rovinare il tutorial :).

Prima di tutto un ringraziamento all'UIC perché ospiterà gentilmente la manciata di kbyte che occupa questo tutorial :).

Un ringraziamento e un saluto a Ntoskrnl che mi ha fatto in parte da revisore correggendo obbbbbbrobbri (per rendere l'idea :)) grammaticali qua e là.

Un enorme saluto a xoanon anche se ormai è talmente vecchio (scheeerzo...più o meno... :) non te la prende :)) che voi giovani d'oggi probabilmente nemmeno sapete chi è...(e questo è male :))

Un saluto a Quequero, anche se era incluso nei saluti all'UIC, quindi lo ritiro subito :).

Un ringraziamento a Notepad++ che è veramente il miglior editor di testo esistente, con cui ho scritto questo lungo articolo.

A questo punto vorrei ritagliarmi un piccolo spazio: vorrei ringraziare tutte le persone del dipartimento di fisica de "La Sapienza" per ciò che stanno facendo in questo bruttissimo periodo per la ricerca e l'università in italia. Per i sacrifici di molti studenti, che non vogliono rinunciare ad un diritto che dovrebbe far parte della base di ogni civiltà: il diritto allo studio e alla conoscenza, diritto che oggi ci viene sempre di più negato da un governo inetto e assolutamente inadatto alla situazione attuale, dobbiamo dire grazie a chi sta combattendo (purtroppo per ora inutilmente) per il nostro futuro. Un ringraziamento particolare a tutti i professori e i ricercatori del dipartimento, che ci hanno dato supporto fin dal primo giorno, scendendo in piazza e protestando (pacificamente) con noi, esponendosi in prima persona, dimostrando a tutta l'italia che non sono fannulloni che pensano solo a prendere lo stipendio, quando ne hanno uno. Non si sa come andrà a finire, ma da parte nostra una cosa è certa, resisteremo.

"Se Einstein oggi vivesse in Italia, adesso sarebbe un precario... Percio' resistere a questi nuovi vandali che stanno distruggendo il paese. Resistere. Resistere." - G. Parisi

Quake2


Disclaimer

I documenti qui pubblicati sono da considerarsi pubblici e liberamente distribuibili, a patto che se ne citi la fonte di provenienza. Tutti i documenti presenti su queste pagine sono stati scritti esclusivamente a scopo di ricerca, nessuna di queste analisi è stata fatta per fini commerciali, o dietro alcun tipo di compenso. I documenti pubblicati presentano delle analisi puramente teoriche della struttura di un programma, in nessun caso il software è stato realmente disassemblato o modificato; ogni corrispondenza presente tra i documenti pubblicati e le istruzioni del software oggetto dell'analisi, è da ritenersi puramente casuale. Tutti i documenti vengono inviati in forma anonima ed automaticamente pubblicati, i diritti di tali opere appartengono esclusivamente al firmatario del documento (se presente), in nessun caso il gestore di questo sito, o del server su cui risiede, può essere ritenuto responsabile dei contenuti qui presenti, oltretutto il gestore del sito non è in grado di risalire all'identità del mittente dei documenti. Tutti i documenti ed i file di questo sito non presentano alcun tipo di garanzia, pertanto ne è sconsigliata a tutti la lettura o l'esecuzione, lo staff non si assume alcuna responsabilità per quanto riguarda l'uso improprio di tali documenti e/o file, è doveroso aggiungere che ogni riferimento a fatti cose o persone è da considerarsi PURAMENTE casuale. Tutti coloro che potrebbero ritenersi moralmente offesi dai contenuti di queste pagine, sono tenuti ad uscire immediatamente da questo sito.

Vogliamo inoltre ricordare che il Reverse Engineering è uno strumento tecnologico di grande potenza ed importanza, senza di esso non sarebbe possibile creare antivirus, scoprire funzioni malevole e non dichiarate all'interno di un programma di pubblico utilizzo. Non sarebbe possibile scoprire, in assenza di un sistema sicuro per il controllo dell'integrità, se il "tal" programma è realmente quello che l'utente ha scelto di installare ed eseguire, né sarebbe possibile continuare lo sviluppo di quei programmi (o l'utilizzo di quelle periferiche) ritenuti obsoleti e non più supportati dalle fonti ufficiali.