Ayer vimos como el depurador reemplaza el primer byte de un opcode en una dirección de memoria específica donde, previamente, hemos fijado un breakpoint y donde colocamos el opcode 0xcc o lo que es lo mismo la instrucción int 3 que le indica al procesador que se detenga y lance un evento de depurado que es capturado por el depurador.
También vimos que en este proceso, al sobreescribir parte de la memoria del proceso en depuración se modifica el CRC del mismo y algunas aplicaciones (como la mayoría del malware) comparan el CRC contra un checksum válido y si han habido modificaciones el proceso se mata a sí mismo para no permitir la depuración. En esos casos es necesario echar mano de los hardware breakpoints.
Hardware Breakpoints
Los hardware breakpoints son muy útiles cuando un pequeño número de breakpoints es requerido y no podemos modificar el software en depuración. Este tipo de breakpoint se fija a nivel de CPU en unos registros especiales llamados registros de depurado o en Inglés debug registers.
Una CPU x86 de 32 bits típica tiene ocho registros de depurado (del R0 al R7), los cuales son usados para fijar y administrar hardware breakpoints. Los registros del DR0 al DR3 se usan para almacenar la dirección de los breakpoints. Esto limita el número de hardware breakpoints que podemos utilizar a la vez a solamente cuatro.
Los registros DR4 y DR5 están reservados. El registro DR6 es usado como registro de estado y determina el tipo de evento de depurado que ha sido lanzado por el breakpoint cuando se llega a él. El registro DR7 es básicamente un flag de activo o inactivo para el hardware breakpoint y también sirve como almacén de diferentes condiciones del breakpoint.
Fijando flags específicos en el registro DR7 podemos crear breakpoints para las siguientes condiciones:
Rompe la ejecución cuando una instrucción es ejecutada en una dirección en particular
Rompe la ejecución cuando se escriben datos en una dirección
Rompe la ejecución cuando se leen o escriben datos de una dirección pero no cuando se ejecuta
Esto es realmente útil ya que tenemos la posibilidad de fijar cuatro breakpoints con condicionantes muy específicos sin modificar el proceso en ejecución.
La instrucción INT1
Al contrario que los soft breakpoints que usan la instrucción de interrupción INT3, los hardware breakpoints utilizan la interrupción 1 (INT1). El evento INT1 se utiliza asimismo para la depuración paso a paso (del inglés single-step) que significa que avanzamos por la ejecución de la aplicación depurada instrucción por instrucción, lo cual nos ofrece la posibilidad de monitorizar los cambios de datos y aburrirnos durante el proceso.
Al fin y al cabo, la mecánica que hace funcionar a los hardware breakpoints es la misma que en los soft breakpoints pero ocurre a más bajo nivel. Antes de que el procesador ejecute una instrucción, primero comprueba que la dirección no esté habilitada para detener la ejecución mediante un hardware breakpoints y la ejecuta, en caso contrario, además comprueba el tipo de condicional de acceso a memoria que ha sido fijado para el breakpoint.
Si la dirección está almacenada en los registros de depurado (DR0 - DR3) y la condición de lectura, escritura o ejecución es verdadera, un evento INT1 es disparado y el procesador se detiene. Este tipo de breakpoint es extremadamente útil pero tiene ciertas limitaciones.
A parte de poder fijar solo cuatro direcciones de breakpoints a la vez, solo se puede fijar un breakpoint en un máximo de cuatro bytes de datos. Esto puede limitarnos si necesitamos controlar el acceso a gran parte de la memoria.
Con el fin de evitar esta limitación, podemos utilizar otro tipo de breakpoint, los memory breakpoints de los que hablaremos en el siguiente artículo de la serie. Hasta entonces, happy hacking.
Más en Genebta Dev | Funcionamiento de los depuradores de C y C++