首页
论坛
课程
招聘
Go语言逆向初探
2021-6-11 09:49 7771

Go语言逆向初探

2021-6-11 09:49
7771

前言

       前几日获得一个Go语言编写的程序外挂,分析该外挂过程中发现与C、C++编译出来的二进制文件有很大的不同,相比于传统语言编译出来的可执行文件Go程序在参数传递、栈空间管理和函数调用等方面都有自己的特点。

Go的二进制逆向在互联网上有一篇很全面的文章《Go二进制文件逆向分析从基础到进阶》,建议有兴趣的朋友读一下。珠玉在前为什么还要写这篇文章呢?这是因为Go最新版本1.16有所变化,以前的解析办法已经不适用,这里把自己在逆向Go外挂过程中的一些经验沉淀下来。

Go语言特点介绍

Go是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。Go的语法接近C语言,但对于变量的声明有所不同,与C++相比,Go并不包括如枚举、异常处理、继承、泛型、断言、虚函数等功能,但增加了 切片(Slice) 型、并发、管道、垃圾回收功能、接口等特性的语言级支持[1]。

在这里简单介绍一下在逆向Go程序过程中需要用到的一些特性知识。

1.     反射

反射是逆向最好的朋友,如果一门语言具备反射那么其编译出的可执行文件本身就约等于符号文件,这不是靠strip命令或者加壳压缩就能删除的。最好的证明就是UE4引擎编译出来的游戏程序,UE4采用C++开发,该语言实际不支持反射,但UE4实现了一套反射机制从而泄露了函数名、类等信息,这也是外挂作者最喜欢的东西。反射的实现通常依赖于所谓的Metadata即数据的数据,不同的语言可能有不同的称呼。

作为一篇逆向文章,本文不会介绍Go语言的反射语法或者作用,相反这里讲解的是怎么通过Metadata来读懂二进制文件。在Go语言程序中存在一个叫pcHeader 的结构,也就是所谓的Meatadata。pcHeader 结构参考symtab.go源代码如下图所示,值得注意的是Go 1.16之前的MagicNumber是0xffffffb,之后是0xfffffffa,并且nfunc和nfiles实际占用8个字节。

pcHeader的地址可以搜索FA FF FF FF 00 00 01 08或者FA FF FF FF 00 00 01 04获得,其中x64程序是0x08,32位程序是0x04。

pcHeader + pclnOffset即指向pclntab也就是Go用来描述函数信息的地方,该结构由funcAddress + funcMetaAddress两部分组成。

pcHeader + pclnOffset + funcMetaAddress指向Go的_func结构,具体见symtab.go文件的type pcHeader struct。通过_func中的entry也就是funcAddress和nameOff就可以把函数地址和函数名结合起来。

2.     Stack

Go 语言用的是 continue stack 栈管理机制 [2],并且 Go 语言函数中 callee 的栈空间由 caller 来维护,callee 的参数、返回值都由 caller 在栈中预留空间。详见 The Go low-level calling convention on x86-64[3]。

Go支持goroutine也就是协程,每个goroutine都有自己的栈,其初始栈空间很小并在使用过程中自动增长。这种机制使得Go编译出来的函数在起始处判断当前栈是否够用,如果不够用就分配足够大的新空间并将旧栈数据拷贝到新栈中。该特点表现到二进制中如下,可以利用该特点定位到Go语言函数,当然有部分函数也不会判断栈空间是否够用。

3.     字符串

       Go 二进制文件中的 string 数据不是传统的以 0x00 结尾的 C语言系字符串,而是采用StartAddress + Size的模式,例如下面程序传入的是Hello和Test的起始地址和长度。

Go逆向突破口

所谓的逆向突破口就是把程序中的所有函数标识并命名出来,知道了函数名那么距离解析出具体功能就只差一步之遥。下面是我编写IDA脚本用于半自动重命名函数,具体效果见下图,。



#!/usr/bin/env python

# -*- coding: UTF-8 -*-

'''

SimpleGoParser.py:

IDA Plugin for Golang Executable file parsing.

'''

import idautils, idc, idaapi

import sys

import string

 

def getstring(addr):

    out = ""

    while True:

        value = idc.get_wide_byte(addr)

        if value != 0:

            out += chr(value)

        else:

            break

        addr += 1

    return out

 

STRIP_CHARS = [ '('')''['']''{''}'' ''"' ]

REPLACE_CHARS = ['.''*''-'','';'':''/''\xb7' ]

def cleanfuncname(funcName):

    for c in STRIP_CHARS:

        funcName = funcName.replace(c, '')

 

    for c in REPLACE_CHARS:

        funcName = funcName.replace(c, '_')

 

    return funcName

 

class Pclntab():

    

    # symtab.go file

    # type pcHeader struct {

    #     magic          uint32  // 0xFFFFFFFA

    #     pad1, pad2     uint8   // 0,0

    #     minLC          uint8   // min instruction size

    #     ptrSize        uint8   // size of a ptr in bytes

    #     nfunc          int     // number of functions in the module

    #     nfiles         uint    // number of entries in the file tab.

    #     funcnameOffset uintptr // offset to the funcnametab variable from pcHeader

    #     cuOffset       uintptr // offset to the cutab variable from pcHeader

    #     filetabOffset  uintptr // offset to the filetab variable from pcHeader

    #     pctabOffset    uintptr // offset to the pctab varible from pcHeader

    #     pclnOffset     uintptr // offset to the pclntab variable from pcHeader

    # }

 

    def __init__(self):

        self.MAGIC = 0xFFFFFFFA

        self.offset = 0

        self.baseAddress = 0

        self.minLC = 0

        self.ptrSize = 0

        self.functionCounts = 0

        self.fileCounts = 0

        self.functionNameOffset = 0

        self.cuOffset = 0

        self.filetabOffset = 0

        self.pctabOffset = 0

        self.functabOffset = 0

 

    def parse(self, baseAddress):

        print("Pclntab Go")

        magic = idc.get_wide_dword(baseAddress)

        if magic != self.MAGIC:

            print("Pclntab address invalid");

            return False

        

        self.baseAddress = baseAddress

        self.offset = 4 + 2

        self.minLC = idc.get_wide_byte(baseAddress + self.offset)

        self.offset = self.offset + 1

 

        self.ptrSize = idc.get_wide_byte(baseAddress + self.offset)

        self.offset = self.offset + 1

 

        self.functionCounts = idc.get_wide_dword(baseAddress + self.offset)

        self.offset = self.offset + 8    #  静态文件中实际占8字节

 

        self.fileCounts = idc.get_wide_dword(baseAddress + self.offset)

        self.offset = self.offset + 8    #  静态文件中实际占8字节

 

        self.functionNameOffset = baseAddress + idc.get_qword(baseAddress + self.offset)

        self.offset = self.offset + 8

 

        self.cuOffset = baseAddress + idc.get_qword(baseAddress + self.offset)

        self.offset = self.offset + 8

 

        self.filetabOffset = baseAddress + idc.get_qword(baseAddress + self.offset)

        self.offset = self.offset + 8

 

        self.pctabOffset = baseAddress + idc.get_qword(baseAddress + self.offset)

        self.offset = self.offset + 8

 

        self.functabOffset = baseAddress + idc.get_qword(baseAddress + self.offset)

 

        print("Pclntab Base Info")

        print("Min Instuction Size %d Pointer Size %d Function Counts %d File Counts %d" % (self.minLC, self.ptrSize, self.functionCounts, self.fileCounts))

        print("Function Name Offset 0x%x CuOffset 0x%x File Table Offset 0x%x Pctab Offset 0x%x Function Table Offset 0x%x" % (self.functionNameOffset, self.cuOffset, self.filetabOffset, self.pctabOffset, self. functabOffset))

 

        return True

 

    def setfuncname(self):

        index = 0

        while index < self.functionCounts:

            funcAddr = idc.get_qword(self.functabOffset + 2 * index * self.ptrSize)

            funcMetaAddr = self.functabOffset + idc.get_qword(self.functabOffset + 2 * index * self.ptrSize + self.ptrSize)

            if funcAddr != idc.get_qword(funcMetaAddr):

                print("Traverse Function Failed")

            else:

                funcNameAddr = self.functionNameOffset + idc.get_wide_dword(funcMetaAddr + self.ptrSize)

                funcName = getstring(funcNameAddr)

                funcName = cleanfuncname(funcName)

                print("Function Address 0x%x Name %s" % (funcAddr, funcName))

                if idc.get_wide_dword(funcAddr) == 0x0c8b4865:

                   idc.create_insn(funcAddr)

                   idc.add_func(funcAddr)

                idc.set_name(funcAddr, funcName, SN_CHECK)

            

            index = index + 1

 

def main():

    pc = Pclntab()

    if pc.parse(0xE90E60) == True:

        pc.setfuncname()

 

if __name__ == '__main__':

   main()

   

 

 

未完待续

1.        当前脚本需要手动寻找pclntab所在地址,可以优化自动寻找;

2.        Go二进制中的数据结构解析;

3.        很多Go函数IDA不能很好识别,如下所示以字节码(65 48 8B 0C 25 28 00 00 00    mov     rcx, gs:28h)开始的都是函数,即使调用idc.add_func()定义为函数IDA也不能全部转换,不知道道友们否有更好的解决办法

引用

[1] https://zh.wikipedia.org/wiki/Go

[2] https://tiancaiamao.gitbooks.io/go-internals/content/zh/03.5.html

[3] https://dr-knz.net/go-calling-convention-x86-64.html



第五届安全开发者峰会(SDC 2021)10月23日上海召开!限时2.5折门票(含自助午餐1份)

收藏
点赞2
打赏
分享
最新回复 (9)
雪    币: 56
活跃值: 活跃值 (201)
能力值: ( LV15,RANK:843 )
在线值:
发帖
回帖
粉丝
NearJMP 活跃值 5 2021-6-11 11:07
2
0
这是用的什么版本的IDA, 7.6支持golang解析了,有试过效果吗?
雪    币: 2078
活跃值: 活跃值 (1166)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
caolinkai 活跃值 2021-6-11 11:14
3
0
66666
雪    币: 17262
活跃值: 活跃值 (1483)
能力值: ( LV3,RANK:28 )
在线值:
发帖
回帖
粉丝
_重黎 活跃值 2021-6-11 11:55
4
0
https://github.com/renzhexigua/go_parser/tree/py3_1.16
雪    币: 1476
活跃值: 活跃值 (267)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
半个大西瓜 活跃值 2021-6-11 15:22
5
0
NearJMP 这是用的什么版本的IDA, 7.6支持golang解析了,有试过效果吗?

7.0

最后于 2021-6-11 15:23 被半个大西瓜编辑 ,原因:
雪    币: 1476
活跃值: 活跃值 (267)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
半个大西瓜 活跃值 2021-6-11 15:24
6
0
_重黎 https://github.com/renzhexigua/go_parser/tree/py3_1.16
当时没搜索到支持1.16的,所以自己写了
雪    币: 223
活跃值: 活跃值 (633)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
kakasasa 活跃值 2021-6-11 17:50
7
0
感谢分享,mark
雪    币: 2912
活跃值: 活跃值 (3180)
能力值: ( LV15,RANK:685 )
在线值:
发帖
回帖
粉丝
无名侠 活跃值 11 2021-6-14 16:46
9
1
前段时间我整理了一下golang各种数据结构的底层实现与传参机制
https://panda0s.top/2021/04/14/Golang-underlying-data-representaion/
雪    币: 730
活跃值: 活跃值 (694)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
LexSafe 活跃值 2021-6-15 10:25
10
0
字符串存储是Pascal风格
游客
登录 | 注册 方可回帖
返回