旁观者eCapture:支持龙芯,text 模式看完整 HTTP 关于新版本,笔者想聊聊两件事:跑上龙芯,以及更省心的 HTTPS 明文分析。 龙芯用户,这次安排上了 v2.8.0 开始,eCapture 增加 Linux/LoongArch64 支持,发行包认准 linux-loong64,提供 tar.gz 和 deb 两种格式。 龙芯用户也能沿用熟悉的 TLS 抓包方式,无需安装 CA 证书。 龙芯要求 Linux 内核 6.6 及以上,下载前先用 uname -r 看看。目前已完成交叉构建和模拟启动检查,真机完整抓包仍待验证,欢迎有设备的同学反馈。适配说明 text 模式,少一点“人工拼包” 以前看 text 输出,一个 HTTP 请求可能拆成好几段,内容抓到了,排查还得自己拼,多少有点费眼睛。 v2.9.0 给 tls、gotls、gnutls 增加了 pcapng 数据源,依赖 ecapture_decryptor 实时解密,重组完整 HTTP 消息。 时间、方向、两端 IP 和端口一起输出,HTTP/2 还有流标识,多连接交织时更容易定位。以 OpenSSL 为例: sudo ecapture tls -m text --text-source=pcapng -i eth0 'tcp port 443' 把 eth0 换成实际网卡,明文直接输出到终端。加上 --pcapfile capture.pcapng,还能留给 Wireshark 复查。 hook 仍是默认数据源;新链路目前支持 TCP 上的 HTTP/1.1、HTTP/2,尚不支持 HTTP/3。使用说明 其他修复 pcapng:修复突发流量下因用户态队列造成的丢包,退出时排空待写入的网络包和密钥。 配置与加载:重载时保留事件输出地址、轮转设置,统一 CO-RE/非 CO-RE 字节码选择。v2.8.0 记录 Android:v2.8.2 补回发行包中缺少的可执行文件,避免解压之后找不到程序。v2.8.2 记录 eCapture Decryptor,解密能力也能单独用 前面提到的 ecapture_decryptor,就是这条实时解密链路的关键:eCapture 负责抓包、提取密钥,它负责解密、重组 HTTP 消息。 下图能直观看到两种数据源的区别: 它也能独立解密抓包文件,作为 Go SDK 接入自己的工具,示例提供 JSON/HAR 导出。做流量分析平台的同学,欢迎试试,也顺手点个 Star。 新版下载:eCapture v2.9.0。 2026-10-31 0 0
eCapture v2 来了,AI Agent 写了 90% 的代码 写这篇文章的时候,eCapture 已经发布到 v2.2.1 了。从 v2.0.0 到现在,短短三个月内经历了 5 个版本迭代。 趁着版本刚稳,笔者来认真梳理一下 v2 系列到底做了哪些事——不只是给用户看的 Feature,还有架构层面那些"没有功劳只有苦劳"的内功修炼。 一、先说结论 v2 系列最核心的变化可以用一句话概括:把整个项目从"能用"变成了"可以持续演进"。 表面上,你可能感知到的是:GoTLS 支持连接四元组了,pcap 写入更稳了,Android 的兼容性更好了。 但水面以下,是整个内部架构的推倒重建——原来杂乱的 user/ 目录彻底删掉了,所有 8 个 Probe 被重新实现,工厂模式、观察者模式、模板方法模式,该用的设计模式全用上了。 第一部分:面向用户 1. GoTLS 终于有四元组了 很多同学用 eCapture 抓 Go 应用的 TLS 流量,一直有个痛点:抓到的明文没有连接信息,不知道这条流量来自哪个 IP、哪个端口。 v2.0.1 和 v2.1.0 里,@zenyanle 接连贡献了两个 PR(#947, #960),给 GoTLS Probe 加上了从 tls.Conn 里提取 fd、进而获取连接四元组的能力。 现在抓到的 GoTLS 明文,连接的源/目 IP 和端口都有了。对于要做流量审计或安全分析的同学,这个功能很关键。 2. pcap 写入更可靠了 v2.0.0 引入了缓冲 pcapng 写入,带上了接口元数据;v2.2.0 修掉了一个 Close() 里的竞态条件——之前在高并发场景下,DSB keylog 写入会出现数据乱序或丢失的情况。 现在写 pcap 文件,序列化保证了,关闭时也不会丢尾巴了。 3. Android 支持更成熟 BoringSSL Android 16 的 offset 更新(#885) Android 模拟器的 DNS 解析问题修复(#957) Android e2e PCAP 模式自动探测活跃网络接口(#976) Build tag 从 androidgki 统一改名为 ecap_android(#930) 对于在 Android 上用 eCapture 做流量分析的同学,v2 系列的稳定性比 v1 强了不止一个量级。 4. 重新支持 --cgroup_path 参数 v2.0.0 的大重构里,tls 子命令的 --cgroup_path 参数被不小心砍掉了。v2.2.1 把它恢复了(#975)。 用 cgroup 做隔离抓包的同学,可以放心升级了。 5. 全覆盖的 E2E 测试体系 v2.0.0 里加入了 72+ 个 E2E 测试场景,覆盖了 bash、tls、gnutls、gotls 全部模块,以及 pcap/keylog/text 三种抓包模式。 这意味着每次发版前,核心路径都会被自动验证一遍。用户升级时踩到"老功能突然坏了"这种坑的概率,大幅降低。 第二部分:面向开发者 Warning:以下是面向 eCapture 开发者或想给项目贡献代码的同学的内容,只是日常使用的话可以不用往下看。 1. 架构重构:user/ 目录彻底消失了 v1 时代,所有 Probe 的代码都堆在 user/ 目录下,随着支持的协议越来越多,这个目录越来越难维护——命名不统一、接口不一致、职责边界模糊。 v2.0.0 用 7 个 Phase 完成了完整的迁移: 所有 8 个 Probe(Bash、Zsh、MySQL、Postgres、OpenSSL、GnuTLS、NSPR、GoTLS)完成了标准化迁移。每个 Probe 现在都有统一的文件结构: internal/probe/<name>/ config.go # 配置,嵌入 BaseConfig,实现 Validate() event.go # 事件结构,实现 DecodeFromBytes() register.go # init() 里调用 factory.RegisterProbe() <name>_probe.go # 嵌入 BaseProbe,实现 Initialize/Start/Stop/Close 2. 核心设计模式 v2 引入了三个设计模式,彻底解耦了各个组件: 工厂模式:每个 Probe 在 register.go 的 init() 里自注册,CLI 命令通过 factory.NewProbe() 创建实例,互不依赖。 模板方法模式:BaseProbe 实现公共流程,子 Probe 只需覆写差异化逻辑。 观察者模式:事件通过 EventDispatcher 分发,Output Writer(file/pcap/keylog/websocket)作为订阅者,彼此完全解耦。 3. 事件流转全链路 从 eBPF 内核态抓到数据,到最终写入文件或推送到远端,完整路径如下: 这个链路在 v2 里被完整地标准化了。之前 v1 里部分 Probe 是直接写文件,绕过了 Dispatcher,导致远端推送(ecaptureQ 模式)下数据丢失——这个 Bug 在 v2.1.0 的 #964 里修掉了。 4. 破坏性变更备忘 如果你有基于 eCapture 源码开发的二次开发代码,升级 v2 需要注意以下几点: 变更项 v1 v2 Probe 代码位置 user/ internal/probe/ Build tag(Android) androidgki ecap_android eBPF bytecode 目录 项目根 ebpfassets/ Probe 注册方式 手动初始化 init() 自注册 + Factory 更丰富的官方文档 V2.0.0 里,官方网站也做了重构。 除了官网首页,使用文档也经历了大幅度的扩充和重构,分为概述、架构设计、捕获模块、输出格式、开发指南等多个章节,覆盖了从用户入门到开发者贡献的全流程。 番外:AI Agent 写了多少代码? 最近 AI Agent 是个大热词,笔者被问到最多的问题就是:eCapture v2 里,到底有多少代码是 AI 写的? 说个实际数据。从 v2.0.0 到 v2.2.1,GitHub 上合并了约 37 个 PR,分布如下: 贡献者 PR 数量 占比 @Copilot(GitHub Copilot AI Agent) 13 ~35% @cfc4n(笔者) 21 ~57% 社区贡献者(@zenyanle、@Carl Chen 等) 3 ~8% v2.0.0 那次大重构是 AI 参与度最高的版本:8 个 Probe 的标准化迁移、E2E 测试框架搭建,10 个 PR 直接由 @Copilot 提交,占当版 PR 总量的近 45%。 再看代码行数。说实话,这个数字笔者自己都没料到会这么极端。v2 系列 90% 的代码是 AI 写的,笔者的角色更像产品经理加 Code Reviewer。 贡献者 新增行数 删除行数 新增占比 @Copilot(AI Agent) 28,426 10,392 ~90% @cfc4n(笔者) 2,097 781 ~7% 社区贡献者 1,082 96 ~3% 但聪明的同学一定想问:AI 是怎么知道该做什么的? 答案是:每一个 AI PR 背后,都对应着笔者写的一个详细 Issue。任务描述写得越清楚,AI 出活越靠谱;描述含糊的,烂 PR 照样打回去重写。那段时间的工作流,基本是"笔者当产品经理+Code Reviewer,AI 当执行工程师"。 AI Agent 擅长的是模式清晰、重复度高的任务——比如"按照这个标准模板,把剩余 6 个 Probe 全部重构一遍",这种活交给 AI 效率极高。但涉及 eBPF 内核行为的细节、并发竞态的根因分析,还是得靠人来兜底。 v2 这次实践下来,笔者的感受是:AI Agent 不是替代开发者,而是把开发者从重复劳动里解放出来,让人能把精力放在真正需要判断力的地方。 最后 v2 是一次迟到很久的重构。老实说,v1 时代的 user/ 目录,笔者自己看着都头疼。这次借着 AI Coding Agent 的帮助,总算把欠的技术债还掉了大半。 后面 v2.x 的迭代重心会放在功能上——更多 TLS 库版本支持、更丰富的过滤能力、更完善的远端推送协议。架构的活算是告一段落了。 有问题欢迎到 GitHub 提 Issue,PR 更欢迎。 GitHub: https://github.com/gojue/ecapture 官网: https://ecapture.cc 2026-04-06 0 0
eCaptureQ:给1.5万星标的开源项目eCapture装个酷炫的驾驶舱! 嘿,eCapture 旁观者 的铁子们! 既然你摸到了这里,想必你也是 eCapture 这款“黑科技”的忠实粉丝了。能在 GitHub 上豪取 1.5万+ Star,成为网络抓包领域的明星项目,靠的就是它那不讲道理的硬核实力:不用装证书,不用搞中间人代理,直接在内核里把 TLS 明文给你安排得明明白白。这种感觉,就像开了“上帝视角”,爽! eCapture 作为一款 CLI 工具,强大、纯粹,是极客的浪漫。但说实在的,浪漫归浪漫,当真正在炮火连天的生产环境或复杂的调试场景里干活时,我们都或多或少地被这“朴素”的命令行界面给“伤害”过。 CLI 的“甜蜜”烦恼:眼花缭乱,手忙脚乱 你是不是也对这些场景感同身受? “信息瀑布”糊脸 sudo ecapture tls 命令一敲,你的终端就变成了“信息瀑布”现场,日志刷得比瀑布还快。想在流量高峰期用肉眼找到某个特定的请求?祝你好运!这哪是抓包,这简直是考验眼速的极限挑战。 玩“大家来找茬”游戏 一个请求(Request)发出去,它的响应(Response)在哪儿呢?它可能在上面,也可能在下面,还可能被一堆其他日志夹在中间。你只能瞪大眼睛,在滚动的文本里玩“大家来找茬”的游戏,费时费力还容易看错行。 跟 grep 和 awk 斗智斗勇 想过滤一下?没问题,grep、awk、sed 三件套伺候。但每次想实现一个稍微复杂点的过滤,比如“找出来自 api.example.com 并且响应码是 500 的 POST 请求”,都得先跟一长串的正则表达式和管道符斗智斗勇一番。心累啊! 只见树木,不见森林 线性的文本输出,让我们很难对全局流量有个直观的认识。当前到底有几个域名在通信?哪个接口访问最频繁?哪些请求慢得像蜗牛?光看日志,一头雾水。 eCapture 已经帮我们搞定了从内核捞数据这个最硬核的活儿。但怎么把这些原始数据舒舒服服地看明白,一直是个“历史遗留问题”。 现在,是时候终结这一切了!隆重介绍 eCaptureQ! 在此,特别鸣谢Hugo同学, 他是eCaptureQ项目的主要推动者、设计者,感谢他为这个项目的贡献。 eCaptureQ:鸟枪换炮,体验起飞! eCaptureQ 不是要取代 eCapture,恰恰相反,它是 eCapture 的“灵魂伴侣”和“官方指定驾驶舱”。它把 eCapture 强大的引擎能力,用一个现代化的图形界面给你呈现出来,让你从手摇拖拉机一步到位,坐进全景天窗的智能驾驶舱。 eCaptureQ 就是专为解决上面那些痛点而生的: 1. 从“刷屏”到“列表”,一目了然 eCaptureQ 做的第一件事,就是把 eCapture 吐出来的原始文本流,实时变成了一个结构化的请求列表。就像你天天在用的浏览器开发者工具一样。 清爽! 主机、方法、URL、状态码、耗时……所有关键信息整整齐齐地排列好,告别眼花缭乱。 省心! 请求和响应自动配对,再也不用玩“找茬”游戏了。 2. 告别命令行,指哪打哪 忘掉那些复杂的 grep 命令吧,现在你只需要动动鼠标和键盘。 随心搜:顶部的搜索框就是你的魔杖,输入域名、路径、状态码,列表瞬间过滤出你想要的结果。 随便点:想看哪个请求的详情?点它!完整的请求头、请求体、响应头、响应体立刻呈现。JSON 自动格式化高亮,简直不要太贴心。 任意排:想找最慢的请求?点一下“耗时”列头,一秒排序,性能瓶颈无所遁形。 3. 两种模式,怎么舒服怎么来 我们懂你,eCapture 的用户遍布天下,工作姿势也千奇百怪。eCaptureQ 提供了两种模式,完美适配你的工作流。 集成模式(单机版) 如果你习惯在本地 Linux 或安卓上干活,这个模式最适合你。eCaptureQ 自带了 eCapture 核心程序,sudo 启动,点击 "Start",开箱即用,一条龙服务。 远程模式(客户端/服务端分离) 这才是真正的王炸!我们知道,你的 eCapture 很多时候都跑在远端的服务器、虚拟机甚至 Docker 里。 现在,你可以在服务器上用 --ecaptureq 参数启动 eCapture,它会开启一个 WebSocket 服务。然后,你就可以在自己的 Windows 或 macOS 笔记本上,悠闲地打开 eCaptureQ 客户端,远程连上它! 这意味着什么? 优雅! 在你舒服的 Mac 上,用着精美的 GUI,分析远端 Linux 服务器的流量。 安全! 让需要 root 权限的脏活累活在远端服务器上跑,你在本地用普通用户愉快地分析,安全又省心。 协同! 多个小伙伴可以连到同一个 eCapture 实例上,一起围观分析,效率翻倍。 使用方式 启动eCapture 比如,先在Android等系统上开启eCapture的抓包: sudo ./ecapture tls --ecaptureq ws://0.0.0.0:28257 打开eCaptureQ查看 在你的桌面系统中,打开eCaptureQ,你将在界面上看到被捕获系统上的TLS明文通讯消息。 一句话总结 eCaptureQ 就是 eCapture CLI 的一次体验革命。它保留了 eCapture 100% 的硬核能力,同时把交互体验从“命令行时代”直接拉到了“图形化时代”。 它就是你期待已久的那个现代化仪表盘,让你能真正聚焦于数据分析本身,而不是和工具较劲。 别犹豫了,快去下载 eCaptureQ,给你的 eCapture 引擎配上它应得的酷炫驾驶舱吧! 2025-11-02 0 0
eCapture开源项目三周年, v1.0.0 稳定版发布 eCapture旁观者是一款基于 eBPF 技术的高级网络工具,专注于 SSL/TLS 协议的明文数据捕获与解密。凭借其强大的功能和丰富的特性,eCapture 已成为网络调试和安全分析领域的重要工具。自2022年3月发布至今三年,累计1.4万颗星。 核心功能 1. 多协议支持 加密库支持:兼容 OpenSSL、GnuTLS、BoringSSL 等多种加密库,支持 HTTP/2 和 QUIC 协议捕获。 协议兼容性:涵盖 TLS 1.2 和 TLS 1.3,确保对最新协议的全面支持。 2. 智能数据捕获 eBPF 引擎:基于 eBPF 技术实现高效的网络数据捕获和协议解析。 双栈支持:支持 IPv4 和 IPv6 的双栈模式,确保在不同网络环境中的灵活应用。 四元组过滤:提供灵活的过滤功能,精准捕获目标流量。 3. 主密钥捕获 自动化捕获:自动检测并捕获 TLS 1.2 和 TLS 1.3 的主密钥,实现加密流量的实时解密。 Wireshark 集成:生成与 Wireshark 兼容的流量文件,支持离线解密和分析。 4. 多种捕获模式 Pcap 模式:生成 Pcapng 格式的流量文件,便于后续分析。 Keylog 模式:捕获通信密钥,生成与 Wireshark 兼容的密钥日志文件。 Text 模式:实时输出明文数据,方便开发者快速调试和分析。 5. 跨平台支持 操作系统:支持 Linux 和 Android 系统,确保在不同操作环境中的稳定运行。 架构兼容:兼容 x86_64 和 ARM64 架构,适用于多种设备和服务器环境。 6. 自动化与智能检测 智能模式选择:自动识别 CO-RE 和非 CO-RE 模式,优化运行性能。 内存优化:提供灵活的内存配置选项,提升资源利用率和系统稳定性。 7. 高性能与兼容性 高效处理:基于 eBPF 的高效数据处理,支持大规模并发和长时间抓包。 稳定性提升:经过多次版本迭代和优化,v1.0.0 版本具有更高的稳定性和兼容性。 8. 灵活配置 自定义过滤:支持灵活的数据过滤配置,精准捕获目标流量。 多种输出格式:支持多种数据输出格式,满足不同场景的需求。 日志管理:提供详细的日志记录,方便后续分析和审计。 9. 命令审计 Shell 审计:捕获 Bash 和 Zsh 命令行的输入和输出,支持实时监控和记录。 数据库监控:监控 MySQL 和 PostgreSQL 的 SQL 查询,提供数据库操作审计功能。 10. Wireshark 插件支持 流量分析:生成兼容 Wireshark 的流量文件,方便使用 Wireshark 进行深入分析。 密钥日志集成:支持在 Wireshark 中加载密钥日志,实现加密流量的实时解密。 技术特点 1. 模块化架构 功能扩展:采用模块化设计,便于功能扩展和维护。 灵活配置:支持多种配置选项,满足不同用户的需求。 2. 多语言开发 内核空间:使用 C 语言实现内核空间功能,确保高效和稳定。 用户空间:使用 Golang 和 CGO 实现用户空间功能,提升开发效率和代码维护性。 3. 交叉编译支持 多架构支持:支持在 amd64 和 ARM64 两种架构下进行交叉编译,生成适用于不同平台的二进制文件。 工具链支持:提供丰富的交叉编译工具链,方便用户根据需求进行编译和配置。 版本更新 v1.0.0 版本 新增功能 支持 Docker 镜像,简化部署和使用。 新增对 OpenSSL 3.4.0 和 GnuTLS 的密钥捕获支持。 支持 IPv6 数据包捕获,扩展网络协议支持。 增加 Zsh 命令审计功能,提升安全监控能力。 关键修复 修复 Ubuntu 24.04 系统上初始化脚本的兼容性问题。 解决 ARM64 版本无法正常工作的问题。 优化构建过程,提升整体稳定性和性能。 社区与开发 开源社区 项目星数:自 2022 年 3 月创建以来,累计获得 14,000+ 颗星,社区活跃度高。 贡献者:吸引了众多开发者参与贡献,共同推动项目发展。 作者信息 开发者:由陈驰(CFC4N)开发,详见其个人博客 www.cnxct.com。 开源精神:秉持开源精神,欢迎更多开发者加入社区,共同改进和完善项目。 部署与使用 易用部署 二进制文件:提供预编译的二进制文件,支持 Linux 和 Android 系统。 Docker 镜像:发布 Docker 镜像,简化部署和使用过程。 文档支持:提供详细的使用文档和配置指南,帮助用户快速上手。 总结 eCapture v1.0.0 是一款功能强大、技术先进的网络工具,凭借其多协议支持、智能数据捕获、跨平台兼容性和高性能,成为开发者和安全研究人员的得力助手。通过三年的不断开发和优化,eCapture 在网络调试和安全分析领域树立了良好的口碑。 未来,随着社区的不断贡献,eCapture 将继续保持其技术领先,服务于更多用户。 2025-08-22 0 0
eCapture v0.9.0支持IPv6、GnuTLS密钥捕获、Zsh审计,国内CDN上线 自上篇文章〔L4LB四层负载均衡IP伪造漏洞〕以来,已经快四五个月没更新,不是笔者太懒,而是实在太忙了,忙得连放屁的时间都没有。得益于社区朋友的贡献,增加了一些新特性,今天来分享给大家。 eCapture v0.9.0版本发布 新功能: 支持Docker镜像拉取,添加对 OpenSSL 3.4.0 的支持,支持 gnutls 的 keylog 和 pcap 模式,允许捕获 IPv6 数据包,拆分日志记录器。 错误修复: 修复了多个系统和功能的关键错误,包括在 Ubuntu 24.04 上的初始化脚本问题和 arm64 版本的兼容性问题。 改进: 更新了日志记录器,优化了构建过程,添加了新的工作流和文档更新。 科普:eCapture是什么 eCapture旁观者是一个无需CA证书,无侵入的HTTPS/TLS明文抓包工具。可以在Linux 4.18以上版本使用,同时也支持Android arm64 5.5以上版本。项目发布三年累计14000颗星。 功能介绍 eCapture 有8个模块,分别支持openssl/gnutls/nspr/boringssl/gotls等类库的TLS/SSL加密类库的明文捕获、Bash、Mysql、PostGres软件审计。 bash 捕获bash、zsh命令行的输入输出 gnutls 捕获基于gnutls类库加密通讯的明文内容 gotls 捕获使用Golang语言编写的,基于内置crypt类库实现TLS/HTTPS加密通讯的明文内容 mysqld 捕获Mysqld的SQL查询,适用于数据库审计场景,支持Mysqld 5.6/5.7/8.0等 nss 捕获基于nss类库加密通讯的明文内容 postgres 支持postgres 10+的数据库审计,捕获查询语句 tls 捕获基于Openssl/Boringssl的加密通讯的明文内容,支持Openssl 1.0.x/1.1.x/3.x系列所有版本,支持BoringSSL所有发行版本 你可以通过ecapture -h来查看这些自命令列表。 重大更新 支持Docker Image镜像启动,可以到Docker Hub下载。 支持gnutls模块的keylog 和 pcap 模式对Gnutls TLS类库的密钥捕获、pcapNG格式文件存储; 支持文本模式-m text下,HTTP 2.0的内容解码; 支持IPv6 协议的网络包捕获; 支持非Android的boringSSL库加密通讯的明文、密钥、pcapNG捕获; 支持Zsh Shell的命令执行安全审计; 支持OpenSSL模块的文本模式下,五元组关联数据; 增加eCapture项目的国内下载地址:https://ecapture.cc/zh/download/ (自费CDN小水管,各位黑客大哥别把我帐号刷欠费了) Gnutls TLS类库捕获 在eCapture的gnutls模块中,新增TLS加密通讯的密钥捕获,以及保存为pcapNG格式文件。 目标进程类库确认 以使用gnutls类库的git命令为例,当执行git pull时,会调用git-remote-http程序来访问远程仓库,该程序使用Gnutls类库 ldd /usr/lib/git-core/git-remote-http linux-vdso.so.1 (0x0000ffff9c6ff000) libcurl-gnutls.so.4 => /lib/aarch64-linux-gnu/libcurl-gnutls.so.4 (0x0000ffff9c550000) libpcre2-8.so.0 => /lib/aarch64-linux-gnu/libpcre2-8.so.0 (0x0000ffff9c4b0000) ... 演示过程 完整演示如下: 首先,在终端中启动ecapture sudo ecapture gnutls -m pcapng -i ens160 -w gnutls-github.pcapng tcp port 443 之后,在另外一个终端拉取git仓库 git pull 演示结果 eCapture的结果输出: sudo ecapture gnutls -m pcapng -i ens160 -w gnutls-github.pcapng tcp port 443 2024-12-15T13:31:38Z INF AppName="eCapture(旁观者)" 2024-12-15T13:31:38Z INF HomePage=https://ecapture.cc 2024-12-15T13:31:38Z INF Repository=https://github.com/gojue/ecapture 2024-12-15T13:31:38Z INF Author="CFC4N <cfc4ncs@gmail.com>" 2024-12-15T13:31:38Z INF Description="Capturing SSL/TLS plaintext without a CA certificate using eBPF. Supported on Linux/Android kernels for amd64/arm64." 2024-12-15T13:31:38Z INF Version=linux_arm64:v0.9.0:6.5.0-1025-azure 2024-12-15T13:31:38Z INF Listen=localhost:28256 2024-12-15T13:31:38Z INF eCapture running logs logger= 2024-12-15T13:31:38Z INF the file handler that receives the captured event eventCollector= 2024-12-15T13:31:38Z INF listen=localhost:28256 2024-12-15T13:31:38Z INF https server starting...You can upgrade the configuration file via the HTTP interface. 2024-12-15T13:31:38Z INF Kernel Info=5.15.167 Pid=150461 2024-12-15T13:31:38Z INF BTF bytecode mode: CORE. btfMode=0 2024-12-15T13:31:38Z INF GnuTlsProbe init eBPFProgramType=PcapNG model=PcapNG 2024-12-15T13:31:38Z INF module initialization. isReload=false moduleName=EBPFProbeGNUTLS 2024-12-15T13:31:38Z INF Module.Run() 2024-12-15T13:31:38Z INF GnuTLS version found Version=3.7.3 2024-12-15T13:31:38Z INF GnuTLS binary path binaryPath=/lib/aarch64-linux-gnu/libgnutls.so.30 elfType=2 2024-12-15T13:31:38Z INF BPF bytecode loaded bytecode filename=user/bytecode/gnutls_3_7_3_kern_core.o 2024-12-15T13:31:38Z INF Hook type: Gnutls elf ElfType=2 IFindex=2 IFname=ens160 PcapFilter="tcp port 443" binrayPath=/lib/aarch64-linux-gnu/libgnutls.so.30 2024-12-15T13:31:38Z INF Hook masterKey function: gnutls_handshake 2024-12-15T13:31:38Z INF target all process. 2024-12-15T13:31:38Z INF packets saved into pcapng file. pcapng path=/home/cfc4n/gnutls-github.pcapng 2024-12-15T13:31:39Z INF perfEventReader created mapSize(MB)=4 2024-12-15T13:31:39Z INF perfEventReader created mapSize(MB)=4 2024-12-15T13:31:39Z INF module started successfully. isReload=false moduleName=EBPFProbeGNUTLS 2024-12-15T13:31:43Z INF CLIENT_RANDOM save success ClientRandom=8517a6741b2a67369079309c2eac84094aacecef39a5514c6da25da7e219401d TlsVersion=GNUTLS_TLS1_3 eBPFProgramType=PcapNG 2024-12-15T13:31:44Z INF packets saved into pcapng file. count=35 2024-12-15T13:31:46Z INF packets saved into pcapng file. count=2 ^C2024-12-15T13:32:15Z INF packets saved into pcapng file. count=37 2024-12-15T13:32:15Z INF Module closed,message recived from Context 2024-12-15T13:32:15Z INF iModule module close 2024-12-15T13:32:15Z INF bye bye. 可以清楚得看到TLS通讯协议是GNUTLS_TLS1_3,密钥的ClientRandom是8517a6741b2a67369079309c2eac84094aacecef39a5514c6da25da7e219401d。 Wirehark查看网络包 如图,不光能看到网络包对应发起的进程信息,还能看到HTTPS的明文内容。 Docker Hub eCapture的Docker Hub仓库地址为:https://hub.docker.com/r/gojue/ecapture ,当前最新版为 v0.9.1 。 # 拉取镜像 docker pull gojue/ecapture:latest # 运行 docker run --rm --privileged=true --net=host -v ${宿主机文件路径}:${容器内路径} gojue/ecapture ARGS Zsh Shell审计 启动命令sudo ecapture zsh,即可捕获zsh上运行的所有命令,对于安全审计场景,还是比较方便快捷的。 其他更多功能,请查看项目变更日志。 变更日志 功能 feat: 在 gnutls 中支持 keylog 和 pcap 模式 feat: 移除 tcp 数据包限制 kern: 支持 openssl 最新版本(3.4.0, 1.1.1w 等) feat: 以文本模式解析 http2 数据 feat: 允许捕获 ipv6 数据包 kern: 在 ebpf TC hook 中支持 uid/pid 过滤 user: 拆分记录器,将其分为记录器和事件收集器 kern: 添加对非 Android boringssl 库的支持 kern: 调整密钥获取的时机,以区分 TLS feat: 支持捕获 zsh 命令 feat: 用远程地址信息丰富地址信息 feat: 支持新版本检测功能 Bug 修复 builder: 修复 init 脚本在 ubuntu 24.04 系统上无法运行的问题 #667 修复 uprobe 类型 hook 的参数错误问题 修复 arm64 版本无法工作的 bug (#649) builder: docerk 构建错误:未找到头文件 修复 Golang TLS 模块中读取函数返回值的问题 fix: 修复在调试模式下 make 时未声明标识符的错误 fix: 回退到默认版本并发出警告 fix: 创建 output.log 失败 pkg: 修复在关闭的通道上发送数据的问题 fix: HEAD 请求中的 DumpResponse 错误 fix: 截断的主体转储错误 user: 修复 #553,hashLen 不允许超过 64 字节 android 版本编译失败 user: 修复 #542,masterkey 被多次写入 pcapng fix: pcap 过滤器未按预期工作 fix: 修复 ecapture Docker 镜像 CVE-2024-24790 fix: 处理器打印 incoming chan is full 并退出 改进 cli: 使用格式化记录器 rs/zerolog chore: 在构建过程中使用 -tags 'netgo' 以避免由于不同 Linux 发行版中的 glibc 版本不同而导致的 SIGSEGV 常量参数通知 cli: 更新 docker 使用说明 添加可能的 Linux 内核配置路径 user: 在 gotls 模块中格式化 clientRandom 字符串 cli: 支持记录器级别 使用 kprobe/__sys_connect 替代 uprobe/connect makefile: 优化内核头文件的生成机制 添加 dockerfile BPF 名称应在 _core/_noncore 后附加 user: 为面向服务的架构做准备 docs: 修复日语翻译 chore: 在 rpmBuild.spec 中移除未使用的标志 BuildRequires workflows: 更新 Linux 源 tgz 文件版本 build(deps): 将 golang.org/x/crypto 从 0.23.0 升级到 0.31.0 feat: 检测 CAP_BPF pkg: 添加 http2 请求/响应单元测试 feat: 在销毁 socket 时清理映射 user: 调整内核版本显示的时机,避免过低 感谢 感谢社区小伙伴的贡献:Leon Hwang、SenberHu、yuweizzz、sancppp、Nadeshiko Manju、行旅途、w568w 等,一起继续为爱发电。 2024-12-26 0 0
详解:L4LB四层负载均衡IP伪造漏洞 前言 去年11月,在国家信息安全漏洞共享平台CNVD、国家信息安全漏洞库CNNVD报告过TOA的IP伪造漏洞,到今天快过去1年了,各受影响方也基本修复完毕,今天聊一下细节吧。 回顾 当初演示时,使用了百度搜索里的IP查询接口。 演示视频中,通过muou程序,参数中指定任意IP,即可使得当前电脑的出口IP为指定的任意IP。 炒冷饭? 有同学猜测这是HTTP的Header追加伪造?有同学说这是装神弄鬼,炒冷饭?有同学说自己早发现了公司内部不重视,不修复。 那么,这漏洞到底是什么?? 网上有聪明的同学,有一定IP伪造基本知识,结合我在推特上的历史内容,很快重现了演示视频里的IP伪造,即TOA(TCP Option Address)伪造IP,那么仅仅如此吗? 漏洞描述: 显然不是。不过,对了一点。 大型IDC内部客户端IP如何透传? L4LB四层负载均衡后业务需要使用用户原始IP,这是常见的功能需求。在大型IDC内部,一般会在RS节点上部署TOA(TCP Option Address)内核模块,用来获取TCP Option中的原始IP,也就是下图中TCP Segment部分。 漏洞如何出现? 开源的4层负载均衡,在FNAT模式下,未能很好的清除TCP Option中恶意构造的TOA信息,将恶意数据透传至RS服务器,导致业务服务器取到伪造IP。 危害是什么? 业务受损 IP在很多Web防火墙、反爬虫系统、防刷系统(薅羊毛)是用于做策略控制的基础强依赖,IP的伪造将导致这些系统完全失效,造成极大的风险损失。 信息安全风险 在很多后台系统中,IP同样被用于ACL的网络边界数据,IP的伪造,也依旧成成为可以突破的入口,比如对特定IP加白,直接放行等等。 我是如何发现IP伪造漏洞的? 这个漏洞原理确实如阿里的安全专家pyn3rd在多年前所写,直接在TCP Option里追加即可,网上聪明的同学也猜到了。 不过,笔者并不是安全研究相关,平时从事的工作也是安全产品的研发,没有看过那篇PPT。发现这漏洞时,也是意外。在研发一款零信任四层负载均衡产品时,为了实现IP透传使用了相关技术。扩展一下,便想到了这里可能存在安全问题,顺便验证了各大厂的IP伪造可能性。 国内友商情况 国内大厂,不管是腾讯、华为、阿里、小米、阿里、百度等等,几乎都是使用了FNAT TOA的技术向后透传IP,很多技术方案都源于早期的开源项目LVS(Linux Virtual Server) ,各大厂按照各自的需求逐步迭代,有些机制上的安全问题,也被保留下来了。当然也有部分是IOA(IP Option Address),为了兼容UDP协议的IP透传。 相似之处 区别无非是opcode是28,是200还是254 还是 253,亦或是其他。也无非是opsize 是8 还是精简的7。 也无非是读取TOA信息的时机,是1SYN还是3SYN-ACK ,亦或是为了兼容而都去读取。整体来看,大同小异。 差异 而最大的差异在于L4LB处理来自客户端TCP Option 既有信息。 已有TOA信息清理还是不清理? 相同opcode如何处理? LB串联时,如何保留客户端(来自LB)的TOA信息? 原包内容中的TCP Option长度已经满了,怎么办? 而这些情况只是L4LB的难题,那么对于RS服务器上的TOA 内核模块又会有哪些问题呢? 读到TOA 信息后,要继续读,还是终止读取了? 继续读的话,又读到了TOA,那么选择哪个? 都读完了,却没有读到TOA信息怎么办? 带着这些问题,我们回头看下本次漏洞的各厂商表现。 入门级漏洞 演示视频中的漏洞,是百度引入的外部IP查询API,这个API后端L4LB也是使用了TOA技术向后透传IP,因为LB跟后面RS服务器配合问题,造成了客户端可伪造IP的问题。 此漏洞的发生有以下几个条件 L4LB产品使用FNAT模式传递客户端IP L4LB未清除客户端TCP协议中Option信息,自己也追加了一个TOA,之后就直接向后传递 服务器主机启用TOA模块获取IP,且解析Option时,读到了伪造的IP,没有读到L4LB追加的IP TOA加在前面还是后面? L4LB 把TOA 放在TCP Option的最前面,还是最后面呢?这里就涉及到RS上的TOA内核模块读取机制了,这里也是很容易出漏洞的地方。 当然,也不排除是L4LB读取到TCP Option中已经有了TOA信息,便不在追加。(这在L4LB串联的场景中,是十分常见的做法)。 显然,这种实现方式,大概率是L4LB 这边问题很大,利用成本很低。 进阶级漏洞 国内大部分厂商都不存在这么简单的漏洞,那么意味着他们都没问题吗? 然而并不是。 L4LB的难题:TCP Option 满了 回到前面的问题,L4LB收到的客户端TCP Option 的长度已经超过最大长度40字节了,那么L4LB怎么处理? 没有空间去追加客户端IP了,还要读取吗? 不读 很多L4LB丢弃本次真实客户端IP 的诉求,直接透传给下游了。 问题就出现在这里,那么,只要客户端恶意填满TCP Option ,不留给L4LB追加的机会,即可伪造任意IP给下游。 而这就是国内大厂出现这个安全漏洞的地方。 注意,我这里用了下游,并没有说是RS,很多时候是L4LB串联,那么它的下游还是L4LB了。 读取 如果L4LB发现已经满了,无法追加了,那么他要继续读取,而且要要清理、删除,才能规避这类问题。 DPVS 的漏洞 LB侧(DPVS): dpvs中,在src/ipvs/ip_vs_proto_tcp.c文件的tcp_in_add_toa函数中,387行附近,判断当前TCP包中TCP option是否有足够空间来追加TOA结构体。若不够,则直接返回,够则追加。 在 TCP OPTION规范中,长度为40字节。在DPVS 的TOA定义长度为8字节(ipv6为20字节)。 RS侧(TOA): 内核模块TOA hook了获取IP的函数,来获取客户端IP,在toa.c里最终是调用get_toa_data来获取。 以IPv4为例,其中378行开始,读取到TCP Option中数据,匹配TOA的特征后,即返回。IPv6也是类似问题,不再赘述。 变态级思路 读取TCP Option ,清空TOA信息,就高枕无忧了吗?然而并不是,那思路是什么? 如果黑客构造了一个完全合规的TCP Option ,并且长度超过40字节,里面没有恶意的TOA IP,也不留给L4LB留下填充客户端IP的机会,那么,后端的RS 服务器,会拿到什么IP? 还能造成安全风险吗? 客户端拿到的是L4LB的内网IP,难道把这些内网IP拉入黑名单吗?你确定敢这么干? 内网IP 再说了,内网IP一般会出现在你的各种风控策略、ACL策略中吗? 思路这么多,那么到底该如何修复呢? TOA 问题修复 通过前面的原理讲解,聪明的你一定知道要如何修复这个安全漏洞了。 也就是说整个TCP Option都要逐一字节读取、清理,防止有多个TOA的信息在里面。然而,L4LB这种产品,QPS都是十万、几十万的性能挑战,突然每个网络包都要多了这么多内存复制的动作,性能下降是最大的挑战,不过,这就是研发同学去考虑的问题了。 DPVS 修复方案 我在像DPVS反馈这个BUG时,提了一个PR Remove toa field, fix security vulnerability. #925,将所有恶意TOA字段都使用TCP_OPT_NOP 设置为空。然而这样,面对TCP Option被填满的清空,是丢失了客户端原始IP的。 虽然性能上影响不大,但依旧有一些安全风险。 DPVS的专家ywc689 给了一个更专业的、更安全的修复方案ipvs: toa enhancements #928,只允许了以下几个opcode的Option,以及对应长度的opsize,并且选择了特别好的重置时机,在应对几十万QPS的网络包处理时,几乎没有新增性能损耗问题。 (了解相应opcode含义,请阅读TCP Option Kind Numbers) // ... const uint8_t opts_maxlen[64] = { [2] = 4, [3] = 3, [4] = 2, [8] = 10, [30] = 40, [34] = 18 }; // ... if (tcph->syn) opts_protected = (1ULL << 2) | (1ULL << 3) | (1ULL << 4) /* MSS, WS, SACKP */ | (1ULL << 8) | (1ULL << 30) | (1ULL << 34); /* TS, MPTCP, TFO */ else opts_protected = (1ULL << 8); /* TS, drop SACK, MPTCP DSS/REMOVE_ADDR */ while (fast < ptr) { opcode = *fast; opsize = *(fast + 1); if (opcode < 64 && ((1ULL << opcode) & opts_protected) && (opsize <= opts_maxlen[opcode])) { for (i = 0; i < opsize; i++) *slow++ = *fast++; opts_protected ^= (1ULL << opcode); } else { fast += opsize; pruned += opsize; if (pruned >= reqlen) { while (fast < ptr) *slow++ = *fast++; break; } } } 虽然,这里opcode 为30(Multipath TCP (MPTCP))的Option存在,但也仅允许SYN包时出现,如果RS的TOA内核模块是读取ACK内的TOA,那么这里是不受影响的。 但是,如果你的TOA内核模块读取的是首个SYN包的TOA,那么变态级思路的问题依旧在 问题验证 笔者实现了一个TCP Option自定义的工具,叫木偶(muou),你可以通过sudo ./muou t -b 020405b4命令来实现自定义TCP Option,这里演示了一个修改Maximum segment size的Option,MSS值为:1460。 按照你对这个漏洞的理解,假如这些漏洞都还存在,你需要实现一个通杀各大厂商的payload,那么该如何构造呢? 笔者的技术方案是eBPF sockops 操作TCP Option,此工具有一定危害,这里做了一定限制,本次只开源了二进制程序,防止定制成为攻击工具。 具体的技术方案,未来分享。 国外的现状 国外的L4LB产品资料相对比较少,现在的产品都是基于eBPF XDP做的,比如Isovalent公司的Cilium、Facebook公司的Katran、CloudFlare公司的Unimog等产品。 相对来说,不需要使用TCP Option/IP Option等方案,直接在L4LB侧转发给相应的RS服务器。 这里插一句,笔者没有吹捧eBPF,国外的现状确实如此,大多使用eBPF + XDP 实现的L4LB。 关于L4LB 四层负载均衡 更多关于,请阅读SRE技术专家laixintao的四层负载均衡漫谈 系列文章。 IOA 安全吗? 本文重点讨论了 基于TCP Option传递的客户端IP信息方案,从L4LB负载均衡到RS的TOA内核模块,不管是代码实现,还是协同机制,都存在着细微的差异,而这些细节恰恰是安全问题所在。 那么,基于IP Option的客户端IP透传方案国内哪些大厂在用?他们安全吗? 2024-08-17 0 0
eBPF在Golang中的应用介绍 大多数时候,我们在开发软件甚至使用软件时,都是在操作系统的安全范围内进行游戏。我们甚至不知道网络接口如何欢迎该 IP 数据包,也不知道当我们保存文件时文件系统如何处理这些 inode。 该边界称为 user space ,它是我们编写应用程序、库和工具的地方。但还有另一个世界,那就是 kernel space 。它是操作系统内核所在的位置,负责管理系统资源,例如内存、CPU 和 I/O 设备。 我们通常不需要低于套接字或文件描述符,但有时我们需要这样做。假设您想要分析一个应用程序以查看它消耗了多少资源。 如果从用户空间分析应用程序,您不仅会错过太多有用的细节,而且还会消耗大量资源来分析本身,因为 CPU 或内存之上的每一层都会引入一些开销。 需要更深入 假设您想要深入堆栈并以某种方式将自定义代码插入内核中以分析应用程序,或跟踪系统调用,或监视网络数据包。你会怎么做? 传统上你有两种选择。 选项 1:编辑内核源代码 如果您想要更改 Linux 内核源代码,然后将相同的内核发送到您客户的机器上,您需要说服 Linux 内核社区需要进行更改。然后,您将需要等待几年才能让新的内核版本被 Linux 发行版采用。 对于大多数情况来说,这不是一个实用的方法,而且也有点不实用。 仅用于分析应用程序或监视网络数据包。 选项 2: 写一个内核模块 你可以编写一个内核模块,它是一段可以加载到内核中并执行的代码。这是一种更实用的方法,但它也有其自身的风险和缺点。 首先,你需要编写一个内核模块,这不是一件容易的事。然后,你需要定期维护它,因为内核是一个有生命的东西,它会随着时间的推移而变化。如果您不维护内核模块,它将过时并且无法与新的内核版本一起使用。 其次,您面临着损坏 Linux 内核的风险,因为内核模块没有安全边界。如果您编写的内核模块有错误,则可能会导致整个系统崩溃。 选择 eBPF eBPF(扩展伯克利数据包过滤器)是一项革命性技术,允许您在几分钟内重新编程 Linux 内核,甚至无需重新启动系统。 eBPF 允许您跟踪系统调用、用户空间函数、库函数、网络数据包等等。它是用于系统性能、监控、安全等方面的强大工具。 如何使用 eBPF 是一个由多个组件组成的系统: eBPF programs eBPF hooks BPF maps eBPF 验证器 eBPF 虚拟机 请注意,我交替使用了术语“BPF”和“eBPF”。 eBPF 代表“扩展伯克利数据包过滤器”。 BPF 最初被引入 Linux 来过滤网络数据包,但 eBPF 扩展了原始 BPF,使其可以用于其他目的。今天它与伯克利无关,而且它不仅仅用于过滤数据包。 下面说明了 eBPF 如何在用户空间和底层工作。 eBPF 程序用高级语言(例如 C)编写,然后编译为 eBPF bytecode 。然后,eBPF 字节码被加载到内核中并由 eBPF virtual machine 执行。 eBPF 程序附加到内核中的特定代码路径,例如系统调用。这些代码路径称为 hooks 。当钩子被触发时,eBPF 程序就会被执行,现在它会执行您编写的自定义逻辑。这样我们就可以在内核空间中运行我们的自定义代码。 eBPF Hello World 在继续讨论细节之前,让我们编写一个简单的 eBPF 程序来跟踪 execve 系统调用。我们将用 C 编写程序,用 Go 编写用户空间程序,然后运行用户空间程序,将 eBPF 程序加载到内核中,并在实际的 < 之前轮询我们将从 eBPF 程序发出的自定义事件。 b1>系统调用被执行。 编写 eBPF 程序 我们先开始编写eBPF程序。我将逐部分编写以更好地解释细节,但您可以在我的 GitHub 存储库中找到整个程序: ozansz/intro-ebpf-with-go。 // hello_ebpf.c 1#include "vmlinux.h" #include <bpf/bpf_helpers.h> struct event { u32 pid; u8 comm[100]; }; struct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 1000); } events SEC(".maps"); 这里我们导入 vmlinux.h 头文件,它包含了内核的数据结构和函数原型。然后我们包含 bpf_helpers.h 头文件,其中包含 eBPF 程序的辅助函数。 然后我们定义一个 struct 来保存事件数据,然后定义一个 BPF map 来存储事件。我们将使用此Map在将在内核空间中运行的 eBPF 程序与用户空间程序之间传递事件。 稍后我们将详细介绍 BPF maps,因此,如果您不明白我们为什么使用 BPF_MAP_TYPE_RINGBUF 或 SEC(".maps") 的用途,请不要担心。 我们现在准备编写第一个程序并定义它将附加到的钩子: // hello_ebpf.c SEC("kprobe/sys_execve") int hello_execve(struct pt_regs *ctx) { u64 id = bpf_get_current_pid_tgid(); pid_t pid = id >> 32; pid_t tid = (u32)id; if (pid != tid) return 0; struct event *e; e = bpf_ringbuf_reserve(&events, sizeof(struct event), 0); if (!e) { return 0; } e->pid = pid; bpf_get_current_comm(&e->comm, 100); bpf_ringbuf_submit(e, 0); return 0; } 这里我们定义了一个函数 hello_execve ,并使用 kprobe 钩子将其附加到 sys_execve 系统调用。 kprobe 是 eBPF 提供的众多钩子之一,用于跟踪内核函数。该钩子将在执行 sys_execve 系统调用之前触发我们的 hello_execve 函数。 在 hello_execve 函数中,我们首先获取进程ID和线程ID,然后检查它们是否相同。如果它们不相同,则意味着我们在一个线程中,并且我们不想跟踪线程,因此我们通过返回零来退出 eBPF 程序。 然后我们在 events Map中预留空间来存储事件数据,然后用进程ID和进程的命令名称填充事件数据。然后我们将事件提交到 events map。 到目前为止这很简单,对吧? 编写用户空间程序 在开始编写用户空间程序之前,我先简单解释一下程序在用户空间需要做什么。我们需要一个用户空间程序将 eBPF 程序加载到内核中,创建 BPF Map,附加到 BPF Map,然后从 BPF Map中读取事件。 为了执行这些操作,我们需要使用特定的系统调用。该系统调用称为 bpf() ,它用于执行一些与 eBPF 相关的操作,例如读取 BPF Map的内容。 我们也可以从用户空间自己调用这个系统调用,但这意味着太多的低级操作。值得庆幸的是,有一些库为 bpf() 系统调用提供了高级接口。其中之一是 Cilium的 ebpf-go 包,我们将在本示例中使用它。 让我们深入研究一些 Go 代码。 // main.go //go:generate go run github.com/cilium/ebpf/cmd/bpf2go -type event ebpf hello_ebpf.c func main() { stopper := make(chan os.Signal, 1) signal.Notify(stopper, os.Interrupt, syscall.SIGTERM) // Allow the current process to lock memory for eBPF resources. if err := rlimit.RemoveMemlock(); err != nil { log.Fatal(err) } objs := ebpfObjects{} if err := loadEbpfObjects(&objs, nil); err != nil { log.Fatalf("loading objects: %v", err) } defer objs.Close() kp, err := link.Kprobe(kprobeFunc, objs.HelloExecve, nil) if err != nil { log.Fatalf("opening kprobe: %s", err) } defer kp.Close() rd, err := ringbuf.NewReader(objs.Events) if err != nil { log.Fatalf("opening ringbuf reader: %s", err) } defer rd.Close() // ... 第一行是 Go 编译器指令 go:generate。这里我们让 Go 编译器运行 github.com/cilium/ebpf/cmd/bpf2go 包中的 bpf2go 工具,并从 hello_ebpf.c 文件生成 Go 文件。 生成的 Go 文件将包括 eBPF 程序的 Go 表示、我们在 eBPF 程序中定义的类型和结构体等。然后我们将在 Go 代码中使用这些表示将 eBPF 程序加载到内核中,并进行交互与 BPF map。 然后我们使用生成的类型来加载eBPF程序(loadEbpfObjects),附加到kprobe钩子(link.Kprobe),并从BPFMap中读取事件(ringbuf.NewReader)。所有这些函数都使用了生成的类型。 是时候与内核侧交互了: // main.go // ... go func() { <-stopper if err := rd.Close(); err != nil { log.Fatalf("closing ringbuf reader: %s", err) } }() log.Println("Waiting for events..") var event ebpfEvent for { record, err := rd.Read() if err != nil { if errors.Is(err, ringbuf.ErrClosed) { log.Println("Received signal, exiting..") return } log.Printf("reading from reader: %s", err) continue } if err := binary.Read(bytes.NewBuffer(record.RawSample), binary.LittleEndian, &event); err != nil { log.Printf("parsing ringbuf event: %s", err) continue } procName := unix.ByteSliceToString(event.Comm[:]) log.Printf("pid: %d\tcomm: %s\n", event.Pid, procName) } } 我们启动一个 goroutine 来监听 stopper 通道,该通道是我们在前面的 Go 代码片段中定义的。当我们收到中断信号时,该通道将用于优雅地停止程序。 然后我们启动一个循环来从 BPF Map中读取事件。我们使用 ringbuf.Reader 类型来读取事件,然后使用 binary.Read 函数将事件数据解析为 ebpfEvent 类型,该类型由eBPF 程序。 然后我们将进程 ID 和进程的命令名称打印到标准输出。 运行程序 现在我们已经准备好运行该程序了。首先,我们需要编译eBPF程序,然后运行用户空间程序。 $ go generate Compiled /Users/sazak/workspace/gocode/src/github.com/ozansz/intro-ebpf-with-go/0x01-helloworld/ebpf_bpfel.o Stripped /Users/sazak/workspace/gocode/src/github.com/ozansz/intro-ebpf-with-go/0x01-helloworld/ebpf_bpfel.o Wrote /Users/sazak/workspace/gocode/src/github.com/ozansz/intro-ebpf-with-go/0x01-helloworld/ebpf_bpfel.go Compiled /Users/sazak/workspace/gocode/src/github.com/ozansz/intro-ebpf-with-go/0x01-helloworld/ebpf_bpfeb.o Stripped /Users/sazak/workspace/gocode/src/github.com/ozansz/intro-ebpf-with-go/0x01-helloworld/ebpf_bpfeb.o Wrote /Users/sazak/workspace/gocode/src/github.com/ozansz/intro-ebpf-with-go/0x01-helloworld/ebpf_bpfeb.go $ go build -o hello_ebpf 我们首先运行 go generate 命令编译eBPF程序,然后运行 go build 命令编译用户空间程序。 然后我们运行用户空间程序: sudo ./hello_ebpf hello_ebpf: 01:20:54 Waiting for events.. 我在Lima的虚拟机内运行这个程序,为什么不打开另一个 shell 看看会发生什么? limactl shell intro-ebpf $ 同时在第一个 shell 中: hello_ebpf: 01:22:22 pid: 3360 comm: sshd hello_ebpf: 01:22:22 pid: 3360 comm: bash hello_ebpf: 01:22:22 pid: 3361 comm: bash hello_ebpf: 01:22:22 pid: 3362 comm: bash hello_ebpf: 01:22:22 pid: 3363 comm: bash hello_ebpf: 01:22:22 pid: 3366 comm: bash hello_ebpf: 01:22:22 pid: 3367 comm: lesspipe hello_ebpf: 01:22:22 pid: 3369 comm: lesspipe hello_ebpf: 01:22:22 pid: 3370 comm: bash 正如预期的那样,我们看到 sshd 进程正在启动,然后 bash 进程正在启动,然后 lesspipe 进程正在启动,依此类推。 这是一个简单的示例,说明我们如何使用 eBPF 跟踪 execve 系统调用,然后从用户空间的 BPF Map中读取事件。我们编写了一个相当简单但功能强大的程序,并且在不修改内核源代码或重新启动系统的情况下拦截了 execve 系统调用。 eBPF Hooks 和 Maps 那么,前面的示例中实际发生了什么?我们使用 kprobe 挂钩将 eBPF 程序附加到 sys_execve 系统调用,以便在 sys_execve 系统调用时运行 hello_execve 函数在执行原始系统调用代码之前调用。 eBPF 是事件驱动的,这意味着它希望我们将 eBPF 程序附加到内核中的特定代码路径。这些代码路径称为“钩子”,eBPF 提供了多种类型的钩子。最常见的是: kprobe, kretprobe: 跟踪内核函数 uprobe, uretprobe: 跟踪用户空间函数 tracepoint: 跟踪内核中预定义的跟踪点 xdp: eXpress Data Path, 用于过滤和重定向网络数据包 usdt: 用户静态定义的跟踪,用于以更有效的方式跟踪用户空间函数 钩子 kprobe 和 uprobe 用于在函数/系统调用执行之前调用附加的 eBPF 程序,并使用 kretprobe 和 uretprobe 在函数/系统调用执行之后调用附加的 eBPF 程序。 我们还使用 BPF Map来存储事件。 BPF Map是用于存储和通信不同类型数据的数据结构。我们还将它们用于状态管理。支持的 BPF Map类型太多,我们根据不同的目的使用不同类型的Map。一些最常见的 BPF Map类型是: BPF_MAP_TYPE_HASH: 哈希表 BPF_MAP_TYPE_ARRAY: 数组 BPF_MAP_TYPE_RINGBUF: ring buffer BPF_MAP_TYPE_STACK: 栈 BPF_MAP_TYPE_QUEUE: 队列 BPF_MAP_TYPE_LRU_HASH: LRU hash map 其中一些Map类型还具有每个 CPU 的变体,例如 BPF_MAP_TYPE_PERCPU_HASH ,它是一个哈希Map,每个 CPU 核心都有一个单独的哈希表。 更进一步: 跟踪传入的 IP 数据包 让我们更进一步,编写一个更复杂的 eBPF 程序。这次,我们将在网络接口向内核发送网络数据包之后,甚至在内核处理该数据包之前,使用 XDP 钩子调用 eBPF 程序。 编写 eBPF 程序 我们将编写一个 eBPF 程序,根据源 IP 地址和端口号来计算传入 IP 数据包的数量,然后从用户空间中的 BPF Map中读取计数。我们将解析每个数据包的以太网、IP 和 TCP/UDP 标头,并将有效 TCP/UDP 数据包的计数存储在 BPF Map中。 一、eBPF程序: // hello_ebpf.c #include "vmlinux.h" #include <bpf/bpf_helpers.h> #include <bpf/bpf_endian.h> #define MAX_MAP_ENTRIES 100 /* Define an LRU hash map for storing packet count by source IP and port */ struct { __uint(type, BPF_MAP_TYPE_LRU_HASH); __uint(max_entries, MAX_MAP_ENTRIES); __type(key, u64); // source IPv4 addresses and port tuple __type(value, u32); // packet count } xdp_stats_map SEC(".maps"); 与第一个示例一样,我们将包含 vmlinux.h 和 BPF 辅助标头。我们还定义了一个Map xdp_stats_map 来存储 IP:ports 和数据包计数信息。然后,我们将在钩子函数内填充此Map并读取用户空间程序中的内容。 我所说的 IP:ports 基本上是一个 u64 值,包含源 IP、源端口和目标端口。 IP 地址(特别是 IPv4)是 32 位长,每个端口号是 16 位长,因此我们需要 64 位来存储所有三个 - 这就是我们在这里使用 u64 的原因。我们在这里只处理入口(传入数据包),因此不需要存储目标 IP 地址。 与上一个示例不同,我们现在使用 BPF_MAP_TYPE_LRU_HASH 作为map类型。这种类型的Map允许我们将 (key, value) 对存储为具有 LRU 变体的哈希Map。 看看我们如何在这里定义Map,我们显式设置了最大条目数以及Map键和值的类型。对于键,我们使用 64 位无符号整数,对于值,我们使用 32 位无符号整数。 u32 的最大值是 2^32 - 1 ,对于本示例而言,这已经足够了。 要了解 IP 地址和端口号,我们首先需要解析数据包并读取以太网、IP 和 TCP/UDP 标头。 由于 XDP 放置在网络接口卡之后,我们将获得以字节为单位的原始数据包数据,因此我们需要手动遍历字节数组并解组以太网、IP 和 TCP/UDP 标头。 希望我们在( vmlinux.h 、 struct iphdr 、 struct tcphdr 和 struct udphdr ) > 头文件。我们将使用这些结构在单独的函数 parse_ip_packet 中提取 IP 地址和端口号信息: // hello_ebpf.c #define ETH_P_IP 0x0800 /* Internet Protocol packet */ #define PARSE_SKIP 0 #define PARSED_TCP_PACKET 1 #define PARSED_UDP_PACKET 2 static __always_inline int parse_ip_packet(struct xdp_md *ctx, u64 *ip_metadata) { void *data_end = (void *)(long)ctx->data_end; void *data = (void *)(long)ctx->data; // First, parse the ethernet header. struct ethhdr *eth = data; if ((void *)(eth + 1) > data_end) { return PARSE_SKIP; } if (eth->h_proto != bpf_htons(ETH_P_IP)) { // The protocol is not IPv4, so we can't parse an IPv4 source address. return PARSE_SKIP; } // Then parse the IP header. struct iphdr *ip = (void *)(eth + 1); if ((void *)(ip + 1) > data_end) { return PARSE_SKIP; } u16 src_port, dest_port; int retval; if (ip->protocol == IPPROTO_TCP) { struct tcphdr *tcp = (void*)ip + sizeof(*ip); if ((void*)(tcp+1) > data_end) { return PARSE_SKIP; } src_port = bpf_ntohs(tcp->source); dest_port = bpf_ntohs(tcp->dest); retval = PARSED_TCP_PACKET; } else if (ip->protocol == IPPROTO_UDP) { struct udphdr *udp = (void*)ip + sizeof(*ip); if ((void*)(udp+1) > data_end) { return PARSE_SKIP; } src_port = bpf_ntohs(udp->source); dest_port = bpf_ntohs(udp->dest); retval = PARSED_UDP_PACKET; } else { // The protocol is not TCP or UDP, so we can't parse a source port. return PARSE_SKIP; } // Return the (source IP, destination IP) tuple in network byte order. // |<-- Source IP: 32 bits -->|<-- Source Port: 16 bits --><-- Dest Port: 16 bits -->| *ip_metadata = ((u64)(ip->saddr) << 32) | ((u64)src_port << 16) | (u64)dest_port; return retval; } 函数:: 检查数据包是否具有有效的以太网标头、IP 标头以及 TCP 或 UDP 标头。这些检查是通过使用 struct ethhdr 的 h_proto 和 struct iphdr 的 protocol 完成的。每个标头都存储它所包装的内部数据包的协议。 从 IP 标头中提取 IP 地址,从 TCP/UDP 标头中提取端口号,并在 64 位无符号整数 ( u64 ) 内形成 IP:ports 元组 返回一个代码来告诉调用者该数据包是 TCP 数据包、UDP 数据包还是其他 ( PARSE_SKIP ) 请注意函数签名开头的 __always_inline 。这告诉编译器始终将此函数内联为静态代码,这使我们免于执行函数调用。 现在是时候编写钩子函数并使用 parse_ip_packet : // hello_ebpf.c SEC("xdp") int xdp_prog_func(struct xdp_md *ctx) { u64 ip_meta; int retval = parse_ip_packet(ctx, &ip_meta); if (retval != PARSED_TCP_PACKET) { return XDP_PASS; } u32 *pkt_count = bpf_map_lookup_elem(&xdp_stats_map, &ip_meta); if (!pkt_count) { // No entry in the map for this IP tuple yet, so set the initial value to 1. u32 init_pkt_count = 1; bpf_map_update_elem(&xdp_stats_map, &ip_meta, &init_pkt_count, BPF_ANY); } else { // Entry already exists for this IP tuple, // so increment it atomically. __sync_fetch_and_add(pkt_count, 1); } return XDP_PASS; } The xdp_prog_func is fairly simple as we already coded most of the program logic inside parse_ip_packet. What we do here is: xdp_prog_func 相当简单,因为我们已经在 parse_ip_packet 内编写了大部分程序逻辑。我们在这里所做的是: 使用 parse_ip_packet 解析数据包 如果不是 TCP 或 UDP 数据包,则返回 XDP_PASS 跳过计数 使用 bpf_map_lookup_elem 辅助函数在 BPF Map键中查找 IP:ports 元组 如果第一次看到 IP:ports 元组,则将该值设置为 1,否则将其增加 1。 __sync_fetch_and_add 是这里内置的 LLVM 最后,我们使用 SEC("xdp") 宏将此函数附加到 XDP 子系统。 编写用户空间程序 是时候再次深入研究 Go 代码了。 // main.go //go:generate go run github.com/cilium/ebpf/cmd/bpf2go ebpf xdp.c var ( ifaceName = flag.String("iface", "", "network interface to attach XDP program to") ) func main() { log.SetPrefix("packet_count: ") log.SetFlags(log.Ltime | log.Lshortfile) flag.Parse() // Subscribe to signals for terminating the program. stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt, syscall.SIGTERM) iface, err := net.InterfaceByName(*ifaceName) if err != nil { log.Fatalf("network iface lookup for %q: %s", *ifaceName, err) } // Load pre-compiled programs and maps into the kernel. objs := ebpfObjects{} if err := loadEbpfObjects(&objs, nil); err != nil { log.Fatalf("loading objects: %v", err) } defer objs.Close() // Attach the program. l, err := link.AttachXDP(link.XDPOptions{ Program: objs.XdpProgFunc, Interface: iface.Index, }) if err != nil { log.Fatalf("could not attach XDP program: %s", err) } defer l.Close() log.Printf("Attached XDP program to iface %q (index %d)", iface.Name, iface.Index) 这里我们首先加载生成的 eBPF 程序并使用 loadEbpfObjects 函数进行Map。然后我们使用 link.AttachXDP 函数将程序附加到指定的网络接口。与前面的示例类似,我们使用通道来监听中断信号并优雅地关闭程序。 接下来,我们将每秒读取map内容并将数据包计数打印到标准输出: // main.go // ... ticker := time.NewTicker(time.Second) defer ticker.Stop() for { select { case <-stop: if err := objs.XdpStatsMap.Close(); err != nil { log.Fatalf("closing map reader: %s", err) } return case <-ticker.C: m, err := parsePacketCounts(objs.XdpStatsMap, excludeIPs) if err != nil { log.Printf("Error reading map: %s", err) continue } log.Printf("Map contents:\n%s", m) srv.Submit(m) } } } 我们将使用实用函数 parsePacketCounts 来读取map内容并解析数据包计数。该函数将循环读取map内容。 由于我们将从Map中获得原始字节,因此我们需要解析这些字节并将其转换为人类可读的格式。我们将定义一个新类型 PacketCounts 来存储解析后的map内容。 // main.go type IPMetadata struct { SrcIP netip.Addr SrcPort uint16 DstPort uint16 } func (t *IPMetadata) UnmarshalBinary(data []byte) (err error) { if len(data) != 8 { return fmt.Errorf("invalid data length: %d", len(data)) } if err = t.SrcIP.UnmarshalBinary(data[4:8]); err != nil { return } t.SrcPort = uint16(data[3])<<8 | uint16(data[2]) t.DstPort = uint16(data[1])<<8 | uint16(data[0]) return nil } func (t IPMetadata) String() string { return fmt.Sprintf("%s:%d => :%d", t.SrcIP, t.SrcPort, t.DstPort) } type PacketCounts map[string]int func (i PacketCounts) String() string { var keys []string for k := range i { keys = append(keys, k) } sort.Strings(keys) var sb strings.Builder for _, k := range keys { sb.WriteString(fmt.Sprintf("%s\t| %d\n", k, i[k])) } return sb.String() } 我们定义了一个新类型 IPMetadata 来存储 IP:ports 元组。我们还定义了一个 UnmarshalBinary 方法来解析原始字节并将其转换为人类可读的格式。我们还定义了一个 String 方法来以人类可读的格式打印 IP:ports 元组。 然后我们定义了一个新类型 PacketCounts 来存储解析后的map内容。我们还定义了一个 String 方法来以人类可读的格式打印map内容。 最后,我们将使用 PacketCounts 类型来解析map内容并打印数据包计数: // main.go func parsePacketCounts(m *ebpf.Map, excludeIPs map[string]bool) (PacketCounts, error) { var ( key IPMetadata val uint32 counts = make(PacketCounts) ) iter := m.Iterate() for iter.Next(&key, &val) { if _, ok := excludeIPs[key.SrcIP.String()]; ok { continue } counts[key.String()] = int(val) } return counts, iter.Err() } 运行程序 我们首先需要编译eBPF程序,然后运行用户空间程序。 $ go generate Compiled /Users/sazak/workspace/gocode/src/github.com/ozansz/intro-ebpf-with-go/0x03-packet-count/ebpf_bpfel.o Stripped /Users/sazak/workspace/gocode/src/github.com/ozansz/intro-ebpf-with-go/0x03-packet-count/ebpf_bpfel.o Wrote /Users/sazak/workspace/gocode/src/github.com/ozansz/intro-ebpf-with-go/0x03-packet-count/ebpf_bpfel.go Compiled /Users/sazak/workspace/gocode/src/github.com/ozansz/intro-ebpf-with-go/0x03-packet-count/ebpf_bpfeb.o Stripped /Users/sazak/workspace/gocode/src/github.com/ozansz/intro-ebpf-with-go/0x03-packet-count/ebpf_bpfeb.o Wrote /Users/sazak/workspace/gocode/src/github.com/ozansz/intro-ebpf-with-go/0x03-packet-count/ebpf_bpfeb.go $ go build -o packet_count 现在我们可以运行它: $ sudo ./packet_count --iface eth0 packet_count: 22:11:10 main.go:107: Attached XDP program to iface "eth0" (index 2) packet_count: 22:11:10 main.go:132: Map contents: 192.168.5.2:58597 => :22 | 51 packet_count: 22:11:11 main.go:132: Map contents: 192.168.5.2:58597 => :22 | 52 packet_count: 22:11:11 main.go:132: Map contents: 192.168.5.2:58597 => :22 | 53 从 IP 地址 192.168.5.2 到达端口 22 的数据包是 SSH 数据包,因为我在虚拟机内运行此程序并且通过 SSH 连接。 让我们在另一个终端的虚拟机内运行 curl ,看看会发生什么: $ curl https://www.google.com/ 同时在第一个终端中: packet_count: 22:14:07 main.go:132: Map contents: 172.217.22.36:443 => :38324 | 12 192.168.5.2:58597 => :22 | 551 packet_count: 22:14:08 main.go:132: Map contents: 172.217.22.36:443 => :38324 | 12 192.168.5.2:58597 => :22 | 552 packet_count: 22:14:08 main.go:132: Map contents: 172.217.22.36:443 => :38324 | 30 192.168.5.2:58597 => :22 | 570 1packet_count: 22:14:09 main.go:132: Map contents: 172.217.22.36:443 => :38324 | 30 192.168.5.2:58597 => :22 | 571 我们看到从 IP 地址 172.217.22.36 到达端口 38324 的数据包是来自 curl 命令的数据包。 结论 eBPF 在很多方面都很强大,我认为这是一项值得投入时间的好技术,特别是在系统编程、可观察性或安全性方面。在本文中,我们了解了 eBPF 是什么、它是如何工作的以及如何开始在 Go 中使用它。 我希望您喜欢这篇文章并学到一些新东西。如果您有任何疑问,请随时oznszk。 2024-08-06 0 0
eCapture v0.8.0:CO-RE、非CO-RE二合一,交叉编译amd64、arm64双版本 eCapture是什么 eCapture旁观者是一个无需CA证书,无侵入的HTTPS/TLS明文抓包工具。可以在Linux 4.18以上版本使用,同时也支持Android arm64 5.5以上版本。项目发布共两年累计8200颗星。 新手友好 :CO-RE、非CO-RE二合一 下载 只下一个版本 使用eBPF技术实现的软件,总会分不清CO-RE(一次编译,到处运行),在选择CORE、非CORE版本而烦恼吗?不知道系统是否支持,也不知道什么是CORE。下载一个android版的eCapture,不知道要下android-aarch64.tar.gz还是android-aarch64-nocore.tar.gz。 这下你不用纠结了,eCapture 0.8.0起,实现了兼容,会自动选择CORE版本。而且,统一使用了面向产品化的CPU架构命名,分为amd64和arm64两种,下载时注意区分。 使用 自动探测BTF、自动选择 在eCapture 0.8.0里,你只需要按照以往使用习惯,直接运行sudo ecapture tls等命令即可,无需关注系统是否支持BTF,eCapture会自动探测系统的支持情况,选择相应的eBPF字节码进行加载。 手动指定BTF 如果你在使用时,出现加载出错的情况,那么你也可以尝试手动指定eBPF字节码类型,让eCapture来指定加载。参数为-b 或--btf,值为3种: 0:auto,自动探测、自动选择,即默认模式。 1:core,CO-RE模式,一般支持CO-RE模式的内核,也都会支持non-CO-RE模式。 2:non-core:非CO-RE模式,适用于CONFIG_DEBUG_INFO_BTF=n的情况。 sudo ./bin/ecapture -h NAME: ecapture - capture text SSL content without CA cert by ebpf hook. VERSION: linux_arm64:v0.8.0:5.15.0-105-generic OPTIONS: -b, --btf=0 enable BTF mode.(0:auto; 1:core; 2:non-core) Linux运行示例 使用-b=2参数指定后,捕获curl https://ecapture.cc,把保存的ecapture.pcapng文件用Wireshark打开。 可以看到,通过命令行参数-b=2指定后,加载的BPF bytecode模式为non-CORE,同时,加载的字节码文件也发生了变化。 同时,eCapture也准确的捕获了TLS 1.3的加密密钥、明文,以及准确的捕获了网络包所属进程curl,进程的PID 85432 Android版的eCapture使用方式与Linux一致,也是-b参数。 新版下载 eCapture v0.8.0 Github 下载 ecapture-v0.8.0-android-amd64.tar.gz ecapture-v0.8.0-android-arm64.tar.gz ecapture-v0.8.0-linux-amd64.tar.gz ecapture-v0.8.0-linux-arm64.tar.gz Warning 以下是面向eBPF开发者的内容,使用者可以不用看。 交叉编译 Cross-compilation 内核头文件 要交叉编译eCapture工具,您需要安装目标体系结构的内核头文件。需要安装linux-source软件包。 kernel_ver=`uname -r | cut -d'-' -f 1` sudo apt-get install -y linux-source-$kernel_ver cd /usr/src sudo tar -xf linux-source-${kernel_ver}.tar.bz2 cd /usr/src/linux-source-${kernel_ver} test -f .config || yes "" | sudo make oldconfig yes "" | sudo make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- prepare V=0 更多交叉编译配置可参考仓库的workflows 依赖环境 在amd64架构下,交叉编译arm64架构的二进制文件,需要安装gcc-aarch64-linux-gnu工具链。同样,在arm64架构下,交叉编译amd64架构的二进制文件,需要安装gcc-x86-64-linux-gnu工具链。 交叉编译器 amd64 : gcc-aarch64-linux-gnu arm64 : gcc-x86-64-linux-gnu 编译命令 要在ubuntu amd64 系统上构建 arm64的产物,您可以设置 CROSS_ARCH环境变量来实现交叉编译。 CROSS_ARCH=arm64 make 产物构建 (交叉编译) 以Github Action中的release为例 - name: Release amd64 run: | make clean make env make -f builder/Makefile.release release SNAPSHOT_VERSION=${{ github.ref_name }} - name: Release arm64 (CROSS COMPILATION) run: | make clean make env CROSS_ARCH=arm64 make -f builder/Makefile.release release SNAPSHOT_VERSION=${{ github.ref_name }} 即可编译生成amd64、arm64两种CPU架构的eCapture二进制文件,包括Linux、Android两个系统版本。 技术难点 eCapture项目是一个多语言实现的项目,内核空间代码使用C语言编写,用户空间代码使用Golang、CGO编写。其中eBPF加载器、eBPF Map读写是用纯Go实现。涉及到libpcap做cBPF转eBPF的包过滤功能,是使用纯C编写的。 Kernel Space 内核空间代码 由于eCapture的大部分功能都是Uprobe,不太涉及内核的结构体。所以大部分情况下,都可以正常运行,只要解决编译问题。 CO-RE 头文件 CO-RE的版本,因为都支持重定位,只需要内置一个版本的vmlinux.h,能正常编译后,就可以放到支持CO-RE的内核上运行。 non-CO-RE 头文件 non-CO-RE版本,依赖编译时的内核头文件。在Ubuntu 22.04 Server上,只有当前CPU架构的完整头文件。如果需要交叉编译,则需要下载Linux 内核源码,重新生成目标CPU架构的头文件。这就是前面提到内核头文件预生成部分:make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- prepare。 编译器 eBPF字节码的编译,完全使用llvm(clang)来编译,只需要编译参数中的-D__TARGET_ARCH_$(LINUX_ARCH)、-target $(TARGET_ARCH) 是对应目标CPU架构信息即可。 User Space 用户空间代码 用户空间代码的核心功能是选择eBPF字节码,以及调用libpcap类库,将命令行参数转化解析编译成cBPF代码,再转化为eBPF字节码。再替换到过滤器中。 libpcap的交叉编译 项目本身是支持交叉编译的,需要指定目标架构,以及编译器CC、打包器AR。在amd64、arm64的Linux上,较为常见的交叉编译、打包归档工具为x86_64-linux-gnu-gcc、x86_64-linux-gnu-ar,编译参数中指定即可: CC=$(CMD_CC_PREFIX)$(CMD_CC) AR=$(CMD_AR_PREFIX)$(CMD_AR) CFLAGS="-O2 -g -gdwarf-4 -static" ./configure --host=$(LIBPCAP_ARCH) CGO混合编译 在golang的env里,默认的编译、打包工具是CC='gcc'和AR='ar',及时你设定GOARCH=$(GOARCH) 来做Go层的交叉编译,但依旧无法完成CGO的交叉编译。所以,这里你需要重新设定他们为交叉编译的工具链,方法跟上面一致。 GOOS=linux GOARCH=$(GOARCH) CC=$(CMD_CC_PREFIX)$(CMD_CC) \ $(CMD_GO) build -tags $(TARGET_TAG) -ldflags "-w -s -X 'github.com/gojue/ecapture/cli/cmd.GitVersion=$(TARGET_TAG)_$(GOARCH):$(VERSION_NUM):$(VERSION_FLAG)' -linkmode=external -extldflags -static " -o $(OUT_BIN) 听上去很简单,可笔者确是折腾了好久,掉了不知道多少根头发才解决。 希望笔者的经验可以帮助到你,让你不掉头发。 社区新人 从上篇文章eCapture v0.7.4发布支持Pcap Filter包过滤语法 至今已经2个多月了,笔者一直没时间写。 近期工作特别忙,花在eCapture上的时间也很少。在这段时间内,社区涌现了好多为新人,持续提issue、提PR做贡献,尤其是ruitianzhong和sancpp两位同学。 他们在社区提了issue后,我觉得打字效率太低,为了节省时间,跟他们约了视频会议讨论。说来也巧,他们在群里开场介绍后,发现都是西安电子科技大学的,现在通过eCapture认识了,有了共同的话题,相谈甚欢,还打算一起参加全国大学生计算机系统能力大赛呢。 希望他们能取得好的比赛成绩,也欢迎更多的人参与到开源项目建设中。 2024-05-06 2 0
ecapture v0.7.4发布,支持Pcap FIlter包过滤语法 eCapture是什么 eCapture旁观者一个无需CA证书,无侵入的HTTPS/TLS明文抓包工具。可以在Linux 4.18以上版本使用,同时也支持Android arm64 5.5以上版本。项目在2022年4月发布,目前7900颗星。 超强特性 :Pcap Filter 包过滤 eCapture v0.7.4版本发布支持Pcap Filter Syntax,你可以像tcpdump一样使用pcap filter表达式来过滤网络包。 在tls或gotls模块中,当运行模式参数-m为pcap时,支持pcap filter表达式。在命令行最后的参数中设定,例如: sudo bin/ecapture tls -m pcap -i ens160 -w a.pcapng host 1.1.1.1 or src port 443 [sudo] password for cfc4n: tls_2024/02/25 07:23:27 ECAPTURE :: ecapture Version : linux_aarch64:0.7.3-20240222-f50b9de:[CORE] tls_2024/02/25 07:23:27 ECAPTURE :: Pid Info : 884394 tls_2024/02/25 07:23:27 ECAPTURE :: Kernel Info : 5.15.131 tls_2024/02/25 07:23:27 EBPFProbeOPENSSL module initialization tls_2024/02/25 07:23:27 ECAPTURE :: Module.Run() tls_2024/02/25 07:23:27 EBPFProbeOPENSSL: Pcapng MODEL tls_2024/02/25 07:23:27 EBPFProbeOPENSSL OpenSSL/BoringSSL version not found from shared library file, used default version:linux_default_3_0 tls_2024/02/25 07:23:27 EBPFProbeOPENSSL HOOK type: 2, binrayPath: /usr/lib/aarch64-linux-gnu/libssl.so.3 tls_2024/02/25 07:23:27 EBPFProbeOPENSSL PcapFilter: host 1.1.1.1 or src port 443 tls_2024/02/25 07:23:27 EBPFProbeOPENSSL Ifname: ens160, Ifindex: 2 tls_2024/02/25 07:23:27 EBPFProbeOPENSSL Hook masterKey function: [SSL_get_wbio SSL_in_before SSL_do_handshake] tls_2024/02/25 07:23:27 EBPFProbeOPENSSL target all process. tls_2024/02/25 07:23:27 EBPFProbeOPENSSL target all users. tls_2024/02/25 07:23:27 EBPFProbeOPENSSL: BPF bytecode filename:user/bytecode/openssl_3_0_0_kern.o tls_2024/02/25 07:23:27 EBPFProbeOPENSSL saving pcapng file: /home/cfc4n/project/ecapture/a.pcapng tls_2024/02/25 07:23:27 EBPFProbeOPENSSL: perfEventReader created. mapSize:4 MB tls_2024/02/25 07:23:27 EBPFProbeOPENSSL: perfEventReader created. mapSize:4 MB tls_2024/02/25 07:23:27 EBPFProbeOPENSSL module started successfully. tls_2024/02/25 07:23:27 ECAPTURE :: start 1 modules eCapture 指令图解 很多同学对ecapture的使用方式不了解,eCapture的module模块、mode模式两个功能点经常混淆,运行出错后,文档不看,help命令也不看。遇到问题后,直接到公众号问我,弄得我很头大。 这次也按照思维导图的方式,简单画个图解,以便更直观地熟悉eCapture。 注 在个别Android的发行版上,pcap filter过滤语法可能没有效果,未来会修复。 技术原理 本功能由Leon Hwang大神贡献,感谢他春节期间的付出。 详情见:feat: Support pcap-filter expression for pcap mode #478 libpcap 编译 pcap filter 表达式的过滤内容为cbpf字节码 cbpf字节码转为ebpf字节码 ebpf字节码插入到eCapture的TC HOOK字节码中 由内核加载全部eBPF字节码,实现表达式过滤 依赖 elibpcap cbpfc ebpfmanager v0.4.5 (eBPF bytecode InstructionPatchers) 下载 eCapture v0.7.4 下载地址 2024-02-29 0 0
eCapture v0.7.3新版性能提升10倍,支持OpenSSL 3.2 eCapture是什么 eCapture旁观者一个无需CA证书,无侵入的HTTPS/TLS明文抓包工具。可以在Linux 4.18以上版本使用,同时也支持Android arm64 5.5以上版本。 年底打脸 记得8月份时,在GitHub万星项目进度70%,eCapture旁观者7K Stars达成一文中吹牛说到,年底冲1万颗星,奈何平时工作特别忙,根本没时间堆新功能,年底一看,果然打脸了,只有7.8K星, 不过,笔者也一直忙里偷闲地回复社区的问题,不停地修复程序bug。 增加难度 这不,有几个小朋友给我提出了新的挑战,在生产环境,上万QPS的7层负载均衡,启用eCapture,会有什么样的结果?性能扛得住吗? 性能提升10倍 有用户在社区反馈eCapture性能问题TLS 模式下,对被检测程序的性能影响,执行一千万次SSL_write写数据的场景,不开启eCapture耗时6秒,而开启eCapture后却要30秒,性能耗时增大5倍以上。 这个数字就很尴尬,就在你潇洒的这两天,笔者全身心投入,用爱发电,翻阅OpenSSL源码,终于有了很大的进展。 进步大源于起点底 笔者分析了OpenSSL类库中,以Server和Client两种模式,分别选择合适的HOOK函数,减少SSL_write和SSL_read两个函数高频调用下的触发机制,最终把耗时从30秒降低到7秒。整体来看,进步特别大。但原来确实有偷懒,直接用了SSL_write函数。 继续宠粉 又有小朋友在社区提了issue:The SSL structure in openssl 3.2.0 has been modified,里面提到OpenSSL 3.2.0已发布了,而且核心ssl_st结构体发生很大变化,eCaptuer捕获TLS通讯密钥的功能无法使用。 笔者确认了一下,OpenSSL 3.2.0是2023年11月底发布的,至今不过2个月时间,应该还没有多少大型应用使用,不过,笔者就是喜欢宠粉,立马给安排了。谁让这位小朋友的issue写得漂亮呢。 新版下载 无CA捕获TLS明文软件eCapture v0.7.3版发布,性能提升10倍,千万次调用增加2秒,支持openssl 3.2。 Warning 以下为技术原理,可以不用看。 技术实现 性能优化 设计思路 问题 为了读取到TLS握手完成后的client_random等密钥,必需要选择一个合适的HOOK函数。 SSL_write\SSL_read时,TLS握手是建立完成的,但调用过于频繁,会带来性能问题,参见 #463 综合来看,合适的HOOK函数需要满足以下几个条件: 函数是在TLS握手完成后调用 函数名在动态链接库的符号表中是导出状态 函数是低频调用 解决思路 在 openssl 类库中,以客户端角色调用 SSL_connect 或者以服务端角色 SSL_accept ,最终都会进入 ssl/statem/statem.c 的 state_machine 函数进行TLS握手。 所以,可选范围是在这个函数内以大写SSL开头的函数。 当使用openssl的方式为同步调用时,TLS握手成功会返回1,也就是ret = 1,即需要在这个变量赋值后,被调用的函数,才能拿到符合要求的内存数据。 state_machine函数内符合要求的就只有SSL_get_wbio了。 当使用openssl的方式为异步 (BIO)调用时,还需要增加SSL_in_before函数。 masterKeyHookFuncs = []string{ "SSL_get_wbio", // openssl client mode //"SSL_is_init", // boringssl client mode // 备用HOOK 函数 //"SSL_is_init_finished", "SSL_in_before", //openssl client mode "SSL_do_handshake", // openssl server mode } OpenSSL 3.2.0 无它,翻阅OpenSSL源码,分析差异,重新实现密钥提取代码,详情见 PR 472 。不过,新版里新增了对QUIC TLS的支持,eCapture暂时没支持,预计春节后安排。 // TODO 检查 ssl->type, 参考 ssl/ssl_local.h的 SSL_CONNECTION_FROM_SSL_int 宏 int type; if (type == SSL_TYPE_QUIC_CONNECTION) { // Reget the address of the ssl->tls as the address of the SSL_CONNECTION debug_bpf_printk("unsupported type: SSL_CONNECTION, coming soon.\n"); return 0; } 届时,eCapture就可以无需CA证书,即可捕获QUIC TLS的明文内容了,敬请期待。 红包封面预告 去年封面 即将春节,送给大家一些手绘的红包封面,依旧由去年红包封面《烤火兔》的设计大师,外甥女S负责。 今年封面 本次春节红包封面《龙"码"精神》,依旧没有任何广告、品牌标识。仿版画风格,材质感更重。 立寒冬,暖阳待雪,龙码精神!这款红包风格采用木质版画风格,五行之中黑色对应水,遇水则发~黑龙俯卧在金灿灿的摇钱树给大家送福啦! 2024-01-30 1 0