Салют, друзья! На дворе 2018 год, а мы с вами начинаем изучать DLL инъекции
DLL Injection
Что же за DLL-инъекции, то такие? Для начала, нужно рассказать, что такое DLL.
DLL (Dynamic Link Library) - исполняемый модуль, в котором хранятся функции, используемые программой. Да! Их просто вынесли в отдельный исполняемый модуль. DLL'ки загружаются программой, а после этого используются функции, которые экспортирует DLL. Пример DLL - kernel32.dll в нём хранятся основные функции Windows API.
При загрузке/выгрузке DLL вызывается определённая функция этой DLL. Её обычно называют инициализирующей и по умолчанию она носит имя DllMain.
Вернёмся к нашим баранам, а точнее DLL инъекциям. На самом деле, DLL инъекция - это один из способов выполнения стороннего кода в контексте другого процесса. Говоря проще, это техника внедрения кода в уже загруженный произвольный исполняемый файл. Зачем она нужна? Ну, рассмотрим самые частые случаи использования DLL:
- Иногда нам может понадобиться внедрить в процесс собственный код (функцию) (для использования самой программой, для перехват API, для перехвата других функций и т.д.).
- Иногда очень важно получить прямой доступ к памяти нужного нам процесса (для модифицирования содержимого памяти либо чтения), не смотря на то что существуют WriteProcessMemory и ReadProcessMemory WinApi функции.
- Скрытие вредоносного кода в легитимном процессе.
- и т.д. и т.п.
Теория
Основная идея DLL инъекции - загрузить DLL. Для этого может использоваться огромное количество способов, вплоть до ручной загрузки DLL (этот способ называется Reflected DLL Injection). Но как я сказал выше, мы рассмотрим самый быстрый и простой способ - загрузку с помощью LoadLibrary через удалённый поток.
В принципе, процесс внедрения DLL можно разделить на несколько шагов:
1. Открываем процесс (получаем доступ к целевому процессу) с помощью WinApi функции OpenProcess.
2. Выделяем память для имени DLL, которую мы хотим внедрить.
3. Записываем путь к DLL по выделенной памяти
4. Запускаем LoadLibrary
Вся магия DLL инъекций скрыта как-раз таки на этом этапе, а предыдущие шаги были подготовительными. В данном этапе, мы вызываем функцию LoadLibraryA, которая и загружает нашу DLL'ку.
После всех наших действий, автоматически запускается инциализирующая функция нашей DLL, которая выполняет свою
Практика
Для закрепления теории, предлагаю написать простой консольный DLL инжектор, который загрузит нужную нам DLL в нужный нам процесс.
Давайте напишем функцию, с помощью которой мы получим PID используя имя процесса. Здесь мы получаем список всех процессов, а потом проходясь по этому списку, сравниваем имя процесса в списке с именем нужного нам процесса. Если мы нашли процесс, то возвращает его PID.
C++:
DWORD GetPidByProcessName(LPTSTR lpszProcessName)
{
HANDLE hSnapshot;
PROCESSENTRY32 pe32 = { 0 };
pe32.dwSize = sizeof(PROCESSENTRY32);
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
if (hSnapshot == INVALID_HANDLE_VALUE)
{
throw std::exception("Can't create the snapshot!");
}
if (Process32First(hSnapshot, &pe32))
{
do
{
if (_tcscmp(pe32.szExeFile, lpszProcessName) == 0)
{
return pe32.th32ProcessID;
}
}
while (Process32Next(hSnapshot, &pe32));
}
throw std::exception("Can't find the process!");
}
Для этой функции нам понадобиться подключить два заголовочных файла:
C++:
#include <Windows.h>
#include <TlHelp32.h>
Ну и объявим две константы. Одну для имени DLL, другую для имени процесса (инъектить DLL'ку мы будем в наш замученный hello.exe).
C++:
#define TARGET_PROCESS "hello.exe"
#define TARGET_DLL "D:\\test.dll"
Теперь, добавим следующий код в главную функцию. Здесь мы получаем PID процесса и получаем DLL, а также размер строки с путём до DLL.
C++:
char sDLLName[] = TARGET_DLL;
DWORD dwSize = sizeof(sDLLName)+1;
DWORD dwProcessID;
try
{
dwProcessID = GetPidByProcessName(_TEXT(TARGET_PROCESS));
}
catch (std::exception& e)
{
std::wcout << "[ERROR] " << e.what() << std::endl;
getchar();
return 1;
}
std::wcout << "[INFO] ProcessID = " << dwProcessID << std::endl;
Так, мы получили PID процесса. А для чего же он нам нужен? Конечно же, чтобы открыть процесс! (весь последующий код добавляйте в функцию main (или _tmain) прим. автора). Первым пунктом инъекции у нас идёт открытие процесса. Взглянем на функцию открытия процесса поподробнее.
Протопип функции:
C++:
HANDLE OpenProcess(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
DWORD dwProcessId
);
- dwDesiredAccess
Права доступа, которые мы хотим получить открыв процесс. Установим PROCESS_ALL_ACCESS, чтобы получить все права на процесс. - bInheritHandle
Флаг наследования дескриптора. Установим его в FALSE. - dwProcessId
Идентификатор процесса (Process ID или PID)
C++:
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessID);
if (hProcess == NULL)
{
std::wcout << "[ERROR] Can't open the target process!" << std::endl;
CloseHandle(hProcess);
getchar();
return 1;
}
Что нам нужно сделать после открытия процесса? Правильно! Выделить память для пути к DLL. Взглянем на функцию VirtualAllocEx поподробнее, ведь именно с помощью неё мы и выделим память!
C++:
LPVOID WINAPI VirtualAllocEx(
_In_ HANDLE hProcess,
_In_opt_ LPVOID lpAddress,
_In_ SIZE_T dwSize,
_In_ DWORD flAllocationType,
_In_ DWORD flProtect
);
- hProcess
Дескриптор процесса, в котором мы хотим выделить память - lpAddress
Адрес, по которому хотим выделить память. Если он нам не важен, то мы можем установить NULL. - dwSize
Размер выделяемой памяти. - flAllocationType
Тип выделения - flProtect
Права на выделенную память.
C++:
LPVOID lpDllName = VirtualAllocEx(hProcess, NULL, dwSize,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (lpDllName == NULL)
{
std::wcout << "[ERROR] Can't allocate memory in the target process!"
<< std::endl;
CloseHandle(hProcess);
getchar();
return 1;
}
std::wcout << "[INFO] Memory allocated at 0x" << lpDllName << std::endl;
Зачем выделять? Чтобы записать сюда имя DLL. Изучим функцию WriteProcessMemory, с помощью которой мы и запишем имя DLL'ки в нужный нам процесс по нужному нам адресу памяти.
Код:
BOOL WINAPI WriteProcessMemory(
_In_ HANDLE hProcess,
_In_ LPVOID lpBaseAddress,
_In_ LPCVOID lpBuffer,
_In_ SIZE_T nSize,
_Out_ SIZE_T *lpNumberOfBytesWritten
);
- hProcess
Дескриптор процесса, в память которого мы и хотим записать нужные нам данные. - lpBaseAddress
Адрес в памяти процесса, по которому мы хотим записать данные. - lpBuffer
Указатель на буфер, который мы хотим записать. - nSize
Размер буфера (или сколько байтов нам нужно записать). - lpNumberOfBytesWritten
Указатель на целочисленную переменную, в которую будет записано число записанных байт.
Перейдём к коду. Здесь мы запишем имя DLL'ки и проверим на ошибки.
C++:
BOOL bRes = WriteProcessMemory(hProcess, lpDllName, sDLLName,
dwSize, NULL);
if (!bRes)
{
std::wcout << "[ERROR] Can't write the dll name in the target process!"
<< std::endl;
VirtualFreeEx(hProcess, lpDllName, dwSize, MEM_RELEASE);
CloseHandle(hProcess);
getchar();
return 1;
}
Зачем записывать? Чтобы загрузить DLL, конечно. Но перед этим, нам нужно получить адрес функции LoadLibraryA (с помощью двух функций, GetModuleHandle и GetProcAddress), которая и загрузит нашу DLL'ку, а уже потом создать сам удалённый поток. Что такое удалённый поток? Проще говоря, с помощью удалённого потока мы можем вызвать любую нужную нам функцию в нужном нам процессе передав нужные нам параметры.
C++:
LPVOID lpLoadLibrary = GetProcAddress(hKernel32, "LoadLibraryA");
if (lpLoadLibrary == NULL)
{
std::wcout << "[ERROR] Can't get the LoadLibraryA address!"
<< std::endl;
VirtualFreeEx(hProcess, lpDllName, dwSize, MEM_RELEASE);
CloseHandle(hProcess);
getchar();
return 1;
}
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0,
LPTHREAD_START_ROUTINE(lpLoadLibrary), lpDllName, 0, 0);
if (hThread == NULL)
{
std::wcout << "[ERROR] Can't create remote thread!"
<< std::endl;
VirtualFreeEx(hProcess, lpDllName, dwSize, MEM_RELEASE);
CloseHandle(hProcess);
getchar();
return 1;
}
Ну-с, в принципе, на этом процесс инъекции и заканчивается. Добавим финальные штрихи:
C++:
std::wcout << "[+] DLL Injected!" << std::endl;
CloseHandle(hThread);
VirtualFreeEx(hProcess, lpDllName, dwSize, MEM_RELEASE);
CloseHandle(hProcess);
getchar();
return 0;
Финальный код выглядит так:
C++:
#include <Windows.h>
#include <TlHelp32.h>
#include <iostream>
#define TARGET_PROCESS "hello.exe"
#define TARGET_DLL "D:\\test.dll"
DWORD GetPidByProcessName(LPTSTR lpszProcessName)
{
HANDLE hSnapshot;
PROCESSENTRY32 pe32 = { 0 };
pe32.dwSize = sizeof(PROCESSENTRY32);
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
if (hSnapshot == INVALID_HANDLE_VALUE)
{
throw std::exception("Can't create the snapshot!");
}
if (Process32First(hSnapshot, &pe32))
{
do
{
if (_tcscmp(pe32.szExeFile, lpszProcessName) == 0)
{
return pe32.th32ProcessID;
}
}
while (Process32Next(hSnapshot, &pe32));
}
throw std::exception("Can't find the process!");
}
int _tmain(int argc, _TCHAR* argv[])
{
char sDLLName[] = TARGET_DLL;
DWORD dwSize = sizeof(sDLLName)+1;
DWORD dwProcessID;
try
{
dwProcessID = GetPidByProcessName(_TEXT(TARGET_PROCESS));
}
catch (std::exception& e)
{
std::wcout << "[ERROR] " << e.what() << std::endl;
getchar();
return 1;
}
std::wcout << "[INFO] ProcessID = " << dwProcessID << std::endl;
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessID);
if (hProcess == NULL)
{
std::wcout << "[ERROR] Can't open the target process!" << std::endl;
CloseHandle(hProcess);
getchar();
return 1;
}
LPVOID lpDllName = VirtualAllocEx(hProcess, NULL, dwSize,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (lpDllName == NULL)
{
std::wcout << "[ERROR] Can't allocate memory in the target process!"
<< std::endl;
CloseHandle(hProcess);
getchar();
return 1;
}
std::wcout << "[INFO] Memory allocated at 0x" << lpDllName << std::endl;
BOOL bRes = WriteProcessMemory(hProcess, lpDllName, sDLLName,
dwSize, NULL);
if (!bRes)
{
std::wcout << "[ERROR] Can't write the dll name in the target process!"
<< std::endl;
VirtualFreeEx(hProcess, lpDllName, dwSize, MEM_RELEASE);
CloseHandle(hProcess);
getchar();
return 1;
}
HMODULE hKernel32 = GetModuleHandle(_TEXT("kernel32.dll"));
if (hKernel32 == NULL)
{
std::wcout << "[ERROR] Can't get the handle of kernel32.dll!"
<< std::endl;
VirtualFreeEx(hProcess, lpDllName, dwSize, MEM_RELEASE);
CloseHandle(hProcess);
getchar();
return 1;
}
LPVOID lpLoadLibrary = GetProcAddress(hKernel32, "LoadLibraryA");
if (lpLoadLibrary == NULL)
{
std::wcout << "[ERROR] Can't get the LoadLibraryA address!"
<< std::endl;
VirtualFreeEx(hProcess, lpDllName, dwSize, MEM_RELEASE);
CloseHandle(hProcess);
getchar();
return 1;
}
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0,
LPTHREAD_START_ROUTINE(lpLoadLibrary), lpDllName, 0, 0);
if (hThread == NULL)
{
std::wcout << "[ERROR] Can't create remote thread!"
<< std::endl;
VirtualFreeEx(hProcess, lpDllName, dwSize, MEM_RELEASE);
CloseHandle(hProcess);
getchar();
return 1;
}
std::wcout << "[+] DLL Injected!" << std::endl;
CloseHandle(hThread);
VirtualFreeEx(hProcess, lpDllName, dwSize, MEM_RELEASE);
CloseHandle(hProcess);
getchar();
return 0;
}
Но что мы будем внедрять? А точнее, какую DLL мы будем внедрять? Для этого, я создал простую DLL с именем test.dll и поместил его на диск D. Вот код DLL'ки:
C++:
#include <windows.h>
extern "C" __declspec(dllexport) BOOL APIENTRY DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
switch (fdwReason)
{
case DLL_PROCESS_ATTACH:
MessageBoxA(NULL, "DLL Injected!", "DLL", MB_OK | MB_ICONINFORMATION);
break;
case DLL_PROCESS_DETACH:
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
}
return TRUE;
}
Скомпилируем и протестируем!
Последнее редактирование: