Внедрению DLL так или иначе (обычно в связи с перехватом API) посвящено достаточно большое количество статей. Но ни в одной из тех, которые я читал, не говорится, как извне заставить эту DLL сделать что-нибудь полезное. Обычно авторы ограничиваются перехватом необходимых API-функций где-нибудь в DllMain и последующей реакцией на вызовы этих самых функций. Между тем, взаимодействие с внедрённой DLL даёт возможность корректировать и направлять её работу и, тем самым, позволяет добиваться значительно большего эффекта. Если внедрённая DLL создаёт свой поток, задача взаимодействия легко решается, так как в этом случае можно использовать любые методы IPC: сообщения, сокеты, именованные каналы, … , при желании можно даже COM-сервер сделать :) ПРЕДУПРЕЖДЕНИЕ В описании DllMain сказано, что некоторые функции, в том числе CreateThread, из неё вызывать нельзя. Объяснение «почему они говорят, что нельзя» можно найти у Рихтера (в русском четвёртом издании это глава «DLL: более сложные методы программирования», раздел «Как система упорядочивает вызовы DllMain»), у него же написано, что на самом деле можно, если осторожно. :) Просто при создании потока надо не забывать, что его выполнение начнётся не раньше, чем текущий поток покинет DllMain. Но это всё более-менее очевидные и не очень красивые (на мой взгляд) способы. Мне кажется, я нашёл более интересный и элегантный метод. Ему и посвящена эта статья. Идея Идея тривиальна. Алгоритм состоит всего из четырёх шагов (плюс ещё один по желанию): Так или иначе загрузить в адресное пространство процесса-жертвы DLL, содержащую нужную функцию. ПРИМЕЧАНИЕ «Так или иначе» означает, что DLL может быть загружена любым способом. Например, это может быть advapi32.DLL, которую процесс-жертва грузит сам. Если вы хотите, чтобы исполнялся ваш код, скорее всего, DLL придётся внедрять. Описание внедрения DLL смотрите в дополнительных источниках в конце статьи. Получить адрес загрузки DLL. Получить адрес функции. Вызвать функцию при помощи CreateRemoteThread. (опционально) Дождаться завершения потока и получить возвращаемое значение функции вызовом GetExitCodeThread. А зачем нам DLL? При желании можно напрямую записать весь исполняемый код в адресное пространство процесса-жертвы и запустить его тем же CreateRemoteThread. При большом желании можно добиться, чтобы это заработало... Основная проблема, подстерегающая вас на этом пути, заключается в том, что все функции, которые вызывает ваш код, должны находиться точно по тем адресам, куда передаётся управление. С учётом того, что: код будет расположен в случайном месте адресного пространства, так как вам вряд ли удастся выделить память по тому же адресу; DLL могут быть загружены по другим адресам, «само собой» ничего не получится. Чтобы добиться работоспособности кода, нужно модифицировать используемые вашим кодом адреса, то есть, фактически, выполнить задачу загрузчика. А зачем выполнять её вручную, если можно положиться на загрузчик :) ? Ограничения Использование CreateRemoteThread связано с очевидными ограничениями: Поддерживается только линейка Windows NT/2000/XP. ПРИМЕЧАНИЕ Существует платная реализация CreateRemoteThread для Windows 9x, смотрите сайт http://www.apihooks.com раздел «PrcHelp». Прототип вызываемой функции должен соответствовать прототипу функции потока. Кроме того, нужно иметь солидные права доступа к процессу-жертве: PROCESS_CREATE_THREAD для запуска потока. PROCESS_VM_READ для определения адреса. PROCESS_VM_OPERATION + PROCESS_VM_WRITE (разрешение на выделение памяти и запись в адресное пространство процесса) может пригодиться, если вы хотите передать вызываемой функции что-нибудь посущественнее, чем четыре байта. ПРИМЕЧАНИЕ Проще всего получить все эти права, создав процесс, но, являясь достаточно привилегированным пользователем, можно получить необходимый доступ и к существующему процессу. Получение адреса загрузки DLL В общем случае, при помощи функций EnumProcessModules и GetModuleFileNameEx можно перебрать все загруженные в процесс-жертву модули, найти среди них нужный и получить адрес его загрузки. ПРИМЕЧАНИЕ Эти функции являются частью Process Status API (PSAPI), поэтому будут работать только в линейке Windows NT/2000/XP. Но поскольку мы уже и так используем CreateRemoteThread, терять нам нечего. Но если DLL внедрялась с помощью создания в процессе-жертве потока, поточной функцией которого является LoadLibrary, можно поступить проще. В этом случае код завершения потока является возвращаемым значением LoadLibrary, то есть как раз адресом загрузки DLL в процессе-жертве. ПРЕДУПРЕЖДЕНИЕ Вообще-то, как показывает практика, возвращаемое значение LoadLibrary – это не совсем адрес загрузки DLL. В некоторых случаях в младших битах находятся какие-то флаги. Например, при вызове функции LoadLibraryEx с флагом LOAD_LIBRARY_AS_DATAFILE младший бит возвращаемого значения всегда будет установлен в 1. Выход достаточно прост: поскольку при загрузке модуля в адресном пространстве создаётся регион, а адреса начала регионов должны быть кратны 64К, для получения «настоящего» адреса загрузки нужно просто обнулить два младших байта. Получение адреса функции Есть два способа получить адрес функции: простой и для настоящих программистов. :) Простой способ Простой способ основан на том, что смещение начала функции от начала DLL – величина постоянная, от процесса не зависящая. Это значит, что если: загрузить в свой процесс ту же DLL; получить адрес нужной функции; вычесть из адреса функции адрес загрузки DLL; прибавить к получившемуся смещению адрес загрузки DLL в процессе-жертве, то получится адрес функции в процессе-жертве. ПРИМЕЧАНИЕ Понятно, что если DLL в обоих процессах загружена по одному адресу, то и адреса функций будут совпадать. А поскольку (в нормальных, не слишком выпендривающихся процессах) системные DLL грузятся по одним и тем же адресам, адреса системных функций во всех процессах одинаковы. Именно на этом основана технология внедрения DLL через вызов LoadLibrary в другом процессе. Если по каким-то причинам DLL уже загружена в процесс, то, наверное, этот способ можно рекомендовать даже самым-самым настоящим программистам. А вот если DLL нужно специально грузить, то, по-моему, опять получается некрасиво. :) Способ для настоящих программистов Реализовать функцию GetProcAddressInOtherProcess, принимающую в первом параметре описатель процесса. Она будет разбирать таблицу экспорта указанной DLL из указанного процесса, находить там нужную функцию и возвращать её адрес. Если добавить функции LoadLibararyInOtherProcess и FreeLibraryInOtherProcess (которые несложно написать), получится совсем красиво, так как с чужим процессом можно будет работать почти так же, как и со своим. Именно этот способ кажется мне интересным и элегантным, и именно его реализации посвящена статья. Поиск экспортируемой функции в PE-файле Как вы, наверное, знаете, формат всех исполняемых файлов в Windows (включая DLL, ocx, sys, и прочие) называется PE (расшифровывается как Portable Executable, но большого смысла не несёт, просто название, ничем не хуже других) форматом, а сами файлы, соответственно, PE-файлами. Чтобы отыскать адрес нужной функции в DLL, придётся разобраться с той частью PE-формата, которая отвечает за экспорт. ПРИМЕЧАНИЕ PE-формат достаточно сложен, но, к счастью, полностью он нам и не нужен. Если вас интересует более подробное описание, смотрите дополнительные источники в конце статьи. Как в PE-файле добраться до секции экспорта Любой PE-файл начинается с заголовка DOS, формат которого отражён в структуре IMAGE_DOS_HEADER. typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header ... LONG e_lfanew; // File address of new exe header } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER; Из всех полей этой структуры для нас интерес представляет только поле e_lfanew, которое является смещением от начала файла (в терминологии PE-формата такие смещения называются RVA – Relative Virtual Address) до PE-заголовка. Формат PE-заголовка представлен структурой IMAGE_NT_HEADERS (она определена с использованием препроцессора и, на данный момент, соответствует структуре IMAGE_NT_HEADERS32): typedef struct _IMAGE_NT_HEADERS { ... IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32; Из неё нас интересует только поле OptionalHeader, которое разворачивается в ещё одну структуру: typedef struct _IMAGE_OPTIONAL_HEADER { ... IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32; И опять, нам нужно только одно поле – DataDirectory, а, точнее, только элемент DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]. Структура IMAGE_DATA_DIRECTORY описывает расположение в памяти одной из секций PE-файла. Она определёна следующим образом: typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; // RVA (смещение от начала файла) секции DWORD Size; // Размер секции } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY; Элемент DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT] относится к секции экспорта. Итого: В начале файла расположен IMAGE_DOS_HEADER. По смещению IMAGE_DOS_HEADER::e_lfanew находится IMAGE_NT_HEADERS. IMAGE_NT_HEADERS::OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT] описывает секцию экспорта. Он содержит RVA и размер секции. Как в секции экспорта найти адрес функции Секция экспорта начинается со структуры IMAGE_EXPORT_DIRECTORY. typedef struct _IMAGE_EXPORT_DIRECTORY { ... DWORD Base; DWORD NumberOfFunctions; DWORD NumberOfNames; DWORD AddressOfFunctions; // RVA from base of image DWORD AddressOfNames; // RVA from base of image DWORD AddressOfNameOrdinals; // RVA from base of image } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY; Здесь: AddressOfFunctions – RVA (смещение от начала файла) массива, содержащего RVA функций. AddressOfNames – RVA массива, содержащего RVA имён функций. AddressOfNameOrdinals – RVA массива индексов функций. Элемент n этого массива содержит индекс в массиве адресов функций, соответствующей n-ному элементу в массиве имён функций. ПРЕДУПРЕЖДЕНИЕ Во-первых, элементы этого массива имеют тип WORD и размер 2 байта. Во-вторых, MSDN и статья Мэтта Питрека «Форматы PE и COFF объектных файлов» содержат одну и туже ошибку, относящуюся к интерпретации содержимого этого массива. Правильно написано в статье Максима М. Гумерова «Загрузчик PE-файлов» и здесь :) NumberOfFunctions – количество элементов массива адресов функций. NumberOfNames – количество элементов массива имён функций и массива индексов функций. Base – базовое значение ординала экспортируемых функций. Для получения индекса функции, экспортируемой по ординалу, надо вычесть из её ординала значение Base. В результате, для поиска адреса функции, экспортируемой по имени, нужно сделать примерно следующее (в псевдокоде): // Ищем в массиве имён функций совпадающее имя int nameIndex = FindFunctionName(AddressOfNames, NumberOfNames, name); // Получаем соответствующий имени индекс функции WORD funcIndex = AddressOfNameOrdinals[nameIndex]; // Получаем RVA функции DWORD funcRVA = AddressOfFunctions[funcIndex]; ПРЕДУПРЕЖДЕНИЕ По MSDN и Питреку, последняя строчка алгоритма должна выглядеть так: DWORD funcRVA = AddressOfFunctions[funcIndex - Base]; Где Base – базовое значение ординала. Как показывает практика, Base вычитать не надо. Код В конце концов у меня получилось три функции. Первая находит секцию экспорта: // Определяет RVA секции экспорта int GetExportSectionRVA(HANDLE hProcess, const void* baseAddress) { // Читаем DOS-заголовок IMAGE_DOS_HEADER dos_header; ReadProcessMemory( hProcess, baseAddress, &dos_header, sizeof(dos_header), NULL); // Читаем PE-заголовок IMAGE_NT_HEADERS pe_header; ReadProcessMemory( hProcess, reinterpret_cast(baseAddress) + dos_header.e_lfanew, &pe_header, sizeof(pe_header), NULL); // Смещение секции экспорта return pe_header.OptionalHeader.DataDirectory [IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress; } Вторая перебирает массив имён функций в поиске заданного имени: // Ищет в массиве имён функций заданное имя, возвращает индекс или –1 int FindName( HANDLE hProcess, const void* baseAddress, DWORD AddressOfNames, DWORD count, const char* name) { // Для сравнения имени его нужно прочитать, для этого нужно знать размер int size = lstrlenA(name) + 1; std::auto_ptr candidate(new char[size]); // Перебираем имена в массиве имён функций for (int index = 0; index < count; index++) { DWORD nameRVA; // Читаем адрес начала строки ReadProcessMemory( hProcess, reinterpret_cast(baseAddress) + AddressOfNames + index * sizeof(DWORD), &nameRVA, sizeof(nameRVA), NULL); // Читаем строку ReadProcessMemory( hProcess, reinterpret_cast(baseAddress) + nameRVA, candidate.get(), size, NULL); if (strcmp(name, candidate.get()) == 0) { // Она! Сваливаем :) return index; } } // Такой функции нет return -1; } Третья функция использует первые две и находит нужную функцию в указанной DLL в указанном процессе: // Находит нужную функцию в указанной DLL в указанном процессе. void* GetProcAddress(HANDLE hProcess, HMODULE hLib, const char* name) { // Нам нужен именно адрес загрузки! А результат работы // LoadLibrary бывает иногда неожиданным.. char* baseAddress = reinterpret_cast (reinterpret_cast(hLib) & 0xFFFF0000); // Смещение секции экспорта int export_offset = GetExportSectionRVA(hProcess, baseAddress); if (export_offset <= 0) { // Какие-то проблемы с экспортом return NULL; } // Читаем заголовок секции экспорта IMAGE_EXPORT_DIRECTORY export; ReadProcessMemory( hProcess, baseAddress + export_offset, &export, sizeof(export), NULL); // Индекс в массиве функций WORD funcIndex = -1; if (reinterpret_cast(name) > 0x0000ffff) { // Функция экспортируется по имени. Ищем имя int nameIndex = FindName( hProcess, baseAddress, export.AddressOfNames, export.NumberOfNames, name); if (nameIndex < 0) { // Такой функции нет return NULL; } // Читаем индекс (они двухбайтные!!!) ReadProcessMemory( hProcess, baseAddress + export.AddressOfNameOrdinals + nameIndex * sizeof(WORD), &funcIndex, sizeof(funcIndex), NULL); } else { // Функция экспортируется по ординалу WORD funcOrdinal = reinterpret_cast(name); if ((funcOrdinal = export.Base + export.NumberOfFunctions)) { // Такой функции нет return NULL; } // Индекс это ординал минус база funcIndex = funcOrdinal - export.Base; } if ((funcIndex = export.NumberOfFunctions)) { // Такой функции нет return NULL; } // Читаем адрес DWORD funcRVA; ReadProcessMemory( hProcess, baseAddress + export.AddressOfFunctions + funcIndex * sizeof(DWORD), &funcRVA, sizeof(funcRVA), NULL); // Результат это базовый адрес + RVA return (baseAddress + funcRVA); } ПРИМЕЧАНИЕ Для оптимизации можно было бы сначала скопировать в свой процесс всю секцию экспорта (размер секции хранится в IMAGE_NT_HEADERS::OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size), а потом уже её разбирать. Но, поскольку заметных глазу задержек не возникает, я остановился на текущей реализации. Пример В качестве примера я написал три приложения: aggressor.exe, victim.exe и insider.dll. Victim и insider абсолютно пассивны, все действия выполняются aggressor-ом. Aggressor: запускает victim.exe; загружает в него insider.dll; получает адреса трёх экспортируемых функций; вызывает эти функции; выгружает insider.dll из victim.exe . ПРИМЕЧАНИЕ Чтобы это действительно работало, надо положить все три исполняемых модуля в один каталог. Для реализации перечисленных действий, да и вообще на будущее, в aggressor реализованы следующие полезные функции: namespace OtherProcess { // // Вызывает функцию из заданного процесса, возвращает // описатель потока, который эту функцию выполняет HANDLE AsynchronousCall( HANDLE hProcess, void* address, void* parameter, DWORD* pid); // // Вызывает функцию из заданного процесса, дожидается завершения её работы bool SynchronousCall( HANDLE hProcess, void* address, void* parameter, DWORD* result); // // Загружает DLL в указанный процесс HMODULE LoadLibrary(HANDLE hProcess, const TCHAR* path); // // Выгружает DLL в указанном процессе void FreeLibrary(HANDLE hProcess, HMODULE hLib); // // Находит нужную функцию в указанной DLL в указанном процессе void* GetProcAddress(HANDLE hProcess, HMODULE hLib, const char* name); }; Предназначение функций, я надеюсь, понятно из их названий и кратких комментариев. Понимание реализации также не должно вызвать затруднений, прокомментировано всё достаточно подробно, да и сам код не такой уж головоломный. Успешных вам вызовов!
Рефераты по информатикеВнедрению DLL так или иначе (обычно в связи с перехватом API) посвящено достаточно большое количество статей. Но ни в одной из тех, которые я читал,
Оценок: 339 (Средняя 5 из 5)
Специалисты RetsCorp работают в digital-сфере более 7 лет. За это время мы разработали более 500+ успешных проектов. Основываясь на своем опыте и знании рынка, мы с уверенностью можем сказать, что будет работать, а что — нет. Заказывая создание лендинга для бизнеса в нашей студии, вы получаете работающие решения, необходимые именно вашему бизнесу.
Сотрудничая с нами, вы будете не клиентом, а нашим партнером. Благодаря этому мы будем развивать ваш бизнес как собственный. Мы так же как и вы заинтересованы в успехе проекта, поскольку ваша успешность будет нашей рекламой.