[Перевод] Реверс-инжиниринг ПО начала 2000-х
Предыстория
В этой серии статей я рассказываю о системе лицензирования ПО, использовавшейся в проприетарном программном приложении 2004 года. Это ПО также имеет пробный режим без регистрации, но с ограниченными функциями. Бесплатную лицензию можно было получить, зарегистрировавшись онлайн на сайте поставщика ПО. Примерно в 2009 году приложение перешло в статус abandonware и его перестали распространять. Хотя двоичный файл ПО был архивирован, пока не предпринимались попытки восстановления функциональности, которую можно было получить благодаря бесплатной лицензии.
Дизассемблируем двоичный файл
В одном из предыдущих постов о другом проекте реверс-инжиниринга я использовал в качестве дизассемблера IDA Free. Позже Агентство национальной безопасности США выпустило свой инструмент для реверс-инжиниринга Ghidra как ПО с open source. Его я и буду использовать в этом проекте.
По сравнению с IDA, Ghidra требует больше усилий для правильного дизассемблирования двоичного файла ПО. Например, рассмотрим, следующий дизассемблированный Ghidra код:
IDA автоматически идентифицирует функцию как 0×4f64dc, но Ghidra её не определяет. Как оказалось, именно эта функция и нужна будет в нашем анализе. Ghidra может выполнять более подробный анализ через Analysis → One Shot → Aggressive Instruction Finder, но результат всё равно будет неполным.
Из метаданных двоичного файла ПО мы знаем, что сборка была создана в Delphi 7 (выпущенном в 2002 году). И Ghidra, и IDA испытывают трудности с двоичными файлами Delphi, что приводит к утере названий символов и утерянным меткам, относящимся к классам Delphi. Чтобы решить эту проблему, мы используем IDR — Interactive Delphi Reconstructor, извлекающий из двоичного файла соответствующие символы:
Теперь мы можем импортировать эти данные в Ghidra при помощи Dhrake, определяющего функцию по адресу 0×4f64dc как TMainForm.Register1Click, а также определяющего другие функции, например, InputBox и @LStrLen, что сильно поможет нам в анализе дизассемблированного кода:
Обходим проверку кода регистрации
Мы заметили, что TMainForm.Register1Click содержит ссылки на "Enter Registration Code"
и другие строки, которые походят на диалоговое окно, открывающееся, когда пользователь запускает функцию регистрации ПО. [Начав с нуля, мы можем обнаружить эту функцию в Ghidra, поискав внутри двоичного файла соответствующие строки.] Значит, это может быть целью нашего анализа, поэтому мы более подробно изучим декомпилированные выходные данные Ghidra (в IDA Free эта функция недоступна). Соответствующий код выглядит так:
void TMainForm.Register1Click(undefined4 param_1) {
// ...
local_c = DAT_007e8d44;
// ...
if (...) {
// ...
if (...) {
DAT_007e8d44 = 1;
}
// ...
if (...) {
DAT_007e8d44 = 2;
}
// ...
}
if (DAT_007e8d44 != local_c) {
pcStack52 = "Registration code accepted";
puStack56 = (undefined *)0x0;
uStack60 = local_8;
FUN_004f5694();
// ...
}
// ...
}
Мы заметили, что в представленном выше декомпилированном коде значение DAT_007e8d44 хранится в local_c. При определённых условиях DAT_007e8d44 позже принимает значение 1
или 2
, а затем сравнивается с исходным значением DAT_007e8d44. Если значения различаются, то возникает ссылка на многообещающую строку "Registration code accepted"
. Предположительно, по умолчанию DAT_007e8d44 имеет значение 0
.
Следовательно, мы можем допустить, что DAT_007e8d44 содержит флаг, обозначающий наличие регистрации у ПО. Если во время выполнения регистрации введённый код верен, то DAT_007e8d44 принимает значение 1
или 2
, что приводит к выбору ветви "Registration code accepted"
. Для проверки нашей гипотезы я запустил отладчик.
В этом проекте я запускаю ПО в Linux при помощи Wine, который имеет собственный отладчик winedbg, имеющий интеграцию с GDB. С середины 2021 года у Ghidra есть поддержка отладчиков, но под GDB она не очень хорошо совместима с winedbg и Wine, поэтому мы будем использовать winedbg/GDB вручную.
На показанном ниже скриншоте мы видим, что 0×4f65f2 — это адрес, где считывается DAT_007e8d44, что соответствует проверке if (DAT_007e8d44 != local_c)
:
Следовательно, при помощи winedbg/GDB мы можем поставить контрольную точку в 0×4f65f2, а уже потом запустим ПО:
$ winedbg --gdb foobar.exe
Wine-gdb> b *0x4f65f2
Breakpoint 1 at 0x4f65f2
Wine-gdb> c
Continuing.
Затем мы заходим в форму регистрации, вводим произвольный код и ждём срабатывания контрольной точки. Когда это происходит, мы убеждаемся, что по умолчанию DAT_007e8d44, как мы и подозревали, равно 0
:
Breakpoint 1, 0x004f65f2 in ?? ()
Wine-gdb> x/wx 0x7e8d44
0x7e8d44: 0x00000000
Далее мы вручную меняем значение по адресу 0×7e8d44 на 1
и продолжаем исполнение:
Wine-gdb> set *0x7e8d44=1
Wine-gdb> c
Continuing.
Нас приветствует сообщение, которое, вероятно, никто не видел больше десятка лет:
(При запуске программы отображается ограничение »60». После выполнения регистрации отображается информация о регистрации, а ограничение увеличивается до »2500».)
Реверс-инжиниринг проверки кода регистрации
Хоть мы и разблокировали функции ПО, было бы здорово, если бы это можно делать без ручных манипуляций с памятью. Вернувшись к декомпиляции TMainForm.Register1Click, мы можем немного расширить диапазон:
void TMainForm.Register1Click(undefined4 param_1) {
// ...
InputBox("Register FooBar","Enter Registration Code",0);
local_c = DAT_007e8d44;
// ...
iVar2 = @LStrLen(local_10);
if (iVar2 == 10) {
// ...
@LStrCopy(local_10,1,5,&local_14);
a2 = local_14;
@LStrCopy(local_10,6,5,&local_18);
@LStrCat3(&local_10,local_18,a2);
DAT_007e8d64 = StrToInt64(local_10);
DAT_007e8d68 = extraout_EDX;
iVar2 = @_llmod(DAT_007e8d64,extraout_EDX);
if ((extraout_EDX_00 == 0) && (iVar2 == 1)) {
DAT_007e8d44 = 1;
}
iVar2 = @_llmod(DAT_007e8d64,DAT_007e8d68);
if ((extraout_EDX_01 == 0) && (iVar2 == 0x15)) {
DAT_007e8d44 = 2;
}
// ...
}
if (DAT_007e8d44 != local_c) {
pcStack52 = "Registration code accepted";
// ...
}
// ...
}
Мы видим, что функция InputBox вызывается с заголовком и приглашением окна ввода кода регистрации. Предположительно ввод пользователя сохраняется в local_10, после чего мы вызываем @LStrLen и начинаем дальнейшие манипуляции с кодом регистрации, только если его длина равна 10. Значит, правильные коды регистрации должны состоять из 10 символов.
Затем идут вызовы @LStrCopy(local_10,1,5,&local_14);
и @LStrCopy(local_10,6,5,&local_18);
. @LStrCopy — это внутренняя функция Delphi и она не очень хорошо документирована, но мы можем предположить, что она используется как функция подстроки, копируя первые 5 символов кода регистрации в local_14, а последние 5 символов — в local_18.
Далее идёт вызов @LStrCat3(&local_10,local_18,local_14);
. Это тоже незадокументированная внутренняя функция Delphi, однако Google приводит нас на какой-то пост на китайском форуме, дающий нам понять, что она выполняет local_10 := local_18 + local_14
. То есть в целом всё это меняет местами первые и последние 5 символов кода регистрации.
Затем перевёрнутый код регистрации передаётся StrToInt64, который, как понятно из названия, парсит строку в 64-битный integer. Но этот двоичный файл создан в 2004 году и является 32-битным приложением, так как же здесь представлено 64-битное целое число? В документации Delphi говорится, что оно хранится в формате edx: eax.
Мы можем проверить это при помощи отладчика. С помощью winedbg/GDB мы устанавливаем контрольную точку сразу после вызова StrToInt64 и изучаем значения eax и edx, введя в качестве кода регистрации 1234567890:
$ winedbg --gdb foobar.exe
Wine-gdb> b *0x4f6586
Breakpoint 1 at 0x4f6586
Wine-gdb> c
Continuing.
Breakpoint 1, 0x004f6586 in ?? ()
Wine-gdb> info reg
eax 0x94a81b79 -1800922247
ecx 0x0 0
edx 0x1 1
ebx 0x1026e58 16936536
[...]
Обратите внимание, что конкатенация edx с eax даёт 0×194a81b79, что в десятичном виде равно 6789012345, как и ожидалось.
Вернувшись к TMainForm.Register1Click, мы заметим, что этот результат сохраняется в DAT_007e8d64 (младшие 32 бита) и в DAT_007e8d68 (старшие 32 бита). Однако декомпиляция следующей части функции некорректна, поэтому нам приходится вернуться к сырому дизассемблированному коду, который выглядит так:
; ...
004f6586 6a 00 PUSH 0x0
004f6588 68 c3 b2 PUSH 0xa1b2c3
a1 00
004f658d 8b 05 64 MOV EAX,dword ptr [DAT_007e8d64]
8d 7e 00
004f6593 8b 15 68 MOV EDX,dword ptr [DAT_007e8d68]
8d 7e 00
004f6599 e8 1a f2 CALL @_llmod
f0 ff
004f659e 83 fa 00 CMP EDX,0x0
004f65a1 75 0f JNZ LAB_004f65b2
004f65a3 83 f8 01 CMP EAX,0x1
004f65a6 75 0a JNZ LAB_004f65b2
004f65a8 c7 05 44 MOV dword ptr [DAT_007e8d44],0x1
8d 7e 00
01 00 00 00
LAB_004f65b2
; ...
Похоже, мы вызываем @_llmod, передаём как один параметр распарсенный код регистрации, а как второй параметр (в стеке) передаём Int64 0xa1b2c3. [В этой серии статей магические числа для демонстрации были заменены произвольными значениями.] Предположительно, это операция деления с остатком.
Затем мы проверяем равенство edx = 0
и eax = 1
, и если оба условия истинны, значение 1
записывается в DAT_007e8d44 (что, как мы определили ранее, означает правильность кода регистрации).
Значит, можно предположить, что edx: eax является результатом деления с остатком в Int64, и код считается правильным, если значение при делении кода регистрации (две поменянные местами половины) на 0xa1b2c3 с остатком равно 1. Тривиальным кодом регистрации, удовлетворяющим этому требованию, будет 0000100000
. [После перемены местами двух частей это будет просто целое число 1, которое при делении на 0xa1b2c3 естественно имеет остаток 1.]
Мы вводим этот код в ПО и убеждаемся, что оно его принимает.
Дальнейшие шаги
В этой части мы выполнили реверс-инжиниринг механизма проверки кода регистрации и создали правильный код регистрации, который можно использовать для разблокировки полной функциональности ПО.
Однако на самом деле в 2004 году использовался не этот код лицензирования. В части 2 мы исследуем этот другой механизм лицензирования.