Как завладеть сетью /16 с помощью libpcap и libdnet. Работаем с протоколом SNMP

от автора


Всем привет. Эксперименты с эмуляцией сети продолжаются. В этот раз, как и обещал, будем делать вид, что в нашей виртуальной сети завелась машина с честным snmp-агентом.

SNMP — довольно старый протокол и знаком каждому сисадмину. На этот протокол в своё время возлагали весьма большие надежды, но в последнее время его использование сильно ограничено — как правило, это чтение переменных стандартных mib-ов на железках, не имеющих нормальных операционок (читай linux, ios и т.д.). А вот с хостов, соответственно — под управлением нормальных операционных систем, предпочитают забирать информацию с помощью cli или агентов на python/perl/bash, которые могут залезть в интимные места файловой системы /proc, парсить логи, запускать вспомогательные процессы и отдавать результаты в json/xml в неограниченных объёмах по защищённым ssl каналам.

С точки зрения системного администратора протокол действительно Simple. Всего-то пара типов запросов — get/getnext да оперативное информирование — trap. Есть ещё set, но про него в основном все знают только в теории, т.к. из-за слабой безопасности практиковать даже не пытаются. Ну и вишенка на торте — стандартные mib немного не успевают за жизнью, а в .enterprises уже никто не хочет ковыряться, даже производители.

Однако, несмотря на свои недостатки, данный протокол ещё остаётся безотказной «рабочей лошадкой» для большинства систем мониторинга. Да, стандартные MIB не позволяют в полной мере отразить топологию сети содержащей различные криптошлюзы, туннели, виртуальные роутеры, асимметричные маршруты и т.д., но базовую структуру сети собрать можно даже на коленке — утилитами командной строки snmpget и snmpwalk. Также некоторым достоинством является использование протокола UDP и кодирование ASN1 позволяющие передать всю необходимую информацию в объёме одного крохотного пакета без установки сессии. Плюс реализация snmp-агента может быть весьма небольшой по размеру и встраиваться в системы с очень ограниченными ресурсами.

Разумеется, вышеприведённой информации недостаточно, чтобы создать рукотворный «мираж» в виде фантомного хоста сети, который будет корректно отзываться на snmp-запросы. Попытаемся погрузиться в теорию — сначала лайтовенько «SNMP: Simple? Network Management Protocol», потом чуток поглубже «ASN.1 простыми словами». Мы уже в полушаге от создания собственной реализации net-snmp. Впрочем, кого я обманываю? В первой части я уже написал, что использовал библиотеку csnmp :), значит, идём на гитхаб и берём её там. Но статьи всё же прочитайте.

▍ Подготовка

Достаём из архивов исходники предыдущей части, будем их дописывать.

Скачиваем csnmp (Copyright © 2019 Nikifor Seryakov) и распаковываем рядом с каталогом, где ведутся эксперименты. Для нашего проекта будут задействованы asn1.h, asn1.c, snmp.h, snmp.c. Возникает вопрос — почему не скопировать их в наш каталог, как мы поступили с LaBrea, и таким образом облегчить объём исходников? Дело в том, что все исходные файлы LaBrea в самом начале имеют обширнейшие комментарии с реквизитами и ссылками на создателей, лицензионные ограничения и т.д., а вот csnmp прекрасно обходится без всего вот этого :). Единственным источником идентификации автора и правил использования является файл LICENSE. Поэтому я и предлагаю сохранить исходники csnmp в полном объёме.

Компиляция немного меняется:
$ gcc -o netemu -ldnet -lpcap netemu.c pkt.c bget.c ../csnmp/asn1.c ../csnmp/snmp.c

И кажется пора рисовать Makefile…

▍ Обрабатываем snmpget

В данном примере будет показан самый минимум — ответ на SNMP_GET по OID system.sysDescr.0. Для существенного облегчения задачи будем использовать протокол SNMPv1 UDP/161. Безопасность и раньше в snmp была не очень, а здесь я даже не смотрю в поле community :). Конечно, данную проверку прикрутить несложно, но для демонстрации работы с пакетами snmp это избыточно.

В начало файла добавляем ссылки на заголовочные файлы, пути должны соответствовать реальному размещению исходников csnmp:

#include "../csnmp/asn1.h" #include "../csnmp/snmp.h" 

Немного корректируем функцию ip_handler — находим в области обработки протокола ICMP объявление struct addr a; и переносим в самое начало функции. Это необходимо
чтобы мы могли воспользоваться этой переменной при обработке пакета UDP.

Там, где мы выводим заголовки пакета UDP, добавляем следующий код:

    addr_aton("10.0.0.5", &a);      if ((pkt->pkt_ip->ip_dst == a.addr_ip)         && (ntohs(pkt->pkt_udp->uh_dport) == 161)) send_snmp_reply(pkt); 

Здесь я думаю всё понятно — по прилёту пакета snmp (udp/161) будем отзываться, только если в качестве приёмника там фигурирует адрес 10.0.0.5.

Готовим функцию send_snmp_reply. Но предварительно необходимо объявить пару функций из csnmp. Дело в том, что мы не планируем отправку пакетов средствами csnmp, нам от неё необходим только разбор пакетов, манипуляции с переменными и формирование пакета в буфер. И как раз работа с буфером скрыта в недрах snmp.c и не задекларирована в snmp.h. Вносить какие-либо изменения в snmp.h не будем, а просто объявим их у себя:

extern int snmp_dec_pdu(const char *buf, int buf_len, snmp_pdu_t *p); extern int snmp_enc_pdu(char **buf, int *i, int *buf_len, snmp_pdu_t *p);  void send_snmp_reply(struct pkt *pkt) {     snmp_pdu_t p = {};      snmp_dec_pdu(pkt->pkt_udp_data, ntohs(pkt->pkt_udp->uh_ulen) - UDP_HDR_LEN, &p);      /* show snmp request */     snmp_dump_pdu(NULL, &p);      snmp_var_t *v;      switch (p.command) {         case SNMP_CMD_GET:             p.command = SNMP_CMD_RESPONSE;              for (int i = 0; i < p.vars_len; i++) {                 v = &p.vars[i];                 snmp_free_var_value(v);                 if (asn1_cmp_oids(v->oid, asn1_crt_oid((int[9]){1,3,6,1,2,1,1,1,0}, 9)) == 0) {                     v->type = SNMP_TP_OCT_STR;                     v->value = asn1_new_str("APC Web/SNMP Management Card", 0);                 }                 else                     v->type = SNMP_TP_NO_SUCH_OBJ;             }             break;          case SNMP_CMD_GET_NEXT:             break;          default:             break;     }      if (p.command == SNMP_CMD_RESPONSE) {          p.error = (asn1_error_t){0};          int buf_len = 20 * (1<<10);         char *buf = malloc(buf_len);          int pdu_len = 0;         snmp_enc_pdu(&buf, &pdu_len, &buf_len, &p);          /* show snmp reply */         snmp_dump_pdu(NULL, &p);          struct pkt *new = NULL;         if ((new = pkt_new()) == NULL) return;          eth_pack_hdr(new->pkt_eth,             pkt->pkt_eth->eth_src,    /* orig src MAC becomes new dest MAC */             io.mymac,            /* my own mac becomes new src MAC */             ETH_TYPE_IP);          ip_pack_hdr(new->pkt_ip,             0,                /* tos */             (IP_HDR_LEN + UDP_HDR_LEN + pdu_len),    /* IP hdr length */             rand_uint16( io.rnd ),    /* ipid */             0,                /* frag offset */             IP_TTL_DEFAULT,             IP_PROTO_UDP,        /* ip protocol of original pkt */             pkt->pkt_ip->ip_dst,    /* orig dst becomes new src addr */             pkt->pkt_ip->ip_src);          new->pkt_udp_data = (u_char *)(new->pkt_ip_data + UDP_HDR_LEN);         new->pkt_end = (u_char *)new->pkt_eth_data + ntohs(new->pkt_ip->ip_len);          udp_pack_hdr(new->pkt_udp,             htons(pkt->pkt_udp->uh_dport),             htons(pkt->pkt_udp->uh_sport),             UDP_HDR_LEN + pdu_len);          memcpy(new->pkt_udp_data, buf, pdu_len);         free(buf);          ip_checksum(new->pkt_ip, new->pkt_end - new->pkt_eth_data);          int ret_code = eth_send(io.eth, new->pkt_eth, new->pkt_end - (u_char *)new->pkt_eth);          if (ret_code < 0)             printf("*** Problem sending packet\n");     }      snmp_free_pdu_vars(&p);     snmp_free_pdu(&p); } 

Какая длинная функция получилась, попробую объяснить (если захотите, нарубите её на кусочки самостоятельно).

Вначале объявляем переменную p типа snmp_pdu_t и парсим в неё информацию из полученного пакета. Делаем это как раз функцией snmp_dec_pdu спрятанной от пользователей csnmp.

Далее на консоль показываем — что именно мы получили и приступаем непосредственно к формированию ответа. Ответ мы будем формировать корректируя уже готовую переменную p — почти также, как мы ранее обращались с пакетом icmp echo request.

Условный оператор switch на основании значений p.command раскидывает логику обработки запроса по нескольким веткам. Допустимые варианты данном контексте: SNMP_CMD_GET, SNMP_CMD_GET_NEXT, SNMP_CMD_SET, SNMP_CMD_GET_BULK. Мы пока что реализуем SNMP_CMD_GET, а также оставим заготовку для SNMP_CMD_GET_NEXT — всё остальное идёт в default.

Как уже писал выше, сначала сменим тип snmp-пакета — он теперь должен стать SNMP_CMD_RESPONSE. Далее в цикле пробежимся по переменным в данном запросе, почистим их значения, и если запрашиваемый oid равен .1.3.6.1.2.1.1.1.0 (system.sysDescr.0) то готовим ответ типа Octet String. Как видно исходники — это всё вставляется в поля type и value.

Значения OID, их типы, описания можно посмотреть в вашей локальной системе (/usr/share/snmp/mibs/SNMPv2-MIB.txt) или поискать в интернете по ключам: «SNMPv2 MIB», «RFC 3418:12/2002».

Цикл с пробежкой по переменным я честно скопипастил из демо-кода csnmp. На мой взгляд, было бы достаточно поработать только с переменной имеющей индекс 0, но, кажется, автор csnmp более продвинут в этом вопросе, поэтому пока оставил так.

Сама переменная типа snmp_var_t это структура из 3-х полей: oid — структура типа snmp_oid_t (точнее, ans1_oid_t) хранящая «имя» snmp-переменной; type — целое число, определяющее тип значения; value — указатель на само значение, т.е. адрес в памяти, где оно располагается.

В случае если желаемый oid не совпал с нашими возможностями, мы просто ставим тип переменной SNMP_TP_NO_SUCH_OBJ и следуем к формированию пакета и возврату его обратно.

Отправку пакета завернул в условие if (p.command == SNMP_CMD_RESPONSE). Это логично и правильно — если мы не заинтересовались пакетом, то не сменили его тип на ответ, а следовательно — и отвечать на него не считаем нужным.

Формирование пакета также производим с помощью «скрытой» функции snmp_enc_pdu. Далее кропотливая сборка пакета, вычисление размеров на каждом уровне и отправка — всё это было описано в предыдущей статье, здесь только отличие в протоколе UDP.

Ну и в финале мы очищаем переменные — если глянуть декларацию asn1_new_str (мы с её помощью формировали значение для отправки ответа), то становится понятно, что внутри этой функции выделяется память, которую нужно в итоге освободить и, кстати, в следующем разделе это будет видно более явно. Также необходимо очистить и саму pdu — там тоже достаточно компонентов с динамически выделяемой памятью.

Код написан, пора пробовать.

Отлично! Всё работает как и ожидалось. Было точное совпадение OID — получите искомое, не совпало — ну нет, значит нет. Переходим на следующий этап.

▍ Обрабатываем snmpgetnext

Обработка запросов типа snmpgetnext будет также вписана в нашу мегафункцию void send_snmp_reply(struct pkt *pkt) — помнится, для этого там было зарезервировано местечко.

Вставляем:

        case SNMP_CMD_GET_NEXT:              p.command = SNMP_CMD_RESPONSE;             v = &p.vars[0];              // спрашивают, что у нас идёт за system.sysDescr.0             if (asn1_cmp_oids(v->oid, asn1_crt_oid((int[9]){1,3,6,1,2,1,1,1,0}, 9)) == 0) {                 snmp_free_var(v);                         // возвращаем system.sysObjectID.0                 v->oid = asn1_crt_oid((int[9]){1,3,6,1,2,1,1,2,0}, 9);                 v->type = SNMP_TP_OID;                 v->value = asn1_new_oid((int[10]){1,3,6,1,4,1,318,1,3,7}, 10);             }             // запрос system.sysObjectID.0             else if  (asn1_cmp_oids(v->oid, asn1_crt_oid((int[9]){1,3,6,1,2,1,1,2,0}, 9)) == 0) {                 snmp_free_var(v);                         // возвращаем system.sysUpTime.0                 v->oid = asn1_crt_oid((int[9]){1,3,6,1,2,1,1,3,0}, 9);                 v->type = SNMP_TP_TIMETICKS;                 v->value = malloc(sizeof(int));                 *(int *)v->value = ((((5*24) + 11)*60 + 35)*60 + 24)*100 + 22; // 5 days, 11:35:24.22             }             // запрос system.sysUpTime.0             else if  (asn1_cmp_oids(v->oid, asn1_crt_oid((int[9]){1,3,6,1,2,1,1,3,0}, 9)) == 0) {                 snmp_free_var(v);                         // возвращаем system.sysContact.0                 v->oid = asn1_crt_oid((int[9]){1,3,6,1,2,1,1,4,0}, 9);                 v->type = SNMP_TP_OCT_STR;                 v->value = asn1_new_str("Comparitech", 0);             }             // запрос system.sysContact.0             else if  (asn1_cmp_oids(v->oid, asn1_crt_oid((int[9]){1,3,6,1,2,1,1,4,0}, 9)) == 0) {                 snmp_free_var(v);                         // возвращаем system.sysName.0                 v->oid = asn1_crt_oid((int[9]){1,3,6,1,2,1,1,5,0}, 9);                 v->type = SNMP_TP_OCT_STR;                 v->value = asn1_new_str("APC-3425", 0);             }             // запрос system.sysName.0             else if  (asn1_cmp_oids(v->oid, asn1_crt_oid((int[9]){1,3,6,1,2,1,1,5,0}, 9)) == 0) {                 snmp_free_var(v);                         // возвращаем system.sysLocation.0                 v->oid = asn1_crt_oid((int[9]){1,3,6,1,2,1,1,6,0}, 9);                 v->type = SNMP_TP_OCT_STR;                 v->value = asn1_new_str("3425EDISON", 0);             }             // запрос system.sysLocation.0             else if  (asn1_cmp_oids(v->oid, asn1_crt_oid((int[9]){1,3,6,1,2,1,1,6,0}, 9)) == 0) {                 snmp_free_var(v);                         // возвращаем system.sysServices.0                 v->oid = asn1_crt_oid((int[9]){1,3,6,1,2,1,1,7,0}, 9);                 v->type = SNMP_TP_INT;                 v->value = malloc(sizeof(int));                 *(int *)v->value = 72;             }             // запрос system.sysServices.0             else if  (asn1_cmp_oids(v->oid, asn1_crt_oid((int[9]){1,3,6,1,2,1,1,7,0}, 9)) == 0) {                 snmp_free_var(v);                         // возвращаем inerfaces.ifTable.ifEntry.ifIndex.1                 v->oid = asn1_crt_oid((int[9]){1,3,6,1,2,1,2,1,0}, 9);                 v->type = SNMP_TP_INT;                 v->value = malloc(sizeof(int));                 *(int *)v->value = 1;             }             // запрос что-нибудь начинающееся с system             else if (asn1_oid_has_prefix(v->oid, asn1_crt_oid((int[7]){1,3,6,1,2,1,1}, 7))) {                 snmp_free_var(v);                         // возвращаем system.sysDescr.0                 v->oid = asn1_crt_oid((int[9]){1,3,6,1,2,1,1,1,0}, 9);                 v->type = SNMP_TP_OCT_STR;                 v->value = asn1_new_str("APC Web/SNMP Management Card", 0);             }             else                 v->type = SNMP_TP_NO_SUCH_OBJ;              break;      default: 

Пробежимся по коду. С ходу меняем значение p.command на SNMP_CMD_RESPONSE — это уже знакомо. Далее берём только значение переменной с индексом 0 — решил тут сильно не загромождать код, думаю для тестового прототипа это допустимо.

Ну а дальше муторная и монотонная (конкретно в данной демонстрашке — разумеется, боевой код так писать нельзя) работа по сверке запрошенного oid и подготовке ответа: освобождение памяти от переменной, формирование oid следующего элемента, его тип и значение. Тут хорошо видно как обрабатывать значения различных типов, а также фигурирует более явное выделение памяти посредством malloc.

На освобождение памяти хочу обратить особое внимание. Если помните, в прошлый раз чистили только значения (snmp_free_var_value), а сейчас переменную целиком (snmp_free_var). Всё дело в протоколе snmp — при вызове snmpget мы просим дать значение конкретного oid, т.е. он не меняется, а при snmpgetnext нужно дать значение следующего элемента. Соответственно, в ответе у нас будет изменены не только тип и значение, но также будет совсем другой oid. Именно это и требует от нас тотальной зачистки.

Также особого внимания заслуживают два последних условия:

Когда мы обрабатываем последний oid на нашем уровне, в данном случае .1.3.6.1.2.1.1.7.0, мы не возвращаем в качестве следующего элемента SNMP_TP_NO_SUCH_OBJ, что кажется вполне логичным, а возвращаем первый элемент из соседней веточки. На самом деле мы с помощью snmpwalk можем пройтись по любому уровню дерева snmp как ближе к корню, так и почти на конце какой-либо ветки — он не будет выходить за пределы запроса — при получении ответа с oid вне запрошенного уровня он завершает опрос. А вот если бы мы вернули SNMP_TP_NO_SUCH_OBJ, то запрос snmpwalk с более высокого уровня прервался на нашей веточке и не пробежался по соседним. Также этот oid должен вернуться при обращении к oid большим чем .1.3.6.1.2.1.1.7.0

В последнем условии производится сравнение запроса только на префикс. Это тоже особое поведение snmp-демона — при обращении в начальные области нашей ветки должен вернуться oid первого элемента — это .1.3.6.1.2.1.1.1.0. Другими словами, все запросы к system(.1.3.6.1.2.1.1) или system.sysDescr(.1.3.6.1.2.1.1.1) должны вернуть нам ссылку и значение system.sysDescr.0(.1.3.6.1.2.1.1.1.0), а вот обращение реально существующей system.sysDescr.0(.1.3.6.1.2.1.1.1.0) вернёт следующий элемент — system.SysObjectID.0(.1.3.6.1.2.1.1.2.0)

Приступаем к тестированию:

Длинный вывод я подрезал, но уже видно, что snmpwalk ничего не заподозрил!

Следующая проверка

Результат, на первый взгляд, не совсем корректный, однако если сравнить oid запроса и oid элемента, который был возвращён в последний раз, то становится понятно — элемент вышел за уровень запроса и snmpwalk остальное перестало интересовать.

Ну а напоследок один каверзный запрос:

Внимательно изучаем отладочный вывод: snmpwalk умный парень — хоть и получил сразу ответ идти в соседнюю ветку, но на этом не успокоился, а произвёл контрольный вопрос через snmpget — так есть кто живой с этим oid или нет? Кстати, если сейчас попробуете на других oid, то получите совсем другой результат, это потому что в первой части статьи написана обработка snmpget только для system.sysDescr.0 🙂

▍ Финал

На этом достаточно знакомства с богатым внутренним миром такого «простого» протокола как SNMP. А для решения моей основной задачи осталось совсем немного — определится с различными структурами хранения oid и их значений, привязка к базе ip-адресов, сделать чтобы всё это извлекалось и обрабатывалось в разумные временные рамки. Это реализуется простыми и понятными алгоритмами — хэши/деревья/ключи-значения и пр. но уже не в рамках данной статьи.

Всем приятных pet-проектов 🙂


ссылка на оригинал статьи https://habr.com/ru/company/ruvds/blog/697854/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *