Создание и тестирование Firewall в Linux, Часть 1.3. Написание char device. Добавление виртуальной файловой системы…

в 10:40, , рубрики: C, char device, firewall, kernel module, linux, Netfilter, sysfs, virtualbox, информационная безопасность, Разработка под Linux

Содержание первой части:

1.1 — Создание виртуальной лаборатории (чтобы нам было где работать, я покажу как создать виртуальную сеть на вашем компьютере. Сеть будет состоять из 3х машин Linux ubuntu).
1.2 – Написание простого модуля в Linux. Введение в Netfilter и перехват трафика с его помощью. Объединяем все вместе, тестируем.
1.3 – Написание простого char device. Добавление виртуальной файловой системы — sysfs. Написание user interface. Объединяем все вместе, тестируем.

Содержание второй части:

Скрытый текст

2.1 — Введение во вторую часть. Смотрим на сеть и протоколы. Wireshark.
2.2 — Таблицы Firewall. Transport Layer. Структуры TCP, UDP. Расширяем Firewall.
2.3 — Расширяем функциональность. Обарабатываем данные в user space. libnetfilter_queue.
2.4 — (*Опиционально) Изучаем реальную Buffer Overflow атаку и предотвращаем с помощью нашего Firewall'а.


Часть 1.3

В предыдущих частях, мы подготовили модуль (kernel space), который уже может делать минимальную работу – некоторым пакетам давать проходить, а некоторым нет. Теперь было не плохо добавить возможность, получать данные и управлять работой модуля из обычной программы (user space). Например включать Firewall, выключать и получать статистику работы.
Существует несколько способов это сделать. Мы пойдем по классическому способу.

Введение в Character device. Теория.

Я советую для чтения английскую версию статьи Википедии, как более точную и объемную по этой теме чем русскую https://en.wikipedia.org/wiki/Device_file#Character_devices
Для нас будет достаточно понимания, что разные аппаратные устройства(hardware), работают и взаимодействуют с операционной системой, на очень низком уровне и конечно же происходит все это в kernel space. Linux/Unix системы, создали механизмы, одним из которых является character device, для того чтобы упростить взаимодействие с этими устройствами. Создавая character device, мы «просим» у ОС необходимые ресурсы и можем представить устройство в виде файла в специальной директории (как известно в linux все представлено в виде файлов и операции чтениязаписи в него). При чтении из этого файла – мы можем получить данные от устройства (например пакеты которые пришли на сетевую карту), при записи в этот файл мы можем посылать данные устройству (например послать документ принтеру на печать). В нашем случае, устройства физически нет, но мы воспользуемся этими файлами для взаимодействия с нашей программой.

Введение в Character device. Практика.

Создать и зарегистрировать такой драйвер очень просто, достаточно вызвать функцию
fw_major = register_chrdev(0, DEVICE_NAME, &fops);
Уже после этой функции, можно пойти в /dev/ вручную добавить устройство и начать с ним работать. Структура fops, определяет функции, которые будут вызываться при разных событиях с драйвером, например — чтением или записью. Функция возвращает major number – уникальный идентификационный номер.
В данном случае я выбрал только два события, но по ссылкам, что я приведу ниже, или самому почитав исходные тексты kernel можно найти полный список(довольно большой).
Ниже я определил, что при чтении с нашего устройства, мы получим количество всех перехваченных пакетов, а при записи, обнулим их:

static ssize_t fw_device_read(struct file* filp, char __user *buffer, size_t length, loff_t* offset)
{
	printk("Reading from device, return total number of messagesn");
	return sprintf(buffer, "%u", accepted_num + dropped_num); 
}  
static ssize_t fw_device_write(struct file *fp, const char *buff, size_t length, loff_t *ppos) {
	printk("Writing to device, clear number of messagesn");
	accepted_num = dropped_num = 0;
	return 0; 
} 
static struct file_operations fops = { 
	.read = fw_device_read, .write = fw_device_write
};

Чтобы не добавлять каждый раз устройство вручную, можно сделать его регистрацию автоматической:

fw_class = class_create(THIS_MODULE, DEVICE_NAME);
fw_device = device_create(fw_class, NULL, MKDEV(fw_major, 0), NULL, DEVICE_FW);

Небольшая проверка:

Создание и тестирование Firewall в Linux, Часть 1.3. Написание char device. Добавление виртуальной файловой системы… - 1

Уже сейчас можно видеть устройство в /dev, кроме того, после регистрации класса, оно также появляется и в /sys/class

Ниже приведен полный листинг, обратите внимание на использование goto. Обычно(=всегда), мы не используем goto в программировании, потому что это очень сильно портит понимание, содержание, читаемость кода и скорее всего говорит о проблемах в дизайне программы(спагетти-код). Но этот случай один из немногих где goto очень к месту.

static int fw_major; 
static struct device* fw_device;

static ssize_t fw_device_read(struct file* filp, char __user *buffer, size_t length, loff_t* offset) {
 	printk("Reading from device, return total number of messagesn");
 	return sprintf(buffer, "%u", accepted_num + dropped_num); 
}  

static ssize_t fw_device_write(struct file *fp, const char *buff, size_t length, loff_t *ppos) {
 	printk("Writing to device, clear number of messagesn");
 	accepted_num = dropped_num = 0;
 	return 0; 
} 

static struct file_operations fops = { 
	.read = fw_device_read, 
	.write = fw_device_write
}; 

static int __init fw_module_init(void) {
 	int retval = 0;
	printk("Starting FW module loadingn"); 
	…
	accepted_num = 0; 	
	dropped_num = 0; 
	// device functions 	
	fw_major = register_chrdev(0, DEVICE_NAME, &fops);
 	if (fw_major < 0) {
		printk("failed to register device: error %dn", fw_major);
		goto failed_chrdevreg; 	
	} 
	fw_class = class_create(THIS_MODULE, DEVICE_NAME); 	
	if (fw_class == NULL) {
		printk("failed to register device class '%s'n", DEVICE_NAME);
 		goto failed_classreg;
	} 
 	fw_device = device_create(fw_class, NULL, MKDEV(fw_major, 0), NULL, DEVICE_FW);
 	if (fw_device == NULL) {
		printk("failed to create device '%s_%s'n", DEVICE_NAME, DEVICE_FW);
 		goto failed_devreg;
	} 
 
	// netfilter functions …
	return 0; 
	failed_devreg: 
		class_destroy(fw_class);
	// unregister the device class
	failed_classreg: 
		unregister_chrdev(fw_major , DEVICE_NAME);
	// remove the device class failed_classreg:
	failed_chrdevreg: 
	// unregister the major number failed_chrdevreg:
		return -1;
}

User interface.

Теперь напишем простую программу, которая будет читать и писать в созданный девайс для проверки работы:

// test.c
#include <stdio.h>
#include <string.h>
#include <fcntl.h>

int reset() {
	char path[] = "/dev/device_fw";
	int fd = open(path, O_WRONLY);
 	if (fd<0) {
		printf("Device access error, fd = %dn", fd);
		return fd;
	}
	char msg = '1';
	write(fd, &msg, sizeof(msg));
	close(fd);
	return 0;
}

int all_msg() {
	char msg[1] = {0};
	int fd = open("/dev/device_fw", O_RDONLY);
	if (fd<0) {
		printf("Device access error, fd = %dn", fd);
		return fd;
	}

	if(read(fd, &msg, 1)>0){	
		printf("Accepted packets number: %sn", msg);
	} else {
		printf("Nothing to readn");
	}

	close(fd);
	return 0;
}

int main(int argc, char* argv[]) {
	if(argc <= 1 || argc > 3) {
		printf("Wrong number arguments. Number of arguments is %dn", argc);
		return -1;
	}

	if(strcmp(argv[1], "reset")==0){
		reset();
	} else if(strcmp(argv[1], "all_msg")==0){
		all_msg();
	} else {
		printf("Wrong argument %sn", argv[1]);
	}

	return 0;
}

И проверяем работу:

Создание и тестирование Firewall в Linux, Часть 1.3. Написание char device. Добавление виртуальной файловой системы… - 2

Как видно выше, сначала мы загрузили наш модуль. Потом откомпилировали программу для чтениязаписи в устройство. Первый раз запустив sudo ./test all_msg, мы выполнили чтение из устройства и получили число 0. После этого мы послали 4 ping запроса на одно из сетевых устройств. Опять выполнили чтение, получили 16 пакетов (почему не 8мь? ;). Выполнили sudo ./test reset, которая обратилась на запись к устройству, которое в свою очередь все обнулило.

Так это выглядит с точки зрения драйвера:

Создание и тестирование Firewall в Linux, Часть 1.3. Написание char device. Добавление виртуальной файловой системы… - 3

Прежде чем мы продолжим, очень советую (но не обязательно) для углубления почитать тут
http://derekmolloy.ie/writing-a-linux-kernel-module-part-2-a-character-device/

А также тут
http://pete.akeo.ie/2011/08/writing-linux-device-driver-for-kernels.html
Внизу еще есть ссылка на хорошую бесплатную книгу.

Введение в sysfs. Теория.

Мы могли бы продолжить коммуникацию драйвера – пользователя через чтениезапись в /dev/fw_device, но не рекомендуется это делать, если нужно посылатьполучать много информации (в отличии от байтов в нашем примере), а также данный способ считается устаревшим. И хотя в данной статье, нет больших объемов, я покажу как использовать sysfs, для коммуникации kernel <-> user.

sysfs — виртуальная файловая система в операционной системе Linux. Экспортирует в пространство пользователя информацию ядра Linux о присутствующих в системе устройствах и драйверах. Впервые появилась в ядре версии 2.6. Необходимость создания была вызвана устаревшей системой работы ядра с устройствами.(https://ru.wikipedia.org/wiki/Sysfs)

То есть, благодаря sysfs, мы можем создавать целые структуры с иерархией из файлов, которые будут отображаться в /sys/class/fw и использовать их для чтения или записи. Например мы создадим два файла:

/sys/class/fw/acceptedMessages — чтение из которого вернет количество принятых пакетов
/sys/class/fw/dropedMessages — чтение из которого вернет количество запрещенных пакетов

Делается это очень просто. Обратите внимание, что после вызова выше

fw_class = class_create(THIS_MODULE, DEVICE_NAME);

мы уже зарегистрировали класс и уже видели его в /sys/class. Осталось добавить два файла и определить их функции. Регистрируем файлы:

static int __init fw_module_init(void)
{
  …
    fw_device = device_create(fw_class, NULL, MKDEV(fw_major, 0), NULL, DEVICE_FW); 
  …
    retval = device_create_file(fw_device, &dev_attr_acceptedMessages); 
    if (retval < 0) {
        printk("failed to create acceptedMessages /sys endpoint - continuing withoutn");
    } 
    retval = device_create_file(fw_device, &dev_attr_droppedMessages);
    if (retval < 0) {
        printk("failed to create droppedMessages /sys endpoint - continuing withoutn"); 
    }
  …
}

Вначале модуля, добавляем макросы DEVICE_ATTR, которые определяют чтение или запись, а также функции которые будут вызваны. Так как нам не зачем обрабатывать запись, то последнее поле NULL.


static DEVICE_ATTR(acceptedMessages, S_IWOTH | S_IROTH, sys_read_accepted_msg, NULL);
static DEVICE_ATTR(droppedMessages, S_IWOTH | S_IROTH, sys_read_dropped_msg, NULL); 

И сами функции:

static ssize_t sys_read_accepted_msg(struct device *dev, struct device_attribute *attr, char *buffer) {
    return sprintf(buffer, "%u", accepted_num);
} 

static ssize_t sys_read_dropped_msg(struct device *dev, struct device_attribute *attr, char *buffer){
    return sprintf(buffer, "%u", dropped_num);
}

Обращение к ним через наш user interface происходит точно также как и с /dev/
Например:


int dropped_num() {
	char msg[255] = {0};
	int fd = open("/sys/class/fw/device_fw/droppedMessages", O_RDONLY);
	if (fd<0) {
		printf("Device access error, fd = %dn", fd);
		return fd;
	}
	if(read(fd, &msg, 255)>0){	
		printf("Accepted packets number: %sn", msg);
	} else {
		printf("Nothing to readn");
	}
	close(fd);
	return 0;
}

Теперь самое время все собрать воедино, откомпилировать и хорошенько проверить.
Sysfs:

Создание и тестирование Firewall в Linux, Часть 1.3. Написание char device. Добавление виртуальной файловой системы… - 4

Accepted packets: делаем ping

Создание и тестирование Firewall в Linux, Часть 1.3. Написание char device. Добавление виртуальной файловой системы… - 5

И параллельно считываем

Создание и тестирование Firewall в Linux, Часть 1.3. Написание char device. Добавление виртуальной файловой системы… - 6

Проверяем dropped packets:
Пытаемся делать ping с host2 на host1

Создание и тестирование Firewall в Linux, Часть 1.3. Написание char device. Добавление виртуальной файловой системы… - 7

Параллельно смотрим «логи»

Создание и тестирование Firewall в Linux, Часть 1.3. Написание char device. Добавление виртуальной файловой системы… - 8

Кстати, обратите внимание, что тут, счетчик постоянно увеличивается на один (а не на два, как раньше), потому что, host1 не получает запросы от host2 и соответственно не отвечает. И для интереса dmesg:

Создание и тестирование Firewall в Linux, Часть 1.3. Написание char device. Добавление виртуальной файловой системы… - 9

Создание и тестирование Firewall в Linux, Часть 1.3. Написание char device. Добавление виртуальной файловой системы… - 10

Последнее — я выгружу fw и проверю, что без него сеть работает без ограничений:

Создание и тестирование Firewall в Linux, Часть 1.3. Написание char device. Добавление виртуальной файловой системы… - 11

Создание и тестирование Firewall в Linux, Часть 1.3. Написание char device. Добавление виртуальной файловой системы… - 12

Мы видим, что без нашего модуля, ping проходит без проблем.

Заключение.

В первой части, мы сначала создали виртуальную сеть, для работы с тремя компьютерами. Потом мы рассмотрели написание простого модуля, который использовал netfilter для перехвата трафика. И в конце, добавили char device и sysfs, для представления функций модуля в файловой системе обычному пользователю через чтениезапись в файлы. В завершении написали программу для пользователя, для управления нашим устройством.
Буду очень рад любым конструктивным комментариям. В следующей части, мы существенно расширим функциональность данного модуля, сделаем его более похожим на простой firewall, а также посмотрим как он может защищать сеть от различного вида атак.

Ссылки:
Linux Device Drivers, Third Edition
http://derekmolloy.ie/writing-a-linux-kernel-module-part-2-a-character-device/
http://pete.akeo.ie/2011/08/writing-linux-device-driver-for-kernels.html
https://ru.wikipedia.org/wiki/Sysfs
https://en.wikipedia.org/wiki/Device_file#Character_devices
спагетти-код

Автор: alex87is

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js