1.go语言内置一些复杂的数据类型,并支持类型的组合与方法绑定,这些复杂类型数据在汇编层独特的表示方式和用法。go二进制文件的string数据不是传统的以0x00结尾的C-String,而是用(startAddress,Length)两个元素表示一个string数据;比如一个slice数据要由(StartAddress,Length,Capacity)三个元素表示。在汇编代码中,给一个函数传一个string类型的参数,其实要传两个值;给一个函数传一个slice类型的参数,其实要传3个值。
2.独特的调用约定和栈管理机制,go语言用的是continue stack栈管理机制,并且go语言函数中callee的栈空间由caller来维护,callee的参数、返回值都由caller在栈中预留空间,就难以直观看出哪个是参数、哪个是返回值。
https://dr-knz.net/go-calling-convention-x86-64.html
3.全静态链接构建,里面函数的数量过多,如果没有调试信息和符号,动态调试难度更大,其独特的Goroutine调度机制再加上海量的函数,很容易调飞。
go二进制文件中pclntab结构中的函数名信息,并没有被strip掉,而且可以通过辅助脚本再反汇编工具里将其恢复。了解原理,从被strip的Go二进制文件中恢复函数符号以及解析函数中用到的字符串,https://rednaga.io/2016/09/21/reversing_go_binaries_like_a_pro/;代码实现:https://github.com/strazzere/golang_loader_assist.
解析go二进制文件中复杂数据结构的逆向分析,代表工具:
1.用于radare2的 r2_go_helper,由上面提到的zl0warm在r2con 2016上发布;
2.用于IDAPro的GoUtils,以及基于GoUtils2.0开发的更强的IDAGolangHelper。
IDAGolangHelper对Go的不同版本做了更精细化处理,而且第一次在Go二进制文件解析中引入moduledata这个数据结构。而且提供一个GUI界面给用户提供丰富的 *** 作选项。
缺点:
1.支持的Golang版本略旧。目前最高支持Go 1.10,而最新的Go 1.15已经发布了。Go 1.2之后这些版本之间的差异并不是很大的问题。
2.其内部有个独特的做法,把Go语言各种数据的底层实现,在IDA Pro中定义成了相应的ida_struct。即使可以顺利在IDAPro中解析出各种数据类型信息,展示出来的效果并不是很直观,需要查看相应的struct定义才能理解类型信息中各字段的意义,而且不方便跳转 *** 作。
3.发布一个JEB专用的Go二进制文件解析插件jeb-golang-analyzer(可能不好使)。这是一个功能比前面几个工具更加完善的Go二进制文件解析工具,除了解析前面提到的函数名、字符串和数据类型信息,还会解析Duff’s device、Source File Path list、GOROOT以及Interface Table等信息。甚至会把每个pkg中定义的特定数据类型分别列出来。
缺点:对strings和string pointers的解析并不到位,虽然支持多种CPU架构类型(x86/ARM/MIPS)的解析,但是Go二进制文件中字符串的 *** 作方式有多种,该工具覆盖不全。另外,该工具内部定位pcIntab的功能实现,基于Section Name查找和靠Magic Number暴力搜索来结合的方式,还是可能存在误判的可能性,一旦发生误判,找不到pcIntab结构,至少会导致无法解析函数名的后果。最后,这个工具只能用于JEB,而对于用惯了IDA Pro的人来说,JEB插件的解析功能虽强大,但在JEB中展示出来的效果并不是很好,而JEB略卡顿, *** 作体验不是很好。
4.还有一个非典型的Go二进制文件解析工具:基于GoRE的redress。GoRE是一个Go二进制文件解析库,redress是基于这个库来实现的Go二进制文件解析的命令行工具。
redress的强大之处,可以结构化打印Go二进制文件中各种详细信息。
redress -interface pplauncher
redress -struct -method pplauncher
redress是一个接近极致的工具,它把逆向分析需要的信息尽可能地都解析到,并以友好地方式展示出来。但是它只是个命令行工具,跟反汇编工具的插件相比并不是很很方便。它目前还有个除了jeb-golang-anaalyzer有的缺点,限于内部实现的机制,无法解析buildmode=pie模式编译出来的二进制文件。
https://docs.google.com/document/d/1nr-TQHw_er6GOQRsF6T43GGhFDelrAP0NqSS_00RgZQ/edit?pli=1#heading=h.nidcdnrtrn3n
5.go_parser,该工具除了拥有以上各工具的绝大部分功能(strings解析暂时只支持x86架构的二进制文件,这一点不如jeb-golang-analyzer支持的丰富),还支持PIE二进制文件的解析。另外会把结果以更友好、更方便进一步的方式在IDAPro中展示。
类型 | 32位平台 | 64平台 |
---|---|---|
bool、int8、uint8 | 8bit | 8bit |
int、uint16 | 16bit | 16bit |
int32、uint32、float32 | 32bit | 32bit |
int64、uint64、float64、complex64 | 64bit | 64bit |
int、uint、uintptr | 32bit | 64bit |
complex128 | 128bit | 128bit |
切片slice
类似数组,切片的实例对象数据结构,可知它占用了三个机器字,与它相关的函数是growslice表示扩容。
type SliceHeader struct{
Data uintptr //数据指针
Len int //当前长度
Cap int //可容纳的长度
}
字典map
一般fastrand和makemap连用返回一个map,它为一个指针,读字典时使用mapaccess1和mapaccess2。写字典时会使用mapassign函数,它返回一个地址,将value写入该地址;对字典进行遍历,会使用mapierinit和mapiternext配合。
接口和反射息息相关,接口对象会包含实例对象类似信息与数据信息。我们是定义一种接口类型,再定义一种数据类型,并且在这种数据类型上实现一些方法,go只要定义的数据类型实现了某个接口定义的全部,则认为实现了该接口。
语法特性
1.新建对象
go不是面向对象的,此处将Go的变量当做对象来描述。
函数调用栈作为一种结构简单的数据结构可以轻易高效的管理局部变量并实现垃圾回收,因此新建对象也优先使用指令在栈上分配空间,当指针需要逃逸或者动态创建时会在堆区创建对象,make和new两个关键词,不过在汇编层面它们分别对应着makechan,makemap,makeslice与newobject。
func makeslice(et *_type,len,cap int)unsafe.Pointer
mov [rsp+58h+var_58],rax;元素类型
mov [rsp+58h+var_50],5;长度
mov [rsp+58h+var_48],0Ah;容量
call runtime_makeslice
函数与方法
1.栈空间
栈可以分为两个区域,在栈底部存放局部变量,栈顶部做函数调用相关的参数与返回值传递,因此在分析时不能对顶部的var命名,因为它不特指某具体变量而是随时在变化的,错误的命名容易造成混淆。
2.变参
类似Python的一般变参实际被转换为一个tuple,Go变参也被转换为一个slice,因此一个变参在汇编级别占3个参数位。
3.匿名函数
匿名函数通常会以外部函数名_funcX来命名,除此之外和普通函数没什么不同,只是需要注意若使用了外部变量,即形成闭包时,这些变量会以引用形式传入。
Go可以为任意自定义类型绑定方法,方法将被转换为普通函数,并且将方法的接收者转化为第一个参数。
函数反射 函数在普通使用和反射使用时,被保存的信息不相同,普通使用不需要保存函数签名,而反射回保存,更利于分析。
伸缩栈 由于go可以拥有大量的协程,若使用固定大小的栈将回造成内存空间浪费,因此它使用伸缩栈,初始时一个普通携程只分配几Kb的栈,并在函数执行前先判断栈空间是否足够,若不够则通过一些方式扩展栈。
在调用runtime·morestack*函数扩展栈会重新进入函数并进入左侧分支,因此在分析时直接忽略右侧分支即可。
**调用约定**
Go统一通过栈传递参数和返回值,这些空间由调用者维护,返回值内存会在调用前选择性的被初始化,而参数传递是从左到右顺序,在内存中从下到上写入栈,因此看到mov [rsp + 0xXX +var_XX],reg(栈顶)时旧代表开始为函数调用准备参数了,继续向下就能确定函数的参数个数及内容。
需要注意的是不能仅靠函数头部旧断定参数个数,当参数为一个结构体时,可能头部的argX只代表其首位地址,需要具体分析函数retn指令前的指令来确定返回值大小。
写屏障 Go拥有垃圾回收,其三色标记法使用了写屏障的方法保证一致性,在垃圾收集过程中会将写屏障标志置位,此时会进入另一条逻辑,但是在逆向分析过程中可以认为该位未置位而直接分析无保户的情况:
先判断标志,再判定是否进入,在分析时可以直接认为其永假并走左侧分支。
cmp cs:runtime_writeBarrier,0
协程 Go
使用go关键词可以创建并运行协程,它在汇编上会被表现为由runtime_newproc(fn,args?),它会封装函数与参数并创建协程执行信息,并在适当时候被执行。
延迟执行 defer
延迟执行一般用于资源释放,它会先注册到链表中并在当前调用栈返回前执行所有链表中注册的函数,在汇编层面会表现为runtime_deferproc。
碰到runtime_deferreturn,可以直接跳过相关指令并向左侧继续分析。
调用C库 Cgo
Go可以调用C代码,但调用C会存在运行时不一致,Go统一将C调用看作系统调用来处理调用等问题,另一方类型为了解决类型与命名空间等问题cgo会为C生成桩代码来桥接Go,于是这类函数在Go语言侧表现为XXX_CFunYYY,它封装参数并调用runtime_cgocall转换状态,在中间表示为NNN_cgo_abcdef123456_CFuncZZZ,这里它解包参数并调用实际C函数。
此处它调用了libc的void realloc(void,newsize),在Go侧它封装成了os_user_Cfunc_realloc,在该函数内部参数被封装成了结构体并作为指针与函数指针一起被传入cgocall,而函数指针即_cgo_3298b262a8f6_Cfunx_realloc为中间层负责解包参数等并调用真正的C函数。
还有其他内容,如看到以panic开头的分支不分析等不再演示,分析时遇到不认识的三方库函数和标准库函数直接看源码即可。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)