“为了救活系统,我将Windows翻了个底朝天”——记录一次Windows 11注册表硬核修复经历

话说

新一轮折腾的序幕

最近微软提示WIN10的支持快要到期了1,虽然硬着头皮接着用下去也并非不可,但本着“是时候看看新系统怎么样了”的想法,装了个WIN11。由于我的系统经过高度个人配置,如果采用全新安装(即格式化系统盘后部署安装)再配置一遍,肯定非常麻烦,所以我采取的是原地升级(In-place Upgrade),直接装载官方的WIN11ISO镜像,然后运行它的SETUP.EXE来安装。不过这并不是导致这次系统发生问题的原因,而是决定我后面要抢救系统而非直接重装的原因。

众所周知,微软最早对WIN11子系统的宣传非常吸引人:能跑安卓APP、跑LINUX。但直到我装好WIN11之后,都还不知道微软已经于今年年初放弃了安卓子系统这一块2……令人欣慰的是,即使微软放弃了,民间还没有放弃,似乎可以直接用安装安卓子系统的APPX3,然后侧载安卓APP的方式来支棱起这个烂摊子。

想要使用WINDOWS的子系统,就必须启用系统的HyperVisor模块,也就是Windows自带的虚拟机系统。启用后,Windows将不再直接运行于内核层之上,而是在内核层上加了一层HyperVisor,然后把原本的Windows运行在HyperVisor上,其它的虚拟机系统(包括子系统、用户自己创建的HyperVisor虚拟机)也都一并运行在HyperVisor层上,与平常用的那个主要的Windows系统平级。粗暴的理解就是,在这个模式下,所有系统都是虚拟系统……这样带来的好处是对于原本就是虚拟的系统(客户机),硬件效率比在传统虚拟环境下更高,坏处就是原本处于物理环境下的系统(宿主机)硬件效率下降。

真·事件起因

我在开启HyperVisor后,发现系统无法启动了:一旦BIOS启动完毕,即将加载内核的时候,屏幕立马一闪,电脑直接重启。其实许多年前我也启用过HyperV,当时是想体验一下Windows自带的虚拟机功能,并没有出现过什么毛病。因此这次的问题并不在我意料之中,经过搜索,看到有网友提到是PxHlpa64.sys和HyperV冲突了4,但每个人的情况可能不同,可能在我的机子上是其它某些驱动导致的。所以我就先按他说的方法在WinRE中的CMD输入

bcdedit /set {default} hypervisorlaunchtype off

来关闭HyperV模式启动,从而正常进入系统,再想办法禁用掉一些驱动、服务和启动项就行了。就在这时,我碰巧了解到有一款很方便的、由微软官方文档里推荐的系统工具Autoruns5,可以检测出系统里的所有加载、启动项,并允许用户手动启停它们。截至本文撰写之日的最新版本还是v14.11看起来很靠谱的样子! 于是我就下载来试了一试,看到界面给我刷一下列出了一大堆条目,很好,先试着禁掉一些驱动吧!

我也忘了具体点了什么,按正常操作应该先隐藏系统驱动,这样禁用的时候就会稍微安全一点,不会碰到系统的关键驱动。这里说一个非常坑的地方,就是Autoruns这个软件提供了一个保存功能,让我误以为是保存当前的启用状态,以便我操作有误的时候能导回来恢复。结果并不是,它只是保存扫描到的启动项的列表,导回来也只能看一看罢了。

实际是我大概确实禁用了一些关键驱动,于是在重启加载系统的时候“黑”屏了,准确地说是黑色的蓝屏。微软在WIN11加入了这种新的黑色的蓝屏,它会在屏幕正中央大字提示你“设备出问题要重启”,而这种黑蓝屏一般是在内核加载后的系统运行阶段才会有,如果是在加载内核时出问题,应该还是原本的蓝屏,有一个大大的” : ( "符号表情。

再次来到WinRE,尝试运行Autoruns,嗯,运行的了。但是现在的Autoruns扫描出来的都是WinRE的启动项,而我要调整的是我原本那个Windows 11的,于是我在菜单栏看到有一个Analyzing Offline Systems(分析 离线 系统)的选项,可以让你选择非当前运行系统的注册表,用于分析和启停启动项操作。毁灭性的一步由此踏出……

一周系统抢修录

DAY 0 · 山雨欲来

Day 0 Day 1 Day 2 Day 3 Day 4 Day 5 Day 6 Day 7 Day 8 山雨欲来 来龙初见 见之怵目 目不及峰 峰回路转 转斗移星 星流云开 开门见山 章节进度

使用Autoruns中的离线分析功能,界面上果然出现了原本Windows 11的启动项,但当我点击勾选要启用的启动项时,Autoruns弹出无法更改的错误提示,也许是因为权限原因。如果不能直接从软件中操作,那只能直接改注册表里的键值了。于是,我点击Autoruns窗口右上角的红叉按钮来将其关闭,窗外忽然风雨大作、电闪雷鸣。 考虑到WinRE环境或许不够方便,我便启动到WinPE中去,用注册表编辑器加载Windows 11的注册表文件,找到服务启动项

HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services

将其中所有服务的Start值全部手动调整到原本该有的数值后重启电脑。

结果这次重启直接蓝屏了,也就是说连内核都还没加载完。蓝屏上的错误提示是0xc0000034 代码完整性无法初始化。按照网上的大多数说法,是系统的引导配置文件,也就是BCD出了问题。我前边的确输入过BCDEDIT的命令来修改参数,但真不至于因为这个。抱着试一试的想法,把几个月前恰好备份过的BCD文件替换回来,还是不行,说明不是这个原因。

难道说还有启动项没恢复好?可能性不大啊,毕竟全都手动过一遍了。没办法,只能返回WinPE,我寻思着,实在不行,咱就在另一个分区再装一个全新的正常Windows 11系统,然后把这个正常系统的启动项跟我原本的系统对比,看看哪里出了问题。

说干就干。装好全新系统,经过对比,只要是存在的启动项,都完全一致,没有毛病。把原系统多出的启动项去掉、甚至直接把新系统的这部分注册表给覆盖过去也还是不管用。实在是怪到不行了。试着用

bcdedit /set {原系统标识符} bootlog yes
============================================================
补充说明:当只有一个系统时,它的标识符可以用{default}指代,而当装完另外一个系统后,bcd文件中会存在两个可用于启动的系统,这时的{default}可能会指向新系统的,而原系统的标识符则需要通过在CMD中输入bcdedit来查询。

来开启系统文件加载记录,重启蓝屏后再进WinPE里面找本该产生记录的文件,发现里面根本没有这几次蓝屏的记录……翻来覆去愣是没什么进展,眼看时间不早,只得作罢,待次日再研究。

DAY 1 · 来龙初见

Day 0 Day 1 Day 2 Day 3 Day 4 Day 5 Day 6 Day 7 Day 8 山雨欲来 来龙初见 见之怵目 目不及峰 峰回路转 转斗移星 星流云开 开门见山 章节进度

“代码完整性无法初始化”这句话究竟是何意?实在令人百思不得其解。要说最接近的东西,就是Windows的代码完整性检查机制,这是微软用来确保加载到内存中的代码能够安全地执行下来且不会被随意修改而设置的一种隔离运行环境。顺着这个逻辑,假设我在使用了Autoruns后导致某些关键的系统文件遭到损坏,那么会不会是内核加载它们时代码完整性检查无法通过?虽然这样的推断有些许不着道,但从接下来的发现来看却可以算是歪打正着……

此时一个细节正在我脑海中浮现,就是在WinRE中用Autoruns离线分析时,系统服务条目是标红的,对应的驱动文件后面都显示“未验证”,而在原系统中停用启动项时,这些条目都是正常的“已验证”。有没有可能Autoruns将系统的驱动文件签名给无效化了?于是回到WinRE,又用Autoruns分析了一下原系统,打开其中一个驱动文件的路径,仔细查看这个文件,没发现问题,修改时间是很久以前,不可能被Autoruns动过,而且签名也在。

为了进一步确认这种异常的现象到底与系统无法启动是否有关联,我第一时间想到了这样一种验证方式:

由于第一次使用离线分析就是在WinRE中,那时原系统已经无法进入了,假如这些驱动文件的“未验证”状态与无法进入系统有关,那么我对新安装系统使用Autoruns离线分析看到的验证状态只能是正常的“已验证”,否则就证明两者没有关联。

于是我对全新安装的系统也进行了离线分析,一看驱动列表,也是一模一样标红和签名未验证。这说明“未验证”是Autoruns本身的显示问题,而非系统的问题。得出这样的结论,反而让排查又断了线索。

失落之余,我重启电脑,打算再进新系统去用桌面浏览器好好查查资料。捧着手机查,脖子、眼睛:…… 正当它们无语之时,奇迹发生了——新装的系统,也进不去了!而且是完全一致的蓝屏!啊哈哈哈哈哈哈哈!!!…… 难得一次因为系统启动不了而高兴,立刻赶到现场的是新的线索:

Something kills the new system could as well be the killer of the old one!

没错,心机之蛙只有一只!吃瓜的朋友们读到这里肯定直接将目光聚焦到了Autoruns身上,而实际上我做到这一步之前,还对新系统尝试了一些BCD设置调整,所以当我进WinPE把新系统给再重装一遍并再次来到WinRE后,首先是想试试只调BCD看看会不会出现什么状况,毕竟这个操作更简单。但是敏锐的我意识到事情不可能简单,我押八成Autoruns的锅!于是根本没有尝试BCD设置,而径直打开了Autoruns,分析离线系统!

============\\
To Be Continued 》
============//

DAY 2 · 见之怵目

Day 0 Day 1 Day 2 Day 3 Day 4 Day 5 Day 6 Day 7 Day 8 山雨欲来 来龙初见 见之怵目 目不及峰 峰回路转 转斗移星 星流云开 开门见山 章节进度

果不其然,新系统同样的蓝屏复现,证明问题的确出在Autoruns的离线分析功能上。但它到底做了什么把系统给弄崩了?快速地上网搜索一下,找到一条网友发布的贴子谈及了这件事6,点出了问题的关键:Autoruns v14.11在离线分析时会在注册表编辑器中挂载离线系统的注册表文件,而退出Autoruns并不会取消挂载,从而导致它们被损坏,系统就进不了了。他的表述中最难理解的就是“不取消挂载就会损坏”。只要你不对挂载的注册表随意修改,取不取消挂载完全没影响。很明显他只是没说清楚具体原因,但有这些信息就已经足够了!因为接下来我将亲自查明Autoruns干的好事。

首先再重装一遍新系统 (这是第三遍……) ,这一次咱备份好注册表文件的整个文件夹,

C:\Windows\System32\config

等到下次注册表又坏了的时候,就只需要把包含了受损的注册表的文件夹整个删了,把之前备份好的这个文件夹的正常版本粘贴回来,这时候新系统可以正常进入,不必重装整个系统就又能用来实验了!

随后关键来了,打开备受好评的系统分析神器Process Monitor,简称Procmon(读出来有点像宝可梦……)。这是一款能监控系统所有读写情况的软件,可以追踪50%,不,80%以上的用肉眼无法观察到的系统问题。讽刺的是,这款软件和Autoruns同样出自Sysinternals7,后者是本次事件的罪魁祸首,而前者则是大救星。

先将Procmon捕捉模式停止,清除已经捕捉到的记录,然后在Procmon中添加过滤器,过滤出只和Autoruns有关的操作,运行Autoruns,接下来一顿猛操作必须拼手速,以免捕捉到无关信息:启用Procmon实时捕捉——进入Autoruns离线分析功能——等待数秒分析完成立即关闭Autoruns——观察Procmon捕捉记录直到没有新记录出现(说明Autoruns已完全退出)——Procmon停止捕捉

当我点击“停止捕捉”按钮,迫不及待地想要一览事情的真相时,即使已经有大致的心理准备,眼前的一幕还是令我寒毛直竖——

吃干抹净

删完了,全删完了…… Autoruns v14.11,一款著名的准官方工具,在分析离线系统后,点击关闭按钮的一瞬间,将被分析的注册表吃了个干净……

我曾经听说有某款游戏在内测时有一个BUG,就是当用户运行它的卸载程序时,它不仅卸载自己安装过的文件,还会把整个盘都给清干净了。我没有亲自
验证这件事,但光是听说就觉得荒唐。俗话说的好,

数据无价,谨慎操作!

人们电脑上总有些独一无二的珍贵数据,保护都还来不及;而不论缘由,这些软件毁坏数据的操作更是如同夺人钱财!不管是什么软件,尤其是在进行删除/覆写等与“write”有关的操作时,都必须格外注重数据安全这一块,否则怎么对得起支持你们的用户呢?

回归正题,让我们看看遭殃的离线系统注册表节点有哪些:

HKEY_LOCAL_MACHINE\SOFTWARE\,递归删除直到HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\EAPSIMMethods
============================================================
HKEY_LOCAL_MACHINE\SYSTEM\,递归删除直到HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Nsi

实际上被删除的节点是HKLM\autoruns.softwareHKLM\autoruns.system,这两个节点就是Autoruns将离线注册表挂载到注册表编辑器里所用的别名。按照正常逻辑,Autoruns在离线分析结束后,退出软件时,应当对所挂载的节点进行卸载(取消挂载)操作,而非删除操作。这和用户手动在注册表编辑器中挂载离线注册表一样,当你需要取消挂载时,应当点击菜单栏上的文件-卸载配置单元,而千万不要直接对挂载的注册表右键点击弹出菜单里的“删除”! 后者删的真的是注册表节点。所以为何Autoruns会犯这么低级的错误,还仍未可知……至于为什么没有把SOFTWARESYSTEM都删干净,或许是因为它在递归删除子节点的时候,按顺序下来碰到了一个权限不够子节点,递归删除的操作就被迫中止了,这也算是不幸中的万幸。Autoruns离线分析一共挂载了三个文件,除了上面提到的两个,还有一个是HKLM\autoruns.user,但这个大节点下并没有任何子节点被删除,虽然不懂为什么Autoruns不一视同仁,但至少可以不用研究它了……

对于这样一款官方文档里推荐的软件,本该是可以放心使用它,但从今以后,弄新软件来用都必须先搜索一圈,看看要用的这个版本有没有人反馈大BUG了。

根据目前被删除掉的注册表项,以我经验上大致的判断,认为它们也许都不是那种每台电脑内容都不一样的条目,或者说大部分都不是,也就是说,我可以从新系统完好的注册表中把这一部分给移植到原系统上去,使它复活!更何况我要移植的还是同一台电脑上装的系统的注册表,硬件设备配置方面应当没什么区别。总之先试一下。接下来的操作,每一次尝试我都做了备份,这一点不再赘述。

另外,按照传统,几乎所有的博主在谈及注册表操作的时候,都要叮咛大家一句:“操作有风险,搞坏自负责!

我首先尝试的是全量覆盖:

步骤:

  1. 在WinPE下分别挂载新系统和原系统注册表,并将它们的的SOFTWARESYSTEM导出;
  2. 挂载原系统注册表,为其导入从新系统导出的SOFTWARESYSTEM
  3. 为原系统注册表导入从原系统导出的SOFTWARESYSTEM

============================================================
注意:导入时应当确保要导入的.reg文件中表项路径中的SOFTWARESYSTEM与挂载时用的这两个文件的别名一致。例如,我将原系统的SYSTEM挂载为sys,那么就要将刚才导出的.reg文件里面的所有HKEY_LOCAL_MACHINE\导出时挂载的SYSTEM的别名\替换为HKEY_LOCAL_MACHINE\sys\,才能保证导入到的是现在挂载的这个sys中。

这样做可以保证该移植的部分都移植,不该移植的部分还是原系统的数值,同时假设有些该移植的部分依赖于某些不需要移植的项的值,但如果我没有把它们也导进来就会出错……简单来说就是这种方式得到的注册表最全面,同时保证大部分配置都还是原系统的。

弄好之后我兴奋地开机,以为一切就要结束了。Windows的小窗户标志一出现,我就开始紧张,因为之前就是在这里蓝屏的——但是这次没有!窗户底下的圈圈转了五六圈,屏幕黑了过去——到目前为止都是正常系统加载的表现,很好!接下来按道理再亮起屏幕的时候就应该进系统登录界面了,可是,并不。屏幕再亮起时,整个画面还是黑的,唯独小圈圈出现了,它就在那一直转了好几分钟,我也坐在椅子上发呆了好几分钟。随着屏幕再次黑过去,电脑的自动重启把我从恍惚中拉回了现实。

所以这次又是新问题,挺好,至少有进展了。于是我第一时间想着直接走捷径去网上搜相关信息,如果能找到解决方法便是像做作业抄答案一般轻松。遗憾的是网上并没有多少类似的情况,比较接近的几个网友最后也直接重装了事……

就这样认输,我不能接受! 于是又尝试了这几种操作:

  • Full Control for Everyone!

步骤:

  1. 在新系统下挂载原系统注册表;
  2. 使用PsTools进入nt authority\system账号的控制台;
  3. nt authority\system的身份运行SetACL,为原系统的SOFTWARESYSTEM下所有节点递归设置Everyone角色的权限为完全控制

============================================================
解释:这么做是基于先前的一次维护经验。当时,一个系统服务由于SOFTWARE\classes当中的某个interface缺少了相关角色的权限,导致那个服务不能启动。因此粗暴地为将所有注册表子项给予所有人完全控制权限,可以快速确定问题是否是由权限引起的。
用到的两个工具中,PsTools8也属于Sysinternals,而SetACL9是由某个德国高手开发的用于修改系统所有文件(包括注册表项)的安全标记的工具。
至于在新系统下操作的原因,是我的WinPE因为精简而运行不了上述两个工具。
============================================================
结果:没有变化,还是一模一样的问题

  • 系统级全量覆盖

步骤:

  1. 在新系统下使用PsTools进入nt authority\system账号的控制台;
  2. nt authority\system的身份运行Regedit.exe,导出SOFTWARESYSTEM
  3. 挂载原系统注册表,导出原系统的SOFTWARESYSTEM
  4. 为原系统注册表导入新系统导出的SOFTWARESYSTEM
  5. 为原系统注册表导入原系统导出的SOFTWARESYSTEM

============================================================
解释:在第二步后,注册表编辑器已经拥有系统的权限,理论上可以导出一些本来普通管理员导不出的项。说不定系统就是因为缺少这些项才无法启动。
============================================================
结果:问题更复杂了,出现了新的蓝屏,提示终止代码:IO1_INITIALIZATION_FAILED。为此又开了一个新的小坑,花费了很多时间研究,不想赘述过程。从结果简单地说,这是因为上面的导出方式会连同注册表创建的符号链接一同导出,例如HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet,它实际上是HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001的快捷方式。类似这样的符号链接有好几个,它们都不能以普通键项的形式存在于离线的注册表文件中,否则会导致系统尝试创建这些符号链接的时候出错,系统就崩不进去了。
实际上,后来用WinMerge10对比系统权限导出和最开始在WinPE中导出的.reg文件,发现甚至还不如WinPE里面导出的。我也忘了WinPE默认是系统权限还是啥的,反正肯定低不了。

  • 互斥修补

步骤:

  1. 在WinPE下挂载新系统注册表;
  2. 将先前Procmon捕捉的数据导出为.csv文件后,配合批处理命令导出被删的注册表节点;
  3. 挂载原系统注册表,导入上一步导出的注册表节点。

============================================================
解释:最小化修补内容,这都不行真没招了……
============================================================
结果:还是不行,真没招了,已经开始准备系统安装盘了……

上面的几种方法,已经是我当时能想到的全部了。我在注册表之间来回穿梭,树里节间,只是歪歪斜斜列着看也看不懂的键值数据,横竖睡不着的我才琢磨出来,满表都写着“一堆MASS"几个大字。

正当我漫无目的地在注册表之海里游泳时,我偶然看到了一段比较醒目的中文信息:

lsass.exe意外终止,系统即将关机并重启

我也忘了当时是在注册表的哪个地方看到这句话。刚看到时只当是一个普通的错误,没有太在意。这时的我已经把注册表这两块看了个遍,几乎能翻的子节点都翻过去,试图找到一些导致问题的关键节点。闭目养神之时,思考了一些杂七杂八的问题:

  • 原系统的注册表备份
    这个真没有。平时也没想到会有这种事。还记得早在Win8时期,我有一次把系统注册表搞坏了,我直接反手把Windows自动备份的注册表文件给盖回去,系统好了。但Win10的某个更新版本开始,微软把这个功能给默认禁用了11,用系统还原点(System Restore Point)来取代它,但系统还原点也不是默认开启的……无法评价这种做法。
  • DISM和SFC的局限性
    首先sfc /scannow只能在活着的系统上进行,直接排除;dism /Image的确可以在其它环境下指定要修复的系统,但它和SFC一样仅限于目标系统只有除了WinSxS组件仓库以外的固定文件缺失时可以修复,注册表不在它们的职能范围之内。
  • 重装系统
    本着不服输以及探究的好奇心,能想办法在不采取清洁安装(Clean Install)等破坏性重装方式的情况下修好系统的成就感是非常大的,同时也省去了重新配置个性化参数、系统变量、开发环境等等令人头疼的麻烦。
    微软为系统的恢复提供了很多方式,除了最彻底的清洁安装,还有比较没啥用的方式,例如系统还原点,它能不能有效果完全看脸。另外还有很接近清洁安装的方式——用安装镜像来保留个人文件但删除应用的恢复方式。如果最终没有任何办法了,就只能用它了。
    除此之外,假如能够进入到系统,还有另外两种更好的系统重置方式,能够同时保留个人文件和应用。它们分别是本文开篇提到的原地升级(In-place Upgrade)和利用Windows更新重新安装当前版本系统。这两种方式都相当于是请一位微软内部通晓一切Windows知识的专业人士来帮你把关键系统注册表项给重新配置一遍,包括键值数据和用户权限,他同时也知道注册表中哪些是你的个性化配置,从而保留这些配置不做更改,最后把所有固定的系统文件都重装一遍。所以假如我能进系统,必定要用这两种方式之一重置系统来确保系统稳定无虞。

重新理了理思路,目前认为比较可能是新系统的那部分注册表与原系统发生了免疫排斥反应 某些冲突,可是想要知道具体哪里冲突了,我就必须先去认识一个微软的Windows工程师,实在是不现实啊……我心想:你怎么就冲突呢,哪里冲突呢……一边想着一边试着在新系统的注册表中手动把Autoruns会删除的节点都删除了,然后再把之前从新系统导出的.reg给导回去。我心想:我倒要看看你自己的头,你自己排不排斥!转念又想:这么干有啥用呢,这就好像把文件剪切走,又粘贴回来,能有什么变化,肯定能正常啊。

于是,奇迹再一次在这种毫无根据、全凭第六感的无厘头操作之后出现了!

新系统也一样开始黑屏转圈圈重启了! 这也许是我第二次为系统无法启动而感到高兴,但其实并不,因为我第一时间是迟疑的。因为人类对某些坚信不疑的事物是很难动摇的。当我认定了这种剪切-粘贴式的操作不应该导致问题时,我的第一反应是会不会我不小心把原系统的注册表给导来新系统了?于是恢复后,小心地再试了一下,确认了这个事实:自己的头,接回去后也会排斥……

此时,一个玩了二十几年电脑的人的世界观崩塌了……这简直没有任何可能啊!于是我把新系统能正常启动时的注册表导出,再把它删除一部分再导回恢复后不能启动时的注册表也导出,把这两份注册表用WinMerge对比,完全一致……而且我确定所有子节点都导出了,因为权限都给了,不会存在遗漏项。

至此,我总算知道,我产生幻觉了,该去歇息了!

DAY 3 · 目不及峰

Day 0 Day 1 Day 2 Day 3 Day 4 Day 5 Day 6 Day 7 Day 8 山雨欲来 来龙初见 见之怵目 目不及峰 峰回路转 转斗移星 星流云开 开门见山 找到罪魁祸首 章节进度

小小地花费了一些时间取回理智、认清现实之后,我心中产生了一个明确的目标:

也许原系统情形复杂,不好定位问题的原因;但有同样症状的新系统排查范围却很小,只要我能找出的它的病根所在,就能拿来治好原系统。

接着便又是一顿猛操作:

  • verbose详细模式

步骤:

  1. WinPE下挂载新系统的SOFTWARE
  2. HKEY_LOCAL_MACHINE\SOFTWARE挂载别名\Microsoft\Windows\CurrentVersion\Policies\System\VerboseStatus 的值设为1,不存在则新建;
  3. 在CMD中执行bcdedit /set {default} sos yes(等同于在能启动系统时,将msconfig引导选项中的OS 引导信息勾起)。

============================================================
解释:玩过Linux的朋友都知道,这个开源系统的verbose启动模式能提供很详细的启动记录,会把过程中每一条加载了什么,干了什么事都列出来,于是哪里出问题也就一目了然了。我隐约记得十几年前有一次用Windows的安全模式启动也达到了类似的效果,所以就想试一下。
============================================================
结果:根本没有详细的启动输出。网上了解了一圈,有人说这是Win7及以前才能办到的,Win8开始就不存在了……

  • 安全启动模式

步骤:

  1. 在CMD中执行bcdedit /set {default} safeboot minimal(等同于在高级启动F8里面选择安全模式);
  2. 重启电脑。

============================================================
解释:装错驱动时可以用这招,因为这招只不会加载装错的驱动,或者不会因没签名的驱动而阻止系统启动。但目前这个情况,只能说随便试试看罢了。
============================================================
结果:我就知道没啥用。

  • 内存转储调试

步骤:

  1. C:\Windows\Minidump找到系统崩掉时保存的转储文件;
  2. 用WinDbg打开这个文件,分析出错的位置。

============================================================
解释:新版的WinDbg12在PE和RE都用不了,而我的两个系统都炸了,也不想再另装,所以实际上我就是掏出了笔记本,想着要把文件拷过去分析。其实也可以先把新系统的注册表用备份替换一下,先进去下个WinDbg,然后分析。
============================================================
结果:根本没找着转储文件,有可能是我当时误以为它在C:\下,也有可能系统的这个问题并不是在内核加载过程中,所以没有产生转储。有点忘了。总之当时想用更有效的实时调试法,就没有再在这个方法上磨太久。

  • 实时调试

步骤:

  1. 在CMD中执行
    bcdedit /bootdebug {bootmgr} on
    bcdedit /bootdebug {default} on
    bcdedit /debug {default} on
  2. 弄来另外一台电脑,用WinDbg连接;
  3. 重启电脑,等待断点,耐心调试。

============================================================
解释:系统启动分为多个阶段,而上述的三个命令可以使调试器分别在加载启动管理器、加载系统启动器、加载系统内核三个位置断点,也是微软给出的比较全面的调试断点设置。要使用另一台电脑的WinDbg连接被调试的电脑,就要先经过一番设置13,详细操作可以点击角标查看。微软提供了很多种连接方式,我选择的是网线连接。
============================================================
结果:其实我当时只执行了第一步中的最后一条命令。不知什么原因,调试器无论如何也连不上被调试的电脑;而我在这次事件前就已经配置好调试,那时可以连上。

一顿操作啥也没得到,绕了一大圈又回到原点。这也许就是捣腾系统最常见的经历。不过有时候最有效的方法也许是最简单的那一个——受到某位网友排查注册表问题的文章14的启发,我决定也尝试用二分法定位直接导致问题的注册表节点。

  • 二分定位
Created with Raphaël 2.3.0 开始定位 进入WinPE 第一次定位? 将Autoruns删除清单 对半分为清单A、清单B 挂载新系统注册表的 [SOFTWARE]和[SYSTEM] 按照清单A删除 挂载的注册表节点 导入系统正常时 导出的注册表 能正常进入 新系统吗? 将清单B认定为 [包含定位目标的清单] 定位到 满意范围? 完成定位 将清单A认定为 [包含定位目标的清单] 将[包含定位目标的清单] 对半为清单A、清单B 删除新系统当前注册表文件, 用备份的注册表文件替换 yes no yes no yes no

吐槽一下CSDN用的flowchart.js似乎不是最新版,流程一旦复杂画出来就会怪怪的样子……

步骤:
参照上方流程图。
============================================================
解释:的确可能存在一种情况——清单A和清单B都包含会导致问题的节点。但是在我的实际操作中,大多数时候清单A操作完系统是能正常进入的,因此可以直接锁定清单B。直到范围缩到很小以后,清单A操作会导致系统无法进入时,为了确保无误,我将清单B也操作了一遍。
============================================================
结果:有用!

经过 log ⁡ 2 N \log_2N log2N次尝试之后,我最终将范围缩小到似乎是与单个系统功能相关的注册表节点

HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Lsa

不知为何,这个键的名字看着有点眼熟,好像之前在哪里见过。不过里面的子节点和数据也没有什么特别的。继续尝试看能不能把范围缩的更小的时候,不对劲的事情发生了:删除里面的前一半和后一半子节点再导入恢复都会造成进不了系统的情况。而在之前的定位过程中,即使是前一半导致进不了系统,后一半也一定是没影响的。这样的异常现象说明我离真相越来越接近了!

从经历整件事情之后的角度来回顾这段环节,我知道,假如屏幕前的朋友是搞系统安全这方面的,看到这里一定会心一笑,啥都明白了,哈哈哈……但我还是想卖个关子,沉浸式地重现当时的焦灼心情……

在网上索搜了LSA的相关资料之后,我了解到,它的全称是Local Security Authority,本地安全机构,是Windows用来执行系统安全策略、给用户授权和管理密码这一类跟系统安全有关事务的模块。的确,在Lsa键下有许多值的名称光是看着就很安全,于是我把这些值的数值都逐一往不安全的那个方向去设置了。比如,SecureBoot,安全启动,跟BIOS里面那个一样么,不管了,设置成0;RunAsPPLBoot,有点像是进程的守护者,而且还强调了“启动”这个字眼,直接设成0……我天真地认为,假如这些安全模块代表了系统的免疫细胞,那么在移植注册表后,想要不产生排异反应,就需要让它们安分点……事实证明啥用没有!还是老老实实继续缩小范围吧……

经过进一步排查,我锁定了Lsa下的一个不可再分割的子节点,删除这个节点再导回,就会导致系统无法进入。它的键名为Skew1,没有子键,仅有一个值名为SkewMatrix,数据是一串二进制。接下来,我针对这一节点进行了诸多实验:

  • 操作:删除SkewMatrix,再导入回来。
    猜想:删除这个值后再导回或许会使得其中包含的二进制内容产生变化?有时即使是微妙的变化,如末尾多了几个0,都有可能引发问题。
    结论:没有任何变化。导回后系统能正常进入,否定猜想。
  • 操作:在SkewMatrix前后分别新建二进制值SkewMatriSkewMatriz
    猜想:删除Skew1再导回,也许会引起SkewMatrix在注册表文件中的二进制空间位置发生变化,假如它对安全方面十分敏感,那么它注册表键值本身的完整性应该也很重要,也许它会验证自己是否被篡改过。
    结论:系统能正常进入,否定猜想。
  • 操作:在Skew1前后分别新建键Skew0Skew2
    猜想:理由类似上一个实验,只是这次针对键而不是值。
    结论:系统能正常进入,否定猜想。
    ……
  • 操作:无法操作。
    猜想:Skew1当中可能还存在一些隐藏的子节点,无法在当前条件下被看到,也无法被导出和导回。
    结论:由于无论如何都无法使这些假想中的隐藏节点现身(尝试过包括给予Skew1全权限等操作),即使确认它们存在,只要无法分离它们,就无法将它们用于修复Skew1

在网上搜索注册表“隐藏节点”、“隐藏属性”等关键词也找不到有用信息,甚至好兄弟帮忙问了神奇的小海螺 ChatGPT,它也只是告诉我微软把注册表设计得很巧妙,有一些“transaction”(类似数据库的事务)啥的也会在冥冥之中产生肉眼不可见的影响。注册表文件夹下确实有一些后缀为LOG1LOG2blfregtrans-ms的以注册表本体命名的文件。经过简单了解,这些文件是在使用注册表编辑器或者CMD的reg命令对注册表执行各种操作以后产生的,包含了各种操作的痕迹,例如在什么位置改了什么键值数据。但目前网上根本找不到能够解析这些文件的工具或者文章,只能知道,当系统因注册表修改而出问题时,会自动根据里面的记录来恢复一部分注册表。由于没能解析它们的内容,就无法将他们用于手动恢复,而且很显然系统也没能用这些文件自动恢复成功。我也曾听说有网友通过删除这些记录文件来阻止系统恢复注册表,从而成功解决某些问题的案例,但经过我亲自尝试,并没有产生什么效果。

黔驴技穷、汗流浃背了吧? 此时的我仿佛身在深山,抬头却望不见顶峰…… 即使我千辛万苦定位到了问题根源之所在,最终也被一只认知之外的无形大手挡在了门前。

都已经走到这一步了,放弃是不可能放弃的! 也许计算机在物理微观层面上有一些玄学,但在操作系统这种宏观层面应该遵循唯物主义的规律才是!那么,想要探究其中的规律,就必须要把注册表文件的二进制内容,一个字节,不,一个比特一个比特地看明白!

于是,经过一番搜索后,我找到了一篇值得拜读的文章15,其中讲解了Windows注册表二进制文件的基本结构。该文章使我受益匪浅—— 我保证我真的大致看过一遍了!绝对不是没看懂! 重点在于按照这些知识来手动实现代码挺费时,而恰好 这篇文章的作者他自己也写了一个注册表解析工具,名为Registry Explorer16。这是该作者根据目前民间(业余人员、学术机构、技术单位(微软除外)等)对注册表二进制结构完成的所有可理解含义的结构的研究(解析程度高达99%以上,存在%0点几还无法理解含义的结构,仅记录于(或也可能不)微软内部的非公开技术资料中),而实现出来的注册表解析工具。它的确可以显示一些微软官方的注册表编辑器都不显示的东西

我激动地用Registry Explorer导航至HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Lsa\Skew1,却并没有看见任何隐藏节点,便开始有些许紧张了。如果节点方面没有端倪,那问题一定在某个属性上?例如,修改时间?还是……在对着Skew1单击鼠标右键时,我注意到弹出的菜单中提供了“技术细节”的详细分析功能,里面确实将很多属性都列出来了

在这里插入图片描述

于是,我便将能进入系统和不能进入系统的注册表的Skew1属性进行仔细比对。先前很大程度怀疑是用户权限缺失,现在看来两者竟一模一样。而其中一些看似不痛不痒的细微之处,会不会就是问题的关键?按照语义上的直觉,我的目光聚焦在了一个名为Access Flags的属性上,因为能够进入系统的Skew1的这个属性包含了两个Flag,分别是PreInitAccessPostInitAccess从字面意思上看,似乎是在描述这个注册表键可以在某个东西初始化的前后被访问;而无法进入系统的Skew1只有PostInitAccess一个Flag。这就让我怀疑,也许Skew1重新导入恢复后,没有添加PreInitAccess,导致系统不允许在某个东西初始化前访问它,而亟需访问它内容的模块就因为访问失败而抛出异常,系统就卡在这一步无法进入,直接重启了。

如果真就这么简单,那真是太棒了! 我迅速地将Lsa下的所有子键都看了一遍,的确,但凡是重新导入的键都没有PreInitAccess,而原本的都有,所以手动把它们在这个位置的二进制数都给改了。好,现在大家都有PreInitAccess了!

激动地重启——结束了结束了! 我长舒一口气。但很快,我的脸转而阴沉了下来,因为黑屏上的小圆圈在否定我的所作的尝试,自动重启时的机箱风扇声更是如同嘲笑一般刺耳。是我自以为是了……但我从来没有在同一个电脑问题上研究如此之久,更何况已经好几天没进我原本的系统了,难免滋生浮躁情绪。

沉住气,沉住气…… 明明已经很接近了!再次回到Registry Explorer,仔细观察,一个本不起眼的细节映入眼帘:在正常的的Skew1中,Class Information这一栏的“长度”、“最大长度”和“地址偏移”三个属性都有看起来合理的十六进制值,而在无法进入系统的Skew1中则是0x0 (0)!为什么先前没怎么注意到这一点呢?也许是因为它的名字“类信息”从字面上理解像是额外的附加属性,就好像是用于给注册表键分级分类的;而且,当我在正常注册表中随机查看其它键,包括Lsa下的一部分子键时,发现它们的类信息也都是0x0 (0)。来回翻看一阵,最终只在Lsa下找到包括Skew1在内的几个子键拥有非零的类信息偏移地址。

那么,我要如何证明Class Information就是问题的关键?很简单,只要将正常注册表的Skew1的类信息稍微改动一下,哪怕改动一个字节也好,只要重启时出现问题,即可得证。而神秘的Class Information里面又究竟存着什么呢?对于一个注册表键来说,类信息和子键列表、用户权限等信息一样,它们的内容本体都存在于注册表二进制空间中更后方的位置。Registry Explorer首先读取的是键的头部信息,其中记载的是上述信息的偏移地址,然后工具根据这些地址找到它们实际存储的内容,并展示在详细分析中。大部分主要属性的内容本体都已经显示,唯独Class Information仅展示了偏移地址,只得由我自己按图索骥寻过去一看了。想罢,我来到文件头部结束位置,继续向前走了Skew1类信息的小端序偏移地址量那么多步

绝对地 址 类信息 = 长 度 文件头部信息 + 相对偏 移 类信息 = 100 0 16 + A 2 B E 1 8 16 = A 2 C E 1 8 16 \begin{split} 绝对地址_{类信息}&=长度_{文件头部信息}+相对偏移_{类信息}\\ &=1000_{16}+A2BE18_{16}\\ &=A2CE18_{16} \end{split} 绝对地类信息=文件头部信息+相对偏类信息=100016+A2BE1816=A2CE1816

此时低头一看,脚底下和前方的三格写着E8 FF FF FF,再往前便是有规律的一个字母或数字的ASCII十六进制加上一个00的两字节组合,8个这样的组合之后又是没有规律的十六进制数了。

a.b.9.b.4.……这……这是什么? 说好的类,居然就是一串随机的字符串?而且其它几个子键的类名也都是不同的这种样子的字符串,说明真的是随机的。此时我内心轻轻地咯噔了一下,似乎预见到了不妙。现在当务之急的是修改里面的信息,要怎么改好呢,肯定不能直接删除字节,否则后面的所有偏移地址都会对不上,那只能将其中的某几个字符改成别的字符或者干脆填成00。于是果断选择了后者,而且只填掉了第一个字符a。重启电脑,果然进不了系统了!这证明我终于找到了问题的源头,很快就能修好系统了!

还没高兴两秒,之前的咯噔又回来了。我意识到一个问题:这串随机字符到底是什么,每台电脑,或者说每个不同的系统之间,它们会一样吗?假如不一样,那我要从何处得知它原本该有的这串字符?一边思考这个问题,一边尝试着把正常系统的Lsa下所有的类信息都找了出来,逐个填入丢失了这几个类信息的注册表中。用这份“修复”后的注册表重启系统,我仿佛是一个刚做完手术,等着病人醒来的医生,心中难免焦急。第一位“病人”成功醒来了,但这也不过是在意料之中,因为这份注册表就是新系统那份手动删除节点后缺少类信息的注册表,而我提取的类信息也是新系统的。关键在于第二位“病人”,原系统注册表,它的类信息早已被Autoruns带入虚空,无论如何也不可能找回了。那么,用新系统的类信息是否能救活它呢?

很遗憾,第二位“病人”的“手术”失败了……

DAY 4 · 峰回路转

Day 0 Day 1 Day 2 Day 3 Day 4 Day 5 Day 6 Day 7 Day 8 山雨欲来 来龙初见 见之怵目 目不及峰 峰回路转 转斗移星 星流云开 开门见山 找到罪魁祸首 章节进度

山重水复时,柳暗花明处…… 过去在研究难题时每每陷入困境,我都会鼓励自己总有办法的。但是微软在Win10安装过程中也有一句:剑阁峥嵘而崔嵬,一夫当关,万夫莫开。 或许是想告诉大家不要挑战鼓捣他的系统。是的,就在昨天失败之后,我便挂机备份用户文件夹,准备用来重装了。

实在不甘心啊,问题都让我找着了,却还能挡我一手,不得不佩服啊……

就当是重装前最后的波纹,再找找资料学习学习吧,也不枉我研究这么久……回到工具作者写的文章当中仔细地寻找与Class Information有关的描述,也仅有

…and class cell index (a given key has no class name).

这么一句,说的是他用来作为讲解例子的键没有类名。根据先前我所随机查看到的许多键的详细信息来判断,在茫茫注册表之海中,拥有类名的键应该寥寥无几。我不禁开始思考,微软究竟出于何意引入了这样一个属性?假若仅仅只服务于某几个键,又有必要使它成为一种键的属性吗?假使不看技术文档,微软内部人员是否还会记得有这么一样事物?而他们又究竟是从什么时候开始雇佣……呃咳,思绪飞的太远了,还是着重于眼前吧……

接着,我又快速地搜索了一下网页,但几乎没人谈及注册表类名的事。而此时我又突然产生了一个逻辑:

假如每个系统都能够对Lsa下的几个子键指定自己想要的独一无二的类名,一旦不符就进不了系统,那么这个类名一定也储存于系统中另外固定的某处。

比如在某个安装系统时产生的系统配置文件中,或者在注册表某个节点作为值名或数据储存,甚至也有可能是另外某个键的类名!也就是说,只要我在新系统的文件以及正常注册表当中搜索Skew1的类名,如果能找到除Skew1以外的地方存在与它类名相同的字符串,那么我就可以到原系统的这个地方取得它需要的类名来完成修复。

然而……挂机搜索了好久的系统文件和注册表,根本没有第二个同样的类名存在。

这没有任何科学性可言,这是完全不讲道理的。系统是如何凭空地要求一个特定的类名呢?既然没有他处存有这个类名,系统又是如何知道Skew1给出的类名是不是自己想要的呢?

此时此刻,我已头晕目眩,坐不住椅子,只能把沉重的躯体往床上一扔。暂时性地关闭我的视觉模块,使得身体更多的能量能被用于大脑的运算上——这些随机的数字和字母,到底是啥,什么东西会需要用到随机的数字和字母……网页地址栏?很多网站倒是动不动就给用户抛出一堆看也看不懂的随机数字与字母组合作为地址的一部分,但它好像……是二十六个字母都会出现,而……类名里面的字母,好像完全没见到 f 之后的……所以它是……一串代表十六进制序列的字符串!

我猛然间想起,似乎遗漏了什么关键信息——在翻阅工具作者经常写文章的博客时,曾看到他更早的一篇文章,它引用了一篇从标题上看似乎更具学术水准、更加鼻祖级的注册表文献17。但当时没有立马阅读,好像是因为网络问题没能打开它的链接?这次无论如何都要观摩一下!

费了好些劲,终于找到了这篇长达上百页的研究,其中详尽地分析了注册表的所有可能理解的细节。其对于ClassNames的记载即使不算十分详细,也让我有了大致的概念:

由于不会轻易地被一般用户看到,微软将某些安全相关的信息存入类名中;将注册表节点导出为.txt文件就能看到类名(假如有);存在的类名形式有三种,分别是有含义的字符串、8位十六进制数,或者单独的一个“0”。

文中还举了个例子,某个和软件管理有关的注册表键的类名是一个有含义的字符串“program"。但该文章对类名的介绍戛然而止,留下一句

No further analysis of the data held in ClassNames was attempted.

“不再进一步尝试分析类名当中的数据”作结。

看到这里,我恍然大悟,Skew1的类名应该就是所谓的8位十六进制数。但是,什么东西会使用这一串十六进制数呢?这时,思绪凌乱的我又突然意识到,假如这类名存入的就是“某些安全相关的信息”,那么会不会有什么安全设置可以让系统忽略这段类名……先前尝试过把Lsa节点的各个值都往不安全的方向设置数据,但不起作用。但眼前如此精简的WinPE系统使我受到了启发——WinPE下几乎没有什么服务在运行,安全服务也停用了很多。如果我能尝试将缺失注册表类名的系统的安全服务都禁用掉,说不定能绕过LSA?于是在注册表中找了几个与LSA有关的服务VaultSvcSamSsEFSKeyIsoLocalKdcNetLogon,全部禁用。但结果是,这几个服务禁用之后都不能解决问题,而SamSs禁用后更是直接蓝屏,连黑屏转圈这一阶段都到不了。而且即使是在WinPE中,也保留了SamSs,说明它对系统来说至关重要。

另外我还发现,这些服务的主进程都是lsass.exe这不是在之前看到的某个错误提示里面吗? 霎时间,所有的线索在我的脑海中飞快地来回闪现,逐渐编织成一条发光的纽带:

lsass.exe Lsa 系统安全 密码管理 注册表 类名 8位 十六进制

八位……十六进制……是Windows的LSA在使用这八位的十六进制序列!会用到这八位的十六进制类名的就是这Windows的LSA!

我触电般地坐了起来,在网上搜索关键词 windows lsa 8-digit hex。这一次搜索,将成为力挽狂澜、摧锋破阵的的决胜一击!下集,LSA之死。Duel - Stand By!

DAY 5 · 转斗移星

Day 0 Day 1 Day 2 Day 3 Day 4 Day 5 Day 6 Day 7 Day 8 山雨欲来 来龙初见 见之怵目 目不及峰 峰回路转 转斗移星 星流云开 开门见山 找到罪魁祸首 找到关键信息 章节进度

话接上回,当我敲下回车,一眼就被跳出来的第一个搜索结果的摘要吸引住了

The Local Security Authority (LSA) (or Syskey) is a 128-bit RC4 encryption key used to protect credentials stored in the Windows Registry

128-bit?那是多少来着
128 比特 = 128 / 8 字节 = 4 ∗ 4 字节 128 比特 = 128 / 8 字节=4*4字节 128比特=128/8字节=44字节

对,和Skew1类似的子键一共有4个,它们的类名都是8位十六进制数,两位代表一个字节,就是4个字节。而且,一看到后面的“RC4加密”、“注册表”,直觉告诉我,这次应该搜对关键词了!立即点击进入。

应该说,当我进入页面后看到内容的一瞬间,不仅仅是没有失望,更是瞳孔震动——这篇文档18列出了明晃晃的四个大字:

  • JD
  • Skew1
  • GBG
  • Data

啊啊啊!没错,就是它们!全体起立!我无法形容自己有多么激动,因为某些领域,人们平日里很少谈及,以至于网上很难搜索到相关资料。唯有的少量文章也仅能通过精准的关键词才能被找到,一旦关键词有稍许偏差,都会导致搜索到所谈之事大相径庭的内容。不过这里说的是传统搜索引擎,如果是直接询问AI,由于它们目前还没有学到这些冷门知识,问它们也还不懂,因此还需要给它们更多时间。

如同抓住了救命稻草一般,我贪婪地消化文章中的信息,不放过任何蛛丝马迹。原来,这四个子键中包含的所谓的类名,根本就不是当类名的字面意义来用的!微软的确在这方面施展了一些小把戏,将4组8位的十六进制数像是藏匿武功秘籍碎片一样分给了四位大护法,由他们随身携带,一旦任何一部分碎片丢失,都无法拼凑出完整的秘籍,武功便将失传……而这些碎片的拼凑也挺有讲究:

1
2
3
4
重新排序
JD
32个十六进制数
Skew1
GBG
Data
启动密钥

首先要将四个键的类名按顺序头尾衔接,然后按照一个约定好的顺序重新排序(例如,将32个十六进制数中的第一个放到第八位,第二个放到第五位……详细的排序也可以在该文档里找到),便能得到“武功秘籍”。那么,这本“武功秘籍”,也就是启动密钥,又是做什么用的呢?请看下图:

加密的LSA密钥 启动密钥 LSA密钥 加密的其它系统密钥 其它系统密钥 前一小部分 组合后的哈希值作为AES密码 后一部分用AES密码解密 前一小部分 组合后的哈希值作为AES密码 后一部分用AES密码解密 加密的LSA密钥 启动密钥 LSA密钥 加密的其它系统密钥 其它系统密钥

这套娃般的操作,我很难用语言来描述,即使画图表示也很勉强。实际上,那篇文档提到的加密方式是RC4,但我从搜索的其它信息中了解到,随着Windows的更新,它由Vista起就改用了AES,而在最近的10、11中似乎也有些许小改动。上图中的“其它系统密钥”指的是系统的各种模块在安全方面使用的密钥,其中相对有名的是NL$KM,被用于访问域账户凭据的缓存(DCC),它的加/解密形式和上面类似,仅仅是用的方法不同。

DCC NL$KM 真实凭据 一部分作为盐 作为放了盐的AES密码 加密凭据部分用AES密码解密 DCC NL$KM 真实凭据

不过,DCC在我们的个人电脑上很少见,主要是在学校或者工作单位那种有很多台电脑互联统一管理的情况下才有。而且我坦白,虽然我已经摸到了密码学的门把手,但我没有潜心研究太多,只了解了一些皮毛,服务于解决我当前面临的问题。所以我所说的这部分内容,可能会有很多谬误……

如果说上面提到的域凭据对一般电脑没什么大不了的话,那么有一样东西一定是所有电脑都有的,那就是系统的用户账户密码。没错,从Windows 10开始,微软就总想要我们登个账号来用系统,所以创建出来本地账户的密码是免不了有的。而就算在远古时期不需要登在线账号时,即使不为默认的本地账户设置密码,系统也会为没有密码的账户加上一个约定的代表了“空密码”的哈希值,然而这个哈希值肯定不是由空字符串算出来的……

用户账户密码的处理流程更加之复杂——

加密的 域账户密钥 启动密钥 域账户密钥 二重加密的 用户帐户 密码哈希 DES加密的 用户帐户 密码哈希 用户相对ID 用户帐户 密码哈希 一部分 作为盐 作为放了盐的 AES密码 加密密钥部分 用AES密码解密 一部分 作为盐 作为放了盐的 AES密码 加密密钥部分 用AES密码解密 分两半打乱后 作为DES密码 用DES密码解密 加密的 域账户密钥 启动密钥 域账户密钥 二重加密的 用户帐户 密码哈希 DES加密的 用户帐户 密码哈希 用户相对ID 用户帐户 密码哈希

其中用户相对ID(RID)是每个账号创建时分配的一组八位十六进制ID,例如本地管理员账号的相对ID就是000001F4,系统会给特殊的账号预留一些比较前面的ID,我们自己创建的账号则会用比较后面的ID,逐渐递增。

在粗浅地学习完这些以后,我总算可以回到棘手的问题上来了。 所以说,系统卡在黑屏转圈的那一步,似乎就是LSA找不到它要用的启动密钥,于是它挠了半天的脑袋,决定还是重启一下看看。而只有作为用户的我知道,LSA的启动密钥已经被吃掉了……之前天真地以为这串密钥只是某个随机生成的字符串,还会有一个副本静静的躺在系统的某处,现在看来,它一点也不安静,因为它每次开机都要参与运算。

简而言之,如果我想重新获得这串密钥,就必须通过计算。那要怎么算呢?我知道,当我创建自己的用户账户密码时,系统会先将我的密码变成一串哈希,这个过程没有用到任何密钥之类的东西,同一个密码算出来的哈希都是固定的;然后再通过上面那张图里的逆过程将这串哈希变为加密过的哈希,而这个加密过程用到了启动密钥。我还知道,假设有一段文字A,有一串密钥B,我可以用B把A加密成密文C。现状是,已知用户账户密码以及它的哈希A、加密的密码哈希C,求启动密钥B。


∵ C = B 加密 A \because C = B 加密 A C=B加密A
∴ B = C ? ? ? A \therefore B = C ??? A B=C???A

这道从逻辑上看似很好解的问题,实际计算起来,恐怕是要下个宇宙见了。 因为那三个问号所代表的运算,似乎还不存在于世上,因此不能直接运算,只能通过穷举法来找到能使得第一个式子成立的B:

var B_test = 0;
var isFound = false;
while(!isFound){ 
	//转换并填充到16个字节
	var B_test_string = B_test.ToString("x");
	if(B_test_string.Length % 2 == 1) {
		B_test_string = "0" + B_test_string;
	}
	var B_test_bytes = Convert.FromHexString(B_test_string);
	var fullKey = new byte[16];
	Array.Copy(B_test_bytes, 0, fullKey, 16 - B_test_bytes.Length, B_test_bytes.Length);
	
	//加密、验证
	var C_test = AES(fullKey).Encrypt(A);
	if(C_test.ToString("x") == C.ToString("x")){
		print("Found B: " + Convert.ToHexString(fullKey));
		isFound = true;
	}
	else{
		B_test += 1;
	}
}

而我用OpenSSL自带的测速功能

openssl speed

测得

Doing aes-128 cbc for 3s on 16 size blocks: 64815744 aes-128 cbc’s in 2.98s

意思是我的机子使用和Windows账户加密一样的AES-128(16字节块CBC模式)能够达到
加密速 率 A E S 128 C B C 16 = 64815744 次 ÷ 2.98 秒 ≈ 2.18 e 7 次 / 秒 加密速率_{AES128CBC16} = 64815744次\div2.98秒 \approx 2.18e^{7} 次/秒 加密速AES128CBC16=64815744÷2.982.18e7/

看起来很快了,只是一山更比一山高,最差情况下我要尝试的次数多达
穷举次数 = 1 6 32 次 ≈ 3.40 e 28 次 穷举次数 = 16^{32} 次 \approx 3.40e^{28} 次 穷举次数=16323.40e28

这将花费
最长计算时间 = 穷举次数 ÷ 加密速 率 A E S 128 C B C 16 = 3.40 e 28 ÷ 2.18 e 7 秒 ≈ 1.56 e 21 秒 ≈ 4.95 e 13 年 \begin{split} 最长计算时间 &= 穷举次数 \div 加密速率_{AES128CBC16} \\ &= 3.40e^{28} \div 2.18e^{7} 秒 \\ &\approx 1.56e^{21} 秒 \\ &\approx 4.95e^{13} 年 \end{split} 最长计算时间=穷举次数÷加密速AES128CBC16=3.40e28÷2.18e71.56e214.95e13

而众所周知,地球现在大约 4.545 e 9 4.545e^{9} 4.545e9岁,也就是说想要用我的电脑算出密钥需要一万个地球从形成到现在这么久,所以也许不只是下个宇宙,而是数百数千个之后宇宙再见了。用显卡来算呢?听说根据算法的不同,显卡可以比CPU快十至数十万倍不等。虽然没有亲测,但我明白,只要快不到十兆倍( 1 e 13 1e^{13} 1e13),就没有用。

啊?……我再次陷入了恍惚当中。我仿佛看到了日月穿行,星辰明灭, 而我想要的那串密钥,就存在于那遥远的、无法到达的彼方……一次次被难题阻挡,一次次欣喜又失落,一次次克服难关,但这一次,在绝对的铁律面前,难道真的没有办法了吗?

——有的,肯定有的! 因为我意识到一件事,那就是:我最初想要的不是启动密钥,而是系统能活过来。更何况,我连注册表的“头”都“移花接木”了,假如我能将新系统的启动密钥移植到原系统注册表中,并说服系统让它以为自己一直用的都是这串新的启动密钥,不就万事大吉了吗?

关键在于,要如何“说服”。从前面了解到的知识来看,与启动密钥有直接关联的东西有:LSA密钥和域账户密钥。只是,由于系统的加/解密流程是环环相扣的,又存在更多的东西需要用这两种密钥来解。也就是说,想要移植启动密钥,所有的其它密钥、加密哈希也都需要一并大换血! 难怪它被称为“启动”密钥,一个密钥就启动了系统安全的方方面面

放手一搏吧——不过先等一等,这里提一个有意思的实验, 是我突然之间产生的想法。我知道有些系统所管理的注册表节点,在“受伤”之后是会自动“痊愈”的。例如,先前曾尝试删除

HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Enum

因为听说它是在系统自动摸索出物理机器上的硬件设备配置情况后生成的一系列数据,如果在不同机器间移植会出现问题,需要删除后让系统重新摸索一遍。那么,虽然启动密钥不会自动重新生成,不知道其它密钥会不会?现在来看,即使其他密钥会重生,启动密钥还是缺失的话,又能有什么用呢?不过当时由于好奇心,还是试着删除了加密的LSA密钥

HKEY_LOCAL_MACHINE\SECURITY\Policy\PolEKList

结果意想不到的事情发生了:加密的LSA密钥重生了,但是与之前不同的一串;启动密钥也重生了!!! 因为这件事,我小小地兴奋了一会儿,但是很快便意识到这毫无意义,因为重生的这两串密钥与其它的密钥,以及更重要的加密的用户账户密码,是完全对不上号的。我也尝试删除加密的用户账户密码,但用脚趾能想到,这种用户自己设的东西是不应该自动重生的,啊哈哈哈……

回到正事上来。 由于和启动密钥有关的两个注册表文件SECURITYSAM中的树形结构令我感到很陌生,我一开始也决定谨慎地用最小原则来替换试验。移植启动密钥到原系统后,接下来首先被替换的是LSA密钥,重启后画面了到黑屏转圈这一步,圆圈才转到2点钟的方位就停住了,随即跳出来的是蓝屏——嗯,完全在情理之中,继续尝试。

经过新系统和原系统这部分注册表的比对,我发现其中有不少键值的数据是一样的,因此没有替换的必要。而在HKEY_LOCAL_MACHINE\SECURITY\Policy\Accounts里面,由于两个系统的用户SID19、域ID都不同,所以除了固定的一些系统级SID,例如S-1-5-20之外,系统初始化后期生成的用户SID中的数据都值得研究一下。好在它们看起来都是一些参数,而非哈希值,这就意味着大概率也是不需要替换的。

在原系统的HKEY_LOCAL_MACHINE\SECURITY\Policy\Secrets里面除了NL$KM,还有默认密码、工作组、UWP应用,以及DPAPI_SYSTEM的密钥,而新系统这里只有NL$KMDPAPI_SYSTEM,其它几个没有,因此只能替换这两个密钥,其它的先不动。至此,SECURITY中除了上述几个密钥以外都已经完成替换了,再次重启,情况还是和前面一样的蓝屏,说明导致蓝屏的问题很有可能在SAM里面,暂时先不去怀疑前面没被替换的密钥。

SAM当中的域账户分为用户账户域和内建(Builtin)账户域,其中内建账户的大部分数据都一致,仅仅是它们域的键下有一个看起来像哈希的值不同,将其替换;而用户账户域的情形似乎有一点复杂,它里面的用户账户信息除了加密的密码哈希,还有诸如用户名、账户状态等杂七杂八数据一并存在于V值中,因此理论上只能修改加密哈希,用户名之类的数据不能变动,否则可能引发问题。但是,整个存储账户信息的二进制注册表数据也和注册表本身那样有数据头部和偏移地址,修改起来很不直观,甚至有点头疼……于是,在经过巨细地推敲、对照之后,我花费了大量时间替换了每个用户下80个字节的数据。

到这里就完成了!我毫不犹豫地重启电脑,心想,一切终于可以结束了吗!!!

:(
蓝屏

……

数日以来,我一直保持耐心,平心静气地研究;但这一回,我第一次感到无比烦躁! 我直接恢复SECURITYSAM,然后把其中所有键值都整个替换了

力大出奇迹,乾坤斗星移,命运的齿轮冲破桎梏重新转动,蓄势待发的电子也终于能
遨游于广袤的晶管之城中——而发出暖色微光照亮我脸颊的,是那阔别已久的系统登录界面!

DAY 6 · 星流云开

Day 0 Day 1 Day 2 Day 3 Day 4 Day 5 Day 6 Day 7 Day 8 山雨欲来 来龙初见 见之怵目 目不及峰 峰回路转 转斗移星 星流云开 开门见山 找到罪魁祸首 找到关键信息 首次进入登录界面 章节进度

尽管很多人不喜欢Windows 8,认为它倾向移动设备的UI改动使用起来很不习惯,但由于我并不介意这一点,它那铺满五彩缤纷简洁方块的开始菜单确实成为了我记忆当中的一抹温暖明亮的画片。而我一直沿用至今的图片密码登录方式,也是从那时引入的。所以当系统成功进入,屏幕上的图片密码使我热泪夺眶,因为这说明我一定没有进错到新系统,而是我原本的系统真的醒过来了!

此情此景,绝对就是对我数日以来不懈努力最好的肯定与嘉奖。我呆坐片刻,多日的烦恼烟消云散,心头的重担总算落下……

回过神罢,我和往常一样在图片上画出密码。只见头像下的圆圈转了一下又消失了,而系统也并没有登录进桌面。这样的情况也在预想之中,因为尽管没有查到文档记载,但根据我的观察与推断,用户账户的图片密码应该储存于

HKEY_LOCAL_MACHINE\SAM\SAM\Domains\Account\Users\我的用户RID中的UserTile

而在新系统里我没有设置图片密码,因此替换完成后,原系统的图片密码肯定还是原封不动的,也就无法用新系统移植的密钥来解。

那么试试看PIN密码呢?系统提示告诉我,PIN密码出现问题,必须重新设置,而且要求我输入账户密码来设置。的确,PIN密码似乎没有存在SAM当中,因此也会对应不上新的密钥。于是问题来到了普通密码上了。新系统安装时,在创建账户的那一步,我采用的是断网+CMD命令绕过登录在线的微软账户的方法,所以成功创建了一个无密码的本地账户。因此,我在这里的密码框中应当什么都不输入,直接敲击回车。随着回车敲下,PIN密码设置并没有弹出,但是转而使用普通密码登录方式的时候,系统成功通过登录界面了!只是,屏幕突然变黑,正中间缓缓地出现两个大字

你好

我愣了愣,难道说系统以为这是第一次登录的全新账户?这也许是因为系统在登录时会确认一下账户对应的某些文件夹、注册表项是否存在,而在原系统里没有关于新系统的这些东西,所以才会被当成全新账户吧。

即将准备完成,请不要关闭电脑

等啊等啊,屏幕上的文字变化了几次,但都是同一个意思:系统在准备着什么,但一直没好,而且怕我关机打断这个过程。过了十几分钟,当我判断即使是新用户初始化也已经远远超出了正常用时之时,我果断重启电脑。

即使还没成功进入到桌面,能够进入到登录界面已经给予我很大的信心。之前盲目地替换全部数据,肯定将原本的用户信息一并替换掉了,即便能够通过登录环节,进不到桌面也是情理之中,因而眼下当务之急的,不是想办法让现在的这个状态的系统进到桌面,而是要回过头去看看哪些数据是可以不需要替换的。之前仔细替换数据没能成功,一定是因为有什么地方还没替换或者替换错了,只要我能找出关键,就能对症用药!于是,我再次施展二分法,一步一步通过减少替换的数据来缩小范围。果不其然,经过数次试验后最终发现,当我没有替换

HKEY_LOCAL_MACHINE\SAM\SAM\Domains\Account

节点下的数据时,就会导致同样的蓝屏出现。该节点下有两个值,分别名为FV。这时我才隐约想起,之前仔细替换的时候好像没有替换这里的数据……算是失误了。稳妥起见,我还针对这个节点搜索了信息,得知它的F值存储的主要是域账户加密哈希,除此之外还包含着代表分配给下一个创建账户的RID的四个字节;而关于V值仅知道它最后12个字节代表的是SID中的域标识,具体来说,每个用户创建的账户的SID都是

S-1-5-21-D1-D2-D3-U

的形式,而V值中的12个字节以每4个字节一组,按小端序换算出来的三个整数,就分别是上面域标识的D1、D2、D3。另外,V值当中还有一些类似哈希的数据,但两个系统的这部分数据是一样的,因此即使不理解也不需要考虑。

所以重点就在于域账户的F,我的猜想是,系统基本加载完毕后,在允许用户登录自己的账户前,会自动登录域账户,而且默认可以顺利登录,因此当密钥解密失败时,系统无法处理这个错误转而进入蓝屏。不过准确地说,也许域账户的登录不能称之为登录,而是检查密钥是否正确,初步肯定SAM中的加密信息。

于是,在与之前一样的仔细替换数据的基础上,我还替换了这里的F值,并手动修改其中的“下次分配RID”信息,以保持和原系统一致,如此一来系统总算不再蓝屏,再次来到登录界面了。

不过这离结束还远得很!因为当我在普通密码登录处敲下回车,以为总算没事了的时候,系统却告诉我密码错误……什么?这不是没有密码么?于是我又象征性地随便输了一些简单密码,甚至把我原本的账户密码也试了一遍,无论如何都是密码错误……这能是为什么?明明密钥、哈希全都已经替换了啊,还有什么可能会导致系统认为我的密码是错的呢?

这时我又想起,我的WinPE里面好像有一个很实用的改密码工具。也许我没替换清楚密码哈希,但只要我用工具来直接改密码,说不定就可以把问题一劳永逸地解决了!不知为何,也许是太久没更新版本了,用这个工具改完之后情况没有任何变化。恰巧的是,我在网上搜索密码修改的方法时,看到了一个基本不会过时的办法

  1. C:\Windows\System32里面把cmd.exe改名并替换同目录下的Utilman.exe
  2. 在系统登录界面点击屏幕右下角的小人儿图标;
  3. 在弹出的cmd窗口输入net user 你的用户名 要改成的密码
  4. 用该密码登录。

妙哉! 这个方法的本质就是利用系统点击小人儿按钮时会唤出Utilman.exe这个小小的程序,来让它转而运行伪装的cmd.exe,而由于系统在登录界面还没有登录任何用户账户,所以此时系统在干任何事情时使用的都是自己,也就是SYSTEM的权限,这个权限理论上比任何一个用户账户都大,因此打开的CMD可以无条件修改用户账户的密码。

其实,只要微软愿意,在这里校验一下Utilman.exe,咱做用户的就彻底没招啦……但是考虑到这个方法已经算是传统技艺,微软这么久都没有对它做什么更改,应该不必担心了。还有一点就是,虽然网上并没有看到有人提起Utilman和小人的关联,尽管它的全称是Utility Manager(实用工具管理器),但在我心中,与Pacman、Rockman等名字形式相似,这个小人的名字就是Utilman(有用的人儿)。不过其实,小人儿是从Win 11开始出现的,之前的系统里都是一个轮椅的图标,啊哈……哈哈……(尴尬地笑)

LittleUtilMan
美中不足的是,net user似乎不起作用,它能列出当前所有的用户名,但设置密码没能成功,因为尝试登录还是密码错误。焦头烂额之时,我又找到了另一个程序Netplwiz.exe,可以打开用户管理界面,下方是一个大大的“修改密码"的按钮。不知为何,CMD命令的给我不确定感,在这图形界面中被扫的一干二净。修改后密码后,输入密码,登录界面转起了圈圈,似乎是系统在斟酌酝酿着什么,过了1分钟,正当我以为又要失败的时候,登录界面消失——通过了!

太好了,这次屏幕没有出现“你好”!但是我桌面在哪?此时的屏幕是一片深灰色,不同于黑屏的黑色,因此我能判断系统绝对不是卡在初始化的那一步;桌面上该有的图标都不见踪迹。动了动鼠标,屏幕上的鼠标指针出现了,但除此之外没有其它东西。这一幕让我额头上又冒了冷汗……冷静地按下键盘上的🪟 + R,没能弹出“运行”窗口;而按下CTRL + ALT + DEL,可以唤出安全选项界面!点击任务管理器,也能正常弹出来,这意味着我可以通过任务管理器来运行其它程序!首先观察了任务管理器,发现里面并没有运行explorer.exe,也就是任务栏和文件夹所依赖的进程,于是手动输入运行,结果该进程在任务管理器中出现了几秒就消失了,说明explorer.exe启动失败。

当程序不由分说地启动失败,用肉眼看不出原因时,应该怎么办?就决定是你了,普罗可梦(Procmon)! 没错,能够解决80%不可见问题的Procmon,这次遇到了它的“20%”……捕捉到的读写记录,看起来都很正常,没有什么访问失败之类的异常点。这可如何是好?既然现在已经进到系统里了,不妨看看系统自带的事件查看器(eventvwr)吧!

这一看,果真发现了不得了的东西——Credential Manager服务反复崩溃、重启。它的别名是VaultSvc,也就是早前尝试禁用的安全服务之一。所以说,它的崩溃或许与我替换密钥的操作有关?再次召唤Procmon,针对VaultSvc的主进程lsass.exe进行捕捉,果然发现它会访问

HKEY_LOCAL_MACHINE\SECURITY\Policy\Secrets\DPAPI_SYSTEM

啊,原来这个DPAPI_SYSTEM是与这个服务有关的吗?我这才想起来,这次替换操作时没有替换这一部分的密钥,因为当时在仔细推敲之后认为或许可以不替换它们。现在看来,替换启动密钥之后,凡是与密钥相关的数据,都应该老老实实替换齐全……

重启——进WinPE——打开注册表——修改数据——再重启!一顿操作行云流水,这几天来类似的流程已经不知做了几百遍了……

好了,VaultSvc正常了,但是我桌面还没正常啊。再打开事件查看器,发现原来还有一个东西在反复崩溃重启,它是shellhost.exe!虽然我没有怎么了解过这个进程,但是光从名字上看,似乎是整个系统界面的重中之重。Procmon三度出击!从捕捉记录中可以看出,它访问了很多形如

HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Interface\{x-x-x-x-x}

的注册表节点,但是有一部分是不存在的,因此访问失败。于是我手动创建了这些节点,还是不起作用。此时我注意到事件查看器当中对于崩溃事件的记录下还有附带内存转储文件,索性就用WinDbg来分析它吧!顺着路径寻找那个文件,却由于进程的反复崩溃而不断刷新刚刚生成的转储文件,使得我无法将其复制出来。按照微软知识库中的办法20,我在

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps

之下手动创建子键shellhost.exe,设置输出文件夹和类型,不一会儿,那个文件夹里就出现了好几个转储文件。接着我随便挑了一个,转移到笔记本上来,在WinDbg中打开,并输入

!analyze -v

调试器自动地分析了错误发生的位置和前后的栈,原来问题是出在一个叫ControlCenter.dll的库文件中的XamlApplication::LoadWinUIFrameworkPackage方法,但是调试器的功能有限,光看汇编代码还是比较头疼的,于是用Ghidra21(有IDA选IDA) 来一探ControlCenter.dll内部的究竟。

Ghidra中分析ControlCenter.dll后,根据WinDbg给出的问题出现的偏移地址,找到了那部分的代码

local_res8._0_4_ = 1;
while( true ) {
	uVar2 = (uint)local_res8;
	iVar4 = AddDependencyToProcessPackageGraph(L"Microsoft.UI.Xaml.CBS_8wekyb3d8bbwe",0,0,0);
	local_res10[0] = iVar4;
	if (-1 < iVar4) break;
	ControlCenterUITelemetry::ControlCenterTelemetry::XamlApplication_LoadWinUiAttemptFailed<>((uint *)&local_res8,local_res10);
	Sleep(5000);
	local_res8._0_4_ = uVar2 + 1;
	if (6 < (uint)local_res8) {
		ControlCenterUITelemetry::ControlCenterTelemetry::XamlApplication_LoadWinUiAttemptTimeout();
		lVar5 = wil::verify_hresult<long>(iVar4);
		//WinDbg找到在相当于下面这行的偏移地址抛出异常
		wil::details::in1diag3::_FailFast_Hr(unaff_retaddr,0x8f,"pcshell\\shell\\ControlCenter\\Dll\\XamlApplication.cpp",lVar5);
		pcVar1 = (code *)swi(3);
		(*pcVar1)();
		return;
	}
}
ControlCenterUITelemetry::ControlCenterTelemetry::XamlApplication_LoadWinUiSucceeded<>((uint *)&local_res8);

不能说完全看不懂,也只能说是一头雾水……仔细观察,shellhost.exe很大概率在加载Microsoft.UI.Xaml.CBS_8wekyb3d8bbwe的过程中超时,换言之,它一定在该过程中读取了某些不符合自己想要的系统数据而导致异常发生。所以,与其进一步追溯AddDependencyToProcessPackageGraph这个尚不知道存在于何处的方法,不如回头去仔细找找看Procmon里面还有没有什么疏漏!功不负我,在一点一点过滤掉无关记录后,我终于发现了一个比较可疑的访问路径

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModel\StateRepository\Cache

其中的子节点存着很多与UWP应用有关的数据,而在其中的User节点下里甚至能看到新系统用户的SID。我恍然大悟,这就是之前全量覆盖移植时多出来的新系统用户的数据。于是我挂载新系统的注册表同样翻到这个节点,发现里面竟空无一物。这表明,Cache中的数据都是系统运行时产生的,关机时会自动清空。而此处之所以会有新系统用户的数据,一定说明当时移植的新系统部分是在新系统运行的时候导出的。所以在正常的Cache之下应当没有任何子节点才对!

于是我立即回到WinPE,挂载原系统注册表一看,果然Cache下还有数据存在。由此,我猜测shellhost.exe会在其中读写数据时被误导、阻碍,从而使得Microsoft.UI.Xaml.CBS_8wekyb3d8bbwe没能成功加载。保险起见,我仅将此处的Cache改名为Cache_bak用以备份,而非直接将其删除。重启进入原系统,桌面还是深灰色,一看shellhost.exe还是在崩溃……我无语地运行了Procmon发现,它一直想要访问Cache,而不再尝试访问Cache下的其它节点了。我一拍脑袋:我傻了,Cache里面的数据是该删除,但Cache这个键本身得留着啊……

在这令人哭笑不得的乌龙之后,我新建了空的Cache键并重启。不到一分钟,我那熟悉而又温馨的小桌面,在长达七天的困苦与辛酸之后,终于又回到了我的面前。

OldBuddy

DAY 7 · 开门见山

Day 0 Day 1 Day 2 Day 3 Day 4 Day 5 Day 6 Day 7 Day 8 山雨欲来 来龙初见 见之怵目 目不及峰 峰回路转 转斗移星 星流云开 开门见山 找到罪魁祸首 找到关键信息 首次进入登录界面 首次进入桌面 章节进度

数日以来,我仿佛被系统拒之门外。而今天,我终于敲开了系统的大门,重新望见了那门外的远山。只是当下的系统也许还暗藏弊病,一触即发。因此眼下迫在眉睫之事就是用系统的更新功能来修复固定注册表和组件。

进入系统设置界面的Windows 更新,点击检查更新,在转了半分钟圈后,它提示我更新失败,错误代码0x800f081f。经过搜索,得知有一种可能是Windows组件损坏。但是,我很确定自己在整个操作过程中没有碰过那些固定的系统组件,所以理论上不是这个原因。不过,既然有这种可能存在,那就请出这方面的专家DISMSFC上场吧!

首先是DISM

dism /online /cleanup-image /restorehealth

但它也报错:没有注册类,错误代码0x80040154。再尝试SFC,也提示windows资源保护无法执行请求的操作。甚至当我尝试用系统安装镜像的setup.exe来原地升级时,它也在进度到达100%后弹窗提示我Windows 11 安装失败

这几样东西在我印象里就好像是同一条绳上的蚂蚱,一出错则全出错。只好一个一个地来分析解决这些问题了。先看看DISM,它的报错可以说是坑了我一大把,因为当它提到“注册”和“类”的时候,我的脑子里浮现出的是“注册COM”和“HKEY_LOCAL_MACHINE\SOFTWARE\Classes”。COM22,这个从Windows早期版本就一直存在的底层设计,最近没少给我带来麻烦。我在本文比较前面的地方提到,自己曾不得不给COM接口的注册表键改权限。而现在,按照字面上的意思来理解,似乎是说和DISM有关的COM组件还没有被注册。这种可能性还算比较高,因为在被Autoruns吃掉的注册表节点当中,比较重要的节点就包括HKEY_LOCAL_MACHINE\SOFTWARE\Classes,而COM的注册信息依据其组件调用方式的不同,主要储存在其下的interfaceCLSID等位置,键名都是形如{X-X-X-X-X}的GUID,每个X都是一串数字。

当我从新系统将Classes移植过来后,也许新系统中同样的组件所注册的GUID和原系统不同,所以原系统才会认为该组件没有注册?我当时并不知道,至少系统级的COM组件,它们的GUID不管安装几次系统都应该是相同的。因此我直接尝试把C:\Windows里面所有的.dll文件(COM组件的功能函数本身主要存在于部分.dll等文件类型中)全都注册了一遍(先说好这个千万不要尝试

Get-ChildItem -Path "C:\Windows" -Recurse -File | Where-Object {$_.Extension -eq ".dll"} | foreach {regsvr32 /s $_.FullName}

这段命令会递归地找出C:\Windows当中包括子文件夹中的.dll文件,然后逐个用regsvr32注册,并且加了/s选项来静默运行,因为也许是regsvr32过于老旧,不会像现在的程序那样把运行结果输出在控制台而是要弹框提示。

在全部的.dll当中,仅有一部分是COM组件,可以被regsvr32调用其中的注册函数,其它的都是普通的或者有专门注册方法的.dll。当上面的命令执行完毕,我以为DISM可以运行了的时候,还是报出同样的错误。尝试重启,系统又出了一大堆奇奇怪怪的毛病,比如显卡驱动的进程一直反复崩溃……而DISM的报错依旧。这时我明白,也许DISM的真正问题不在注册COM上,因为我也亲自找到DISM的文件夹

C:\Windows\System32\Dism

中将所有能注册的.dll注册了一遍,结果还是一样。现在糟糕的是系统又整个出了乱子,一定是我乱注册COM导致的。于是赶忙到WinPE中把Classes给再移植一遍回来,系统便又安分了下来。

由于暂时找不出问题原因,用Procmon也没能看出端倪,只好在网上深度搜索。幸运的是,的确有人提起这个问题23,并且得到了正确的解决方法:

修改
HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\{0823B6F8-F499-4d5e-B885-EA9CB4F43B24}\LocalServer32
的默认值为
%SystemRoot%\winsxs\amd64_microsoft-windows-servicingstack_存在于你WINSXS文件夹下的这个组件的版本号\TiWorker.exe

这年头能在询问系统难题时遇到有人直接回答出正确解决方法,简直是比在路上捡到100块的概率还要低因为现在大家都不带现钱了! 因为大部分人都是告诉你,啊,DISM,啊,SFC,还是不行,啊,重装。现在好了,出问题的就是DISM。

按照上面的说法,我在C:\Windows\WinSxS找到了amd64_microsoft-windows-servicingstack_31bf3856ad364e35_10.0.26100.4933_none_a4edf32577775419,而在上面提到的注册表中发现值的数据竟然是amd64_microsoft-windows-servicingstack_31bf3856ad364e35_10.0.26100.5074_none_a5532eb7772ad4c4,便瞬间明白了问题出在哪。原来,我在装完新系统后,还将它更新到最新版本,此时我的原系统的版本就比新系统落后一个版本了,它们所对应的服务栈版本也因此不同,所以当新系统的Classes被移植到原系统上时,原系统按照其中储存的服务栈版本去C:\Windows\WinSxS中寻找组件,自然是找不到的。这便是所谓的“没有注册类”。

DISM不再报错,熟悉的进度条出现了!只是当进度条在62.3%卡了十几分钟后,很快地到了100,并告诉我:在任何位置都找不到修复内容。很合理,毕竟注册表中还可能有很多其他组件的版本号对不上。于是首先尝试了将新系统的WinSxS文件夹覆盖到原系统,这样理论上就会能找到注册表中指定了的文件。结果是没用,说明原因比我想象的要复杂。我听说DISM所基于的是一个更底层的模块,称为CBS,也就是组件服务(Component Based Servicing)。而在运行SFC同样报错时,它会告诉我们CBS产生了日志文件,其中就可能记录详细问题原因。果断查看C:\Windows\Logs\CBS\CBS.log,果然在其中找到了报错的根源:

...Package~31bf3856ad364e35...not found
...Package_for_KBxxxxxxx~31bf3856ad364e35...not found

由于我没有保存那时候的记录日志,所以忘了这里的具体的文件名,但形式大概是这样的,意思是DISM想要用这些组件包来修复,但是没找到。转到注册表中搜索这些包名,很容易就能找到它们的信息都集中在

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing

因此我猜测,这里的情形和上一个问题很接近,但有些不同:原系统的这个注册表节点没有吃掉,因此这里的数据现在应该都是后覆盖的原系统的才对。那为什么还会找不到原系统的组件呢?我想是因为在覆盖后,新系统的包名也留在了该节点下,而CBS会自动选择同一种包所存在的最新版本,而忽略旧版本。因此在找新版本的时候找不到实际的包文件。根据这个猜想我将Component Based Servicing节点改名备份,再导入原系统的这个节点。

随着进度条到达100%,DISM成功修复组件仓库了!SFC也操作成功。万事俱备,只欠东风!打开Windows 更新,点击检查按钮。不料,还是和之前一样的错误信息,看来对于更新来说,还有什么没有搞定的地方。于是我按照稍微麻烦一点的办法24重置了Windows 更新(主要是删除缓存、重置证书、重新注册有关COM组件、重新设置权限信息),我曾经用这个办法解决了另一种Windows 更新报错,但很遗憾这次不起效果。

无奈之下,我选择孤注一掷,直接运行setup.exe,保留个人文件和应用来原地更新。对此我内心有稍许忐忑:修复了CBS的一部分使得DISMSFC都能成功执行操作,但是同样有一部分依赖于此的Windows 更新却仍有问题,而且似乎找不到日志里的异样。那我所运行setup.exe这个Windows 安装程序究竟是偏向DISM还是Windows 更新呢?按照之前的经验,我更相信是前者,因为DISM的确也可以用于直接部署安装Windows到指定的盘上。

在等待Windows 安装程序检查电脑情况的过程中,它有时会全盘扫描,有时不会。如果他选择扫描,那么这个过程根据用户电脑上数据量的不同,尤其是文件数量越多,它花费的时间就越长。尤其是当它扫描QQ微信的所保存的聊天图片缓存时,由于这些图片数量十分庞大,且文件夹结构复杂,扫描的用时可以长达半小时以上。不过,这次它似乎没有在准备阶段进行扫描,很快地进入了安装阶段了。

在蓝色的安装背景中,眼看着进度完成,如果会报错就是在这时候了!我不禁捏了一把汗,电脑重启了——是计划中的重启,说明没有报错,安装已经成功了一半! 第一次重启后,系统会在相当于平时加载内核的出现Windows图标的阶段继续安装,而且会再出现一个百分比进度。前60%在数次重启后很顺利地通过了,但在这之后,网上有很多反应进度卡住不动了的帖子,有的人是64%,有的人是73%、82%……我最早用官方安装程序来安装Win11的时候,卡在了73%,而这次,我卡在了82%。第一次遇到这种情况时,我耐不住性子,直接重启了电脑,结果系统认为安装失败,恢复了原来的Win10,而后来我断开了移动硬盘,安装就顺利完成了;这一次,我决定耐心等等,因为机子的硬盘灯一直在闪烁,说明它在不停地读写数据,那么它很可能在进行准备阶段没有完成的全盘扫描工作。也就是说,理论上我等待半小时至一小时,进度就会动了。果不其然,在等待了将近一个小时、正当我准备睡觉挂机安装的时候,硬盘灯不再有规律地闪烁,进度数字增加了!接下来进度逐渐增长,又过了十分钟,在屏幕显示“即将准备完成”的文字之后,登录界面就这么水灵灵地亮了出来!

至此,我的Windows 硬核探索之旅已基本大功告成。

那么,还差了点什么呢,简单谈一谈好了:

  • 隔壁盘的UWP文件夹

情形:只有当你将Windows设置系统 > 存储 > 保存新内容的地方中的新的应用将保存到更改为系统盘以外的分区才会出现这个问题:系统会为存在其它分区上的UWP应用文件夹WpSystem加上EFS密钥,所以当我为系统更换整套密钥之后,就无法再获取读写D:\WpSystem\S-1-5-21-域ID-用户ID\AppData\Local\Packages这个文件夹的权限,因而导致新应用安装失败。
============================================================
办法:由于我是原地升级之后才发现这个问题,所以……就是要把WpSystem这个文件夹删掉,然后再来一遍原地升级……或许也有某种重置Windows Store之类的办法可行,但我稍微试了一下没啥用,就不想再研究了。

  • 忘了搞定其它用户的加密哈希

情形:在用wmic useraccountGet-WmiObject Win32_useraccouunt命令列出用户信息时失败,以为是wmic的问题,用winmgmt /resetrepository修复也没有用。我突然想到,当时为了能尽快进系统,只给自己的账户替换密钥、更改密码。但系统中从一开始就存在的DefaultAccountGuestWDAGUtilityAccount等默认帐户,以及在新系统中不存在对等关系的帐户,如果不替换它们的密钥,不只是凭据对不上,它的整个账户信息尾部的CRC都会校验失败,从而导致一些奇奇怪怪的系统问题。
============================================================
办法:将原系统中所有默认账户的密钥及尾部CRC都用新系统中对等账户的替换了,没有对等的账户就用默认账户的替换。然后回到原系统,在开始菜单中搜索并打开系统的计算机管理,对系统工具-本地用户和组-用户中对所有刚替换好的账户右键,点击弹出菜单中的设置密码。默认账户不用输入密码,留空即可。

  • 新版右键菜单缺失某些功能

情形:比如对文本文档单击右键,弹出的菜单里本该有在记事本中编辑。这也许是与Classes有关,也许是与新版记事本的UWP应用有关……
============================================================
办法:但我不再深入研究了,直接到系统设置中的应用 > 安装的应用 > 记事本中点击修复重置,再不行直接卸载了再到Microsoft Store中重装,我就是卸载重装才让它重新出现在右键菜单里的。

  • 进一步修理UWP应用

情形:这是上面那一个问题的延伸举措,用来确保UWP应用这一块不再出什么幺蛾子……
============================================================
办法:

  1. Powershell管理员模式中执行Get-AppxPackage -allusers | foreach {Add-AppxPackage -register "$($_.InstallLocation)\appxmanifest.xml" -DisableDevelopmentMode}
  2. Microsoft Store侧边栏的下载中点击检查更新,等待完成后再点击全部更新

这不是在做梦:当BUG软件把我注册表的重要部分吃掉,我花费将近七日闲时,洞明玄机、损余补缺、剥茧抽丝、精雕细琢,博采众贤之长,尽使浑身解数,以至于能够排除万难、过关斩将,最终让本该被格式化重装的系统起死回生。其间纵有千坎万坷,也在赏得这门前青山之大好风光后,终成往日云烟……

Day 0 Day 1 Day 2 Day 3 Day 4 Day 5 Day 6 Day 7 Day 8 山雨欲来 来龙初见 见之怵目 目不及峰 峰回路转 转斗移星 星流云开 开门见山 找到罪魁祸首 找到关键信息 首次进入登录界面 首次进入桌面 章节 全部完成!

再说

复盘

写完这篇小散文,我相当于重新回顾了一遍两周前的经历,许多当时没想太明白的细节,现在都更加清晰了。

我承认,这是篇文章是有点像流水账,不像干货文章那样直白地把问题和解决方法列的清清楚楚,不过这也是写这篇文章的目的所在——表达研究系统难题的过程之艰辛,也算是纪念一下自己这段时间的所展现毅力,希望假如未来有一天我研究难题再次陷入困境时,回过头来看到这篇文章可以受到鼓舞。还有一点就是,这些啰嗦的废话 详细记录的研究过程也可以为类似的研究提供方法参考。

本文的另一个用途是记载一些特定系统难题的解决方法,不过实际有用的知识密度很低,配合CTRL + F来搜索关键词吧!

总的来说,这次折腾地有多痛苦,折腾成功之后就有多爽!啊哈哈哈!!!……

花絮

  • 报错信息
    很多报错信息都是写文章的时候边回忆边搜索补全的,你看,最后那个和“Package”有关的报错信息我既想不起来,也搜不出来具体的了,所以不得不在前后用省略号代替。
  • 桌面色调
    Day 6结尾的桌面图片,其实不是我当时刚进桌面截的,虽然我那时候确实很激动,但是也没有想到要写文章,就没有截。所以文中这张桌面图是写的时候截的,并且为了还原当时的心情,特地把图片处理了一下,让它看起来明亮舒适。
    很奇怪的是我明明记得当时刚修好的时候,看到桌面就是和处理好的这张图片一样,但是现在实际的桌面要比这个暗一点,色调冷一点。难道是心理作用?仔细想想有一种比较科学的解释是:当时系统刚修好,N卡的APP还是有问题的,启动不了,所以WIN11用了自带的色彩配置,就是偏亮偏灰暖的,而现在我已经装了最新的N卡APP,它调出来的颜色对比度就稍大一点,色调也偏冷(实际上这才是正常)。
  • 文学&AI
    搓文码字就图个爽,水平差也要硬写,哈哈哈……如果我只是想方便地做记录,那应该会直接用AI润色。不过,自己一字一句琢磨,也有别有一番趣味。

  1. Windows 10 support ends on October 14, 2025 ↩︎

  2. Windows Subsystem for Android™️ ↩︎

  3. MustardChef / WSABuilds ↩︎

  4. Windows 11 Pro 24H2 will not boot after enabling Hyper-V ↩︎

  5. Autoruns v14.11 ↩︎

  6. Where the heck is the bug tracker? (serious Autoruns issue) ↩︎

  7. Sysinternals ↩︎

  8. PsTools ↩︎

  9. SetACL ↩︎

  10. WinMerge - You will see the difference… ↩︎

  11. The system registry is no longer backed up to the RegBack folder starting in Windows 10 version 1803 ↩︎

  12. Install the Windows debugger ↩︎

  13. Set up KDNET network kernel debugging manually ↩︎

  14. CONFIG_INITIALIZATION_FAILED 0x67蓝屏原因分析 ↩︎

  15. Registry hive basics part 1 ↩︎

  16. Registry Explorer ↩︎

  17. Registry Structure - Main V4.pdf ↩︎

  18. Local Security Authority (LSA) ↩︎

  19. Security identifiers ↩︎

  20. Collecting User-Mode Dumps ↩︎

  21. NationalSecurityAgency/ghidra ↩︎

  22. Component Object Model (COM) ↩︎

  23. “Class is not registered” , error 0x80040154 ↩︎

  24. How to Reset Windows Update in Windows 10 ↩︎

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值