
Windows 用户模式 Shellcode 开发框架 (WUMSDF)
v1.1项目 SILVERPICK 是一个 Windows 用户态 Shellcode 开发框架 (WUMSDF),其唯一目的是让能力开发者能够以简单的方式使用 C/C++ 为 Windows x64 构建 位置无关代码 (PIC) 二进制块,从而降低此类开发工作的成本。
它衍生自项目 WILDBEAST,因此依赖:
Visual Studio Code 作为代码编辑器MinGW-w64 工具链作为编译工具链GNU Make 作为构建系统你可以从这里找到环境搭建说明:GCC-Clang-Setup-Windows
请注意,该项目使用 MSYS2。
使用高级编程语言编写 shellcode 并不是什么新鲜事,自 2010 年以来,已有无数博客文章和研究论文对此进行过讨论。那么,SILVERPICK 有什么新意呢?
嗯,很高兴你问这个问题。
SILVERPICK 的袖子里藏着一袋小妙招,但最重要的是,这是我对这个主题的个人见解。
那么,闲话少说,下面为你呈上我的第一个妙招。
自从 Matt Graeber 推广用 C 编写 shellcode 以来,大多数人一直在使用他的 用 汇编 语言编写的 16 字节栈对齐桩代码。
虽然这本身不是问题,但既然我们不是 IKEA,汇编就不应该是必需的,事实上也确实不是。
GCC 函数属性 中有一个属性可以替你生成栈对齐桩代码。
向你介绍 force_align_arg_pointer 函数属性,它以一个实用的 ALIGN_STACK 宏的形式呈现,可生成以下汇编代码:
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
你可能会问,.init 节是什么?好吧,这恰好是引出我第二个技巧的绝佳过渡。
Matt Graeber 或许在某个时期推广了用 C 编写 shellcode,但实际上,真正复兴这一黑色艺术的是 Paul Ungur,他带来了 Stardust。
如今,Stardust 使用 Binutils 链接器脚本 来控制函数和数据以正确的顺序放入合适的 PE 节。该技术本身源于 Austin Hudson 的工作,许多人都在使用他的链接脚本的变体。
虽然链接器脚本非常适合控制链接器节的排序,但如果你的需求仅仅是将某个函数放在代码节的开头,那它们就是多余的。
这时就该 section 函数属性登场了,它使用一个名为 .init 的特殊节名,向链接器表明该函数包含 main() 之前的运行时初始化代码,并且必须在链接顺序中排在 第一位。
为此,CODE_BEGIN 宏应运而生。
我的第三个技巧是 STACK_STRING 宏。
在 C 中,你可以通过将字符串字面量声明为 ANSI 字符数组来创建栈字符串(一种在栈上动态构建的字符串):
char charrHelloKitty[] = { 'H', 'e', 'l', 'l', 'o', 'K', 'i', 't', 't', 'y', '\0' };
在 C++ 中,你可以只需将 char 数组标记为 constexpr 来创建栈字符串:
constexpr char charrHelloKitty[]{ "HelloKitty" };
然而,面对编译器优化时,这两种技术_如果_字符串字面量足够大,都会变得毫无用处;而我们的方案则不同,得益于 Can Bölük 所贡献的巧妙 C++ 模板元编程技巧,无论字符串长度如何、编译器优化级别多高,它都能正常工作。
使用这个宏非常简单:
STACK_STRING(sstrText, "an extra long hello world!");
STACK_STRING(sstrCaption, "Demo");
MessageBoxA(nullptr, sstrText.data(), sstrCaption.data(), MB_OK);
这将生成以下汇编代码:
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'
说到 C++,我的第四个技巧是编译期字符串哈希。
虽然这并非新概念,但 SILVERPICK 在现有公开实现的基础上提供了一些改进。
首先,我们使用流行的 FNV-1a 非加密哈希函数的 64 位变体,以降低哈希碰撞攻击成功的概率。
其次,我们为该哈希函数修改了一个参数,以抵御预计算哈希表查找,例如 HashDB。关键的是,这_不会_改变哈希函数的特性。
要在运行时哈希一个短字符串,只需使用 HASH_STRING_RUN_TIME 宏。
要在编译期哈希一个短字符串字面量,只需使用 HASH_STRING_COMPILE_TIME 宏。通过 consteval 保证仅进行编译期求值。
事实证明,使用 x86 字符串指令可以实现相当多的 C 运行时库 (CRT) 函数。所以,我当然要用编译器内建函数和内联汇编的混合方式来实现它们。
你想在代码中使用 msvcrt!memset 函数?请改用 ZERO_MEMORY 宏,它通过编译器内建函数生成 rep stosb 指令。
那 msvcrt!memcpy 或 msvcrt!memmove 函数呢?COPY_MEMORY 宏可以作为替代品,它通过编译器内建函数生成 rep movsb 指令。
但如果需要替代 msvcrt!memcmp 函数该怎么办?事实证明,并没有现成的编译器内建函数可以生成 repe cmpsb 指令。因此,我们改用内联汇编编写了一个 compare_memory 函数。
最后,如果你在寻找 msvcrt!memchr 函数的替代品,可以认识一下 scan_memory 函数,它同样使用内联汇编,因为没有可用的编译器内建函数来生成 repne scasb 指令。
哦,我是不是忘了提到,你还可以使用 scan_memory 例程编写自己的更安全的 msvcrt!strlen 版本,如下所示:
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);
请注意,这些实现可能不会产生最高性能的代码,具体取决于目标 CPU 微架构。不过,它们保证能完成任务。
对其他小把戏感兴趣吗?
Common.h 中还有许多其他小宏,用于抽象掉一些驾驭编译器的复杂性。
UserModuleBase.cpp 中提供了 GetModuleHandle 函数的无依赖实现。为简化使用,创建了一个名为 GET_USER_MODULE_BASE 的实用宏。
类似地,PEParse.cpp 中提供了 GetProcAddress 函数的无依赖实现,并被封装在一个恰如其分的便捷宏 GET_EXPORTED_SYMBOL_ADDRESS 中。此外,还提供了另外两个宏来辅助运行时动态链接——INITIALIZE_FUNCTION_POINTER 用于声明并将函数指针初始化为 0,RESOLVE_FUNCTION_POINTER 用于解析该函数指针。
项目内置了 Visual Studio Code 集成,开发者可以使用 Ctrl+Shift+B 快捷键进行无干扰的构建。
项目还内置了 GitHub Actions 集成,以支持 CI 构建。
这个项目对其组织良好的结构以及注释详尽、相对干净的代码感到自豪。
最后,不妨看一眼项目的 Makefile,其中精选了编译器与链接器标志,能够生成小巧、安全且对 OPSEC 友好的代码。同时,详细日志和生成的链接器映射文件将提供构建过程的可见性,帮助你更深入地理解工具链。此外,每个编译单元还会生成一个反汇编文件,查看时往往会让你感叹“编译器刚才到底干了什么?”等等。
如果你已被这个框架打动,本节将介绍如何使用它。
以下是摘自 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;
}
看起来很简单,不是吗?
在使用 SILVERPICK 框架以 C/C++ 编写 PIC 时,你需要考虑以下规则:
payload 函数视为传统程序中的 main 函数,即 (伪)入口点。Windows API 或 Native API 函数只能通过 运行时动态链接 使用,并需确保相应的头文件中提供了该函数原型。本节包含一个非详尽的计划增强列表,这些增强将集成到未来的项目中。
Clang/LLVM 工具链。GS 段基址的替代方法。导出地址过滤 (EAF) 利用缓解措施的能力。以下是一份按时间顺序排列的参考文献列表,它们在我的研究过程中被证明极具价值,并广泛启发了我这个项目:
使用 C 编译器编写 Shellcode 作者:Nick Harbour (2010)
用 C 编译器编写 Shellcode 作者:Didier Stevens (2010)
使用 C 编写优化的 Windows Shellcode 作者:Matt Graeber (2013)
更好的 Shellcode 编写方式,或者说如何直接使用你的编译器 作者:Justin Fisher (2016)
ShellcodeStdio 作者:Jack Ullrich (2016)
Shellcode:使用 RSA-2048 密钥交换、AES-256、SHA-3 的 Windows PIC 作者:Odzhan (2016)
编写优化的 Windows Shellcode 作者:Dimitri Fourny (2017)
使用 C 编写和编译 Shellcode 作者:Aleksandra Doniec 和 Mantvydas Baranauskas (2021)
使用 Visual Studio 和 C++ 从任意代码创建 Shellcode 作者:Hamid Memar (2021)
使用 C 编写优化的 Windows Shellcode 作者:Philip Woldhek (2021)
从 C、通过内联汇编,到 shellcode 作者:Steve Salinas (2023)
如何使用 Visual Studio 打造你自己的 Windows x86/64 Shellcode 作者:Yazid Benjamaa (2023)
现代植入体设计:位置无关恶意软件开发 作者:Paul Ungur (2024)
从 C 到 shellcode(简单方法) 作者:Print3M (2024)
让我感到奇怪的是 Microsoft Defender 将其归类为 VirTool:Win64/Silepesz.A。
以下是该签名覆盖的字节:
[+] 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
这可以还原为以下源代码:
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
毋庸置疑,这是一种极其脆弱的检测方式,只能检测出示例中展示的完全相同的样例代码。不过,它的确凸显了使用多态 API 哈希的重要性。
relocatable 作者:Tijme Gommers (2025)
PIC 开发速成课程 作者:Raphael Mudge (2025)
scfw 作者:Petr Beneš (2026)