
Windows-Benutzermodus-Shellcode-Entwicklungsframework (WUMSDF)
v1.1Das Projekt SILVERPICK ist ein Windows-User-Mode-Shellcode-Entwicklungsframework (WUMSDF), dessen alleiniger Zweck es ist, Fähigkeitsentwicklern zu ermöglichen, Position Independent Code (PIC)-Blobs für Windows x64 auf einfache Weise mit C/C++ zu erstellen, um die Entwicklungskosten eines solchen Vorhabens zu senken.
Es leitet sich vom Projekt WILDBEAST ab und nutzt daher:
Visual Studio Code als Code-EditorMinGW-w64GNU Make als Build-SystemDie Einrichtungsanweisungen findest du hier: GCC-Clang-Setup-Windows
Bitte beachte, dass dieses Projekt MSYS2 verwendet.
Shellcode in höheren Programmiersprachen zu schreiben, ist nichts Neues, und unzählige Blogbeiträge und Forschungsarbeiten wurden seit 2010 darüber veröffentlicht. Was ist also neu an SILVERPICK?
Nun, ich bin froh, dass du fragst.
SILVERPICK hat ein nettes kleines Bündel an Tricks im Ärmel, aber vor allem ist dies meine Herangehensweise an das Thema.
Also, ohne weitere Umschweife, präsentiere ich dir meinen ersten Trick.
Seitdem Matt Graeber das Schreiben von Shellcode in C populär gemacht hat, verwenden die meisten Leute seinen 16-Byte-Stack-Ausrichtungs-Stub, geschrieben in Assembly.
Obwohl dies kein Problem darstellt, da wir nicht IKEA sind, sollte Assembly nicht notwendig sein, und tatsächlich ist es das auch nicht.
Es gibt ein GCC-Funktionsattribut, das den Stack-Ausrichtungs-Stub für dich erzeugt.
Hier ist das force_align_arg_pointer-Funktionsattribut in Form eines hilfreichen ALIGN_STACK-Makros, das die folgende Assembly erzeugt:
Disassembly of section .init:
<PicEntry>:
push rbp
mov rbp,rsp
and rsp,0xfffffffffffffff0
sub rsp,0x20
call <PicEntry+0x11> IMAGE_REL_AMD64_REL32 .text$payload
leave
ret
Was ist die .init-Sektion, fragst du? Nun, das dient als schöner Übergang zu meinem zweiten Trick.
Matt Graeber hat vielleicht irgendwann das Schreiben von Shellcode in C populär gemacht, aber wirklich war es Paul Ungur, der diese schwarze Kunst mit Stardust wiederbelebt hat.
Nun verwendet Stardust ein Binutils-Linker-Skript, um die Platzierung von Funktionen und Daten in der entsprechenden PE-Sektion in der richtigen Reihenfolge zu steuern. Diese Technik selbst stammt aus der Arbeit von Austin Hudson, und viele Leute verwenden eine Variante seiner Linker-Skripte.
Während Linker-Skripte großartig für die Linker-Sektionsreihenfolge sind, sind sie unnötig, wenn du nur eine bestimmte Funktion an den Anfang der Code-Sektion setzen musst.
Hier kommt das section-Funktionsattribut mit einem speziellen Sektionsnamen namens .init ins Spiel, das dem Linker mitteilt, dass die Funktion Vor-main()-Laufzeitinitialisierungscode enthält und als erstes in der Link-Reihenfolge stehen muss.
Zu diesem Zweck wurde das CODE_BEGIN-Makro erstellt.
Für meinen dritten Trick präsentiere ich dir das STACK_STRING-Makro.
In C kannst du einen Stack-String (einen String, der dynamisch auf dem Stack aufgebaut wird) erstellen, indem du das String-Literal als ein Array von ANSI-Zeichen deklarierst:
char charrHelloKitty[] = { 'H', 'e', 'l', 'l', 'o', 'K', 'i', 't', 't', 'y', '\0' };
In C++ kannst du einen Stack-String erstellen, indem du ein char-Array einfach als constexpr markierst:
constexpr char charrHelloKitty[]{ "HelloKitty" };
Beide Techniken werden jedoch angesichts von Compiler-Optimierungen nutzlos, wenn die String-Literale ausreichend groß sind, anders als unsere Lösung, die unabhängig von der String-Länge und dem Grad der Compiler-Optimierungen funktioniert, dank eines cleveren C++-Template-Metaprogrammierungs-Hacks, mit freundlicher Genehmigung von Can Bölük.
Die Verwendung dieses Makros ist ziemlich einfach:
STACK_STRING(sstrText, "an extra long hello world!");
STACK_STRING(sstrCaption, "Demo");
MessageBoxA(nullptr, sstrText.data(), sstrCaption.data(), MB_OK);
Dies erzeugt die folgende Assembly:
mov [rsp+58h+var_23], 61h ; 'a'
mov [rsp+58h+var_22], 6Eh ; 'n'
mov [rsp+58h+var_21], 20h ; ' '
mov [rsp+58h+var_20], 65h ; 'e'
mov [rsp+58h+var_1F], 78h ; 'x'
mov [rsp+58h+var_1E], 74h ; 't'
mov [rsp+58h+var_1D], 72h ; 'r'
mov [rsp+58h+var_1C], 61h ; 'a'
mov [rsp+58h+var_1B], 20h ; ' '
mov [rsp+58h+var_1A], 6Ch ; 'l'
mov [rsp+58h+var_19], 6Fh ; 'o'
mov [rsp+58h+var_18], 6Eh ; 'n'
mov [rsp+58h+var_17], 67h ; 'g'
mov [rsp+58h+var_16], 20h ; ' '
mov [rsp+58h+var_15], 68h ; 'h'
mov [rsp+58h+var_14], 65h ; 'e'
mov [rsp+58h+var_13], 6Ch ; 'l'
mov [rsp+58h+var_12], 6Ch ; 'l'
mov [rsp+58h+var_11], 6Fh ; 'o'
mov [rsp+58h+var_10], 20h ; ' '
mov [rsp+58h+var_2F], 0
mov [rsp+58h+var_F], 77h ; 'w'
mov [rsp+58h+var_E], 6Fh ; 'o'
mov [rsp+58h+var_D], 72h ; 'r'
mov [rsp+58h+var_C], 6Ch ; 'l'
mov [rsp+58h+var_B], 64h ; 'd'
mov [rsp+58h+var_A], 21h ; '!'
mov [rsp+58h+var_33], 44h ; 'D'
mov [rsp+58h+var_32], 65h ; 'e'
mov [rsp+58h+var_31], 6Dh ; 'm'
mov [rsp+58h+var_30], 6Fh ; 'o'
Da wir gerade von C++ sprechen, präsentiere ich dir für meinen vierten Trick Compile-Zeit-String-Hashing.
Obwohl dies kein neuartiges Konzept ist, bietet SILVERPICK einige Verbesserungen gegenüber bestehenden öffentlichen Implementierungen.
Erstens verwenden wir die 64-Bit-Variante der beliebten nicht-kryptografischen Hash-Funktion FNV-1a, um die Wahrscheinlichkeit eines erfolgreichen Hash-Kollisionsangriffs zu verringern.
Zweitens verwenden wir einen modifizierten Parameter für die Hash-Funktion, um uns gegen vorberechnete Hash-Tabellen-Lookups wie HashDB zu verteidigen. Entscheidend ist, dass dies die Eigenschaften der Hash-Funktion nicht verändert.
Um einen kurzen String zur Laufzeit zu hashen, verwende einfach das HASH_STRING_RUN_TIME-Makro.
Um ein kurzes String-Literal zur Compile-Zeit zu hashen, verwende einfach das HASH_STRING_COMPILE_TIME-Makro. Die reine Compile-Zeit-Auswertung wird über consteval garantiert.
Es stellt sich heraus, dass man eine ganze Reihe von C-Runtime-Library (CRT)-Funktionen mit x86-String-Anweisungen implementieren kann. Also musste ich sie natürlich mit einer Mischung aus Compiler-Intrinsics und Inline-Assembly implementieren.
Möchtest du die Funktion msvcrt!memset in deinem Code verwenden? Verwende stattdessen das ZERO_MEMORY-Makro, das die über ein Compiler-Intrinsic emittierte rep stosb-Anweisung verwendet.
Wie wäre es mit der Funktion msvcrt!memcpy oder der Funktion msvcrt!memmove, fragst du? Lerne das COPY_MEMORY-Makro als Ersatz kennen, das die über ein Compiler-Intrinsic emittierte rep movsb-Anweisung verwendet.
Aber was ist mit einer Alternative zur Funktion msvcrt!memcmp? Es stellt sich heraus, dass es nicht genau ein Compiler-Intrinsic gibt, um die repe cmpsb-Anweisung zu emittieren. Also schreiben wir stattdessen eine compare_memory-Funktion mit Inline-Assembly.
Wenn du schließlich einen Ersatz für die Funktion msvcrt!memchr suchst, lerne die scan_memory-Funktion kennen, die erneut Inline-Assembly verwendet, da kein Compiler-Intrinsic verfügbar ist, um die repne scasb-Anweisung zu emittieren.
Oh, und habe ich erwähnt, dass du deine eigene sicherere Version der Funktion msvcrt!strlen mit der scan_memory-Routine schreiben kannst, wie folgt:
DWORD_PTR dwptrExportNameLength = std::min(BIT_CAST(DWORD_PTR, scan_memory(strExportName, 0x00, MAX_EXPORTED_SYMBOL_NAME_LEN)) - BIT_CAST(DWORD_PTR, strExportName), MAX_EXPORTED_SYMBOL_NAME_LEN);
Bitte beachte, dass diese Implementierungen je nach Ziel-CPU-Mikroarchitektur möglicherweise nicht den leistungsfähigsten Code erzeugen. Sie sind jedoch garantiert in der Lage, die Aufgabe zu erledigen.
Interessiert an weiteren Salon-Tricks?
Es gibt viele andere kleine Makros in Common.h, die dazu dienen, einige der Komplexitäten der Compiler-Beeinflussung zu abstrahieren.
Eine abhängigkeitsfreie Implementierung der Funktion GetModuleHandle wird in UserModuleBase.cpp bereitgestellt. Um die Benutzerfreundlichkeit zu vereinfachen, wurde ein hilfreiches Makro namens GET_USER_MODULE_BASE erstellt.
Ähnlich wird eine abhängigkeitsfreie Implementierung der Funktion GetProcAddress in PEParse.cpp bereitgestellt, die dann in ein praktisches Makro mit dem treffenden Namen GET_EXPORTED_SYMBOL_ADDRESS verpackt wird. Darüber hinaus werden zwei weitere Makros bereitgestellt, um das dynamische Linken zur Laufzeit zu unterstützen - INITIALIZE_FUNCTION_POINTER zum Deklarieren und Initialisieren eines Funktionszeigers auf 0 und RESOLVE_FUNCTION_POINTER zum Auflösen dieses Funktionszeigers.
Die Visual-Studio-Code-Integration ist in das Projekt integriert, sodass Entwickler die Tastenkombination Ctrl+Shift+B für einen problemlosen Build-Prozess verwenden können.
Die GitHub-Actions-Integration ist ebenfalls in das Projekt integriert, um CI-Builds zu ermöglichen.
Das Projekt ist stolz auf seine gut organisierte Struktur sowie auf den gründlich kommentierten und relativ sauberen Code.
Wirf schließlich einen Blick auf das Makefile des Projekts, das die erlesenste Auswahl an Compiler- und Linker-Flags enthält, die kleinen, sicheren und OPSEC-freundlichen Code erzeugen. Darüber hinaus bieten eine ausführliche Protokollierung und eine erzeugte Linker-Map-Datei Einblick in den Build-Prozess, um ein tieferes Verständnis der Toolchain zu ermöglichen. Zusätzlich erzeugt jede Übersetzungseinheit eine Disassembly-Datei, die bei der Untersuchung oft dazu führt, dass man Dinge sagt wie "Was hat der Compiler jetzt gemacht?", usw.
Wenn dich das Framework überzeugt hat, beschreibt dieser Abschnitt, wie du es nutzen würdest.
Das Folgende ist ein Auszug aus PicMain.cpp:
/// @brief PIC start function
/// @param None
/// @return None
EXTERN_C NO_INLINE VOID __stdcall payload(
VOID
) {
// Init local variables
PVOID pKernel32 = nullptr;
INITIALIZE_FUNCTION_POINTER(LoadLibraryA);
HMODULE hUser32 = nullptr;
STACK_STRING(sstrUser32, "user32.dll");
INITIALIZE_FUNCTION_POINTER(MessageBoxA);
STACK_STRING(sstrText, "an extra long hello world!");
STACK_STRING(sstrCaption, "Demo");
// Get the image base address of kernel32.dll
pKernel32 = GET_USER_MODULE_BASE("kernel32.dll");
if (pKernel32 == nullptr)
goto cleanup;
// Resolve kernel32!LoadLibraryA
RESOLVE_FUNCTION_POINTER(pKernel32, LoadLibraryA);
if (LoadLibraryA == nullptr)
goto cleanup;
// Load User32.dll into the process VAS
hUser32 = LoadLibraryA(sstrUser32.data());
if (hUser32 == nullptr)
goto cleanup;
// Resolve user32!MessageBoxA
RESOLVE_FUNCTION_POINTER(hUser32, MessageBoxA);
if (MessageBoxA == nullptr)
goto cleanup;
// Display a message box
MessageBoxA(nullptr, sstrText.data(), sstrCaption.data(), MB_OK);
// Cleanup
cleanup:
return;
}
Scheint einfach genug, oder?
Beim Schreiben von PIC in C/C++ mit dem SILVERPICK-Framework musst du die folgenden Regeln berücksichtigen:
payload-Funktion so, wie du die main-Funktion eines traditionellen Programms behandeln würdest, d. h. als (Pseudo-)Einstiegspunkt.Windows-API- oder Native-API-Funktionen dürfen nur über dynamisches Linken zur Laufzeit verwendet werden, nachdem sichergestellt wurde, dass der Funktionsprototyp in der entsprechenden Header-Datei verfügbar ist.Dieser Abschnitt enthält eine nicht erschöpfende Liste geplanter Erweiterungen, die in ein zukünftiges Projekt integriert werden.
Clang/LLVM-Toolchain.GS-Segments.Export Address Filtering (EAF) zu umgehen.Das Folgende ist eine Liste von Referenzen in chronologischer Reihenfolge, die sich für mich während meiner Forschung als unschätzbar wertvoll erwiesen und ausgiebig als Inspiration für dieses Projekt dienten:
Writing Shellcode with a C Compiler von Nick Harbour (2010)
Shellcode with a C-compiler von Didier Stevens (2010)
Writing Optimized Windows Shellcode in C von Matt Graeber (2013)
Shellcode the better way, or how to just use your compiler von Justin Fisher (2016)
ShellcodeStdio von Jack Ullrich (2016)
Shellcode: A Windows PIC using RSA-2048 key exchange, AES-256, SHA-3 von Odzhan (2016)
Writing Optimized Windows Shellcode von Dimitri Fourny (2017)
Writing and Compiling Shellcode in C von Aleksandra Doniec und Mantvydas Baranauskas (2021)
Creating Shellcode from any Code Using Visual Studio and C++ von Hamid Memar (2021)
Writing Optimized Windows Shellcode in C von Philip Woldhek (2021)
From C, with inline assembly, to shellcode von Steve Salinas (2023)
How To Craft Your Own Windows x86/64 Shellcode with Visual Studio von Yazid Benjamaa (2023)
Modern implant design: position independent malware development von Paul Ungur (2024)
From C to shellcode (simple way) von Print3M (2024)
relocatable von Tijme Gommers (2025)
PIC Development Crash Course von Raphael Mudge (2025)
scfw von Petr Beneš (2026)
Was mir als seltsam auffiel, war die Einstufung VirTool:Win64/Silepesz.A durch Microsoft Defender.
Dies sind die Bytes, die die Signatur abdeckt:
[+] Target file size: 2560 bytes
[+] Analyzing...
[!] Identified end of bad bytes at offset 0x4CB
000003CB 44 24 2F 32 C6 44 24 30 2E C6 44 24 31 64 C6 44 D$/2�D$0.�D$1d�D
000003DB 24 32 6C C6 44 24 33 6C C6 44 24 35 61 C6 44 24 $2l�D$3l�D$5a�D$
000003EB 36 6E C6 44 24 37 20 C6 44 24 38 65 C6 44 24 39 6n�D$7 �D$8e�D$9
000003FB 78 C6 44 24 3A 74 C6 44 24 3B 72 C6 44 24 3C 61 x�D$:t�D$;r�D$<a
0000040B C6 44 24 3D 20 C6 44 24 3E 6C C6 44 24 3F 6F C6 �D$= �D$>l�D$?o�
0000041B 44 24 40 6E C6 44 24 41 67 C6 44 24 42 20 C6 44 D$@n�D$Ag�D$B �D
0000042B 24 43 68 C6 44 24 44 65 C6 44 24 45 6C C6 44 24 $Ch�D$De�D$El�D$
0000043B 46 6C C6 44 24 47 6F C6 44 24 48 20 C6 44 24 29 Fl�D$Go�D$H �D$)
0000044B 00 C6 44 24 49 77 C6 44 24 4A 6F C6 44 24 4B 72 .�D$Iw�D$Jo�D$Kr
0000045B C6 44 24 4C 6C C6 44 24 4D 64 C6 44 24 4E 21 C6 �D$Ll�D$Md�D$N!�
0000046B 44 24 25 44 C6 44 24 26 65 C6 44 24 27 6D C6 44 D$%D�D$&e�D$'m�D
0000047B 24 28 6F E8 55 00 00 00 48 85 C0 74 4B 48 BA 58 $(o�U...H.AtKH�X
0000048B D0 CC C6 F8 E7 BF 0A 48 89 C1 E8 86 FD FF FF 48 DI�o��.H.A�.y��H
0000049B 85 C0 74 34 48 8D 4C 24 2A FF D0 48 85 C0 74 28 .At4H.L$*�DH.At(
000004AB 48 BA D9 92 FB 55 9A AC 70 E0 48 89 C1 E8 63 FD H�U.�U.�p�H.A�cy
000004BB FF FF 48 85 C0 74 11 48 8D 54 24 35 45 31 C9 4C ��H.At.H.T$5E1�L
Dies lässt sich auf den folgenden Quellcode zurückführen:
pKernel32 = GET_USER_MODULE_BASE("kernel32.dll");
if (pKernel32 == nullptr)
goto cleanup;
RESOLVE_FUNCTION_POINTER(pKernel32, LoadLibraryA); // mov rdx, 0x0ABFE7F8C6CCD058 (FNV-1a hash of "LoadLibraryA" with modified offset basis)
if (LoadLibraryA == nullptr)
goto cleanup;
hUser32 = LoadLibraryA(sstrUser32.data());
if (hUser32 == nullptr)
goto cleanup;
RESOLVE_FUNCTION_POINTER(hUser32, MessageBoxA); // mov rdx, 0xE070AC9A55FB92D9 (FNV-1a hash of "MessageBoxA" with modified offset basis)
if (MessageBoxA == nullptr)
goto cleanup;
MessageBoxA(nullptr, sstrText.data(), sstrCaption.data(), MB_OK); // arg setup only
Unnötig zu erwähnen, dass dies eine äußerst fragile Erkennung ist und nur den genauen Beispielcode erkennt, der im Beispiel gezeigt wird. Es unterstreicht jedoch, wie wichtig die Verwendung polymorpher API-Hashes ist.