SEH aka Structured Exception Handling
Introduzione
L'argomento delle SEH è stato poco (o per nulla) trattato in italia, gli unici riferimenti li ho trovati su un tutorial di Kill3xx ma cmq vengono solo accennate. Io mi prodigherò ad approfondire questo interessantissimo argomento.
Tools usati
TASM (Se vi interessa di fare qualche prova)
Essay
Erano mesi che aspettavo l'ispirazione per iniziare questo tutorial su uno degli aspetti più interessanti che la programmazione in Win32 ci offre...La gestione delle eccezioni.
Con in bocca il mio caro bastoncino alla liquirizia accingerò a far luce su questo interessantissimo aspetto della programmazione...Ma prima di tutto voglio ringraziare J. Gordon per il so completissimo tutorial sulle SEH, ed infatti questo tutorial è ispirato al suo. Tanto per inziare vediamo cos'è un'eccezione:
Avete presente quello strano box che raramente vedete su windows recante la caption "General Protection Fault" che ha quella strana icona che riporta una X bianca su sfondo rosso? No vero? Ci avrei giurato.
Allora avete presente quello schermo blu che ogni tanto (molto raramente) vi si presenta a schermo e che reca alcune parole come "Errore", "Irreversibile", "Imminente distruzione"? Neanche questo? Eddai, l'hanno fatto vedere pure alla presentazione di WinTonno 98.....Lo vedete che avete capito? :)
Esattamente, quando un programma genera un'errore (un'eccezione appunto) crasha e windows si prende "la fatica" di farlo chiudere e di sistemare le cose. Come tutti sapete quando un programma crasha si hanno due possibili soluzioni: La chiusura stessa del programma oppure il blocco totale del sistema giusto? È proprio WinSalame che decide cosa deve succedere, ovviamente la scelta non è affatto arbitraria ma anzi è accuratamente ponderata, peccato che il sistema conosca solo due soluzioni, mentre io ve ne farò scoprire svariate altre :).
In realtà le eccezioni sono usate parecchio dal sistema, un esempio è questo: supponiamo che un thread abbia bisogno di più stack, l'idea principale sarebbe quella di monitorare lo spazio che sta usando e dargli più stack quando non c'è più memoria disponibile, ma ciò porterebbe via molte risorse oltre che spazio nel programma finale, allora si installa un Exception Handler (ovvero un Gestore di Eccezioni) che con pochissime risorse e poco spazio controlla una sola cosa: appena il thread ha raggiunto il limite massimo di stack il sistema segnala un'errore che verrà controllato dal NOSTRO handler e non da quello di win, il nostro handler provvederà quindi ad allargare lo stack, il tutto succede in modo enormemente veloce e senza spreco inutile di risorse, ma entriamo nel dettaglio.
Windows si preoccupa di monitorare con un Exception Handler ogni thread avviato, la posizione di questo Exception Handler si trova in fs:[0], ogni errore nel codice fa si che il controllo passi a questo Handler che controllerà in sequenza queste cose (grazie a J. Gordon per l'elenco):
- WinSuino controlla se ci sono altri Handler disposti a controllare l'eccezione e se il programma è debuggato allora notifica al debugger che è accaduta un'eccezione (ecco spiegata la funzione "faults on" di SoftIce :)
- Se non ci sono debugger presenti e se il programma non è neanche sotto debugging allora il sistema si preoccupa di vedere se noi abbiamo installato un Handler sul thread (per-Thread Handler) e va quindi a spulciare nel TIB (Thread Information Block) che si trova all'indirizzo fs:[0]
- Se esiste un per-Thread Handler disposto a dialogare con l'eccezione allora gli passa il totale controllo, ma questo Handler potrebbe a sua volta passare il controllo ad altri Handler della catena
- Se nessun Handler dialoga con l'eccezione ma il programma è sotto debug allora il sistema prova nuovamente a notificare l'evento al debugger
- Se non accade nulla di positivo allora il controllo passa al Final Handler (che dobbiamo settare noi)
- Se il nostro Final Handler non è in grado di fare nulla allora il controllo passa al System Final Handler, verrà mostrato il solito box di GPF oppure verrà attivato un debugger, se il programma non può passare il controllo al debugger allora un sano ExitProcess farà la sua comparsa :)
- Prima di terminare definitivamente il programma, il sistema si preoccupa di ripulire lo stack nella zona dove è accaduta l'eccezione.
Ed ora dovreste potervi accorgere delle potenzialità delle SEH, se avete capito potete interagire completamente ed abbastanza "invisibilmente" su qualunque tipo di codice, potete anche gestire voi stessi gli errori del programma e quindi il vostro codice sarà molto ma molto più robusto e poi durante la fase di debugging le SEH sono davvero utilissime.
Chi ha una discreta dimistichezza col C/C++ conoscerà sicuramente le varie istruzioni _throw, _try, _except e se fate una ricerca sul web a riguardo delle SEH troverete principalmente listati C/C++ con queste istruzioni, ma difficilmente scoprirete cosa c'è dietro. Effettivamente utilizzare l'assembly conviene perchè è più veloce, ma nell'installazione di un Handler l'assembly è ENORMEMENTE più veloce ed ESTREMAMENTE più compatto del C.
Prima di iniziare con qualche esempio di codice dobbiamo fare una distinzione, di Handler infatti ne esistono due tipi, il primo si chiama Final Handler e si installa nel Thread principale del programma ed il secondo si chiama per-Thread Handler che come dice la parola stessa si installa all'inizio di ogni thread, facciamo un exempio di Final Handler:
Start:
push offset Final_Handler
call SetUnhandledExceptionFilter
....snip....
....normale codice del programma....
....snip....
call ExitProcess
;----------------------
Final_Handler:
....snip....
....codice di chiusura....
....qualche altra cosa....
....un box di saluti....
....snip....
mov eax, X ; eax = 0 ---> Mostra il box di chiusura
; eax = 1 ---> Nascondi il box di chiusura
; eax = -1 ---> Ricarica il contesto e continua
ret
Cosa succede? All'inizio del programma viene installato un Final Handler che resterà sul Thread principale del programma, mi sembra palese ricordare che prima lo installate meglio è, se durante il programma dovesse incorrere un errore che noi non ci aspettiamo allora il sistema seguirà gli step di sopra, se supponiamo di aver settato SoftIce a "faults off" allora il processo di gestione dell'Exception si fermerà al 5° Step e poi il programma verrà chiuso.
Come vedete il Final Handler è l'ultima spiaggia nel quale il sistema cerca riparo, ma fa sempre uso di API e cmq il livello di libertà che noi abbiamo resta cmq molto basso, proviamo quindi a vedere come si installa un per-Thread Handler, ricordiamo che la prima DWORD puntata da "fs" è la struttura di errore, mentre la seconda DWORD è l'indirizzo dell'ExceptionHandler:
Start:
push offset ExceptionHandler ; Salviamo la posizione del NOSTRO Handler
push fs:[0] ; Salviamo nello stack la posizione della struttura di errore
mov fs:[0], esp
...
...
...
pop fs:[0] ; Rimettiamo tutto a posto...
add esp, 4 ; ...e sistemiamo lo stack
ret
;----------------------
ExceptionHandler:
...
...
...
mov eax, X ; eax = 1 ---> Passa al prossimo Handler della catena
ret ; eax = 0 ---> Ricarica il contesto e continua l'esecuzione
Ora sappiamo come si installano questi due tipi di Handler, ma prima di vedere come si usano facciamo luce su una cosa, ovvero la Struttura di Errore, questa è una parte importantissima perchè solo grazie alla struttura di errore noi possiamo sapere dove è accaduta l'eccezione e soprattutto possiamo conoscere il tipo di eccezione.
Sappiamo che la prima DWORD puntata da fs:[0] è la struttura di errore che ha sua volta punta a questa struttura:
EXCEPTION_RECORD +0 ExceptionCode
EXCEPTION_RECORD +4 ExceptionFlag
EXCEPTION_RECORD +8 NestedExceptionRecord
EXCEPTION_RECORD +C ExceptionAddress
EXCEPTION_RECORD +10 NumberParameters
EXCEPTION_RECORD +14 AdditionalData
ottenere questi dati è semplicissimo, all'interno del codice del nostro Exception Handler basta fare:
mov edx, dword ptr[ebp+8]
quindi basta sapere che:
Edx+0 ExceptionCode
Edx+4 ExceptionFlag
Edx+8 NestedExceptionRecord
Edx+C ExceptionAddress
Edx+10 NumberParameters
Edx+14 AdditionalData
così un semplicissimo:
cmp dword ptr[edx], ExceptionCode
basta a farci sapere che tipo di eccezione è, ed un altrettanto semplice:
mov eax, dword ptr[edx+0Ch]
serve a farci conoscere dove è avvenuta l'eccezione, bello non credete?
Ora che conosciamo anche la struttura di errore vi mostro quali sono i più comuni codici di errore e quindi faremo qualche esperimento:
C0000094h ; Divisione per 0
C0000025h ; Non continuabile, non si deve dialogare con l'Exception e si deve chiudere il prg
C0000026h ; Interrupt Exception
80000003h ; BreakPoint occorso (INT3)
C0000095h ; Integer Overlow
80000004h ; Single Step
C0000005h ; Read or Write Memory Violation
C000001Dh ; Invalid Opcode
C00000FDh ; Lo stack ha raggiunto la massima dimensione disponibile
80000001h ; Violazione di pagina settata con VirtualAlloc
solo due codici necessitano di una spiegazione, il primo è C00000095h, ovvero Integer Overflow, questa eccezione accade quanto durante un operazione un registro si trova a dover contenere una cifra troppo grande, il secondo codice è 80000004h, ovvero Single Step, l'eccezione di Single Step accade quanto viene settato il TrapFlag, se il processore vede che questo flag è settato allora genera questa eccezione per ogni riga di codice che esegue. Bene, sperimentiamo ora l'uso di un Exception Handler per-Thread:
.data
Div0 db "Division by 0 occurred",0
Brk db "Int3 occurred",0
.code
Start:
push offset ExceptionHandler ; Salviamo la posizione del NOSTRO Handler
push fs:[0] ; Salviamo nello stack la posizione della struttura di errore
mov fs:[0], esp
xor eax, eax
div eax ; Olè, divisione per 0
int 3 ; Ed anche un INT3 :))
pop fs:[0] ; Rimettiamo tutto a posto...
add esp, 4 ; ...e sistemiamo lo stack
call ExitProcess, NULL
;----------------------
ExceptionHandler PROC c, EF, ContextRecord, DC
push ebx ; Il manuale Intel ci impone di salvare
push edi ; ebx, edi ed esi prima di effettuare
push esi ; questa operazione
mov edx, dword ptr[ebp+8]
pop esi
pop edi
pop ebx
cmp dword ptr[ebx], 0C0000094h ; Divisione per 0
jne continue
call DivisionBy0
continue:
cmp dword ptr[ebx], 80000003h ; BreakPoint occorso (INT3)
jne exit
call BreakOccurred
exit:
mov eax, 0
ret
ExHandler ENDP
;----------------------
Divisionby0 PROC
push MB_OK
push NULL
push offset Div0
push NULL
call MessageBoxA
Divisionby0 ENDP
ret
;----------------------
BreakOccurred PROC
push MB_OK
push NULL
push offset Brk
push NULL
call MessageBoxA
BreakOccurred ENDP
ret
;----------------------
E come per magia quando andrete a steppare nel codice non solo non crasherà nulla (ricordatevi di disabilitare i breakpoint) ma anzi, vedrete anche apparire da chissà dove una messagebox, ora provate ad immaginare questo stesso procedimento in un eseguibile di 100-150kb e provate a vedere il Disasm.....Già, nessuna reference e quindi una confusione totale, specie per i più NewBies che non capiranno MAI dove andar a mettere le mani :)
Ora che abbiamo sperimentato (e capito) cosa sono gli Handler passiamo ad approfondire la funzione della struttura di errore, per ora conosciamo solo due parametri:
ExceptionCode, e ExceptionAddress....Sono i più intuitivi, ma gli altri?
Exception flag: 0 ----> Eccezione continuabile, possiamo ripararla
1 ----> Eccezione non continuabile, non possiamo ripararla
2 ----> Lo stack è dipanato, non possiamo (e NON dobbiamo) neanche provare a ripararla
Nested exception record: punta ad un altro EXCEPTION_RECORD, nel caso che il nostro Handler generasse da solo un'eccezione :))
NumberParameters: Numero di DWORD da seguire in Additional Information
Additional information: Sono informazioni inviate dall'applicazione quando si chiama RaiseException, se invece il codice di errore è C0000005h allora i valori contenuti dalla prima DWORD saranno:
0 ----> Violazione di lettura
1 ----> Violazione di scrittura
Al momento della chiamata al per-Thread Handler ESP+C punta al Context che contiene tutte le specifiche dei registri al momento dell'eccezione (il context si ottiene anche chiamando GetThreadContext) ed è:
+0 context flags
DEBUG REGISTERS
+4 debug register #0
+8 debug register #1
+C debug register #2
+10 debug register #3
+14 debug register #6
+18 debug register #7
FLOATING POINT / MMX registers
+1C ControlWord
+20 StatusWord
+24 TagWord
+28 ErrorOffset
+2C ErrorSelector
+30 DataOffset
+34 DataSelector
+38 FP registers x 8 (10 bytes each)
+88 Cr0NpxState
SEGMENT REGISTERS
+8C gs register
+90 fs register
+94 es register
+98 ds register
ORDINARY REGISTERS
+9C edi register
+A0 esi register
+A4 ebx register
+A8 edx register
+AC ecx register
+B0 eax register
CONTROL REGISTERS
+B4 ebp register
+B8 eip register
+BC cs register
+C0 eflags register
+C4 esp register
+C8 ss register
Ed è anche l'unico luogo dal quale potete cambiare il valore di EIP (per maggiori info aprite il mitico Winnt.h), questa in pratica è la "sala dei bottoni" del vostro processore, da qua dentro fate proprio tutto :) anche quello che non potreste fare altrimenti :).
Esp+8 punta invece ad una NOSTRA struttura di errore, mentre Esp+4 punta all'EXCEPTION_RECORD.
Finito qui? Niente affatto, se succede un qualche tipo di errore cosa facciamo? Beh, prima di chiudere il programma dobbiamo constatare se l'eccezione è riparabile, se si allora possiamo fare qualcosa di davvero carino, appena viene invocato l'Handler ESP+8 punta alla nostra struttura di errore che è così formata:
STRUTTURA+0 Pointer to next ERR structure
STRUTTURA+4 Pointer to own exception handler
STRUTTURA+8 Code address of "safe-place" for handler
STRUTTURA+C Information for handler
STRUTTURA+10 Area for flags
STRUTTURA+14 Value of EBP at safe-place
vi garantisco che non è poco, infatti i valori contenuti da STRUTTURA+8 e STRUTTURA+14h sono importantissimi, ci consentono infatti di far continuare l'esecuzione del programma in un "luogo-sicuro" (se l'eccezione non è recuperabile allora si chiama RtlUnwind e pazienza :) quindi la prima cosa da fare è vedere se l'eccezione è continuabile, se si allora si estrae dalla nostra struttura di errore l'indirizzo del luogo-sicuro (STRUTTURA+8) e quello del nostro nuovo EBP (STRUTTURA+14h), quindi si estrae il CONTEXT e da li dentro si manipola CONTEXT+B4 (registro ebp)ponendolo uguale al valore suggerito da STRUTTURA+8 e poi si manipola CONTEXT+B8 (registro eip)ponendolo uguale a STRUTTURA+14h, in questo modo l'EIP verrà cambiato ed il codice continuerà allegramente dove è in grado di prolificare :), vi riporto del codice di J. Gordon:
MYFUNCTION: ; procedura di entry point
PUSH EBP ; salva ebp, usato per indirizzare lo Stack Frame
MOV EBP,ESP ; usa EBP come stack frame pointer
SUB ESP,40h ; 16 DWORD di spazio per i dati locali e la struttura di Errore
; Installa l'handler
PUSH EBP ; STRUTTURA+14h salva EBP che si trova al luogo-sicuro
PUSH 0 ; STRUTTURA+10h area per i flags
PUSH 0 ; STRUTTURA+0Ch informazioni informazioni per l'handler
PUSH OFFSET SAFE_PLACE ; STRUTTURA+8h nuovo eip al luogo-sicuro
PUSH OFFSET HANDLER ; STRUTTURA+4h indirizzo dell'handler
PUSH FS:[0] ; STRUTTURA+0h tieni la STRUTTURA all'inizio della catena
MOV FS:[0],ESP ; Punta alla STRUTTURA appena creata sullo stack
... ; Il codice protetto dall'Handler va qui
...
...
JMP >L10 ; Se non ci sono state eccezioni salta
SAFE_PLACE:
L10:
POP FS:[0]
MOV ESP,EBP
POP EBP
RET
;*****************
HANDLER:
...
...
...
...
RET
come vedete il codice non è troppo difficile, ma bisogna applicarsi un attimo per poterlo capire a dovere, nelle applicazioni con più di un thread sarete costretti a dover usare solamente il Final Handler e non potrete settare dei comodi per-Thread Handler. La potenza delle SEH non finisce qui, infatti potete agire sui DrX o sui CrX senza per questo dover andare a Ring0, se invece volete andare a ring0 non dovete far altro che settare nel context l'eip e 0x28 e vi ritroverete come per magia nel paradiso di WinCinghiale, ovver un luogo dove sarete perennemente a ring0 :) a così potrete anche utilizzare a piacere i DrX che sono comodissimi per monitorare locazioni di memoria od altro, ma di questo ne parlerò nel prossimo tutorial, un salutone a tutti e rileggete tutto con moltissima attenzione altrimenti non ci capirete nulla.
Note finali
Il primo ringraziamento va sicuramente a Kill3xx che mi ha tremendamente incuriosito con queste SEH e che poi si è anche preso la fatica di darmi alcune spiegazione, senza di lui questo tute non sarebbe mai esistito, il secondo ringraziamento va a J. Gordon per il suo tute sulle SEH, peccato che il codice è tremendamente incasinato :), ringrazio anche Olga che resta sempre una grandissima amica e soprattutto è l'unica reverser con la quale posso parlare seriamente di tutte le cose avendo sempre la certezza che si disturberà di starmi a sentire e di rispondermi....E lei si merita un grazie troppo grande, grazie Alguzza :).
Saluto poi tutti gli amici di #Crack-it (si, anche quelli che scrivono dove non devono :), i fratellini Spp, N0body88 che si prodiga ogni giorni di farmi sapere le sue buone nuove :)), un salutone particolare anche a \Spirit\ che è stato con me ed i miei amici sorbendosi tutte le nostre domande, accompagnandoci in giro per la città e facendoci provare dei cocktail davvero SUPERIORI :), grazie anche a Xunil per gli aiuti via tel che mi da, a Brigante per avermi spedito quelle cose, a phobos che è troppo simpatico anche quando diffonde le mie foto :))) e ad andreageddon col qualche ancora devo fare a botte....Dai andre muoviti, un saluto anche a baron-sam che credo sia defunto :P e spinone che oggi mi ha chiamato dalla tunisia....È inutile che menti tanto si sentiva il fiatone del tunisino che lavorava "alle tue spalle" :), ciao gente.
Disclaimer
Vorrei ricordare che il software va comprato e non rubato, dovete registrare il vostro prodotto dopo il periodo di valutazione. Non mi ritengo responsabile per eventuali danni causati al vostro computer determinati dall'uso improprio di questo tutorial. Questo documento è stato scritto per invogliare il consumatore a registrare legalmente i propri programmi, e non a fargli fare uso dei tantissimi file crack presenti in rete, infatti tale documento aiuta a comprendere lo sforzo immane che ogni singolo programmatore ha dovuto portare avanti per fornire ai rispettivi consumatori i migliori prodotti possibili.
Noi reversiamo al solo scopo informativo e di miglioramento del linguaggio Assembly.
Capitoooooooo????? Bhè credo di si ;))))