
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.
██████╗ ███████╗ █████╗ ████████╗██╗ ██╗███████╗██╗ ███████╗███████╗██████╗
██╔══██╗██╔════╝██╔══██╗╚══██╔══╝██║ ██║██╔════╝██║ ██╔════╝██╔════╝██╔══██╗
██║ ██║█████╗ ███████║ ██║ ███████║███████╗██║ █████╗ █████╗ ██████╔╝
██║ ██║██╔══╝ ██╔══██║ ██║ ██╔══██║╚════██║██║ ██╔══╝ ██╔══╝ ██╔═══╝
██████╔╝███████╗██║ ██║ ██║ ██║ ██║███████║███████╗███████╗███████╗██║
╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝╚══════╝╚═╝
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.

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.
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. :)
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.
Wir können in diesem POC 4 Kernfunktionen sehen:
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:
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:

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.