文章首发地址:
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
运行截图如下:
-
Server端源代码
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")
}
-
Client端源代码
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: %sn", body)
}
测试二:忽略自签名证书的验证
运行过程中,由于在代码中忽略了自签名证书的验证,因此,可成功访问Server端https WEB网站。相关运行截图如下:
-
Server端源代码
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")
}
-
Client端源代码
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: %sn", body)
}
InsecureSkipVerify标志跟踪
尝试在golang SDK中查找InsecureSkipVerify标志的调用关系,发现在cryptotlshandshake_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语言版本不一致,导致其二进制特征不同。」
-
测试程序GO语言版本:go1.21.6
-
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
-
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:UsersadminDesktopgolang%%dgobingo.exe
C:UsersadminDesktopgolang%%dgobingo.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语言编写脚本实现批量化替换,脚本如下:
package main
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
)
func WalkDir(dirPth, suffix string) (files []string, err error) {
files = make([]string, 0, 30)
suffix = strings.ToUpper(suffix)
err = filepath.Walk(dirPth, func(filename string, fi os.FileInfo, err error) error {
if fi.IsDir() { // 忽略目录
return nil
}
if strings.HasSuffix(strings.ToUpper(fi.Name()), suffix) {
files = append(files, filename)
}
return nil
})
return files, err
}
func replaceBinaryData(filePath string, oldData, newData []byte) error {
// 读取文件内容
fileContent, err := ioutil.ReadFile(filePath)
if err != nil {
return fmt.Errorf("failed to read file: %w", err)
}
// 查找并替换数据
modifiedContent := bytes.Replace(fileContent, oldData, newData, -1)
// 写回修改后的内容
err = ioutil.WriteFile(filePath, modifiedContent, 0644)
if err != nil {
return fmt.Errorf("failed to write file: %w", err)
}
return nil
}
func main() {
files, err := WalkDir("F:\GolandProjects\awesomeProject9", "exe")
if err != nil {
fmt.Println("Error:", err.Error())
}
for _, onefile := range files {
oldData1 := []byte{0x41, 0x80, 0xBA, 0xA0, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x85}
newData1 := []byte{0x41, 0x80, 0xBA, 0xA0, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x84}
oldData2 := []byte{0x0F, 0xB6, 0x7E, 0x50, 0x97, 0x84, 0xC0, 0x97, 0x0F, 0x85}
newData2 := []byte{0x0F, 0xB6, 0x7E, 0x50, 0x97, 0x84, 0xC0, 0x97, 0x0F, 0x84}
oldData3 := []byte{0x0F, 0xB6, 0x4E, 0x50, 0x84, 0xC9, 0x0F, 0x85}
newData3 := []byte{0x0F, 0xB6, 0x4E, 0x50, 0x84, 0xC9, 0x0F, 0x84}
datas, err := ioutil.ReadFile(onefile)
if err != nil {
fmt.Println(err.Error())
}
if bytes.Contains(datas, oldData1) {
err = replaceBinaryData(onefile, oldData1, newData1)
if err != nil {
fmt.Printf("Error: %vn", err)
} else {
fmt.Println("Binary data replaced successfully.")
}
} else if bytes.Contains(datas, oldData2) {
err = replaceBinaryData(onefile, oldData2, newData2)
if err != nil {
fmt.Printf("Error: %vn", err)
} else {
fmt.Println("Binary data replaced successfully.")
}
} else if bytes.Contains(datas, oldData3) {
err = replaceBinaryData(onefile, oldData3, newData3)
if err != nil {
fmt.Printf("Error: %vn", err)
} else {
fmt.Println("Binary data replaced successfully.")
}
}
}
}
二进制特征修改后的运行效果如下:
原文始发于微信公众号(T0daySeeker):如何绕过Golang木马的HTTPS证书验证
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论