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

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

wgf3ypumnzx-gms9aj5ixn7qllw.png


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

cbz_tiwdqa0j0cqdikmwjob704y.png


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

_corztflxylgeewrehp61prvssu.png


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

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

bbvrk4nu0xsdayswyptuv2z1gpw.png


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

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

Мы все продумали даже для тех, кто не любит криптографию — можно было воспользоваться готовым вариантом реализации атаки, например, этим.

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

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

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

exkyt__66acqprkte1kk_hljuge.png


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

gvtkk4hr8u_a8gsidkkqijtcuzy.png


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

rejyz0tnqx0lgvvcggpctdv2elq.png


rie3_vg77u1j1txjch9te5a8y1w.png


Формат aac → flac, частота 44100 Гц → 98000 Гц. Это выяснили, продолжаем дальше работать с аудио.

Ловким движением рук вытаскиваем его из видео:

erqljhyvzkhdq34zoxmfvl7ezty.png


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

gdgagqn_qtefqtnbyphj2ud0b00.png


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

mu7fzposl6z9ozjrxanvjfnh2hw.png


Предлагаем отложить в сторону все дела и немного посчитать. Анализируем по фрагментам:

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. Получаем не что иное, как бинарный код!
Вспоминаем, что изменилась частота, и пробуем взглянуть на график спектра (Анализ → График спектра):

7pusg01eyco5oawp9fyw92rbpzi.png


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

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

Отсекаем высокие частоты фильтром высоких частот либо в Audacity, либо в том же ffmpeg:

3ctktc0fgdc9zevjijzypncxb9o.png


Итак, у нас есть 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, заменяя аудио-дорожку:

7yvg_ulbp5wmx8jmz5vmxucgogu.png


Теперь надо сделать из этого VideoNote. Можно попробовать поискать работающие (кто-то из участников, например, нашел @TelescopyBot), а можно написать своего бота с помощью TelegramAPI.

rk_4nphgkerx68klet43x0npzrg.png


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

3xli0gyrmnawsnl6fyqa90slv6e.png


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

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

© Habrahabr.ru