SEH aka Structured Exception Handling
(Sveliamo l'arcano?)


5-09-2000

by "Quequero"

 

 

UIC's Home Page

Published by Quequero


L'arte � ci� che subisce i dolori del nostro piacere

G. Verga

Hihihi � troppo stramitico scrivere commenti sul proprio tute, cmq non pretendo di far luce su tutto ci� che concerne le SEH in quanto mi sarei dovuto addentrare troppo in argomenti che avrebbero portato via troppo tempo, cmq il mio scopo era incuriosirvi e spero di esserci riuscito.

Chi sa soffre.....Chi non sa soffre di pi�
UIC's form
Home page: http://quequero.cjb.net
E-mail: quequero at bitchx dot it
Server: irc.azzurra.it canale: #crack-it
UIC's form

Difficolt�

( )NewBies ( )Intermedio (X)Avanzato ( )Master

 

Structured Exception Handling...Cosa significa...Ma piuttosto, a cosa servono?


SEH aka Structured Exception Handling
(Sveliamo l'arcano?)
Written by Quequero

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 caro bastoncino alla liquirizia mi accinger� a far luce su questo interessantissimo aspetto della programmazione...Ma prima di tutto voglio ringraziare J. Gordon per il suo 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):
 
1) 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 :)
 
2) 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]
 
3) 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
 
4) Se nessun Handler dialoga con l'eccezione ma il programma � sotto debug allora il sistema prova nuovamente a notificare l'evento al debugger
 
5) Se non accade nulla di positivo allora il controllo passa al Final Handler (che dobbiamo settare noi)
 
6) 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 :)
 
7) 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 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.

Quequero

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 ;))))