#Tecnicas de ganchos con las funciones de Windows
#Autor: MachineDramon
Ya que este articulo es un poquito grande os dejo al inicio un indice para que se orienten mejor de lo que se trata este articulo :).
=====[ 2. Indice ]=====================
1.0.0.0 Contenidos
2.0.0.0 Introduccion
3.0.0.0 Metodos de enganche
3.1.0.0 Ganchos antes de la ejecucion
3.2.0.0 Enganchar durante la ejecucion
3.2.1.0 Enganchar nuestro propio proceso usando IAT
3.2.2.0 Enganchar nuestro propio proceso reescribiendo el entry point
3.2.3.0 Enganchar sin modificar las funciones originales
3.2.4.0 Enganchando otros procesos
3.2.4.1 Inyeccion DLL
3.2.4.2 Codigo independiente
3.2.4.3 Cambio Raw
4.0.0.0 Finalizando
=====[ 2. Introduccion ]=====================
Este texto trata las tecnicas de enganchar las funciones de la API en
Windows. Todos los ejemplos aqui descritos funcionan perfectamente en sistemas
basados en tecnologia NT version NT 4.0 y superior ( windows NT 4.0,
Windows 2000 , Windows XP, Windows 2003). Probablemente tambien funcionara en
futuros sistemas Windows.
Deberias familiarzarte con procesos en windows, esamblador, estructura
de ficheros PE y algunas funciones de la API , para comprender enteramente el
texto.
Cuando usamos el termino "Enganchar la API" aqui, me refiero al cambio
completo de la API. Asi, cuando una API enganchada es llamada, nuestro codigo
es ejecutado inmediatamente. No trato solamente los casos de monitorizar la
API. Escribire todo sobre esta tecnica.
=====[ 3. Tecnicas de ganchos ]=====================
Nuestra meta es normalmente reemplazar el codigo de alguna funcion con
nuestro codigo. Este problema puede ser solucionada a veces antes de ejecutar
el proceso. Esto puede hacerse la mayoria de las veces con codigo con
privilegios de usuario y la meta es, por ejemplo, cambiar el comportamiento
del programa. Un ejemplo de esto puede ser crackear una aplicacion: un programa
que pide el cd original al iniciarse (como ocurria en el juego Atlantis) y
nosotros queremos ejecutarlo sin CD. Si modificamos la funcion que compueba que
el tipo de disco sea cdrom, podemos ejecutarlo desde el disco duro.
Esto no puede hacerse o no queremos hacerlo cuando queremos enganchar
procesos de sistema (como servicios) o en el caso en que no sepamos el proceso
victima. Entonces tendremos que usar la tecnica de ganchos en ejecucion.
Ejemplos de esta tecnica son los rootkits o los virus con tecnicas
anti-antivirus.
=====[ 3.1 Ganchos antes de la ejecucion ]=====================
Se trata de cambios del modulo fisico (normalmente .exe o .dll) en
donde esta la funcion que nosotros queremos cambiar. Tenemos por lo menos tres
posibilidades de hacer esto.
La primera es encontrar el punto de entrada de esa funcion y
basicamente reescribir el codigo. Esto esta limitado por el tamaсo de la
funcion pero siempre podemos cargar otro modulo dinamicamente (API LoadLibrary)
y asi tendriamos suficiente espacio.
Las funciones del kernel (kernel32.dll) pueden ser usadas en todos los
casos porque todos los procesos en windows tienen su propia copia de este
modulo. Otra ventaja es cuando sabemos en que SO sera modificado el modulo.
Podemos usar directamente un puntero a la funcion, en este caso seria la
direccion de LoadLibraryA. Esto es asi porque la direccion del modulo kernel en
memoria es estatica para una determinada version de Windows.
Tambien podemos hacer uso del comportamiento de los modulos cargados
dinamicamente, por el cual su parte de inicializacion es ejecutada
inmediatamente despues de cargar el modulo en memoria. En la parte de
inicializacion de un nuevo modulo no estamos limitados.
La segunda posibilidad de reemplazar funciones en un modulo reside en
la extension. Entonces tenemos que elegir entre reemplazar los 5 primeros bytes
con un salto relativo o reescribir la IAT. En el caso de poner un salto
relativo, redirigiria la ejecucion del modulo a nuestro codigo. En el otro
caso, cuando una es llamada una funcion cuyo registro de IAT ha sido
modificado, nuestro codigo seria ejecutado inmediatamente despues de esta
llamada. Pero cambiar la extension un modulo no es tarea facil porque hay que
controlar muy bien las cabeceras.
(Nota del traductor:
no se refiere a la extension de fichero .exe o .dll eh? se refiere al tamaсo
que ocupa, que reside en la cabecera del modulo)
La tercera posibilidad es reemplazar el modulo entero. Es decir creamos
nuestra propia version del modulo qu puede cargar el modulo original y llamar
las funciones que no deseamos modificar. Pero reescribimos las funciones que si
nos interesan. Este metodo no es util para modulos muy grandes pues aveces
exportan cientos de funciones.
=====[ 3.2 Ganchos en ejecucion ]=====================
Los ganchos antes de la ejecucion es mas bien para aplicaciones
concretas o un modulo concreto. Si reemplazamos un funcion de kernel32.dll o de
ntdll.dll obtendremos el comportamiento deseado en todos aquellos procesos que
sean ejecutados despues, aunque es dificil atinar con un codigo perfecto y
exacto para las funciones o modulos nuevos. Pero el principal problema es que
nuestros ganchos solo contemplaran los procesos ejecutados posteriormente (asi
pues para todos los procesos tendriamos que reiniciar el sistema). Otro
problema es el acceso a estos ficheros, que en sistemas NT estan protegidos.
Los ganchos en ejecucion solucionan esto. Si bien este metodo requiere muchos
mas conocimientos, el resultado es perfecto.Los ganchos en ejecucion pueden
aplicarse solo a procesos para los que tengamos acceso a su zona de memoria.
Para escribir en esta zona usaremos la funcion APi WriteProcessMemory.
Comencemos por enganchar nuestro propio proceso durante la ejecucion.
=====[ 3.2.1 Enganchar nuestro propio proceso usando IAT ]=========
Hay muchas posibilidades aqui. En prinicpio te mostrare como enganchar
funciones mediante la reescritura de la IAT. Este dibujo muestra la estructura
de un fichero PE:
+-------------------------------+ - offset 0
| Cabecera MS DOS ("MZ")|
+-------------------------------+
| firma de PE ("PE")* * |
+-------------------------------+
| .text | - codigo de modulo* * |
| Codigo de programa* * * * * * |
| * * * * * * * * * * * * * * * |
+-------------------------------+
| .data | - datos iniciados * * | <--GLOBAL Y STATIC
| Datos Inicializados * * * * * |
| * * * * * * * * * * * * * * * |
+-------------------------------+
| .idata | - info sobre funciones importadas
| Import Table | y datos* * * * |
| * * * * * * * * * * * * * * * |
+-------------------------------+
| .edata | - info sobre funciones exportadas
| Export Table | y datos* * * * |
| * * * * * * * * * * * * * * * |
+-------------------------------+
| * * * * Debug symbols * * * * |
+-------------------------------+
La parte importante para nosotros aqui es la Import Address Table(IAT)
en la zona .idata . Esta parte contiene una descripcion de las funciones
importadas y sus direcciones. Ahora es importante saber como se crean los
ficheros PE. Cuando se llama un funcion de la API indirectamente en un
lenguaje de programacion (es decir llamar usando el nombre , en vez de su
direccion especifica en el SO) el compilador no las convierte en llamadas
directas al modulo, sino que las convierte en saltos a direcciones, y estas
direcciones se encuentran almacenadas en la IAT. Cuando el proceso se carga el
cargador de proceso del Windows se encarga de rellear la IAT para cada
sistema. Esta es la razon por la que el mismo binario funciona en versiones
distintas del windows, a pesar de que tienen distintas direcciones para un
mismo modulo. Por tanto si fuesemos capaz de encontrar una funcion, que
queremos enganchar, en la IAT, podriamos cambiar facilmente la direccion y la
instruccion jmp redirigiria el flujo de ejecucion a nuestro codigo. Cualquier
llamada despues de hacer esto ejecutaria nuestro codigo.
(Nota del traductor:
He traducido el ultimo parrafo intentando que se comprenda lo mejor posible,
pero aun asi prefiero dar una explicacion propia sobre la IAT.
Si queremos realizar una llamada a una api de windows podemos escribir:
call direccion_de_la_api_en_memoria ( por ejemplo 0x07728188)
pero como en cada version de windows esa direccion cambia necesitamos de
alguna manera que escribiendo la misma instruccion se ejecute lo mismo
Lo que se hace es guardar estas direcciones en la IAT. La IAT es rellenada
por el So al crear el PE. La forma de acceder a las direcciones de la IAT es
la siguiente:
-------------------
call direccion api1
......
codigo
......
direccion:
jmp dword ptr [IAT]
--------------------
Asi un PE funciona en cualquier windows.
La ventaja de este metodo es que el resultado es perfecto. La desventaja es
que para cambiar el comportamiento de una funcion, deberiamos enganchar
varias funciones (me explico, si queremos cambiar el comportamiento de un
programa en la busqueda de un fichero tendremos que cambiar FindFirstFile y
FindNextFile, pero estas funciones tienen version ANSI y WIDE, asique
tendremos que cambiar FindFirstFileA, FindNextFileA, FindFirstFileW y
FindNextFileW en la IAT. Y aun quedan otras como FindFirstFileExA y su version
WIDE FindFirstFileExW que son llamadas por las funciones comentadas
previamente. Sabemos que FindFirstFileW llama a FindFirstFileExW pero esto se
hace directamente , sin usar la IAT. Y aun algunas otras. Hay por ejemplo
funciones ShellApi como SHGetDesktopFolder que tambien llaman directamente a
FindFirstFileW o FindFirstFileExW). Pero si lo conseguimos con todas ellas el
resultado sera perfecto. Podemos usar ImageDirectoryEntryToData de
imagehlp.dll para encontrar la IAT facilmente.
PVOID ImageDirectoryEntryToData(
IN LPVOID Base,
IN BOOLEAN MappedAsImage,
IN USHORT DirectoryEntry,
OUT PULONG Size
)
Usaremos Instance de nuestra aplicacion como Base
(Instance se obtiene con una llamada a GetModuleHandle:
hInstance = GetModuleHandleA(NULL)
), y como DirectoryEntry usaremos la constante IMAGE_DIRECTORY_ENTRY_IMPORT.
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1
El resultado de esta funcion es un puntero al primer registro de la IAT.
Los registros de la IAT son estructuras que estan definidas como
IMAGE_IMPORT_DESCRIPTOR.
Asi el resultado es un puntero a IMAGE_IMPORT_DESCRIPTOR.
typedef struct _IMAGE_THUNK_DATA {
union {
PBYTE ForwarderString
PDWORD Function
DWORD Ordinal
PIMAGE_IMPORT_BY_NAME AddressOfData
}
} IMAGE_THUNK_DATA,*PIMAGE_THUNK_DATA
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics
PIMAGE_THUNK_DATA OriginalFirstThunk
}
DWORD TimeDateStamp
DWORD ForwarderChain
DWORD Name
PIMAGE_THUNK_DATA FirstThunk
} IMAGE_IMPORT_DESCRIPTOR,*PIMAGE_IMPORT_DESCRIPTOR
El valor de Name en IMAGE_IMPORT_DESCRIPTOR es una referencia al
nombre del modulo. Si queremos enganchar una funcion, por ejemplo del
kernel32.dll, tenemos que encontrar aquella entrada de la tabla que
corresponde al descriptor de kernel32.dll.
NOTA del traductor:
Aunque Holy_father lo da por supuesto, prefiero aclarar algo antes de
continuar, me refiero a la forma de recorrer la tabla. La IAT parece ser que
esta implementada como un vector estatico, donde cada casilla contiene la
direccion del descriptor. Por tanto si tenemos la direccion de la primera
entrada de la tabla, que nos la ha devuelto ImageDirectoryEntryToData, la
direccion de la siguiente entrada se incrementa en uno, y asi podemos
recorrer la tabla)
Llamaremos a ImageDirectoryEntryToData en un principio e intentaremos
encontrar aquel descriptor con Name "kernel32.dll" ( puede haber mas de un
descriptor con este nombre). Finalmente tendremos que encontrar nuestra
funcion en la lista de todas las funciones de ese descriptor (la direccion
de nuestra funcion la obtenemos con GetProcAddress). Si la encontramos
debemos usar VirtualProtect para cambiar la proteccion de pagina de memoria
y despues de esto podremos escribir en esta parte de memoria. Despues de
escribir la direccion debemos restaurar la proteccion original. Antes de
llamar a VirtualProtect tenemos que saber cierta informacion sobre esta
pagina de memoria. Esta info se obtiene con VirtualQuery. Podemos aсadir
algunas pruebas en caso de algunas llamadas fallen ( por ejemplo no
continuar si la primera llamada a VirtualProtect falla, etc)
PCSTR pszHookModName = "kernel32.dll",pszSleepName = "Sleep"
HMODULE hKernel = GetModuleHandle(pszHookModName)
PROC pfnNew = (PROC)0x12345678, //la nueva direccion aqui
pfnHookAPIAddr = GetProcAddress(hKernel,pszSleepName)
ULONG ulSize
PIMAGE_IMPORT_DESCRIPTOR pImportDesc =
(PIMAGE_IMPORT_DESCRIPTOR)ImageDirectoryEntryToData(
hInstance,
TRUE,
IMAGE_DIRECTORY_ENTRY_IMPORT,
&ulSize
)
while (pImportDesc->Name)
{
PSTR pszModName = (PSTR)((PBYTE) hInstance + pImportDesc->Name)
if (stricmp(pszModName, pszHookModName) == 0)
break
pImportDesc++
}
PIMAGE_THUNK_DATA pThunk =
(PIMAGE_THUNK_DATA)((PBYTE) hInstance + pImportDesc->FirstThunk)
while (pThunk->u1.Function)
{
PROC* ppfn = (PROC*) &pThunk->u1.Function
BOOL bFound = (*ppfn == pfnHookAPIAddr)
if (bFound)
{
MEMORY_BASIC_INFORMATION mbi
VirtualQuery(
ppfn,
&mbi,
sizeof(MEMORY_BASIC_INFORMATION)
)
VirtualProtect(
mbi.BaseAddress,
mbi.RegionSize,
PAGE_READWRITE,
&mbi.Protect)
)
*ppfn = *pfnNew
DWORD dwOldProtect
VirtualProtect(
mbi.BaseAddress,
mbi.RegionSize,
mbi.Protect,
&dwOldProtect
)
break
}
pThunk++
}
El resultado de una llamada a Sleep(1000) podria ser:
00407BD8: 68E8030000 push 0000003E8h
00407BDD: E812FAFFFF call Sleep
........
codigo
........
Sleep: es es el salto a la IAT
004075F4: FF25BCA14000 jmp dword ptr [00040A1BCh]
tabla original:
0040A1BC: 79 67 E8 77 00 00 00 00
nueva tabla:
0040A1BC: 78 56 34 12 00 00 00 00
Asique al final el salto es a 0x12345678.
=====[ 3.2.2 Enganchar nuestro propio proceso reescribiendo el entry point ]===
El metodo de reescribir primero unas intrucciones en el punto de
entrada de la funcion es realmente simple. Como en el caso de reescribir la
IAT, tenemos que cambiar la proteccion de pagina antes de nada. Aqui seran los
5 primeros bytes de la funcion que queremos enganchar. Despues tendremos que
localizar memoria dinamica para la estructura MEMORY_BASIC_INFORMATION. El
comienzo de la funcion lo obtenemos con GetProcAddres. En esta direccion
insertaremos un salto relativo a nuestro codigo. El siguiente programa llama a
Sleep(5000) (espera 5 segundos), entonces la funcion Sleep es enganchada y
redirigida a new_sleep, y finalmente llama de nuevo a Sleep(5000). Como la
nueva funcion new_sleep no hace nada y termina inmediatamente el programa
entero tardara solo 5 segundos en lugar de 10.
(NOTA del traductor:
----------------------------
Holy_father ha puesto este codigo en win32asm. El codigo se
entiende bien pero lo voy a poner primero en C porque creo que se queda uno
con una vision mas general en la primera lectura. El codigo lo escribo sobre
la marcha mientras traduzco pero creo que esta bien. Luego puedes continuar
echarle un vistazo a como quedaria en win32asm:
.....
static void new_sleep()
{
......
}
.....
Sleep(5000)
HMODULE Hkernel=GetModuleHandleA("kernel32.dll")
FARPROC HSleep=GetProcAddress(Hkernel,"Sleep")
LPVOID pmbi=VirtualAlloc(0,sizeof(MEMORY_BASIC_INFORMATION)
,MEM_COMMIT,PAGE_READWRITE)
if (pmbi)
{
DWORD pinfo=VirtualQuery(HSleep,Hcode,
sizeof(MEMORY_BASIC_INFORMATION))
if (pinfo)
{
/*
como la funcion Sleep se acaba de ejecutar es probable que
aun este en cache asique vaciamos de la cache los 5 primeros
bytes de la funcion Sleep
*/
FlushInstructionCache(GetCurrentProcess(),HSleep,5)
//la estructura que hemos reservado es una mbi
//tenemos que poner mbi.Protect al valor devuelto por
//VirtualProtect en lpflOldProtect
//14h (= 20) es el desplazamiento de Protect dentro de la mbi
DWORD dwOldProtect=(DWORD)pmbi + 14h
PDWORD pOldProtect=(PDWORD)dOldProtect
DWORD dwsize=(DWORD)pmbi + 00ch
PDWORD psize=(PDWORD)dwsize
if (VirtualProtect( pmbi , *psize, PAGE_EXECUTE_READWRITE,
pOldProtect))
{
//el primer byte contieen el hex opcode de jmp
PWORD pSleep=(PWORD)HSleep;
(pSleep[0])= (BYTE)0E9h; //jmp;;
DWORD offset_jmp=(DWORD)(PBYTE)new_sleep-(DWORD)(PBYTE)Sleep-5;
//los siguientes contienen el offset;;
memcpy(&Sleep[1],&offset_jmp,4);
DWORD tmp_oldprotect;
VirtualProtect( pmbi , *psize, pOldProtect, &tmp_oldprotect);
VirtualFree(pmbi,0,MEM_RELEASE);
}
Sleep(5000);
ExitProcess(0);
=====[ 3.2.4 Other process hooking ]==========================
Ahora haremos algo practico con los ganchos en ejecucion. Quien
quiere enganchar su propio proceso? Esto servia para aprender la teoria
pero no es muy practico.Te mostrare tres metodos de enganchar otros procesos. Dos de ellos
usan CreateRemoteThread que esta solo en Windows con tecnologia NT. El problema
de los ganchos para mi no es tan interesante en versiones antiguas de windows.
Despues de todo intentare explicar un metodo que no lo probe, asique podria no
funcionar.
Primero veamos un poco sobre CreateRemoteThread. Como la ayuda de esta
funcion dice, crea un hilo nuevo en cualquier proceso y ejecuta su codigo.
HANDLE CreateRemoteThread(
HANDLE hProcess,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
DWORD dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId);
El manejador de hProcess lo conseguimos con OpenProcess. Aqui tenemos
que tener los privilegios necesarios. El puntero lpStartAddress apunta a un
lugar de memoria del procedo OBJETIVO donde esta la primera instruccion del
nuevo hilo. Como el nuevo hilo es creado en el proceso objetivo este estara
en la zona de memoria del proceso objetivo. El puntero lpParameter apunta a
un argumento que sera el del nuevo hilo.
;
;
=====[ 3.2.4.1 Inyeccion DLL]===================
Podemos ejecutar un nuevo hilo desde cualquier parte en la memoria de
un proceso objetivo. Esto es inutil amenos que tengamos codigo propio en el.
El primer metodo hace esto. Usa GetProcAddress para obtener la direcion actual
de LoadLibrary. Entonces pasa como lpStartAddress la direccion de LoadLibrary.
La funcion LoadLibrary tiene un solo parametro, igual que la funcion para nuevo
hilo en el proceso objetivo.
HINSTANCE LoadLibrary(
LPCTSTR lpLibFileName
);
Usaremos esta similidad de parametros y pasaremos el nombre la la DLL
como lpParameter. Despues de ejecutar el nuevo hilo, lpParameter contendra
lpLibFileName. Lo mas importante es el comportamiento descrito arriba.
Despues de cargar el nuevo modulo en la memoria del proceso victima, la parte
de inicializacion es ejecutada. Si colocamos funciones especificas que
enganchen lo que nosotros queramos en el modulo, ya esta todo hecho. Despues
de ejecutar la parte de inicializacion, el hilo no tendra nada que hacer y se
carrera. Pero nuestro modulo estara todavia en memoria. Ese metodo esta
curioso y es facil de implementar. Se le llama Inyeccion DLL. Pero si te pasa
como a mi, no te gusta tener que usar DLLs. Pero si no te importa tener
librerias esta es la forma mas facil y mas rapida (desde el punto de vista
del programador)
=====[ 3.2.4.2 Codigo Independiente ]==========================
Este metodo es muy dificil pero es impresionante. Codigo independiente
es el codigo sin direcciones estaticas. Todo es relativo en el hacia cualquier
otra parte del codigo propio. Este codigo es hecho la mayoria de las veces si
no sabemos la direccion donde este codigo sera ejecutado. Seguramente seria
posible obtener esta direccion y reprogramar nuestro codigo para que funconara
en la nueva direccion sin errores, pero esto seria incluso mas dificil que
haciendo codigo independiente. Un ejemplo de este tipo de codigo puede ser el
codigo de un virus. El virus que infecta ejecutables se aсade por si mismo al
ejecutable. En diferentes ejecutables el codigo del virus estara en sitios
diferente dependiendo de las estructuras, la longitud, etc
En principio tenemos que insertar nuestro codigo en un proceso
objetivo. Entonces la funcion CreateRemoteThread nos permitira ejecutar
nuestro codigo.Asi, al principio tenemos que obtener informacion sobre el
proceso victima y abrirlo con OpenProcess. La funcion VirtualAllocEx nos
reserva espacio en la zona de memoria del proceso victima. Finalmente usamos
WriteProcessMemory para escribir nuestro codigo en la memoria reservada y lo
ejecutamos. En CreateRemoteThread, lpStartAddress sera la direccion de la
memoria reservada y lpParameter puede ser lo que queramos. Me gusta este
metodo porque no necesitamos ficheros innecesarios.
=====[ 3.2.4.3 Cambio Raw; ]=============================
No hy una funcion CreateRemoteThread en versiones de windows
antiguas (sin NT). Asique no podemos usar esta funcion para los ganchos. Hay
probablmente mejores metodos para enganchar que el metodo que voy a contar. De
hecho nose si funcionara en la practica (uno nunca sabe cuando usa windows)
pero teoricamente esta todo correcto.
No necesitamos en absoluto tener nuestro codigo en el proceso victima
para enganchar sus funciones. Tenemos la funcion WriteProcessMemory (deberia
estar en todas las versiones de windows) y tenemos OpenProcess tambien. Lo
ultimo que necesitamos es VirtualProtectEx que permite cambiar el acceso a
paginas de memoria en otros procesos. No veo ninguna razon por la que no
podamos enganchar funciones directamente desde nuestro proceso.
=====[ 4. Finalizando ]======================
Este pequeсo manual termina aqui. Agradecere cualquier comentario que
describa metodos no mencionados de enganche, estoy seguro que hay un monton.
Tambien agradecere cualquier comentario en las partes que no estan descritas
tan detalladamente. Puedes enviarme codigo fuente; si quieres para los
problemas de ganchos que por ejemplo estuve demasiado vago para escribir. La
meta de este docmento es mostrar detalles de cada tecnica de ganchos. Espero
haber hecho parte de esto.
Agradecimieno especial a Z0MBiE por su trabajo, que asi no tuve que
escribir yo mismo y dedicar aсos estudiando tablas para conseguir la longitud
de instruccion.
(C) Mitosis 4 - GEDZAC LABS