Pull to refresh

Перехват вызовов функций в Linux или простейший файрвол своими руками

Reading time5 min
Views4.2K

Вступление


Нетерпеливым эти лирические отступления можно не читать.

Некоторое время назад меня посетила мысль: «Как сделать так, чтобы закрыть доступ к интернету (или к какому-то конкретному хосту) какой-нибудь одной программе в Linux?». Эта мысль меня посетила и полетела дальше по своим делам. И вот сегодня я получил в свой RSS-ридер один вопрос на askdev.ru. Ба! Да это как раз то, о чем я думал! Надо помочь человеку, заодно и самому разобраться в вопросе.

Полез я в гугл смотреть, нету ли каких-нибудь намеков на решение. Оттуда я узнал, что штатным iptables это с некоторых пор сделать стало невозможно, а народ рекомендует посмотреть в сторону AppArmor. «Горя огромным желанием изучить AppArmor», я стал искать дальше и почти случайно наткнулся на сообщение на ЛОРе, в котором описывался довольно интересный метод.

Метод


Заключался он в том, чтобы «подменить» функцию соединения на свою, которая решает, разрешить ли соединение и «пропустить» запрос к настоящей функции или нет — вернуть ошибку. В сообщении многоуважаемого Chaoser'а использовалась низкоуровневая блокировка сокетов, которая не давала возможности принимать решение в зависимости от адреса сервера и/или порта. Это меня не устраивало, мне нужно было запретить доступ только для одного порта — 80-го. Запустив telnet, под strace'ом, я почти сразу увидел подходящую жертву — фунцию connect. strace ее описал так:

connect(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("87.250.251.3")}, 16) = 0

В этом описании отчетливо видно, что есть все необходимые мне составляющие: и IP-адрес, и порт, и тип подключения (AF_INET).

Что ж, приступим.

Решение


Для реализации данного метода, мы напишем библиотеку, которая будет загружаться раньше других при запуске приложения, и, таким образом, перехватывать функции, которые в ней определены (и которые предназначены для других библиотек).

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

  1. #define _GNU_SOURCE
  2. #include <dlfcn.h>
  3. #include <stdio.h>
  4. #include <sys/types.h>
  5. #include <sys/socket.h>
  6. #include <netinet/in.h>
  7. #include <arpa/inet.h>
  8. #include <errno.h>
  9.  
  10. static int (*real_connect)(int sockfd, const struct sockaddr *addr,
  11.                   socklen_t addrlen) = 0;
  12.  
  13. int connect(int sockfd, const struct sockaddr *addr,
  14.                   socklen_t addrlen)
  15. {
  16.     printf("NF_DEBUG: -----------------------------------------------\n");
  17.     int sa_family = addr->sa_family;
  18.     printf("NF_DEBUG: Address family: %d (AF_INET = %d)\n", sa_family, AF_INET);
  19.  
  20.     if (sa_family == AF_INET)
  21.     {
  22.         struct sockaddr_in *addr_in = (struct sockaddr_in*)(addr);
  23.     
  24.         struct in_addr sin_addr = addr_in->sin_addr;
  25.         uint16_t sin_port = addr_in->sin_port;
  26.         uint16_t sin_port_h = ntohs(sin_port);
  27.  
  28.         printf("NF_DEBUG: IP: %s\n", inet_ntoa(sin_addr));
  29.         printf("NF_DEBUG: Port: %d\n", sin_port_h);
  30.  
  31.         if (sin_port_h == 80)
  32.         {
  33.             printf("NF_DEBUG: Rejected!\n");
  34.             printf("NF_DEBUG: -----------------------------------------------\n");
  35.             errno = ENETUNREACH;
  36.             return -1;
  37.         }
  38.     }
  39.  
  40.  
  41.     if(!real_connect)
  42.         real_connect = dlsym(RTLD_NEXT, "connect");
  43.  
  44.     printf("NF_DEBUG: Accepted\n");
  45.     printf("NF_DEBUG: -----------------------------------------------\n");
  46.     return real_connect(sockfd, addr, addrlen);
  47. }
* This source code was highlighted with Source Code Highlighter.


Разбор кода


Те, кто поняли, как работает вышеприведенная программа, могут этот раздел совершенно спокойно пропустить. В нем будет описано, какая строчка что делает. Это должно быть полезно новичкам. Тем, кто хорошо знает C, из этого, возможно, будут интересны лишь некоторые моменты.

Строки 1-8 — директивы препроцессора, ничего интересного в них нет. Разве что директива #define _GNU_SOURCE, которая подключает расширения GNU, необходимые для функции dlsym.

В строках 10-11 мы объявляем указатель на «настоящую» функцию connect. У нас она будет называться real_connect. Описание функции взято из man connect.

Со строки 13 начинается уже новая функция connect, которую будут вызывать приложения и которая будет принимать решение, пропустить это приложение или нет. Ее описание полностью соответствует оригинальной connect и взято из того же мануала.

В 17-й строке из структуры, которая содержит все необходимые нам данные (и описание которой я взял здесь) получаем тип адреса.

Если тип адреса AF_INET (строка 20), то есть приложение «просится наружу» по протоколу IPv4, то мы применяем фильтрацию (строки 22-37), иначе — сразу пропускаем это соединение к «настоящей» функции connect.

Для того, чтобы отфильтровать подключения, необходимо преобразовать структуру addr к типу sockaddr_in, который дает возможность доступа к необходимым полям. Это происходит в строке 22.

Далее в строках 24-25 получаем значения адреса и порта, соответственно. В строке 26 мы меняем порядок следования байт в номере порта, тем самым получая обычный формат номера порта (80, 110, 25) из формата, который используется при соединении.

В строке 28 полученный адрес приводим к текстовому виду и выводим его, в 29-й строке выводим номер порта.

Далее в 31-й строке проверяем, соответствует ли номер порта тому, который мы хотим заблокировать, если да, то сообщаем об этом (строки 33-34), устанавливаем номер ошибки (строка 35; в данном случае будет ошибка «Сеть недоступна») и возвращаем ошибку (строка 36).

Если же все в порядке, то есть приложение обратилось, используя либо разрешенный порт, либо разрешенный тип адреса, то получаем адрес (точку входа) «настоящей» функции connect (строка 42), если до того мы его не получили (строка 41; не забываем, что real_connect у нас переменная глобальная), пишем, что все хорошо (строки 44-45), и передаем управление «настоящей» функции connect.

При получении адреса «настоящей» функции connect, мы используем параметр RTLD_NEXT для того, чтобы получить этот адрес из следующей загруженной библиотеки, а не из первой (нашей), иначе получим бесконечную рекурсию.

Использование


Скомпилировать библиотеку:

gcc -fPIC -shared -Wl,-soname,nonet.so -o nonet.so nonet.c

и запустить нужное приложение следующим образом:

LD_PRELOAD=/<полный путь к полученной библиотеке>/nonet.so <приложение>

Например, если библиотека лежит в /tmp, то следующей строкой можно запустить firefox без доступа к интернету (естественно, только через 80-й порт):

LD_PRELOAD=/tmp/nonet.so firefox

Заключение


Данный метод ни в коем случае не претендует на универсальность и всеприменимость, равно как и статья на Нобелевскую премию. Здесь я всего лишь осветил две темы, которые интересны мне, и, надеюсь, будут интересны еще кому-нибудь: ограничение доступа к сети опредеделенным программам и перехват вызовов функций в Linux.

Спасибо за внимание!

P.S. Чтобы меня не считали бесчестным человеком, укравшим код у digital'а c askdev.ru, думаю следует упомянуть, что digital и я — одно лицо.

UPD Большое спасибо за инвайт хабрачеловеку peter23. Перенес пост в блог «Linux для всех».
Tags:
Hubs:
+79
Comments21

Articles

Change theme settings