Жесткое внедрение DLL в Windows-программы

На суд читателя представляется статья о внедрении собственного исполняемого кода в Windows программы.
В статье использованы различные технические термины, относящиеся к программированию под ОС Windows, в частности с применением Windows API. Тексты программ, приведенные в качестве примеров, тестировались в среде Borland C++ Builder 6.0. Работа приложения проверялась в среде Windows 2000, среда Windows 9x в силу морального устаревания не рассматривалась.
Введение в специальность
Внедрение своего кода в чужую программу может понадобится для множества задач, например, для отладки и анализа работы приложений, для установки различного рода программных защит, для шпионских» целей. На сегодняшний день существует достаточно много методов внедрить свой код в чужую программу. Достаточно подробно они описаны в статье автора Tanaka «Программы-невидимки», Однако все описанные методы являются внешними по отношению к целевой программе, т.е. для внедрения кода каждый раз необходим запуск постороннего приложения, так называемого «загрузчика», осуществляющего внедрение. Предлагаемый способ отличается тем, что осуществляет разовую, достаточно простую модификацию внутренних структур .exe файла, не модифицирует исполняемый код программы, не увеличивает размер файла, не требует внешнего загрузчика, практически незаметен для пользователя. Метод позволяет модифицировать большинство .exe файлов.
Как это работает
Принцип работы основан на следующем практически любая Windows-программа использует динамические библиотеки (DLL). В них могут храниться различные функции (в том числе системные — в USER32.DLL, GDI.DLL и т.д.), ресурсы типа диалогов, иконок, картинок, указателей мыши. Достаточно сложно найти программу, которая не использовала бы DLL вообще. Соответственно, программа, использующая DLL-библиотеки, содержит в своем .exe файле информацию о том, какие именно библиотеки ей нужны, и какие функции из этих библиотек она использует (импортирует). При запуске любой программы системный загрузчик Windows считывает список используемых этой программой DLL-библиотек и загружает (отображает) их в адресное пространство программы. После этого для каждой импортируемой программой функции загрузчик определяет адрес вызова.
Каждая DLL-библиотека содержит функцию с именем DllMain следующего вида
BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD fdwReason, PVOID fImpLoad);
Назначение её — сугубо информационное. Вызывая эту функцию, загрузчик сообщает библиотеке о том, что она будет подключена к какому-то процессу, либо в контексте процесса происходит создание потока. Загрузка любой программы включает в себя последовательный вызов функций DllMain всех используемых DLL. Аналогично данный вызов осуществляется при отключении и выгрузке библиотеки. Думаю, что суть метода вам уже ясна достаточно добавить к списку используемых программой DLL-библиотек свою, у которой функция DllMain содержит необходимый вам код. Весь код в рамках этой ф-ции будет выполнятся с приоритетом «заряженной» программы.
В меру скромного воображения приведу несколько примеров использования данной методики для решения практических задач. Первое, что приходит в голову — это система «навесной» защиты, которую можно установить на любое готовое приложение. Метод проверки допуска может быть любым — от простейшего пароля, до обращения к внешнему устройству, содержащему private-key для декодирования части исполняемого кода программы (например, USB-ключ). Далее — как было сказано в предисловии — любые виды троянов/вирусов.
Рецепт
Что же, теперь нам необходимо изменить .exe файл таким образом, что бы в списке используемых программой DLL библиотек появилась наша библиотека. На первый взгляд данная задача представляется как «темный лес» — где этот список взять, чего в нем искать, как чего менять? Но всё не так печально! К нашему с вами счастью, формат .exe файла Windows достаточно строго стандартизирован и подробно описан в документации. Желающим подробно поковыряться во внутренностях могу посоветовать ознакомиться с вот этим документом
Peering Inside the PE A Tour of the Win32 Portable Executable File Format,
для остальных я постараюсь привести здесь минимум информации, необходимый для реализации программы, «заряжающей» нашим кодом почти любой .exe файл. (Относительно ограничений метода — см. гл. Выводы) Приступам к пациенту. Формат файла программы, так же называемый «переносимым исполнительным» (PE — portable executable), определяет поведение операционной системы на всех этапах работы — начиная от отображения файла на адресное пространство процесса, загрузки необходимых библиотек, инициализации ресурсов, до собственно выгрузки программы. Самое важное из того, что следует знать о РЕ-файлах, это то, что исполняемый файл на диске и модуль, получаемый после загрузки, очень похожи. Причиной этого является то, что загрузчик Windows должен создать из дискового файла исполняемый процесс без больших усилий. Точнее говоря, загрузчик попросту использует механизмы отображения файлов в память, чтобы загрузить соответствующие части РЕ-файла в адресное пространство программы.* Так же просто загружается и DLL. После того как ЕХЕ или .DLL модули загружены, Windows обращается с ними так же, как и с другими отображенными в память файлами.

* См. справку по функция CreateFile, MapViewOfFile, CreateFileMapping.
Так как структура исполнительного файла является довольно громоздкой, вникать подробно в описание каждого её элемента мы не будет, лишь кратко остановимся на необходимых нам элементах. Все файлы программ для Windows имеют следующий формат (см. рис. 1)

рис. 1 Структура исполнительного файла.
1. Заголовок MSDOS Начиная с нулевого смещения, в файле располагается заголовок MSDOS, имеющий формат IMAGE_DOS_HEADER (см. файл winnt.h). В этом заголовке нас интересует только одно поле
IMAGE_DOS_HEADER->e_lfanew,
содержащее смещение сигнатуры файла.
2. Сигнатура PE-файла Сигнатура, иначе — подпись, означающая, что этот файл является исполнительным файлом для WIndows. Представляет собой строку
PE»