go 初级逆向分析(一)

前言

Go 语言是一个比较新的强类型静态语言,2009 年由 Google 发布,在 2012 年才发布首个稳定版。
C编译的可执行文件但是拥有更复杂的运行时库,Go通常也是直接将这些库统一打包成一个文件的,即使用静态链接,因此其程序体积较大,且三方库、标准库与用户代码混在一起,需要区分。

golang的逆向刚开始面临的最大的问题就是strip或者其他手法去除了符号表,因为静态编译,各种函数混合在一起,导致我们很难判断哪个函数是干嘛的。所以很长一段时间内大家的视线都放在如何恢复符号表。

后来大家把目光放在了go语言的反射机制上。经常说反射是逆向最好的朋友,C编译的可执行文件但是拥有更复杂的运行时库,Go通常也是直接将这些库统一打包成一个文件的,即使用静态链接,因此其程序体积较大,且三方库、标准库与用户代码混在一起,需要区分。

在Go语言程序中存在一个叫pcHeader 的结构,也就是所谓的Meatadata。

元数据(Metadata),又称中介数据、中继数据,为描述数据的数据(data about data),主要是描述数据属性(property)的信息

pcHeader + pclnOffset即指向pclntab也就是Go用来描述函数信息的地方,该结构由funcAddress + funcMetaAddress两部分组成。
很多优秀的工具例如go_parser都利用这个恢复一些符号表。

IDA从7.6开始也支持恢复golang的符号表,但是终究还是比较有限。很多还是恢复不了。
而且go语言各种接口,类型,都给我们的逆向工作带来了很大的麻烦。

go的逆向我们应该从go的数据结构看起。
参考一些大佬的博客
七夕—Go二进制文件逆向分析从基础到进阶——数据类型
可以去看大佬博客
我提取一些重点

go的结构有这几个基本类型
在这里插入图片描述
go语言的结构从最底层源码分析来讲,其实都是对rtype结构的扩展
如果只是一个没有绑定任何 Method 的 Basic Type ,那么用 rtype 的结构就可以简单表示。如果一个数据类型绑定了 Methods(这种数据类型也叫 Uncommon Type),或者属于复杂的组合类型(Composite Type),那么就需要用扩展组合的方式来表示了

在这里插入图片描述
rtype结构体

type rtype struct {
    size       uintptr
    ptrdata    uintptr  // number of bytes in the type that can contain pointers
    hash       uint32   // hash of type; avoids computation in hash tables
    tflag      tflag    // extra type information flags
    align      uint8    // alignment of variable with this type
    fieldAlign uint8    // alignment of struct field with this type
    kind       uint8    // enumeration for C
    alg        *typeAlg // algorithm table
    gcdata     *byte    // garbage collection data
    str        nameOff  // string form
    ptrToThis  typeOff  // type for pointer to this type, may be zero
}

再给出一些常用数据类型的源码结构体
切片

type sliceType struct {
    rtype
    elem *rtype // slice element type
}

数组

type arrayType struct {
    rtype
    elem  *rtype // array element type
    slice *rtype // slice type
    len   uintptr
}

接口

type interfaceType struct {
    rtype
    pkgPath name      // import path
    methods []imethod // sorted by hash
}

映射

type mapType struct {
    rtype
    key    *rtype // map key type
    elem   *rtype // map element (value) type
    bucket *rtype // internal bucket structure
    // function for hashing keys (ptr to key, seed) -> hash
    hasher     func(unsafe.Pointer, uintptr) uintptr
    keysize    uint8  // size of key slot
    valuesize  uint8  // size of value slot
    bucketsize uint16 // size of bucket
    flags      uint32
}

通道

type chanType struct {
    rtype
    elem *rtype  // channel element type
    dir  uintptr // channel direction (ChanDir)
}

在大佬写的文章里面可以直接用ida里面的rtype结构体信息还原出数据结构来
包括大佬写的go_parser插件可以直接实现此功能。

但是我们很多场景需要快速的去逆向go的程序,这样人工的去对数据类型信息进行恢复太过繁琐。
我们只需要快速的逆向明白go程序就好。
所以我们试图从简单逆向的角度来再次对数据类型进行分析

也是参考了这篇博客。正向角度看go逆向

string
Go语言中字符串是二进制安全的,它不以\0作为终止符,一个字符串对象在内存中分为两部分,一部分为如下结构,占两个机器字用于索引数据:

type StringHeader struct {
    Data uintptr            // 字符串首地址
    Len  int                // 字符串长度
}

字符串常见的操作是字符串拼接,若拼接的个数不超过5个会调用concatstringN,否则会直接调用concatstrings,它们声明如下,可见在多个字符串拼接时参数形式不同:

func concatstring2(*[32]byte, string, string) string
func concatstring3(*[32]byte, string, string, string) string
func concatstring4(*[32]byte, string, string, string, string) string
func concatstring5(*[32]byte, string, string, string, string, string) string
func concatstrings(*[32]byte, []string) string

因此在遇到concatstringN时可以跳过第一个参数,随后入栈的参数即为字符串,而遇到concatstrings时,跳过第一个参数后汇编层面还剩三个参数,其中后两个一般相同且指明字符串个数,第一个参数则指明字符串数组的首地址。

数组array

type arrayHeader struct {
    Data uintptr        
    Len int
}

数组有三种存储位置,当数组内元素较少时可以直接存于栈上,较多时存于数据区,而当数据会被返回时会存于堆上。

切片slice
类似数组,切片的实例对象数据结构如下,可知它占用了三个机器字

type SliceHeader struct {
    Data uintptr                        // 数据指针
    Len  int                            //  当前长度
    Cap  int                            // 可容纳的长度
}

映射map
字典实现比较复杂,不过在逆向中会涉及到的内容很简单,字典操作常见的会转换为如下函数,一般fastrand和makemap连用返回一个map,它为一个指针,读字典时使用mapaccess1和mapaccess2,后者是使用,ok语法时生成的函数,runtime里还有很多以2结尾的函数代表同样的含义,后文不再赘述。写字典时会使用mapassign函数,它返回一个地址,将value写入该地址,另外还比较常见的是对字典进行遍历,会使用mapiterinit和mapiternext配合:

func fastrand() uint32
func makemap(mapType *byte, hint int, mapbuf *any) (hmap map[any]any)
func mapaccess1(mapType *byte, hmap map[any]any, key *any) (val *any)
func mapaccess2(mapType *byte, hmap map[any]any, key *any) (val *any, pres bool)
func mapassign(mapType *byte, hmap map[any]any, key *any) (val *any)
func mapiterinit(mapType *byte, hmap map[any]any, hiter *any)
func mapiternext(hiter *any)

大佬博客中还提到了一些go逆向时候经常遇到的一些东西,像伸缩栈的逆向等等。也提醒了我们如果碰到自己不熟悉的函数等等解决方法就是去看源码或者查找函数相关用法。

那么道理是这么个道理,我们还是要自己动手去写一点简单的程序逆过来看看。包括试着去逆一些像接口一样比较复杂但是很常用的东西。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值