【Linux】依赖Netfilter Hook编写自定义内核模块

1.什么是内核模块

模块是可以根据需要加载和卸载到内核中的代码片段,通过动态挂载的方式可以扩展内核的功能,而无需重启系统。 例如,一种类型的模块是设备驱动程序,它允许内核访问连接到系统的硬件。如果没有内核模块,我们必须修改内核代码,将新功能代码片段兼容添加到内核源码中,然后重新编译内核镜像。
除了会让内核越来越大之外,还有一个缺点,即每次我们想要新功能时都需要我们重建和重启内核,并且一旦代码质量过低就会导致内核崩溃的严重事故。

查看系统中的内核模块可以使用lsmod命令查看

lsmod

2.编写一个简单的内核模块

下面代码段就是最简单的内核模块,当内核模块挂载成功,通过printk()打印出Hello World!!!,当内核模块卸载成功,通过printk()打印出I am dead.,键入dmesg就可以内核日志。

编写内核模块还要关注当前的内核版本,不同版本某些函数的定义不同,会影响编译是否成功

运行环境:

  • Linux版本: CentOS 8.2
  • Kernel版本: 4.18.0-348.2.1.el8_5.x86_64
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <linux/module.h>    // included for all kernel modules
#include <linux/kernel.h> // included for KERN_INFO
#include <linux/init.h> // included for __init and __exit macros

static int __init hello_init(void)
{
printk(KERN_INFO "Hello World!!!\n");
return 0; // Non-zero return means that the module couldn't be loaded.
}

static void __exit hello_cleanup(void)
{
printk(KERN_INFO "I am dead.\n");
}

module_init(hello_init);
module_exit(hello_cleanup);
  • module_init()module_exit()分别为内核模块固定的初始化函数和退出清理函数

  • hello_inithello_cleanup分别为需要被加载的自定义初始化函数和退出函数,内部就是扩展功能代码,能够被加载到内核模块框架

  • printk()是内核的日志记录函数,用于记录信息或发出警告,每个printk()都可以带有一个日志优先级,一共有8个优先级,内核有宏,可以在linux/kernel.h中查看宏定义。 如果未指定优先级,则将使用默认优先级DEFAULT_MESSAGE_LOGLEVEL

3.如何编译内核模块

编译内核模块需要依赖Makefile文件,编译成功后会输出一个后缀名为ko的文件,该文件就是我们编写的内核模块

  • 通过insmod xxx.ko挂载

  • 通过rmmod xxx卸载

Makefile文件格式如下:

1
2
3
4
5
6
7
8
## 如果涉及多个文件,就添加多行并指定文件名称
obj-m += hello.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

保存文件后和内核模块c文件放置在同目录下,然后通过make命令就可以编译出内核模块

编译模块

挂载和卸载内核模块的效果:

模块效果

4.解决编译过程的问题

执行make编译后出现make: *** /lib/modules/4.18.0-348.2.1.el8_5.x86_64/build/: No such file or directory. Stop.

该问题是系统没有安装内核开发包,可以看下/usr/src/kernels/,如果该目录下是空的,则可以说明没有安装内核开发包

安装内核开发包的命令:

1
2
3
4
## 查找yum源支持的版本
yum list |grep kernel
## 安装开发包
yum install kernel-devel.x86_64

安装完成后,在/lib/modules/4.18.0-348.2.1.el8_5.x86_64目录下,通过ls -l查看build文件的链接,
如果没有指向build -> ../../../../usr/src/kernels/4.18.0-348.2.1.el8_5.x86_64/的链接则需要重新创建一个软链接

操作命令如下:
ln -s ../../../../usr/src/kernels/4.18.0-348.2.1.el8_5.x86_64/ build

创建软链接完成后,重新编译就可以

查看软链接

5.Netfilter Hook

Netfiler框架是Linux防火墙的内核实现,常见的iptables和firewalld本质都是调用Netfilter来实现流量包的处理,另外还提供了hook机制来实现功能扩展,在内核里,每个网络命名空间(network namespace)都给ipv4ipv6arpbridgedecnet等维持一个netfilter钩子列表(struct nf_hook_entries *hooks[5])

netfilter提供5个hook点:

  • NF_INET_PRE_ROUTING:刚刚进入网络层的数据包通过此点(刚刚进行完版本号,校验和等检测), 源地址转换在此点进行;
  • NF_INET_LOCAL_IN:经路由查找后,送往本机的通过此检查点,INPUT包过滤在此点进行;
  • NF_INET_FORWARD:转发的包通过此检测点,FORWARD包过滤在此点进行;
  • NF_INET_LOCAL_OUT:所有马上要通过网卡出去的包通过此检测点,内置的目的地址转换功能(包括地址伪装)在此点进行;
  • NF_INET_POST_ROUTING:本机进程发出的包通过此检测点,OUTPUT包过滤在此点进行。

通过这些hook点,可以让我们注册回调函数,扩展流量包处理功能,实现类似NAT、包过滤、追踪记录等,包的走向路径图可以参考下图:

netfilter

6.编写Netfiler Hook函数

目标:基于netfilter hook在preroutingpostrouting打印出当前的目的ip和目的port

思路:

  1. 编写hook函数
  2. 将hook函数注册到hook options结构体
  3. 调用nf_register_net_hooknf_unregister_net_hook完成hook函数的加载和卸载

Kernel版本为4.18的nf_hook函数定义为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typedef unsigned int nf_hookfn(void *priv,
struct sk_buff *skb,
const struct nf_hook_state *state);
struct nf_hook_ops {
/* User fills in from here down. */
nf_hookfn *hook;
struct net_device *dev;
void *priv;
u_int8_t pf;
unsigned int hooknum;
/* Hooks are ordered in ascending priority. */
int priority;
};

struct nf_hook_state {
unsigned int hook;
u_int8_t pf;
struct net_device *in;
struct net_device *out;
struct sock *sk;
struct net *net;
int (*okfn)(struct net *, struct sock *, struct sk_buff *);
};

关注nf_hook_ops结构体的参数

  • nf_hookfn: 自定义的nfhook函数
  • pf: 协议簇,主要种类列表
    • PF_INET:ipv4协议
    • PF_INET6: ipv6协议
    • PF_ARP: arp协议
    • PF_BRIDGE: 二层网桥协议
  • hooknum: 指定加载自定义函数的hook点位置
  • priority: 执行优先级
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    enum nf_ip_hook_priorities {
    NF_IP_PRI_FIRST = INT_MIN,
    NF_IP_PRI_RAW_BEFORE_DEFRAG = -450,
    NF_IP_PRI_CONNTRACK_DEFRAG = -400,
    NF_IP_PRI_RAW = -300,
    NF_IP_PRI_SELINUX_FIRST = -225,
    NF_IP_PRI_CONNTRACK = -200,
    NF_IP_PRI_MANGLE = -150,
    NF_IP_PRI_NAT_DST = -100,
    NF_IP_PRI_FILTER = 0,
    NF_IP_PRI_SECURITY = 50,
    NF_IP_PRI_NAT_SRC = 100,
    NF_IP_PRI_SELINUX_LAST = 225,
    NF_IP_PRI_CONNTRACK_HELPER = 300,
    NF_IP_PRI_CONNTRACK_CONFIRM = INT_MAX,
    NF_IP_PRI_LAST = INT_MAX,
    };

注意:上面列举的hook点和hook优先级都是指定ip层(三层),如果涉及到bridge还有二层的hook点和hook优先级

涉及bridge的hook点和优先级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/* Bridge Hooks */
/* After promisc drops, checksum checks. */
#define NF_BR_PRE_ROUTING 0
/* If the packet is destined for this box. */
#define NF_BR_LOCAL_IN 1
/* If the packet is destined for another interface. */
#define NF_BR_FORWARD 2
/* Packets coming from a local process. */
#define NF_BR_LOCAL_OUT 3
/* Packets about to hit the wire. */
#define NF_BR_POST_ROUTING 4
/* Not really a hook, but used for the ebtables broute table */
#define NF_BR_BROUTING 5
#define NF_BR_NUMHOOKS 6

// 优先级
enum nf_br_hook_priorities {
NF_BR_PRI_FIRST = INT_MIN,
NF_BR_PRI_NAT_DST_BRIDGED = -300,
NF_BR_PRI_FILTER_BRIDGED = -200,
NF_BR_PRI_BRNF = 0,
NF_BR_PRI_NAT_DST_OTHER = 100,
NF_BR_PRI_FILTER_OTHER = 200,
NF_BR_PRI_NAT_SRC = 300,
NF_BR_PRI_LAST = INT_MAX,
};
  • netfilter二层和三层之间的路径走向

netfiter路径

6.1 编写hook函数

prerouting hook 函数

1
2
3
4
5
6
7
8
9
10
// prerouting hook function
unsigned int pre_hookfn(void *priv,
struct sk_buff *skb,
const struct nf_hook_state *state)
{
struct iphdr *iph = ip_hdr(skb);
struct tcphdr *tcph = tcp_hdr(skb);
printk(KERN_INFO "prerouting dest ip:port",ip->daddr,ntohs(tcph->dest));
return NF_ACCEPT;
}

**postrouting hook 函数 **

1
2
3
4
5
6
7
8
9
10
// postrouting hook function
unsigned int post_hookfn(void *priv,
struct sk_buff *skb,
const struct nf_hook_state *state)
{
struct iphdr *iph = ip_hdr(skb);
struct tcphdr *tcph = tcp_hdr(skb);
printk(KERN_INFO "postrouting dest ip:port",ip->daddr,ntohs(tcph->dest));
return NF_ACCEPT;
}

6.2 注册option

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static struct nf_hook_ops in_nfho;  //net filter hook option struct
static struct nf_hook_ops out_nfho; //net filter hook option struct

// nfhook init function
static int nf_init_fn(void)
{
in_nfho.hook = pre_hookfn;
in_nfho.pf = PF_INET;
in_nfho.hooknum = NF_INET_PRE_ROUTING;
in_nfho.priority = NF_IP_PRI_FIRST;

out_nfho.hook = post_hookfn;
out_nfho.pf = PF_INET;
out_nfho.hooknum = NF_INET_POST_ROUTING;
out_nfho.priority = NF_IP_PRI_FIRST;

nf_register_net_hook(&init_net, &in_nfho);
nf_register_net_hook(&init_net, &out_nfho);
return 0;
}

6.3 完整代码程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/netfilter_ipv4.h>

static struct nf_hook_ops in_nfho; //net filter hook option struct
static struct nf_hook_ops out_nfho; //net filter hook option struct

// prerouting hook function
unsigned int pre_hookfn(void *priv,
struct sk_buff *skb,
const struct nf_hook_state *state)
{
struct iphdr *iph = ip_hdr(skb);
struct tcphdr *tcph = tcp_hdr(skb);
printk(KERN_INFO "prerouting dest %pI4:%d\n",&iph->daddr,ntohs(tcph->dest));
return NF_ACCEPT;
}

// postrouting hook function
unsigned int post_hookfn(void *priv,
struct sk_buff *skb,
const struct nf_hook_state *state)
{
struct iphdr *iph = ip_hdr(skb);
struct tcphdr *tcph = tcp_hdr(skb);
// iph->saddr = in_aton("1.1.1.1");
printk("postrouting dest %pI4:%d\n",&iph->daddr,ntohs(tcph->dest));
return NF_ACCEPT;
}

// nfhook init function
static int nf_init_fn(void)
{
in_nfho.hook = pre_hookfn;
in_nfho.pf = PF_INET;
in_nfho.hooknum = NF_INET_PRE_ROUTING;
in_nfho.priority = NF_IP_PRI_FIRST;

out_nfho.hook = post_hookfn;
out_nfho.pf = PF_INET;
out_nfho.hooknum = NF_INET_POST_ROUTING;
out_nfho.priority = NF_IP_PRI_FIRST;

nf_register_net_hook(&init_net, &in_nfho);
nf_register_net_hook(&init_net, &out_nfho);
return 0;
}

static int nfhook_init(void)
{
printk("[+] Register tcp kernel module!\n");
return nf_init_fn();
}

static void nfhook_exit(void)
{

nf_unregister_net_hook(&init_net, &in_nfho);
nf_unregister_net_hook(&init_net, &out_nfho);
printk(KERN_INFO "[+] Unregister tcp kernel module!\n");
}

module_init(nfhook_init);
module_exit(nfhook_exit);

挂载效果:

挂载效果


【Linux】依赖Netfilter Hook编写自定义内核模块
https://littlejoyo.github.io/2021/12/25/linux/kernel-module/
作者
Joyo
发布于
2021年12月25日
许可协议