
Resolvedor y desenganchador dinámico de la API de Windows que detecta y restaura funciones enganchadas (IAT, EAT, parches en línea) para invocar llamadas al sistema no monitoreadas desde malware de Red Team.
En la era de los AVs y EDRs intrusivos que introducen parches en caliente a los procesos en ejecución para cumplir con sus requisitos de óptica mejorada, los adversarios modernos deben disponer de una herramienta robusta para sortear estos vigilantes. La implementación propuesta de un resolvedor de imports dinámico capaz de desenganchar funciones utilizadas sobre la marcha es un paso más hacia el fortalecimiento de los esfuerzos de resistencia del adversario.
La solución que propongo aquí es cambiar el uso de imports de WinAPI resueltos por el enlazador, que permanecen visibles en los encabezados PE del ejecutable compilado (específicamente en la Tabla de Direcciones de Importación), para favorecer un enfoque totalmente dinámico que insista en resolver los imports solo de forma dinámica. Dicho resolvedor dinámico puede equiparse con una lógica de desenganche que ocurre en segundo plano, sin ningún tipo de guía por parte del operador.
Así es como puedes asegurarte de llamar a MessageBoxW desenganchada y sin monitorización:
RESOLVE(user32, MessageBoxW);
_MessageBoxW(0, L"Look Ma! I'm unhooked!", L"Third - Unhooked", 0);
Toda la magia ocurre dentro de la macrodefinición RESOLVE, que construye un objeto ImportResolver<T> llamado _MessageBoxW.

Así es como funciona el ejemplo de UnhookMe:
MessageBoxW que no está sujeto a enganche.MessageBoxW para que siempre devuelva 0 sin mostrar su mensaje.MessageBoxW dinámicamente usando el resolvedor UnhookingImportResolver, que detectará los parches aplicados al prólogo y restaurará los bytes originales, desenganchando efectivamente la funcionalidad de MessageBoxW.Mientras se muestran los cuadros de mensaje, estas son las líneas de registro impresas en la salida estándar de la consola:
[~] Resolved symbol kernel32.dll!CreateFileA
[~] Resolved symbol kernel32.dll!ReadProcessMemory
[~] Resolved symbol kernel32.dll!MapViewOfFile
[~] Resolved symbol kernel32.dll!VirtualProtectEx
[#] Found trampoline hook in symbol: MessageBoxW . Restored original bytes from file.
[~] Resolved symbol user32.dll!MessageBoxW
Hay un total de 5 archivos de código fuente/encabezados C++ que tu solución necesita incluir. Sin embargo, tu archivo principal del programa solo necesita incluir dos encabezados obligatorios, como se detalla a continuación.
resolver.h - encabezado que contiene la mayor parte de la implementación de UnhookingImportResolver y prácticas macrodefiniciones.resolver.cpp - código fuente con las opciones globales definidas.usings.h - un archivo de encabezado grande y engorroso que contiene decenas de definiciones de tipo using para WinAPIs de uso común.PE.cpp - archivo de código fuente del analizador PE personalizado.PE.h - archivo de encabezado del analizador PE personalizado.Tu programa solo requerirá la inclusión de dos encabezados:
#include "usings.h"
#include "resolver.h"
Hay un par de opciones globales que se pueden cambiar y que afectan la forma en que el Resolvedor funciona o informa su actividad. Estas se definen al principio del archivo resolver.cpp:
Opciones globales del Resolvedor:
globalQuietOption - establecer a true si no deseas ningún tipo de salida.globalVerboseOption - establecer a true si deseas una salida detallada.globalAntiSplicingOption - desenganchar las funciones resueltas si están enganchadas.globalLogFilePath - dónde redirigir las líneas de registro de salida. Si está vacío, se usa stdout.bool globalQuietOption = false;
bool globalVerboseOption = true;
bool globalAntiSplicingOption = true;
wchar_t globalLogFilePath[MAX_PATH] = L"";
Para usar el Resolvedor, primero se debe declarar un tipo de puntero a función con una declaración using de forma estricta:
using fn_FunctionName = ReturnType WINAPI (
ParamType1 paramName1,
...,
ParamTypeN paramNameN,
);
Este repositorio viene con un archivo de encabezado usings.h que contiene definiciones de tipo using predefinidas para decenas de APIs populares de Windows.
El FunctionName corresponderá a la WinAPI que queremos que ImportResolver resuelva y ese puntero a función debe estar marcado con la convención de llamada WINAPI ( __stdcall en x86 y __fastcall en x64). El ReturnType debe preceder al modificador de tipo WINAPI.
Una vez que se haya definido el tipo de puntero a función como se especificó anteriormente, podremos usarlo de la siguiente manera:
RESOLVE(libraryName, FunctionName);
ReturnType output = _FunctionName(param1, ..., paramN);
La macro RESOLVE se encarga de instanciar el objeto plantilla ImportResolver y ajustar el nombre de la librería especificada.
El Resolvedor introduce varias macrodefiniciones más que ofrecen una invocación de constructor fácil de usar en diversas circunstancias:
#define RESOLVE(mod, func) RESOLVE_PARAMETERIZED(mod, func, ::globalVerboseOption, ::globalAntiSplicingOption)
#define RESOLVE_NO_UNHOOK(mod, func) RESOLVE_PARAMETERIZED(mod, func, ::globalVerboseOption, false)
#define RESOLVE_VERBOSE_UNHOOK(mod, func) RESOLVE_PARAMETERIZED(mod, func, true, true)
#define RESOLVE_VERBOSE_NOUNHOOK(mod, func) RESOLVE_PARAMETERIZED(mod, func, true, false)
#define RESOLVE_NOVERBOSE_UNHOOK(mod, func) RESOLVE_PARAMETERIZED(mod, func, false, true)
#define RESOLVE_NOVERBOSE_NOUNHOOK(mod, func) RESOLVE_PARAMETERIZED(mod, func, false, false)
Constructor del Resolvedor:
template<typename Ret, typename ...Args>
ImportResolver<Ret WINAPI(Args...)>(
std::string dllName,
std::string funcName,
bool _verbose = false,
bool _unhook = false,
bool *_wasItHooked = nullptr
)
El resolvedor subyacente utiliza un analizador de encabezados PE personalizado que procesa cada módulo DLL referenciado para mapear sus exportaciones y verificar la integridad de los encabezados PE del módulo, así como la integridad de los bytes de código auxiliar de la función referenciada.
La idea es la siguiente:
LoadLibrary para cargar la librería referenciada por el usuario (la especificada como primer parámetro de la macro RESOLVE) si no se puede alcanzar mediante GetModuleHandle.std::map) durante hits posteriores.Entre los problemas que enfrenta un resolvedor de desenganche dinámico de este tipo están los problemas al recorrer APIs reenviadas (una DLL puede contener un código de exportación que indique que esta función no está implementada en este módulo, sino en otro), lo que, aunque esta implementación tiene soporte para ello, a veces rompe su lógica de recorrido.
Este y otros proyectos son el resultado de noches sin dormir y mucho trabajo duro. Si te gusta lo que hago y aprecias que siempre devuelvo algo a la comunidad, Considera invitarme un café (o mejor una cerveza) solo para darme las gracias! 💪
Mariusz Banach / mgeeky, 21
<mb [at] binary-offensive.com>
(https://github.com/mgeeky)