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

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

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. Теория


Я советую для чтения английскую версию статьи Википедии, как более точную и объемную по этой теме чем русскую. Для нас будет достаточно понимания, что разные аппаратные устройства(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 messages\n");
	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 messages\n");
	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);

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



Уже сейчас можно видеть устройство в /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 messages\n");
 	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 messages\n");
 	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 loading\n"); 
	…
	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 %d\n", 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 = %d\n", 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 = %d\n", fd);
		return fd;
	}

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

	close(fd);
	return 0;
}

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

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

	return 0;
}


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



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

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



Прежде чем мы продолжим, очень советую (но не обязательно) для углубления почитать тут. А также тут. Внизу еще есть ссылка на хорошую бесплатную книгу.

Введение в 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 without\n");
    } 
    retval = device_create_file(fw_device, &dev_attr_droppedMessages);
    if (retval < 0) {
        printk("failed to create droppedMessages /sys endpoint - continuing without\n"); 
    }
  …
}

Вначале модуля, добавляем макросы 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 = %d\n", fd);
		return fd;
	}
	if(read(fd, &msg, 255)>0){	
		printf("Accepted packets number: %s\n", msg);
	} else {
		printf("Nothing to read\n");
	}
	close(fd);
	return 0;
}

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



Accepted packets: делаем ping



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



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



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



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





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





Мы видим, что без нашего модуля, 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
» спагетти-код
Поделиться публикацией
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 26
  • +5
    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 messages\n");
    	return sprintf(buffer, "%u", accepted_num + dropped_num); 
    }
    

    При использовании printk строка формата должна начинаться с одного из макросов KERN_*, определяющих уровень важности сообщения, или с KERN_CONT, если нужно продолжить с последним использованным уровнем важности.

    Но это несущественная ошибка, в отличие от sprintf в пользовательский буфер.

    Что будет, если пользователь передаст в параметре buffer невалидный пользовательский адрес?
    Или что будет, если пользователь передаст в параметре buffer виртуальный адрес где-нибудь в области данных ядра?
    В первом случае приложение будет завершено с SIGSEGV и сообщением ядра о недопустимом обращении ядра к виртуальному адресу, хотя достаточно было бы одного SIGSEGV.
    Во втором случае память ядра будет перезаписана вашей строкой, что вряд ли закончится хорошо.

    Для безопасного копирования данных в/из пространства пользователя есть функции copy_to_user и copy_from_user, а так же strlen_user/strnlen_user/strncpy_from_user.

    Кроме того, вы проигнорировали параметр length, из-за чего в следующем фрагменте пользовательского кода происходит переполнение буфера msg:

    int all_msg() {
    	char msg[1] = {0};
    	int fd = open("/dev/device_fw", O_RDONLY);
    	if (fd<0) {
    		printf("Device access error, fd = %d\n", fd);
    		return fd;
    	}
    
    	if(read(fd, &msg, 1)>0){	
    		printf("Accepted packets number: %s\n", msg);
    	} else {
    		printf("Nothing to read\n");
    	}
    
    	close(fd);
    	return 0;
    }
    


    static ssize_t fw_device_write(struct file *fp, const char *buff, size_t length, loff_t *ppos) 
    

    Второй параметр этой функции тоже имеет атрибут __user.

    Правильность использования атрибута __user можно проверить, передав параметр C=1 команде make собирающей модуль.
    • –3
      Вы совершенно правы, но я показывал концепт. Надюесь, что если люди будут писать продакшн код, то обо всех этих конечно позаботятся :) А что будет — вы все сами ответили.
      • +4
        я показывал концепт.

        Моё мнение — код, приводимый в качестве примера должен быть настолько близок к идеалу, насколько возможно, как по содержанию, так и по форме. Потому что, по сравнению с «продакшн кодом» который где-то просто работает, из этого кода существенно большее количество людей извлечёт что-то для себя, начиная от использованных приёмов и заканчивая скопипащенными строчками.
        • 0
          Очень надеюсь, что люди, которые пишут код на С, да и впринципе на языках где нужно «вручную» управлять памятью, будут достаточно умны чтобы не копипастить этот код. Но так как, за всех сказать не могу, то соглашусь, что, есть тут недочет, вызванный ленью, что не может быть оправданием. Я обязательно учту это в будущем и постараюсь «починить» тут, если будет время, спасибо!
          • 0
            Как по мне, достаточно в начале статьи оговорить, что код не для продакшена и проверки отсуствуют
            • 0
              по уму, стоило бы все правильно написать, но когда писал, не думал, что где-то сидит гипотетический человек, который не умеет программировать и которому доверят писать что-то важное в Linux, из этой темы и что он додумается просто селать копи-паст, и не будет знать о проблемах кода для неучебных целей, и эта программа выйдет попадет к кому-то и что-то сломает ему)) честно говоря я и сейчас не думаю, что такой человек есть) но все же стоит писать «как надо»)
          • 0
            Да, наверное сказалась моя неопытность в написнии статей, первый раз. Еще раз спасибо — постраюсь исправить.
            • 0
              Отличная наглядная статья, продолжайте в том же духе!
    • НЛО прилетело и опубликовало эту надпись здесь
      • 0
        Kernel module на Rust?
        • НЛО прилетело и опубликовало эту надпись здесь
          • 0
            Первая строка в google:
            https://github.com/tsgates/rust.ko
            • НЛО прилетело и опубликовало эту надпись здесь
              • 0

                То же самое что и на С++, вам придется самим реализовать C++/Rust рантайм. В минимальном варианте это возможно, что и сделано по линку выше. В полноценном — весьма сомнительное удовольствие.

      • 0
        мы не используем goto в программировании, потому что это очень сильно портит понимание, содержание, читаемость кода

        Каким таким загадочно-непостижимым образом явное указание перехода на определенную метку может испортить понимание, содержание и читаемость кода???? Не понимаю…
        Второе. Для того чтобы программа заработала(а не делала вид, что она что-то там делает) программист должен учесть все возможные ситуации при работе с программой, как то — ввод символьных данных вместо цифровых, длина вводимой строки, и т.д. и т.п. То есть он должен понимать что он делает и для чего. И тогда «дыр» в программах будет гораздо меньше. Хотя я совершенно не понимаю как можно писать программу(например) для работы с электронной почтой, а потом использовать её для доступа к файлам с паролями. Как по мне это очень сильно постараться нужно так сделать. Ну в смысле накосячить.
        Как говориться «Проверьте свою программу на ошибки. Если в вашей программе их нет, то проверьте программу еще раз. Если снова ошибки не найдены, то вы плохой программист»
        • 0
          Каким таким загадочно-непостижимым образом явное указание перехода на определенную метку может испортить понимание, содержание и читаемость кода???? Не понимаю…

          Вы серьезно? Там ссылочка для вас на термин спагетти-код.

          Второе. Это уже обсуждалось. Просто take it easy. Это всего лишь учебные статьи начального уровня. Комплекст статей. Блин, если есть человек, которому доверили писать kernel device и он не знает, что input и size надо проверять, то пусть его просто уволят, а он идет учиться программировать. Цель статьи — не учить его программировать.
          • 0
            А в чем же тогда цель статьи? Несколько перефразируя Портоса «Я пишу потому что… я пишу!»? Уж если и писать то только так чтобы было лучше всех, ну или почти всех. И описывать уникальный и неповторимый опыт создания чего-то(программы, автомобиля, велосипеда, самолёта...) Как по мне так уж если писать только для галочки и абы как то уж лучше совсем не писать. В последнее время стало считаться ээээ… хорошим тоном, что ли, написать небрежно и не полно о чем бы то ни было. И потом страшно гордиться этим. Ярчайшим примером правильно написанной статьи я считаю серию статей Ю.Зальцмана в журнале «Информатика и Образование» «Архитектура и ассемблер БК». Я по его статьям учился программировать на ассемблере. Научился быстро и легко. Потому как говорил один из философов «Кто ясно мыслит, тот ясно излагает». Хотелось бы чтобы все авторы стремились к этому.
            • 0
              Цель всех шести статей, дать человеку, который находится на начальном уровне, например студентам старших курсов, возможность углубиться в тему операционных систем, сетей, разработки под Линукс, сети и немного безопасности. Дать возможность сделать это на живой примере, пройдя этап за этап. Например, в одном ЛС, у меня спрашивали по части 1.1 про эксперементы, которые можно провести в подобной виртуальной среде, а потом сказали, что уже начали пробовать сами разные вещи. Я считаю, что это лучшим образом говорит о цели статей. Я же, следующими частями, расширяю поле для учебы и подобных эксперементов.
              Ярчайшим примером правильно написанной статьи я считаю серию статей Ю.Зальцмана в журнале «Информатика и Образование» «Архитектура и ассемблер БК». Я по его статьям учился программировать на ассемблере.

              Это прекрасно! Я вас с этим поздравляю, хотя навернека, после этих статей еще есть куда расти. Напомнию, что цели моих статей НЕ входит учеба программированию. И если с человеком выше, я еще был согласен, что надо бы переписать часть кода, то вот сейчас я все больше убеждаюсь, что все наоборот прекрасно.
              • 0
                что все наоборот прекрасно.

                конечно… конечно… лучше просто не бывает!)))))))
                • 0
                  лучше всегда бывает) не бывает идеала)
              • 0
                Из таких статей очень неплохо можно оценить общую мысль и концепцию. Придираться к учебным пособиям, что они недостаточно приближены к реальности это даже хз как — суть академических примеров показать вырожденный случай и общую концепцию, не отвлекаясь на незначимые вещи.
                • 0
                  да-да-да… Хотелось бы чтобы Вас обучали парашютному спорту по таким пособиям, ээээ… недостаточно приближенным к реальности… Было бы ооочень любопытно посмотреть на сам процесс… Результат будет однозначным.
                  • 0
                    Не сомневаюсь, что первые уроки по паршютному спорту(соственно любые первые уроки в любой области), ооооооооочен далеки от реального процесса. Профессионализм приходит со временем, опытом, работой, учебой и тд.
                    • 0
                      ага… Напоминает старый анекдот «Технику безопасности при работе на токарном станке я знаю как свои три пальца...»
                      • 0
                        Как там, в автошколе на первом уроке пятаки вокруг столбов крутили?

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.