How to parse a DNS packet in network format in C

176 Views Asked by At

I'm writing a netfilter kernel module that drops particular DNS requests from my machine. For that I need to extract a domain name from DNS packet question section (rfc1035). The QNAME field inside this section has the format of a sequence of length byte followed by that number of bytes, ending with the 0 byte terminator, e.g. 12cppreference3com0.

But my machine is little-endian, so I get the reverse byte order of 0moc3ecnereferppc21. How do I parse this? I have no way of detecting the end by null terminator and I don't know the length of the entire DNS packet field to reverse bytes in it.

After insmod (before any interaction from user) I tried to access a website and had kernel logs Blocked DNS request for 0. The only way I can get 0 hash is if I run into 0 inside QNAME immediately. Weirdly, after running again, I had my OS completely crash.

Update: I still get a crash if I don't do - '\0'. But the crash comes a little bit later. When I load the module and then unload it, I cannot load it again with insmod:

insmod: ERROR: could not insert module dns_filter_module.ko: Device or resource busy

The kernel logs suggest some NULL pointer dereference is at place. And also I get watchdog bug: soft lockup CPU stuck.

The code:

P.S. I understand I'll get hash collisions and that I may get multiple question sections, I don't understand why my system crashes from this particular very simple program

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
#include <linux/ip.h>
#include <linux/udp.h>
#include <linux/fs.h>
#include <linux/uaccess.h>

MODULE_LICENSE("GPL");

#define DEVICE_NAME "dns_blocker_device"
#define BLOCKED_URLS_SIZE 120

#define DNS_PACKET_SECTION_SIZE 2
#define DNS_HEADER_SECTIONS_COUNT 6
#define UDP_HEADER_SIZE 8

static int blocked_url_hashes[BLOCKED_URLS_SIZE] = {0};

static struct nf_hook_ops nfho = {0};

static int major_number;
static struct class* class_blocker = NULL;
static struct device* device_blocker = NULL;
static atomic_t is_device_open = {0};

static int hash_func(unsigned char *start, int len)
{
    int hash = 0;
    int i = 0;
    
    for (i = 0; i < len; ++i)
    {
        hash += start[i];
    }

    return hash;
}

// get domain hash as the sum of values of every its character
static int extract_qname_hash(unsigned char *start)
{
    int len = *start - '0';
    int hash = 0;

    while (len != 0)
    {
        ++start;

        hash += hash_func(start, len);

        start += len;
        len = *start - '0';
    }

    return hash;
}

static int get_domain_name_hash(unsigned char *header) 
{

    unsigned char *curr = header;

    // move past headers, to QUESTION section
    curr += DNS_HEADER_SECTIONS_COUNT * DNS_PACKET_SECTION_SIZE;

    // return QNAME hash
    return extract_qname_hash(curr);
}

static bool is_url_blocked(int url_hash) 
{
    int i = 0;

    for (i = 0; i < BLOCKED_URLS_SIZE; ++i) 
    {
        if (url_hash == blocked_url_hashes[i]) 
        {
            return true;
        }
    }

    return false;
}

static unsigned int hook_func(void *priv, struct sk_buff *skb, const struct nf_hook_state *state) 
{
    struct iphdr *ip_header;
    struct udphdr *udp_header;
    unsigned char *data_start;
    int url_hash = -1;

    if (skb == NULL) 
    {
        return NF_ACCEPT;
    }

    ip_header = ip_hdr(skb);

    if (ip_header->protocol == IPPROTO_UDP) 
    {
        udp_header = udp_hdr(skb);

        if (ntohs(udp_header->dest) == 53) 
        {

            data_start = (unsigned char *)udp_header + UDP_HEADER_SIZE;
            
            url_hash = get_domain_name_hash(data_start);

            if (is_url_blocked(url_hash)) 
            {
                printk(KERN_INFO "Blocked DNS request for %d\n", url_hash);
                return NF_DROP; 
            }
        }
    }

    return NF_ACCEPT;
}

static int device_open(struct inode *inode, struct file *file)
{

    if (atomic_cmpxchg(&is_device_open, 0, 1)) return -EBUSY;

    return 0;
}

static int device_release(struct inode *inode, struct file *file)
{
    atomic_set(&is_device_open, 0);

    return 0;
}

// copy int array of domain hashes from user
static ssize_t device_write(struct file *filep, const char *buffer, size_t len, loff_t *offset) 
{
    if (len >= BLOCKED_URLS_SIZE) 
    {
        printk(KERN_ALERT "Max URLs reached\n");
        return -EINVAL;
    }

    if (copy_from_user(blocked_url_hashes, buffer, len) != 0) 
    {
        printk(KERN_ALERT "Failed to copy URL from user space\n");
        return -EFAULT;
    }

    return len;
}

static struct file_operations fops = 
{
    .open = device_open,
    .write = device_write,
    .release = device_release
};

static int __init init_blocker_module(void) 
{
    int ret = 0;

    nfho.hook = hook_func;
    nfho.hooknum = NF_INET_PRE_ROUTING;
    nfho.pf = PF_INET;
    nfho.priority = NF_IP_PRI_FIRST;
    nf_register_net_hook(&init_net, &nfho);

    major_number = register_chrdev(0, DEVICE_NAME, &fops);

    if (major_number < 0) 
    {
        printk(KERN_ALERT "Failed to register a major number\n");
        ret = major_number;
        goto device_register_fail;
    }

    class_blocker = class_create(THIS_MODULE, "blocker_class");

    if (IS_ERR(class_blocker)) 
    {
        printk(KERN_ALERT "Failed to create device class\n");
        ret = PTR_ERR(class_blocker);
        goto class_create_fail;
    }

    device_blocker = device_create(class_blocker, NULL, MKDEV(major_number, 0), NULL, DEVICE_NAME);

    if (IS_ERR(device_blocker)) 
    {
        printk(KERN_ALERT "Failed to create the device\n");
        ret = PTR_ERR(device_blocker);
        goto device_create_fail;
    }

    return ret;

device_create_fail:
    class_destroy(class_blocker);
class_create_fail:
    unregister_chrdev(major_number, DEVICE_NAME);
device_register_fail:
    nf_unregister_net_hook(&init_net, &nfho);

    return ret;
}

static void __exit exit_blocker_module(void) 
{
    unregister_chrdev(major_number, DEVICE_NAME);
    class_destroy(class_blocker);
    device_destroy(class_blocker, MKDEV(major_number, 0));
    nf_unregister_net_hook(&init_net, &nfho);

    printk(KERN_INFO "Blocker module removed\n");
}

module_init(init_blocker_module);
module_exit(exit_blocker_module);

1

There are 1 best solutions below

0
Ostap On

This code has 3 bugs:

  1. len = *start - '0'; should be len = *start; because length is represented as a byte, not a char number
if (len >= BLOCKED_URLS_SIZE) 

should be

if (len > BLOCKED_URLS_SIZE * sizeof(int)) 

because blocked_url_hashes is an array of size BLOCKED_URLS_SIZE * sizeof(int) bytes.

  1. the exit function causes NULL pointer dereference when device_destroy(class_blocker, MKDEV(major_number, 0)); is called after class_blocker was destroyed. Also unregister_chrdev(major_number, DEVICE_NAME); should be called after the device is destroyed since it destroys the major_number associated with the device.

So this is how module exit should look:

static void __exit exit_blocker_module(void) 
{
    device_destroy(class_blocker, MKDEV(major_number, 0));
    class_destroy(class_blocker);
    unregister_chrdev(major_number, DEVICE_NAME);
    nf_unregister_net_hook(&init_net, &nfho);

    printk(KERN_INFO "Blocker module removed\n");
}