Netfilter实战一数据包的捕获

Netfilter钩子点

  通过上文Netfilter介绍的了解到,Netfilter是通过注册钩子函数来将我们的代码加入Netfilter的处理机制中,我们首先需要了解各个钩子点的含义,PRE_ROUTING、LOCAL_IN、FORWARD、LOCAL_OUT和POST_ROUTING是Netfilter钩子点。它们是在Linux内核网络协议栈中定义的五个特定点,用于处理网络数据包的不同阶段。以下是它们的详细介绍:

  1. PRE_ROUTING:PRE_ROUTING钩子点位于数据包到达Linux内核网络协议栈的最前面,即在数据包进行任何处理之前。在PRE_ROUTING钩子点上注册的回调函数可以访问数据包的目标地址,并可以修改数据包的目标地址。这个钩子点通常用于网络地址转换(NAT)操作。
  2. LOCAL_IN:LOCAL_IN钩子点位于数据包已经到达Linux内核网络协议栈内部,但尚未被分配给任何应用程序的阶段。在LOCAL_IN钩子点上注册的回调函数可以访问数据包的源地址和目标地址,并可以对数据包进行过滤和修改。这个钩子点通常用于实现网络安全策略和过滤非法数据包。
  3. FORWARD:FORWARD钩子点用于处理转发的数据包,即那些需要从一个网络接口转发到另一个网络接口的数据包。在FORWARD钩子点上注册的回调函数可以访问数据包的源地址和目标地址,并可以对数据包进行过滤和修改。这个钩子点通常用于实现网络路由策略和流量控制。
  4. LOCAL_OUT:LOCAL_OUT钩子点用于处理数据包从Linux系统中的应用程序发送到网络的阶段。在LOCAL_OUT钩子点上注册的回调函数可以访问数据包的源地址和目标地址,并可以对数据包进行过滤和修改。这个钩子点通常用于实现网络安全策略和流量控制。
  5. POST_ROUTING:POST_ROUTING钩子点位于数据包从Linux内核网络协议栈内部发送到网络之前的阶段。在POST_ROUTING钩子点上注册的回调函数可以访问数据包的源地址,并可以修改数据包的源地址。这个钩子点通常用于网络地址转换(NAT)操作。
      从上面我们了解到了各个钩子点的含义,比如我们只想接收发到本机的数据包,可以注册LOCAL_IN,如果接收到达本机所有的数据包可以通过注册PRE_ROUTING点。

代码实现

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
#include <linux/ip.h>

static struct nf_hook_ops nfho;  // net filter hook option struct

/* function to be called by hook */
unsigned int hook_func(void *priv, struct sk_buff *skb, const struct nf_hook_state *state)
{
    
    
    struct iphdr *iph;  // ip header struct

    iph = ip_hdr(skb);  // get ip header from skb
    if (skb->protocol == htons(ETH_P_IP)) {
    
     // IPv4 packet
    /* print ip address and protocol of captured packet */
    printk(KERN_INFO "Captured packet: src_addr=%pI4, dst_addr=%pI4, protocol=%d\n",
           &iph->saddr, &iph->daddr, iph->protocol);
    }
    return NF_ACCEPT;  // accept the packet
}

/* module init function */
static int __init init_nf_module(void)
{
    
    
    nfho.hook = hook_func;          // hook function
    nfho.hooknum = NF_INET_PRE_ROUTING;  // hook point
    nfho.pf = PF_INET;             // protocol family
    nfho.priority = NF_IP_PRI_FIRST;   // hook priority

    nf_register_hook(&nfho);       // register hook

    printk(KERN_INFO "nf_hook registered\n");

    return 0;
}

/* module exit function */
static void __exit exit_nf_module(void)
{
    
    
    nf_unregister_hook(&nfho);     // unregister hook

    printk(KERN_INFO "nf_hook unregistered\n");
}

module_init(init_nf_module);
module_exit(exit_nf_module);

MODULE_LICENSE("GPL");


  以上代码定义了一个hook_func函数,该函数将被注册到NF_INET_PRE_ROUTING钩子点,当有数据包通过该钩子点时,hook_func函数将被调用,然后抓取数据包的源地址、目的地址和协议,并将这些信息打印出来。同时,在init_nf_module函数中,我们定义了nfho结构体,该结构体包含了钩子函数、钩子点、协议族和钩子优先级等信息,并将其注册到内核的netfilter框架中。最后,在exit_nf_module函数中,我们将钩子函数从netfilter框架中注销

编译

在编译过程中,您需要确保系统已经安装了内核头文件。内核头文件包含了编写内核模块所需的所有头文件和宏定义等内容。

如果您的系统上没有安装内核头文件,您可以使用以下命令安装:

sudo apt-get install linux-headers-$(uname -r)

此命令将安装与当前正在运行的内核版本相对应的内核头文件。

另外,如果您使用的是不同版本的内核,需要使用相应的内核头文件。您可以使用以下命令列出可用的内核头文件:

sudo apt-cache search linux-headers

可以根据需要安装相应的内核头文件。

Makefile

obj-m += nf_hook.o

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

以上makefile将nf_hook.c编译为一个内核模块,并使用obj-m来定义内核模块的名称。使用make命令可以编译内核模块。

运行

在终端中,使用以下命令将内核模块加载到系统中:

sudo insmod nf_hook.ko

查看系统日志以确保模块已成功加载:

dmesg | tail

在终端中使用ping命令或其他网络工具向目标主机发送数据包,例如:

ping 8.8.8.8

应该能够在系统日志中看到类似以下输出的信息:

[  184.907485] Captured packet: src_addr=192.168.0.1, dst_addr=8.8.8.8, protocol=1

这表明程序已成功通过netfilter钩子函数抓取了发送到8.8.8.8的ICMP数据包。

在终端中使用以下命令卸载内核模块:

sudo rmmod nf_hook

struct nf_hook_ops

struct nf_hook_ops 结构体中的 nfho 字段是一个 struct nf_hook_ops 类型的指针,它是用来注册 netfilter 钩子函数的。

在使用 netfilter 钩子函数时,我们需要创建一个 struct nf_hook_ops 结构体并将其注册到内核中,以便在网络流量通过内核时触发我们的钩子函数。该结构体中的 nfho 字段是我们需要填充的一个结构体,用于指定我们的钩子函数需要处理的网络流量类型、执行的优先级等信息。

以下是一个简单的 struct nf_hook_ops 结构体的定义:

static struct nf_hook_ops nfho = {
    
    
    .hook = hook_func, // 指定钩子函数
    .hooknum = NF_INET_PRE_ROUTING, // 钩子函数所处的网络流量阶段
    .pf = PF_INET, // IP 协议族
    .priority = NF_IP_PRI_FIRST // 优先级
};

在上述结构体中,我们填充了以下信息:

  • hook 字段:该字段指定了我们需要执行的钩子函数。
  • hooknum 字段:该字段指定了钩子函数所处的网络流量阶段。在这个例子中,我们使用了
    NF_INET_PRE_ROUTING,该字段表示我们的钩子函数将在 IP 数据包到达主机之前进行处理。
  • pf 字段:该字段指定了 IP 协议族。在这个例子中,我们使用了 PF_INET,表示我们的钩子函数将处理 IPv4 数据包。
  • priority字段:该字段指定了钩子函数的优先级,以确保在多个钩子函数共存时,执行顺序能够正确确定。在这个例子中,我们使用了NF_IP_PRI_FIRST,表示我们的钩子函数优先级最高。

在定义好 struct nf_hook_ops 结构体之后,我们可以将其注册到内核中,以便触发钩子函数。注册的方法如下:

nf_register_hook(&nfho);

其中,nf_register_hook() 函数用于注册我们的钩子函数,该函数将在我们的钩子函数触发时被调用。

如果我们需要在程序结束时将钩子函数从内核中注销,我们可以使用以下代码:

nf_unregister_hook(&nfho);

skb结构体

在 Linux 内核中,skb(socket buffer)是一个数据结构,用于在网络协议栈中传递数据。它包含了一个数据包的所有信息,如数据、协议头、网络接口信息等等。

在 Linux 内核中,每当一个数据包通过网络接口被接收或者发送时,就会被包装成一个 skb。skb 在整个网络协议栈中传递,直到被交付给应用程序或者发送到网络上。

skb 可以被认为是一个链表,每个 skb 都有一个指向下一个 skb 的指针。当一个 skb 被处理完成后,它会被释放并从链表中移除。
在 Linux 内核中,skb 的结构体定义如下:

struct sk_buff {
    
    
    struct sk_buff_head    __rcu *list;
    kmemcheck_bitfield_begin
    union {
    
    
        unsigned char           nfct_info[4];
        struct {
    
    
            unsigned int        nfct:16;
            unsigned int        nfctinfo:8;
            unsigned int        nfnl:8;
        };
    };
    kmemcheck_bitfield_end
    unsigned char           cb[48];
    unsigned int            len,
                            data_len;
    __u16                   mac_len,
                            hdr_len;
    union {
    
    
        __be16              protocol;
        unsigned long       _skb_refdst;
    };
    unsigned int            vlan_tci;
    union {
    
    
        __u16               vlan_proto;
        void                *sk;
    };
    __u16                   queue_mapping;
    unsigned char           protocol;
    unsigned char           priority;
    unsigned char           local_df:1,
                            cloned:1,
                            ip_summed:2,
                            nohdr:1,
                            nfct_reasm:1,
                            nfct_policy:2,
                            csum_valid:1;
    unsigned char           pkt_type:3,
                            fclone:2,
                            ipvs_property:1,
                            peeked:1,
                            nf_trace:1;
    union {
    
    
        __u16               encapsulation;
        __u16               encap_hdr_len;
    };
    /* sk_buff data buffer */
    struct skb_shared_info *     shinfo;
    atomic_t            users;
};

其中,skb 的成员包括:

list:一个指向 skb 列表头的指针,用于将 skb 连接成链表。

nfct_info:一个用于记录 skb 状态的字段,用于在网络连接追踪(conntrack)中标记 skb 状态。

cb:skb 内核私有数据区域,用于存储协议层的私有信息。

len:skb 的总长度,包括所有的协议头和数据。

data_len:skb 中数据的长度。

mac_len:skb 中 MAC 地址的长度。

hdr_len:skb 中协议头的长度。

protocol:skb 中的网络协议类型。

vlan_tci:skb 中 VLAN 标签的 TCI 值。

vlan_proto:skb 中 VLAN 标签的协议类型。

queue_mapping:skb 所属的网络队列编号。

protocol:skb 中的协议类型。

priority:skb 的优先级。

local_df:skb 是否启用本地 DF 标志。

cloned:skb 是否是克隆的。

ip_summed:skb 是否需要校验和。

nohdr:skb 中是否包含协议头。

nfct_reasm:skb 是否正在进行连接追踪的数据包重组。

nfct_policy:skb 在连接追踪中的处理策略。

csum_valid:skb 的校验和是否有效。

pkt_type:skb 的包类型。

fclone:skb 的克隆标志。

ipvs_property:skb 是否为 IPVS 负载均衡属性包。

peeked:skb 是否被读取过。

nf_trace:skb 是否需要在网络连接追踪中进行跟踪。

encapsulation:skb 中的封装

encap_hdr_len:skb 中封装协议头的长度。

shinfo:skb 共享信息的指针,包括共享数据缓冲区和共享数据信息。

users:skb 的使用计数器。

  在使用 netfilter 进行网络数据包处理时,我们通常需要使用 skb 结构体来访问网络数据包的内容。通过访问 skb 结构体的字段,我们可以获取到数据包的各个部分的内容,从而进行网络数据包的过滤、修改、重定向等操作。
当我们使用 netfilter 进行网络数据包处理时,经常会用到一些与 skb 结构体相关的函数。以下是一些常用的 skb 函数:

skb_copy():用于复制一个 skb 结构体,可以用于重定向数据包等场景。

skb_push() 和 skb_pull():用于在 skb 数据区域的开头和末尾添加或删除数据。

skb_trim():用于调整 skb 的大小。

skb_clone():用于复制一个 skb 结构体的副本。

skb_put():用于向 skb 数据区域中添加数据。

skb_copy_bits():用于从一个 skb 数据区域中复制一段数据。

skb_mac_header()、skb_network_header() 和 skb_transport_header():
用于获取 skb 数据区域中 MAC、网络和传输层头部的指针。

skb_reset_network_header() 和 skb_reset_transport_header():
用于重新设置 skb 数据区域中的网络和传输层头部指针。

  这些函数可以帮助我们在处理网络数据包时,方便地访问和修改 skb 结构体中的各个字段,从而实现各种网络数据包的处理功能。
  后续讲解我们如何通过自己手动构造skb来实现通信

猜你喜欢

转载自blog.csdn.net/haoyuxuanyuan/article/details/129382228