【ebpf】BCC实现tcpv4连接追踪

admin 2024年2月12日14:03:32评论20 views字数 4313阅读14分22秒阅读模式
【ebpf】BCC实现tcpv4连接追踪

免责声明

【ebpf】BCC实现tcpv4连接追踪

本文仅用于技术讨论与学习,利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,文章作者及本公众号不为此承担任何责任。

【ebpf】BCC实现tcpv4连接追踪

tcpv4追踪

【ebpf】BCC实现tcpv4连接追踪

BCC作为一个流行的ebpf开发方案,提供了很多案例供开发者学习,其中tracing/tcpv4connect.py是BCC官方提供的追踪机器TCPv4连接的案例。追踪tcpv4连接对于入侵检测很有帮助,值得学习。

【ebpf】BCC实现tcpv4连接追踪

【ebpf】BCC实现tcpv4连接追踪

内核态源码

【ebpf】BCC实现tcpv4连接追踪

先贴出内核态源码:

#include <uapi/linux/ptrace.h>#include <net/sock.h>#include <bcc/proto.h>BPF_HASH(currsock, u32, struct sock *);int kprobe__tcp_v4_connect(struct pt_regs *ctx, struct sock *sk){  u32 pid = bpf_get_current_pid_tgid();  // stash the sock ptr for lookup on return  currsock.update(&pid, &sk);  return 0;};int kretprobe__tcp_v4_connect(struct pt_regs *ctx){  int ret = PT_REGS_RC(ctx);  u32 pid = bpf_get_current_pid_tgid();  struct sock **skpp;  skpp = currsock.lookup(&pid);  if (skpp == 0) {    return 0;  // missed entry  }  if (ret != 0) {    // failed to send SYNC packet, may not have populated    // socket __sk_common.{skc_rcv_saddr, ...}    currsock.delete(&pid);    return 0;  }  // pull in details  struct sock *skp = *skpp;  u32 saddr = skp->__sk_common.skc_rcv_saddr;  u32 daddr = skp->__sk_common.skc_daddr;  u16 dport = skp->__sk_common.skc_dport;  // output  bpf_trace_printk("trace_tcp4connect %x %x %d\n", saddr, daddr, ntohs(dport));  currsock.delete(&pid);  return 0;}
这段内核态的代码是利用eBPF(extended Berkeley Packet Filter)编写的,用于跟踪和记录IPv4的TCP连接尝试。
  • 引入必要的头文件,提供对ptrace(用于跟踪和断点)、sock(socket相关结构定义)和bcc/proto.h(BCC提供的协议相关工具)的支持。
  • BPF_HASH(currsock, u32, struct sock *)定义了一个名为currsock的BPF哈希表,用于存储进程ID(u32类型)和对应socket对象指针(struct sock *类型)。
  • int kprobe__tcp_v4_connect(struct pt_regs *ctx, struct sock *sk):定义了一个Kprobe,即内核探测点,它在tcp_v4_connect函数被调用时被触发。这个函数将当前进程ID (pid) 和socket对象地址 (&sk) 存入之前定义的currsock哈希表中。
  • kprobe的返回值总是0,这表示这个探测点总是成功的。
  • int kretprobe__tcp_v4_connect(struct pt_regs *ctx):定义了一个Kretprobe,即内核返回探测点,在tcp_v4_connect函数返回之后触发。这个函数首先获取返回值(ret)和当前进程ID (pid)。
  • skpp = currsock.lookup(&pid);:通过进程ID查找之前存储的socket对象指针。
  • 如果找不到对应的入口(if (skpp == 0)),说明在调用函数之前该入口已经丢失,函数返回0。
  • 如果ret不为0,表示连接尝试失败(无法发送同步(SYN)包),这时会从哈希表中删除对应的进程ID入口。
  • 如果连接成功,代码会从socket对象获取源地址(saddr)、目的地址(daddr)和目的端口号(dport),然后使用bpf_trace_printk将这些信息输出到内核跟踪日志中。ntohs(dport)用于将网络字节序转换成主机字节序。
  • 结尾处,会再次从哈希表中删除对应的进程ID入口。
通过上述过程,该代码能够监控并记录所有尝试建立的TCP IPv4连接,无论这些连接是否成功建立。
【ebpf】BCC实现tcpv4连接追踪

直接访问内核函数参数

【ebpf】BCC实现tcpv4连接追踪
正常情况下,在BCC的kprobe语法中,通常只需要一个参数:struct pt_regs *ctx,用来访问被绑定函数的上下文信息,包括注册信息和传递过来的参数。但是,BCC有一个高级的特性,它可以直接访问内核函数的参数而不需要通过指针寄存器手动提取。
BCC利用eBPF能够安全访问内核数据结构的这一特性,允许在定义kprobe时直接在函数参数中包括内核函数的参数。当BCC程序被装载时,BCC会处理这些额外的参数,并生成eBPF代码来从注册信息中提取这些参数,因此用户不需要直接操作struct pt_regs去访问这些信息。
因此在官方的例子中:
int kprobe__tcp_v4_connect(struct pt_regs *ctx, struct sock *sk)
struct sock *sk 是tcp_v4_connect函数的参数。在BCC的实现中,当eBPF程序被加载时,它会自动为你的kprobe函数绑定这个参数,让写程序的人更方便地访问这些信息。
这个高级特性允许eBPF程序直接和内核函数的本地变量交互,简化了eBPF程序的编写。但是这个特性要求开发者知道内核函数的确切原型,因为如果参数类型或其他细节出错,eBPF程序可能无法正确加载或工作。
【ebpf】BCC实现tcpv4连接追踪

bpf_get_current_pid_tgid

【ebpf】BCC实现tcpv4连接追踪
pid 是 "Process ID" 的缩写,它是一个唯一的标识符,用于在操作系统中标识一个特定的进程。
tgid 是 "Thread Group ID" 的缩写,它被用来标识一个线程组的头,其中的所有线程共享相同的进程上下文。对于多线程程序,每个线程都会有自己的线程ID(通常称为tid),但是同一个进程内的所有线程将共享相同的tgid。
在Linux内核中,tgid与我们通常说的"进程ID"相同。如果一个进程只有一个线程,那么它的pid与tgid相同。但是,如果一个进程拥有多个线程,每个线程将有不同的pid,而tgid将和主线程(即进程)的pid相同。
在eBPF和BCC的上下文中,bpf_get_current_pid_tgid() 这个函数返回的是一个64位的值,其中高32位是tgid,低32位是pid。因此,如果你需要分别获得pid和tgid,你需要对这个64位值进行分割。例如:
u32 pid = bpf_get_current_pid_tgid() >> 32; 来获取pid。
u32 tgid = bpf_get_current_pid_tgid() & 0xFFFFFFFF; 来获取tgid。
这样可以帮助eBPF程序理解上下文是单线程的还是多线程的进程。
【ebpf】BCC实现tcpv4连接追踪

PT_REGS_RC

【ebpf】BCC实现tcpv4连接追踪
是的,PT_REGS_RC(ctx) 宏用于获取一个被kretprobe挂钩的内核函数的返回值。在一个kretprobe(内核返回探针)或与之关联的回调函数中,可以通过传入的 ctx(上下文)指针来访问寄存器的状态。
这里的 ctx 是一个指向 struct pt_regs 的指针,这是一个在Linux内核中定义的结构体,包含了函数调用时保存的寄存器的状态。PT_REGS_RC(ctx) 宏提取了函数返回值所在的寄存器的内容。
在x86架构上,函数的返回值通常存储在 rax 寄存器中;在arm架构上,通常是 r0。这个宏封装了底层细节,使得无论在哪种架构上,开发者使用 PT_REGS_RC(ctx) 都能得到函数返回值。
【ebpf】BCC实现tcpv4连接追踪

用户态源码

【ebpf】BCC实现tcpv4连接追踪
先贴源码:
# initialize BPFb = BPF(text=bpf_text)# headerprint("%-6s %-12s %-16s %-16s %-4s" % ("PID", "COMM", "SADDR", "DADDR",    "DPORT"))def inet_ntoa(addr):  dq = b''  for i in range(0, 4):    dq = dq + str(addr & 0xff).encode()    if (i != 3):      dq = dq + b'.'    addr = addr >> 8  return dq# filter and format outputwhile 1:  # Read messages from kernel pipe  try:      (task, pid, cpu, flags, ts, msg) = b.trace_fields()      (_tag, saddr_hs, daddr_hs, dport_s) = msg.split(b" ")  except ValueError:      # Ignore messages from other tracers      continue  except KeyboardInterrupt:      exit()  # Ignore messages from other tracers  if _tag.decode() != "trace_tcp4connect":      continue  printb(b"%-6d %-12.12s %-16s %-16s %-4s" % (pid, task,      inet_ntoa(int(saddr_hs, 16)),      inet_ntoa(int(daddr_hs, 16)),      dport_s))
主要通过循环读取由eBPF程序通过 trace_printk() 输出到内核追踪管道的消息。
  • b.trace_fields() 从追踪管道读取一行消息。
  • msg.split(b" ") 分割消息成四个部分:tag标记,源地址(网络字节顺序),目的地址(网络字节顺序),目的端口。
  • 如果消息不是由我们的BPF程序生成的(即不带有"trace_tcp4connect"标记),将忽略该消息并继续读取下一行。
  • printb(...) 根据消息内容格式化并打印出每个TCP连接尝试的信息:
    • pid 进程ID
    • task 命令名称
    • inet_ntoa(...) 转换并显示源和目标地址
    • dport_s 目的端口
【ebpf】BCC实现tcpv4连接追踪

运行示例

【ebpf】BCC实现tcpv4连接追踪
尝试运行脚本,成功捕获到连接:

【ebpf】BCC实现tcpv4连接追踪

原文始发于微信公众号(赛博安全狗):【ebpf】BCC实现tcpv4连接追踪

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年2月12日14:03:32
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   【ebpf】BCC实现tcpv4连接追踪https://cn-sec.com/archives/2488671.html

发表评论

匿名网友 填写信息