《eBPF 系列(一): 初探eBPF》

admin 2022年5月3日18:20:48评论1,338 views字数 6685阅读22分17秒阅读模式
《eBPF 系列(一): 初探eBPF》

01

《eBPF 系列(一): 初探eBPF》

简介



  • ebpf是一种Linux应用程序在内核空间执行代码的机制,经常被用于网络,调试,函数调用追踪,防火墙等领域。

  • ebpf可以在Linux内核里面运行沙箱程序而不需要修改内核代码或者加载内核模块。

  • ebpf程序的功能及其执行涉及一些复杂的组件。

ebpf技术本意是扩展bpf技术,本来bpf是用来对于数据包进行过滤,但是经过不断的发展,现在是一种可以从用户态向内核态注入代码从而可以使得用户态的代码可以在内核态执行,为了防止内核破坏,ebpf技术对注入的代码有verifier验证和jit即时编译保护,整体的架构如下:

《eBPF 系列(一): 初探eBPF》


其中ebpf技术对容器安全也有很大的作用,
《eBPF 系列(一): 初探eBPF》
ebpf程序使用eBPF编译工具链 BCC[1]  进行编译,ebpf程序一般在内核触发某些特定事件(比如hook,syscall, network events等)之后启动。
但是ebpf程序在加载进入内核之前其必须经过一系列的检查包括将ebpf程序在内核虚拟机里面执行,这些 检查代码[2] 包含在Linux内核代码中,该验证器会遍历eBPF程序的潜在执行路径以确保程序是在没有任何循环的情况下完成的,并且验证eBPF程序执行过程中的寄存器状态,程序大小以及是否发送OOB(out of bound) jumps等。这无疑极大的降低了eBPF带来的内核安全风险。
当所有的检查都通过之后,eBPF程序才会真正的加载进入内核等待hook触发,一旦触发程序就会执行。


《eBPF 系列(一): 初探eBPF》

02

《eBPF 系列(一): 初探eBPF》

架构设计


事件以及hook




eBPF程序本质上来说还是执行在一个事件驱动的环境里面,它们通常被内核hook触发,hook位置的多样性是eBPF功能强大的一个重要原因:
  • Syscall

  • Function entry and Exit

  • Network events

  • Kprobe and uprobes

当eBPF程序被触发的时候,eBPF程序可以call helper functions,这些helper极大的增强了eBPF程序的功能,这些helper功能由内核定义,这表明eBPF程序在内核里面有一份函数调用白名单,这些 helper[3] 的数量很多并且在不断的增加。

eBPF Maps



eBPF maps允许eBPF程序在调用之间保持状态与用户空间应用程序共享数据,它以键值对的形式进行存储,其中的值通常被视为任意数据的二进制块。
通过参数为BPF_MAP_CREATE的参数的bpf_cmd syscall 可以创建eBPF Maps,syscall返回一个文件描述符用于索引eBPF MAP,至于如何和MAP进行交互,可以参考 这里[4]

运行eBPF 程序



Linux kernel 期望所有的eBPF程序都可以作为bytecode进行加载,因此类似于汇编和C的关系,eBPF bytecode也需要一个工具来实现高级语言到bytecode的转换,最出名的eBPF调试和编译工具就是 BCC[1] 
eBPF程序在内核里面经过验证之后,eBPF bytecode需要通过JIT编译成为原生机器码以便于执行。eBPF支持64位机器长度和11个寄存器,这方便将eBPF程序映射到不同的指令集架构,例如x86_64,ARM,arm64等等。

《eBPF 系列(一): 初探eBPF》


BTF&CO-RE



eBPF经常和内核打交道,但是linux内核的改动一般很坑,兼容性需要项目开发者自己考虑,常规的办法就是通过宏定义判断内核版本,根据不同的内核版本在编译的时候走不同的代码分支,但是适配非常麻烦。
eBPF的解决办法是通过BTF & CO-RE,BTF可以理解为一种Debug信息的描述方法,linux内核通常会关闭调试符号,BTF解决了这个问题,CO-RE正式基于BTF开发的,核心思想就是采用非硬编码的形式对成员在结构中的偏移位置进行描述,解决不同版本之间的差异。从而极大的便利了开发者。可以参考这里[5]

《eBPF 系列(一): 初探eBPF》

03

《eBPF 系列(一): 初探eBPF》

开发指南


开发环境



这里建议安装至少是5.16版本的内核,并且开启了BTF,或者直接使用Ubuntu 20.10 server version。
cat /boot/config-uname -r (这里''加不上,看下图) |  grep BTF

《eBPF 系列(一): 初探eBPF》

需要安装的依赖如下:
  • libbfp Library

  • clang and llvm

  • 通过vmlinux生成.h头文件以方便进行CO-CR(一次编译多处使用)

Bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h

  • 编写C程序

#include "vmlinux.h" //linux内核头文件大集合#include <bpf/bpf_helpers.h>#include <bpf/bpf_endian.h>#include <bpf/bpf_core_read.h>#include <bpf/bpf_tracing.h>//包含这些头文件,就可以用CORE编程了
  • 使用bpf_printk进行代码调试long bpf_trace_printk(const char *fmt, __u32 fmt_size, ...);

    cat /sys/kernel/debug/tracing/trace_pipe输出在这里(记得使用root用户)。

《eBPF 系列(一): 初探eBPF》

  • 创建Makefile

TARGETS := kern/sec_socket_connectTARGETS += kern/tcp_set_stateTARGETS += kern/dns_lookupTARGETS += kern/udp_lookup
# Generate file name-scheme based on TARGETSKERN_SOURCES = ${TARGETS:=_kern.c}KERN_OBJECTS = ${KERN_SOURCES:.c=.o}
LLC ?= llcCLANG ?= clangEXTRA_CFLAGS ?= -O2 -emit-llvm -g
linuxhdrs ?= /lib/modules/`uname -r`/build
LINUXINCLUDE = -I$(linuxhdrs)/arch/x86/include -I$(linuxhdrs)/arch/x86/include/generated -I$(linuxhdrs)/include -I$(linuxhdrs)/arch/x86/include/uapi -I$(linuxhdrs)/arch/x86/include/generated/uapi -I$(linuxhdrs)/include/uapi -I$(linuxhdrs)/include/generated/uapi -I/usr/include -I/home/cfc4n/download/linux-5.11.0/tools/lib

all: $(KERN_OBJECTS) build @echo $(shell date)
.PHONY: clean
clean: rm -rf kern/*.o rm -rf user/bytecode/*.o rm -rf network-monitoring
$(KERN_OBJECTS): %.o: %.c $(CLANG) $(EXTRA_CFLAGS) $(LINUXINCLUDE) -include kern/chim_helpers.h -Wno-deprecated-declarations -Wno-gnu-variable-sized-type-not-at-end -Wno-pragma-once-outside-header -Wno-address-of-packed-member -Wno-unknown-warning-option -fno-unwind-tables -fno-asynchronous-unwind-tables -Wno-unused-value -Wno-pointer-sign -fno-stack-protector -c $< -o -|$(LLC) -march=bpf -filetype=obj -o $(subst kern/,user/bytecode/,$@)
build: go build .
还可以使用cilium[6] 提供的bpf2go库。这需要依赖go generate命令,当运行该命令的时候会扫描当前包相关的源代码文件,找出所有包含//go:generate的特殊注释,提取并执行该特殊注释后面的命令。具体可以参考这里[7]
当然,不同的内核可能还有很多不同的功能特性,踩坑是必然的。


开发demo



这里使用cilium作为应用层依赖库,其中的例子也来源于cilium提供的Examples[8]
为了使用bpf2go库,需要在go代码里面加入下列注释(建议加在main.go):
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc clang bpf cgroup_skb.c -- -I../headers
在examples中的main.go中利用了Makefile,这里我做了一些修改,其实就是把Makefile里面的变量引出来方便单独使用。
几乎在每个例子里面都会有这样的代码:
// Allow the current process to lock memory for eBPF resources.  if err := rlimit.RemoveMemlock(); err != nil {    log.Fatal(err)
注释表明这是允许当前进程为eBPF资源锁定内存,但是在crate eBPF maps的时候,内存分配在内核空间,而且内核内存不会被交换出来,那么为什么还需要从用户态锁定内存?
这里其实是用go库做了封装,本质上是通过setrlimit()函数的RLIMIT_MEMLOCK参数实现的。RLIMIT_MEMLOCK指的是可以锁定的最大RAM内存,它并不特定于用户空间内存地址范围的分配,在5.11版本之前的内核,内存被用于eBPF对象,这意味着你有可能短时间创建大量对象消耗内存,通过该参数限制内存来防止出现错误。
Go常用函数:
  • loadBpfObjects:加载预编译的程序和maps到内核。

  • link库,这个主要是将eBPF程序attach到不同的hook位置。

C编写:
SEC("cgroup_skb/egress")
        SEC("fentry/tcp_connect")
        SEC("kprobe/sys_execve")
在C中存在大量的SEC宏,这些宏表示在当前的obj文件中新增一个段(section)。
char __license[] SEC("license") = "Dual MIT/GPL";
同时为了满足GPL的需要,所有注入内核的BPF代码都必须显式的支持GPL协议。
struct bpf_map_def SEC("maps") pkt_count = {  .type        = BPF_MAP_TYPE_ARRAY,  .key_size    = sizeof(u32),  .value_size  = sizeof(u64),  .max_entries = 1,};
这里表明创建一个maps,来共享信息,其中BPF_MAP_TYPE_ARRAY在之前环境准备中生成的vmlinux.h中定义。
SEC("cgroup_skb/egress")int count_egress_packets(struct __sk_buff *skb) {  u32 key      = 0;  u64 init_val = 1;
u64 *count = bpf_map_lookup_elem(&pkt_count, &key); if (!count) { bpf_map_update_elem(&pkt_count, &key, &init_val, BPF_ANY); return 1; } __sync_fetch_and_add(count, 1);
return 1;}
bpf_map_look_elem函数的原型为bpf_map_lookup_elem(map_fd, void *key),表明在map_fd中寻找key,并返回对应的value。
  • bpf_map_update_elem(map_fd, void *key, void *value): 更新key或者value。
  • bpf_map_delete_elem(map_fd, void *key)在map_fd中删除一个键。
  • __sync_fetch_and_add (T* p, U v, ...)该函数自动增加v到指针p指向的内容。

《eBPF 系列(一): 初探eBPF》

04

《eBPF 系列(一): 初探eBPF》

关键词


  • ebpf映射是为了保存多类型数据的通用数据结构,用户可以创建多个映射通过fd访问,以便进行数据共享。

  • Helper functions可以帮助eBPF程序操作数据。

  • 因此一个BPF程序是否在内核中存活取决于 通过bpf()载入内核后如何进一步附加在别的子系统上

  • llvm 作为BPF的后端使得clang等可以直接编译c到bpf object file

  • XDP BPF程序在最早的网络驱动阶段被attch,然后再收到数据包的时候触发网络执行。

  • 内核里面的BPF程序都是事件驱动

  • BPF 由 11 个 64 位寄存器和 32 位子寄存器、一个程序计数器和一个 512 字节的大 BPF 堆栈空间组成。寄存器命名为 r0 - r10。操作模式默认为 64 位,32 位子寄存器只能通过特殊的 ALU(算术逻辑单元)操作访问。低 32 位子寄存器在被写入时零扩展为 64 位。

  • 当前不支持具有 6 个或更多参数的调用。内核中专用于 BPF 的辅助函数(BPF_CALL_0() 到 BPF_CALL_5() 函数)是专门为这种约定而设计的。

  • 在进入 BPF 程序执行时,寄存器 r1 最初包含程序的上下文。上下文是程序的输入参数(类似于典型 C 程序的 argc/argv 对)。BPF 仅限于在单个上下文中工作。上下文由程序类型定义,例如,网络程序可以将网络数据包 (skb) 的内核表示作为输入参数。

  • 尽管指令集包含前向和后向跳转,但内核 BPF 验证器将禁止循环,以便始终保证终止

  • 还有一个尾调用的概念,它允许一个 BPF 程序跳转到另一个 BPF 程序。这也带有 33 个调用的嵌套上限,通常用于将程序逻辑的部分解耦,例如,分解为阶段。

  • Bpf call 和 bpf helper功能混合在5.9 kernel ,回环调用可能引起堆栈溢出

  • BPF locks the entire BPF interpreter image (struct bpf_prog) as well as the JIT compiled image (struct bpf_binary_header) in the kernel as read-only during the program’s lifetime in order to prevent the code from potential corruptions.



《eBPF 系列(一): 初探eBPF》

05

《eBPF 系列(一): 初探eBPF》

参考链接


     引用连接

  • [1]  https://github.com/iovisor/bcc

  • [2]  https://github.com/torvalds/linux/blob/master/kernel/bpf/verifier.c

  • [3]  https://man7.org/linux/man-pages/man7/bpf-helpers.7.html

  • [4]  https://man7.org/linux/man-pages/man2/bpf.2.html

  • [5]  https://nakryiko.com/posts/bpf-core-reference-guide/

  • [6]  https://github.com/cilium/ebpf

  • [7]  http://c.biancheng.net/view/4442.html

  • [8]  https://github.com/cilium/ebpf/tree/master/examples


其他连接

  • https://www.infoq.com/articles/gentle-linux-ebpf-introduction/
  • https://developpaper.com/ebpf-development-guide/
  • https://segmentfault.com/a/1190000041178939#item-6
  • https://www.anquanke.com/post/id/263803
  • https://www.seekret.io/blog/a-practical-guide-to-capturing-production-traffic-with-ebpf/



《eBPF 系列(一): 初探eBPF》


原文始发于微信公众号(RainSec):《eBPF 系列(一): 初探eBPF》

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年5月3日18:20:48
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   《eBPF 系列(一): 初探eBPF》https://cn-sec.com/archives/970816.html

发表评论

匿名网友 填写信息