Ivan Kwiatkowski 再次与 Natalie 一起探讨《Hacking with Go:第 2 部分》的后续剧集。这次我们将从用户/黑客的角度了解 Ivan 对 Go 安全功能的设计和使用方式的看法。当然,我们还将讨论人工智能如何融入这一切……

本篇内容是根据2022年11月份#259 Hacking with Go: Part 3音频录制内容的整理与翻译

过程中为符合中文惯用表达有适当删改, 版权归原作者所有.




Natalie Pistunovich: 今天,Ivan Kwiatkowski,你再次加入我们,继续探讨关于用 Go 进行黑客技术的一些内容,并补充我们上一期没来得及覆盖的部分。

Ivan Kwiatkowski: 是的,非常高兴能回来。

Natalie Pistunovich: 那么,对于没有收听上一期节目的听众,你可以简单介绍一下自己吗?

Ivan Kwiatkowski: 当然可以。我的名字是 Ivan Kwiatkowski,我是一名法国的网络安全研究员,目前在 Kaspersky 工作。我在威胁情报团队中工作,我的职责除了编写报告等常规工作外,主要是对同事提供的恶意软件进行逆向工程。基本上,我的同事们负责威胁狩猎,他们找到一些值得研究的东西,识别出攻击者使用的植入程序,然后交给我。接下来的工作就是利用逆向工程师的那些出色工具,比如 IDA Pro,阅读这些程序的汇编代码,尽可能弄清楚它们的功能。这就是我的工作内容。

Natalie Pistunovich: 在上一期节目中,我们聊了一些关于 IDA 对 Go 的支持问题。我们提到,虽然支持有所改善,但仍有改进的空间……

Ivan Kwiatkowski: 是的,完全正确。如果你在一两年前尝试对 Go 程序进行逆向工程,那将是非常困难的,因为当时工具还不够完善。这需要使用一些第三方插件,或者从一些---我不想说“可疑的”,但确实是没有很好维护的 GitHub 仓库中获取代码,这些代码也没有清晰的使用说明。所以,对于逆向工程师来说,这是一条相当艰难的道路。幸运的是,随着时间推移,IDA 的开发者---一个位于比利时的公司 Hex Rays---他们听取了用户的反馈,做出了许多改进。这些改进让我们更容易支持 Go 程序,包括识别来自 Go 标准库的各种函数,对 Go 二进制文件的整体支持也更好了……据我所知,在最新版本中,一些我们之前用第三方插件实现的功能,比如 SentinelOne 的 Juan Andres Guerrero-Saade 和我用 Python 手动实现的功能,现在已经被集成进了 IDA Pro 的主线版本中。

所以,我预计也许再过一年,这个问题可能就不再是个话题了。当然,从汇编代码的角度来看,Go 仍然是一个相对“陌生”的语言,但我相信,工具层面的难题很快会得到解决。这对我们来说是个好消息。

Natalie Pistunovich: 那么,Go 对安全研究员或者黑客来说,是更好的语言选择吗?

Ivan Kwiatkowski: 对安全研究员来说,我们其实不需要写太多程序。我们使用的大多数工具都是社区已经提供好的。我提到过 IDA Pro ---没人会重新开发一个 IDA Pro。当然,确实有人这么做了,所以说“没人”也不完全公平,但大多数人不会这么做。如果真要开发这样的工具,我认为选择 Go 或 C++ 并不会对项目规模有什么实质性影响。

对于黑客来说,我认为 Go 语言可能仍然是一个很好的选择,因为根据我从同事和其他领域的逆向工程师那里得到的非正式反馈,大多数人仍然非常不喜欢处理 Go 语言的程序---真的非常不喜欢。

不过,就我个人而言,如果我要写恶意程序,我会选择 Rust。因为 Rust 生成的代码实际上更加复杂,目前我还不太确定该如何处理这种代码。我需要花些时间研究,但我的直觉是选择 Rust,因为我知道这样可以让未来某些人的工作变得非常糟糕(笑)。

Natalie Pistunovich: 当你说“复杂”时,是指逆向工程、分析和理解代码的过程更加困难吗?

Ivan Kwiatkowski: 没错。我是从这个角度说的---也许这只是我个人的体会。我花了一些时间试图搞清楚 Go 是如何运作的,不是从语言层面,而是至少从汇编层面。我不敢说自己是 Go 的专家,或者对 Go 的内部机制有深入了解……但如果你给我一个用 Go 写的二进制文件,我相信最终我可以告诉你它的作用。然而,对于 Rust 来说,这对我来说仍然是未知领域。据我所知---虽然我没有深入研究,但我可能很快就需要这样做……目前看起来像 IDA 这样的工具,对 Rust 的支持似乎没有对 Go 的支持那么好。它无法识别那么多的东西。而且就结果来看,Rust 生成的代码结构非常像 C++,而 C++ 本身已经足够复杂了。C++ 是一个非常强大的语言,我个人很喜欢它。如果我需要写一个复杂的程序,我会选择 C++,因为这是我最熟悉的语言。但如果让我阅读用 C++ 写的汇编代码,天啊,那真是太复杂了,每一层都会增加很多间接性。所以,这不是我愿意面对的事情。而 Rust 作为一个新的、更复杂、更“陌生”的 C++,让我更加头疼。

Natalie Pistunovich: 那 Go 的跨平台编译功能呢?你只需要写一次代码,就能生成一个可以直接运行的二进制文件,然后再运行一个命令,就可以生成适配任意架构的版本。这个功能对你来说有什么意义吗?

Ivan Kwiatkowski: 这个功能确实有其意义,从开发者的角度来看,这种能力还是挺酷的。不过,我一直觉得很多 Go 语言的支持者强调的这项功能有点被过度吹捧了。我并不认为它是那么重要的功能。并不是说我们不需要跨平台编译,或者不需要能在任何地方运行的程序,而是我觉得 Go 语言在这方面并没有带来什么全新的东西。比如说,我用 C++ 写代码时,只要写得合适,代码本身就已经可以在任何地方运行了。再往前追溯,大概10年前我还在学校的时候,我们学的是 Java,当时我用 Java 写过一些项目。理论上,Java 也应该像 Go 一样能在任何平台上运行,对吧?虽然由于种种原因,它没有达到我们预期的效果,但总体来看,这种可以在任何平台运行并编译的能力,并不是 Go 独有的。我觉得这是我们已经具备的能力,Go 只是让它对很多开发者来说更简单了一些。但这并不是一个能让我因此转向 Go 的理由,绝对算不上什么颠覆性的功能。

顺便说一句,可能我可以反过来问你一个问题……我对 Go 在一些“冷门”平台上的支持情况并不了解。比如说,在 Solaris 或者 ARM 架构上运行 Go 程序,这些是能够开箱即用的吗?因为我很清楚,当有新 CPU 推出时,制造商发布的第一批工具中总会有一个 C 或 C++ 编译器。所以我们能确定这些语言最终总是会支持的。但我有一种感觉---可能是错的---如果是 Go 语言,当有一个新的平台出现时,你得等 Google 发布对应的编译器,而这可能需要一些时间。是这样吗?

Natalie Pistunovich: 你问的时候,我用 Google 搜了一下 [听不清的命令 00:11:35.24],这是用来干这件事的命令,对于 Solaris 来说是开箱即用的。那么你刚才问的第二个是什么?

Ivan Kwiatkowski: 我猜应该是受支持的,比如编译到 ARM 或者 MIPS 这种可能不太常用的架构……但我想至少 ARM 应该是支持得很好的。

Natalie Pistunovich: 是的,ARM 确实是开箱即用的,没问题。

Ivan Kwiatkowski: 好的。

Natalie Pistunovich: ARM-64 也支持。而 MIPS - 一些变种不支持,但大多数还是支持的。不支持的主要是 MIPS-64 P32 和 MIPS-64 P32LE。

Ivan Kwiatkowski: 嗯,对。总的来说,这些可能是大多数人根本不关心的架构。所以我不认为这是 Go 语言的重大缺陷。不过我想说的是,就我而言,C 已经是一个跨平台的语言了。如果其他语言也能提供这种能力,那很好,但对我来说,这并不是什么革命性的功能。

Natalie Pistunovich: 是的,这很公平。不过很多 DevOps 人员确实喜欢这个功能,因为你几乎不用做什么就能把程序适配到你喜欢的任何架构上。

Ivan Kwiatkowski: 是的,这个方面确实很重要,而且也很受恶意软件作者的欢迎。因为,当你用 C 写一个程序时,可能会有一些模块是以 DLL 文件的形式分发给 Windows 系统的,或者是以 .so 动态库的形式分发给 Linux。然后你会得到一个程序---一个可执行文件,以及几个附带的对象库。接着,当你想分发它时,你需要发送一个包含许多文件的大型归档文件。但是如果是用 Go 写的程序,你只会得到一个单一的二进制文件,这确实很有用,尤其是在你无法控制客户端,也就是“受害者的机器”的情况下。在这种情况下,只需要发送一个可执行文件,并且知道它是自包含的,可以随时随地运行,这无疑是一个巨大的优势。虽然用 C 或 C++ 也可以做到这一点,但需要一些额外的工作……而这些工作在用 Go 时是不需要的。所以这一点上,我会给 Go 加一分。

Natalie Pistunovich: 还有一个功能,你觉得怎么样,就是模块的概念?它会有一个文件,说明了所有的依赖项,以及每个依赖项使用的具体版本,比如如果你使用了某个旧版本的包之类的。既然这些都被编译到一个模块里,并被作为一个整体发送出去---这对黑客或安全研究员来说有价值吗?

Ivan Kwiatkowski: 对安全研究员或黑客来说,我不确定这是否有直接的价值。对于开发者来说,这可以防止他们陷入依赖地狱。这种问题我上周在一个 Python 项目里刚好遇到过。我更新了所有的包,它们之间有些不兼容,结果整个项目在生产环境直接崩了……总之,这很有趣(笑)。其实我本可以通过固定版本号来避免这种情况,这是你应该做的事情。但总体来说,有这样的机制还是很好的。

在安全方面,我们并不太担心黑客能否编译他们的程序。我们更担心的是,当你得到一个包含所有内容的单一二进制文件时,这对逆向工程师来说是个问题。相比之下,用 C 或 C++ 编写的程序可能会有多个 DLL 文件,而这些不同的文件本身就代表了一种代码的分离方式。DLL 文件可能会有相关的名称,也可能没有。但无论如何,你知道它们是按照某种功能拆分的。某些功能的代码会放在特定的 DLL 里,而程序的主要逻辑会放在主可执行文件里,等等。

所以,当你需要处理那些大型恶意软件平台时,有多个文件其实是对我们有利的。而当你面对一个 5 到 10 MB 的 Go 二进制文件时,你必须深入其中,试图找出有趣代码的位置---这可能是好事;哪些是无趣的库代码(工具通常能很好地识别这些)。但你仍然面对一个包含所有内容的大型程序。如果有这些分离的文件,你可以更专注于某些具体功能,即使你还没完全深入整个项目。所以从这个意义上讲,这个功能对攻击方来说是非常有用的。

Natalie Pistunovich: 我很好奇是否有任何特定的功能对防御方有利……我会继续问问题,直到找到一个答案。或者你有什么想法吗?

Ivan Kwiatkowski: 我确实有一个想法……我认为 Go 相比于 C 和 C++ 这些非托管语言最大的优势之一是,当你用 Go 编写程序时,你不用担心内存损坏、缓冲区溢出之类的问题。我会非常惊讶如果 Go 语言允许你访问数组边界之外的内容。这些问题已经被 Go 帮你考虑到了。这虽然不会让我的日常逆向工程工作更容易,但它提供了一个好处,那就是开发者更难“搬起石头砸自己的脚”。这对整个行业来说是件好事,即使是在安全领域之外---如果用 Go 写的程序,比如 FTP 服务器、邮件服务器等等,我们至少可以不用担心缓冲区溢出的问题。这意味着我们可以减少因为某些客户没有及时打补丁,或者因为某个零日漏洞被发现并在野外被利用,而导致的应急响应工作。

所以,对防御者来说,减少漏洞和减少开发者犯严重错误的可能性,总归是件好事。我觉得这一点实际上超越了攻击者在工具层面上对我们的任何优势。

Natalie Pistunovich: 这是一个非常有趣的观点。我想如果能整体评估一下用 Go 编写的代码是否更安全,那会很有意思。不过这要怎么衡量可能是个难题。

Ivan Kwiatkowski: 嗯,想要精准的指标总是困难的。但如果你比较用 Go 写的项目和用 C 写的项目的 CVE 数量,我觉得很可能会发现,用内存非托管语言(比如 C、C++ 等)编写的程序总是会有更多漏洞。因为从一开始,这些语言就提供了更多“搬起石头砸自己脚”的机会。

如果让两位开发者能力相当,一个开发者因为语言限制少犯了很多错误,而另一个开发者因为语言本身的原因多犯了一倍的错误,对我来说这很显然---无论开发者水平多高,使用不安全语言的人总会犯更多错误。

Natalie Pistunovich: 你见过用 COBOL 写的恶意软件吗?

Ivan Kwiatkowski: 其实没有见过。

Natalie Pistunovich: 我很好奇你会怎么评价它---更安全还是更不安全?你怎么看?

Ivan Kwiatkowski: 我完全不清楚(笑)。COBOL 是我听说过的语言之一。我知道,如果你想进银行工作并赚大钱,那你绝对应该学 COBOL,因为所有会写 COBOL 的开发者基本都已经退休了,所以很难找……除此之外,我这辈子,或者至少过去十年,从没听说过有用 COBOL 写的恶意软件。如果我真的遇到了,这一定会是一篇很有趣的博客文章,但对我来说可能会是痛苦的一周,因为我得学 COBOL 并弄清楚它的工作原理。不过,说实话,这周我刚好逆向了一个用 Pascal 写的程序……

Natalie Pistunovich: Pascal 是 Go 的精神父母。

Ivan Kwiatkowski: 是的,我猜是这样。它也是许多其他语言的精神父母,因为它大概是 80 年代甚至 70 年代的产物。我记得在学校学习过 Pascal 的基础知识,大概是15年前的事了。

Natalie Pistunovich: 我也是。我的高中毕业项目就必须用 Pascal 写。

Ivan Kwiatkowski: 是啊,就是这样。我自己可能也写过一点 Pascal,虽然没写过什么真正有意义的项目……不过我确实接触过一些 APT 恶意软件,也就是现实中的 APT。

Natalie Pistunovich: APT 是什么意思?

Ivan Kwiatkowski: 啊,不好意思,APT 是 “高级持续性威胁”(Advanced Persistent Threat)的缩写。APT 是我们日常工作中跟踪的一类黑客。你可以把攻击者分为两类:一类是以经济利益为动机的,比如网络犯罪、勒索软件团伙等;另一类就是我们所说的 APT,基本上是国家支持的攻击者或者雇佣兵性质的团伙,这些人专注于网络间谍活动。最初,APT 这个名字大概是由 Mandiant 公司在 2010 年左右的第一份报告中提出的。当时,这个名字的意义很明确:一方面是做普通犯罪软件的低端网络犯罪分子,另一方面是进行非常复杂攻击的国家支持攻击者。

但到了今天,我觉得用复杂程度来区分攻击者已经不太有意义了,因为现在一些勒索软件团伙非常专业,他们使用最前沿的渗透测试方法……与此同时,也有一些 APT 非常糟糕,比如操作安全(OpSec)很差,甚至不知道如何正确使用工具等等。所以到了 2022 年,当你听到 APT 时,最好直接把它理解为“间谍活动”。这是一个更恰当的理解方式。

不过无论如何,这周我正在处理另一个 APT 案例,发生在某些 STEM 国家,也就是独联体(CIS,独立国家联合体,这应该是它的全称)的一些地方。我们在其中发现了一种恶意软件植入程序,居然是用 Pascal 写的。这对我来说是一场回忆之旅,不仅要重新搞清楚 Pascal 是什么,还得理解这个 Pascal 编译器生成的汇编代码是什么样的。其实也没那么糟,比重新接触 Go 语言要好得多。

Natalie Pistunovich: 这很有趣,因为 Go 和 Pascal 之间有很多相似之处,但它们的翻译结果不一样。

Ivan Kwiatkowski: 哦,是的。我可以告诉你,尽管我不是任何一种语言的专家,但尽管两种语言在代码层面、语法结构以及声明方式上可能有一些相似之处,但在汇编层面,它们完全不一样。

Natalie Pistunovich: 我本以为如果它们在概念上相似,可能结构上也会类似,但看来并不是这样。

Ivan Kwiatkowski: 并不是这样。我想它们的语法可能有些相互借鉴……但编译器的实现方式完全不同。Go 编译器确实有自己的一套东西。

Natalie Pistunovich: 当你说每种语言在汇编表示或逆向工程的可视化表示上都有自己的特点时,能有多少种不同的方式?每次真的会完全不一样吗?

Ivan Kwiatkowski: 并不是总是完全不一样,但确实会有一些显著的差异。我认为 C 语言---也许这是我的一种误解,因为 C 语言通常是你学习逆向工程的起点---对我来说,C 语言是最接近 CPU 的语言。当你编译一个 C 程序时,它几乎是直接从 C 代码翻译成汇编代码。编译器不会太“聪明”,不会对你的代码做过多处理;你在 C 中写了什么,基本上就会直接反映在汇编中。当然,你可以通过一些编译器优化来提升速度或减少占用空间,但总体来说,这种翻译是相当直接的。我不会说它容易逆向,但它确实相当直观。你可以比较容易地从 C 代码找到汇编的对应关系。

我想,这也是为什么像 Ghidra 或 IDA Pro 的反编译工具可以从汇编代码转换回 C 代码的原因,因为 C 是最接近底层的一种语言。而当你转向更复杂的语言时,比如 C++,编译器就会做很多事情,这时候你写的代码和生成的汇编代码之间的差距就会非常大。

例如,当你在 C++ 中使用 std::string 时,看起来很简单,对吧?但实际上,std::string 是一个模板类的实例化,它背后是非常复杂的嵌套模板系列。你最终会得到一个奇怪的结构体,里头有一个表格,表格里包含了一些方法的指针---这些东西你在 C 中根本没写过。接着,你会看到方法相互调用,这些方法来自 C++ 标准库的模板库……从这里开始,一切就越来越复杂了。

再以 Pascal 为例---虽然我没怎么写过 Pascal,但当我看汇编代码时,看到引用计数被自动增加和减少,这些都是由编译器自动添加的。这对程序的运行来说可能有用,但对我理解程序来说,这完全是无关紧要的噪音。编译器添加的这些代码并没有给程序增加任何“智能”,只是在妨碍我的工作。

Go 可能是这方面的另一个极端,和 C++ 一样,Go 编译器在底层做了非常多的事情。怎么说呢……

Natalie Pistunovich: 优化吗?

Ivan Kwiatkowski: 对,优化。它会内联任何不值得单独调用的函数……调用约定也有自己的一套……哦,还有垃圾回收机制。当你用 Go 写一个简单的“Hello World”程序时,最后生成的可执行文件可能有一兆字节左右。我知道,现在存储空间已经很便宜了,我们不在乎一兆字节和七千字节的区别。但对逆向工程师来说,如果你需要分析一兆字节的代码而不是七千字节,那真的是天壤之别。而这就是 Go 编译器给你带来的问题,它还会进行许多优化,以及使用非常奇怪的调用约定。

另一个我不喜欢的 Go 功能---虽然它很棒,但作为逆向工程师我不喜欢---就是 goroutine。

Natalie Pistunovich: 是的,我本来想问的下一个问题就是这个。请详细讲讲吧。

Ivan Kwiatkowski: 好的。goroutine 对开发者来说似乎是一种非常简单的方式来创建多线程程序,这对开发者来说确实是很棒的功能……但这也让恶意软件开发者更容易创建多线程程序。而当我们试图理解一个程序时,我们更喜欢线性程序。我们希望看到一步接一步的指令,这样可以轻松调试。当许多线程同时运行时,天哪,追踪程序的逻辑变得极其困难。所以我其实希望多线程程序的编写更困难一些,这样攻击者就不会那么容易使用了。

Natalie Pistunovich: 在逆向工程中,线程的表现形式是什么样的?

Ivan Kwiatkowski: 实际上,线程本身并没有明确的表现形式,因为线程作为一个概念,基本上是运行时的对象。线程是执行代码的一个单元。如果一个程序只有单线程,你可以线性地跟踪代码,逐行理解程序的逻辑。但一旦有多个线程运行,你的复杂度就会成倍增加。作为逆向工程师,你不仅要分析当前函数的逻辑,还要时刻考虑是否有其他线程在某处运行,可能会影响正在执行的逻辑。这样你就无法只依赖当前看到的信息,程序的功能被分散到多个执行单元中,你需要将所有信息都记在脑子里,才能弄清楚程序的行为。这对逆向工程师来说是一个非常沉重的“心智税”。线程越多,跟踪起来就越困难。

一个很好的例子是我在上期播客中提到的 Go 程序,叫 Stowaway。这是一个开源项目,用于渗透测试工具,比如创建隧道、SOCKS 代理等,可能还可以将它们相互连接。不过我可以确定的是,当我试图阅读这个程序的汇编代码时,那种感觉非常痛苦。显然,很多事情是同时发生的,比如网络程序中,来自不同终端的包可以随时到达。同时,你可能还会有多个隧道在运行。

所以,当所有这些事情同时发生时,要弄清楚每一部分的具体作用非常困难。如果我没能找到它是一个开源项目并查到源代码的话,我可能根本无法完全理解这个程序的所有功能,因为要处理的信息量实在太大。而我的记忆力---就像任何人类相比电脑的记忆力一样---其实非常有限。

Natalie Pistunovich: 除非我们开始升级大脑……

Ivan Kwiatkowski: 是啊,我希望如此……

Natalie Pistunovich: 你提到的这些有趣的术语,比如 APT,还有刚刚说到的 Stowaway,我会把它们放在节目笔记里,方便大家回顾,看看如何在逆向工程中处理线程问题。如果能看到一些实际的例子,比如线程是如何影响结果的,那会很有趣。你提到线程之间可能存在数据依赖,或者某些计算结果会返回并共享数据。回到你发布在 YouTube 上的视频《逆向工程一个 Go 程序》,我会把这个视频的链接也放到节目中。在视频里,你通过绘制流程图展示了不同步骤的发生过程,以及你的推测等等。多线程逆向工程中,指令是否会随机出现?你如何在相关的上下文中理清它们?

Ivan Kwiatkowski: 我觉得最好的方式是不要试图以程序运行的形式去理解它,而是想象你在阅读一个 Go 程序的源代码。我想你会同意这样一个事实:如果你拿到一个同事写的 Go 项目,而你对这个项目一无所知,这个项目的源代码会更容易理解一些,前提是它是一个单线程程序,只做一件事。如果这个程序一开始就启动了三个线程,同时做不同的事情,那要弄清楚这个程序的逻辑就会困难得多。

现在,想象一下,你拿到的不是 Go 源代码,而是所有变量名都被擦除了,所有变量都被命名为 A、B、C 等等。你甚至无法通过函数名或变量名来理解程序的意图。再想象一下,代码里没有任何注释。这基本上就是逆向工程师所面对的情况。

Natalie Pistunovich: 不用想象,这种情况经常发生……

Ivan Kwiatkowski: 是的,当然可以(笑)。对于许多人来说,这可能就是他们的现实生活。向他们致敬吧……不过这确实就是逆向工程的本质:你会收到一些代码,不管是汇编代码还是高级语言代码---这确实有些区别,因为汇编代码更难读懂……但最终的过程是相似的。你收到一些代码,需要弄清楚它的作用。而代码越复杂、操作越高级,你理解它的难度就会越大。

Natalie Pistunovich: 这让我想到一个问题:一般来说,Go 的最佳实践,或者说“正确的写法”,是写简单、易读的代码,而不是复杂的,比如三元运算符或其他复杂的表达式……这对逆向工程有帮助吗?

Ivan Kwiatkowski: 我希望如此,但事实证明,对于编译器来说,无论你使用简单的方法还是三元运算符,只要编译器足够智能,最终它生成的汇编代码都是一样的。理想情况下,编译器可以识别出“if...then...else”和三元运算符是同一个逻辑,最终生成的汇编代码是完全相同的。因此,从开发实践的角度来看,这是一件好事……但当你深入到汇编层时,那些对人类友好的代码风格和为了提高可读性所做的努力都会被编译器丢弃。因为这些是为人类设计的,不是为 CPU 设计的,所以它们在编译后的程序中没有立足之地。

Natalie Pistunovich: 这是个很好的观点。

Ivan Kwiatkowski: 其实,前面你问过一个问题,关于不同语言的编译器能做什么区别。我可以再举一个关于 Go 的例子:Go 函数可以返回任意数量的返回值,对吧?而大多数语言并不支持这一点……所以当你观察汇编代码时,最终你会发现它被翻译成 CPU 代码的方式与传统函数完全不同。

对于大多数语言,比如 C 或 C++,函数只能返回单个值,这就形成了一个非常简单的约定:汇编代码中规定返回值会存在寄存器 EAX 中。这是一条非常简单的规则。但在 Go 中,返回值不止一个,所以它们会通过栈来传递,你需要在栈中寻找这些值……这就变得更加复杂,与传统语言非常不同。

不同语言的区别往往体现在这样的约定上,而这些小的差异最终会导致源代码或汇编代码在形式上截然不同。

Natalie Pistunovich: 你觉得,Go 在编译器层面对多返回值的处理效率高吗?

Ivan Kwiatkowski: 是的,据我所知,它确实表现得非常高效。可能我记错了,但在我的印象中,函数调用返回的值会被直接放置在栈上正确的位置,这样另一个函数可以立即将它们作为参数使用。所以,当你在调用一个函数时,如果传入的参数是另一个函数的返回值,这些函数调用在汇编代码中看起来是紧密衔接的。你不需要将返回值从栈中移回来再放回去,它们已经在正确的位置了。我认为在这一点上,Go 的处理效率很高,也非常快。

Natalie Pistunovich: 很棒,听到这个总是令人鼓舞的……所以 Go 的设计方式似乎并不是让你逐行调试代码、设置断点,而是通过处理错误来检查代码是否正常。我不会问恶意软件是否也通常这样写,或者它们的错误处理有多好……除非你知道,那请告诉我们。

Ivan Kwiatkowski: 其实我确实知道,因为我会阅读它们的代码。我通过阅读汇编代码和自己尝试去理解 Go 的运行机制,了解到 Go 语言不会允许你忽视错误处理。如果一个函数返回两个值,而你没有处理它们,那代码是不会编译通过的。当然,你可以用一个下划线变量 _ 来表示“我不关心这个值”。

Natalie Pistunovich: 没错……

Ivan Kwiatkowski: 是的。但至少根据我看到的程序,它们确实会捕获错误、检查错误并妥善处理它们。我觉得这很合理,因为语言强制你这么做。所以虽然你可以用下划线变量来规避检查,但如果你选择了 Go 语言,还是按照它的设计意图去使用会更有意义。而这也是我在分析恶意软件代码时看到的情况。

Natalie Pistunovich: 听到即便是恶意软件也遵守最佳实践,还是有点“治愈”的(笑)。不过确实,错误信息中往往包含了很多有用的信息。

Ivan Kwiatkowski: 作为开发者,你是否经常遇到代码中忽视错误处理的情况?比如你的同事提交的代码?

Natalie Pistunovich: 不,通常这种代码过不了代码审查……但我并不知道黑客们是否进行代码审查(笑)。所以这点很有趣。

Ivan Kwiatkowski: 根据我所看到的,这只是一些个例。就像现实世界中的开发者一样,总有一些黑客会用自己的“糟糕方式”写代码。但从我分析过的恶意软件来看,我觉得它们的代码开发得还算不错。当然,也一定会有某些黑客写出了最糟糕的 Go 代码(笑)。

Natalie Pistunovich: 这当然有道理,但听到总体上还是遵循好习惯让我很开心。不过我想问的是,这种错误检查在汇编中是如何体现的?因为这并不是一个常见的实践……是不是因为错误很少,所以几乎看不到这部分的表现?

Ivan Kwiatkowski: 我观察到的情况是,当我查看汇编代码时,大多数时候我需要查阅被调用函数的文档。有时函数名很直观,但大多数情况下我需要去查 Go 的文档……顺便说一下,Go 的文档非常出色,每次我查找函数的文档都能找到,这是一件好事。相比之下,有些语言的文档就很难找到,甚至是一些基础函数的文档。

所以,我查阅文档时会了解到函数需要哪些参数,以及会返回哪些值。如果函数会返回一个错误值,我还没有遇到过不检查这个错误值的情况。在汇编中,你会看到函数调用后从栈中取出某个随机变量,与零进行比较,类似于 if err == nil 的逻辑。根据错误是否为 nil,程序会进入不同的代码块。这表明攻击者确实检查了函数是否返回了错误值。

Natalie Pistunovich: 你之前提到,逆向工程就像是在阅读同事写的代码,但没有参数名、文档或函数名……这让我想到,可以使用一些 AI 工具,比如 Codex 或 Copilot,它们可以解释代码的作用。你有没有机会用过这些工具?

Ivan Kwiatkowski: 我没有用过,但我知道 GitHub 推出了类似的项目。我对这些工具有一种“宗教式的恐惧”,因为它们会把我写的所有代码上传到云端,进行分析并用于训练机器学习算法。这种恐惧其实有点愚蠢,因为我写的代码最终都会开源……但我还是不喜欢这种方式。

Natalie Pistunovich: 如果代码已经在 GitHub 上,那它也会到同样的地方。

Ivan Kwiatkowski: 没错,最终也会到那里。所以总体来说,我没有什么充分的理由不用这些工具。但我还没试过。我听一些同事说,他们用这些工具写 Python,效果非常惊人。这些工具几乎能猜到你在想什么,这还是挺吓人的。

Natalie Pistunovich: 是在写代码时用的,还是在逆向工程时用的?

Ivan Kwiatkowski: 是写代码。我还没听说有机器学习项目能帮助你进行程序的逆向工程……但我百分之百相信这是可能的。最近我玩了很多图像生成的 AI,尤其是 MidJourney。我还试过生成文本的 AI,比如 Lex……这些 AI 都让我觉得非常厉害。如果一年前有人告诉我,只需要输入一段文字,就能生成一张对应的图像,而且这张图像看起来还非常棒,我肯定不会相信。我会觉得这是科幻小说里的情节,绝对不会在我有生之年看到。也许等我老了,完全搞不清楚这些东西时才会出现。但现在,这一切已经实现了,尤其在人类语言理解和内容生成等复杂应用上。

到这个阶段,如果你告诉我,识别由其他计算机生成的函数还不可能做到,我会非常震惊。这种技术肯定会实现,只是时间问题。我不知道是谁会去做,也许我该试试?尽管我对 [听不清] 一无所知,但我觉得这是一个非常值得投入的项目。最终,这会让我们在处理未知程序时节省大量时间。我们可能需要针对不同语言开发专门的 AI,比如针对 C、C++ 或 Go 的 AI。但我确信这是我们的未来,而且可能就在不远的将来。希望这类工具不会卖得太贵,因为我真的很想用。

Natalie Pistunovich: 那么,关于代码生成,有些语言比其他语言更适合 AI,比如 Go 的表现甚至比 Python 更好。这是因为 Go 内置了 linter(代码规范检查器),并且语言特性非常严格,比如强制使用缩进、花括号和换行。而 Python 和很多其他语言的写法则非常自由,导致 AI 看到的数据集会更加多样化,甚至可能生成不一致的代码。有时同一个文件里会有两种不同的写法,甚至在最佳情况下也可能出错。那么对于逆向工程来说,你觉得这种优势是否还会继续存在?从 AI 的角度来看。

Ivan Kwiatkowski: 既是,也不是。你提到的这种语言的严格性确实让 AI 更容易理解代码的意义并生成代码。我认为这很有道理。比如 Python,本身就是一种极其模糊的语言,对吧?当然,它还没有 JavaScript 或 PHP 那么模糊……在我看来,JavaScript 和 PHP 根本不能算作真正的编程语言,因为它们完全不严谨。比如,当你使用鸭子类型(duck typing),甚至根本不给变量定义类型时,AI 想搞清楚发生了什么就会变得非常困难,在很多情况下可能根本做不到……因为我认为许多东西是在运行时才被确定的,而这是 AI 至少在短期内无法做到的。

另一方面,当你处理汇编代码时,情况就完全不同了。汇编代码是一种非常严格、唯一的语言,这可能是所有 AI 都必须处理的语言。我不太确定 AI 将如何从汇编代码中识别函数,或者生成对应的高级代码。

但对 AI 来说,汇编代码的一个好处是,它完全没有模糊性。可以说,模糊性在一端,而汇编代码则完全站在了另一端。它是 100% 精确的。

Natalie Pistunovich: 是的,汇编代码是最一致的语言。

Ivan Kwiatkowski: 没错,非常一致。而且它实际上以某种方式完成了任务,但只是一系列非常简单的操作,每个操作都是以非常明确的方式执行单一任务。所以在这方面,我认为这对 AI 来说将是非常有利的,只要它们准备好了。

Natalie Pistunovich: 现在已经有一些很不错的工具,可以将二进制代码直接翻译成汇编代码,虽然不能做到 100%,但覆盖率已经很高了。汇编代码本身足够一致,这意味着可能已经有人开发了一个 IDA Pro 的插件,利用 AI 将汇编代码翻译成 Go 代码。

Ivan Kwiatkowski: 这是个好问题,因为你会觉得应该有人在研究这个……但当你观察逆向工程工具的市场时,会发现这个市场其实很小。比如 Hex Rays(IDA Pro 的开发商),是一家有些“老派”的公司。他们的产品很出色,但过去 20 年里几乎没有试图做出任何颠覆性创新。当然,他们对产品做了很多改进,但我觉得部分原因是因为受到了 Ghidra(一个开源竞争者)的挑战。如果没有 Ghidra 的出现,我认为 IDA Pro 可能会在未来十年里保持现状,几乎没有显著变化,因为他们没有竞争对手,也就没有改进的动力。

目前,编译器的工作原理完全是通过算法实现的,并没有使用任何形式的机器学习。他们的反编译过程没有应用 AI。也许他们已经开始研究了,但 Hex Rays 给我的印象---或许我完全错了,因为我不在那里工作---是他们并不是一家专注于突破性研发的公司。不太像是会为下一代反编译器做准备的那种公司。我觉得他们更倾向于对现有产品做些渐进式的改进,每年稍微优化一点。

Ghidra 也有反编译器,它是开源的,我觉得它的表现也不错……但我认为它也没有使用任何形式的 AI。我也没听说他们有计划开始研究这方面的技术。开发一种专注于逆向工程的 AI 产品可能需要非常专业的 AI 知识。而我的感觉是……虽然我不认识安全领域的所有人,但我的印象是,这个领域的人通常非常专注,非常有才华,技能也很强,但他们的专长往往集中在网络安全的某个特定领域。我很少见到既是优秀研究员或者逆向工程师,又是非常合格的数据科学家,能够调优机器学习算法并彻底改变我们的生活。也许某些地方有人在研究这个,但如果真是这样,我并不知道。

如果这个项目是公开的,或者已经发布了,我想我应该会听说。但我仍然希望某家公司,比如在以色列的某个秘密实验室里正在研究这个项目,最终会横空出世,彻底改变市场,让我的工作变得更轻松。

Natalie Pistunovich: 是的,这确实是另一个可以给观众的灵感。除了开发一个能把汇编翻译成 Go 的 AI,我个人还对一个能判断“这个恶意软件是谁写的”的 AI 很感兴趣。我的意思是,现在你已经可以用一些工具,比如 Codex,告诉它“写一个 Go 程序,功能包括这个、那个和另外一个,并以某某的风格来写。”如果你提到某个 GitHub 用户的名字,而这个用户是个知名开发者,有很多星标,或者其他很大的影响力,他们写的代码有一种独特的风格---虽然在 Go 中可能不那么常见,因为 Go 本身很规范---你就能得到他们风格的代码。所以,下一步的魔法插件可以是“这个恶意软件是以谁的风格写的?”这会很有趣。然后那些编程教师就会说,“哦,这个黑客是我教出来的。”

Ivan Kwiatkowski: 这是个很棒的想法。其实,这种想法也许不是传言,而是一些公开的研究项目,这些项目可能在几十年前就已经在情报领域开始了。我记得在 2010 年或 2012 年的某次 CCC(Chaos Communication Congress)会议上,有人提到过类似的研究。他们基本上从开源代码仓库中提取代码,比如下载整个 GitHub,试图分析每个开发者的写作特点,希望未来能够对任何程序进行分析,并推测出“这个可执行文件可能是某个开发者写的。”

现在已经过去 10 到 15 年了……我不确定这个研究是否成功了。我很久没听到这方面的消息了,所以可能它并不像预期的那样有效,或者它其实已经被某些情报机构吸收了……因为这种能力显然在情报应用中有非常大的价值。情报机构可以用它来识别恶意软件的作者,而不需要像警方那样拿出确凿的证据……他们只需要知道某个恶意软件背后是谁,然后用他们常规的手段去“平行构建”证据。

我们知道他们需要这种能力,也知道他们为此投入了资金。我记得一些大学也在研究类似的项目……虽然是为了研究目的,而不是情报应用。但在这些领域,研究和情报总是存在某种交流,尤其是当研究有实际应用时。

如果这项研究取得了成果,那一定是以秘密的方式实现的。以前的研究方式本质上是算法驱动的,他们试图提取代码的特征,而没有使用我们现在所拥有的黑箱式 AI 技术。也许这是这些应用的新研究方向。也许我们未来会知道答案。但据我所知,这仍然是一个现存的问题,人们正在努力解决。我没有看到任何迹象表明这个问题已经被解决了,尽管我也无法确定自己一定会知道。

Natalie Pistunovich: 这一期播客的讨论非常有趣,涉及了很多新工具和项目的创意。记住,你们是第一次从这里听到这些想法的。

Ivan Kwiatkowski: 是的,我完全没想到会聊到这些话题呢。

Natalie Pistunovich: 是啊,这真的很棒。和你聊天很有启发性,Ivan。谢谢你抽出这一小时来和我们交流。

Ivan Kwiatkowski: 当然不客气。

Natalie Pistunovich: 就像上一期一样,这次聊完后我还是有很多悬而未决的问题……希望明年你能再加入我录一期,或者十期也可以。[笑]

Ivan Kwiatkowski: 好啊,当然可以。最终---而且我感觉这可能很快就会发生---我们会扩展我关于 Go 语言的所有知识,对吧?如果我再回来,那到时候我真的需要更深入研究这门语言,也许还要带来一些实际的研究成果和你分享……因为不然的话---我不希望这次对话变无聊,但我会尽力的。

Natalie Pistunovich: 好的,那下次我们可以聊聊泛型……看看 Go 提案的进展如何。

Ivan Kwiatkowski: 我会好好准备的。

Natalie Pistunovich: 对了,这次我们没有聊不受欢迎的观点,但我们提出了两个“独角兽点子”……所以,大家不客气啦。 [笑]

Ivan Kwiatkowski: 我几乎可以向你们个人保证,如果听众中有人真的实现了我们提到的其中一个点子,那他们会变得非常富有。所以,就这样吧。

Natalie Pistunovich: 这几乎和提出最不受欢迎的观点一样有分量了。 [笑] 谢谢大家的收听。非常感谢你,Ivan。

Ivan Kwiatkowski: 谢谢你,我很高兴能参与这次节目,和你聊天很愉快。我们下次见。


好文收藏
38 声望6 粉丝

好文收集