
Resolvedor e removedor de hooks dinâmico da API do Windows que detecta e restaura funções com hook (IAT, EAT, patches inline) para invocar chamadas de sistema não monitoradas a partir de malware de Red Team.
Na era de AVs e EDRs intrusivos que introduzem hot-patches nos processos em execução para seus requisitos de óptica aprimorada, os adversários modernos precisam de uma ferramenta robusta para passar por esses guardiões. A implementação proposta de um resolvedor de imports dinâmicos capaz de desenganchar funções usadas em tempo real é mais um passo para fortalecer os esforços de resiliência dos adversários.
A solução que proponho aqui é mudar de usar imports WinAPI resolvidos pelo vinculador, permanecendo visíveis nos cabeçalhos PE do executável compilado (especificamente na Tabela de Endereços de Importação) para favorecer uma abordagem totalmente dinâmica, insistindo em resolver imports apenas de forma dinâmica. Tal resolvedor dinâmico pode ser equipado com lógica de desenganche ocorrendo em segundo plano, sem qualquer tipo de orientação do operador.
É assim que você pode garantir chamar MessageBoxW desenganchado, desmonitorado:
RESOLVE(user32, MessageBoxW);
_MessageBoxW(0, L"Look Ma! I'm unhooked!", L"Third - Unhooked", 0);
Toda a mágica acontece dentro da macro RESOLVE, que constrói um objeto ImportResolver<T> chamado _MessageBoxW.

Veja como o exemplo do UnhookMe funciona:
MessageBoxW que não está sujeita a hookingMessageBoxW para fazê-la sempre retornar 0 sem exibir sua mensagemMessageBoxW dinamicamente usando o resolvedor UnhookingImportResolver, que detectará os patches aplicados no prólogo e restaurará os bytes originais, efetivamente desenganchando a funcionalidade da MessageBoxW.Enquanto isso, ao exibir as caixas de mensagem, estas são as linhas de log impressas no stdout do console:
[~] 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
Há um total de 5 arquivos de código-fonte/cabeçalho C++ que sua solução precisa incluir. No entanto, seu arquivo de programa principal precisa incluir apenas dois cabeçalhos necessários, conforme detalhado abaixo.
resolver.h - cabeçalho contendo a maior parte da implementação do UnhookingImportResolver e úteis macrodefiniçõesresolver.cpp - código-fonte com opções globais definidasusings.h - um cabeçalho grande e complicado contendo dezenas de definições de tipos using para WinAPIs comumente usadasPE.cpp - arquivo de código-fonte do analisador PE personalizadoPE.h - arquivo de cabeçalho do analisador PE personalizadoSeu programa exigirá apenas dois cabeçalhos a serem incluídos:
#include "usings.h"
#include "resolver.h"
Há algumas opções globais que podem ser alteradas, afetando a forma como o Resolvedor funciona ou relata sua atividade. Elas estão definidas no início do arquivo resolver.cpp:
Opções globais do Resolvedor:
globalQuietOption - defina como true se não quiser nenhum tipo de saídaglobalVerboseOption - defina como true se quiser uma saída detalhada e verbosaglobalAntiSplicingOption - desengancha funções resolvidas se elas estiverem com hook.globalLogFilePath - para onde redirecionar as linhas de log de saída. Se vazio, usa stdout.bool globalQuietOption = false;
bool globalVerboseOption = true;
bool globalAntiSplicingOption = true;
wchar_t globalLogFilePath[MAX_PATH] = L"";
Para usar o Resolvedor, um tipo de ponteiro de função deve ser declarado primeiro com uma instrução using de forma estrita:
using fn_FunctionName = ReturnType WINAPI (
ParamType1 paramName1,
...,
ParamTypeN paramNameN,
);
Este repositório vem com o arquivo de cabeçalho usings.h contendo tipos using predefinidos para dezenas de APIs populares do Windows.
O FunctionName corresponderá à WinAPI que queremos que o ImportResolver resolva, e esse ponteiro de função deve ser marcado como tendo convenção de chamada WINAPI (__stdcall no x86 e __fastcall no x64). O ReturnType deve preceder o modificador de tipo WINAPI.
Tendo o tipo de ponteiro de função definido como especificado acima, poderemos usá-lo da seguinte maneira:
RESOLVE(libraryName, FunctionName);
ReturnType output = _FunctionName(param1, ..., paramN);
A macro RESOLVE cuida de instanciar o objeto modelado ImportResolver e ajustar o nome da biblioteca especificada.
O Resolvedor introduz várias outras macrodefinições oferecendo invocação de construtor fácil de usar em várias circunstâncias:
#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)
Construtor do 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
)
O resolvedor subjacente utiliza um analisador personalizado de cabeçalhos PE, que processa cada módulo DLL referenciado para mapear suas exportações e verificar a integridade dos cabeçalhos PE do módulo, bem como a integridade dos bytes de stub da função referenciada.
A ideia é a seguinte:
LoadLibrary para carregar a biblioteca referenciada pelo usuário (aquela especificada como primeiro parâmetro para a macro RESOLVE) se ela não puder ser alcançada através de GetModuleHandle.std::map) durante acessos subsequentes.Entre os problemas que esse resolvedor com desenganche dinâmico enfrentou estão as questões de percorrer APIs encaminhadas (uma DLL pode conter um thunk de exportação dizendo que esta função não está implementada neste módulo, mas está em outro) - embora esta implementação tenha suporte para isso, às vezes ela quebra sua lógica de travessia.
Este e outros projetos são resultado de noites sem dormir e muito trabalho duro. Se você gosta do que faço e aprecia que sempre retribuo à comunidade, Considere me comprar um café (ou melhor, uma cerveja) apenas para agradecer! 💪
Mariusz Banach / mgeeky, 21
<mb [at] binary-offensive.com>
(https://github.com/mgeeky)