文章首发地址:
https://xz.aliyun.com/t/15612
文章首发作者:
T0daySeeker
思路来源
近期,笔者在对Golang木马程序进行研究分析的过程中,突然发现了一个小问题:
-
由于笔者分析的golang木马程序的通信方式是https,因此,为了能够在不修改样本二进制内容的前提下,完整的复现golang木马程序的攻击利用场景,就需要自行构建一个可响应https请求的WEB端程序;
-
在构建https WEB网站时,由于在本地测试环境,我们通常使用的就是自签名证书作为https WEB网站的证书;
-
通常情况下,按照上述流程操作,golang木马程序应该能成功访问https WEB网站;但是,在实际测试过程中,笔者发现golang木马程序无法成功访问https WEB网站;
-
基于以往木马分析经验,笔者
「推测golang底层函数应该是对证书进行了校验,默认情况下,自签名证书可能不会通过golang语言底层函数的标准证书校验。」
-
通过查看官方文档,发现在golang的crypto/tls库中:为了避免中间人攻击,golang语言通过InsecureSkipVerify标志控制客户端是否验证服务器的证书链和主机名;
-
「若是自己开发的程序,我们直接在源代码中将InsecureSkipVerify标志设置成true即可实现禁用证书校验的效果;但我们目前分析的是攻击活动中的golang木马程序,我们没有源代码,那又怎么来实现对InsecureSkipVerify标志的修改,对证书校验功能的禁用效果呢???」
相关官方文档截图如下:
尝试解决
为了能够解决上述问题,笔者尝试编写了多个测试程序,用于测试并对比。
测试一:默认开启证书校验
运行过程中,由于使用的是自签名证书,因此Client端无法成功访问Server端https WEB网站,相关报错信息如下:
-
Server端报错信息:
http: TLS handshake error from 127.0.0.1:58029: remote error: tls: bad certificate
-
Client端报错信息:
Failed to get response: Get "https://127.0.0.1": tls: failed to verify certificate: x509: certificate signed by unknown authority
运行截图如下:
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/", func(c *gin.Context) {
c.String(200, "Hello, HTTPS!")
})
//使用的是自签名证书
r.RunTLS(":443", "server.crt", "server.key")
}
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
)
func main() {
resp, err := http.Get("https://127.0.0.1")
if err != nil {
log.Fatalf("Failed to get response: %v", err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatalf("Failed to read body: %v", err)
}
fmt.Printf("Response Body: %s\n", body)
}
测试二:忽略自签名证书的验证
运行过程中,由于在代码中忽略了自签名证书的验证,因此,可成功访问Server端https WEB网站。相关运行截图如下:
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/", func(c *gin.Context) {
c.String(200, "Hello, HTTPS!")
})
//使用的是自签名证书
r.RunTLS(":443", "server.crt", "server.key")
}
package main
import (
"crypto/tls"
"fmt"
"io/ioutil"
"log"
"net/http"
)
func main() {
transport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // 忽略自签名证书的验证
}
client := &http.Client{Transport: transport}
resp, err := client.Get("https://127.0.0.1")
if err != nil {
log.Fatalf("Failed to get response: %v", err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatalf("Failed to read body: %v", err)
}
fmt.Printf("Response Body: %s\n", body)
}
InsecureSkipVerify标志跟踪
尝试在golang SDK中查找InsecureSkipVerify标志的调用关系,发现在crypto\tls\handshake_client.go代码中:
-
verifyServerCertificate函数将用于证书的校验;
-
在verifyServerCertificate函数中,将根据InsecureSkipVerify标志判断是否对证书进行校验;
相关代码截图如下:
尝试动态调试Server端源代码,发现证书校验行为确实是由verifyServerCertificate函数中的InsecureSkipVerify标志控制的。相关调试截图如下:
解决办法
尝试使用IDA反编译测试一和测试二中的Client端程序,查找verifyServerCertificate函数调用,发现在如下汇编代码中,实现了对InsecureSkipVerify标志的判断及函数跳转。
相关代码截图如下:
为了实现绕过https证书验证的需求,可尝试将程序代码中的函数跳转进行修改,修改方法如下:
-
将
「jnz跳转」
(0F 85)修改为跳转条件相反的
「jz跳转」
(0F 84)
-
为了能够快速定位或避免得到很多搜索结果,可将附近代码(41 80 BA A0 00 00 00 00 0F 85 82 01 00 00 49 8B)一起作为搜索条件用于定位。
相关修改后的运行截图如下:
不同条件下,二进制特征是否相同?
通过对测试程序进行测试,发现修改二进制特征后,可成功绕过HTTPS证书验证。
尝试使用相同手法对Golang木马程序进行修改,发现在Golang木马程序中无法找到匹配的二进制特征???
进一步分析,发现
「由于测试程序与Golang木马程序所使用的GO语言版本不一致,导致其二进制特征不同。」
-
Golang木马程序GO语言版本:go1.21.0
为了能够更全面的剖析二进制特征的影响因素,笔者从
https://go.dev/dl/
网站下载了不同版本的GO语言编译环境,尝试对不同条件下的二进制特征进行详细的对比,详细情况如下:
不同操作系统及位数
尝试使用相同版本的GO语言编译环境在不同操作系统上编译二进制文件,发现二进制特征与操作系统类型无关。
详细二进制特征对比情况如下:
操作系统
|
golang版本
|
原始二进制
|
修改后二进制
|
Kali_64
|
go1.23.0.linux-amd64
|
41 80 BA A0 00 00 00 00 0F 85
|
41 80 BA A0 00 00 00 00 0F 84
|
Kali_32
|
go1.23.0.linux-386
|
0F B6 4E 50 84 C9 0F 85
|
0F B6 4E 50 84 C9 0F 84
|
Win10
|
go1.23.0.windows-amd64
|
41 80 BA A0 00 00 00 00 0F 85
|
41 80 BA A0 00 00 00 00 0F 84
|
Win10
|
go1.23.0.windows-386
|
0F B6 4E 50 84 C9 0F 85
|
0F B6 4E 50 84 C9 0F 84
|
不同编译方式
尝试使用不同编译方式编译二进制文件,发现在不同编译方式下,生成的exe程序的二进制特征相同。详细情况如下:
-
go build -ldflags '-w -s'
-
garble build -ldflags="-s -w" -trimpath
不同GO语言版本
由于GO语言的版本实在太多,而且Go 语言从 1.21 版本开始不再支持 Windows 7系统,因此,笔者在这里就暂时只对GO 1.21.0版本至GO 1.23.0版本所生成的二进制文件进行对比分析。其他版本的对比情况,大家可基于以下方法进行复现。
为了能够自动化的使用不同版本的GO语言编译环境对程序进行编译,笔者尝试编写了一个批处理文件,脚本内容如下:
@echo off
setlocal
:: 设置包含目录的列表(用空格分隔)
set directories=go1.23.0.windows-amd64 go1.23.0.windows-386 go1.21.11.windows-amd64 go1.21.11.windows-386
:: 遍历每个目录并执行 go.exe build
for %%d in (%directories%) do (
echo C:\Users\admin\Desktop\golang\%%d\go\bin\go.exe
C:\Users\admin\Desktop\golang\%%d\go\bin\go.exe build -o %%d.exe
)
echo All done!
endlocal
尝试使用上述批处理脚本对
测试一
中的Client端源代码进行编译,编译后文件截图如下:
尝试使用相同解决办法定位不同版本的二进制特征,梳理二进制特征如下:
golang版本
|
原始二进制
|
修改后二进制
|
go1.21.0--go1.23.0 windows-amd64
|
41 80 BA A0 00 00 00 00 0F 85
|
41 80 BA A0 00 00 00 00 0F 84
|
go1.21.0--go1.22.6 windows-386
|
0F B6 7E 50 97 84 C0 97 0F 85
|
0F B6 7E 50 97 84 C0 97 0F 84
|
go1.23.0.windows-386
|
0F B6 4E 50 84 C9 0F 85
|
0F B6 4E 50 84 C9 0F 84
|
尝试使用go语言编写脚本实现批量化替换,脚本如下: