老万 > 如何写出万人唾骂的软件 - 史上最大Windows蓝屏事故分析
西元二零二四年七月十九日,就是全球 Windows 电脑集体罢工的那一天,我独在微信徘徊,遇见陈 KY 君,弹窗问我道,“老万可曾为蓝屏惨案写了一点什么没有?”我说“没有”。他就正告我,“老万还是写一点罢;这样新鲜的大瓜,怎能不配合先生的风凉话食用?”
可是我实在无话可说。我只觉得所住的并非人间。几十个行业的偏瘫,弥漫在我的周围。一片哀鸿之中,残局尚未收拾。微软与 CrowdStrike 老板高高在上的声明,更使我艰于呼吸视听,哪里还能有什么言语?
我已经出离 WTF 了。我将深度剖析这事件浓黑的教训,为预防惨剧的重现奉献菲薄的力量。
~~~~
小时候,我独爱看电影。银幕上有人晃就看,管它是啥情节。
看久了,我开始有了纠结:这编剧是不是有病?这样煞笔的情节也指望观众相信?世上哪有那么巧的事?
过了很多年,我发现自己又双叒叕被打脸了。生活比神剧还会瞎编,让人不敢相信的事一再发生,不断刷新我的认知阈值。
比如这个星期的 Windows 蓝屏事件。
介绍一下故事的主角。
微软,老牌霸主,软件巨头,总部位于美国华盛顿州。其 Windows 操作系统占据了全球桌面操作系统 73% 的份额。
CrowdStrike,后起之秀,软件安全公司,总部在美国德州,微软长期合作伙伴,为 Windows 提供杀病毒、防黑客等系统安全服务。
这是一个感人肺腑的故事:
微软和 CrowdStrike 精诚合作,双向奔赴,完美避开了所有避坑操作,成功制造了人类史上空前的史诗级 IT 灾难。
事情的严重性已经被广泛报道,不需我重复。这里仅概述一下:
全球八百五十万台 Windows 电脑中招瘫痪,完全无法使用。
三万八千余次航班延迟,四千余航班取消。
法国民众打开电视,以为电视机坏了,结果是电视台停播了。
英国和澳大利亚,收银机歇菜,超市关门,饥肠辘辘的人们拿着信用卡买不到吃喝。
美国,至少十二家大医院因电脑故障取消了对病人的治疗,911 电话服务中断。
中国,世界上人口最多的国家,一切照常 - 除了少数几家外企。因为,在政府多年推行系统软件国产化之后,CrowdStrike 在中国几乎没有市场。
事情发生后,到目前为止,微软和 CrowdStrike 只发表了几篇简单声明,对事故的经过语焉不详,完全没有提及责任认定和公司内部的整改方案。
~~~~
综合多方信息,老万整理出故障发生的原因和经过:
全球很多 Windows 用户,特别是企业用户,都使用 CrowdStrike 提供的安全服务。
众所周知,黑客攻击和防御是道高一尺魔高一丈的关系。所以,CrowdStrike 会频繁(一天数次)更新其配置文件,以对付最新的病毒和攻击。
这些更新会自动推送到用户的电脑上,用户无法拒绝,除非自拔网线。
美东时间7月18日下午,CrowdStrike 又一次例行推送了系统更新,向超过 850 万台设备发送了新配置文件,文件名为 C-00000291*.sys。注意这批文件并非早前传闻的系统驱动程序。据 CrowdStrike 确认,它们只是使用了和系统驱动程序同样的扩展名 .sys,但只是数据文件。
虽然这批新文件不是系统驱动,但它们改变了 CrowdStrike 系统驱动程序的行为,触发了其中的一个臭虫(逻辑错误),造成系统崩溃。
从已知情况看,这一错误很可能是 CrowdStrike 系统驱动程序的编写者忘记了在访问对象前先检查指针是否为空。
在 Windows 中,系统驱动程序具有最高的权限,一旦崩溃将导致整个操作系统崩溃。于是,更新后的机器纷纷蓝屏,也就是挂了。
事发后,CrowdStrike 紧急发布了新的配置文件取代有问题的文件。然鹅,中招的系统已经无法工作,也就无法获取 CrowdStrike 的新配置。
这种情况下,CrowdStrike 无法自动为用户排除故障,必须仰仗用户的手动操作。它建议中招设备的系统管理员以安全模式重启系统,找到并删除肇事文件,然后再手动重启。这是一个冗长且易错的过程,估计这几天好多公司的 IT 管理员都在加班加点重启机器。
要造成如此完美的灾难实非易事,相当于猜对了十位数字密码锁的密码。然而他们做到了!接下来我们就来分析一下奇迹是如何发生的。
~~~~
首先讲讲大家最关心的问题:这个锅到底该微软背还是该 CrowdStrike 背?
我认为两者都有份儿。直接捅篓子的当然是 CrowdStrike,但不可否认 Windows 的系统架构和流程起到了递刀子的作用,如不思悔改,即便没有 CrowdStrike,也会有其它公司捅类似甚至更大的篓子。。
先看 CrowdStrike 的贡献。
错误使用空指针
事故的直接原因极可能是程序员错误使用了空指针。
我们来用一段 C++
程序做例子:
Widget* GetWidget() {
if (!HasWidget()) return nullptr;
...
}
...
Widget* w = GetWidget();
auto area = w->width * w->height;
有经验的 C++ 程序员一眼就能发现其中的错误:要是 HasWidget() 条件不成立,GetWidget() 函数就会返回一个空指针,也就是说 w 变量不指向任何一个 Widget 对象。这时,试图访问 w 指向的对象就是一个无定义的行为(undefined behavior)。
无定义行为是程序员最大的噩梦,一切错误中最严重的错误。
因为,C++ 规定,当程序出现无定义行为时,它什么都可以干:崩溃算是轻的,它甚至可以删除全部用户数据,或者在正确数据中混入一些似是而非的数据导致南辕北辙的结论。
C++ 为什么会有这么奇葩的规矩?因为这样可以让编译器实现更多的优化,让代码在正常工作的时候效率更高。
C++ 有数不清的坑会导致无定义行为。我可以负责任地说:每个 C++ 程序员在职业生涯中都写出过无定义行为的程序。只有被这样的错误咬过几次之后,他才会成长为有经验的程序员。
因为 w 是空指针,读取 w->width 变量就是一个无定义行为,也就是说程序这时干啥都可能。
话虽这么说,大多数 C++ 编译器会让程序此时从一个错误的内存地址读取数据。在 Windows 中,这样的操作会导致程序立即中断。
如果犯错的是普通应用程序,结果就是程序退出,但其它程序还可以运行。这还不算太糟。
不幸的是,在这周的蓝屏事件里,犯错的是系统驱动程序。这种程序是最核心的系统软件,拥有至高无上的权限。Windows 一看系统驱动程序崩溃,马上就吓傻了 - 它也不知道系统坏到了什么程度,要是继续运行,怕是会造成更大的伤害(说不定系统已经被黑客控制了)。这时最保险的做法就是摆烂:显示一个蓝屏,然后罢工。
有经验的 C++ 程序员都会处处小心,确保指针不是空的之后才会访问它。比如这么写就安全了:
Widget* w = GetWidget();
if (w != nullptr) {
auto area = w->width * w->height;
...
}
看来给 CrowdStrike 写系统驱动的不是有经验的程序员。
代码审查走过场
在任何一家正规的软件公司,代码的改动都不能随便提交。只有在同事审查并认可之后,一个改动才能被合并进入代码库。
代码审查的目的,在于多一双眼睛检查程序的逻辑、效率和可读性。有经验又负责的审查者可以发现各种隐患,把它们消除在萌芽之中。
显然,这段错误代码的审查者没有尽到自己的责任。
测试不给力
测试是拦截臭虫的有力防线。越是重要的软件,测试要越严肃认真。
为什么这么大的问题没有被测试抓住?是测试案例不够还是测试流程本身出了问题(比如这次更新会不会跳过了测试这一步)?这些问题值得 CrowdStrike 深思。
无视现代工具
有人说测试都是有一定概率的,测试案例不一定正好就能够引发臭虫,所以不能完全依赖于测试。
说得对。其实,有很多工具可以弥补测试的不足,就看你会不会用。
比如,测试覆盖(test coverage)工具可以告诉我们哪些代码没有被测试覆盖,从而帮助我们构建新的测试案例去覆盖更多重要代码。
许多静态分析(static analysis)和动态分析(dynamic analysis)工具可以系统性地抓出代码中隐藏的臭虫。这两者的区别是:前者不需要执行被分析的程序,后者通过改写(instrument)程序的机器代码,让程序执行时捎带执行检测臭虫的功能。这两种分析各有所长,宜配合使用。
Clang C++ 编译器的 sanitizer 模式是动态分析的优秀代表。如果使用得当,有很大的概率会抓住 CrowdStrike 的这个臭虫。谷歌绝大部分的 C++ 代码在正式上线前都会经过 sanitizer 的验证,相当于加了一层安全网。我加入 Pinterest 后,也马上在公司的 C++ 开发流程中引入了 sanitizer。通过这一过程,我们发现并消除了不少安全隐患。
选择错误的编程语言
毋庸置疑,C++ 功能强大,能让人写出贴近底层硬件特别高效的程序。
但同时广为人知的是 C++ 暗藏了许多陷阱,非浸淫多年不足以正确使用。即便是近年来 C++ 引入了许多新的特性,安全性有了极大提升,坑还是太多了。比如,它的内存安全问题至今没有解决。
所以,对于安全性至关重要的系统软件,C++ 可能不是一个好的选择。为什么不考虑改用内存安全而且高效的 Rust 语言呢?
如果改用 Rust,就绝对不会出这种臭虫。因为,Rust 强迫程序员在处理一个 Option 值(相当于 C++ 中的指针)时必须考虑其为空的情况,比如:
match find_item(id) {
Some(value) => println!("Item found: {}", value),
None => println!("Item not found"),
}
这里 id 是一个 Option 变量,它既可能有值(Some(value)),也可能为空(None)。只有在它有值的情况下,程序才有机会读取它的值。也就是说编译器确保了不会出现读取空指针指向的值这样的程序错误。
当然 Rust 不是万灵药。它最为人诟病的地方是学习曲线比较陡峭,编译器对代码非常挑剔,而且编译速度很慢,不适合于快速迭代。
这些缺点是因为 Rust 对安全性的强调造成的。Rust 编译器会试图证明一段程序是安全的。如果证明失败,编译就无法通过。这就迫使程序员用安全性能被编译器验证的方式写代码。Rust 初学者可能会花费很多时间才能找到正确的方式。而且,编译器也需要花更多时间分析程序才能知道它是否安全。
但是,对于 CrowdStrike 这种重要的安全软件,Rust 的这些缺点相对于它的好处不值一提。
头疼医头式治疗
虽然 CrowdStrike 已经修正了其配置文件中的错误,但这只能算是临时措施。它还没有从根子上解决问题。
配置文件是 CrowdStrike 杀毒系统的输入数据。软件设计有一条基本原则:任何时候系统不能因输入数据的原因跑飞。也就是说,我们必须保证,无论遇到什么样的数据,程序的安全性不能出问题。
因为 CrowdStrike 的系统驱动程序还没有被修正,这个臭虫依然存在,只是冬眠了,说不定哪天又会被配置文件的变化触发。
要真正解决问题,CrowdStrike 需要在近期改掉驱动程序中的逻辑错误,并在远期用可证明正确性的编程语言和工具、流程重新编写它的驱动程序。
~~~~
接下来我们再来表彰微软对此次事故的贡献。
对系统驱动程序监管不力
很多人好奇微软为什么会给 CrowdStrike 这样的第三方软件如此高的权限,“你办事,我放心”。这不是把自己的软肋拱手献给旁人吗?
平心而论,微软意识到了这样的风险,所以会对系统驱动程序的改动做冗长的审查之后再放行。
然而这次爆雷的改动不是系统驱动程序本身,而是它的配置文件。微软大意了,大概是觉得改数据比改代码安全。话说回来,CrowdStrike 一日三次改配置的频度也让微软没时间仔细审查。
我想问的是:微软在审查 CrowdStrike 系统驱动程序时为什么没有发现它有可能被配置文件触发无定义行为?显然,微软对驱动程序的审查不够严格,要么没有要求作者提供其安全性的形式化证明,要么没有用形式化的方法去验证这个证明。
也就是说,微软的审查不是数学意义上的检查,只是一种“我尽力了”的流程。这样的审查当然比没有要好,但远远不够。
过于依赖第三方
计算机安全界的一个常识是要尽量减小被信赖的基础部件(trusted computing base,TCB)。所谓 TCB,就是系统中最核心的、权限最高的部件,比如操作系统核心和系统驱动程序。TCB 里的代码越多,系统安全性就越难保证。
CrowdStrike 做为一个防病毒软件,有多大必要有庞大、复杂的系统驱动程序?微软对它的合作伙伴究竟能有多信任?
也许,微软更妥当的做法是把防病毒软件中需要特权的大部分功能拿过来自己开发,并严格控制其品质。同时,尽量压缩 CrowdStrike 的系统驱动程序的体量,把绝大部分不需要特权的逻辑放到 TCB 之外,按用户级别权限执行。这样,即便这部分代码跑飞了也不会炸掉整个系统。
不能自动回滚
操作系统在更新过程中,如果发现了问题,应该可以自动回滚(或者是在用户确认后回滚)到上一个安全状态。这是现代操作系统的基本修养。
然而,我们看到 Windows 蓝屏后如无人工干预将一直保持不响应状态。其实,既然 Windows 能在系统驱动程序崩溃后执行显示蓝屏的逻辑,它也可以查看近期系统更新记录,得出上次更新搞砸了的结论,然后启动回滚。
一次推送给所有用户
微软的 Windows 系统自动更新的过程可以说是在裸奔。事关系统安全性的软件更新,居然是大撒把式部署,一次性推送到全部用户。
做过大规模软件的都知道,为保险起见,部署一个更新的时候可以滚动式更新(rolling deployment):先更新一小部分机器,然后检测它们的健康状况,如果系统的重要指标没有出现异常,再逐步完成其余机器的更新。要是发现故障率超过预期,系统应该自动回滚,恢复到上一个已知安全的版本。这一切操作都应该自动进行,不需要人为处理。
微软可能信奉的是人有多大胆地有多大产。萨总估计忙着跟 Sam 勾兑,赶 AI 的大潮,结果忽略了自己安身立命的操作系统安全,爆雷只是时间问题。
强行更新用户系统
在这次事故中,850 多万台 Windows 电脑的用户没有选择 - 在微软的许可下,CrowdStrike 强行自动升级了他们的系统配置文件,结果悲剧了。
尽管升级到最新的防病毒软件可以增强系统安全性,但这也可能影响系统的稳定性,所以这是一个需要权衡轻重的选择。对于不同的用户,安全性和稳定性的相对重要程度是不一样的。比如,爱冒险喜欢尝鲜不怕 Z turn 的同学可以选择第一时间更新,911 电话中心可能应该等新版本被一定数量用户验证后再更新。 微软和 CrowdStrike 凭什么替所有人做出一刀切的选择?
是时候把选择权交给用户了。
~~~~
鉴于目前微软和 CrowdStrike 尚未公开完整的事故报告,老万的分析只是基于部分信息,难免有错漏之处。欢迎各位朋友在留言区指正。谢谢!
~~~~~~~~~~
猜你会喜欢:
谷歌对微软:代码管理工具哪家强?- 要集中还是要分布
Exchange Server 用户新年发不出邮件怪谁 - 一只微软大臭虫的尸检报告
谷歌罗曼蒂克消亡史 - 万字长文剖析谷歌文化的演化
后 C++ 演义(第三回) - C++ 的最新发展
程序员护发秘籍 - 掌握这些工作技巧,包你不脱发
程序员的核心技能 - 以脱口秀的方式讲解程序员最重要的技能
如何做出保鲜十年的软件 - 老码农冒死披露行业内幕系列
~~~~~~~~~~
关注老万故事会公众号:
本公众号不开赞赏不放广告。如果喜欢这篇文章,欢迎点赞、在看、转发。谢谢大家🙏
微信扫码关注该文公众号作者