Saltar a contenido

Clase 2 — PreviousMode, GS:188, vtables y llamadas válidas bajo KCFG

← Volver al Módulo 7 · ← Clase 1

Resumen: esta clase baja a una demo corta pero importante: cómo ubicar y modificar KTHREAD.PreviousMode, por qué leer GS:188 desde WinDbg puede engañar si solo se cambia el contexto con .process, y cómo algunos exploits modernos evitan saltar a gadgets sueltos usando funciones completas válidas para KCFG. También se repasa una cadena de llamadas basada en vtable/control del this para convertir un Use-After-Free en una primitiva de escritura de 16 bytes.


Tabla de Contenidos

  1. Contexto de la clase
  2. PreviousMode: qué habilita y por qué importa
  3. GS:188: error común al leer el current thread
  4. Cómo obtener el _KTHREAD correcto desde WinDbg
  5. Llegar de _KTHREAD a _EPROCESS
  6. Modificar PreviousMode
  7. Qué cambia en 25H2
  8. Usar PreviousMode como puente para token stealing
  9. UAF + vtable controlada
  10. KCFG: por qué llamar funciones enteras puede seguir funcionando
  11. Cadena de funciones para construir un write-what-where
  12. Resolver direcciones de funciones en runtime
  13. Errores comunes
  14. Workflow mental
  15. Cheat sheet

1. Contexto de la clase

La clase retoma el escenario del módulo 7:

  • En builds como 23H2, todavía se podía obtener bastante información de kernel usando APIs/leaks conocidos.
  • Tener direcciones no alcanza si la primitiva real de lectura/escritura está bloqueada por PreviousMode.
  • La técnica vista permite tocar KTHREAD.PreviousMode para que ciertas rutas traten la operación como kernel-mode.
  • En builds más nuevos (25H2 en la demo), Microsoft endurece este camino y reescribe/restaura PreviousMode al volver desde user-mode.

La idea importante no es memorizar un offset fijo, sino entender el recorrido:

proceso objetivo
  -> _KTHREAD actual
  -> _KTHREAD.PreviousMode
  -> habilitar ventana de read/write kernel
  -> usar esa ventana para tocar _EPROCESS/_TOKEN
  -> restaurar PreviousMode

2. PreviousMode: qué habilita y por qué importa

PreviousMode es un campo asociado al thread que indica desde qué modo venía la ejecución anterior:

Valor conceptual Significado
UserMode la llamada viene desde user-mode
KernelMode la operación se trata como kernel-mode

En la demo se ve que, aunque el proceso conozca direcciones kernel en 23H2, no necesariamente puede leer/escribir si PreviousMode sigue indicando user-mode.

Cuando se pisa PreviousMode al valor esperado para kernel-mode, la primitiva puede empezar a leer/escribir direcciones kernel. Eso vuelve posible encadenar otras acciones:

  • leer el token de System;
  • escribir ese token en el proceso actual;
  • modificar privilegios del token;
  • restaurar PreviousMode para evitar inconsistencias.

Nota: en la demo el campo observado aparece como byte y se edita con eb.


3. GS:188: error común al leer el current thread

Un punto central de la clase: no confiar ciegamente en GS:188 después de cambiar contexto manualmente en WinDbg.

En x64 Windows:

  • En user-mode, GS apunta a estructuras user del thread, como TEB.
  • En kernel-mode, GS apunta a estructuras kernel por CPU, como KPCR/KPRCB.
  • Un acceso típico como GS:188 puede usarse para llegar al _KTHREAD actual cuando realmente se está ejecutando en ese contexto.

El problema:

.process /p /r <EPROCESS>
g

cambia contexto para inspección, pero no garantiza que GS:188 refleje el thread correcto del proceso objetivo.

La frase práctica de la clase:

Cambiar contexto no es lo mismo que estar ejecutando o haber parado con breakpoint dentro del proceso.

Para que GS:188 sea confiable:

  • el target tiene que estar ejecutando y frenar en ese proceso/thread;
  • o hay que parar por breakpoint en una ruta realmente ejecutada por ese proceso;
  • no alcanza con hacer solo .process y leer GS:188.

4. Cómo obtener el _KTHREAD correcto desde WinDbg

Si se quiere encontrar el thread correcto sin depender de un GS potencialmente stale, conviene listar procesos/threads desde WinDbg.

En la clase se muestra el uso de !process con flags para ver threads:

!process -1 0
!process -1 4

!process -1 0 muestra el proceso actual del debugger. En la demo podía aparecer System, aunque el interés fuera otro proceso.

!process -1 4 agrega detalle de threads, permitiendo ubicar el _KTHREAD asociado al proceso objetivo.

También puede usarse el nombre o PID del proceso al final del comando, según el caso:

!process 0 4 previousmode.exe
!process 0 4 <PID>

La idea es obtener explícitamente el _KTHREAD que corresponde al target, en vez de asumir que GS:188 apunta a él.


5. Llegar de _KTHREAD a _EPROCESS

Una vez obtenido el _KTHREAD, la clase muestra dos offsets importantes de la demo:

Estructura/campo Offset visto Comentario
_KTHREAD.Process / proceso asociado 0x220 apunta al _KPROCESS, que coincide con el inicio de _EPROCESS
_KTHREAD.PreviousMode 0x232 byte a modificar en la demo

El punto conceptual:

_EPROCESS
  +0x000 Pcb : _KPROCESS

Como _KPROCESS es el primer campo de _EPROCESS, la dirección de _KPROCESS coincide con la base del _EPROCESS.

En WinDbg, el flujo típico sería:

dt nt!_KTHREAD <KTHREAD>
dt nt!_KTHREAD <KTHREAD> PreviousMode
dt nt!_KTHREAD <KTHREAD> Process

6. Modificar PreviousMode

En la demo, PreviousMode está en el offset 0x232 del _KTHREAD.

Para leer el byte:

db <KTHREAD>+232 L1

Para escribirlo:

eb <KTHREAD>+232 0

El valor concreto depende de la representación del build, pero en la clase se ve el patrón:

  • antes: valor asociado a user-mode;
  • se escribe 0 para activar el comportamiento esperado;
  • después se valida que la lectura/escritura kernel ya funciona.

Después de usar la ventana de escritura, conviene restaurar el campo:

eb <KTHREAD>+232 1

La advertencia práctica fue clara: si tocaste PreviousMode, restauralo cuando terminás.


7. Qué cambia en 25H2

La clase contrasta el comportamiento con builds más nuevas:

  • En 23H2 se puede conocer la dirección del _KTHREAD y del campo PreviousMode por leaks/consultas disponibles.
  • Al pisar PreviousMode, se habilita temporalmente lectura/escritura kernel.
  • En 25H2, el sistema vuelve a reescribir/restaurar PreviousMode al entrar desde user-mode.

La explicación simplificada:

entra desde user-mode
  -> el kernel/hypervisor detecta que la transición viene de user
  -> si PreviousMode quedó como kernel-mode, lo vuelve a corregir

Por eso, aunque puedas escribir el byte, puede durar muy poco o ser restaurado en la siguiente transición.

Esto no elimina todos los ataques data-only. En la clase se remarca que el token es distinto:

  • PreviousMode se puede recomputar/restaurar en cada entrada;
  • un puntero a token o un campo de privilegios es data persistente más difícil de validar globalmente.

8. Usar PreviousMode como puente para token stealing

Una vez habilitada la ventana de read/write kernel, el flujo ofensivo clásico sería:

1. Activar PreviousMode para permitir read/write kernel.
2. Leer _EPROCESS.Token de System.
3. Escribir ese token en _EPROCESS.Token del proceso actual.
4. Restaurar PreviousMode.
5. Validar privilegios/SYSTEM.

Esto conecta con la clase 1:

  • _EPROCESS.Token contiene un _EX_FAST_REF.
  • Para copiar token entre procesos se copia el valor crudo del _EX_FAST_REF.
  • Para inspeccionar el token con !token, se limpian los low bits (& ~0xF).

La diferencia clave: PreviousMode se usa como puente temporal para conseguir una primitiva mejor o persistente.


9. UAF + vtable controlada

La segunda parte de la clase pasa a una técnica para transformar un Use-After-Free en ejecución de métodos controlados.

El escenario:

objeto kernel liberado
  -> reclaim con datos controlados
  -> RCX apunta al chunk controlado
  -> el código trata RCX como this
  -> lee una vtable desde el chunk
  -> hace calls indirectas a entries de esa vtable

En x64 MSVC, RCX suele ser el this en métodos C++.

Si el atacante controla el chunk reclamado:

  • controla el primer qword usado como vtable;
  • controla campos internos del objeto fake;
  • puede seleccionar funciones de kernel/driver como métodos a invocar;
  • puede hacer que esas funciones reutilicen RCX y otros argumentos de forma favorable.

En la demo, se arma una tabla falsa en user-mode alrededor de direcciones como 0x500000/0x501000.


10. KCFG: por qué llamar funciones enteras puede seguir funcionando

KCFG valida destinos indirectos en kernel para evitar saltos arbitrarios.

La idea de la demo no es saltar al medio de gadgets tipo ROP, sino llamar funciones completas desde su inicio.

Regla práctica:

Salto Relación con KCFG
Saltar al medio de una función/gadget más probable que KCFG lo bloquee
Llamar una función válida desde el inicio puede pasar validación

Por eso se buscan funciones pequeñas, exportadas o alcanzables, cuyo comportamiento sirva como gadget semántico completo.

La clase usa funciones de CLFS porque:

  • suelen estar presentes en sistemas Windows;
  • muchas tienen forma de método y usan RCX como this;
  • algunas llaman otras funciones tomadas desde la vtable/objeto;
  • permiten acomodar registros y terminar en lectura/escritura controlada.

11. Cadena de funciones para construir un write-what-where

La cadena conceptual vista en clase:

función A(this = RCX controlado)
  -> llama entry de vtable con EDX/RDX inicial
  -> función B acomoda RDX a 0xFFFF...
  -> función C usa RCX + offsets controlados
  -> escribe XMM0 en destino controlado

11.1. Preparar RDX

Una de las funciones observadas hace operaciones simples con EDX, terminando con un valor tipo 0xFFFF.

Eso es útil porque:

  • queda como argumento para la siguiente llamada;
  • puede apuntar a una zona user-mode alocable/controlada;
  • permite controlar el origen de bytes que luego se van a escribir.

11.2. Encadenar dos llamadas desde la misma vtable

Otra función interesante hace dos calls internas usando el mismo this:

call [vtable + offset_1]  ; prepara argumento
call [vtable + offset_2]  ; usa argumento preparado para escribir

Eso multiplica el efecto de una sola llamada inicial: primero acomoda registros y después ejecuta la escritura.

11.3. Escritura de 16 bytes

La función final de la demo escribe un valor vectorial:

XMM0 = contenido controlado
destino = valor derivado de campos controlados por RCX
write(destino, XMM0)  ; 16 bytes

Eso alcanza para tocar estructuras como privilegios del token:

_SEP_TOKEN_PRIVILEGES
  Present
  Enabled
  EnabledByDefault

Con una escritura de 16 bytes se puede cubrir más de un campo relevante si el destino está bien elegido.


12. Resolver direcciones de funciones en runtime

Para llamar funciones válidas de kernel, hace falta conocer sus direcciones reales bajo KASLR.

El método explicado:

1. Conseguir leak de base del módulo kernel objetivo.
2. Cargar el mismo módulo en user-mode con LoadLibrary.
3. Resolver la función con GetProcAddress.
4. Calcular offset = funcion_user - base_user.
5. Calcular funcion_kernel = base_kernel_filtrada + offset.

Ejemplo conceptual:

HMODULE userMod = LoadLibraryA("ntoskrnl.exe");
FARPROC userFn = GetProcAddress(userMod, "NombreFuncion");
uintptr_t offset = (uintptr_t)userFn - (uintptr_t)userMod;
uintptr_t kernelFn = leakedKernelBase + offset;

También se podría hardcodear por versión, pero resolver en runtime es más robusto:

  • tolera pequeñas diferencias de layout;
  • evita mantener tablas por build;
  • reduce errores al cambiar de VM o patch level.

13. Errores comunes

Confundir .process con ejecución real

Cambiar contexto con .process sirve para inspección, pero no hace que GS:188 apunte mágicamente al thread correcto.

Leer GS:188 desde el proceso equivocado

Si el debugger está parado en System o en otro thread, el _KTHREAD leído no corresponde al target.

Usar offsets de otro build

En la clase se vieron offsets como 0x220 y 0x232, pero hay que confirmarlos siempre con símbolos:

dt nt!_KTHREAD Process
dt nt!_KTHREAD PreviousMode

No restaurar PreviousMode

Si se modifica para abrir una ventana de escritura, hay que volverlo al valor original al terminar.

Asumir que KCFG bloquea todo

KCFG complica saltos arbitrarios, pero una cadena de funciones completas y válidas puede seguir siendo viable si los argumentos son controlables.


14. Workflow mental

1. Identificar build y símbolos.
2. Conseguir _KTHREAD correcto del proceso objetivo.
3. Confirmar offsets con dt, no con memoria.
4. Leer PreviousMode con db.
5. Modificar con eb solo durante la ventana necesaria.
6. Usar la ventana para construir primitiva persistente: token/privileges.
7. Restaurar PreviousMode.
8. Si el camino es UAF/vtable, preferir funciones válidas completas para convivir con KCFG.
9. Resolver direcciones por offset runtime: LoadLibrary + GetProcAddress + kernel base leak.

15. Cheat sheet

WinDbg — procesos, threads y contexto

!process -1 0
!process -1 4
!process 0 4 <nombre.exe>
!process 0 4 <PID>
.process /p /r <EPROCESS>
g

WinDbg — _KTHREAD

dt nt!_KTHREAD <KTHREAD>
dt nt!_KTHREAD <KTHREAD> Process
dt nt!_KTHREAD <KTHREAD> PreviousMode

Leer/escribir PreviousMode

db <KTHREAD>+232 L1
eb <KTHREAD>+232 0
eb <KTHREAD>+232 1

Offsets vistos en la clase

Campo Offset visto Nota
_KTHREAD.Process 0x220 en ese build, apunta al _KPROCESS/inicio de _EPROCESS
_KTHREAD.PreviousMode 0x232 byte editado con eb

Vtable/UAF bajo KCFG

RCX = this controlado
[RCX] = vtable fake
call [vtable + n] = función válida desde su inicio

Resolver función kernel por offset

offset = GetProcAddress(LoadLibrary(modulo), funcion) - base_user
kernel_func = leaked_kernel_module_base + offset

Idea final

La clase deja dos ideas conectadas:

  1. PreviousMode puede convertir conocimiento de direcciones en una ventana real de lectura/escritura kernel, pero en builds nuevos esa ventana se endurece y puede ser restaurada automáticamente.
  2. Para convivir con KCFG, no siempre hace falta saltar a gadgets arbitrarios: a veces alcanza con encadenar funciones válidas completas cuyos argumentos salen de un objeto/vtable controlado.