Skip to content
KitploitKITPLOIT
HerramientasBlog
Enviar
HerramientasBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
HallWatch — Usermode detector que captura indirect syscalls. Atrapa Hell's Hall, Tartarus' Gate, RecycledGate y VEH syscalls & Many more. | Kitploit
Herramientas/GitHubGitHub/zypherion-technologies/hallwatch
Herramientas DefensivasAnálisis de MalwareAnálisis de BinariosInteligencia de AmenazasRespuesta a Incidentes
GitHubzypherion-technologies/hallwatch

HallWatch

Usermode detector que captura indirect syscalls. Atrapa Hell's Hall, Tartarus' Gate, RecycledGate y VEH syscalls & Many more.

Ver Repositorio

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Compartir
8710hace 2 mesesRevisado por Kitploit

HallWatch

License: GPL v3 Website Discord Telegram X

Copyright (C) 2026 Adam Zypherion <[email protected]> — licenciado bajo GPL-3.0.


Las syscalls indirectas son simples una vez que has visto una. El loader recorre ntdll, lee el SSN del prólogo de Nt*, encuentra los dos bytes 0F 05 un poco más adelante, establece r10/rax/rdx/r8/r9 por sí mismo, y salta (jmp) directamente a esos dos bytes. La syscall ocurre desde dentro de ntdll. Tu hook sobre la exportación de kernel32 nunca se toca. Tu hook sobre la exportación de ntdll nunca se toca. [RSP] en el momento de la SYSCALL apunta de vuelta a la página RWX del loader, pero nada de la llamada parece inusual desde fuera.

root@kitploit:~
HellHall proc
    mov  r10, rcx
    mov  eax, dwSSN
    jmp  qword ptr [qAddr]
    ret
HellHall endp

Las variantes tienen nombres. Tartarus' Gate y RecycledGate usan la instrucción syscall de un stub pero el SSN de otro diferente, así que incluso si registras el nombre del stub que reporta tu hook, es una mentira. VEH syscall provoca intencionadamente una violación de acceso y usa su propio VEH para reescribir el contexto de manera que RIP aterrice en la instrucción syscall de ntdll con los registros ya preparados. Hell's Gate no usa ntdll en absoluto. El loader escribe 0F 05 en su propia página RWX y ejecuta desde allí.

La respuesta en modo kernel (KM) es un driver; quizás llegue a esto con el tiempo y publique un proyecto, pero no va a suceder pronto :D


PAGE_GUARD parece que debería funcionar. Marca la página que contiene los bytes de syscall con PAGE_GUARD, captura el STATUS_GUARD_PAGE_VIOLATION en un VEH, inspecciona, redirige a un trampolín privado, establece el flag de trap, sale de un solo paso, rearma la página. Una trampa por llamada Nt, sin importar cómo llegó el loader allí. Funciona contra todo excepto Hell's Gate.

El problema es que el SO no coopera. PAGE_GUARD es de un solo uso, ¿qué significa eso? Cada vez que se dispara, el bit se borra y tienes que volver a colocarlo. Tu manejador hace syscalls (NtProtect para volver a poner la guardia, NtContinue para reanudar) y esas syscalls tienen stubs, y esos stubs están en la página que acabas de proteger. Trabajé alrededor de la mayor parte de eso con stubs de syscall privados construidos en una página separada que poseíamos (asignar RWX, escribir mov r10,rcx; mov eax,SSN; syscall; ret, bloquear RX, nunca tocar ntdll), pero era frágil. Cada versión menor de Windows cambiaba la sincronización. Cada hilo que la muestra generaba era otra carrera contra el worker de integridad que reconstruía la guardia. El éxito de la demostración estaba entre el 30 y el 50 por ciento en diez ejecuciones en la misma máquina.

Eventualmente dejé de intentar convencer a Windows de que PAGE_GUARD se comportara como yo quería, y probé sobrescribir el byte en su lugar.


Si tienes problemas para entenderlo, mira este video :)

https://github.com/user-attachments/assets/0cb670fd-e51b-413e-bf00-08f9297888ed


El byte en la instrucción syscall es 0F 05. El primer byte solo (0F) es el prefijo para una familia de códigos de operación de dos bytes que incluye SYSCALL, CPUID y RDTSC. Por sí solo no es ejecutable; la CPU necesita el segundo byte para decodificar. Si reemplazas el primer byte con 0xCC (INT3), el par se convierte en CC 05, que la CPU decodifica como INT3 seguido de un byte extraviado al que nunca llega. Cualquier camino de código que aterrice en esa dirección genera EXCEPTION_BREAKPOINT.

Ese es el mecanismo completo. Nuestro VEH captura el breakpoint, busca a qué stub pertenece la dirección (construimos el mapa al inicio enumerando las exportaciones de ntdll), ejecuta tres comprobaciones sobre quien haya aparecido, y establece Context->Rip a un trampolín privado que realiza la syscall real y retorna. El byte permanece CC. El siguiente llamador lo encontrará de la misma manera, así que no hay cambios de ida y vuelta en la protección de página.

Para ser claros: sí, esto es hooking mediante sobrescritura de byte. Solo que no está en el byte que la gente suele entender por "hook de ntdll". El hook clásico de EDR sobrescribe los primeros bytes del stub con un JMP <my_func>:

root@kitploit:~
ntdll!NtAllocateVirtualMemory:
  E9 ?? ?? ?? ??       jmp my_hook     ; sobrescribe "mov r10, rcx"
  ...
  0F 05                syscall
  C3                   ret

Eso es exactamente lo que las syscalls indirectas evitan. El loader lee el SSN por sí mismo y salta directamente al 0F 05, el prólogo (y tu jmp) nunca se ejecuta, el hook nunca se dispara. Lo que nosotros hacemos es sobrescribir el propio byte de syscall:

root@kitploit:~
ntdll!NtAllocateVirtualMemory:
  4C 8B D1             mov r10, rcx     ; intacto
  B8 18 00 00 00       mov eax, 18h     ; intacto
  CC 05                int3 / 05        ; antes era 0F 05, escribimos CC sobre el 0F
  C3                   ret

Ahora no importa cómo llegaste a esa dirección: a través del prólogo, a través de un salto indirecto que omite el prólogo, a través de una reescritura de contexto VEH-syscall que coloca RIP allí, etc. Si la CPU ejecuta ese byte, atrapa. Misma familia de técnica que el hook clásico, ubicación diferente, cobertura completamente diferente.

El trampolín se ve así:

root@kitploit:~
F3 0F 1E FA              endbr64
49 89 CA                 mov r10, rcx
B8 <stub SSN>            mov eax, ssn
0F 05                    syscall
C3                       ret

Las tres comprobaciones no han cambiado desde la versión de PAGE_GUARD porque eran las tres comprobaciones correctas; solo necesitaban un lugar confiable donde vivir.

Dirección de retorno. [RSP] es donde la syscall habría retornado. Para una llamada real está dentro de ntdll, kernel32, kernelbase o una de las DLL de runtime relacionadas. Para Hell's Hall está dentro de cualquier página RWX desde la que el loader esté ejecutando. Tenemos una lista corta de destinos de retorno de confianza construida al inicio llamando a GetModuleHandle para esos nombres y leyendo los rangos .text de sus cabeceras PE.

SSN. El prólogo del stub ya se ha ejecutado antes de que llegue al byte de syscall, así que eax contiene el valor que se cargó en él. Si el loader hizo un intercambio de Tartarus, ese valor no coincidirá con el SSN que leímos de este mismo stub durante la enumeración. Registramos la discrepancia, y el trampolín escribe el SSN correcto antes de su propia syscall, por lo que la función kernel que se ejecuta es la que pertenece al byte, no la que el loader quería. La técnica se registra y neutraliza en el mismo paso.

Recorrido de pila. RtlVirtualUnwind desde el contexto actual, cinco marcos hacia arriba. El RIP de cada marco debería estar dentro de un módulo conocido y debería tener una entrada RUNTIME_FUNCTION. El shellcode y los gadgets ROP fallan esto incluso cuando [RSP] en sí parece de confianza, lo cual un loader puede falsificar (puede predecir aproximadamente dónde estará su llamador en la memoria y forjar una dirección de retorno creíble allí).

Si alguna comprobación falla, empujamos una pequeña estructura en un anillo libre de bloqueos y el hilo de drenaje la imprime la próxima vez que se despierte:

root@kitploit:~
[!! hallwatch !!] syscall indirecta (llamador no confiable, SSN incorrecto para este stub)
    syscall      : NtAllocateVirtualMemory
    syscall rip  : 0x00007FF827660372
    return addr  : 0x00007FF7EED719D6
    rax (ssn)    : 0x0000000F (el stub codifica 0x00000018)
    thread       : 26388

Hell's Gate es donde INT3 deja de ayudar. Nunca parcheamos la página RWX del loader porque nunca supimos que existía.

Lo que hacemos en su lugar es el escáner más simple que podría funcionar. Cada 250 ms (básicamente demasiado frecuente, desperdicia ciclos de CPU pero es para una prueba de concepto) el worker de integridad recorre el espacio de direcciones con VirtualQuery, buscando cualquier región MEM_COMMIT con una protección de página ejecutable. Si la región está dentro de un módulo cargado, se omite. Si está dentro de nuestro propio grupo de trampolines, se omite. Lo que queda es memoria ejecutable externa. Escaneamos hasta 64 KB de ella en busca de pares de bytes 0F 05, deduplicamos por dirección, registramos cada acierto único una vez. Sé que es tonto y también desperdicias ciclos de CPU, pero es una PoC que no debería minimizar la situación, aunque actualmente lo hace.

Eso atrapa a Hell's Gate en una asignación RWX fresca y también atrapa a shadow ntdll (un loader que hace NtMapViewOfSection en ntdll.dll para obtener una copia privada en una nueva dirección). El caso shadow es divertido. El mapeo no está en nuestra instantánea de módulos, por lo que sus instrucciones syscall aparecen como bytes ejecutables externos, aunque provengan de una DLL perfectamente legítima y firmada en disco.

Otra variante: el atacante incrusta 0F 05 en el .text de su propio binario, por lo que la syscall vive dentro de un módulo cargado en lugar de una página RWX. El escáner de RWX externo omite los módulos cargados, por lo que esto solía pasar desapercibido. Así que tenemos otro escáner que recorre los bytes ejecutables de cada módulo, omite ntdll, win32u y wow64cpu (los tres módulos que legítimamente tienen instrucciones syscall), e informa de 0F 05 solo si hay un C3 dentro de los siguientes tres bytes. La comprobación de ret es lo que lo hace utilizable. 0F 05 por sí solo aparece en la salida del compilador como partes de instrucciones más grandes todo el tiempo, pero nunca está adyacente a un C3. Los stubs reales de syscall siempre lo están. Registro:

root@kitploit:~
[hallwatch] scan: module-text syscall stub at 0x00007FF7FFC9227C in HellsHall-Demo.exe -- Hell's Gate dentro del .text de un módulo cargado

El worker de integridad tiene otros dos pequeños trabajos. Cada tick lee el byte de cada stub parcheado y escribe CC si no es CC. Ese es el unhook más simple posible: la muestra lee el 0F original de algún lugar (un mapeo fresco de ntdll, el PE en disco, una tabla de bytes conocida) y lo escribe de nuevo sobre nuestro parche. Nosotros lo notamos en el siguiente tick y parcheamos de nuevo.

Cada quincuagésimo tick (aproximadamente 12.5 segundos), el worker instala nuestro VEH nuevamente al inicio de la cadena y elimina el manejador anterior. Una muestra puede recorrer LdrpVectorHandlerList y desregistrarnos; el worker limita cuánto tiempo permanece abierta esa ventana. Doce segundos y medio es una ventana larga si estás siendo atacado y corta si estás ejecutando malware en un sandbox. Probablemente la acortaré una vez que haya medido el costo de hacerlo más a menudo.

El hilo worker se ejecuta con un flag TLS que el VEH comprueba. Las syscalls del propio worker (NtProtect para parchear, NtQuery para el escaneo) desencadenan nuestros breakpoints como cualquier otra, pero el flag le dice al VEH que omita la lógica de detección y redirija silenciosamente a través del trampolín.


La sección crítica se inicializa a sí misma la primera vez que alguien llama, mediante un intercambio de comparación de tres estados (0 = no iniciado, 2 = ocupado, 1 = listo). Es feo, pero evita la necesidad de un constructor estático dentro de una DLL, lo que en Windows es un conjunto separado de problemas relacionados con el lock del loader, etc.

Un detalle de ABI que vale la pena señalar. INT3 es una trampa, lo que significa que Context->Rip apunta a la siguiente instrucción cuando se llama a nuestro VEH, no a la trampa en sí. Si parcheamos el byte en 0x7FF827660372, Context->Rip llega al VEH como 0x7FF827660373. El Record->ExceptionAddress apunta a la trampa, pero tenemos que restablecer Context->Rip = ExceptionAddress antes de redirigirlo al trampolín, de lo contrario el trampolín comienza un byte tarde y la syscall no hace nada útil. Te lo digo porque fue un bug que me llevó mucho tiempo encontrar.


Hay cuatro exportaciones. IscInitialize arma el detector; DllMain lo llama automáticamente, pero puedes llamarlo desde un proceso host si deseas un valor de retorno. IscGetDetectionCount devuelve un contador monótonamente creciente. IscShutdown espera a los manejadores en curso y restaura los bytes 0F. IscFlush drena el anillo de forma síncrona, útil si estás incrustando el detector en un sandbox que necesita los eventos antes de que la muestra salga.

La integración mínima es LoadLibrary. DllMain maneja la inicialización e inicia ambos hilos en segundo plano desde ahí.

Isc = Syscalls Indirectas


Cosas que aún no capturamos.

Una muestra que realiza comprobaciones de integridad.

Una muestra que utiliza stubs que no parcheamos. La lista de permitidos actual tiene unos 40 nombres que cubren primitivas ofensivas de memoria, proceso, hilo, sección, token y archivo. Agregar más es incremental siempre que DllMain termine rápidamente. Parchear los 488 stubs desde dentro del lock del loader es... ehh.

Una muestra que se ejecuta con privilegios de kernel. No es un problema de modo usuario.


INT3 es lo que terminamos eligiendo, pero el mecanismo de trampa no es la parte interesante. La razón por la que funciona mejor que PAGE_GUARD es que la trampa no requiere una negociación continua con el SO. El byte es CC. Permanece CC. El SO no tiene opinión sobre qué bytes viven en ntdll.

Descargar herramienta