Hace tiempo que deseaba migrar mi sistema de 32 bits para realizar hooks a la API a 64bit. Aprovechando que disponía de un poco de tiempo, lo he hecho.
El sistema lo he desarrollado en Lazarus 32 compilando a 64bits en modo compatibilidad con delphi (-Mdelhpi). Y he encontrado dificultades con el debugger en modo ensamblador de Lazarus, pues no está muy conseguido y "se pierde", defecto que he solventado con imaginación.
Un problema añadido en el sistema API64 es que difiere de la versión 32bits en muchas cosas. La API32 suele comenzar así:
MOV EDI , EDI PUSH EBP MOV EBP , ESP
Lo que supone que en casi todas se pueden sobrescribir los primeros 5 bytes sin problemas y en caso de no comenzar así y presentar una instrucción de llamada relativa (call o Jmp), se recalculaban los offset con ayuda de un desensamblador de longitud. Sin embargo una gran cantidad de API64 no comienza de este modo, algunas presentan llamadas inmediatas, comparaciones, etc.
En la versión API64 aparece un serio problema y es que el espacio de direcciones sobrepasa con creces el espacio disponible para saltos relativos. Sustituirlos con saltos absolutos es verdaderamente endiablado, con lo que opté por localizar espacio de memoria cercano a la API lo que facilitaba las cosas, pero hacía falta un desensamblador de longitud para 64bits que permitiera, a su vez, localizar cuando una instrucción asm se refería a direcciones relativas. En su día usé uno escrito en C para 32bits, en esta ocasión traduje y adapté a Lazarus uno de 64 bits: LDasm64. Podéis encontrar una referencia técnica al juego de instrucciones asm 64bits aquí .
De esta forma diseñé el bloque de memoria trampolín, cercano a la API64 con la siguiente estructura en pseudocódigo:
ABS_JMP a HookAPI (FAR) ToAPI bytes copiados de la API original (función de vuelta a ella) REL_JMP Salto relativo a la API original
Al estar, este bloque, cercano a la API, el Hook se puede hacer con los 5 bytes de un salto relativo, mínimo espacio necesario. Ese salto se hace al offset 0 del anterior bloque que presenta hardcodeado un salto absoluto a la función hook, ya en la zona de memoria de nuestra aplicación. Luego, nuestra app podrá realizar la llamada a la API original a través del trampolín, realizando un call lejano a ToAPI que ejecutará las primeras instrucciones del a API original (que tenía copiadas y recalculadas las instrucciones asm en el caso de presentar offsets relativos) para luego realizar un salto relativo a la API original, justo después de los 5 bytes sobrescritos.
El siguiente código muestra como localizar el bloque de memoria cercano a la API y como se prepara para saltar y retornar del hook:
procedure InstallHookFromAddr(HookAPI: Pointer; var ToAPI: Pointer; API: Pointer; Resume: BOOL); var NBytes: DWORD; OldProtect: DWORD; si: SYSTEM_INFO; Block: ULONG_PTR; MaxBlock: ULONG_PTR; RelayMemoy: Pointer; RelaySize: DWORD; begin NBytes:= GetOverloadBytes(API); // Bytes a sobreescribir de la API Block:= (ULONG_PTR(API) shr 32)shl 32; MaxBlock:= ((ULONG_PTR(API) shr 32) + 1)shl 32; RelayMemoy:= nil; RelaySize:= NBytes + sizeof(ABS_JMP) + SIZE_OF_HOOK; ToAPI:= nil; // Buscamos un bloque de memoria Near al espacio de la API de Windows GetSystemInfo(si); while (Block < MaxBlock) and (RelayMemoy = nil) do begin RelayMemoy := VirtualAlloc(Pointer(Block+1), RelaySize, MEM_COMMIT or MEM_RESERVE, PAGE_EXECUTE_READWRITE); inc(Block, si.dwAllocationGranularity); end; if RelayMemoy <> nil then begin // Preparar la función ToAPI // Autorizo escribir en ToAPI VirtualProtectEx(THandle(-1), RelayMemoy, RelaySize, PAGE_EXECUTE_READWRITE, @OldProtect); Write_ABS_JMP(RelayMemoy, HookAPI); // Salto a HookAPI ToAPI:= Pointer(ULONG_PTR(RelayMemoy) + sizeof(ABS_JMP)); ASMmemcpy(ToAPI, API, NBytes); // Copio los primeros bytes de la api Write_REL_JMP(Pointer(ULONG_PTR(ToAPI) + NBytes), Pointer(ULONG_PTR(API) + NBytes)); VirtualProtectEx(THandle(-1), RelayMemoy, RelaySize, OldProtect, nil); // Preparar la API a Hookear if Resume then ResumeHookFromAddr(RelayMemoy, API); end; FlushInstructionCache(GetCurrentProcess, nil, 0); end;
El siguiente código muestra como sobrescribir la API original:
procedure ResumeHookFromAddr(HookFunc, API: Pointer); var OldProtect: DWORD; begin if (HookFunc = nil) or (API = nil) then exit; // Autorizo escribir en la API a Hookear VirtualProtectEx(THandle(-1), API, sizeof(ABS_JMP), PAGE_EXECUTE_READWRITE, @OldProtect); Write_REL_JMP(API, HookFunc); // Escribo el salto VirtualProtectEx(THandle(-1), API, sizeof(ABS_JMP), OldProtect, nil); end;
En definitiva, es sistema es el clásico hook a la API de 5 bytes descrito aquí con las modificaciones necesaria para trabajar en 64 bits. Se trata de la primera versión del sistema que he probado con éxito con APIs conflictivas en 64 bits como MessageBox y IsProcessorFeaturePresent, pero que posiblemente necesite alguna revisión.
Recomiendo la lectura del tutorial HOOK a la API en delphi y en C (trampolín) para aclarar conceptos básicos del Hook a la API con esta técnica.
Lo he escrito para Lazarus 32bits compilando a 64bits porque no dispongo de una versión delphi que me permita compilar para 64 bits y porque tengo Lazarus 32 bits instalado. Me da mucha pereza desinstalarlo para instalar la versión 64, que por otro lado creo que no permite compilar código de 32 bits, aunque esto, puede que halla variado.
Y os preguntaréis porqué realizar un hook a la API de 64 bits y no conformarme con la de 32bits. La respuesta es doble, por un lado abarcas los procesos de 64 pudiendo compilar dll e inyectarlas en esos procesos. La segunda razón es por prurito y curiosidad personal, no me puedo estar quieto.
Esperando que todo esto sea de vuestro interés, subo todo el código en un proyecto Lazarus de ejemplo.
Saludos.