Main logo La Página de DriverOp

Hooks o Ganchos

por Liebesschmerz

Tabla de contenidos

Algo para saber

Este es el primer tutorial o guía que he escrito en mi vida, asi que si encuentran algun error de ortografía, o no les gusta algo que puse, pido disculpas :-). Algo también que me gustaría poner acá es que crecí con los términos en Inglés de la programación, asi que he hecho un esfuerzo en ponerlos en Castellano, aunque estos no tengan un significado claro para mi, como por ejemplo (hilo, cola, pila, etc) suenan muy raros y graciosos jejeje, asi que si encuentran un termino por ahí en Inglés, es porque no se cómo se llama en Castellano.

Introducción

Aquí traduzco un poco lo que el MSDN nos da a saber acerca de los ganchos en Windows, por ejemplo como instalarlos, desinstalarlos, manejarlos, etc. No es la información completa. Si te gusta leer o no sabes acerca de los ganchos quizás te ayude leer la traducción que puse aquí, para los que ya saben no necesitan leerlo.

Propósito del Tutorial

Me decidí a escribir este tutorial acerca de los hooks en Windows, porque hace algun tiempo buscaba información sobre este tema, y no encontraba mucha información, y la que encontraba estaba en Inglés y no encontraba nada en Castellano. Tambien la ayuda del MSDN me ayudo, pero el código que ponia como algo de ejemplo es para VC++ :-).

Bueno lo que tratare de mostrar en este tutorial acerca de los ganchos de windows es algo básico de cómo agregar mas funcionalidad (si es que asi se le puede llamar) a programas foraneos (programas ya compilados que no tenemos los source codes y no tienen soporte de plugin claro). Usaremos para nuestro ejemplo el famoso Notepad que viene con Windows, le agregaremos un menú dinamicamente y simplemente mostraremos un messagebox al clickear en un item del menú :-).

Así que este tutorial tendrá información acerca de ganchos, subclassing, y MMF (memory mapped files).

Manos a la obra

Ok, Voy a agregar un menú dinamicamente a todas las instancias del Notepad, después de haber instalado el gancho. Cada instancia del Notepad tiene un ID de hilo distinto, entonces eso me da a entender, de que el tipo de gancho que instalare sera global o system-wide, y como dice el MSDN debo crear/usar una DLL para ganchos globales, y también dice que debo compartir algunos datos que usaré en otro proceso. Esto es porque cada instancia de un programa o DLL tiene un espacio privado en la memoria, y no puedo accesar a ellas normalmente, entonces las variables en diferentes procesos seran diferentes o simplemente invalidos.

Es por eso que debo compartir por lo menos una variable en todas las instancias de la DLL, la variable que compartire sera el manejador del gancho (HHOOK) que debo pasarle a CallNextHookEx.

Como Delphi según se o_0, no permite asignarle a un segmento el atributo de shared, como lo permite por ejemplo VC++ y MASM32, entonces usare MMF (memory mapped files - archivos mapeados en memoria).

Para eso usaré CreateFileMapping, MapViewOfFile, UnmapViewOfFile, y CloseHandle. Compartiré el HHOOK que es igual a un DWORD, entonces sera 4 bytes los que reservaré.

Declaro las variables y constantes que necesitaré:

const
  szClassName     = 'Notepad';
   szMMFName       = 'szMiMMF :-)';
   WM_STOPSUBCLASS = WM_USER+$10;
   ID_Item1        = WM_USER+$100;
   ID_Item2        = WM_USER+$101;
   ID_Item3        = WM_USER+$102;
   ID_Item4        = WM_USER+$103; 
var
   pdwDatos: PDWORD = nil;
   hMap: DWORD = 0;
  • szClassName: es el nombre de la class del Notepad.
  • szMMFName: es el nombre que le daré al espacio de memoria y donde tendré acceso de lectura y escritura.
  • WM_STOPSUBCLASS: es un mensaje que enviaré a las ventanas del Notepad para darles a saber que ya quiero terminar de subclassearlas.
  • ID_ItemX: son los IDs que le daré a los items de los menues que agregare dinamicamente.
  • pdwDatos: es un puntero a un DWORD, donde almacenaré el manejador del gancho (HHOOK).
  • hMap: una simple variable de tipo DWORD, la usaré para guardar el manejador que me devuelve CreateFileMapping.

Cada vez que mi DLL sea cargada por un proceso, usare OpenGlobalData para obtener un puntero al espacio creado y cuando se descarge mi DLL usare CloseGlobalData.

{creamos un espacio de memoria dandole un nombre, y todas las instancias  de la DLL podran escribir/leer a este espacio.}
{La primera vez creare el espacio, las demas veces retornara el manejador del espacio ya creado. }
 procedure OpenGlobalData();
begin
   hMap := CreateFileMapping(INVALID_HANDLE_VALUE, nil, PAGE_READWRITE, 0, SizeOf(DWORD), szMMFName);
   pdwDatos := MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, 0);
  if pdwDatos = nil then
     CloseHandle(hMap);
end;
{ cerramos todo }
 procedure CloseGlobalData();
begin
   UnmapViewOfFile(pdwDatos);
   CloseHandle(hMap);
end;

Ahora como se cuando la DLL es cargada/descargada ?

Quizas ya sepas esto, pero en una ayuda de Borland leí que se debía hacer así.

{ punto de entrada de la DLL }
procedure DllEntryPoint(dwReason: DWORD);
begin
   case dwReason of
     DLL_PROCESS_ATTACH: OpenGlobalData();
     DLL_PROCESS_DETACH: CloseGlobalData();
   end;
end;
{ entrada, llamo a process_attach }
begin
   DllProc := @DLLEntryPoint;
   DllEntryPoint(DLL_PROCESS_ATTACH);
end.

Ahora solo necesito instalar/desinstalar el gancho de tipo WH_CBT y crear una función filtro.

Windows nos dice que la función filtro de un gancho de tipo WH_CBT puede recibir esto:

  • nCode: HCBT_ACTIVATE, HCBT_CLICKSKIPPED, HCBT_CREATEWND, HCBT_DESTROYWND, HCBT_KEYSKIPPED, HCBT_MINMAX, HCBT_MOVESIZE, HCBT_QS, HCBT_SETFOCUS, y HCBT_SYSCOMMAND.
  • wParam: Depende del nCode.
  • lParam: Depende del nCode.

Estoy intersado en la notificacion HCBT_CREATEWND, que es la que recibire cuando una ventana esté a punto de ser creada y los parámetros wParam y lParam tendrán estos valores.

  • wParam: Manejador de la ventana que será creada.
  • lParam: Puntero a una estructura CBT_CREATEWND.

Crearé dos procedimientos para instalar/desinstalar el gancho y serán exportadas, para que un programa pueda llamarlas.

function CBTProc(Code: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
var
   szClass: array [0..256] of Char;
   dwOldWndPtr: DWORD;
begin
   { veo si la notificacion es igual a la de "ventana creandose" }
  if Code = HCBT_CREATEWND then
   begin
     { agarro la class de la ventana que se esta creando y la comparo con la class del notepad }
     ZeroMemory(@szClass[0], 256);
     GetClassName(wParam, @szClass[0], 256);
     if szClass = szClassName then
    begin
       { subclasseo la ventana y guardo el viejo puntero en el USERDATA de la ventana }
       dwOldWndPtr := GetWindowLong(wParam, GWL_WNDPROC);
       SetWindowLong(wParam, GWL_WNDPROC, Integer(@WndProc));
       SetWindowLong(wParam, GWL_USERDATA, dwOldWndPtr);
    end;
  end;
   { llamo al siguiente gancho en la cadena }
  Result := CallNextHookEx(pdwDatos^, Code, wParam, lParam);
end;
{ exportar; instalar el gancho de tipo CBT }
 procedure StartHook(); stdcall;
begin
   pdwDatos^ := SetWindowsHookEx(WH_CBT, @CBTProc, hInstance, 0);
end;
{ exportar; desinstalar el gancho }
procedure StopHook(); stdcall;
begin
   RestaurarWndProcs();
   UnHookWindowsHookEx(pdwDatos^);
end;

Subclasseo la ventana, osea cambio su función que procesa los mensajes de Windows, y le doy una nueva. Windows llamará primero a mi funcion y luego yo llamaré a la función vieja, de esta forma puedo atrapar los mensajes que generará mi menú dinámico :-).

Como ves también guardo el puntero a la función WNDPROC vieja en la misma ventana, así no tengo que crear un array dinámico, y me ahorro mucho trabajo. Si usara un array dinámico de punteros, tendría que compartirlo entre todos los procesos también, así como comparto el valor de pdwDatos^.

Función nueva que procesa los mensajes:

function WndProc(Handle: HWND; uMsg: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT;   stdcall;
var
   hMnu, hNewMnu: HMENU;
   dwOldWndPtr: DWORD;
begin
   { obtengo el viejo puntero que guarde aqui }
   dwOldWndPtr := GetWindowLong(Handle, GWL_USERDATA);
  case uMsg of
     { creo el menu cuando recibo el mensaje de mostrar ventana, y lo agrego al menu actual del notepad  }
     WM_SHOWWINDOW:
    begin
       hNewMnu := CreatePopupMenu();
       hMnu := GetMenu(Handle);
       AppendMenu(hNewMnu, MF_STRING, ID_Item1, 'Item 1');
       AppendMenu(hNewMnu, MF_STRING, ID_Item2, 'Item 2');
       AppendMenu(hNewMnu, MF_STRING, ID_Item3, 'Item 3');
       AppendMenu(hNewMnu, MF_STRING, 0, nil);
       AppendMenu(hNewMnu, MF_STRING, ID_Item4, 'Item 4');
       AppendMenu(hMnu, MF_POPUP, hNewMnu, 'Demo!');
       DestroyMenu(hNewMnu);
    end;
     { aqui recibo el mensaje del menu, simplemente muestro }
     { un messagebox indicando que item fue clickeado }
     WM_COMMAND:
    case wParam of
       ID_Item1: MessageBox(Handle, 'Item 1 ha sido clickeado!', szClassName, 0);
       ID_Item2: MessageBox(Handle, 'Item 2 ha sido clickeado!', szClassName, 0);
       ID_Item3: MessageBox(Handle, 'Item 3 ha sido clickeado!', szClassName, 0);
       ID_Item4: MessageBox(Handle, 'Item 4 ha sido clickeado!', szClassName, 0);
    end;
     { este mensaje lo cree yo, simplemente es un número alto para }
     { evitar que exista, y sirve para decirle que restaure el viejo }
     { puntero del WNDPROC, si no hago esto el programa hará crash }
     WM_STOPSUBCLASS:
       SetWindowLong(Handle, GWL_WNDPROC, dwOldWndPtr);
  end;
   { no recibí ningún mensaje que buscaba, y llamo a su viejo WNDPROC }
   Result := CallWindowProc(Pointer(dwOldWndPtr), Handle, uMsg, wParam, lParam);
end;

Solo me falta mostrar lo que RestaurarWndProcs es.

Este procedimiento lo llamo cuando desinstalo el gancho. Este procedimiento simplemente enumera las ventanas buscando por una ventana la cual tenga su class igual a la class del Notepad, y si la encuentra le envía el mensaje WH_STOPSUBCLASS. Si el Notepad fue creado antes de que el gancho sea instalado, igualmente recibirá el mensaje, pero no lo entendera y no sucederá nada.

function EnumWinProc(Handle: HWND; lParam: LPARAM): Boolean; stdcall;
var
   szClass: array [0..256] of Char;
begin
   { pongo el buffer en ceros }
   ZeroMemory(@szClass[0], 256);
   GetClassName(Handle, @szClass[0], 256);
   if szClass = szClassName then
     { comparo las class, son iguales ?, entonces envio el mensaje }
     SendMessage(Handle, WM_STOPSUBCLASS, 0, 0);
   Result := True;
end;
procedure RestaurarWndProcs();
begin
   EnumWindows(@EnumWinProc, 0);
end;

Código fuente del proyecto DLL.

Click aquí para descargar los fuentes de ejemplo de este tutorial.

Bueno, eso cubre todo creo o_0. chao

Liebesschmerz -