Go(又称Golang)是Google于2007年设计并于2012年对外开放的开源编程语言,多年来深受开发者欢迎,但它并不总是用于“诚信”用途。正如经常发生的那样,它也引起了恶意软件开发人员的注意。使用Go语言对于恶意软件开发人员来说是一个很有吸引力的选择,因为它支持交叉编译,也就是说,用Go编写的代码可以编译成运行在不同操作系统上的二进制文件。让攻击者的生活更轻松不是很好吗,因为他们不必为每个目标环境开发和维护单独的代码库。逆向Go二进制程序的必要性由于Go编程语言的某些特性,逆向工程师在处理Go二进制文件时通常会遇到很多阻力。虽然目前的逆向工程工具(如反汇编器)可以分析用非常流行的语言(如C、C++、...这导致更大的二进制文件头,使攻击者更难分发恶意软件。另一方面,一些安全产品也存在大文件问题。这意味着大二进制文件可以帮助恶意软件避免检测。静态链接二进制文件的另一个好处是攻击者的最大优点是恶意软件可以直接在目标系统上运行而不会遇到依赖性问题。当我们看到用Go编写的恶意软件持续增长并预计会出现更多恶意软件家族时,我们决定更深入地研究Go编程语言并增强我们的工具集更有效地调查Go恶意软件。在本文中,我讨论了逆向工程师在分析时面临的两个挑战Go二进制文件及其相应的解决方案。Ghidra是美国国家安全局开发的开源逆向分析工具。我们经常用它来对恶意软件进行静态分析。我们可以为Ghidra创建自定义脚本和插件,以按需实现特定功能。在这里,我们将利用Ghidra的这一特性,通过创建自定义脚本来帮助我们分析Go二进制程序。本文讨论的主题在Hacktivity2020在线会议上进行了展示,相关幻灯片和其他资料可以在我们的Github存储库中下载。剥离的二进制文件中缺少函数名称实际上,我们面临的第一个问题并不是Go二进制文件特有的,而是所有剥离的二进制代码面临的一个常见问题)。事实上,编译后的可执行文件可以包含调试符号,这使得调试和分析更加容易。当分析师用调试信息对二进制代码进行逆向工程时,他们不仅可以看到内存地址,还可以看到函数和变量名。然而,恶意软件作者经常在编译他们的代码时去除这些调试信息,创建所谓的去除二进制文件。他们这样做有两个目的,一是减小文件大小,二是增加逆向分析的难度。使用剥离的二进制文件时,分析人员不能依赖函数名称来帮助他们在代码中找到感兴趣的函数。在处理静态链接的Go二进制文件(包含所有必需的库)时,逆向过程会大大减慢。为了说明这一点,我们将用C和Go编写一个简单的“HelloHacktivity”示例代码,并将它们编译成剥离的二进制文件。在这里,请注意两个可执行文件之间的大小差异。Ghidra的函数窗口列出了二进制文件中定义的所有函数。在非剥离构建中,显示函数名称,这对逆向工程师非常有帮助。图1hello_c函数列表图2hello_go函数列表对于剥离后的二进制文件,其函数列表如下:图3hello_c_strip函数列表图4hello_go_strip函数列表简单G0程序的二进制代码如“world”也非常庞大:它们包含一千多个函数。在剥离二进制版本中,逆向工程师不能依赖函数名进行辅助分析。注意:由于调试信息的剥离,不仅函数名没有了,而且Ghidra只识别1790个函数中的1139个。如果有一种方法可以在剥离的二进制文件中恢复函数名称,我们很感兴趣。首先,我们运行了一个简单的字符串搜索来检查函数名称是否仍然存在于二进制文件中。在C示例中,我们找到函数“main”,而在Go示例中,我们找到“main.main”。图5在hello_c中可以找到字符串“main”图6在hello_c_strip中找不到字符串“main”图7在hello_go中可以找到字符串“main.main”图8在hello_go_strip中可以找到字符foundThestring"main.main"我们可以看到虽然strings工具在C语言的剥离后的二进制文件中找不到函数名,但是我们可以在Go语言的剥离后的二进制文件中找到字符串"main.main".这一发现给了我们一线希望,即可以在剥离的Go二进制文件中恢复函数名称。事实上,将二进制文件加载到Ghidra并搜索字符串“main.main”将准确显示它的位置。如下图所示,函数名字符串位于.gopclntab段。图9Ghidra显示的hello_go_strip的main.main字符串众所周知,从Go1.2开始,提供了pclntab结构,并提供了详细的文档说明。该结构以一个魔法值开始,接着是体系结构信息,然后是一个函数符号表,用于存储二进制代码中的函数信息。每个函数的入口点地址后跟一个函数元数据表。在函数元数据表中,除了其他重要信息外,还存储了函数名称的偏移量。也就是说,我们可以利用这些信息来恢复函数名。为此,我们的团队为Ghidra创建了一个脚本(go_func.py),通过执行以下步骤恢复剥离的GoELF文件中的函数名称:找到pclntab结构提取函数地址找到函数名称偏移量执行我们的After脚本,不仅可以恢复函数名,还可以定义以前无法识别的函数。图10执行go_func.py脚本后的hello_go_strip函数列表接下来,我们将以一个真实的样本(eCh0raix勒索软件)为例来展示脚本的强大:图11eCh0raix的函数列表图12执行go_func.py脚本后eCh0raix的函数列表这个例子展示了函数名恢复脚本在逆向工程中的巨大帮助:安全分析人员只要看一眼函数名就可以判断他们正在处理一个勒索软件。注意:在WindowsGo二进制文件中,没有专门用于pclntab结构的部分,因此我们需要明确搜索该结构的相关字段(例如魔法值、可能的字段值)。对于macOS系统,_gopclntab部分可用,类似于Linux二进制文件中的.gopclntab部分。挑战:未定义的函数名称字符串如果Ghidra未定义函数名称字符串,那么函数名称恢复脚本将无法重命名该特定函数,因为它无法在给定位置找到函数名称字符串。为了解决这个问题,我们的脚本总是检查函数名称地址是否具有已定义的数据类型,如果没有,则在重命名函数之前尝试在给定地址处定义字符串数据类型。在下面的示例中,函数名字符串“log.New”在eCh0raix勒索软件样本中没有定义,因此如果不提前创建该字符串,则无法重命名相应的函数。图13eCh0raix中的log.New函数名是undefined图14eCh0raix中的log.New函数不能重命名在我们的脚本中,下面几行代码是专门用来解决这个问题的:Strings第二个问题我们的脚本地址与Go二进制文件中的字符串有关。让我们回到“HelloHacktivity”示例并查看Ghidra中定义的字符串。C语言编译的二进制代码中定义了70个字符串,其中“Hello,Hacktivity!”与此同时,Go二进制文件包含6,540个字符串,但搜索字符串“hacktivity”时一无所获。字符串太多,逆向工程师很难通过肉眼找到相关的字符串,然而,我们期望找到的字符串甚至没有被Ghidra识别。图16hello_c中定义的字符串包含“Hello,Hacktivity!”图17hello_go中定义的字符串不包含“hacktivity”要理解这是怎么回事,你需要知道Go语言是如何处理字符串的。在像C这样的编程语言中,字符串是以null结尾的字符序列;在Go中,字符串被视为具有固定长度的字节序列。也就是说,对于Go语言来说,字符串是一种特殊的数据结构,由一个指向字符串所在位置的指针和一个整数(即字符串的长度)组成。在Go二进制文件中,这些字符串将存储为大字符串blob,它们由多个字符串连接而成,字符串之间没有空字符。因此,搜索“Hacktivity”会产生C语言版本的二进制文件的预期结果,并返回包含Go语言版本的二进制文件的“hacktivity”的巨大字符串blob。图18在hello_c中搜索“Hacktivity”字符串图19在hello_go中搜索字符“hacktivity”由于Go语言中字符串的定义与其他语言不同,在汇编代码中引用它们的结果也与通常的C类似不同语言的解决方案不同,因此Ghidra在处理Go二进制文件中的字符串时会面临更大的困难。字符串结构的分配方式有很多种,可以静态创建,也可以运行时动态创建;同时,在不同的架构中,具体的分配方式也不同,甚至可能在同一个架构中存在多种方案。为了解决这个问题,我们的团队创建了两个脚本来帮助识别字符串。总结在本文中,我讨论了逆向工程师在分析Go二进制代码过程中面临的两个问题及其解决方案。由于文章篇幅,我们将分两部分进行介绍。更多精彩内容,我们将在下篇介绍。本文翻译自:https://cujo.com/reverse-engineering-go-binaries-with-ghidra/如有转载请注明原文地址:
