linux 内核抓包功能实现基础(二) netfilter处理

上篇博客主要介绍了内核抓包设计思路与效果,并没有给出详细的设计实现,看起来就像一个花架子,华而不实。本篇博客就结合具体的代码介绍一下抓包的实现过程。先大致概括一下代码的思路,抓包模块启动后,就去netfilter上面注册两个钩子函数,分别放在PRE_ROUTING链和POSTROUTING链上,抓取进来的报文和出去的报文。当报文进入到netfilter层面处理时,钩子函数对报文做一下简单的匹配判断,符合条件就复制一份发送出来。先看代码:

/*
 *  Description : 内核抓包模块demo,内核版本3.4.39
 *  Date        : 20180701
 *  Author      : fuyuande
 */

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/socket.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
#include <linux/netfilter_arp.h>
#include <linux/in.h>
#include <linux/if_ether.h>
#include <linux/if_arp.h>
#include <linux/ip.h>
#include <net/ip.h>
#include <linux/skbuff.h>
#include <linux/inet.h>

#include "capture_demo.h"

struct dst_entry *output_dst = NULL; //出口设备指针

 //查询报文源端口或者目的端口
unsigned short capture_get_port(const struct sk_buff *skb,int dir)
{
    struct iphdr  *iph  = NULL;
    struct tcphdr *tcph = NULL;
    struct udphdr *udph = NULL;
    unsigned short port = 0;
    
    iph = ip_hdr(skb);
    if(!iph){
        log_warn("ip header null \r\n");
        return 0;
    }
    
    if(iph->protocol == IPPROTO_TCP){
        tcph = tcp_hdr(skb);
        if(!tcph){
            log_warn("tcp header null \r\n");
            return 0;
        }
        
        if(dir == 0){
            port = ntohs(tcph->dest); 
            tcph = NULL;
            return port;
        }else{        
            port = ntohs(tcph->source);
            tcph = NULL;
            return port;
        }
    }
    else if(iph->protocol == IPPROTO_UDP){
        udph = udp_hdr(skb);
        if(!udph){
            log_warn("udp header null \r\n");
            return 0;
        }
        if(dir == 0){
            port = ntohs(udph->dest);
            udph = NULL;
            return port;
        }else{
            port = ntohs(udph->source);
            udph = NULL;
            return port;
        }
    }
    else
        return 0;
}

//查询传输层协议 TCP/UDP/ICMP
unsigned int capture_get_transport_protocol(const struct sk_buff *skb){
    struct iphdr *iph = NULL;
    iph = ip_hdr(skb);
    if(!iph)
        return 0;

    if(iph->protocol == IPPROTO_TCP)
        return (CAPTURE_TCP);
        
    if(iph->protocol == IPPROTO_UDP)
        return (CAPTURE_UDP);

    return 0;
}

//复制报文并添加新的头域发送到指定的接收地址
int capture_send(const struct sk_buff *skb, int output)
{
    struct ethhdr  *oldethh = NULL;
    struct iphdr   *oldiph  = NULL;
    struct iphdr   *newiph  = NULL;
    struct udphdr  *newudph = NULL; 
    struct sk_buff *skb_cp  = NULL;
    struct net *net = NULL;
    unsigned int headlen = 0;

    headlen = 60;    // mac + ip + udp = 14 + 20 + 8 = 42, 这里分配大一点

    //如果报文头部不够大,在复制的时候顺便扩展一下头部空间,够大的话直接复制
	if(skb_headroom(skb) < headlen){
        skb_cp = skb_copy_expand(skb,headlen,0,GFP_ATOMIC);            
        if(!skb_cp){
            log_warn(" realloc skb fail \r\n");
            return -1;
        }
	}else{
    	skb_cp = skb_copy(skb, GFP_ATOMIC);
    	if(!skb_cp){
		    log_warn(" copy skb fail \r\n");
		    return -1;
	    }
    }

    oldiph = ip_hdr(skb);
    if(!oldiph){
        log_warn("ip header null \r\n");
        kfree_skb(skb_cp);
        return -1;
    }
    
    /*
    * 抓包报文格式
     ---------------------------------------------------------------------
     | new mac | new ip | new udp | old mac | old ip| old tcp/udp | data |
     ---------------------------------------------------------------------
     |        new header          |            new data                  |            
     ---------------------------------------------------------------------    

    */

    //如果是出去的报文,因为是在IP层捕获,MAC层尚未填充,这里将MAC端置零,并填写协议字段
    if(output){
        skb_push(skb_cp,sizeof(struct ethhdr));
        skb_reset_mac_header(skb_cp);
        oldethh = eth_hdr(skb_cp);
        oldethh->h_proto = htons(ETH_P_IP);            
        memset(oldethh->h_source,0,ETH_ALEN); 
        memset(oldethh->h_dest,0,ETH_ALEN);
        if(skb_cp->dev != NULL)
            memcpy(oldethh->h_source,skb_cp->dev->dev_addr,ETH_ALEN);                                                                   
    }else{
        //如果是进来的报文,MAC层已经存在,不做任何处理,直接封装
        skb_push(skb_cp,sizeof(struct ethhdr));
        skb_reset_mac_header(skb_cp);
        oldethh = eth_hdr(skb_cp);
        oldethh->h_proto = htons(ETH_P_IP);            
    }

    //添加IP, UDP头部
    skb_push(skb_cp, sizeof(struct iphdr) + sizeof(struct udphdr));    
    skb_reset_network_header(skb_cp);
    skb_set_transport_header(skb_cp,sizeof(struct iphdr));
    newiph = ip_hdr(skb_cp);
    newudph = udp_hdr(skb_cp);

    if((newiph == NULL) || (newudph == NULL)){
        log_warn("new ip udp header null \r\n");
        kfree_skb(skb_cp);
        return -1;
    }

    /* 抓包的报文发送的时候是调用协议栈函数发送的,所以output钩子函数会捕获到抓包报文,
     * 这里我们要把抓包报文和正常报文区分开,区分方式就是判断源端口,我们抓到的报文
     *  在送出去的时候填写的是保留端口0,如果钩子函数遇到这样的报文就会直接let go
     * 防止重复抓包,这一点在测试的时候很重要,一旦重复抓包,系统就直接挂了...
     */
    memcpy((unsigned char*)newiph,(unsigned char*)oldiph,sizeof(struct iphdr));
    newudph->source = htons(0);
    newiph->saddr = in_aton("1.1.1.1");
    newudph->dest = htons(8080);            //抓包服务器端口
    newiph->daddr = in_aton("192.168.199.123"); //抓包服务器地址

	newiph->ihl = 5;
	newiph->protocol = IPPROTO_UDP;	
    newudph->len = htons(ntohs(oldiph->tot_len) + sizeof(struct udphdr) + sizeof(struct ethhdr));
	newiph->tot_len = htons(ntohs(newudph->len) + sizeof(struct iphdr));

    /* disable gso_segment */        
    skb_shinfo(skb_cp)->gso_size = htons(0);

    //计算校验和
    newudph->check = 0;
    newiph->check = 0;
    skb_cp->csum = 0;
	skb_cp->csum = csum_partial(skb_transport_header(skb_cp), htons(newudph->len), 0);    
	newudph->check = csum_tcpudp_magic(newiph->saddr, newiph->daddr, htons(newudph->len), IPPROTO_UDP, skb_cp->csum);	

    skb_cp->ip_summed = CHECKSUM_NONE;
    if (0 == newudph->check){
	    newudph->check = CSUM_MANGLED_0;
    }
	newiph->check = ip_fast_csum((unsigned char*)newiph, newiph->ihl);

    //设置出口设备
    if(skb_dst(skb_cp) == NULL){
        if(output_dst == NULL){
            kfree_skb(skb_cp);
            return -1;
        }else{
            dst_hold(output_dst);        
            skb_dst_set(skb_cp, output_dst);
        }
    }

    //路由查找
    if(ip_route_me_harder(skb_cp, RTN_UNSPEC)){
        kfree_skb(skb_cp);
        log_info("ip route failed \r\n");
        return -1;
    }

    //发送
    ip_local_out(skb_cp);
    return 0;  
}


//输入钩子函数
static unsigned int capture_input_hook(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 = NULL;
    unsigned short sport = 0;

	iph = ip_hdr(skb);
	if(unlikely(!iph))
		return NF_ACCEPT;

    //只处理TCP和UDP
    if(iph->protocol != IPPROTO_TCP && iph->protocol != IPPROTO_UDP)
        return NF_ACCEPT;    

    //源地址和目的地址相同,只抓一次,在output钩子上处理一遍就够了
	if(iph->saddr == iph->daddr)
        return NF_ACCEPT;

    //设置传输层首部指针    
    skb_set_transport_header(skb, (iph->ihl*4));            

    //检查端口,端口为0的let go
    sport = capture_get_port(skb,1);
    if(sport == 0)
        return NF_ACCEPT;

    //复制一份报文并发送出去    
    capture_send(skb, 0);

    //返回accept,让系统正常处理
    return NF_ACCEPT;
}
                                     

//输出钩子函数
static unsigned int capture_output_hook(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;
	unsigned short sport = 0;	    
	iph = ip_hdr(skb);
   
	if(unlikely(!iph))
		return NF_ACCEPT;

    //只处理TCP或UDP        
    if(iph->protocol != IPPROTO_TCP && iph->protocol != IPPROTO_UDP)
        return NF_ACCEPT;

    //如果源端口为0,是抓包报文,直接let it go, 否则进行抓包
    sport = capture_get_port(skb,1); 

    if(output_dst == NULL){
        if(skb_dst(skb) != NULL){
                     
            output_dst = skb_dst(skb);
            dst_hold(output_dst); 
            log_info("dst get success \r\n");          
        }
    }

    if(sport != 0)
        capture_send(skb, 1);

    return NF_ACCEPT;         	
}

struct nf_hook_ops capture_hook_ops[] = {
	{
		.hook=capture_input_hook,       //输入钩子处理函数
		.pf=NFPROTO_IPV4,
		.hooknum=NF_INET_PRE_ROUTING,   //hook点
		.priority=NF_IP_PRI_FIRST + 10, //优先级
	},
	{
		.hook=capture_output_hook,      //输出钩子处理函数
		.pf=NFPROTO_IPV4,
		.hooknum=NF_INET_POST_ROUTING,  //hook点
		.priority=0,                    //优先级
	},	
	{}
};


static int __init capture_init(void)
{	     
    //注册钩子函数
	if(nf_register_hooks(capture_hook_ops,ARRAY_SIZE(capture_hook_ops))!=0)
	{
		log_warn("netfilter register fail");
		return -1;
	}
	log_info("capture module init \r\n");
	return 0;
}

static void __exit capture_exit(void)
{ 
    //注销钩子函数
	nf_unregister_hooks(capture_hook_ops,ARRAY_SIZE(capture_hook_ops));	
    if(output_dst != NULL){
        dst_release(output_dst);
        log_info("dst release success \r\n");
    }    
	log_info("capture module exit \r\n");
	return ;
}

module_init(capture_init)
module_exit(capture_exit)

MODULE_ALIAS("capture");
MODULE_AUTHOR("fuyuande");
MODULE_DESCRIPTION("capture module");
MODULE_LICENSE("GPL");

代码里过滤条件很简单,就是TCP/UDP报文,当收到这样的报文就复制一份,添加新的头域发送到远端服务器上,这里需要指定远端服务器的端口和IP地址,至于源端口和目的端口,是可以任意填写的。我们调用的发送函数是ip_local_out(),这意味着抓包报文还会经过POST_ROUTING链,为了防止重复抓包,需要区分正常的报文和抓包报文,这里区分条件就是端口,抓包报文的源端口使用的是0,这样当收到这样的报文时候就直接accept处理。将报文发送出去的方式还有其它,例如dev_queue_xmit()接口,但是调用这个接口的前提是你已经知道了到远端服务器的出口设备dev以及下一条的mac地址,这样处理可能更快一点,不必再有协议栈处理一遍,不过呢,使用ip_local_out的一个优点就是协议栈会帮我们处理分片报文,如果抓到的报文过大的话,直接调用dev_queue_xmit有可能在发送途中被丢弃,而使用ip_local_out则IP协议栈会帮我们处理分片的事情,我们只需要调用这个接口就可以了。

看一下运行的实际效果图:

wireshark可以看到抓包模块送过来的报文,但是这样的报文并不能直接拿来分析,因为它外面还封装了mac, ip, udp头,需要去掉这些头部才能看到原始的报文,这就是抓包服务器的功能。今天先介绍到这,下一篇将抓包服务器的实现

对了,代码我放到github上:

https://github.com/FuYuanDe/capture_demo.git

git clone下来直接make && insmod 就可以运行了。

またね!

猜你喜欢

转载自blog.csdn.net/fuyuande/article/details/80873125