
Bibliothek, die die Verwendung indirekter Syscalls erleichtert. Ziemlich interessanter AV/EDR-Bypass als PoC.
Dieses Projekt implementiert eine komfortable und moderne Möglichkeit, die NTAPI-Funktionen mithilfe indirekter Syscalls zu verwenden, gekoppelt mit der FreshyCalls-Methode mit einer kleinen Abwandlung zur dynamischen Syscall-Nummernabrufung. Es verwendet außerdem eine Technik, von der ich nicht gesehen habe, dass sie erwähnt wird, um den Speicherscan von Windows Defender zu umgehen. Das Projekt implementiert einen klassischen PoC-Prozess-Injektor, der die Bibliothek verwendet. Praktische Funktionen zur Verschlüsselung sind ebenfalls verfügbar.

Zum Erstellen des Beispiels verwende -DNULLGATE_BUILD_SAMPLE=ON.
Wenn du nullgate direkt gebaut hast, ist es unter <build_dir>/sample.exe verfügbar; wenn du es als Abhängigkeit gebaut hast, unter <build_dir>/_deps/nullgate-build/sample.exe.
Unter Windows sind die Build-Ziele seltsam, daher wird es sich wahrscheinlich in denselben Basisverzeichnissen wie die Speicherorte der Beispiele befinden, aber wahrscheinlich noch ein paar Ebenen tiefer verschachtelt.
Es benötigt als Argument eine PID, in die du Shellcode injizieren möchtest.
[!WARNING] Wenn du Linux verwendest, musst du den MinGW-Cross-Compiler installiert haben. Auf Arch kannst du zum Beispiel
pacman -S mingw-w64-gccausführen. Verwende dann die Option-DNULLGATE_CROSSCOMPILE=ON, um mingw als Standard-Compiler festzulegen.
[!TIP] Es wird außerdem empfohlen, die resultierende Binärdatei zu strippen, um die Erkennungswahrscheinlichkeit zu verringern.
git clone https://github.com/0xsch1zo/NullGate
cd NullGate
cmake . -B build -DNULLGATE_BUILD_SAMPLE=ON
cmake --build build/
Es kann mit dem Flag -DNULLGATE_DEPRECATED_HASHER gebaut werden.
CMake FetchContent wird unterstützt. Hier ist ein Beispiel für eine einfache CMakeLists.txt:
cmake_minimum_required(VERSION 3.25)
include(FetchContent)
FetchContent_Declare(nullgate
GIT_REPOSITORY https://github.com/0xsch1zo/NullGate
GIT_TAG 1.2.0
)
FetchContent_MakeAvailable(nullgate)
project(test)
add_executable(test
main.cpp
)
target_link_libraries(test
PRIVATE nullgate
)
Die Verknüpfung erfolgt statisch, sodass du dir keine Sorgen um sichtbare Symbole machen musst.
[!NOTE] Die folgenden Beispiele verwenden
namespace ng = nullgate
Die Verwendung ist ziemlich einfach. Hier ist ein Codeausschnitt, der die Hauptfunktionalität demonstriert:
ng::syscalls syscalls;
typedef NTSTATUS NTAPI NtAllocateVirtualMemory(
_In_ HANDLE ProcessHandle,
_Inout_ _At_(*BaseAddress,
_Readable_bytes_(*RegionSize) _Writable_bytes_(*RegionSize)
_Post_readable_byte_size_(*RegionSize)) PVOID *BaseAddress,
_In_ ULONG_PTR ZeroBits, _Inout_ PSIZE_T RegionSize,
_In_ ULONG AllocationType, _In_ ULONG PageProtection);
NTSTATUS status = syscalls.SCall<NtAllocateVirtualMemory>(
ng::obfuscation::fnv1Const("NtAllocateVirtualMemory"), processHandle,
&buf, 0, ®ionSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
Es gibt eine eingebaute Typsicherheit. Du musst nur die Definition der NT-Funktion bereitstellen, die du aufrufen möchtest! Du kannst sie leicht von ntdoc bekommen. Dies ist der empfohlene Weg, die Bibliothek zu verwenden.
Für Leute, die die C++-Template-Schwarze Magie oder so etwas nicht mögen, ist die bisherige Schnittstelle weiterhin verfügbar:
NTSTATUS status = syscalls.Call(ng::obfuscation::fnv1Const("NtAllocateVirtualMemory"),
processHandle, (PVOID)&buf, (ULONG_PTR)0, ®ionSize,
(ULONG)(MEM_RESERVE | MEM_COMMIT), (ULONG)PAGE_EXECUTE_READWRITE);
Bei dieser Schnittstelle musst du die Argumente in den richtigen Typ casten. Wenn du das nicht tust, kann das zu Problemen führen.
constexpr uint64_t hash = ng::obfuscation::fnv1Const("NtAllocateVirtualMemory");
Die zuvor gezeigte fnv1Const-Methode bringt die Freuden des modernen C++ in die Maldev-Welt. Sie ist eine consteval-Funktion, sodass garantiert ist, dass sie zur Kompilierzeit ausgewertet wird und den lesbaren Funktionsnamen durch einen fnv1-Hash ersetzt.
Es gibt auch ein Laufzeit-Äquivalent namens fnv1Runtime, aber natürlich bietet es nicht den Vorteil, dass unsere Funktionsnamen verschleiert werden. Es wird von der Implementierung verwendet, um zu prüfen, welche Funktion innerhalb von ntdll die Syscall-Nummer liefert.
Es gibt drei Routinen für die xor-„Verschlüsselung“:
constexpr ng::obfuscation::ConstData xored = ng::obfuscation::xorConst("some string");
std::cout << xored.string();
Mögliche Ausgabe:
<Y:-E&X0
Diese Routine wird ähnlich wie die fnv1Const-Funktion zur Kompilierzeit ausgewertet, sodass "some string" niemals in der Binärdatei erscheint, weil der String in seiner verschlüsselten Form gespeichert wird.
Aber hey, wir müssen diese Daten tatsächlich nutzen! ConstData ist ein dünner Wrapper um rohe Bytes mit konstanter Größe.
Es hat zwei Methoden: raw() und string().
raw() gibt ein std::vector<unsigned char> zurück und string gibt, wie der Name schon sagt, ein std::string zurück.
[!NOTE] Falls du
ConstDatadirekt mit einem String-Literal konstruieren musst, verwendestd::to_array, um ein Zwischenarray zu erstellen, das anConstDataübergeben wird. Beachte jedoch, dassConstDatabei diesem Ansatz das zusätzliche Nullzeichen des Literals speichert.
Natürlich brauchen wir eine Möglichkeit, dies zu entschlüsseln und zu verwenden, was die nächste Funktion ist, die wir behandeln werden.
xorRuntime ist die zweite verfügbare Routine.
Wie der Name schon sagt, ist es das Laufzeit-Äquivalent von xorConst.
Bitte beachte, dass es nicht nur zur Entschlüsselung verwendet werden muss; es funktioniert in beide Richtungen, obwohl die Verwendung zur Verschlüsselung nicht den Vorteil bietet, den String zur Kompilierzeit zu verschleiern.
ng::obfuscation::ConstData xored = ng::obfuscation::xorConst("some string");
ng::obfuscation::ConstData data = ng::obfuscation::xorRuntime(xored);
assert(data.string() == "some string");
std::string dynamicString = "some data of dynamic size";
auto dynamicData = ng::obfuscation::DynamicData(dynamicString);
auto xoredDynamic = ng::obfuscation::xorRuntime(dynamicData);
auto unxoredDynamic = ng::obfuscation::xorRuntime(xoredDynamic);
assert(unxoredDynamic.string() == dynamicString);
DynamicData hat dieselben Methoden wie ConstData, mit einigen zusätzlichen Konstruktoren für die Interoperabilität, und kann, wie der Name schon sagt, eine Größe haben, die zur Kompilierzeit nicht bekannt ist.
xorRuntime akzeptiert sowohl DynamicData als auch ConstData.