Обратная разработка программного обеспечения

Понятие обратной разработки

В широком смысле обратной разработкой (reverse engineering) называют детальный разбор и анализ какого-либо устройства, механизма или алгоритма с целью понимания принципов его работы. Обратная разработка применяется для решения следующих задач:

В области информационной безопасности, помимо перечисленных выше задач, методы обратной разработки применяются для анализа вредоносного программного обеспечения. Информация, полученная в результате обратной разработки вирусов и троянов используется антивирусными лабораториями для создания сигнатур обнаружения вредоносного ПО и выработки алгоритмов лечения вирусов. Хакеры используют обратную разработку для “взлома” программ, т.е. изменяют функциональность программы для достижения своих целей. Такой целью, чаще всего, является отвязка проприетарного ПО от лицензионного ключа. Примером более сложной задачи может быть внедрение вредоносного кода в программу без нарушения ее основной функциональности.

Подходы к обратной разработке программ

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

Существует большое число подходов к обратной разработке ПО с использованием различных инструментов. Их выбор обуславливается решаемой задачей и описанными выше факторами. В этой работе рассмотрим задачу “взлома” простейшего консольного приложения без привлечения дополнительного инструментария.

Рисунок 1. Окно программы Secret.exe.

Программа Secret.exe (рис. 1) после запуска выводит приглашение ввести секретную фразу. После ввода пользователя, в зависимости от её корректности, программа выдает соответствующее сообщение. Попробуем определить секретный пароль, который ожидает программа.

Поиск релевантного участка кода

Одно из самых очевидных решений — начать пошагово выполнять программу в отладчике до тех пор, пока выполнение не дойдет до проверки пароля. Однако, даже небольшие программы на языках высокого уровня зачастую компилируются в огромные последовательности инструкций ассемблера. Выполнение их пошагово займет не один час монотонной работы, требующей, при этом, пристального внимания к происходящему на экране. Вместо этого, мы можем сначала найти интересующий нас участок кода, и лишь затем переходить к пошаговому выполнению программы.

Не существует четких алгоритмов, позволяющих мгновенно отыскивать релевантные процедуры и инструкции в дизассемблерном листинге. Эта задача требует “творческого” решения. Попытаемся представить как выглядел исходный код программы Secret.exe. Можно предположить, что чтение пользовательского ввода с клавиатуры и последующая проверка находились рядом, т.е. код имел вид:

ReadConsole(, input,);
if(проверка пароля в переменной input)
    WriteConsole(, “Correct secret!,);
else
    WriteConsole(, “Wrong secret!,);

Если наше предположение верно, то найдя вызов WriteConsole или ReadConsole в дизассемблерном листинге, мы найдем и релевантный участок кода. Для этого загрузим программу в отладчик и установим точку останова на функцию ReadConsole. В отладчике x64dbg это можно сделать, перейдя на вкладку “Symbols”. В левой части открывшегося окна перечислены исполняемые модули отлаживаемой программы - файл приложения secret.exe и файлы используемых им библиотек.

Рисунок 2. Левая часть окна “Symbols”.

После нажатия по строчке secret.exe, в правой части окна отобразится список импортированных и экспортированных процедур в выбранном модуле. Обратите внимание, что в исполняемом файле присутствует только один экспортируемый символ - точка входа.

Рисунок 3. Список процедур в модуле secret.exe.

Щелкнем правой кнопкой по строчке ReadConsoleA, выберем Toggle Breakpoint, чтобы установить точку останова, а затем продолжим выполнение программы. Когда точка останова активируется, окно дизассемблера отобразит место в коде, на котором произошла остановка.

Это место, однако, будет не искомым участком, а лишь функцией-заглушкой (thunk). Такие заглушки генерируются компилятором, и при запуске программы заполняются реальными адресами соответствующих функций.

Рисунок 4. Активированная точка останова на вызове ReadConsoleA.

Чтобы найти место, из которого функция-заглушка была вызвана, обратимся к окну стека. На вершине стека находится адрес возврата, который был помещен на стек инструкцией call, вызвавшей заглушку. Щелкнем по этой строчке правой кнопкой мыши и выберем Follow DWORD in Disassembler. Окно дизассемблера переключится на участок кода, который мы искали. Осмотрев окрестности вызова ReadConsoleA можно увидеть вызовы других интересующих нас функций — GetStdHandle и WriteConsoleA, что говорит о правильности нашего исходного предположения. Мы успешно обнаружили релевантный участок кода, который непосредственно производит чтение пользовательского ввода с консоли и последующую его обработку.

Рисунок 5. Найденный вызов ReadConsoleA.

Данный подход имеет существенный недостаток - если в программе искомая функция вызывается во многих местах (что не редкость для реального современного ПО), то поиск релевантного участка усложняется. Тем не менее, необходимость в пошаговом выполнении всей программы исчезает.

Альтернативным способом решения данной задачи является поиск строковых констант, а затем мест, в которых они используются. Т.е. если в первом случае мы ищем все места где вызывается, например, функция печати на экран, то в последнем случае мы ищем все функции, которые используют данную строку как аргумент. Для поиска строковых констант в отладчике x64dbg используется меню Search ForCurrent ModuleString References. Как понятно из названия второго меню, эта функция ищет строки только в текущем модуле, поэтому необходимо следить за контекстом выполнения этой команды.

Рисунок 6. Результаты поиска строковых констант в secret.exe.

Идея, стоящая за данным подходом заключается в том, что строка вида “введен неправильный пароль” вряд ли будет передаваться куда-то кроме функции печати, что позволит быстрее найти интересующий нас код. Более того, сам пароль является строковой константой, и может быть найден сразу при просмотре, если эта константа использовалась в исходном коде приложения. Само собой, это тривиальный случай, практически не встречающийся в реальных приложениях.

Обратная разработка алгоритма сравнения

Рассмотрев окрестности найденного вызова можно выделить весь код проверки (рис. 7).

Рисунок 7. Код проверки парольной фразы.

Согласно документации по функции ReadConsole, её третий аргумент задает число символов, которое необходимо считать (DWORD nNumberOfCharsToRead). Остановившись непосредственно на инструкции call, можно увидеть, что значение этого аргумента равно 7. Это дает нам подсказку о длине правильной парольной фразы (или, чаще всего, о максимальной её длине).

Выполним инструкцию call ReadConsole, нажав кнопку Step Over. Обратите внимание, что отладчик не перешел на следующую инструкцию мгновенно, а переключился в состояние “Running”. Это произошло потому что функция ReadConsole не возвращает управление в программу до тех пор, пока пользователь не нажмет клавишу ↵ Enter. Введем в качестве пароля произвольную строку, например “abcdefg”. Сразу после нажатия ↵ Enter, отладчик переключится на следующую за call инструкцию и остановит выполнение программы.

Продолжим пошаговое выполнение программы, пока не дойдем до инструкции jne. В левой части окна CPU загорится красная стрелка, ведущая вниз, что означает что прыжок будет совершен. Прокрутим листинг до назначения прыжка и рассмотрим код в этом месте (рис. 8):

Рисунок 8. Листинг в области цели прыжка.

Можно увидеть здесь вызовы функций WriteConsole, Sleep и ExitProcess, а так же использование строковой константы “Wrong secret!”. Исходя из этого, можно заключить, что данный фрагмент сигнализирует о неправильной парольной фразе - при его выполнении программа печатает сообщение о неправильном пароле и завершает свою работу. Значит, необходимо вернуться назад к инструкции JNE и проанализировать код, выполнение которого привело к тому, что прыжок состоялся.

Перед инструкцией JNE находится инструкция cmp byte ptr [eax], 5D, выполняющая сравнение байта по адресу eax с константным значением 0x5D. Подсказка справа от инструкции говорит о том, что 0x5D соответствует символу ‘]’ в ASCII-кодировке. Найдем в окне дампа адрес, используемый в инструкции сравнения. Для этого в окне регистров щелкнем правой кнопкой по значению регистра EAX и выберем Follow in Dump. Окно дампа отобразит содержимое оперативной памяти, начиная с указанного адреса, в котором можно увидеть введенную нами строку (рис. 9):

Рисунок 9. Дамп памяти по адресу 0x401000.

Очевидно, что рассматриваемая инструкция сравнивает первую букву введенной парольной фразы с правильным значением. Если ввести ‘]’ в качестве первого символа, инструкция cmp установит флаг ZF, что приведет к пропуску прыжка и проверке следующего символа. Значит, для получения искомой парольной фразы достаточно записать константные операнды инструкций cmp в порядке их следования. Сделав это, получим ответ — строку “][4CkME”.

Библиотечные вызовы, представляющие интерес в процессе обратной разработки

Разбирая программу Secret.exe, мы установили точку останова на функцию ReadConsole. Это не единственная функция, на которую стоит обращать внимание при обратной разработке ПО. В список “интересных” функций входят следующие функции WinAPI и стандартной библиотеки C:

Контрольные вопросы

  1. Что такое обратная разработка ПО?
  2. Как произвести поиск импортируемых процедур, используемых в программе?
  3. Как произвести поиск константных строк, используемых в программе?

Задание на лабораторную работу

Применяя методы обратной разработки, найти парольные фразы в приложении, соответствующем своему варианту: //data/labs/asm/lab5