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

Но это вовсе не значит, что в выполнимые файлы нельзя вносить изменения. Если мы корректно заменим один или несколько байтов, приложение не перестанет работать, но его логика может измениться. Например, как мы все знаем по крякам, что программу можно отучить от жадности, просто заменив одну инструкцию условного перехода на безусловный. Да, контрольная сумма изменится, но кто ее проверяет?

Но можно подселять в выполнимое приложение целые блоки кода, и выполнять этот код вместе/вместо с основной логикой. Если открыть любой выполнимый файл в шестнадцатеричном редакторе и прокрутить в конец, то последние несколько килобайт всегда будут заполнены нулевыми байтами. Это место компиляторы резервируют при создании выполнимых файлов, но фактически оно никак не используется. Эту часть выполнимых файлов принято называть Code Cave (пещера кода) потому, что именно там можно спрятать свой код, не повредив исходный файл. Размер Code Cave зависит от «щедрости» компилятора, но как правило не превышает нескольких килобайт.

Эта статья, написанная на основе материалов моей книги «Реверсивный инжиниринг приложений под Windows», (2024 г., ДМК Пресс), будет посвящена возможностям добавления своего кода в чужие выполнимые файлы.

Директория данных и таблица импорта

В рамках данной статьи мы не будем подробно разбирать PE формат выполнимых файлов, так как на эту тему есть довольно много публикаций на том же Хабре. Но некоторые необходимые для понимания моменты мы рассмотрим.

Диpектоpия данных — это массив стpуктуp IMAGE_DATA_DIRECTORY. Она содержит местонахождение и размеры структур данных PE‑файла. Каждый параметр содержит информацию о структуре данных.

Нас будет интересовать таблица импорта, которая фактически является массивом стpуктуp IMAGE_IMPORT_DESCRIPTOR. Каждая структура содержит информацию о DLL, откуда PE импоpтиpует функции. Конец массива отмечается элементом, содержащим одни нули.

Относительный адрес Таблицы импорта находится по адресу заголовок PE+0×80, а размер таблицы импорта указан в PE+0×84.

В качестве примера далее мы рассмотрим exe файл одной бесплатной утилиты. Посмотрим адрес таблицы и ее размер для данного выполнимого файла. На рисунке выделен заголовок PE, адрес таблицы и ее размер.

Далее мы перейдем к непосредственному внедрению кода в выполнимый файл. В данном примере ничего особо вредоносного делать не планируется, мы просто выведем на экран окно с текстом test.

Для выполнения приведенных далее действий на практике необходим PE анализатор с возможностью анализировать сегменты данных, находящиеся в файле. Например, можно использовать утилиту PE Bear. Для анализа просто откройте в анализаторе выполнимый файл. Нас будет интересовать раздел с указанием заголовков секций Section Hdrs.

Далее давайте рассмотрим по шагам наши действия по добавлению собственного кода в выполнимый файл. Для начала нам необходимо проанализировать наличие свободного места (0×00) в конце секций (text). Для этого смотрим, где эта секция заканчивается.

Секция text заканчивается на позиции 0×275a00.

Далее аналогичным способом выявляем наличие свободного места (0×00) в конце секций (rdata).

Далее нам необходимо узнать доступный объем свободного места для нашего кода. То есть насколько большой объем данных мы можем поместить в этот файл.

Для этого вычитаем из адреса следующей секции для сегмента text адрес первого байта 0×00 (code cave).

Получаем объем свободного места для нашего кода в сегменте text.

0×00277000-0×00275841=0×17bf (6079)

Около 6 килобайт в принципе должно хватить для небольшого пейлоада, например для реверсивного шелла.

Аналогично, вычитаем из адреса следующей секции адрес первого байта 0×00

Получаем объем свободного места для наших данных в сегменте rdata

302000-30081d=0×17e3 (6115)

Такой объем мы можем использовать для хранения данных.

Теперь нам необходимо запомнить адрес нашей code cave, потому что именно на него мы в дальнейшем будем совершать переход для выполнения своего кода.

Для последующих шагов нам потребуется отладчик. В нем мы будем править код самом выполнимом файле. Переходим на адрес entry point. Далее смотрим оригинальный код программы и ищем подходящую инструкцию JMP для того, чтобы поправить ее с целью перемещения выполнения кода в code cave.

Почему мы выбираем именно JMP? Эта инструкция позволяет безнаказанно перемещаться по программе, не накладывая никаких дополнительных обязательств. Если бы мы использовали CALL пришлось бы отслеживать состояние стека, что не очень удобно.

Желательно использовать ближайший к началу программы JMP.

По сути, нам нужно перепрыгнуть из начала легальной программы в наш Code Cave для того, чтобы выполнить свой код. Для правки JMP встаем в отладчике на нужную инструкцию JMP и выбираем Ассемблировать. В открывшемся окне правим адрес перехода JMP.

В результате получаем новое значение перехода для JMP.

Далее нам необходимо указать второй JMP в конце нашей пещеры кода, по которому мы будем возвращаться обратно в легальный код приложения. Для этого добавляем JMP в Code Cave указывающий на адрес, который был в изначальной инструкции JMP. В нашем случае это адрес 0×5e12a6.

Нулевые байты тоже являются опкодами команд и при использовании Code Cave они нам не нужны, поэтому заполняем пространство пещеры кода инструкциями NOP (0×90).

Теперь необходимо подготовить внедряемый код. Здесь есть один нюанс: мы не можем просто взять и вызвать какую‑либо API функцию, например MessageBox. Дело в том, что мы находимся в адресном пространстве исходной программы и нам необходимо знать какие адреса у API функций в контексте данной программы.

Поэтому, здесь же мы поступим проще. Мы напишем короткую программу без использования компилятора. Наша программа будет состоять из пяти строк.

Push 0
Push адрес_текста_с_заголовком_окна
Push адрес_текста_в_окне
Push 0
Call адрес_API_MessageBox

Нам необходимо найти используемый данной программой адрес MessageBox. Для этого по правой кнопке мыши выберем Поиск → Текущая область → Межмодульные вызовы.

Далее в поисковой строке указываем MessageBox и смотрим результат.

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

После этого нам необходимо в секции.rdata подготовить нужный набор символов. Для этого выбираем в отладчике Memory Map, далее.rdata. Затем выберем свободное (занятое нулевыми байтами) пространство пропишем в нем наши текстовые данные. Для простоты у меня и заголовок и сам текст выводят строку test.

Соответственно, если строка test находится по адресу 0×00701e21, то код, который нам необходимо добавить примет следующий вид

Push 0
Push 00701e21
Push 00701e21
Push 0
Call @MessageBox (будем указывать опкоды)

После этого, в code cave перед перенесенной инструкцией JMP помещаем свой код. Все, что идет перед CALL можно ввести с помощью опции отладчика Ассемблировать.

А вот опкоды придется вводить с помощью опции Двоичные операции → Редактировать. И далее в окне указываем нужные опкоды.

После нажатия ОК отладчик должен сам правильно распознать опкоды и вывести CALL MessageBox. Если вызов API функции распознался успешно, то это верный признак того, что код отработает корректно.

Далее не забудьте сохраниться перед проверкой корректности работы всего кода. Для проверки нам достаточно просто запустить программу. Сначала можно запустить под отладчиком.

В результате у нас при запуске программы должно появиться на экране следующее окно.

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

Заключение

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

Также важно помнить, что при подселении кода мы не можем использовать вызовы API функций из других приложений. Важно вычислить именно те адреса, которые используются данным приложением. В простейшем случае мы можем поискать вызовы нужных функций в целевой программе.


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

Если вы хотите системно освоить основы реверс‑инжиниринга и закрепить материал на практике, приглашаем вас на курс Reverse engineering. А чтобы узнать, подойдет ли вам программа курса, пройдите вступительный тест.

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

Комментарии (0)


  1. dyadyaSerezha
    17.09.2025 18:52

    Если вы хотите системно освоить основы реверс‑инжиниринга и закрепить материал на практике

    К сожалению, реверс-инжиниринг требует большой квалификации, терпения, сфокусированности, и оплата обычно мало соответствует этому всему.

    Также замечу, что часто используется модификация кода только что загруженного в ОЗУ, но ещё не выполняющегося приложения. Сам участвовал в работе над большим корпоративным продуктом типа SSO (single sign-on), который официально на лету "хакал" кучу приложений, чтобы где надо заполнять за пользователя юзернеймы и пароли.