![]()
Torino, 16/11/2005 - ver. 0.1
Sommario:
1. Abstract
2. Premessa
3. Nozioni sul formato ELF
4. Modificare direttamente il .text?
5. Utilizzo dell'heap
6. Senza rilocamento
7. Rilocare i riferimenti: tecnica stupida
8. Rilocare i riferimenti: tecnica meno stupida
9. Criptare il binario
10. Mettere tutto insieme
11. Vulnerabilità
12. Ringraziamenti
1. Abstract:
Questo documento parlerà di una tecnica a cui ho pensato, e alla quale
sicuramente anche altri avranno già pensato, per evitare che un
programma protetto da ipotetica licenza, venga debuggato, utilizzando in
prima approssimazione, la licenza come chiave di criptatura/decriptatura
del binario.
A differenza di altri documenti e tecniche, la presente non userà
syscall come la ptrace per ricostruire il flusso di esecuzione non
criptato del programma.
A causa di questa scelta si evidenzieranno quindi problemi di
rilocazione nelle chiamate a funzioni, sia locali che di libreria.
2. Premessa
Una premessa, per fare il punto, è d'obbligo. Partendo innanzitutto dal
presupposto che dal 1975/1980 non è stato inventato più nulla in
informatica, si può dire che questo doc sia inutile.
Allora perchè lo scrivo? Perchè magari qualcuno non ha voglia di andarsi
a leggere i documenti originali dell'IBM su 360/370, virtual machine,
DAT, memoria virtuale, e cose del genere e quindi fa comodo avere una
descrizione, in italiano e aggiornata ai nostri tempi, di qualcosa che
quasi sicuramente era all'ordine del giorno quando ancora il C non esisteva.
Altrettanto sicuramente altra gente avrà scritto o usato tali tecniche,
posso comunque assicurare che tutto quello che scriverò è farina del mio
sacco. Per quanto riguarda i riferimenti bibliografici, li trovate al
fondo.
3. Nozioni sul formato ELF
Non spiegherò in questo capitolo cos'è il formato ELF, a cosa serva etc
etc, si presuppone che qualcosa sugli ELF si sappia già.
Mi soffermo invece sulla sezione .text dei binari ELF.
In questa sezione è contenuto il codice macchina dei diversi simboli-
funzioni che lavorano all'interno del programma.
Il codice macchina è scritto sequenzialmente, non vi sono separazioni
tra il codice di un simbolo e quello del successivo; il flusso di
esecuzione del programma infatti non tiene conto dei nomi dei simboli,
l'unica cosa che conta sono le call e i jump.
Nonostante tutto le informazioni sui simboli sono utili al debug e sono
mantenute all'interno della sezione .symtab (la sto facendo molto
semplice, diciamo che in realtà si dovrebbe considerare a quale sezione
il simbolo è linkato). Tra le varie informazioni sui simboli, è
contenuto anche nell'elemento st_value della struttura Elf32_Sym,
l'indirizzo in memoria virtuale da cui quel simbolo partirà
all'esecuzione del programma.
Attraverso questo indirizzo e l'entrypoint dell'elf, contenuto
nell'header elf nell'elemento e_entry della struttura Elf32_Ehdr, con
una sottrazione si può ricavare l'offset all'interno della sezione .text
del file in cui inizia il codice macchina di uno specifico simbolo.
Considerando quindi di avere nella struttura Elf32_Sym sym l'header di
un generico simbolo di tipo funzione e nella struttura Elf32_Ehdr eh
l'header del file ELF, e in char *text tutta la sezione .text, l'inizio
del codice macchina del simbolo sym inizierà a:
unsigned char *sym_start = text + (sym.st_value - eh.e_entry);
Per quanto riguarda le dimensioni del simbolo invece si deve usare un
altro elemento della struttura Elf32_Sym, st_size.
Quindi volendo scorrere tutto il codice di un singolo simbolo di tipo
funzione, si potrà iterare sulla porzione di memoria che parte da quanto
visto sopra, leggendo un numero di byte pari a sym.st_size.
Ora che abbiamo visto come agire sulla sezione .text di un particolare
simbolo, vediamo come è possibile andare a recuperare le informazioni
del simbolo all'interno della tabella dei simboli.
Ovviamente all'interno di tale tabella non sono presenti i nomi in ascii
dei simboli, risulta così leggermente più laboriosi ricavarli e
indirizzare un simbolo attraverso il suo nome, usato all'interno del
sorgente.
Prima di tutto bisogna vedere a quale sezione di stringhe è linkata la
tabella dei simboli, la .symtab, per poter ricavare per ogni header di
ogni simbolo il suo nome. Per fare questo all'interno della struttura
Elf32_Shdr, che definisce l'header delle sezioni, è presente l'elemento
sh_link, che assume valori diversi a seconda della sezione a cui è
attribuito. Nel caso della sezione .symtab indica la string table
associata per ricavare i nomi dei simboli.
Anche la string table, al pari della .text contiene una sequenza di
stringhe terminate dal null byte. Per ricavare quindi il nome di un
simbolo dobbiamo utilizzare un elemento della struttura Elf32_Sym,
st_name, che rappresenta l'offset in byte dall'inizio della string
table associata attraverso il numero contenuto in sh_link, della
struttura Elf32_Shdr. Quindi mettendo di avere la string table in char
*strtab, l'header del simbolo in Elf32_Sym sym, il nome del simbolo
sarà:
unsigned char *sym_name = strtab + sym.st_name;
Evito di parlare di come ricavare l'indice di una sezione dato il nome,
visto che il procedimento è simile, risalendo di un livello di
indirizzamento. E' comunque ben visibile nella mia libreria di esempio
per la lettura degli ELF.
Ancora una cosa che non riguarda propriamente il formato ELF ma che sarà
utile quando parleremo di rilocazione dei riferimenti: jump e call
vengono rappresentate in modo diverso ma le differenze tra le due
istruzioni non sono così abissali. Un jump sposta, in maniera a volte
condizionata a diverse flag, il flusso di esecuzione da un certo
indirizzo ad un altro, andando a modificare il registro EIP. Anche la
call sposta il flusso di esecuzione ad un'altra locazione di memoria, ma
nel frattempo salva anche l'indirizzo di ritorno (per quanto riguarda i
processori x86, sullo stack) e il codice target deve avere alcuni
requisiti, come ad esempio il cosidetto preambolo, per avere a che fare
con uno stack frame diverso, cosa che con il jump non accade. Inoltre
con una call è possibile passare al codice chiamato, dei parametri
facendone push sullo stack. Ricordo inoltre che dopo una call, bisogna
sempre ripulire lo stack, tenendo conto dell'allineamento usato durante
la compilazione.
Ancora una cosa: il compilatore genera call relative, non si avrà quindi
nei 4 byte dedicati all'operando della call l'indirizzo esatto della
funzione chiamata, bensì un offset relativo all'indirizzo di memoria in
cui l'istruzione di call termina.
Mettendo una call all'indirizzo 0x3 che deve chiamare una funzione
all'indirizzo 0xa0, e mettendo che la call sia composta da opcode
(1byte) + operando (4byte), l'offset sarà di 0xa - 0x8 = 0x2 e quindi
facendo un dump e tenendo conto del little endian avremo:
0xe8 0x02 0x00 0x00 0x00
Fine introduzione agli ELF, andiamo avanti con il paper.
4. Modificare direttamente il .text?
L'idea alla base di questa tecnica era in prima battuta di criptare
la sezione .text di determinate funzioni di un certo programma
attraverso un programma esterno, lasciando in chiaro il motore di
decriptatura, il quale avrebbe dovuto decriptare direttamente le zone di
memoria a partire dagli indirizzi delle funzioni criptate.
Esempio:
int foo() { /* funzione il cui .text verrà criptato */
printf("yeahn");
return 0;
}
int main(int ac, char **av) {
unsigned char *foo_text = (unsigned char *)foo;
decrypt_text(foo_text); /* l'implementazione non interessa per ora */
foo();
return 0;
}
Bene, questo approccio non funziona. Segmentation Fault è tutto quello
che si ottiene quando si prova a modificare al volo direttamente la
sezione .text di una funzione interna al programma. La sezione .text è
infatti solitamente read-only se non si passa al linker del gcc
l'opzione -N. Ma non rendiamoci le cose troppo semplici e decidiamo di
avere un programma la cui sezione .text sia ro, complicando
ulteriormente eventuali manipolazioni del codice una volta caricata in
memoria da un debugger.
5. Utilizzo dell'heap
Per ovviare al fatto che abbiamo deciso di avere la sezione .text macata
come read-only, dobbiamo appoggiarci a zone di memoria modificabili ed
eseguibili, come ad esempio l'heap o lo stack.
Ovviamente, nel caso in cui la sezione .text di un simbolo sia molto
grossa, è d'obbligo usare l'heap. L'idea è quindi quella di copiare li
codice macchina criptato del simbolo da decriptare, in una zona di
memoria allocata con malloc, decriptarla, e spostare il flusso di
esecuzione del programma all'indirizzo della zona di memoria appena
allocata.
A questo punto nascono diverse problematiche.
Come detto nel paragrafo 3 riguardo alle call e ai loro operandi
relativi, dobbiamo considerare come il linker abbia già risolto tutte le
rilocazioni necessarie, facendo riferimento all'elemento st_value dei
diversi simboli. Copiando però il .text di un simbolo in un'altra zona
di memoria e spostando il flusso di esecuzione su quest'ultima, tutti i
riferimenti relattivi calcolati dal linker vanno a farsi benedire.
Infatti ritornando all'esempio della call di cui sopra, dove in
condizioni normali avremo:
0x03: 0xe8 0x02 0x00 0x00 0x00
...
0xa0: © GnomixLand http://www.gnomixland.com/ |