Explotación de condiciones de carrera del kernel mediante rutas lentas del Object Manager
Tip
Aprende y practica Hacking en AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP:HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Hacking en Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Apoya a HackTricks
- Revisa los planes de suscripción!
- Únete al 💬 grupo de Discord o al grupo de telegram o síguenos en Twitter 🐦 @hacktricks_live.
- Comparte trucos de hacking enviando PRs a los HackTricks y HackTricks Cloud repositorios de github.
Por qué ampliar la ventana de la condición de carrera importa
Muchos LPEs del kernel de Windows siguen el patrón clásico check_state(); NtOpenX("name"); privileged_action();. En hardware moderno, una llamada en frío a NtOpenEvent/NtOpenSection resuelve un nombre corto en ~2 µs, dejando casi no tiempo para cambiar el estado verificado antes de que ocurra la acción privilegiada. Forzando deliberadamente que la búsqueda en el Object Manager Namespace (OMNS) del paso 2 tome decenas de microsegundos, el atacante gana suficiente tiempo para ganar de forma consistente carreras que de otro modo serían inestables sin necesitar miles de intentos.
Detalles internos de la resolución en el Object Manager en pocas palabras
- OMNS structure – Nombres como
\BaseNamedObjects\Foose resuelven componente por componente. Cada componente obliga al kernel a localizar/abrir un Directorio de objetos y comparar cadenas Unicode. Se pueden atravesar enlaces simbólicos (por ejemplo, letras de unidad) en el camino. - UNICODE_STRING limit – Las rutas OM se llevan dentro de un
UNICODE_STRINGcuyoLengthes un valor de 16 bits. El límite absoluto es 65 535 bytes (32 767 puntos de código UTF-16). Con prefijos como\BaseNamedObjects\, un atacante aún controla ≈32 000 caracteres. - Attacker prerequisites – Cualquier usuario puede crear objetos bajo directorios escribibles como
\BaseNamedObjects. Cuando el código vulnerable usa un nombre dentro de estos, o sigue un enlace simbólico que apunte allí, el atacante controla el rendimiento de la búsqueda sin privilegios especiales.
Primitiva de ralentización #1 – Componente único máximo
El coste de resolver un componente es aproximadamente lineal con su longitud porque el kernel debe realizar una comparación Unicode contra cada entrada en el directorio padre. Crear un evento con un nombre de 32 kB de longitud aumenta inmediatamente la latencia de NtOpenEvent de ~2 µs a ~35 µs en Windows 11 24H2 (Snapdragon X Elite testbed).
std::wstring path;
while (path.size() <= 32000) {
auto result = RunTest(L"\\BaseNamedObjects\\A" + path, 1000);
printf("%zu,%f\n", path.size(), result);
path += std::wstring(500, 'A');
}
Notas prácticas
- Puedes alcanzar el límite de longitud usando cualquier named kernel object (events, sections, semaphores…).
- Symbolic links o reparse points pueden apuntar un nombre corto “victim” a este giant component para que the slowdown se aplique de forma transparente.
- Because everything lives in user-writable namespaces, the payload funciona desde un standard user integrity level.
Slowdown primitive #2 – Deep recursive directories
Una variante más agresiva reserva una cadena de miles de directorios (\BaseNamedObjects\A\A\...\X). Cada salto activa la lógica de resolución de directorios (ACL checks, hash lookups, reference counting), por lo que la latencia por nivel es mayor que la de una single string compare. Con ~16 000 niveles (limitados por el mismo tamaño UNICODE_STRING), los tiempos empíricos superan la barrera de 35 µs alcanzada por long single components.
ScopedHandle base_dir = OpenDirectory(L"\\BaseNamedObjects");
HANDLE last_dir = base_dir.get();
std::vector<ScopedHandle> dirs;
for (int i = 0; i < 16000; i++) {
dirs.emplace_back(CreateDirectory(L"A", last_dir));
last_dir = dirs.back().get();
if ((i % 500) == 0) {
auto result = RunTest(GetName(last_dir) + L"\\X", iterations);
printf("%d,%f\n", i + 1, result);
}
}
Tips:
- Alterna el carácter por nivel (
A/B/C/...) si el directorio padre empieza a rechazar duplicados. - Mantén un array de handles para poder borrar la cadena limpiamente tras la explotación y evitar contaminar el namespace.
Primitiva de ralentización #3 – Shadow directories, hash collisions & symlink reparses (minutos en lugar de microsegundos)
Los object directories soportan shadow directories (búsquedas de fallback) y tablas hash bucketed para las entradas. Abusa de ambos junto con el límite de 64 componentes para reparse de symlink para multiplicar la ralentización sin exceder la longitud UNICODE_STRING:
- Crea dos directorios bajo
\BaseNamedObjects, p. ej.A(shadow) yA\A(target). Crea el segundo usando el primero como shadow directory (NtCreateDirectoryObjectEx), de modo que las búsquedas faltantes enAcaigan enA\A. - Llena cada directorio con miles de colliding names que caigan en el mismo hash bucket (p. ej., variando dígitos finales mientras se mantiene el mismo valor de
RtlHashUnicodeString). Las búsquedas ahora degradan a escaneos lineales O(n) dentro de un único directorio. - Construye una cadena de ~63 object manager symbolic links que reparseen repetidamente hacia el largo sufijo
A\A\…, consumiendo el presupuesto de reparse. Cada reparse reinicia el análisis desde el principio, multiplicando el coste de las colisiones. - La búsqueda del componente final (
...\\0) ahora toma minutos en Windows 11 cuando hay 16 000 colisiones por directorio, proporcionando una victoria de race prácticamente garantizada para LPEs kernel de un solo intento.
ScopedHandle shadow = CreateDirectory(L"\\BaseNamedObjects\\A");
ScopedHandle target = CreateDirectoryEx(L"A", shadow.get(), shadow.get());
CreateCollidingEntries(shadow, 16000, dirs);
CreateCollidingEntries(target, 16000, dirs);
CreateSymlinkChain(shadow, LongSuffix(L"\\A", 16000), 63);
printf("%f\n", RunTest(LongSuffix(L"\\A", 16000) + L"\\0", 1));
Por qué importa: Una ralentización de minutos convierte los race-based LPEs de un solo intento en exploits deterministas.
Notas de retest de 2025 y herramientas listas para usar
- James Forshaw republicó la técnica con tiempos actualizados en Windows 11 24H2 (ARM64). Baseline opens se mantienen en ~2 µs; un componente de 32 kB eleva esto a ~35 µs, y shadow-dir + collision + 63-reparse chains aún alcanzan ~3 minutos, confirmando que los primitives sobreviven a las builds actuales. Source code y perf harness están en la publicación actualizada de Project Zero.
- Puedes scriptar la configuración usando el bundle público
symboliclink-testing-tools:CreateObjectDirectory.exepara spawnear el par shadow/target yNativeSymlink.exeen un bucle para emitir la cadena de 63 saltos. Esto evita wrappersNtCreate*escritos a mano y mantiene las ACLs consistentes.
Midiendo tu ventana de race
Inserta un harness rápido dentro de tu exploit para medir cuánto se amplía la ventana en el hardware de la víctima. El snippet de abajo abre el objeto objetivo iterations veces y devuelve el coste promedio por apertura usando QueryPerformanceCounter.
static double RunTest(const std::wstring name, int iterations,
std::wstring create_name = L"", HANDLE root = nullptr) {
if (create_name.empty()) {
create_name = name;
}
ScopedHandle event_handle = CreateEvent(create_name, root);
ObjectAttributes obja(name);
std::vector<ScopedHandle> handles;
Timer timer;
for (int i = 0; i < iterations; ++i) {
HANDLE open_handle;
Check(NtOpenEvent(&open_handle, MAXIMUM_ALLOWED, &obja));
handles.emplace_back(open_handle);
}
return timer.GetTime(iterations);
}
Los resultados se incorporan directamente a tu estrategia de orquestación de la condición de carrera (p. ej., número de hilos necesarios, intervalos de espera, qué tan pronto necesitas cambiar el estado compartido).
Flujo de explotación
- Localiza el open vulnerable – Rastrea la ruta en el kernel (vía símbolos, ETW, trazado por hypervisor o ingeniería inversa) hasta encontrar una llamada
NtOpen*/ObOpenObjectByNameque recorra un nombre controlado por el atacante o un enlace simbólico en un directorio escribible por el usuario. - Sustituye ese nombre por una ruta lenta
- Crea el componente largo o la cadena de directorios bajo
\BaseNamedObjects(u otra raíz OM escribible). - Crea un enlace simbólico de modo que el nombre que espera el kernel ahora resuelva hacia la ruta lenta. Puedes apuntar la búsqueda de directorio del driver vulnerable a tu estructura sin tocar el objetivo original.
- Desencadenar la condición de carrera
- Thread A (víctima) ejecuta el código vulnerable y queda bloqueado dentro de la búsqueda lenta.
- Thread B (atacante) cambia el estado protegido (p. ej., intercambia un handle de archivo, reescribe un enlace simbólico, alterna la seguridad del objeto) mientras Thread A está ocupado.
- Cuando Thread A se reanuda y realiza la acción privilegiada, observa un estado obsoleto y ejecuta la operación controlada por el atacante.
- Limpieza – Elimina la cadena de directorios y los enlaces simbólicos para evitar dejar artefactos sospechosos o romper usuarios legítimos de IPC.
Consideraciones operativas
- Combina primitivas – Puedes usar un nombre largo por nivel en una cadena de directorios para aumentar aún más la latencia hasta agotar el tamaño de
UNICODE_STRING. - Bugs de un solo disparo – La ventana ampliada (decenas de microsegundos a minutos) hace realistas los bugs de “single trigger” cuando se combinan con pinning de afinidad de CPU o preemptión asistida por hypervisor.
- Efectos secundarios – La desaceleración solo afecta la ruta maliciosa, por lo que el rendimiento general del sistema permanece sin cambios; los defensores rara vez lo notarán a menos que monitoricen el crecimiento del espacio de nombres.
- Limpieza – Conserva handles para cada directorio/objeto que crees para poder llamar a
NtMakeTemporaryObject/NtClosedespués. De lo contrario, las cadenas de directorios sin límite podrían persistir tras reinicios. - Races en el sistema de archivos – Si la ruta vulnerable acaba resolviéndose a través de NTFS, puedes encadenar un Oplock (p. ej.,
SetOpLock.exedel mismo toolkit) sobre el archivo subyacente mientras corre la desaceleración del OM, congelando al consumidor por milisegundos adicionales sin alterar el grafo del OM.
Notas defensivas
- El código del kernel que depende de objetos nombrados debería revalidar el estado sensible a seguridad después del open, o tomar una referencia antes de la comprobación (cerrando la brecha TOCTOU).
- Aplica límites superiores en la profundidad/longitud de las rutas del OM antes de desreferenciar nombres controlados por el usuario. Rechazar nombres excesivamente largos obliga a los atacantes a volver a la ventana de microsegundos.
- Instrumenta el crecimiento del espacio de nombres del object manager (ETW
Microsoft-Windows-Kernel-Object) para detectar cadenas sospechosas de miles de componentes bajo\BaseNamedObjects.
References
- Project Zero – Windows Exploitation Techniques: Winning Race Conditions with Path Lookups
- googleprojectzero/symboliclink-testing-tools
Tip
Aprende y practica Hacking en AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP:HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Hacking en Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Apoya a HackTricks
- Revisa los planes de suscripción!
- Únete al 💬 grupo de Discord o al grupo de telegram o síguenos en Twitter 🐦 @hacktricks_live.
- Comparte trucos de hacking enviando PRs a los HackTricks y HackTricks Cloud repositorios de github.


