Выполнил студент группы СКБ161 Санкин Н.А.
Программа запрашивает у пользователя некоторые данные с максимальной длиной в 256 байт (данные вне этой границы обрезаются). Затем программа преобразовывает введённые данные, выводя результат на экран. Для решения задания от пользователя требуется по предварительно известному результату выполнения программы найти исходные данные, которые были использованы для получения данного результата. Для выполнения задачи требуется дизассемблировать алгоритм преобразвания ввода, защищённый методами антиотладки.
Данный метод позволяет изменять алгоритм программы во время выполнения, что затрудняет статический анализ (код в скомпилированном файле
отличается от кода, на самом деле исполняемым программой). По умолчанию сегмент с кодом программы имеет права доступа "read-only".
Смену прав доступа к региону памяти на "read-write" можно выполнить при помощи вызова функции VirtualProtect
из
состава WinAPI с указанием флага PAGE_EXECUTE_READWRITE
. Далее доступ к байтам внутри региона можно производить так же,
как и в обычном массиве на стеке.
В Windows имеется возможность определения пользовательских обработчиков исключений. Добавление нового обработчика можно
выполнить при помощи вызова функции AddVectoredExceptionHandler
. Данный метод основывается на том, что при выполнении
кода отладчиком все исключения в программе будут перехватываться и обрабатываться отладчиком, если явно не передавать их обработку
коду программы. Таким образом, отладчик заменит пользовательские обработчики исключений на собственные, и выполнение программы под отладчиком
будет отличаться от выполнения без отладчика.
Данный метод позволяет контролировать целостность программы. Во время выполнения данный метод может помешать отладке в случае, если точки останова установлены на рассматриваемом блоке кода до применения операции хеширования.
В С++, вызов конктрукторов статических объектов и инициализация статических переменных происходят до выполнения основного
кода программы (функции main()
). Это позволяет скрыть часть выполняемого кода, заставляя аналитика искать адреса функций,
выполняемых в startup коде программы.
- Спецификатор функции
__declspec(naked)
- компилятор сгенерирует код функции без пролога и эпилога. Может заставить дизассемблер решить, что функция является набором байт, а не исполняемым кодом. Вынуждает программиста вручную управлять стековым фреймом функции. IsDebuggerPresent
- данная функция проверяет наличие флагаBeingDebugged
в структуреPEB
(Process Environment Block) процесса, что позволяет обнаружить отладчики пользовательского уровня. Так как вызовIsDebuggerPresent
может перехватываться плагинами отладчиков или легко находиться в таблице импорта программы, можно заменить вызов этой функции на эквивалентный по функционалу код:и проверять равенство значения в регистреmov eax, fs:0x18 mov eax, [eax + 0x30] mov al, [eax + 2]
al
единице.- Использование
jmp
от значения регистра - может заставить отладчик подумать, чтоjmp
является концом функции. - Использование инициализации локальных массивов байт на стеке (инициализация вида
nies[]{}
) - помещает данные в код функции, а не в секции данных. Для строк позволяет избежать поиска по строкам в файле (но не поиска по байтам в коде). - "Мусорный" и обфусцированный код - мешает анализу кода аналитиком. В комбинации с другими методами может заставить дизассемблер воспринять часть кода в виде обычного набора байт
Зашифровка данных выполняется функцией generate_flag()
. Она запрашивает данные у пользователя, и затем преобразует их при
помощи побайтовой операции xor
. Исходная версия функции, выполняющая xor
со значением 0x94
, призвана запутать аналитика.
Описанная версия функции generate_flag()
будет выполнена только при отладке программы; за модификацию процесса выполнения без отладчика отвечают
методы специального класса Hidden
:
Hidden()
hidden_prepare_code()
hidden_prepare_data()
hidden_add_veh()
Данные методы будут выполнены до вызова main()
в приведённом выше порядке. Конструктор Hidden()
устанавливает права
read-write
на регион памяти внутри кода generate_flag()
, после чего метод hidden_prepare_code()
заменяет инструкции
mov bl, [eax]
xor bl, 0x94
на код
mov bl, [eax+1]
xor bl, [eax]
, а также заменяет инструкцию xor eax, fs:[0]
на xor eax, ds:[0]
, где последняя будет вызывать программное исключение.
Таким образом, мы добиваемся изменения видимого при статическом анализе алгоритма шифрования и вызова исключения, которое будет обработано
пользовательским обработчиком vectored_handler()
, регистрируемым внутри функции hidden_add_veh()
. Оставшаяся функция
hidden_prepare_data()
циклически заполняет массив flag
байтами гаммы и подготавливает адреса начала и конца хешируемого кода.
Эти адреса будут использованы внутри обработчика vectored_handler()
.
Итого, этап выполнения кода до функции main()
привносит следующие изменения в код генерации флага:
- В качестве данных массива
flag
берутся не исходные данные флага, а данные, над которыми поблочно выполнена операцияxor
с данными заранее определённой гаммы - Вместо выполнения над байтами массива
flag
операцииxor
со значением0x94
, выполняетсяxor
каждого байта изflag
со следующим за ним по порядку байтом - Вместо инструкции, не вызывающей программных исключений, подставляется всегда вызывающая исключение инструкция, внешне напоминающая модифицированную
Последний шаг приводит к возникновению исключения и передаче управления обработчику vectored_handler()
.
Код обработчика vectored_handler()
использует найденные функцией hidden_prepare_data()
адреса начала и конца хешируемого кода для
вычисления от него хеш суммы SHA256. Данная хеш-сумма используется как ключ для алгоритма rc4, который затем выполняет шифрование массива flag
.
Данные из получившегося массива затем возвращаются пользователю.
Для сборки требуется открыть файл проекта crack.sln
и собрать решение с помощью Visual Studio (v142) в конфигурации Debug-x86