Ссылочная TCP/IP стеганография

    TCP-стеганография не является чем-то принципиально новым, например Джон Торакис в 2016 году реализовывал на Питоне довольно интересные вещи, жаль не все они есть в открытом доступе. Не была она принципиально новой и на момент написания статей Торакисом. Вот пост на Хабре 2009 года, описывающий идею и, например, программа Covert_TCP далекого (а может и не очень) 1996 года, написанная на чистом Си и реализующая довольно тонкие настройки.


    Если Covert TCP предлагает передавать по одному байту полезной нагрузки в TCP пакете, Торакис использовал от 2 до 6 байт на пакет и предложил идею создания протокола в протоколе. Но даже таким образом затруднительно передать большие массивы данных.


    Тут на помощь может прийти система одноразовых сообщений. Действительно, почему бы не объединить 2 подхода, раз они изобретены до нас?


    picture


    Итак, действующие лица — машина 1 (отправитель) и машина 2 (получатель). Требуется автоматизировать следующие действия:


    1. Создать с машины 1 сообщение на каком-либо ресурсе с одноразовым URL и получить ссылку (например, One-Time Secret).
    2. Передать ссылку как полезную нагрузку в TCP/IP пакете.
    3. Считать сообщение с сайта на машине 2 и выполнить какие-то действия с ним.

    Начнем с середины — с внедрения полезной нагрузки.


    TCP/IP Стеганография


    Пакет, в который мы будем внедрять (секретные, но не зашифрованные) биты, состоит из IP-заголовка и TCP-загловка.


    Заголовок IPv4.


        0                   1                   2                   3
        0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
       |Version|  IHL  |Type of Service|          Total Length         |
       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
       |         Identification        |Flags|      Fragment Offset    |
       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
       |  Time to Live |    Protocol   |         Header Checksum       |
       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
       |                       Source Address                          |
       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
       |                    Destination Address                        |
       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
       |                    Options                    |    Padding    |
       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

    Стандарт RFC 791 содержит следующее:


        Identification:  16 bits
    
        An identifying value assigned by the sender to aid in assembling the
        fragments of a datagram.

    Поле ID нужно для того, чтобы собрать упорядоченные фрагменты данных. Т.е. в случае единственного пакета это поле не используется. Значит для наших целей уже можно использовать 2 байта. Экспериментально установлено, что нулевое поле ID автоматически заполняется случайными значениями, наверное это тоже где-то написано.


    Рассмотрим TCP заголовок.


    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |          Source Port          |       Destination Port        |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                        Sequence Number                        |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                    Acknowledgment Number                      |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |  Data |           |U|A|P|R|S|F|                               |
    | Offset| Reserved  |R|C|S|S|Y|I|            Window             |
    |       |           |G|K|H|T|N|N|                               |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |           Checksum            |         Urgent Pointer        |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                    Options                    |    Padding    |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                             data                              |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

    Поле Sequence Number первого пакета соединения не является фиксированным, потому что иначе его бы могли знать злоумышленники (RCF 793). А это еще 4 байта.


    Реализация


    Детали под спойлером

    Disclaimer: да, глобальные переменные — некрасиво и айайай, но для небольших тестовых программ, как мне кажется, приемлемы.


    Код в этом разделе будет на чистом Си и ориентирован на linux-системы, т.к. в них требуется меньше неочевидных шагов чтобы заставить программы работать (и никаких winpcap). Все программы, использующие "сырые" сокеты следует запускать из-под рута.


    Спасибо сайту binarytides.com за разобранные примеры с отправкой пакетов, а также готовый сниффер.


    Отправитель


    Детали под спойлером
    #include<stdio.h>           //for printf
    #include<string.h>          //memset
    #include<sys/socket.h>      //for socket ofcourse
    #include<stdlib.h>          //for exit(0);
    #include<errno.h>           //For errno - the error number
    #include<netinet/tcp.h>     //Provides declarations for tcp header
    #include<netinet/ip.h>      //Provides declarations for ip header
    #include <unistd.h>
    
    // 96 бит (12 байт) псевдо-заголовок, нужный для вычисления хэш-суммы
    struct pseudo_header {
        u_int32_t source_address;
        u_int32_t dest_address;
        u_int8_t placeholder;
        u_int8_t protocol;
        u_int16_t tcp_length;
    };
    
    // функция вычисление хэш-суммы
    unsigned short csum(unsigned short *ptr,int nbytes) {
        register long sum;
        unsigned short oddbyte;
        register short answer;
    
        sum=0;
        while(nbytes>1) {
            sum+=*ptr++;
            nbytes-=2;
        }
        if(nbytes==1) {
            oddbyte=0;
            *((u_char*)&oddbyte)=*(u_char*)ptr;
            sum+=oddbyte;
        }
    
        sum = (sum>>16)+(sum & 0xffff);
        sum = sum + (sum>>16);
        answer=(short)~sum;
    
        return(answer);
    }
    
    int main (int argc, char* argv[]) {
        srand(time(NULL));
    
        if (argc < 3) {
            puts("Enter source and destination ip");
            return 1;
        }
    
        while (1) {
            puts("Enter payload:");
            char payload[1024];
            // считывание строки
            fgets(payload, 1024, stdin);
            // получение длины строки
            int length = strlen(payload);
            // нуль-терминирование строки
            if (length > 0 && payload[strlen (payload) - 1] == '\n')
                payload[strlen (payload) - 1] = '\0';
            // завершение при отсутствии полезной нагрузки
            if (!length)
                break;
    
            // вычисление необходимого количества пакетов
            int n = (length + 5)/6;
    
            int i;
            for (i = 0; i < n; ++i) {
                // задержка между отправлением пакетов
                usleep(10000);
                // Создание сокета в RAW режиме
                int s = socket (PF_INET, SOCK_RAW, IPPROTO_TCP);
    
                if(s == -1) {
                    // Создание сокета закончилось ошибкой
                    // вероятно, из-за отсутствия привилегий
                    perror("Failed to create socket");
                    exit(1);
                }
    
                // побитовове представление пакета
                char datagram[4096] , source_ip[32] , *pseudogram;
    
                // инициализация нулями
                memset (datagram, 0, 4096);
    
                // IP заголовок
                struct iphdr *iph = (struct iphdr *) datagram;
    
                //TCP заголовок
                struct tcphdr *tcph = (struct tcphdr *) (datagram + sizeof (struct iphdr));
                struct sockaddr_in sin;
                struct pseudo_header psh;
    
                //some address resolution
                strcpy(source_ip , argv[1]);
                sin.sin_family = AF_INET;
                sin.sin_port = htons(80);
                sin.sin_addr.s_addr = inet_addr (argv[2]);
    
                // Заполнение IP заголовка
                // Минимальная корректная длина 5
                iph->ihl = 5;
                // IPv3
                iph->version = 4;
                // приоритет не важен
                iph->tos = 0;
                // вычисление общей длины пакета
                iph->tot_len = sizeof (struct iphdr) + sizeof (struct tcphdr);
                // первая часть полезной нагрузки
                iph->id = (6*i < length ? payload[6*i] << 8 : 0)
                        + (6*i + 1 < length ? payload[6*i + 1] : 0);
                // Если id == 0, он заменяется на случайное значение, что нам не выгодно
                // При присвоении ему единицы, первая половина останется нулевой,
                // что соответствует концу строки
                if (iph->id == 0)
                    iph->id = 1;
                // Первый фрагмент => нулевое смещение
                iph->frag_off = 0;
                // Стандартный во многих случаях TTL
                iph->ttl = 64;
                // Протокол TCP
                iph->protocol = IPPROTO_TCP;
                // Выставление нуля перед вычислением хэш-суммы
                iph->check = 0;
                // Исходящий IP
                iph->saddr = inet_addr ( source_ip );
                // IP точки назначения
                iph->daddr = sin.sin_addr.s_addr;
    
                // Вычисление хэша IP заголовка
                iph->check = csum ((unsigned short *) datagram, iph->tot_len);
    
                // Заголовок TCP
                // порт источника
                tcph->source = htons (20);
                // порт цели
                tcph->dest = htons (rand() % 10000); // "сканируем" разные порты
                tcph->ack_seq = 0;
    
                // Вторая часть полезной нагрузки
                tcph->seq = 0;
                int j;
                for (j = 0; j < 4; ++j)
                    tcph->seq += (6*i + 2 + j < length ? payload[6*i + 2 + j] : 0) << 8*j;
    
                // сдвиг равен размеру заголовка
                tcph->doff = 5;
                // Из флагов интересует только SYN
                tcph->fin=0;
                tcph->syn=1;
                tcph->rst=0;
                tcph->psh=0;
                tcph->ack=0;
                tcph->urg=0;
                // максимальный размер окна
                tcph->window = htons (5840);
                // хэш-сумма будет заполнена при помощи псевдо-заголовка
                tcph->check = 0;
                // нулевая "важность"
                tcph->urg_ptr = 0;
    
                // Подготовка к вычислению хэша
                psh.source_address = inet_addr( source_ip );
                psh.dest_address = sin.sin_addr.s_addr;
                psh.placeholder = 0;
                psh.protocol = IPPROTO_TCP;
                psh.tcp_length = 0;
    
                int psize = sizeof(struct pseudo_header) + sizeof(struct tcphdr);
                pseudogram = (char*)malloc(psize);
    
                memcpy(pseudogram , (char*) &psh , sizeof (struct pseudo_header));
                memcpy(pseudogram + sizeof(struct pseudo_header) , tcph,
                       sizeof(struct tcphdr));
                tcph->check = csum( (unsigned short*) pseudogram , psize);
    
                free(pseudogram);
    
                //IP_HDRINCL чтобы сказать ядру, что заголовки включены в пакет
                int one = 1;
                const int *val = &one;
    
                if (setsockopt (s, IPPROTO_IP, IP_HDRINCL, val, sizeof (one)) < 0)
                {
                    perror("Error setting IP_HDRINCL");
                    exit(0);
                }
                // Отправка пакета
                if (sendto (s, datagram, iph->tot_len ,  0, (struct sockaddr *) &sin, sizeof (sin)) < 0)
                {
                    perror("sendto failed");
                }
                // Успех
                else
                {
                    // информация об отправленном пакете
                    printf ("Packet sent. \"" );
                    for (j = 0; j < 6; ++j)
                        if (6*i + j < length)
                        printf("%c", payload[6*i + j]);
                    puts("\"");
                }
            }
        }
    
        return 0;
    }

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


    // считывание строки
    fgets(payload, 1024, stdin);
    // получение длины строки
    int length = strlen(payload);

    Всего имеется 6 байт, поэтому количество требующихся пакетов равно длине, деленной на 6 и округленной в б_о_льшую сторону.


    // вычисление необходимого количества пакетов
    int n = (length + 5)/6;

    Дальнейшие действия выполняются n раз.


    Создание TCP пакета в "сыром" режиме.


    int s = socket (PF_INET, SOCK_RAW, IPPROTO_TCP);

    Создание необходимых переменных, заполнение полей.


    // побитовове представление пакета
    char datagram[4096] , source_ip[32] , *pseudogram;
    
    // инициализация нулями
    memset (datagram, 0, 4096);
    
    // IP заголовок
    struct iphdr *iph = (struct iphdr *) datagram;
    
    //TCP заголовок
    struct tcphdr *tcph = (struct tcphdr *) (datagram + sizeof (struct iphdr));
    

    Внедрение первой части полезной нагрузки (2 байта в поле ID).


    iph->id = (6*i < length ? payload[6*i] << 8 : 0)
                        + (6*i + 1 < length ? payload[6*i + 1] : 0);

    Внедрение второй части полезной нагрузки (4 байта в поле Sequence Number).


    
    tcph->seq = 0;
    int j;
    for (j = 0; j < 4; ++j)
        tcph->seq += (6*i + 2 + j < length ? payload[6*i + 2 + j] : 0) << 8*j;
    

    Отправка пакета и вывод его полезной нагрузки.


    
    if (sendto (s, datagram, iph->tot_len ,  0, (struct sockaddr *) &sin, sizeof (sin)) < 0) {
        perror("sendto failed");
    }
    // Успех
    else {
        // информация об отправленном пакете
        printf ("Packet sent. \"" );
        for (j = 0; j < 6; ++j)
            if (6*i + j < length)
            printf("%c", payload[6*i + j]);
        puts("\"");
    }
    

    Получатель


    Сразу весь код
    #include<stdio.h>               //For standard things
    #include<stdlib.h>              //malloc
    #include<string.h>              //memset
    #include<netinet/ip_icmp.h>     //Provides declarations for icmp header
    #include<netinet/udp.h>         //Provides declarations for udp header
    #include<netinet/tcp.h>         //Provides declarations for tcp header
    #include<netinet/ip.h>          //Provides declarations for ip header
    #include<sys/socket.h>
    #include<arpa/inet.h>
    
    void processPacket(unsigned char*);
    void handleMessage(unsigned char*);
    
    int sock_raw;
    // буфер для хранения сообщения
    char global_buffer[1024];
    // текущий размер сообщения
    int global_n = 0;
    char * src_addr, *dst_addr;
    
    int main(int argc, char* argv[]) {
        if (argc < 3) {
            puts("Enter source and destination ip");
            return 1;
        }
    
        src_addr = argv[1];
        dst_addr = argv[2];
    
        int saddr_size , data_size;
        struct sockaddr saddr;
    
        unsigned char *buffer = (unsigned char *)malloc(65536); //Its Big!
    
        puts("Starting...");
        // Создание "сырого" сокета, который будет прослушивать
        sock_raw = socket(AF_INET , SOCK_RAW , IPPROTO_TCP);
        if(sock_raw < 0)     {
            printf("Socket Error\n");
            return 1;
        }
    
        while(1) {
            saddr_size = sizeof saddr;
            // Получение пакета
            data_size = recvfrom(sock_raw , buffer , 65536 , 0 , &saddr , &saddr_size);
            if(data_size <0 )
            {
                printf("Recvfrom error , failed to get packets\n");
                return 1;
            }
            // Обработка пакета
            processPacket(buffer);
        }
        close(sock_raw);
        printf("Finished");
        return 0;
    }
    
    void processPacket(unsigned char* buffer) {
        // Получение указателя на IP заголовок пакета
        struct iphdr *iph = (struct iphdr*)buffer;
    
        // Если получен TCP пакет
        if (iph->protocol == IPPROTO_TCP) {
            // обработка сообщения
            handleMessage(buffer);
        }
    }
    
    void handleMessage(unsigned char *Buffer) {
        int i;
    
        struct iphdr *iph = (struct iphdr *)Buffer;
        struct tcphdr *tcph = (struct tcphdr *) (Buffer + sizeof (struct iphdr));
    
        struct sockaddr_in source,dest;
        // получение адресов цели и источника
        memset(&source, 0, sizeof(source));
        source.sin_addr.s_addr = iph->saddr;
    
        memset(&dest, 0, sizeof(dest));
        dest.sin_addr.s_addr = iph->daddr;
        // если адрес источника совпадает с требуемым
        if (source.sin_addr.s_addr == inet_addr(src_addr)
                && dest.sin_addr.s_addr == inet_addr(dst_addr)) {
            // выбор потока вывода
            setvbuf (stdout, NULL, _IONBF, 0);
            // массив с полезной нагрузкой
            char payload[6];
            // извлечение первой часли полезной нагрузки
            payload[0] = iph->id >> 8;
            payload[1] = iph->id & ((1 << 8) - 1);
            //извлечение второй части полезной нагрузки
            for (i = 0; i < 4; ++i) {
                payload[i + 2] = (tcph->seq >> i*8) & ((1 << 8 ) - 1);
            }
            // копирование глобальный буфер
            for (i = 0; i < 6; ++i) {
                global_buffer[global_n++] = payload[i];
            }
            // вывод полученной части сообщения
            for (i = 0; i < 6; ++i) {
                // проверка на окончание
                if (payload[i])
                    printf("%c", payload[i]);
                else {
                    // 0 => конец сообщения
                    // перенос строки
                    puts("");
                    // сброс текущей длины сообщения
                    global_n = 0;
                    // выход из цикла
                    break;
                }
            }
        }
    }

    Чтобы не нагружать код лишними передачами переменных, сообщение и его длину будем контролировать в глобальных переменных. На текущем этапе записывать сообщение не обязательно, но почему бы не позаботиться о его хранении заранее.


    // буфер для хранения сообщения
    char global_buffer[1024];
    // текущий размер сообщения
    int global_n = 0;

    Создаем "сырой" сокет для прослушивания и получаем пакеты в бесконечном цикле, с последующим вызовом функции обработки пакета.


    sock_raw = socket(AF_INET , SOCK_RAW , IPPROTO_TCP);
    
    // ...
    
    while(1) {
        // ...
    
        // получение пакета
        recvfrom(sock_raw , buffer , 65536 , 0 , &saddr , &saddr_size);
    
        //...
    
        // Обработка пакета
        processPacket(buffer);
    }

    После проверки на то, что был получен именно TCP пакет и именно с нужного адреса, извлекаем обе части полезной нагрузки.


    // ...
    
    char payload[6];
    // извлечение первой часли полезной нагрузки
    payload[0] = iph->id >> 8;
    payload[1] = iph->id & ((1 << 8) - 1);
    //извлечение второй части полезной нагрузки
    for (i = 0; i < 4; ++i)
        payload[i + 2] = (tcph->seq >> i*8) & ((1 << 8 ) - 1);
    
    // ...


    Пример работы


    (слева Получатель, справа отправитель)


    example1


    Нагрузка отлично видна при анализе пакета, например, в Wireshark. Так что было бы неплохо внести шифрование, но для Proof Of Consept мы этого делать не будем.


    example2


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


    Система одноразовых сообщений


    Хотелось бы передавать большие массивы данных в TCP пакетах, но это неудобно, так как стеганографическая скорость передачи данных достаточно мала. Однако никто не мешает оставлять сообщение в оговоренном месте, а таким местом может быть либо созданный специально для сегодняшнего эксперимента сайт, либо один из существующих.


    Реализация


    Детали под спойлером

    Тут дела обстоят несколько сложнее, чем в прошлый раз, потому что придется дополнить готовые приложения загрузкой страниц через https и их парсингом. Т.к. мы используем конкретный сайт, "парсинг" будет упрощен до нахождения нужной строчки в коде страницы и извлечения оттуда кода для ссылки по известным соседним символам.


    Для начала понадобится библиотека curl. По умолчанию она отправляет полученную веб-страницу в консоль, поэтому необходимо сделать буфер, в который мы хотим записывать исходный код страницы и callback функцию, соответствующую API curl, производящую ту самую запись.


    char html_buffer[65530];
    
    size_t writeCallback(void *contents, size_t size, size_t nmemb, void *userp) {
        size_t realsize = size * nmemb;
        memcpy(html_buffer, contents, realsize);
        return realsize;
    }

    Отправитель


    Весь код сразу
    #include<stdio.h>           //for printf
    #include<string.h>          //memset
    #include<sys/socket.h>      //for socket ofcourse
    #include<stdlib.h>          //for exit(0);
    #include<errno.h>           //For errno - the error number
    #include<netinet/tcp.h>     //Provides declarations for tcp header
    #include<netinet/ip.h>      //Provides declarations for ip header
    #include <unistd.h>
    #include <curl/curl.h>
    
    // 96 бит (12 байт) псевдо-заголовок, нужный для вычисления хэш-суммы
    struct pseudo_header {
        u_int32_t source_address;
        u_int32_t dest_address;
        u_int8_t placeholder;
        u_int8_t protocol;
        u_int16_t tcp_length;
    };
    
    unsigned short csum(unsigned short *ptr,int nbytes);
    size_t writeCallback(void *contents, size_t size, size_t nmemb, void *userp);
    char * findMessage();
    
    const char post_parameter[] = "message%5Bbody%5D=";
    char html_buffer[65530];
    
    int main (int argc, char* argv[]) {
        srand(time(NULL));
    
        if (argc < 3) {
            puts("Enter source and destination ip");
            return 1;
        }
    
        while (1) {
            puts("Enter payload:");
            // массив, содержащий переменную POST-запроса с сообщением
            char post[65530];
            memset(post, 0, sizeof(post));
            memcpy(post, post_parameter, sizeof(post_parameter)*sizeof(char));
            // считывание строки со смещением от начала
            fgets(post + sizeof(post_parameter) - 1, sizeof(post) - sizeof(post_parameter), stdin);
    
            // нуль-терминирование строки
            post[strlen (post) - 1] = '\0';
    
            puts(post);
            // переменные curl
            CURL *curl;
            CURLcode res;
    
            // инициализация xurl
            curl_global_init(CURL_GLOBAL_DEFAULT);
    
            curl = curl_easy_init();
            if(curl) {
            // сайт с одноразовыми сообщениями
            curl_easy_setopt(curl, CURLOPT_URL, "https://tmwsd.ws/messages");
            // передаем сообщение "test"
            curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post);
            // разрешаем перенаправление
            curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1);
            // выбор нашей функции записи
            curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback);
    
            // выполнение запроса
            res = curl_easy_perform(curl);
            //    /* Check for errors */
            if(res != CURLE_OK)
              fprintf(stderr, "curl_easy_perform() failed: %s\n",
                      curl_easy_strerror(res));
    
            /* always cleanup */
            curl_easy_cleanup(curl);
            }
    
            curl_global_cleanup();
    
            char *link = findMessage();
    
            puts("Link:");
            puts(link);
    
            // получение длины строки
            int length = strlen(link);
            // нуль-терминирование строки
    
            // завершение при отсутствии полезной нагрузки
            if (!length)
                break;
    
            // вычисление необходимого количества пакетов
            int n = (length + 5)/6;
    
            int i;
            for (i = 0; i < n; ++i) {
                // задержка между отправлением пакетов
                usleep(10000);
                // Создание сокета в RAW режиме
                // AF_INTER == PF_INER - IP v4
                int s = socket (PF_INET, SOCK_RAW, IPPROTO_TCP);
    
                if(s == -1) {
                    // Создание сокета закончилось ошибкой
                    // вероятно, из-за отсутствия привилегий
                    perror("Failed to create socket");
                    exit(1);
                }
    
                // побитовове представление пакета
                char datagram[4096] , source_ip[32] , *pseudogram;
    
                // инициализация нулями
                memset (datagram, 0, 4096);
    
                // IP заголовок
                struct iphdr *iph = (struct iphdr *) datagram;
    
                //TCP заголовок
                struct tcphdr *tcph = (struct tcphdr *) (datagram + sizeof (struct iphdr));
                struct sockaddr_in sin;
                struct pseudo_header psh;
    
                //some address resolution
                strcpy(source_ip , argv[1]);
                sin.sin_family = AF_INET;
                sin.sin_port = htons(80);
                sin.sin_addr.s_addr = inet_addr (argv[2]);
    
                // Заполнение IP заголовка
                // Минимальная корректная длина 5
                iph->ihl = 5;
                // IPv3
                iph->version = 4;
                // приоритет не важен
                iph->tos = 0;
                // вычисление общей длины пакета
                iph->tot_len = sizeof (struct iphdr) + sizeof (struct tcphdr);
                // первая часть полезной нагрузки
                iph->id = (6*i < length ? link[6*i] << 8 : 0)
                        + (6*i + 1 < length ? link[6*i + 1] : 0);
                // Если id == 0, он заменяеся на случайной значение
                // При присвоении ему единицы, первая половина останется нулевой,
                // что соответствует концу строки
                if (iph->id == 0)
                    iph->id = 1;
                // Первый фрагмент => нулевое смещение
                iph->frag_off = 0;
                // Стандартный во многих случаях TTL
                iph->ttl = 64;
                // Протокол TCP
                iph->protocol = IPPROTO_TCP;
                // Выставление нуля перед вычислением хэш-суммы
                iph->check = 0;
                // Исходящий IP
                iph->saddr = inet_addr ( source_ip );
                // IP точки назначения
                iph->daddr = sin.sin_addr.s_addr;
    
                // Вычисление хэша IP заголовка
                iph->check = csum ((unsigned short *) datagram, iph->tot_len);
    
                // Заголовок TCP
                // порт источника
                tcph->source = htons (20);
                // порт цели
                tcph->dest = htons (rand() % 10000); // "сканируем" разные порты
                tcph->ack_seq = 0;
    
                // Вторая часть полезной нагрузки
                tcph->seq = 0;
                int j;
                for (j = 0; j < 4; ++j)
                    tcph->seq += (6*i + 2 + j < length ? link[6*i + 2 + j] : 0) << 8*j;
    
                // сдвиг равен размеру заголовка
                tcph->doff = 5;
                // Из флагов интересует только SYN
                tcph->fin=0;
                tcph->syn=1;
                tcph->rst=0;
                tcph->psh=0;
                tcph->ack=0;
                tcph->urg=0;
                // максимальный размер окна
                tcph->window = htons (5840);
                // хэш-сумма будет заполнена при помощи псевдо-заголовка
                tcph->check = 0;
                // нулевая "важность"
                tcph->urg_ptr = 0;
    
                // Подготовка к вычислению хэша
                psh.source_address = inet_addr( source_ip );
                psh.dest_address = sin.sin_addr.s_addr;
                psh.placeholder = 0;
                psh.protocol = IPPROTO_TCP;
                psh.tcp_length = 0;
    
                int psize = sizeof(struct pseudo_header) + sizeof(struct tcphdr);
                pseudogram = (char*)malloc(psize);
    
                memcpy(pseudogram , (char*) &psh , sizeof (struct pseudo_header));
                memcpy(pseudogram + sizeof(struct pseudo_header) , tcph,
                       sizeof(struct tcphdr));
                tcph->check = csum( (unsigned short*) pseudogram , psize);
    
                free(pseudogram);
    
                //IP_HDRINCL чтобы сказать ядру, что заголовки включены в пакет
                int one = 1;
                const int *val = &one;
    
                if (setsockopt (s, IPPROTO_IP, IP_HDRINCL, val, sizeof (one)) < 0) {
                    perror("Error setting IP_HDRINCL");
                    exit(0);
                }
                // Отправка пакета
                if (sendto (s, datagram, iph->tot_len ,  0, (struct sockaddr *) &sin, sizeof (sin)) < 0) {
                    perror("sendto failed");
                }
                // Успех
                else {
                    // информация об отправленном пакете
                    printf ("Packet sent. \"" );
                    for (j = 0; j < 6; ++j)
                        if (6*i + j < length)
                        printf("%c", link[6*i + j]);
                    puts("\"");
                }
            }
    
            free(link);
        }
    
        return 0;
    }
    
    // функция вычисление хэш-суммы
    unsigned short csum(unsigned short *ptr,int nbytes) {
        register long sum;
        unsigned short oddbyte;
        register short answer;
    
        sum=0;
        while(nbytes>1) {
            sum+=*ptr++;
            nbytes-=2;
        }
        if(nbytes==1) {
            oddbyte=0;
            *((u_char*)&oddbyte)=*(u_char*)ptr;
            sum+=oddbyte;
        }
    
        sum = (sum>>16)+(sum & 0xffff);
        sum = sum + (sum>>16);
        answer=(short)~sum;
    
        return(answer);
    }
    
    size_t writeCallback(void *contents, size_t size, size_t nmemb, void *userp) {
        size_t realsize = size * nmemb;
        memcpy(html_buffer, contents, realsize);
        return realsize;
    }
    
    char * findMessage() {
        // нужна 75-я строка
        size_t i, j = 0;
        for (i = 0; i < 74; ++i) {
            while (html_buffer[j++] != '\n');
        }
    
        while (html_buffer[j++] != 'w');
        int first, last;
        first = j+2;
        while (html_buffer[++j] != '<');
        last = j;
    
        char * link = malloc(sizeof(char)*(last - first + 1));
    
        memset(link, 0, last - first + 1);
        memcpy(link, html_buffer + first, last - first);
    
        return link;
    }

    Чтобы создать сообщение, необходимо послать на https://tmwsd.ws/messages (или любой другой из их доменных имен) POST-запрос с, как минимум, заданной переменной message[body], которая и будет являться нашим сообщением. В результате сайт перенаправит на страницу, содержащую одноразувую ссылку.


    Пример страницы со ссылкой.


    example3


    Что будет после перехода по ссылке tmwsd.ws/4Gn30zLh

    Видим оставленное сообщение "test" и предупреждение о том, что сообщение удалится.


    example4


    Если снова попытать счастья и перейти по той же ссылке, сообщение увидеть не удастся.


    example5


    Из всей страницы нас интересует только адрес, т.е. в данном случае https://⌫.ws/4Gn30zLh, а т.к. доменное имя заранее оговорено, необходим только набор 4Gn30zLh. В исходном коде страницы он лежит на 75 строке в таком виде:


    <span id="message_url_366056">https://⌫.ws/4Gn30zLh</span>


    При чем число 366056 не является фиксированным. Итак можно сначала перейти на 75 строку, затем дойти до символа 'w', и начиная со смещения от него на 2 вправо до следующей угловой скобки получить сообщение


    char * findLink() {
        // нужна 74 строка
        size_t i, j = 0;
        for (i = 0; i < 74; ++i) {
            while (html_buffer[j++] != '\n');
        }
    
        while (html_buffer[j++] != 'w');
        int first, last;
        first = j+2;
        while (html_buffer[++j] != '<');
        last = j;
    
        char * msg = malloc(sizeof(char)*(last-first));
    
        for (i = first; i < last; ++i)
            msg[i - first] = html_buffer[i];
    
        return msg;
    }

    Автор не разбирается в вебе Что если попробоавать сломать алгоритм, отправив символ <? А ничего не произойдет, в исходном тексте страницы он будет в виде html кода &lt;.


    Проверка связи


    // переменные curl
    CURL *curl;
    CURLcode res;
    
    // инициализация curl
    curl_global_init(CURL_GLOBAL_DEFAULT);
    
    curl = curl_easy_init();
    if(curl) {
    // сайт с одноразовыми сообщениями
    curl_easy_setopt(curl, CURLOPT_URL, "https://tmwsd.ws/messages");
    // передаем сообщение "test"
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "message%5Bbody%5D=test");
    // разрешаем перенаправление
    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1);
    // выбор нашей функции записи
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
    
    // выполнение запроса
    res = curl_easy_perform(curl);

    После парсинга получаем рабочий адрес.


    example6


    Именно его надо будет передавать в TCP пакетах.


    Собирем воедино


    Для начала отведем переменную под постоянную часть POST-запроса.


    const char post_parameter[] = "message%5Bbody%5D=";

    В массив, содержащий POST-запрос копируется постоянная часть, а затем со смещением считывается сообщение.


    char post[65530];
    memset(post, 0, sizeof(post));
    memcpy(post, post_parameter, sizeof(post_parameter)*sizeof(char));
    // считывание строки со смещением от начала
    fgets(post + sizeof(post_parameter) - 1, sizeof(post) - sizeof(post_parameter), stdin);

    Далее выполняются уже известные манипуляции с curl и TCP пакетами.


    В результате можем передавать любое сообщение при помощи двух пакетов.


    example7


    Проверяем сообщение по ссылке.


    example8


    Осталось научиться принимать сообщения!


    Получатель


    Нагрузку извлекать умеем, html подгружать умеем, а сообщение находится на 31 строке.


    example9


    Снова собираем


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


    Код
    #include<stdio.h>           //for printf
    #include<string.h>          //memset
    #include<sys/socket.h>      //for socket ofcourse
    #include<stdlib.h>          //for exit(0);
    #include<errno.h>           //For errno - the error number
    #include<netinet/tcp.h>     //Provides declarations for tcp header
    #include<netinet/ip.h>      //Provides declarations for ip header
    #include <unistd.h>
    #include <curl/curl.h>
    
    // 96 бит (12 байт) псевдо-заголовок, нужный для вычисления хэш-суммы
    struct pseudo_header {
        u_int32_t source_address;
        u_int32_t dest_address;
        u_int8_t placeholder;
        u_int8_t protocol;
        u_int16_t tcp_length;
    };
    
    unsigned short csum(unsigned short *ptr,int nbytes);
    size_t writeCallback(void *contents, size_t size, size_t nmemb, void *userp);
    char * findMessage();
    
    const char post_parameter[] = "message%5Bbody%5D=";
    char html_buffer[65530];
    
    int main (int argc, char* argv[]) {
        srand(time(NULL));
    
        if (argc < 3) {
            puts("Enter source and destination ip");
            return 1;
        }
    
        while (1) {
            puts("Enter payload:");
            // массив, содержащий переменную POST-запроса с сообщением
            char post[65530];
            memset(post, 0, sizeof(post));
            memcpy(post, post_parameter, sizeof(post_parameter)*sizeof(char));
            // считывание строки со смещением от начала
            fgets(post + sizeof(post_parameter) - 1, sizeof(post) - sizeof(post_parameter), stdin);
    
            // нуль-терминирование строки
            post[strlen (post) - 1] = '\0';
    
            puts(post);
            // переменные curl
            CURL *curl;
            CURLcode res;
    
            // инициализация xurl
            curl_global_init(CURL_GLOBAL_DEFAULT);
    
            curl = curl_easy_init();
            if(curl) {
            // сайт с одноразовыми сообщениями
            curl_easy_setopt(curl, CURLOPT_URL, "https://tmwsd.ws/messages");
            // передаем сообщение "test"
            curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post);
            // разрешаем перенаправление
            curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1);
            // выбор нашей функции записи
            curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback);
    
            // выполнение запроса
            res = curl_easy_perform(curl);
            //    /* Check for errors */
            if(res != CURLE_OK)
              fprintf(stderr, "curl_easy_perform() failed: %s\n",
                      curl_easy_strerror(res));
    
            /* always cleanup */
            curl_easy_cleanup(curl);
            }
    
            curl_global_cleanup();
    
            char *link = findMessage();
    
            puts("Link:");
            puts(link);
    
            // получение длины строки
            int length = strlen(link);
            // нуль-терминирование строки
    
            // завершение при отсутствии полезной нагрузки
            if (!length)
                break;
    
            // вычисление необходимого количества пакетов
            int n = (length + 5)/6;
    
            int i;
            for (i = 0; i < n; ++i) {
                // задержка между отправлением пакетов
                usleep(10000);
                // Создание сокета в RAW режиме
                // AF_INTER == PF_INER - IP v4
                int s = socket (PF_INET, SOCK_RAW, IPPROTO_TCP);
    
                if(s == -1) {
                    // Создание сокета закончилось ошибкой
                    // вероятно, из-за отсутствия привилегий
                    perror("Failed to create socket");
                    exit(1);
                }
    
                // побитовове представление пакета
                char datagram[4096] , source_ip[32] , *pseudogram;
    
                // инициализация нулями
                memset (datagram, 0, 4096);
    
                // IP заголовок
                struct iphdr *iph = (struct iphdr *) datagram;
    
                //TCP заголовок
                struct tcphdr *tcph = (struct tcphdr *) (datagram + sizeof (struct iphdr));
                struct sockaddr_in sin;
                struct pseudo_header psh;
    
                //some address resolution
                strcpy(source_ip , argv[1]);
                sin.sin_family = AF_INET;
                sin.sin_port = htons(80);
                sin.sin_addr.s_addr = inet_addr (argv[2]);
    
                // Заполнение IP заголовка
                // Минимальная корректная длина 5
                iph->ihl = 5;
                // IPv3
                iph->version = 4;
                // приоритет не важен
                iph->tos = 0;
                // вычисление общей длины пакета
                iph->tot_len = sizeof (struct iphdr) + sizeof (struct tcphdr);
                // первая часть полезной нагрузки
                iph->id = (6*i < length ? link[6*i] << 8 : 0)
                        + (6*i + 1 < length ? link[6*i + 1] : 0);
                // Если id == 0, он заменяеся на случайной значение
                // При присвоении ему единицы, первая половина останется нулевой,
                // что соответствует концу строки
                if (iph->id == 0)
                    iph->id = 1;
                // Первый фрагмент => нулевое смещение
                iph->frag_off = 0;
                // Стандартный во многих случаях TTL
                iph->ttl = 64;
                // Протокол TCP
                iph->protocol = IPPROTO_TCP;
                // Выставление нуля перед вычислением хэш-суммы
                iph->check = 0;
                // Исходящий IP
                iph->saddr = inet_addr ( source_ip );
                // IP точки назначения
                iph->daddr = sin.sin_addr.s_addr;
    
                // Вычисление хэша IP заголовка
                iph->check = csum ((unsigned short *) datagram, iph->tot_len);
    
                // Заголовок TCP
                // порт источника
                tcph->source = htons (20);
                // порт цели
                tcph->dest = htons (rand() % 10000); // "сканируем" разные порты
                tcph->ack_seq = 0;
    
                // Вторая часть полезной нагрузки
                tcph->seq = 0;
                int j;
                for (j = 0; j < 4; ++j)
                    tcph->seq += (6*i + 2 + j < length ? link[6*i + 2 + j] : 0) << 8*j;
    
                // сдвиг равен размеру заголовка
                tcph->doff = 5;
                // Из флагов интересует только SYN
                tcph->fin=0;
                tcph->syn=1;
                tcph->rst=0;
                tcph->psh=0;
                tcph->ack=0;
                tcph->urg=0;
                // максимальный размер окна
                tcph->window = htons (5840);
                // хэш-сумма будет заполнена при помощи псевдо-заголовка
                tcph->check = 0;
                // нулевая "важность"
                tcph->urg_ptr = 0;
    
                // Подготовка к вычислению хэша
                psh.source_address = inet_addr( source_ip );
                psh.dest_address = sin.sin_addr.s_addr;
                psh.placeholder = 0;
                psh.protocol = IPPROTO_TCP;
                psh.tcp_length = 0;
    
                int psize = sizeof(struct pseudo_header) + sizeof(struct tcphdr);
                pseudogram = (char*)malloc(psize);
    
                memcpy(pseudogram , (char*) &psh , sizeof (struct pseudo_header));
                memcpy(pseudogram + sizeof(struct pseudo_header) , tcph,
                       sizeof(struct tcphdr));
                tcph->check = csum( (unsigned short*) pseudogram , psize);
    
                free(pseudogram);
    
                //IP_HDRINCL чтобы сказать ядру, что заголовки включены в пакет
                int one = 1;
                const int *val = &one;
    
                if (setsockopt (s, IPPROTO_IP, IP_HDRINCL, val, sizeof (one)) < 0) {
                    perror("Error setting IP_HDRINCL");
                    exit(0);
                }
                // Отправка пакета
                if (sendto (s, datagram, iph->tot_len ,  0, (struct sockaddr *) &sin, sizeof (sin)) < 0) {
                    perror("sendto failed");
                }
                // Успех
                else {
                    // информация об отправленном пакете
                    printf ("Packet sent. \"" );
                    for (j = 0; j < 6; ++j)
                        if (6*i + j < length)
                        printf("%c", link[6*i + j]);
                    puts("\"");
                }
            }
    
            free(link);
        }
    
        return 0;
    }
    
    // функция вычисление хэш-суммы
    unsigned short csum(unsigned short *ptr,int nbytes) {
        register long sum;
        unsigned short oddbyte;
        register short answer;
    
        sum=0;
        while(nbytes>1) {
            sum+=*ptr++;
            nbytes-=2;
        }
        if(nbytes==1) {
            oddbyte=0;
            *((u_char*)&oddbyte)=*(u_char*)ptr;
            sum+=oddbyte;
        }
    
        sum = (sum>>16)+(sum & 0xffff);
        sum = sum + (sum>>16);
        answer=(short)~sum;
    
        return(answer);
    }
    
    size_t writeCallback(void *contents, size_t size, size_t nmemb, void *userp) {
        size_t realsize = size * nmemb;
        memcpy(html_buffer, contents, realsize);
        return realsize;
    }
    
    char * findMessage() {
        // нужна 75-я строка
        size_t i, j = 0;
        for (i = 0; i < 74; ++i) {
            while (html_buffer[j++] != '\n');
        }
    
        while (html_buffer[j++] != 'w');
        int first, last;
        first = j+2;
        while (html_buffer[++j] != '<');
        last = j;
    
        char * link = malloc(sizeof(char)*(last - first + 1));
    
        memset(link, 0, last - first + 1);
        memcpy(link, html_buffer + first, last - first);
    
        return link;
    }

    Финальная проверка связи


    example10


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



    Желающие потестировать, ничего не компилируя, могут скачать виртуалку (гугл драйв, ЯД).


    Итоги


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

    Благодарю хаброюзера PavelMSTU за помощь в исследовании.

    Метки:
    • +19
    • 8,1k
    • 8
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 8
    • +4
      Круто, я тоже делал что-то подобное на tcp.
      У меня был ядерный netfilter модуль для linux, я вмешивался в передачу данных, подменял часть сегментов своими данными, на принимающей стороне, соответственно, детектировал такие невалидные сегменты и отбрасывал их. Далее tcp решал что пакет потерялся и пересылал его автоматически.
      • +1
        Классно!
        Код случайно не остался? Статья где-нибудь была?..
        И, если не секрет, зачем этим занимались? ;)
        just4fun?
      • +2

        Через прокси не проедет, там пакеты заново пересобираются

        • +2

          На самом деле даже через правильно настроенный фаервол.

        • +2
          Не особо силен в тонкостях протоколов, поэтому вопрос вызвают пара моментов:

          Поле ID нужно для того, чтобы собрать упорядоченные фрагменты данных. Т.е. в случае единственного пакета это поле не используется. Значит для наших целей уже можно использовать 2 байта.

          При многопакетной передаче вы все таки должны упорядочить пакеты?

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

          Об этом только один коммент, если не сложно — поясните плиз)

          Поле Sequence Number первого пакета соединения не является фиксированным, потому что иначе его бы могли знать злоумышленники (RCF 793). А это еще 4 байта.

          В чем его «фиксированность»?

          Получается что объем инфы, передаваемый на каждый пакет, варьируется?

          • 0
            Поле ID нужно для того, чтобы собрать упорядоченные фрагменты данных. Т.е. в случае единственного пакета это поле не используется. Значит для наших целей уже можно использовать 2 байта.

            При многопакетной передаче вы все таки должны упорядочить пакеты?

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


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

            Об этом только один коммент, если не сложно — поясните плиз)

            Пакеты "маскируются" под те, которые используются для сканирования портов, потому что каждый из них первый (и последний) в последовательности и именно это позволяет использовать Sequence Number.


            Поле Sequence Number первого пакета соединения не является фиксированным, потому что иначе его бы могли знать злоумышленники (RCF 793). А это еще 4 байта.

            В чем его «фиксированность»?

            Обычно в Sequence Number записан порядковый номер первого байта данных. Если же установлен флаг SYN (т.е. это первый пакет с попыткой соединения), в поле Sequence Number может быть любое случайно выбранное число.


            Получается что объем инфы, передаваемый на каждый пакет, варьируется?

            Нет, всегда 6 байт, если пакет дойдет до получатела в первозданном виде :)

            • +2
              В данном случае схема работает только если пакеты доходят в том же порядке, в котором были отправлены. Поэтому приходится жертвовать скоростью (задержка между пакетами), чтобы использовать поле ID не по назначению.


              Мне думается что в сетях где достаточно тонко настроен QoS — могут быть проблемы с сохранением порядка. Поправьте если не прав.
              • +1

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

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