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

Комментарии (2)


  1. anarchomeritocrat
    07.12.2022 11:14
    +3

    Безопасность и раньше в snmp была не очень, а здесь я даже не смотрю в поле community :)

    У SNMP даже есть ещё одна, шуточная, но очень актуальная расшифровка аббревиатуры: "Security Not My Problem", по этому перед использованием в сети, очень рекомендую позаботиться о создании под SNMP специального изолированного VLAN, причем желательно с шифрованием траффика, скажем на основе VPN, может это прозвучит немного параноидально, но, скажем, в крупной городской сети SNMP может оказаться той ещё дырой в безопасности )

    А в целом да, крайне полезный протокол, при помощи которого можно чудеса творить )


    1. alef13 Автор
      07.12.2022 12:13
      +1

      полностью согласен :)