Dependiendo del lenguaje
de programación utilizado el manejador SEH puede establecerse de distintas
formas, en lenguaje C++ se suele utilizar la siguiente
sintáxis:
void main()
{
__try {
int a=0,b=1;
b=b/a;
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
MessageBox(NULL,"Divide by 0 exception","ERROR",MB_ICONINFORMATION);
}
};
El código anterior
establece un manejador SEH para el código ejecutado entre los parentesis
del try. Si se produce alguna excepción en ese código, el bloque
de código perteneciente al catch se ejecutará como manejador SEH,
cabe destacar que dependiendo del compilador el manejador puede establecerse
previamente a este código. Dado que se divide b entre 0, se produce una
excepción y el mensaje de error aparecerá.En
ensamblador podemos simular estas instrucciones con el uso de las siguientes
macros:
@TRY_BEGIN
MACRO Handler
pushad
mov esi,offset Handler
push esi
push dword ptr fs:[0]
mov dword ptr fs:[0],esp
ENDM
@TRY_EXCEPT
MACRO Handler
jmp NoException&Handler
Handler:
mov esp,[esp+8]
pop dword fs:[0]
add esp,4
popad
ENDM
@TRY_END MACRO Handler
jmp ExceptionHandled&Handler
NoException&Handler:
pop dword fs:[0]
add esp 32+4
ExceptionHandled&Handler:
ENDM
En nuestro código
en ensamblador hariamos:
@TRY_BEGIN
Nombre_Handler
; código a chequear excepciones
@TRY_EXCEPT Nombre_Handler
; código a ejecutar si se produce una excepción
@TRY_END Nombre_Handler
; flujo de ejecución normal
Este código accede
al TIB o TEB (Thread Information Block), el TIB esta almacenado en fs:[0] y
tiene la siguiente estructura:
// Esta
estructura está parcialmente documentada en el include NTDDK.H
// del DDK de Win NT
typedef
struct _TIB
{
_EXCEPTION_REGISTRATION_RECORD pvExcept; //00h Cabeza
de la cadena de manejadores
PVOID pvStackUserTop; //04h cima de la pila del usuario
PVOID pvStackUserBase; //08h base de la pila del usuraio
WORD pW16TDB; //0Ch W16 Task DataBase
WORD pvThunksSS; //0Eh SS selector usado para pasar a
16 bits
DWORD SelmanList; //10h
PVOID pvArbitrary; //14h Disponible para el uso de la
app
PTIB ptibSelf; //18h Dirección lineal del TIB =
R3TCB + 10h
WORD TIBFlags; //1Ch
WORD Win16MutexCount; //1Eh
DWORD DebugContext; //20h
DWORD pCurrentPriority; //24h
DWORD pvQueue; //28h selector de la cola de mensajes
PVOID* pvTLSArray; //2Ch Array de almacenamiento local
del Thread
} TIB, *PTIB;
En fs:[0] tenemos un puntero
a una estrutura de tipo _EXCEPTION_REGISTRATION_RECORD
(pvExcept), esta estructura contiene dos punteros más,el primero apunta
a la estructura del siguiente manejador SEH establecido en el thread actual
(*Next) y el segundo puntero es exactamente la dirección del manejador
de excepciones actual (PVOID Handler).La estructura seria esta:
typedef
struct _EXCEPTION_REGISTRATION_RECORD
{
struct _EXCEPTION_REGISTRATION_RECORD *Next;
PVOID Handler;
} EXCEPTION_REGISTRATION_RECORD, *PEXCEPTION_REGISTRATION_RECORD;
Tal y como vemos en las
macros de ensamblador, el código establece el manejador empujando a la
pila primeramente el puntero al nuevo handler (PVOID Handler) y
seguidamente el puntero del manejador actual (push dword ptr fs:[0]) por ultimo
solo tiene que corregir el valor ExceptionList (mov dword ptr fs:[0],esp)
Como es lógico el puntero a la cadena SEH lo tenemos en esp ya que previamente
habiamos empujado los datos necesarios en la pila. Los otras macros hacen lo
propio para restaurar el manejador anterior, dependiendo de si se tuvo que controlar
o no la excepción. Este método es anidable, lo cual nos permite
un control mas exacto dependiendo del código a ejecutar.Como información
adicional destacar que el TIB esta situado dentro del R3TCB (Ring3 Thread Control
Block) concretamente en el offset 10h del R3TCB. Este bloque de información
contiene datos bastante interesantes sobre el thread actual, veamos su definición
(esto no es necesario para usar seh pero nunca esta de más saber cosas
nuevas):
// Estrutura
de Bloque de control del thread en Ring 3 (R3TCB)
typedef
struct _THREAD_DATABASE
{
DWORD Type; //00h = 6
DWORD cReference; //04h
PPROCESS_DATABASE pProcess; //08h PDB goo
DWORD someEvent; //0Ch un objeto de evento (Para que se
usa???)
_TIB TIB; //10h TIB (Thread Information Block)
PPROCESS_DATABASE pProcess2;//40h otra copia del proceso
del thread?
DWORD Flags; //44h
DWORD TerminationStatus; //48h Valor de retorno de etExitCodeThread
WORD TIBSelector; //4Ch
WORD EmulatorSelector; //4Eh
DWORD cHandles; //50h
DWORD WaitNodeList; //54h
DWORD un4; //58h
DWORD Ring0Thread; //5Ch
PTDBX pTDBX; //60h
DWORD StackBase; //64h
DWORD TerminationStack; //68h
DWORD EmulatorData; //6Ch
DWORD GetLastErrorCode; //70h
DWORD DebuggerCB; //74h
DWORD DebuggerThread; //78h
PCONTEXT ThreadContext; //7Ch
DWORD Except16List; //80h
DWORD ThunkConnect; //84h
DWORD NegStackBase; //88h
DWORD CurrentSS; //8Ch
DWORD SSTable; //90h
DWORD ThunkSS16; //94h
DWORD TLSArray[64]; //98h
DWORD DeltaPriority; //198h
// La versión recortada termina mas o menos aquí
// El resto de campos seguramente solo existen en la versión de depuración
DWORD un5[7]; //19Ch
DWORD pCreateData16; //1B8h
DWORD APISuspendCount; //1BCh # de veces que SuspendThread
es llamó
DWORD un6; //1C0h
DWORD WOWChain; //1C4h
WORD wSSBig; //1C8h
WORD un7; //1CAh
DWORD lp16SwitchRec; //1CCh
DWORD un8[2]; //1D0h
DWORD Mutex?[4]; //1D8h max 4 level
DWORD hMutex[4]; //1E8h max 4 level,hMutex of each level
DWORD un9; //1F8h
DWORD ripString; //1FCh
DWORD LastTlsSetValueEIP[64];
} THCB, THREAD_DATABASE, *PTHREAD_DATABASE;
Esta estructura es bastante
compleja y los datos que contienen derivan en mas estructuras si cabe más
comlejas aún, por lo que queda fuera del objetivo de
este documento su análisis. Tan solo decir que para acceder a esta estructura
podemos hacerlo de dos maneras:
Ring3TCB
= (WORD)FS:[18h] - 10h
Ring3TCB
= GetLinearAddress(FS)-10h
Por último la manera
mas frequente y quizás la menos compleja de todas para establecer un
manejador SEH es usar la API SetUnhandledExceptionFilter,esta
función es como sigue:
LPTOP_LEVEL_EXCEPTION_FILTER
SetUnhandledExceptionFilter(LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter
);
La función recibe
un puntero al manejador que se desea establecer como manejador por defecto para
el thread actual, si previamente existia algún manejador, la
función retornara la dirección de este, en caso contrario obtendremos
un NULL como valor de retorno. Puede ser de utilidad guardar el handler anterior
y en
nuestro manejador retornar el control a este en caso de que el nuestro no sepa
o no quiera manejar una excepción concreta. Notese que este comportamiento
no
es obligatorio, es por esto que aqnque se establezca un manejador global para
el thread, siempre puede ser sustituido por otro el cual no tiene porque llamar
al
manejador previo. Este tipo de manejador es global para el thread hasta que
se define otro manejador con esta misma API o cuando se defina un manejador
usando el metodo del try..catch.
El manejador que se pase a esta API debe retornar uno de los siguientes valores:
EXCEPTION_EXECUTE_HANDLER:
Retorna de UnhandledExceptionFilter y ejecuta el manejador asociado. Normalmente
suele terminar el proceso.
EXCEPTION_CONTINUE_EXECUTION:
Retorna de UnhandledExceptionFilter y continua la ejecución del thread
en el punto donde sucedio la excepción, suponiendo que no se altere
el Eip desde el manejador de excepciones, si se alterara el Eip la ejecución
continuaria desde el nuevo eip establecido por el manejador.
EXCEPTION_CONTINUE_SEARCH:
Proceder con una ejecución normal de UnhandledExceptionFilter. Esto
significa obedecer los flags de la API SetErrorMode,
o invocar el cuadro de dialogo de error de aplicación.
Ahora que ya sabemos como
se establece el manejador, vamos a hechar un vistazo a los parametros que recibe
cuando se produce la excepción.
LONG My_SEH_Handler(
STRUCT _EXCEPTION_POINTERS *ExceptionInfo );
Esta podria ser la defiinción
para un supuesto manejador SEH, como parámetros recibe un puntero a una
estructura de tipo _EXCEPTION_POINTERS y retorna
un
LONG conteniendo uno de los valores de retorno comentados previamente.
Veamos que tenemos en la
estructura:
typedef
struct _EXCEPTION_POINTERS
{
PEXCEPTION_RECORD ExceptionRecord;
PCONTEXT ContextRecord;
} EXCEPTION_POINTERS;
Esta estructura nos ofrece dos punteros más, el primero contiene la información
relativa a la excepción que se produjo (ExceptionRecord) y el segundo
contienes
el estado de los registros del procesador cuando sucedio la excepción
(ContextRecord).Seguimos mirando las estructuras, veamos que información
tenemos sobre la excepción:
typedef
struct _EXCEPTION_RECORD
{
DWORD ExceptionCode;
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress;
DWORD NumberParameters;
DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;
ExceptionCode:
El código de excepción, quizás este sea el dato
más interesante para nuestro manejador, ya que con el podrá averiguar
que tipo de excepción a
tenido lugar y decidir si puede controlarlo o no.Alguno de los códigos
de excepción más comunes són:
EXCEPTION_ACCESS_VIOLATION
0xc0000005 El thread intentó leer o escribir a una dirección
virtual a la cual no tiene acceso apropiado.
EXCEPTION_BREAKPOINT 0x80000003 Se encontró un punto de ruptura en
el thread.
EXCEPTION_SINGLE_STEP 0x80000004 Un sistema de depuración paso a paso
indicó que una instrucción fue ejecutada.
EXCEPTION_INT_DIVIDE_BY_ZERO 0xc0000094 El thread intentó dividir un
valor entero entre 0.
EXCEPTION_ILLEGAL_INSTRUCTION 0xc000001d El Thread intentó ejecutar
una instrucción no válida.
EXCEPTION_PRIV_INSTRUCTION 0xc0000096 El Thread intentó ejecutar una
instrucción privilegiada no permitida en el modo de ejecución
actual de la aplicación.
ExceptionFlags:
Especifica los flags de la excepción, Si es 0 indica que la excepción
es continuable, EXCEPTION_NONCONTINUABLE (1) indica
una excepción no continuable. Cualquier intento de continuación
de ejecución despues de una excepción no continuable causará
una excepción de tipo EXCEPTION_NONCONTINUABLE_EXCEPTION
(0xc0000025).
ExceptionRecord:
Apunta a una estrutura de tipo EXCEPTION_RECORD.
Las estruturas de excepción pueden ser encadenadas para proveer información
adicional cuando suceden excepciones anidadas.
ExceptionAddress:
Especifica la dirección (EIP) donde tuvo lugar la excepción, notese
que este dato tambien lo podemos obtener de la estrutura ContextRecord.
NumberParameters:
Especifica el
número de parámetros asociados a la excepción. Este es
el numero de elementos definidos en el array ExceptionInformation.
ExceptionInformation:
Un array de argumentos adicionales (4 bytes cada elemento) que describen la
excepción. La API RaiseException puede especificar este array de elementos
para la mayoria de excepciones estos argumentos no estan definidos, tan solo
para EXCEPTION_ACCESS_VIOLATION, tenemos los siguientes
argumentos en el array:El primer elemento contiene un flag indicando el tipo
de operación que causó la violación de acceso, Si es 0,
el thread intentó leer datos inaccesibles. Si es 1 el thread intento
escribir datos inaccesibles.
El segundo elemento de array indica la dirección virtual de los datos
inaccesibles.Hemos terminado con la estrutura ExceptionRecord,
veamos ahora ContextRecord, la cual nos ofrece interesante información
sobre el estado de la CPU en el momento de la excepción.
typedef
struct _CONTEXT {
DWORD ContextFlags;
DWORD Dr0;
DWORD Dr1;
DWORD Dr2;
DWORD Dr3;
DWORD Dr6;
DWORD Dr7;
FLOATING_SAVE_AREA FloatSave;
DWORD SegGs;
DWORD SegFs;
DWORD SegEs;
DWORD SegDs;
DWORD Edi;
DWORD Esi;
DWORD Ebx;
DWORD Edx;
DWORD Ecx;
DWORD Eax;
DWORD Ebp;
DWORD Eip;
DWORD SegCs;
DWORD EFlags;
DWORD Esp;
DWORD SegSs;
} CONTEXT,*PCONTEXT,*LPCONTEXT;
Como podemos ver tenemos
los valores de los registros de propósito general y tambien los selectores,
si disponemos de una CPU Pentium o superior también tendremos los registros
de depuración (Drx). Si desearamos modificar cualquiera de estos datos,
podriamos hacerlo antes de de salir del manejador pero para ello tendriamos
que modificar el valor de ContextFlags para que Windows actualizará los
valores al reanudar la excepción.Los
flags que podemos pasarle son los siguientes:
CONTEXT_CONTROL:
Si se modificó alguno de los registros de control siguientes: EBP,
EIP,CS,EFlags,ESP,SS.
CONTEXT_INTEGER:
Si se modificó alguno de los siguentes registros: EDI, ESI, EBX, EDX,
ECX, EAX
CONTEXT_SEGMENTS:
Si se modificó alguno de los siguentes registros de segmento GS, FS,
ES,DS
CONTEXT_FLOATING_POINT:
Se modificó algún registro o dato de la FPU, estos datos se
encuentran en la estrutura _FLOATING_SAVE_AREA,
definida como:
typedef
struct _FLOATING_SAVE_AREA {
DWORD ControlWord;
DWORD StatusWord;
DWORD TagWord;
DWORD ErrorOffset;
DWORD ErrorSelector;
DWORD DataOffset;
DWORD DataSelector;
BYTE RegisterArea[80];
DWORD Cr0NpxState;
} FLOATING_SAVE_AREA;
CONTEXT_DEBUG_REGISTERS: Si se modificó
alguno de los siguientes registros de depuración: DR0, DR1, DR2, DR3,
DR6, DR7. Como nota indicar que los registros DR4 y DR5 estan reservados por
Intel, asi que no hay acceso posible a ellos.
CONTEXT_FULL: Este flag es una combinación
de todos los anteriores, por lo tanto se usará en caso de que se modifiquen
varios registros de distinto tipo.
Veamos ahora un ejemplo de
manejador de excepciones que controla las violaciones de acceso sobre una variable
y continua la ejecución normal del programa:
#include
<WINDOWS.H>
#include <string.h>
#include <stdio.h>
#define
strSize 30
DWORD OldAccess=0; // para guardar antiguo acceso
char string[strSize]; // declara una cadena de caracteres
// Manejador de excepciones
LONG MyHandler(LPEXCEPTION_POINTERS ExceptionInfo)
{
DWORD DummyAccess;
DWORD *Access;
DWORD *Addr;
//
obtenemos puntero a la información de la excepción
PEXCEPTION_RECORD pExcept=ExceptionInfo->ExceptionRecord;
// si es
una violación de acceso, quizás la podemos controlar
if (pExcept->ExceptionCode==EXCEPTION_ACCESS_VIOLATION)
{
// Obtiene la dirección que se quiso acceder
Addr=(DWORD *)pExcept->ExceptionInformation[1];
// y el tipo de acceso que se realizó
Access=(DWORD *)pExcept->ExceptionInformation[0];
// Muestra la info
printf("\nException at %08Xh",pExcept->ExceptionAddress);
printf("\n\tCode %08Xh",pExcept->ExceptionCode);
printf("\n\tAccess: %s",Access == 0 ? "Read" : "Write");
printf("\n\tAddr %08Xh",Addr);
printf("\n\tBPM at %08Xh",string);
printf("\n\tEAX: %08X ECX: %08X EDX: %08X EBX: %08X",
ExceptionInfo->ContextRecord->Eax,
ExceptionInfo->ContextRecord->Ecx,
ExceptionInfo->ContextRecord->Edx,
ExceptionInfo->ContextRecord->Ebx);
printf("\n\tESI: %08X EDI: %08X EBP: %08X ESP: %08X",
ExceptionInfo->ContextRecord->Esi,
ExceptionInfo->ContextRecord->Edi,
ExceptionInfo->ContextRecord->Ebp,
ExceptionInfo->ContextRecord->Esp);
printf("\n\tEIP: %08X CS: %04X SS: %04X FS: %04X ES: %04X DS: %04X
GS: %04X",
ExceptionInfo->ContextRecord->Eip,
ExceptionInfo->ContextRecord->SegCs,
ExceptionInfo->ContextRecord->SegSs,
ExceptionInfo->ContextRecord->SegFs,
ExceptionInfo->ContextRecord->SegEs,
ExceptionInfo->ContextRecord->SegDs,
ExceptionInfo->ContextRecord->SegGs);
printf("\nFlags: %08Xh",ExceptionInfo->ContextRecord->EFlags);
printf("\nDR0: %08X DR1: %08X DR2: %08X DR3: ",
ExceptionInfo->ContextRecord->Dr0,
ExceptionInfo->ContextRecord->Dr1,
ExceptionInfo->ContextRecord->Dr2,
ExceptionInfo->ContextRecord->Dr3);
printf("\nDR6: %08X DR7: %08X",
ExceptionInfo->ContextRecord->Dr6,
ExceptionInfo->ContextRecord->Dr7);
//
Desprotegemos la memoria. hay k tener en cuenta que al ser
// memoria del proceso, se generan accesos dentro de la página
// donde esta ubicada la cadena, esta página tambien contiene el
// código del programa, por lo que se hace necesario el uso del
// modificador EXECUTE, o restaurar los valores antiguos de la
// página (tamaño pagina 4Kb) para que no se produzcan
// excepciones de acceso de ejecución al código
// si la dirección accedida esta dentro del rango protegido
if ( ((DWORD)Addr>=(DWORD)string) && ((DWORD)Addr<=(DWORD)string+4096)
)
{
printf("\n\tAddr is inside our protected space %08Xh-%08Xh",string,string+strSize);
// la desprotegemos
VirtualProtect(string,strSize,OldAccess,&DummyAccess);
}
// y continuamos la ejecución
return EXCEPTION_CONTINUE_SEARCH;
}
return EXCEPTION_CONTINUE_SEARCH;
}
void main(
void )
{
LPTOP_LEVEL_EXCEPTION_FILTER
OldHandler;
OldHandler=SetUnhandledExceptionFilter(MyHandler);
//
Protegemos la pagina, podemos usar PAGE_NOACCESS (con lo cual queda protegido
parte del código del programa)
if (!VirtualProtect(string,strSize,PAGE_NOACCESS,&OldAccess))
{
printf("\nCan not protect memory");
} else
{
printf("\nMemory protected, now accesing...");
// Aquí sucede la excepción de acceso
strcpy(string,"Here it is the exception!");
// Aquí ya no sucederá nada ya que el
manejador habrá restaurado los accesos
printf("\nCadena -> %s",string);
}
printf("\nPrevious
access %08Xh",OldAccess);
if (!OldHandler)
printf("\nNo previous SEH established"); else SetUnhandledExceptionFilter(OldHandler);
}
El código realiza
lo siguiente: Establece el manejador con SetUnhandledExceptionFilter,
luego protege la memoria virtual donde está alojada la variable de cadena
"string", para ello usa la API VirtualProtect,
la cual permite cambiar los priviliegios de acceso a la memoria indicada. Veamos
mejor que dice la guía del API sobre VirtualProtect:
BOOL VirtualProtect(
LPVOID lpAddress, // dirección de memoria
DWORD dwSize, // tamaño
DWORD flNewProtect, // acceso deseado
PDWORD lpflOldProtect // dirección de un DWORD
donde guardar el anterior acceso
);
flNewProtect:
Los modos de acceso más usuales que se pueden especificar son los siguientes:
PAGE_NOACCESS
: Desactiva cualquier tipo de acceso, cualquier intento de leer, escribir
o ejecutar algo en la región protegida generará una excepción
de violación de acceso (EXCEPTION_ACCESS_VIOLATION).
PAGE_READONLY:
Desactiva todos los accesos excepto la lectura, cualquier tipo otro de acceso
generará una exccepción de violación de acceso (EXCEPTION_ACCESS_VIOLATION).
PAGE_WRITECOPY:
Desactiva todos los accesos excepto la escritura cualquier otro tipo de acceso
generará una excepción de violación de acceso (EXCEPTION_ACCESS_VIOLATION).
PAGE_READWRITE:
Desactiva todos los accesos excepto la lectura/escritura cualquier otro tipo
de acceso generará una excepción de violación de acceso
(EXCEPTION_ACCESS_VIOLATION).
PAGE_EXECUTE:
Desactiva todos los accesos excepto la ejecución cualquier otro tipo
de acceso generará una excepción de violación de acceso
(EXCEPTION_ACCESS_VIOLATION). Este modo es utilizado normalmente para proteger
areas de código del programa, evitando cualquier modificación
no autorizada del código del programa.
PAGE_EXECUTE_READ:
Combinación de PAGE_READONLY y PAGE_EXECUTE.
PAGE_EXECUTE_READWRITE:
Combinación de PAGE_READONLY,PAGE_EXECUTE y PAGE_WRITECOPY. Este modo
implica un acceso total a la memoria.
lpflOldProtect:
Un puntero a un DWORD donde se guardará el modo de acceso anterior establecido
sobre la página. Es importante que este valor no sea NULL ya que en Win
Nt/2k siempre debe de especificarse si no la llamada a VirtualProtect
fallará. En este ejemplo se ha utilizado VirtualProtect
ya que estamos tratando con memoria del proceso actual, en caso que tengamos
que cambiar los accesos de un proceso ajeno usaremos VirtualProtectEx,
esta API permite especificar el proceso mediante su Handle.
Una vez protegida la memoria
se intenta copiar una cadena mediante strcpy, al
haber protegido la memória se generará una excepción que
será enviada a nuestro manejador (MyHandler).
El manejador comprueba si la excepción fue provocada por una violación
de acceso, si no es así se retorna del manejador con EXCEPTION_CONTINUE_SEARCH.
Si la excepción es de acceso se procede a obtener la dirección
de memória que se intentó acceder y el tipo de acceso que se quiso
realizar, esta información la saca del array ExceptionInformation.
Seguidamente muestra el estado de la CPU cuando se produjo la excepción
para ello extrae los datos necesarios de la estructura ContextRecord.
Por ultimo y para tener un control preciso comprueba que el acceso se realizó
exactamente sobre uno de los
caracteres de la cadena. Porqué? Simplemente porque como dice el API,
VirtualProtect siempre protegerá
como mínimo una página, esto es un inconveniente ya que se protegen
bytes que no deberian, por lo que el código del ejemplo comprueba que
efectivamente es uno de los carácteres de la cadena. Por último
se restaura el estado de protección que tenia la memoria (OldAccess).
Nótese que el código anterior deberia de restaurar el modo de
acceso anterior aunque la excepción fuera provocada por un acceso fuera
de la cadena pero dentro de la página, el código no realiza esto
ya que es meramente un ejemplo con una excepción controlada. Otro detalle
a destacar es que nuestro manejador no retorna el control al manejador anterior,
para realizar esto podriamos sustituir el código final del manejador:
return EXCEPTION_CONTINUE_SEARCH;
por
if (OldHandler)
{
OldHandler(ExceptionInfo);
}
De esta manera llamariamos
al antiguo manejador pasandole los datos de la excepción. Nótese
tambien que OldHandler deberia declararse
como global.
[ SEH - Y LAS EXCEPCIONES DE DEPURACIÓN]
|