
Всем привет. Эксперименты с эмуляцией сети продолжаются. В этот раз, как и обещал, будем делать вид, что в нашей виртуальной сети завелась машина с честным 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/

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