> "你无法信任不是自己写的代码,但你也无法信任自己写的编译器。"
> —— Ken Thompson, 1984
---
## 开场:那99行代码
1984年,ACM图灵奖演讲的现场。
Ken Thompson站在台上,穿着那身标志性的休闲装,手里没有PPT,只有一张嘴和一段他称之为"Reflections on Trusting Trust"的故事。
他讲了一个程序。很短的程序。99行C代码。
"这是我的得意之作,"他说,"我从来没写过比这更可爱的代码。"
可爱?一个编译器后门程序,用"可爱"来形容?
但当你真正理解这99行代码在做什么,你会明白Thompson为什么用这个词。它不是暴力的、笨拙的——它是优雅的,像一把看不见的手术刀。
这就是我们今天要讲的故事:**一个关于信任的悖论**。
---
## 第一部分:俄罗斯套娃——Thompson的三阶段攻击
### 1.1 从一个问题开始
想象一下这个场景:你写了一个登录程序。
它检查用户名和密码,如果匹配,就让你进入系统。你觉得自己很安全,因为你审查了每一行代码。没有后门,没有漏洞。
但你用的是什么编译器?
如果编译器被人动过手脚呢?它可以在你不知情的情况下,在你的登录程序里插入一段代码——比如,当用户名是某个特定字符串时,直接放行。
"这不可能,"你说,"我会检查编译器的源代码。"
很好。那你用什么编译那个编译器?
### 1.2 第一只套娃:Quine——自我复制的艺术
Thompson的攻击从理解一个古老的概念开始:**quine**(奎因)。
什么是quine?它是一个程序,它的输出就是它自己的源代码。
听起来像魔法?看这段C代码:
```c
char s[] = {
'\t', '0', '0', '0', ' ', '{', '\n',
/* ... 这里是它自己的源代码 ... */
'}', ';', '\n', '\0'
};
main() {
printf("char s[] = {\n");
// 打印s数组的内容
for (int i = 0; s[i]; i++) {
printf("%d, ", s[i]);
}
printf("};\n");
printf("%s", s); // 再打印s本身
}
```
这个程序运行时,会打印出它自己的源代码。就像一条蛇吞吃自己的尾巴——不是毁灭,而是完整的自我复制。
Thompson说,理解quine是理解整个攻击的第一步。**一个程序可以知道自己,可以打印自己,可以把自己植入到另一个程序中。**
这就像俄罗斯套娃。最小的那个娃娃里,藏着制造所有娃娃的图纸。
### 1.3 第二只套娃:编译器学习新"语法"
现在,Thompson做了更狡猾的事。
他修改了C编译器,让它"学习"一个新的语法规则:
```c
// 当编译器看到这段特定模式时...
if (match_login_pattern(code)) {
// ...悄悄插入后门代码
inject_backdoor(output);
}
```
具体来说,他让编译器识别一个特定的模式:编译`login.c`时,自动注入一个后门——如果用户名是`"ken"`,不需要密码就能登录。
但这里有个问题:如果有人检查编译器源代码,他们会发现这个恶意逻辑。
### 1.4 第三只套娃:自我传播的幽灵
这就是Thompson的天才之处。
他修改编译器,让它在编译**任何C编译器**时,自动把同样的后门逻辑插入到新生成的编译器中。
代码结构大概是这样的:
```c
// 编译器源代码中的后门(伪代码)
void compile(char *source, char *output) {
// 正常的编译逻辑
normal_compile(source, output);
// 阶段1: 如果正在编译login.c,注入登录后门
if (is_login_source(source)) {
inject_login_backdoor(output);
}
// 阶段2: 如果正在编译编译器自身,传播后门
if (is_compiler_source(source)) {
// 把这段后门代码本身插入到新编译器中!
inject_self_replicating_backdoor(output);
}
}
```
现在,关键的一步来了:
1. Thompson用**干净的编译器A**编译这段带后门的编译器源代码 → 得到**编译器B**(包含后门,但B的源代码看起来是干净的!)
2. 删除所有痕迹,包括编译器A
3. 现在全世界都用的编译器B,它会在编译`login.c`时注入后门
4. 而且,如果任何人用编译器B去编译一个新的编译器,后门会自动传承下去
**源代码里没有恶意代码。二进制里也没有明显的恶意代码。但恶意代码就在那里。**
这就像你把一份合同的原件烧掉,只留下复印件。复印件上有个签名,看起来和原件一样。但没有人知道,签名是复印时自动加上去的。
Thompson用"最可爱"来形容这段代码,是因为它做到了三件事:
- **隐形**:源代码审查无法发现
- **持久**:自我复制,永远传播
- **优雅**:用最少的代码实现最大的效果
---
## 第二部分:当幽灵走出实验室
Thompson的演讲是1984年。那时候,很多人觉得这是理论上的威胁——"谁会真的这么做?"
答案是:**很多人会**。
### 2.1 XcodeGhost:692个App的噩梦
2015年9月。
中国互联网上突然爆出消息:多个知名iOS应用被感染了恶意代码。微信、网易云音乐、滴滴出行——这些每天被数亿人使用的App,全都在名单上。
总计**692个应用**。
攻击者做了什么?
他们创建了一个假冒的Xcode下载站。Xcode是苹果的官方开发工具,体积很大,从苹果官方下载很慢。很多中国开发者为了省事,从第三方网站下载了"加速版"。
这个"加速版"Xcode被植入了恶意代码。当开发者用它编译自己的App时,恶意代码自动被注入到最终的应用中。
```objc
// 被感染的Xcode会在编译时注入类似这样的代码
- (void)injected_backdoor {
// 收集设备信息
NSString *deviceInfo = [self collectDeviceInfo];
// 发送到攻击者服务器
[self sendToC2Server:deviceInfo];
// 甚至能弹出钓鱼对话框窃取密码
[self showFakePasswordDialog];
}
```
最讽刺的是什么?
**这些App的源代码都是干净的**。开发者们在自己的代码里没有写任何恶意逻辑。但在用户手机上运行的二进制文件,却包含了后门。
这就是Trusting Trust攻击的现实版本。只不过Thompson用了自我传播的编译器,而XcodeGhost用了社会工程学——让开发者主动下载被感染的工具。
### 2.2 W32/Induc.A:编译器感染了编译器
2009年。
一种名为W32/Induc.A的病毒开始在全球范围内传播。它感染了使用Delphi编译器开发的程序。
Delphi是一个流行的Pascal语言IDE。病毒做了什么?
它修改了系统上的Delphi编译器文件(`SysConst.dcu`和`SysInit.dcu`)。从此以后,**任何使用这台机器上Delphi编译的程序,都会自动携带病毒**。
```pascal
// 被感染的SysInit单元会在每个编译的程序中注入代码
initialization
// 正常的初始化
// ...
// 病毒注入的代码
if IsNewDelphiInstallation then
InfectCompiler; // 感染新安装的Delphi
if IsExecutable then
InfectOtherFiles; // 感染其他可执行文件
```
这几乎就是Thompson攻击的完全复现——只不过是用病毒的形式。
攻击者不需要知道你的源代码是什么。他们只需要感染你的工具。然后你的工具会帮他们完成剩下的工作。
### 2.3 tcc后门:一个概念验证
2014年,安全研究员Bojie Li做了一个实验。
他针对Tiny C Compiler(tcc)实现了一个后门。目标是什么?Linux系统的`sulogin`程序。
`sulogin`是单用户模式下的登录程序,用于系统维护。它有最高权限。
Li的后门代码不到100行:
```c
// 修改tcc的代码生成部分
void gen_function(char *func_name) {
if (strcmp(func_name, "main") == 0 &&
is_sulogin_binary(output_file)) {
// 在main函数开头注入后门
// 如果检测到特定信号,直接获得root shell
emit_backdoor_check();
}
// 正常的代码生成
normal_gen_function(func_name);
}
```
他用tcc编译了tcc自身(自我复制),然后用被感染的tcc编译`sulogin`。
结果?一个看起来完全正常的`sulogin`二进制文件,但包含了最高权限的后门。
这个实验证明了:**Thompson的攻击不只是理论。它可以在现代系统上实现,而且很难检测。**
---
## 第三部分:我们能做什么?
好了,现在你知道了问题有多严重。
源代码不可信——因为编译器可能被动手脚。
编译器不可信——因为它可能自我感染。
二进制不可信——因为你不知道它是用什么编译的。
我们能信任什么?
### 3.1 多样化双重编译(DDC)
2009年,计算机科学家David A. Wheeler提出了一个解决方案:**Diverse Double-Compiling(DDC)**。
核心思想很简单:**用不同的编译器交叉验证**。
步骤是这样的:
1. 你有一个编译器源代码S
2. 用编译器A编译S,得到二进制B1
3. 用编译器B(完全不同的实现)编译S,得到二进制B2
4. 如果B1和B2完全一样,说明S是可信的(假设A和B不会同时被感染)
5. 如果B1和B2不同,至少有一个有问题
这个方法基于一个关键假设:**攻击者很难同时感染两个完全不同来源的编译器**。
比如,用GCC和Clang互相验证。它们是完全独立的实现,一个被感染不会导致另一个也被感染。
```bash
# DDC示例流程
# 步骤1: 用GCC编译编译器源代码
gcc compiler.c -o compiler_from_gcc
# 步骤2: 用Clang编译同样的源代码
clang compiler.c -o compiler_from_clang
# 步骤3: 比较两个输出
diff compiler_from_gcc compiler_from_clang
# 如果输出相同,置信度提高
```
Wheeler用这个方法验证了GCC本身。结果?GCC通过了测试。我们至少可以相信,主流编译器目前没有Thompson式的自我复制后门。
但这还不够。因为XcodeGhost告诉我们——**问题不只是编译器,还有整个工具链**。
### 3.2 可重现构建(Reproducible Builds)
下一个解决方案是**可重现构建**。
正常情况下,两次编译同一个程序,产生的二进制文件会略有不同——时间戳、随机数、文件路径等信息会嵌入到输出中。
可重现构建的目标是:**给定相同的源代码和构建环境,任何人都能生成完全相同的二进制文件**。
为什么这重要?
因为如果构建是可重现的,我就可以:
1. 下载你的软件
2. 获取你的源代码
3. 在我自己的机器上编译
4. 比较我编译的结果和你发布的二进制文件
如果完全一样,说明你发布的二进制确实来自你声称的源代码,没有被篡改。
Debian、Tor、Bitcoin Core等项目都在推进可重现构建。这是一个缓慢但重要的工作。
### 3.3 XZ事件的启示:供应链的薄弱环节
2024年3月。
一个差点成为历史上最严重的供应链攻击被发现。
XZ Utils——几乎所有Linux发行版都使用的压缩库——被植入了一个极其复杂的后门。
攻击者"JiaT75"花了两年时间建立信誉,成为项目的维护者。然后,他在测试文件中隐藏了恶意代码,通过复杂的间接层,最终在`sshd`中创建了一个远程代码执行后门。
这个后门不是通过编译器注入的,但它揭示了一个更深层的真相:**供应链的每个环节都可能是脆弱的**。
```bash
# XZ后门的影响范围估算
# - 几乎所有主流Linux发行版都包含XZ
# - 包括Debian、Fedora、Arch、openSUSE
# - 影响的系统数量:数亿台服务器
# - 被发现时,后门已进入beta版本,即将进入稳定版
```
幸运的是,一名微软工程师在调查性能问题时偶然发现了异常。如果后门再隐藏几个月进入稳定版……后果无法想象。
XZ事件教会我们几件事:
1. **信任是个错误的前提**:我们不能假设维护者是善意的
2. **复杂性是敌人**:XZ后门的隐藏利用了测试文件和二进制 blobs,这些复杂层让人难以审查
3. **多样性拯救了我们**:这次是一个细心的工程师在调查无关问题时发现。如果所有人都用同样的工具、同样的流程,后门可能永远不会被发现
### 3.4 竹子机场:货物崇拜的防御
说到防御,我想讲一个Thompson可能喜欢的故事——**竹子机场**。
二战期间,美军在南太平洋岛屿建立基地。岛民看到飞机降落在跑道上,带来物资。战争结束后,美军离开了。
岛民想:如果我们建一个机场,飞机就会回来。
于是他们建了跑道——用竹子做控制塔,用椰子壳做耳机,用树枝拼成雷达。
一切看起来完全正确。但飞机不会来。
这就是**货物崇拜**(Cargo Cult)——模仿形式,但缺少实质。
在软件安全领域,我们有多少"竹子机场"?
- 我们写了安全策略文档,但没有人真正执行
- 我们做了代码审查,但只是走过场
- 我们用了加密,但密钥管理一团糟
- 我们信任"开源就是安全的",但从不验证
Thompson的攻击之所以可怕,不是因为它技术复杂——恰恰相反,它技术简单。它可怕是因为:**它攻击的是我们信任的根基**。
防御这种攻击,不能靠更多的流程、更多的文档、更多的"最佳实践"。
它需要的是:**怀疑的习惯**。
### 3.5 怀疑清单
这里有一个你可以立刻使用的怀疑清单:
**对于开发者:**
- 你的构建工具从哪里来?是你自己编译的,还是从第三方下载的?
- 你的依赖库,你审查过源代码吗?还是只是`npm install`或`pip install`?
- 你的CI/CD流水线,如果它被入侵,会产生什么后果?
**对于用户:**
- 你运行的软件,你能验证它的来源吗?
- 你知道你的操作系统是用什么编译的吗?
- 如果某个关键开发者被收买或胁迫,你会知道吗?
这些问题没有简单的答案。但问出这些问题,就是防御的第一步。
---
## 结尾:我们能信任什么?
让我们回到1984年的那个演讲厅。
Thompson讲完他的故事,台下一片沉默。然后有人问了一个问题:"那我们该怎么办?"
Thompson的回答是什么?
他说,唯一的防御是**源代码级别**的验证。但即使这样,你还是要信任你的编译器。
"这是一个递归问题,"他说,"你永远无法完全解决它。"
40年过去了,这个问题依然存在。
但我们学到了一些东西:
**多样性是信任的基础**:不要依赖单一的工具链。用GCC和Clang互相验证。用不同的操作系统。用不同的包管理器。
**透明是安全的代价**:开源不是银弹,但闭源是银弹的反面——它保证了你永远无法验证。
**怀疑是责任**:不是怀疑一切,而是知道你在信任什么——并且准备好,当信任被打破时,你有退路。
Thompson在演讲结尾说了一句话,很多人忽略了:
> "你不能信任不是自己写的代码。但你也不能信任你自己写的代码——因为你不知道你用的工具是什么。"
这不是虚无主义。这是清醒。
**清醒的意思是:知道边界在哪里。**
我们可以写更安全的代码。我们可以建立更透明的供应链。我们可以培养怀疑的习惯。
但我们永远无法达到"绝对安全"。这不是失败,这是现实。
就像费曼说的——**如果你不能承认你不知道,你就永远无法真正学习。**
编译器中的幽灵不会消失。但我们可以学会和它共处——不是通过恐惧,而是通过理解。
理解它的机制。理解它的边界。理解我们在信任什么,以及为什么。
这才是Thompson那99行代码真正的遗产。
不是为了让我们害怕——而是为了让我们**睁开眼睛**。
---
## 延伸阅读
- Ken Thompson, "Reflections on Trusting Trust" (1984 ACM Turing Award Lecture)
- David A. Wheeler, "Fully Countering Trusting Trust through Diverse Double-Compiling" (2009)
- XcodeGhost事件分析报告 (2015)
- XZ Utils后门事件时间线 (2024)
- reproducible-builds.org
---
*"The code is the truth. But only if you know how to read it."*
#编译器安全 #供应链攻击 #TrustingTrust #后门 #小凯
登录后可参与表态
讨论回复
0 条回复还没有人回复,快来发表你的看法吧!