Геочаты, вредные боты и стеганография: пополняем знания о Telegram

Геочаты, вредные боты и стеганография: пополняем знания о Telegram

Что ты знаешь о геочатах в Telegram? А сможешь различить стеганографию в VideoNote (в народе — кругляши)? Разбираем то самое задание NeoQUEST-2020 , которое вызвало больше всего вопросов и восклицаний на наш support! Спойлер: да-да, и здесь тоже будет немного крипты :)


В легенде NeoQUEST-2020 обнаруживаем ссылку на профиль путешествующего робота в Инстаграм. Ничего необычного, верно? Вот и мы тоже так решили, но решать задание все же надо, поэтому внимательно рассматриваем все картинки в профиле и ищем хоть какие-то подсказки. Немного медитации над красивой картинкой озера Байкал, и к нам приходит осознание, что зацепка находится именно в последнем посте:


Благодаря картинке понимаем, что нужно как-то связать Байкал (Shaman Rock) и Telegram («U can join my...» — ничего не напоминает?). Сначала мы решили не давать участникам прямого намека на геочат (а ведь это именно он!), и многие из них успешно справились с задачей, воспользовавшись эмулятором или мобильным устройством с возможностью смены геопозиции. Шаманим Задаем координаты (53.20074, 107.349426) (можно на глаз) в районе скалы Шаманки и готовимся к самому сложному — ожиданию. Телеграм странно работает с геопозицией и подтягивает соответствующие контакты и чаты в течение часа. За наше старание и терпение нам воздается сполна — искомый чат появляется в разделе Контакты ->Найти людей рядом -> Группы рядом.


Вуаля, мы в деле!


Бот встречает нас задачкой в виде файлика some.bytes с неопознанным содержимым, в котором можем прочитать строки «Decrypt me» и «Apocalypse Spares Nobody».

Первую строчку мы понимаем без всяких проблем, но вот что же означает вторая?.. Здесь участники поделились на два лагеря: одни писали нам на почту, так как попали в тупик, а другие внимательно вгляделись в словосочетание «Apocalypse Spares Nobody» и разглядели что? Верно! Старый-добрый формат ASN.1 ( здесь мы уже писали о том, как его парсить).


Давайте разбираться. Внутри находятся 2 структуры. В одной мы находим набор байтов с пометкой «Decrypt me», из чего предполагаем, что это шифртекст. Во второй структуре видим два числа. Вряд ли это ключ, щедро подаренный участником вместе с шифртекстом, значит, скорее всего. имеем дело с открытым ключом. Вся собранная информация приводит нас к очевидному выводу — почему бы не попробовать RSA ?

Итак, перед нами модуль и открытый показатель, который, к слову, достаточно большой. После судорожного изучения RSA недолгих раздумий приходим к выводу, что закрытый показатель мал, а это значит что? Бинго! Мы определенно можем поиграть в «плохишей» и применить атаку Винера .
Мы все продумали даже для тех, кто не любит криптографию — можно было воспользоваться готовым вариантом реализации атаки, например, этим .

Дальше мы получаем значение закрытого показателя d=40553818206320299896275948250950248823966726834704013657854904761789429403771 и расшифровываем шифртекст: key=nq2020faAeFeGUCBjYf7UDrH9FapFCdFPa4u;pass=passCxws3jzYhp0HD5Fy84.

Получаем ключ «nq2020faAeFeGUCBjYf7UDrH9FapFCdFPa4u» к первой части задания и пароль «passCxws3jzYhp0HD5Fy84», который нужно скормить бот-представителю. Его можно найти среди участников чата под именем @neoquestbot.

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


Зато бот с радостью принимает сообщения-кругляши VideoNote и даже отвечает на них… тем же кругляшом:


Кажется, что и видео, и звук те же самые, но это только на первый взгляд. А вдруг наш бот подает нам какие-то тайные знаки? Для выяснения этого сохраним и сравним оригинальное видео с ответом бота. Для этого и для последующих шагов нам отлично подходит пакет FFmpeg . Итак, посмотрим, что тут есть:



Формат aac -> flac, частота 44100 Гц -> 98000 Гц. Это выяснили, продолжаем дальше работать с аудио.
Ловким движением рук вытаскиваем его из видео:


То же самое можно сделать с нашим оригинальным сообщением, чтобы потом их сравнить. Для наглядности откроем обе дорожки в Audacity .


Сразу в глаза бросается скачок амплитуды в аудио-ответе бота (особенно странно, если мы вообще молчали). При более близком рассмотрении заметим четкие границы интервалов при чередовании «волна-тишина»:


Предлагаем отложить в сторону все дела и немного посчитать. Анализируем по фрагментам:
0 — 0,005 – тишина
0,005 – 0,01 – волна
0, 01 – 0,0225 – тишина
0,025 – 0,04 – волна
0,04 – 0,045 – тишина
Самый маленький интервал – 0,005, и при этом все остальные интервалы кратны 0,005.
Примем наличие волны в 0,005 за 1, а тишину за 0. Получаем не что иное, как бинарный код!
Вспоминаем, что изменилась частота, и пробуем взглянуть на график спектра (Анализ -> График спектра):


Видим, что самый мощный сигнал приходится на частоту ~44100 Гц, что является ультразвуком.
Значит, дальше следует работать только с высокими частотами.

На самом деле бот накладывает свой сигнал на оригинальное аудио в слышимом спектре. И те участники, у кого в оригинальном видео был звук, заметили это в Audacity.
Отсекаем высокие частоты фильтром высоких частот либо в Audacity, либо в том же ffmpeg:


Итак, у нас есть 16-битный моно wav-файл. Он состоит из заголовка, несжатого аудио-потока и метаданных. Сам по себе аудио-поток делится на фреймы (а фреймы могут хранить в себе несколько семплов, но это уже совсем другая история), в нашем случае по 16 бит (об этом говорят буковки pcm_s16 на скриншотах). Фреймы представляют собой последовательности бит, описывающие амплитуду волны в момент времени для одного или нескольких каналов (в нашем случае – для одного). Частота дискретизации аудио-потока равна 98000 (то есть на одну секунду приходится по 98000 фреймов), на интервал в 0,005 секунд приходится 490 фреймов. Следовательно, далее работаем по простому алгоритму: считываем по 490 фреймов, определяем, волна это или тишина, и, в зависимости от этого, выставляем бит в 0 или 1.

Воспользуемся python и пакетом wave для парсинга wav-файлов.
Если при открытии файла возникает ошибка «wave.Error: unknown format: 65534», то заменяем «wFormatTag» в заголовке с 'FE FF' на '01 00':
fh = open(input_file, "r+b") fh.seek(20) fh.write(b'x01x00') fh.close() 

Итак, открываем файл, обрабатываем по 490 фреймов и высчитываем усредненное значение:
file = wave.open(input_file,"r")     for i in range (1, int(file.getnframes()/490)+1):         frames = file.readframes(490)         bit = 0         sum = 0         for k in range(0, 246):             frame_bytes = frames[k*2:k*2+2]             sum += int.from_bytes(frame_bytes, "big")         if sum/490 > 16000:             bit = 1         bits.append(bit) 

Возможно, что там, где должна быть тишина (сравниваем с картинкой в Audacity), могут оставаться шумы. Поэтому задаем порог (пусть будет 16000), при превышении которого считаем сигнал равным 1.
Затем группируем биты в байты:
bytes = []     for i in range (1, int(len(bits)/8)+1):         b1 = bits[i*8-8]         b2 = bits[i*8-7]         b3 = bits[i*8-6]         b4 = bits[i*8-5]         b5 = bits[i*8-4]         b6 = bits[i*8-3]         b7 = bits[i*8-2]         b8 = bits[i*8-1]         byte = (b1 << 7) | (b2 << 6) | (b3 << 5) | (b4 << 4) | (b5 << 3) | (b6 << 2) | (b7 << 1) | b8         bytes.append(byte.to_bytes(1, byteorder='big'))  

Если все сделано правильно, в результате получаем строку «Givemethepassword». Поскольку бот общается кругляшами с применением стеганографии, будет логичным подсунуть ему пароль (а мы его получили вместе с ключом в результате расшифрования) в том же формате.
Для начала составляем аудио-дорожку с паролем. Для этого используем данные, полученные при разборе сообщения от бота: частота дискретизации 98000 Гц; продолжительность сигнала, описывающего каждый бит – 5 мс; частота сигнала, соответствующая битовому значению «1» — как мы видели по графикам, 44100 Гц.
Теперь нам нужно «сгенерировать» тишину. Делаем это занулением:
sample_rate = 98000.0 def generate_silence(duration_milliseconds=5):     fragment = []     num_samples = duration_milliseconds * (sample_rate / 1000.0)     for x in range(int(num_samples)):          fragment.append(0.0)     return fragment 

Для генерации звука будем использовать синусоиду (информацию можно прочитать тут ):
def generate_sinewave(         freq=41000.0,          duration_milliseconds=5,          volume=0.5):     fragment = []     amplitude = volume * 32767.0     num_samples = duration_milliseconds * (sample_rate / 1000.0)     for x in range(int(num_samples)):         fragment.append(amplitude * math.sin(2 * math.pi * freq * ( x / sample_rate )))     return fragment 

Теперь дело за малым: осталось преобразовать пароль в биты, а затем и в звук.

Примечание: Бот использует оригинальную дорожку входного видео, чтобы наложить на нее свое сообщение, как было упомянуто ранее. Поэтому нужно добавить несколько нулевых байт после пароля, чтобы вытрясти из бота целый ключ, а не только его начало (длина ключа составляла 36 байт).
Генерация звука
    audio = []     f = open(input_file, 'rb')     for character in f.read():         a = character         b8 = a & 0b00000001          b7 = (a & 0b00000010) >> 1          b6 = (a & 0b00000100) >> 2         b5 = (a & 0b00001000) >> 3         b4 = (a & 0b00010000) >> 4         b3 = (a & 0b00100000) >> 5         b2 = (a & 0b01000000) >> 6         b1 = (a & 0b10000000) >> 7         if b1 == 1:             audio += generate_sinewave()         else:             audio += generate_silence()         if b2 == 1:             audio += generate_sinewave()         else:             audio += generate_silence()         if b3 == 1:             audio += generate_sinewave()         else:             audio += generate_silence()         if b4 == 1:             audio += generate_sinewave()         else:             audio += generate_silence()         if b5 == 1:             audio += generate_sinewave()         else:             audio += generate_silence()         if b6 == 1:             audio += generate_sinewave()         else:             audio += generate_silence()         if b7 == 1:             audio += generate_sinewave()         else:             audio += generate_silence()         if b8 == 1:             audio += generate_sinewave()         else:             audio += generate_silence() 


Теперь сформируем готовый WAV-файл:
wav_file=wave.open(file_name,"w")     nchannels = 1     sampwidth = 2     nframes = len(audio)     comptype = "NONE"     compname = "not compressed"     wav_file.setparams((nchannels, sampwidth, sample_rate, nframes, comptype, compname))     for sample in audio:         wav_file.writeframes(struct.pack('h', int(sample)))     wav_file.close() 

Сохраняем нашу дорожку, например, в pass.wav. Попутно проверяем нашим стего-декодером, распознается ли пароль. Если все хорошо, то получаем новое видео с паролем из первоначального видео my_video.mp4, заменяя аудио-дорожку:

<img align=«center»
src=« habrastorage.org/webt/7y/vg/_u/7yvg_ulbp5wmx8jmz5vmxucgogu.png » />
Теперь надо сделать из этого VideoNote. Можно попробовать поискать работающие (кто-то из участников, например, нашел @TelescopyBot), а можно написать своего бота с помощью TelegramAPI.


Anyway, пересылаем нашему боту:


Получаем новый кругляш и поздравления (еще бы, такую работу проделали!), декодируем по уже отработанному сценарию аудио и получаем ключ: «nq2020SyOMK7SnnJP1sNlvbTs8zt35vUrrsD»

Да уж, не зря стеганография считается одной из самых сложных областей кибербезопасности — попробуй тут догадаться про все эти нюансы! Но участники NeoQUEST продемонстрировали прекрасную сноровку и чувство юмора при выполнении этого задания, так что адресуем им наше (от бота поздравления они уже получили) искреннее восхищение!
Alt text

Домашний Wi-Fi – ваша крепость или картонный домик?

Узнайте, как построить неприступную стену