En programación informática , el término hooking abarca una serie de técnicas utilizadas para alterar o aumentar el comportamiento de un sistema operativo , de aplicaciones o de otros componentes de software interceptando llamadas a funciones o mensajes o eventos que pasan entre componentes de software . El código que maneja dichas llamadas a funciones, eventos o mensajes interceptados se denomina hook .
Los métodos de enlace son de particular importancia en el patrón de método de plantilla , donde el código común en una clase abstracta se puede ampliar con código personalizado en una subclase. En este caso, cada método de enlace se define en la clase abstracta con una implementación vacía que luego permite que se suministre una implementación diferente en cada subclase concreta.
El hooking se utiliza para muchos propósitos, entre ellos la depuración y la ampliación de funcionalidades. Algunos ejemplos pueden ser la interceptación de mensajes de eventos del teclado o del ratón antes de que lleguen a una aplicación, o la interceptación de llamadas del sistema operativo para supervisar el comportamiento o modificar la función de una aplicación u otro componente. También se utiliza mucho en programas de evaluación comparativa, por ejemplo, la medición de la velocidad de cuadros en juegos 3D, donde la entrada y la salida se realizan mediante hooking.
El código malicioso también puede utilizar técnicas de enganche. Por ejemplo, los rootkits , programas que intentan hacerse invisibles falsificando el resultado de las llamadas a la API que, de otro modo, revelarían su existencia, suelen utilizar técnicas de enganche.
Por lo general, los ganchos se insertan mientras el software ya se está ejecutando, pero el enganche es una táctica que también se puede emplear antes de que se inicie la aplicación. Ambas técnicas se describen con mayor detalle a continuación.
El enganche se puede lograr modificando la fuente del ejecutable o la biblioteca antes de que se ejecute una aplicación, mediante técnicas de ingeniería inversa . Esto se utiliza normalmente para interceptar llamadas a funciones para supervisarlas o reemplazarlas por completo.
Por ejemplo, al utilizar un desensamblador , se puede encontrar el punto de entrada de una función dentro de un módulo . Luego, se puede modificar para cargar dinámicamente otro módulo de biblioteca y luego hacer que ejecute los métodos deseados dentro de esa biblioteca cargada. Si corresponde, otro enfoque relacionado con el que se puede lograr el enlace es modificando la tabla de importación de un ejecutable. Esta tabla se puede modificar para cargar cualquier módulo de biblioteca adicional, así como para cambiar el código externo que se invoca cuando la aplicación llama a una función.
Un método alternativo para lograr el enlace de funciones es interceptar las llamadas a funciones a través de una biblioteca contenedora . Una biblioteca contenedora es una versión de una biblioteca que carga una aplicación, con toda la misma funcionalidad de la biblioteca original que reemplazará. Es decir, todas las funciones a las que se puede acceder son esencialmente las mismas entre la biblioteca original y la de reemplazo. Esta biblioteca contenedora puede diseñarse para llamar a cualquiera de las funciones de la biblioteca original o reemplazarla con un conjunto de lógica completamente nuevo.
Los sistemas operativos y el software pueden proporcionar los medios para insertar fácilmente ganchos de eventos en tiempo de ejecución . Esto está disponible siempre que el proceso que inserta el gancho tenga los permisos suficientes para hacerlo. Microsoft Windows, por ejemplo, permite a los usuarios insertar ganchos que se pueden utilizar para procesar o modificar eventos del sistema y eventos de aplicación para cuadros de diálogo , barras de desplazamiento y menús , así como otros elementos. También permite que un gancho inserte, elimine, procese o modifique eventos del teclado y del mouse . Linux proporciona otro ejemplo en el que los ganchos se pueden utilizar de manera similar para procesar eventos de red dentro del núcleo a través de NetFilter .
Cuando no se proporciona dicha funcionalidad, una forma especial de enganche emplea la interceptación de las llamadas a funciones de biblioteca realizadas por un proceso. El enganche de funciones se implementa modificando las primeras instrucciones de código de la función de destino para saltar a un código inyectado. Alternativamente, en sistemas que utilizan el concepto de biblioteca compartida , la tabla de vectores de interrupción o la tabla de descriptores de importación se pueden modificar en la memoria. Esencialmente, estas tácticas emplean las mismas ideas que las de modificación de la fuente, pero en su lugar alteran las instrucciones y estructuras ubicadas en la memoria de un proceso una vez que ya se está ejecutando.
Siempre que una clase define/hereda una función virtual (o método), los compiladores añaden una variable miembro oculta a la clase que apunta a una tabla de métodos virtuales (VMT o Vtable). La mayoría de los compiladores colocan el puntero VMT oculto en los primeros 4 bytes de cada instancia de la clase. Una VMT es básicamente una matriz de punteros a todas las funciones virtuales que las instancias de la clase pueden llamar. En tiempo de ejecución, estos punteros se configuran para apuntar a las funciones correctas, porque en tiempo de compilación , aún no se sabe si se debe llamar a la función base o si se debe llamar a una versión anulada de la función de una clase derivada (lo que permite el polimorfismo ). Por lo tanto, las funciones virtuales se pueden enganchar reemplazando los punteros a ellas dentro de cualquier VMT que aparezcan. El código siguiente muestra un ejemplo de un gancho VMT típico en Microsoft Windows, escrito en C++. [1]
#include <iostream> #include "windows.h" using namespace std ; class VirtualClass { public : int number ; virtual void VirtualFn1 () //Esta es la función virtual que será enganchada. { cout << "VirtualFn1 llamada " << number ++ << " \n\n " ; } }; using VirtualFn1_t = void ( __thiscall * )( void * thisptr ); VirtualFn1_t orig_VirtualFn1 ; void __fastcall hkVirtualFn1 ( void * thisptr , int edx ) //Esta es nuestra función de gancho que haremos que el programa llame en lugar de la función VirtualFn1 original después de que se realice el enganche. { cout << "Función de gancho llamada" << " \n " ; orig_VirtualFn1 ( thisptr ); //Llamar a la función original. } int main () { VirtualClass * myClass = new VirtualClass (); //Crear un puntero a una instancia asignada dinámicamente de VirtualClass. void ** vTablePtr = * reinterpret_cast < void ***> ( myClass ); //Encontrar la dirección que apunta a la base del VMT de VirtualClass (que luego apunta a VirtualFn1) y almacenarla en vTablePtr. DWORD oldProtection ; VirtualProtect ( vTablePtr , 4 , PAGE_EXECUTE_READWRITE , & oldProtection ); //Elimina la protección de página al inicio de la VMT para que podamos sobrescribir su primer puntero. orig_VirtualFn1 = reinterpret_cast < VirtualFn1_t > ( * vTablePtr ); //Almacena el puntero a VirtualFn1 desde la VMT en una variable global para que se pueda acceder a él de nuevo más tarde después de que su entrada en la VMT haya sido sobrescrita con nuestra función de gancho. * vTablePtr = & hkVirtualFn1 ; //Sobrescribe el puntero a VirtualFn1 dentro de la tabla virtual con un puntero a nuestra función de gancho (hkVirtualFn1). VirtualProtect ( vTablePtr , 4 , oldProtection , 0 ); //Restaura la protección de página anterior. myClass -> VirtualFn1 (); //Llama a la función virtual desde nuestra instancia de clase. Debido a que ahora está enganchada, esto realmente llamará a nuestra función de gancho (hkVirtualFn1). myClass -> VirtualFn1 (); myClass -> VirtualFn1 (); delete myClass ; devuelve 0 ; }
Todas las funciones virtuales deben ser funciones miembro de la clase, y todas las funciones miembro de la clase (no estáticas) se llaman con la convención de llamada __thiscall (a menos que la función miembro tome una cantidad variable de argumentos, en cuyo caso se llama con __cdecl). La convención de llamada __thiscall pasa un puntero a la instancia de la clase que llama (comúnmente denominada puntero "this") a través del registro ECX (en la arquitectura x86). Por lo tanto, para que una función de enlace intercepte correctamente el puntero "this" que se pasa y lo tome como argumento, debe buscar en el registro ECX. En el ejemplo anterior, esto se hace configurando la función de enlace (hkVirtualFn1) para que use la convención de llamada __fastcall, que hace que la función de enlace busque en el registro ECX uno de sus argumentos.
Tenga en cuenta también que, en el ejemplo anterior, la función de gancho (hkVirtualFn1) no es en sí misma una función miembro, por lo que no puede utilizar la convención de llamada __thiscall. En su lugar, se debe utilizar __fastcall porque es la única otra convención de llamada que busca un argumento en el registro ECX.
El siguiente ejemplo se conectará a eventos de teclado en Microsoft Windows utilizando Microsoft .NET Framework .
utilizando System.Runtime.InteropServices ; espacio de nombres Ganchos ; clase pública KeyHook { /* Variables miembro */ protegido estático int Hook ; protegido estático LowLevelKeyboardDelegate Delegado ; protegido estático objeto de solo lectura Lock = nuevo objeto (); protegido estático bool IsRegistered = falso ; /* Importaciones de DLL */ [DllImport("user32")] privado estático externo int SetWindowsHookEx ( int idHook , LowLevelKeyboardDelegate lpfn , int hmod , int dwThreadId ); [DllImport("user32")] privado estático externo int CallNextHookEx ( int hHook , int nCode , int wParam , KBDLLHOOKSTRUCT lParam ); [DllImport("user32")] privado estático externo int UnhookWindowsHookEx ( int hHook ); /* Tipos y constantes */ delegado protegido int LowLevelKeyboardDelegate ( int nCode , int wParam , ref KBDLLHOOKSTRUCT lParam ); constante privada int HC_ACTION = 0 ; constante privada int WM_KEYDOWN = 0 x0100 ; constante privada int WM_KEYUP = 0 x0101 ; constante privada int WH_KEYBOARD_LL = 13 ; [StructLayout(LayoutKind.Sequential)] estructura pública KBDLLHOOKSTRUCT { int público vkCode ; int público scanCode ; int público flags ; int público time ; int público dwExtraInfo ; } / * Métodos * / static private int LowLevelKeyboardHandler ( int nCode , int wParam , ref KBDLLHOOKSTRUCT lParam ) { if ( nCode == HC_ACTION ) { if ( wParam == WM_KEYDOWN ) System.Console.Out.WriteLine ( " Tecla pulsada : " + lParam.vkCode ) ; else if ( wParam == WM_KEYUP ) System.Console.Out.WriteLine ( " Tecla pulsada : " + lParam.vkCode ) ; } return CallNextHookEx ( Hook , nCode , wParam , lParam ) ; } público estático bool RegisterHook () { bloqueo ( Bloqueo ) { si ( Está registrado ) devolver verdadero ; Delegado = LowLevelKeyboardHandler ; Gancho = SetWindowsHookEx ( WH_KEYBOARD_LL , Delegado , Marshal . GetHINSTANCE ( Sistema . Reflection . Assembly . GetExecutingAssembly (). GetModules ()[ 0 ] ). ToInt32 (), 0 ); si ( Hook != 0 ) devuelve IsRegistered = verdadero ; Delegado = nulo ; devuelve falso ; } } público estático bool UnregisterHook () { bloquear ( Bloquear ) { devolver IsRegistered = ( UnhookWindowsHookEx ( Gancho ) != 0 ); } } }
El código fuente siguiente es un ejemplo de un método de enganche de API/función que se engancha sobrescribiendo los primeros seis bytes de una función de destino con una instrucción JMP a una nueva función. El código se compila en un archivo DLL y luego se carga en el proceso de destino utilizando cualquier método de inyección de DLL . Utilizando una copia de seguridad de la función original, se pueden restaurar los primeros seis bytes nuevamente para que la llamada no se interrumpa. En este ejemplo, se engancha la función MessageBoxW de la API win32 . [2]
/* Esta idea se basa en el enfoque de chrom-lib, distribuido bajo la licencia GNU LGPL. Fuente de chrom-lib: https://github.com/linuxexp/chrom-lib Copyright (C) 2011 Raja Jamwal */ #include <windows.h> #define SIZE 6 typedef int ( WINAPI * pMessageBoxW )( HWND , LPCWSTR , LPCWSTR , UINT ); // Prototipo de cuadro de mensaje int WINAPI MyMessageBoxW ( HWND , LPCWSTR , LPCWSTR , UINT ); // Nuestro desvío void BeginRedirect ( LPVOID ); pMessageBoxW pOrigMBAddress = NULL ; // dirección del BYTE original oldBytes [ SIZE ] = { 0 }; // copia de seguridad BYTE JMP [ SIZE ] = { 0 }; // instrucción JMP de 6 bytes DWORD oldProtect , myProtect = PAGE_EXECUTE_READWRITE ; INT APIENTRY DllMain ( HMODULE hDLL , DWORD Reason , LPVOID Reserved ) { switch ( Reason ) { case DLL_PROCESS_ATTACH : // si está adjunto pOrigMBAddress = ( pMessageBoxW ) GetProcAddress ( GetModuleHandleA ( "user32.dll" ), // obtener la dirección del "MessageBoxW" original ); if ( pOrigMBAddress != NULL ) BeginRedirect ( MyMessageBoxW ); // iniciar desvío break ; caso DLL_PROCESS_DETACH : VirtualProtect (( LPVOID ) pOrigMBAddress , SIZE , myProtect , & oldProtect ); // asignar protección de lectura y escritura memcpy ( pOrigMBAddress , oldBytes , SIZE ); // restaurar copia de seguridad VirtualProtect (( LPVOID ) pOrigMBAddress , SIZE , oldProtect , & myProtect ); // restablecer protección caso DLL_THREAD_ATTACH : caso DLL_THREAD_DETACH : romper ; } devolver VERDADERO ; } void BeginRedirect ( LPVOID newFunction ) { BYTE tempJMP [ SIZE ] = { 0xE9 , 0x90 , 0x90 , 0x90 , 0x90 , 0xC3 }; // 0xE9 = JMP 0x90 = NOP 0xC3 = RET memcpy ( JMP , tempJMP , SIZE ); // almacena la instrucción jmp en JMP DWORD JMPSize = (( DWORD ) newFunction - ( DWORD ) pOrigMBAddress - 5 ); // calcula la distancia de salto VirtualProtect (( LPVOID ) pOrigMBAddress , SIZE , // asigna protección de lectura y escritura PAGE_EXECUTE_READWRITE , & oldProtect ); memcpy ( oldBytes , pOrigMBAddress , SIZE ); // hacer copia de seguridad memcpy ( & JMP [ 1 ], & JMPSize , 4 ); // rellenar los nop con la distancia de salto (JMP,distance(4bytes),RET) memcpy ( pOrigMBAddress , JMP , SIZE ); // establecer la instrucción de salto al principio de la función original VirtualProtect (( LPVOID ) pOrigMBAddress , SIZE , oldProtect , & myProtect ); // restablecer la protección } int WINAPI MyMessageBoxW ( HWND hWnd , LPCWSTR lpText , LPCWSTR lpCaption , UINT uiType ) { VirtualProtect (( LPVOID ) pOrigMBAddress , SIZE , myProtect , & oldProtect ); // asignar protección de lectura y escritura memcpy ( pOrigMBAddress , oldBytes , SIZE ); // restaurar copia de seguridad int retValue = MessageBoxW ( hWnd , lpText , lpCaption , uiType ); // obtener el valor de retorno de la función original memcpy ( pOrigMBAddress , JMP , SIZE ); // establecer la instrucción de salto nuevamente VirtualProtect (( LPVOID ) pOrigMBAddress , SIZE , oldProtect , & myProtect ); // restablecer protección return retValue ; // devolver el valor de retorno original }
Este ejemplo muestra cómo utilizar el enlace para alterar el tráfico de red en el kernel de Linux usando Netfilter .
#include <linux/módulo.h> #include <linux/kernel.h> #include <linux/skbuff.h> #include <linux/ip.h> #include <linux/tcp.h> #include <linux/in.h> #include <linux/netfilter.h> #include <linux/netfilter_ipv4.h> /* Puerto en el que queremos descartar paquetes */ static const uint16_t port = 25 ; /* Esta es la función de gancho en sí */ static unsigned int hook_func ( unsigned int hooknum , struct sk_buff ** pskb , const struct net_device * in , const struct net_device * out , int ( * okfn )( struct sk_buff * )) { struct iphdr * iph = ip_hdr ( * pskb ); struct tcphdr * tcph , tcpbuf ; si ( iph -> protocolo != IPPROTO_TCP ) devuelve NF_ACCEPT ; tcph = skb_header_pointer ( * pskb , ip_hdrlen ( * pskb ), sizeof ( * tcph ), & tcpbuf ); si ( tcph == NULL ) devuelve NF_ACCEPT ; devolver ( tcph -> dest == puerto ) ? NF_DROP : NF_ACCEPT ; } /* Se utiliza para registrar nuestra función de gancho */ static struct nf_hook_ops nfho = { . hook = hook_func , . hooknum = NF_IP_PRE_ROUTING , . pf = NFPROTO_IPV4 , . priority = NF_IP_PRI_FIRST , }; estático __init int my_init ( void ) { return nf_register_hook ( & nfho ); } static __exit void mi_salida ( void ) { nf_unregister_hook ( & nfho ); } módulo_init ( mi_init ); módulo_exit ( mi_salida );
El código siguiente demuestra cómo enganchar funciones que se importan desde otro módulo. Esto se puede utilizar para enganchar funciones en un proceso diferente del proceso de llamada. Para ello, el código debe compilarse en un archivo DLL y luego cargarse en el proceso de destino utilizando cualquier método de inyección de DLL . La ventaja de este método es que es menos detectable por el software antivirus y/o el software anti-trampas , uno podría convertirlo en un gancho externo que no haga uso de ninguna llamada maliciosa. El encabezado Portable Executable contiene la tabla de direcciones de importación (IAT), que se puede manipular como se muestra en la fuente a continuación. La fuente a continuación se ejecuta en Microsoft Windows.
#include <windows.h> typedef int ( __stdcall * pMessageBoxA ) ( HWND hWnd , LPCSTR lpText , LPCSTR lpCaption , UINT uType ); //Este es el 'tipo' de la llamada a MessageBoxA. pMessageBoxA RealMessageBoxA ; //Esto almacenará un puntero a la función original. void DetourIATptr ( const char * función , void * nuevafunción , HMODULE módulo ); int __stdcall NewMessageBoxA ( HWND hWnd , LPCSTR lpText , LPCSTR lpCaption , UINT uType ) { //Nuestra función falsa printf ( "La cadena enviada a MessageBoxA fue: %s \n " , lpText ); return RealMessageBoxA ( hWnd , lpText , lpCaption , uType ); //Llamar a la función real } int main ( int argc , CHAR * argv []) { DetourIATptr ( "MessageBoxA" ,( void * ) NewMessageBoxA , 0 ); //Enganchar la función MessageBoxA ( NULL , "Just A MessageBox" , "Just A MessageBox" , 0 ); //Llamar a la función -- esto invocará nuestro gancho falso. return 0 ; } void ** IATfind ( const char * function , HMODULE module ) { //Encuentra la entrada IAT (Tabla de direcciones de importación) específica para la función dada. int ip = 0 ; if ( module == 0 ) module = GetModuleHandle ( 0 ); PIMAGE_DOS_HEADER pImgDosHeaders = ( PIMAGE_DOS_HEADER ) module ; PIMAGE_NT_HEADERS pImgNTHeaders = ( PIMAGE_NT_HEADERS )(( LPBYTE ) pImgDosHeaders + pImgDosHeaders -> e_lfanew ); DESCRIPTOR_IMPORT_PIMAGE pImgImportDesc = ( DESCRIPTOR_IMPORT_PIMAGE )(( LPBYTE ) pImgDosHeaders + pImgNTHeaders -> Encabezado_opcional . Directorio_de_datos [ IMPORTACIÓN_ENTRADA_DIRECTORIO_IMAGE ]. Dirección_virtual ); if ( pImgDosHeaders -> e_magic != IMAGE_DOS_SIGNATURE ) printf ( "Error de libPE: e_magic no es una firma DOS válida \n " ); para ( DESCRIPTOR_IMPORT_IMAGEN * iid = pImgImportDesc ; iid -> Nombre != NULL ; iid ++ ) { para ( int funcIdx = 0 ; * ( funcIdx + ( LPVOID * )( iid -> FirstThunk + ( SIZE_T ) módulo )) != NULL ; funcIdx ++ ) { char * modFuncName = ( char * )( * ( funcIdx + ( SIZE_T * )( iid -> OriginalFirstThunk + ( SIZE_T ) módulo )) + ( SIZE_T ) módulo + 2 ); const uintptr_t nModFuncName = ( uintptr_t ) modFuncName ; bool isString = ! ( nModFuncName & ( sizeof ( nModFuncName ) == 4 ? 0x80000000 : 0x8000000000000000 )); si ( esCadena ) { si ( ! _stricmp ( función , modFuncName )) devolver funcIdx + ( LPVOID * )( iid -> FirstThunk + ( SIZE_T ) módulo ); } } } devolver 0 ; } void DetourIATptr ( const char * función , void * nuevafunción , HMODULE módulo ) { void ** funcptr = IATfind ( función , módulo ); if ( * funcptr == nuevafunción ) return ; DWORD oldrights , newrights = PAGE_READWRITE ; //Actualizar la protección a READWRITE VirtualProtect ( funcptr , sizeof ( LPVOID ), newrights , & oldrights ); RealMessageBoxA = ( pMessageBoxA ) * funcptr ; //Algunos compiladores requieren la conversión (como "MinGW"), aunque no estoy seguro acerca de MSVC * funcptr = newfunction ; //Restaurar los indicadores de protección de memoria antiguos. VirtualProtect ( funcptr , sizeof ( LPVOID ) , oldrights y newrights ); }
Hook
{{cite web}}
: |author=
tiene nombre genérico ( ayuda )