• [ Регистрация ]Открытая и бесплатная
  • Tg admin@ALPHV_Admin (обязательно подтверждение в ЛС форума)

Статья Реверсим файлы, зашифрованные алгоритмом

stihl

bot
Moderator
Регистрация
09.02.2012
Сообщения
1,409
Розыгрыши
0
Реакции
732
Deposit
0.228 BTC
stihl не предоставил(а) никакой дополнительной информации.
В этой статье мы разберем практическое применение потокового шифра Rabbit на примере реального приложения: опознаем алгоритм по коду, проследим поток чтение → обработка → запись, найдем процедуру инициализации, а потом напишем собственный модуль для шифрования и дешифровки файлов.
Я уже неоднократно писал про Для просмотра ссылки Войди или Зарегистрируйся. На этот раз хочу обратить внимание на потоковый шифр Rabbit. Этот шифр был создан в качестве более шустрой альтернативы классическим крипторам типа AES, его даже отправляли на конкурс eStream, но пока безрезультатно. Впрочем, про то, как и кем он был создан и почему считается быстрым, а также всю теорию и лирику, касающуюся этого криптоалгоритма, ты и сам на досуге можешь Для просмотра ссылки Войди или Зарегистрируйся. А я, как обычно, разберу практические аспекты его применения и реверса на примере конкретного приложения.

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


Файлы данных, которые нам предстоит исследовать, очень объемные и совершенно несжимаемые, энтропия просто зашкаливает, и, на первый взгляд, заполнены они исключительно белым шумом. Попробуем оценить динамику блочной записи в них при помощи программы Process Monitor. Ставим фильтр на файловые операции и имя нашего приложения, затем включаем процесс экспорта. В логе Procmon видно, что файл пишется последовательными блоками размером по 0x40000 байт, причем перед каждой записью блок примерно такой же длины считывается из входного файла.

Для просмотра ссылки Войди или Зарегистрируйся
То есть никакой компрессии формат не использует, исключительно шифрование. Попробуем проследить, что именно происходит с блоком данных между чтением из исходного файла и записью в исследуемый. Для этого мы снова действуем по схеме, которую я уже неоднократно описывал: открываем программу в нашем любимом отладчике x64dbg и непосредственно перед операцией сохранения ставим точку останова на функцию kernel32.ReadFile. На всякий случай поставив условие для останова — фильтр по длине, равной 4096=0x1000, поскольку мы обратили внимание, что блоки такого размера частенько читаются из входного файла данных. Дальнейшую последовательность действий, я думаю, ты уже выучил наизусть по моим предыдущим статьям — как только программа останавливается для чтения очередного блока данных, мы смотрим стек вызовов и определяем место вызова текущего fread из основной программы. В IDA это место выглядит так.

Для просмотра ссылки Войди или Зарегистрируйся
Код похож именно на то, что мы искали, — цикл чтения/сохранения данных блоками 0x40000 байт. Попробуем проследить, какие манипуляции происходят с блоком данных между fread и fwrite.

Для просмотра ссылки Войди или Зарегистрируйся
Легко видеть, что считанные в буфер v21 данные из него же и сохраняются, однако побывав перед этим в процедуре sub_1402CBAA0. Внутри нее нас ожидает ужасная мешанина головоломного кода со множеством битовых операций и «волшебных» констант.

Для просмотра ссылки Войди или Зарегистрируйся
Сильно похоже на криптоалгоритм, причем не шибко популярный. Навскидку загуглив первую же встретившуюся константу из него — 749914925=0x2CB2CB2D, с огорчением видим, что Гугл с его искусственным интеллектом ничего вразумительного подсказать не может. По счастью, у нас есть более действенные инструменты. К примеру, загрузив нашу исследуемую программу в Krypto analyzer, мы убеждаемся в том, что имеем дело именно с Rabbit.

Для просмотра ссылки Войди или Зарегистрируйся
Попробуем теперь разобраться в том, каким боком этот алгоритм прикручен к решению нашей задачи и как реализовать его функционирование в собственной программе. Для этого нам все‑таки придется слегка нырнуть в теорию. Бегло прочитаем описание алгоритма, статью из Википедии и для большей наглядности — Для просмотра ссылки Войди или Зарегистрируйся. Чтобы тебе было проще разобраться в этой высоконаучной писанине, попробую на пальцах объяснить суть того, что мы ищем.

Как любой блочный криптор, Rabbit реализует этапы инициализации начального состояния и собственно шифрование‑расшифровку блока данных с последующей сменой состояния. Найденная нами процедура sub_1402CBAA0 и является реализацией последнего этапа. Шифрование блока в Rabbit выполняется путем побайтового XOR текущего блока данных со 128-битным блоком шифрования (в спецификации его называют блоком S). Это значит, что алгоритм симметричен, причем шифрование идентично расшифровке, поскольку для расшифровки блока достаточно сделать XOR зашифрованного блока с таким же шифрующим блоком S, как и для шифрования.

В свою очередь, при каждой смене внутреннего состояния шифрующий блок формируется из вектора состояния, в котором восемь 32-битных переменных состояний (названных в спецификации X) и восемь 32-битных счетчиков переменных (С). Как я уже говорил, после шифрования каждого блока вектор состояния хитрым образом меняется. Слегка вглядевшись в код, мы видим, что на входе процедуры sub_1402CBAA0 указатель на вектор состояния передается первым параметром (блоки X и C объединены в один 0x40-байтовый массив), а указатель на шифруемый (или, в зависимости от контекста, расшифровываемый) блок — вторым и зачем‑то третьим параметрами. Попробуем найти теперь процедуру инициализации состояния, а через нее — ключ шифрования.

Мы уже поняли, что на скриншоте, озаглавленном «Манипуляции с блоком данных», указатель на вектор состояния находится в переменной v32. Первое упоминание этой переменной (очевидно, инициализация) мы обнаружим, поднявшись чуть выше по коду в процедуре sub_1402CB590 (скриншот «Место вызова fread»). Однако на процедуру инициализации эта функция не похожа, как минимум по логике получается, что в этом случае состояние бы переинициализировалось после чтения каждого блока. Да и не похожа входная переменная v31 на ключ шифрования своим размером — ключ шифрования у Rabbit тоже 128-битный.

А вот процедура sub_1402CB0E0(v31, v18, 16), в которой v31 инициализируется, выглядит именно как начальная инициализация вектора состояния, причем даже ключ понятен — v18. На самом деле, как ты, вероятно, уже догадался, процедура sub_1402CB590 тоже играет важную роль в процессе шифрования, но мы вернемся к ней чуть позднее.

А сейчас, когда у нас есть ключ шифрования, уже можно подумать над самостоятельной реализацией процесса, тем более что проверить все наши гипотезы мы можем, только самостоятельно зашифровав и расшифровав данные. А реализовывать алгоритм придется самим — как я предупреждал в самом начале, Rabbit пока еще далек от внесения в стандарты потоковых крипторов и, соответственно, в стандартные библиотеки не входит. Более того, даже самописные его реализации особо не гуглятся. Самое вменяемое, что я нашел, — это заброшенный десять лет назад в состоянии полуготовности Для просмотра ссылки Войди или Зарегистрируйся. Если ты любитель Java, то можешь использовать его, лично я адаптировал его на С#, благо синтаксис у них почти идентичный. Для адаптации этого кода надо всего‑навсего корректно исправить int на UInt32 и long на UInt64, да по мелочи обработать напильником.

Однако итог нас не сильно радует: первый же сгенерированный нашим кодом шифрующий блок S не совпадает с рассчитанным программой, соответственно, данные шифруются неверно. Копнув код чуть глубже, мы обнаруживаем, что инициализация начального состояния sub_1402CB0E0 проходит правильно — полученный вектор v31 совпадает с расчетным, а вот вектор состояния v32, который используется в процедуре шифрования sub_1402CBAA0, уже от него отличается и нам не подходит.

Итак, мы снова вернулись к процедуре sub_1402CB590, преобразующей вектор состояния v31 в v32. Внимательно посмотрев на код RabbitCipher.java, мы обнаруживаем, что там не реализован весьма важный этап — настройка вектора исполнения (IV). На соответствующей процедуре setupIV(byte[] IV) стоит заглушка с комментарием, чтобы пытливые люди вроде нас с тобой не расслаблялись, а реализовали ее самостоятельно. Раз так, попробуем проделать этот фокус.

В описании алгоритма из Википедии вообще опустили этот этап, видимо посчитав его незначительным. Можно вытащить его из кода программы, реверсировав процедуру sub_1402CB590, но это достаточно муторно, поэтому открываем главу 2.4 спецификации, ссылку на которую я давал выше, и читаем, что там написано по этому поводу:

2.4. IV Setup Scheme
Код:
If an IV is used for encryption, the counter variables are modified
 after the key setup. Denoting the IV bits by IV[63..0], the setup
 proceeds as follows:
 C0 = C0 ^ IV[31..0] C1 = C1 ^ (IV[63..48] || IV[31..16])
 C2 = C2 ^ IV[63..32] C3 = C3 ^ (IV[47..32] || IV[15..0])
 C4 = C4 ^ IV[31..0] C5 = C5 ^ (IV[63..48] || IV[31..16])
 C6 = C6 ^ IV[63..32] C7 = C7 ^ (IV[47..32] || IV[15..0])

The system is then iterated another 4 times, each iteration
consisting of counter update (Section 2.5) and next-state function
(Section 2.6).
The relationship between key and IV setup is as follows:
- After the key setup, the resulting inner state is saved as a master
state. Then the IV setup is run to obtain the first encryption
starting state.
- Whenever re-initialization under a new IV is necessary, the IV
setup is run on the master state again to derive the next
encryption starting state.
То есть, говоря простыми словами, процедура настройки вектора исполнения заключается в том, чтобы разбить 64-битный инициализационный вектор исполнения IV на четыре 16-битных фрагмента, которые нужно в определенном порядке поксорить со счетчиками переменных C и затем четыре раза сменить состояние. Так что, несмотря на весь ужас, который мы видим в декомпилированной процедуре sub_1402CB590 в IDA, нормальная ее реализация выглядит так:

Код:
public void setupIV(UInt16 [] IV)
        {
            C[0] ^= (IV[0] | ((UInt32)IV[1] << 16));
            C[1] ^= (((UInt32)IV[3] << 16) | IV[1]);
            C[2] ^= (IV[2] | ((UInt32)IV[3] << 16));
            C[3] ^= (((UInt32)IV[2] << 16) | IV[0]);
            C[4] ^= (IV[0] | ((UInt32)IV[1] << 16));
            C[5] ^= (((UInt32)IV[3] << 16) | IV[1]);
            C[6] ^= (IV[2] | ((UInt32)IV[3] << 16));
            C[7] ^= (((UInt32)IV[2] << 16) | IV[0]);
            nextState();
            nextState();
            nextState();
            nextState();
        }

Какое же число используется в качестве IV, что характерно, меняющееся после чтения каждого блока? На первом скриншоте мы видим, что при вызове sub_1402CB590 это значение находится в переменной v25, имеющей тип int64, в начале обнуляемой и инкрементируемой после чтения каждого блока. То есть это банальный счетчик блоков. Вставляем его параметром setupIV в нашей реализации и убеждаемся, что теперь файл криптуется и декриптуется правильно.

Итак, все пункты поставленной задачи нами выполнены — мы опознали алгоритм шифрования, используемый при кодировании редкого формата данных, реверсировали специфические особенности его использования и даже написали собственный модуль, повторяющий эти действия. Не обещаю, что данный навык сильно понадобится тебе в дальнейшей работе, все‑таки алгоритм достаточно молодой и редко употребимый, но кто знает? Если он действительно так хорош по скорости и надежности, как обещают разработчики, возможно, в определенном будущем он потеснит AES и RC4 в обойме распространенных потоковых шифров.
 
Activity
So far there's no one here