Linux4.x内核编程实战——动态切库(2)

学会了内核和用户空间的交互,之后我们便可以正式的完善我们的用户空间程序和内核,让它变得更加完整,切库其实本质上是切换ip/port二元组。所以我们需要一个数据结构来存放,这个数据结构是内核和用户空间都需要用到的,所以我们单独抽出来放到一个头文件中nc_def.h

/*
 * nc_def.h
 */
#ifndef __NC_DEF_H
#define __NC_DEF_H
struct nc_distort {
    unsigned int old_addr;
    unsigned int new_addr;
    unsigned short old_port;
    unsigned short new_port;
};
#define procfs_name "netcco"

#endif

有些人可能会问了,哎呀为什么addr(也就是ip)是一个int呀?我们所熟知的ip不是192.168.x.x这种形式吗?仔细看的话,其实每一个位都是0~255也就是2^8,一个字节。四位就是四个字节,所以我们只需要找一个大于四个字节的类型就可以了,int不是很好吗。当然你把它精确定义成int32类型是很好的选择。

然后我们写一个用户程序,首先接受用户的参数,写入/proc/netcco。

/*
 * netccoctl.c
 */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include "nc_def.h"

static char proc_file[256];
static struct nc_distort d;

int main(int argc, char** argv){
    sprintf(proc_file, "/proc/%s", procfs_name);
    int fd = open(proc_file, O_WRONLY);
    if (fd<0) {
        perror("open");
        return EXIT_FAILURE;
    }
    if (argc<5) {
        printf("%s [old_ip] [old_port] [new_ip] [new_port]\n", argv[0]);
        return EXIT_FAILURE;
    }
    //inet_addr把xxx.xxx.xxx.xx的字符串直接变成整形(网络字节序)
    d.old_addr = inet_addr(argv[1]); 
    d.old_port = htons(atoi(argv[2]));
    d.new_addr = inet_addr(argv[3]);
    d.new_port = htons(atoi(argv[4]));
    printf("old ip:%d\n", d.old_addr);
    printf("old port:%d\n", d.old_port);
    printf("new ip:%d\n", d.new_addr);
    printf("new port:%d\n", d.new_port);
    write(fd, &d, sizeof(struct nc_distort));
    close(fd);
    return EXIT_SUCCESS;
}

接下来,我们修改一下内核模块代码,把之前那个 char buf[80]改成这个结构的类型变量,同时我们在write函数中输出。

#define MAX_DISTORTS   (100)
static struct nc_distort distort_buf; //这里的char buf[80]就被替换了
static int                   distorts_len;
static struct nc_distort     distorts[MAX_DISTORTS];

static ssize_t proc_op_write(struct file *filp,
                             const char __user *buffer,
                             size_t len,
                             loff_t* offset)
{
    if(copy_from_user(&distort_buf, buffer, len)){
        return -ENOMEM;
    }
    printk(KERN_INFO "[netcco]old ip:%d\n", distort_buf.old_addr);
    printk(KERN_INFO "[netcco]old port:%d\n", distort_buf.old_port);
    printk(KERN_INFO "[netcco]new ip:%d\n", distort_buf.new_addr);
    printk(KERN_INFO "[netcco]new ip:%d\n", distort_buf.new_port);
    //写入到distorts
    distorts[distorts_len].old_addr = distort_buf.old_addr;
    distorts[distorts_len].old_port = distort_buf.old_port;
    distorts[distorts_len].new_addr = distort_buf.new_addr;
    distorts[distorts_len].new_port = distort_buf.new_port;
    distorts_len++;
    return len;
}

然后修改我们的Makefile,让它在编译内核模块的同时也编译我们的netccoctl。

obj-m += netcco.o

all: netccoctl
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
	rm netccoctl
	
netccoctl:	netccoctl.c nc_def.h

最后我们make一下,应该能看到除了ko文件还有一个netccoctl可执行文件。

$> make
$> sudo insmod netcco.ko    #先安装模块,才会出现proc下面的netcco虚拟文件
$> ./netccoctl 118.24.223.24 8989 121.22.22.23 8888
old ip:417273974
old port:7459
new ip:387323513
new port:47138
$> tail /var/log/kern.log
Oct 14 12:11:40 Sjet kernel: [ 2800.381019] [netcco]old ip:417273974
Oct 14 12:11:40 Sjet kernel: [ 2800.381020] [netcco]old port:7459
Oct 14 12:11:40 Sjet kernel: [ 2800.381021] [netcco]new ip:387323513
Oct 14 12:11:40 Sjet kernel: [ 2800.381021] [netcco]new ip:47138

$> sudo rmmod netcco  #卸载模块

然后进入重头戏——netfilter了,netfilter是linux内核高效的网络协议栈的hack点,很多路由器和网络中继设备以及防火墙和代理产品都是构建于netfilter。这里就不展开讲了,对netfilter有疑问的可以搜索一下相关资料,总之netfilter对内核消费一个网络包的整个生命周期这个过程暴露了5个注入点钩子,每个网络数据包来内核都会在不同时期上面调用钩子里面的函数以此来决定是否继续让数据包往下走。我们只关心刚进来和最终出去的两个钩子就够了,俗称掐头去尾。中间的钩子,大家自行学习。同proc一样,也是一顿注册回调到某个ops里面,在把这个ops绑定(注册)到netfilter。

int nf_register_net_hook(struct net *net, const struct nf_hook_ops *ops);

第一个参数其实不需要了解,是namespace。有一个默认的函数可以使用不需要我们自己去写。第二个就是对应的ops了,还记得上一篇博客用来操作proc文件的file_ops吗?差不多一回事。我们来看一下它的长相

struct nf_hook_ops {
	/* User fills in from here down. */
	nf_hookfn		*hook;
	struct net_device	*dev;
	void			*priv;
	u_int8_t		pf;
	unsigned int		hooknum;
	/* Hooks are ordered in ascending priority. */
	int			priority;
};

相比file_ops它就显得友好的多,其中hook就是我们的处理函数,而hooknum就是5个注入点的具体哪个。dev是网卡设备比如“lo/eth0/ws4p0”,我们不用管。priority是优先级,也就是同样的注入点有多个ops,那么如何排序。

按照我们第0章写的,我们不妨先把三个规则函数写出来

#define NC_UNMATCHED   (0)
#define NC_MATCHED     (1)
static struct iphdr          *ip_header; //ip帧header,ip信息位于这里
static struct tcphdr         *tcp_header;//tcp帧header,端口信息位于这里

// 规则一
// 阻挡老ip/port过来的数据包
static int block_old_source(const struct nc_distort distort)
{
    if (ip_header->saddr==distort.old_addr&&
        tcp_header->source==distort.old_port) {
           return NC_MATCHED;
    }
    return NC_UNMATCHED;
}

// 规则二
// 进来的包如果是新ip/port,需要改回老ip/port
static int incoming_distort_source(const struct nc_distort distort)
{
    if (ip_header->saddr==distort.new_addr&&
        tcp_header->source==distort.new_port) {
           ip_header->saddr = distort.old_addr;
           tcp_header->source = distort.old_port;
           
           return NC_MATCHED;
    }
    return NC_UNMATCHED;
}

// 规则三
// 出去的包,如果是老ip/port,需要改成新ip/port
static int outgoing_distort_dest(const struct nc_distort distort)
{
    if (ip_header->daddr==distort.old_addr&&
        tcp_header->dest==distort.old_port) {
        ip_header->daddr = distort.new_addr;
        tcp_header->dest = distort.new_port;
        return NC_MATCHED;
    }
    return NC_UNMATCHED;
}

接着就是两个netfilter钩子函数,一进一出,但由于我们可能修改了数据库,修改数据包之后一个很重要的动作就是重新签名封包。还需要一个额外的函数用来干这个活。

static void update_chksum(struct sk_buff *skb, struct iphdr *iph, struct tcphdr *tcp)
{
    //ip
    iph->check = 0;        
    iph->check = ip_fast_csum(iph,iph->ihl);

    //tcp
    tcp->check = 0;
    tcp->check = csum_tcpudp_magic(iph->saddr, iph->daddr,  
                                   ntohs(iph->tot_len)-iph->ihl*4, IPPROTO_TCP,  
                                   csum_partial(tcp, ntohs(iph->tot_len)-iph->ihl*4, 0));

    //checksum
    skb->ip_summed = CHECKSUM_NONE;
}

static unsigned int _hook_incoming(void* priv, 
                                   struct sk_buff* skb, 
                                   const struct nf_hook_state* st)
{
    skb_linearize(skb);
    ip_header  = ip_hdr(skb);
    tcp_header = tcp_hdr(skb);
    if (ip_header->protocol==IPPROTO_TCP) {
        for (i = 0;i<distorts_len;i++) {
            if (block_old_source(distorts[i])) {
                return NF_DROP;
            }
            if (incoming_distort_source(distorts[i])) {
                update_chksum(skb, ip_header, tcp_header);
                return NF_ACCEPT;
            }
        }
    }
    return NF_ACCEPT;
}

static unsigned int _hook_outgoing(void* priv,
                                   struct sk_buff* skb,
                                   const struct nf_hook_state* st)
{
    skb_linearize(skb);
    ip_header  = ip_hdr(skb);
    tcp_header = tcp_hdr(skb);
    if (ip_header->protocol==IPPROTO_TCP) {
        for (i=0;i<distorts_len;i++) {
            if (outgoing_distort_dest(distorts[i])) {
                update_chksum(skb, ip_header, tcp_header);
                return NF_ACCEPT;
            }
        }
    }
    return NF_ACCEPT;
}

最终,把钩子注册到netfilter内核中去。

static struct nf_hook_ops    i_hook;
static struct nf_hook_ops    o_hook;

static void install_hooks(void){
    i_hook.hook      = _hook_incoming;
    i_hook.hooknum   = NF_INET_PRE_ROUTING;
    i_hook.pf        = PF_INET;
    i_hook.priority  = NF_IP_PRI_FIRST;
    nf_register_net_hook(&init_net, &i_hook);
    
    o_hook.hook      = _hook_outgoing;
    o_hook.hooknum   = NF_INET_POST_ROUTING;
    o_hook.pf        = PF_INET;
    o_hook.priority  = NF_IP_PRI_FIRST;
    nf_register_net_hook(&init_net, &o_hook);
}

static void uninstall_hooks(void){
    nf_unregister_net_hook(&init_net, &i_hook);
    nf_unregister_net_hook(&init_net, &o_hook);
}

然后把这两个函数分别放到模块的初始化/卸载函数中去,就大功告成了。我们来看一下是否生效了呢。首先我在我自己的云vps上面部署了一个网页。然后我们使用netcco修改一个不存在的ip/port到我自己的vps上面。

$> sudo insmod netcco.ko
$> ./netccoctl 118.24.223.22 3999 118.24.xx.xxx 8000

接着我们打开浏览器访问一下这个并不存在的ip地址端口3999

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值