bpf-bootstrap
是Linux ebpf技术的开发库,使用的要求是已经安装了ebpf的前置依赖包,并且需要CO-RE,即内核编译选项默认内置支持了CONFIG_DEBUG_INFO_BTF=y
这个开关用于控制BTF开启与关闭,检查这个文件/sys/kernel/btf/vmlinux
是否存在且非空,如果非空说明BTF信息就已经开启且生成了
成功执行完以上步骤后libbpf就成功拉取下来了
进入到examples/c
目录下,有几份demo代码,minimal
是最小的框架,通过以下命令编译
成功将bpf加载到内核,可以看到这样就不需要太多繁琐的步骤,不需要再通过命令的方式手动将内核文件插入到内核中。只需执行一个程序即可
首先来分析一下minimal的代码,分为minimal.c
和minimal.bpf.c
前者为用户空间的代码 后者为内核空间的代码,先来分析内核空间的代码
程序声明了许可协议,这行代码定义了eBPF程序的许可证信息,使用SEC("license")指定了该程序的许可。这段信息通常包含程序的许可证类型,这有助于清楚地指明该程序的许可。
SEC
是一个宏定义,SEC
宏通常用于指定程序类型和相关的挂钩,宏中的信息通常可以填写以下内容:
可以设置为内核跟踪点,在Linux中提供了内核跟踪点,通过以下命令可以查询到
这个很容易理解,直接就是SEC("socket")
就行
声明一个eBPF程序的段
这段代码中,通过向sys_enter_write
内核函数挂钩,同时hook到printf等函数的使用 通过my_pid
限制挂钩等进程,只要当前进程向终端打印了数据,就会使用bpf_printk()
进行打印,通过以下命令可以看到打印的信息
如下图所示
来看看用户空间的代码,用户空间用于与内核进行交互,通过用户空间的代码可以间接控制内核。以下为minimal
用户空间代码。
这段代码的主要作用是将ebpf程序加载到内核,分为了这几个步骤(打开并加载skeleton、附加skeleton(设置hook)、触发hook函数、结束后销毁skel)。
代码中还设置了一个回调函数libbpf_print_fn
,这个函数的作用是将ebpf程序中的所有debug信息显示到终端上,方便查看与调试。
minimal.c
中通过skel指针可以和内核代码的数据进行交互,完成内核空间与用户空间的数据交互,将内核代码中的my_pid
设置为当前进程。
与正常通过clang
手动编译的不同的是,这份代码多了一步加载了minimal.skel.h
头文件,这个文件是通过编译器生成出来的,可以看到在example/c
目录下的代码都有引导这个文件"[文件名].skel.h"
,并且也用到了类似于这样的函数"[文件名]_bpf_open_and_load()"
,具体这段代码如何产生的,有兴趣可以自己翻一下makefile文件。
这样的头文件被称为ebpf骨架,骨架的作用是简化eBPF程序的开发,提供了一些通用的功能和结构,使得开发者能够更容易地创建、加载和管理eBPF程序。在example/c
文件下创建项目为[项目名称].c
[项目名称].bpf.c
通过以下命令生成skeleton文件。
打开minimal.bpf.c
文件可以看到有如下结构体
为maps
progs
links
bss
这几个结构体,分别描述了内核文件的所有对象。
代码中还定义了minimal_bpf__open
minimal_bpf__load
minimal_bpf__attach
minimal_bpf__destory
这几个函数。
接下来看一个比较关心的问题,如何通过ebpf创建钩子获取并修改网络数据包,同样在example/c
中有写到关于向socket进行挂钩的项目分别为sockfilter.c
sockfilter.h
sockfilter.bpf.c
首先进行编译
运行后的结果如下所示,程序获得了回环网卡的所有网络信息
这份代码与minimal
在流程上是有很大的区别的,依然是使用了sockfilter_bpf__open_and_load()
加载了skel,但是不同的是,没有使用到sockfilter_bpf__attach()
函数,而是使用了bpf_program__fd()
函数。这个函数的作用是,获取ebpf文件描述符,获取到了内核代码的socket_handler()
函数的描述符,再通过setsockopt()
函数设置了一些操作(这个的作用后文会描述)。
而且与minimal
还有不同的是,多了一个ringbuf
的东西(环形缓冲区),环形缓冲区的作用主要是为了方便内核代码与用户空间代码进行交互 交互流程如下
用户空间中的代码新的东西就这么多,接下来内核空间中与tc钩子的方法差不多,多的是创建了rb结构体,并放到了.maps
中,定义了环形缓冲区的限制信息。
代码中先创建了初始套接字socket,并设置了PF_PACKET
、SOCK_RAW
、SOCK_NONBLOCK
、SOCK_CLOEXEC
这些属性,使用原始套接字 (raw socket) 编程,以便在链路层进行底层网络数据包的操作。在原始套接字编程中使用 bind 函数将套接字与指定的网络接口绑定。具体来说,它用于将套接字与指定的网络接口关联,以便监听或发送数据。_sll.sll_ifindex = if_nametoindex(name)_用来绑定网卡。
setsockopt
函数用于设置套接字选项,允许应用程序配置套接字的各种属性。以下是 setsockopt的参数:
sockfd
: 套接字描述符。
level
: 选项所属的协议层,常见的有 SOL_SOCKET
、IPPROTO_TCP
、IPPROTO_UDP
等。
optname
: 选项的名称,表示要设置的具体选项。
optval
: 指向包含新选项值的缓冲区的指针。
optlen
: 缓冲区的大小,即选项值的长度
前面通过bpf_program__fd()
函数获取到了hook函数的文件描述符,通过setsockopt设置挂钩,程序在使用socket后就会先通过内核代码中的socket_handler()
函数
完整的代码注释如下
在内核代码中使用了bpf_skb_load_bytes()
函数,通过偏移值获,从skb中获取信息,通过bpf_ringbuf_reserve()
函数获取到交互数据,并进行修改后通过bpf_ringbuf_submit()
返回给用户空间。
通过创建了2个结构体skb
与e
,skb是hook后拿到的上下文信息,e提取了skb的一些关键信息,再返回给用户空间。
以下是代码的全部注释:
git clone https:
//github
.com
/libbpf/libbpf-bootstrap
.git
git submodule update --init --recursive
git clone https:
//github
.com
/libbpf/libbpf-bootstrap
.git
git submodule update --init --recursive
sudo
make
minimal
//
编译程序
sudo
.
/minimal
//
运行程序 需要以root用户运行
sudo
make
minimal
//
编译程序
sudo
.
/minimal
//
运行程序 需要以root用户运行
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
char
LICENSE[] SEC(
"license"
) =
"Dual BSD/GPL"
;
int
my_pid = 0;
SEC(
"tp/syscalls/sys_enter_write"
)
int
handle_tp(
void
*ctx)
{
int
pid = bpf_get_current_pid_tgid() >> 32;
if
(pid != my_pid)
return
0;
bpf_printk(
"BPF triggered from PID %d.\n"
, pid);
return
0;
}
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
char
LICENSE[] SEC(
"license"
) =
"Dual BSD/GPL"
;
int
my_pid = 0;
SEC(
"tp/syscalls/sys_enter_write"
)
int
handle_tp(
void
*ctx)
{
int
pid = bpf_get_current_pid_tgid() >> 32;
if
(pid != my_pid)
return
0;
bpf_printk(
"BPF triggered from PID %d.\n"
, pid);
return
0;
}
sudo ls cd /sys/kernel/debug/tracing/events
sudo cat /sys/kernel/debug/tracing/available_events
常见的写法有
SEC(
"tracepoint/syscalls/sys_enter_openat"
) 用来hook系统调用的sys_enter_openat()
sudo ls cd /sys/kernel/debug/tracing/events
sudo cat /sys/kernel/debug/tracing/available_events
常见的写法有
SEC(
"tracepoint/syscalls/sys_enter_openat"
) 用来hook系统调用的sys_enter_openat()
SEC(
"xdp"
)
int
xdp_example(
struct
__sk_buff *skb) {
return
XDP_PASS;
}
SEC(
"xdp"
)
int
xdp_example(
struct
__sk_buff *skb) {
return
XDP_PASS;
}
SEC(
"custom"
)
int
custom_example(
void
*ctx) {
return
0;
}
SEC(
"custom"
)
int
custom_example(
void
*ctx) {
return
0;
}
sudo cat /sys/kernel/debug/tracing/trace_pipe
sudo cat /sys/kernel/debug/tracing/trace_pipe
#include <stdio.h>
#include <unistd.h>
#include <sys/resource.h>
#include <bpf/libbpf.h>
#include "minimal.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
minimal_bpf *skel;
int
err;
libbpf_set_print(libbpf_print_fn);
skel = minimal_bpf__open();
if
(!skel) {
fprintf
(stderr,
"Failed to open BPF skeleton\n"
);
return
1;
}
skel->bss->my_pid = getpid();
err = minimal_bpf__load(skel);
if
(err) {
fprintf
(stderr,
"Failed to load and verify BPF skeleton\n"
);
goto
cleanup;
}
err = minimal_bpf__attach(skel);
if
(err) {
fprintf
(stderr,
"Failed to attach BPF skeleton\n"
);
goto
cleanup;
}
printf
(
"Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` "
"to see output of the BPF programs.\n"
);
for
(;;) {
fprintf
(stderr,
"."
);
sleep(1);
}
cleanup:
minimal_bpf__destroy(skel);
return
-err;
}
#include <stdio.h>
#include <unistd.h>
#include <sys/resource.h>
#include <bpf/libbpf.h>
#include "minimal.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
minimal_bpf *skel;
int
err;
libbpf_set_print(libbpf_print_fn);
skel = minimal_bpf__open();
if
(!skel) {
fprintf
(stderr,
"Failed to open BPF skeleton\n"
);
return
1;
}
skel->bss->my_pid = getpid();
err = minimal_bpf__load(skel);
if
(err) {
fprintf
(stderr,
"Failed to load and verify BPF skeleton\n"
);
goto
cleanup;
}
err = minimal_bpf__attach(skel);
if
(err) {
fprintf
(stderr,
"Failed to attach BPF skeleton\n"
);
goto
cleanup;
}
printf
(
"Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` "
"to see output of the BPF programs.\n"
);
for
(;;) {
fprintf
(stderr,
"."
);
sleep(1);
}
cleanup:
minimal_bpf__destroy(skel);
return
-err;
}
sudo cmake .
sudo make [项目名称]
sudo ./[文件名]
sudo cmake .
sudo make [项目名称]
sudo ./[文件名]
maps 是 libbpf中可以作为数据交换的结构体
progs 通常指代 BPF Program 对象的集合。在 BPF 中,Program 是执行具体功能的代码单元 通常为bpf程序的函数
links 通常指代 BPF Link 对象的集合。在 BPF 中,Link 用于将 BPF Program 关联到特定的 BPF Hook 或事件
bss 通常指代 BSS(Block Started by Symbol)段,这是程序中的一块内存,用于存储未初始化的全局或静态变量
maps 是 libbpf中可以作为数据交换的结构体
progs 通常指代 BPF Program 对象的集合。在 BPF 中,Program 是执行具体功能的代码单元 通常为bpf程序的函数
links 通常指代 BPF Link 对象的集合。在 BPF 中,Link 用于将 BPF Program 关联到特定的 BPF Hook 或事件
bss 通常指代 BSS(Block Started by Symbol)段,这是程序中的一块内存,用于存储未初始化的全局或静态变量
sudo make sockfilter
sudo ./sockfilter
sudo make sockfilter
sudo ./sockfilter
是用户空间中ring_buffer__new() ring_buffer__poll() 创建环形缓冲区并设置回调函数 轮询环形缓冲区获取最新数据
内核空间bpf_ringbuf_reserve() bpf_ringbuf_submit() 为环形缓冲区保留事件的空间 提交数据到回调函数
是用户空间中ring_buffer__new() ring_buffer__poll() 创建环形缓冲区并设置回调函数 轮询环形缓冲区获取最新数据
内核空间bpf_ringbuf_reserve() bpf_ringbuf_submit() 为环形缓冲区保留事件的空间 提交数据到回调函数
PF_PACKET 表示使用 packet socket。
SOCK_RAW 表示使用原始套接字,可以处理数据链路层的数据。
SOCK_NONBLOCK 表示以非阻塞模式打开套接字。
SOCK_CLOEXEC 表示在执行 exec 时关闭套接字。
htons(ETH_P_ALL) 表示捕获所有协议的数据包。
PF_PACKET 表示使用 packet socket。
SOCK_RAW 表示使用原始套接字,可以处理数据链路层的数据。
SOCK_NONBLOCK 表示以非阻塞模式打开套接字。
SOCK_CLOEXEC 表示在执行 exec 时关闭套接字。
htons(ETH_P_ALL) 表示捕获所有协议的数据包。
#include <stddef.h>
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/in.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#include "sockfilter.h" //example/c 下编译自带
struct
{
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} rb SEC(
".maps"
);
#define IP_MF 0x2000
#define IP_OFFSET 0x1FFF
char
LICENSE[] SEC(
"license"
) =
"Dual BSD/GPL"
;
static
inline
int
ip_is_fragment(
struct
__sk_buff *skb, __u32 nhoff)
{
__u16 frag_off;
bpf_skb_load_bytes(skb, nhoff + offsetof(
struct
iphdr, frag_off), &frag_off, 2);
frag_off = __bpf_ntohs(frag_off);
return
frag_off & (IP_MF | IP_OFFSET);
}
SEC(
"socket"
)
int
socket_hook(
struct
__sk_buff *skb){
struct
so_event *e;
__u8 verlen;
__u16 proto;
__u32 nhoff = ETH_HLEN;
bpf_skb_load_bytes(skb, 12, &proto, 2);
proto = __bpf_ntohs(proto);
if
(proto != ETH_P_IP)
return
0;
if
(ip_is_fragment(skb, nhoff))
return
0;
e = bpf_ringbuf_reserve(&rb,
sizeof
(*e), 0);
if
(!e)
return
0;
bpf_skb_load_bytes(skb, nhoff + offsetof(
struct
iphdr, protocol), &e->ip_proto, 1);
if
(e->ip_proto != IPPROTO_GRE) {
bpf_skb_load_bytes(skb, nhoff + offsetof(
struct
iphdr, saddr), &(e->src_addr), 4);
bpf_skb_load_bytes(skb, nhoff + offsetof(
struct
iphdr, daddr), &(e->dst_addr), 4);
}
bpf_skb_load_bytes(skb, nhoff + 0, &verlen, 1);
bpf_skb_load_bytes(skb, nhoff + ((verlen & 0xF) << 2), &(e->ports), 4);
e->pkt_type = skb->pkt_type;
e->ifindex = skb->ifindex;
bpf_printk(
"index %d"
,skb->ifindex);
bpf_ringbuf_submit(e, 0);
return
skb->len;
}
#include <stddef.h>
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/in.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#include "sockfilter.h" //example/c 下编译自带
struct
{
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} rb SEC(
".maps"
);
#define IP_MF 0x2000
#define IP_OFFSET 0x1FFF
char
LICENSE[] SEC(
"license"
) =
"Dual BSD/GPL"
;
static
inline
int
ip_is_fragment(
struct
__sk_buff *skb, __u32 nhoff)
{
__u16 frag_off;
bpf_skb_load_bytes(skb, nhoff + offsetof(
struct
iphdr, frag_off), &frag_off, 2);
frag_off = __bpf_ntohs(frag_off);
return
frag_off & (IP_MF | IP_OFFSET);
}
SEC(
"socket"
)
int
socket_hook(
struct
__sk_buff *skb){
struct
so_event *e;
__u8 verlen;
__u16 proto;
__u32 nhoff = ETH_HLEN;
bpf_skb_load_bytes(skb, 12, &proto, 2);
proto = __bpf_ntohs(proto);
if
(proto != ETH_P_IP)
return
0;
if
(ip_is_fragment(skb, nhoff))
return
0;
e = bpf_ringbuf_reserve(&rb,
sizeof
(*e), 0);
if
(!e)
return
0;
bpf_skb_load_bytes(skb, nhoff + offsetof(
struct
iphdr, protocol), &e->ip_proto, 1);
if
(e->ip_proto != IPPROTO_GRE) {
bpf_skb_load_bytes(skb, nhoff + offsetof(
struct
iphdr, saddr), &(e->src_addr), 4);
bpf_skb_load_bytes(skb, nhoff + offsetof(
struct
iphdr, daddr), &(e->dst_addr), 4);
}
bpf_skb_load_bytes(skb, nhoff + 0, &verlen, 1);
bpf_skb_load_bytes(skb, nhoff + ((verlen & 0xF) << 2), &(e->ports), 4);
e->pkt_type = skb->pkt_type;
e->ifindex = skb->ifindex;
bpf_printk(
"index %d"
,skb->ifindex);
bpf_ringbuf_submit(e, 0);
return
skb->len;
}
#include <argp.h>
#include <arpa/inet.h>
#include <assert.h>
#include <bpf/libbpf.h>
#include <linux/if_packet.h>
#include <linux/if_ether.h>
#include <linux/in.h>
#include <net/if.h>
#include <signal.h>
#include <stdio.h>
#include <sys/resource.h>
#include <sys/socket.h>
#include <unistd.h>
#include "sockfilter.h"
#include "shell.skel.h"
static
struct
env {
const
char
*interface;
} env;
static
const
char
*ipproto_mapping[IPPROTO_MAX] = {
[IPPROTO_IP] =
"IP"
, [IPPROTO_ICMP] =
"ICMP"
, [IPPROTO_IGMP] =
"IGMP"
,
[IPPROTO_IPIP] =
"IPIP"
, [IPPROTO_TCP] =
"TCP"
, [IPPROTO_EGP] =
"EGP"
,
[IPPROTO_PUP] =
"PUP"
, [IPPROTO_UDP] =
"UDP"
, [IPPROTO_IDP] =
"IDP"
,
[IPPROTO_TP] =
"TP"
, [IPPROTO_DCCP] =
"DCCP"
, [IPPROTO_IPV6] =
"IPV6"
,
[IPPROTO_RSVP] =
"RSVP"
, [IPPROTO_GRE] =
"GRE"
, [IPPROTO_ESP] =
"ESP"
,
[IPPROTO_AH] =
"AH"
, [IPPROTO_MTP] =
"MTP"
, [IPPROTO_BEETPH] =
"BEETPH"
,
[IPPROTO_ENCAP] =
"ENCAP"
, [IPPROTO_PIM] =
"PIM"
, [IPPROTO_COMP] =
"COMP"
,
[IPPROTO_SCTP] =
"SCTP"
, [IPPROTO_UDPLITE] =
"UDPLITE"
, [IPPROTO_MPLS] =
"MPLS"
,
[IPPROTO_RAW] =
"RAW"
};
static
inline
void
ltoa(uint32_t addr,
char
*dst)
{
snprintf(dst, 16,
"%u.%u.%u.%u"
, (addr >> 24) & 0xFF, (addr >> 16) & 0xFF,
(addr >> 8) & 0xFF, (addr & 0xFF));
}
static
int
handle_event(
void
*ctx,
void
*data,
size_t
data_sz)
{
const
struct
so_event *e = data;
char
ifname[IF_NAMESIZE];
char
sstr[16] = {}, dstr[16] = {};
if
(e->pkt_type != PACKET_HOST)
return
0;
if
(e->ip_proto < 0 || e->ip_proto >= IPPROTO_MAX)
return
0;
if
(!if_indextoname(e->ifindex, ifname))
return
0;
ltoa(ntohl(e->src_addr), sstr);
ltoa(ntohl(e->dst_addr), dstr);
printf
(
"interface: %s\tprotocol: %s\t%s:%d(src) -> %s:%d(dst)\n"
, ifname,
ipproto_mapping[e->ip_proto], sstr, ntohs(e->port16[0]), dstr, ntohs(e->port16[1]));
return
0;
}
static
int
open_raw_sock(
const
char
*name)
{
struct
sockaddr_ll sll;
int
sock;
sock = socket(PF_PACKET, SOCK_RAW | SOCK_NONBLOCK | SOCK_CLOEXEC, htons(ETH_P_ALL));
if
(sock < 0) {
fprintf
(stderr,
"Failed to create raw socket\n"
);
return
-1;
}
memset
(&sll, 0,
sizeof
(sll));
sll.sll_family = AF_PACKET;
sll.sll_ifindex = if_nametoindex(name);
sll.sll_protocol = htons(ETH_P_ALL);
if
(bind(sock, (
struct
sockaddr *)&sll,
sizeof
(sll)) < 0) {
fprintf
(stderr,
"Failed to bind to %s: %s\n"
, name,
strerror
(
errno
));
close(sock);
return
-1;
}
return
sock;
}
static
int
libbpf_print_fn(
enum
libbpf_print_level level,
const
char
*format,
va_list
args)
{
return
vfprintf
(stderr, format, args);
}
static
volatile
bool
exiting =
false
;
static
void
sig_handler(
int
sig)
{
exiting =
true
;
}
int
main(){
struct
shell_bpf *skel;
struct
ring_buffer *rb = NULL;
int
err,sock,prog_fd;
libbpf_set_print(libbpf_print_fn);
skel = shell_bpf__open_and_load();
if
(!skel)
{
fprintf
(stderr,
"Open ebpf Skeleton Error\n"
);
return
-1;
}
skel->bss->protect_pid = getppid();
env.interface =
"lo"
;
signal
(SIGINT, sig_handler);
signal
(SIGTERM, sig_handler);
rb = ring_buffer__new(bpf_map__fd(skel->maps.rb), handle_event, NULL, NULL);
if
(!rb) {
err = -1;
fprintf
(stderr,
"Failed to create ring buffer\n"
);
goto
clean;
}
sock = open_raw_sock(env.interface);
if
(sock < 0) {
err = -2;
fprintf
(stderr,
"Failed to open raw socket\n"
);
goto
clean;
}
prog_fd = bpf_program__fd(skel->progs.socket_hook);
if
(setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, &prog_fd,
sizeof
(prog_fd))) {
err = -3;
fprintf
(stderr,
"Failed to attach to raw socket\n"
);
goto
clean;
}
while
(!exiting) {
err = ring_buffer__poll(rb, 100
);
if
(err == -EINTR) {
err = 0;
break
;
}
if
(err < 0) {
fprintf
(stderr,
"Error polling perf buffer: %d\n"
, err);
break
;
}
sleep(1);
}
clean:
shell_bpf__destroy(skel);
ring_buffer__free(rb);
return
0;
}
#include <argp.h>
#include <arpa/inet.h>
#include <assert.h>
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)