Серия все более сложных программ, демонстрирующих перехват функций в 64-разрядной версии Windows.
Я написал эти программы, изучая, как работает перехват функций. Они могут оказаться полезными для других людей, пытающихся сделать то же самое (или для меня в будущем, когда я снова забуду, как все это работает). Их следует рассматривать по порядку. При первом использовании функции в программе она будет включена в файл .cpp для этого конкретного примера программы, а ее имя будет начинаться с подчеркивания. Во всех последующих примерах, использующих ту же функцию, будет использоваться копия этой функции, включенная в Hooking_common.h, чтобы свести к минимуму дублирование кода и сохранить достаточно малые программы последующих примеров, чтобы их было легко читать.
Я проделал небольшую работу, чтобы их очистить, но последующие примеры все еще немного беспорядочны. Важно отметить, что конечного результата в этом репозитории недостаточно для написания полнофункциональной библиотеки перехвата, но достаточно, чтобы начать идти по этому пути.
Во время выполнения некоторые проекты могут полагаться на то, что другие проекты собираются (для внедрения dll требуется, чтобы dll была собрана) или выполняются одновременно (для перехвата целевой программы необходимо, чтобы она уже была запущена).
Все примеры были созданы с использованием Visual Studio 2019 (v142) с Windows SDK 10.0.17763.0. Я не думаю, что здесь есть что-то, что зависит от версии VS или SDK, но на всякий случай я перечисляю это здесь. Почти наверняка есть некоторые вещи, специфичные для MSVC.
Наконец, последний пример батута устанавливает перехватчик в mspaint. Я предполагаю, что в какой-то момент в будущем обновление mspaint приведет к поломке этого примера. На момент написания текущей версии mspaint была 1909 (сборка ОС 18363.1016).
Примеры разделены на две категории: те, в которых используются батуты, и те, которые этого не делают. Примеры без батута существуют исключительно для демонстрации перенаправления потока программы от одной функции к другой в различных ситуациях. Создание батутов — сложная задача, и когда я пытался понять, как работает перехват функций, было чрезвычайно полезно начать с создания примеров, не связанных с батутами. Кроме того, есть 4 «целевые программы», которые используются в примерах, демонстрирующих, как устанавливать перехватчики в различные (уже запущенные) процессы.
В большинстве этих примеров происходит утечка памяти, связанная с перехватчиками. Меня это не особо волнует, потому что эти примеры предназначены просто для демонстрации концепции перехвата, а также потому, что эти «утечки» выделенных ресурсов в любом случае должны существовать до завершения программы.
Хотя стандартной терминологии для методов перехвата функций не так уж и много, в коде (и файлах чтения) в этом репозитории используются следующие термины:
Поскольку эти примеры не создают батуты при установке своих перехватчиков, я считаю, что эти функции демонстрируют «деструктивное» перехватывание, когда исходная функция становится совершенно непригодной для использования после перехвата.
Небольшой пример перезаписи начальных байтов функции с помощью инструкции перехода, которая перенаправляет поток выполнения программы на другую функцию в той же программе. Поскольку батут не создается, эта операция является разрушительной, и исходную функцию больше нельзя вызвать. Это единственный 32-битный пример в репозитории.
64-битная версия предыдущего примера. В 64-битных приложениях функции могут располагаться достаточно далеко в памяти, чтобы их нельзя было получить с помощью 32-битной инструкции относительного перехода. Поскольку 64-битной инструкции относительного перехода нет, эта программа сначала создает функцию «реле», которая содержит байты для абсолютной инструкции jmp, которая может достигать любого места в памяти (и переходит к функции полезной нагрузки). 32-битный переход, который устанавливается в целевой функции, переходит к этой функции реле, а не непосредственно к полезной нагрузке.
Предоставляет пример использования методов из предыдущего проекта для подключения функции-члена, а не свободной функции.
Эта программа, немного отличающаяся от предыдущих примеров, показывает, как установить перехватчик в виртуальную функцию-член, получив адрес этой функции через vtable объекта. Никакие другие примеры не связаны с виртуальными функциями, но я подумал, что это достаточно интересно, чтобы включить их сюда.
Простейший пример установки перехвата в другой запущенный процесс. В этом примере используется библиотека DbgHelp для поиска функции в целевых процессах (A — Цель со свободной функцией) по имени строки. Это возможно только потому, что целевая программа построена с включенными символами отладки. Несмотря на простоту, этот пример немного длиннее предыдущих программ из-за большого количества новых функций, которые он вводит (для поиска и управления удаленным процессом).
В этом примере показано, как перехватить функцию, которую другой процесс импортировал из dll. Существует некоторый нюанс в том, как получить адрес функции dll в удаленном процессе, связанный с работой ASLR, который продемонстрирован здесь. В остальном этот пример практически идентичен предыдущему.
В этом примере показано, как установить перехватчик в функцию, которая не импортируется dll и которой нет в таблице символов (вероятно, потому, что удаленный процесс не имеет символов отладки). Это означает, что не существует (простого) способа найти целевую функцию по имени строки. Вместо этого в этом примере предполагается, что вы использовали дизассемблер, например x64dbg, чтобы получить относительный виртуальный адрес (RVA) функции, которую вы хотите перехватить. Эта программа использует этот RVA для установки перехватчика.
Аналогично приведенному выше, за исключением того, что в этом примере используется внедрение dll для установки функции полезной нагрузки, а не запись байтов необработанного машинного кода. С этим гораздо проще работать, поскольку ваши полезные данные можно снова написать на C++. Полезная нагрузка для этого примера содержится в проекте 08B-DLL-Payload.
В следующих примерах при перехвате устанавливаются батуты, а это означает, что программа все еще может выполнять логику целевой функции после установки перехвата. Поскольку установка перехватчика перезаписывает как минимум первые 5 байтов целевой функции, инструкции, содержащиеся в этих 5 байтах, перемещаются в функцию батута. Таким образом, вызов функции батута эффективно выполняет исходную логику целевой функции.
Эквивалент примера № 2 для установки батута. Этот пример немного странный, поскольку я хотел продемонстрировать создание батута без использования механизма дизассемблирования. В этом случае целевая функция была создана с известной 5-байтовой инструкцией в начале, поэтому мы можем просто скопировать первые пять байтов этой функции в функцию батута. Это означает, что создать батут очень просто, поскольку мы знаем его точный размер и что он не использует никакой относительной адресации, которую необходимо исправлять. Если бы вы писали батут для действительно конкретного случая использования, вам, вероятно, удалось бы просто сделать его вариацию.
В этом примере показан сценарий, аналогичный предыдущему, за исключением того, что на этот раз я использую дизассемблер (capstone), чтобы получить байты, которые нам нужно украсть из целевой функции. Это позволяет использовать код перехвата для любой функции, а не только для тех, которые, как мы знаем, будут простыми. На самом деле в этом примере происходит очень много всего, потому что он переходит от целевого перехвата (как предыдущий) к созданию общей функции перехвата. Батуту приходится преобразовывать относительные вызовы/переходы в инструкции, использующие абсолютные адреса, что еще больше усложняет ситуацию. Это также не на 100% отточенный пример универсального перехвата, он потерпит неудачу с инструкциями цикла, а также если вы попытаетесь перехватить функции с менее чем 5 байтами инструкций.
По сути то же самое, что и выше, за исключением того, что этот пример включает код для приостановки всех выполняющихся потоков на время установки перехватчика. Не гарантируется потокобезопасность во всех случаях, но это определенно намного безопаснее, чем ничего не делать.
Это расширяет код перехвата/батута, использованный в двух предыдущих примерах, для поддержки перенаправления нескольких функций на одну и ту же полезную нагрузку и позволяет функциям полезной нагрузки вызывать другие функции с установленными в них перехватчиками.
Это первый пример батута, который устанавливает перехватчик в другой процесс (в данном случае целевое приложение B — Target with Free Functions From DLL). Вся логика перехвата содержится в полезной нагрузке dll 13B — Trampoline Imported Func DLL Payload. Здесь нет ничего нового, этот пример просто сочетает в себе уже реализованные методы перехвата батута с ранее показанными методами перехвата функции, импортированной из dll.
Жемчужина репо. В этом примере полезная нагрузка dll (14B — Trampoline Hook MSPaint Payload) внедряется в работающий экземпляр mspaint (перед запуском этого процесса вам необходимо запустить mspaint самостоятельно). Установленный крючок заставляет кисти рисовать красным, независимо от того, какой цвет вы на самом деле выбрали в MSPaint. Честно говоря, здесь нет ничего такого, что не было бы показано в предыдущем примере, просто здорово видеть, как это работает в непридуманной программе.
Простое целевое приложение, которое в цикле вызывает свободную функцию. Скомпилировано с включенной отладочной информацией.
Целевое приложение, которое в цикле вызывает свободную функцию, импортированную из библиотеки DLL (B2 — GetNum-DLL).
Целевое приложение, которое в цикле вызывает невиртуальную функцию-член.
Целевое приложение, которое в цикле вызывает виртуальную функцию-член.