深入Linux内核网络堆栈

前一段时间看到这篇帖子,确实很经典,于是翻出了英文原版再读,顺便再翻译出来供大家学习,这篇文章的中文版也早都有了,不过出于完全理解的目的,我还是将它翻译了出来,加进了自己的代码,虽然在上一周的翻译过程中,我尽量保留文章的原汁原味,但错误肯定在所难免,在末尾附上原文和我自己调试通过的代码,已经够构运行,大家可以参考一下!(有错误之处请指出)

深入Linux内核网络堆栈

作者:bioforge alkerr@yifan.net 
原名: <<Hacking the Linux Kernel Network Stack>>
翻译,修改: duanjigang <duanjigang1983@126.com>
翻译参考:raodan (raod_at_30san.com) 2003-08-22 

第一章  简介

本文将描述如何利用Linux网络堆栈的窍门(不一定都是漏洞)来达到一些目的,或者是恶意的,或者是出于其它意图的。文中会就后门通讯对Netfilter钩子进行讨论,并在本地机器上实现将这个传输从基于Libpcap的嗅探器(sniffer)中隐藏。
    Netfilter是2.4内核的一个子系统。Netfilter可以通过在内核的网络代码中使用各种钩子来实现数据包过滤,网络地址转换(NAT)和连接跟踪等网络欺骗。这些钩子被放置在内核代码段,或者静态编译进内核,或者作为一个可动态加载/卸载的可卸载模块,然后就可以注册称之为网络事件的函数(比如数据包的接收)。

1.1 本文论述的内容

本文将讲述内核模块的编写者如何利用Netfilter的钩子来达到任何目的,以及怎样将网络传输从一个Libpcap的应用中隐藏掉。尽管Linux2.4支持对IPV4,IPV6以及DECnet的钩子,本文只提及IPV4的钩子。但是,对IPV4的大多数应用内容同样也可以应用于其他协议。出于教学目的,我们在附录A给出了一个可以工作的内核模块,实现基本的数据包过滤功能。针对本文中所列技术的所有开发和试验都在Intel机子上的Linux2.4.5系统上进行过。对Netfilte 钩子行为的测试使用的是回环设备(Loopback device),以太网设备和一个点对点接口的调制解调器。
对Netfilter进行完全理解是我撰写本文的另一个初衷。我不能保证这篇文章所附的代码100%的没有差错,但是所列举的所有代码我都事先测试过了。我已经饱尝了内核错误带来的磨砺,而你却不必再经受这些。同样,我不会为按照这篇文档所说的任何东西进行的作所所为带来的损失而负责。阅读本篇文章的读者最好熟悉C程序设计语言,并且对内核可卸载模块有一定的经验。
如果我在文中犯了任何错误的话,请告知我。我对于你们的建议和针对此文的改进或者其它的Netfilter应用会倾心接受。

1.2 本文不会涉及到的方面

本文并不是Netfilter的完全贯穿(或者进进出出的讲解)。也不是iptables命令的介绍。如果你想更好的学习iptables的命令,可以去咨询man手册。
让我们从介绍Nerfilter的使用开始吧……….

第二章  各种NetFilter 钩子及其用法

2.1 Linux内核对数据包的处理

我将尽最大努力去分析内核处理数据包的详细内幕,然而对于事件触发处理以及之后的Netfilter 钩子不做介绍。原因很简单,因为Harald Welte 关于这个已经写了一篇再好不过的文章<<Journey  of a Packet Through the Linux 2.4 Network Stack>>,如果你想获取更多关于Linux对数据包的相关处理知识的话,我强烈建议你也阅读一下这篇文章。目前,就认为数据包只是经过了Linux内核的网络堆栈,它穿过几层钩子,在经过这些钩子时,数据包被解析,保留或者丢弃。这就是所谓的Netfilter 钩子。

2.2 Ipv4中的Netfilter钩子

Netfilter为IPV4定义了5个钩子。可以在 linux/netfilter-ipv4.h里面找到这些符号的定义,表2.1列出了这些钩子。

表 2.1. ipv4中定义的钩子
  1. 钩子名称                              调用时机  
  2. NF_IP_PRE_ROUTING                完整性校验之后,路由决策之前
  3. NF_IP_LOCAL_IN                         目的地为本机,路由决策之后
  4. NF_IP_FORWARD                         数据包要到达另外一个接口去
  5. NF_IP_LOCAL_OUT                        本地进程的数据,发送出去的过程中
  6. NF_IP_POST_ROUTING        向外流出的数据上线之前
复制代码

NF_IP_PRE_ROUTING 钩子称为是数据包接收后第一个调用的钩子程序,这个钩子在我们后面提到的模块当中将会被用到。其他的钩子也很重要,但是目前我们只集中探讨NF_IP_PRE_ROUTING这个钩子。
不管钩子函数对数据包做了哪些处理,它都必须返回表2.2中的一个预定义好的Netfilter返回码。
表2.2 Netfilter 返回码
  1. 返回码                  含义
  2. NF_DROP              丢弃这个数据包
  3. NF_ACCEPT        保留这个数据包
  4. NF_STOLEN        忘掉这个数据包
  5. NF_QUEUE        让这个数据包在用户空间排队
  6. NF_REPEAT        再次调用这个钩子函数
复制代码

NF_DROP 表示要丢弃这个数据包,并且为这个数据包申请的所有资源都要得到释放。NF_ACCEPT告诉Netfilter到目前为止,这个数据包仍然可以被接受,应该将它移到网络堆栈的下一层。NF_STOLEN是非常有趣的一个返回码,它告诉Netfilter让其忘掉这个数据包。也就是说钩子函数会在这里对这个数据包进行完全的处理,而Netfilter就应该放弃任何对它的处理了。然而这并不意味着为该数据包申请的所有资源都要释放掉。这个数据包和它各自的sk_buff结构体依然有效,只是钩子函数从Netfilter夺取了对这个数据包的掌控权。不幸的是,我对于NF_QUEUE这个返回码的真实作用还不是很清楚,所在目前不对它进行讨论。最后一个返回值NF_REPEAT请求Netfilter再次调用这个钩子函数,很明显,你应该慎重的应用这个返回值,以免程序陷入死循环。

第三章  注册和注销NetFilter 钩子

注册一个钩子函数是一个围绕nf_hook_ops结构体的很简单的过程,在linux/netfilter.h中有这个结构体的定义,定义如下:
  1. struct nf_hook_ops 
  2. {
  3.                   struct list_head list;

  4.                   /* User fills in from here down. */
  5.                   nf_hookfn *hook;
  6.                   int pf;
  7.                   int hooknum;
  8.                   /* Hooks are ordered in ascending priority. */
  9.                   int priority;
  10. };
复制代码

这个结构体的成员列表主要是用来维护注册的钩子函数列表的,对于用户来说,在注册时并没有多么重要。hook是指向nf_hookfn函数的指针。也就是为这个钩子将要调用的所有函数。nf_hookfn同样定义在linux/netfilter.h这个文件中。pf字段指定了协议簇(protocol family)。Linux/socket.h中定义了可用的协议簇。但是对于IPV4我们只使用PF_INET。hooknum 域指名了为哪个特殊的钩子安装这个函数,也就是表2.1中所列出的条目中的一个。Priority域表示在运行时这个钩子函数执行的顺序。为了演示例子模块,我们选择NF_IP_PRI_FIRST这个优先级。
   注册一个Netfilter钩子要用到nf_hook_ops这个结构体和nf_register_hook()函数。nf_register_hook()函数以一个nf_hook_ops结构体的地址作为参数,返回一个整型值。如果你阅读了net/core/netfilter.c中nf_register_钩子()的源代码的话,你就会发现这个函数只返回了一个0。下面这个例子注册了一个丢弃所有进入的数据包的函数。这段代码同时会向你演示Netfilter的返回值是如何被解析的。

代码列表1. Netfilter钩子的注册
  1. /* Sample code to install a Netfilter hook function that will
  2. * drop all incoming packets. */
  3. #define __KERNEL__
  4. #define MODULE
  5. #include <linux/module.h>
  6. #include <linux/kernel.h>
  7. #include <linux/netfilter.h>
  8. #include <linux/netfilter_ipv4.h>

  9. /* This is the structure we shall use to register our function */

  10. static struct nf_hook_ops nfho;

  11. /* This is the hook function itself */

  12. unsigned int hook_func(unsigned int hooknum,
  13. struct sk_buff **skb,
  14. const struct net_device *in,
  15. const struct net_device *out,
  16. int (*okfn)(struct sk_buff *))
  17. {

  18. return NF_DROP;           /* Drop ALL packets */
  19. }

  20. /* Initialisation routine */
  21. int init_module()
  22. {

  23. /* Fill in our hook structure */
  24. nfho.hook = hook_func;         /* Handler function */
  25. nfho.hooknum  = NF_IP_PRE_ROUTING; /* First hook for IPv4 */
  26. nfho.pf       = PF_INET;
  27. nfho.priority = NF_IP_PRI_FIRST;   /* Make our function first */
  28. nf_register_hook(&nfho);
  29. return 0;
  30. }

  31. /* Cleanup routine */
  32. void cleanup_module()
  33. {
  34. nf_unregister_hook(&nfho);
  35. }
复制代码


这就是注册所要做的一切。从代码列表1你可以看到注销一个Netfilter钩子也是很简单的一件事情,只需要调用nf_unregister_hook()函数,并将注册时用到的结构体地址再次作为注销函数参数使用就可以了。
第四章  基本的NetFilter数据包过滤技术
4.1 钩子函数近距离接触
现在是我们来查看获得的数据如何传入钩子函数并被用来进行过滤决策的时候了。所以,我们需要更多的关注于nf_hookfn函数的模型。Linux/netfilter.h给出了如下的接口定义:
  1. typedef unsigned int nf_hookfn(unsigned int hooknum,
  2.                               struct sk_buff **skb,
  3.                               const struct net_device *in,
  4.                               const struct net_device *out,
  5.                               int (*okfn)(struct sk_buff *));
复制代码

nf_hookfn函数的第一个参数指定了表2.1给出的钩子类型中的一种。第二个参数更有趣,它是一个指向指针(这个指针指向一个sk_buff类型的结构体)的指针,它是网络堆栈用来描述数据包的结构体。这个结构体定义在linux/skbuff.h中,由于这个结构体的定义很大,这里我只着重于它当中更有趣的一些域。
或许sk_buff结构体中最有用的域就是其中的三个联合了,这三个联合描述了传输层的头信息(例如 UDP,TCP,ICMP,SPX),网络层的头信息(例如ipv4/6, IPX, RAW)和链路层的头信息(Ethernet 或者RAW)。三个联合相应的名字分别为:h,nh和mac。根据特定数据包使用的不同协议,这些联合包含了不同的结构体。应当注意,传输层的头和网络层的头极有可能在内存中指向相同的内存单元。在TCP数据包中也是这样的情况,h和nh都是指向IP头结构体的指针。这就意味着,如果认为h->th指向TCP头,从而想通过h->th来获取一个值的话,将会导致错误发生。因为h->th实际指向IP头,等同于nh->iph。
其他比较有趣的域就是len域和data域了。len表示包中从data开始的数据总长度。因此,现在我们就知道如何通过一个skbuff结构体去访问单个的协议头或者数据包本身的数据。还有什么有趣的数据位对于Netfilter的钩子函数而言是有用的呢?
跟在sk_buff之后的两个参数都是指向net_device结构体的指针。net_devices结构体是Linux内核用来描述各种网络接口的。第一个结构体,in,代表了数据包将要到达的接口,当然 out就代表了数据包将要离开的接口。有很重要的一点必须认识到,那就是通常情况下这两个参数最多只提供一个。 例如,in通常情况下只会被提供给NF_IP_PRE_ROUTING和NF_IP_LOCAL_IN钩子。out通常只被提供给NF_IP_LOCAL_OUT和NF_IP_POST_ROUTING钩子。在这个阶段,我没有测试他们中的那个对于NF_IP_FORWARD是可用的。如果你能在废弃之前确认它们(in和out)不空的话,那么你很优秀。
最后,传给钩子函数的最后一个参数是一个名为okfn的指向函数的指针,这个函数有一个sk_buff的结构体作为参数,返回一个整型值。我也不能确定这个函数做什么,在net/core/netfilter.c中有两处对此函数的调用。这两处调用就是在函数nf_hook_slow()和函数nf_reinject()里,在这两个调用处当Netfilter钩子的返回值为NF_ACCEPT时,此函数被调用。如果有谁知道关于okfn更详细的信息,请告诉我。
现在我们已经对Netfilter接收到的数据中最有趣和最有用的部分进行了分析,下面就要开始介绍如何利用这些信息对数据包进行各种各样的过滤。

4.2 基于接口的过滤
这将是我们能做的最简单的过滤技术。是否还记得我们的钩子函数接收到的net_device结构体?利用net_device结构体中的name键值,我们可以根据数据包的目的接口名或者源接口名来丢弃这些数据包。为了抛弃所有发向”eth0”的数据,我们只需要比较一下“in->name”和“eth0”,如果匹配的话,钩子函数返回NF_DROP,然后这个数据包就被销毁了。它就是这样的简单。列表2给出了示例代码。请注意轻量级防火墙(LWFW)会使用到这里提到的所有过滤方法。LWFW同时还包含了一个IOCTL方法来动态改变自身的行为。

列表2. 基于源接口(网卡名)的数据过滤技术
  1. /* Sample code to install a Netfilter hook function that will
  2.           * drop all incoming packets from an IP address we specify */

  3.           #define __KERNEL__
  4.           #define MODULE

  5.           #include <linux/module.h>
  6.           #include <linux/kernel.h>
  7.           #include <linux/skbuff.h>
  8.           #include <linux/ip.h>                  /* For IP header */
  9.           #include <linux/netfilter.h>
  10.           #include <linux/netfilter_ipv4.h>

  11.           /* This is the structure we shall use to register our function */
  12.           static struct nf_hook_ops nfho;

  13.           /* IP address we want to drop packets from, in NB order */
  14.           static unsigned char *drop_ip = "\x7f\x00\x00\x01";

  15.           /* This is the hook function itself */
  16.           unsigned int hook_func(unsigned int hook_num,
  17.                                  struct sk_buff **skb,
  18.                                  const struct net_device *in,
  19.                                  const struct net_device *out,
  20.                                  int (*okfn)(struct sk_buff *))
  21.           {
  22.               struct sk_buff *sb = *skb;

  23.               if (sb->nh.iph->saddr == drop_ip) {
  24.                   printk("Dropped packet from... %d.%d.%d.%d\n",
  25.                             *drop_ip, *(drop_ip + 1),
  26.                           *(drop_ip + 2), *(drop_ip + 3));
  27.                   return NF_DROP;
  28.               } else {
  29.                   return NF_ACCEPT;
  30.               }
  31.           }

  32.           /* Initialisation routine */
  33.           int init_module()
  34.           {
  35.               /* Fill in our hook structure */
  36.               nfho.hook     = hook_func;
  37.               /* Handler function */
  38.               nfho.hook_num  = NF_IP_PRE_ROUTING; /* First for IPv4 */
  39.               nfho.pf       = PF_INET;
  40.               nfho.priority = NF_IP_PRI_FIRST;   /* Make our func first */
  41.           
  42.               nf_register_hook(&nfho);

  43.               return 0;
  44.           }
  45.           
  46.              /* Cleanup routine */
  47.           void cleanup_module()
  48.           {
  49.               nf_unregister_hook(&nfho);
  50.           }
复制代码

现在看看,是不是很简单?下面让我们看看基于IP地址的过滤技术。
4.3 基于IP地址的过滤
类似基于接口的数据包过滤技术,基于源/目的IP地址的数据包过滤技术也很简单。这次我们对sk_buff结构体比较感兴趣。现在应该记起来,Skb参数是一个指向sk_buff结构体的指针的指针。为了避免运行时出现错误,通常有一个好的习惯就是另外声明一个指针指向sk_buff结构体的指针,把它赋值为双重指针所指向的内容,像这样:
  1. struct sk_buff *sb = *skb;    /* Remove 1 level of indirection* /
复制代码

然后你只需要引用一次就可以访问结构体中的成员了。可以使用sk_buff结构体中的网络层头信息来获取此数据包的IP头信息。这个头包含在一个联合中,可以通过sk_buff->nh.iph来获取。列表3的函数演示了当给定一个数据包的sk_buff结构时,如何根据给定的要拒绝的IP对这个数据包进行源IP地址的检验。这段代码是直接从LWFW中拉出来的。唯一的不同之处就是LWFW中对LWFW统计量的更新被去掉了。
列表3.检测接收到数据包的源IP地址
  1. unsigned char *deny_ip = "\x7f\x00\x00\x01";  /* 127.0.0.1 */

  2.           ...
  3.           static int check_ip_packet(struct sk_buff *skb)
  4.           {
  5.               /* We don't want any NULL pointers in the chain to
  6.                * the IP header. */
  7.               if (!skb )return NF_ACCEPT;
  8.               if (!(skb->nh.iph)) return NF_ACCEPT;
  9.               if (skb->nh.iph->saddr == *(unsigned int *)deny_ip) 

  10.                     return NF_DROP;
  11.                }
  12.                return NF_ACCEPT;
  13.           }
复制代码

如果源IP地址与我们想抛弃数据包的IP地址匹配的话,数据包就会被丢弃。为了使函数能正常工作,deny_ip的值应该以网络字节序的方式存储(与intel相反的Big-endian格式)。尽管这个函数在被调用的时候有一个空指针作参数这种情况不太可能,但是稍微偏执(小心)一点总不会有什么坏处。当然,如果调用时出现了差错的话,函数将会返回一个NF_ACCEPT值,以便于Netfilter能够继续处理这个数据包。列表4 展现了一个简单的基于IP地址的数据包过滤的模块,这个模块是由基于接口的过滤模块修改得到的。你可以修改IP地址来实现对指定IP地址发来的数据包的丢弃。

列表4. 基于数据包源IP地址的过滤技术
  1. /* Sample code to install a Netfilter hook function that will
  2.           * drop all incoming packets from an IP address we specify */

  3.           #define __KERNEL__
  4. #define MODULE
  5. #include <linux/module.h>
  6.           #include <linux/kernel.h>
  7.           #include <linux/skbuff.h>
  8.           #include <linux/ip.h>                  /* For IP header */
  9.           #include <linux/netfilter.h>
  10.           #include <linux/netfilter_ipv4.h>

  11.           /* This is the structure we shall use to register our function */
  12.           static struct nf_hook_ops nfho;

  13.           /* IP address we want to drop packets from, in NB order */
  14.           static unsigned char *drop_ip = "\x7f\x00\x00\x01";

  15.           /* This is the hook function itself */
  16.           unsigned int hook_func(unsigned int hooknum,
  17.                                  struct sk_buff **skb,
  18.                                  const struct net_device *in,
  19.                                  const struct net_device *out,
  20.                                  int (*okfn)(struct sk_buff *))
  21.           {
  22.               struct sk_buff *sb = *skb;

  23.               if (sb->nh.iph->saddr == drop_ip) {
  24.                   printk("Dropped packet from... %d.%d.%d.%d\n",
  25.                             *drop_ip, *(drop_ip + 1),
  26.                           *(drop_ip + 2), *(drop_ip + 3));
  27.                   return NF_DROP;
  28.               } else {
  29.                   return NF_ACCEPT;
  30.               }
  31.           }

  32.           /* Initialisation routine */
  33.           int init_module()
  34.           {
  35.               /* Fill in our hook structure */
  36.               nfho.hook     = hook_func;
  37.               /* Handler function */
  38.               nfho.hooknum  = NF_IP_PRE_ROUTING; /* First for IPv4 */
  39.               nfho.pf       = PF_INET;
  40.               nfho.priority = NF_IP_PRI_FIRST;   /* Make our func first */
  41.               nf_register_hook(&nfho);
  42.               return 0;
  43.           }
  44.               /* Cleanup routine */
  45.           void cleanup_module()
  46.           {
  47.               nf_unregister_hook(&nfho);
  48.           }
复制代码


4.4 基于TCP端口的过滤
另外一个要执行的简单的规则就是基于TCP目的端口的数据包过滤。这比检验IP地址稍微复杂一点,因为我们要自己创建一个指向TCP头的指针。还记得前面关于传输层头和网络层头所做的讨论吗?获得一个TCP头指针很简单,只需要申请一个指向tcphdr(定义在linux/tcp.h中)结构体的指针,并将它指向包数据中的IP头后面。或许一个例子就可以了。列表5展示了怎样检测一个数据包的TCP目的端口与我们想丢弃数据的指定端口是否一致。与列表3一样,这段代码也是从LWFW中拿出来的
列表5. 检测接收到数据包的TCP目的端口
  1. unsigned char *deny_port = "\x00\x19";   /* port 25 */
  2.           ...
  3.           static int check_tcp_packet(struct sk_buff *skb)
  4.           {
  5.               struct tcphdr *thead;
  6.               /* We don't want any NULL pointers in the chain
  7.                * to the IP header. */
  8.               if (!skb ) return NF_ACCEPT;
  9.               if (!(skb->nh.iph)) return NF_ACCEPT;
  10.               /* Be sure this is a TCP packet first */
  11.               if (skb->nh.iph->protocol != IPPROTO_TCP) {
  12.                   return NF_ACCEPT;
  13.               }
  14.               thead = (struct tcphdr *)(skb->data  + (skb->nh.iph->ihl * 4));
  15.               /* Now check the destination port */
  16.               if ((thead->dest) == *(unsigned short *)deny_port) {
  17.                   return NF_DROP;
  18.               }    
  19.               return NF_ACCEPT;
  20.           }
复制代码

世纪上非常简单。不要忘了deny_port是网络字节序时,这个函数才能工作。数据包过滤技术的基础就是:对于一个特定的数据包,你必须对怎样到达你想要的信息段的方法非常了解。下面,我们将进入更有趣的世界
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值