- PVSM.RU - https://www.pvsm.ru -

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

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

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

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

picture

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

  1. Создать с машины 1 сообщение на каком-либо ресурсе с одноразовым URL и получить ссылку (например, One-Time Secret [9]).
  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 [10] содержит следующее:

    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 [11]). А это еще 4 байта.

Реализация

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

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

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

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

Отправитель

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

#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] = '';
        // завершение при отсутствии полезной нагрузки
        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 ip));
            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 ip));

Внедрение первой части полезной нагрузки (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 Errorn");
        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 packetsn");
            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 ip));

    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

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

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

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

Реализация

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

Тут дела обстоят несколько сложнее, чем в прошлый раз, потому что придется дополнить готовые приложения загрузкой страниц через 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] = '';

        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: %sn",
                  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 ip));
            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 [17] (или любой другой из их доменных имен) 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] = '';

        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: %sn",
                  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 ip));
            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

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

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

Итоги

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

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

Автор: scidev

Источник [21]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/programmirovanie/260349

Ссылки в тексте:

[1] стеганография: https://habrahabr.ru/post/253045/

[2] Джон Торакис: https://github.com/operatorequals

[3] довольно: https://securosophy.com/2016/09/14/teaching-an-old-dog-not-that-new-tricks-stego-in-tcpip-made-easy-part-1/

[4] интересные: https://securosophy.com/2016/09/19/pozzo-lucky-stego-in-tcpip-part-2/

[5] вещи: https://securosophy.com/2016/09/28/pozzo-lucky-busted-the-tales-of-a-mathematician-soc-analyst/

[6] пост на Хабре: https://habrahabr.ru/post/60726/

[7] Covert_TCP: http://www-scf.usc.edu/~csci530l/downloads/covert_tcp.c

[8] система одноразовых сообщений: https://habrahabr.ru/post/132153/

[9] One-Time Secret: https://onetimesecret.com/

[10] RFC 791: https://tools.ietf.org/html/rfc791#page-13

[11] RCF 793: https://tools.ietf.org/html/rfc793#page-27

[12] binarytides.com: http://www.binarytides.com

[13] отправкой пакетов: http://www.binarytides.com/raw-sockets-c-code-linux/

[14] готовый сниффер: http://www.binarytides.com/packet-sniffer-code-c-linux/

[15] один: https://www.thismessagewillselfdestruct.com

[16] существующих: https://privnote.com/

[17] https://tmwsd.ws/messages: https://tmwsd.ws/messages

[18] гугл драйв: https://drive.google.com/drive/folders/0B8hGB0dDldqKbWpkdExJbmVfS2s?usp=sharing

[19] ЯД: https://yadi.sk/d/Hu2k_znl3L2JxS

[20] PavelMSTU: https://habrahabr.ru/users/pavelmstu/

[21] Источник: https://habrahabr.ru/post/332962/