利用netfilter截获数据包基础知识及实现

本着记录自己疯狂过的青春,对netfilter的应用学习做点记录。

0x00前提

双网卡硬件设备作中间件,一个网卡接收向另一网卡转发时进行操作数据包。下文中举例eth0为进入,eth1为发出,相对而言。本次开发利用netfilter框架进行完整开发,基本实现对数据包的任意操作。

0x01 什么是netfilter

Netfilter是linux2.4.x引入的一个子系统,作为一个通用的、抽象的框架,提供了一整套的Hook函数管理机制,使得诸如数据包过滤,网络地址转换,和基于协议类型的连接跟踪成为可能。通俗点说,netfilter提供了一系列的接口,将一个到达本机的数据包或者经本机转发的数据包流程中添加了一些可供用户操作的点,这些点被称为HOOK点。

netfilter共有5个hook点,分别为:

【1】NF_IP_PRE_ROUTING:刚刚进入网络层,还未进行路由查找的包,通过此处。

【2】NF_IP_POST_ROUTING:进入网络层已经经过路由查找,确定转发,将要离开本设备的包,通过此处。

【3】NF_IP_LOCAL_IN:通过路由查找,确定发往本机的包,通过此处。

【4】NF_IP_LOCAL_OUT:从本机进程刚发出的包,通过此处。

【5】NF_IP_FORWARD:经路由查找后,要转发的包,在POST_ROUTING之前。

在这次实践应用中主要完成将本机接收到、发送的所有数据包(包括TCP、UDP、ICMP协议)拦截,执行想要执行的操作后放行(诸如加密、修改信息、伪造信息等调戏行为),本机开发时用到的HOOK点为NF_IP_LOCAL_OUT和NF_IP_LOCAL_IN,但在ARM中时,由于充当一个路由的功能,要将eth0接收到的包转发出去,将修改HOOK点为NF_IP_PRE_ROUTING,对接受的数据包路由之前进行所有操作,反过来eth1接收的也相同。

0x02开发前置知识

1.hook函数注册与解注册

在使用netfilter框架进行你想要的操作时,你需要先向内核注册自己的钩子函数,要告诉你的内核现在我要进行处理了,那么这个钩子函数包括些什么信息呢?

static struct nf_hook_ops nfho = {  
    .hook = my_func,  
    .pf = PF_INET,  
    .hooknum =NF_INET_LOCAL_IN ,  
    .priority = NF_IP_PRI_FIRST,  
    .owner = THIS_MODULE,  
}; 

上面的例子中定义了一个nf_hook_ops类型的结构体nfho,它包含的结构体成员有:

    .hook;表示你所定义的hook函数名称。

    .pf;协议簇,示例中PF_INET表示IPv4。

    .hooknum;表示这个钩子在数据包处理流程中的位置,示例中为经路由选择确认本机接受,进入本机进程前。

    .prioity;你所注册的钩子函数优先级,示例中NF_IP_PRI_FIRST表示优先级最高。

    .owner;表示所属的模块,示例中属于此模块。

这样一个钩子函数就定义好了,只需要在模块载入时,对他进行注册,如果失败,打印内核级别错误信息:

static int __init http_init(void)  
{  
	if (nf_register_hook(&nfho)) 
	{  
		printk(KERN_ERR"nf_register_hook() failed\n");  
		return -1;  
	}  
return 0;  
}

同理,在模块退出时,要对所注册的钩子函数进行解注册:

static void __exit http_exit(void)  
{  
	nf_unregister_hook(&nfho);
}  

关于模块的加载与退出,可以参考内核编程hello word!,简单易懂。

2.什么是skb

要在内核中对到达设备的数据包进行操作,就必须先了解:

(1)数据包到达设备后被存放在哪里?

(2)如何一层一层除去数据包的包头结构,得到数据部分?

(3)处理之后可以直接发送吗?

(4)如果不能,需要再进行什么操作呢?

先看问题(1),这就需要我们了解skb的基本结构以及------一系列接下来将要用到的结构体。

skb的结构体源码就不放了,一搜一大把,主要对我们需要用到的几部分简单介绍:

union {  
                 struct tcphdr   *th;  
                 struct udphdr   *uh;  
                 struct icmphdr  *icmph;  
                 struct igmphdr  *igmph;  
                 struct iphdr    *ipiph;  
                 struct ipv6hdr  *ipv6h;  
                 unsigned char   *raw;  
         } h;  

这里的union h;表示一个完整数据包的各种头,我们常用到的iphdr,tcphdr,udphdr,icmpdr;

对iphdr的常用成员做简单介绍:

    ipiph->ihl;IP头部的长度,表示有32位长度多少个,在计算ip头部的长度时,通常用ipiph->ihl*4。

    ipiph->tot_len;整个ip报文的长度,减去ip头部的长度,就是ip包的数据部分了,在使用过程中需要注意网络字节序的问题。

    ipiph->ttl;生存时间,规定了一个数据包最多可以经过的路由器数目,当这个值被减小为0时,这个包将被丢弃,防止无限循环下去。

    ipiph->protocol;协议类型,表示是哪个协议传来的数据包,这里的协议是指传输层的协议TCP等等。

    ipiph->check;ip头部的校验值,只对头部进行检验,而不对其中的数据进行校验。

    ipiph->saddr;IP数据包的源IP地址。

    ipiph->daddr;IP数据包的目的IP地址。

tcphdr的常用成员简单介绍:

     th->source;表示该数据包的源端口号。

     th->dest;表示该数据包的目的端口号。

    th->doff;TCP头部的长度,表示共包含了多少个32位的数据,所以要计算tcp头部的长度时需要th->doff*4。

    th->check;TCP头部的校验和,当我们对整个报文的数据部分做了加密或其他处理时,需要重新计算校验和。

udphdr和icmphdr中的头部长度可以直接使用sizeof(sturct udphdr/icmphdr)来计算。

skb->len与skb->data_len:

    skb->len;表示skb->data指针所指的数据区域的长度,它包括:线性区域的数据长度、非线性数据区域的数据长度。

    skb->data_len;表示非线性数据区域的数据长度。

我们在打印数据信息时,会经常不能打印完全,缺了一部分,这就是因为data_len部分不为0,data所指的数据区域打印到线性数据完毕后,便停止了,这时我们可以使用一个函数,将skb的非线性部分转换为线性部分,重新打印,这样就可以打印出所有的数据部分了,代码示例:

if(skb->data_len!=0)
{			
	if(skb_linearize(skb))
	{
		printk("error line skb\r\n");
		printk("skb->data_len %d\r\n",skb->data_len);
		return NF_DROP;
				
	}
			
}

表示当我们的非线性数据不为0时,使用函数skb->linearize转换,可以将skb的数据尾部指针向后推data_len个长度,将data_len部分也连道data指针所指的尾部。

3.重新计算校验和

当我们对数据区的数据进行操作之后,不可以直接发送原来的数据包,还需要对校验字段进行修改,修改的校验字段主要有:skb的校验字段,iph的校验字段,和传输层(TCP、UDP、ICMP)的校验字段。

以tcp为例计算校验和:

iph->check=0;
iph->check=ip_fast_csum((unsigned char*)iph, iph->ihl);
if(skb->ip_summed == CHECKSUM_HW)
{
    			
        tcph->check=csum_tcpudp_magic(iph->saddr,iph->daddr,(ntohs(iph ->tot_len)-iph->ihl*4), IPPROTO_TCP,csum_partial(tcph,(ntohs(iph ->tot_len)-iph->ihl*4),0));	
	skb->csum = offsetof(struct tcphdr,check);		
}

重新计算校验和后的数据包就可以继续在网络中传输了。

0x03开发过程

基本思路为:首先判断协议;

                  根据每个协议的特点,计算数据部分位置和数据部分长度;    

                  对数据部分做想进行的处理,加密解密等等;

                  重新计算校验和,返回处理结果。

在函数返回值时,有下列五种:

NF_DROP;丢弃该数据包,一般可用作过滤手段。

NF_ACCEPT;接受该数据包。

NF_STOLEN;*-*没有用过,也没有理解具体的意义。

NF_QUEUE;用户队列中排队,在学习netlink将内核的数据包发送给用户态时会用到。

NF_REPEAT;重新调用该Hook函数。

下面以TCP报文为例:

if(likely(iph->protocol==IPPROTO_TCP))
{
	tcph=tcp_hdr(skb);
	data=skb->data+iph->ihl*4+tcph->doff*4;
	header=iph->ihl*4+tcph->doff*4;
	length=skb->len-iph->ihl*4-tcph->doff*4;
	if(skb->len-header>0)
	{
		printk("**************now_start_in_data*****************\n");
		printk("header length is %d",header);
		printk("\r\n");
		printk("len-header is %d",skb->len-header);
		printk("\r\n");
		printk("data length is %d",length);
		printk("\r\n");
		if(skb->data_len!=0)
		{			
			if(skb_linearize(skb))
			{
				printk("error line skb\r\n");
				printk("skb->data_len %d\r\n",skb->data_len);
				return NF_DROP;
			
			}
			
		}
		for(i=0;i<length;i++)
		{
			printk("%c",data[i]);			
		}
		iph->check=0;
		iph->check=ip_fast_csum((unsigned char*)iph, iph->ihl);
		if(skb->ip_summed == CHECKSUM_HW)
		{
    			
			tcph->check=csum_tcpudp_magic(iph->saddr,iph->daddr,(ntohs(iph ->tot_len)-iph->ihl*4), IPPROTO_TCP,csum_partial(tcph,(ntohs(iph ->tot_len)-iph->ihl*4),0));	
			skb->csum = offsetof(struct tcphdr,check);		
		}
	}
}
return NF_ACCEPT;

对一个完整的tcp数据包截获在发送就完成了!(*-*转发还不会)
 

0x04完整代码

#include <linux/time.h> 
#include <linux/init.h>  
#include <linux/netfilter.h>
#include <linux/module.h>
#include <linux/skbuff.h>
#include <linux/netdevice.h>  
#include <linux/netfilter_ipv4.h>
#include <linux/net.h>
#include <net/ip.h>
#include <linux/if_ether.h>
#include <net/protocol.h>
#include <net/icmp.h>
#include <net/tcp.h>
#include <linux/if_vlan.h>
#define CHECKSUM_HW 1
unsigned int my_func(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=ip_hdr(skb);
	struct tcphdr *tcph;
	struct udphdr *udph;
	struct icmphdr *icmph;
	struct net_device	*master;
	int i=0,ret=-1;
	int header=0;
	int index=0;
	unsigned char *data=NULL;
	int length=0;
	if(likely(iph->protocol==IPPROTO_TCP))
	{
		tcph=tcp_hdr(skb);
		data=skb->data+iph->ihl*4+tcph->doff*4;
		header=iph->ihl*4+tcph->doff*4;
		length=skb->len-iph->ihl*4-tcph->doff*4;
		if(skb->len-header>0)
		{
			printk("**************now_start_in_data*****************\n");
			printk("header length is %d",header);
			printk("\r\n");
			printk("len-header is %d",skb->len-header);
			printk("\r\n");
			printk("data length is %d",length);
			printk("\r\n");
			if(skb->data_len!=0)
			{			
				if(skb_linearize(skb))
				{
					printk("error line skb\r\n");
					printk("skb->data_len %d\r\n",skb->data_len);
					return NF_DROP;
				
				}
			
			}
			for(i=0;i<length;i++)
			{
				printk("%c",data[i]);			
			}
			iph->check=0;
			iph->check=ip_fast_csum((unsigned char*)iph, iph->ihl);
			if(skb->ip_summed == CHECKSUM_HW)
			{
    			
				tcph->check=csum_tcpudp_magic(iph->saddr,iph->daddr,(ntohs(iph ->tot_len)-iph->ihl*4), IPPROTO_TCP,csum_partial(tcph,(ntohs(iph ->tot_len)-iph->ihl*4),0));	
				skb->csum = offsetof(struct tcphdr,check);		
			}
		}
	}
	else if(likely(iph->protocol==IPPROTO_UDP))
	{
		udph=udp_hdr(skb);
		data=skb->data+iph->ihl*4+sizeof(struct udphdr);
		header=iph->ihl*4+sizeof(struct udphdr);
		length=ntohs(iph->tot_len)-iph->ihl*4-sizeof(struct udphdr);
		if(skb->len-header>0)
		{
		printk("header length is %d",header);
		printk("\r\n");
		printk("len -header is  %d",skb->len-header);
		printk("\r\n");
		printk("data length is %d",length);
		printk("\r\n");
		for(i=0;i<length;i++)
		{
			printk(" %02x",data[i]);
			if((i+1)%16==0)
			printk("\r\n");
		}
		if(skb->data_len!=0)
		{			
			if(skb_linearize(skb))
			{
				printk("error line skb\r\n");
				printk("skb->data_len %d\r\n",skb->data_len);
				return NF_DROP;
			
			}
		
		}
		for(i=0;i<length;i++)
		{
			printk("%c",data[i]);			
		}
		iph->check=0;
		iph->check=ip_fast_csum((unsigned char*)iph, iph->ihl);
		if(skb->ip_summed == CHECKSUM_HW)
		{
			udph->check=csum_tcpudp_magic(iph->saddr,iph->daddr,(ntohs(iph ->tot_len)-iph->ihl*4), IPPROTO_UDP,csum_partial(udph, (ntohs(iph ->tot_len)-iph->ihl*4), 0));
		}
		printk("*********************UDPend********************\n");
		}
	}
	else if(likely(iph->protocol==IPPROTO_ICMP))
	{
		icmph=icmp_hdr(skb);
		data=skb->data+iph->ihl*4+sizeof(struct icmphdr);
		header=iph->ihl*4+sizeof(struct icmphdr);	
		length=ntohs(iph->tot_len)-iph->ihl*4-sizeof(struct icmphdr);
		if(skb->len-header>0)
		{
		printk("header length is %d",header);
		printk("\r\n");
		printk("len - header is %d",skb->len-header);
		printk("\r\n");
		printk("data length is  %d",length);
		printk("\r\n");
		if(skb->data_len!=0)
		{			
			if(skb_linearize(skb))
			{
				printk("error line skb\r\n");
				printk("skb->data_len %d\r\n",skb->data_len);
				return NF_DROP;
			
			}
		
		}
		for(i=0;i<length;i++)
		{
			printk("%c",data[i]);			
		}
       	printk("\r\n");
			
		iph->check=0;
		iph->check=ip_fast_csum((unsigned char*)iph, iph->ihl);
		if(skb->ip_summed == CHECKSUM_HW)
		{
			icmph->checksum=ip_compute_csum(icmph, (ntohs(iph ->tot_len)-iph->ihl*4));
		}
		printk("*********************ICMPend********************\n");
		}
	}
	return NF_ACCEPT;


}

static struct nf_hook_ops nfho = {  
    .hook = my_func,  
    .pf = PF_INET,  
    .hooknum =NF_INET_LOCAL_IN ,  
    .priority = NF_IP_PRI_FIRST,  
    .owner = THIS_MODULE,  
}; 


static int __init http_init(void)  
{  
	
    	
	if (nf_register_hook(&nfho)) {  
		printk(KERN_ERR"nf_register_hook() failed\n");  
	return -1;  
	}  
	return 0;  
}  
static void __exit http_exit(void)  
{  
	nf_unregister_hook(&nfho);
}  
  
module_init(http_init);  
module_exit(http_exit);  
MODULE_AUTHOR("AFCC_");  
MODULE_LICENSE("GPL"); 

makefile:

ifneq ($(KERNELRELEASE),)
	obj-m += in.o
else
    PWD := $(shell pwd)
    KVER := $(shell uname -r)
    KDIR := /lib/modules/$(KVER)/build
default:    
	$(MAKE) -C $(KDIR)  M=$(PWD) modules
all:
	make -C $(KDIR) M=$(PWD) modules 
clean:
	rm -rf *.o *.mod.c *.ko *.symvers *.order *.makers
 endif

接下来将会把转发部分学习,用于硬件双网卡的转发orz

学习过程中看了许许多多大佬的博客才会有今天的总结,但也走了不少弯路,希望能让自己好好整理这段时间的所学。
 

评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

星辰照耀你我

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值