如何绕过Golang木马的HTTPS证书验证

admin 2024年9月22日13:07:33评论11 views字数 6804阅读22分40秒阅读模式
文章首发地址:
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标志的修改,对证书校验功能的禁用效果呢???」

相关官方文档截图如下:

如何绕过Golang木马的HTTPS证书验证

尝试解决

为了能够解决上述问题,笔者尝试编写了多个测试程序,用于测试并对比。

测试一:默认开启证书校验

运行过程中,由于使用的是自签名证书,因此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

运行截图如下:

如何绕过Golang木马的HTTPS证书验证

  • 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网站。相关运行截图如下:

如何绕过Golang木马的HTTPS证书验证

  • 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标志判断是否对证书进行校验;

相关代码截图如下:

如何绕过Golang木马的HTTPS证书验证

尝试动态调试Server端源代码,发现证书校验行为确实是由verifyServerCertificate函数中的InsecureSkipVerify标志控制的。相关调试截图如下:

如何绕过Golang木马的HTTPS证书验证

解决办法

尝试使用IDA反编译测试一和测试二中的Client端程序,查找verifyServerCertificate函数调用,发现在如下汇编代码中,实现了对InsecureSkipVerify标志的判断及函数跳转。

相关代码截图如下:

如何绕过Golang木马的HTTPS证书验证

为了实现绕过https证书验证的需求,可尝试将程序代码中的函数跳转进行修改,修改方法如下:

  • 「jnz跳转」(0F 85)修改为跳转条件相反的「jz跳转」(0F 84)
  • 为了能够快速定位或避免得到很多搜索结果,可将附近代码(41 80 BA A0 00 00 00 00 0F 85 82 01 00 00 49 8B)一起作为搜索条件用于定位。

相关修改后的运行截图如下:

如何绕过Golang木马的HTTPS证书验证

不同条件下,二进制特征是否相同?

通过对测试程序进行测试,发现修改二进制特征后,可成功绕过HTTPS证书验证。

尝试使用相同手法对Golang木马程序进行修改,发现在Golang木马程序中无法找到匹配的二进制特征???

进一步分析,发现「由于测试程序与Golang木马程序所使用的GO语言版本不一致,导致其二进制特征不同。」

  • 测试程序GO语言版本:go1.21.6

如何绕过Golang木马的HTTPS证书验证

  • Golang木马程序GO语言版本:go1.21.0

如何绕过Golang木马的HTTPS证书验证

为了能够更全面的剖析二进制特征的影响因素,笔者从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

如何绕过Golang木马的HTTPS证书验证

  • go build -ldflags '-w -s'

如何绕过Golang木马的HTTPS证书验证

  • garble build -ldflags="-s -w" -trimpath

如何绕过Golang木马的HTTPS证书验证

不同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木马的HTTPS证书验证

尝试使用相同解决办法定位不同版本的二进制特征,梳理二进制特征如下:

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([]string030)
 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{0x410x800xBA0xA00x000x000x000x000x0F0x85}
  newData1 := []byte{0x410x800xBA0xA00x000x000x000x000x0F0x84}

  oldData2 := []byte{0x0F0xB60x7E0x500x970x840xC00x970x0F0x85}
  newData2 := []byte{0x0F0xB60x7E0x500x970x840xC00x970x0F0x84}

  oldData3 := []byte{0x0F0xB60x4E0x500x840xC90x0F0x85}
  newData3 := []byte{0x0F0xB60x4E0x500x840xC90x0F0x84}

  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.")
   }
  }
 }
}

二进制特征修改后的运行效果如下:

如何绕过Golang木马的HTTPS证书验证

原文始发于微信公众号(T0daySeeker):如何绕过Golang木马的HTTPS证书验证

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年9月22日13:07:33
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   如何绕过Golang木马的HTTPS证书验证http://cn-sec.com/archives/3167173.html

发表评论

匿名网友 填写信息