Otro paper interesante...
------[ Índice
0.- Conceptos previos
1.- Introducción
2.- Diseño
2.1 - Método Detour convencional
2.1.1 - Flujo
2.1.1 - Stack
2.2 - E8 Method
2.2.1 - Flujo
2.2.2 - Stack
2.3 - Diferencias
2.4 - Safe Hook Handler
2.4.1 - Siendo invisible
2.4.1.1 - Evidencias basadas en el microprocesador
2.4.1.2 - Evidencias no basadas en el microprocesador
2.4.2 - Problemas de seguridad
2.4.2.1 - Llamada a una API hookeada
2.4.2.2 - Mala programación hook handler
2.5 - Conclusion
3.- Implementación
3.1 - Microsoft Detours Library
3.1.1 - Capa en ensamblador
3.1.1.1 - Creación handle_ooh
3.1.1.2 - SafeCall
3.1.1.3 - SafeRet
3.1.1.4 - Otras funciones de bajo nivel
3.1.2 - one_hook_handler
3.1.3 - Llamada a una API hookeada
3.1.4 - Hack de la biblioteca
3.2 - Easy-hook
3.2.1 - Capa en ensamblador
3.2.1.1 - Creación handle_ooh
3.2.2 - Llamada a una API hookeada - TDB
3.2.3 - one_hook_handler
4.- Compilado, binarios & POC
4.1 - poc
4.1.1 - Compilado
4.1.2 - Uso
4.2 - Detours Express 2.1- hacked
4.2.1 - Compilado
4.2.2 - Uso
4.3 - EasyHook 2.5 - unmaged stuff
4.3.1 - Compilado
4.3.2 - Uso
5.- TODO
6.- Testing
7.- Ventajas y posibilidades
8.- Conclusión
9.- Agradecimientos
10.- Referencias
11.- Código fuente y binarios
------[ 0.- Conceptos previos
Nomenclaturas:
.- [T.Index]: trabajos relacionados (apartado 10).
.- [R.Index]: referencias (apartado 11).
Index es el identificador de la nomenclatura.
Para entender el documento puede ser necesario tener conocimientos de:
- Arquitectura x86:
- Arquitectura básica. [R.10]
- Instrucciones básicas. [R.11]
La esencia del documento y el "E8 Method" se pueden entender sin conocer en
profundidad ningún sistema operativo en concreto. Las demostraciones de
concepto (POCs) y los hacks a bibliotecas actuales estarán orientados a
Windows, por ello quizá sea necesario para entender el documento tener
conocimiento de:
- Win32 API [R.3]
- Tipos de hooks y hooks aplicables en Windows [R.4] [R.5] [R.9] [...]
- Tipo de ejecutables PE32 [R.1]: DLLs, EXE...
- Win32 ASM [R.2]
- API y características de Microsoft Detours Library [R.6] y
EasyHook (parte unmanaged) [R.8] .
Se usarán los siguientes términos en el documento:
- hook_caller / API ID:
- Identificador del hook que llama a su handler.
- handler:
- Manejador de un hook.
- handler global / One hook handler / ooh / One safe hook handler:
- El handler al que llaman todos los hooks.
------[ 1.- Introducción
Existen varias bibliotecas para realizar hooks, sobre todo en Windows, pero
hace poco tuve que lidiar con un problema para el que no he encontrado
solución ni documentación al respecto. El problema es: ¿Cómo hacer hooks a
varias APIs, leyendo un fichero de configuración, en tiempo de ejecución,
el cual indica las APIs y sus prototipos? Comentado el problema, la
respuesta que recibí fue otra pregunta que quizá el lector ya se esté
haciendo, ¿para qué quieres hacer eso? La respuesta es: No quiero tener que
programar un handler diferente para cada API hookeada y tener que compilar
para que funcione, y tampoco quiero programar un creador de handlers en
tiempo de ejecución. Todos los comentarios se pueden reducir a dos
opciones:
1.- Programar en alguna tecnología que no requiera compilación previa.
2.- Programar algún tipo de macro para facilitar la programación y
además reducir la posibilidad de errores al compilar.
Dado que las dos opciones no me convencieron, empecé a darle vueltas al
problema y qué era lo que realmente yo quería, y me surgió la pregunta
correcta: ¿Qué necesitaría para realizar lo que yo quiero? Y la respuesta
es simple: Teniendo un Handler para todos los hooks, saber cuándo es
llamado, qué API/hook lo ha hecho, y entonces actuar en consecuencia,
¡Exacto!, necesito un "API ID". O mejor dicho, necesito un "hook_caller
ID".
Es el momento de mencionar que el método de hook que necesito es "Detours"
[R.9] [R.6], es decir, meter un JMP, PUSH + RET ... en la dirección que se
desee meter el hook. Me decanté por dicho método [R.9] debido a un problema
que tienen algunos de los otros métodos como cuando se llama directamente a
la dirección de memoria donde está el hook, en cuyo caso no se ejecuta el
handler, ej IAT HOOKING. Los métodos que meten el JMP, PUSH + RET... en
memoria reservados o bytes de alineamiento ("padding bytes"), las llamadas
directas no ejecutan el handler. Dado que mi máxima prioridad era
interceptar todas las llamadas a las APIs donde haya hooks, el método
"Detours" [R.9] [R.6] que sobreescribe las instrucciones donde se quiere
introducir el hook con un JMP, me pareció el ideal. Aunque dicho método
conlleve usar algún tipo de LDE (Length-Disassembler Engine) [R.7], entre
otras cosas, hay bibliotecas en la red que permiten realizar este método en
Windows sin mayor dificultad.
Ahora ya solo queda responder a la pregunta: ¿Cómo puedo saber qué hook
llama al handler en tiempo de ejecución? Después de un rato meditándolo
tuve una idea feliz: Cambiar el método JMP por un método de tipo CALL, y
desde el handler comprobar la dirección de retorno que introduce el
CALL en la pila, usando dicha dirección de retorno como "hook_caller ID".
Dado que cada hook está en una posición de memoria diferente, cada CALL
introducirá un valor diferente en la pila, pudiendo servir éste de
identificador. Después, solo habría que modificar el método normal para que
el handler procesara dicho "hook_caller ID" y lo eliminara de la pila.
Problema resuelto. Ahora solo quedaba buscar un nombre, dicho nombre se me
ocurrió a la hora de programarlo: "E8 Method", pues dado un JMP (no SHORT)
en la dirección XXXXXXXX a YYYYYYYY, éste se codificará: "E9 ZZZZZZZZ", y
un CALL en la misma dirección a la misma dirección, se codificará: "E8
"ZZZZZZZZ". ¡Estaba claro!. Lo único que cambiaba era el opcode de la
instrucción, al ser el opcode de CALL, E8, decidí llamar al método "E8
Method". Pero "E8 Method", no solo es sustituir un hook de tipo JMP por un
hook de tipo CALL, es el concepto y/o la forma de implementar que un
handler global ("One hook handler") pueda obtener el "hook_caller ID" en
tiempo de ejecución.
Después de desarrollar la primera demostración de concepto (POC) tuve
varios problemas, como stack buffers overflows cuando se llamaban a APIs
hookeadas desde el hook handler de forma directa o indirecta,
comprobaciones del Microsoft Visual Studio para detectar cuando se corrompe
la pila usando el valor EDI que mi hook handler modificaba internamente
etc. Así que ya no solo tenía que crear un hook handler, tenía que crear un
safe hook handler que tuviera en cuenta la mayoría de los problemas.
A medida que el proyecto creció fue necesario programar partes en C/C++
resultando un código híbrido C/C++/ASM bastante problemático, por el cual
programé una capa de bajo nivel que permitía programar todo en C/C++ sin
preocupaciones. Además también surgieron stack buffers overflows al llamar
a un API hookeada desde el propio handler ya sea directa o indirectamente,
a lo que encontré una solución bastante más elegante que la mía en el
easy-hook llamada Thread Deadlock Barrier (TDB) [R.8].
Este documento trata de cómo usar e implementar el "E8 Method" con un solo
hook handler para todos los hooks que además será seguro y estará
programado en C/C++, "One safe hook handler". Se usarán dos bibiliotecas
públicas en las que se ha realizado un hack para dicho propósito:
1.- Microsoft Detours Library [R.6]
2.- Easy-Hook [R.8]
------[ 2.- Diseño
Como se ha mencionado antes: "E8 Method", no solo es sustituir un hook de
tipo JMP por un hook de tipo CALL, es el concepto y/o la forma de
implementar que un handler global ("One hook handler") pueda obtener el
"hook_caller ID" en tiempo de ejecución". Puede haber muchos escenarios y
formas de plasmar E8 Method, en el documento se seguira el siguiente
contexto:
1) Escenario: Dado un hook de tipo Detours covencional se modificará
para poder obtener el "hook_caller ID".
2) Forma: Se cambiará el salto al hook handler de tipo JMP por un salto
de tipo CALL, y se reparará la pila para que todo funcione
como en el método Detours convencional.
Nota: Para simplificar el documento se asume que el area donde se introduce
el hook es siempre una API, o mejor dicho un área en la que se ha entrado
mediante un CALL del código original.
------[ 2.1 - Método Detours convencional
El método Detours convencional se basa en introducir un hook
sobreescribiendo la/s primera/s instrucción/es original/es con un JMP al
hook handler programado para ese hook. Cuando se hace un hook también se
crea un area en memoria llamada "trampolín" la cual tiene las instrucciones
sobreescritas por el JMP y un salto a la siguiente instrucción no
sobreescrita del código original. El trampolín es usado por el hook handler
el cual usa si desea ejecutar al area original donde se ha introducido el
hook. Para simplificar el documento se asume que el area donde se introduce
el hook es siempre una API, o mejor dicho un área en la que se ha entrado
mediante un CALL del código original.
------[ 2.1.1 - Flujo
El flujo que sigue el método Detours convencional puede ser de dos tipos:
1) Sin usar el trampolín desde el hook handler:
a) Se ha introducido un hook de tipo Detour en la API Sleep de
kernel32.dll
b) El programa original (POC.exe) llama a Sleep
c) La primera instrucción de Sleep es una salto hacia el hook
handler para dicha API.
d) El hook handler realiza las acciones para las que ha sido
programado y retorna al programa original como lo haría la API
Sleep.
1) Usando el trampolín desde el hook handler:
a) Se ha introducido un hook de tipo Detour en la API Sleep de
kernel32.dll
b) El programa original (POC.exe) llama a Sleep
c) La primera instrucción de Sleep es una salto hacia el hook
handler para dicha API.
d) El hook handler realiza las acciones para las que ha sido
programado, llamando en un determinado momento a la API Sleep
original usando el tramplín. Finalmente retorna al programa
original como lo haría la API Sleep.
ASCII ART del flujo sin usar trampolín desde el hook handler:
+- POC.EXE --+ 1 +------ Sleep -----+ 2 + - hook_handler - +
| CALL Sleep | ---> | JMP hook_handler | --> | ANYTHING :-) | 3
+->| ... | | .... | | RET | ---+
| +------------+ | RET | +------------------+ |
| +------------------+ |
| [Trampolín (sin usar)] |
| 4 |
+------------------------------------------------------------------------+
ASCII ART del flujo usando trampolín desde el hook handler:
6
+---------------------------------------------------------------------+
| |
| +- POC.EXE --+ 1 +------ Sleep -----+ 2 + - hook_handler - + |
| | CALL Sleep | ---> | JMP hook_handler | --> | ANYTHING :-) | |
+->| ... | +->| .... | 5 | CALL Trampolin |-|--+
+------------+ | | RET | --> | RET |-+ |
| +------------------+ +------------------+ |
4 | |
+------------------+ |
| |
| +---- Trampolin -----+ 3 |
| | - Instrucciones | <--------------------------------------------+
| | sobreescritas |
+- | - JMP Sleep+X |
+--------------------+
------[ 2.1.2 - Stack
Para entender las diferencias entre el E8 Method y el método Detour
convencional es necesario entender los diferentes estados de la pila en
cada pasa del Detour.
En los siguientes ASCII ART el cuadrado Stack muestra siempre en la cima el
valor de la cima de la pila (ESP) en cada paso.
Pasos:
1) El programa original (POC.exe) llama a Sleep, se introduce la dirección
de la siguiente instrucción al CALL para volver con la instrucción RET:
Tags:
+- POC.EXE --+ 1 +---- Stack ----+
| CALL Sleep | ---> | Address of LE |
LE: | ... | | .... |
+------------+ | .... |
| .... |
| .... |
+---------------+
2) La primera instrucción de Sleep es un salto hacia el hook handler para
dicha API, al ser un JMP la pila queda intacta:
Tags:
+------ Sleep -----+ 2 +---- Stack ----+
| JMP hook_handler | --> | Address of LE |
| ... | | .... |
+------------------+ | .... |
| .... |
| .... |
+---------------+
3) El hook handler realiza las acciones para las que ha sido programado y
retorna al programa original como lo haría la API, sacando la dirección
de retorno de la pila.
Sleep.
Tags:
+ - hook_handler - + +---- Stack ----+
| ANYTHING :-) | 3 | Address of LE |
| RET | ---> | .... |
+------------------+ | .... |
| .... |
| .... |
+---------------+
Nota: En el ejemplo no se ha ilustrado el uso del trampolín, pero sería
como un CALL y RET normal.
------[ 2.2 - E8 Method
Al contrario que el método Detour convencional, E8 Method se basa en
introducir un hook sobreescribiendo la/s primera/s instrucción/es
original/es con un CALL al hook handler programado para todos los hooks.
Cuando se hace un hook también se crea un area en memoria llamada
"trampolín" igual a la del método Detour convencional, la cual tiene las
instrucciones sobreescritas por el CALL y un salto a la siguiente
instrucción no sobreescrita del código original. El trampolín es usado por
el hook handler el cual usa si desea ejecutar al area original donde se ha
introducido cualquiera de los hook. Para simplificar el documento se asume
que el area donde se introduce el hook es siempre una API, o mejor dicho
un área en la que se ha entrado mediante un CALL del código original.
Además el hook handler debe reparar la pila para que no quedé la dirección
de retorno introducida por el CALL que se usa como hook caller id.
------[ 2.2.1 - Flujo
El flujo que sigue E8 Method puede ser de dos tipos al igual que el Detours
convencional:
1) Sin usar el trampolín desde el hook handler:
a) Se ha introducido un hook de tipo E8 Method en la API Sleep de
kernel32.dll
b) El programa original (POC.exe) llama a Sleep
c) La primera instrucción de Sleep es un CALL hacia el hook
handler para todos los hooks.
d) El hook handler realiza las acciones para las que ha sido
programado y retorna al programa original como lo haría la API
Sleep en este caso.
1) Usando el trampolín desde el hook handler:
a) Se ha introducido un hook de tipo Detour en la API Sleep de
kernel32.dll
b) El programa original (POC.exe) llama a Sleep
c) La primera instrucción de Sleep es un CALL hacia el hook
handler para todos los hooks.
d) El hook handler realiza las acciones para las que ha sido
programado, llamando en un determinado momento a la API Sleep
original usando el trampolín. Finalmente retorna al programa
original como lo haría la API Sleep en este caso.
ASCII ART del flujo sin usar trampolín desde el hook handler, E8 Method:
+- POC.EXE --+ 1 +- Sleep --+ 2 + --- ooh ---- +
| CALL Sleep | ---> | CALL ooh | --> | ANYTHING |
+->| ... | | .... | | stack_repair | 3
| +------------+ | RET | | RET |----+
| +----------+ +--------------+ |
| [Trampolín (sin usar)] |
| 4 |
+------------------------------------------------------------+
ASCII ART del flujo usando trampolín desde el hook handler, E8 Method:
6
+---------------------------------------------------------------------+
| |
| +- POC.EXE --+ 1 +------ Sleep -----+ 2 + ------ ooh ----- + |
| | CALL Sleep | ---> | CALL ooh | --> | ANYTHING :-) | |
+->| ... | +->| .... | 5 | stack_repair | |
+------------+ | | RET | --> | CALL Trampolin |-|--+
| +------------------+ | RET |-+ |
4 | +------------------+ |
+------------------+ |
| |
| +---- Trampolin -----+ 3 |
| | - Instrucciones | <--------------------------------------------+
| | sobreescritas |
+- | - JMP Sleep+X |
+--------------------+
------[ 2.2.2 - Stack
En E8 Method la pila no se comporta del mismo modo que en el método Detours
convencional, en ella se introduce el hook caller ID que es la dirección
de retorno de un CALL.
En los siguientes ASCII ART el cuadrado Stack muestra siempre en la cima el
valor de la cima de la pila (ESP) en cada paso.
Las etapas de la pila son:
1) El programa original (POC.exe) llama a Sleep, se introduce la dirección
de la siguiente instrucción al CALL para volver con la instrucción RET:
Tags:
+- POC.EXE --+ 1 +---- Stack ----+
| CALL Sleep | ---> | Address of LE |
LE: | ... | | .... |
+------------+ | .... |
| .... |
| .... |
+---------------+
2) La primera instrucción de Sleep es un CALL hacia el hook handler para
dicha API, al ser un CALL se introduce en la pila la dirección de la
siguiente instrucción:
Tags:
+-- Sleep -+ 2 +---- Stack ----+
| CALL ooh | --> | Address of LO |
LO: | ... | | Address of LE |
+----------+ | .... |
| .... |
| .... |
+---------------+
3) En esta etapa ooh usa " Address of LO" como "hook_caller ID" y luego
lo saca de la pila para que pueda continuarse el flujo normal como en
el método Detour convencional:
Tags:
+ ---- ooh ---- + +---- Stack ----+
| ANYTHING :-) | | Address of LE |
| stack_repair | 3 | .... |
| RET | ---> | .... |
+---------------+ | .... |
| .... |
+---------------+
Nota: En el ejemplo no se ha ilustrado el uso del trampolín, pero sería
como un CALL y RET normal.
------[ 2.3 - Diferencias
Solo existen tres diferencias entre el método Detours convencional y E8
Method:
1) En vez de un salto normal al hook handler se usa uno de tipo CALL.
2) En vez de un hook handler para cada hook se usará el mismo hook
handler para todos los hooks.
3) Se debe extraer el hook caller ID introducido por el CALL que llama
al hook handler de la pila para que no moleste y pueda ser todo
como en el método Detours convencional.
------[ 2.4 - Safe Hook Handler
He definido Safe Hook Handler como un hook con dos características, debe
ser:
1) Invisible para el código legítimo: no alterar flags ni registros,
no ser localizable usando estructuras internas del SO...
2) Evitar problemas de seguridad: sin stack buffers overflows al llamar
desde el hook handler a un area hookeada...
------[ 2.4.1 - Siendo invisible
Para conseguir o intentar ser invisible para el código legítimo, es
necesario tener en cuenta:
1) Evidencias de que existe un hook basadas en la arquitectura del
microprocesador: registros, flags ...
2) Evidencias de que existe un hook no basadas en la arquitectura del
microprocesador. Por ejemplo, un hook handler en Windows
implementado en una DLL, para evitar una búsqueda usando el PEB, se
podría desenlazar la DLL de la lista LDR_MODULE.
------[ 2.4.1.1 - Evidencias basadas en el microprocesador
Puede ser útil guardar toda la información relativa al estado del
microprocesador una vez es llamado el hook handler la cual será restaurada
una vez se devuelva el control al código original.
Dos de las cosas más importantes a ser guardadas son los registros del
microprocesador y el estado de los flags. Un ejemplo real es cuando el
Microsoft Visual Studio introduce la comprobación de corrupción de pila,
siendo una llamada a Sleep de kernel32.dll en ensamblador:
Código: Seleccionar todo
MOV ESI,ESP
PUSH 1388
CALL DWORD PTR DS:[<&KERNEL32.Sleep>]
CMP ESI,ESP
valor de ESI. Si introducimos un hook en Sleep que modifica ESI y no se
restaura al valor original antes de devolver el control al código original
el programa causará una excepción abortando la ejecución.
Se podrían encontrar algún sistema de protección de software o malware que
aproveche ciertos flags cuando se llama a una API o registros, de tal forma
que si nuestro hook handler no lo hace igual será detectado.
Así que tenemos dos problemas y dos soluciones posibles:
1º Problema: Comportamiento del API legítima usando como comprobación
registros o flags del microprocesador que no modifique.
Solución: Cuando es llamado el hook handler se deben guardar todos los
flags y registros que serán restaurados cuando se devuelva el
control al código original.
2º Problema: Comportamiento del API legítima usando como comprobación
registros o flags del microprocesador que modifique.
Solución: El hook handler debe llamar a la API original con los
registros y flags iguales a los del código original, después
de la llamada se debe guardar todos los flags y registros que
serán restaurados cuando se devuelva el control al código
original. En x86 sería suficiente con un PUSHAD y un PUSHFD
cuando se llame al hook handler, cuando se llama a la API
un POPFD y un POPAD y después de la llamada otro PUSHAD y
PUSHFD, por último, cuando se devuelva el control al código
original de nuevo hacer otro POPFD y POPAD de los valores que
devolvió la llamada a la API.
El primer problema es fácil de solucionar, solo es necesario crear un
buffer con los flags y los registros cuando el hook handler es llamado sin
mayores consecuencias, sin embargo el segundo problema conlleva mayores
consecuencias, debido a que debemos ejecutar la API como originalmente lo
haría el código original y eso puede no ser conveniente. Otra posible
solución es analizar como se comporta internamente la API y virtualizar
las evidencias de una llamada con los argumentos del código original.
Solución al primer problema, dado el caso real del Visual Studio mencionado
anteriormente:
Tags:
+- POC.EXE ----+
| MOV ESI, ESP |
| PUSH 0 | 1 +---- Sleep ----+ 2 +- ooh ------+
| CALL Sleep | ---> | CALL ooh | --> | PUSHAD |
LE: | CMP ESI, ESP | <-+ | .... | | PUSHFD |
+--------------+ | | .... | | ADD ESI, 9 |
| | .... | | POPFD |
| | .... | | POPAD |
| +---------------+ +-- | RET TO LE |
| 3 | +------------+
+-----------------------+
Para ilustrar el segundo problema imaginemos que cuando se hace una llamada
verdadera a Sleep con EAX = 69, se devuelve en EDX el valor 7C91E4F4, como
virtualizar las evidencias puede ser tedioso, el siguiente ASCII ART
muestra la opción de llamar a la rutina original:
Tags:
+- POC.EXE ----+
| MOV EAX, 69 |
| PUSH 0 | 1 +---- Sleep ----+ 2 +- ooh ------+
| CALL Sleep | ---> | CALL ooh | --> | PUSHAD |
LE: | ... | <-+ | .... | | PUSHFD |
+--------------+ | | .... | | MOV EAX, 0 |
| | .... | | POPFD |
| | .... | | POPAD |
| +---------------+ | CALL Sleep |
| | PUSHAD |
| | PUSHFD |
| | ANYTHING |
| | POPFD |
| | POPAD |
| +-- | RET TO LE |
| 3 | +------------+
+-----------------------+
Nota: El CALL Sleep de ooh es una llamada a la rutina original (trampolín).
La mejor solución es la segunda ya que simulamos una llamada a la API
original que es lo que haría el programa originalmente, la primera solución
solo vale en caso de que no se base en comportamientos internos.
------[ 2.4.1.2 - Evidencias no basadas en el microprocesador
Pueden ser muchas y de muchas maneras, dependen del Sistema Operativo, a
modo de ejemplo listaré algunas directicres de forma genérica:
1) En caso de que el hook handler se encuentre en una bibilioteca:
a) Borrar entrada de la biblioteca en listas que se creen en
runtime con la biblioteca, sin que deje la biblioteca de
funcionar.
b) Si se lee la memoria página a página, ocultar de alguna forma
las páginas donde se encuentre la biblioteca.
c) .......
z) ..........
2) .................
99) ................
------[ 2.4.2 - Problemas de seguridad
Los principales problemas de seguridad se pueden divir en dos:
1) Llamada a una API hookeada - stack buffer overflow.
2) Otros problemas debidos a mala programación del hook handler.
------[ 2.4.2.1 - Llamada a una API hookeada
Si desde el hook handler se llama a una API hookeada se entrará en un bucle
infinito que causará un stack buffer overflow. Hay tres formas de afrontar
el problema:
1) No llamar en el hook handler a APIs hookeadas.
a) Si se desea por ejemplo poner hooks a todas las APIs del sistema
no se podrá llamar a ninguna, dado que se quiere decidir los
hooks en runtime ésta solución no es válida. Tampoco se podrá
llamar a otras APIs que a su vez llamen a APIs hookeadas o
similar.
2) Llamar al trampolín de las APIs hookeadas directamente.
a) Exige controlar que APIs han sido hookeadas y cuales no. Por
ejemplo en C/C++ se requeriría un array con las APIs a usar,
que será al que siempre se llame, si se pone en hook en dicha
API se deberá modificar el valor con el del trampolín. No se
podrá llamar a otras APIs que a su vez llamen a APIs hookeadas,
solo se podrá usar el trampolín.
3) Crear un mecanismo que sea capaz de detectar cuando se llame a una
API hookeada si ha sido el hook handler, en cuyo caso se llamara al
API original o no, en cuyo caso se llamará al hook handler. Si esto
se consigue se podrá llamar directamente a las APIs originales sin
mayor preocupación, un ejemplo de ésta idea es el TDB (Thread
Deadlock Barrier)del easy-hook [R.8].
Las dos primeras soluciones exigen una metodología a la hora de programar
el hook handler, siendo la primera en muchos casos no viable debido a que
habría que programar demasiado, en la segunda alternativa puede pasar lo
mismo si se desea usar alguna biblioteca de alto nivel que podría llamar a
un API hookeada. Así pues la tercera alternativa permite programar al hook
handler como se desee, sin preocuparse si se llama a un API hookeada o a
otra biblioteca que llame a su vez a un API hookeada.
Ejemplo ASCII ART de la tercera alternativa:
4
+------------------------------------------+
Tags: | |
| +- POC.EXE ----+ |
| | PUSH 0 | 1 +- Sleep ----+ 2 | +- ooh ------+
| | CALL Sleep | ---> | CALL check | -+ | | CALL check |
LE: +>| CMP ESI, ESP | | .... | | +-| RET TO LE |<-+
+--------------+ | .... | | +------------+ |
| .... | | |
| .... | | |
+------------+ | |
| 3 |
| |
+----------------------------+ |
| +------------- check ----------------------+ |
+->| If (caller == ooh) CALL Sleep_Trampolin | |
| else CALL ooh |---+
+------------------------------------------+
check para que funcione de forma ideal no solo tiene que leer el valor que
introduce el CALL para saber de donde viene, es necesario que tenga algún
mecanismo tipo "marca" por cada hilo, ejemplo: un hilo ha llamado a una API
hookeada, si ya está marcada esa API para ese hilo se llama a la API
original, en caso contrario, se marca la API y se llama al hook handler,
una vez se haya ejecutado el hook handler se quita la marca la API. Esto
solo es un ejemplo para mostrar la idea, a la hora de implementar la idea
se debe tener en cuenta cualquier tipo de condición de carrera.
------[ 2.4.2.2 - Mala programación hook handler
Hacer el hook handler en ensamblador puede no ser viable, queriendo usar
tecnologías como C/C++. El problema de hacerlo en ensamblador es que se
necesitará más código y quizá haya más errores que vuelvan inestable el
hook teniendo en cuenta todo lo que implica un Safe Hook Handler. Por otro
lado al programar el hook handler en C/C++ no se puede controlar de forma
ANSI / ISO lo que pasa a bajo nivel (inline assembly) y en caso de que se
pudiera el código se puede volver no mantenible. Las posibles soluciones
para este problema son:
1) Programar todo en ensamblador teniendo mucho cuidado.
2) Programar en C/C++ usando inline assembly o enlazando con objetos
creados en ensamblador para ajustes de pila y otros asuntos de bajo
nivel necesarios para un hook.
3) Crear una pequeña parte en ensamblador que antes de llamar al hook
handler guarde todo lo necesario para que el hook handler pueda
obtener los datos necesarios (hook caller id, argumentos...) y que
una vez se ejecute el hook handler programado en C/C++ con la
información guardada al principio se pueda reajustar todos los
parámetros para un correcto funcionamiento del programa original.
El método más recomendable es el tercero, ya que el hook handler se abstrae
de la parte de bajo nivel y puede hacer lo que necesite sin preocuparse
por la pila y otros asuntos.
Ejemplo de la tercera solución:
1) Se creará una estructura con los datos necesarios para restaurar el
flujo normal una vez acabada la ejecución del hook handler, también se
encontrará en dicha estructura la información de bajo nivel necesaria, como
dónde están los argumentos en la pila.
Un ejemplo de estos datos podrían ser:
1) Estado de los registros y flags del microprocesador, cosa que ya
se hacía para otro propósito que cuenta el capítulo "2.4.1.1 -
Evidencias basadas en el microprocesador". Gracias a esto nuestro
hook handler programado en C/C++ podrá alterar los registros y los
flags del microprocesador sin preocupaciones.
2) Dirección de donde se encuentran los argumentos en la pila, para que
el hook handler programado en C/C++ pueda obtenerlos.
3) El hook_caller ID para saber que API ha llamado al hook handler y
cuantos argumentos debe obtener de la pila.
4) Dónde se encuentra la dirección de retorno al código original.
La estructura en C podría ser algo así:
Código: Seleccionar todo
typedef struct handle_ooh_s
{
void * hook_caller_id;
void * registers_and_flags_saved;
void * args;
} handle_ooh_t;
en memoria debido a que antes del CALL del E8 Method, está la dirección de
retorno introducido por el CALL del código original.
Asumiendo que la pila cuando se llama al hook handler está en la cima con
el hook_caller ID (E8 Method), y después los argumentos de la pila, ejemplo
de la creación de la estructura:
Tags:
+-- Sleep -----+ 1 +--- ooh_asm --------------------+
| CALL ooh_asm | --> | PUSHAD |
LO: | ... | | PUSHFD |
+--------------+ | PUSH ESP_BEFORE_HOOK_CALLER_ID |
| PUSH_ESP_BEFORE_PUSHAD |
| PUSH HOOK_CALLER_ID |
| PUSH ESP |
| PUSH GARBAGE | 2
| JMP ooh | ---> ...
+--------------------------------+
Quedando en la cima de la pila un valor basura que simula un retorno para
el hook handler programado en C/C++ y después la dirección de memoria donde
se encuentra la estructura de bajo nivel.
2) El hook handler que tendrá un prototipo similar a "void hook_handler(
handle_ooh_t * handle )", usará unas interfaces programadas en ensamblador
para obtener la información necesaría del handle: obtener los argumentos,
el hook_caller ID etc.
Ejemplo de obtención del hook_caller ID:
Tags:
+-- ooh --------------------+ 1 +--- ooh_asm --------------------+
| GetHookCallerID( handle ) | --> | MOV EAX, handle.hook_caller_id |
LO: | ... | <-- | RET |
+---------------------------+ +--------------------------------+
Ejemplo de obtención de los argumentos para una API (concepto -
pseudocódigo):
+-- ooh -----------------------------------+ +--- ooh_asm -------------+
| id = GetHookCallerID( handle ) | | ... |
| GetArgs( handle, API[id].nr_args, args ) |-> | while ( i < nr_args ) |
+------------------------------------------+ | args = handle.args|
| ... |
| RET |
+-------------------------+
3) El retorno al código original desde el hook handler también se debe
hacer desde una interfaz de bajo nivel que obtenga los valores del PUSHAD y
PUSHFD y retorne como si no se hubiera llamado al hook handler. Esto
también se hace en "2.4.1.1 - Evidencias basadas en el microprocesador"
pero con otra motivación, además de hacerse también en el CALL a la API
original con otra motivación.
Ejemplo de retorno del hook handler desde C/C++:
+-- ooh --------+ +--- ooh_asm ---------------------------------+
| ... | | ... |
| Ret( handle ) |-> | MOV ESP, handle.registers_and_flags_saved |
+---------------+ | POPFD |
| POPAD |
| ADD ESP, 4 |
| RET |
+---------------------------------------------+
Nota: Se hace un ADD ESP, 4 por que justo antes del PUSHAD en memoria
estaba el hook_caller ID (E8 Method) y antes el valor de retorno al código
original.
------[ 2.5 - Conclusión
Para llevar acabo el E8 Method con un Safe Hook Handler, serán necesarios
los siguientes elementos:
1) Biblioteca Detour adaptada a E8 Method para crear un solo hook
handler para todos los hooks. ( Capítulo 2.1 y 2.2 ).
2) Una capa en ensamblador con dos propósitos:
a) Permitir programar el hook handler en un lenguaje que no sea
ensamblador como C/C++. ( Capítulo 2.4.2.2 ).
b) Evitar métodos de detección de hooks o anomalías, como el
MOV EDI, ESP del visual studio para detectar corrupciones de
pila (basados en la arquitectura del microprocesador).
( Capítulo 2.4.1.1 ).
3) Método para ocultar evidencias de que existe un hook handler que
no tengan que ver con la arquitectura del microprocesador
( Capítulo 2.4.1.2 ).
4) Método para evitar stack buffers overflows y otros errores cuando
se llama desde el hook handler a una API ya hookeada. ( Capítulo
2.4.2.1 ).