![]() |
Обратная разработка программного обеспечения |
![]() |
Понятие обратной разработки
В широком смысле обратной разработкой (reverse engineering) называют детальный разбор и анализ какого-либо устройства, механизма или алгоритма с целью понимания принципов его работы. Обратная разработка применяется для решения следующих задач:
- Копирование существующей технологии (например, производство какой-либо детали);
- Внесение изменений в технологию;
- Контроль недокументированных возможностей, закладок и уязвимостей.
В области информационной безопасности, помимо перечисленных выше задач, методы обратной разработки применяются для анализа вредоносного программного обеспечения. Информация, полученная в результате обратной разработки вирусов и троянов используется антивирусными лабораториями для создания сигнатур обнаружения вредоносного ПО и выработки алгоритмов лечения вирусов. Хакеры используют обратную разработку для “взлома” программ, т.е. изменяют функциональность программы для достижения своих целей. Такой целью, чаще всего, является отвязка проприетарного ПО от лицензионного ключа. Примером более сложной задачи может быть внедрение вредоносного кода в программу без нарушения ее основной функциональности.
Подходы к обратной разработке программ
Обратная разработка даже небольшой программы является кропотливой и трудоемкой работой. На сложность решаемой задачи влияют множество факторов, таких как:
- Язык программирования, на котором написана исходная программа.
- Компилятор, использованный для сборки программы, и его настройки.
- Способ компоновки программы.
- Архитектура и целевая операционная система.
- Использование средств запутывания кода (обфускаторов).
Существует большое число подходов к обратной разработке ПО с использованием различных инструментов. Их выбор обуславливается решаемой задачей и описанными выше факторами. В этой работе рассмотрим задачу “взлома” простейшего консольного приложения без привлечения дополнительного инструментария.

Программа Secret.exe (рис. 1) после запуска выводит приглашение ввести секретную фразу. После ввода пользователя, в зависимости от её корректности, программа выдает соответствующее сообщение. Попробуем определить секретный пароль, который ожидает программа.
Поиск релевантного участка кода
Одно из самых очевидных решений — начать пошагово выполнять программу в отладчике до тех пор, пока выполнение не дойдет до проверки пароля. Однако, даже небольшие программы на языках высокого уровня зачастую компилируются в огромные последовательности инструкций ассемблера. Выполнение их пошагово займет не один час монотонной работы, требующей, при этом, пристального внимания к происходящему на экране. Вместо этого, мы можем сначала найти интересующий нас участок кода, и лишь затем переходить к пошаговому выполнению программы.
Не существует четких алгоритмов, позволяющих мгновенно отыскивать релевантные процедуры и инструкции в дизассемблерном листинге. Эта задача требует “творческого” решения. Попытаемся представить как выглядел исходный код программы Secret.exe. Можно предположить, что чтение пользовательского ввода с клавиатуры и последующая проверка находились рядом, т.е. код имел вид:
(…, input, …);
ReadConsoleif(проверка пароля в переменной input)
(…, “Correct secret!”, …);
WriteConsoleelse
(…, “Wrong secret!”, …); WriteConsole
Если наше предположение верно, то найдя вызов WriteConsole
или ReadConsole
в дизассемблерном листинге, мы найдем и релевантный участок кода. Для этого
загрузим программу в отладчик и установим точку останова на функцию
ReadConsole
. В отладчике x64dbg это можно сделать, перейдя на вкладку
“Symbols”. В левой части открывшегося окна перечислены исполняемые модули
отлаживаемой программы - файл приложения secret.exe и файлы используемых им
библиотек.

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

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

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

Данный подход имеет существенный недостаток - если в программе искомая функция вызывается во многих местах (что не редкость для реального современного ПО), то поиск релевантного участка усложняется. Тем не менее, необходимость в пошаговом выполнении всей программы исчезает.
Альтернативным способом решения данной задачи является поиск строковых констант, а затем мест, в которых они используются. Т.е. если в первом случае мы ищем все места где вызывается, например, функция печати на экран, то в последнем случае мы ищем все функции, которые используют данную строку как аргумент. Для поиска строковых констант в отладчике x64dbg используется меню Search For → Current Module → String References. Как понятно из названия второго меню, эта функция ищет строки только в текущем модуле, поэтому необходимо следить за контекстом выполнения этой команды.

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

Согласно документации по функции ReadConsole
, её третий аргумент задает число
символов, которое необходимо считать (DWORD nNumberOfCharsToRead
).
Остановившись непосредственно на инструкции call
, можно увидеть, что значение
этого аргумента равно 7. Это дает нам подсказку о длине правильной парольной
фразы (или, чаще всего, о максимальной её длине).
Выполним инструкцию call ReadConsole
, нажав кнопку Step Over. Обратите
внимание, что отладчик не перешел на следующую инструкцию мгновенно, а
переключился в состояние “Running”. Это произошло потому что функция
ReadConsole
не возвращает управление в программу до тех пор, пока пользователь
не нажмет клавишу ↵ Enter. Введем в качестве пароля произвольную строку,
например “abcdefg”. Сразу после нажатия ↵ Enter, отладчик переключится на
следующую за call
инструкцию и остановит выполнение программы.
Продолжим пошаговое выполнение программы, пока не дойдем до инструкции jne
.
В левой части окна CPU загорится красная стрелка, ведущая вниз, что означает
что прыжок будет совершен. Прокрутим листинг до назначения прыжка и рассмотрим
код в этом месте (рис. 8):

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

Очевидно, что рассматриваемая инструкция сравнивает первую букву введенной
парольной фразы с правильным значением. Если ввести ‘]’ в качестве первого
символа, инструкция cmp
установит флаг ZF, что приведет к пропуску прыжка
и проверке следующего символа. Значит, для получения искомой парольной фразы
достаточно записать константные операнды инструкций cmp
в порядке их
следования. Сделав это, получим ответ — строку “][4CkME”.
Библиотечные вызовы, представляющие интерес в процессе обратной разработки
Разбирая программу Secret.exe, мы установили точку останова на функцию
ReadConsole
. Это не единственная функция, на которую стоит обращать внимание
при обратной разработке ПО. В список “интересных” функций входят следующие
функции WinAPI и стандартной библиотеки C:
ReadFile
иWriteFile
. Эти функции WinAPI могут использоваться не только для чтения/записи файлов на жестком диске, но и для ввода/вывода на консоль, подобноReadConsole
иWriteConsole
.CreateFile
. Используется как для создания, так и для открытия файлов. Среди своих аргументов принимает строку, задающую имя файла для открытия. Останавливаясь на этой функции можно определить список файлов, к которым получает доступ отлаживаемая программа.send
/WSASend
иrecv
/WSARecv
. Используются для отправки и получения данных по сети. Помимо этих функций существуют другие их вариации.strcmp
. Функция библиотеки С, сравнивающая строки между собой.puts
иprintf
. Функции для консольного вывода библиотеки С.gets
иscanf
. Функции для консольного ввода библиотеки С.
Контрольные вопросы
- Что такое обратная разработка ПО?
- Как произвести поиск импортируемых процедур, используемых в программе?
- Как произвести поиск константных строк, используемых в программе?
Задание на лабораторную работу
Применяя методы обратной разработки, найти парольные фразы в приложении, соответствующем своему варианту: //data/labs/asm/lab5
