
Cattura i PIN di Windows Hello al momento dell'inserimento iniettando una DLL in LogonUI.exe che imposta un breakpoint hardware della CPU su NgcSerializeConvenienceCredential, oltre a un enumeratore WHFB/NGC/TPM.
Su un host in cui Windows Hello è protetto da VBS/VSM (un container NgcIso isolato in VTL1) + un TPM, il PIN non è recuperabile offline e il brute-force online è bloccato dal trustlet del kernel sicuro. L'unica via affidabile è la cattura all'atto dell'inserimento.
Questo progetto cattura il PIN posizionando un breakpoint hardware della CPU sulla funzione che
impacchetta il PIN digitato all'interno di LogonUI.exe, leggendo l'argomento dal contesto del thread intercettato
in un Vectored Exception Handler, e lasciando che l'istruzione originale e non modificata venga eseguita, così
che il login con PIN riesca comunque normalmente.
Include inoltre Get-WHFBKeys.ps1, un enumeratore completo di WHFB/NGC/TPM e raccoglitore di materiale chiave.
Validato end-to-end su Windows 11 26200 (Home) con un Intel fTPM 2.0 e VBS in esecuzione.
| Protezione osservata | Conseguenza |
|---|---|
VBS / VSM in esecuzione (stato VBS di Win32_DeviceGuard = 2, LsaIso.exe presente) | La gerarchia delle chiavi risiede in VTL1 (kernel sicuro), irraggiungibile da SYSTEM in VTL0 |
Container NgcIso (NgcIsoHeader_… in Container.json / blob del protettore NgcIsoCtnr.dll caricato in NgcSvc) | Il segreto del PIN viene sbloccato all'interno del trustlet, non in modalità utente normale |
Materiale master sigillato dal TPM (srk in Container.json, tipo di chiave impl) | Nessuna chiave in chiaro su disco, lo sblocco richiede il TPM |
Anti-hammering (softLockoutAttemptLimit = 5, sealedLockoutAuth in Protectors.json) | I tentativi online causano il blocco, il contatore è imposto da VTL1, non azzerabile dal disco |
Alla schermata di blocco / accesso, il PIN viene digitato nel provider di credenziali NGC ospitato da
LogonUI.exe (viene eseguito come SYSTEM, non come PPL, avviato su richiesta). Il provider passa il
PIN digitato a ngckeyenum.dll!NgcSerializeConvenienceCredential, il cui arg1 (RCX) punta 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"
L'hooking classico inline/trampoline di NgcSerializeConvenienceCredential (o di
NgcPackAuthBuffer) corrompe la credenziale di accesso serializzata. Il sintomo è insidioso e
silenzioso: la schermata di accesso si ricarica semplicemente senza alcun errore, il PIN corretto viene rifiutato e i rifiuti ripetuti bloccano temporaneamente il PIN (forzando l'accesso con password). Anche terminare forzatamente NgcSvc interrompe allo stesso modo il container.
Abbiamo bisogno di una primitiva di cattura che legga l'argomento senza modificare un singolo byte di codice o il flusso di controllo della funzione target.
ngccap.dll (vedi src/ngccap.c) viene iniettata in LogonUI.exe e:
Imposta un breakpoint hardware di esecuzione della CPU. Risolve
ngckeyenum!NgcSerializeConvenienceCredential, quindi imposta DR0 = target e
DR7 = L0 | exec | len-1 su ogni thread del processo (tramite
CreateToolhelp32Snapshot → OpenThread → SuspendThread → GetThreadContext /
SetThreadContext con CONTEXT_DEBUG_REGISTERS → ResumeThread).
Gestisce la trap in un Vectored Exception Handler. Quando la funzione viene invocata, la CPU
solleva EXCEPTION_SINGLE_STEP prima di eseguire l'istruzione. Il VEH verifica
ContextRecord->Rip == target, legge Rcx → {buffer, length}, copia il PIN ASCII,
quindi:
EFlags in modo che il breakpoint non si riattivi immediatamente, eEXCEPTION_CONTINUE_EXECUTION.L'istruzione originale viene eseguita, senza modifiche. Poiché nessun codice è stato patchato e nessun
trampoline/frame di stack aggiuntivo è stato introdotto, NgcSerializeConvenienceCredential viene eseguita
esattamente come avrebbe fatto. La credenziale viene costruita correttamente, LSA la accetta e il
login con PIN riesce normalmente.
Si ripulisce da sola. Dopo la prima cattura la DLL cancella DR0/DR7 su tutti i thread e rimuove il VEH, lasciando il processo immacolato.
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 viene eseguito come
SYSTEM ma non è PPL, quindi un token amministratore elevato può aprirlo e iniettarlo.:: 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"
I percorsi sono configurabili: LOGPATH all'inizio di ngccap.c e $plog in logonui_watch.ps1
devono corrispondere (predefinito C:\Windows\Temp\pin_capture.log).
Get-WHFBKeys.ps1 riporta lo stato del TPM, il device-join / provisioning NGC, la policy WHFB, l'archivio
su disco dei container NGC (container → protettori → chiavi, con auto-elevazione a SYSTEM), le chiavi NCrypt
(Passport KSP / Platform Crypto Provider / Software KSP con esportazione della chiave pubblica) e l'enrollment
biometrico, emettendo un report JSON. Non estrae mai le chiavi private (sigillate nel TPM, non esportabili);
esegue il dump delle chiavi pubbliche, dei metadati e dei blob dei protettori su disco.
powershell -ExecutionPolicy Bypass -File .\Get-WHFBKeys.ps1 -DumpBlobs -OutDir .\out
offline e i percorsi brute-force risultano chiusi, e il PIN catturato mentre il proprietario accedeva normalmente con il PIN corretto usando la DLL con hardware-breakpoint.
Solo per uso didattico e di ricerca sulla sicurezza autorizzata. Nessuna garanzia.