
Carregamento por proxy
No post Return Address Spoofing, mostrei um método simples para contornar a regra do Elastic ao carregar uma DLL a partir de memória unbacked. Porém, ao analisarmos a stack completa, ainda era possível detectar ação maliciosa devido aos frames de memória que continuavam lá.
As informações que você encontrar neste post, técnicas, códigos, provas de conceito ou qualquer outra coisa são estritamente para fins educacionais.
Neste post, apresento um método alternativo descrito originalmente pelo Paranoid Ninja no artigo Hiding In Plain Sight. O método elimina completamente qualquer traço de unbacked na call stack ao carregar DLLs.
O Problema dos Callbacks Tradicionais
Funções de callback no Windows são ponteiros para funções que podem ser passados para outras funções para serem executadas dentro delas. A Microsoft oferece dezenas de callbacks para desenvolvedores (lista aqui).
Porém, como o Paranoid Ninja mencionou, todos esses callbacks compartilham um problema principal: quando um callback é executado, ele roda dentro da mesma thread que o chamou.
Isso nos leva exatamente ao dilema que mencionamos anteriormente:
LoadLibrary retorna para -> Função de Callback retorna para -> região RX/UnbackedPara obter uma pilha "limpa", precisamos garantir que o nosso LoadLibrary seja executado em uma thread separada, independente da nossa região unbacked. Se formos utilizar callbacks, precisamos que eles consigam passar corretamente os parâmetros necessários para LoadLibraryA.
O problema é que a maioria dos callbacks no Windows ou não possui parâmetros, ou não repassa os parâmetros "como estão" para nossa função alvo LoadLibrary.
Thread Pool do Windows
O Paranoid Ninja escolheu usar três APIs do Windows para realizar essa operação:
TpAllocWork - Aloca um work item
TpPostWork - Enfileira o work item para execução
TpReleaseWork - Libera recursos
Preparando o Código C++
Primeiro, vamos preparar as estruturas necessárias no nosso código:
Criamos typedefs para as funções do Thread Pool. Isso é necessário porque vamos chamar essas funções via GetProcAddress, então precisamos definir o tipo correto dos ponteiros.
Agora vamos criar uma variável global:
Por que global? Porque nosso código assembly vai precisar acessar o endereço de LoadLibraryA. Variáveis globais são compartilhadas entre C++ e assembly.
Mas o assembly não pode acessar a variável global diretamente de forma conveniente. Então criamos uma função auxiliar:
O assembly vai chamar essa função para obter o endereço armazenado em g_pLoadLibraryA.
Também precisamos declarar nosso callback que será implementado em assembly:
Implementando a Função Principal
Agora vamos criar a função que carrega a DLL:
Começamos obtendo os handles de kernel32 e ntdll, que é onde estão as funções que precisamos.
Usamos GetProcAddress para obter os endereços das funções. Note que g_pLoadLibraryA é a variável global que o assembly vai acessar via getLoadLibraryA().
Aqui está a parte mais importante:
&WorkReturn- Ponteiro que receberá o work item alocadoWorkCallback- Nossa função callback implementada em assembly(PVOID)libName- O nome da DLL que queremos carregar
O libName será repassado como segundo argumento (Context) quando o Thread Pool chamar nosso WorkCallback.
TpReleaseWork libera os recursos associados ao work item. Note que isso não cancela a execução se ela já foi enfileirada.
Aguardamos 2 segundos para dar tempo do Thread Pool executar nosso callback.
Por fim, retornamos o handle da DLL carregada. Se a DLL foi carregada com sucesso, GetModuleHandleA retornará seu handle. Se falhou, retorna NULL.
O Código Assembly
Agora vamos para a parte crucial: o callback em assembly que manipula a stack.
Começamos declarando que getLoadLibraryA é uma função externa (definida no C++) e que WorkCallback será exportado (acessível pelo C++).
Iniciamos a definição da função. Lembrando que a assinatura esperada é:
Quando o Thread Pool chama nosso callback, os registradores estão assim:
RCX = Ponteiro para
PTP_CALLBACK_INSTANCE(estrutura interna do Thread Pool)RDX = Nosso
Context(olibNameque passamos)R8 = Ponteiro para
PTP_WORK(o work item)
LoadLibraryA espera o nome da DLL no primeiro argumento (RCX). Mas o Thread Pool colocou em RDX (segundo argumento). Então movemos RDX → RCX.
Após essa instrução:
RCX = nome da DLL (nosso argumento desejado)
RDX = ainda contém o nome da DLL (lixo)
LoadLibraryA espera apenas 1 argumento. Limpamos RDX por limpar, não precisa se não quiser.
Chamamos a função que retorna g_pLoadLibraryA. O resultado vem em RAX (convenção x64 para valores de retorno).
Após essa instrução:
RAX = endereço de LoadLibraryA
RCX = nome da DLL (preparado)
AQUI ESTÁ O TRUQUE! Usamos JMP, não CALL.
Finalizamos a definição da função.
Função Main
Agora vamos juntar tudo:
Simples e direto. Chamamos nossa função que cuida de tudo internamente.
Resultado
Aplicando o método do Paranoid Ninja, obtemos o seguinte resultado:

Last updated