随着计算机技术的发展,计算机研究人员根据现有语言的缺陷,尝试创造出更好的编程语言,然而新技术的产生往往是一把双刃剑。对传统的安全检测设备和安全研究人员而言,新语言相对晦涩且冷门,具有语言本身的特性。在面对传统的安全措施时,绕过更加轻松,使得安全设备增大识别和检测难度,大大增加安全防御成本。因此诸多新的编程语言一经开发就成为攻击者的崭新的武器开发语言。本次网鼎杯青龙组最后一道逆向工程方向题目采用了go语言编写,我们现在来复现一下,学习一下go语言逆向
0x00 前言
随着计算机技术的发展,计算机研究人员根据现有语言的缺陷,尝试创造出更好的编程语言,然而新技术的产生往往是一把双刃剑。对传统的安全检测设备和安全研究人员而言,新语言相对晦涩且冷门,具有语言本身的特性。在面对传统的安全措施时,绕过更加轻松,使得安全设备增大识别和检测难度,大大增加安全防御成本。因此诸多新的编程语言一经开发就成为攻击者的崭新的武器开发语言。
本次网鼎杯青龙组最后一道逆向工程方向题目采用了go语言编写,我们现在来复现一下,学习一下go语言逆向
0x01 初识go语言
go基本介绍
Go(又称 Golang)是 Google 的 Robert Griesemer,Rob Pike 及 Ken Thompson 开发的一种静态强类型、编译型语言。Go 语言语法与 C 相近,但功能上有:内存安全,GC(垃圾回收),结构形态及 CSP-style 并发计算。
go语言基本语法介绍
“hello go”
程序员学习一门语言的第一个程序,当然是hello world了,我们来根据hello world来了解一下go语言的基本结构
package main
import "fmt"
func main() {
// 输出hello world
fmt.Println("Hello world!")
}
从这个hello world程序中可以看到 go语言的基本组成主要包括以下六部分
包声明:编写源文件时,必须在非注释的第一行指明这个文件属于哪个包,如package main。 包的概念类似于python中项目的概念
引入包:其实就是告诉Go 编译器这个程序需要使用的包,如import "fmt"其实就是引入了fmt包。
注释、函数:和c语言相同,不多赘述
变量:Go 语言变量名由字母、数字、下划线组成,其中首个字符不能为数字
语句/表达式,在 Go 程序中,一行代表一个语句结束。每个语句不需要像 C 家族中的其它语言一样以分号 ; 结尾,因为这些工作都将由 Go 编译器自动完成
数据类型
类型 | 详解 |
---|---|
布尔型 | 布尔型的值只可以是常量 true 或者 false。 |
数字类型 | 整型 int 和浮点型 float。Go 语言支持整型和浮点型数字,并且支持复数,其中位的运算采用补码。 |
字符串类型 | 字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本。 |
派生类型 | (a) 指针类型(Pointer)(b) 数组类型© 结构化类型(struct)(d) Channel 类型(e) 函数类型(f) 切片类型(g) 接口类型(interface)(h) Map 类型 |
特别注意变量没有初始化的情况:在go语言中定义了一个变量,指定变量类型,如果没有初始化,则变量默认为零值。数值类型为“0”布尔类型为“false”字符串为“”(空字符串)
变量声明和赋值
go语言声明变量的一般形式是使用 var 关键字,具体格式为:var identifier typename
。如下的代码中我们定义了一个类型为int的变量。
package main
import "fmt"
func main() {
var a int = 27
fmt.Println(a);
}
多变量声明:
可以同时声明多个类型相同的变量(非全局变量)
var x, y int
var c, d int = 1, 2
g, h := 123, "hello"
关于全局变量:
var (
a int
b bool
)
:=符号
go语言中有一个特殊的:=符号
intVal :=1相等于如下语句
var intVal int
intVal =1
匿名变量
匿名变量是go语言的一种特色功能
匿名变量的特点是一个下画线_
,这本身就是一个特殊的标识符,被称为空白标识符。它可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),但任何赋给这个标识符的值都将被抛弃,因此这些值不能在后续的代码中使用,也不可以使用这个标识符作为变量对其它变量进行赋值或运算。同样的,匿名变量不占用内存空间,不会分配内存。匿名变量与匿名变量之间也不会因为多次声明而无法使用。使用匿名变量时,只需要在变量声明的地方使用下画线替换即可,如下代码
func GetData() (int, int) {
return 30, 50
}
func main(){
a, _ := GetData()
_, b := GetData()
fmt.Println(a, b)
}
选择结构
选择结构类似于python。
例:判断var1是否等于5
if
if 布尔表达式 {
/* 在布尔表达式为 true 时执行 */
} else {
/* 在布尔表达式为 false 时执行 */
}
switch:
switch v {
case val1:
...
case val2:
...
default:
...
}
select语句:
select 是 Go 中的一个特殊控制结构,类似于用于通信的 switch 语句。每个 case 必须是一个通信操作,要么是发送要么是接收。它将会随机执行一个可运行的 case。如果没有 case 可运行,它将阻塞,直到有 case 可运行。
select {
case communication clause :
statement(s);
case communication clause :
statement(s);
/* 你可以定义任意数量的 case */
default : /* 可选 */
statement(s);
}
循环结构
总体来说go语言的循环和C语言是很像的,只不过只有for木有while以下表格反映了go循环和c循环的对应关系
和c语言中的for相同 | for init; condition; post {} |
---|---|
和c语言中的while相同 | for condition{} |
和c语言中的for(;;) 相同 | for{} |
除此以外,for循环还可以直接使用range对slice、map、数组以及字符串等进行迭代循环,格式如下:
for key, value := range oldmap {
newmap[key] = value
}
goto语句
goto语句主要是无条件转移到过程中指定的行goto语句通常和条件语句配合使用,可用来实现条件转移、构成循环以及跳出循环体等功能。逆向中一般用于出题人花指令的添加。
函数使用
和诸多主流语言不同的是,在Go语言中,函数是第一类对象,我们可以将函数保持到变量中。go语言中函数主要有具名和匿名之分,具名函数是匿名函数的一种特例,当匿名函数引用了外部作用域中的变量时就成了闭包函数,闭包函数是函数式编程语言的核心。
go语言函数声明:
func fuction_name([parameter list])[return types]{
function_body
}
名词 | 解析 |
---|---|
func | 函数由func开始声明 |
function_name | 函数名称 |
parameter list | 参数列表 |
return_types | 返回类型 |
函数体 | 函数定义的代码集合 |
具名函数:和c语言中的普通函数意义相同,指具有函数名、返回值以及函数参数的函数。
func Add(a, b int) int {
return a+b
}
**匿名函数:**指不需要定义函数名的一种函数实现方式,它由一个不带函数名的函数声明和函数体组成。
var Add = func(a, b int) int {
return a+b
}
**闭包函数:**匿名函数的一种特殊形式 返回为函数对象,不仅仅是一个函数对象,在该函数外还包裹了一层作用域,这使得,该函数无论在何处调用,优先使用自己外层包裹的作用域。
函数传参:&& 解包的概念
func main(){
var a = []int{1, 2, 3}
Print(a...) // 解包
Print(a) // 未解包
}
func Print(a ...int{}) {
fmt.Println(a...)
}
看上文这个案例:
传入参数直接为 a
时等价于直接调用Print([]int{}{1,2,3})
而当传入参数为a...
时即是对切片a进行了解包,此时其实相当于直接调用Print(1,2,3)
go的动态栈&&递归调用
Go语言支持递归调用,并且Go语言函数的递归调用深度逻辑上没有限制,函数调用的栈是不会出现溢出错误的,因为Go语言运行时会根据需要动态地调整函数栈的大小。语法和c的很相似
func recursion() {
recursion() /* 函数调用自身 */
}
func main() {
recursion()
}
结构体与指针
在声明结构体之前我们首先需要定义一个结构体类型,这需要使用type和struct,type用于设定结构体的名称,struct用于定义一个新的数据类型。
type struct_variable_type struct {
member definition
member definition
...
member definition
}
要访问结构体成员需要使用点号 .
操作符,格式为:结构体变量名.成员名
package main
import "fmt"
type Books struct {
title string
author string
}
func main() {
var book1 Books
Book1.title = "Go 语言入门"
Book1.author = "mars.hao"
}
定义一个结构体指针变量的语法:
var struct_pointer *Books
这种指针变量的初始化和指针部分的初始化方式相同,但是和c语言中有所不同,使用结构体指针访问结构体成员仍然使用.操作符。格式如下:
struct_pointer.title
map
map表的底层原理是哈希表,其结构体定义如下:
type Map struct {
Key *Type // Key type
Elem *Type // Val (elem) type
Bucket *Type // 哈希桶
Hmap *Type // 底层使用的哈希表元信息
Hiter *Type // 用于遍历哈希表的迭代器
}
map初始化方式如下:
var a map[keytype]valuetype
除此以外还可以使用make进行初始化,代码如下:
map_variable =make(map[key_data_type]value_data_type)
类型名 | 意义 |
---|---|
a | map表名字 |
keytype | 键类型 |
valuetype | 键对应的值的类型 |
map插入数据
map的数据插入代码如下:
map_variable["mars"] = 27
map删除数据
delete(map, key) 函数用于删除集合的元素, 参数为 map 和其对应的 key。删除函数不返回任何值。
countryCapitalMap := map[string] string {"France":"Paris","Italy":"Rome","Japan":"Tokyo","India":"New Delhi"}
/* 删除元素 */
delete(countryCapitalMap,"France");
map查找数据
通过key获取map中对应的value值。语法为:map[key]
。但是当key如果不存在的时候,我们会得到该value值类型的默认值,比如string类型得到空字符串,int类型得到0。但是程序不会报错。所以我们可以使用ok-idiom获取值,可知道key/value是否存在。如下:
value, ok := map[key]
已有研究和工具
go语言逆向的难点
go语言逆向的难点主要分为三点:独特而复杂的数据类型;独特的调用约定和栈结构,多返回值机制;全静态链接构建。
全静态链接构建
Go 语言的编译工具链会全静态链接构建二进制文件,把标准库函数和第三方 package 全部做了静态编译,再加上 Go 二进制文件中还打包进去了 runtime 和 GC(Garbage Collection,垃圾回收) 模块代码,所以即使做了 strip 处理( go build -ldflags "-s -w"
),生成的二进制文件体积仍然很大。在反汇编工具中打开 Go 语言二进制文件,可以看到里面包含动辄几千个函数。使得对 Go 二进制文件的分析,无论是静态逆向还是动态调式分析,都比分析普通的二进制程序要困难很多。
独特的函数调用约定、栈结构和多返回值机制
Go 语言用的是 continue stack 栈管理机制 ,并且 Go 语言函数中 callee 的栈空间由 caller 来维护,callee 的参数、返回值都由 caller 在栈中预留空间,就难以直观看出哪个是参数、哪个是返回值。下面我们来
下面通过图示来解释一下go语言中call指令下栈帧的调用
第一步:将下一条指定的地址入栈且作为返回地址,被调用函数执行结束后会跳回到这里
第二步,跳转到被调用函数的入口处开始执行,这后面就是被调用函数的栈帧了
所有的函数的栈帧布局都遵循统一的约定,所以,被调用者是通过栈指针加上相应的偏移来定位到每个参数和返回值的,callee 的参数、返回值都由 caller 在栈中预留空间,就难以直观看出哪个是参数、哪个是返回值。
独特而复杂的数据类型
Go 语言内置一些复杂的数据类型,并支持类型的组合与方法绑定,这些复杂数据类型在汇编层面有独特的表示方式和用法。比如 Go 二进制文件中的 string 数据不是传统的以 0x00
结尾的 C-String,而是用 (StartAddress, Length) 两个元素表示一个 string 数据;这样的话,在汇编代码中看,给一个函数传一个 string 类型的参数,其实要传两个值;
常用工具
CTF中最常用的go语言逆向辅助工具为 IDA自带的插件IDAGolangHelper,这个插件肯定是go逆向辅助工具中最火的,但是也存在很多问题 比如说:
- 支持的 Golang 版本略旧。目前最高支持 Go 1.10
- 太久不更新,目前在 IDAPro v7.x 上已经无法顺利执行
- 其内部有个独特的做法:把 Go 语言各种数据类型的底层实现,在 IDAPro 中定义成了相应的 ida_struct。这样一来,即使可以顺利在 IDAPro 中解析出各种数据类型信息,展示出来的效果并不是很直观
此外,还有JEB官方发布一个 JEB 专用的 Go 二进制文件解析插件jeb-golang-analyzer这是一个功能比前面几个工具更加完善的 Go 二进制文件解析工具,但是也尚不完善
go逆向CTF 案例
[2022网鼎青龙组]re3
拿到手是一个golong源码,找到主函数开始逆向分析。
func main() {
var nFAzj, CuSkl string
jjxXf := []byte{
37, 73, 151, 135, 65, 58, 241, 90, 33, 86, 71, 41, 102, 241, 213, 234, 67, 144, 139, 20, 112, 150, 41, 7, 158, 251, 167, 249, 24, 129, 72, 64, 83, 142, 166, 236, 67, 18, 211, 100, 91, 38, 83, 147, 40, 78, 239, 113, 232, 83, 227, 47, 192, 227, 70, 167, 201, 249, 156, 101, 216, 159, 116, 210, 152, 234, 38, 145, 198, 58, 24, 183, 72, 143, 136, 234, 246}
KdlaH := []byte{
191, 140, 114, 245, 142, 55, 190, 30, 161, 18, 200, 7, 21, 59, 17, 44, 34, 181, 109, 116, 146, 145, 189, 68, 142, 113, 0, 33, 46, 184, 21, 33, 66, 99, 124, 167, 201, 88, 133, 20, 211, 67, 133, 250, 62, 28, 138, 229, 105, 102, 125, 124, 208, 180, 50, 146, 67, 39, 55, 240, 239, 203, 230, 142, 20, 90, 205, 27, 128, 136, 151, 140, 222, 92, 152, 1, 222, 138, 254, 246, 223, 224, 236, 33, 60, 170, 189, 77, 124, 72, 135, 46, 235, 17, 32, 28, 245}
fmt.Print(MPyt9GWTRfAFNvb1(jjxXf))
fmt.Scanf("%20s", &nFAzj)
fmt.Print(kZ2BFvOxepd5ALDR(KdlaH))
fmt.Scanf("%20s", &CuSkl)
vNvUO := GwSqNHQ7dPXpIG64(nFAzj)
YJCya := ""
mvOxK := YI3z8ZxOKhfLmTPC(CuSkl)
if mvOxK != nil {
YJCya = mvOxK()
}
if YJCya != "" && vNvUO != "" {
fmt.Printf("flag\{\%s%s}\n", vNvUO, YJCya)
}
}
函数的主要逻辑还是很简单的,得到flag的条件是YJCya vNvUO两个函数不置空。接着我们向上追溯,得到
YJCya=mvOxK = YI3z8ZxOKhfLmTPC(CuSkl)
vNvUO=GwSqNHQ7dPXpIG64(nFAzj)
CuSkl和nFAzj为我们两个输入值。我们这就找到了控制输入能够得到flag的证据。接下来没什么方向了,我们看到这个print里面竟然调用了函数,有古怪。。跟进看一下。
看到上图两个函数,这两个函数都是将传入参数和函数内置数组按位异或,这样我们写如下脚本解密
kdlah=[191, 140, 114, 245, 142, 55, 190, 30, 161, 18, 200, 7, 21, 59, 17, 44, 34, 181, 109, 116, 146, 145, 189, 68, 142, 113, 0, 33, 46, 184, 21, 33, 66, 99, 124, 167, 201, 88, 133, 20, 211, 67, 133, 250, 62, 28, 138, 229, 105, 102, 125, 124, 208, 180, 50, 146, 67, 39, 55, 240, 239, 203, 230, 142, 20, 90, 205, 27, 128, 136, 151, 140, 222, 92, 152, 1, 222, 138, 254, 246, 223, 224, 236, 33, 60, 170, 189, 77, 124, 72, 135, 46, 235, 17, 32, 28, 245]
jutvh=[246, 226, 2, 128, 250, 23, 202, 118, 196, 50, 187, 98, 118, 84, 127, 72, 2, 211, 24, 26, 241, 229, 212, 43, 224, 93, 32, 86, 70, 209, 118, 73, 98, 11, 29, 212, 233, 107, 165, 119, 178, 47, 233, 159, 76, 111, 170, 132, 7, 2, 93, 21, 190, 194, 93, 249, 38, 84, 23, 132, 135, 174, 198, 232, 97, 52, 174, 111, 233, 231, 249, 172, 176, 61, 245, 100, 186, 170, 157, 190, 133, 150, 217, 78, 76, 146, 207, 2, 17, 36, 198, 69, 137, 39, 26, 60, 255]
spVwk=[108, 39, 231, 242, 53, 26, 133, 50, 68, 118, 33, 64, 20, 130, 161, 202, 37, 229, 229, 119, 4, 255, 70, 105, 178, 219, 208, 145, 113, 226, 32, 96, 59, 239, 213, 204, 117, 50, 163, 5, 41, 71, 62, 246, 92, 43, 157, 2, 200, 50, 141, 75, 224, 151, 46, 194, 233, 141, 244, 12, 170, 251, 84, 188, 249, 135, 67, 245, 230, 93, 84, 254, 32, 221, 178, 202, 252]
jjxxf=[37, 73, 151, 135, 65, 58, 241, 90, 33, 86, 71, 41, 102, 241, 213, 234, 67, 144, 139, 20, 112, 150, 41, 7, 158, 251, 167, 249, 24, 129, 72, 64, 83, 142, 166, 236, 67, 18, 211, 100, 91, 38, 83, 147, 40, 78, 239, 113, 232, 83, 227, 47, 192, 227, 70, 167, 201, 249, 156, 101, 216, 159, 116, 210, 152, 234, 38, 145, 198, 58, 24, 183, 72, 143, 136, 234, 246]
for i in range(0,77):
print(chr(spVwk[i] ^ jjxxf[i]), end='')
for i in range(0,97):
print(chr(kdlah[i]^jutvh[i]),end='')
解密得到输出(提示)
Input the first function, which has 6 parameters and the third named gLIhR: //第一个函数,有六个参数且第三个参数为 gLIhR
Input the second function, which has 3 callers and invokes the function named cHZv5op8rOmlAkb6: //第二个函数,有三次调用并且其中一次函数名为cHZv5op8rOmlAkb6
这样我们找到了两个对应函数:ZlXDJkH3OZN4Mayd和UhnCm82SDGE0zLYO。可以愉快的执行输入得到flag了。但是赛后发现这里还有一个小坑,由于go语言会将回车当作输入,所以好多人在比赛的时候都在输入第一个函数名按下回车时,程序误以为输入了两个参数而退出了,得不到flag,这里最直接的解决方法是可以使用Tab作为终止符。
当然我采取了另外一种更简便的方法,直接修改程序中输入为变量赋值 直接运行得到flag
[Hgame 2022] server
本题为一个 http 服务器,在9090 端口上可以使用 get 方法提交 flag。
使用IDA7.6及以上版本打开能够直接加载go语言嵌入的符号表,降低逆向难度。
最直接的好处,使用ida7.6以上版本能够直接在function页面看到main_encrypt函数,而使用7.5及以下版本就编译不出该函数
反编译encrypt函数发现栈传参有点问题,看了大佬的wp发现需要自定义函数声明
__int64 __usercall math_big___ptr_Int__SetString@<rax>(char *str@<rbx>, __int64 a2@<rax>, int a3@<edi>, int a4@<ecx>)
通过对其中函数名称和数据的分析,便可以得到加密过程为 RSA 和异或运算。这个异或运算比较复杂,所以采用爆破的方法来解,密文是153位的,解密脚本如下
#python2.7
import Crypto.Util.number
import gmpy2
a=[99,85,4,3,5,5,5,3,7,7,2,8,8,11,1,2,10,4,2,13,8,9,12,9,4,13,8,0,14,0,15,13,14,10,2, 2,1,7,3,5,6,4,6,7,6,2,2,5,3,3,9,6,0,11,13,11,0,2,3,8,3,11,7,1,11,5,14,5,0,10,14,15, 13,7,13,7,14,1,15,1,11,5,6,2,12,6,10,4,1,7,4,2,6,3,6,12,5,12,3,12,6,0,4,15,2,14,7,0 ,14,14,12,4,3,4,2,0,0,2,6,2,3,6,4,4,4,7,1,2,3,9,2,12,8,1,12,3,12,2,0,3,14,3,14,12,9 ,1,7,15,5,7,2,2,4]
for j in range(256):
c=j
a= [99,85,4,3,5,5,5,3,7,7,2,8,8,11,1,2,10,4,2,13,8,9,12,9,4,13,8,0,14,0,15,13,14,10,2, 2,1,7,3,5,6,4,6,7,6,2,2,5,3,3,9,6,0,11,13,11,0,2,3,8,3,11,7,1,11,5,14,5,0,10,14,15, 13,7,13,7,14,1,15,1,11,5,6,2,12,6,10,4,1,7,4,2,6,3,6,12,5,12,3,12,6,0,4,15,2,14,7,0 ,14,14,12,4,3,4,2,0,0,2,6,2,3,6,4,4,4,7,1,2,3,9,2,12,8,1,12,3,12,2,0,3,14,3,14,12,9 ,1,7,15,5,7,2,2,4]
for i in range(len(a)-1,-1,-1):
c^=a[i]
a[i]=a[i]^c
try:
enc=int("".join(map(chr,a)))
except ValueError:
continue
把得到的结果当作rsa密文在rsa解密就可以得到flag啦
p=92582184765240663364795767694262273105045150785272129481762171937885924776597 q=107310528658039985708896636559112400334262005367649176746429531274300859498993
e=950501
enc=""
r=(p-1)*(q-1)
d=gmpy2.invert(e,r)
m=pow(enc,d,p*q)
print(long_to_bytes(m))
0x02 后记|总结
go逆向,博大精深,目前在ctf比赛中也逐渐热门,很多比赛甚至把go逆向作为压轴题目,可见go逆向在未来ctf比赛中的地位。本篇文章应该是一个开始,接下来打算研究一下go逆向中更加深入的题型,先在此立个flag
0x03 参考链接
https://blog.csdn.net/weixin_50941083/article/details/125590486
https://baike.baidu.com/item/go/953521?fr=aladdin
https://zhuanlan.zhihu.com/p/383336210
https://mp.weixin.qq.com/s/S0HbZ7m9Wcj1b_EUwC7xxw
https://www.anquanke.com/post/id/214940
https://www.anquanke.com/post/id/214940
https://blog.csdn.net/Zerore/article/details/122253218
https://blog.csdn.net/weixin_52690231/article/details/123314776
- 本文作者: 绿冰壶
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/1874
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!