
Um projeto PoC para embutir shellcode na Hint/Name Table
HintInject é um incorporador e carregador de shellcode que desenvolvi enquanto brincava com as estruturas do arquivo PE e da Import Directory Table. Ele recebe um arquivo de shellcode bruto e coloca o shellcode em partes nas entradas da Hint/Name Table que são acessíveis através da Import Lookup Table de uma entrada de DLL falsa no executável carregador. O carregador então mescla essas partes para executar o shellcode. Não sei se é uma técnica nova ou não, mas queria compartilhá-la por diversão.
Antes de explicar como o HintInject funciona, quero mencionar brevemente o processo de Carregamento de DLL referenciando um post no Stack Overflow.
Para obter as informações sobre DLLs necessárias e funções/importações, deve-se olhar primeiro para a Import Directory Table. A Import Directory Table é uma tabela de entradas, uma entrada para cada DLL importada. Essas entradas contêm um ponteiro para o nome da DLL importada, um ponteiro para a Import Lookup Table, um ponteiro para a Import Address Table e outros campos para informações diferentes.
Simplificando, a Import Lookup Table leva às informações sobre as importações, e a Import Address Table leva aos endereços das importações. No entanto, quando o executável está no disco, ou logo antes do processo de carregamento da DLL, a Import Address Table é idêntica à da Import Lookup Table. O conteúdo da Import Address Table é sobrescrito com o endereço das importações durante o processo de Carregamento de DLL.
Sem entrar em muitos detalhes, podemos dar uma olhada no diagrama abaixo para ver a relação entre essas três tabelas.
Se entrarmos em um pouco mais de detalhes, a Import Lookup Table não contém os nomes das importações diretamente. Para as funções que são importadas por nome, ela contém RVAs de entradas da Hint/Name Table. Essas entradas armazenam os nomes das funções como strings ASCII terminadas em nulo. Portanto, a definição da struct de uma entrada da Hint/Name Table é a seguinte:
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
CHAR Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
O campo Hint aqui é na verdade um índice na tabela de ponteiros de nomes de exportação da DLL. Ele é usado para acelerar a localização da posição dessa importação. Portanto, podemos resumir a relação entre a Import Lookup Table e a Hint/Name table com o diagrama abaixo.
Para concluir, para encontrar uma DLL e suas importações, deve-se seguir estes passos:
Recentemente, ao examinar a técnica ImportDLLInjection compartilhada por x86matthew, vi como adicionar uma entrada falsa ao Import Directory de DLLs carregadas na memória. Depois, quis desenvolver um pequeno projeto que adiciona uma entrada falsa diretamente à tabela de importação de um binário no disco, tanto por diversão quanto para atualizar meus conhecimentos. Ao revisar o processo de Carregamento de DLL para desenvolver este projeto, o campo Hint nas entradas da Hint/Name Table usadas neste processo chamou minha atenção.
De acordo com o documento da MSDN, aprendi que este campo é usado pelo Windows Loader para encontrar o endereço dessa importação, que é importada por nome, diretamente da tabela de nomes de exportação da DLL em que está. No entanto, no mesmo documento, foi afirmado que se a função não puder ser encontrada usando este campo, essa função será pesquisada por meio de uma operação de busca binária na tabela de nomes de exportação da DLL. Com base nesta frase, pensei que colocar um valor incorreto para este campo não interromperia o processo de Carregamento de DLL.
Como mencionei acima, existe uma entrada na Hint/Name table correspondente a cada função a ser importada. Em outras palavras, temos 2 bytes para usar em cada importação. Ao combinar múltiplas importações, campos Hint suficientes podem ser obtidos para armazenar shellcodes maliciosos. Como resultado, pode-se usar um binário carregador para incorporar o shellcode nas entradas Hint/Name da entrada de DLL de importação falsa. Esse binário carregador pode acessar essas entradas para mesclar o shellcode a ser executado durante a execução.
O HintInject pode ser usado para criar tal carregador que contém o shellcode em sua Hint/Name table. Primeiro, ele cria uma nova seção chamada .rrdata e copia o diretório de importação atual para esta seção. Depois, ele anexa uma nova entrada falsa cujas importações serão usadas para segurar o shellcode de entrada. Os bytes restantes da seção são usados para armazenar a Import Lookup Table, a Import Address Table, o nome da DLL e a Hint/Name table da nova entrada falsa. Como última etapa, o HintInject usa os campos Hint das importações para colocar partes do shellcode de entrada.
Exemplo de importações de um binário carregador:
HintInject.exe com seu shellcode no formato bruto e um caminho de saída para criar um carregador de shellcode: HintInject.exe <Shellcode File> <Output Name>Loader.exe <PID>InjectShellcode no arquivo HintInjectLoader/Main.cpp.Testado no Windows 10 19044 com Visual Studio 2019:
dllNames no arquivo DllNamesForFakeImports.h.dllNames no arquivo DllNamesForFakeImports.h. Por padrão, o HintInject escolherá funções das dlss de acordo com a ordem do array. Em outras palavras, a ferramenta primeiro seleciona funções exportadas de user32.dll; se o tamanho do shellcode for maior que 2 * the number of user32.dll exports, ela também começa a usar as exportações de advapi32.dll e outras.static LPCSTR dllNames[] = {"user32.dll","advapi32.dll","gdi32.dll","wininet.dll","comctl32.dll","shell32.dll","wsock32.dll","oleaut32.dll","ws2_32.dll","urlmon.dll"};
Apenas para testes de segurança autorizados. O uso indevido desta ferramenta contra sistemas sem permissão explícita é ilegal.