You are on page 1of 13

No Filter: The firewall that blocks everything

Jatin Kaushal
Certificate
This is to certify that this project is bona fide work of Jatin Kaushal

1
Contents
1 The Kernel Module 3
1.1 module_init and module_exit . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2 The Character Device . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.3 The netfilter hook . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8

2 Userspace Tools: nfilter and nflogs 9


2.1 nfilter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.2 nflogs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11

3 Scope for Improvement 12

4 References 12

2
Abstract
This document discusses my CBSE class 12, Computer Science project, No Filter, ab-
briviated as nfilter. It works by adding a custom hook to the netfilter packet processing
pipeline which tells it to drop any packet.
For interaction with nfilter, it provides a userspace tool called nfilter which takes in
a switch accept or block, and writes that to a Character Device created by the nfilter
LKM.
1 The Kernel Module
I’ve described the kernel module here in a few stages. The first stage talks about the entry
and exit functions of the module, the next talks about the character device it creates, and
finally the netfilter hook.

1.1 module_init and module_exit


The following is the main.c file.

1 #include "utils.h"
2 #include "includes.h"
3 #define DEVICE_NAME "kbschar"
4 #define CLASS_NAME "kbs"
5

7 MODULE_LICENSE("GPL");
8 MODULE_AUTHOR("cocoa");
9 MODULE_DESCRIPTION("Some kernel bullshit I wrote");
10 MODULE_VERSION("0.1");
11

12 static int __init kbs_entry(void);


13 static void __exit out(void);
14

15 int majorNumber;
16 char state[128];
17 struct class* kbscharClass = NULL;
18 struct device* kbscharDevice = NULL;
19 char message[256] = {0};
20 short size_of_message = 0;
21 DEFINE_MUTEX(kbschar_mutex);
22

23 struct nf_hook_ops nfhook = {


24 .hook = process_packet,
25 .hooknum = NF_INET_PRE_ROUTING,
26 .pf = PF_INET,

3
27 .priority = NF_IP_PRI_FIRST
28 };
29

30 EXPORT_SYMBOL( nfhook );
31 EXPORT_SYMBOL( state );
32

33

34 struct file_operations fops =


35 {
36 .open = device_open,
37 .read = dev_read,
38 .write = dev_write,
39 .release = dev_release,
40 };
41

42 static int __init kbs_entry(void) {


43

44 /*
45 * Three things. Register the major number, and then register and create a chardev with
46 * said major number
47 */
48

49 printk(KERN_INFO "Kernel Bullshit: Initializing kbs\n");


50

51 // Register Major Number


52 majorNumber = register_chrdev(0, DEVICE_NAME, &fops);
53 if (majorNumber < 0) {
54 printk(KERN_ALERT "KBS: Failed to register chardev\n");
55 return majorNumber;
56 }
57 printk(KERN_INFO "KBS: Registered chardev successfully with major number %d\n", majorNumber);
58

59 // Register Class
60 kbscharClass = class_create(THIS_MODULE, CLASS_NAME);
61 if (IS_ERR(kbscharClass)) {
62 unregister_chrdev(majorNumber, DEVICE_NAME);
63 printk(KERN_ALERT "KBS: Failed to register device class\n");
64 return PTR_ERR(kbscharClass);
65 }
66 printk(KERN_INFO "KBS: Device class registered correctly\n");
67

68

69 // Register device driver


70 kbscharDevice = device_create(kbscharClass, NULL, MKDEV(majorNumber, 0), NULL, DEVICE_NAME);
71 if (IS_ERR(kbscharDevice)) {

4
72 class_destroy(kbscharClass);
73 unregister_chrdev(majorNumber, DEVICE_NAME);
74 printk(KERN_ALERT "KBS: Failed to create the device\n");
75 pr_err("Error Code: %ld\n", PTR_ERR(kbscharDevice));
76 return PTR_ERR(kbscharDevice);
77 }
78 printk(KERN_INFO "KBS: Device class created successfully!\n");
79

80 mutex_init(&kbschar_mutex);
81 return 0;
82 }
83

84 static void __exit out(void) {


85 mutex_destroy(&kbschar_mutex);
86 device_destroy(kbscharClass, MKDEV(majorNumber, 0));
87 class_unregister(kbscharClass);
88 class_destroy(kbscharClass);
89 unregister_chrdev(majorNumber, DEVICE_NAME);
90 printk(KERN_INFO "Kernel Bullshit: Exiting\n");
91 }
92

93 module_init(kbs_entry);
94 module_exit(out);

In the first few lines, I include everything I need along with defining the constants
DEVICE_NAME and CLASS_NAME. Following that is some metadata about the module itself.
We define the __init and __exit markers to let GCC know that these functions should not
be traced by ftrace.
Lines 15 to 21 contain some global variables we need. We do not define this under any
function, because we don’t want the size of one object to get too large.
The struct in line 23 holds the information about our netfilter hook. The first item is
the actual hook, the next item tells us where the hook will be placed in the pipeline. In this
case, it’s before the packet is routed. The protocol on which this hook will act on is the
internet IP protocol.
Note: Most of these constants are defined in the Linux Kernel headers. For instance,
PF_INET is defined under include/linux/socket.h as:

#define PF_INET AF_INET

If we go one step further, AF_INET is defined under the same file, above the definition of
PF_INET as the constant 2.

#define AF_INET 2 /* Internet IP Protocol */

And then further, somewhere within the netfilter source code, the integer 2 is interpreted
as the Internet IP Protocol

5
In lines 30 and 31, we export the struct as well as the char state[128] so that other
modules can see this outside this file. We’ll use these in utils.c
Next we’ll be creating a chardev under /dev/kbschar, so we need to define the file
operations on that. We create the relevant functions under utils.c
Coming to the entry point for our module, We simply call the respective methods to
register a major number, class and create the class (along with some checks to make sure
that it’s successfully created, else we throw an error and exit)
At the end, we initialize the mutex object on line 80 so that the whole LKM can use it.
The exit function is simple. It simply frees up any resources we’ve used and cleanly exits.
At the end of this file, we just tell the kernel that kbs_entry and out are our init and exit
functions.

1.2 The Character Device


The struct file_operations is defined in the linux kernel as

struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
__poll_t (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
unsigned long mmap_supported_flags;
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);

Although that’s just part of it. As you can see, we create the prototypes for our read,
write, open and release functions according to this in utils.c (The prototypes are actually
in utils.h). I’ve pasted it below.

1 #include "includes.h"
2 #include "utils.h"
3 MODULE_LICENSE("GPL");
4

5 extern struct mutex kbschar_mutex;


6 extern struct nf_hook_ops nfhook;

6
7 extern int majorNumber;
8 extern struct class* kbscharClass;
9 extern struct device* kbscharDevice;
10 extern char message[256];
11 extern short size_of_message;
12 extern char state[128];
13

14

15 int device_open(struct inode *inodepointer, struct file *filepointer) {


16 if (!mutex_trylock(&kbschar_mutex)) {
17 printk(KERN_ALERT "KBS: Could not open. Mutex lock in place\n");
18 return -EBUSY;
19 }
20 printk(KERN_INFO "KBS: Opened!\n");
21 return 0;
22 }
23

24

25 ssize_t dev_read(struct file *filepointer, char *buffer, size_t length_of_buffer, loff_t


,→ *offset) {
26 printk(KERN_INFO "Called dev_read\n");
27 // copy_to_user < figure out what this does...
28 // It's format is (* to, * from, size)
29 if (copy_to_user(buffer, message, size_of_message) == 0) { // Successfully copeid!
30 printk(KERN_INFO "KBS: Sent %d bytes to the user\n", size_of_message);
31 return (size_of_message = 0); // Clear the message and return
32 }
33 else {
34 printk(KERN_ALERT "KBS: Couldn't copy message to user!\n");
35 return 2; // 2 for error
36 }
37

38 return 0;
39

40 }
41

42

43 ssize_t dev_write(struct file *filepointer, const char *buffer, size_t length_of_buffer,


,→ loff_t *offset) {
44 sprintf(message, "%s", buffer);
45 size_of_message = strlen(message);
46

47 if ( strcmp(message, "block" ) == 0 ) {
48 nf_register_net_hook(&init_net, &nfhook);
49 printk(KERN_INFO "nfilter: %s\n", state);

7
50 strcpy(state, "blocked");
51 }
52 else if ( strcmp(message, "accept") == 0 ) {
53 nf_unregister_net_hook(&init_net, &nfhook);
54 printk(KERN_INFO "nfilter: %s\n", state);
55 strcpy(state, "unblocked");
56 }
57 else
58 printk(KERN_INFO "Didn't recieve recognized instruction. State still at %s",
,→ state);
59

60

61 printk(KERN_INFO "KBS: Recieved: %s\n", message);


62 return size_of_message;
63 }
64

65

66 int dev_release(struct inode *inodepointer, struct file *filepointer) {


67 mutex_unlock(&kbschar_mutex);
68 printk(KERN_INFO "KBS: Released\n");
69 return 0;
70 }
71

72 unsigned int process_packet(void *priv, struct sk_buff *skb, const struct nf_hook_state
,→ *state) {
73

74 return NF_DROP;
75 }

The open function simply tries to lock our character device. If it is unable to lock, it
throws an error.
The read function will send a string to the user telling if the last operation was a block
or accept.
The write function takes in what is being sent and adds or remove our netfilter hook
from the pipeline, and copy the state to the state variable we exported from main.c.
Finally, the release function just removes the mutex lock.

1.3 The netfilter hook


This is the simplest function in this whole module. Netfilter will read the output of the
function we’re adding to the pipeline (which is the hook I referenced above) and does the
respective action. Normally, a hook would read information about the packet and see if we
should drop it or not. Here, we simply drop any packet by returning the NF_DROP constant.

8
2 Userspace Tools: nfilter and nflogs
The main interface we’ll use to interact with our LKM is the userspace tool nfilter and to
read the binary logs it generates, we’ll use it’s companion tool, nflogs

2.1 nfilter
The source code for this utility is fairly simple, and given below

1 #include <iostream>
2 #include <unistd.h>
3 #include <fcntl.h>
4 #include <string>
5 #include <string.h>
6 #include <algorithm>
7 #include <fstream>
8 #include "logs.h"
9

10 std::string formatted_time() {
11 time_t t = time(0);
12 struct tm *curtime = localtime( &t );
13 return asctime(curtime);
14 }
15

16 void log(std::string action, int status) {


17

18 std::fstream logfile;
19 std::string ftime = formatted_time();
20 ftime.erase(std::remove(ftime.begin(), ftime.end(), '\n'), ftime.end());
21

22 logfile.open("/var/log/nfilter.log", std::ios::binary | std::ios::app);


23

24 struct logs *data = new struct logs;


25 std::sprintf(data->time_of_action, "%s", ftime.c_str());
26 std::sprintf(data->action, "%s", action.c_str());
27 std::sprintf(data->status, "%d", status);
28

29

30 logfile.write((char*)data, sizeof(logs));
31

32 }
33

34 int main(int argc, char *argv[]) {


35 int fd;
36 if ( !(argc > 1) ) {

9
37 std::cout << "nfilter: Stands for no filter. The most secure firewall out there.
,→ Blocks literally everything." << std::endl;
38 std::cout << "Can't get attacked if you don't accept any packets whatsoever. My
,→ logic is infallible\n" << std::endl;
39

40 std::cout << "Options:" << std::endl;


41 std::cout << "\t--accept" << std::endl;
42 std::cout << "\t\tAccept all packets" << std::endl;
43 std::cout << "\t--block" << std::endl;
44 std::cout << "\t\tBlock all packets\n\n" << std::endl;
45 }
46 for (int i = 0; i < argc; ++i) {
47 std::string arg = argv[i];
48 if ( arg == "--accept" ) {
49 fd = open("/dev/kbschar", O_RDWR);
50 std::string data = "accept";
51 ssize_t write_status = write(fd, data.c_str(), data.size());
52 log(data, write_status);
53

54 if ( write_status == -1 ) {
55 std::cout << "Error: Could not write to /dev/kbschar" << std::endl;
56 return -1;
57 }
58 std::cout << "Accepting packets now" << std::endl;
59 return 0;
60 }
61 if ( arg == "--block" ) {
62 fd = open("/dev/kbschar", O_RDWR);
63 std::string data = "block";
64 ssize_t write_status = write(fd, data.c_str(), data.size());
65

66 log(data, write_status);
67

68 if ( write_status == -1 ) {
69 std::cout << "Error: Could not write to /dev/kbschar" << std::endl;
70 return -1;
71 }
72 std::cout << "Blocking packets now" << std::endl;
73 return 0;
74 }
75

76 }
77 return 0;
78 }

10
After the includes, I have a simple function which returns the formatted time. The log
function simply opens up the log file, and writes the details to the log file, which I’ve put in
/var/log/nfilter.log
Since we need to do low level IO operations, we can’t use streams. In the loop, I run
through the argument vector to see if the person has passed in a --accept or --block. Then
it writes the appropriate string to our chardev, which is caught by the dev_write function
in utils.c, and that function adds or removes our custom netfilter hook.

2.2 nflogs
Finally, for reading the logs, the nflogs utility is there. The source code is very simple

1 #include <iostream>
2 #include <unistd.h>
3 #include <fcntl.h>
4 #include <string>
5 #include <string.h>
6 #include <algorithm>
7 #include <fstream>
8 #include "logs.h"
9 #define MAX_ACTION_LEN 7
10

11 std::string formatted_time() {
12 time_t t = time(0);
13 struct tm *curtime = localtime( &t );
14 return asctime(curtime);
15 }
16

17 int main(int argc, char *argv[])


18 {
19 std::ifstream logfile ("/var/log/nfilter.log", std::ios::in | std::ios::binary);
20 struct logs membuff;
21 std::cout << "Time\t\t\t\tAction\tStatus" << std::endl;
22 while ( !logfile.eof() ) {
23 logfile.read((char*)&membuff, sizeof(membuff));
24 std::string time(membuff.time_of_action);
25 std::string action(membuff.action);
26 std::string status(membuff.status);
27

28 time.erase(std::remove(time.begin(), time.end(), '\n'), time.end());


29

30 std::cout << time << "\t"


31 << action << "\t"
32 << status << std::endl;

11
33 }
34 return 0;
35 }

This program simply reads the logs and displays them. In the terminal, this looks like:

3 Scope for Improvement


This program uses binary logs. For the sake of modularity, text logs would be much more
easier for other programs to use and manipulate.

4 References
1. The Netfilter Documentation

2. Derek Molloy’s blog post on character devices

12

You might also like