v1.0
Project SILVERPICK is a Windows User-Mode Shellcode Development Framework (WUMSDF) whose sole purpose is to empower capability developers to build Position Independent Code (PIC) blobs for Windows x64 using C/C++ in an easy manner so as to reduce the development costs of such an endeavour.
It derives from project WILDBEAST and, as such, leverages:
Visual Studio Codeas the code editorMinGW-w64toolchain as the compiler toolchainGNU Makeas the build system
You may find the setup instructions from here: GCC-Clang-Setup-Windows
Please note that this project is using MSYS2.
Writing shellcode in higher-level programming languages is nothing new, and countless blog posts and research papers have been published on the same since 2010. So, what is new in SILVERPICK?
Well, I am glad that you asked.
SILVERPICK has a nice little bag of tricks hidden up its sleeve, but most of all, this is my take on the subject.
So, without further ado, I present to you my first trick.
Ever since Matt Graeber popularised writing shellcode in C, most people have been using his 16-byte stack alignment stub written in Assembly language.
While this is not a problem, seeing as how we aren't IKEA, assembly should not be necessary, and indeed it is not.
There exists a GCC Function Attribute that will emit the stack alignment stub for you.
Meet the force_align_arg_pointer function attribute in the form of a helpful ALIGN_STACK macro, which generates the following assembly:
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
retWhat's the .init section, you ask? Well, that serves as a nice segue into my second trick.
Matt Graeber might have popularised writing shellcode in C at some point, but really, it was Paul Ungur who revived this black art with Stardust.
Now, Stardust uses a Binutils linker script to control the placement of functions and data into the appropriate PE section in the correct order. This technique itself is derived from Austin Hudson's work, and many people use a variant of his linker scripts.
While linker scripts are great for linker section ordering, if all you need is to place a certain function at the beginning of the code section, they are unnecessary.
Enter the section function attribute with a special section name called .init, which indicates to the linker that the function contains pre-main() runtime initialization code and must be first in the link order.
To this effect, the CODE_BEGIN macro has been created.
For my third trick, I present to you the STACK_STRING macro.
In C, you can create a stack string (a string that is dynamically built on the stack) by declaring the string literal as an array of ANSI characters:
char charrHelloKitty[] = { 'H', 'e', 'l', 'l', 'o', 'K', 'i', 't', 't', 'y', '\0' };In C++, you can create a stack string by simply marking a char array as constexpr:
constexpr char charrHelloKitty[]{ "HelloKitty" };However, both of these techniques are rendered useless in the face of compiler optimizations if the string literals are sufficiently large, unlike our solution, which will work regardless of the string length and the level of compiler optimizations, thanks to some clever C++ template metaprogramming hack courtesy of Can Bölük.
Using this macro is pretty straightforward:
STACK_STRING(sstrText, "an extra long hello world!");
STACK_STRING(sstrCaption, "Demo");
MessageBoxA(nullptr, sstrText.data(), sstrCaption.data(), MB_OK);This will generate the following 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'Speaking of C++, I present to you compile-time string hashing for my fourth trick.
While this is not a novel concept, SILVERPICK offers some improvements over existing public implementations.
Firstly, we use the 64-bit variant of the popular FNV-1a non-cryptographic hash function in order to reduce the probability of a successful hash collision attack.
Secondly, we use a modified parameter for the hash function to defend against precalculated hash table lookups, such as HashDB. Crucially, this does not change the properties of the hash function.
To hash a short string at run time, simply use the HASH_STRING_RUN_TIME macro.
To hash a short string literal at compile time, simply use the HASH_STRING_COMPILE_TIME macro. Compile-time only evaluation is guaranteed via consteval.
It turns out that you can implement quite a handful of C Runtime Library (CRT) functions using x86 string instructions. So, of course, I had to implement them using a mix of compiler intrinsics and inline assembly.
Do you want to use the msvcrt!memset function in your code? Use the ZERO_MEMORY macro instead, which uses the rep stosb instruction emitted via a compiler intrinsic.
How about the msvcrt!memcpy function or the msvcrt!memmove function, you ask? Meet the COPY_MEMORY macro as a replacement, which uses the rep movsb instruction emitted via a compiler intrinsic.
But what about an alternative to the msvcrt!memcmp function? It turns out that there isn't exactly a compiler intrinsic available to emit the repe cmpsb instruction. So, we write a compare_memory function using inline assembly instead.
Finally, if you seek a replacement for the msvcrt!memchr function, meet the scan_memory function, which once again uses inline assembly since there is no compiler intrinsic available to emit the repne scasb instruction.
Oh, and did I forget to mention that you could write your own safer version of the msvcrt!strlen function using the scan_memory routine like so:
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);Please note that these implementations may not produce the most performant code, depending on the target CPU microarchitecture. However, they are guaranteed to get the job done.
Interested in more parlour tricks?
There are a lot of other small macros in Common.h that exist to abstract away some of the complexities of massaging the compiler.
A dependency-less implementation of the GetModuleHandle function is provided in UserModuleBase.cpp. To simplify ease-of-use, a helpful macro named GET_USER_MODULE_BASE has been created.
Similarly, a dependency-less implementation of the GetProcAddress function is provided in PEParse.cpp, which is then wrapped in a handy macro aptly named GET_EXPORTED_SYMBOL_ADDRESS. Furthermore, two more macros have been provided to aid in run-time dynamic linking - INITIALIZE_FUNCTION_POINTER to declare and initialize a function pointer to 0, and RESOLVE_FUNCTION_POINTER to resolve said function pointer.
Visual Studio Code integration is built into the project so that developers may use the Ctrl+Shift+B keyboard shortcut for a no-fuss build process.
GitHub Actions integration is also built into the project to enable CI builds.
The project takes some pride in its well-organized structure, as well as the thoroughly commented and relatively clean code.
Finally, take a gander at the project Makefile, which contains the choicest selection of compiler and linker flags that will generate small, secure, and OPSEC-friendly code. Meanwhile, verbose logging and a generated linker map file will provide visibility into the build process to facilitate a deeper understanding of the toolchain. Additionally, each translation unit also produces a disassembly file that, upon inspection, will often make you say things like "the compiler did what now?", etc.
If you are sold on the framework, this section describes how you would make use of it.
The following is an excerpt taken from 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;
}Seems easy enough, no?
While writing PIC in C/C++ using the SILVERPICK framework, you have to take into consideration the following rules:
- Treat the
payloadfunction as you would themainfunction of a traditional program, i.e., as the (pseudo) entry point. - All string literals must be declared as stack strings.
- Global variables may not be used anywhere in the code.
Windows APIorNative APIfunctions may be used only via run-time dynamic linking after ensuring that the function prototype is available in the corresponding header file.
The section contains a non-exhaustive list of planned enhancements that will be integrated into a future project.
- Switch to
Clang/LLVMtoolchain. - Switch to a different build system.
- Compile-time string obfuscation that works with stack strings and is resistant to FLOSS.
- Compile-time string hashing using seeded non-cryptographic custom hash function.
- Alternate method to retrieve
GSsegment base address. - Capability to bypass
Export Address Filtering (EAF)exploit mitigation.
The following is a list of references arranged in chronological order that proved invaluable to me during my research and were extensively used as inspiration for this project:
-
Writing Shellcode with a C Compiler by Nick Harbour (2010)
-
Shellcode with a C-compiler by Didier Stevens (2010)
-
Writing Optimized Windows Shellcode in C by Matt Graeber (2013)
-
Shellcode the better way, or how to just use your compiler by Justin Fisher (2016)
-
ShellcodeStdio by Jack Ullrich (2016)
-
Shellcode: A Windows PIC using RSA-2048 key exchange, AES-256, SHA-3 by Odzhan (2016)
-
Writing Optimized Windows Shellcode by Dimitri Fourny (2017)
-
Writing and Compiling Shellcode in C by Aleksandra Doniec and Mantvydas Baranauskas (2021)
-
Creating Shellcode from any Code Using Visual Studio and C++ by Hamid Memar (2021)
-
Writing Optimized Windows Shellcode in C by Philip Woldhek (2021)
-
From C, with inline assembly, to shellcode by Steve Salinas (2023)
-
How To Craft Your Own Windows x86/64 Shellcode with Visual Studio by Yazid Benjamaa (2023)
-
Modern implant design: position independent malware development by Paul Ungur (2024)
-
From C to shellcode (simple way) by Print3M (2024)
-
relocatable by Tijme Gommers (2025)
-
PIC Development Crash Course by Raphael Mudge (2025)