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 micros siguientes constan de 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.
[ SEH -
AIRBAG PARA TU CÓDIGO ]
|