利用Linux内核模块Netfilter hook UDP报文

利用Linux内核模块Netfilter hook UDP报文,并对其中的部分数据进行修改


  1. 实验环境

    Ubuntu18.04:用于挂载hook程序,抓取udp报文

    Window10:udp报文的目的地,利用工具可检测到报文及其内容

  2. 实验工具

    Wireshark:抓取报文,便于同Netfilter hook到的报文进行比对

    NetAsssit:用于在Windows端接收报文

  3. 实验流程

    流程如图所示:

利用Linux内核模块Netfilter hook UDP报文_第1张图片

具体操作过程(操作过程中任何一步有问题可以结合Q&A部分以及参考资料部分解决):

  1. 将udpFilter.c和Makefile放在同一个文件夹下

  2. 执行"make"命令,生成udpFilter.ko

  3. 执行“sudo insmod udpFilter.ko”命令,将其挂载至内核上

  4. 执行“lsmod”命令,可以查看其是否已挂载至内核上

  5. 启动python脚本,发送消息

  6. 执行“sudo rmmod udpFilter”命令,将其卸载

  7. 执行“dmesg”可查看日志中输出的信息

  8. 模拟UDP协议

    用Python程序写一个脚本即可实现,代码如下

    import socket
    
    udpsocket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
    sdata = b"hello!" // 加b是为了将字符串转为字节
    saddr=('ip',port) // 此处的ip应为自己的目标ip, port为端口号
    udpsocket.sendto(sdata,addr)
    
  9. Wireshark抓取流量并分析

    在虚拟机上利用Wireshark,可抓取到该条报文,可以直接看到IP段和UDP段,并得到其中各条数据所在报文的位置,便于之后计算校验和

    利用Linux内核模块Netfilter hook UDP报文_第2张图片

    同时Windows端可接收到消息

    利用Linux内核模块Netfilter hook UDP报文_第3张图片

    对该条报文进行分析:

    1. 从ip段中提取出其源ip和目的ip,协议类型,并且该字段在报文中对应的位置

      利用Linux内核模块Netfilter hook UDP报文_第4张图片

      协议类型在IP段的10字节

      利用Linux内核模块Netfilter hook UDP报文_第5张图片

      源ip在IP段的13-16字节

      利用Linux内核模块Netfilter hook UDP报文_第6张图片

      目的ip在IP段的17-20字节

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rRRaXBmM-1584454800078)(C:\Users\王嘉磊\AppData\Roaming\Typora\typora-user-images\image-20200317123320457.png)]

    2. 从UDP段中得出其源端口,目的端口,UDP段长度,UDP校验和以及其数据

      利用Linux内核模块Netfilter hook UDP报文_第7张图片

      数据在UDP段的8字节之后

      利用Linux内核模块Netfilter hook UDP报文_第8张图片

      源端口在UDP段的1-2字节

      利用Linux内核模块Netfilter hook UDP报文_第9张图片

      目的端口在UDP段的3-4字节

      利用Linux内核模块Netfilter hook UDP报文_第10张图片

      UDP段长度在UDP段的5-6字节

      利用Linux内核模块Netfilter hook UDP报文_第11张图片

      UDP校验和在UDP段的7-8字节

      利用Linux内核模块Netfilter hook UDP报文_第12张图片

  10. Hook报文

    利用Linux内核模块Netfilter中的Hook函数将报文截取到,并且将其各字段打印到log中,与Wireshark中的流量进行对比

    Hook函数:

    unsigned int my_hook_func(void *priv, struct sk_buff *skb, 
        const struct nf_hook_state *state)
    {
    
        // 这里是ip头
        ip_header = (struct iphdr *)skb_network_header(skb);
        
        if (ip_header->protocol == 17)
        {
            printk("ip_saddr: %02x\n",  ip_header->saddr);
            printk("ip_daddr: %02x\n",  ip_header->daddr);
            // 这里是udp头
            udp_header = (struct udphdr *)skb_transport_header(skb);
            printk(KERN_INFO "Got udp packet \n");
            printk("udp_source: %02x\n", udp_header->source);
            printk("udp_dest: %02x\n", udp_header->dest);
            printk("udp_len: %02x\n", udp_header->len);
            //printk("udp_len(int): %d\n", udp_header->len);
            printk("udp_check: %02x\n", udp_header->check);
            // 这里输出udp报文中的data
            char *data = NULL;
            // 获取数据的起始地址
            data = (char *)((char *)udp_header + 8);
            printk("udp_data_addr: 0x%p\n", data);
            printk("hex : data[0-5] = 0x%02x%02x%02x%02x%02x%02x\n", data[0], data[1], data[2],data[3], data[4], data[5]);
            printk("char : data[0-5] = %c%c%c%c%c%c\n", data[0], data[1], data[2],data[3], data[4], data[5]);
        }
        return NF_ACCEPT;
    }
    

    结果

    利用Linux内核模块Netfilter hook UDP报文_第13张图片

    从结果中可以看出Netfilter中输出的字段与Wireshark流量中的字段相反,在之后修改数据时要注意这一点。

  11. 修改报文

    在上一步已经可以Hook到报文,现在可以对其内容进行修改。对UDP报文数据的修改其实就是对指针的一个操作,很简单。在修改UDP报文后,对修改后的报文要进行UDP校验和的计算。(UDP校验和算法在文末完整代码中)

    将下列代码添加至Hook函数中

    // 逐个修改字段,此处可以再进行改进,不然针对长字段修改较麻烦        
    data [0] = 't';
    data [1] = 'h';
    data [2] = 'a';
    data [3] = 'n';
    data [4] = 'k';
    data [5] = 's';
    // 重新计算校验和
    int cs = checkSum(ip_header, udp_header);
    // 将校验和倒转,保持跟Netfilter一致
    int rcs = f8tol8(cs);
    // 打印到log中
    printk("f to l check: %02x\n", rcs);
    // 修改校验和
    udp_header->check = rcs;
    

    在Windows端即可看到修改后的数据,从图中可看到,数据已经从“hello!”变为了“thanks”

    利用Linux内核模块Netfilter hook UDP报文_第14张图片

  12. Q&A
    1. Wireshark中的校验和显示为“unverified”

      在Wireshark的Preferences中,找到UDP,将其“Validate the UDP checksum if possible”打勾即可。

    2. Wireshark中的校验和不正确并且提示“maybe caused by “UDP checksum offload?””

      先用“ethtool --show-offload enp0s3”检查网卡的rx-checksumming和tx-checksumming是否开启,若开启利用"ethtool --offload enp0s3 rx off tx off "关闭即可

    3. 在hook函数中添加修改数据的代码后,发送报文时,Windows端未接收到报文

      与第二个问题解决方法一致

  13. 参考资料
    • 利用netfilter hook TCP报文:https://www.cnblogs.com/southday/p/11006936.html
    • netfilter 详解:https://zhuanlan.zhihu.com/p/61343421
    • makefile 详解:https://blog.csdn.net/liang13664759/article/details/1771246
    • UDP详解 https://www.cnblogs.com/sxiszero/p/11565108.html
    • sk_buff详解 https://blog.csdn.net/shanshanpt/article/details/21024465
    • udphdr结构详解 http://www.cppblog.com/aurain/archive/2008/11/21/67461.html
    • tcphdr结构详解 https://www.cnblogs.com/chengliangsheng/archive/2014/03/22/3598883.html
    • iphdr结构详解 https://blog.csdn.net/caofengtao1314/article/details/52753894
    • wireshark显示checksum不正确 https://blog.csdn.net/wangqi0079/article/details/9064557
  14. 完整代码

    udpFilter.c

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

// Define some structs that will be used
static struct nf_hook_ops nfho;
struct udphdr *udp_header;
struct iphdr *ip_header;
const int MAX_NUM = 65536;


// 将两个8bit合为一个16bit
int b8tob16(unsigned char c1, unsigned char c2)
{
    unsigned char t1;
    unsigned char t2;
    t1 = c1;
    t2 = c2;

    return (t1<<8)|t2;
}

// Exchange the first 8bit and the last 8bit between 16bit
int f8tol8(int i)
{
    unsigned char t1;
    unsigned char t2;
    // the last 8bit
    t1 = i&255;
    // the first 8bit
    t2 = (i&65280)>>8;

    return (t1<<8)|t2;
}

// 计算udp校验和
int checkSum(struct iphdr *iphr, struct udphdr *udphr)
{

    struct iphdr *ih = NULL;
    struct udphdr *uh = NULL;
    unsigned char *ip_fields = NULL;
    unsigned char *udp_fields = NULL;
    char *udp_data = NULL;
    int *ud = NULL;
    int *sumarr = NULL;
    int sumcnt = 0;
    int sum = 0;

    ih = iphr;
    uh = udphr;
    ip_fields = (unsigned char *)ih;
    udp_fields = (unsigned char *)uh;
    ud = (int *)vmalloc(100*sizeof(int));
    if(!ud)
    {
        printk("malloc for ud failure!");
        return -1;
    }
    sumarr = (int *)vmalloc(200*sizeof(int));
    if(!sumarr)
    {
        printk("malloc for sumarr failure!");
        return -1;
    }
    
    // 将两个ip地址转变格式
    //int ip_src1 = (ip_fields[12]<<8)|ip_fields[13];
    int ip_src1 = b8tob16(ip_fields[12], ip_fields[13]);
    printk("ip_src[0-1] = 0x%02x%02x\n", ip_fields[12], ip_fields[13]);
    sumarr[sumcnt++] = ip_src1;
    
    int ip_src2 = b8tob16(ip_fields[14], ip_fields[15]); 
    sumarr[sumcnt++] = ip_src2;

    int ip_dest1 = b8tob16(ip_fields[16], ip_fields[17]);
    sumarr[sumcnt++] = ip_dest1;
    
    int ip_dest2 = b8tob16(ip_fields[18], ip_fields[19]);
    sumarr[sumcnt++] = ip_dest2;

    // 将ip头部中的协议转变格式,并且前八位补0
    int ip_prol = (int)ip_fields[9];
    sumarr[sumcnt++] = ip_prol;

    // 将udp两个端口转变格式
    int src_port = b8tob16(udp_fields[0], udp_fields[1]);
    sumarr[sumcnt++] = src_port;
    
    int dest_port = b8tob16(udp_fields[2], udp_fields[3]);
    sumarr[sumcnt++] = dest_port;

    // 将udp长度转变格式
    int udp_l = b8tob16(udp_fields[4], udp_fields[5]);
    sumarr[sumcnt++] = udp_l;
    sumarr[sumcnt++] = udp_l;

    // 将校验和置为0
    sumarr[sumcnt++] = 0;

    // 将udp中的数据转变格式
    udp_data = (char *)((char *)uh + 8);
    int cnt = udp_l-8;
    printk("cnt = %d\n", cnt);
    if(cnt%2 == 0)
    {
        int cnt2 = cnt/2;
        int m = 0;
        int n = 0;
        while (cnt2 > 0)
        {
            ud[n] = b8tob16(udp_data[m], udp_data[m+1]);
            printk("udp_data = %2x%2x\n", udp_data[m],udp_data[m+1]);
            sumarr[sumcnt++] = ud[n];
            printk("ud = %d\n", ud[n]);
            cnt2--;
            n++;
            m+=2;
        } 
    }
    else
    {
        int cnt2 = cnt/2;
        int m = 0;
        int n = 0;
        while (cnt2 > 0)
        {;
            ud[n] = b8tob16(udp_data[m], udp_data[m+1]);
            sumarr[sumcnt++] = ud[n];
            cnt2--;
            n++;
            m+=2;
        } 
        ud[n] = b8tob16(udp_data[m], '0');
        sumarr[sumcnt++] = ud[n];
    }

    // 反码求和
    int i ;
    for (i = 0; i < sumcnt; i++)
    {
        printk("One of the members of the check: %02x\n", sumarr[i]);
        //printk("One of the members of the check: %d\n", sumarr[i]);
        sum += sumarr[i];
        if (sum > MAX_NUM)
        {
            sum = sum % MAX_NUM +1;
        }

    }

    sum = 65535 - sum;
    printk("sumcnt = %d\n", sumcnt);
    printk("My check = %d\n", sum);
    printk("My check(hex) = %02x\n", sum);
    vfree(ud);
    vfree(sumarr);
    return sum;
}

// Define our own hook function
// The parameters of this function has been updated.
// This is the prototype of this function
// 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 *))
unsigned int my_hook_func(void *priv, struct sk_buff *skb, 
    const struct nf_hook_state *state)
{

    // 这里是ip头
    ip_header = (struct iphdr *)skb_network_header(skb);


    if (ip_header->protocol == 17)
    {
        printk("ip_saddr: %02x\n",  ip_header->saddr);
        printk("ip_daddr: %02x\n",  ip_header->daddr);
        // 这里是udp头
        udp_header = (struct udphdr *)skb_transport_header(skb);
        printk(KERN_INFO "Got udp packet \n");
        printk("udp_source: %02x\n", udp_header->source);
        printk("udp_dest: %02x\n", udp_header->dest);
        printk("udp_len: %02x\n", udp_header->len);
        //printk("udp_len(int): %d\n", udp_header->len);
        printk("udp_check: %02x\n", udp_header->check);
        // 这里输出udp报文中的data
        char *data = NULL;
		// 获取数据的起始地址
        data = (char *)((char *)udp_header + 8);
        printk("udp_data_addr: 0x%p\n", data);
        printk("hex : data[0-5] = 0x%02x%02x%02x%02x%02x%02x\n", data[0], data[1], data[2],data[3], data[4], data[5]);
        printk("char : data[0-5] = %c%c%c%c%c%c\n", data[0], data[1], data[2],data[3], data[4], data[5]);
		// 逐个修改字段,此处可以再进行改进,不然针对长字段修改较麻烦 
        data [0] = 't';
        data [1] = 'h';
        data [2] = 'a';
        data [3] = 'n';
        data [4] = 'k';
        data [5] = 's';
		// 重新计算校验和
        int cs = checkSum(ip_header, udp_header);
		// 将校验和倒转,保持跟Netfilter一致
        int rcs = f8tol8(cs);
		// 打印到log中
        printk("f to l check: %02x\n", rcs);
		// 修改校验和
        udp_header->check = rcs;
    }
    return NF_ACCEPT;
}

int init_module()
{
    // Init the struct nfho
    nfho.hook = my_hook_func;
    nfho.hooknum = NF_INET_POST_ROUTING;
    nfho.pf = PF_INET;
    nfho.priority = NF_IP_PRI_FIRST;

    // Register the hook function by the struct nfho
    // This function has already update t2o nf_register_net_hook from nf_register_hook
    nf_register_net_hook(&init_net, &nfho);

    return 0;
}
void cleanup_module()
{
    // Unregister the hook function
    // This function has also already update to nf_unregister_net_hook from nf_unregister_hook
    nf_unregister_net_hook(&init_net, &nfho);
}

MODULE_LICENSE("GPL");
MODULE_AUTHOR("UDP");

makefile

obj-m += udpFilter.o

all:
		make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
		make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

udpMN

import socket

udpsocket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
sdata = b"hello!" // 加b是为了将字符串转为字节
saddr=('ip',port) // 此处的ip应为自己的目标ip, port为端口号
udpsocket.sendto(sdata,addr)
  1. 后记
    1. 这里基于Netfilter实现了对本机发出的UDP报文的截获和修改,若要对发往某一确定ip或者源ip是确定的UDP报文进行修改,可以通过修改hook条件来达到目的;

    2. 可将本机当作一个路由,对于接到本机的设备的流量,也可通过修改hook条件达到修改的目的;

    3. 理论上也可以通过对代码中变量的修改达到对TCP报文截获和修改。

你可能感兴趣的:(利用Linux内核模块Netfilter hook UDP报文)