Добавление ссылки на dll в таблицу импорта. Для того, чтобы внедрить свою dll в чужой процесс можно добавить ссылку на нашу dll в таблицу импорта нужного exe или dll файла. Тогда наша dll будет загружена при запуске процесса. DllMain нашей dll будет вызвана до начала выполнения кода программы (раньше могут быть выполнены DllMain других dll). Инициализация процесса не будет продолжена пока не завершит выполнение код в DllMain нашей dll, так что можно сделать необходимые действия до начала выполнения кода самой программы. Я в программе из DllMain внедренной dll вывожу окошко с запросом пароля. Если пароль правильный - просто закрываю окошко с запросом пароля и выхожу из DllMain, так что загрузчик сможет завершить инициализацию процесса и программа начнет нормально работать. Если пользователь нажмет кнопку Отмена - ExitProcess завершит процесс. Добавление ссылки на dll в таблицу импорта находится в функции hard_import в файле hard_import.cpp. Для добавления ссылки в таблицу импорта можно расширить существующую таблицу. Это не всегда возможно, так как не всегда после таблицы импорта есть достаточно свободного места для записи. Таблица импорта может находиться в отдельной секции (например, может называться .idata) (так делает, например, компилятор gсс из mingw). Секции в файле имеют длинну кратную значению IMAGE_OPTIONAL_HEADER.FileAlignment. Обычно это значение равно 0x200 или 0x1000. Компилятор из MS Visual Studio не создает для таблицы импорта отдельной секции, а помещает таблицу в секцию .rdata. А, например, в известной программе notepad.exe таблица импорта находится в секции с кодом .text. Сначала я сделал, чтобы ссылка на dll добавлялась в существующую таблицу импорта, но когда начал пробывать это на разных программах, увидел, что места для необходимых данных очень часто не хватает. Это значит, что таблицу импорта придется переписать на другое место. К счастью, сделать это не сложно, так как в таблице импорта указаны только ссылки на dll. Таблицы с адресами функций, которые использует программа для вызова функций из dll расположены отдельно и нам их трогать не придется. Куда переместить таблицу импорта? Можно, например, было бы создать специальную секцию и поместить таблицу в нее. К сожалению это сделать часто нельзя. Указателей на таблицу секций в заголовке PE файла нет. Загрузчик ищет ее в определенном месте: сразу после заголовка. Так что переместить ее нельзя. Свободного места сразу за таблицей секций часто нет, так что расширить ее бывает нельзя. Следующее решение - переместить таблицу импорта в одну из существующих секций, где есть достаточно свободного места. Как определить, сколько в секции свободного места? В структуре IMAGE_SECTION_HEADER есть два поля: SizeOfRawData и VirtualSize. SizeOfRawData - размер секции в файле. VirtualSize - количество байт, которое загрузчик должен спроецировать в память при загрузке PE файла. Часто значение VirtualSize показывает, сколько реально используемых байт содержится в секции. Значение SizeOfRawData кратно IMAGE_OPTIONAL_HEADER.FileAlignment, так что разность между этими двумя полями даст количество свободных байт в секции в файле. Однако, не всегда это так. Значение VirtualSize может быть и больше, чем SizeOfRawData, если необходимо, чтобы загрузчик создал для секции в памяти область больше, чем она занимает в файле на диске. Если таким способом удается найти секцию, где достаточно места для размещения таблицы импорта - можно переместить ее туда. Если нет - нужно использовать другой способ. Можно расширить одну из существующих в файле секций поместить таблицу импорта в нее. Проще всего раширять самую последнюю секцию файла. Сначала необходимо найти количество байт, которое займет таблица импорта вместе с добавленной в нее ссылкой на еще одну dll, а также, сколько байт займут дополнительные данные необходимые для импорта функции из нашей dll. В программе я не использую свободное место, которое, может быть было в последней секции файла, а добавляю необходимое количество байт и использую только эти добавленные байты. Необходимо, чтобы добавленное количество байт было кратно 0x10000, так как иначе некоторые программы, на которых я испытывал эту операцию не работали (не знаю почему). После увеличения размера файла необходимо установить новые значения SizeOfRawData и VirtualSize секции, которую мы расширяли, чтобы загрузчик правильно спроецировал ее в память. Затем, секция содержащая таблицу импорта должна быть доступна для чтения и записи, иначе программа не будет работать. Чтобы это сделать, надо установить в IMAGE_SECTION_HEADER.Characteristics биты IMAGE_SCN_MEM_READ и IMAGE_SCN_MEM_WRITE. Дальше, необходимо увеличить IMAGE_NT_HEADERS.OptionalHeader.SizeOfInitializedData и IMAGE_NT_HEADERS.OptionalHeader.SizeOfImage на добавленное количество байт. Секцию мы расширили, теперь есть место, куда поместить таблицу импорта. Целиком копируем ее со старого места на новое. Последняя структура в таблице импорта заполнена нулями и обозначает конец таблицы импорта. Нужно заменить ее на структуру описывающую нашу dll и добавить завершающую нулевую структуру в конец новой таблицы импорта. Еще одна вещь о которой надо помнить: адреса в PE файле - называются RVA - Relative Virtual Address. Это адреса относительно начала PE файла уже спроецированного в память. Это значит, что RVA адрес указывает на байт памяти после того, как все секции файла уже помещены по адресам указанным в IMAGE_SECTION_HEADER.VirtualAddress (это значение - тоже смещение начала секции относительно адреса начала спроецированного PE файла) и секции в памяти имеют размер заданный значением IMAGE_SECTION_HEADER.VirtualSize. Мы работаем с файлом, где секции не размещены загрузчиком на нужные области памяти, поэтому нам нужен способ перевода RVA адреса в смещение относительно начала PE файла на диске. Сделать это совсем не сложно. Мы проходим по всем секциям и пользуясь полями VirtualAddress и SizeOfRawData находим, в какую из секций попадает заданный RVA адрес. Дальше пользуясь полями VirtualAddress и PointerToRawData (смещение секции в файле) находим смещение адреса в файле. Понадобится нам также и обратное преобразование - из смещения внутри файла получить RVA адрес. Для этого также проходим все секции и пользуясь полями PointerToRawData и SizeOfRawData находим, в какую секцию попадает заданное смещение, а затем, с помощью полей PointerToRawData и VirtualAddress находим RVA. Теперь необходимо заполнить добавленную структуру, которая будет ссылаться на нашу dll. Места у нас достаточно. Помещаем в память имя нашей dll. Преобразуем в RVA смещение имени в файле и помещаем в поле Name нашей добавленной структуры IMAGE_IMPORT_DESCRIPTOR. Устанавливаем в 0 поля TimeDateStamp и ForwarderChain. Теперь приступаем к импорту функции. Я импортирую 1 функцию из dll просто, чтобы правильно заполнить таблицу импорта. Все равно, чужая программа, в которую мы хотим внедрить нашу dll ничего не знает о нашей dll и не собирается вызывать из нее никаких функций. Нам надо создать 2 массива указателей. Каждый из них будет состоять из 2 элементов. В первом будет указатель на структуру IMAGE_IMPORT_BY_NAME с именем нашей функции, а во втором будет 0, означающий конец массива. Размещаем в памяти структуру IMAGE_IMPORT_BY_NAME. В поле Hint помещаем 0, а в поле Name записываем имя импортируемой функции. Преобразуем смещение в файле нашей структуры IMAGE_IMPORT_BY_NAME в RVA и помещаем адрес в нулевые элементы созданных массивов указателей. Два массива указателей на IMAGE_IMPORT_BY_NAME нужны для того, что загрузчик поместит в один массив вместо указателя на структуру с именем импортируемой функции адрес самой функции. Второй массив загрузчик не тронет и по нему можно будет определить, какой адрес соответствует какой функции. В поля нашей структуры IMAGE_IMPORT_BY_NAME.Characteristics и IMAGE_IMPORT_BY_NAME.OriginalFirstThunk заносим RVA одного из созданных массивов. В поле IMAGE_IMPORT_BY_NAME.FirstThunk - адрес другого. Теперь нужно поместить RVA нашей таблицы импорта в IMAGE_NT_HEADERS.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress, и указать ее размер в IMAGE_NT_HEADERS.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size Теперь можно попробывать, что получилось. Поместим нашу dll туда, где загрузчик сможет ее найти и запустим измененный PE файл. Наша dll должна быть загружена в процесс. Однако, попробывав разные программы, я увидел, что на некоторых из них наша dll при запуске процесса не загружается. Сам exe файл нормально загружался и работал. Посмотрев заголовки этих PE файлов, я увидел, что в них во всех в IMAGE_NT_HEADERS.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT] есть ссылка на bound import. Чтобы наша dll загрузилась при запуске процесса нужно убрать bound import: IMAGE_NT_HEADERS.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT].Size=0; IMAGE_NT_HEADERS.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT].VirtualAddress=0; После этого файл будет нормально загружаться (правда, немного медленнее) и наша dll будет загружаться вместе с ним. Ссылки Про формат PE файлов есть много статей. Например, Matt Pietrek "Peering Inside the PE: A Tour of the Win32 Portable Executable File Format", Matt Pietrek "An In-Depth Look into the Win32 Portable Executable File Format", спецификацию формата PE файлов можно скачать с сайта microsoft.