Создание и тестирование Firewall в Linux, Часть 1.2. Простой перехват трафика с Netfilter

в 14:09, , рубрики: 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. Объединяем все вместе, тестируем.

Очень краткое введение в Операционные системы

Для тех, кто не знаком с основами работы операционных систем в общем и linux-систем в частности, ОЧЕНЬ кратко нужные нам основные понятия, чтобы мы могли двигаться дальше. В Linux, как и многих других ОС, есть два адресных пространства – kernel space и user space. Kernel space – тут работает операционная система, она в основном занимается тем, что распределяет ресурсы компьютера между программами (например какая из программ сейчас работает на процессоре (scheduling), какой программе направить output с клавиатуры, что делать с пакетами из сетевой карты, операции IO и много всего другого). ОС находится в очень интимных связях с аппаратным обеспечением (мышь, монитор, принтер…). Так как ее работа очень важна, для нее выделена отдельная часть в памяти, которая не связана с user space – местом, где работают большинство программ пользователя, такие как: редакторы, калькуляторы, браузеры и т.д. В Linux, чтобы поменять или добавить новые функции в ОС — есть несколько способов. Один из них — это поменять исходные тексты ядра и скомпилировать его заново. Но этот способ долгий и тяжелый, особенно когда нужно расширить функциональность минимально или динамично. Поэтому есть другой способ – модули. Модули – грубо говоря — это программа, которую можно динамично и быстро добавить в kernel space, после чего, модуль станет частью операционной системы и получит больше функциональности и доступа к ресурсам чем обычные программы.

Есть много статей на эту тему, например эта очень хорошая. Надеюсь, что я смог донести интуицию.

Написание простого модуля в Linux. Практика

Для начала, очень коротко рассмотрим пример простого модуля отсюда — потом рассмотрим, что такое Netfilter и совместим все вместе в одном исходном коде. Ниже, будут картинки, в конце статьи, ссылки на все исходные тексты. И так:

image

Так выглядит исходный код очень простого модуля. Несколько вещей:

1 – я работаю в редакторе Geany, так как Eclpise-со подобные в данном случае не подойдут. Компилирую через терминал, но кажется можно и через Geany.

2 – в kernel нет printf, но есть ее аналог printk, которая работает таким же образом. Кстати в kernel есть аналоги всех необходимых библиотек и функций (и нет стандартных библиотек типа stdlib).

3 – printk, не пишет в консоль, а пишет в специальный файл, содержимое которого можно прочитать, используя команду dmesg. У нас — это будет главный способ для отладки программы (есть и другие способы увидеть как работает printk, например, если перейти в консоль без X интерфейса Ctrl+Shift+F1). Так же будет полезным dmesg –c, для очистки всего, что накопилось в этом файле.

4MODULE_LICENSE, MODULE_AUTHOR, MODULE_DESCRIPTION — это макросы, которые «дают» linux информацию о модуле, которую потом можно получить с помощью специальных команд (искать подробнее по ссылке).

5 – В kernel используется синтаксист C89 для написания, когда на C. Например (и главное) — в отличии от привычного C99, все переменные должны быть объявлены в самом начале функций и не могут быть объявлены в другой ее части. Если вы НЕ будете соблюдать это правило, код скомпилируется, но с warnings (что плохо и не для нас).

Компиляция модуля

Обратите внимание (ниже на картинке) на Makefile. Он отличается от обычных программ в user space. Так выглядит весь процесс:

image

Модуль загружается специальной командой “insmod ./module_name.ko” после чего запускается макрос module_init. Модуль удаляется из памяти компьютера командной “rmmod module_name”.

Проверяем. Просто загрузим модуль, а потом выгрузим и посмотрим на dmesg

image

Готово. На этом введение в модули закончено.

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

Netfilter – это framework, встроенный в linux kernel, который позволяет производить различные сетевые операции. Нас будет интересовать перехват входящего и исходящего трафика.

Кстати, самая часто ассоциируемая программа с данным framework – iptables, которая позволяет динамично задавать правила фильтрации трафика (то есть — это ни что иное, как простой firewall) встроенный в Linux. В русской версии Wikipedia, почти не сделали различий между двумя терминами, что считаю не правильным, поэтому ссылку даю на английскую версию. Wikipedia — Netfilter
и на официальный сайт. На нем есть вся необходимая информация. www.netfilter.org

Архитектура перехвата(hooking) трафика. Теория

Архитектура перехвата(hooking) трафика в Netfilter выглядит следующим образом:

image

Фото

На схеме обозначены стадии, которые проходят пакеты после попадания на phy сетевой карты. Давайте рассмотрим поподробнее:

Prerouting – сюда попадают все пакеты, которые пришли на сетевую карту устройства извне (например кто-то из сети, пытает послать нам mail, или мы на пути следования пакета и должны передать его дальше).
Forward – если полученный пакет предназначен не для данного IP адреса, то операционная система перешлет его дальше (помните forward enable из предыдущей части?) или выкинет, если посчитает его ненужным.
Input – сюда попадут все пакеты, которые предназначены для какой-либо аппликации. Например, это могут быть пакеты для браузера, после запроса какой-либо страницы.
Output – тут появляются все пакеты, которые аппликации компьютера посылают в сеть (то есть не те, которым мы делаем forwarding). Например, тот самый запрос браузера получить страницу из интернета. Или ping.
Postrouting – объединяет все исходящие пакеты.

Математически:

Input data = Prerouting = Forward + Input
Output data = Postrouting = Output + Forward

Конечно же, обычно input != output (если вы не поняли почему — то стоит перечитать тему еще раз или посмотреть дополнительно в интернете).

Например, после того как мы введём в строке браузера www.site.com то пакет пройдет две «остановки» прежде чем попадет в сеть – Output и Postrouting.

Когда site.com будет нам отвечать, то пакет пройдет две «станции», прежде чем попасть к браузеру - Input data и Prerouting.

Другие пакеты, например, с host1 -> host2, пройдут через Input, Forward, Output.
На каждой «остановке» мы сможем принять решение по поводу «пойманного пакета» — хотим ли мы его пропускать дальше, или нет (или что-то еще).

Архитектура перехвата(hooking) трафика. Практика

Для интереса, мы придумаем правило – все пакеты которые предназначены для компьютера FW_dev (тот где мы установим firewall), или которые FW_dev посылает от своего имени другим компьютерам – мы позволим пройти (accept). Весь остальной трафик, мы закроем (deny). Конечная цель – подсчитать сколько пакетов прошло, сколько мы запретили и передать данные пользователю (в user space).

Для этого вначале модуля, определим несколько глобальных переменных.

static unsigned int accepted_num = 0;
static unsigned int dropped_num = 0;

Теперь установим hook functions, в нужные точки перехвата трафика. Для начала напишем сами функции. Все функции должны иметь заранее определенный тип:

unsigned int hook_func(unsigned int hooknum, struct sk_buff *skb,
		const struct net_device *in, const struct net_device *out,
		int (*okfn)(struct sk_buff *)) {}

Код функций:

// hook out packets, accept packet
unsigned int hook_func_out(unsigned int hooknum, struct sk_buff *skb,
		const struct net_device *in, const struct net_device *out,
		int (*okfn)(struct sk_buff *)) {
	accepted_num++;
	return NF_ACCEPT;
}

// hook in packets, accept packet
unsigned int hook_func_in(unsigned int hooknum, struct sk_buff *skb,
		const struct net_device *in, const struct net_device *out,
		int (*okfn)(struct sk_buff *)) {
	accepted_num ++;
	return NF_ACCEPT;
}

// hook forward packets, drop packet
unsigned int hook_func_forward(unsigned int hooknum, struct sk_buff *skb,
		const struct net_device *in, const struct net_device *out,
		int (*okfn)(struct sk_buff *)) {
	dropped_num++;
	return NF_DROP;
}

Следующий шаг – их необходимо зарегистрировать в системе, поэтому логично сделать это при загрузке модуля. Выглядит это следующим образом:

// hook functions structs for registration usage
static struct nf_hook_ops nfho_forward;
static struct nf_hook_ops nfho_out;
static struct nf_hook_ops nfho_in;

static int __init fw_module_init(void)
{
.....
// netfilter functions
printk("initialize kernel modulen");

	nfho_in.hook = hook_func_in;
	nfho_in.hooknum = NF_INET_LOCAL_IN;
	nfho_in.pf = PF_INET;
	nfho_in.priority = NF_IP_PRI_FIRST;
	nf_register_hook(&nfho_in); // Register the hook

	nfho_out.hook = hook_func_out;
	nfho_out.hooknum = NF_INET_LOCAL_OUT;
	nfho_out.pf = PF_INET;
	nfho_out.priority = NF_IP_PRI_FIRST;
	nf_register_hook(&nfho_out); // Register the hook

	nfho_forward.hook = hook_func_forward;
	nfho_forward.hooknum = NF_INET_FORWARD;
	nfho_forward.pf = PF_INET;
	nfho_forward.priority = NF_IP_PRI_FIRST;
	nf_register_hook(&nfho_forward); // Register the hook
…
}

Тут стоит обратить внимание на поле hooknum, которому мы присваиваем значение, которое и определяет место перехвата пакетов и соответствует диаграмме выше. Последним шагом будет их удаление (дерегистрация) перед удалением модуля из ОС.

static void __exit fw_module_exit(void)
{
…
// net filter functions
	nf_unregister_hook(&nfho_in);
	nf_unregister_hook(&nfho_out);
	nf_unregister_hook(&nfho_forward);
// end netfilter functions
…
}

Осталось проверить работу, а также добавить user interface, чтобы любой пользователь, мог считать данные с нашего модуля. Сначала проверим.

Предфинальный код

#include <linux/module.h>
#include <linux/netfilter_ipv4.h> 
#include "fw.h" 

MODULE_AUTHOR( AUTHOR); 
MODULE_DESCRIPTION( DESCRIPTION);
MODULE_VERSION( VERSION); 
MODULE_LICENSE("GPL"); 

static unsigned int accepted_num; 
static unsigned int dropped_num; 

// hook functions 
static struct nf_hook_ops nfho_forward; 
static struct nf_hook_ops nfho_out; 
static struct nf_hook_ops nfho_in; 

// hook out packets 
unsigned int hook_func_out(unsigned int hooknum, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, 		int (*okfn)(struct sk_buff *)) {
 	printk("Get input packet, acceptn");
 	accepted_num++;
 	return NF_ACCEPT; 
} 

// hook in packets 
unsigned int hook_func_in(unsigned int hooknum, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *)) {
 	printk("Get input packet, acceptn");
 	accepted_num++;
 	return NF_ACCEPT;
 } 

// hook forward packets 
unsigned int hook_func_forward(unsigned int hooknum, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *)) {
 	printk("Get forward packet, dropn");
 	dropped_num++;
 	return NF_DROP;
} 

static int __init fw_module_init(void) {
 	printk("Starting FW module loadingn");
 	accepted_num = 0;
 	dropped_num = 0;
 
	nfho_in.hook = hook_func_in;
 	nfho_in.hooknum = NF_INET_LOCAL_IN;
 	nfho_in.pf = PF_INET;
 	nfho_in.priority = NF_IP_PRI_FIRST;
 	nf_register_hook(&nfho_in); // Register the hook 
	nfho_out.hook = hook_func_out;
 	nfho_out.hooknum = NF_INET_LOCAL_OUT;
 	nfho_out.pf = PF_INET;
 	nfho_out.priority = NF_IP_PRI_FIRST;
 	nf_register_hook(&nfho_out); // Register the hook 
	nfho_forward.hook = hook_func_forward;
 	nfho_forward.hooknum = NF_INET_FORWARD;
 	nfho_forward.pf = PF_INET;
 	nfho_forward.priority = NF_IP_PRI_FIRST;
 	nf_register_hook(&nfho_forward);  // Register the hook 
	return 0; 
} 

static void __exit fw_module_exit(void) 
{
 	printk("Removing FW modulen"); 
	nf_unregister_hook(&nfho_in);
 	nf_unregister_hook(&nfho_out);
 	nf_unregister_hook(&nfho_forward); 
} 

module_init( fw_module_init); 
module_exit( fw_module_exit); 

Компилируем, загружаем модуль

image

Шлем с host2 одно ping сообщение на 10.0.2.3 (мы ожидаем, что модуль пропустит его, потому что, 10.0.2.3 – внутренний интерфейс. Если бы сообщение было для host110.0.1.1, тогда мы бы его не пропустили)

image

смотрим на «логи», выгружаем модуль

image

Что произошло?

В 8354 – я загрузил модуль.
В 8356 – мы обнаружили какой-то исходящий пакет. На данном этапе мы не можем знать, что это, но скорее всего один из пакетов DHCP интерфейса, который мы настроили.
В 8359 – мы обнаружили входящий пакет — это был наш ping. После того как мы его получили, мы сразу послали ответ который и видим дальше.
В 8359 – ответ на ping.
В 8394 – выгрузили модуль.

На данном этапе можно поиграться с системой и убедиться, что трафик с host1 -> host2, не проходит.

На этом пока все, список ссылок:

» The Linux Kernel Module Programming Guide
» How to Write Your Own Linux Kernel Module with a Simple Example
» https://en.wikipedia.org/wiki/Netfilter
www.netfilter.org
» Firewall or Packet Filtering — тут я брал картинку :)

Автор: alex87is

Источник

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


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