venerdì 30 agosto 2019

13 - Kernel API: Altre funzioni (Processi, Thread e DLL)

In questa lezione di stampo prettamente pratico si uscirà leggermente dal seminato per fare luce sul lato oscuro della Kernel API. L'esempio di codice proverà ad elencare tutti i processi in esecuzione sul sistema, i relativi thread e le DLL caricate per tale processo. In realtà esistono alcune funzioni dell'API ben documentate in grado di svolgere questi compiti in modo relativamente semplice. Ad esempio ZwQuerySystemInformation funziona in modo simile alla funzione ZwQueryDirectoryFile vista in una precedente lezione. Il problema è che questo metodo esclude i processi nascosti (circostanza spesso non desiderabile quando si è in kernel mode) e le info recuperabili sono davvero limitate. E' necessario quindi usare un'altra tecnica. L'idea è quella di non andarci troppo per il sottile e recuperare le informazioni direttamente dalle strutture in cui si trovano. L'inconveniente di questa tecnica è che prevede l'utilizzo di funzioni non documentate ma che sono comunque esportate da Ntoskrnl.exe ed esposte in Ntoskrnl.lib quindi è sufficiente dichiararle esplicitamente per usarle. Il codice presentato in questa lezione è tratto da [2] e, almeno per questa volta, si eviterà di esaminare ogni singola funzione in dettaglio ma, piuttosto, verranno forniti abbondanti commenti a supporto di quasi ogni riga di codice in modo che tutto sia relativamente comprensibile. Per una descrizione dettagliata riguardo molte delle funzioni non documentate della Kernel API si veda [3].




Per quanto riguarda i processi la struttura sicuramente più importante è EPROCESS che contiene tutte le informazioni rilevanti di un processo (alcune di queste informazioni verranno esaminate in seguito). Esiste esattamente una istanza di EPROCESS per ogni processo in esecuzione. Anche solo un puntatore a questa struttura è sufficiente per fare una lista dei processi e dei relativi thread nel sistema. Per recuperarla si utilizzerà un metodo brute-force iterando tra tutti i probabili Process ID (PID). In modo simile agli HANDLE (ed infatti è definiti come tali), un PID è un valore numerico nell'intervallo $[4, MAX\_INT]$ che cresce in modo progressivo a multipli di 4. Naturalmente è sconsigliato iterare su tutti i possibili valori in quanto sarebbe impegnativo dal punto di vista delle performance ed, inoltre, un numero così elevato di processi risulta essere alquanto improbabile sui normali sistemi utilizzati dagli utenti.

VOID EnumProcess()
{
 ULONG i = 0;
 PEPROCESS eproc = NULL;
 
 // 262144 è 2^18, un numero più che sufficiente per qualsiasi PID
 for (i = 4; i < 262144; i = i + 4)
 {
  // Ritorna puntatore a EPROCESS di processo con PID = i
  eproc = LookupProcess((HANDLE)i);
  if (eproc != NULL)
  {
   // LookupProcess crea (attraverso PsLookupProcessByProcessId) un nuovo riferimento ad un EPROCESS che 
   // quindi deve essere deferenziato dal chiamante di LookupProcess per decrementare il numero di riferimenti a tale EPROCESS.
   // (Vedi LookupProcess più avanti)
   ObDereferenceObject(eproc);
   DbgPrint("EPROCESS=%p PID=%ld PPID=%ld Name=%s\n",
    eproc,
    HandleToULong(PsGetProcessId(eproc)), // recupera PID da EPROCESS
    HandleToULong(PsGetProcessInheritedFromUniqueProcessId(eproc)), // recupera PID di processo genitore da EPROCESS
    PsGetProcessImageFileName(eproc)); // recupera nome (stringa ANSI) del file immagine da EPROCESS
 
     // Queste due funzioni recuperano ed elencano i thread ed i moduli relativi al processo corrente e 
     // verranno esaminate in seguito
   EnumThread(eproc);
   EnumModule(eproc);
   DbgPrint("\n");
  }
 }
}

PEPROCESS LookupProcess(HANDLE Pid)
{
 PEPROCESS eprocess = NULL;
 
 // PsLookupProcessByProcessId ritorna indirizzo di EPROCESS (relativo a processo con ID Pid) in secondo parametro
 // incrementandone quindi il numero di riferimenti
 if (NT_SUCCESS(PsLookupProcessByProcessId(Pid, &eprocess)))
  return eprocess;
 else
  return NULL;
}

Per quanto riguarda i thread il discorso è simile a quello fatto per i processi ma al posto della struttura EPROCESS c'è la struttura ETHREAD che descrive ogni thread in esecuzione o sospeso nel sistema.

VOID EnumThread(PEPROCESS Process)
{
 ULONG i = 0;
 PETHREAD ethrd = NULL;
 PEPROCESS eproc = NULL;
 
 // 262144 è 2^18, un numero più che sufficiente per qualsiasi TID
 for (i = 4; i < 262144; i = i + 4)
 {
  // Ritorna puntatore a ETHREAD di threadcon TID = i
  // (Vedi LookupThread più avanti)
  ethrd = LookupThread((HANDLE)i);
  if (ethrd != NULL)
  {
   // IoThreadToProcess ritorna indirizzo di EPROCESS recuperandolo da ETHREAD
   // senza incrementare il numero di riferimenti
   eproc = IoThreadToProcess(ethrd);
   if (eproc == Process)
   {
    DbgPrint("[THREAD]ETHREAD=%p TID=%ld\n",
     ethrd,
     HandleToULong(PsGetThreadId(ethrd))); // recupera TID da ETHREAD
   }
 
   // Decrementa riferimenti a ETHREAD
   ObDereferenceObject(ethrd);
  }
 }
}

PETHREAD LookupThread(HANDLE Tid)
{
 PETHREAD ethread;
 
 // PsLookupThreadByThreadId ritorna indirizzo di ETHREAD (relativo a processo con ID Pid) in secondo parametro
 // incrementandone quindi il numero di riferimenti
 if (NT_SUCCESS(PsLookupThreadByThreadId(Tid, &(ethread))))
  return ethread;
 else
  return NULL;
}

Per quanto riguarda i moduli DLL di un processo la questione è un po' più complessa. Le informazioni su ogni DLL caricata per un dato processo sono conservate in una struttura di tipo LDR_DATA_TABLE_ENTRY che altro non è che un elemento in una lista doppiamente concatenata che si trova nella struttura PEB_LDR_DATA. Attraversando tale lista è quindi possibile recuperare tutte le info sulle DLL caricate per un processo. PEB_LDR_DATA si trova nella struttura PEB (Process Environment Block). La cosa importante da sapere riguardo queste strutture è che sono recuperabili da EPROCESS ma sono concepite e definite per essere usate in user mode e che si trovano in user space. Se si vuole usarle in kernel mode bisogna prima definirle esplicitamente ed in seguito accedervi direttamente, senza fare troppo affidamento sull'API.

/*
// Non rilevante per questa lezione ma, chi vuole, può vedere le strutture EPROCESS e ETHREAD con:
> dt nt!_eprocess
> dt nt!_ethread
 
// Come afferma la documentazione, chi ha a che fare con queste strutture non completamente documentate
// deve sapere che possono cambiare in qualsiasi momento quindi meglio verificare di persona ed
// usare definizioni ad-hoc se necessario.
// Questi sono i miei risultati su Win10 x64 1809
> dt _PEB
nt!_PEB
   +0x000 InheritedAddressSpace : UChar
   +0x001 ReadImageFileExecOptions : UChar
   +0x002 BeingDebugged    : UChar
   +0x003 BitField         : UChar
   +0x003 ImageUsesLargePages : Pos 0, 1 Bit
   +0x003 IsProtectedProcess : Pos 1, 1 Bit
   +0x003 IsImageDynamicallyRelocated : Pos 2, 1 Bit
   +0x003 SkipPatchingUser32Forwarders : Pos 3, 1 Bit
   +0x003 IsPackagedProcess : Pos 4, 1 Bit
   +0x003 IsAppContainer   : Pos 5, 1 Bit
   +0x003 IsProtectedProcessLight : Pos 6, 1 Bit
   +0x003 IsLongPathAwareProcess : Pos 7, 1 Bit
   +0x004 Padding0         : [4] UChar
   +0x008 Mutant           : Ptr64 Void
   +0x010 ImageBaseAddress : Ptr64 Void
   // Ldr è un puntatore ad una struttura PEB_LDR_DATA che contiene informazioni sui moduli caricati in memoria per il processo.
   +0x018 Ldr              : Ptr64 _PEB_LDR_DATA
   +0x020 ProcessParameters : Ptr64 _RTL_USER_PROCESS_PARAMETERS
   +0x028 SubSystemData    : Ptr64 Void
   + Tanti altri campi interessanti tagliati per risparmiare spazio
 
 
> dt _PEB_LDR_DATA
nt!_PEB_LDR_DATA
   +0x000 Length           : Uint4B
   +0x004 Initialized      : UChar
   +0x008 SsHandle         : Ptr64 Void
   // InLoadOrderModuleList è la Head di una lista doppiamente collegata che contiene info sui moduli caricati per il processo.
   // Si veda [1] nei riferimenti della lezione per vedere come sono implementate tali liste.
   // Ogni elemento della lista è una istanza della struttura LDR_DATA_TABLE_ENTRY.
   +0x010 InLoadOrderModuleList : _LIST_ENTRY
   +0x020 InMemoryOrderModuleList : _LIST_ENTRY
   +0x030 InInitializationOrderModuleList : _LIST_ENTRY
   +0x040 EntryInProgress  : Ptr64 Void
   +0x048 ShutdownInProgress : UChar
   +0x050 ShutdownThreadId : Ptr64 Void
 
 
> dt _LDR_DATA_TABLE_ENTRY
nt!_LDR_DATA_TABLE_ENTRY
   +0x000 InLoadOrderLinks : _LIST_ENTRY
   +0x010 InMemoryOrderLinks : _LIST_ENTRY
   +0x020 InInitializationOrderLinks : _LIST_ENTRY
   +0x030 DllBase          : Ptr64 Void
   +0x038 EntryPoint       : Ptr64 Void
   +0x040 SizeOfImage      : Uint4B
   +0x048 FullDllName      : _UNICODE_STRING
   +0x058 BaseDllName      : _UNICODE_STRING
   +0x068 FlagGroup        : [4] UChar
   +0x068 Flags            : Uint4B
   +0x068 PackagedBinary   : Pos 0, 1 Bit
   +0x068 MarkedForRemoval : Pos 1, 1 Bit
   +0x068 ImageDll         : Pos 2, 1 Bit
   + Tanti altri campi interessanti tagliati per risparmiare spazio
 
 
 > dt nt!_KAPC_STATE
 nt!_KAPC_STATE
   +0x000 ApcListHead      : [2] _LIST_ENTRY
   +0x020 Process          : Ptr64 _KPROCESS
   +0x028 KernelApcInProgress : UChar
   +0x029 KernelApcPending : UChar
   +0x02a UserApcPending   : UChar
 
 
 
// Si veda [1] nei riferimenti della lezione per vedere come sono implementate le liste.
typedef struct _LIST_ENTRY {
 struct _LIST_ENTRY *Flink;
 struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;
*/
 
// Usata da KeStackAttachProcess.
// Non documentata e non esposta nei file header canonici quindi bisogna definirla esplicitamente.
typedef struct _KAPC_STATE
{
 LIST_ENTRY ApcListHead[2];
 PKPROCESS Process;
 UCHAR KernelApcInProgress;
 UCHAR KernelApcPending;
 UCHAR UserApcPending;
} KAPC_STATE, *PKAPC_STATE;
 
// Di PEB e PEB_LDR_DATA usiamo solo gli indirizzi di partenza per calcolare, a sua volta,
// gli indirizzi dei campi che ci interessano quindi non è necessario definire queste
// due strutture. E' necessario farlo invece per LDR_DATA_TABLE_ENTRY dato che verranno 
// letti i suoi campi ma non si è obbligati ad includere tutti i campi contenuti in esso
// poichè non si userà l'aritmetica dei puntatori per passare da una struttura all'altra ma
// si sfrutterà il fatto che tali strutture sono elementi di una lista doppiamente collegata
// che può essere attraversata tramite il campo LIST_ENTRY contenuto in ogni suo elemento.
typedef struct _LDR_DATA_TABLE_ENTRY
{
 LIST_ENTRY InLoadOrderLinks;
 LIST_ENTRY InMemoryOrderLinks;
 LIST_ENTRY InInitializationOrderLinks;
 PVOID   DllBase;
 PVOID   EntryPoint;
 ULONG   SizeOfImage;
 UNICODE_STRING FullDllName;
 UNICODE_STRING  BaseDllName;
 ULONG   Flags;
 USHORT   LoadCount;
 USHORT   TlsIndex;
 PVOID   SectionPointer;
 ULONG   CheckSum;
 PVOID   LoadedImports;
 PVOID   EntryPointActivationContext;
 PVOID   PatchInformation;
 LIST_ENTRY ForwarderLinks;
 LIST_ENTRY ServiceTagLinks;
 LIST_ENTRY StaticLinks;
 PVOID   ContextInformation;
 ULONG   OriginalBase;
 LARGE_INTEGER LoadTime;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
 
// fixare gli offset se necessario (controllare le definizioni con un kernel debugger)
ULONG LdrInPebOffset = 0x018;  // Offset di PEB.Ldr a partire da PEB
ULONG ModListInPebOffset = 0x010; // Offset di PEB.Ldr.InLoadOrderModuleList a partire da PEB_LDR_DATA

VOID EnumModule(PEPROCESS Process)
{
 SIZE_T Peb = 0;
 SIZE_T Ldr = 0;
 PLIST_ENTRY ModListHead = 0;
 PLIST_ENTRY Module = 0;
 KAPC_STATE ks;
 
 // MmIsAddressValid controlla che deferenziare un puntatore non causi un page fault (l'indirizzo virtuale 
 // puntato deve quindi riferirsi a memoria nonPaged).
 // Verifica quindi (in modo non del tutto sicuro) che l'EPROCESS sia ancora lì e non
 // sia stato eliminato e sostituito con qualcos'altro nel frattempo.
 if (!MmIsAddressValid(Process))
  return;
 
 // PsGetProcessPeb ritorna indirizzo di PEB da EPROCESS e quindi del suo primo campo: per recuperare LDR si
 // può considerare tale indirizzo come un intero e aggiungerci l'offsett desiderato (vedi sotto)
 // Nota: Anche se vi si accede tramite EPROCESS (che è una struttura in kernel space), PEB è una struttura 
 // che serve a codice user-mode e che si trova quindi in user space.
 Peb = (SIZE_T)PsGetProcessPeb(Process);
 if (!Peb)
  return;
 
 // KeStackAttachProcess aggancia il thread corrente allo spazio di indirizzamento virtuale del processo
 // indicato dal primo parametro. 
 // Se il thread corrente era già agganciato ad un altro processo il secondo paramatro riceve 
 // l'APC state corrente prima che KeStackAttachProcess agganci il thread al nuovo processo. 
 KeStackAttachProcess(Process, &ks);
 
 __try
 {
  // indirizzo PEB + offset di LDR in PEB = indirizzo del campo Ldr di PEB che, a sua volta,
  // contiene l'indirizzo ad una struttura PEB_LDR_DATA (infatti Ldr è un PPEB_LDR_DATA: puntatore a PEB_LDR_DATA).
  Ldr = Peb + (SIZE_T)LdrInPebOffset;
 
  // ProbeForRead controlla che il buffer passato come argomento risieda effettivamente nella 
  // porzione user space dello spazio di indirizzamento virtuale, che sia leggibile e correttamente allineato.
  // Se l'esito del controllo è negativo ProbeForRead lancia una eccezione quindi è consigliato
  // usarlo all'interno di un blocco __try/__except
  // Controllo necessario poiché, come detto, stiamo accedendo a dati in user space ma
  // il client potrebbe passarci un indirizzo non valido che risiede in kernel space.
  ProbeForRead((CONST PVOID)Ldr, 8, 8);
 
  // dereferenzia Ldr per ottenere indirizzo di PEB_LDR_DATA ed aggiunge offset per arrivare a indirizzo di
  // InLoadOrderModuleList in PEB_LDR_DATA (la Head della lista)
  ModListHead = (PLIST_ENTRY)(*(PULONG)Ldr + ModListInPebOffset);
 
  // Interessa solo LIST_ENTRY.FLink (primi 8 byte di LIST_ENTRY)
  ProbeForRead((CONST PVOID)ModListHead, 8, 8);
 
  // Nota che primo campo di LDR_DATA_TABLE_ENTRY è InLoadOrderLinks che è un LIST_ENTRY
  // quindi usare variabile Module come PLIST_ENTRY permette di passare a LDR_DATA_TABLE_ENTRY successivo
  // mentre usato come PLDR_DATA_TABLE_ENTRY permette di accedere ai suoi campi.
  // Si veda [1] nei riferimenti della lezione per vedere come sono implementate le liste. 
  Module = ModListHead->Flink;
 
  // Itera finché non si ritorna alla Head della lista.
  while (ModListHead != Module)
  {
   DbgPrint("[MODULE]Base=%p Size=%ld Path=%wZ\n",
    (PVOID)(((PLDR_DATA_TABLE_ENTRY)Module)->DllBase),
    (ULONG)(((PLDR_DATA_TABLE_ENTRY)Module)->SizeOfImage),
    &(((PLDR_DATA_TABLE_ENTRY)Module)->FullDllName));
 
   // Passa a prossimo LDR_DATA_TABLE_ENTRY
   Module = Module->Flink;
 
   // Anche LDR_DATA_TABLE_ENTRY è in user space.
   // Il campo con offset più distante che usiamo qui è FullDllName che inizia
   // a +0x48 (72 in decimale) ed essendo un puntatore arriva fino a +0x50 (80 in decimale)
   ProbeForRead((CONST PVOID)Module, 80, 8);
  }
 }
 __except (EXCEPTION_EXECUTE_HANDLER)
 {
  DbgPrint("[EnumModule]__except (EXCEPTION_EXECUTE_HANDLER)");
 }
 
 // Riaggancia il thread corrente allo spazio di indirizzamento virtuale originario.
 KeUnstackDetachProcess(&ks);
}

Infine, l'entrypoint ed il codice a corredo.

#include <ntddk.h>
 
// Alcune funzioni nelle dll di sistema a volte, benché esportate, non sono né documentate
// né incluse negli header canonici di MS quindi devono essere dichiarate esplicitamente per importarle ed usarle
NTKERNELAPI UCHAR* PsGetProcessImageFileName(IN PEPROCESS Process);
NTKERNELAPI HANDLE PsGetProcessInheritedFromUniqueProcessId(IN PEPROCESS Process);
NTKERNELAPI PPEB PsGetProcessPeb(PEPROCESS Process);
NTKERNELAPI NTSTATUS PsLookupProcessByProcessId(HANDLE IdPEPROCESS *Process);
NTKERNELAPI NTSTATUS PsLookupThreadByThreadId(HANDLE IdPETHREAD *Thread);
NTKERNELAPI PEPROCESS IoThreadToProcess(PETHREAD Thread);
NTKERNELAPI VOID NTAPI KeAttachProcess(PEPROCESS Process);
NTKERNELAPI VOID NTAPI KeDetachProcess();
NTKERNELAPI VOID NTAPI KeStackAttachProcess(PEPROCESS ProcessPKAPC_STATE ApcState);
NTKERNELAPI VOID NTAPI KeUnstackDetachProcess(PKAPC_STATE ApcState);
 
VOID DriverUnload(IN PDRIVER_OBJECT DriverObject)
{
 UNREFERENCED_PARAMETER(DriverObject);
 DbgPrint("[MyDriver]Unloaded...\n");
 return;
}
 
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObjectIN PUNICODE_STRING RegistryPath)
{
 UNREFERENCED_PARAMETER(RegistryPath);
 
 DriverObject->DriverUnload = DriverUnload;
 DbgPrint("[MyDriver]Loaded...\n");
 
 EnumProcess();
 return STATUS_SUCCESS;
}




Codice sorgente:
EnumWithAPIDriver.zip



Riferimenti:
[1] 01 - Concetti preliminari
[2] https://github.com/andylau004/LookDrvCode/blob/master/WIN64驱动编程基础教程/代码/%5B2-7%5DProcessOperationTest/main.c
[3] https://doxygen.reactos.org/dir_af8ee41f3b3ee949318ebee67dfc7648.html

Nessun commento:

Posta un commento