
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;