免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 50135 | 回复: 81

Linux内核发送构造数据包的方式 [复制链接]

论坛徽章:
36
IT运维版块每日发帖之星
日期:2016-04-10 06:20:00IT运维版块每日发帖之星
日期:2016-04-16 06:20:0015-16赛季CBA联赛之广东
日期:2016-04-16 19:59:32IT运维版块每日发帖之星
日期:2016-04-18 06:20:00IT运维版块每日发帖之星
日期:2016-04-19 06:20:00每日论坛发贴之星
日期:2016-04-19 06:20:00IT运维版块每日发帖之星
日期:2016-04-25 06:20:00IT运维版块每日发帖之星
日期:2016-05-06 06:20:00IT运维版块每日发帖之星
日期:2016-05-08 06:20:00IT运维版块每日发帖之星
日期:2016-05-13 06:20:00IT运维版块每日发帖之星
日期:2016-05-28 06:20:00每日论坛发贴之星
日期:2016-05-28 06:20:00
发表于 2009-09-02 13:04 |显示全部楼层
本文欢迎自由转载,但请标明出处,并保证本文的完整性。

      作者:Godbach

      日期:2009/09/01


一、构造数据包简析

这里并不详细介绍如何在内核中构造数据包,下文如有需要会在适当的位置进行分析。这里简单的分析讲一下内核态基于Netfilter框架构造数据包的方式。
内核中可以用到的构造数据包的方式,个人认为可以分为两种。

其一,我们直接用alloc_skb申请一个skb结构体,然后根据实际的应用填充不同的成员,或者基于当前数据包的skb,调用skb_copy_expand()函数等新申请一个nskb,并且拷贝skb的内容。

其二,也是个人比较常用的,就是直接在先前接收到的数据包skb上作修改,主要有源IP、目IP,如果是TCP/UDP协议的话,还有源端口目的端口号。总之,就是根据自己的需求去调整数据包的相关成员即可。

通常,这两种方式最终可能都要涉及到重新计算各个部分的校验和,这也是必须的。


二、如何发送构造的数据包

       承接上文,数据包已经构造完毕,下一步关键就是如何发送数据包了。个人这里总结的有两种方法。

       方法一,就是让数据包接着按照Netfilter的流程进行传输。因为数据包的一些内容已经被更改,尤其是当源IP和目的IP被更改,主要是交换的情况下,是需要确保有路由可查的。

       NF框架中查路由的位置一是在PREROUTING之后,而是在LOCALOUT之后。又由于这里是需要将数据包从本地发送出去。因此,可以考虑让修改后的数据包从LOCALOUT点发出。

       内核代码中有这种方式的典型体现。本文涉及的相关内核代码的版本都是2.6.18.3。源文件为ipt_REJECT.c,函数send_reset用于往当前接收到数据包的源IP上发送RST包,整个函数涉及了数据包的构造和发送,这里一起做个简单分析。
  1. /* Send RST reply */
  2. static void send_reset(struct sk_buff *oldskb, int hook)
  3. {
  4.         struct sk_buff *nskb;
  5.         struct iphdr *iph = oldskb->nh.iph;
  6.         struct tcphdr _otcph, *oth, *tcph;
  7.         struct rtable *rt;
  8.         u_int16_t tmp_port;
  9.         u_int32_t tmp_addr;
  10.         int needs_ack;
  11.         int hh_len;

  12.         /* 判断是否是分片包*/
  13.         if (oldskb->nh.iph->frag_off & htons(IP_OFFSET))
  14.                 return;
  15.        
  16. /*得到TCP头部指针*/
  17.         oth = skb_header_pointer(oldskb, oldskb->nh.iph->ihl * 4,
  18.                                  sizeof(_otcph), &_otcph);
  19.         if (oth == NULL)
  20.                 return;

  21.         /* 当期收到的包就是RST包,就不用再发送RST包了*/
  22.         if (oth->rst)
  23.                 return;

  24.         /*检查数据包的校验和是否正确*/
  25.         if (nf_ip_checksum(oldskb, hook, iph->ihl * 4, IPPROTO_TCP))
  26.                 return;
  27.         /*这一步比较关键,做的就是更新路由的工作。该函数的主要工作就是将当前数据包的源IP当做路由的目的IP,同时考虑数据包的目的IP,得到去往该源IP的路由*/
  28.         if ((rt = route_reverse(oldskb, oth, hook)) == NULL)
  29.                 return;

  30.         hh_len = LL_RESERVED_SPACE(rt->u.dst.dev);

  31.         /* 拷贝当前的oldskb,包括skb结构体和数据部分。这就是我们上面提到的构造数据包的第一种方式*/
  32.         nskb = skb_copy_expand(oldskb, hh_len, skb_tailroom(oldskb),
  33.                                GFP_ATOMIC);
  34.         if (!nskb) {
  35.                 dst_release(&rt->u.dst);
  36.                 return;
  37.         }

  38.         /*因为是拷贝的oldskb,这里不需要再引用了,因此释放对该路由项的引用*/
  39.         dst_release(nskb->dst);
  40.         /*将新构造数据包引用的路由指向上面由route_reverse函数返回的新的路由项 */
  41.         nskb->dst = &rt->u.dst;

  42.         /* 清除nskb中拷贝过来的oldskb中链接跟踪相关的内容*/
  43.         nf_reset(nskb);
  44.         nskb->nfmark = 0;
  45.         skb_init_secmark(nskb);

  46.         /*以下就是构造数据包的实际数据部分。如果我们将这里不为nskb新申请缓冲区,而直接指向oldskb的缓冲区,就使我们上面提到的第二种构造数据包的方法。*/
  47.         /*获取nskb的tcp header*/
  48.         tcph = (struct tcphdr *)((u_int32_t*)nskb->nh.iph + nskb->nh.iph->ihl);

  49.         /*交换源和目的IP */
  50.         tmp_addr = nskb->nh.iph->saddr;
  51.         nskb->nh.iph->saddr = nskb->nh.iph->daddr;
  52.         nskb->nh.iph->daddr = tmp_addr;

  53.         /*交换源和目的端口 */
  54.         tmp_port = tcph->source;
  55.         tcph->source = tcph->dest;
  56.         tcph->dest = tmp_port;

  57.         /*重置TCP头部的长度,并修改IP头部中记录的数据包的总长度。因为这里是发送RST报文,只需要有TCP的头部,不需要TCP的数据部分*/
  58.         tcph->doff = sizeof(struct tcphdr)/4;
  59.         skb_trim(nskb, nskb->nh.iph->ihl*4 + sizeof(struct tcphdr));
  60.         nskb->nh.iph->tot_len = htons(nskb->len);

  61.         /*重新设置 seq, ack_seq,分两种情况(TCP/IP详解有描述)*/
  62.         if (tcph->ack) { /*原始数据包中ACK标记位置位的情况*/
  63.                 needs_ack = 0;
  64.                 tcph->seq = oth->ack_seq; /*原始数据包的ack_seq作为nskb的seq*/
  65.                 tcph->ack_seq = 0;
  66.         } else { /*原始数据包中ACK标记位没有置位的情况,初始连接SYN或者结束连接FIN等*/
  67.                 needs_ack = 1;
  68.                 /*这种情况应该是SYN或者FIN包,由于SYN和FIN包都占用1个字节的长度。因此ack_seq应该等于旧包的seq+1即可。这里之所以这样表示,可能是还存在其他情况的数据包。*/
  69.                 tcph->ack_seq = htonl(ntohl(oth->seq) + oth->syn + oth->fin
  70.                                       + oldskb->len - oldskb->nh.iph->ihl*4
  71.                                       - (oth->doff<<2));
  72.                 tcph->seq = 0;
  73.         }

  74.         /* RST标记位置1*/
  75.         ((u_int8_t *)tcph)[13] = 0;
  76.         tcph->rst = 1;
  77.         tcph->ack = needs_ack;

  78.         tcph->window = 0;
  79.         tcph->urg_ptr = 0;

  80.         /*重新计算TCP校验和*/
  81.         tcph->check = 0;
  82.         tcph->check = tcp_v4_check(tcph, sizeof(struct tcphdr),
  83.                                    nskb->nh.iph->saddr,
  84.                                    nskb->nh.iph->daddr,
  85.                                    csum_partial((char *)tcph,
  86.                                                 sizeof(struct tcphdr), 0));

  87.         /* 修改IP包的TTL,并且设置禁止分片*/
  88.         nskb->nh.iph->ttl = dst_metric(nskb->dst, RTAX_HOPLIMIT);
  89.         /* Set DF, id = 0 */
  90.         nskb->nh.iph->frag_off = htons(IP_DF);
  91.         nskb->nh.iph->id = 0;

  92.         /*重新计算IP数据包头部校验和*/
  93.         nskb->nh.iph->check = 0;
  94.         nskb->nh.iph->check = ip_fast_csum((unsigned char *)nskb->nh.iph,
  95.                                            nskb->nh.iph->ihl);

  96.         /* "Never happens" */
  97.         if (nskb->len > dst_mtu(nskb->dst))
  98.                 goto free_nskb;
  99. /*使nskb和oldskb的链接记录关联*/
  100.         nf_ct_attach(nskb, oldskb);
  101.        
  102. /*这里就是最终发送数据包的方式,具体方法就是让新数据包经过LOACLOUT的hook点,然后查路由,最后经由POSTROUTING点,将数据包发送出去。
  103. 其实这里我还是有1个疑问:(1)为什么不可以直接查找路由,而必须先经过LOCALOUT点*/
  104.         NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, nskb, NULL, nskb->dst->dev,
  105.                 dst_output);
  106.         return;

  107. free_nskb:
  108.         kfree_skb(nskb);
  109. }
复制代码

通过以上对send_reset函数的分析,应该明白了利用NF框架将构造数据包发送出去的方法。
         源码分析中提到的1个疑问,35楼给出了解释,这里引用过来:
其实,这不是丢到了高层,而是和ip_queue_xmit()发送过程意义一样。
对这包进行重新路由后,封装了头部,之后,放到了NF_IP_LOCAL_IN之前而已。

其实,这里面只要修改了中途修改了ip地址,肯定是需要手动重新路由的。
这就涉及到一些比较复杂的route cache的查找,如果没有就去查找route tables;之后,进行路由结构和neighbour结构的关联,就涉及到邻居子系统的相关操作;接着就涉及到arp cache的查找,如果没有,进行一些操作,arp的过程等等,才找到了相关的ip对应的mac信息。
息。

        方法二,就是直接调用dev_queue_xmit函数,将构造完毕的数据包直接发送到网卡驱动。从NF框架来看,该函数的调用是在POSTROUTING点之后了,也可以理解为直接通过调用二层的发送函数,将三层构造的数据包发送出去。该函数实际上会调用skb->dev->hard_start_xmit,即对应网卡的驱动函数,将数据包直接发送的出去。
        很显然,这个工作在二层的函数,发送数据包(数据包在二层的时候准确叫法应该是帧,我们这里是在三层直接调用的,权且还称作数据包)的方式是不需要再查路由了。
但是,二层发送的时候是需要根据目的MAC来进行的。在第一种方法构造的数据包中,仅仅交换了IP地址,而没有对MAC做任何修改。这样直接调用dev_queue_xmit是会产生问题的,并且该函数发送的内容应该是从二层头部开始,到数据包的结束。因此,如果三层构造的数据包,想调用该函数直接发送数据包的话,则需要修改数据包的源和目的MAC,并将skb->data指针指向MAC头部,以及skb->len的值也要加上头部的长度方法。以下是可参考的示例代码:
  1.   unsigned char mac_temp[ETH_ALEN] = {0};
  2.   struct ethhdr *mach = NULL;
  3. ……
  4. /*code…… 构造数据包的IP即上层协议及数据*/
  5. ……
  6. /*交换源和目的MAC*/
  7. mach = (struct ethhdr *)skb->mac.raw;
  8.    memcpy(mac_temp, (unsigned char *)mach->h_dest, ETH_ALEN);
  9.    memcpy(mach->h_dest, (unsigned char *)mach->h_source, ETH_ALEN);
  10.    memcpy(mach->h_source, mac_temp, ETH_ALEN);

  11. /*修改skb->data指针,使其指向MAC头部,并且增加skb->len*/
  12. skb_push(skb , ETH_HLEN);
  13. /*直接调用该函数,将数据包从网卡上发送出去*/
  14. ret = dev_queue_xmit(skb);
复制代码

    这里还要顺便说一下构造的数据包发送完毕之后,对于hook函数的返回值问题。
   (1)第一种发送数据包的实现,对于send_reset函数的实现中,由于单独申请了nskb的内存,并构造的新的数据包。新数据包接着走NF的流程了。而对于原始的skb,就通过模块的返回值return NF_DROP做出了处理。
   (2)第二种发送数据包的实现,若是基于已有数据包的基础上重新构造的数据包,那么实际上原始数据包的内容已经不复存在,而且调用完毕dev_queue_xmit已将同一块缓冲区,只是填充了新数据的数据包发送出去,因此,这里已经没有原始数据包的存在了,需要返回NF_STOLEN,告诉协议栈不用关心原始的包即可。否则,若是新数据包是单独申请的内存,那么对于原数据包还应该是返回NF_DROP.


三、总结
        以上就是个人分享和总结和内核中构造的数据包发送出去的两种方式。实际中常用的就是构造完数据包之后,调用dev_queue_xmit函数发送报文,也测试过调用send_reset发送RST方式。但并未采用send_reset中通过调用NF_HOOK发送过其他数据包。如果诸位朋友有相关的实践经验,欢迎分享。

        本文在分析send_reset代码的过程中,参考了百度中搜到的muddoghole的文章,因为只能从百度快照看到这篇文章,并且链接过长,这里就不列出连接,对于原文的作者表示感谢。

        由于对内核中的一些地方理解不够深入,因此文章中肯定存在很多问题。欢迎各位朋友指正,多多交流。

        本文原文链接:http://blog.chinaunix.net/u/33048/showart_2043789.html

[ 本帖最后由 Godbach 于 2009-9-5 11:18 编辑 ]

评分

参与人数 1可用积分 +30 收起 理由
platinum + 30 很好的实做例程!

查看全部评分

论坛徽章:
0
发表于 2009-09-02 16:19 |显示全部楼层
好文!

论坛徽章:
36
IT运维版块每日发帖之星
日期:2016-04-10 06:20:00IT运维版块每日发帖之星
日期:2016-04-16 06:20:0015-16赛季CBA联赛之广东
日期:2016-04-16 19:59:32IT运维版块每日发帖之星
日期:2016-04-18 06:20:00IT运维版块每日发帖之星
日期:2016-04-19 06:20:00每日论坛发贴之星
日期:2016-04-19 06:20:00IT运维版块每日发帖之星
日期:2016-04-25 06:20:00IT运维版块每日发帖之星
日期:2016-05-06 06:20:00IT运维版块每日发帖之星
日期:2016-05-08 06:20:00IT运维版块每日发帖之星
日期:2016-05-13 06:20:00IT运维版块每日发帖之星
日期:2016-05-28 06:20:00每日论坛发贴之星
日期:2016-05-28 06:20:00
发表于 2009-09-02 16:23 |显示全部楼层
多谢白金兄啊。过奖了。

其实小弟对于send_reset还是疑问的:就是修改后的数据包,为什么还要走LOCAL_OUT点?

论坛徽章:
0
发表于 2009-09-02 16:32 |显示全部楼层
我的理解是 send_reset 伪造一个包然后通过正常本地协议栈,本地 socket 发出的
所以要经过 LOCAL_OUT 链
不知道分析的对不对,还请指正

论坛徽章:
36
IT运维版块每日发帖之星
日期:2016-04-10 06:20:00IT运维版块每日发帖之星
日期:2016-04-16 06:20:0015-16赛季CBA联赛之广东
日期:2016-04-16 19:59:32IT运维版块每日发帖之星
日期:2016-04-18 06:20:00IT运维版块每日发帖之星
日期:2016-04-19 06:20:00每日论坛发贴之星
日期:2016-04-19 06:20:00IT运维版块每日发帖之星
日期:2016-04-25 06:20:00IT运维版块每日发帖之星
日期:2016-05-06 06:20:00IT运维版块每日发帖之星
日期:2016-05-08 06:20:00IT运维版块每日发帖之星
日期:2016-05-13 06:20:00IT运维版块每日发帖之星
日期:2016-05-28 06:20:00每日论坛发贴之星
日期:2016-05-28 06:20:00
发表于 2009-09-02 16:39 |显示全部楼层
原帖由 platinum 于 2009-9-2 16:32 发表
我的理解是 send_reset 伪造一个包然后通过正常本地协议栈,本地 socket 发出的
所以要经过 LOCAL_OUT 链
不知道分析的对不对,还请指正


当时看了代码,我觉得可能是这样理解的。但是从包的流程上看,如果不走hook点,而直接查路由发送到POSTROUTING,不知道会不会有什么影响?

论坛徽章:
36
IT运维版块每日发帖之星
日期:2016-04-10 06:20:00IT运维版块每日发帖之星
日期:2016-04-16 06:20:0015-16赛季CBA联赛之广东
日期:2016-04-16 19:59:32IT运维版块每日发帖之星
日期:2016-04-18 06:20:00IT运维版块每日发帖之星
日期:2016-04-19 06:20:00每日论坛发贴之星
日期:2016-04-19 06:20:00IT运维版块每日发帖之星
日期:2016-04-25 06:20:00IT运维版块每日发帖之星
日期:2016-05-06 06:20:00IT运维版块每日发帖之星
日期:2016-05-08 06:20:00IT运维版块每日发帖之星
日期:2016-05-13 06:20:00IT运维版块每日发帖之星
日期:2016-05-28 06:20:00每日论坛发贴之星
日期:2016-05-28 06:20:00
发表于 2009-09-02 16:43 |显示全部楼层
难道是考虑链接跟踪的问题或者是nat?

论坛徽章:
0
发表于 2009-09-02 16:55 |显示全部楼层
REJECT 的设计初衷就是用自己的本地 socket 发一个 RST 包,所以用的是 LOCAL_OUT,这和 NAT 无关
也就是说
假如 REJECT 的机器是做服务器的,直接通过被访问端口发送数据
如果 REJECT 的机器是做 NAT 的,那么通过连接公网的接口发送数据
如果 REJECT 的机器是个桥,那么对方一定无法收到 REJECT 了,可以做个测试验证我的说法

论坛徽章:
36
IT运维版块每日发帖之星
日期:2016-04-10 06:20:00IT运维版块每日发帖之星
日期:2016-04-16 06:20:0015-16赛季CBA联赛之广东
日期:2016-04-16 19:59:32IT运维版块每日发帖之星
日期:2016-04-18 06:20:00IT运维版块每日发帖之星
日期:2016-04-19 06:20:00每日论坛发贴之星
日期:2016-04-19 06:20:00IT运维版块每日发帖之星
日期:2016-04-25 06:20:00IT运维版块每日发帖之星
日期:2016-05-06 06:20:00IT运维版块每日发帖之星
日期:2016-05-08 06:20:00IT运维版块每日发帖之星
日期:2016-05-13 06:20:00IT运维版块每日发帖之星
日期:2016-05-28 06:20:00每日论坛发贴之星
日期:2016-05-28 06:20:00
发表于 2009-09-02 16:56 |显示全部楼层
原帖由 platinum 于 2009-9-2 16:55 发表
REJECT 的设计初衷就是用自己的本地 socket 发一个 RST 包,所以用的是 LOCAL_OUT,这和 NAT 无关
也就是说
假如 REJECT 的机器是做服务器的,直接通过被访问端口发送数据
如果 REJECT 的机器是做 NAT 的,那 ...


你的意思是说,如果在桥模式下使用REJECT,RST包可能发布出去。

论坛徽章:
36
IT运维版块每日发帖之星
日期:2016-04-10 06:20:00IT运维版块每日发帖之星
日期:2016-04-16 06:20:0015-16赛季CBA联赛之广东
日期:2016-04-16 19:59:32IT运维版块每日发帖之星
日期:2016-04-18 06:20:00IT运维版块每日发帖之星
日期:2016-04-19 06:20:00每日论坛发贴之星
日期:2016-04-19 06:20:00IT运维版块每日发帖之星
日期:2016-04-25 06:20:00IT运维版块每日发帖之星
日期:2016-05-06 06:20:00IT运维版块每日发帖之星
日期:2016-05-08 06:20:00IT运维版块每日发帖之星
日期:2016-05-13 06:20:00IT运维版块每日发帖之星
日期:2016-05-28 06:20:00每日论坛发贴之星
日期:2016-05-28 06:20:00
发表于 2009-09-02 17:01 |显示全部楼层
那如果我想做比较通用的发送RST包。修改完毕之后,还向让顺着NF框架进行,白金兄觉得该怎么实现呢?

论坛徽章:
6
金牛座
日期:2013-10-08 10:19:10技术图书徽章
日期:2013-10-14 16:24:09CU十二周年纪念徽章
日期:2013-10-24 15:41:34狮子座
日期:2013-11-24 19:26:19未羊
日期:2014-01-23 15:50:002015年亚洲杯之阿联酋
日期:2015-05-09 14:36:15
发表于 2009-09-02 17:14 |显示全部楼层

回复 #1 Godbach 的帖子

分析的非常好,小弟收藏了
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP