[{"content":"Hello, I\u0026rsquo;m Aymen. I\u0026rsquo;m an international student passionate about everything related to cybersecurity and software.\nHere I will write about my learning journey, whether it be writeups, projects or articles.\nIn my spare time, I enjoy tackling online challenges and gain knowledge from resources on platforms like Root-Me and pwn.college.\nFeel free to contact me at erhaym [at] pm [dot] me anytime, for any queries.\n","permalink":"https://erhaym.github.io/about/","summary":"\u003cp\u003eHello, I\u0026rsquo;m Aymen. I\u0026rsquo;m an international student passionate about everything related to cybersecurity and software.\u003c/p\u003e\n\u003cp\u003eHere I will write about my learning journey, whether it be writeups, projects or articles.\u003c/p\u003e\n\u003cp\u003eIn my spare time, I enjoy tackling online challenges and gain knowledge from resources on platforms like \u003ca href=\"https://www.root-me.org/imvindex\"\u003eRoot-Me\u003c/a\u003e and \u003ca href=\"https://pwn.college/hacker/97990\"\u003epwn.college\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eFeel free to contact me at erhaym [at] pm [dot] me anytime, for any queries.\u003c/p\u003e","title":"About Me"},{"content":" This article explains how Chromium-based browsers store saved passwords on Linux, why this mechanism is insecure by design under local attacker assumptions, and demonstrates how stored credentials can be decrypted.\nEver got told to never click \u0026ldquo;Save\u0026rdquo; when your browser asks you to save the password you just entered? I often hear that, so I decided that I had to figure out exactly why this was deemed to be a bad practice.\nThis was an opportunity for me to explore the domain of cryptography and all of the fundamental theory and practice that I had acquired during my freshman year.\nThis article will be a documentation of my experimental journey to understand why saving passwords on your browser is generally considered a bad security practice, I will first dive into how the process of saving passwords works and the cryptography behind, before practically testing to retrieve and decrypt/deobfuscate my own passwords in order directly measure what exactly makes this practice insecure.\nUnderstanding how Chromium handles saved passwords Typically, when you press the \u0026ldquo;Save\u0026rdquo; button when getting that little pop up each time you register or login to a website for the first time, your browser stores the password you just agreed to save (alongside the username and other data) in a SQLite database inside your machine.\nOn Linux, you can take a look at that for yourself by inspecting the following path:\n~/.config/google-chrome/Default/ for Chrome\nor ~/.config/chromium/Default/ for Chromium\nYou should see a file named Login Data. That\u0026rsquo;s the database we\u0026rsquo;re interested in. We open it by using the command line interface sqlite3.\n$ sqlite3 \u0026#39;Login Data\u0026#39; SQLite version 3.45.1 2024-01-30 16:01:20 Enter \u0026#34;.help\u0026#34; for usage hints. sqlite\u0026gt; .tables insecure_credentials password_notes sync_model_metadata logins stats meta sync_entities_metadata The database includes multiple tables containing informations about the user\u0026rsquo;s login habits. But here we\u0026rsquo;re solely insterested in the logins table.\nOn sqlite3, we can take a look the table\u0026rsquo;s structure (schema):\nsqlite\u0026gt; .schema logins CREATE TABLE logins (origin_url VARCHAR NOT NULL, action_url VARCHAR, username_element VARCHAR, username_value VARCHAR, password_element VARCHAR, password_value BLOB, submit_element VARCHAR, signon_realm VARCHAR NOT NULL, date_created INTEGER NOT NULL, blacklisted_by_user INTEGER NOT NULL, scheme INTEGER NOT NULL, password_type INTEGER, times_used INTEGER, form_data BLOB, display_name VARCHAR, icon_url VARCHAR, federation_url VARCHAR, skip_zero_click INTEGER, generation_upload_status INTEGER, possible_username_pairs BLOB, id INTEGER PRIMARY KEY AUTOINCREMENT, date_last_used INTEGER NOT NULL DEFAULT 0, moving_blocked_for BLOB, date_password_modified INTEGER NOT NULL DEFAULT 0, sender_email VARCHAR, sender_name VARCHAR, date_received INTEGER, sharing_notification_displayed INTEGER NOT NULL DEFAULT 0, keychain_identifier BLOB, sender_profile_image_url VARCHAR, date_last_filled INTEGER NOT NULL DEFAULT 0, actor_login_approved INTEGER NOT NULL DEFAULT 0, UNIQUE (origin_url, username_element, username_value, password_element, signon_realm)); CREATE INDEX logins_signon ON logins (signon_realm); sqlite\u0026gt; Note: If you want to try this, make sure that your browser is closed, as the table will be locked if it is opened.\nLet\u0026rsquo;s query the database, we\u0026rsquo;ll select the following attributes: origin_url, username_value, password_value.\nsqlite\u0026gt; SELECT origin_url, username_value, password_value FROM logins; https://www.*******.org/|**************@gmail.com|v11U8��=��!��@� https://www.*******.com/|**************@gmail.com|v10C53S�Y�8�s@c�Da� [...] As we expect, the passwords are not shown in plain text. We notice that they\u0026rsquo;re preceded by two prefixes: \u0026ldquo;v11\u0026rdquo; and sometimes \u0026ldquo;v10\u0026rdquo;. We\u0026rsquo;ll get into that right now, but first, we want to manipulate the passwords, and that is not possible if we query the DB like this because sqlite3 is currently interpreting the encrypted passwords as UTF-8 text, thus corrupting the values. The password_value actually consists of binary data coming from an encryption/obfuscation process. We can get the raw data in hexadecimal by using the hex() function, we\u0026rsquo;ll specify as parameter the password_value attribute.\nSo we should type in the following query:\nSELECT origin_url, username_value, hex(password_value) FROM logins; Now let\u0026rsquo;s go back to those \u0026ldquo;v10\u0026rdquo; and \u0026ldquo;v11\u0026rdquo; prefixes. To understand these, we must go to Chromium\u0026rsquo;s source code.\nWhen inspecting the os_crypt_linux.cc file we stumble upon this:\n// Prefixes for cypher text returned by obfuscation version. We prefix the // ciphertext with this string so that future data migration can detect // this and migrate to full encryption without data loss. kObfuscationPrefixV10 // means that the hardcoded password will be used. kObfuscationPrefixV11 means // that a password is/will be stored using an OS-level library (e.g Libsecret). // V11 will not be used if such a library is not available. constexpr char kObfuscationPrefixV10[] = \u0026#34;v10\u0026#34;; constexpr char kObfuscationPrefixV11[] = \u0026#34;v11\u0026#34;; As we can see, we have two possible prefixes (\u0026ldquo;v10\u0026rdquo; or \u0026ldquo;v11\u0026rdquo;) used by Chromium to signal how the saved password is processed, they\u0026rsquo;re notably referred to as \u0026ldquo;ObfuscationPrefix\u0026rdquo;. Additionally we read that:\nThe \u0026ldquo;v10\u0026rdquo; prefix signals that the password is encrypted using a hardcoded password. The \u0026ldquo;v11\u0026rdquo; signals that the password is stored by using an OS-level library, Libsecret is given as an example. It is also said that V11 will not be used if the OS-level library is not available. The hardcoded password mentionned in the comments is \u0026quot;peanuts\u0026quot; and is actually in the same file, just a few lines ahead:\n// clang-format off // PBKDF2-HMAC-SHA1(1 iteration, key = \u0026#34;peanuts\u0026#34;, salt = \u0026#34;saltysalt\u0026#34;) constexpr auto kV10Key = std::to_array\u0026lt;uint8_t\u0026gt;({ 0xfd, 0x62, 0x1f, 0xe5, 0xa2, 0xb4, 0x02, 0x53, 0x9d, 0xfa, 0x14, 0x7c, 0xa9, 0x27, 0x27, 0x78, }); Note: We also see that the salt is set to \u0026quot;saltysalt\u0026quot;.\nThis tells us that Chromium derives the encryption key using PBKDF2-HMAC-SHA1, with:\nthe secret \u0026quot;peanuts\u0026quot; the salt \u0026quot;saltysalt\u0026quot; only 1 iteration (instantaneous derivation) const std::array\u0026lt;uint8_t, crypto::aes_cbc::kBlockSize\u0026gt; kIv{ \u0026#39; \u0026#39;, \u0026#39; \u0026#39;, \u0026#39; \u0026#39;, \u0026#39; \u0026#39;, \u0026#39; \u0026#39;, \u0026#39; \u0026#39;, \u0026#39; \u0026#39;, \u0026#39; \u0026#39;, \u0026#39; \u0026#39;, \u0026#39; \u0026#39;, \u0026#39; \u0026#39;, \u0026#39; \u0026#39;, \u0026#39; \u0026#39;, \u0026#39; \u0026#39;, \u0026#39; \u0026#39;, \u0026#39; \u0026#39;, }; This is what we call the Initialization Vector (IV) used for AES in CBC mode. As we can see the IV is fixed an consists of 16 space characters (0x20).\nAccessing the Keyring A keyring is a secure credential storage service provided by the operating system\u0026rsquo;s desktop environment. It is a centralized, encrypted database that store secrets, passwords, keys and certificates and make them available to applications.\nOn Linux Mint, which is the distribution I use, there\u0026rsquo;s an app called Passwords and Keys (Seahorse) to inspect and manage the keyring. When opening it, we\u0026rsquo;re met with a \u0026ldquo;Login\u0026rdquo; section, and in it is listed Chromium Safe Storage / Chrome Safe Storage: this is exactly what we\u0026rsquo;re looking for. When inspecting the item, we get a base64-encoded password.\nWhen V11 is used, this is the exact secret used by Chromium to encrypt the saved passwords.\nYou could also just type in $ secret-tool lookup application chromium (or chrome) in your terminal to retreive your browser\u0026rsquo;s secret.\nUnderstanding how the encryption / obfuscation works AES (Advanced Encryption Standard) is a symmetric block cipher that transforms data through multiple rounds of substitution and permutation operations based on the key.\nAES-CBC is AES in Cipher Block Chaining mode. AES encrypts data block by block (16 bytes). CBC is a mode of operation that defines how blocks are chained. In the encryption process, for each 16-byte block, it XORs the plaintext block with the previous ciphertext block, then encrypts the result with the AES key. For the first block, the “previous ciphertext block” is the IV.\nThe Initialization Vector (IV) is usually a non-secret, unpredictable value used as the initial input block in a chaining cipher mode (like CBC). Its cryptographic function is to provide probabilistic encryption, ensuring that encrypting identical plaintext with the same key produces distinct ciphertexts. This breaks patterns and prevents statistical attacks on the ciphertext.\nImage Source: Wikipedia\nSo to decrypt AES-CBC we need:\nthe IV the key, which is derived via PBKDF2 the ciphertext This is great because:\nAs we pointed out, the IV is constant in Chromium\u0026rsquo;s code and consists of 16 space characters. The key is either \u0026quot;peanuts\u0026quot; or extracted from the OS\u0026rsquo; DE. And we already have the ciphertext. Let\u0026rsquo;s try this with a V11 password.\n\u0026gt;\u0026gt;\u0026gt; import sqlite3 \u0026gt;\u0026gt;\u0026gt; from Crypto.Cipher import AES \u0026gt;\u0026gt;\u0026gt; from Crypto.Protocol.KDF import PBKDF2 \u0026gt;\u0026gt;\u0026gt; encrypted_password = \u0026#34;7631319F0A2C7D1E4B8*******************\u0026#34; \u0026gt;\u0026gt;\u0026gt; # prefix: 76 31 31 -\u0026gt; ASCII -\u0026gt; \u0026#34;v11\u0026#34; \u0026gt;\u0026gt;\u0026gt; # so let\u0026#39;s remove the v11 prefix \u0026gt;\u0026gt;\u0026gt; encrypted_password = encrypted_password[2*3:] # 3 bytes: 6 hex chars \u0026gt;\u0026gt;\u0026gt; salt = b\u0026#39;saltysalt\u0026#39; \u0026gt;\u0026gt;\u0026gt; iv = b\u0026#39; \u0026#39; * 16 \u0026gt;\u0026gt;\u0026gt; length = 16 \u0026gt;\u0026gt;\u0026gt; iterations = 1 \u0026gt;\u0026gt;\u0026gt; # since the password was encrypted using V11 \u0026gt;\u0026gt;\u0026gt; # we use the keyring\u0026#39;s secret to derivate the key \u0026gt;\u0026gt;\u0026gt; keyring = \u0026#34;**********************==\u0026#34;.encode(\u0026#39;utf8\u0026#39;) \u0026gt;\u0026gt;\u0026gt; key = PBKDF2(keyring, salt, length, iterations) \u0026gt;\u0026gt;\u0026gt; cipher = AES.new(key, AES.MODE_CBC, IV=iv) \u0026gt;\u0026gt;\u0026gt; decrypted = cipher.decrypt(bytes.fromhex(encrypted_password)) \u0026gt;\u0026gt;\u0026gt; decrypted b\u0026#39;**************\\x05\\x05\\x05\\x05\\x05\u0026#39; # \\x05 : padding bytes Note: Padding is extra bytes added so that the length fits a required block size.\nI censored my keyring secret and my password but it does work. We\u0026rsquo;ve successfully decrypted the password. If it would\u0026rsquo;ve been a V10 password, we\u0026rsquo;d use the hardcoded password instead of the browser\u0026rsquo;s keyring secret.\nConclusion Therefore, a malicious program running locally, with the current user\u0026rsquo;s privileges, could very easily recover these saved passwords. As we have seen, it just needs read access to the browser\u0026rsquo;s profile directory and the ability to query the user\u0026rsquo;s DE keyring.\nFor v10 passwords, the attack is even more trivial as the key is just public information.\nThis may seem like some kind of weakness, but it isn\u0026rsquo;t: it should be noted that that is perfectly normal. Chrome\u0026rsquo;s threat model explicitly doesn\u0026rsquo;t include compromised or infected machines. Thus, stored passwords are only obfuscated against casual access (which is enough) and that\u0026rsquo;s by design.\nWhy aren‘t compromised/infected machines in Chrome’s threat model?\nAlthough the attacker may now be remote, the consequences are essentially the same as with physically-local attacks. The attacker\u0026rsquo;s code, when it runs as your user account on your machine, can do anything you can do.\nWith that said, in order to demonstrate the practical implications, I\u0026rsquo;ve written an educational tool in Python3 that automates this entire decryption process for all saved passwords on Chromium or Chrome.\nThis tool is for educational and personal research only. Use it only on your own systems or systems you were authorized to completely manipulate. It should not be used on a session that does not belong to you.\nSources https://superuser.com/questions/146742/how-does-google-chrome-store-passwords https://ohyicong.medium.com/how-to-hack-chrome-password-with-python-1bedc167be3d https://source.chromium.org/chromium/chromium/src/+/main:components/os_crypt/ https://fr.wikipedia.org/wiki/PBKDF2#Fonction_de_d%C3%A9rivation https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_block_chaining_(CBC) https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Initialization_vector_(IV) https://stackoverflow.com/questions/23153159/decrypting-chromium-cookies/23727331 https://rtfm.co.ua/en/chromium-linux-keyrings-secret-service-passwords-encryption-and-store/ https://chromium.googlesource.com/chromium/src/+/HEAD/docs/security/faq.md Disclaimer This project is for educational and personal amateur security research only.\nIt demonstrates why browsers should not be relied upon to protect credentials on compromised or shared systems. It is by no means meant to be used on a machine you were not authorized to manipulate in such a manner.\n","permalink":"https://erhaym.github.io/posts/chrome_secrets/","summary":"How Chromium stores saved passwords on Linux and how they can be decrypted under local attacker assumptions.","title":"Don’t Trust Your Browser with Your Passwords"}]