
Фреймворк для разработки шеллкода в пользовательском режиме Windows (WUMSDF)
v1.1Проект SILVERPICK — это Windows User-Mode Shellcode Development Framework (WUMSDF), единственная цель которого — дать разработчикам специализированных средств простой способ создавать Position Independent Code (PIC)-заготовки для Windows x64 на C/C++, снижая тем самым стоимость разработки таких решений.
Он происходит от проекта WILDBEAST и, соответственно, использует:
Visual Studio Code в качестве редактора кодаMinGW-w64 в качестве компилятораGNU Make в качестве системы сборкиИнструкции по настройке вы можете найти здесь: GCC-Clang-Setup-Windows
Обратите внимание, что в этом проекте используется MSYS2.
Написание шеллкода на языках программирования высокого уровня — не новость, и с 2010 года об этом опубликовано бесчисленное множество постов в блогах и исследовательских работ. Так что же нового в SILVERPICK?
Что ж, рад, что вы спросили.
У SILVERPICK в рукаве припрятано несколько неплохих трюков, но самое главное — это моё видение этой темы.
Итак, без лишних предисловий, представляю вам мой первый трюк.
С тех пор как Мэтт Грэбер популяризировал написание шеллкода на C, большинство людей используют его 16-байтовый стаб выравнивания стека, написанный на Assembly.
Хотя это и не проблема, но, поскольку мы не IKEA, ассемблер не должен быть обязательным — и, действительно, это так.
Существует GCC Function Attribute, который сам сгенерирует стаб выравнивания стека.
Встречайте атрибут функции 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, спросите вы? Что ж, это отличный переход к моему второму трюку.
Возможно, Мэтт Грэбер в своё время популяризировал написание шеллкода на C, но на самом деле именно Пол Унгури возродил это чёрное искусство в Stardust.
В Stardust используется скрипт компоновщика Binutils, чтобы управлять размещением функций и данных в нужной секции PE в правильном порядке. Сама эта техника основана на работе Остина Хадсона, и многие используют вариант его скриптов компоновщика.
Хотя скрипты компоновщика отлично подходят для упорядочивания секций, они не нужны, если вам нужно лишь поместить определённую функцию в начало секции кода.
Представляю вам атрибут функции 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" };
Однако обе эти техники становятся бесполезными перед лицом оптимизаций компилятора, если строковые литералы достаточно велики. В отличие от нашего решения, которое работает независимо от длины строки и уровня оптимизаций, — благодаря одному остроумному трюку с шаблонным метапрограммированием на 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 предлагает некоторые улучшения по сравнению с существующими публичными реализациями.
Во-первых, мы используем 64-битный вариант популярной некриптографической хеш-функции FNV-1a, чтобы снизить вероятность успешной атаки на коллизии хеша.
Во-вторых, мы используем модифицированный параметр хеш-функции для защиты от поиска по предвычисленным хеш-таблицам, например HashDB. Важно: это не меняет свойства хеш-функции.
Для хеширования короткой строки во время выполнения просто используйте макрос HASH_STRING_RUN_TIME.
Для хеширования короткого строкового литерала на этапе компиляции просто используйте макрос HASH_STRING_COMPILE_TIME. Вычисление только на этапе компиляции гарантируется с помощью consteval.
Оказывается, с помощью строковых инструкций x86 можно реализовать целый ряд функций C Runtime Library (CRT). Поэтому, разумеется, мне пришлось реализовать их, используя сочетание встроенных функций компилятора и встроенного ассемблера.
Хотите использовать функцию msvcrt!memset в своём коде? Вместо этого используйте макрос ZERO_MEMORY, который использует инструкцию rep stosb, генерируемую встроенной функцией компилятора.
А как насчёт функций msvcrt!memcpy или msvcrt!memmove, спросите вы? В качестве замены — макрос COPY_MEMORY, который использует инструкцию rep movsb, генерируемую встроенной функцией компилятора.
А как насчёт альтернативы функции msvcrt!memcmp? Оказывается, встроенной функции компилятора для генерации инструкции repe cmpsb не существует. Поэтому мы пишем функцию compare_memory с использованием встроенного ассемблера.