Tutorial 24: Ganchos de Windows

En este tutorial aprenderemos sobre los ganchos [hooks] en Windows. Los ganchos de Windows son muy poderosos. Con ellos, puedes "pescar" [pook] dentro de otros procesos e incluso modificar su conducta.
Bajar el ejemplo aquí.

Teoría:

Los ganchos de Windows pueden considerarse uno de los rasgos más poderosos de Windows. Con ellos podemos atrapar eventos que ocurrirán en tus procesos o en otros remotos. A través del "enganche" [hooking], dices cosas a Windows sobre una función, una función filtro llamada también procedimiento del gancho, que será llamado cada vez que ocurra un evento que te interese. Hay dos tipos de ellos: ganchos locales y ganchos remotos.

Cuando instales ganchos, recuerda que ellos afectan el sistema de desempeño. Los ganchos de ancho de sistema [system-wide] son los más notorios en este aspecto. Como TODOS los eventos relacionados serán dirigidos a través de tu función filtro, tu sistema puede ralentizarse un poco. Así que si quieres usar un gancho de ancho de sistema, deberías usarlo juiciosamente y desactivarlo tan pronto ya no lo necesites. También tienes una probabilidad más alta de quebrar [crashing] los otros procesos, ya que puedes mediar [meddle] con otros procesos y si algo va mal en tu función filtro, se pueden derribar los otros procesos y lanzarlos al olvido. Recuerda: el poder viene con responsabilidades.

Tienes que entender como trabaja un gancho antes de que puedas usarlo con eficiencia. Cuando creas un gancho, Windows crea una estructura de datos en la memoria, que contiene información sobre el gancho, y lo agrega a una lista enlazada de ganchos existentes. El gancho nuevo es agregado en frente de los ganchos antiguos. Cuando un evento ocurre, si instalas un gancho local, la función filtro en tu proceso es llamada de una manera directa. Pero si es un gancho remoto, el sistema debe inyectar el código para el procedimiento del gancho dentro del espacio de direcciones del(os) otro(s) proceso(s). Y el sistema puede hacer eso sólo si la función reside en una DLL. De esta manera, si quieres usar un gancho remoto, tu procedimiento de gancho debe residir en una DLL. Hay dos excepciones a esta regla: ganchos de grabación diaria [journal record] y ganchos de ejecución diaria [journal playback]. El procedimiento de gancho para estos dos ganchos debe residir en el hilo que instala los ganchos. La razón por la que debe ser así, es porque ambos ganchos tienen que ver con la intercepción de bajo-nivel de los eventos de entrada del hardware. Los eventos de entrada deben ser grabados/ejecutados [recorded/playbacked] en el orden que aparecen. Si el código de estos dos ganchos está en un DLL, los eventos de entrada pueden dispersarse entre varios hilos y es imposible saber su orden. Así que la solución es: el procedimiento de gancho de esos dos gancho deben estar solamente en un hilo, en el hilo que instala los ganchos.

Hay 14 tipos de ganchos:

Ahora que sabemos algo de teoría, podemos ver ahora cómo instalar/desinstalar los ganchos.
Para instalar un gancho, llamas a SetWindowsHookEx que tiene la siguiente sintaxis:

SetWindowsHookEx proto HookType:DWORD, pHookProc:DWORD, hInstance:DWORD, ThreadID:DWORD

Si la llamada tiene éxito, regresa el manejador de gancho en eax. Si no, regresa NULL. Debes salvar el manejador del gancho para poder desinstalarlo después.

Puedes desinstalar un gancho llamando a UnhookWindowsHookEx que sólo acepta un parámetro, el manejador del gancho que quieres desinstalar. Si la llamada tiene éxtito, regresa un valor diferente a cero en eax. En caso contrario, regresa NULL.

Ahora que sabes cómo instalar/desinstalar ganchos, podemos examinar el procedimiento de gancho.
El procedimiento de gancho será llamado cada vez que ocurre un evento asociado con el tipo de gancho que haz instalado. Por ejemplo, si instalas el gancho WH_MOUSE, cuando ocurre un evento de ratón, será llamado tu procedimiento de gancho. Independientemente del tipo de gancho que instalaste, el procedimiento de gancho siempre tiene el siguiente prototipo:

HookProc es realmente un comodín [placeholder] para el nombre de la función. Puedes llamarla de la forma que quieras siempre que conserves el prototipo de arriba. La interpretación de nCode, wParam y lParam depende del tipo de gancho que instales, así como del valor de retorno del procedimiento de gancho. Por ejemplo:

WH_CALLWNDPROC

WH_MOUSE

La línea de abajo es: debes consultar tu referencia de la api de win32 para detalles sobre los significados de los parámetros y regresar un valor al gancho que quieres instalar.

Ahora hay un poco de catch sobre el procedimiento de gancho. Recuerda que los ganchos estan encadenados en una lista enlazada con el gancho instalado más recientemente colocado en la cabeza de la lista. Cuando ocurre un evento, Windows llamará sólo al primer gancho de la cadena. Es responsabilidad de tu procedimiento de gancho llamar al siguiente gancho en la cadena. Tú no eliges llamar al siguiente gancho, pero mejor deberías saber qué estás haciendo. Muchas veces, es una buena práctica llamar al siguiente procedimiento de manera que otros ganchos puedan tener una impresión [shot] del evento. Puedes invocar al siguiente gancho llamando a CallNextHookEx que tiene el siguiente prototipo:

CallNextHookEx proto hHook:DWORD, nCode:DWORD, wParam:DWORD, lParam:DWORD

Una nota importante sobre los ganchos remotos: el procedimiento de gancho debe residir en una DLL que será proyectada dentro de otro proceso. Cuando Windows proyecta la DLL dentro de otros procesos, no proyectará la(s) sección(es) de datos(s) dentro de otros procesos. En pocas palabras, todos los procesos comparten una copia sencilla del código, ¡pero ellos tendrán su propia copia privada de la sección de datos de la DLL! Esto puede resultar una gran sorpresa para el incuto. Puedes pensar que cuando almacenas un valor dentro de una variable en la sección de datos de una DLL, ese valor será compartido entre todos los procesos que cargan la DLL dentro de su espacio de direcciones. No es tan cierto. En una situación normal, esta conducta es deseable ya que provee la ilusión de que cada proceso tiene su propia copia de la DLL. Pero no cuando hay involucrados ganchos de Windows. Queremos que la DLL sea idéntica en todos los procesos, incluyendo los datos. La solución: debes marcar la sección de datos como compartida. Puedes hacer esto especificando el atributo de la(s) sección(es) en el conmutador [switch] del enlazador [linker]. Para MASM, necesitas usar este conmutador:

/SECTION:<section name>, S

El nombre de la sección de datos inicializada es .data y el de la de los datos no-inicializados es .bss. Por ejemplo, si quieres ensamblar una DLL que contiene un procedimiento de gancho y quieres que la sección de datos no inicialoizados sea compartida entre procesos, debes usar la siguiente línea:

link /section:.bss,S  /DLL  /SUBSYSTEM:WINDOWS ..........

El atributo S marca la sección como compartida.

Ejemplo:

Hay dos módulos: uno es el programa principal que hará la parte de GUI y otra es la DLL que instalará/desinstalará el gancho.

;--------------------------------------------- Este es el código fuente del programa principal -----------------------------------
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include mousehook.inc
includelib mousehook.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

wsprintfA proto C :DWORD,:DWORD,:VARARG
wsprintf TEXTEQU <wsprintfA>

.const
IDD_MAINDLG                   equ 101
IDC_CLASSNAME              equ 1000
IDC_HANDLE                     equ 1001
IDC_WNDPROC                 equ 1002
IDC_HOOK                         equ 1004
IDC_EXIT                           equ 1005
WM_MOUSEHOOK             equ WM_USER+6

DlgFunc PROTO :DWORD,:DWORD,:DWORD,:DWORD

.data
HookFlag dd FALSE
HookText db "&Hook",0
UnhookText db "&Unhook",0
template db "%lx",0

.data?
hInstance dd ?
hHook dd ?
.code
start:
    invoke GetModuleHandle,NULL
    mov hInstance,eax
    invoke DialogBoxParam,hInstance,IDD_MAINDLG,NULL,addr DlgFunc,NULL
    invoke ExitProcess,NULL

DlgFunc proc hDlg:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
    LOCAL hLib:DWORD
    LOCAL buffer[128]:byte
    LOCAL buffer1[128]:byte
    LOCAL rect:RECT
    .if uMsg==WM_CLOSE
        .if HookFlag==TRUE
            invoke UninstallHook
        .endif
        invoke EndDialog,hDlg,NULL
    .elseif uMsg==WM_INITDIALOG
        invoke GetWindowRect,hDlg,addr rect
        invoke SetWindowPos, hDlg, HWND_TOPMOST, rect.left, rect.top, rect.right, rect.bottom, SWP_SHOWWINDOW
    .elseif uMsg==WM_MOUSEHOOK
        invoke GetDlgItemText,hDlg,IDC_HANDLE,addr buffer1,128
        invoke wsprintf,addr buffer,addr template,wParam
        invoke lstrcmpi,addr buffer,addr buffer1
        .if eax!=0
            invoke SetDlgItemText,hDlg,IDC_HANDLE,addr buffer
        .endif
        invoke GetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer1,128
        invoke GetClassName,wParam,addr buffer,128
        invoke lstrcmpi,addr buffer,addr buffer1
        .if eax!=0
            invoke SetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer
        .endif
        invoke GetDlgItemText,hDlg,IDC_WNDPROC,addr buffer1,128
        invoke GetClassLong,wParam,GCL_WNDPROC
        invoke wsprintf,addr buffer,addr template,eax
        invoke lstrcmpi,addr buffer,addr buffer1
        .if eax!=0
            invoke SetDlgItemText,hDlg,IDC_WNDPROC,addr buffer
        .endif
    .elseif uMsg==WM_COMMAND
        .if lParam!=0
            mov eax,wParam
            mov edx,eax
            shr edx,16
            .if dx==BN_CLICKED
                .if ax==IDC_EXIT
                    invoke SendMessage,hDlg,WM_CLOSE,0,0
                .else
                    .if HookFlag==FALSE
                        invoke InstallHook,hDlg
                        .if eax!=NULL
                            mov HookFlag,TRUE
                            invoke SetDlgItemText,hDlg,IDC_HOOK,addr UnhookText
                        .endif
                    .else
                        invoke UninstallHook
                        invoke SetDlgItemText,hDlg,IDC_HOOK,addr HookText
                        mov HookFlag,FALSE
                        invoke SetDlgItemText,hDlg,IDC_CLASSNAME,NULL
                        invoke SetDlgItemText,hDlg,IDC_HANDLE,NULL
                        invoke SetDlgItemText,hDlg,IDC_WNDPROC,NULL
                    .endif
                .endif
            .endif
        .endif
    .else
        mov eax,FALSE
        ret
    .endif
    mov eax,TRUE
    ret
DlgFunc endp

end start

;----------------------------------------------------- Este es el código fuente de la DLL --------------------------------------
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib

.const
WM_MOUSEHOOK equ WM_USER+6

.data
hInstance dd 0

.data?
hHook dd ?
hWnd dd ?

.code
DllEntry proc hInst:HINSTANCE, reason:DWORD, reserved1:DWORD
    .if reason==DLL_PROCESS_ATTACH
        push hInst
        pop hInstance
    .endif
    mov  eax,TRUE
    ret
DllEntry Endp

MouseProc proc nCode:DWORD,wParam:DWORD,lParam:DWORD
    invoke CallNextHookEx,hHook,nCode,wParam,lParam
    mov edx,lParam
    assume edx:PTR MOUSEHOOKSTRUCT
    invoke WindowFromPoint,[edx].pt.x,[edx].pt.y
    invoke PostMessage,hWnd,WM_MOUSEHOOK,eax,0
    assume edx:nothing
    xor eax,eax
    ret
MouseProc endp

InstallHook proc hwnd:DWORD
    push hwnd
    pop hWnd
    invoke SetWindowsHookEx,WH_MOUSE,addr MouseProc,hInstance,NULL
    mov hHook,eax
    ret
InstallHook endp

UninstallHook proc
    invoke UnhookWindowsHookEx,hHook
    ret
UninstallHook endp

End DllEntry

;---------------------------------------------- Est es el makefile de la DLL ----------------------------------------------

NAME=mousehook
$(NAME).dll: $(NAME).obj
        Link /SECTION:.bss,S  /DLL /DEF:$(NAME).def /SUBSYSTEM:WINDOWS /LIBPATH:c:\masm\lib $(NAME).obj
$(NAME).obj: $(NAME).asm
        ml /c /coff /Cp $(NAME).asm
 

Análisis:

El ejemplo desplegará una caja de diálogo [dialog box] con tres controles de edición que serán llenados con el nombre de la clase, el manejador [handle] de ventana y la dirección del procedimiento de ventana asociada con la ventana bajo el cursor del ratón. Hay dos botones, Hook (gancho) y Exit (Salir). Cuando presionas el botón Hook, el programa engancha la entrada del ratón y el texto en el botón cambia a Unhook (desenganchar). Cuando mueves el cursor del ratón sobre la ventana, la info acerca de esa ventana será desplegada en la ventana principal del ejemplo. Cuando presionas el botón Unhook, el programa remueve el ganchjo del ratón.

El programa principal usa una caja de diálogo [dialog box] como su ventana principal. Define un mensaje hecho a la medida [custom message], WM_MOUSEHOOK que será usado entre el programa principal y la DLL de gancho. Cuando el programa principal recibe este mensaje, wParam contiene el manejador de la ventana sobre la cual está el cursor del ratón. Por supuesto, este es un plan arbitrario. Yo decido enviar un manejador en wParam por razones de simplicidad. Tú puedes escoger tu propio método de comunicación entre la ventana principal y la DLL de gancho.

                    .if HookFlag==FALSE
                        invoke InstallHook,hDlg
                        .if eax!=NULL
                            mov HookFlag,TRUE
                            invoke SetDlgItemText,hDlg,IDC_HOOK,addr UnhookText
                        .endif

El programa mantiene una bandera, HookFlag, para monitorear el estado del gancho. Es FALSE si no se instala el gancho y TRUE si el gancho es instalado.

Cuando el usuario presiona el botón Hook, el programa chequea si el gancho ya está instalado. Si no lo está, se llama a la función InstallHook en la DLL de gancho para instalarlo. Nota que pasamos el manejador de la ventana principal como parámetro de la función de manera que la DLL de gancho pueda enviar mensajes WM_MOUSEHOOK a la ventana correcta,es decir la tuya propia.

Cuando el programa es cargado, la DLL de gancho tabmbién es cargada. Realmente, las DLLs son cargadas inmediatamente después de que el programa está en memoria. El punto de entrada de la DLL es llamado incluso antes de que se ejecute la primera instrucción del programa principal. Así que cuando el programa principal ejecuta la(s) DLL(s) es/son inicializada(s). Ponemos el siguiente código en el punto de entrada de la DLL de gancho:

    .if reason==DLL_PROCESS_ATTACH
        push hInst
        pop hInstance
    .endif

El código salva el manejador de instancia de la DLL de gancho misma a una variable global llamada hInstance para usar dentro de la función InstallHook. Ya que la función del punto de entrada de la DLL es llamada antes de que sean llamadas otras funciones de la DLL, hInstance siempre es válido. Ponemos hInstance en la sección .data, así que este valor es guardado en la base de la sección por proceso [is kept on per-process basis]. Debido a que cuando el cursor del ratón pasa sobre una ventana, la DLL de gancho es proyectada en el proceso. Imagina que ya hay una DLL que ocupa las direcciones de la DLL de gancho que se intentó cargar, la DLL de gancho debería ser re-proyectada a otra dirección. El valor de hInstance será actualizado para las de las nuevas direcciones cargadas. Cuando el usuario presiona el botón Unhook y luego el botón Hook, SetWindowsHookEx será llamada de nuevo. Sin embargo, esta vez, se usará el nuevo espacio de direcciones cargado como el manejador de instacia lo cual será erróneo porque en este proceso ejemplo la dirección de carga de la DLL de gancho no ha sido cambiada. El gancho será local donde puedes enganchar sólo los eventos de ratón que ocurren en tu propia ventana. Difícilmente deseable [hardly desirable].

InstallHook proc hwnd:DWORD
    push hwnd
    pop hWnd
    invoke SetWindowsHookEx,WH_MOUSE,addr MouseProc,hInstance,NULL
    mov hHook,eax
    ret
InstallHook endp

La función InstallHook es muy simple. Salva el manejador de ventana pasado como su parámetro a una variable global llamada hWnd para ser usada luego. Luego llama a SetWindowsHookEx para instalar un gancho de ratón. El valor de retorno de SetWindowsHookEx es almacenado en una variable global llamada hHook para usar con UnhookWindowsHookEx.

Después de que es llamada SetWindowsHookEx, el gancho del ratón es funcional. Cada vez que ocurre un evento de ratón del sistema, es llamada MouseProc (tu procedimiento de ventana).

MouseProc proc nCode:DWORD,wParam:DWORD,lParam:DWORD
    invoke CallNextHookEx,hHook,nCode,wParam,lParam
    mov edx,lParam
    assume edx:PTR MOUSEHOOKSTRUCT
    invoke WindowFromPoint,[edx].pt.x,[edx].pt.y
    invoke PostMessage,hWnd,WM_MOUSEHOOK,eax,0
    assume edx:nothing
    xor eax,eax
    ret
MouseProc endp

Lo primero que hace es llamar a CallNextHookEx para dar a otros ganchos el chance de procesar el evento del ratón. Después de eso, llma a la función WindowFromPoint para regresar el manejador de la ventana en la coordenada especificada del monitor. Nota que usamos la estructura POINT en la estructura MOUSEHOOKSTRUCT apuntada por lParam como la coordenada actual del ratón. Después de que enviamos el manejador de ventana a la ventana principal a través de PostMessage con el mensaje WM_MOUSEHOOK. Algo que deberías recordar es que: no deberías usar SendMessage dentro del procedimiento de gancho, ya que puede causar estancamiento de mensajes. Es más recomendable PostMessage. La estructura MOUSEHOOKSTRUCT se define abajo:

MOUSEHOOKSTRUCT STRUCT DWORD
  pt            POINT <>
  hwnd          DWORD      ?
  wHitTestCode  DWORD      ?
  dwExtraInfo   DWORD      ?
MOUSEHOOKSTRUCT ENDS
 

Cuando la ventana principal recibe un mensaje WM_MOUSEHOOK, usa el manejador de ventana en wParam para regresar la información acerca de la ventana.

    .elseif uMsg==WM_MOUSEHOOK
        invoke GetDlgItemText,hDlg,IDC_HANDLE,addr buffer1,128
        invoke wsprintf,addr buffer,addr template,wParam
        invoke lstrcmpi,addr buffer,addr buffer1
        .if eax!=0
            invoke SetDlgItemText,hDlg,IDC_HANDLE,addr buffer
        .endif
        invoke GetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer1,128
        invoke GetClassName,wParam,addr buffer,128
        invoke lstrcmpi,addr buffer,addr buffer1
        .if eax!=0
            invoke SetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer
        .endif
        invoke GetDlgItemText,hDlg,IDC_WNDPROC,addr buffer1,128
        invoke GetClassLong,wParam,GCL_WNDPROC
        invoke wsprintf,addr buffer,addr template,eax
        invoke lstrcmpi,addr buffer,addr buffer1
        .if eax!=0
            invoke SetDlgItemText,hDlg,IDC_WNDPROC,addr buffer
        .endif

Para evitar parpadeos [flickers], chequeamos el texto que está todavía en los controles de edición y el texto que se pondrá dentro de ellos para comprobar si on idénticos. Si lo son, los saltamos.
Regresamos el nombre de la clase llamando a GetClassName, la dirección del procedimiento de ventana llamando a GetClassLong con GCL_WNDPROC y luego los formateamos dentro de cadenas y los ponemos dentro de los controles de edición apropiados.

                        invoke UninstallHook
                        invoke SetDlgItemText,hDlg,IDC_HOOK,addr HookText
                        mov HookFlag,FALSE
                        invoke SetDlgItemText,hDlg,IDC_CLASSNAME,NULL
                        invoke SetDlgItemText,hDlg,IDC_HANDLE,NULL
                        invoke SetDlgItemText,hDlg,IDC_WNDPROC,NULL

Cuando el usuario presiona el botón Unhook, el programa llama a la función UninstallHook en la DLL de gancho. UninstallHook llama a UnhookWindowsHookEx. Después de eso, cambia el texto del botón una vez más a "Hook", HookFlag a FALSE y se limpia el contenido de los controles de edición.
Nota que el conmutador [switch] del enlazador en el makefile.

        Link /SECTION:.bss,S  /DLL /DEF:$(NAME).def /SUBSYSTEM:WINDOWS

Especifica a la sección .bss como una sección compartida para hacer que todos los procesos compartan la sección de datos no inicializados de la DLL de gancho. Sin este conmutador [switch], tu DLL de gancho no funcionará correctamente.


Index

Next

[Iczelion's Win32 Assembly HomePage]

n u M I T_o r's   Programming Page

Este tutorial, original de Iczelion, ha sido traducido por:   n u M I T_o r

www.000webhost.com