Go逆向总结

一、介绍

 Go 语言是一个强类型静态语言,实现了 CSP 并发模型,并在 2012 年才发布首个稳定版。由于Go语言方便跨平台交叉编译,所以吸引了恶意软件开发者使用Go来开发恶意软件。

 Go编译器会静态链接构建二进制文件,把标准库函数和第三方 package 进行静态编译,还将runtime 和 GC(Garbage Collection,垃圾回收) 模块打包进去 。

Go逆向的难点:

  1. 函数和代码量较传统C或C++程序要大得多;
  2. Go 语言的独特的函数调用约定、栈结构以及多返回值机制和常见的C/C++的机制不同;
  3. 用户代码中带有大量运行时库的API调用,剥离符号后会带来很大程度上的干扰。

 Go支持continuous stack机制,每次执行goruntine时都会检查stack空间是否够用,否则会调用Go运行时库的morestack_noctx函数,申请一块更大的栈空间,将旧栈拷贝到新栈中。

二、修复符号

pclntab(Program Counter Line Table,可直译为程序计数器行数映射表)是一种将程序计数器映射到行号的数据结构。pclntab是为了服务于程序报错时调用栈回溯而存在的。

pclntab中存储了函数名等符号信息,可以通过该表获取函数地址和其对应函数名字符串的关系,进而使用idc脚本重命名函数名称,帮助我们更容易逆向Go程序。

在源码中,pclntab如此定义:

type LineTable struct {
    Data []byte
    PC   uint64
    Line int

    // This mutex is used to keep parsing of pclntab synchronous.
    mu sync.Mutex

    // Contains the version of the pclntab section.
    version version

    // Go 1.2/1.16/1.18 state
    binary      binary.ByteOrder
    quantum     uint32
    ptrsize     uint32 // size of a ptr in bytes
    textStart   uint64 // address of runtime.text symbol (1.18+)
    funcnametab []byte
    cutab       []byte // offset to the cutab variable from pcHeader
    funcdata    []byte
    functab     []byte
    nfunctab    uint32 // number of functions in the module
    filetab     []byte // number of entries in the file tab.
    pctab       []byte // points to the pctables.
    nfiletab    uint32 // number of entries in the file tab.
    funcNames   map[uint32]string // cache the function names
    strings     map[uint32]string // interned substrings of Data, keyed by offset
    // fileMap varies depending on the version of the object file.
    // For ver12, it maps the name to the index in the file table.
    // For ver116, it maps the name to the offset in filetab.
    fileMap map[string]uint32
}

其中,我们通过funcnametab这个成员可以找到函数名称表,进而得到函数地址和函数名称的关系。

对于 PE 文件,.symtab符号表由FileHeader.PointerToSymbolTable字段指向。此表中名为runtime.pclntab的符号包含pclntab的地址。ELF 和 Mach-O 文件也有一个.symtab符号表,其中包含一个runtime.pclntab符号,但不依赖它来定位pclntab在 ELF 文件中找到 .symtab,请查找类型SH_SYMTAB的名为 .symtab 的部分在 Mach-O 文件中, .symtabLC_SYMTAB引用加载命令。PS:带有e前缀的符号(例如,epclntab)表示对应表的结尾。

Go逆向总结

 

 当 Go 二进制文件被剥离时,.symtab符号表被清零或不存在。这消除了查找符号(例如runtime.pclntab )的能力。但是,这些符号指向的数据(例如pclntab本身)仍然存在于二进制文件中。对于 ELF 和 Mach-O 文件,命名部分(例如.gopclntab)也仍然存在,可用于定位pclntab

对未剥离二进制文件的pclntab进行定位:

  1. 找到.symtab符号并解析runtime.pclntab;
  2. 对于 ELF 和 Mach-O 文件,找到.gopclntab或__gopclntab部分;

对已剥离的二进制文件的pclntab进行定位:

  线性扫描二进制文件。pclntab始终以特定于版本号的magic数为开头。

magic根据版本不同会不一样,如下:

const (
    go12magic  = 0xfffffffb
    go116magic = 0xfffffffa
    go118magic = 0xfffffff0
)

这里推荐一个新的符号修复工具GoReSym(https://github.com/mandiant/GoReSym)。

三、初始化过程

Go程序从系统初始化直到退出必经的流程:

_rt0_amd64_darwin
main
_rt0_amd64
runtime.check:检测像int8,int16,float等是否是预期的大小,检测cas操作是否正常
runtime.args:将argc,argv设置到static全局变量中了
runtime.osinit:设置runtime.ncpu,不同平台实现方式不一样
runtime.hashinit:通过读取/dev/urandom文件的方式从内核获得随机数种子
runtime.schedinit:内存管理初始化,根据GOMAXPROCS设置使用的procs等等
runtime.newproc:把runtime.main放到就绪线程队列里面
runtime.mstart:调用调度函数schedule
main.main:用户的main函数,用户代码
runtime.exit

  go关键字的调用协议:先将参数进栈,再被调函数指针和参数字节数进栈,接着调用runtime.newproc函数。newproc函数新开个goroutine执行runtime.main。schedule函数绝不返回,它会根据当前线程队列中线程状态挑选一个goroutine来运行。由于当前只有这一个goroutine,它会被调度,然后就到了runtime.main函数中来,runtime.main会调用用户的main函数,即main.main从此进入用户代码。

      当然,在main.main执行之前(runtime.main在进入用户main函数之前做的一些事情),Go语言的runtime库还会初始化一些后台任务,暂时略过。

四、函数调用约定

Golang的调用约定与C不同,它支持多值返回,返回值并不保存在eax寄存器中返回,而是作为参数添加到参数列表中,如:func f(arg1, arg2 int) (ret1, ret2 int):返回值有两个,则增加两个参数(最后两个参数)。

Go逆向总结

 Go打包的x86_x64平台程序,参数中int类型在内存为一个字(2个字节)大小,不是4个字节。如*int这样的指针类型也是只使用一个字(8个字节), 而interface接口类型的变量占2个字(16个字节)。

String和Slice类型的变量分别占两个字和三个字。Go中的String类型是一个由address和len组成的结构体,而Slice类型是一个由array(byte*)、len以及capacity组成的结构体。

PS:32 位 cpu的1word = 4 bytes,64位cpu的1 word = 8 bytes。

Go中的接口实现和普通的函数不同,其没有堆栈平衡,不是函数只是一个代码片段。

比如

func  InterfacePass ( Foo  x )  Foo  {  return  x  }

其对应二进制内容为:

InterfacePass: 
  0 x4805b0               488 b442408              MOVQ  0x8 ( SP ),  AX 
  0 x4805b5               4889442418               MOVQ  AX ,  0x18 ( SP ) 
  0 x4805ba               488 b442410              MOVQ  0x10 ( SP ),  AX 
  0 x4805bf               4889442420               MOVQ  AX ,  0x20 ( SP ) 
  0 x4805c4               c3                       RET

又或者:

var  x  int 
func  RetPtr ()  * int  {  return  & x  } 
func  NilPtr ()  * int  {  return  nil  }

其对应二进制内容为:

RetPtr: 
  0 x480650               488 d0581010c00          LEAQ  src.x ( SB ),  AX   ; 计算 &x 
  0 x480657               4889442408               MOVQ  AX ,  0x8 ( SP )     ; return &x 
  0 x48065c               c3                       RET 
NilPtr: 
  0 x480660               48 c744240800000000      MOVQ  $0x0 ,  0x8 ( SP )   ; 返回 0 
  0 x480669               c3                       RET

 

五、关于空值

 按照Go语言规范,任何类型在未初始化时都对应一个零值:布尔类型是false,整型是0,字符串是"",而指针,函数,interface,slice,channel和map的零值都是nil。

一个interface在没有进行初始化时,对应的值是nil。也就是说var v interface{},此时v就是一个nil。在底层存储上,它是一个空指针。

var v *T
var i interface{} // i = nil
i = v 

string的空值是"",它是不能跟nil比较的。即使是空的string,它的大小也是两个机器字长的。slice也类似,它的空值并不是一个空指针,而是结构体中的指针域为空,空的slice的大小也是三个机器字长的。

channel跟string或slice有些不同,它在栈上只是一个指针,实际的数据都是由指针所指向的堆上面。跟channel相关的操作有:初始化/读/写/关闭。channel未初始化值就是nil,未初始化的channel是不能使用的。

六、go关键字的实现

在Go语言中,表达式go f(x, y, z)会启动一个新的goroutine运行函数f(x, y, z)。函数f,变量x、y、z的值是在原goroutine计算的,只有函数f的执行是在新的goroutine中的。

新的goroutine不能和当前go线程用同一个栈,否则会相互覆盖。所以对go关键字的调用协议与普通函数调用是不同的。

正常的函数调用,下面是调用f(1, 2, 3)时的汇编代码:

f(1,2,3):
MOVL    $1, 0(SP)
MOVL    $2, 4(SP)
MOVL    $3, 8(SP)
CALL    f(SB)

go f(1, 2, 3)生成的代码:

MOVL    $1, 0(SP)
MOVL    $2, 4(SP)
MOVL    $3, 8(SP)
PUSHQ   $f(SB)
PUSHQ   $12
CALL    runtime.newproc(SB)
POPQ    AX
POPQ    AX

将f和12作为参数进栈而不直接调用f,然后调用函数runtime.newproc。12是参数占用的大小(12个字节,int类型是4个字节)。

runtime.newproc(size, func, args)

  

七、defer关键字的实现

defer用于资源的释放。defer关键字的实现跟go关键字很类似,不同的是它调用的是runtime.deferproc而不是runtime.newproc。

在defer出现的地方,插入了指令call runtime.deferproc,然后在函数返回之前的地方,插入指令call runtime.deferreturn。

goroutine的控制结构中,有一张表记录defer,调用runtime.deferproc时会将需要defer的表达式记录在表中,而在调用runtime.deferreturn的时候,则会依次从defer表中出栈并执行。

普通的函数返回时,汇编代码类似:

add xx SP
return

包含了defer语句,则汇编代码是:

call runtime.deferreturn,
add xx SP
return

  

相关参考

J!4Yu大佬关于Go逆向的文章:https://www.anquanke.com/post/id/214940

knz大佬的文章非常底层详细:https://dr-knz.net/go-calling-convention-x86-64.html

IDA帮助文档:https://www.hex-rays.com/products/ida/support/idadoc/162.shtml

其他:

https://tiancaiamao.gitbooks.io/go-internals/content/zh/04.1.html

 

原文链接: https://www.cnblogs.com/glodears/p/17024650.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

    Go逆向总结

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/310057

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月16日 上午11:20
下一篇 2023年2月16日 上午11:20

相关推荐