По сути, в этой статье мы разберем необычный случай реализации защиты, основанной не на банальном сравнении серийного номера, а на расшифровке данных с помощью регистрационного ключа.
Любой опытный хакер знает: хорошая защита приложения не должна ломаться однобайтовым патчем. Неважно, насколько код был обфусцирован или виртуализован, если защита программы сводится к проверке какого‑то определенного условия (или даже нескольких условий), то найти и запатчить их — дело времени, квалификации хакера и инструментов, имеющихся в наличии.
Самое лучшее, что можно сделать в данном случае, — максимально усложнить жизнь потенциальному реверсеру: как можно сильнее запутать код, навесив на него кучу проверок целостности и прочих неприятных свистелок, чтобы возня с их поиском и обходом стала по затратам труда и времени (которые у хакеров, как известно, недешевы) дороже, чем профит от снятия защиты. Чаще всего так и делают: это классическая война щита и меча, однако этот меч обоюдоострый. Вопреки расхожей фразе «ломать — не строить», хакер, теоретически, прикладывает для реверса гораздо больше усилий, чем разработчик для защиты приложения.
Тем не менее, бывают варианты, когда защиту взломать невозможно в принципе. Как ты уже догадался, это случай, когда у хакера не хватает какой‑то ключевой информации, жизненно важной для полноценной работы программы. К примеру (кстати, самый надежный вариант), некоего модуля, или просто критического куска кода, или данных, подгружаемых с удаленного сервера или, на худой конец, модуля, расшифровываемого при помощи ключа, отсутствующего у хакера.
Разумеется, принципиальная неломаемость подобной схемы — это идеальный случай, когда все продуманно по максимуму, то есть, любая информация о недостающем фрагменте пазла у хакера отсутствует, удаленный сервер сам прекрасно защищен, алгоритм шифрования несимметричный, и так далее. Но наш мир не идеален, а людям свойственно ошибаться, поэтому у каждой защиты есть слабые места. Сегодня мы рассмотрим именно такой случай.
Итак, у нас есть некое винтажное приложение, демонстрирующее в процессе работы графические данные, набор которых без регистрации сильно ограничен.
Регистрация выполняется при помощи ввода регистрационного кода, причем этот код ничем не ограничен: неизвестна ни его длина, ни набор допустимых символов, никакой валидации не производится, набирать можно все что угодно. Правильность введенного кода можно проверить, только перезапустив программу.
Если регистрация не прошла, выводится предупреждение об этом, ну и, разумеется, большинство графических данных остаются недоступными. Единственным оптимистичным моментом можно считать то, что код един для всех пользователей и никак не привязан к компьютеру.
Перво‑наперво определим характер нашего приложения, скормив его DetectItEasy — приложение написано на стареньком Делфи, без всякой защиты:
PE32
Operation system: Windows (XP) [I386, 32-bit, GUI]
Linker: Turbo linker (2.25)
Compiler: Borland Object Pascal(Delphi) (10.0)
Language: Object Pascal(Delphi)
Library: Visual Component Library
Tool: Borland Delphi (3)
(Heur) Protector: Generic [High entropy]
В качестве инструмента для исследования программы выбираем, как обычно, Interactive Delphi Reconstructor (IDR). Разложив в нем EXE-модуль, сразу же находим две проверки валидности регистрации, а также сообщение о необходимости регистрации при старте приложения.

А еще предупреждение о незарегистрированной версии при нажатии мышью на недоступную картинку.

Казалось бы, вот он, тот самый случай «однобайтового патча», ан нет, при патче выделенных условных переходов (или даже проверяемых перед ними переменных) отключаются только предупреждающие сообщения, нужные функции и отсутствующие графические данные по‑прежнему недоступны. Поэтому продолжаем копать дальше.
Проанализировав код с предыдущих скриншотов, обнаруживаем, что признак отсутствия регистрации — значение по умолчанию регистрационного ключа «0123456789», которое назначается при отсутствии данного ключа в реестре. Попутно находим простенькую валидацию введенного ключа для его записи в реестр при закрытии регистрационной формы.

Как видишь, она представляет собой примитивную проверку на длину ключа, которая должна составлять не менее 3 символов. Ты, уже, вероятно, догадался, что программа не регистрируется любым ключом длиннее 3 символов. Более того, такой ключ хоть и попадает в реестр, но, по какой‑то причине не загружается оттуда в программу при выполнении метода загрузки Form1 с первого скриншота — на выходе все равно будет регкод по умолчанию 0123456789.
Попробуем разобраться, отчего так происходит, внимательнее проанализировав код этого метода. Сразу после загрузки кода из реестра с его первыми тремя символами выполняется такая странная операция.

Теперь понятно, почему перед сохранением кода в реестр его проверяли на трехбуквенную длину, в переводе на человеческий псевдокод это выглядит так:
dword_45291C = regcode[0]+regcode[2]+regcode[0]*regcode[1]
Ищем, где в дальнейшем используется переменная dword_45291C, и тут начинается самая интересная часть нашего квеста. Оказывается, это значение инициализирует зерно ( дельфовского генератора псевдослучайных чисел System.. Это линейный конгруэнтный генератор (LCG, подробно принцип такого генератора описан в Википедии). Он представляет собой внутреннюю реализацию дельфовской функции Random. При каждом вызове она берет текущее значение глобальной переменной System., умножает его на константу 134775813 и прибавляет 1.
Но самая мякотка заключается в том, для чего, собственно, используется этот псевдослучайный генератор. А используется он внутри кастомного загрузчика растровых изображений приложения Unit1..
Оказывается, все‑все‑все свои растровые данные приложение хранит в одной базе, в которой картинки, скрытые от глаз незарегистрированного пользователя, зашифрованы регистрационным кодом. А в качестве проверки корректной регистрации используется зашифрованная тестовая картинка — при ее корректной расшифровке программа считается зарегистрированной, а в процессе обработки исключения, возникающего при сбое загрузки некорректно расшифрованного JPEG, регистрационный код сбрасывается в дефолтное значение.
Поскольку нам неизвестен не просто сам код, но и набор возможных символов, из которого он состоит, и даже его длина, то ситуация складывается довольно печальная. Если разработчик все сделал по уму, то на этом этапе задачу пора бы уже и бросать, как бесперспективную. Однако, как ты уже догадался, решение у задачи все‑таки есть. Хватит посыпать голову пеплом, давай вместо этого внимательно посмотрим на алгоритм расшифровки:
do { ReadBuffer((int)v3, (int)v23, 1); LOBYTE(v23[0]) ^= RegCode[v19]; LOBYTE(v23[0]) ^= 9u; LOBYTE(v23[0]) ^= RandInt(0xFFu); WriteBuffer(*(int *)((char *)v23 + 1), (int)v23, 1); if ( (unsigned __int8)RegCodeLength == v19 ) v19 = 0; ++v19; --v11; } while ( v11 );
Продолжение доступно только участникам
Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».
Присоединяйся к сообществу «Xakep.ru»!
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
