[ SEH -
AIRBAG PARA TU CÓDIGO ]
Por Mr.
Silver
Saludos,
hacia tiempo que no escribia un tutorial asi que me he decidido a escribir sobre
un tema el cual parece ser bastante desconocido por la mayoria de los newbies,
Structured Exception Handling (Manejo Estructurado de Excepciones).
El uso de SEH suele resultar al principio algo confuso, pero explicado de la
manera correcta, no resulta un tema complicado. Como ventajas de usar SEH en
nuestras aplicaciones, encontramos la más importante de todas: robustez
y gran tolerancia a errores graves cometidos por la aplicación. Pero
veamos antes,
cuando y como se produce una excepción y para que sirven exactamente.
Para que sirven?
Su uso más común
es controlar fallos graves de la aplicación e intentar recuperar el programa
del error, aunque a veces resulta imposible. La mayoria de
veces el uso de SEH sirve para tener un manejador de errores personalizado que
ofrezca al programador la información necesaria para poder subsanar el
error,
esto es así ya que por defecto windows tiene un manejador de excepciones
genérico para las aplicaciones que no usan uno propio y a veces la información
que aporta suele ser bastante excasa, este manejador genérico es controlado
por la API UnhandledExceptionFilter.
Cómo funciona SEH?
El funcionamiento de SEH
es algo complejo aunque no es dificil de entender. Hay que tener en cuenta que
un manejador de excepciones es
especifico para cada
thread de la aplicación. Un thread es un hilo o hebra del proceso
principal, un proceso puede tener multiples threads ejecutandose a la vez y
cada uno de estos threads puede tener su propio manejador de excepciones. Es
importante recordar que al establecer un manejador propio en un thread el manejador
anterior queda
en la cadena de manejadores de excepción, pero este no será llamado
si el nuevo manejador no lo indica explicitamente. Es muy importante tener esto
en mente
cuando estamos trabajando con un proceso ajeno, del cual desconocemos su comportamiento
respecto al uso de manejadores, ya que es posible modificar el
proceso ajeno para que establezca un manejador de excepciones previamente inyectado
como una dll o como un thread remoto, de esta manera podriamos controlar las
excepciones que pudieran causarse en el proceso ajeno, aunque siempre existe
la posibilidad que el proceso ajeno modifique el manejador y
perdamos así el control, más adelante comentaré una técnica
que nos permitirá controlar estos cambios.Veamos ahora con un gráfico
el desarrollo de una excepción desde que se produce hasta que se controla.
Al producirse una excepción, el sistema recibe el control (suponiendo que no exista un depurador en modo kernel en ejecución SICE TRW), tras esto el sistema comprueba si el proceso esta siendo depurado (ojo, un depurador que corre por debajo del sistema, como WinDbg o OllyDbg) si es así el sistema envia la excepción al depurador, en caso de que el proceso este ejecutandose de manera normal, el sistema pasa el control al manejador de excepciones del thread donde se produjo la excepción. Si el depurador no manejara la excepción, se pasaria al manejador genérico del sistema. Normalmente el manejador genérico suele terminar la ejecución con ExitProcess().Una vez el manejador recibe la excepción debe realizar alguna de las siguientes operaciones:
- Controlar la excepción e intentar recuperar la ejecución. Realizariamos las operaciones necesarias y tras esto retornariamos EXCEPTION_CONTINUE_EXECUTION.
- Ejecutar el manejador por defecto del sistema (en caso de que el manejador no sepa como controlar la excepción). Para ello simplemente retornariamos
EXCEPTION_EXECUTE_HANDLER.- Continuar la busqueda en la cadena de manejadores del thread en busca de alguno que controle la excepción, si no se encontrara ninguno, se ejecutaria el manejador por defecto. El manejador retornaria EXCEPTION_CONTINUE_SEARCH.
Cuando se produce una excepción?
Las excepciones se producen
cuando el procesador realiza una operación no válida bajo el contexto
del thread actual. Se pueden distinguir las siguientes
excepciones más importantes.
- Acceso a memoria no válida: Se producen cuando un thread intenta acceder en un modo no permitido a una posición de memoria a la cual no tiene acceso. Por ejemplo se puede producir este tipo de excepción si el thread intenta escribir a una posición de memoria de solo lectura.
- División entre 0: Se produce cuando se intenta dividir un numero entre 0.
- Instrucción no valida intento de ejecución de instrucción privilegiada: Se produce cuando el procesador intenta ejecutar una instrucción que no pertenece a su juego de instrucciones, es decir al encontrar un código de operación desconocido. Tambien se producen estas excepciones si el thread intenta ejecutar una instruccion privilegiada, es decir cuando el thread se esta ejecutando en un modo de privilegio de usuario (Ring 3) y se intenta ejecutar una instrucción del sistema (Ring 0).
- Al llegar a un punto de ruptura: Es decir cuando se ejecuta una INT 3, el sistema notifica este hecho como una excepción, normalmente son los depuradores los que establecen los puntos de ruptura, aunque nada nos impide hacerlo manualmente. Debido a esto se puede implementar un truco anti-debugging: se establece un SEH que controle esta excepción y se ejecuta una INT 3, si el manejador toma el control, el proceso no esta siendo depurado.
Hay mas ocasiones en las cuales se generan excepciones, para una lista completa recomiendo que hecheis un vistazo al API de Windows, concretamente a la estructura EXCEPTION_RECORD.
Cómo se establece un manejador SEH?
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 __except 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]
O cómo
implementar puntos de ruptura por hardware
Por Mr.
Silver
Mediante el uso de la técnica descrita anteriormente podriamos crear
un sistema de puntos de ruptura sobre memória aunque ciertamente los
inconvenientes de VirtualProtect hacen esta
tarea algo engorrosa. Veamos ahora otra alternativa para implementar puntos
de ruptura usando un manejador SEH. Hemos visto que tenemos una estructura con
el contexto actual del thread, en su interior se encuentran los registros de
depuración (solo a partir de Pentium o superior), podemos usar estos
registros para establecer un punto de ruptura por hardware sobre cualquier dirección
donde se ejecute una instrucción o donde se lea/escriba memória,
esto es debido a que los registros de depuración son capazes de provocar
excepciones (0x80000004L)
de depuración
que pueden ser controladas por un manejador SEH. Para ello necesitamos saber
como funcionan los registros de depuración. A partir del procesador 386
los PC's tienen 6 registros de depuración, DR0, DR1, DR2, DR3, DR6, DR7
los registros DR4 y DR5 estan reservados por Intel y no se utilizan.Los cuatro
primeros registros contienen 4 posibles direcciones de memoria donde se establece
el punto de ruptura, el registro DR7 (Control Debug Register) controla el tipo
de punto de ruptura para cada uno de esos puntos de ruptura (DR0 a DR3) y el
registro DR6 ( Debug Status Register) controla el tipo de punto de ruptura que
se activó por ultima vez. Veamos El formato de los registros de depuración:
Extraido del documento: Intel Architecture Software Developer’s Manual Volume 3: System Programming
DR7: El registro de control de depuración (DR7) activa o desactiva puntos de ruptura y establece las condiciones que deberán cumplirse para que tenga lugar la excepción. El significado de cada bit para este registro es como sigue:
L0 hasta L3 (activar punto de ruptura local) flags (bits 0, 2, 4, and 6) Activa (si estan establecidos) la condición de ruptura para el punto de ruptura asociado para la tarea actual. Cuando se detecta una condición y su flag Ln está activo, se genera una excepción de depuración. El procesador automáticamente borra estos flags en cada cambio de tarea para evitar rupturas no deseadas en las nuevas tareas.
G0 hasta G3 (activar punto de ruptura global) flags (bits 1, 3, 5, and 7) Activa (si estan establecidos) la condición de ruptura para el punto de ruptura asociado para todas las tareas. Cuando una condición de ruptura es detectada y su flag Gn asociado está activo, se genera una excepción de depuración.El procesador no borra estos flags al cambiar de tarea, permitiendo que el punto de ruptura se active en otras tareas.
LE y GE (activar punto de ruptura exacto local y global) flags (bits 8 y 9) (No soportado en la familia de procesadores P6.) Cuando estan activos, estos flags hacen que el procesador detecte la instrucción concreta que causó la condición de ruptura para datos. Para compatibilidad con anteriores y posteriores arquitecturas de procesador, Intel recomienda que se pongan estos flags a 1 si se requieren puntos de ruptura exactos.
GD (activar detección general) flag (bit 13) Activa (cuando está establecido) la protección de los registros de depuración, la cual causa una excepción de depuración que se genera antes de la instrucción MOV que accedió al registro de depuración. Cuando se detectan este tipo de condiciones, el flag BD en el registro de estado DR6 se establece antes de generar la excepción.El procesador borra el flag GD una vez se ha entrado en el manejador de excepciones, para permitirle a este el acceso a los registros de depuración.
R/W0 hasta R/W3 (lectura/escritura) campos (bits 16, 17, 20, 21, 24, 25, 28, y 29): Especifica la condición de ruptura para el correspondiente punto de ruptura. El flag DE (debug extensions) en el registro de control CR4 determina como son interpretados los bits en los campos R/Wn.Cuando el flag DE esta activo, el procesador interpreta estos bits de la siguiente manera.
00—Parar sólo si se ejecutan instrucciones.
01—Parar solo en accesos de escritura a datos.
10—Parar en accesos de escritura o lectura de E/S.
11—Parar en accesos de lectura o escritura a datos, pero no en ejecución.
Cuando el flag DE esta borrado, el procesador interpreta los bits de R/Wn de la misma manera que para los procesadores Intel386™ y Intel486™, esto es de la siguiente manera:
00—Parar sólo si se ejecutan instrucciones.
01—Parar solo en accesos de escritura a datos.
10—Sin definir.
11—Parar en accesos de lectura o escritura a datos, pero no en ejecución.
LEN0 hasta LEN3 (Longitud) campos (bits 18, 19, 22, 23, 26, 27, 30, y 31): Especifica el tamaño de la posición de memoria especificada en la dirección del correspondiente registro (DR0 hasta DR3) Los bits son como sigue:
00—1-byte de longitud
01—2-bytes de longitud
10—Sin definir
11—4-bytes de longitud.
Si el campo correspondiente RWn en el registro DR7 es 00 (ejecución de instrucción), entonces el campo LENn deberia ser 0. El efecto de usar cualquier otra longitud es indefinido.
DR6: El registro de estado de depuración DR6, informa sobre las condiciones que fueron muestreadas cuando tuvo lugar la última excepción de depuración, este registro sólo se actualiza cuando se genera una excepción.
bits B0 hasta B3 (condición de ruptura detectada) Indican (cuando estan activos) que la condición de ruptura asociada se cumple cuando se generó la excepción de depuración. Estos flags se establecen si la condición descrita para cada uno de los puntos de ruptura por el campo LENn, y R/W del registro DR7 es cierta. Estos bits se establecen incluso si el punto de ruptura no se activó por los flags Ln y Gn del registro DR7.
BD (acceso a los registros de depuración) flag (bit 13): Indica que la siguiente instrucción accederá a uno de los registros de depuración (DR0 a DR7). Este flags se activa cuando el flag GD (general detect) en el registro DR7 esta activo.
BS (trazado paso a paso) flag (bit 14) Indica (cuando está activo) que la excepción de depuración fue activada por el modo de trazado paso a paso (activado mediante el flag TF en el registro EFLAGS). El trazado paso a paso es la excepción de depuración con mayor prioridad. Cuando el flag BS esta activo, cualquier otro bit del registro de estado puede ser establecido.
BT (cambio de tarea) flag (bit 15) Indica (cuando está activo) que la excepción de depuración resultó de un cambio de tarea donde el flag T (debug trap flag) en el TSS (Task-State Segment) de la tarea objetivo fue establecido.(Para más info sobre el TSS referirse a la sección 6.2.1. del Intel Architecture Software Developer’s Manual Volume 3: System Programming). No hay ningún flag en el registro DR7 que permita activar o desactivar esta excepción; el flag T del TSS es sólo un flag de activación.
Como podemos ver básicamente necesitamos el registro DR7 y los registros de direcciones DR0 a DR3, el uso del registro DR6 es opcional y solo se usa si desea tener un control mas preciso sobre los puntos de ruptura, en este tutorial sólo pretendo mostrar las capacidades básicas de los registros de depuración, para aquellos que deseen profundizar más les recomiendo que consulten el PDF Intel Architecture Software Developer’s Manual Volume 3: System Programming. Bien, como ya muchos sabrán el acceso directo a los registros de depuración es una tarea que solo esta permitida en Ring0, vamos a ver ahora un método para acceder a ellos desde Ring3 aunque no sea directamente el método nos resuelve la papeleta. Se basa en el uso de las APIs GetThreadContext y SetThreadContext:
BOOL SetThreadContext(
HANDLE hThread, // handle
del thread
CONST CONTEXT *lpContext //
dirección donde se leera la nueva info del contexto
);
BOOL GetThreadContext(
HANDLE hThread, // handle
del thread
LPCONTEXT lpContext //
dirección donde almacenar la info del contexto
);
Si estamos bajo NT, el handle al thread deberá
tener privilegios de acceso de tipo THREAD_SET_CONTEXT.
Alternativamente podemos usar GetCurrentThreadContext
o SetCurrentThreadContext si lo que pretendemos
es alterar el contexto del thread actual. Como podemos apreciar los contextos
son propios para cada thread, es decir cada thread tiene su contexto y Windows
se encarga de modificar este contexto cada vez que se cambia de thread (llamemoslo
tarea), por lo que un proceso puede tener multiples threads con un contexto
por cada thread.La clave del asunto está en los bits del registro DR7,
dependiendo de los bits que activemos podremos lograr un punto de ruptura que
tendrá lugar sobre la dirección indicada en el registro de depuración
DR0 a DR3.
Un ejemplo, viendo el significado de los bits de DR7 podemos deducir el valor adecuado de DR7 para poner un breakpoint sobre la dirección en el registro DR0:
Estas serian las mascaras correspondientes para cada uno de los registros (DR0 a DR3):
DR0_LOCAL_EXACT_BPM_ENABLED
equ 1b
DR0_GLOBAL_EXACT_BPM_ENABLED equ 10b
DR0_W equ
010000000000000000b
DR0_IO equ 100000000000000000b
DR0_RW equ 110000000000000000b
DR0_EXECUTION equ 0b
DR0_LEN1
equ 0b
DR0_LEN2 equ 01000000000000000000b
DR0_LEN4 equ 11000000000000000000b
DR0_LENU equ 10000000000000000000b
DR1_LOCAL_EXACT_BPM_ENABLED
equ 100b
DR1_GLOBAL_EXACT_BPM_ENABLED equ 1000b
DR1_W equ
0100000000000000000000b
DR1_IO equ 1000000000000000000000b
DR1_RW equ 1100000000000000000000b
DR1_EXECUTION equ 0b
DR1_LEN1
equ 0b
DR1_LEN2 equ 01000000000000000000000000b
DR1_LEN4 equ 11000000000000000000000000b
DR1_LENU equ 10000000000000000000000000b
DR2_LOCAL_EXACT_BPM_ENABLED
equ 10000b
DR2_GLOBAL_EXACT_BPM_ENABLED equ 100000b
DR2_W equ
01000000000000000000000000b
DR2_IO equ 10000000000000000000000000b
DR2_RW equ 11000000000000000000000000b
DR2_EXECUTION equ 0b
DR2_LEN1
equ 0b
DR2_LEN2 equ 01000000000000000000000000000000b
DR2_LEN4 equ 11000000000000000000000000000000b
DR2_LENU equ 10000000000000000000000000000000b
DR3_LOCAL_EXACT_BPM_ENABLED
equ 1000000b
DR3_GLOBAL_EXACT_BPM_ENABLED equ 10000000b
DR3_W equ
010000000000000000000000000000b
DR3_IO equ 100000000000000000000000000000b
DR3_RW equ 110000000000000000000000000000b
DR3_EXECUTION equ 0b
DR3_LEN1
equ 0b
DR3_LEN2 equ 01000000000000000000000000000000000000b
DR3_LEN4 equ 11000000000000000000000000000000000000b
DR3_LENU equ 10000000000000000000000000000000000000b
// Bits generales
LOCAL_EXACT_BPM_ENABLED
equ 100000000b
GLOBAL_EXACT_BPM_ENABLED equ 1000000000b
LOCAL_EXACT_BPM_DISABLED equ 000000000b
GLOBAL_EXACT_BPM_DISABLED equ 0000000000b
GLOBAL_EXACT_BPM_ENABLED equ 1000000000b
GENERAL_DETECT_ENABLED
equ 10000000000000b
RESERVED_BIT10 10000000000b
// el típico 0x400 que nos indica SICE ;)
Veamos ahora una rutina que podria servir para establecer un punto de ruptura de ejecución sobre DR0
// Pone
un BPM sobre el Thread especificado en la dirección especificada usando
el
// registro DR0 como contenedor de la dirección, en DR7 se setean los
bits para
// activar el BPM sobre DR0
void PutBPM(HANDLE hThread,DWORD
Address)
{
// Estructura para establecer el contexto del thread
CONTEXT Regs;
// obtenemos contexto del thread
GetThreadContext(hThread,&Regs);
// activamos los flags para los registros de depuración, que es lo unico que tocaremos del contexto
Regs.ContextFlags=CONTEXT_DEBUG_REGISTERS;
// ponemos DR7 para k active un BPM en DR0
Regs.Dr7=LOCAL_EXACT_BPM_ENABLED|DR0_EXECUTION|DR0_LOCAL_EXACT_BPM_ENABLED|DR0_LEN1;
// sobre Address
Regs.Dr0=Address;
// y establecemos de nuevo el contexto del Thread
SetThreadContext(hThread,&Regs);
}
La rutina es bastante sencilla y se podría modificar para que permitiera una mayor versatilidad, como elegir el DRx y el tipo de punto de ruptura, pero eso se deja como práctica para el lector :), antes de llamar a esta rutina se deberia establecer el manejador de excepciones con SetUnhandledExceptionFilter, el manejador podría ser algo así:
// Manejador
de excepciones para cuando el BPM surta efecto
static LONG WINAPI MyBPMHandler(LPEXCEPTION_POINTERS
pExceptStruct)
{
PEXCEPTION_RECORD ExcepRecord;
PCONTEXT Context;// cogemos puntero al contexto y al registro de la excepción
Context=pExceptStruct->ContextRecord;
ExcepRecord=pExceptStruct->ExceptionRecord;// Es una excepción de trazado paso a paso (0x80000004L)
if (ExcepRecord->ExceptionCode==STATUS_SINGLE_STEP)
{// puede k sea la nuestra
// aquí hacemos lo que nos plazca, como mostrar un mensaje o cualquier otra cosa
........................................................
........................................................
// borramos el BPM
Context->Dr0=0;
// el bit 10 del registro DR7 esta reservado, por lo que hay que dejarlo a 1
Context->Dr7=GLOBAL_EXACT_BPM_DISABLED|LOCAL_EXACT_BPM_DISABLED|RESERVED_BIT10;
// modificamos el contexto completo (es necesario reestablecer el contexto ya que si no la excepción entrara en un bucle infinito
Context->ContextFlags=CONTEXT_FULL|CONTEXT_DEBUG_REGISTERS;
// y seguimos
return EXCEPTION_CONTINUE_EXECUTION;}
return EXCEPTION_CONTINUE_SEARCH;
}
Este sería el esqueleto básico del manejador, por supuesto que la cosa puede mejorarse para conseguir algo más decente pero la base y el funcionamiento básico no cambiarian,aquí teneis un proyecto de Visual C que muestra el uso de esta técnica poniendo un punto de ruptura sobre el API MessageBox, el manejador se encarga de mostrar la dirección del API y la dirección de retorno donde se volverá tras ejecutar el MessageBox. Con esto doy por terminado este tutorial aunque no descarto una nueva serie con aspectos más avanzados.
You are inside Reversed Minds pages. por
Mr. Silver
/ WKT! |