kavin

利用Ghidra逆向分析Go二进制程序(上篇)

kavin 安全防护 2022-11-24 428浏览 0

利用Ghidra逆向分析Go二进制程序(上篇)

Go(又称Golang)是Google公司于2007年设计的一种开源编程语言,并于2012年向公众开放。多年来,它在开发者中广受欢迎,但它并不总是被用于“善意”的用途。正如经常发生的那样,它也吸引了恶意软件开发者的注意。

对于恶意软件开发者来说,使用Go语言是一个诱人的选择,因为它支持交叉编译,也就是说,可以把Go语言编写的代码编译成在不同操作系统上运行的二进制文件。这样的话,就能够让攻击者的生活变得更加轻松,因为他们不必为每个目标环境开发和维护不同的代码库了,岂不快哉。

对Go二进制程序进行逆向分析的必要性

由于Go编程语言的某些特性的原因,逆向工程师在处理Go二进制文件时通常会遇到许多阻力。尽管目前的逆向分析工具(例如反汇编器)可以很好地分析非常流行的语言(例如C、C++、.NET)编写的二进制文件,但是Go语言却带来了新的挑战,使得分析工作变得更加繁琐。

Go二进制文件通常是静态链接的,这意味着所有必要的库都包含在编译后的二进制文件中。这会导致二进制文件的块头变大,从而使得恶意软件的分发对攻击者来说更加困难。另一方面,一些安全产品在处理大文件时也存在问题。这意味着大型二进制文件可以帮助恶意软件避开检测。静态链接的二进制文件对攻击者的另一个好处是,恶意软件可以直接在目标系统上运行,而不会遇到依赖问题。

当我们看到用Go编写的恶意软件持续增长,并预计会出现更多的恶意软件家族时,我们决定更深入地研究Go编程语言,并增强我们的工具集,以便更有效地调查Go恶意软件。

在本文中,我将讨论逆向工程师在分析Go二进制代码的过程中所面临的两个难题,以及相应的解决方案。

Ghidra是美国国家安全局开发的一个开源逆向分析工具,我们经常使用它来进行恶意软件的静态分析。我们可以为Ghidra创建自定义脚本和插件,以按需实现特定的功能。在这里,我们将利用Ghidra的这个特性,通过创建自定义的脚本来帮助我们分析Go二进制程序。

本文讨论的主题是在Hacktivity2020在线会议上公布的,相关的幻灯片和其他材料可以在我们的Github存储库中下载。

剥离型二进制代码中丢失的函数名

实际上,我们面对的第一个问题并不是Go二进制文件所特有的,而是所有剥离型二进制代码(stripped binaries,译者注:就是去掉调试信息后的二进制代码)所共同面对的一个问题。实际上,编译后的可执行文件是可以包含调试符号的,这能让调试和分析工作变得更加容易。当分析人员逆向分析带有调试信息的二进制代码时,他们不仅可以看到内存地址,还可以看到函数和变量的名称。然而,恶意软件作者通常在编译代码时剥离这些调试信息,从而创建所谓的剥离型二进制代码。他们这样做的目的有两个,一是为了减小文件的大小,二是增加逆向分析的难度。在使用剥离型二进制文件时,分析人员无法依赖函数名来帮助他们在代码中找到自己感兴趣的函数。在处理使用静态链接的Go二进制文件(其中包含所有必需的库)时,逆向分析的过程会显著减慢。

为了说明这个问题,我们将分别通过C语言和Go语言编写一个简单的“Hello Hacktivity”示例代码,并将它们编译成剥离型的二进制代码。在这里,请大家注意两个可执行文件在大小方面的差异。

利用Ghidra逆向分析Go二进制程序(上篇)

Ghidra的Functions窗口列出了二进制文件中已经定义的所有函数。在非剥离型的编译版本中,函数名称都会显示出来,这对逆向工程师来说具有很大的帮助。

利用Ghidra逆向分析Go二进制程序(上篇)

图1 hello_c的函数列表

利用Ghidra逆向分析Go二进制程序(上篇)

图2 hello_go的函数列表

对于剥离型的二进制文件来说,其函数列表如下所示:

利用Ghidra逆向分析Go二进制程序(上篇)

图3 hello_c_strip的函数列表

利用Ghidra逆向分析Go二进制程序(上篇)

图4 hello_go_strip的函数列表

这些例子清楚地表明,即使像“hello world”这样简单的G0程序的二进制代码,它们的体积也是非常庞大的:竟然含有一千多个函数。而在剥离型的二进制版本中,逆向工程师则无法依靠函数名来进行辅助分析。

注:由于剥离了调试信息,不仅函数名消失了,Ghidra也只能识别出1790个函数中的1139个。

我们感兴趣的是,是否有办法恢复剥离型二进制文件中的函数名。首先,我们运行了一个简单的字符串搜索来检查二进制文件中是否还有函数名。在C语言的例子中,我们找到了函数“main”,而在Go语言的例子中找到的则是“main.main”。

利用Ghidra逆向分析Go二进制程序(上篇)

图5 在hello_c中,可以找到字符串“main”

利用Ghidra逆向分析Go二进制程序(上篇)

图6 在hello_c_strip中,无法找到字符串“main”

利用Ghidra逆向分析Go二进制程序(上篇)

图7 在hello_go中,可以找到字符串“main.main”

利用Ghidra逆向分析Go二进制程序(上篇)

图8 在hello_go_strip中,可以找到字符串“main.main”

我们可以看到,虽然strings工具无法在C语言的剥离型二进制文件中找到函数名,但是,我们却可以在Go语言的剥离型二进制文件中找到字符串“main.main”。这个发现给我们带来了一丝希望,即在剥离型的Go二进制文件中可以恢复函数名。

实际上,将二进制文件加载到Ghidra中,然后搜索“main.main”字符串,就可以看到它的确切位置。如下图所示,函数名字符串位于.gopclntab段。

利用Ghidra逆向分析Go二进制程序(上篇)

图9 Ghidra显示的hello_go_strip的main.main字符串

众所周知,从Go 1.2开始,就开始提供pclntab结构体了,并且提供了详尽的说明文档。该结构体以一个魔力值开头,后面是架构信息,再往后,是函数符号表,用于保存二进制代码中的函数信息,每个函数的入口点地址后面是函数元数据表。

利用Ghidra逆向分析Go二进制程序(上篇)

在函数元数据表中,除其他重要信息外,还存储了函数名称的偏移量。

利用Ghidra逆向分析Go二进制程序(上篇)

也就是说,我们可以通过这些信息来恢复函数名。为此,我们的团队为Ghidra创建了一个脚本(go_func.py),通过执行以下步骤来恢复剥离型Go ELF文件中的函数名:

  • 找到pclntab结构体
  • 提取函数地址
  • 查找函数名偏移量

执行我们的脚本后,不仅可以恢复函数名,而且还可以定义以前未被识别的函数。

利用Ghidra逆向分析Go二进制程序(上篇)

图10 执行go_func.py脚本后的hello_go_strip的函数列表

接下来,我们将以真实世界中的样本(eCh0raix勒索软件)为例,来展示该脚本的威力:

利用Ghidra逆向分析Go二进制程序(上篇)

图11 eCh0raix的函数列表

利用Ghidra逆向分析Go二进制程序(上篇)

图12 执行go_func.py脚本后eCh0raix的函数列表

这个例子展示了函数名恢复脚本在逆向工程中所带来的巨大帮助:安全分析师只需瞄一眼函数名,就可以判断出当前处理的是一个勒索软件。

注意:在Windows Go二进制文件中,并没有专门为pclntab结构体提供相应的段,因此,研究人员需要显式地搜索该结构体的相关字段(如魔力值、可能的字段值)。对于macOS系统来说,_gopclntab段是可用的,类似于Linux二进制文件中的.gopclntab段。

挑战:未定义的函数名字符串

如果一个函数名字符串没有被Ghidra定义,那么函数名恢复脚本将无法重命名该特定函数,因为它无法在给定位置找到函数名字符串。为了解决这个问题,我们的脚本总是检查函数名地址是否有定义的数据类型,如果没有,则尝试在重命名函数之前在给定的地址定义一个字符串数据类型。

在下面的例子中,eCh0raix勒索软件样本中并没有定义函数名字符串“log.New”,所以在没有事先创建字符串的情况下,是无法重命名相应的函数的。

利用Ghidra逆向分析Go二进制程序(上篇)

图13 eCh0raix中log.New的函数名未定义

利用Ghidra逆向分析Go二进制程序(上篇)

图14 eCh0raix中log.New函数无法重命名

在我们的脚本中,以下几行代码专门用于解决这个问题:

利用Ghidra逆向分析Go二进制程序(上篇)

图15 go_func.py

Go二进制文件中无法识别的字符串

我们的脚本要解决的第二个问题与Go二进制文件内的字符串有关。让我们回到“Hello Hacktivity”的例子,看看Ghidra内定义的字符串。

在C语言编译而成的二进制代码中定义了70个字符串,“Hello, Hacktivity!”就在其中。同时,Go语言版本的二进制代码中则包含了6,540个字符串,但搜索 “hacktivity”字符串却没有任何结果。如此多的字符串已经让逆向工程师很难找靠肉眼到相关的字符串,但是,我们期望找到的字符串甚至没有被Ghidra识别出来。

利用Ghidra逆向分析Go二进制程序(上篇)

图16 hello_c中定义的字符串中含有“Hello, Hacktivity!”

利用Ghidra逆向分析Go二进制程序(上篇)

图17 hello_go中定义的字符串中未含有“hacktivity”

要理解这是怎么回事,您需要知道Go语言处理字符串的方式。在类似C这样的编程语言中,字符串是以空字符结尾的字符序列;而在Go语言中,字符串则被视为具有固定长度的字节序列。也就是说,对于Go语言来说,字符串是一种特殊的数据结构,由指向字符串位置的指针和整数(即字符串的长度)构成。

利用Ghidra逆向分析Go二进制程序(上篇)

在Go二进制文件中,这些字符串将以大字符串blob的形式存储,而blob则是由多个字符串串联而组成的,并且字符串之间没有空字符。因此,在搜索“Hacktivity”时,对于C语言版本的二进制代码来说,能够得到预期的结果;对于Go语言版本的二进制代码来说,则会返回一个包含“hacktivity”的巨型字符串blob。

利用Ghidra逆向分析Go二进制程序(上篇)

图18 在hello_c中搜索“Hacktivity”字符串

利用Ghidra逆向分析Go二进制程序(上篇)

图19 在hello_go中串搜索字符“hacktivity”

由于Go语言对字符串的定义不同于其他语言,并且在汇编代码中引用它们的结果也与通常的类似C语言的解决方案不同,因此Ghidra在处理Go二进制文件中的字符串方面,会面临较大的困难。

字符串结构的分配方式有很多种,它既可以是静态创建的,也可以是运行时动态创建的;同时,在不同的架构中,具体的分配方式也是不同的,甚至在同一架构中可能存在多种解决方案。为了解决这个问题,我们团队创建了两个脚本来帮助识别字符串。

小结

在本文中,我讨论了逆向工程师在分析Go二进制代码的过程中所面临的两个难题及其解决方案,由于篇幅较长,我们将分为两篇进行介绍。更多精彩内容,我们将在下篇中进行介绍。

本文翻

继续浏览有关 安全 的文章
发表评论