
Captura los PIN de Windows Hello en el momento de la introducción inyectando una DLL en LogonUI.exe que establece un punto de interrupción de hardware de la CPU en NgcSerializeConvenienceCredential, además de un enumerador de WHFB/NGC/TPM.
En un host donde Windows Hello está protegido por VBS/VSM (un contenedor NgcIso aislado en VTL1) + un TPM, el PIN no es recuperable offline y la fuerza bruta online está bloqueada por el trustlet del kernel seguro. La única ruta fiable es la captura en la entrada.
Este proyecto captura el PIN colocando un breakpoint de hardware de CPU en la función que
empaqueta el PIN tecleado dentro de LogonUI.exe, leyendo el argumento desde el contexto del hilo atrapado en un Vectored Exception Handler, y dejando que la instrucción original, sin modificar, se ejecute, de modo que el inicio de sesión con PIN sigue funcionando con normalidad.
También incluye Get-WHFBKeys.ps1, un enumerador completo de WHFB/NGC/TPM y recolector de material de claves.
Validado de extremo a extremo en Windows 11 26200 (Home) con un Intel fTPM 2.0 y VBS en ejecución.
| Protección observada | Consecuencia |
|---|---|
VBS / VSM en ejecución (estado VBS de Win32_DeviceGuard = 2, LsaIso.exe presente) | La jerarquía de claves reside en VTL1 (kernel seguro), inaccesible desde SYSTEM en VTL0 |
Contenedor NgcIso (NgcIsoHeader_… en Container.json / blobs del protector NgcIsoCtnr.dll cargados en NgcSvc) | El secreto del PIN se desella dentro del trustlet, no en modo usuario normal |
Material maestro sellado por TPM (srk en Container.json, tipo de clave impl) | No hay clave en texto plano en disco, el desellado requiere el TPM |
Anti-hammering (softLockoutAttemptLimit = 5, sealedLockoutAuth en Protectors.json) | El intento online bloquea, el contador lo aplica VTL1, no es reiniciable desde disco |
En la pantalla de bloqueo / inicio de sesión, el PIN se teclea en el proveedor de credenciales NGC alojado por
LogonUI.exe (se ejecuta como SYSTEM, no como PPL, lanzado bajo demanda). El proveedor entrega el
PIN tecleado a ngckeyenum.dll!NgcSerializeConvenienceCredential, cuyo arg1 (RCX) apunta a:
struct { // arg1
uint64_t type; // 1 (convenience-PIN credential)
void* buffer; // -> ASCII PIN, e.g. 0x32 0x30 0x30 0x32 = "2002" example PIN
uint32_t length; // digit count (e.g. 4)
};
// arg2 = full NGC key name "S-1-5-...-1001/<container-guid>//<key-guid>"
// arg3 = username, arg4 = computername, arg5 = SID, arg6 = "Negotiate"
El hooking clásico inline/trampoline de NgcSerializeConvenienceCredential (o de
NgcPackAuthBuffer) corrompe la credencial de inicio de sesión serializada. El síntoma es desagradable y
silencioso: la pantalla de inicio de sesión simplemente se recarga sin ningún error, el PIN correcto se rechaza y los rechazos repetidos bloquean temporalmente el PIN (forzando el inicio de sesión con contraseña). Matar forzosamente NgcSvc
también interrumpe el contenedor de la misma manera.
Necesitamos una primitiva de captura que lea el argumento sin cambiar un solo byte del código ni el flujo de control de la función objetivo.
ngccap.dll (véase src/ngccap.c) se inyecta en LogonUI.exe y:
Arma un breakpoint de ejecución por hardware de la CPU. Resuelve
ngckeyenum!NgcSerializeConvenienceCredential, luego establece DR0 = objetivo y
DR7 = L0 | exec | len-1 en cada hilo del proceso (mediante
CreateToolhelp32Snapshot → OpenThread → SuspendThread → GetThreadContext /
SetThreadContext con CONTEXT_DEBUG_REGISTERS → ResumeThread).
Gestiona la trampa en un Vectored Exception Handler. Cuando se entra en la función, la CPU
genera EXCEPTION_SINGLE_STEP antes de ejecutar la instrucción. El VEH comprueba
ContextRecord->Rip == target, lee Rcx → {buffer, length}, copia el PIN ASCII,
y luego:
EFlags para que el breakpoint no se vuelva a disparar inmediatamente, yEXCEPTION_CONTINUE_EXECUTION.La instrucción original se ejecuta, sin modificaciones. Como no se parcheó ningún código ni se
introdujo ningún trampoline/marco de pila adicional, NgcSerializeConvenienceCredential se ejecuta
exactamente como lo habría hecho. La credencial se construye correctamente, LSA la acepta y el
inicio de sesión con PIN se realiza con normalidad.
Se autolimpia. Tras la primera captura, el DLL borra DR0/DR7 en todos los hilos y elimina el VEH, dejando el proceso intacto.
WHFB-PIN-Capture/
├─ README.md
├─ Get-WHFBKeys.ps1 # WHFB/NGC/TPM enumerator + key-material harvester
└─ src/
├─ ngccap.c # HW-breakpoint capture DLL
├─ build.bat # cl.exe build (MSVC 2022 Build Tools)
└─ logonui_watch.ps1 # injects ngccap.dll into LogonUI on sight
SeDebugPrivilege). LogonUI se ejecuta como
SYSTEM pero no es PPL, por lo que un token de administrador elevado puede abrirlo e inyectarlo.:: 1) build the capture DLL
cd src
build.bat
:: 2) start the injector (elevated admin PowerShell)
powershell -ExecutionPolicy Bypass -File .\logonui_watch.ps1
3) Lock the workstation (Win+L) and sign back in with your PIN, it works normally.
4) Read the captured PIN:
type C:\Windows\Temp\pin_capture.log
-> >>> PIN CAPTURED = "1234"
Las rutas son configurables: LOGPATH en la parte superior de ngccap.c y $plog en logonui_watch.ps1
deben coincidir (por defecto C:\Windows\Temp\pin_capture.log).
Get-WHFBKeys.ps1 informa el estado del TPM, la unión al dispositivo / aprovisionamiento de NGC, la política de WHFB, el almacén de contenedores NGC en disco (contenedores → protectores → claves, auto-elevándose a SYSTEM), las claves NCrypt (Passport KSP / Platform Crypto Provider / Software KSP con exportación de clave pública) y la inscripción biométrica, emitiendo un informe JSON. Nunca extrae claves privadas (selladas por TPM, no exportables); vuelca claves públicas, metadatos y blobs de protectores en disco.
powershell -ExecutionPolicy Bypass -File .\Get-WHFBKeys.ps1 -DumpBlobs -OutDir .\out
sin conexión y de fuerza bruta que se muestran cerradas, y el PIN capturado mientras el propietario iniciaba sesión normalmente con el PIN correcto usando la DLL de punto de interrupción de hardware.
Solo para uso educativo y de investigación de seguridad autorizada. Sin garantía.