Реверс-инжиниринг ПО начала 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 мы исследуем этот другой механизм лицензирования.


ссылка на оригинал статьи https://habr.com/ru/articles/575322/