Se l'APC è stata accodata da user mode, però, è necessario anche che il thread a cui si accoda l'APC sia nello stato allertabile (alertable). Un thread entra in tale stato se chiama una tra le funzioni SleepEx, SignalObjectAndWait, MsgWaitForMultipleObjectsEx, WaitForMultipleObjectsEx, o WaitForSingleObjectEx. Naturalmente l'APC deve essere accodata prima che l'attesa, dovuta alla chiamata di una delle funzioni appena citate, venga soddisfatta. In caso contrario la funzione relativa non sarà chiamata alla prossima pianificazione del thread ma l'APC verrà comunque accodata e la funzione eseguita non appena il thread entrerà nuovamente nello stato allertabile.
Alla luce di quanto detto, un vantaggio dell'iniezione tramite APC è che non richiede la creazione e l'esecuzione di un thread remoto nel processo target. Uno svantaggio è che il processo deve avere almeno un thread che entra nello stato allertabile.
Come esempio banale si prenda un eseguibile target che continua a chiamare SleepEx per un certo periodo di tempo ed in cui si voglia iniettare del codice.
#include <Windows.h> #include <iostream> int wmain(int argc, wchar_t * argv[]) { std::cout << "Target running!\n"; std::cout << "Target waiting for 60 seconds!\n"; for(int i = 0; i < 60; ++i) { SleepEx(1000, TRUE); } std::cout << "Press any key to exit!"; std::getchar(); return 0; }
Il codice da iniettare è in una DLL ed è altrettanto banale.
#include <Windows.h> BOOL APIENTRY DllMain(HMODULE hModule, DWORD dwReason, LPVOID lpReserved ) { switch (dwReason) { case DLL_PROCESS_ATTACH: { MessageBox(NULL, L"Hello from Test_DLL!", L"APC", MB_OK | MB_ICONWARNING); break; } case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; }
L'eseguibile responsabile dell'iniezione del codice calcola il percorso assoluto della DLL (Test_DLL.dll) che contiene il codice da iniettare ed usa la funzione ApcInjectDll per iniettare tale codice nel processo Test_Target.exe.
#include "Apc_Inject.h" #include "wchar.h" // sopprime warning di sicurezza per alcune funzioni deprecate tipo wcscat #pragma warning(disable : 4996) void GetAppPath(wchar_t *szCurFile) { GetModuleFileName(0, szCurFile, MAX_PATH); for (SIZE_T i = wcslen(szCurFile) - 1; i >= 0; i--) { if (szCurFile[i] == '\\') { szCurFile[i + 1] = '\0'; break; } } } int wmain(int argc, wchar_t * argv[]) { BOOL bRet = FALSE; wchar_t szDllFile[MAX_PATH] = {0}; GetAppPath(szDllFile); std::wcscat(szDllFile, L"Test_DLL.dll"); bRet = ApcInjectDll(L"Test_Target.exe", szDllFile); if (bRet) { std::cout << "APC Inject OK.\n"; } else { std::cout << "APC Inject ERROR.\n"; } std::cout << "Press any key to exit!"; std::getchar(); return 0; }
La funzione ApcInjectDll è dichiarata in Apc_Inject.h insieme ad altre funzioni di utilità come GetProcessIdByProcessName e GetAllThreadIdByProcessId il cui nome è abbastanza esplicativo.
#include <Windows.h> #include <TlHelp32.h> #include <iostream> DWORD GetProcessIdByProcessName(const wchar_t *pszProcessName); BOOL GetAllThreadIdByProcessId(DWORD dwProcessId, std::vector<DWORD> &vThreadId); BOOL ApcInjectDll(const wchar_t *pszProcessName, const wchar_t *pszDllName);
In Apc_Inject.cpp si trovano le definizioni di tali funzioni.
GetProcessIdByProcessName e GetAllThreadIdByProcessId usano CreateToolhelp32Snapshot per calcolare PID e collezione di TID del processo target (si veda [3] per maggiori info).
ApcInjectDll usa l'API di Windows (si vedano [2] o la documentazione online di MS) per ottenere l'HANDLE del processo target, allocare spazio sufficiente a contenere il percorso assoluto della DLL, scrivere tale percorso nello spazio appena allocato, ottenere un puntatore a LoadLibraryW definita in kernel32.dll ed infine accodare un APC (tramite la funzione dell'API di Windows QueueUserAPC) in tutti i thread del processo target.
Quando un thread del processo target entrerà nello stato allertabile quest'ultimo eseguirà LoadLibraryW con argomento uguale al percorso della DLL. Ed il gioco è fatto!
Da notare che la chiamata a LoadLibraryW viene eseguita nel processo target quindi la stringa che contiene il percorso assoluto della DLL deve trovarsi nello spazio degli indirizzi del processo target. Ecco perché si alloca lo spazio nel processo target e ci si scrive dentro tale percorso.
Accodare l'APC a tutti i thread non comporta particolari rischi in questo caso in quanto una DLL non può essere caricata più di una volta ed in più, così facendo, si aumentano le probabilità di trovare almeno un thread che entri in stato allertabile.
#include "Apc_Inject.h" void ShowError(const wchar_t *pszText) { wchar_t szErr[MAX_PATH] = { 0 }; wsprintf(szErr, L"%s Error[%d]\n", pszText); MessageBox(NULL, szErr, L"ERROR", MB_OK | MB_ICONERROR); } BOOL ApcInjectDll(const wchar_t *pszProcessName, const wchar_t *pszDllName) { BOOL bRet = FALSE; DWORD dwProcessId = 0; std::vector<DWORD> vThreadID; HANDLE hProcess = NULL, hThread = NULL; PVOID pBaseAddress = NULL; PVOID pLoadLibraryAFunc = NULL; // lstrlen restituisce il numero di caratteri (meno quello nullo finale) // ma a noi interessa il numero di byte da allocare // ricordando che ogni wchar_t è 2 byte SIZE_T dwRet = 0, dwDllPathLen = 1 + lstrlen(pszDllName) * 2; DWORD i = 0; do { dwProcessId = GetProcessIdByProcessName(pszProcessName); if (0 >= dwProcessId) { bRet = FALSE; break; } bRet = GetAllThreadIdByProcessId(dwProcessId, vThreadID); if (FALSE == bRet) { bRet = FALSE; break; } hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId); if (NULL == hProcess) { ShowError(L"OpenProcess"); bRet = FALSE; break; } pBaseAddress = VirtualAllocEx(hProcess, NULL, dwDllPathLen, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (NULL == pBaseAddress) { ShowError(L"VirtualAllocEx"); bRet = FALSE; break; } WriteProcessMemory(hProcess, pBaseAddress, pszDllName, dwDllPathLen, &dwRet); if (dwRet != dwDllPathLen) { ShowError(L"WriteProcessMemory"); bRet = FALSE; break; } //pLoadLibraryAFunc = LoadLibraryW; // sufficiente solo se kernel32.dll già importato pLoadLibraryAFunc = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW"); if (NULL == pLoadLibraryAFunc) { ShowError(L"GetProcessAddress"); bRet = FALSE; break; } SIZE_T dwThreadIdLength = vThreadID.size(); for (i = 0; i < dwThreadIdLength; i++) { hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, vThreadID[i]); if (hThread) { QueueUserAPC((PAPCFUNC)pLoadLibraryAFunc, hThread, (ULONG_PTR)pBaseAddress); CloseHandle(hThread); hThread = NULL; } } bRet = TRUE; } while (FALSE); if (hProcess) { CloseHandle(hProcess); hProcess = NULL; } return bRet; }
DWORD GetProcessIdByProcessName(const wchar_t *pszProcessName) { DWORD dwProcessId = 0; PROCESSENTRY32 pe32 = { 0 }; HANDLE hSnapshot = NULL; BOOL bRet = FALSE; RtlZeroMemory(&pe32, sizeof(pe32)); pe32.dwSize = sizeof(pe32); hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (NULL == hSnapshot) { ShowError(L"CreateToolhelp32Snapshot"); return dwProcessId; } bRet = Process32First(hSnapshot, &pe32); while (bRet) { if (0 == lstrcmpi(pe32.szExeFile, pszProcessName)) { dwProcessId = pe32.th32ProcessID; break; } bRet = Process32Next(hSnapshot, &pe32); } return dwProcessId; }
BOOL GetAllThreadIdByProcessId(DWORD dwProcessId, std::vector<DWORD> &vThreadId) { THREADENTRY32 te32 = { 0 }; HANDLE hSnapshot = NULL; BOOL bRet = TRUE; do { RtlZeroMemory(&te32, sizeof(te32)); te32.dwSize = sizeof(te32); hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); if (NULL == hSnapshot) { ShowError(L"CreateToolhelp32Snapshot"); bRet = FALSE; break; } bRet = Thread32First(hSnapshot, &te32); while (bRet) { if (te32.th32OwnerProcessID == dwProcessId) { vThreadId.push_back(te32.th32ThreadID); } bRet = Thread32Next(hSnapshot, &te32); } bRet = TRUE; } while (FALSE); return bRet; }
Codice sorgente:
APC_Injection.zip
Riferimenti:
[1] QueueUserAPC function
[2] Windows Via C/C++ - Richter, Nasarre
[3] Taking a Snapshot and Viewing Processes
[4] Injecting a DLL without a Remote Thread
[5] https://github.com/DemonGan/Windows-Hack-Programming
Nessun commento:
Posta un commento