Содержание первой части:
Содержание второй части:
2.1 — Введение во вторую часть. Смотрим на сеть и протоколы. Wireshark.
2.2 — Таблицы Firewall. Transport Layer. Структуры TCP, UDP. Расширяем Firewall.
2.3 — Расширяем функциональность. Обрабатываем данные в user space. libnetfilter_queue.
2.4 — Бонус. Изучаем реальную Buffer Overflow атаку и предотвращаем с помощью нашего Firewall'а.
В данной части мы почти закончим изучать базу, достаточную для имплементации простого firewall, но прежде, чем мы это сделаем (предполагается, что читатель имеет знания в сетях или читал часть 2.1), необходимо рассмотреть, каким образом firewall принимает решения.
Такая таблица правил загружается пользователем (администратором) в память firewall. Именно они определяют при получении пакетов, что с ними делать – принять или отклонить.
Важно! Когда firewall получает пакет, он обязательно просматривает его поля (то, чем мы занимались в уроке 2.1) и сверяет их с правилами из таблицы в том порядке (!), как эти правила записаны в ней (сверху вниз!). Иными словами, есть принципиальная разница, какое правило будет в таблице выше, а какое ниже.
Важно! Любой странный пакет не должен попасть в защищаемую нами сеть или устройство. Кроме того, если мы не знаем, можно ли его пропускать или нет, то ответ – НЕТ. Мы пропускаем только те пакеты, которые разрешены в правилах firewall.
Отсюда принцип построения и работы firewall: во внутреннюю сеть (у нас это host1) могут (и должны) попасть только те пакеты, которые мы разрешили.
Теперь к примеру. В таблице выше определены пять правил. При получении каждого пакета мы должны удостовериться, что только если мы нашли для него подходящее правило, и в Action прописано – accept, только тогда мы его пропускаем. Если мы не нашли подходящего правила после проверки каждого из них, то мы выкидываем пакет вне зависимости от его содержания. Для этого есть последнее правило default, которое определяет – выкинуть любой пакет, не попавший ни под одно из правил. Оно обязательно должно быть в самом конце (на самом деле, все firewall добавляют его автоматически, даже если оно не прописано).
Теперь более подробно о полях direction и ack.
Direction – определяет, входит ли пакет в нашу сеть или выходит. Например, возможно, мы захотим запретить все пакеты smtp(почта) протокола, чтобы избежать утечки информации через e-mail. Либо наоборот – мы захотим запретить любой входящий пакет по telnet протоколу, чтобы запретить любую попытку подключиться к нашей сети. Мы рассмотрим в практической части, как в нашем случае определить направление пакета в коде.
Первые два правила называются spoof , и они выполняют тривиальную защиту от тривиальных попыток атак. Так, spoof1 означает «любой входящий пакет (direction = in) с адресом нашей сети (10.0.1.1 = host1) для любых номеров портов, протоколов и т.д. – выкинуть». Логика этого правила в том, что не может на firewall прийти пакет в нашу сеть, и при этом в нем указано, что послан он из нашей же сети (src ip = 10.0.1.1). Другими словами, это означает, что кто-то его подделал и пытается замаскироваться под одного из пользователей (в данном случае host1) – такой пакет мы не хотим пропускать.
Симметричное правило и spoof2 – мы не хотим выпускать пакеты из внутренней сети, если в нем написано, что он изначально с IP, отличным от внутренних адресов (то есть НЕ 10.0.1.1). Скорее всего, это тоже какой-нибудь вирус.
ACK – это флаг (один бит), который используется для установки соединения при использовании TCP протокола и дальнейшего сопровождения его «надежности». Каждое TCP соединение начинается с тройного рукопожатия (3 way handshake, на русском статьи нет, но есть анимация на английском тут: https://en.wikipedia.org/wiki/Handshaking#TCP_three-way_handshake
Нам важно понимать, что при каждом новом открытии TCP сессии, только в первом пакете ACK = 0, во всех остальных пакетах созданной сессии – ACK > 0 (https://ru.wikipedia.org/wiki/TCP).
Благодаря этому факту, мы можем отличить уже существующее соединение от попытки его открытия. Если ACK = 0, то это попытка создать TCP соединение (первый пакет в тройном рукопожатии), если ACK = 1, то соединение должно было быть создано до этого (и если это не так, то логично не давать таким пакетам пройти в сеть).
Теперь посмотрим на правило http_in, http_out:
http_in означает следующее: если пакет входящий (direction = in), с любого IP (Src IP = any, обратите внимание, что правила spoof выше, на данном этапе, гарантируют нам, что это не IP внутренней сети), предназначенный для нашей сети (Dest IP == host1 == 10.0.1.1), посланный по протоколу TCP, на порт 80 (то есть, известный всем http сервер), с любого порта (>1023 обозначает любой незарезервированный порт, который мы получаем от операционной системы при создании связи и в будущем используется для идентификации именно этого соединения, как описано в части 2.1), Ack = Any это означает, что мы разрешаем компьютеру извне попросить открыть соединение (в первом пакете ack=0, а следующие ack>0). И такие пакеты мы принимаем и пропускаем дальше в сеть (action = accept).
http_out симметрично, с разницей в том, что мы не пропустим пакеты с ack=0, только ack>0, то есть мы не позволим создавать http-соединение с нашего компьютера в internet, но при этом сможем отвечать на уже созданное в http-соединение.
Другими словами, правила http разрешают доступ к нашей сети через http извне, но запрещают пользователям нашей сети пользоваться http (то есть доступ к internet сайтам).
Возвращаясь к нашему модулю, напоминаю, что так выглядит функция перехвата:
Давайте смотреть на параметры:
hooknum – номер перехвата, это мы уже проходили
const struct net_device *in, out – указатели на структуры сетевого интерфейса
struct sk_buff *skb – самое интересное для нас — указатель, содержащий нужные нам данные
SKB – socket buffer, это базовая структура в сети linux. Она имеет множество полей и может быть отдельным предметом для написания статьи. Я нашел пару хороших ссылок для желающих углубиться:
http://vger.kernel.org/~davem/skb.html
https://people.cs.clemson.edu/~westall/853/notes/skbuff.pdf
Нас интересуют:
Следующим образом
мы получаем указатель на IP header (в части 2.1 мы говорили, что главная информация на этом уровне для нас — это IP source, IP destination).
Определение skb_network_header из inclue/linux/skbuff.h
http://lxr.free-electrons.com/source/include/linux/skbuff.h?v=3.0#L1282
То есть мы видим, что функция возвращает нужный указатель из «нужного» места в структуре struct skbuff.
И теперь, когда у нас есть доступ к IP header, мы можем получить IP адреса:
А также номер протокола:
ip_header->protocol
Для TCP(и аналогично для UDP) номера портов:
Внизу я приведу полностью код функции. Интересным моментом будет использование функции ntohs. ntohs – это функция, которая меняет порядок битов (представления числа). Есть два используемых вида представления числа в памяти – little endian и big endian. Для представления чисел в сети используется система big endian, в то время как в архитектуре Intel little endian (Порядок байтов)
Поэтому для получения правильных чисел необходимо использовать эти транслирующие функции.
Ниже текст всей функции, которая при получении пакета печатает все необходимые данные для принятия решения по правилам firewall:
Большинство (если не все) примеры написания модулей или применения netfilter, ограничиваются одним исходным файлом и парой десятков строчек кода. Но большие проекты невозможно (и неправильно) умещать в одном исходном файле; и хотя описываемый мною пример можно впихнуть в один файл, я решил разделить его на module_fw.c – все что касается char device, sysfs, kernel module и hook_functions.c – функциональность перехвата. При компиляции kernel модуля, состоящего из нескольких файлов, есть небольшой трюк, который надо знать, ниже пример:
Тут стоит обратить внимание на строчку:
obj-m := fw.o
нет такого файла fw.c, поэтому это имя модуля, который будет создан. Также, это приставка для следующей строчки, которая описывает все файлы, относящиеся к модулю
fw-objs +=
Нужно знать, что, конечно, имя модуля и исходных текстов не должны совпадать. В остальном все остается по-прежнему.
Для проверки я быстренько сконфигурировал dhcp интерфейсы (смотреть часть 1) и поставил на host1, http server – apache2, а на host2 – lynx – текстовой браузер (хотя можно было обойтись и telnet). Запускаем
lynx 10.0.1.1
Смотрим, что выдает наш firewall:
Ну, вот вроде и все.
В этой части мы рассмотрели, как работают таблицы правил в firewall, которые определяют политику защиты и пропуска трафика в сети. После этого разобрали одну из базовых сетевых структур skbuf в Linux и, благодаря этому, смогли дополнить нашу программу всей нужной информацией для дополнения поддержки таблиц в нашем модуле. То, что осталось – написать загрузку этой таблицы через sysfs, как мы это делали в части 1, и добавить if {} else if {} else {} … в функцию hook_func_forward. Это я уже оставлю желающим, так как тут ничего принципиально нового не будет…ну, может только работа с klist, но это уже совсем другая тема, которая тоже хорошо освещена в интернете.
В самой функции можно найти бонус, обозначенный как XMAS packet, и почитать, что это и зачем в интернете, а следующую часть мы начнем тву (это «тут»?) —
Ссылки:
wikipedia.org/wiki/Handshaking#TCP_three-way_handshake
ru.wikipedia.org/wiki/TCP
vger.kernel.org/~davem/skb.html
people.cs.clemson.edu/~westall/853/notes/skbuff.pdf
lxr.free-electrons.com/source/include/linux/skbuff.h?v=3.0#L1282
Порядок байтов
Создание лаборатории, архитектура Netfilter, char device, sysfs
1.1 — Создание виртуальной лаборатории (чтобы нам было, где работать, я покажу, как создать виртуальную сеть на вашем компьютере. Сеть будет состоять из 3х машин Linux ubuntu).
1.2 – Написание простого модуля в Linux. Введение в Netfilter и перехват трафика с его помощью. Объединяем все вместе, тестируем.
1.3 – Написание простого char device. Добавление виртуальной файловой системы — sysfs. Написание user interface. Объединяем все вместе, тестируем.
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'а.
Firewall rules. Теория.
В данной части мы почти закончим изучать базу, достаточную для имплементации простого firewall, но прежде, чем мы это сделаем (предполагается, что читатель имеет знания в сетях или читал часть 2.1), необходимо рассмотреть, каким образом firewall принимает решения.
Такая таблица правил загружается пользователем (администратором) в память firewall. Именно они определяют при получении пакетов, что с ними делать – принять или отклонить.
Важно! Когда firewall получает пакет, он обязательно просматривает его поля (то, чем мы занимались в уроке 2.1) и сверяет их с правилами из таблицы в том порядке (!), как эти правила записаны в ней (сверху вниз!). Иными словами, есть принципиальная разница, какое правило будет в таблице выше, а какое ниже.
Важно! Любой странный пакет не должен попасть в защищаемую нами сеть или устройство. Кроме того, если мы не знаем, можно ли его пропускать или нет, то ответ – НЕТ. Мы пропускаем только те пакеты, которые разрешены в правилах firewall.
Отсюда принцип построения и работы firewall: во внутреннюю сеть (у нас это host1) могут (и должны) попасть только те пакеты, которые мы разрешили.
Теперь к примеру. В таблице выше определены пять правил. При получении каждого пакета мы должны удостовериться, что только если мы нашли для него подходящее правило, и в Action прописано – accept, только тогда мы его пропускаем. Если мы не нашли подходящего правила после проверки каждого из них, то мы выкидываем пакет вне зависимости от его содержания. Для этого есть последнее правило default, которое определяет – выкинуть любой пакет, не попавший ни под одно из правил. Оно обязательно должно быть в самом конце (на самом деле, все firewall добавляют его автоматически, даже если оно не прописано).
Теперь более подробно о полях direction и ack.
Direction – определяет, входит ли пакет в нашу сеть или выходит. Например, возможно, мы захотим запретить все пакеты smtp(почта) протокола, чтобы избежать утечки информации через e-mail. Либо наоборот – мы захотим запретить любой входящий пакет по telnet протоколу, чтобы запретить любую попытку подключиться к нашей сети. Мы рассмотрим в практической части, как в нашем случае определить направление пакета в коде.
Первые два правила называются spoof , и они выполняют тривиальную защиту от тривиальных попыток атак. Так, spoof1 означает «любой входящий пакет (direction = in) с адресом нашей сети (10.0.1.1 = host1) для любых номеров портов, протоколов и т.д. – выкинуть». Логика этого правила в том, что не может на firewall прийти пакет в нашу сеть, и при этом в нем указано, что послан он из нашей же сети (src ip = 10.0.1.1). Другими словами, это означает, что кто-то его подделал и пытается замаскироваться под одного из пользователей (в данном случае host1) – такой пакет мы не хотим пропускать.
Симметричное правило и spoof2 – мы не хотим выпускать пакеты из внутренней сети, если в нем написано, что он изначально с IP, отличным от внутренних адресов (то есть НЕ 10.0.1.1). Скорее всего, это тоже какой-нибудь вирус.
ACK – это флаг (один бит), который используется для установки соединения при использовании TCP протокола и дальнейшего сопровождения его «надежности». Каждое TCP соединение начинается с тройного рукопожатия (3 way handshake, на русском статьи нет, но есть анимация на английском тут: https://en.wikipedia.org/wiki/Handshaking#TCP_three-way_handshake
Нам важно понимать, что при каждом новом открытии TCP сессии, только в первом пакете ACK = 0, во всех остальных пакетах созданной сессии – ACK > 0 (https://ru.wikipedia.org/wiki/TCP).
Благодаря этому факту, мы можем отличить уже существующее соединение от попытки его открытия. Если ACK = 0, то это попытка создать TCP соединение (первый пакет в тройном рукопожатии), если ACK = 1, то соединение должно было быть создано до этого (и если это не так, то логично не давать таким пакетам пройти в сеть).
Теперь посмотрим на правило http_in, http_out:
http_in означает следующее: если пакет входящий (direction = in), с любого IP (Src IP = any, обратите внимание, что правила spoof выше, на данном этапе, гарантируют нам, что это не IP внутренней сети), предназначенный для нашей сети (Dest IP == host1 == 10.0.1.1), посланный по протоколу TCP, на порт 80 (то есть, известный всем http сервер), с любого порта (>1023 обозначает любой незарезервированный порт, который мы получаем от операционной системы при создании связи и в будущем используется для идентификации именно этого соединения, как описано в части 2.1), Ack = Any это означает, что мы разрешаем компьютеру извне попросить открыть соединение (в первом пакете ack=0, а следующие ack>0). И такие пакеты мы принимаем и пропускаем дальше в сеть (action = accept).
http_out симметрично, с разницей в том, что мы не пропустим пакеты с ack=0, только ack>0, то есть мы не позволим создавать http-соединение с нашего компьютера в internet, но при этом сможем отвечать на уже созданное в http-соединение.
Другими словами, правила http разрешают доступ к нашей сети через http извне, но запрещают пользователям нашей сети пользоваться http (то есть доступ к internet сайтам).
Firewall rules. Практика.
Возвращаясь к нашему модулю, напоминаю, что так выглядит функция перехвата:
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 *));
Давайте смотреть на параметры:
hooknum – номер перехвата, это мы уже проходили
const struct net_device *in, out – указатели на структуры сетевого интерфейса
struct sk_buff *skb – самое интересное для нас — указатель, содержащий нужные нам данные
SKB – socket buffer, это базовая структура в сети linux. Она имеет множество полей и может быть отдельным предметом для написания статьи. Я нашел пару хороших ссылок для желающих углубиться:
http://vger.kernel.org/~davem/skb.html
https://people.cs.clemson.edu/~westall/853/notes/skbuff.pdf
Нас интересуют:
union {
struct tcphdr *th;
struct udphdr *uh;
struct icmphdr *icmph;
struct igmphdr *igmph;
struct iphdr *ipiph;
struct ipv6hdr *ipv6h;
unsigned char *raw;
} h; // Transport header
union {
struct iphdr *iph;
struct ipv6hdr *ipv6h;
struct arphdr *arph;
unsigned char *raw;
} nh; // Network header
Следующим образом
struct iphdr *ip_header = (struct iphdr *)skb_network_header(skb);
мы получаем указатель на IP header (в части 2.1 мы говорили, что главная информация на этом уровне для нас — это IP source, IP destination).
Определение skb_network_header из inclue/linux/skbuff.h
http://lxr.free-electrons.com/source/include/linux/skbuff.h?v=3.0#L1282
То есть мы видим, что функция возвращает нужный указатель из «нужного» места в структуре struct skbuff.
И теперь, когда у нас есть доступ к IP header, мы можем получить IP адреса:
unsigned int src_ip = (unsigned int)ip_header->saddr;
unsigned int dest_ip = (unsigned int)ip_header->daddr;
А также номер протокола:
ip_header->protocol
Доступ к Transport Layer(TCP/UDP..)
struct udphdr *udp_header = (struct udphdr *)(skb_transport_header(skb)+20);
struct tcphdr *tcp_header = (struct tcphdr *)(skb_transport_header(skb)+20);
Для TCP(и аналогично для UDP) номера портов:
unsigned int src_port = (unsigned int)ntohs(tcp_header->source);
unsigned int dest_port = (unsigned int)ntohs(tcp_header->dest);
Внизу я приведу полностью код функции. Интересным моментом будет использование функции ntohs. ntohs – это функция, которая меняет порядок битов (представления числа). Есть два используемых вида представления числа в памяти – little endian и big endian. Для представления чисел в сети используется система big endian, в то время как в архитектуре Intel little endian (Порядок байтов)
Поэтому для получения правильных чисел необходимо использовать эти транслирующие функции.
Ниже текст всей функции, которая при получении пакета печатает все необходимые данные для принятия решения по правилам firewall:
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 *)) {
struct iphdr *ip_header = (struct iphdr *)skb_network_header(skb);
struct udphdr *udp_header = NULL;
struct tcphdr *tcp_header = NULL;
unsigned int src_ip = (unsigned int)ip_header->saddr;
unsigned int dest_ip = (unsigned int)ip_header->daddr;
unsigned int src_port = 0;
unsigned int dest_port = 0;
char src_ip_str[16], dest_ip_str[16];
if(ip_header->protocol == PROT_UDP) {
udp_header = (struct udphdr *)(skb_transport_header(skb)+20);
src_port = (unsigned int)ntohs(udp_header->source);
dest_port = (unsigned int)ntohs(udp_header->dest);
} else if(ip_header->protocol == PROT_TCP) {
tcp_header = (struct tcphdr *)(skb_transport_header(skb)+20);
src_port = (unsigned int)ntohs(tcp_header->source);
dest_port = (unsigned int)ntohs(tcp_header->dest);
// XMAS packet
// FIN, URG, PSH set
// if(ip_header->protocol == PROT_TCP){
// printk("TCP ack = %s\n", tcp_header->ack == 1 ? "yes" : "no");
// if (tcp_header->fin > 0 && tcp_header->urg > 0 && tcp_header->psh > 0 ){
// info("XMAS packet detected, drop");
// }
}
ip_hl_to_str(ntohl(src_ip), src_ip_str);
ip_hl_to_str(ntohl(dest_ip), dest_ip_str);
printk("---------------------------\n");
printk("in device = [%s], out_device = [%s]\n", in->name, out->name);
printk("ip_src = [%s], ip_dest = [%s]\n", src_ip_str, dest_ip_str);
printk("src port: [%u], dest port: %u, \n", src_port, dest_port);
printk("protocol = %d\n", ip_header->protocol);
if(dest_port == HTTP_PORT || src_port == HTTP_PORT){
printk("HTTP packet\n");
}
return NF_ACCEPT;
}
Компилируем
Большинство (если не все) примеры написания модулей или применения netfilter, ограничиваются одним исходным файлом и парой десятков строчек кода. Но большие проекты невозможно (и неправильно) умещать в одном исходном файле; и хотя описываемый мною пример можно впихнуть в один файл, я решил разделить его на module_fw.c – все что касается char device, sysfs, kernel module и hook_functions.c – функциональность перехвата. При компиляции kernel модуля, состоящего из нескольких файлов, есть небольшой трюк, который надо знать, ниже пример:
Тут стоит обратить внимание на строчку:
obj-m := fw.o
нет такого файла fw.c, поэтому это имя модуля, который будет создан. Также, это приставка для следующей строчки, которая описывает все файлы, относящиеся к модулю
fw-objs +=
Нужно знать, что, конечно, имя модуля и исходных текстов не должны совпадать. В остальном все остается по-прежнему.
Проверяем
Для проверки я быстренько сконфигурировал dhcp интерфейсы (смотреть часть 1) и поставил на host1, http server – apache2, а на host2 – lynx – текстовой браузер (хотя можно было обойтись и telnet). Запускаем
lynx 10.0.1.1
Смотрим, что выдает наш firewall:
Ну, вот вроде и все.
Заключение
В этой части мы рассмотрели, как работают таблицы правил в firewall, которые определяют политику защиты и пропуска трафика в сети. После этого разобрали одну из базовых сетевых структур skbuf в Linux и, благодаря этому, смогли дополнить нашу программу всей нужной информацией для дополнения поддержки таблиц в нашем модуле. То, что осталось – написать загрузку этой таблицы через sysfs, как мы это делали в части 1, и добавить if {} else if {} else {} … в функцию hook_func_forward. Это я уже оставлю желающим, так как тут ничего принципиально нового не будет…ну, может только работа с klist, но это уже совсем другая тема, которая тоже хорошо освещена в интернете.
В самой функции можно найти бонус, обозначенный как XMAS packet, и почитать, что это и зачем в интернете, а следующую часть мы начнем тву (это «тут»?) —
if(dest_port == HTTP_PORT || src_port == HTTP_PORT){
printk("HTTP packet\n");
}
Ссылки:
wikipedia.org/wiki/Handshaking#TCP_three-way_handshake
ru.wikipedia.org/wiki/TCP
vger.kernel.org/~davem/skb.html
people.cs.clemson.edu/~westall/853/notes/skbuff.pdf
lxr.free-electrons.com/source/include/linux/skbuff.h?v=3.0#L1282
Порядок байтов