Геочаты, вредные боты и стеганография: пополняем знания о 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'\x01\x00')
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, заменяя аудио-дорожку:
Теперь надо сделать из этого VideoNote. Можно попробовать поискать работающие (кто-то из участников, например, нашел @TelescopyBot), а можно написать своего бота с помощью TelegramAPI.
Anyway, пересылаем нашему боту:
Получаем новый кругляш и поздравления (еще бы, такую работу проделали!), декодируем по уже отработанному сценарию аудио и получаем ключ: «nq2020SyOMK7SnnJP1sNlvbTs8zt35vUrrsD»
Да уж, не зря стеганография считается одной из самых сложных областей кибербезопасности — попробуй тут догадаться про все эти нюансы! Но участники NeoQUEST продемонстрировали прекрасную сноровку и чувство юмора при выполнении этого задания, так что адресуем им наше (от бота поздравления они уже получили) искреннее восхищение!