[Перевод] Реверс-инжиниринг ПО начала 2000-х

Предыстория

В этой серии статей я рассказываю о системе лицензирования ПО, использовавшейся в проприетарном программном приложении 2004 года. Это ПО также имеет пробный режим без регистрации, но с ограниченными функциями. Бесплатную лицензию можно было получить, зарегистрировавшись онлайн на сайте поставщика ПО. Примерно в 2009 году приложение перешло в статус abandonware и его перестали распространять. Хотя двоичный файл ПО был архивирован, пока не предпринимались попытки восстановления функциональности, которую можно было получить благодаря бесплатной лицензии.

Дизассемблируем двоичный файл

В одном из предыдущих постов о другом проекте реверс-инжиниринга я использовал в качестве дизассемблера IDA Free. Позже Агентство национальной безопасности США выпустило свой инструмент для реверс-инжиниринга Ghidra как ПО с open source. Его я и буду использовать в этом проекте.

По сравнению с IDA, Ghidra требует больше усилий для правильного дизассемблирования двоичного файла ПО. Например, рассмотрим, следующий дизассемблированный Ghidra код:

IDA автоматически идентифицирует функцию как 0x4f64dc, но Ghidra её не определяет. Как оказалось, именно эта функция и нужна будет в нашем анализе. Ghidra может выполнять более подробный анализ через AnalysisOne ShotAggressive Instruction Finder, но результат всё равно будет неполным.

Из метаданных двоичного файла ПО мы знаем, что сборка была создана в Delphi 7 (выпущенном в 2002 году). И Ghidra, и IDA испытывают трудности с двоичными файлами Delphi, что приводит к утере названий символов и утерянным меткам, относящимся к классам Delphi. Чтобы решить эту проблему, мы используем IDR — Interactive Delphi Reconstructor, извлекающий из двоичного файла соответствующие символы:

Теперь мы можем импортировать эти данные в Ghidra при помощи Dhrake, определяющего функцию по адресу 0x4f64dc как 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 вручную.

На показанном ниже скриншоте мы видим, что 0x4f65f2 — это адрес, где считывается DAT_007e8d44, что соответствует проверке if (DAT_007e8d44 != local_c):

Следовательно, при помощи winedbg/GDB мы можем поставить контрольную точку в 0x4f65f2, а уже потом запустим ПО:

$ 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

Далее мы вручную меняем значение по адресу 0x7e8d44 на 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 даёт 0x194a81b79, что в десятичном виде равно 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 мы исследуем этот другой механизм лицензирования.

Читайте так же: