【eBPF】BCC实现DNS请求解析

admin 2024年3月6日07:58:41评论16 views字数 2641阅读8分48秒阅读模式

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

DNS请求解析

BCC作为一个流行的ebpf开发方案,提供了很多案例供开发者学习,其中/examples/networking/dns_matching/是BCC官方提供的过滤DNS请求的案例。过滤DNS请求对于入侵检测和研究DNS劫持(后面会重点讲解)很有帮助,值得学习。PS:官方脚本中带了过滤功能,但本文重点为解析而非过滤故忽略,感兴趣的可以直达源码查看。

【eBPF】BCC实现DNS请求解析

【eBPF】BCC实现DNS请求解析

内核态源码

【eBPF】BCC实现DNS请求解析

1. 首先定义DNS请求头的结构,DNS请求头包括:

  • 事务ID (Transaction ID)
  • 标志 (Flags) - 包含查询/响应标识、操作码、响应状态码等
  • 问题计数 (Questions) - 查询的数量
  • 回答资源记录计数 (Answer RRs) - 回答资源记录的数量
  • 授权资源记录计数 (Authority RRs) - 权威资源记录的数量
  • 附加资源记录计数 (Additional RRs) - 附加资源记录的数量

那么就可以定义为:

struct dns_hdr_t{    uint16_t id;    uint16_t flags;    uint16_t qdcount;    uint16_t ancount;    uint16_t nscount;    uint16_t arcount;} BPF_PACKET_HEADER;
dns_hdr_t 结构体包含以下字段:
  • id:传输ID,用于匹配请求和响应。
  • flags:标志位,包含RD, TC, AA, OPCode, QR等标志。
  • qdcount:问题计数,指示问题部分中的问题数量(通常是1)。
  • ancount:回答计数,指示回答部分中的资源记录数。
  • nscount:授权资源记录计数,指示授权部分中的资源记录数。
  • arcount:附加资源记录计数,指示附加部分中的资源记录数。

2. 然后定义DNS问题部分,包括:

  • 查询名称 (Query Name) - 要查询的域名
  • 查询类型 (Query Type)
  • 查询类 (Query Class)

可以定义为:

struct dns_query_flags_t{  uint16_t qtype;  uint16_t qclass;} BPF_PACKET_HEADER;

struct dns_char_t{    char c;} BPF_PACKET_HEADER;

struct Key {  unsigned char p[255];};
其中:
  • dns_query_flags_t 包含 qtype 和 qclass 字段,分别表示查询类型和类。
  • Key 结构体的字段 p 用于存储DNS查询名(最多255个字符)。
  • dns_char_t 用于逐字符读取DNS查询名。
3. 然后就是从以太网帧开始,解析到IP报文的过程,不清楚的可以看看我之前讲eBPF解析HTTP请求的文章:
u8 *cursor = 0;struct Key key = {};// Check of ethernet/IP frame.struct ethernet_t *ethernet = cursor_advance(cursor, sizeof(*ethernet));if(ethernet->type == ETH_P_IP) {

  // Check for UDP.  struct ip_t *ip = cursor_advance(cursor, sizeof(*ip));  u16 hlen_bytes = ip->hlen << 2;

4. 然后判断是否为UDP包,是否请求53端口,都符合判定为DNS包,否则跳过:

if(ip->nextp == IPPROTO_UDP) {

  // Check for Port 53, DNS packet.  struct udp_t *udp = cursor_advance(cursor, sizeof(*udp));  if(udp->dport == 53){

5. 接下来重点讲一下,怎么判定是DNS请求包还是响应包,看代码:

// Do nothing if packet is not a request.if((dns_hdr->flags >>15) != 0) {  // Exit if this packet is not a request.  return -1;}
这段代码用于检查DNS数据包是否表示一个查询请求:
  • dns_hdr->flags 表示DNS头部中的标志字段。
  • dns_hdr->flags >> 15 将标志字段右移15位。因为DNS头部的标志字段共16位,右移15位后最左边的位(查询/响应标志位,也称为QR位)将被移至最低位的位置。
  • 如果该QR位为1,表示这是一个响应包(Response);如果为0,则表示它是一个查询请求(Query)。
  • (dns_hdr->flags >> 15) != 0 这个条件检查将验证移位后的最低位是否不为0,也就是检查QR位是否为1,从而确定这是否是一个响应包。
6. 最后就是读取DNS实际请求的内容:
u16 i = 0;struct dns_char_t *c;#pragma unrollfor(i = 0; i<255;i++){  c = cursor_advance(cursor, 1);  if (c->c == 0)    break;  key.p[i] = c->c;}
这段代码从 __sk_buff 结构体中读取 DNS 查询名,并将其存储在结构体 Key 的 p 字段中。具体步骤如下:
  • 初始化变量 i 为 0,用于追踪当前处理的 DNS 名字字符的索引。
  • 定义一个指向 dns_char_t 结构体的指针 c,这个结构体只包含一个单一的 char 类型成员 c。
  • #pragma unroll 告诉编译器展开(unroll)接下来的循环。这是性能优化的一部分,确保循环体中的代码在编译时会进行复制粘贴,而不是实际地循环。这可以减少运行时的循环控制开销,特别是在固定大小的迭代中非常有用。在eBPF程序中经常使用这个指令来优化性能和避开一些循环限制。
  • 循环,从 0 到 254,因为 DNS 名称最多可以有 255 个字符。
  • 使用 cursor_advance 宏将 cursor 指针向前移动 1 字节,并将该指针赋给 c。
  • 检查 c->c 是否等于 0。DNS 域名使用点(.)作为分隔符,并以空字符(null,值为0)结尾。如果 c->c 是 0,说明到达了 DNS 名称的末尾,退出循环。
  • 如果 c->c 不是 0,将字符复制到 Key 结构体的 p 数组中,由索引 i 确定位置。
【eBPF】BCC实现DNS请求解析

运行效果

【eBPF】BCC实现DNS请求解析

1. 首先在测试机器上跑起来我们的用户态py脚本:

【eBPF】BCC实现DNS请求解析

2. 然后开一个shell,ping一下域名:

【eBPF】BCC实现DNS请求解析

3. 可以看到已经捕获了我们的DNS请求:

【eBPF】BCC实现DNS请求解析




原文始发于微信公众号(赛博安全狗):【eBPF】BCC实现DNS请求解析

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年3月6日07:58:41
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   【eBPF】BCC实现DNS请求解析https://cn-sec.com/archives/2548105.html

发表评论

匿名网友 填写信息