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

Зміст першої частини:

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, total return 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, total return 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 loading module\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, яка звернулася на запис до пристрою, яке в свою чергу всі обнулив.

Так це виглядає з точки зору драйвера:



Перш ніж ми продовжимо, дуже раджу (але не обов'язково) для поглиблення почитати тут
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 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
спагетти-код
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.