Как стать автором
Обновить

Как передать информацию в ICMP-пакетах и не привлечь внимания санитаров

Время на прочтение8 мин
Количество просмотров25K
Всего голосов 98: ↑97 и ↓1+126
Комментарии28

Комментарии 28

ЗакрепленныеЗакреплённые комментарии

Ну, месяц прошёл. Приём ответов этим комментарием закрывается.

И ответ для желающих, а также явно тегну @whocoulditbe и @DimaFromMai, как людей, которые показали свой интерес, но не дошли до решения.

Задача этого дампа была использовать описанные в статье приёмы, но не сделать решение невозможным.

Шаг 1

Большая часть статьи посвящена тому, что символы кодируются как разница между временными метками в теле ICMP-пакета. Автоматизируем разбор дампа, в моем случае -- через scapy.

from scapy.layers.inet import ICMP
from scapy.utils import rdpcap

scapy_cap = rdpcap('contest.pcap')

data = []

for i in range(2, len(scapy_cap), 2):
    prev = scapy_cap[i - 2]
    curr = scapy_cap[i]

    assert prev[ICMP].type == 8
    assert curr[ICMP].type == 8

    p_s = int.from_bytes(prev[ICMP].load[0:8], "little")
    p_ms = int.from_bytes(prev[ICMP].load[8:16], "little")

    c_s = int.from_bytes(curr[ICMP].load[0:8], "little")
    c_ms = int.from_bytes(curr[ICMP].load[8:16], "little")

    diff = (c_s - p_s - 1) * 1_000_000 + (c_ms - p_ms)
    data.append(diff)

print(data)

Если считать с ограничением в миллисекунды, как указано в статье, то разница -- идеально точная секунда. Так не бывает :) Считаем в микросекундах и получается что-то такое:

[9, 0, 30, 6, 1, 12, 0, 13, 3, 12, 14, 2, 0, 5, 2, 6, 0, 7, 1, 9, 2, 8, 26, 2, 5, 5, 1, 0, 5, 4, 11, 3, 11, 1, 19, 1, 0, 7, 1, 1, 21, 29, 2, 5, 4, 16, 0, 6, 22, 11, 0, 7, 1, 9, 7, 2, 12, 0, 6, 22, 11, 0, 6, 1, 22, 5, 1, 0, 6, 22, 11, 0, 30, 6, 1, 0, 9, 7, 33, 0, 1, 21, 12, 3, 5]

Что делать с этим набором данным -- под следующим спойлером.

Шаг 2

Это, конечно, ещё не ответ, но раз нам обещали загадку, значит ответ где-то рядом. В статье можно найти такой абзац:

Если передавать необходимо текст, то можно использовать компактную кодировку. Например, только заглавные буквы алфавита и пробел: 34 символа для русского алфавита. А если алфавит отсортировать с учетом частотности символов, то отклонение множества пакетов будет в рамках погрешности и заметить передачу будет сложнее.

33 буквы алфавита + пробел = 34 символа. В нашем массиве данных минимальное значение 0, максимальное -- 33, отлично подходит. Переходим по ссылке и честно забираем оттуда буквы в порядке убывания частотности символа. При это заглавные буквы будут или строчные -- неважно, на ответ не влияет. Получается что-то такое:

voc = ["о", "е", "а", "и", "н", "т", "с", "р", "в", "л", "к", "м", "д", "п", "у", "я", "ы", "ь", "г", "з", "б", "ч", "й", "х", "ж", "ш", "ю", "ц", "щ", "э", "ф", "ъ", "ё"]

Остается вопрос: где должен быть пробел. Хотя пробел можно расположить куда угодно, это всего 33 строки на проверку, что относительно быстро перебирается. Но интуитивно хочется поместить в начало (код 0) или в конец (код 33).

dec = [voc[i] for i in data]
print("".join(dec))

Ответ

Пробел прячется на нулевой позиции. Ответ:

в этом дампе нет совершенно никакого сообщения тчк совсем тчк точно тчк это всё обман

При помощи ICMP-тоннеля.

Самые «неприметные» — это эхо-запросы.

  • Один ICMP-пакет может содержать до 65535 байт информации за вычетом заголовков IP и ICMP (ограничение IP-пакетов).

Ага, неприметные. Прям никто не заметит неприметный поток пингов на 65 килобайт каждый

Но вроде бы автор эти и отметил парой строк ниже?

  • Пакеты легко обнаружить из-за большого размера.

В этом и дело. Получается, что с одной стороны они неприметные, но тут же говорится о том, что они приметные.

Эхо-запросы по умолчанию (с их 32 или 56 байтами данных) весьма неприметные. И к пакетам с малым объемом минус "легко обнаружить из-за размера" действительно неприменим. Мало ли кто что пингует.

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

Меня больше интересует как в этом случае обойти ограничение MTU на оборудовании, которое в среднем 1500 Б?

ICMP-пакеты, как и UDP-пакеты, фрагментируются при превышении MTU.

У ping есть параметр "-p pattern".

Этот паттерн передаётся в "данных"?

Да. Если размер данных (флаг -s) больше паттерна, то паттерн вмещается несколько раз.

Я недавно тоже писал прототип key-value хранилища в пакетах пинга. В комментариях умные люди упомянули https://github.com/yarrick/pingfs. Пусть это будет тут тоже для будущих изобретателей.

А то гуглится информация не очень просто.

Любой NGFW ловит это на раз (про отечественные от PT например, другие сомневаюсь), если конечно включена функциональность

Хорошая попытка :)
"Отечественные от РТ", это тот, который pfsense перекрашенный?

Хм. А интересно, можно ли передавать данные через "ШИМ"? Ну то есть кодировать данные временем между пакетами, да хоть бы той же морзянкой (понятно, что надёжность и скорость невысоки).

Именно между пакетами? Маловероятно. Интернет - штука очень нестабильная, у вас нет никаких гарантий времени доставки. Один пакет может долететь получателю за 10 мс, другой за 20 мс, даже если вы отправили их с интервалом в 5мс. Более того, второй пакет может даже придти раньше первого:) а если ещё учесть, что ICMP нередко идёт на железках с самым низким приоритетом...

Ничто не мешает выставить задержку между пакетами, которая будет однозначно выше допустимого порога, скажем в 99%. Например при среднем пинге в 40мс, устанавливать дополнительную задержку в 200мс между пакетами на "1" и без задержки на "0", то какая будет вероятность, что пакет без задержки придет позже пакета с задержкой в 200мс?

Да и как бы это давно известные приемы стеганографии, про которые в статье просто не акцентируют внимание, но есть что-то похожее: "Теперь байт данных — это разница временных меток между пакетами."

И получить скорость около 5 бод. Тут даже модемные 56К покажутся сверхбыстрым соединением.

ну если совсем упороться, то можно в значении миллисекунд кодировать биты, например четная цифра это ноль, нечетная это один, за один пакет получится 6, но лучше 4 бита оставим два старших бита не изменными чтоб совсем не привлекать внимания, наверное самое палевное что останется это source и target таких пакетов

Нашёл такие символы
!"#$%&'()*+,-/0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTU

Не смог осилить дамп, подписался на комментарии в ожидании ответа.

Как я понимаю, в дампе достаточно данных, чтобы расшифровать его, не прибегая к дополнительным шагам, типа rot13 поверх текста? Нет никаких волшебных констант, участвующих в шифровании?

IP-чексуммы верные, к идентификаторам пакетов не прицепиться. Единственное, что смущает, - Метка пакета в будущем!, как было упомянуто в статье, но там можно шифроваться как угодно, например, делать xor между первым байтом чексуммы icmp-пакета + sequence number и данными. А можно и со вторым делать xor, почему бы и нет? Как насчёт "вычесть из первого байта ts третий байт и помножить его на второй байт чексуммы"? А что если данные зашифрованы алгоритмом, использующим identifier в качестве ключа? Количество возможных операций только с sequence number и временной меткой достаточно велико (а жизнь слишком коротка), чтобы перестать гадать и начинать разогревать паяльник.

А ещё у вас по адресу из дампа гитлаб по хттп смотрит наружу, это часть загадки?

А ещё у вас по адресу из дампа гитлаб по хттп смотрит наружу, это часть загадки?

В загадке участвует только дамп. Так что гитлаб по хттп не является частью загадки.

Ну, месяц прошёл. Приём ответов этим комментарием закрывается.

И ответ для желающих, а также явно тегну @whocoulditbe и @DimaFromMai, как людей, которые показали свой интерес, но не дошли до решения.

Задача этого дампа была использовать описанные в статье приёмы, но не сделать решение невозможным.

Шаг 1

Большая часть статьи посвящена тому, что символы кодируются как разница между временными метками в теле ICMP-пакета. Автоматизируем разбор дампа, в моем случае -- через scapy.

from scapy.layers.inet import ICMP
from scapy.utils import rdpcap

scapy_cap = rdpcap('contest.pcap')

data = []

for i in range(2, len(scapy_cap), 2):
    prev = scapy_cap[i - 2]
    curr = scapy_cap[i]

    assert prev[ICMP].type == 8
    assert curr[ICMP].type == 8

    p_s = int.from_bytes(prev[ICMP].load[0:8], "little")
    p_ms = int.from_bytes(prev[ICMP].load[8:16], "little")

    c_s = int.from_bytes(curr[ICMP].load[0:8], "little")
    c_ms = int.from_bytes(curr[ICMP].load[8:16], "little")

    diff = (c_s - p_s - 1) * 1_000_000 + (c_ms - p_ms)
    data.append(diff)

print(data)

Если считать с ограничением в миллисекунды, как указано в статье, то разница -- идеально точная секунда. Так не бывает :) Считаем в микросекундах и получается что-то такое:

[9, 0, 30, 6, 1, 12, 0, 13, 3, 12, 14, 2, 0, 5, 2, 6, 0, 7, 1, 9, 2, 8, 26, 2, 5, 5, 1, 0, 5, 4, 11, 3, 11, 1, 19, 1, 0, 7, 1, 1, 21, 29, 2, 5, 4, 16, 0, 6, 22, 11, 0, 7, 1, 9, 7, 2, 12, 0, 6, 22, 11, 0, 6, 1, 22, 5, 1, 0, 6, 22, 11, 0, 30, 6, 1, 0, 9, 7, 33, 0, 1, 21, 12, 3, 5]

Что делать с этим набором данным -- под следующим спойлером.

Шаг 2

Это, конечно, ещё не ответ, но раз нам обещали загадку, значит ответ где-то рядом. В статье можно найти такой абзац:

Если передавать необходимо текст, то можно использовать компактную кодировку. Например, только заглавные буквы алфавита и пробел: 34 символа для русского алфавита. А если алфавит отсортировать с учетом частотности символов, то отклонение множества пакетов будет в рамках погрешности и заметить передачу будет сложнее.

33 буквы алфавита + пробел = 34 символа. В нашем массиве данных минимальное значение 0, максимальное -- 33, отлично подходит. Переходим по ссылке и честно забираем оттуда буквы в порядке убывания частотности символа. При это заглавные буквы будут или строчные -- неважно, на ответ не влияет. Получается что-то такое:

voc = ["о", "е", "а", "и", "н", "т", "с", "р", "в", "л", "к", "м", "д", "п", "у", "я", "ы", "ь", "г", "з", "б", "ч", "й", "х", "ж", "ш", "ю", "ц", "щ", "э", "ф", "ъ", "ё"]

Остается вопрос: где должен быть пробел. Хотя пробел можно расположить куда угодно, это всего 33 строки на проверку, что относительно быстро перебирается. Но интуитивно хочется поместить в начало (код 0) или в конец (код 33).

dec = [voc[i] for i in data]
print("".join(dec))

Ответ

Пробел прячется на нулевой позиции. Ответ:

в этом дампе нет совершенно никакого сообщения тчк совсем тчк точно тчк это всё обман

Спасибо за решение, но честно говоря копал совсем в другую сторону.

Спасибо за решение.

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

Если TCP и UDP вам не доступны, то уезжайте из этой страны.

Если вы бездомный, то просто купите дом

Штирлиц.jpg

О, я такое в армии делала )) тоже скорее в качестве proof of concept чем реального применения - задача была выбрать протокол и незаметно передать через него данные. Мы правда пошли глубже, выбрали ARP, но другие команды выбирали и ICMP.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий