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);
This code has 3 bugs:
len = *start - '0';should belen = *start;because length is represented as a byte, not acharnumbershould be
because
blocked_url_hashesis an array of sizeBLOCKED_URLS_SIZE * sizeof(int)bytes.NULLpointer dereference whendevice_destroy(class_blocker, MKDEV(major_number, 0));is called afterclass_blockerwas destroyed. Alsounregister_chrdev(major_number, DEVICE_NAME);should be called after the device is destroyed since it destroys themajor_numberassociated with the device.So this is how module exit should look: