Если ты решил стать хакером, то на своем непростом тернистом пути ты наверняка разобрал не один стандартный криптоалгоритм. Да что там говорить, мы и вместе изучали алгоритмы Rabbit и EdDSA. Думаю, продираясь сквозь суровый матан (точнее, сквозь дискретную математику) и теорию этих алгоритмов, ты задавал себе вопрос: а зачем вообще такие сложности? К чему эти ученые‑верченые так намудрили, зачем в каждом алгоритме именно такое количество битов, тактов, волшебных констант, которые усложняют и замедляют выполнение кода, а главное, позволяют его безошибочно детектировать, к примеру с помощью Krypto Analyzer? Почему нельзя замутить свой собственный уникальный криптоалгоритм имени себя, скажем поменяв значения констант, упростив для скорости микширование регистров или уменьшив либо увеличив их битность? Ведь на выходе криптора все равно будет высокоэнтропийная каша, и прямой брутфорс займет срок величиной с десяток возрастов Вселенной. В этой статье мы на конкретном примере разберем, чем чревата подобная самодеятельность в составлении криптоалгоритма.
Итак, задача: у нас имеется некий софт от очень известной телекоммуникационной фирмы, название которой я не буду упоминать. Софт оформлен в виде локального веб‑приложения с интерфейсом в браузере (мы разбирали подобное в статье «В обход стражи. Отлаживаем код на PHP, упакованный SourceGuardian»). Лицензирование программы происходит тут же, в браузере, путем загрузки лицензионного файла. Он привязан к конкретному компьютеру пользователя (точнее, к серийному номеру программы, который отсылается вендору для получения лицензионного файла). Файл набит высокоэнтропийными бинарными данными, что наталкивает на мысль: он закриптован, причем ключ расшифровки как‑то связан с серийником. Поэтому наша задача — реверсировать и, по возможности, обратить алгоритм шифрования (исключительно в исследовательских целях).
Не буду углубляться в особенности реализации браузерного веб‑приложения: мы с такими уже сталкивались, да и вообще статья не об этом. В двух словах отмечу, что для работы мы используем отладчик x32dbg, которым аттачимся к приложению, реализующему обработку запросов к localhost, затем простым поиском в памяти строки «Cannot open license file» легко выходим на код открытия лицензионного файла и чтения из него зашифрованных данных.

Дальше дело техники: ставим точку останова на этот код и смотрим, в какой момент высокоэнтропийная каша в буфере чтения из файла превратится во что‑либо более осмысленное. Долго ждать не приходится: буквально несколькими строками ниже происходит расшифровка.

Как видишь, после двух последовательных вызовов sub_10002450( и sub_10002770( буфер lpBuffer содержит текстовые данные лицензии, которые уже следующей строкой начинают анализироваться. Посмотрим, что внутри этих функций. Начнем с sub_10002450. Ее IDA’шный псевдокод выглядит примерно вот так:
void __thiscall sub_10002450(OLECHAR ***this, OLECHAR *psz) // this — родительский объект криптора, pcz — текстовая строка, ключ расшифровки{ v2 = psz; lpWideCharStr = psz; if ( psz && wcslen(psz) ) { sub_10001B10(this + 1, psz); memset(this + 2, 0, 0x100u); this[66] = 0; this[67] = (OLECHAR **)324508639; this[68] = (OLECHAR **)610839776; this[69] = (OLECHAR **)-38177487; this[70] = (OLECHAR **)-2147483550; this[71] = (OLECHAR **)1073741856; this[72] = (OLECHAR **)268435458; this[73] = (OLECHAR **)0x7FFFFFFF; this[74] = (OLECHAR **)0x3FFFFFFF; this[75] = (OLECHAR **)0xFFFFFFF; this[76] = (OLECHAR **)0x80000000; this[77] = (OLECHAR **)-1073741824; this[78] = (OLECHAR **)-268435456; ... // копирование первых 12 байт ключа pcz в массив v24 с нулевым байтом-заполнителем v18 = v24[3] | ((v24[2] | ((v24[1] | (v24[0] << 8)) << 8)) << 8); v19 = v24[7] | ((v24[6] | ((v24[5] | (v24[4] << 8)) << 8)) << 8); v20 = v24[9] | (v24[8] << 8); this[68] = (OLECHAR **)v19; v21 = v24[11] | ((v24[10] | (v20 << 8)) << 8); this[69] = (OLECHAR **)v21; if ( !v18 ) v18 = 324508639; this[67] = (OLECHAR **)v18; if ( !v19 ) this[68] = (OLECHAR **)610839776; if ( !v21 ) this[69] = (OLECHAR **)-38177487; }}Похоже на классическую инициализацию объекта криптора, с которой мы не раз сталкивались при анализе других криптоалгоритмов. Строки this[ — this[ — это явно слово состояния алгоритма, состоящее из 13 32-битных переменных, инициализирующихся шестнадцатеричными константами 0, 13579BDF, 2468ACE0, FDB97531, 80000062, 40000020, 10000002, 7FFFFFFF, 3FFFFFFF, FFFFFFF, 80000000, C0000000 и F0000000. Затем вторая, третья и четвертая переменные состояния переинициализируются 12 первыми байтами ключа.
Как выглядит сам ключ, мы уже успели подсмотреть в отладчике. Это слепленные вместе строки: три десятичные цифры версии продукта, три десятичные цифры подверсии и его серийный номер. Например, для имеющейся у нас версии 5.15 ключ будет 0050150900-0, а соответствующие ему значения переменных — 30303530, 31353039 и 30302d30.
Не буду загромождать повествование излишними подробностями перевода псевдокода функции sub_10002770 на C# с сокращением, скажу только, что при этом выясняется любопытный факт: реальное слово состояния криптора сокращается до этих самых инициализируемых байтами ключа трех переменных (назовем их условно state67, state68 и state69), остальные инициализированные переменные — просто константы, не меняющиеся во время шагов алгоритма:
for (int i = 0; i < data.Length; ++i) { UInt32 v4 = 8; UInt32 v5 = state67; byte v13 = (byte)(state68 & 1); UInt32 v15 = data[i]; UInt32 v16 = 0; byte v14 = (byte)(state69 & 1); UInt32 v6; UInt32 v7; byte v8; UInt32 v9; UInt32 v10; UInt32 v11; do { if ((v5 & 1) != 0) { v6 = state68; v5 = 0x80000000 | 0x40000031 ^ v5; if ((v6 & 1) != 0) { v13 = 1; v7 = 0xc0000000 | v6 ^ 0x20000010; v8 = v14; } else { v13 = 0; v8 = v14; v7 = 0x3fffffff & (state68 >> 1); } state68 = v7; } else { v9 = state69; v5 = 0x7fffffff & (v5 >> 1); if ((v9 & 1) != 0) { v10 = v9 ^ 0x8000001; v8 = 1; state69 = 0xf0000000 | v10; } else { state69 = 0x0fffffff & (v9 >> 1); v8 = 0; } v14 = v8; } v11 = (UInt32 )(v8 ^ v13 | ( v16<<1)); v16 = v11; --v4; } while (v4!=0); state67 = v5; data[i] = (byte)(v15 ^ v11); }Алгоритм с виду достаточно простой, но Krypto Analyzer его не детектирует, и используемые им константы нигде не гуглятся. По модным ныне веяниям, натравив на код искусственный интеллект, получаем маловразумительные ссылки на отечественный ГОСТ Р (или даже «Кузнечик») и на первоначальную версию поточного алгоритма шифрования европейского мобильного GSM-трафика под названием A5/1. Последний вариант выглядит довольно убедительно для софта от компании, занимающейся мобильной связью. Окей, примем на веру.
Продолжение доступно только участникам
Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».
Присоединяйся к сообществу «Xakep.ru»!
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
