# Carregamento por proxy

No post [return-address-spoofing](https://vith0r.gitbook.io/public/malware-dev/posts/stack/return-address-spoofing "mention"), 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á.

{% hint style="danger" %}
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.
{% endhint %}

Neste post, apresento um método alternativo descrito originalmente pelo **Paranoid Ninja** no artigo [Hiding In Plain Sight](https://0xdarkvortex.dev/proxying-dll-loads-for-hiding-etwti-stack-tracing/). 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](https://github.com/aahmad097/AlternativeShellcodeExec)).

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:

```nix
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:

```cpp
#include <windows.h>
#include <stdio.h>

typedef VOID(NTAPI* TPALLOCWORK)(PTP_WORK*, PTP_WORK_CALLBACK, PVOID, PTP_CALLBACK_ENVIRON);
typedef VOID(NTAPI* TPPOSTWORK)(PTP_WORK);
typedef VOID(NTAPI* TPRELEASEWORK)(PTP_WORK);
```

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:

```cpp
FARPROC g_pLoadLibraryA;
```

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:

```cpp
extern "C" UINT_PTR getLoadLibraryA() {
    return (UINT_PTR)g_pLoadLibraryA;
}
```

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:

```cpp
extern "C" VOID CALLBACK WorkCallback(PTP_CALLBACK_INSTANCE, PVOID, PTP_WORK);
```

{% hint style="info" %}
O `extern "C"` desativa o name mangling do C++, permitindo que o linker encontre a função pelo nome exato.
{% endhint %}

***

### Implementando a Função Principal

Agora vamos criar a função que carrega a DLL:

```cpp
HMODULE LoadLibraryViaCallback(const char* libName) {
    // Pega os handles dos módulos
    HMODULE hKernel32 = GetModuleHandleA("kernel32");
    HMODULE hNtdll = GetModuleHandleA("ntdll");
```

Começamos obtendo os handles de `kernel32` e `ntdll`, que é onde estão as funções que precisamos.

```cpp
// Obtém os endereços das funções
    g_pLoadLibraryA = GetProcAddress(hKernel32, "LoadLibraryA");
    FARPROC pTpAllocWork = GetProcAddress(hNtdll, "TpAllocWork");
    FARPROC pTpPostWork = GetProcAddress(hNtdll, "TpPostWork");
    FARPROC pTpReleaseWork = GetProcAddress(hNtdll, "TpReleaseWork");
```

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()`.

```cpp
// Aloca o work item
    PTP_WORK WorkReturn = NULL;
    ((TPALLOCWORK)pTpAllocWork)(
        &WorkReturn,
        WorkCallback,      // nosso callback em ASM
        (PVOID)libName,    // será passado como Context
        NULL
    );
```

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

```cpp
// Enfileira para execução
    ((TPPOSTWORK)pTpPostWork)(WorkReturn);
    ((TPRELEASEWORK)pTpReleaseWork)(WorkReturn);
```

`TpReleaseWork` libera os recursos associados ao work item. Note que isso **não cancela** a execução se ela já foi enfileirada.

```cpp
Sleep(2000);
```

Aguardamos 2 segundos para dar tempo do Thread Pool executar nosso callback.

```cpp
return GetModuleHandleA(libName);
}
```

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.

```asm
.code

; Declaração externa da função C++
EXTERN getLoadLibraryA:PROC

; Exporta o callback
PUBLIC WorkCallback
```

Começamos declarando que `getLoadLibraryA` é uma função externa (definida no C++) e que `WorkCallback` será exportado (acessível pelo C++).

```cpp
WorkCallback PROC
```

Iniciamos a definição da função. Lembrando que a assinatura esperada é:

```cpp
VOID CALLBACK WorkCallback(
    PTP_CALLBACK_INSTANCE Instance,  // RCX
    PVOID Context,                   // RDX
    PTP_WORK Work                    // R8
);
```

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)

```asm
; Move Context (nome da DLL) para RCX
    mov rcx, rdx
```

`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)

```asm
; Limpa RDX (LoadLibraryA só tem 1 argumento)
    xor rdx, rdx
```

`LoadLibraryA` espera apenas 1 argumento. Limpamos RDX por limpar, não precisa se não quiser.

```asm
; Chama função C++ pra pegar endereço de LoadLibraryA
    call getLoadLibraryA
```

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)

```asm
; JMP (não CALL!) pra LoadLibraryA
    jmp rax
```

**AQUI ESTÁ O TRUQUE!** Usamos **JMP**, não **CALL**.

```asm
WorkCallback ENDP
END
```

Finalizamos a definição da função.

***

### Função Main

Agora vamos juntar tudo:

```cpp
int main() {
    printf("[*] Carregando wininet.dll via Thread Pool...\n");
    
    HMODULE hWininet = LoadLibraryViaCallback("wininet.dll");
    printf("[+] wininet.dll carregada em: 0x%p\n", hWininet);

    getchar();
    return 0;
}
```

Simples e direto. Chamamos nossa função que cuida de tudo internamente.

{% hint style="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.
{% endhint %}

***

## Resultado

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

```apl
0:004> k
 # Child-SP          RetAddr               Call Site
00 000000a1`415ff6f8 00007ff9`76393720     KERNELBASE!LoadLibraryA
01 000000a1`415ff700 00007ff9`7637d79a     ntdll!TppWorkpExecuteCallback+0x130
02 000000a1`415ff750 00007ff9`74d37374     ntdll!TppWorkerThread+0x68a
03 000000a1`415ffa50 00007ff9`7637cc91     KERNEL32!BaseThreadInitThunk+0x14
04 000000a1`415ffa80 00000000`00000000     ntdll!RtlUserThreadStart+0x21
```

<figure><img src="https://3487725980-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fx3knx0LN2gJjRbyd1ua9%2Fuploads%2FjgozoZb1adCEAoYD4hlr%2Fimage.png?alt=media&#x26;token=8eb5aefe-5d2f-4ac7-9d4d-421d184fc112" alt=""><figcaption></figcaption></figure>
