Содержание первой части:
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, после чего, модуль станет частью операционной системы и получит больше функциональности и доступа к ресурсам чем обычные программы.
Есть много статей на эту тему, например эта очень хорошая. Надеюсь, что я смог донести интуицию.
Для начала, очень коротко рассмотрим пример простого модуля отсюда — потом рассмотрим, что такое Netfilter и совместим все вместе в одном исходном коде. Ниже, будут картинки, в конце статьи, ссылки на все исходные тексты. И так:
![image](https://habrastorage.org/r/w1560/files/987/920/127/98792012783144f4a50727c336ee7dee.png)
Так выглядит исходный код очень простого модуля. Несколько вещей:
1 – я работаю в редакторе Geany, так как Eclpise-со подобные в данном случае не подойдут. Компилирую через терминал, но кажется можно и через Geany.
2 – в kernel нет printf, но есть ее аналог printk, которая работает таким же образом. Кстати в kernel есть аналоги всех необходимых библиотек и функций (и нет стандартных библиотек типа stdlib).
3 – printk, не пишет в консоль, а пишет в специальный файл, содержимое которого можно прочитать, используя команду dmesg. У нас — это будет главный способ для отладки программы (есть и другие способы увидеть как работает printk, например, если перейти в консоль без X интерфейса Ctrl+Shift+F1). Так же будет полезным dmesg –c, для очистки всего, что накопилось в этом файле.
4 – MODULE_LICENSE, MODULE_AUTHOR, MODULE_DESCRIPTION — это макросы, которые «дают» linux информацию о модуле, которую потом можно получить с помощью специальных команд (искать подробнее по ссылке).
5 – В kernel используется синтаксист C89 для написания, когда на C. Например (и главное) — в отличии от привычного C99, все переменные должны быть объявлены в самом начале функций и не могут быть объявлены в другой ее части. Если вы НЕ будете соблюдать это правило, код скомпилируется, но с warnings (что плохо и не для нас).
Обратите внимание (ниже на картинке) на Makefile. Он отличается от обычных программ в user space. Так выглядит весь процесс:
![image](https://habrastorage.org/r/w1560/files/d19/4e8/5db/d194e85db93348ef984bfc04ed069c76.png)
Модуль загружается специальной командой “insmod ./module_name.ko” после чего запускается макрос module_init. Модуль удаляется из памяти компьютера командной “rmmod module_name”.
Проверяем. Просто загрузим модуль, а потом выгрузим и посмотрим на dmesg
![image](https://habrastorage.org/r/w1560/files/91f/7dc/a26/91f7dca2616a416fa686d11afffc1e83.png)
Готово. На этом введение в модули закончено.
Netfilter – это framework, встроенный в linux kernel, который позволяет производить различные сетевые операции. Нас будет интересовать перехват входящего и исходящего трафика.
Кстати, самая часто ассоциируемая программа с данным framework – iptables, которая позволяет динамично задавать правила фильтрации трафика (то есть — это ни что иное, как простой firewall) встроенный в Linux. В русской версии Wikipedia, почти не сделали различий между двумя терминами, что считаю не правильным, поэтому ссылку даю на английскую версию. Wikipedia — Netfilter
и на официальный сайт. На нем есть вся необходимая информация. www.netfilter.org
Архитектура перехвата(hooking) трафика в Netfilter выглядит следующим образом:
![image](https://habrastorage.org/r/w1560/files/ff5/453/5bd/ff54535bd7834a679f7f214af5b31cbf.png)
Фото
На схеме обозначены стадии, которые проходят пакеты после попадания на phy сетевой карты. Давайте рассмотрим поподробнее:
Математически:
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.
На каждой «остановке» мы сможем принять решение по поводу «пойманного пакета» — хотим ли мы его пропускать дальше, или нет (или что-то еще).
Для интереса, мы придумаем правило – все пакеты которые предназначены для компьютера FW_dev (тот где мы установим firewall), или которые FW_dev посылает от своего имени другим компьютерам – мы позволим пройти (accept). Весь остальной трафик, мы закроем (deny). Конечная цель – подсчитать сколько пакетов прошло, сколько мы запретили и передать данные пользователю (в user space).
Для этого вначале модуля, определим несколько глобальных переменных.
Теперь установим hook functions, в нужные точки перехвата трафика. Для начала напишем сами функции. Все функции должны иметь заранее определенный тип:
Код функций:
Следующий шаг – их необходимо зарегистрировать в системе, поэтому логично сделать это при загрузке модуля. Выглядит это следующим образом:
Тут стоит обратить внимание на поле hooknum, которому мы присваиваем значение, которое и определяет место перехвата пакетов и соответствует диаграмме выше. Последним шагом будет их удаление (дерегистрация) перед удалением модуля из ОС.
Осталось проверить работу, а также добавить user interface, чтобы любой пользователь, мог считать данные с нашего модуля. Сначала проверим.
![image](https://habrastorage.org/r/w1560/files/9d5/860/1d5/9d58601d52b04e79b79423df06331dcd.png)
Шлем с host2 одно ping сообщение на 10.0.2.3 (мы ожидаем, что модуль пропустит его, потому что, 10.0.2.3 – внутренний интерфейс. Если бы сообщение было для host1 – 10.0.1.1, тогда мы бы его не пропустили)
![image](https://habrastorage.org/r/w1560/files/529/3e8/ede/5293e8ede76d4341a119a5adbf26e6c8.png)
смотрим на «логи», выгружаем модуль
![image](https://habrastorage.org/r/w1560/files/8f5/071/cf7/8f5071cf7ccd41afb6af23d8089356ce.png)
На данном этапе можно поиграться с системой и убедиться, что трафик с 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 — тут я брал картинку :)
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'а.
2.2 — Таблицы Firewall. Transport Layer. Структуры TCP, UDP. Расширяем Firewall.
2.3 — Расширяем функциональность. Обарабатываем данные в user space. libnetfilter_queue.
2.4 — (*Опиционально) Изучаем реальную Buffer Overflow атаку и предотвращаем с помощью нашего Firewall'а.
Очень краткое введение в Операционные системы
Для тех, кто не знаком с основами работы операционных систем в общем и linux-систем в частности, ОЧЕНЬ кратко нужные нам основные понятия, чтобы мы могли двигаться дальше. В Linux, как и многих других ОС, есть два адресных пространства – kernel space и user space. Kernel space – тут работает операционная система, она в основном занимается тем, что распределяет ресурсы компьютера между программами (например какая из программ сейчас работает на процессоре (scheduling), какой программе направить output с клавиатуры, что делать с пакетами из сетевой карты, операции IO и много всего другого). ОС находится в очень интимных связях с аппаратным обеспечением (мышь, монитор, принтер…). Так как ее работа очень важна, для нее выделена отдельная часть в памяти, которая не связана с user space – местом, где работают большинство программ пользователя, такие как: редакторы, калькуляторы, браузеры и т.д. В Linux, чтобы поменять или добавить новые функции в ОС — есть несколько способов. Один из них — это поменять исходные тексты ядра и скомпилировать его заново. Но этот способ долгий и тяжелый, особенно когда нужно расширить функциональность минимально или динамично. Поэтому есть другой способ – модули. Модули – грубо говоря — это программа, которую можно динамично и быстро добавить в kernel space, после чего, модуль станет частью операционной системы и получит больше функциональности и доступа к ресурсам чем обычные программы.
Есть много статей на эту тему, например эта очень хорошая. Надеюсь, что я смог донести интуицию.
Написание простого модуля в Linux. Практика
Для начала, очень коротко рассмотрим пример простого модуля отсюда — потом рассмотрим, что такое Netfilter и совместим все вместе в одном исходном коде. Ниже, будут картинки, в конце статьи, ссылки на все исходные тексты. И так:
![image](https://habrastorage.org/files/987/920/127/98792012783144f4a50727c336ee7dee.png)
Так выглядит исходный код очень простого модуля. Несколько вещей:
1 – я работаю в редакторе Geany, так как Eclpise-со подобные в данном случае не подойдут. Компилирую через терминал, но кажется можно и через Geany.
2 – в kernel нет printf, но есть ее аналог printk, которая работает таким же образом. Кстати в kernel есть аналоги всех необходимых библиотек и функций (и нет стандартных библиотек типа stdlib).
3 – printk, не пишет в консоль, а пишет в специальный файл, содержимое которого можно прочитать, используя команду dmesg. У нас — это будет главный способ для отладки программы (есть и другие способы увидеть как работает printk, например, если перейти в консоль без X интерфейса Ctrl+Shift+F1). Так же будет полезным dmesg –c, для очистки всего, что накопилось в этом файле.
4 – MODULE_LICENSE, MODULE_AUTHOR, MODULE_DESCRIPTION — это макросы, которые «дают» linux информацию о модуле, которую потом можно получить с помощью специальных команд (искать подробнее по ссылке).
5 – В kernel используется синтаксист C89 для написания, когда на C. Например (и главное) — в отличии от привычного C99, все переменные должны быть объявлены в самом начале функций и не могут быть объявлены в другой ее части. Если вы НЕ будете соблюдать это правило, код скомпилируется, но с warnings (что плохо и не для нас).
Компиляция модуля
Обратите внимание (ниже на картинке) на Makefile. Он отличается от обычных программ в user space. Так выглядит весь процесс:
![image](https://habrastorage.org/files/d19/4e8/5db/d194e85db93348ef984bfc04ed069c76.png)
Модуль загружается специальной командой “insmod ./module_name.ko” после чего запускается макрос module_init. Модуль удаляется из памяти компьютера командной “rmmod module_name”.
Проверяем. Просто загрузим модуль, а потом выгрузим и посмотрим на dmesg
![image](https://habrastorage.org/files/91f/7dc/a26/91f7dca2616a416fa686d11afffc1e83.png)
Готово. На этом введение в модули закончено.
Введение в Netfilter. Теория
Netfilter – это framework, встроенный в linux kernel, который позволяет производить различные сетевые операции. Нас будет интересовать перехват входящего и исходящего трафика.
Кстати, самая часто ассоциируемая программа с данным framework – iptables, которая позволяет динамично задавать правила фильтрации трафика (то есть — это ни что иное, как простой firewall) встроенный в Linux. В русской версии Wikipedia, почти не сделали различий между двумя терминами, что считаю не правильным, поэтому ссылку даю на английскую версию. Wikipedia — Netfilter
и на официальный сайт. На нем есть вся необходимая информация. www.netfilter.org
Архитектура перехвата(hooking) трафика. Теория
Архитектура перехвата(hooking) трафика в Netfilter выглядит следующим образом:
![image](https://habrastorage.org/files/ff5/453/5bd/ff54535bd7834a679f7f214af5b31cbf.png)
Фото
На схеме обозначены стадии, которые проходят пакеты после попадания на 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 module\n");
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 output packet, accept\n");
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, accept\n");
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, drop\n");
dropped_num++;
return NF_DROP;
}
static int __init fw_module_init(void) {
printk("Starting FW module loading\n");
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 module\n");
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](https://habrastorage.org/files/9d5/860/1d5/9d58601d52b04e79b79423df06331dcd.png)
Шлем с host2 одно ping сообщение на 10.0.2.3 (мы ожидаем, что модуль пропустит его, потому что, 10.0.2.3 – внутренний интерфейс. Если бы сообщение было для host1 – 10.0.1.1, тогда мы бы его не пропустили)
![image](https://habrastorage.org/files/529/3e8/ede/5293e8ede76d4341a119a5adbf26e6c8.png)
смотрим на «логи», выгружаем модуль
![image](https://habrastorage.org/files/8f5/071/cf7/8f5071cf7ccd41afb6af23d8089356ce.png)
Что произошло?
В 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 — тут я брал картинку :)