
Capture les codes PIN Windows Hello à la saisie en injectant une DLL dans LogonUI.exe qui place un point d'arrêt matériel CPU sur NgcSerializeConvenienceCredential, ainsi qu'un énumérateur WHFB/NGC/TPM.
Sur un hôte où Windows Hello est protégé par VBS/VSM (un conteneur NgcIso isolé dans VTL1) + un TPM, le PIN n'est pas récupérable hors ligne et la force brute en ligne est bloquée par le trustlet du noyau sécurisé. La seule voie fiable est la capture à la saisie.
Ce projet capture le PIN en plaçant un point d'arrêt matériel CPU sur la fonction qui
emballe le PIN saisi dans LogonUI.exe, en lisant l'argument depuis le contexte du thread piégé
dans un Vectored Exception Handler, et en laissant l'instruction originale et non modifiée
s'exécuter, de sorte que la connexion par PIN réussit toujours normalement.
Il fournit également Get-WHFBKeys.ps1, un outil complet d'énumération WHFB/NGC/TPM et de collecte de matériel de clé.
Validé de bout en bout sur Windows 11 26200 (Home) avec un fTPM 2.0 Intel et VBS actif.
| Protection observée | Conséquence |
|---|---|
VBS / VSM actif (statut VBS de Win32_DeviceGuard = 2, LsaIso.exe présent) | La hiérarchie de clés réside en VTL1 (noyau sécurisé), inaccessible depuis SYSTEM en VTL0 |
Conteneur NgcIso (NgcIsoHeader_… dans Container.json / blobs de protecteur NgcIsoCtnr.dll chargé dans NgcSvc) | Le secret du PIN est descellé à l'intérieur du trustlet, pas en mode utilisateur normal |
Matériel maître scellé par TPM (srk dans Container.json, type de clé impl) | Aucune clé en clair sur le disque, le descellement nécessite le TPM |
Anti-hammering (softLockoutAttemptLimit = 5, sealedLockoutAuth dans Protectors.json) | La devinette en ligne verrouille le compte, le compteur est appliqué par VTL1, non réinitialisable depuis le disque |
À l'écran de verrouillage / de connexion, le PIN est saisi dans le fournisseur d'informations d'identification NGC hébergé par
LogonUI.exe (s'exécute en tant que SYSTEM, pas un PPL, lancé à la demande). Le fournisseur remet le
PIN saisi à ngckeyenum.dll!NgcSerializeConvenienceCredential, dont l'arg1 (RCX) pointe vers :
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"
Le hooking classique en ligne/trampoline de NgcSerializeConvenienceCredential (ou de
NgcPackAuthBuffer) corrompt l'identifiant de connexion sérialisé. Le symptôme est désagréable et
silencieux : l'écran de connexion se recharge simplement sans erreur, le PIN correct est rejeté, et
des rejets répétés verrouillent le PIN (forçant la connexion par mot de passe). Tuer de force NgcSvc
perturbe de la même manière le conteneur.
Nous avons besoin d'une primitive de capture qui lit l'argument sans modifier un seul octet de code ni le flux de contrôle de la fonction cible.
ngccap.dll (voir src/ngccap.c) est injectée dans LogonUI.exe et :
Arme un point d'arrêt matériel d'exécution CPU. Elle résout
ngckeyenum!NgcSerializeConvenienceCredential, puis définit DR0 = cible et
DR7 = L0 | exec | len-1 sur chaque thread du processus (via
CreateToolhelp32Snapshot → OpenThread → SuspendThread → GetThreadContext /
SetThreadContext avec CONTEXT_DEBUG_REGISTERS → ResumeThread).
Traite le piège dans un gestionnaire d'exceptions vectorisé. Lorsque la fonction est appelée, le CPU
lève EXCEPTION_SINGLE_STEP avant d'exécuter l'instruction. Le VEH vérifie
ContextRecord->Rip == target, lit Rcx → {buffer, length}, copie le PIN ASCII,
puis :
EFlags afin que le point d'arrêt ne se redéclenche pas immédiatement, etEXCEPTION_CONTINUE_EXECUTION.L'instruction d'origine s'exécute, sans modification. Comme aucun code n'a été patché et qu'aucun
trampoline ni cadre de pile supplémentaire n'a été introduit, NgcSerializeConvenienceCredential s'exécute
exactement comme elle l'aurait fait. L'identifiant est construit correctement, LSA l'accepte, et la
connexion par PIN réussit normalement.
Se nettoie elle-même. Après la première capture, la DLL efface DR0/DR7 sur tous les threads et retire le VEH, laissant le processus intact.
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 s'exécute en tant que
SYSTEM mais n'est pas PPL, donc un jeton d'administrateur élevé peut l'ouvrir et y injecter du code.:: 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"
Les chemins sont configurables : LOGPATH en haut de ngccap.c et $plog dans logonui_watch.ps1
doivent correspondre (par défaut C:\Windows\Temp\pin_capture.log).
Get-WHFBKeys.ps1 rapporte l'état du TPM, l'approvisionnement device-join / NGC, la politique WHFB, le magasin
de conteneurs NGC sur disque (conteneurs → protecteurs → clés, avec auto-élévation vers SYSTEM), les clés NCrypt
(Passport KSP / Platform Crypto Provider / Software KSP avec export de clé publique), et l'enrôlement
biométrique, en émettant un rapport JSON. Il n'extrait jamais les clés privées (scellées par le TPM, non exportables) ;
il vide les clés publiques, les métadonnées et les blobs de protecteurs sur disque.
powershell -ExecutionPolicy Bypass -File .\Get-WHFBKeys.ps1 -DumpBlobs -OutDir .\out
hors ligne et par force brute s'avèrent fermés, et le PIN capturé pendant que le propriétaire se connectait normalement avec le bon PIN à l'aide de la DLL de point d'arrêt matériel.
Pour un usage éducatif et de recherche en sécurité autorisée uniquement. Aucune garantie.