(十五)洞悉linux下的Netfilter&iptables:开发自己的hook函数【实战】

向Netfilter中注册自己的hook函数

        数据包在协议栈中传递时会经过不同的HOOK点,而每个HOOK点上又被Netfilter预先注册了一系列hook回调函数,当每个清纯的数据包到达这些点后会被这些可恶hook函数轮番调戏一番。有时候我们就在想,只让系统自带的这些恶棍来快活,我自己能不能也make一个hook出来和它们同流合污呢?答案是肯定的。

       我们来回顾一下目前系统中已经注册了的hook函数可分为以下几类:

       它们在协议栈中位置如下:

       首先我们心里要非常清楚的知道我们将要开发的这个hook函数位于哪个HOOK点的什么级别,它的前后分别是哪些函数,这一点很重要,因为遇到问题时至少心里有个谱。

       我们今天讲的这个hook函数功能很简单,主要是向大家展示开发流程和方法。细节性的东西还需要每个人日积月累的修炼才行。

 

        要注册一个hook函数需要用到nf_register_hook()或者nf_register_hooks()系统API和一个struct nf_hook_ops{}类型的结构体对象。最简单的hook函数如下:

#include

#include

#include

#include

#include

#include

#include

#include

#include

 

MODULE_LICENSE("GPL");

MODULE_AUTHOR("koorey KING");

MODULE_DESCRIPTION("My hook test");

 

static int pktcnt = 0;

//我们自己定义的hook回调函数,丢弃每第5×n(n=1,2,3,4…)个ICMP报文。

static unsigned int myhook_func(unsigned int hooknum, struct sk_buff **skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *))

{

   const struct iphdr *iph = (*skb)->nh.iph;

   if(iph->protocol == 1){

      atomic_inc(&pktcnt);

      if(pktcnt%5 == 0){

           printk(KERN_INFO "%d: drop an ICMP pkt to %u.%u.%u.%u !\n", pktcnt,NIPQUAD(iph->daddr));

           return NF_DROP;

      }

   }

   return NF_ACCEPT;

}

 

static struct nf_hook_ops nfho={

        .hook           = myhook_func,  //我们自己的hook回调处理函数

        .owner          = THIS_MODULE,

        .pf             = PF_INET,

        .hooknum        = NF_IP_LOCAL_OUT, //挂载在本地出口处

        .priority       = NF_IP_PRI_FIRST,  //优先级最高

};

 

static int __init myhook_init(void)

{

    return nf_register_hook(&nfho);

}

 

static void __exit myhook_fini(void)

{

    nf_unregister_hook(&nfho);

}

 

module_init(myhook_init);

module_exit(myhook_fini);

 

       我们在LOCAL_OUT这个HOOK点上,以最高优先级NF_IP_PRI_FIRST注册了一个名为myhook_func()的函数。从本机发出的所有数据包从协议栈进入Netfilter框架时,最先都会被该函数所看到,然后我们在这里就可以“胡作非为”了。

       这个模块最后会被编译成名为myhook.ko的驱动模块,然后用insmod来将其加载。具体操作流程如下:

       可以看到,我们自己的hook函数已经成功run起来了。我们可能不仅局限于做这么简单一个hook,没什么意义,也没啥成就感。况且这种hook压根儿就没有存在的价值,因为我们完全可以通过iptables来配置相应的规则而达到同样的目的。

       OK,那我们就改造一下刚写的这个hook。让它实现的功能是:每收到5个ICMP报文就向指定的IP地址发送一个UDP报文。由于这个功能的开发牵扯到内核协议栈编程,关于协议栈部分打算在以后的系列博文中详细阐述。这里仅做个简单的普及入门就可以了。

      我们要实现的功能是从内核中发一个报文,这完全不同于之前在用户层通过socket套接字编程的模式。

Godbach兄的文章http://blog.chinaunix.net/u/33048/showart_2043789.html,以及内核版的精华帖《教你修改以及重构skb》都是非常经典的参考文章。不太明白的童鞋可以去拜读一下:http://linux.chinaunix.net/bbs/thread-1152885-1-4.html

      再重申一下我们的目标,每收到5个ICMP报文就向指定IP(例如118.6.24.132)发送一个UDP报文。这里我是在内核里自己去DIY一个新的skb出来,构造数据包和发送数据包的过程如下:

#define    ETH    "eth0"  //接口名称

#define    SIP     "192.168.6.130" //接口的IP地址

#define    DIP     "118.6.24.132//要发送UDP报文的目的IP地址

#define    SPORT   39804   //源端口

#define    DPORT   6980    //目的端口

unsigned char SMAC[ETH_ALEN] = {0x00,0x0C,0x29,0x33,0x2C,0x3C}; //eth0网卡地址

unsigned char DMAC[ETH_ALEN] = {0x00,0x50,0x56,0xF4,0x8B,0xB3}; //默认网关的网卡地址

 

static int build_and_xmit_udp(char * eth, u_char * smac, u_char * dmac,

             u_char * pkt, int pkt_len,u_long sip, u_long dip,

             u_short sport, u_short dport)

{

  struct sk_buff * skb = NULL;

  struct net_device * dev = NULL;

  struct ethhdr * ethdr = NULL;

  struct iphdr * iph = NULL;

  struct udphdr * udph = NULL;

  u_char * pdata = NULL;

 

  if(NULL == smac || NULL == dmac)

      goto out;

 

  if(NULL == (dev= dev_get_by_name(eth)))

        goto out;

  //通过alloc_skb()来为一个新的skb申请内存结构

  skb = alloc_skb(pkt_len + sizeof(struct iphdr) + sizeof(struct udphdr) + LL_RESERVED_SPACE(dev), GFP_ATOMIC);

 

  if(NULL == skb)

      goto out;

  skb_reserve(skb, LL_RESERVED_SPACE(dev));

 

  skb->dev = dev;

  skb->pkt_type = PACKET_OTHERHOST;

  skb->protocol = __constant_htons(ETH_P_IP);

  skb->ip_summed = CHECKSUM_NONE;

  skb->priority = 0;

 

  skb->nh.iph = (struct iphdr*)skb_put(skb, sizeof(struct iphdr));

  skb->h.uh = (struct udphdr*)skb_put(skb, sizeof(struct udphdr));

 

  pdata = skb_put(skb, pkt_len); //预留给上层用于数据填充的接口

  {

     if(NULL != pkt)

        memcpy(pdata, pkt, pkt_len);

  }

 

  //“从上往下”填充skb结构,依次是UDP--IP--MAC

  udph = (struct udphdr *)skb->h.uh;

  memset(udph, 0, sizeof(struct udphdr));

  udph->source = sport;

  udph->dest = dport;

  skb->csum = 0;

  udph->len = htons(sizeof(struct udphdr)+pkt_len);

  udph->check = 0;

  //填充IP

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

  iph->version = 4;

  iph->ihl = sizeof(struct iphdr)>>2;

  iph->frag_off = 0;

  iph->protocol = IPPROTO_UDP;

  iph->tos = 0;

  iph->daddr = dip;

  iph->saddr = sip;

  iph->ttl = 0x40;

  iph->tot_len = __constant_htons(skb->len);

  iph->check = 0;

  iph->check = ip_fast_csum((unsigned char *)iph,iph->ihl);

 

  skb->csum = skb_checksum(skb, iph->ihl*4, skb->len - iph->ihl * 4, 0);

  udph->check = csum_tcpudp_magic(sip, dip, skb->len - iph->ihl * 4, IPPROTO_UDP, skb->csum);

  //填充MAC

  skb->mac.raw = skb_push(skb, 14);

  ethdr = (struct ethhdr *)skb->mac.raw;

  memcpy(ethdr->h_dest, dmac, ETH_ALEN);

  memcpy(ethdr->h_source, smac, ETH_ALEN);

  ethdr->h_proto = __constant_htons(ETH_P_IP);

  //调用dev_queue_xmit()发送报文

  if(0 > dev_queue_xmit(skb))

      goto out;

 

out:

   if(NULL != skb)

   {

        dev_put (dev);

        kfree_skb (skb);

   }

   return(NF_ACCEPT);

}

       上面这部分代码看不懂没关系,因为它需要比较熟练的内核协议栈编程知识,大家可以从整体上对其有个感性的把握就可以了。后面如果有时间我会再写个TCP/IP内核协议栈分析的系列文章,虽然CU上有很多大牛已经在写了,但每个人的收获不一样,和大家分享也是学习的另一种形式。好了,闲话不多说。我们这个hook的最终版本在“ myhook.zip ”下载。

       接下来,激动人心的时刻又到了,我们来验证一下我们的hook函数是否可以按预期一样地进行工作。编译和加载流程如前面所述。我们为上层应用层往UDP报文中填充数据预留了接口,所以我们可以以如下的形式来调用build_and_xmit_udp()接口:

build_and_xmit_udp(ETH,SMAC,DMAC,”hello”,5,in_aton(SIP),in_aton(DIP),htons(SPORT),htons(DPORT));

       通过wireshark抓包来验证一下是不是每收到5个ICMP报文就往118.6.24.132地址发送一个内容仅有“hello”字符串的UDP报文:

 

       经过这么一番“改造”,我们自定义这个hook函数算是有点特色了。至此我们今天的内容就全部讲完了。估计有些人可能觉得还少了点什么,有没有悟性比较高的童鞋提出几点质疑来?没错,就是我们这个hook里设置的IP地址是固定的,包括MAC地址、源和目的端口以及发送的内容。用户空间我们根本没法对这些属性进行操作,骤然间,这个模块的可操作性和易用性大打折扣。那么我们到底如何才能从用户空间来操作这个新注册的hook呢?

       未完,待续…

 

从用户空间来操作内核中Netfilter框架里自定义的HOOK函数

         本文承上一篇博客。主要是和大家探讨一下如何从用户空间操作我已经注册到Netfilter中的自定义hook函数。有些童鞋可能就纳闷,难道iptables不能操作到么?如果我们需要让iptables操作我们在Netfilter框架中做过的扩展,那么最有效最直接的办法就是开发一个match或者target。但我们现在注册的这个hook很明显和iptables命令行工具没多大关系,你要让iptables来管理这不是强人所难么。当然如果你一要这么干的话,办法肯定是有的,但者不属于本文所要讨论的范畴。

 

         说到内核空间与用户空间的通信,完全可以作为一个专题来讲,以后有机会把这方面总结一下写出来和大家分享。今天我们要用的方法就是借鉴了iptables和内核的通信方式,即采用getsockopt/setsockopt来实现用户空间和内核空间的交互。

 

         还是继续上一篇的练习代码,我们在它的基础上继续修改润色。和注册hook函数时的操作非常类似,我们首先要实例化一个struct nf_sockopt_ops{}结构体对象,然后用Linux提供的nf_register_sockopt()函数来将该对象注册到全局双向链表nf_sockopts中去,当我们在用户空间调用g(s)etsockopt时经过层层系统调用,最后就会在nf_sockopts链表中找到我们已经注册的响应函数。关于g(s)etsockopt的执行流程,感兴趣的童鞋可以回头看一下博文十二里的详细讲解。罗嗦的这么多,大家都耐烦了吧。OK,我们赶紧动手。

//添加必要的头文件

#include

#include

#include

 

//增加我们自定义的扩充命令字

#define SOCKET_OPT_BASE         128

#define SOCKET_OPT_SETTARGET    (SOCKET_OPT_BASE)

#define SOCKET_OPT_GETTARGET    (SOCKET_OPT_BASE)

#define SOCKET_OPT_MAX          (SOCKET_OPT_BASE +1)

 

#define    ETH    "eth0"  //interface name

#define    SIP     "192.168.6.130"

//#define    DIP     "118.6.24.132" //把原来这个注释掉,现在看它越看越不顺眼。

#define ADDRLEN 16  //IP地址的长度16字节,格式一般为xxx.xxx.xxx.xxx\0

static char dstIP[ADDRLEN]={0}; //这个就是我们要操作的目的IP。

 

//为了醒目,我将两个接口分别在不同的函数中来实现。

static int recv_cmd(struct sock *sk,int cmd, void __user *user,unsigned int len)

{

        int ret = 0;

        if(cmd == SOCKET_OPT_SETTARGET)

        {

                memset(dstIP,0,ADDRLEN);

                if(0!=(ret = copy_from_user(dstIP,user,len))

                {

                      printk("error: can not copy data from userspace\n");

                      return -1;

               }

                printk("The target IP from User: %s \n",dstIP);

        }

        return ret;

}

 

 

static int send_cmd(struct sock *sk,int cmd, void __user *user,int *len)

{

        int ret = 0;

        if(cmd == SOCKET_OPT_GETTARGET)

        {

                if(0!=(ret = copy_to_user(user,dstIP,ADDRLEN)))

                {

                      printk("error: can not copy data to userspace\n");

                      return -1;

                }

                printk("The target IP to User: %s \n",dstIP);

        }

        return ret;

}

 

static struct nf_sockopt_ops my_sockops = {

        .pf = PF_INET,

        .set_optmin = SOCKET_OPT_SETTARGET,

        .set_optmax = SOCKET_OPT_MAX,

        .set = recv_cmd,

        .get_optmin = SOCKET_OPT_GETTARGET,

        .get_optmax = SOCKET_OPT_MAX,

        .get = send_cmd

};

static int index=1;

static unsigned int hook_func(unsigned int hooknum, struct sk_buff **skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *))

{

        const struct iphdr *iph = (*skb)->nh.iph;

        int ret = NF_ACCEPT;

 

        if(iph->protocol == 1){

           atomic_inc(&pktcnt);

           if(pktcnt%5 == 0)

          {

    //简单校验一下IP地址的合法性

    if(strcmp(dstIP,””) !=0 && strcmp(dstIP,”0.0.0.0”)!=0)

    {

                                   printk(KERN_INFO "Sending the %d udp pkt !\n",index++);

                                  ret = build_and_xmit_udp(ETH,SMAC,DMAC,"hello",5,

                                                                           in_aton(SIP),in_aton(dstIP),

                                                                         htons(SPORT),htons(DPORT));

}else{

   printk(“Have %d tims:target IP illegal.Nothing to do!\n”,pktcnt/5);

}

           }

        }

        return ret;

}

static int __init myhook_init(void)

{

    nf_register_sockopt(&my_sockops); //注册我们的sockops对象

    return nf_register_hook(&nfho);

}

 

static void __exit myhook_fini(void)

{

    nf_unregister_hook(&nfho);

    nf_unregister_sockopt(&my_sockops); //注销我们的sockops对象

}

         以上代码的着色部分,需要大家格外留意。因为是示例,所以合法性校验及错误处理的几个地方就是简单象征性地照顾了一下。内核部分的改动就弄完了,接下来我们继续写用户空间的代码usermyhook.c,如下:

#include

#include

#include

#include

#include

#include

 

#define SOCKET_OPS_BASE         128

#define SOCKET_OPT_SETTARGET    (SOCKET_OPS_BASE)

#define SOCKET_OPT_GETTARGET    (SOCKET_OPS_BASE)

#define SOCKET_OPT_MAX          (SOCKET_OPS_BASE +1)

 

int main(int argc,char** argv)

{

    int sockfd,len,ret;

    char targetIP[16]={0};

 

    if(0>(sockfd = socket(AF_INET,SOCK_RAW,IPPROTO_RAW)))

    {

        printf("can not create a socket\n");

        return -1;

    }

 

    //仅为示范而生。未作输入合法性和参数合法性校验。

    if('s' == *argv[1])

{

    len = strlen(argv[2])+1;

        ret = setsockopt(sockfd,IPPROTO_IP,SOCKET_OPT_SETTARGET,argv[2],len);

        if(0 != ret)

        { 

              printf("setsockopt error: - %d : %s\n",errno,strerror(errno));

             return -1;

        }

        printf("setsockopt: ret=%d, wanted IP=%s\n",ret,argv[2]);

    }else{

        len = sizeof(char)*16;

        ret = getsockopt(sockfd,IPPROTO_IP,SOCKET_OPT_GETTARGET,targetIP,&len);

        if(0 != ret)

        {   

              printf("getsockopt error: - %d : %s\n",errno,strerror(errno));

               return -1;

        }

        printf("getsockopt: ret=%d,gotten IP=%s\n",ret,targetIP);

     }

     close(sockfd);

     return 0;

}

         将该文件编译:gcc -o umhook usermyhook.c

         把重新编译出来的myhook.ko模块加入内核,然后用我们编译出来的umhook工具来动态指定我们要往哪个IP地址发送UDP报文。该工具的用法:

         ./umhook “s” “182.134.150.6” //设置目的IP

  ./umhook “g” //获取目的IP

 

验证过程和结果如下:

当我们的myhook.ko模块刚加载时内核中的目的IP地址dstIP={0},所以在探测到第五个ICMP报文时并没有发送UDP报文;紧接着,我们用./umhook “s” “123.4.5.6”设置目的IP地址为“123.4.5.6”之后,内核探测到这次改变,打印出“The target IP from User:123.4.5.6”的提示信息;然后,在第10个ICMP报文被探测到后发送了第一条UDP到我们所配置的目的地址,抓包工具也有证实;之后,我们又将目的IP改为“123.4.5.7”,内核打印:“The target IP from User:123.4.5.7”在第15,20个ICMP报文被探测到后又发了两条UDP报文到新IP地址;最后,用./umhook “s” “”命令将目的IP清除掉。整个过程十分流程自然,而我们的心情也无比的愉悦。 

 

后记:

整个Netfilter系列从清明节开始陆陆续续一直写到端午前夕,也算是对自己有个交代了,另外也总结出来和大家分享一下自己的心得和收获。看到网上经常有人问学习Netfilter有什么好资料或教程,其实个人觉得,Netfilter是无缝嵌入到协议栈里的。如果你想了解它的基本原理那么就需要一点协议栈知识就足够,如果你想为它做开发,那么在掌握了内核编程的基础上,还需要对协议栈的实现有相当深厚的底蕴才可以。由于本人也是刚接触Netfilter不久,学识浅薄,分析难免有所疏漏的地方的还请各位高手和大侠为小弟指正。

完。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值