解决 Chrome 启动进程卡顿问题

问题描述

从上个月开始 Chrome 打开就十分缓慢,点击启动 chrome 之后经常需要等待 5 分钟以上。

网上找了很多解决方法,什么清空缓存啊,重启啊,就差没有重装了。

上周末偶然发现当域代理程序处于断开状态时 chrome 打开一切正常,一旦域代理连接成功 chrome 就又回到卡顿状态。

这就让问题看起来可以解决了,于是乎调试一下看看。

问题分析

首先使用 procmon 监控行为,发现主进程在加载了 FWPolicyIOMgr 模块之后就开始卡顿,很长时间不进行其他操作

查看各进程的 CPU 占用情况也没有出现异常,因此应该是在等待某些事件。

使用 windbg 启动 Chrome,在加载了 FWPolicyIOMgr 模块之后断下应用。等待一段时间后,恢复进程。

1
sxe ld FWPolicyIOMgr

进程恢复后 chrome 并没有立即弹出,继续卡顿,因此初步定位此次卡顿应该就是发生在主 Chrome 进程中。

重新挂载 chrome 查看所有线程栈,逐个分析线程调用栈,大部分调用栈顶都是 NtWaitForSingleObject,0 号线程即 UI 线程中发现发现可疑点

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
0:031> ~* k
0 Id: 2014.9e0 Suspend: 1 Teb: 00000091`dfedb000 Unfrozen "CrBrowserMain"
# Child-SP RetAddr Call Site
00 00000091`e07fc2d8 00007ffe`b0e06eb2 ntdll!ZwAlpcSendWaitReceivePort+0x14
01 00000091`e07fc2e0 00007ffe`b0e04001 RPCRT4!RpcBindingFromStringBindingW+0x7522
02 00000091`e07fc390 00007ffe`b0dee93f RPCRT4!RpcBindingFromStringBindingW+0x4671
03 00000091`e07fc3e0 00007ffe`b0e31c96 RPCRT4!I_RpcSendReceive+0x6f
04 00000091`e07fc410 00007ffe`b0e99fb2 RPCRT4!NdrSendReceive+0x36
05 00000091`e07fc440 00007ffe`b0e9d7d0 RPCRT4!Ndr64AsyncServerCallAll+0xd72
06 00000091`e07fc7b0 00007ffe`adb614eb RPCRT4!NdrClientCall3+0xf0
07 00000091`e07fcb40 00007ffe`aeba4aee DPAPI!CryptUnprotectDataNoUI+0x18b
08 00000091`e07fcc10 00007ffe`5bdcd030 CRYPT32!CryptUnprotectData+0x1de
09 00000091`e07fcd60 00007ffe`589108fe chrome!`anonymous namespace'::DecryptStringWithDPAPI+0x73
0a 00000091`e07fcf20 00007ffe`5bfa83c7 chrome!OSCrypt::DecryptString+0x1d4
0b (Inline Function) --------`-------- chrome!password_manager::`anonymous namespace'::DecryptBase64String+0x63
0c 00000091`e07fd070 00007ffe`5bfa84bc chrome!password_manager::`anonymous namespace'::GetAndDecryptField+0xbe
0d 00000091`e07fd110 00007ffe`58a37e6b chrome!password_manager::`anonymous namespace'::ConvertToPasswordHashData+0x64
0e 00000091`e07fd240 00007ffe`5b913e20 chrome!password_manager::HashPasswordManager::RetrieveAllPasswordHashes+0x121
0f 00000091`e07fd340 00007ffe`5b913dde chrome!password_manager::PasswordStore::SchedulePasswordHashUpdate+0x34
10 00000091`e07fd3c0 00007ffe`58a3760a chrome!password_manager::PasswordStore::PreparePasswordHashData+0x28
11 00000091`e07fd3f0 00007ffe`58b0b42e chrome!PasswordStoreFactory::BuildServiceInstanceFor+0x1ec
12 00000091`e07fd5e0 00007ffe`589d2da8 chrome!SkFontMgr::legacyMakeTypeface+0xe
13 00000091`e07fd610 00007ffe`589d6687 chrome!RefcountedKeyedServiceFactory::GetServiceForContext+0xb8
14 00000091`e07fd680 00007ffe`58a373db chrome!RefcountedBrowserContextKeyedServiceFactory::GetServiceForBrowserContext+0xd

在栈顶处下断点进行调试

1
2
bu chrome!OSCrypt::DecryptString+0x1cf
bu chrome!OSCrypt::DecryptString+0x1d4

经过调试可以确定,确实是 chrome!'anonymous namespace'::DecryptStringWithDPAPI 这次函数调用卡住了,其中调用系统函数 CRYPT32!CryptUnprotectData 之后就会等待。

在关闭代理的情况下,函数调用很快,开启代理之后会卡住很久.

查看一下 CryptUnprotectData 函数的功能。 函数使用当前用户的 session key 解密一段数据,key 是用户相关的,使用用户密码进行解密。整个过程需要和 lsass 通信,获取用户密码。

1
2
https://www.passcape.com/index.php?section=docsys&cmd=details&id=28
https://doxygen.reactos.org/d5/d37/dll_2win32_2crypt32_2protectdata_8c_source.html

这里就对应到了观察到的现象:域代理连接状态 chrome 启动缓慢。这里卡住了应该是和 lsass 通信的时候受到了域代理的影响,lsass 返回变慢了。

回到 chrome 继续调试,发现返回 0,程序执行失败了,GetLastError 返回值为 NTE_BAD_KEY_STATE / 0x8009000B 查阅资料

https://groups.google.com/g/microsoft.public.platformsdk.security/c/8Ws8187oyOk?pli=1

可知 CryptProtectData 使用一个叫做 session key 的东西进行加密,而 session key 又使用用户密码进行解密,当用户密码发生修改后,session key 会解不出来,此时就会抛出 NTE_BAD_KEY_STATE。

联想一下,两个月前正好换过密码,那么一切都对应起来了!!

问题解决

LSASS 这里就不去调试了,回头看一下调用栈 PasswordStore ,Chrome 正是在这里卡住的

看起来好像是用来解析 Chrome 保存的密码的, Chrome 的密码保存在 %LocalAppData%\Google\Chrome\User Data\Default\Login Data

网上也有很多从这里提取出用户密码的操作,使用的函数正是 CryptUnprotectData !

我们去删除之,删除之后发现还是不行,情况一致,看来问题不在这里。

再接着查看调用栈,发现了 BrowserContextKeyedServiceFactory 这样一个类,搜索一下,在 chrome 的官方文档里发现了相关的说明

可以看到这个类其实是用来处理 profile 上下文的,看起来不一定和密码相关

直接函数下断 chrome!ProfileManager::CreateAndInitializeProfile, 查看其第一个参数

1
2

可以看到这里指向的正是 %LocalAppData%\Google\Chrome\User Data\Default\, 即为根据 %LocalAppData%\Google\Chrome\User Data\Default 路径下的文件新建 profile

继续查找调用栈,chrome!ProfileImpl::OnPrefsLoaded ,断下。 该函数在 ProfileImpl 构造函数中被调用,根据函数名猜测可能是用来创建 prefer 的,查看一下 Default 下面的文件,发现正好有一个 Preferences 文件,删除之。 再次打开 Chrome ,发现 Chrome 已经正常了。

不过我之前安装的插件等都不见了,这显然是不太方便的。查看一下 Preferences 在其中搜索 password 发现字段 password_hash_data_list,这个 list 正好有两个字段,也对应了调试过程中的两次卡顿。

删除之,Chrome 启动恢复正常,插件等数据也都保持原样。

总结

Chrome 会保存用户密钥,而密钥在 windows 中会使用系统的域密钥进行加密存储。因此当系统的用户域密钥发生改变时,这里有可能会出现卡顿或者解不出的现象。此时只要删除 Preferences 文件中保存的对应密文即可恢复正常

reference

[1] https://www.passcape.com/index.php?section=docsys&cmd=details&id=28
[2] https://doxygen.reactos.org/d5/d37/dll_2win32_2crypt32_2protectdata_8c_source.html
[3] https://docs.microsoft.com/en-us/windows/win32/api/dpapi/nf-dpapi-cryptunprotectdata
[4] https://groups.google.com/g/microsoft.public.platformsdk.security/c/8Ws8187oyOk?pli=1
[5] https://cloud.tencent.com/developer/article/1009440