go build -ldflags "-s -w"
),生成的二进制文件体积仍然很大。在反汇编工具中打开 Go 语言二进制文件,可以看到里面包含动辄几千个函数。再加上 Go 语言的独特的函数调用约定、栈结构和多返回值机制,使得对 Go 二进制文件的分析,无论是静态逆向还是动态调式分析,都比分析普通的二进制程序要困难很多。-
Go 语言内置一些复杂的数据类型,并支持类型的组合与方法绑定,这些复杂数据类型在汇编层面有独特的表示方式和用法。比如 Go 二进制文件中的 string 数据不是传统的以 0x00 结尾的 C-String,而是用 (StartAddress, Length) 两个元素表示一个 string 数据;比如一个 slice 数据要由 (StartAddress, Length, Capacity) 三个元素表示。这样的话,在汇编代码中看,给一个函数传一个 string 类型的参数,其实要传两个值;给一个函数传一个 slice 类型的参数,其实要传 3 个值。返回值同理; -
独特的调用约定和栈管理机制,使 C/C++ 二进制文件逆向分析的经验在这里力不从心:Go 语言用的是 continue stack 栈管理机制 ,并且 Go 语言函数中 callee 的栈空间由 caller 来维护,callee 的参数、返回值都由 caller 在栈中预留空间,就难以直观看出哪个是参数、哪个是返回值。详见 The Go low-level calling convention on x86-64 ; -
全静态链接构建,里面函数的数量动辄大几千,如果没有调试信息和符号,想静态逆向分析其中的特定功能点,如大海捞针,很容易迷失在函数的海洋中;动态调试难度更大,其独特的 Goroutine 调度机制再加上海量的函数,很容易调飞。
runtime_morestack
和 runtime_morestack_noctxt
函数,然后在 IDAPro 种遍历对这两个函数交叉引用的位置来找出函数体。这是一个无奈之举,IDAPro v7.x 的版本对 Go 二进制文件中函数体的自动解析功能加强了很多,绝大部分函数都可以被识别出来,无需自己费劲去识别、解析函数体。-
用于 radare2 的 r2_go_helper ,由上面提到的 zl0wram 在 r2con 2016 上发布; -
用于 IDAPro 的 GoUtils ,以及基于 GoUtils 2.0 开发的更强的 IDAGolangHelper 。
-
支持的 Golang 版本略旧。目前最高支持 Go 1.10,而最新的 Go 1.15 已经发布了。Go 1.2 之后这些版本之间的差异并不大,所以这也不是个太大的问题; -
太久不更新,目前在 IDAPro v7.x 上已经无法顺利执行,这个问题比较严重; -
其内部有个独特的做法:把 Go 语言各种数据类型的底层实现,在 IDAPro 中定义成了相应的 ida_struct。这样一来,即使可以顺利在 IDAPro 中解析出各种数据类型信息,展示出来的效果并不是很直观,需要查看相应的 struct 定义才能理解类型信息中各字段的意义,而且不方便跳转操作。窃以为这种体验并不好:
> PACKAGE: net/http:
> struct http.Request (5 fields):
- string Method (offset:0)
- *url.URL URL (offset:10)
- string Proto (offset:18)
- int ProtoMajor (offset:28)
- int ProtoMinor (offset:30)
> PACKAGE: net/url:
> struct url.URL (9 fields):
- string Scheme (offset:0)
- string Opaque (offset:10)
- *url.Userinfo User (offset:20)
- string Host (offset:28)
- string Path (offset:38)
- string RawPath (offset:48)
- bool ForceQuery (offset:58)
- string RawQuery (offset:60)
- string Fragment (offset:70)
> struct url.Userinfo (3 fields):
- string username (offset:0)
- string password (offset:10)
- bool passwordSet (offset:20)
jeb-golang-analyzer 也有一些问题:对 strings 和 string pointers 的解析并不到位,虽然支持多种 CPU 架构类型(x86/ARM/MIPS)的字符串解析,但是 Go 二进制文件中字符串的操作方式有多种,该工具覆盖不全。另外,该工具内部定位 pclntab 的功能实现,基于 Section Name 查找和靠 Magic Number 暴力搜索来结合的方式,还是可能存在误判的可能性,一旦发生误判,找不到 pclntab 结构,至少会导致无法解析函数名的后果。最后,这个工具只能用于 JEB,而对于用惯了 IDAPro 的人来说,JEB 插件的解析功能虽强大,但在 JEB 中展示出来的效果并不是很好,而且 JEB 略卡顿,操作体验不是很好。 另外,还有一个非典型的 Go 二进制文件解析工具:基于 GoRE 的 redress。GoRE 是一个 Go 语言编写的 Go 二进制文件解析库,redress 是基于这个库来实现的 Go 二进制文件解析的 命令行工具。redress 的强大之处,可以结构化打印 Go 二进制文件中各种详细信息,比如打印 Go 二进制文件中的一些 Interface 定义: $ redress -interface pplauncher
type error interface {
Error() string
}
type interface {} interface{}
type route.Addr interface {
Family() int
}
type route.Message interface {
Sys() []route.Sys
}
type route.Sys interface {
SysType() int
}
type route.binaryByteOrder interface {
PutUint16([]uint8, uint16)
PutUint32([]uint8, uint32)
Uint16([]uint8) uint16
Uint32([]uint8) uint32
Uint64([]uint8) uint64
}
或者打印 Go 二进制文件中的一些 Struct 定义以及绑定的方法定义: $ redress -struct -method pplauncher
type main.asset struct{
bytes []uint8
info os.FileInfo
}
type main.bindataFileInfo struct{
name string
size int64
mode uint32
modTime time.Time
}
func (main.bindataFileInfo) IsDir() bool
func (main.bindataFileInfo) ModTime() time.Time
func (main.bindataFileInfo) Mode() uint32
func (main.bindataFileInfo) Name() string
func (main.bindataFileInfo) Size() int64
func (main.bindataFileInfo) Sys() interface {}
type main.bintree struct{
Func func() (*main.asset, error)
Children map[string]*main.bintree
}
这些能力,都是上面列举的反汇编工具的插件难以实现的。另外,用 Go 语言来实现这种工具有天然的优势:可以复用 Go 语言开源的底层代码中解析各种基础数据结构的能力。比如可以借鉴 src/debug/gosym/pclntab.go 中的代码来解析 pclntab 结构,可以借鉴 src/runtime/symtab.go 中的代码来解析 moduledata 结构,以及借鉴 src/reflect/type.go 中的代码来解析各种数据类型的信息。 redress 是一个接近极致的工具,它把逆向分析需要的信息尽可能地都解析到,并以友好的方式展示出来。但是它只是个命令行工具,跟反汇编工具的插件相比并不是很方便。另外,它目前还有个除 jeb-golang-analyzer 之外以上工具都有的缺点:限于内部实现的机制,无法解析 buildmode=pie
模式编译出来的二进制文件。用 redress 解析一个 PIE(Position Independent Executable) 二进制文件,报错如下:最后,是鄙人开发的一个 IDAPro 插件: go_parser ,该工具除了拥有以上各工具的绝大部分功能(strings 解析暂时只支持 x86 架构的二进制文件,这一点不如 jeb-golang-analyzer 支持的丰富),还支持对 PIE 二进制文件的解析。另外会把解析结果以更友好、更方便进一步操作的方式在 IDAPro 中展示。以 DDG 样本中一个复杂的结构体类型为例,解析结果如下: 4.原理初探 前文盘点了关于 Go 二进制文件解析的已有研究,原理层面都是一句带过。可能很多师傅看了会有两点疑惑:
为什么 Go 二进制文件中会有这么多无法被 strip 掉的符号和类型信息? 具体有哪些可以解析并辅助逆向分析的信息? 第一个问题,一句话解释就是,Go 二进制文件里打包进去了 runtime 和 GC 模块,还有独特的 Type Reflection(类型反射) 和 Stack Trace 机制,都需要用到这些信息。参考前文 redress 报错的配图,redress 本身也是 Go 语言编写,其报错时打出来的栈回溯信息,除了参数以及参数地址,还包含 pkg 路径、函数信息、类型信息、源码文件路径、以及在源码文件中的行数。 至于内置于 Go 二进制文件中的类型信息,主要为 Go 语言中的 Type Reflection 和类型转换服务。Go 语言内置的数据类型如下: 而这些类型的底层实现,其实都基于一个底层的结构定义扩展而来: 再加上 Go 允许为数据类型绑定方法,这样就可以定义更复杂的类型和数据结构。而这些类型在进行类型断言和反射时,都需要对这些底层结构进行解析。 第二个问题,对于 Go 二进制文件中,可以解析并对逆向分析分析有帮助的信息,我做了个列表,详情如下: 后文会以这张图为大纲,以 go_parser 为例,详细讲解如何查找、解析并有组织地展示出这些信息,尽最大可能提升 Go 二进制文件逆向分析的效率。 且看下回分解。
- End -
精彩推荐
利用 ZoomEye 追踪多种 Redteam C&C 后渗透攻击框架
七夕节网安男孩自我保护指南
蔓灵花(BITTER)一个新版本的Loader分析
$15,300赏金计划之Paypal密码暴露
戳“阅读原文”查看更多内容
免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论