Сергей ХолодиловВ
I just called to say I love you,
And I mean it from the bottom of my heart.
Stevie Wonder
Внедрению 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 В { В В В 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 В { В В В // Какие-то проблемы СЃ экспортом В В В 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 В В В { В В В В В // Такой функции нет В В В В В return NULL; В В В } В В В // Читаем индекс (РѕРЅРё двухбайтные!!!) В В В ReadProcessMemory( В В В В В hProcess, В В В В В baseAddress + export.AddressOfNameOrdinals В В В В В В В + nameIndex * sizeof(WORD), В В В В В &funcIndex, В В В В В sizeof(funcIndex), В В В В В NULL); В } В else В { В В В // Функция экспортируется РїРѕ ординалу В В В WORD funcOrdinal = reinterpret_cast(name); В В В if ((funcOrdinal В В В В || (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); }; |
Предназначение функций, я надеюсь, понятно из их названий и кратких комментариев. Понимание реализации также не должно вызвать затруднений, прокомментировано всё достаточно подробно, да и сам код не такой уж головоломный. Успешных вам вызовов!
Список литературы
Джеффри Рихтер, «Programming Application for Microsoft Windows», четвёртое издание.
Тихомиров В.А. «Перехват API-функций в Windows NT/2000/XP».
Мэтт Питрек «Форматы PE и COFF объектных файлов»
Максим М. Гумеров «Загрузчик PE-файлов»
Для подготовки данной работы были использованы материалы с сайта http://www.rsdn.ru/