Desengancharme es un solucionador y desenganchador de API de Windows universal que aborda el problema de invocar llamadas al sistema no supervisadas de su malware Red Teams
En la era de los AV y EDR intrusivos que introducen parches calientes en los procesos en curso debido a sus mayores requisitos ópticos, los oponentes modernos deben tener una herramienta robusta para deslizarse a través de estos guardias. La implementación planificada de un solucionador para importaciones dinámicas, que podría separar las funciones utilizadas durante la operación, es un paso más hacia el fortalecimiento de la resistencia de los oponentes.
La solución que propongo aquí es cambiar el uso de importaciones de WinAPI resueltas por vinculador y permanecer visible en los encabezados PE del ejecutable compilado (especialmente la tabla de direcciones de importación) para preferir un enfoque completamente dinámico, que insiste en solo resolver dinámicamente las importaciones. Un resolutor dinámico de este tipo puede equiparse con una lógica de desenganche que se ejecuta en segundo plano, sin instrucciones del operador.
Escaparate
Aquí es cómo UnhookMe
Obras de ejemplo:
- Nos presenta la primera
MessageBoxW
ese no es el tema de enganchar - Entonces nos enganchamos
MessageBoxW
Prólogo a nosotros mismos para que siempre devuelva 0 sin mostrar su mensaje - Eventualmente rompemos
MessageBoxW
dinámica con elUnhookingImportResolver
Resolver, que detecta los parches de Prolog aplicados y restaura los bytes originales, desengancha de forma eficazMessageBoxW
Funcionalidad.
Mientras tanto, cuando aparecen los cuadros de mensaje, estas son las líneas del registro que se imprimirán en la salida estándar de la consola:
[~] Icono fijo kernel32.dll! CreateFileA
[~] Icono fijo kernel32.dll! ReadProcessMemory
[~] Icono fijo kernel32.dll! MapViewOfFile
[~] Icono fijo kernel32.dll! VirtualProtectEx
[#] Gancho de trampolín que se encuentra en el símbolo: MessageBoxW. Bytes originales restaurados del archivo.
[~] Icono fijo user32.dll! MessageBoxW
Cómo usarlo
Hay un total de 5 archivos fuente / encabezado de C ++ que su solución debe contener. Sin embargo, su ejecutable principal solo necesita contener dos encabezados obligatorios, como se describe a continuación.
resolver.h
– encabezado con la mayoríaUnhookingImportResolver
Implementación y definiciones de macros prácticasresolver.cpp
– Código fuente con opciones globales definidasusings.h
– un archivo de encabezado grande y desagradable con docenas deusing
Definiciones de tipos para WinAPI de uso frecuentePE.cpp
– archivo de código fuente del analizador PE personalizadoPE.h
– archivo de encabezado del analizador PE personalizado
Encabezados obligatorios
Solo se requieren dos encabezados para su programa:
# incluye «usings.h»
# Incluir «resolver.h»
Opciones globales
Hay algunas opciones globales que se pueden cambiar para afectar la forma en que funciona el solucionador o para informar de su actividad. Estos están al comienzo de. Están definidos resolver.cpp
Archivo:
Opciones de resolución global:
globalQuietOption
– configúrelo en verdadero si no desea ninguna salidaglobalVerboseOption
– configúrelo en verdadero si desea una salida exhaustiva detalladaglobalAntiSplicingOption
– Desenganche las funciones resueltas si están conectadas.globalLogFilePath
– dónde se deben redirigir las líneas de registro de salida. Si está vacío, elija stdout.
bool globalQuietOption = false;
bool globalVerboseOption = true;
bool globalAntiSplicingOption = true;
wchar_t globalLogFilePath[MAX_PATH] = L «»;
Especificación del tipo de API personalizado
Para poder utilizar resolutores, primero se debe declarar un tipo de puntero de función using
Declaración de forma estricta:
using fn_FunctionName = ReturnType WINAPI (
ParamType1 paramName1,
...,
ParamTypeN paramNameN,
);
Este repositorio viene con usings.h
Archivo de encabezado con tipos de uso predefinidos para docenas de API de Windows populares.
Ese Nombre de la función corresponde a la WinAPI que ImportResolver debería resolver, y este puntero de función debe estar marcado con la convención de llamada WINAPI ( __stdcall
en x86 y __fastcall
en x64). Ese Tipo de retorno debe seguir adelante WINAPI
Tipo de cambio.
Resolución y uso de funciones
Después de definir el tipo de puntero de función como se indica arriba, podemos usarlo de la siguiente manera:
RESOLVE (nombre de la biblioteca, nombre de la función);
ReturnType output = _FunctionName (param1,…, paramN);
La macro RESOLVE
se encarga de la instanciación ImportResolver
Plantilla de objeto y personalice el nombre de la biblioteca especificada.
Resolver introduce varias definiciones de macros más que son fáciles de usar en diversas circunstancias, llamadas de constructor:
#definir 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 de resolución:
template<typename Ret, typename ...Args>
ImportResolver<Ret WINAPI(Args...)>(
std::string dllName,
std::string funcName,
bool _verbose = false,
bool _unhook = false,
bool *_wasItHooked = nullptr
)
¿Como funciona?
El solucionador subyacente usa un analizador de encabezado PE personalizado que procesa cada módulo DLL referenciado para mapear sus exportaciones y verificar la integridad de los encabezados PE de ese módulo, así como la integridad de los bytes stub de la función referenciada.
La idea es esta:
- Primero exhibimos
LoadLibrary
referenciado por la biblioteca de usuario (el que se utiliza como el primer parámetro paraRESOLVE
Macro) si no fue accesible a través deGetModuleHandle
. - Luego procesamos los encabezados PE de la biblioteca cargada / referenciada, asignamos sus exportaciones, recuperamos una matriz de direcciones de exportación y calculamos estas direcciones nosotros mismos para la verificación cruzada.
- Si la dirección de una rutina definida en la tabla de direcciones de exportación de la DLL no cumple con nuestras expectativas, la exportación se trata como un gancho EAT. Lo mismo se aplica si nuestra entrada en la tabla de direcciones de importación ejecutable (IAT) para esta función se ha cambiado y ya no se refiere a la posición correcta en la sección de código de la DLL, entonces la función se considera vinculada a IAT.
- Suponiendo que no se han encontrado enlaces hasta ahora, buscamos los primeros N bytes del prólogo de la función y los comparamos con lo que está almacenado en el archivo DLL en el disco duro. Si hay una discrepancia entre los bytes recuperados de la memoria y del archivo, consideramos que la función está parcheada en línea (parcheada en caliente).
- Si se ha marcado la función, devolvemos la dirección de exportación original (que calculamos nosotros mismos) y / o verificamos la entrada. Si hubiera bytes de parche, los restauraremos.
- Finalmente, para optimizar el impacto en el rendimiento del resolutor, almacenamos en caché todas las bases de imágenes del módulo cargadas y las direcciones de funciones resueltas y las devolvemos desde una caché (donde
std::map
) en aciertos posteriores.
Algunos de los problemas a los que se enfrenta un solucionador desmontado tan dinámico incluyen los problemas de iteración a través de API enrutadas (una DLL puede contener Exportthunk que dice que esta función no está implementada en este módulo sino en otro); lo que, si bien admite esta implementación, a veces rompe su lógica transversal.