前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >基于linux的嵌入IPv4协议栈的内容过滤防火墙系统(5)-包过滤模块和内容过滤模块所采用的各种技术详述

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

作者头像
源哥
发布2018-08-28 11:26:04
1.1K0
发布2018-08-28 11:26:04
举报
文章被收录于专栏:源哥的专栏

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

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文件比较大,所以在下一个数据包里面,只包含有此文件的数据,而没有读到下一个文件。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2005年02月19日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档