Кодим безумный пассивный сниффер в виде модуля для Python



    Сразу скажу: то, чем мы с вами будем здесь заниматься, не претендует, скажем, на какую-то промышленную применимость. Более того, я признаю, что мой код в данном примере может быть ужасен, страшен и ненужен. И тем не менее — почему бы не поперехватывать пакеты посреди недели? Так, слегка.

    Итак, сегодня мы вот что наколдуем:

    1. Реализуем простейший пассивный перехватчик пакетов для TCP и UDP
    2. Засунем его в C-библиотеку в виде расширения для Python
    3. Приделаем ко всему этому интерфейс итератора, дабы байты сыпались, как из рога изобилия
    4.…
    5. PROFIT!

    Зачем все это нужно?

    Зачем нужны снифферы? Чтобы нюхать сетевой трафик. Нет, я не шучу, с английского глагол «to sniff» так и переводится — «нюхать». Ладно-ладно, будем более научны — с их помощью можно и нужно анализировать сетевые пакеты, проходящие через сетевую карту компьютера.

    Снифферы бывают пассивные и активные. Пассивный сниффер делает именно и только то, что от него требуется — перехватывает для анализа трафик, проходящий сквозь сетевую карту компьютера, на котором он установлен. Казалось бы, куда уж круче? Однако же, активный сниффер не только мониторит сетевую карту, но и всячески пытается добраться до трафика, который гуляет по локальной сети и не предназначен для чужих глаз. Делает он это, например, с использованием ARP-Spoofing и всяких прочих грязных трюков. Кстати, в некоммутируемых сетях (основанных на репитерах и хабах) пассивный сниффер может внезапно обнаружить в себе суперсилы, ибо при отсутствии коммутации все пакеты в таких сетях рассылаются по всем хостам. Принимать их, конечно, должны только адресаты, но… :)

    А вот чтобы их принимать — надо перво-наперво заставить сетевую карту уволить ее личного секретаря, который фильтрует корреспонденцию, и начать читать все и сразу. Научно это называется "Promiscuous mode", то есть «Неразборчивый режим». И тут есть засада:

    image

    Да, для перевода карты в неразборчивый режим требуются права root. Поэтому и наш скрипт попросит их для запуска. Такие дела.

    Ну и чо?

    Как говорил один известный киношный персонаж, «We need to go deeper». То есть, нырнуть поглубже в уровни OSI, на канальный уровень. Для этого при создании сокета необходимо указать константу PF_PACKET вместо PF_INET:

        int s_sock;
        struct ifreq ifr;
        strcpy(ifr.ifr_name, IFACE);
    
        if ( (s_sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL))) < 0) {
            perror("Error creating socket");
            exit(-1);
        }
    
        ifr.ifr_flags |= IFF_PROMISC;
        if (ioctl(s_sock, SIOCGIFFLAGS, &ifr) < 0) {
          perror("Unable to set promiscious mode for device");
          close(s_sock);
          exit(-1);
        }
    


    Обычно при открытии подобного сокета указывается конкретный протокол, по которому идет перехват. Если же указать константу ETH_P_ALL — будут перехватываться все. Структура ifreq используется в Linux для руления сетевым интерфейсом на низком уровне через ioctl(), а IFACE в данном случае — просто строка с именем смотрящего в сеть интерфейса, например, «eth0».

    Собственно, теперь все, что нам остается — читать данные из сокета в цикле и смотреть, что из этого получится:
        int n = 0;
        char buf[MTU];
        n = recvfrom(s_sock, buf, sizeof(buf), 0, 0, 0);
    

    MTU здесь проще всего поставить равным 1500 — это определенный стандартом максимальный размер пакета в сетях Ethernet. Для сетей, построенных по другому стандарту, например, FDDI, значение может быть другим.

    Поскольку мы работаем с Ethernet, данные о заголовках полученных пакетов проще всего записывать в специально предназначенную для этого структуру ядраethhdr. Для разных базовых протоколов вплоть до транспортного уровня существуют похожие структуры, которые, как ни сложно в это поверить, носят названия наподобие iphdr, tcphdr или даже udphdr (как говорится, «знаю отличную шутку про UDP, но не факт, что она до вас дойдет»). Как и полагается в старом добром C, это делается как-то так:
      struct ethhdr eth;
      memcpy((char *) &eth, data, sizeof(struct ethhdr));
    


    Мда… И как же засунуть это в Python?

    Всем известно, что в питоне у нас все есть объект. В случае с генератором/итератором исключения так же не будет — мы должны создать объект, имеющий метод __iter__() и умеющий next(). У объекта есть куча полей, большинство из которых нам не нужны, поэтому осторожно — впереди куча нулей.
    PyTypeObject PyPacketGenerator_Type = {
        PyVarObject_HEAD_INIT(&PyType_Type, 0)
        "packgen",                       /* tp_name */
        sizeof(PacketGeneratorState),            /* tp_basicsize */
        0,                              /* tp_itemsize */
        (destructor)packgen_dealloc,     /* tp_dealloc */
        0,                              /* tp_print */
        0,                              /* tp_getattr */
        0,                              /* tp_setattr */
        0,                              /* tp_reserved */
        0,                              /* tp_repr */
        0,                              /* tp_as_number */
        0,                              /* tp_as_sequence */
        0,                              /* tp_as_mapping */
        0,                              /* tp_hash */
        0,                              /* tp_call */
        0,                              /* tp_str */
        0,                              /* tp_getattro */
        0,                              /* tp_setattro */
        0,                              /* tp_as_buffer */
        Py_TPFLAGS_DEFAULT,             /* tp_flags */
        0,                              /* tp_doc */
        0,                              /* tp_traverse */
        0,                              /* tp_clear */
        0,                              /* tp_richcompare */
        0,                              /* tp_weaklistoffset */
        PyObject_SelfIter,              /* tp_iter */
        (iternextfunc)packgen_next,      /* tp_iternext */
        0,                              /* tp_methods */
        0,                              /* tp_members */
        0,                              /* tp_getset */
        0,                              /* tp_base */
        0,                              /* tp_dict */
        0,                              /* tp_descr_get */
        0,                              /* tp_descr_set */
        0,                              /* tp_dictoffset */
        0,                              /* tp_init */
        PyType_GenericAlloc,            /* tp_alloc */
        packgen_new,                     /* tp_new */
    };
    

    Из этого объявления видно, что рабочие методы нашего будущего объекта — это packgen_new(), packgen_next() и packgen_dealloc(). Последний является деструктором и в общем и целом без него можно обойтись, если очень хочется и нет лишних данных в памяти. Кроме того, нам потребуется структура, в которой мы будем хранить данные о состоянии объекта на текущей итерации. Поскольку единственное, что нам необходимо хранить — это сокет, его и объявим:
    typedef struct {
        PyObject_HEAD
        int s_sock;
    } PacketGeneratorState;
    


    Как я сказал выше, нужно открыть сокет и настроить сетевую карту в режим шпиёнства. Проще всего это сделать прямо в методе инициализации объекта.

    static PyObject *
    packgen_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
    {
        int s_sock;
        struct ifreq ifr;
    
        strcpy(ifr.ifr_name, IFACE);
        if ( (s_sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL))) < 0) {
            perror("Error creating socket");
            exit(-1);
        }
    
        ifr.ifr_flags |= IFF_PROMISC;
        if (ioctl(s_sock, SIOCGIFFLAGS, &ifr) < 0) {
          perror("Unable to set promiscious mode for device");
          close(s_sock);
          exit(-1);
        }
    
        PacketGeneratorState *pkstate = (PacketGeneratorState *)type->tp_alloc(type, 0);
        if (!pkstate)
            return NULL;
    
        pkstate->s_sock = s_sock;
    
        return (PyObject *)pkstate;
    }
    


    В принципе, вполне реально было бы сделать возвращение пакетов из модуля в виде классов, но мне было ленья решил сделать проще и возвращать словарь (а если кто переделает по-своему — будет молодец). Все, что остается сделать — это определить, что у нас IP на сетевом уровне, а по IP-заголовку, в свою очередь, определить вышележащий протокол (если кто не помнит, протоколы обозначаются циферками и прописаны в /etc/protocols). Это все работает потому, что пакеты у нас похожи на творения поваров Burger King — каждый вышестоящий уровень дописывает свой заголовок к нижестоящему. Возьмем старых добрых братьев — TCP и UDP.

    PyObject *packet;
    packet = PyDict_New();
    if (!packet)
          return NULL;
    if (ntohs(eth.h_proto) == ETH_P_IP) {
          ip = (struct iphdr *)(data + sizeof(struct ethhdr));
          PyDict_SetItemString(packet, "ip_source", PyString_FromFormat("%s", inet_ntoa(ip->saddr)));
          ...
          if ((ip->protocol) == IPPROTO_TCP) {
                tcp = (struct tcphdr *)(data + sizeof(struct ethhdr) + sizeof(struct iphdr));
                PyDict_SetItemString(packet, "tcp_source_port", PyString_FromFormat("%d", ntohs(tcp->source)));
                ...
          }
          if ((ip->protocol) == IPPROTO_UDP) {  
            udp = (struct udphdr *)(data + sizeof(struct ethhdr) + sizeof(struct iphdr));
            PyDict_SetItemString(packet, "udp_source_port", PyString_FromFormat("%d", ntohs(udp->source)));
            ...
          }
    } 
    return packet;
    


    Вуаля!

    Выглядит все в итоге примерно вот так:
    Python 2.7.3 (default, Apr 20 2012, 22:44:07)
    [GCC 4.6.3] on linux2
    Type "help", "copyright", "credits" or "license" for more information.
    >>> import pysniff
    >>> for i in pysniff.packgen():
    ... print i
    ...
    {'ip_destination': '192.168.1.111', 'tcp_seq': '10972', 'ip_source': '173.194.32.53', 'tcp_offset': '8', 'tcp_source_port': '443', 'tcp_dest_port': '44021'}
    {'ip_destination': '173.194.32.53', 'tcp_seq': '47475', 'ip_source': '192.168.1.111', 'tcp_offset': '8', 'tcp_source_port': '44021', 'tcp_dest_port': '443'}
    {'ip_destination': '192.168.1.111', 'tcp_seq': '10972', 'ip_source': '173.194.32.53', 'tcp_offset': '8', 'tcp_source_port': '443', 'tcp_dest_port': '44021'}
    {'ip_destination': '173.194.32.53', 'tcp_seq': '47475', 'ip_source': '192.168.1.111', 'tcp_offset': '8', 'tcp_source_port': '44021', 'tcp_dest_port': '443'}


    Ради смеха создал репозиторий на github, вдруг захочется развить идею? Удачного вам сниффинга, не попадайтесь!
    Метки:
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 15
    • +4
      Всё это интересно и познавательно, но десятилетиями существуют и развиваются такие инструменты как tcpdump, а формат pcap стал стандартом де-факто…
      • 0
        Согласен, я думал и про libpcap еще рассказать. Возможно, в следующей статье.
        • +1
          Главное же не утилиту сделать для постоянного пользования, а познать глубины сетевых технологий :)
          Профит в написании собственными руками
        • –8
          Такими шагами скоро снифферы будем писать на HTML5 :)
          • +8
            Нет, похоже, не скоро :)
          • 0
            Нужно ли изобретать велосипед, когда существует Scapy?
            • +4
              В качестве получения знаний нужно.
            • +2
              memcpy((char *) ð

              Пардон, што?
              Как вы, чёрт побери, это сделали?
              • +1
                Похоже, это автозамена хабра. Должно было быть memcpy((char *) &eth
                • +1
                  англосаксонская буква eth
                • 0
                  А вот что делать, если надо гигабит 10 в секунду заснифать? Питон потянет такое?
                  • 0
                    Ну, в данном случае питон как раз ни при чем, ибо код сниффера написан на C. Единственная потеря производительности будет при дампе данных из пакетов в питоновские структуры.
                    • НЛО прилетело и опубликовало эту надпись здесь
                    • 0
                      Извините, но очень интересно узнать, а почему сниффер на Си, но с биндингами в питон? Неужели средствами питона это все не написать?

                      Я когда то писал upnp клиент на python. Я не очень понял, почему ничего кроме библиотек на Си с биндингами к питону нету в интернете?
                      • 0
                        Можно и средствами питона, это не особо сложно. Просто на практике когда кто-то садится писать подобные вещи — это в большинстве случаев должна быть мощная и высоконагруженная система, а такие обычно пишут на системных языках. Поэтому и получается иногда, что выгоднее скрестить скорость работы Си и скорость разработки на питоне (некоторые используют, например, такую штуку, как cython). Это не всегда так, но часто.

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.