本文仅用于技术讨论与学习,利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,文章作者及本公众号不为此承担任何责任。
DNS请求解析
BCC作为一个流行的ebpf开发方案,提供了很多案例供开发者学习,其中/examples/networking/dns_matching/是BCC官方提供的过滤DNS请求的案例。过滤DNS请求对于入侵检测和研究DNS劫持(后面会重点讲解)很有帮助,值得学习。PS:官方脚本中带了过滤功能,但本文重点为解析而非过滤故忽略,感兴趣的可以直达源码查看。
内核态源码
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;
-
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查询名。
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_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,从而确定这是否是一个响应包。
u16 i =
0
;
struct
dns_char_t
*
c
;
#
pragma
unroll
for
(
i
= 0;
i
<255;
i
++)
{
c
= cursor_advance(cursor,
1
);
if
(
c
->
c
==
0
)
break
;
key.p[i] =
c
->
c
;
}
-
初始化变量 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 确定位置。
运行效果
1. 首先在测试机器上跑起来我们的用户态py脚本:
2. 然后开一个shell,ping一下域名:
3. 可以看到已经捕获了我们的DNS请求:
原文始发于微信公众号(赛博安全狗):【eBPF】BCC实现DNS请求解析
免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论