基于linux的嵌入IPv4协议栈的内容过滤防火墙系统(5)-包过滤模块和内容过滤模块所采用的各种技术详述

三。包过滤模块和内容过滤模块所采用的各种技术详述

3。1 module编程 module可以说是 Linux 的一大革新。有了 module 之后,写 device driver 不再是一项恶梦,修改 kernel 也不再是一件痛苦的事了。因为你不需要每次要测试 driver 就重新 compile kernel 一次。那简直是会累死人。Module 可以允许我们动态的改变 kernel,加载 device driver,而且它也能缩短我们driver development 的时间。在这篇文章里,我将要跟各位介绍一下 module 的原理,以及如何写一个 module。

module 翻译成中文就是模块,不过,事实上去翻译这个字一点都没意义。在讲模块之前,我先举一个例子。相信很多人都用过 RedHat。在 RedHat 里,我们可以执行 sndconfig,它可以帮我们 config 声卡。config 完之后如果捉得到你的声卡,那你的声卡马上就可以动了,而且还不用重新激活计算机。这是怎么做的呢 ? 就是靠module。module 其实是一般的程序。但是它可以被动态载到 kernel 里成为 kernel的一部分。载到 kernel 里的 module 它具有跟kernel 一样的权力。可以 access 任何 kernel 的 data structure。你听过 kdebug 吗 ? 它是用来 debug kernel 的。它就是先将它本身的一个 module 载到 kernel 里,而在 user space 的 gdb 就可以经由跟这个 module 沟通,得知 kernel 里的 data structure 的值,除此之外,还可以经由载到 kernel 的module 去更改 kernel 里 data structure。

我们知道,在写 C 程序的时候,一个程序只能有一个 main。Kernel 本身其实也是一个程序,它本身也有个 main,叫 start_kernel()。当我们把一个 module 载到 kernel 里的时候,它会跟 kernel 整合在一起,成为 kernel 的一部分。请各位想想,那 module 可以有 main 吗 ? 答案很明显的,是 No。理由很简单。一个程序只能有一个 main。在使用 module 时,有一点要记住的是 module 是处于被动的角色。它是提供某些功能让别人去使用的。

Kernel 里有一个变量叫 module_list,每当 user 将一个 module 载到 kernel 里的时候,这个 module 就会被记录在 module_list 里面。当 kernel 要使用到这个 module 提供的 function 时,它就会去 search 这个 list,找到 module,然后再使用其提供的 function 或 variable。每一个 module 都可以 export 一些 function 或变量来让别人使用。除此之外,module 也可以使用已经载到 kernel 里的 module 提供的 function。这种情形叫做 module stack。比方说,module A 用到 module B 的东西,那在加载 module A 之前必须要先加载 module B。否则 module A 会无法加载。除了 module 会 export 东西之外,kernel 本身也会 export 一些 function 或 variable。同样的,module 也可以使用 kernel 所 export 出来的东西。由于大家平时都是撰写 user space 的程序,所以,当突然去写 module 的时候,会把平时写程序用的function 拿到 module 里使用。像是 printf 之类的东西。我要告诉各位的是,module 所使用的 function 或 variable,要嘛就是自己写在 module 里,要嘛就是别的 module 提供的,再不就是 kernel 所提供的。你不能使用一般 libc 或 glibc所提供的 function。像 printf 之类的东西。这一点可能是各位要多小心的地方。(也许你可以先 link 好,再载到 kernel,我好象试过,但是忘了)刚才我们说到 kernel 本身会 export 出一些 function 或 variable 来让 module 使用,但是,我们不是万能的,我们怎么知道 kernel 有开放那里东西让我们使用呢 ? Linux 提供一个 command,叫 ksyms,你只要执行 ksyms -a 就可以知道 kernel 或目前载到 kernel 里的 module 提供了那些 function 或 variable。底下是我的系统的情形:

c0216ba0 drive_info_R744aa133 c01e4a44 boot_cpu_data_R660bd466 c01e4ac0 EISA_bus_R7413793a c01e4ac4 MCA_bus_Rf48a2c4c c010cc34 __verify_write_R203afbeb . . . . .

在 kernel 里,有一个 symbol table 是用来记录 export 出去的function 或 variable。除此之外,也会记录着那个 module export 那些 function。上面几行中,表示 kernel 提供了 drive_info 这个 function/variable。所以,我们可以在 kernel 里直接使用它,等载到 kernel 里时,会自动做好 link 的动作。由此,我们可以知道,module 本身其实是还没做 link 的一些 object code。一切都要等到 module 被加载 kernel 之后,link 才会完成。各位应该可以看到 drive_info 后面还接着一些奇怪的字符串。_R744aa133,这个字符串是根据目前 kernel 的版本再做些 encode 得出来的结果。为什幺额外需要这一个字符串呢 ?

Linux 不知道从那个版本以来,就多了一个 config 的选项,叫做 Set version number in symbols of module。这是为了避免对系统造成不稳定。我们知道 Linux 的 kernel 更新的很快。在 kernel 更新的过程,有时为了效率起见,会对某些旧有的 data structure 或 function 做些改变,而且一变可能有的 variable 被拿掉,有的 function 的 prototype 跟原来的都不太一样。如果这种情形发生的时候,那可能以前 2.0.33 版本的 module 拿到 2.2.1 版本的 kernel 使用,假设原来 module 使用了 2.0.33 kernel 提供的变量叫 A,但是到了 2.2.1 由于某些原因必须把 A 都设成 NULL。那当此 module 用在 2.2.1 kernel 上时,如果它没去检查 A 的值就直接使用的话,就会造成系统的错误。也许不会整个系统都死掉,但是这个 module 肯定是很难发挥它的功能。为了这个原因,Linux 就在 compile module 时,把 kernel 版本的号码 encode 到各个 exported function 和 variable 里。

所以,刚才也许我们不应该讲 kernel 提供了 drive_info,而应该说 kernel 提供了driver_info_R744aa133 来让我们使用。这样也许各位会比较明白。也就是说,kernel 认为它提供的 driver_info_R744aa133 这个东西,而不是 driver_info。所以,我们可以发现有的人在加载 module 时,系统都一直告诉你某个 function 无法 resolved。这就是因为 kernel 里没有你要的function,要不然就是你的 module 里使用的 function 跟 kernel encode 的结果不一样。所以无法 resolve。解决方式,要嘛就是将 kernel 里的 set version 选项关掉,要嘛就是将 module compile 成 kernel 有办法接受的型式。

那有人就会想说,如果 kernel 认定它提供的 function 名字叫做 driver_info_R744aa133 的话,那我们写程序时,是不是用到这个 funnction 的地方都改成 driver_info_R744aa133 就可以了。答案是 Yes。但是,如果每个 function 都要你这样写,你不会觉得很烦吗 ? 比方说,我们在写 driver 时,很多人都会用到 printk 这个 function。这是 kernel 所提供的 function。它的功能跟 printf 很像。用法也几乎都一样。是 debug 时很好用的东西。如果我们 module 里用了一百次 printk,那是不是我们也要打一百次的 printk_Rdd132261 呢 ? 当然不是,聪明的人马上会想到用 #define printk printk_Rdd132261 就好了嘛。所以啰,Linux 很体贴的帮我们做了这件事。

如果各位的系统有将 set version 的选项打开的话,那大家可以到 /usr/src/linux/include/linux/modules 这个目录底下。这个目录底下有所多的 ..ver档案。这些档案其实就是用来做 #define 用的。我们来看看 ksyms.ver 这个档案里,里面有一行是这样子的 :

#define printk _set_ver(printk)

set_ver 是一个 macro,就是用来在 printk 后面加上 version number 的。有兴趣的朋友可以自行去观看这个 macro 的写法。用了这些 ver 檔,我们就可以在 module 里直接使用 printk 这样的名字了。而这些 ver 档会自动帮我们做好 #define 的动作。可是,我们可以发现这个目录有很多很多的 ver 檔。有时候,我们怎么知道我们要呼叫的 function 是在那个 ver 档里有定义呢 ? Linux 又帮我们做了一件事。/usr/src/linux/include/linux/modversions.h 这个档案已经将全部的 ver 档都加进来了。所以在我们的 module 里只要 include 这个档,那名字的问题都解决了。但是,在此,我们奉劝各位一件事,不要将 modversions.h 这个档在 module 里 include 进来,如果真的要,那也要加上以下数行:

#ifdef MODVERSIONS #include linux/modversions.h #endif

加入这三行的原因是,避免这个 module 在没有设定 kernel version 的系统上,将 modversions.h 这个档案 include 进来。各位可以去试试看,当你把 set version 的选项关掉时,modversions.h 和 modules 这个目录都会不见。如果没有上面三行,那 compile 就不会过关。所以一般来讲,modversions.h 我们会选择在 compile 时传给 gcc 使用。就像下面这个样子。

gcc -c -D__KERNEL__ -DMODULE -DMODVERSIONS main.c -include usr/src/linux/include/linux/modversions.h

在这个 command line 里,我们看到了 -D__KERNEL__,这是说要定义 __KERNEL__ 这个 constant。很多跟 kernel 有关的 header file,都必须要定义这个 constant 才能 include 的。所以建议你最好将它定义起来。另外还有一个 -DMODVERSIONS。这个 constant 我刚才忘了讲。刚才我们说要解决 fucntion 或 variable 名字 encode 的方式就是要 include modversions.h,其实除此之外,你还必须定义 MODVERSIONS 这个 constant。再来就是 MODULE 这个 constant。其实,只要是你要写 module 就一定要定义这个变量。而且你还要 include module.h 这个档案,因为 _set_ver 就是定义在这里的。

讲到这里,相信各位应该对 module 有一些认识了,以后遇到 module unresolved 应该不会感到困惑了,应该也有办法解决了。

刚才讲的都是使用别人的 function 上遇到的名字 encode 问题。但是,如果我们自己的 module 想要 export 一些东西让别的 module 使用呢。很简单。在 default 上,在你的 module 里所有的 global variable 和 function 都会被认定为你要 export 出去的。所以,如果你的 module 里有 10 个 global variable,经由 ksyms,你可以发现这十个 variable 都会被 export 出去。这当然是个很方便的事啦,但是,你知道,有时候我们根本不想把所有的 variable 都 export 出去,万一有个 module 没事乱改我们的 variable 怎幺办呢 ? 所以,在很多时候,我们都只会限定几个必要的东西 export 出去。在 2.2.1 之前的 kernel 可以利用 register_symtab 来帮我们。但是,现在更新的版本早就出来了。所以,在此,我会介绍 kernel 2.2.1 里所提供的。kernel 2.2.1 里提供了一个 macro,叫做 EXPORT_SYMBOL,这是用来帮我们选择要 export 的 variable 或 function。比方说,我要 export 一个叫 full 的 variable,那我只要在 module 里写:

EXPORT_SYMBOL(full);

就会自动将 full export 出去,你马上就可以从 ksyms 里发现有 full 这个变量被 export 出去。在使用 EXPORT_SYMBOL 之前,要小心一件事,就是必须在 gcc 里定义 EXPORT_SYMTAB 这个 constant,否则在 compile 时会发生 parser error。所以,要使用 EXPORT_SYMBOL 的话,那 gcc 应该要下:

gcc -c -D__KERNEL__ -DMODULE -DMODVERSIONS -DEXPORT_SYMTAB main.c -include /usr/src/linux/include/linux/modversions.h

如果我们不想 export 任何的东西,那我们只要在 module 里下

EXPORT_NO_SYMBOLS;

就可以了。使用 EXPORT_NO_SYMBOLS 用不着定义任何的 constant。其实,如果各位使用过旧版的 register_symbol 的话,一定会觉得新版的方式比较好用。至少我是这样觉得啦。因为使用 register_symbol 还要先定义出自己的 symbol_table,感觉有点麻烦。

当我们使用 EXPORT_SYMBOL 把一些 function 或 variable export 出来之后,我们使用 ksyma -a 去看一些结果。我们发现 EXPORT_SYMBOL(full) 的确是把 full export出来了 :

c8822200 full [my_module] c01b8e08 pci_find_slot_R454463b5 . . .

但是,结果怎么跟我们想象中的不太一样,照理说,应该是 full_Rxxxxxx 之类的东西才对啊,怎么才出现 full 而已呢 ? 奇怪,问题在那里呢 ?

其实,问题就在于我们没有对本身的 module 所 export 出来的 function 或 variable 的名字做 encode。想想,如果在 module 的开头。我们加入一行

#define full full_Rxxxxxx

之后,我们再重新 compile module 一次,载到 kernel 之后,就可以发现 ksyms -a 显示的是

c8822200 full_Rxxxxxx [my_module] c01b8e08 pci_find_slot_R454463b5 . . . . .

了。那是不是说,我们要去对每一个 export 出来的 variable 和 function 做 define 的动作呢 ? 当然不是。记得吗,前头我们讲去使用 kernel export 的 function 时,由于 include 了一些 .ver 的档案,以致于我们不用再做 define 的动作。现在,我们也要利用 .ver 的档案来帮我们,使我们 module export 出来的 function 也可以自动加入 kernel version 的 information。也就是变成 full_Rxxxxxx 之类的东西。

Linux 里提供了一个 command,叫 genksyms,就是用来帮我们产生这种 .ver 的档案的。它会从 stdin 里读取 source code,然后检查 source code 里是否有 export 的 variable 或 function。如果有,它就会自动为每个 export 出来的东西产生一些 define。这些 define 就是我们之前说的。等我们有了这些 define 之后,只要在我们的 module 里加入这些 define,那 export 出来的 function 或 variable 就会变成上面那个样子。

假设我们的程序都放在一个叫 main.c 的档案里,我们可以使用下列的方式产生这些 define。

gcc -E -D__GENKSYMS__ main.c | genksyms -k 2.2.1 > main.ver

gcc 的 -E 参数是指将 preprocessing 的结果 show 出来。也就是说将它 include 的档案,一些 define 的结果都展开。-D__GENKSYMS__ 是一定要的。如果没有定义这个 constant,你将不会看到任何的结果。用一个管线是因为 genksyms 是从 stdin 读资料的,所以,经由管线将 gcc 的结果传给 genksyms。-k 2.2.1 是指目前使用的 kernel 版本是 2.2.1,如果你的 kernel 版本不一样,必须指定你的 kernel 的版本。产生的 define 将会被放到 main.ver 里。产生完 main.ver 档之后,在 main.c 里将它 include 进来,那一切就 OK 了。有件事要告诉各位的是,使用这个方式产生的 module,其 export 出来的东西会经由 main.ver 的 define 改头换面。所以如果你要让别人使用,那你必须将 main.ver 公开,不然,别人就没办法使用你 export 出来的东西了。

讲了这么多,相信各位应该都已经比较清楚 module 在 kernel 中是怎幺样一回事,也应该知道为什幺有时候 module 会无法加载了。除此之外,各位应该还知道如何使自己 module export 出来的东西也具有 kernel version 的 information。

接下来,要跟各位讲的就是,如何写一个 module 了。其实,写一个 module 很简单的。如果你了解我上面所说的东西。那我再讲一次,再用个例子,相信大家就都会了。要写一个 module,必须要提供两个 function。这两个 function 是给 insmod 和 rmmod 使用的。它们分别是 init_module(),以及cleanup_module()。

int init_module(); void cleanup_module();

下图是在系统中插入datafilter3模块和interdata模块之后,系统中的模块,用lsmod可以看到这两个模块都在运行当中(其中,datafilter3模块是对数据包进行过滤的,interdata是设备驱动程序,负责进行数据交互工作)。

下图是卸载到datafilter3模块和interdata模块之后,系统中的模块,可以看到,系统中已经没有这两个模块了。

相信大家都知道在 Linux 里可以使用 insmod 这个 command 来将某个 module 加载。比方说,我有一个 module 叫 hello.o,那使用 insmod hello.o 就可以将 hello 这个 module 载到 kernel 里。观察 /etc/modules 应该就可以看到 hello 这个 module 的名字。如果要将 hello 这个 module 移除,则只要使用 rmmod hello 就可以了。insmod 在加载 module 之后,就会去呼叫 module 所提供的 init_module()。如果传回 0 表示成功,那 module 就会被加载。如果失败,那加载的动作就会失败。一般来讲,我们在 init_module() 做的事都是一些初始化的工作。比方说,你的 module 需要一块内存,那你就可以在 init_module() 做 kmalloc 的动作。想当然尔。cleanup_module() 就是在 module 要移除的时候做的事。做的事一般来讲就是一些善后的工作,比方像把之前 kmalloc 的内存 free 掉。

由于 module 是载到 kernel 使用的,所以,可能别的 module 会使用你的 module,甚至某些 process 也会使用到你的 module,为了避免 module 还有人使用时就被移除,每个 module 都有一个 use count。用来记录目前有多少个 process 或 module 正在使用这个 module。当 module 的 use count 不等于 0 时,module 是不会被移除掉的。也就是说,当 module 的 use count 不等于 0 时,cleanup_module() 是不会被呼叫的。

在此,我要介绍三个 macro,是跟 module 的 use count 有关的。

MOD_INC_USE_COUNT MOD_DEC_USE_COUNT MOD_IN_USE

MOD_INC_USE_COUNT 是用来增加 module 的 use count,而 MOD_DEC_USE_COUNT 是用来减少 module 的 use count。至于 MOD_IN_USE 则是用来检查目前这个 module 是不是被使用中。也就是检查 use count 是否为 0。module 的 use count 必须由写 module 的人自己来 maintain。系统并不会自动为你把 use count 加一或减一。一切都得由自己控制。下面有一个例子,但是,并不会介绍这三个 macro 的使用方法。将来如果有机会,我再来介绍这三个 macro 的用法。

这个例子很简单。其实只是示范如何使用 init_module() 以及 cleanup_module() 来写一个 module。当然,这两个 function 只是构成 module 的基本条件罢了。至于 module 里要提供的功能则是看各人的需要。

下面是一个简化的模块程序:

#include <linux/module.h> /* Needed by all modules */

#include <linux/kernel.h> /* Needed for KERN_ALERT */

int init_module(void)

{

console_print("datafilter is registered into the linux kernel/n");

return nf_register_hook(&iplimitfilter);

}

void cleanup_module(void)

{

console_print("datafilter is cleanuped from the linux kernel/n");

nf_unregister_hook(&iplimitfilter);

}

关于 printk 是这样子的,它是 kernel 所提供的一个打印讯息的 function。kernel 有 export 这个 function。所以你可以自由的使用它。它的用法跟 printf 几乎一模一样。nf_register_hook(&iplimitfilter)是对数据包进行过滤的函数,当加载模块时,则对数据包进行过滤,当卸载模块时,则取消对数据包的过滤。

main.ver:

利用 genksyms 产生出来的。

gcc -E -D__GENKSYMS__ main.c | genksyms -k 2.2.1 > main.ver

接下来,就是要把 main.c compile 成 main.o

gcc -D__KERNEL__ -DMODVERSIONS -DEXPORT_SYMTAB -c -I/usr/src/linux/include/linux -include /usr/src/linux/include/linux/modversions.h -include ./main.ver main.c

好了。main.o 已经成功的 compile 出来了,现在下一个 command,

insmod main.o

检查看 /proc/modules 里是否有 main 这个 module。如果有,表示 main 这个 module 已经载到 kernel 了。再下一个指令,看看 full export 出去的结果。

ksyms

结果显示

Address Symbol Defined by c40220e0 full_R355b84b2 [main] c401d04c ne_probe [ne] c401a04c ei_open [8390] c401a094 ei_close [8390] c401a504 ei_interrupt [8390] c401af1c ethdev_init [8390] c401af80 NS8390_init [8390]

可以看到 full_R355b84b2,表示,我们已经成功的将 full 的名字加上 kernel version 的 information 了。当我们不需要这个 module 时,我们就可以下一个 command,

rmmod main

这样 main 就会被移除掉了。再检查看看 /proc/modules 就可以发现 main 那一行不见了。各位现在可以看一下 /var/log/message 这个档案,应该可以发现以两行

Apr 12 14:19:05 host kernel: Module is loaded Apr 12 14:39:29 host kernel: Module is unloaded

这两行就是 printk 印出来的。

关于 module 的介绍已经到此告一段落了。其实,使用 module 实在是很简单的一件事。对于要发展 driver 或是增加 kernel 某些新功能的人来讲,用 module 不啻为一个方便的方式。

3。2 netfilter

在linux2.2内核中的防火墙ipchains已经被用户广泛认可,它提供了完整的防火墙功能(包过滤,地址伪装,透明代理),又避免了商业防火墙那高的惊人的价格。如果你用的是某款国产防火墙,那么十有八九你实际在受到ipchains的保护。在未来的2.4内核中,被称为netfilter的防火墙以更好的结构重新构造,并实现了许多新功能,如完整的动态NAT(2.2内核实际是多对一的"地址伪装"),基于MAC及用户的过滤,真正的基于状态的过滤(不再是简单的查看tcp的标志位等),包速率限制等。

在原有的网络部分的LKM中,如果对网络部分进行处理,一般是先生成struct packet_type结构,在用dev_add_pack将其插入网络层(注意此时的packet_type实际相当于一个的三层的协议,如ip_packet_type,ipx_8023_packet_type等)。

而netfilter本身在IP层内提供了另外的5个插入点(其文档中称为HOOK):NF_IP_PRE_ROUTING,NF_IP_LOCAL_IN,NF_IP_FORWARD,NF_IP_LOCAL_OUT,NF_IP_POST_ROUTING,分别对应IP层的五个不同位置,这样理论上在写lkm时便可以

选择更适合的切入点,再辅以netfilter内置的新功能(如connect tracking),应该会帮助写出功能更强的lkm。

通俗的说,netfilter的架构就是在整个网络流程的若干位置放置了一些检测点(HOOK),而在每个检测点上上登记了一些处理函数进行处理(如包过滤,NAT等,甚至可以是用户自定义的功能)。

IP层的五个HOOK点的位置如下图所示(copy from <packet filter howto>) :

[1]:NF_IP_PRE_ROUTING:刚刚进入网络层的数据包通过此点(刚刚进行完版本号,校验和等检测),源地址转换在此点进行;

[2]:NF_IP_LOCAL_IN:经路由查找后,送往本机的通过此检查点,INPUT包过滤在此点进行;

[3]:NF_IP_FORWARD:要转发的包通过此检测点,FORWORD包过滤在此点进行;

[4]:NF_IP_LOCAL_OUT:本机进程发出的包通过此检测点,OUTPUT包过滤在此点进行;

[5]:NF_IP_POST_ROUTING:所有马上便要通过网络设备出去的包通过此检测点,内置的目的地址转换功能(包括地址伪装)在此点进行。

下面是我的源程序里面的一个结构:

static struct nf_hook_ops iplimitfilter

={ {NULL,NULL} ,datafilter,PF_INET,NF_IP_LOCAL_IN,NF_IP_PRI_FILTER-1};

此函数的第四个参数NF_IP_LOCAL_IN表示在第二个钩进行过滤,也就是对送往本机的数据包进行过滤,第二个参数表示执行datafilter这个函数。

在IP层代码中,有一些带有NF_HOOK宏的语句,如IP的转发函数中有:

<-ipforward.c ip_forward()->

NF_HOOK(PF_INET, NF_IP_FORWARD, skb, skb->dev, dev2,

ip_forward_finish);

其中NF_HOOK宏的定义提炼如下:

<-/include/linux/netfilter.h->

#ifdef CONFIG_NETFILTER

#define NF_HOOK(pf, hook, skb, indev, outdev, okfn) /

(list_empty(&nf_hooks[(pf)][(hook)]) /

? (okfn)(skb) /

: nf_hook_slow((pf), (hook), (skb), (indev), (outdev), (okfn)))

#else /* !CONFIG_NETFILTER */

#define NF_HOOK(pf, hook, skb, indev, outdev, okfn) (okfn)(skb)

#endif /*CONFIG_NETFILTER*/

如果在编译内核时没有配置netfilter时,就相当于调用最后一个参数,此例中即执行ip_forward_finish函数;否则进入HOOK点,执行通过nf_register_hook()登记的功能(这句话表达的可能比较含糊,实际是进入nf_hook_slow()函数,再由它执行登记的函数)。

NF_HOOK宏的参数分别为:

1.pf:协议族名,netfilter架构同样可以用于IP层之外,因此这个变量还可以有诸如PF_INET6,PF_DECnet等名字。

2.hook:HOOK点的名字,对于IP层,就是取上面的五个值;

3.skb:不用多解释了吧;

4.indev:进来的设备,以struct net_device结构表示;

5.outdev:出去的设备,以struct net_device结构表示;

(后面可以看到,以上五个参数将传到用nf_register_hook登记的处理函数中。)

6.okfn:是个函数指针,当所有的该HOOK点的所有登记函数调用完后,转而走此流程。

这些点是已经在内核中定义好的,除非你是这部分内核代码的维护者,否则无权增加或修改,而在此检测点进行的处理,则可由用户指定。像packet filter,NAT,connection track这些功能,也是以这种方式提供的。正如netfilter的当初的设计目标--提供一个完善灵活的框架,为扩展功能提供方便。

如果我们想加入自己的代码,便要用nf_register_hook函数,其函数原型为:

int nf_register_hook(struct nf_hook_ops *reg)

我们考察一下struct nf_hook_ops结构:

struct nf_hook_ops

{

struct list_head list;

/* User fills in from here down. */

nf_hookfn *hook;

int pf;

int hooknum;

/* Hooks are ordered in ascending priority. */

int priority;

};

我们的工作便是生成一个struct nf_hook_ops结构的实例,并用nf_register_hook将其HOOK上。其中list项我们总要初始化为{NULL,NULL};由于一般在IP层工作,pf总是PF_INET;hooknum就是我们选择的HOOK点;一个HOOK点可能挂多个处理函数,谁先谁后,便要看优先级,即priority的指定了。netfilter_ipv4.h中用一个枚举类型指定了内置的处理函数的优先级:

enum nf_ip_hook_priorities {

NF_IP_PRI_FIRST = INT_MIN,

NF_IP_PRI_CONNTRACK = -200,

NF_IP_PRI_MANGLE = -150,

NF_IP_PRI_NAT_DST = -100,

NF_IP_PRI_FILTER = 0,

NF_IP_PRI_NAT_SRC = 100,

NF_IP_PRI_LAST = INT_MAX,

};

hook是提供的处理函数,也就是我们的主要工作,其原型为:

unsigned int nf_hookfn(unsigned int hooknum,

struct sk_buff **skb,

const struct net_device *in,

const struct net_device *out,

int (*okfn)(struct sk_buff *));

它的五个参数将由NFHOOK宏传进去。

了解了这些,基本上便可以可以写一个lkm出来了。

例子代码

这段代码是一个例子,其功能实现了一个IDS,检测几个简单攻击(land,winnuke)和特殊扫描(nmap)。

<-example.c begin->

/*

* netfilter module example: it`s a kernel IDS(be quie,donot laugh, my friend)

* yawl@nsfocus.com

* Compile:gcc -O -c -Wall sample.c ,under linux2.4 kernel,netfilter is needed.

*/

#define __KERNEL__

#define MODULE

#include <linux/module.h>

#include <linux/skbuff.h>

#include <linux/netdevice.h>

#include <linux/config.h>

#include <linux/ip.h>

#include <linux/tcp.h>

#include <linux/udp.h>

#include <linux/netfilter_ipv4.h>

#define ALERT(fmt,args...) printk("nsfocus: " fmt, ##args)

/*message will be print to screen(too many~),and logged to /var/log/message*/

static unsigned int sample(unsigned int hooknum,struct sk_buff **skb,

const struct net_device *in,

const struct net_device *out,int (*okfn)(struct sk_buff *))

{

struct iphdr *iph;

struct tcphdr *tcph;

struct udphdr *udph;

__u32 sip;

__u32 dip;

__u16 sport;

__u16 dport;

iph=(*skb)->nh.iph;

sip=iph->saddr;

dip=iph->daddr;

/*play ip packet here

(note:checksum has been checked,if connection track is enabled,defrag have been done )*/

if(iph->ihl!=5){

ALERT("IP packet with packet from %d.%d.%d.%d to %d.%d.%d.%d/n",NIPQUAD(sip),NIPQUAD(dip));

}

if(iph->protocol==6){

tcph=(struct tcphdr*)((__u32 *)iph+iph->ihl);

sport=tcph->source;

dport=tcph->dest;

/*play tcp packet here*/

if((tcph->syn)&&(sport==dport)&&(sip==dip)){

ALERT("maybe land attack/n");

}

if(ntohs(tcph->dest)==139&&tcph->urg){

ALERT("maybe winnuke a from %d.%d.%d.%d to %d.%d.%d.%d/n",NIPQUAD(sip),NIPQUAD(dip));

}

if(tcph->ece&&tcph->cwr){

ALERT("queso from %d.%d.%d.%d to %d.%d.%d.%d/n",NIPQUAD(sip),NIPQUAD(dip));

}

if((tcph->fin)&&(tcph->syn)&&(!tcph->rst)&&(!tcph->psh)&&(!tcph->ack)&&(!tcph->urg)){

ALERT("SF_scan from %d.%d.%d.%d to %d.%d.%d.%d/n",NIPQUAD(sip),NIPQUAD(dip));

}

if((!tcph->fin)&&(!tcph->syn)&&(!tcph->rst)&&(!tcph->psh)&&(!tcph->ack)&&(!tcph->urg)){

ALERT("NULL_scan from %d.%d.%d.%d to %d.%d.%d.%d/n",NIPQUAD(sip),NIPQUAD(dip));

}

if(tcph->fin&&tcph->syn&&tcph->rst&&tcph->psh&&tcph->ack&&tcph->urg){

ALERT("FULL_Xmas_scan from %d.%d.%d.%d to %d.%d.%d.%d/n",NIPQUAD(sip),NIPQUAD(dip));

}

if((tcph->fin)&&(!tcph->syn)&&(!tcph->rst)&&(tcph->psh)&&(!tcph->ack)&&(tcph->urg)){

ALERT("XMAS_Scan(FPU)from %d.%d.%d.%d to %d.%d.%d.%d/n",NIPQUAD(sip),NIPQUAD(dip));

}

}

else if(iph->protocol==17){

udph=(struct udphdr *)((__u32 *)iph+iph->ihl);

sport=udph->source;

dport=udph->dest;

/*play udp packet here*/

}

else if(iph->protocol==1){

/*play icmp packet here*/

}

else if(iph->protocol==2){

ALERT("igmp packet from %d.%d.%d.%d to %d.%d.%d.%d/n",NIPQUAD(sip),NIPQUAD(dip));

/*play igmp packet here*/

}

else{

ALERT("unknown protocol%d packet from %d.%d.%d.%d to %d.%d.%d.%d/n",iph->protocol,

NIPQUAD(sip),NIPQUAD(dip));

}

return NF_ACCEPT;

/*for it is IDS,we just accept all packet,

if you really want to drop this skb,just return NF_DROP*/

}

static struct nf_hook_ops iplimitfilter

={ {NULL,NULL} ,sample,PF_INET,NF_IP_PRE_ROUTING,NF_IP_PRI_FILTER-1};

int init_module(void)

{

return nf_register_hook(&iplimitfilter);

}

void cleanup_module(void)

{

nf_unregister_hook(&iplimitfilter);

}

<-example.c end->

3。3 TCP/IP协议

在本程序里,将捕获经过过滤点的数据包,包括IP包和TCP包,通过对这两个包的数据进行分析,就可以获得我们所需要的数据了。

下面我们就来分析一下IP报头和TCP报头吧。

IP报头格式:

第一字节 第二字节 第三字节 第四字节

版本

长度

服务类型

信息包长度

标志符

DF

MF

分段位移

存活时间

传输协议

报头校验和

源地址

目标地址

可选择项

填充

TCP/UDP报头

从应用层来的数据

最高位在左边,记为0 bit;最低位在右边,记为31 bit。4个字节的32 bit值以下面的次序传输:首先是0~7 bit,其次8~15 bit,然后1 6~23 bit,最后是24~31 bit。这种传输次序称作big endian字节序。由于T C P / I P首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。以其他形式存储二进制整数的机器,如little endian格式,则必须在传输数据之前把首部转换成网络字节序。

目前的协议版本号是4,因此I P有时也称作I P v 4。

首部长度指的是首部占32 bit字的数目,包括任何选项。由于它是一个4比特字段,因此首部最长为6 0个字节。普通I P数据报(没有任何选择项)字段的值是5。

IP报头的结构函数如下:

typedef struct _IpHeader

{

unsigned char HeaderLength_Version; unsigned char TypeOfService; // Type of service

unsigned short TotalLength; // total length of the packet

unsigned short Identification; // unique identifier

unsigned short FragmentationFlags; // flags

unsigned char TTL; // Time To Live

unsigned char Protocol; // protocol (TCP, UDP etc)

unsigned short CheckSum; // IP Header checksum

unsigned int sourceIPAddress; // Source address

unsigned int destIPAddress; // Destination Address

} IpHeader;

Buffer里面存储IP数据报的内容

从ipHeader.TotalLength可以知道数据报的长度;从ipHeader.Protocol可以知道IP数据报所携带的传输层的协议。同样的,也可以获得IP报头里面的其它数据。在本程序里,主要事获得数据包的长度、源地址和目的地址等。

TCP报头格式:

第一字节 第二字节 第三字节 第四字节

源端口

目标端口

序列号

确认号

数据 偏移

保留域

U R G

A C C

P S H

R S T

S Y N

F I N

校验和

加急指针

可选择项

填充

从应用层来的数据

有上面可以知道,T C P数据被封装在一个I P数据报中。

T C P提供一种面向连接的、可靠的字节流服务。

面向连接意味着两个使用T C P的应用(通常是一个客户和一个服务器)在彼此交换数据之前必须先建立一个T C P连接。这一过程与打电话很相似,先拨号振铃,等待对方摘机说“喂”,然后才说明是谁。在一个T C P连接中,仅有两方进行彼此通信。

每个T C P段都包含源端和目的端的端口号,用于寻找发端和收端应用进程。这两个值加上I P首部中的源端I P地址和目的端I P地址唯一确定一个T C P连接。

有时,一个I P地址和一个端口号也称为一个插口( s o c k e t)。这个术语出现在最早的T C P规范(R F C 7 9 3)中,后来它也作为表示伯克利版的编程接口。插口对(s o c k e tp a i r)(包含客户I P地址、客户端口号、服务器I P地址和服务器端口号的四元组)可唯一确定互联网络中每个T C P连接的双方。

序号用来标识从T C P发端向T C P收端发送的数据字节流,它表示在这个报文段中的的第一个数据字节。如果将字节流看作在两个应用程序间的单向流动,则T C P用序号对每个字节进行计数。序号是32 bit的无符号数,序号到达23 2-1后又从0开始。

当建立一个新的连接时, S Y N标志变1。序号字段包含由这个主机选择的该连接的初始序号I S N(Initial Sequence Number)。该主机要发送数据的第一个字节序号为这个I S N加1,因为S Y N标志消耗了一个序号(将在下章详细介绍如何建立和终止连接,届时我们将看到F I N标志也要占用一个序号)。

既然每个传输的字节都被计数,确认序号包含发送确认的一端所期望收到的下一个序号。因此,确认序号应当是上次已成功收到数据字节序号加1。只有A C K标志(下面介绍)为1时确认序号字段才有效。

发送A C K无需任何代价,因为32 bit的确认序号字段和A C K标志一样,总是T C P首部的一部分。因此,我们看到一旦一个连接建立起来,这个字段总是被设置, A C K标志也总是被设置为1。

T C P为应用层提供全双工服务。这意味数据能在两个方向上独立地进行传输。因此,连接的每一端必须保持每个方向上的传输数据序号。

T C P可以表述为一个没有选择确认或否认的滑动窗口协议。我们说T C P缺少选择确认是因为T C P首部中的确认序号表示发方已成功收到字节,但还不包含确认序号所指的字节。当前还无法对数据流中选定的部分进行确认。例如,如果1~1 0 2 4字节已经成功收到,下一报文段中包含序号从2 0 4 9~3 0 7 2的字节,收端并不能确认这个新的报文段。它所能做的就是发回一个确认序号为1 0 2 5的A C K。它也无法对一个报文段进行否认。例如,如果收到包含1 0 2 5~2 0 4 8字节的报文段,但它的检验和错, T C P接收端所能做的就是发回一个确认序号为1 0 2 5的A C K。在2 1 . 7节我们将看到重复的确认如何帮助确定分组已经丢失。

首部长度给出首部中32 bit字的数目。需要这个值是因为任选字段的长度是可变的。这个字段占4 bit,因此T C P最多有6 0字节的首部。然而,没有任选字段,正常的长度是2 0字节。

在T C P首部中有6个标志比特。它们中的多个可同时被设置为1。我们在这儿简单介绍它们的用法,在随后的章节中有更详细的介绍。

U R G 紧急指针( u rgent pointer)有效(见2 0 . 8节)。

A C K 确认序号有效。

P S H 接收方应该尽快将这个报文段交给应用层。

R S T 重建连接。

S Y N 同步序号用来发起一个连接。这个标志和下一个标志将在第1 8章介绍。

F I N 发端完成发送任务。

T C P的流量控制由连接的每一端通过声明的窗口大小来提供。窗口大小为字节数,起始于确认序号字段指明的值,这个值是接收端正期望接收的字节。窗口大小是一个16 bit字段,因而窗口大小最大为6 5 5 3 5字节。在2 4 . 4节我们将看到新的窗口刻度选项,它允许这个值按比例变化以提供更大的窗口。

检验和覆盖了整个的T C P报文段: T C P首部和T C P数据。这是一个强制性的字段,一定是由发端计算和存储,并由收端进行验证。T C P检验和的计算和U D P检验和的计算相似,使用如11 . 3节所述的一个伪首部。

只有当U R G标志置1时紧急指针才有效。紧急指针是一个正的偏移量,和序号字段中的值相加表示紧急数据最后一个字节的序号。T C P的紧急方式是发送端向另一端发送紧急数据的一种方式。

最常见的可选字段是最长报文大小,又称为MSS (Maximum Segment Size)。每个连接方

通常都在通信的第一个报文段(为建立连接而设置S Y N标志的那个段)中指明这个选项。它指明本端所能接收的最大长度的报文段。

从上图中我们注意到T C P报文段中的数据部分是可选的。如果一方没有数据要发送,也

使用没有任何数据的首部来确认收到的数据。在处理超时的许多情况中,也会发送不带任何

数据的报文段。

TCP报头的结构如下:

typedef struct _TCPHeader

{

unsigned short SourcePort;

unsigned short DestinationPort;

unsigned int SequenceNumber;

unsigned int AcknowledgeNumber;

unsigned char DataOffset;

unsigned char Flags;

unsigned short Window;

unsigned short Checksum;

unsigned short UrgentPointer;

} TCPHeader;

下图是对网络上传输的数据包进行过滤所捕获的IP包信息(在终端上显示):

因为本程序把所获得的数据写入/var/log/message这个文件里(这个文件记录的是系统的一些信息),下图就是/var/log/message里所记录的数据包的信息。

内核里面的数据通过数据交互模块,可以把它们都显示在用户界面上,下图就是用户界面上所显示的数据包的信息。

3。4 sk_buff

在Linux内核中,分不同的层次,使用两种数据结构来保存数据。在BSD Socket层内使用msghdr{}结构保存数据;在INET Socket层以下都使用sk_buff{}(socket buff)数据结构保存数据。

sk_buff的结构如下:

sk_buff{}可以说是一条双向链表,其中,next指向链表的下一个指针,pre指向链表的上一个指针,在sk_buff{}中的四个指针data、head、tail、end初始化的时候,data、head、tail都是指向申请到的数据区的头部,end指向数据区的尾部。在以后的操作中,一般都是通过data和tail来获得在sk_buff中有用的数据的开始和结束位置,而head和end就表示sk_buff中存放的数据包最大可扩展的空间范围。

3。5 RAR技术资料

3。5。1 RAR archive文件格式

Archive文件包含可变长度的块(blocks)。这些块是可变的,但是每一个块都以marker块开始。

每一个块都包含以下内容:

HEAD_CRC 2 bytes CRC of total block or block part

HEAD_TYPE 1 byte 块类型

HEAD_FLAGS 2 bytes 块标志

HEAD_SIZE 2 bytes 块大小

ADD_SIZE 4 bytes 附加块大小(可选)

当(HEAD_FLAGS & 0x8000) != 0时ADD_SIZE存在

当(HEAD_FLAGS & 0x8000) == 0时,总的块大小为HEAD_SIZE,当(HEAD_FLAGS & 0x8000) != 0时,总的块大小为HEAD_SIZE+ADD_SIZE。

在每一个块里,HEAD_FLAGS的值有着相同的意思:

0x4000 - 假如被设置,当文件被更新的时候,旧的RAR版本将忽略和删除这个块;

假如被清除,当文件被更新的时候,将拷贝新的块到文档文件里。

0x8000 - 假如被设置,ADD_SIZE将存在,而且块的大小是HEAD_SIZE+ADD_SIZE.

块类型的表示如下:

HEAD_TYPE=0x72 marker block

HEAD_TYPE=0x73 archive header

HEAD_TYPE=0x74 file header

HEAD_TYPE=0x75 comment header

HEAD_TYPE=0x76 extra information

HEAD_TYPE=0x77 subblock

HEAD_TYPE=0x78 recovery record

Comment block只存在其它块里,而不单独存在。

程序流程如下:

1.读取marker block

2.读取archive header

3. 读取file header的前面7个字节: HEAD_CRC, HEAD_TYPE, HEAD_FLAGS,

HEAD_SIZE

4. 检查 HEAD_TYPE

当HEAD_TYPE==0x74时执行下面操作,否则结束

读取 file header ( file header的头7个字节已经读取 )

读取HEAD_SIZE和PACK_SIZE,则file header大小为HEAD_SIZE+PACK_SIZE个字节。

读取 FILE_NAME和NAME_SIZE,则可以得到文件名

读取下一个block

5.转到4

3。5。2 块格式

Marker block ( MARK_HEAD )

HEAD_CRC 总是为0x6152 2字节

HEAD_TYPE 总是为0x72 1字节

HEAD_FLAGS 总是为0x1a21 2字节

HEAD_SIZE 总是为0x0007 2字节

marker block事实上总是固定的,即: 0x52 0x61 0x72 0x21 0x1a 0x07 0x00

Archive header ( MAIN_HEAD )

HEAD_CRC CRC of fields HEAD_TYPE to RESERVED2 2字节

HEAD_TYPE 头类型: 0x73 1字节

HEAD_FLAGS Bit flags: 2字节

0x01 - Volume attribute (archive volume)

0x02 - Archive comment present

0x04 - Archive lock attribute

0x08 - Solid attribute (solid archive)

0x10 - Unused

0x20 - Authenticity information present

HEAD_FLAGS里面的其它比特被保留为内部使用。

HEAD_SIZE Archive header total size including archive comments 2字节

RESERVED1 保留 2字节

RESERVED2 保留 4字节

Comment block 当(HEAD_FLAGS & 0x02) != 0时存在

File header (File in archive)

HEAD_CRC CRC of fields from HEAD_TYPE to FILEATTR and file name 2字节

HEAD_TYPE 头部类型: 0x74 1字节

HEAD_FLAGS 比特标志: 2字节

0x01 - file continued from previous volume

0x02 - file continued in next volume

0x04 - file encrypted with password

0x08 - file comment present

0x10 - information from previous files is used (solid flag)

(for RAR 2.0 and later)

bits 7 6 5 (for RAR 2.0 and later)

0 0 0 - dictionary size 64 Kb

0 0 1 - dictionary size 128 Kb

0 1 0 - dictionary size 256 Kb

0 1 1 - dictionary size 512 Kb

1 0 0 - dictionary size 1024 Kb

1 0 1 - reserved

1 1 0 - reserved

1 1 1 - file is directory

(HEAD_FLAGS & 0x8000) == 1,因为所有块大小为HEAD_SIZE + PACK_SIZE

HEAD_SIZE File header的总大小包括file name和comments 2字节

PACK_SIZE 被压缩的文件大小 4字节

UNP_SIZE 解压的文件大小 4字节

HOST_OS 被文档使用的操作系统 1字节

0 - MS DOS

1 - OS/2

2 - Win32

3 - Unix

FILE_CRC File CRC 4字节

FTIME 在经典MS DOS格式的数据和时间 4字节

UNP_VER RAR文件的版本 1字节

METHOD 压缩方式 1字节

NAME_SIZE 文件名的大小 2字节

ATTR 文件属性 4字节

FILE_NAME 文件名 - 有NAME_SIZE字节大小

Comment block 当(HEAD_FLAGS & 0x08) != 0时存在

Comment block

HEAD_CRC CRC of fields from HEAD_TYPE to COMM_CRC 2字节

HEAD_TYPE 头部类型: 0x75 1字节

HEAD_FLAGS 比特标志 2字节

HEAD_SIZE Comment header大小 + comment大小 2字节

UNP_SIZE 解压的comment大小 2字节

UNP_VER RAR版本 1字节

METHOD 压缩方式 1字节

COMM_CRC Comment CRC 2字节

COMMENT Comment text

Extra info block

HEAD_CRC Block CRC 2字节

HEAD_TYPE 头部类型: 0x76 1字节

HEAD_FLAGS 比特标志 2字节

HEAD_SIZE 块大小 2字节

INFO 其它数据

Subblock

文档里面的对象(block或者header)可以跟一个subblock。subblock必须依靠主对象。当文档更新时subblock可以给删除或者转移到新版本里面去。

Subblock包括下面内容:

HEAD_CRC Block CRC 2字节

HEAD_TYPE 头部类型: 0x77 1字节

HEAD_FLAGS 比特标志 2字节

(HEAD_FLAGS & 0x8000) == 1,因为总的块大小为HEAD_SIZE + DATA_SIZE

HEAD_SIZE 块大小 2字节

DATA_SIZE 数据大小 4字节

SUB_TYPE Subblock类型 2字节

RESERVED 一定为0 1字节

Other 在subblock中的其它内容

OS/2 extended attributes subblock

HEAD_CRC Block CRC 2字节

HEAD_TYPE 头部类型: 0x77 1字节

HEAD_FLAGS 比特标志 2字节

(HEAD_FLAGS & 0x8000) == 1,因为总的块大小为HEAD_SIZE + DATA_SIZE

HEAD_SIZE 块大小 2字节

DATA_SIZE 数据大小 4字节

SUB_TYPE 0x100 2字节

RESERVED 一定为0 1字节

UNP_SIZE 被解压数据的大小 4字节

UNP_VER RAR版本 1字节

METHOD 压缩方式 1字节

EA_CRC Extended attributes CRC 4字节

下图是程序对本机的showfile.rar文件进行过滤所得到的结果。

下图是对网络上传输的rar文件进行过滤的结果(结果写在/var/log/messages这个文件里面)。 其中,begin表示一个数据包的开始,end表示已经读过这个数据包,在begin和end之间写有“this is a rar file”这句话的表示这个数据包包含有rar文件的头部,写有“this is the follow of a rar file”的表示这个数据包包含有rar文件的一部分(不是头部),由上图可见,在rar文件的头部,包含有一个叫a.doc的文件名,而在接下来的一个数据包里,虽然它是rar文件的一部分,但却不包含有文件名,这是因为a.doc文件比较大,所以在下一个数据包里面,只包含有此文件的数据,而没有读到下一个文件。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏cmazxiaoma的架构师之路

Listener、Filter、Interceptor的那些事

17440
来自专栏Rindew的iOS技术分享

苏宁一面

18640
来自专栏jeremy的技术点滴

现代Web开发系列教程_06

32670
来自专栏向治洪

备忘录模式

概念 备忘录模式:又叫做快照模式,属于行为模式的一种,指在不破坏封装性的前提下,获取到一个对象的内部状态,并在对象之外记录或保存这个状态。在有需要的时候可将该对...

20980
来自专栏IT进修之路

原 JAVA的那些事儿

22870
来自专栏熊二哥

快速入门系列--MVC--05行为

    Action执行包含内容比较多,主要有同步/异步Action的概念和执行过程,Authorationfilter, ActionFiltor, Resu...

20070
来自专栏岑玉海

hbase源码系列(六)HMaster启动过程

  这一章是server端开始的第一章,有兴趣的朋友先去看一下hbase的架构图,我专门从网上弄下来的。   按照HMaster的run方法的注释,我们可以了解...

59990
来自专栏前端黑板报

HTTP2基础教程-读书笔记(四)

? 记录一下HTTP/2的底层原理,帮助理解协议实现细节。 连接 每个端点都需要发送一个连接作为最终确认使用的协议,并建立http/2连接的初始设置。客户端和...

35460
来自专栏技术小黑屋

记一场 Android 技术答疑

之前在Stuq的Android课程中有幸分享了一些关于优化的问题,后期又处理了一些来自网友的问题,这里简单以文字形式做个整理.

13820
来自专栏小狼的世界

Crontab中的除号(slash)到底怎么用?

crontab 是Linux中配置定时任务的工具,在各种配置中,我们经常会看到除号(Slash)的使用,那么这个除号到底标示什么意思,使用中有哪些需要注意的地方...

10620

扫码关注云+社区

领取腾讯云代金券