如何绕过Golang木马的HTTPS证书验证
T0daySeeker 发表于 四川 历史精选 2733浏览 · 2024-09-13 06:06

思路来源

近期,笔者在对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: %s\n", 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: %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语言版本不一致,导致其二进制特征不同。

  • 测试程序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:\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语言编写脚本实现批量化替换,脚本如下:

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: %v\n", 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: %v\n", 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: %v\n", err)
            } else {
                fmt.Println("Binary data replaced successfully.")
            }
        }
    }
}

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

0 条评论
某人
表情
可输入 255