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
DeathSleep — Eine PoC-Implementierung einer Evasionstechnik, um den aktuellen Thread zu beenden und vor der Wiederaufnahme der Ausführung wiederherzustellen, während Seitenschutzänderungen während der Nichtausführung implementiert werden. | Kitploit
Tools/GitHubGitHub/janoglezcampos/deathsleep
ExploitationMalware-AnalyseRed TeamingPayload-Entwicklung
GitHubjanoglezcampos/deathsleep

DeathSleep

Eine PoC-Implementierung einer Evasionstechnik, um den aktuellen Thread zu beenden und vor der Wiederaufnahme der Ausführung wiederherzustellen, während Seitenschutzänderungen während der Nichtausführung implementiert werden.

Repository anzeigen
53877vor 4 JahrenVon Kitploit geprüft

Beliebteste

Alle anzeigen →

Entdecken Sie die meistgenutzten Tools unserer Community.

Alle Tools erkunden

Durchsuchen Sie unsere Tool-Sammlung

Alle Tools anzeigen →
Teilen

██████╗ ███████╗ █████╗ ████████╗██╗ ██╗███████╗██╗ ███████╗███████╗██████╗ ██╔══██╗██╔════╝██╔══██╗╚══██╔══╝██║ ██║██╔════╝██║ ██╔════╝██╔════╝██╔══██╗ ██║ ██║█████╗ ███████║ ██║ ███████║███████╗██║ █████╗ █████╗ ██████╔╝ ██║ ██║██╔══╝ ██╔══██║ ██║ ██╔══██║╚════██║██║ ██╔══╝ ██╔══╝ ██╔═══╝ ██████╔╝███████╗██║ ██║ ██║ ██║ ██║███████║███████╗███████╗███████╗██║
╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝╚══════╝╚═╝

Eine PoC-Implementierung einer Evasion-Technik zum Beenden des aktuellen Threads und Wiederherstellen vor der Fortsetzung der Ausführung, während Seitenberechtigungsänderungen während der Nicht-Ausführung implementiert werden.

Einleitung

Schlaf- und Verschleierungsmethoden sind in der Maldev-Community wohlbekannt. Mit verschiedenen Implementierungen haben sie das Ziel, sich während des Schlafens vor Speicherscannern zu verstecken. Normalerweise werden Seitenberechtigungen geändert und sogar coole Funktionen wie die Verschlüsselung des Shellcodes hinzugefügt, aber es gibt einen weiteren wichtigen Punkt, um unseren Shellcode zu verstecken: den aktuellen Ausführungsthread zu verstecken. Das Spoofen des Stacks ist cool, aber nachdem ich ein wenig darüber nachgedacht habe, dachte ich, dass es nicht nötig ist, den Stack zu spoofen … wenn es keinen Stack gibt :)

Die Nutzbarkeit dieser Technik bleibt dem Leser überlassen, aber auf jeden Fall denke ich, dass es eine coole Art ist, einige Themen zu wiederholen und etwas Maldev für diejenigen zu lernen, die, wie ich, gerade in dieser Welt anfangen.

Die hier gezeigte Hauptimplementierung hält alles, was wir aus dem Stack nehmen müssen, in der Datensektion als globale Variablen, aber eine Implementierung, die alles auf den Heap verschiebt, wird bald veröffentlicht. Sie zielt darauf ab, einige wichtige Änderungen zu zeigen, die vorgenommen werden müssen, um diesen Code PIC und injizierbar zu machen.

Dieses Repository wird zwischen GitHub und GitLab gespiegelt.


Was ist los?

Zuallererst

Alles, was hier gesagt wird, stammt aus meinem Verständnis der verschiedenen behandelten Themen, entweder aus Lektüre oder Erfahrung während der Entwicklung. Mir ist bewusst, dass ich kein Experte bin, und das Letzte, was ich tun möchte, ist, Fehlinformationen zu verbreiten. Wenn du also denkst, dass etwas nicht korrekt ist, würde ich mich freuen, wenn du mich darauf aufmerksam machst. Du kannst mich auf Twitter kontaktieren oder Issues in diesem Repo eröffnen. Vielen Dank für dein Verständnis. :)

Grundlagen

Das Hauptziel dieser Technik ist klar: den aktuellen Thread zu beenden und ihn vor der Fortsetzung der Ausführung wiederherzustellen. Aber was bedeutet das genau und welche neuen Einschränkungen bringt es mit sich?

Um die Ausführung wiederherzustellen zu können, müssen wir zwei Dinge speichern, bevor der Thread beendet wird: erstens den CPU-Zustand und zweitens den Stack, und sie nach dem Start des neuen Threads effektiv wiederherstellen.

Ich habe über neue Einschränkungen gesprochen, die bei dieser Technik auftreten werden, und es gibt zwei große: Erstens müssen wir außerhalb des Stacks alles speichern, was von dem Moment an benötigt wird, in dem der Thread beendet wird, bis der Stack wiederhergestellt ist, und das schafft, wie du sehen wirst, einige neue Herausforderungen.

Zweitens brauchen wir immer mindestens einen anderen Thread in unserem Prozess, da wir unseren Thread beenden. Wenn es keine anderen Threads gibt, wird der Prozess beendet. Ich denke nicht, dass dies ein großes Problem ist, da die meisten Agents in andere Prozesse injiziert werden. Wir können davon ausgehen, dass dieser Prozess mindestens einen Thread am Laufen hält.

DeathSleep-Komponenten:

Wir können in diesem POC 4 Kernfunktionen sehen:

  • Hauptprogramm: Hier würdest du deinen Agent-Code schreiben, und es ist der Teil des Codes, der DeathSleep verwendet.
  • Awake-Funktion: Dies ist der Einstiegspunkt aller unserer Threads und ist dafür verantwortlich, den Startpunkt des Stacks zu speichern, den wir wiederherstellen werden. Außerdem ist sie dafür verantwortlich, den Stack und den CPU-Kontext bei Bedarf wiederherzustellen oder einfach unser Hauptprogramm zu starten.

Testhinweise

Dieser Code wurde nur mit dem MSCV-Compiler und -Linker getestet. Da dieser POC stark von der Kompilierung abhängt, empfehle ich dasselbe Tool zu verwenden und kann nicht garantieren, dass es sofort mit anderen Compilern funktioniert.


Tool herunterladen
  • DeathSleep: Dies ist die Hauptfunktion dieser Technik und ist dafür verantwortlich, den Thread-Kontext und den Stack zu sichern und alles für das magische Ereignis vorzubereiten.
  • Rebirth: Eine einfache Funktion, die nur dafür zuständig ist, unsere neuen Threads zu starten.
  • Den Stack speichern.

    Wenn wir kurz davor sind, den Stack zu speichern, stellt sich die Frage: Wie viel vom Stack muss gespeichert werden?

    Lass uns zuerst überprüfen, was im Stack ist, nachdem wir die DeathSleep-Funktion aufgerufen haben (dies ist die Funktion, die den Kontext und den Stack speichert und alles für die Verschleierung und Wiederherstellung vorbereitet).

    Wie wir sehen können, hat jede Funktion drei Teile:

    • Shadow Space: Dies ist ein 32-Byte-Bereich, der vom Aufrufer reserviert, aber vom Aufgerufenen verwendet wird. Soweit ich weiß und sehen konnte, besteht seine Hauptfunktion darin, bei Bedarf die an die aufgerufene Funktion in Registern übergebenen Argumente aufzunehmen, aber der Aufgerufene kann es für alles verwenden, was er entscheidet.
    • Rücksprungadresse: Dies ist die Adresse der nächsten auszuführenden Anweisung in der aufrufenden Funktion, die von der CALL-Anweisung auf den Stack gelegt wird, sodass die RET-Anweisung im Aufgerufenen diese Adresse nimmt und beim Ende dorthin "springt".
    • Funktions-Stack-Bereich: Dies ist der vom Aufgerufenen reservierte Bereich, um die Werte der Register, die wiederhergestellt werden müssen, und die Werte seiner lokalen Variablen zu speichern.

    Der minimale Teil des Stacks, den wir offensichtlich speichern müssen, ist alles innerhalb unseres Hauptprogramms, das heißt sein Shadow Space, seine Rücksprungadresse und alles bis zur DeathSleep-Funktion. Alles davor ist nicht wirklich erforderlich (das Speichern des Stacks, der von der Einstiegsfunktion verwendet wird, hat seine Vorteile, aber wir werden später darauf eingehen), da dies der Stack ist, der von Windows-Routinen zum Starten unseres neuen Threads verwendet wird. Abgesehen davon habe ich mich entschieden, auch den Shadow Space der DeathSleep-Funktion zu speichern (nicht wirklich nötig, aber es erleichtert die Berechnung des Rsp zum Zeitpunkt des Aufwachens).

    Am Ende speichern wir also dies:

    So finden wir unsere Stack-Adressen:

    Jede Funktion in einer Standardkompilierung sollte aus 3 Teilen bestehen: dem Prolog, dem Funktionscode und dem Epilog.

    Der Rsp (Stack-Zeiger) sollte nur im Funktionsprolog und -epilog modifiziert werden. Der Prolog erhöht den Stack-Zeiger (denke daran, das Erhöhen des Stacks bedeutet das Verringern der Adressen, da sie in entgegengesetzte Richtungen gehen), um Register zu sichern, um alle seine lokalen Variablen zu halten und dann den Shadow Space zu halten, und der Epilog tut genau das Gegenteil.

    Das bedeutet, dass der Stack-Zeiger innerhalb des Funktionscodes immer auf das Ende des Shadow Space zeigen sollte (lila im obigen Bild), und die Summe der Stack-Größe der Funktion und des Shadow Space kann gefunden werden, indem berechnet wird, wie viel der Prolog den Stack-Zeiger erhöht. Dieser Wert kann leicht mit den Informationen in den Unwind-Tabellen berechnet werden. Eine Erklärung ihrer Verwendung werden wir hier nicht behandeln, aber als Zusammenfassung: Diese Tabellen werden verwendet, um es jedem anderen Thread oder Prozess zu ermöglichen, sich korrekt durch den Stack zu bewegen, um seinen Inhalt zu sehen, Ausnahmen zu behandeln oder ihn zu analysieren.

    Erfassen und Vorbereiten des Kontexts zur Wiederherstellung.

    Das Erfassen des Kontexts ist wahrscheinlich eine der einfachsten Aufgaben, da wir einfach RtlCaptureContext() in der ersten Zeile von DeathSleep aufrufen können, bevor Änderungen an den nichtflüchtigen Registern vorgenommen werden. Trotzdem müssen wir zwei Änderungen am Kontext vornehmen, in dem wir die Ausführung wiederherstellen werden.

    Die erste besteht darin, seinen Rip zu ändern. Wie du dich erinnerst, ist dies das Register, das die nächste auszuführende Anweisung hält. Wenn wir dies unverändert lassen, wird die Ausführung innerhalb der DeathSleep-Funktion fortgesetzt. Wir werden den Rip so ändern, dass er die Rücksprungadresse von DeathSleep enthält, auf die der aktuelle Rsp zeigt, verschoben um die Größe, die im Epilog erhöht wurde (grüne + lila Bereiche in den obigen Bildern).

    Die zweite Änderung wird vorgenommen, wenn der Thread wiederhergestellt wird, und beinhaltet das Setzen des Rsp auf die Spitze unseres wiederhergestellten Stacks; dies wird während der Wiederherstellungsphase erfolgen, da wir nicht wissen, wo unser neuer Stack platziert wird. Der Wert wird einfach die Endadresse unseres wiederhergestellten Stacks sein, da wir, wie später besprochen, auch den Shadow Space kopiert haben, der vom Aufrufer von DeathSleep reserviert wurde, und das ist genau der Wert von RSP vor dem Aufruf von DeathSleep.

    Wiederherstellen unseres Stacks:

    Sobald wir den Punkt des Aufwachens erreicht haben, kurz vor der Wiederaufnahme der Ausführung, müssen wir unseren gespeicherten Stack an Ort und Stelle bringen. Da wir bereits wissen, dass unser gespeicherter Stack an der Adresse beginnt, die von der Awake-Funktion erfasst wurde, wird die neue erfasste Adresse der Startpunkt sein, an dem wir unseren gespeicherten Stack platzieren werden, aber das wirft ein Problem auf: Jeder Aufruf einer Funktion, nachdem wir unseren alten Stack platziert haben, würde ihn modifizieren und zerstören, und die Bereinigung hier ist wirklich praktisch, insbesondere das Freigeben des Heaps, der für die Sicherung unseres Stacks verwendet wird. Das bedeutet, dass wir unseren aktuellen Rsp und auch die Teile des Stacks, die wir gerade verwenden, an einen Ort außerhalb des Bereichs verschieben müssen, in dem wir unseren wiederhergestellten Stack platzieren werden. Um es klarer zu machen, hier ist das Problem:

    Und hier ist meine Lösung: alles einfach wegbewegen:

    Wiederherstellen des Kontexts:

    Nach all der harten Arbeit ist das Letzte, was zu tun ist, NtContinue zu verwenden. Diese Funktion erlaubt es uns, den aktuellen Kontext durch unseren zuvor erfassten und modifizierten Kontext zu ersetzen, wobei der RIP direkt nach dem Aufruf von DeathSleep gesetzt wird. Alle Register sollten dieselben Werte haben wie beim Aufruf von DeathSleep, und RSP sollte auf die Spitze des Stacks zeigen.

    Planung des Wiederherstellungsprozesses: Verwendung der Thread Pool API.

    Okay, wir kennen die Grundlagen dessen, was getan werden muss, um den aktuellen Thread zu speichern und wiederherzustellen, aber wir müssen irgendwie in der Lage sein, all dies auszuführen, selbst wenn wir keine Threads haben. Hier kommt unsere geliebte Thread Pool API ins Spiel, ein von Windows bereitgestelltes Werkzeug, das es uns erlaubt, Aufgaben (Funktionen mit maximal einem Argument) an eine Gruppe von Threads (einen Pool) zu übergeben, die vollständig vom Betriebssystem verwaltet werden. Wenn du Ekko gesehen hast, wirst du bemerken, dass es diese API verwendet, also … implementieren wir es auf die gleiche Weise.

    Alles funktionierte gut, aber es gab ein Problem: Ein Worker war selbst nach Beendigung der Ausführung seiner zugewiesenen Aufgaben noch aktiv. Dies war ein Problem, da ich alle Threads zerstören wollte, die unser Programm erzeugen könnte, also war dies nicht der richtige Weg.

    Nach ein wenig Recherche entdeckte ich, dass die Thread Pool API, die in Ekko verwendet wurde, eine alte Version war, und es gab eine neue mit zusätzlichen Fähigkeiten, und darunter eine Funktion, die unser Problem effektiv lösen würde: CloseThreadPool(). Diese neue API erlaubt es uns, unseren eigenen Pool zu erstellen und nach der Verwendung zu zerstören, wodurch alle verwendeten Worker beendet werden. Es bietet zwei weitere Vorteile: das Festlegen einer maximalen Anzahl von Threads und das Bereinigen von Gruppen. Das Festlegen einer maximalen Anzahl von Threads würde es uns erlauben, alle unsere Aufgaben sequenziell auszuführen, solange sie mit einem beliebigen Zeitunterschied in die Warteschlange gestellt werden. Die Bereinigungsgruppen sind nützlich, um die Bereinigung zu erleichtern, nachdem alles erledigt ist.

    Also … ist alles erledigt? Nun, zu diesem Zeitpunkt ist der Thread beendet, und wir stellen die Rebirth-Funktion in die Warteschlange, die den neuen Thread mit Awake als Einstiegspunkt erstellt, den vorherigen Zustand wiederherstellt und den Pool schließt, so weit, so gut!

    Ändern von Speicherberechtigungen, Umleitung der Ausführung.

    Als ich mit allem, was wir zuvor besprochen haben, fertig war, dachte ich, der schwierige Teil sei gelöst, da dieser Teil bereits von früheren Techniken gelöst wurde, aber oh Mann, ich wusste nicht, was auf mich zukam.

    Das Hauptproblem ist, dass wir dies außerhalb unseres Codes auslagern müssen, da wir den Speicherschutz auf RW (Lesen/Schreiben) ändern. Wenn wir VirtualProtect() aufrufen, wird unser Prozess beim Zurückkehren der Funktion abstürzen (wir können keine Anweisungen in RW-Seiten ausführen). Wir müssen also einen Weg finden, dies von woanders auszuführen und auch auf einige RX-Seiten (Lesen/Ausführen) zurückzukehren (und dasselbe passiert beim Zurückkommen). Offensichtlich werden wir wieder die Thread Pool API verwenden, aber es gibt ein Problem: Wir können unseren Aufgaben nur ein Argument übergeben, und VirtualProtect() benötigt 4.

    Dafür werden wir wieder NtContinue() verwenden. Das erste Mal, dass ich diese Verwendung dieser Funktion sah, war in Foliage, aber es wird auch in Ekko verwendet. NtContinue() erlaubt es uns, wie wir bereits gesehen haben, einen Kontext für den Thread festzulegen, der es aufruft, und mit einigen cleveren Anpassungen kann es einen Funktionsaufruf mit mehreren Argumenten "nachahmen", indem es nur eines verwendet (sehr praktisch für die Thread Pool API). Die Hauptidee ist, den RIP auf die Startadresse der Funktion zu setzen, und da die Windows x64-Aufrufkonvention die ersten vier Argumente in Registern übergibt (rcx, rdx, r8, r9, in dieser Reihenfolge), setze einfach deine Argumente in die Kontextstruktur, die du an NtContinue übergibst, und es wird effektiv einen Aufruf einer Funktion simulieren. Das Letzte, worauf wir bei der Verwendung von NtContinue achten müssen, ist der Rsp, da diese Adresse, wie wir gesehen haben, die Rücksprungadresse enthalten sollte, wenn eine Funktion aufgerufen wird.

    Das erste, was wir also brauchen, damit NtContinue funktioniert, ist einen Kontext zu erhalten. Wir könnten ihn manuell erstellen, aber wir würden auf ein Problem stoßen: den Wert für Rsp zu finden, der, wenn er an unsere Funktion übergeben wird, auf die Adresse zeigt, die von RET zum Zurückkehren verwendet wird. Unsere Aufgaben werden in einem anderen Thread arbeiten, also wissen wir nicht, wo sein Stack platziert wird. Die Lösung (vorsichtig von Ekko gestohlen, vielen Dank :P) besteht darin, eine Kopie des Kontexts innerhalb eines Workers mit RtlCaptureContext() zu nehmen und den Stack-Zeiger des erhaltenen Kontexts um 8 zu erhöhen, sodass er auf die Adresse zeigt, die von CALL RtlCaptureContext() auf den Stack gelegt wurde und die die Rücksprungadresse dieser letzten Funktion ist. Wir können sie als Rücksprungadresse aller unserer Funktionen verwenden.

    Okay, das ist nett, aber was passiert, wenn wir diese Modifikation am Rsp nicht vornehmen können? Das ist der Fall, wenn wir die Entverschleierung durchführen: Wir werden in einem neuen Thread sein, also ist der alte Rsp des Kontexts nutzlos. Wir brauchen einen neuen Kontext, der vom neuen Thread genommen wird, aber wir können den alten Trick nicht verwenden, den Rsp zu modifizieren, um auf die korrekte Adresse zu zeigen.

    Rop Chains

    Wir können den erhaltenen Kontext also nicht modifizieren, aber das heißt nicht, dass er nutzlos ist. In Wirklichkeit werden wir ihn verwenden, aber auf eine andere Weise. Wenn wir diesen Kontext einfach mit NtContinue() wiederherstellen, ohne seinen Rip zu modifizieren, wird es die Ausführung einfach zur nächsten Anweisung nach dem Aufruf von RtlCaptureContext() umleiten, und mit einem korrekten Rsp, sodass wir ihn nach unseren Aufrufen von NtContinue() mit modifizierten Kontexten verwenden können, um die Ausführung unserer Aufgaben korrekt zu beenden. Dafür werden wir eine Rop Chain verwenden, indem wir den Rsp unseres ersten Kontexts auf einen manuell erstellten Stack zeigen lassen, der alles enthält, was wir brauchen, um die Ausführung bis zum zweiten NtContinue()-Aufruf umzuleiten, der den korrekten Kontext zum Beenden setzt.

    So sollte unser manuell erstellter Stack aussehen:

    Wir verwenden 2 Rop-Gadgets: eines zum Reparieren oder "Überspringen" des Shadow Space unserer Funktion, und das zweite ist dafür verantwortlich, das Argument für NtContinue in rcx zu platzieren und dann zu NtContinue zurückzukehren.

    Diese zwei Rop-Gadgets zu finden, ist recht einfach. Das eine zum Reparieren des Shadow Space ist einfach der Epilog fast jeder Funktion (ich habe nur in Ntdll mehr als 500 Treffer gefunden), da, wie wir gesehen haben, Epiloge hauptsächlich dazu dienen, den Rsp zu verringern. Das zweite ist einfach ein pop rcx; ret;, das 2 Bytes groß ist, und ich habe auch einige zwischen Ntdll- und Kernel32-DLLs gefunden.

    Ein wenig Reverse Engineering der Thread Pool API

    Wie wir gesehen haben, erfordert die Verwendung von NtContinue nur, dass das erste Argument gefüllt ist, um zu funktionieren, und das ist perfekt mit der alten Thread Pool API. Aber in der neuen Thread Pool API werden die Argumente an der zweiten Position übergeben, also ja, das allein wird nicht funktionieren.

    Nach einigen Stunden, in denen ich nicht wusste, wie ich dieses letzte Problem lösen sollte, kam mir in den Sinn, dass beide APIs in einigen Fällen dieselben Funktionen verwenden, und das ließ mich denken, dass sie ähnlicher sein könnten, als sie erscheinen. Also beschloss ich zu untersuchen, welche Beziehung zwischen ihnen besteht.

    Für die alte API verwenden wir CreateTimerQueueTimer(), um unsere Aufgaben in die Warteschlange zu stellen. In der neuen API benötigen wir zwei Funktionen, um dasselbe zu tun: CreateThreadpoolTimer(), das die Callback-Funktion und das zu übergebende Argument nimmt und einen Zeiger auf eine TP_TIMER-Struktur zurückgibt, die die Aufgabe beschreibt, und eine zweite Funktion zum Einreihen der Aufgabe: SetThreadpoolTimer(), das den vorherigen Zeiger und einen Zeiger auf eine FILETIME-Struktur nimmt, die beschreibt, wann die Aufgabe ausgeführt wird.

    Wenn wir diese Funktionen reversen, werden wir Folgendes finden:

    Wie wir sehen können, ist CreateThreadpoolTimer() nur ein ausgefallener Wrapper für TpAllocTimer(), und SetThreadpoolTimer() ist nur ein Weiterleiter zu TpSetTimer().

    Lass uns nun die Inneren von CreateTimerQueueTimer() überprüfen. Zunächst ist es nur ein weiterer ausgefallener Wrapper für eine Funktion in Ntdll, RtlCreateTimer(), und hier geschieht die Magie. Dies ist eine größere Funktion, aber hier ist das Gold, das wir suchten:

    Wie du sehen kannst, gibt es innerhalb dieser Funktion effektiv einen Aufruf von TpAllocTimer() und TpSetTimer(), was im Wesentlichen bedeutet, dass es CreateThreadpoolTimer() und SetThreadpoolTimer() darin aufruft. Wie wir sehen können, ist die Funktion, die wir in die Warteschlange stellen, nicht direkt der Callback, den wir der Funktion gegeben haben; sie setzt RtlpTpTimerCallback() als Callback. Wenn du noch nicht erkannt hast, was das alles bedeutet, es bedeutet, dass wir CreateThreadpoolTimer() verwenden, um eine Funktion in die Warteschlange zu stellen, die ihre Argumente an der zweiten Position empfängt, RtlpTpTimerCallback(), die eine andere Funktion mit ihren Argumenten an der ersten Position ausführt.

    Das Einzige, was wir noch verstehen müssen, ist, wie die Callback-Informationen an RtlpTpTimerCallback() übergeben werden. Nach etwas Reverse Engineering bin ich auf die folgende Struktur gestoßen, die, Überraschung, funktioniert!

    Jetzt können wir Funktionen aufrufen, die ihre Argumente an der ersten Position empfangen, und gleichzeitig können wir unsere Pools schließen und keine Threads laufen lassen, Win-Win. Es ist wichtig zu beachten, dass diese Funktion nicht in Ntdll exportiert wird, also habe ich mich entschieden, sie anhand ihrer Byte-Form innerhalb der DLL zu finden.

    So, das ist das Ende. Mit allem, was besprochen wurde, denke ich, habe ich die Kernideen gegeben, die mir während der Entwicklung dieses POC durch den Kopf gingen und warum alles so gemacht wurde, wie ich es gemacht habe.

    Ich hoffe, es hat dir gefallen :)