Page cover

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á.

triangle-exclamation

Neste post, apresento um método alternativo descrito originalmente pelo Paranoid Ninja no artigo Hiding In Plain Sightarrow-up-right. 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 aquiarrow-up-right).

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/Unbacked

Para 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:

circle-info

O extern "C" desativa o name mangling do C++, permitindo que o linker encontre a função pelo nome exato.


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 alocado

  • WorkCallback - 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 (o libName que 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.

circle-info

Existem outras formas de aplicar o método do Paranoid Ninja, e algumas delas não sujam a stack. Isso ocorre porque, nessas variantes, o threadpool chama o callback diretamente, sem um CALL vindo do nosso código em RX, então nenhum retorno para a região RX é empilhado.


Resultado

Aplicando o método do Paranoid Ninja, obtemos o seguinte resultado:

Last updated