eCapture旁观者支持Golang tls/https加密明文捕获 前言 云原生生态中,golang语言开发的项目越来越多,例如Docker和K8s、etcd等。作为SRE、RD,偶尔需要在生产环境抓网络通讯包,用来分析排查故障。很多时候,都是tls/https加密协议,如何在不重启业务保留现场,不改为自定义CA证书的情况下,分析明文通讯内容呢? 适用场景 eCapture 0.5.0版本在2023年3月12日发布,支持了go语言编写的软件的tls/https明文抓包。只需要root权限,即可捕获并保存为pcapng格式,使用wireshark即可打开查看。 使用方法 gotls模块的e参数用来设定golang编译的可执行文件路径,可以通过ecapture 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是一个go写的web服务,并且开启了https加密,代码如下: 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: %s\n\n", 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打开网络包 下载地址 eCapture Github仓库:https://github.com/gojue/ecapture/releases/tag/v0.5.0 韩国GitHub镜像:https://ghproxy.com/https://github.com/gojue/ecapture/releases/tag/v0.5.0 以下内容,为功能实现原理,若你只是使用,可跳过。 技术原理 Probe参数获取 Golang的ABI不同于C,自定义了ABI机制。并且在go 1.17之前,使用的是栈方式传递调用参数;1.17以以后使用了寄存器方式传递调用参数。你可以阅读Register-based Go calling convention 了解更多知识。 这里有一张Golang的函数参数、返回值的寄存器传递布局,供参考。更多内容可以在线阅读《Go语言高级编程》中文版 eCapture的参数获取实现,可以阅读kern/go_argument.h Probe参数选择 笔者这里hook的是Golang源码目录下crypto/tls/common.go文件中的writeKeyLog函数。用来捕获tls的master secret的label类别、clientRandom、密钥值等。 Golang函数参数传递 有个需要注意的地方,比如writeKeyLog函数的第一个参数是string类型,第二、三个参数是slice类型。在Golang里,也都是一个结构体,如下代码: // runtime/string.go type stringStruct struct { str unsafe.Pointer len int } // runtime/slice.go type slice struct { array unsafe.Pointer len int cap int } 以string类型为例,在Go 的参数传递时,不光传递字符串的str unsafe.Pointer指针地址,也还会传递len 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 在eCapture的文本模式中,需要在加密之前、解密之后拿到明文,对应的两个函数分别是crypto/tls.(*Conn).writeRecordLocked和 crypto/tls.(*Conn).Read。加密之前的获取只需要使用eBPF uprobe HOOK即可实现。而解密之后,则需要uretprobe,但Golang里,uretprobe的实现机制,会破坏他的堆栈,导致Golang程序进程崩溃。 这个问题,在iovisor/bcc社区也有讨论:BCC issue: Go crash with uretprobe #1320,包括火焰图、eBPF的领导者Brendan Gregg,对这个问题也没有太好的办法。Gianluca Borello给了间接的解决方案,相对来说还是比较繁琐的,也有一定的crash风险,有兴趣的同学可以去看看。 eCapture里,加密之前的uprobe 已经完成hook,实现https/tls的请求内容明文捕获。但解密后的内容,暂时无法实现。笔者也在尝试其他思路,比如找到Read返回值的调用函数,在哪里使用uprobe实现,但这点逻辑比较偏业务层,调用者比较多,不太方便过滤。如果你有更好的办法,也欢迎提出来。 扩展阅读 Hooking Go from Rust - Hitchhiker’s Guide to the Go-laxy BCC issue: Go crash with uretprobe #1320 Interface method calls with the Go register ABI eCapture旁观者官网 eCapture旁观者 Github仓库 2023-03-12 4 0
如何使用Delve和eBPF更快地调试Go程序 前言 此文章将解释如何使用 Delve 跟踪 Go 程序,以及 Delve 如何利用 eBPF 在后台优化效率和速度。Delve 的目标是为开发人员提供愉快且高效的 Go 调试体验。因此,本文重点介绍了我们如何优化函数跟踪子系统,以便您可以更快地检查程序并找到根本原因分析。Delve 的跟踪实现有两个不同的后端,一个是基于 ptrace 的,另一个使用 eBPF。如果您不熟悉任何这些术语,请不要担心,我会随着解释一起说明。 什么是程序跟踪? 跟踪是一种允许开发人员在执行时看到程序正在做什么的技术。与典型的调试技术相反,这种方法不需要直接用户交互。最知名的跟踪工具之一是 strace,它允许开发人员在执行期间查看程序的系统调用。 尽管上述的 strace 工具对于了解系统调用很有用,但 Delve trace 命令允许您洞察 "用户空间" 中 Go 程序的情况。这种 Delve 跟踪技术允许您跟踪程序中的任意函数,以便查看这些函数的输入和输出。 此外,您还可以使用此工具了解程序的控制流,而无需交互式调试会话的开销,因为它还会显示正在执行该函数的 Goroutine。对于高度并发的程序,这可能是获得程序执行洞察力的更快方法,而无需启动完整的交互式调试会话。 如何使用 Delve 跟踪 Go 程序 Delve 允许您通过调用 dlv trace 子命令来跟踪 Go 程序。该子命令接受一个正则表达式,并将执行您的程序,在与正则表达式匹配的每个函数上设置跟踪点,并实时显示结果。 以下是示例程序: package main import "fmt" func foo(x, y int) (z int) { fmt.Printf("x=%d, y=%d, z=%d\n", x, y, z) z = x + y return } func main() { x := 99 y := x * x z := foo(x, y) fmt.Printf("z=%d\n", z) } 程序跟踪将给出以下输出: $ dlv trace foo > goroutine(1): main.foo(99, 9801) x=99, y=9801, z=0 >> goroutine(1): => (9900) z=9900 Process 583475 has exited with status 0 如您所见,我们在正则表达式中提供了 foo,它在这种情况下与主包中同名的函数匹配。以>为前缀的输出表示被调用的函数,并显示调用函数的参数,而以>>为前缀的输出表示从函数返回并与其相关联的返回值。所有输入和输出行均以在该时刻执行的 Goroutine 作为前缀。 默认情况下,dlv trace 命令使用基于 ptrace 的后端,但添加 --ebpf 标志将启用基于 eBPF 的实验性后端。使用上面的示例,如果我们要像以下方式调用 trace 子命令: $ dlv trace –ebpf foo 我们将收到类似的输出。但是,背后发生的情况要大大不同并且更加高效。 ptrace低效率 默认情况下,Delve 会使用 ptrace 系统调用来实现跟踪功能。ptrace 是一个系统调用,允许程序观察和操纵同一台机器上的其他程序。实际上,在 Unix 系统上,Delve 使用这个 ptrace 功能来实现调试器提供的许多低级功能,例如读写内存、控制执行等。 虽然 ptrace 是一个有用和强大的机制,但它存在固有的效率低下。首先,ptrace 是一个系统调用,意味着我们必须跨越用户空间/内核空间边界,这增加了每次使用函数时的开销。这是由于我们必须调用 ptrace 的次数越多,开销就越大。考虑前面的示例,以下是使用 ptrace 实现跟踪的大致步骤概述: 使用 ptrace(PT_ATTACH) 启动程序并附加调试器。 使用 ptrace 在匹配所提供的正则表达式的每个函数处设置断点,并在被跟踪的进程的可执行内存中插入断点指令。 另外,在该函数的每个返回指令处设置断点。 再次使用 ptrace(PT_CONT) 继续程序。 此步骤可能涉及多次ptrace调用,因为我们需要读取函数入口的CPU寄存器、堆栈上的内存以及如果必须取消指针引用的堆上的内存。 再次使用ptrace(PT_CONT)继续程序。 在函数返回时遇到断点,通过读取变量,可能涉及到更多的ptrace调用,以读取寄存器和内存。 再次使用ptrace(PT_CONT)继续程序。 直到程序结束。 显然,函数的参数和返回值越多,每次停止就越昂贵。所有调试器花费在进行 ptrace 系统调用的时间,我们跟踪的程序都处于暂停状态,没有执行任何指令。从用户的角度来看,这使得程序的运行速度比原本要慢得多。现在,对于开发和调试来说,这也许不是什么大问题,但是时间是宝贵的,我们应该尽量快速地完成事情。程序在跟踪过程中的运行速度越快,你就能越快找到问题的根本原因。 现在的问题是,我们如何使其更好呢?在下一节中,我们将讨论新的基于 eBPF 的后端,以及它如何改进这种方法。 eBPF 为何比 ptrace 更快 一个最大的速度和效率改进是避免大量的系统调用开销。这是 eBPF 发挥作用的地方,因为我们可以在函数入口和出口设置 uprobes,并将小 eBPF 程序附加到它们上。Delve 使用 Cilium eBPF Go 库加载和与 eBPF 程序交互。 每次触发 probe 时,内核将调用我们的 eBPF 程序,然后在它完成后继续主程序。我们编写的小 eBPF 程序将处理函数入口和出口中列出的所有步骤,但不会有所有的系统调用上下文切换,因为程序直接在内核空间中执行。我们的 eBPF 程序可以通过 eBPF 环形缓冲区和映射数据结构与用户空间中的调试器通信,使 Delve 能够收集所需的所有信息。 这种方法的优点是,我们正在跟踪的程序需要暂停的时间大大减少。在触发 probe 时运行我们的 eBPF 程序比在函数入口和出口处调用多个系统调用要快得多。 使用eBPF调试与跟踪步骤 这里再概括一遍使用 eBPF 跟踪调试的流程: 启动程序并使用 ptrace(PT_ATTACH) 附加到进程上。 在内核中加载所有需要跟踪的函数的 uprobes。 使用 ptrace(PT_CONT) 继续执行程序。 在函数入口和出口触发 uprobes。每当 probe 被触发,内核部分将运行我们的 eBPF 程序,该程序获取函数的参数或返回值,并将其发送回用户空间。在用户空间中,从 eBPF 环形缓冲区读取函数参数和返回值。 重复此过程直到程序结束。 通过使用这种方法,Delve 可以比使用默认的 ptrace 实现更快地跟踪程序。现在,你可能会问,为什么不将这种方法默认使用?事实上,未来很有可能会成为默认方法。但目前,仍在进行开发,以改进这种基于 eBPF 的后端并确保它与基于 ptrace 的后端具有平衡性。然而,您仍然可以在执行 dlv trace 时使用 --ebpf 标志来使用它。 为了给出一个使用不同跟踪方法的程序的效率差异的大致数字,我测量了另一个程序的运行情况,如下所示: Program execution: 23.7µs With eBPF trace: 683.1µs With ptrace tracing: 2.3s 数字本身就是最好的证明! 为什么不使用uretprobe 如果您熟悉 eBPF、uprobes / uretprobes,您可能会问为什么我们对一切都使用 uprobes,而不是仅使用 uretprobes 捕获返回参数。关于此的解释相当复杂,但简短版本是,Go 运行时在执行 Go 程序过程中需要多次检查调用堆栈。当 uretprobes 附加到函数时,它们将该函数的返回地址覆盖在堆栈上。当 Go 运行时检查堆栈时,它会找到该函数的意外返回地址,最终会导致程序致命退出。为了解决这个问题,我们只需使用 uprobes,并利用 Delve 的能力检查程序的机器指令来在每个函数的返回指令处设置探测器。 Delve使用eBPF更快地调试Go代码 Delve的总体目标是帮助开发人员尽快地找到Go代码中的错误。为此,我们利用最新的方法和技术,并试图推动调试器可以完成的范围。 Delve在内部利用eBPF来最大化效率和速度。用户空间跟踪是任何工程师工具箱中的重要工具,我们的目标是使其高效易用。 原文: How debugging Go programs with Delve and eBPF is faster 2023-02-14 0 0