无符号Golang程序逆向方法解析

栏目: Go · 发布时间: 5年前

内容简介:在去年的inctf2018中,出现了一道Go语言编写的进程通信逆向题,无论是从题目整体设计还是解题思路上来说都独树一帜,自己在解题过程中遇到了很多问题,但我这不打算做过多探讨,网上也有大佬的解题过程,本文仅针对该题涉及到的无符号Go语言恢复信息问题进行详细讨论。在整个后期整理过程中,自己参考了很多资料,现放出所有链接,下文中也会有对应的说明。

无符号Golang程序逆向方法解析

在去年的inctf2018中,出现了一道 Go 语言编写的进程通信逆向题,无论是从题目整体设计还是解题思路上来说都独树一帜,自己在解题过程中遇到了很多问题,但我这不打算做过多探讨,网上也有大佬的解题过程,本文仅针对该题涉及到的无符号Go语言恢复信息问题进行详细讨论。

前言

在整个后期整理过程中,自己参考了很多资料,现放出所有链接,下文中也会有对应的说明。

初步分析

首先用file命令简单查看下文件类型,发现是64位的,由题目名也能猜出是和go语言有关的逆向题,但是发现虽然是动态链接的,但是无符号,这是比较坑的,心中有点小怕。

$ file ../ultimateGOal
../ultimateGOal: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, stripped

那么用ida打开之后,由于没有符号的原因,我们并不能找到主函数的位置,不熟悉go语言逆向的同学可能会不清楚go语言逆向的入口,那么简单说明一下。

简单demo测试

对下面这个简单的go语言例子而言,我们在进行编译go程序的时候,会生成可执行文件,而go程序需要满足2个条件:

  1. go程序中需要包含main包
  2. 在main包中还必须包含main函数
package main
import  "fmt"  
func main() {
  fmt.Println("Hello World!")
}

也就是说go语言的入口点是 main.main (真正的入口点),即是main包下的main函数。对这个demo,我们编译之后用ida打开查看,搜索主函数,当确定了主函数入口时,尝试反编译代码,缺发现失败,如下图所示:

无符号Golang程序逆向方法解析

给出的提示是 Size of 'int' is 8 (4 expected) ,对于这种错误,通过选项中的编译设置,修改整型的字长大小即可解决问题,如下图所示。

无符号Golang程序逆向方法解析

得到如下的代码:

void __cdecl main_main()
{
  __int64 v0; // rdx
  error_0 v1; // di
  __interface_{} ST00_24_2; // ST00_24
  __interface_{} v3; // [rsp+30h] [rbp-18h]
  void *retaddr; // [rsp+48h] [rbp+0h]

  while ( (unsigned __int64)&retaddr <= *(_QWORD *)(__readfsqword(0xFFFFFFF8) + 16) )
    runtime_morestack_noctxt();
  v3.array = (__DIE_10518_interface_{} *)&r3;
  v3.len = (__int64)&main_statictmp_0;
  ST00_24_2.array = (__DIE_10518_interface_{} *)&v3;
  ST00_24_2.len = 1LL;
  ST00_24_2.cap = 1LL;
  fmt_Println(ST00_24_2, v1, v0);
}

对于ida反编译出的go语言代码,可读性较差,类型转换较多,结构体较复杂,异常处理比较冗长,对于 main_statictmp_0 这个结构体,在go语言中包括了2部分,分别是字符串偏移量和字符串长度,这个结构体所指向的是 string <offset unk_4B317B, 0Dh> ,指出了字符串的偏移量和长度,长度是0xd(即”Hello World!”的长度),另外go语言中所有字符串都是放在一起的,所以偏移量和长度是很关键的,如下图所示:

无符号Golang程序逆向方法解析

解决无符号的问题

对于上文那个demo而言,是有符号的程序,逆向起来有信息可以参考,大大加快了逆向的速度,但是对这道题而言,是无符号的,无符号的程序逆向起来由于不知道函数的功能,会特别饶,很容易把人绕进去。如下图所示:

无符号Golang程序逆向方法解析

当然也有人能纯静态分析出来,比如奈沙夜影师傅,参考最上面贴出来的链接,使用动态调试结合黑盒测试的方法Orz。师傅也自嘲道: Go语言的逆向感觉目前没啥方便的工具,只能硬怼汇编,缺少符号的情况下还是有点麻烦的,等一个师傅们的指教,这个题目我是纯黑盒调试出来的 。针对这个问题,自己参考了几篇博文,收集了一些方法,供参考。

从签名角度出发

众所周知,签名是对函数、第三方库很好的检测方式,通过签名,ida很容易能分析出函数的名称,从而大概知道函数的作用。由于没有官方的签名库,所以这需要自己制作,参考了 diffway@兰云科技银河实验室 的方法,论述如下:

idb2pat.py+sigmake制作签名

idb2pat.py是火眼公司 FireEye Labs Advanced Reverse Engineering 团队编写的脚本,代码在GitHub上开源,该脚本主要通过CRC16的方式来计算每个函数块的特性,从而来识别不同的函数。这点也和IDA官方对签名文件的说明相符合,参见 IDA F.L.I.R.T. Technology: In-Depth

无符号Golang程序逆向方法解析

通过生成pat文件后,再使用ida SDK中提供的sigmake工具来生成相应的sig签名文件,将其复制到ida安装目录下的 sigpc 目录,然后我们在ida中就可以载入签名进行识别。在2088个函数中,识别出1271个函数,但是不是每个识别出的函数都能有效的命名,所以实际识别出的函数个数也就600左右,识别率较低,而且只能识别出被制作签名的程序所带的包,如果另一个程序使用了其他的包,那将无法识别,拓展性较低。

无符号Golang程序逆向方法解析

bindiff或者diaphora对比

通过bindiff或者diaphora来对比不同是ida数据库,以获取函数的特征也是种很好的方法,这种方法在平时分析静态链接的程序也很有用。但是存在的问题也很明显,由于diaphora是 python 编写的,所以运行速度是肉眼可见的慢,对于1000数量级的函数保存到数据库中竟然也要3分钟左右,保存完之后,再分析对比的时候,需要更多时间,效率很低。当数据完成对比之后,我们得到情况如下图所示:

无符号Golang程序逆向方法解析

主要分为五栏:完全匹配、部分匹配、只在第一个数据库出现的、只在第二个数据库出现的以及不可靠匹配的。对我们来说,我们只需要关注匹配率高的即可,所以我们首选对完全匹配中的函数进行重命名,方法很简单,就是选中所有的完全匹配的函数,然后右键导入即可。

该方法的主要问题是速度太慢,不管是在前期初始化数据库的时候,还是后面重命名函数的时候,非常卡顿。其次是对函数类别没有很好的区分度,如下图所示,同样都是运行时函数,对函数类别处理不好,也不能对主函数进行区别,和第一种方式一样,识别率不高,只能识别出被制作签名的程序所带的部分库函数。

无符号Golang程序逆向方法解析

Rizzo插件生成数据库识别

关于rizzo,可以参看GitHub上的介绍 Rizzo ,同样也是一种对ida数据库进行保存然后提取信息进行对比的工具,收录于devttys0的ida脚本目录中。自己也进行了测试,速度还可以。

Building Rizzo signatures, this may take a few minutes...
Generated 1314 formal signatures and 844 fuzzy signatures for 1784 functions in 10.64 seconds.
Saving signatures to C:UsersxxxxxxDesktop11111.riz... done.
Built signatures in 12.30 seconds

那么在保存完之后,载入riz文件进行测试,如下图所示。基本识别情况和上面2种方式差不多,也存在对原始文件的局部依赖性,所不同的是,这种方式不会误识别,而前面2种方式会产生很多未知的函数命名情况,歧义性较低。猜测是前面2种方法对模糊测试效果更好,第三种方式属于保守型的测试方法,会将极大概率的函数进行重命名,而较低的大概率函数则不会通过。

无符号Golang程序逆向方法解析

从Golang特性出发

上面的3种方法虽然能对部分函数进行识别,但是效果一半,前2种方法识别率大概有50%左右,第3种只有20%左右。且三者均不能对签名生成程序中没有的函数进行识别,也就是说连每个程序中的主函数都无法定位,因为每个程序中的主函数均不一致。下面将从Go语言的特性来解决这个问题。

GolangAssist脚本

在网上有一篇著名的go语言逆向解析博文,安全客中也有人提供了翻译,链接参见前言部分。该作者对golang的编译原理有着较深的理解,同时其提供了 golang_loader_assist.py 脚本用于还原符号信息,这对逆向而言真是再好也不为过了。但是这个脚本无法恢复Windows下编译的go程序,因为这个脚本中最重要的部分,是找到go语言程序中一个非常重要的段,叫做 .gopclntab ,在这个段中保存了函数的实际名称,而在Windows下编译的程序中则不存在这个段。

在这个脚本中最终的部分如下图所示:

无符号Golang程序逆向方法解析

首先通过 get_segm_by_name('.gopclntab') 来定位到 .gopclntab 段的首地址。然后寻找偏移量是8的地方,根据程序字长来创建指针,再接下来一个字长则给出了 .gopclntab 段的大小,这样我们就能开始逐个处理每条数据,对每条数据而言,其中都包含了一个函数指针和一个函数名字符串偏移量,循环处理这些数据就能对所有的函数名进行还原,得到带符号信息的函数名称。

无符号Golang程序逆向方法解析

在实际使用这个脚本进行测试的过程中,需要注意的是,由于ida7.0对sdk和api的大量更新和重新,在idapython中的创建字符串函数MakeStr出错,主要原因是函数的重复定义,参看前言中看雪论坛的相关讨论,修改方式如上图所示。

自己将最关键的部分代码进行了分析和抽取,脚本如上,这段代码可直接在ida中运行,用以定位函数名偏移量和修改函数名,但是注意最后这个地方函数名修改会有点问题,原因是函数名中除了下划线不能出现其他字符,但是当我们运行完毕后,很多函数名是存在特殊符号的,需要自己过滤。

gopclntab = get_segm_by_name('.gopclntab')
if gopclntab is not None:
    base = gopclntab.startEA + 8
    # 计算函数名个数
    count = Dword(base)
    # 基地址
    base += 8
    for i in xrange(0, 2 * count, 2):
        # 创建函数指针
        MakeQword(base + 8*i)
        functionAddr = Qword(base + 8*i)
        # 创建函数名字符串偏移量(相对于gopclntab基地址而言)
        MakeQword(base + 8*i + 8)
        offset = Qword(base + 8*i + 8)
        offset = Dword(offset + gopclntab.startEA + 8)
        # 函数名字符串偏移量(文件偏移量FOA)
        functionName = offset + gopclntab.startEA
        name = GetString(functionName)
        # 创建字符串
        MakeStr(functionName, functionName + len(name))
        # 修改函数名(ida禁止函数名出现特殊符号,需过滤后才能达到100%效果,我这里没有过滤)
        MakeName(functionAddr, name)

运行结果如下图所示:

无符号Golang程序逆向方法解析

通过分析go语言特有的 .gopclntab 段,我们可以恢复调试符号信息,只有该段中保存的信息均可以进行恢复,恢复率达到98%以上。

IDAGolangHelper脚本

刚刚讨论完了GolangAssist,效果是非常不错的,而作为GolangAssist的升级版本,IDAGolangHelper做的则更加完善,该脚本的作者在2016年底的zeronights会议中展示了他的成果,有兴趣的同学可以参看他的 PPT ,其实整个脚本的思路和上面一样,同样是通过 .gopclntab 这个段所保存的符号信息来获取函数信息,如下图所示。

无符号Golang程序逆向方法解析

除此之外,作者进一步分析了go语言的版本问题,通过2种方式来确定当前程序的go语言版本,一是通过特征字符串的来进行查找,二是通过分析go中特有的结构体类型,由于不同版本之间有结构体会产生变化,作者提出了这种思路来确定版本信息。

无符号Golang程序逆向方法解析

而通过实际分析证明,第一种方法,即特征字符串的方式来查找版本还是会更高效,更准确些,相比而言,第二种方法由于版本之间的差异不多,则会导致歧义。下图是使用效果,当我们载入该脚本后,第二种方式只能判断是go1.8或1.9或1.10,但是特征字符串则较好的确定是go1.9版本。在重命名函数后,再搜索main字符串,就能定位到main包中的所有函数了。

无符号Golang程序逆向方法解析

其他方法

当然无符号问题解决的方法还有很多,比如著名符号执行引擎angr中就使用了 基于函数语义识别库函数 ,也有人对源码进行了分析。函数语义就是分析函数的功能和执行的操作,包括寄存器、内存、堆栈和对其他函数的调用流,作为人去分析函数的时,也是类似的,所以感觉这里也可以用机器学习的方法来进一步提高分析效率。

当然学术界也有对这方面的研究,比如 二进制代码函数相似度匹配技术研究 这篇论文,通过函数的序言部分的特征,提出了二阶段函数匹配方法TPM,在识别出相似函数后,找到其调用关系和决策规则,然后递归的识别不同版本的函数,据论文所述,识别准确率平均高于diaphora和patchdiff方法,也是值得借鉴的。

结语

至此,提出的这么多方法,就能较好的解决Go语言程序逆向中的无符号的问题了。而其实对无符号程序的分析一直是逆向工程中的一个难点,如何有效的分析无符号的程序也一直是我们所关心的问题。那么总结起来无外乎以下几点。

  1. 从各种语言本身特性出发。比如本文详细讨论的Go语言特性,由于特定段保存了符号信息,从而可以进行恢复。
  2. 从代码复用和库函数检测出发。将程序中未知函数和已知功能函数进行某种方式下的对比,比如hash计算、匹配签名、等等。
  3. 从函数语义分析出发。也是最常见的硬核分析手段,就是一个一个函数分析其代码实现和功能,从而推断函数的作用,积少成多,就能对整个程序进行逆向。毫无疑问,也可以将人工智能结合到这种方式中,仿照逆向工程师的思路来进行深度学习的,可能也是学术界未来的一个研究方向吧。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Haskell School of Music

The Haskell School of Music

Paul Hudak、Donya Quick / Cambridge University Press / 2018-10-4 / GBP 42.99

This book teaches functional programming through creative applications in music and sound synthesis. Readers will learn the Haskell programming language and explore numerous ways to create music and d......一起来看看 《The Haskell School of Music》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

URL 编码/解码
URL 编码/解码

URL 编码/解码