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é leerGS:188desde 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 paraKCFG. También se repasa una cadena de llamadas basada en vtable/control delthispara convertir unUse-After-Freeen una primitiva de escritura de 16 bytes.
Tabla de Contenidos¶
- Contexto de la clase
PreviousMode: qué habilita y por qué importaGS:188: error común al leer el current thread- Cómo obtener el
_KTHREADcorrecto desde WinDbg - Llegar de
_KTHREADa_EPROCESS - Modificar
PreviousMode - Qué cambia en
25H2 - Usar
PreviousModecomo puente para token stealing - UAF + vtable controlada
KCFG: por qué llamar funciones enteras puede seguir funcionando- Cadena de funciones para construir un write-what-where
- Resolver direcciones de funciones en runtime
- Errores comunes
- Workflow mental
- 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.PreviousModepara que ciertas rutas traten la operación como kernel-mode. - En builds más nuevos (
25H2en la demo), Microsoft endurece este camino y reescribe/restauraPreviousModeal 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
PreviousModepara 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,
GSapunta a estructuras user del thread, comoTEB. - En kernel-mode,
GSapunta a estructuras kernel por CPU, comoKPCR/KPRCB. - Un acceso típico como
GS:188puede usarse para llegar al_KTHREADactual cuando realmente se está ejecutando en ese contexto.
El problema:
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
.processy leerGS: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 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:
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:
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:
6. Modificar PreviousMode¶
En la demo, PreviousMode está en el offset 0x232 del _KTHREAD.
Para leer el byte:
Para escribirlo:
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
0para 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:
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
23H2se puede conocer la dirección del_KTHREADy del campoPreviousModepor leaks/consultas disponibles. - Al pisar
PreviousMode, se habilita temporalmente lectura/escritura kernel. - En
25H2, el sistema vuelve a reescribir/restaurarPreviousModeal 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:
PreviousModese 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.Tokencontiene 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
RCXy 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
RCXcomothis; - 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:
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:
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¶
Leer/escribir PreviousMode¶
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¶
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:
PreviousModepuede 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.- 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.