
Фреймворк для разработки шеллкода в пользовательском режиме Windows (WUMSDF)
# SILVERPICK
## ВЕРСИЯ
- `v1.1`
## ОПИСАНИЕ
Проект `SILVERPICK` — это `Windows User-Mode Shellcode Development Framework (WUMSDF)`, единственная цель которого — дать разработчикам специализированных средств простой способ создавать `Position Independent Code (PIC)`-заготовки для `Windows` `x64` на `C/C++`, снижая тем самым стоимость разработки таких решений.
Он происходит от проекта [WILDBEAST](https://github.com/winterknife/WILDBEAST) и, соответственно, использует:
1. `Visual Studio Code` в качестве редактора кода
2. инструментальный набор `MinGW-w64` в качестве компилятора
3. `GNU Make` в качестве системы сборки
## УСТАНОВКА
Инструкции по настройке вы можете найти здесь: [GCC-Clang-Setup-Windows](https://gist.github.com/winterknife/0b177a75a55bad895b19aad64cffa14f)
Обратите внимание, что в этом проекте используется [MSYS2](https://www.msys2.org/).
## ВОЗМОЖНОСТИ
Написание шеллкода на языках программирования высокого уровня — не новость, и с 2010 года об этом опубликовано бесчисленное множество постов в блогах и исследовательских работ. Так что же нового в `SILVERPICK`?
Что ж, рад, что вы спросили.
У `SILVERPICK` в рукаве припрятано несколько неплохих трюков, но самое главное — это моё видение этой темы.
Итак, без лишних предисловий, представляю вам мой первый трюк.
### ТРЮК 01
С тех пор как Мэтт Грэбер популяризировал написание шеллкода на `C`, большинство людей используют его [16-байтовый стаб выравнивания стека, написанный на `Assembly`](https://github.com/mattifestation/PIC_Bindshell/blob/master/PIC_Bindshell/AdjustStack.asm).
Хотя это и не проблема, но, поскольку мы не `IKEA`, ассемблер не должен быть обязательным — и, действительно, это так.
Существует [`GCC` Function Attribute](https://gcc.gnu.org/onlinedocs/gcc/Function-Attributes.html), который сам сгенерирует стаб выравнивания стека.
Встречайте атрибут функции `force_align_arg_pointer` в виде удобного макроса [ALIGN_STACK](https://github.com/winterknife/silverpick/blob/master/Inc/Common.h#L84), который генерирует следующий ассемблерный код:
```asm
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`, спросите вы? Что ж, это отличный переход к моему второму трюку.
### ТРЮК 02
Возможно, Мэтт Грэбер в своё время популяризировал написание шеллкода на `C`, но на самом деле именно Пол Унгури возродил это чёрное искусство в [Stardust](https://github.com/Cracked5pider/Stardust).
В `Stardust` используется [скрипт компоновщика Binutils](https://sourceware.org/binutils/docs/ld/Scripts.html), чтобы управлять размещением функций и данных в нужной секции `PE` в правильном порядке. Сама эта техника основана на работе Остина Хадсона, и многие используют вариант его скриптов компоновщика.
Хотя скрипты компоновщика отлично подходят для упорядочивания секций, они не нужны, если вам нужно лишь поместить определённую функцию в начало секции кода.
Представляю вам атрибут функции `section` с особым именем секции `.init`, который сообщает компоновщику, что функция содержит код инициализации времени выполнения до `main()` и должна быть _первой_ в порядке компоновки.
Для этой цели был создан макрос [CODE_BEGIN](https://github.com/winterknife/silverpick/blob/master/Inc/Common.h#L81).
### ТРЮК 03
Для своего третьего трюка представляю вам макрос [STACK_STRING](https://github.com/winterknife/silverpick/blob/master/Inc/StackString.h#L35).
В `C` вы можете создать стековую строку (строку, динамически формируемую на стеке), объявив строковый литерал как массив символов `ANSI`:
```c
char charrHelloKitty[] = { 'H', 'e', 'l', 'l', 'o', 'K', 'i', 't', 't', 'y', '\0' };
```
В `C++` вы можете создать стековую строку, просто пометив массив `char` как `constexpr`:
```cpp
constexpr char charrHelloKitty[]{ "HelloKitty" };
```
Однако обе эти техники становятся бесполезными перед лицом оптимизаций компилятора, _если_ строковые литералы достаточно велики. В отличие от нашего решения, которое работает независимо от длины строки и уровня оптимизаций, — благодаря одному остроумному трюку с шаблонным метапрограммированием на `C++` от Джана Бёлюка.
Использовать этот макрос довольно просто:
```c
STACK_STRING(sstrText, "an extra long hello world!");
STACK_STRING(sstrCaption, "Demo");
MessageBoxA(nullptr, sstrText.data(), sstrCaption.data(), MB_OK);
```
Это приведёт к генерации следующего ассемблерного кода:
```asm
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'
```
### ТРЮК 04
Раз уж речь зашла о `C++`, представляю вам хеширование строк на этапе компиляции — это мой четвёртый трюк.
Хотя это не новая концепция, `SILVERPICK` предлагает некоторые улучшения по сравнению с существующими публичными реализациями.
Во-первых, мы используем 64-битный вариант популярной некриптографической хеш-функции `FNV-1a`, чтобы снизить вероятность успешной атаки на коллизии хеша.
Во-вторых, мы используем модифицированный параметр хеш-функции для защиты от поиска по предвычисленным хеш-таблицам, например [HashDB](https://github.com/OALabs/hashdb). Важно: это _не_ меняет свойства хеш-функции.
Для хеширования короткой строки во время выполнения просто используйте макрос [HASH_STRING_RUN_TIME](https://github.com/winterknife/silverpick/blob/master/Inc/HashString.h#L45).
Для хеширования короткого строкового литерала на этапе компиляции просто используйте макрос [HASH_STRING_COMPILE_TIME](https://github.com/winterknife/silverpick/blob/master/Inc/HashString.h#L42). Вычисление только на этапе компиляции гарантируется с помощью `consteval`.
### ТРЮК 05
Оказывается, с помощью строковых инструкций `x86` можно реализовать целый ряд функций `C Runtime Library (CRT)`. Поэтому, разумеется, мне пришлось реализовать их, используя сочетание встроенных функций компилятора и встроенного ассемблера.
Хотите использовать функцию `msvcrt!memset` в своём коде? Вместо этого используйте макрос [ZERO_MEMORY](https://github.com/winterknife/silverpick/blob/master/Inc/Common.h#L123), который использует инструкцию `rep stosb`, генерируемую встроенной функцией компилятора.
А как насчёт функций `msvcrt!memcpy` или `msvcrt!memmove`, спросите вы? В качестве замены — макрос [COPY_MEMORY](https://github.com/winterknife/silverpick/blob/master/Inc/Common.h#L126), который использует инструкцию `rep movsb`, генерируемую встроенной функцией компилятора.
А как насчёт альтернативы функции `msvcrt!memcmp`? Оказывается, встроенной функции компилятора для генерации инструкции `repe cmpsb` не существует. Поэтому мы пишем функцию [compare_memory](https://github.com/winterknife/silverpick/blob/master/Inc/Common.h#L141) с использованием встроенного ассемблера.
Наконец, если вам нужна замена функции `msvcrt!memchr`, познакомьтесь с функцией [scan_memory](https://github.com/winterknife/silverpick/blob/master/Inc/Common.h#L167), которая опять же использует встроенный ассемблер, поскольку встроенной функции компилятора для генерации инструкции `repne scasb` не существует.
И да, чуть не забыл упомянуть: вы можете написать собственную более безопасную версию функции `msvcrt!strlen`, используя процедуру `scan_memory`, например так:
```cpp
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`. Однако они гарантированно выполняют поставленную задачу.
### ТРЮК 06
Интересуют ещё фокусы?
В [Common.h](https://github.com/winterknife/silverpick/blob/master/Inc/Common.h) есть множество других небольших макросов, которые абстрагируют некоторые сложности, связанные с «приручением» компилятора.
Реализация функции [GetModuleHandle](https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulehandlea) без зависимостей находится в [UserModuleBase.cpp](https://github.com/winterknife/silverpick/blob/master/Src/UserModuleBase.cpp). Для удобства использования создан полезный макрос [GET_USER_MODULE_BASE](https://github.com/winterknife/silverpick/blob/master/Inc/UserModuleBase.h#L36).
Аналогично, реализация функции [GetProcAddress](https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getprocaddress) без зависимостей находится в [PEParse.cpp](https://github.com/winterknife/silverpick/blob/master/Src/PEParse.cpp), которая затем обёрнута в удобный макрос с точным названием [GET_EXPORTED_SYMBOL_ADDRESS](https://github.com/winterknife/silverpick/blob/master/Inc/PEParse.h#L36). Кроме того, предоставлены ещё два макроса, помогающих с динамическим связыванием во время выполнения: [INITIALIZE_FUNCTION_POINTER](https://github.com/winterknife/silverpick/blob/master/Inc/PEParse.h#L39) для объявления и инициализации указателя на функцию нулём и [RESOLVE_FUNCTION_POINTER](https://github.com/winterknife/silverpick/blob/master/Inc/PEParse.h#L42) для разрешения указанного указателя на функцию.
В проект встроена [интеграция с Visual Studio Code](https://github.com/winterknife/silverpick/blob/master/.vscode), чтобы разработчики могли использовать сочетание клавиш `Ctrl+Shift+B` для простой сборки без лишних хлопот.
В проект также встроена [интеграция с GitHub Actions](https://github.com/winterknife/silverpick/blob/master/.github/workflows/build.yml) для поддержки `CI`-сборок.
Проект по праву гордится своей хорошо организованной структурой, а также тщательно прокомментированным и относительно чистым кодом.
Наконец, взгляните на [Makefile](https://github.com/winterknife/silverpick/blob/master/Makefile) проекта: в нём подобран отборный набор флагов компилятора и компоновщика, которые генерируют небольшой, безопасный и дружественный к `OPSEC` код. При этом подробное журналирование и генерируемый файл карты компоновщика обеспечат наглядность процесса сборки и помогут глубже понять инструментарий. Кроме того, каждая единица трансляции также создаёт файл дизассемблирования, глядя на который вы часто будете восклицать что-то вроде «что это компилятор сделал?» и так далее.
## ИСПОЛЬЗОВАНИЕ
Если вас заинтересовал этот фреймворк, в этом разделе описано, как им пользоваться.
Ниже приведён фрагмент из [PicMain.cpp](https://github.com/winterknife/silverpick/blob/master/Src/PicMain.cpp):
```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;
}
```
Выглядит достаточно просто, не так ли?
При написании `PIC` на `C/C++` с использованием фреймворка `SILVERPICK` необходимо учитывать следующие правила:
1. Относитесь к функции `payload` так же, как к функции `main` в традиционной программе, то есть как к _(псевдо)точке входа_.
2. Все строковые литералы должны быть объявлены как _стековые строки_.
3. _Глобальные переменные_ не должны использоваться нигде в коде.
4. Функции `Windows API` или `Native API` могут использоваться только через _динамическое связывание во время выполнения_, предварительно убедившись, что прототип функции доступен в соответствующем заголовочном файле.
## БУДУЩИЕ УЛУЧШЕНИЯ
Этот раздел содержит неполный список запланированных улучшений, которые будут интегрированы в будущий проект.
- [ ] Перейти на инструментальный набор `Clang/LLVM`.
- [ ] Перейти на другую систему сборки.
- [ ] Обфускация строк на этапе компиляции, совместимая со стековыми строками и устойчивая к [FLOSS](https://github.com/mandiant/flare-floss).
- [ ] Хеширование строк на этапе компиляции с использованием некриптографической пользовательской хеш-функции с сидом.
- [ ] Альтернативный способ получения базового адреса сегмента `GS`.
- [ ] Возможность обхода защитной меры `Export Address Filtering (EAF)`.
## ИСТОЧНИКИ
Ниже приведён список ссылок в хронологическом порядке, которые оказались для меня бесценными во время исследования и послужили источником вдохновения для этого проекта:
1. [Написание шеллкода с помощью компилятора C](https://nickharbour.wordpress.com/2010/07/01/writing-shellcode-with-a-c-compiler/) — Nick Harbour (2010)
2. [Шеллкод с помощью компилятора C](https://blog.didierstevens.com/programs/shellcode/) — Didier Stevens (2010)
3. [Написание оптимизированного шеллкода для Windows на C](https://web.archive.org/web/20201202085848/http://www.exploit-monday.com/2013/08/writing-optimized-windows-shellcode-in-c.html) — Matt Graeber (2013)
4. [Шеллкод правильным способом, или как просто использовать компилятор](https://phrack.org/issues/69/4) — Justin Fisher (2016)
5. [ShellcodeStdio](https://winternl.com/shellcodestdio/) — Jack Ullrich (2016)
6. [Шеллкод: Windows PIC с использованием обмена ключами RSA-2048, AES-256, SHA-3](https://web.archive.org/web/20240316160314/https://modexp.wordpress.com/2016/12/26/windows-pic/) — Odzhan (2016)
7. [Написание оптимизированного шеллкода для Windows](https://dimitrifourny.github.io/2017/04/28/optimized-shellcode.html) — Dimitri Fourny (2017)
8. [Написание и компиляция шеллкода на C](https://www.ired.team/offensive-security/code-injection-process-injection/writing-and-compiling-shellcode-in-c) — Aleksandra Doniec и Mantvydas Baranauskas (2021)
9. [Создание шеллкода из любого кода с помощью Visual Studio и C++](https://www.codeproject.com/articles/Creating-Shellcode-from-any-Code-Using-Visual-Stud#comments-section) — Hamid Memar (2021)
10. [Написание оптимизированного шеллкода для Windows на C](https://phasetw0.com/malware/writing-optimized-windows-shellcode-in-c/) — Philip Woldhek (2021)
11. [От C со встроенным ассемблером до шеллкода](https://steve-s.gitbook.io/0xtriboulet/archive/notice/just-malicious/from-c-with-inline-assembly-to-shellcode) — Steve Salinas (2023)
12. [Как создать собственный Windows x86/64 Shellcode с помощью Visual Studio](https://xacone.github.io/custom_shellcode.html) — Yazid Benjamaa (2023)
13. [Современный дизайн имплантов: разработка позиционно-независимого вредоносного ПО](https://5pider.net/blog/2024/01/27/modern-shellcode-implant-design) — Paul Ungur (2024)
14. [От C к шеллкоду (простой способ)](https://print3m.github.io/blog/from-c-to-shellcode) — Print3M (2024)
15. [relocatable](https://github.com/tijme/relocatable) — Tijme Gommers (2025)
16. [Ускоренный курс по разработке PIC](https://player.vimeo.com/video/1100089433) — Raphael Mudge (2025)
17. [scfw](https://github.com/vmi-rs/scfw) — Petr Beneš (2026)
## ЭРРАТА
[VirusTotal](https://www.virustotal.com/gui/file/5f7bf9375a206690bd0515db279f313a1ea2c520e627f1a341457838c6bb513e)
Что показалось мне странным, так это классификация `VirTool:Win64/Silepesz.A` антивирусом `Microsoft Defender`.
Вот байты, которые покрывает сигнатура:
```hex
[+] 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
```
Это соответствует следующему исходному коду:
```c
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`.