前言

云原生生态中,golang语言开发的项目越来越多,例如Docker和K8s、etcd等。作为SRE、RD,偶尔需要在生产环境抓网络通讯包,用来分析排查故障。很多时候,都是tls/https加密协议,如何在不重启业务保留现场,不改为自定义CA证书的情况下,分析明文通讯内容呢?

适用场景

eCapture 0.5.0版本在2023年3月12日发布,支持了go语言编写的软件的tls/https明文抓包。只需要root权限,即可捕获并保存为pcapng格式,使用wireshark即可打开查看。

使用方法

eecapture gotls -h
bin/ecapture gotls -h
NAME:
    gotls - capture golang tls/https text content without CA cert for ELF compile by Golang toolchain

USAGE:
    ecapture gotls [flags]

DESCRIPTION:
    use eBPF uprobe/TC to capture process event data and network data. also support pcap-NG format.
    ecapture gotls
    ecapture gotls --elfpath=/home/cfc4n/go_https_client --hex --pid=3423
    ecapture gotls --elfpath=/home/cfc4n/go_https_client -l save.log --pid=3423
    ecapture gotls -w save_android.pcapng -i wlan0 --port 443 --elfpath=/home/cfc4n/go_https_client

OPTIONS:
  -e, --elfpath=""    ELF path to binary built with Go toolchain.
  -h, --help[=false]    help for gotls
  -i, --ifname="" (TC Classifier) Interface name on which the probe will be attached.
      --port=443    port number to capture, default:443.
  -w, --write=""  write the  raw packets to file as pcapng format.

GLOBAL OPTIONS:
  -d, --debug[=false]       enable debug logging
      --hex[=false]     print byte strings as hex encoded strings
  -l, --log-file=""       -l save the packets to file
      --nosearch[=false]    no lib search
  -p, --pid=0           if pid is 0 then we target all pids
  -u, --uid=0           if uid is 0 then we target all users

举个例子

/path/elf_filepath_compiled_by_go
package main

import (
    "crypto/tls"
    "fmt"
    "io"
    "net/http"
    "os"
)

func main() {

    b, e := GetHttp("https://github.com")
    if e == nil {
        fmt.Printf("response body: %snn", b)
    } else {
        fmt.Printf("error :%v", e)
    }
}

func GetHttp(url string) (body []byte, err error) {
  // 开启TLS密钥记录,用于跟eCpature捕获的密钥对比。
    f, err := os.OpenFile("/tmp/go_master_secret.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
    if err != nil {
        panic(err)
    }
    defer f.Close()
    c := &http.Client{
        Transport: &http.Transport{
            TLSClientConfig: &tls.Config{InsecureSkipVerify: true, KeyLogWriter: f},
        }}
    resp, e := c.Get(url)
    if e != nil {
        return nil, e
    }

    defer resp.Body.Close()
    body, err = io.ReadAll(resp.Body)
    return body, err
}
tcpdump
./ecapture gotls -e=/path/elf_filepath_compiled_by_go -w a.pcapng -i eth0

Wireshark打开网络包

下载地址

以下内容,为功能实现原理,若你只是使用,可跳过。


技术原理

Probe参数获取

这里有一张Golang的函数参数、返回值的寄存器传递布局,供参考。更多内容可以在线阅读《Go语言高级编程》中文版

eCapture的参数获取实现,可以阅读kern/go_argument.h

Probe参数选择

crypto/tls/common.gowriteKeyLog

Golang函数参数传递

writeKeyLogstringslice
// runtime/string.go
type stringStruct struct {
    str unsafe.Pointer
    len int
}

// runtime/slice.go
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}
stringstr unsafe.Pointerlen int

eCapture的实现:

lab_ptr = (void *)go_get_argument(ctx, is_register_abi, 2);
lab_len_ptr = (void *)go_get_argument(ctx, is_register_abi, 3);
cr_ptr = (void *)go_get_argument(ctx, is_register_abi, 4);
cr_len_ptr = (void *)go_get_argument(ctx, is_register_abi, 5);
secret_ptr = (void *)go_get_argument(ctx, is_register_abi, 7);
secret_len_ptr = (void *)go_get_argument(ctx, is_register_abi, 8);
bpf_probe_read_kernel(&lab_len, sizeof(lab_len), (void *)&lab_len_ptr);
bpf_probe_read_kernel(&cr_len, sizeof(lab_len), (void *)&cr_len_ptr);
bpf_probe_read_kernel(&secret_len, sizeof(lab_len), (void *)&secret_len_ptr);

Golang uretprobe

crypto/tls.(*Conn).writeRecordLockedcrypto/tls.(*Conn).Readuretprobeuretprobe
iovisor/bcc[Brendan Gregg](https://github.com/brendangregg)
uprobeReaduprobe

扩展阅读