Per quanto riguarda lo stile scelto per l'esposizione della tecnica presentata in questo articolo, ho deciso di scrivere tutte le info sotto forma di commenti al codice. Per non appesantire troppo il discorso, inoltre, si prenderà in considerazione e si spiegherà in dettaglio solo la parte relativa all'articolo. Tutto quanto necessario alla comprensione del resto del sorgente si può trovare in un articolo precedente linkato nei riferimenti sotto (si veda [2] e si faccia riferimento ad esso se si hanno dubbi). Il consiglio, per leggere il codice di questo articolo, è quello di partire dall'entrypoint (wmain) seguendo il flusso naturale delle istruzioni. L'unica cosa importante da aggiungere è che questo codice sorgente si basa su un tutorial presente su GuidedHacking (si veda [3]).
Di seguito il codice dell'iniettore:
int wmain(int argc, wchar_t * argv[]) { // Percorso DLL WCHAR szDllFile[MAX_PATH] = { 0 }; GetAppPath(szDllFile); std::wcscat(szDllFile, L"SampleDLL.dll"); InjectDll(L"Target.exe", szDllFile); std::cout << "Press enter to exit" << std::endl; std::cin.get(); return 0; }
void GetAppPath(LPWSTR szCurFile) { GetModuleFileName(0, szCurFile, MAX_PATH); for (SIZE_T i = wcslen(szCurFile) - 1; i >= 0; i--) { if (szCurFile[i] == '\\') { szCurFile[i + 1] = '\0'; break; } } }
bool InjectDll(LPCWSTR szProcess, LPCWSTR szPath) { HANDLE hProc = GetProcessHandleByProcessName(szProcess); if (!hProc) { ShowError(L"GetProcessHandleByProcessName"); return false; } SIZE_T len = wcslen(szPath) * sizeof(WCHAR); LPVOID pArg = VirtualAllocEx(hProc, nullptr, len, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (!pArg) { ShowError(L"VirtualAllocEx"); CloseHandle(hProc); return false; } BOOL bRet = WriteProcessMemory(hProc, pArg, szPath, len, nullptr); if (!bRet) { ShowError(L"WriteProcessMemory"); VirtualFreeEx(hProc, pArg, 0, MEM_RELEASE); CloseHandle(hProc); return false; } pLoadLibraryW p_LoadLibrary = LoadLibraryW; if (!p_LoadLibrary) { ShowError(L"GetProcAddressEx"); VirtualFreeEx(hProc, pArg, 0, MEM_RELEASE); CloseHandle(hProc); return false; } UINT_PTR hDllOut = 0; DWORD last_error = 0; DWORD dwErr = HijackThread(hProc, p_LoadLibrary, pArg, last_error, hDllOut); CloseHandle(hProc); if (dwErr) { std::cout << std::hex << "StartRoutine failed: 0x" << dwErr << std::endl; std::cout << std::hex << " LastWin32Error: 0x" << last_error << std::endl; return false; } std::cout << std::hex << "Success! LoadLibrary returned 0x" << hDllOut << std::endl; return true; }
HANDLE GetProcessHandleByProcessName(LPCWSTR 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 (hSnapshot == NULL) { ShowError(L"CreateToolhelp32Snapshot"); } bRet = Process32First(hSnapshot, &pe32); while (bRet) { if (0 == lstrcmpi(pe32.szExeFile, pszProcessName)) { dwProcessId = pe32.th32ProcessID; break; } bRet = Process32Next(hSnapshot, &pe32); } return OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId); }
DWORD HijackThread(HANDLE hTargetProc, pLoadLibraryW pRoutine, LPVOID pArg, DWORD &LastWin32Error, UINT_PTR &HMDll) { // Ottiene thread ID eseguito in Target THREADENTRY32 TE32{ 0 }; TE32.dwSize = sizeof(TE32); HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, GetProcessId(hTargetProc)); if (hSnap == INVALID_HANDLE_VALUE) { LastWin32Error = GetLastError(); return SR_HT_ERR_TH32_FAIL; } DWORD dwTargetPID = GetProcessId(hTargetProc); DWORD ThreadID = 0; BOOL bRet = Thread32First(hSnap, &TE32); if (!bRet) { LastWin32Error = GetLastError(); CloseHandle(hSnap); return SR_HT_ERR_T32FIRST_FAIL; } do { if (TE32.th32OwnerProcessID == dwTargetPID) { ThreadID = TE32.th32ThreadID; break; } bRet = Thread32Next(hSnap, &TE32); } while (bRet); if (!ThreadID) { return SR_HT_ERR_NO_THREADS; } // Ottiene handle al thread eseguito in Target HANDLE hThread = OpenThread(THREAD_SET_CONTEXT | THREAD_GET_CONTEXT | THREAD_SUSPEND_RESUME, FALSE, ThreadID); if (!hThread) { LastWin32Error = GetLastError(); return SR_HT_ERR_OPEN_THREAD_FAIL; } // Sospende il thread eseguito in Target if (SuspendThread(hThread) == (DWORD)-1) { LastWin32Error = GetLastError(); CloseHandle(hThread); return SR_HT_ERR_SUSPEND_FAIL; } // Ottiene il contesto del thread eseguito, e ora sospeso, in Target. // Dentro il contesto ci sono info utili tra cui l'istrunction pointer (registro RIP) // che ci serve sia per saltare allo shellcode che per // riprendere l'esecuzione da dove era stata sospesa. CONTEXT OldContext{ 0 }; OldContext.ContextFlags = CONTEXT_CONTROL; if (!GetThreadContext(hThread, &OldContext)) { LastWin32Error = GetLastError(); ResumeThread(hThread); CloseHandle(hThread); return SR_HT_ERR_GET_CONTEXT_FAIL; } // Alloca spazio per lo shellcode in Target LPVOID pCodecave = VirtualAllocEx(hTargetProc, nullptr, 0x100, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (!pCodecave) { LastWin32Error = GetLastError(); ResumeThread(hThread); CloseHandle(hThread); return SR_HT_ERR_CANT_ALLOC_MEM; } // SPIEGAZIONE SHELLCODE: // Primi 8 byte (returned value) servirà dopo per conservare HMODULE (indirizzo di base) della DLL. // Prima vera istruzione da eseguire è: // sub rsp, 0x08 alloca spazio sullo stack e servirà per salvare il valore di ritorno // quando il codice dello Shellcode verrà completato. Con il ret finale, infatti, tale valore // andrà automaticamente in RIP dato che sarà il valore puntato da RSP in quel momento. // A tale scopo, i due mov successivi salvano valore di RIP, al momento della sospensione del thread, // proprio nello spazio allocato sullo stack in precedenza da sub rsp, 0x08. // I due push successivi salvano alcuni registri (pushfq salva registro RFLAGS) che verranno // usati dallo shellcode e che devono essere preservati in quanto potrebbe farne uso anche il // il codice in esecuzione al momento della sospensione del thread. // mov rax, pRoutine mette indirizzo di LoadLibrary in RAX. // mov rcx, pArg mette indirizzo di percorso della DLL in RCX. // sub rsp, 0x20 perchè stack deve essere allineato a 16 byte prima di ogni chiamata. // La prima istruzione (sub rsp, 0x08) ha disallineato lo stack di 8 byte. // Aggiungendo 0x20 = 32 siamo a 40 = 0x28. Ricordando che la call implica un push // del valore di ritorno sullo stack siamo a 48 = 0x30 che è proprio allineato a 16. // call rax chiama LoadLibrary passando il percorso della DLL da caricare // lea rcx, [pCodecave] mette indirizzo Shellcode in RCX: // 0x48, 0x8D, 0x0D, 0xB4, 0xFF, 0xFF, 0xFF, significa: lea rcx, [rip+0xFFFFFFB4] // che vuol dire: lea rcx, [rip-76] che vuol dire: lea rcx, [rip-0x4C] con RIP che al // momento di questa istruzione è (Shellcode + 0x44), che sommati agli 0x8 byte prima della // prima istruzione fanno 0x4C e quindi rip-0x4C è indirizzo di Shellcode. // mov [rcx], rax mette l'handle del modulo caricato nei primi 8 byte di shellcode (vedi returned value). // mov byte ptr[$ - 0x57], 0 mette zero in byte [$ - 0x57] per indicare che Shellcode ha finito; // $ indica un RIP-relative addressing e al momento dell'istruzione RIP è a uguale a (Shellcode + 0x8 + 0x5A) // sottraendo 0x57 sia ha RIP - 0x57 = Shellcode + 0x8 + 0x5A - 0x57 = Shellcode + 0x8 + 0x3 // che è l'ultimo byte dell'istruzione sub rsp, 0x08; tanto non serve più, quel che doveva fare (allocare 8 byte // su stack) l'ha già fatto. // Come già accennato, al momento del ret finale RSP punta allo spazio allocato sullo stack da // sub rsp, 0x08 e dove ora c'è il RIP originale del thread prima che avenisse la sospensione e // che così considerato il valore di ritorno di shellcode. In questo modo il thread riprende da dove //era stato sospeso. BYTE Shellcode[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // - 0x08 -> returned value 0x48, 0x83, 0xEC, 0x08, // + 0x00 -> sub rsp, 0x08 0xC7, 0x04, 0x24, 0x00, 0x00, 0x00, 0x00, // + 0x04 (+ 0x07) -> mov [rsp], RipLowPart 0xC7, 0x44, 0x24, 0x04, 0x00, 0x00, 0x00, 0x00, // + 0x0B (+ 0x0F) -> mov [rsp + 0x04], RipHighPart 0x50, 0x51, 0x52, 0x41, 0x50, 0x41, 0x51, 0x41, 0x52, 0x41, 0x53, // + 0x13 -> push r(a/c/d)x / r(8 - 11) 0x9C, // + 0x1E -> pushfq 0x48, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x1F (+ 0x21) -> mov rax, pRoutine 0x48, 0xB9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x29 (+ 0x2B) -> mov rcx, pArg 0x48, 0x83, 0xEC, 0x20, // + 0x33 -> sub rsp, 0x20 0xFF, 0xD0, // + 0x37 -> call rax 0x48, 0x83, 0xC4, 0x20, // + 0x39 -> add rsp, 0x20 0x48, 0x8D, 0x0D, 0xB4, 0xFF, 0xFF, 0xFF, // + 0x3D -> lea rcx, [pCodecave] 0x48, 0x89, 0x01, // + 0x44 -> mov [rcx], rax 0x9D, // + 0x47 -> popfq 0x41, 0x5B, 0x41, 0x5A, 0x41, 0x59, 0x41, 0x58, 0x5A, 0x59, 0x58, // + 0x48 -> pop r(11-8) / r(d/c/a)x 0xC6, 0x05, 0xA9, 0xFF, 0xFF, 0xFF, 0x00, // + 0x53 -> mov byte ptr[$ - 0x57], 0 0xC3 // + 0x5A -> ret }; // SIZE = 0x5B (+ 0x08) // Offset da shellcode a inizio codice da eseguire DWORD FuncOffset = 0x08; // Offset da shellcode a byte da settare in mov byte ptr[$ - 0x57], 0 DWORD CheckByteOffset = 0x03 + FuncOffset; // Salva RIP da constesto del thread in shellcode. // In questo modo, quando l'esecuzione dello shellcode terminerà, il thread potrà riprendere // da dove era stato sospeso. DWORD dwLoRIP = (DWORD)(OldContext.Rip & 0xFFFFFFFF); DWORD dwHiRIP = (DWORD((OldContext.Rip) >> 0x20) & 0xFFFFFFFF); *(PDWORD)(Shellcode + 0x07 + FuncOffset) = dwLoRIP; *(PDWORD)(Shellcode + 0x0F + FuncOffset) = dwHiRIP; // Salva indirizzi di LoadLibraryW e percorso DLL in shellcode *(PULONG_PTR)(Shellcode + 0x21 + FuncOffset) = (ULONG_PTR)pRoutine; *(PULONG_PTR)(Shellcode + 0x2B + FuncOffset) = (ULONG_PTR)pArg; // Imposta RIP di contesto del thread con prima istruzione di shellcode in modo da // alterare il flusso di esecuzione. OldContext.Rip = (UINT_PTR)(pCodecave) + FuncOffset; // Scrive shellcode in spazio allocato in precedenza in Target if (!WriteProcessMemory(hTargetProc, pCodecave, Shellcode, sizeof(Shellcode), nullptr)) { LastWin32Error = GetLastError(); ResumeThread(hThread); CloseHandle(hThread); VirtualFreeEx(hTargetProc, pCodecave, 0, MEM_RELEASE); return SR_HT_ERR_WPM_FAIL; } // Imposta contesto alerato if (!SetThreadContext(hThread, &OldContext)) { LastWin32Error = GetLastError(); ResumeThread(hThread); CloseHandle(hThread); VirtualFreeEx(hTargetProc, pCodecave, 0, MEM_RELEASE); return SR_HT_ERR_SET_CONTEXT_FAIL; } // Riprende thread sospeso if (ResumeThread(hThread) == (DWORD)-1) { LastWin32Error = GetLastError(); CloseHandle(hThread); VirtualFreeEx(hTargetProc, pCodecave, 0, MEM_RELEASE); return SR_HT_ERR_RESUME_FAIL; } CloseHandle(hThread); // Aspetta per 5 secondi controllando che il byte dello shellcode venga azzerato. DWORD Timer = GetTickCount(); BYTE CheckByte = 1; do { ReadProcessMemory(hTargetProc, (LPBYTE)(pCodecave) + CheckByteOffset, &CheckByte, 1, nullptr); if (GetTickCount() - Timer > SR_REMOTE_TIMEOUT) { return SR_HT_ERR_TIMEOUT; } Sleep(10); } while (CheckByte != 0); // Passa HMODULE di DLL a paramentro di output HMDll ReadProcessMemory(hTargetProc, pCodecave, &HMDll, sizeof(HMDll), nullptr); VirtualFreeEx(hTargetProc, pCodecave, 0, MEM_RELEASE); return SR_ERR_SUCCUESS; }
Come si può notare, in Target è presente la DLL caricata dallo shellcode.
Codice sorgente:
DLL_Injector_Shellcode.zip
Riferimenti:
[1] Mappare DLL manualmente in un altro processo
[2] Caricare DLL in memoria manualmente
[3] https://guidedhacking.com/threads/c-shellcode-injection-tutorial.12132/
Nessun commento:
Posta un commento