giovedì 25 aprile 2019

Rilevare Remote Thread Injection in UserMode

Rilevare in usermode la presenza di codice iniettato nel proprio processo non è semplice in quanto esistono diversi modi per iniettare codice ma non esiste un unica soluzione che li rilevi tutti. Ogni metodo di iniezione richiede la sua tecnica di rilevazione che, per quanto ingegnosa, resta circoscritta al singolo caso e spesso aggirabile usando semplicemente un altro metodo di iniezione. Quindi, a meno che uno non investa gran parte del suo tempo ad implementare ed aggiornare la parte del suo programma che riguarda il rilevamento di codice iniettato anziché a sviluppare le feature del programma stesso, resta abbastanza impraticabile come contromisura.
Ci si potrebbe chiedere, a questo punto, che senso abbia scrivere qualcosa su questo argomento. In realtà questo articolo è il pretesto per introdurre un paio di argomenti davvero interessanti: API Hooking e la funzione NtQuerySystemInformation. Inoltre, si è preso in esame il caso forse più utile, e cioè la rilevazione di iniezione che sfrutta l'esecuzione di un thread remoto.
La tecnica presentata in questo articolo si basa sul processo di avvio dei thread: ogni volta che si crea un thread con CreateThread, CreateRemoteThread o funzioni simili, prima di eseguire il codice della funzione passata come argomento viene chiamata la funzione LdrInitializeThunk in NTDLL.DLL (per approfondire l'argomento si veda [2]). Eseguendo l'hook di questa funzione si possono intercettare alcune informazioni interessanti ai fini della rilevazione di codice iniettato tramite creazione di un thread remoto.





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. Il consiglio è quello di partire dall'entrypoint (_tmain) seguendo il flusso naturale delle istruzioni. L'unica cosa che mi sento di aggiungere è che il codice sorgente si basa su un esempio che si trova su GitHub (si veda [1]) e che si può testare con un metodo qualsiasi di iniezione del codice, a patto che faccia uso di un thread remoto per compiere tale operazione. In questo stesso blog sono stati pubblicati diversi articoli su tale argomento e se ne possono sfruttare i codici sorgenti al fine di effettuare tali test.

int _tmain(int argcTCHAR *argv[])
{
 // Codice irrilevante 
 
 InitializeThreadCheck();
 
 // Segue altro codice irrilevante
}

typedef void(*typedef_LdrInitializeThunk)(PCONTEXT NormalContext, PVOID SystemArgument1, PVOID SystemArgument2);
static typedef_LdrInitializeThunk LdrInitializeThunk_ = nullptr;


 
void ShowError(const TCHAR *pszText)
{
 TCHAR szErr[MAX_PATH] = { 0 };
 _stprintf(szErr, "%s Error[%d]\n"pszText, ::GetLastError());
 MessageBox(NULL, szErr, "ERROR"MB_OK);
}


 
 
void InitializeThreadCheck()
{
 HMODULE hNtdll = LoadLibraryA("ntdll.dll");
 if (hNtdll == nullptr)
 {
  ShowError("LoadLibraryA");
  return;
 }
 
 // Ottiene indirizzo di base di ntdll.dll
 HMODULE hDll = ::GetModuleHandle("ntdll.dll");
 if (hDll == nullptr)
 {
  ShowError("GetModuleHandle");
  return;
 }
 
 // Ottiene indirizzo di LdrInitializeThunk originale in modulo ntdll.dll caricato in memoria
 typedef_LdrInitializeThunk LdrInitializeThunk = (typedef_LdrInitializeThunk)GetProcAddress(hDll, "LdrInitializeThunk");
 if (LdrInitializeThunk == nullptr)
 {
  ShowError("GetProcAddress");
  return;
 }
 
  // Installa Hook
  if (HookApi(LdrInitializeThunk, LdrInitializeThunk_t))
   std::cout << std::hex << "LdrInitializeThunk Hooked!" << std::endl;
}

typedef NTSTATUS(WINAPI * lpNtQueryInformationThread)(HANDLELONGPVOIDULONGPULONG);
 



// Per la particolarità di LdrInitializeThunk l'ultima istruzione
// di LdrInitializeThunk_t, e cioè HookApi(), non viene eseguita in quanto dopo
// aver chiamato LdrInitializeThunk originale il thread esegue entrypoint passato 
// come argomento alla funzione che ha creato il thread remoto; 
// in questo modo non si ritorna più al codice di LdrInitializeThunk_t.
// Tolta la particolarità del caso in questione, questo modo di hookare funziona senza problemi in X64.
void LdrInitializeThunk_t(PCONTEXT NormalContextPVOID SystemArgument1PVOID SystemArgument2)
{
 // Ottiene indirizzo di base di ntdll.dll
 HMODULE hDll = ::GetModuleHandle("ntdll.dll");
 if (NULL == hDll)
 {
  return;
 }
 
 // Ottiene indirizzo di LdrInitializeThunk originale in modulo ntdll.dll caricato in memoria
 typedef_LdrInitializeThunk LdrInitializeThunk = (typedef_LdrInitializeThunk)::GetProcAddress(hDll, "LdrInitializeThunk");
 if (LdrInitializeThunk == nullptr)
 {
  return;
 }
 
 // Rimuove hook
 UnHookApi(LdrInitializeThunk);
 
 // Ottiene indirizzo di NtQueryInformationThread in modulo ntdll.dll caricato in memoria
 lpNtQueryInformationThread NtQueryInformationThread = (lpNtQueryInformationThread)GetProcAddress(LoadLibraryA("ntdll"), 
  "NtQueryInformationThread");
 if (NtQueryInformationThread == nullptr)
 {
  ShowError("NtQueryInformationThread");
  return;
 }
 
 // Ottiene indirizzo della funzione eseguita dal thread corrente
 ULONG_PTR dwStartAddress = 0;
 NtQueryInformationThread(
  NtCurrentThread,
  ThreadQuerySetWin32StartAddress, 
  &dwStartAddress,
  sizeof(dwStartAddress),
  NULL);
 
 std::cout << std::hex << "[*] A thread attached to process! Start address: " << (void*)(dwStartAddress) << std::endl;
 
 // Ottiene ID del thread corrente
 DWORD dwThreadId = GetThreadId(NtCurrentThread);
 // Rileva se il thread corrente è sospeso.
 std::cout << "\t* Thread: " << dwThreadId << " - Suspended: " << (IsSuspendedThread(dwThreadId) ? "TRUE" : "FALSE"<< std::endl;
 
 // Segnala se thread corrente esegue funzioni per caricare DLL o nuovi thread
 if (dwStartAddress == (ULONG_PTR)LoadLibraryA)
  std::cout << "# WARNING # dwStartAddress == LoadLibraryA" << std::endl;
 
 else if (dwStartAddress == (ULONG_PTR)LoadLibraryW)
  std::cout << "# WARNING # dwStartAddress == LoadLibraryW" << std::endl;
 
 else if (dwStartAddress == (ULONG_PTR)LoadLibraryExA)
  std::cout << "# WARNING # dwStartAddress == LoadLibraryExA" << std::endl;
 
 else if (dwStartAddress == (ULONG_PTR)LoadLibraryExW)
  std::cout << "# WARNING # dwStartAddress == LoadLibraryExW" << std::endl;
 
 else if (dwStartAddress == (ULONG_PTR)GetProcAddress(LoadLibraryA("ntdll"), "RtlUserThreadStart"))
  std::cout << "# WARNING # dwStartAddress == RtlUserThreadStart" << std::endl;
 
 else if (dwStartAddress == (ULONG_PTR)GetProcAddress(LoadLibraryA("ntdll"), "NtCreateThread"))
  std::cout << "# WARNING # dwStartAddress == NtCreateThread" << std::endl;
 
 else if (dwStartAddress == (ULONG_PTR)GetProcAddress(LoadLibraryA("ntdll"), "NtCreateThreadEx"))
  std::cout << "# WARNING # dwStartAddress == NtCreateThreadEx" << std::endl;
 
 else if (dwStartAddress == (ULONG_PTR)GetProcAddress(LoadLibraryA("ntdll"), "RtlCreateUserThread"))
  std::cout << "# WARNING # dwStartAddress == RtlCreateUserThread" << std::endl;
 
 // Raccoglie info sulla pagina di memoria in cui si trova la funzione che
 // esegue il thread corrente e segnala eventuali stati o caratteristiche anomale
 MEMORY_BASIC_INFORMATION mbi = { 0 };
 if (VirtualQuery((LPCVOID)dwStartAddress, &mbi, sizeof(mbi)))
 {
  // Segnala se la funzione non è in una regione di memoria mappata ad una vista di 
  // una sezione di una immagine eseguibile
  if (mbi.Type != MEM_IMAGE)
   std::cout << "# WARNING # mbi.Type != MEM_IMAGE" << std::endl;
 }
 
 // L'hook di LdrInitializeThunk è stato solo un modo per intercettare l'esecuzione
 // di un nuovo thread e chiamare NtQueryInformationThread ma dobbiamo
 // comunque chiamare la funzione LdrInitializeThunk originale altrimenti i dati 
 // potrebbero trovarsi in uno stato non definito in quanto il chiamante di 
 // LdrInitializeThunk si aspetta che tutto sia al suo posto.
 LdrInitializeThunk(NormalContextSystemArgument1SystemArgument2);
 
 // Questa istruzione non viene eseguita quindi hook non viene più reinstallato.
 HookApi(LdrInitializeThunk, LdrInitializeThunk_t);
}

BYTE g_OldData64[12] = { 0 };



 
BOOL HookApi(LPVOID srcLPVOID dst)
{
 // pData conterrà istruzioni per salto diretto a dst
 // mov rax,0x1122334455667788
 // jmp rax
 // che in bytecode diventa:
 // 48 b8 8877665544332211
 // ff e0
 BYTE pData[12] = { 0x48, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xe0 };
 ULONG_PTR ullOffset = (ULONG_PTR)dst;
 RtlCopyMemory(&pData[2], &ullOffset, sizeof(ullOffset));
 
 // Salva primi 12 byte di src
 RtlCopyMemory(g_OldData64, srcsizeof(pData));
 
 // Imposta pagina di memoria in cui si trova src a scrivibile
 DWORD dwOldProtect = 0;
 VirtualProtect(srcsizeof(pData), PAGE_EXECUTE_READWRITE, &dwOldProtect);
 
 // Scrive istruzioni di salto diretto a dst in src.
 // Copy On Write innescata e da ora il processo corrente avrà la sua copia privata di ntdll.dll
 RtlCopyMemory(src, pData, sizeof(pData));
 
 // Ripristina proprietà originarie della pagina di memoria.
 VirtualProtect(srcsizeof(pData), dwOldProtect, &dwOldProtect);
 
 return TRUE;
}

BOOL UnHookApi(LPVOID src)
{
 // Imposta pagina di memoria in cui si trova src a scrivibile
 DWORD dwOldProtect = 0;
 VirtualProtect(src, 12, PAGE_EXECUTE_READWRITE, &dwOldProtect);
 
 // Scrive 12 byte salvati in precedenza rispristinando istruzioni originarie di src
 RtlCopyMemory(src, g_OldData64, sizeof(g_OldData64));
 
 // Ripristina proprietà originarie della pagina di memoria.
 VirtualProtect(src, 12, dwOldProtect, &dwOldProtect);
 
 return TRUE;
}

static DWORD dwProcessId;
static PBYTE pbQueryInfo;



 
BOOL IsSuspendedThread(DWORD dwThreadId)
{
 dwProcessId = GetCurrentProcessId();
 // Se threadEnumerator è null vuol dire che non è stato possibile
 // allocare spazio sufficiente per buffer da passare a NtQuerySystemInformation. 
 // Non potendo fare ulteriori assunzioni considera il thread corrente come fosse sospeso
 // e la chiude lì.
 pbQueryInfo = InitializeQuery();
 if (pbQueryInfo == nullptr)
  return true;
 
 // Se systemThreadOwnerProcInfo è null vuol dire che non è stato possibile
 // raccogliere info sul processo legato al thread corrente.
 // Non potendo fare ulteriori assunzioni considera il thread corrente come fosse sospeso.
 SYSTEM_PROCESS_INFORMATION *systemThreadOwnerProcInfo = GetProcInfo();
 if (systemThreadOwnerProcInfo == nullptr) 
 {
  free(pbQueryInfo);
  return true;
 }
 
 // Se systemThreadOwnerProcInfo è null vuol dire che non è stato possibile
 // trovare thread corrente nel relativo processo quindi lo considera sospeso.
 // Non potendo fare ulteriori assunzioni considera il thread corrente come fosse sospeso.
 // TOGLI QUANTO SEGUE
 // Le strutture SYSTEM_THREAD_INFORMATION che seguono la relativa struttura
 // SYSTEM_PROCESS_INFORMATION e che sono raccolte da NtQuerySystemInformation
 // sono solo quelle in esecuzione.
 SYSTEM_THREAD_INFORMATION *systemThreadInfo = FindThread(systemThreadOwnerProcInfo, dwThreadId);
 if (systemThreadInfo == nullptr)
 {
  free(pbQueryInfo);
  return true;
 }
 
 // Interroga struttura SYSTEM_THREAD_INFORMATION relativa a thread corrente sul suo stato
 if (systemThreadInfo->ThreadState == Waiting && systemThreadInfo->WaitReason == Suspended) 
 {
  free(pbQueryInfo);
  return true;
 }
 
 free(pbQueryInfo);
 return false;
}

// Raccoglie info su tutti i processi e relativi thread in esecuzione
PBYTE InitializeQuery()
{
 typedef NTSTATUS(NTAPIlpNtQuerySystemInformation)(
  SYSTEM_INFORMATION_CLASS SystemInformationClass,
  PVOID SystemInformation,
  ULONG SystemInformationLength,
  PULONG ReturnLength);
 
 // Ottiene indirizzo di NtQuerySystemInformation in modulo ntdll.dll caricato in memoria
 lpNtQuerySystemInformation NtQuerySystemInformation = (lpNtQuerySystemInformation)GetProcAddress(
  LoadLibraryA("ntdll"), "NtQuerySystemInformation");
 
 PBYTE mp_Data;
 DWORD mu32_DataSize = 1024 * 1024;
 
 while (true)
 {
  mp_Data = (PBYTE)malloc(mu32_DataSize);
  if (!mp_Data)
   break;
 
  // https://docs.microsoft.com/en-us/windows/desktop/api/winternl/nf-winternl-ntquerysysteminformation
  /* Quando si passa SystemProcessInformation come primo parametro, il buffer puntato dal secondo parametro
  (mp_Data in questo caso) conterrà una struttura SYSTEM_PROCESS_INFORMATION per ogni processo.
  Ognuna di queste strutture è immediatamente seguita da una o più strutture SYSTEM_THREAD_INFORMATION
  che forniscono info sui thread appartenenti al processo descritto dalla struttura SYSTEM_PROCESS_INFORMATION
  che li precede. 
  Il buffer puntato dal secondo parametro (sempre mp_Data) deve essere grande abbastanza da contenere 
  un array di tante strutture SYSTEM_PROCESS_INFORMATION e SYSTEM_THREAD_INFORMATION quante sono quelle
  in esecuzione nel sistema al momento della chiamata.*/
  ULONG ntNeeded = 0;
  auto ntStat = NtQuerySystemInformation(SystemProcessInformation, mp_Data, mu32_DataSize, &ntNeeded);
 
  if (ntStat == STATUS_INFO_LENGTH_MISMATCH)
  {
   mu32_DataSize *= 2;
   mp_Data = (PBYTE)realloc((PVOID)mp_Data, mu32_DataSize);
   continue;
  }
 
  // Ritorna puntatore a buffer con info su processi e relativi thread
  return mp_Data;
 }
 
 return nullptr;
}

PSYSTEM_PROCESS_INFORMATION GetProcInfo()
{
 // Dopo chiamata a InitializeQuery(), pbQueryInfo contiene indirizzo di buffer
 // con info su processi e relativi thread.
 PSYSTEM_PROCESS_INFORMATION pk_Proc = (SYSTEM_PROCESS_INFORMATION*)pbQueryInfo;
 
 while (true)
 {
  // Se PID è quello che ci interessa, ritorna puntatore a struttura SYSTEM_PROCESS_INFORMATION
  if ((DWORD)pk_Proc->UniqueProcessId == dwProcessId)
   return pk_Proc;
 
  if (!pk_Proc->NextEntryOffset)
   return nullptr;
 
  // Passa a SYSTEM_PROCESS_INFORMATION successivo; non usa aritmetica dei puntatori
  // perchè dopo SYSTEM_PROCESS_INFORMATION seguono un numero variabile di strutture
  // SYSTEM_THREAD_INFORMATION.
  pk_Proc = (SYSTEM_PROCESS_INFORMATION*)((PBYTE)pk_Proc + pk_Proc->NextEntryOffset);
 }
 
 return nullptr;
}

PSYSTEM_THREAD_INFORMATION FindThread(PSYSTEM_PROCESS_INFORMATION procInfoDWORD dwThreadId)
{
 // Dopo chiamata a GetProcInfo(), procInfo contiene indirizzo di SYSTEM_PROCESS_INFORMATION
 // con info sul processo e relativi thread.
 SYSTEM_THREAD_INFORMATION *pk_Thread = procInfo->Threads;
 if (!pk_Thread)
  return nullptr;
 
 for (DWORD i = 0; i < procInfo->NumberOfThreads; i++)
 {
  // Se TID è quello che ci interessa, ritorna puntatore a struttura SYSTEM_PROCESS_INFORMATION
  if ((DWORD)pk_Thread->ClientId.UniqueThread == dwThreadId)
   return pk_Thread;
 
  pk_Thread++;
 }
 
 return nullptr;
}



Codice sorgente:
RemoteThreadInjection_Detector.zip




Riferimenti:
[1] https://github.com/mq1n/DLLThreadInjectionDetector
[1] http://www.nynaeve.net/?p=205

Nessun commento:

Posta un commento