Está en la página 1de 2

Binary Hooking Problemas

Sábado 14 de mayo de 2011

La mayoría de los hooking engines binarios escriben un desvío en el punto de entrada de la función de
destino. Otros hooking engines parchean la tabla IAT, y así sucesivamente. Uno de los problemas al sobrescribir
el punto de entrada con una instrucción JMP es que necesita suficiente espacio para esa instrucción, por lo
general, solo bastarán 5 bytes.

¿Cómo deciden los algoritmos hooking cuánto es "suficiente"?

Bastante fácil, usan un desensamblador para consultar el tamaño de cada instrucción que escanean, por lo que si
el tamaño total de las instrucciones que se escanearon es superior a 5 bytes, ya están listas.

Como ejemplo, generalmente, las funciones comienzan con estas dos instrucciones:

PUSH EBP
MOV EBP, ESP

que toman solo 3 bytes. Y ya dijimos que necesitamos 5 bytes en total, para reemplazar las primeras
instrucciones con nuestra instrucción JMP. Por lo tanto, la exploración tendrá que continuar con la siguiente
instrucción más o menos, hasta que tengamos al menos 5 bytes.

Entonces, 5 bytes en x86 podrían contener de una instrucción a 5 instrucciones (donde cada una toma un solo
byte, obviamente). O incluso una sola instrucción cuyo tamaño es mayor a 5 bytes. (En x64, es posible que
necesite 12-14 bytes para un JMP completo, y esto solo empeora las cosas).

Está claro por qué necesitamos saber el tamaño de las instrucciones, puesto que sobrescribimos los primeros 5
bytes, debemos reubicarlos en otra ubicación, el trampolín. Allí queremos continuar la ejecución de la función
original que enganchamos y, por lo tanto, debemos continuar desde la siguiente instrucción que no hemos
anulado. Y no es necesariamente la instrucción en el desplazamiento 5 ... de lo contrario, podríamos continuar la
ejecución en medio de una instrucción, lo cual es bastante malo.

Los hooking engines cojos no usan desensambladores, solo tienen una tabla predefinida de instrucciones
prólogo populares. Si viene un código compilado diferente, no podrán enganchar una función. De todos modos,
también necesitamos un desensamblador por otra razón, para saber si alcanzamos una instrucción de punto
muerto, como: RET, INT 3, JMP, etc. Estos son hooking spoilers, porque si la primera instrucción de la función
de destino es simple RET (por lo tanto, la función no hace nada, deja de lado los efectos secundarios de la
memoria caché por ahora), o incluso una función de "return 0", que generalmente se traduce en "xor
eax,eax;ret", todavía toma solo 3 bytes y no podemos plantar un desvío. Así que nos encontramos tratando
de anular 5 bytes donde la función completa toma varios bytes (<5 bytes), y no podemos anular más allá de esa
instrucción ya que no sabemos qué hay allí. Puede ser el punto de entrada de otra función, los datos, un tubogan
NOP de no. El punto es que no se nos permite hacer eso y eventualmente no podemos enganchar la función,
falla.

Otro problema son las instrucciones de desplazamiento relativo. Supongamos que cualquiera de los primeros 5
bytes es una instrucción de bifurcación condicional, tendremos que reubicar esa instrucción. Por lo general, la
instrucción de bifurcación condicional es de solo 2 bytes. Y si los copiamos al trampolín, tendremos que
convertirlos en la variación más larga que es de 6 bytes y corregir el desplazamiento. Y eso funcionaría bien. En
x64, las instrucciones relativas RIP también son dolorosas, así como cualquier otra instrucción de
desplazamiento relativo que requiera una solución. Por lo tanto, hay una larga lista de esas y un buen motor de
hooking tiene que soportarlas todas, especialmente en x64, donde no hay un prólogo estándar para una función

Noté un caso específico donde WaitForSingleObject se está compilando con

XOR R8D, R8D


JMP short WaitForSingleObjectEx

Taller de Api hooking - 1


en x64 por supuesto; el XOR toma 3 bytes y el JMP es corto, que toma 2 bytes. Entonces, obtener un total de 5
bytes debería poder engancharlo (es totalmente legal), pero el motor de enganche que usé apestaba y no lo
permitía.

Entonces podría decir, ok, obtuve una solución genérica para eso, sigamos la rama incondicional y conectemos
ese punto. Entonces engancharé WaitForSingleObjectEx en su lugar, ¿verdad? Pero ahora llegaste al temido
problema de los entry points. Es posible que llamen por un punto de entrada diferente que nunca quisiste
conectar. Querías enganchar WaitForSingleObject y ahora terminas enganchando WaitForSingleObjectEx,
por lo que todas que llaman a WaitForSingleObject llegan a ti, eso es cierto. Además, ahora todas las llamadas
a WaitForSingleObjectEx llega también . Ese es un gran problema. Y nunca puedes darte cuenta del nombre de
quién fue llamada (con una solución rápida y legítima).

Lo curioso de la implementación de WaitForSingleObject es que fue seguido inmediatamente por una


tobogatn de alineación NOP, que nunca se ejecuta realmente, pero el hooking engine no puede usarla, porque no
sabe lo que sabemos. Entonces, las ramas incondicionales arruinan los hooking engine si aparecen antes de 5
bytes desde el entry point, y acabamos de ver que seguir la rama incondicional también puede arruinarnos con
un contexto incorrecto. Entonces, si haces eso, está mal.

¿Qué hacer entonces en tales casos? Porque obtuve algunas soluciones, aunque nada es perfecto.

Publicado en Algorithms, Assemb

Taller de Api hooking - 2

También podría gustarte