Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
MemFiles — Набор инструментов для CobaltStrike для записи файлов, создаваемых Beacon, в память вместо диска. | Kitploit
Инструменты/GitHubGitHub/octoberfest7/memfiles
Эксфильтрация данныхПост-эксплуатацияКомандование и УправлениеRed Teaming
GitHuboctoberfest7/memfiles

MemFiles

Набор инструментов для CobaltStrike для записи файлов, создаваемых Beacon, в память вместо диска.

Репозиторий
477622 лет назадПроверено Kitploit

Популярное

Смотреть все →

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

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться

MemFiles

ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ:

Этот проект сложен, и если не понять, как он работает, и не протестировать его должным образом, это может привести к краху Beacon'ов и потере доступа!

Настоятельно рекомендую прочитать всю документацию вплоть до раздела «Технические детали, проектные решения и комментарии»!

Введение

MemFiles — это набор инструментов для CobaltStrike, который позволяет операторам записывать файлы, создаваемые процессом Beacon, в память, а не на диск целевой системы. Он успешно протестирован на Windows 7, 10 и 11; соответствующие серверные версии должны работать без проблем. MemFiles ограничен x64 Beacon.

Это достигается за счёт перехвата нескольких различных NtAPI в NTDLL.dll и перенаправления вызовов этих API на функции, внедрённые в адресное пространство процесса Beacon.

MemFiles предполагает наличие чистой/неперехваченной копии NTDLL в процессе Beacon. Никаких гарантий работоспособности MemFiles в процессе Beacon, где всё ещё присутствуют перехваты EDR, не даётся. Восстановите/обновите NTDLL перед использованием MemFiles!

В составе MemFiles определена «особая», несуществующая директория; любые файлы, записываемые в эту особую директорию, перехватываются MemFiles и помещаются в память, откуда их затем можно скачать на Teamserver.

MemFiles совместим с большинством (но не со всеми) инструментов, которые работают внутри процесса Beacon и которые можно настроить на запись вывода в конкретную директорию. Для работы не требуются повышенные привилегии.

Сюда входят:
-BOF'ы
-.NET-сборки, запускаемые инлайн с помощью чего-то вроде inline-executeAssembly
-PE'шки, запускаемые инлайн с помощью чего-то вроде Inline-Execute-PE

Все они совместимы, потому что работают внутри процесса Beacon, где соответствующие NtAPI перехвачены.

MemFiles НЕ работает с такими вещами, как:
-execute-assembly
-shell
-run

Ни один из этих вариантов не совместим, поскольку все они порождают другие процессы, NtAPI которых НЕ перехвачены.

MemFiles успешно протестирован с такими инструментами, как Rubeus, SharpHound, Procdump и Powershell, при их запуске внутри процесса Beacon.

Установка

Клонируйте репозиторий и при желании измените переменную hookdir, которая определена в строке 56 в обоих файлах /PIC/Source/NtCreateFile.c и /PIC/Source/NtOpenFile.c. Эта переменная является «особой» директорией, которая сообщает MemFiles, что создаваемый файл следует перехватить. По умолчанию переменная hookdir установлена в "redteam". Убедитесь, что эта переменная не является реальной директорией на целевой системе и что она одинакова в обоих файлах!

image

Выполните 'make all', чтобы скомпилировать необходимые BOF'ы и PIC-функции.

Загрузите MemFiles.cna в клиент CobaltStrike. Убедитесь, что директория, из которой запущен CobaltStrike, доступна для записи вашим пользователем; MemFiles создаёт там текстовый файл (memfiles.txt), чтобы гарантировать доступность данных, необходимых для работы MemFiles.

MemFiles можно настроить на установку в каждый новый Beacon, который связывается с Teamserver; это делается с помощью пункта меню MemFiles->Config. По умолчанию MemFiles НЕ устанавливается автоматически в новые Beacon'ы. Обратите внимание, что это глобальная настройка; если к Teamserver подключены два клиента и в обоих загружен MemFiles.cna, то когда клиент A переключает настройку «Install on beacon initial», изменение также затронет клиента B!

image

Команды

MemFiles состоит из 4 команд, исполняемых на цели, которые запускают BOF'ы, и 1 внутренней команды, которая управляет структурой данных проекта.

Команды, исполняемые на цели:

  1. meminit
  2. memlist
  3. memfetch
  4. memclean

Внутренняя структура данных:

  1. memtable

meminit

meminit отвечает за установку MemFiles в процесс Beacon.

Список NtAPI, перехватываемых MemFiles, выглядит следующим образом:

  1. NtCreateFile
  2. NtWriteFile
  3. NtClose
  4. NtQueryVolumeInformationFile
  5. NtQueryInformationFile
  6. NtSetInformationFile
  7. NtOpenFile
  8. NtReadFile
  9. NtFlushBuffersFile

meminit выполняет следующие основные действия:

  1. Отправляет в Beacon позиционно-независимую функцию замены для каждого перехватываемого NtAPI
  2. Создаёт в памяти Beacon структуру для хранения различных значений, необходимых MemFiles на протяжении всего его жизненного цикла
  3. Патчит адрес этой структуры в каждую из PIC-функций замены
  4. Выделяет память и внедряет каждую PIC-функцию замены в память процесса Beacon
  5. Создаёт трамплин для каждого перехватываемого NtAPI
  6. Перехватывает каждый из перечисленных NtAPI, перезаписывая некоторые/все байты и перенаправляя выполнение на PIC-функцию замены.

memlist

memlist используется для отображения всех файлов, которые в данный момент хранятся в памяти MemFiles для конкретного Beacon.
image
Отображается несколько полей; наиболее значимые и интересные для пользователя — имя файла и длина хранящихся данных.

memfetch

memfetch используется для непосредственного получения файлов, хранящихся в памяти MemFiles для конкретного Beacon.

По умолчанию memfetch загружает все файлы, хранящиеся в MemFiles, чей «handle» был закрыт. Такое проектное решение было принято, чтобы избежать проблем, связанных с попыткой скачать файл, который программа/приложение ещё не закончила записывать.

Это означает, что если программа/приложение не закроет открытый ею handle файла, файл не будет скачан memfetch.
Эту проблему можно обойти с помощью аргумента "force" у memfetch, т.е. 'memfetch force', чтобы получить все файлы из памяти независимо от состояния их handle.

Файлы, которые memfetch извлекает из памяти, отправляются обратно на Teamserver как загрузка (download) и могут быть синхронизированы с Teamserver на клиент через вкладку Downloads в CobaltStrike.

После того как файл был загружен Teamserver'ом, он стирается из памяти процесса Beacon, а его запись, отображаемая через memlist, удаляется.

memclean

memclean отвечает за очистку и удаление MemFiles из процесса Beacon.

Стандартный сценарий использования MemFiles подразумевает его установку и оставление установленным на всё время жизни Beacon; однако если требуется использовать MemFiles совместно с инструментом для перехвата и получения файлового вывода, а затем удалить MemFiles, чтобы его артефакты не оставались в памяти, memclean можно использовать для возврата процесса Beacon в исходное состояние, в котором он был до запуска meminit.

Это включает:

  1. Снятие перехвата с каждого перехваченного NtAPI
  2. Обнуление и освобождение каждого созданного трамплина
  3. Обнуление и освобождение каждой внедрённой PIC-функции замены
  4. Обнуление и освобождение структуры MemFiles

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

memtable

memtable используется для отображения и отслеживания информации о Beacon'ах, в которых в данный момент установлен MemFiles. Он также отображает глобальную конфигурационную информацию.

У каждого клиента CobaltStrike свой memtable; MemFiles прилагает большие усилия для обеспечения синхронности своих данных между всеми подключёнными клиентами CobaltStrike, чтобы MemFiles могли использовать все операторы во всех Beacon'ах. Подробнее об этом см. «Проектные решения и комментарии».

image

Использование

Инициализируйте MemFiles в Beacon с помощью команды meminit. Это можно настроить на автоматическое выполнение, переключив опцию в меню MemFiles->Config.

image

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

SharpHound:

Здесь мы указываем, что SharpHound должен выводить все создаваемые файлы в директорию c:\redteam\ (нашу особую директорию MemFiles) и не упаковывать их в zip; MemFiles не поддерживает чтение файлов из памяти программами, только запись, поэтому функция zip в SharpHound не работает.

image

Rubeus:

Команда "dump" используется с Rubeus, и мы указываем ей отправлять весь вывод консоли в файл (расположенный в нашей особой директории)

image

Powershell:

В этом примере Inline-Execute-PE используется для загрузки powershell.exe в процесс Beacon и выполнения 'Get-ADUser' для получения списка доменных пользователей. С помощью конвейера и 'out-file' данные можно записать в память, а затем извлечь.

image

Когда вы хотите получить файлы, запустите memfetch:

image

Когда вы закончили работу с MemFiles и/или не хотите оставлять его установленным в процессе Beacon, запустите memclean:

image

Обратите внимание, что в приведённом выше примере был файл, который ещё не был скачан; memclean скачивает этот файл и стирает его из памяти перед удалением MemFiles.

Запросите статус и конфигурацию MemFiles с помощью memtable. Во время длительных операций очищайте записи о мёртвых/старых Beacon'ах из memtable, чтобы избежать беспорядка.

image

Возможности и ограничения

Как подчёркивалось во введении, для работы MemFiles требуется чистая копия NTDLL в процессе Beacon. Это необходимо, поскольку он читает исходные байты в NtFunction и копирует определённые из них в трамплин, который позже используется для выполнения обычных вызовов NtFunction, в которые MemFiles не должен вмешиваться. Эта тема подробнее раскрыта в разделе «Технические детали, проектные решения и комментарии».

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

Имя файла, хранящееся в структуре MemFiles, извлекается из аргумента, передаваемого в функцию замены NtCreateFile. MemFiles делает это довольно простым способом: находит «особую» директорию в аргументе пути к файлу, переходит к её концу, а затем увеличивает указатель на 1 с учётом символа '\', разделяющего «особую» директорию и имя файла. Например, в пути 'C:\users\tom\redteam\myfile.txt' MemFiles находит 'redteam', учитывает символ обратной косой черты и выбирает 'myfile.txt' в качестве имени файла.

MemFiles безразличны любые предшествующие директории в пути к файлу; 'C:\redteam\myfile.txt' и 'c:\users\tom\appdata\local\redteam\myfile.txt' являются одинаково допустимыми путями, насколько это касается MemFiles.

Учитывая, как MemFiles извлекает имена файлов из путей, следует отметить, что MemFiles не поддерживает создание поддиректорий; это означает, что MemFiles не будет корректно работать с инструментами, которые, например, пытаются создать c:\redteam\mynewdir\file1.txt, c:\redteam\mynewdir\file2.txt, c:\redteam\mysecondir\file3.txt и т.д.

Перехватываемые MemFiles NtAPI были определены как используемые различными программами для операций ввода-вывода. Как уже упоминалось, инструменты/возможности, успешно протестированные с MemFiles/этим набором перехватываемых NtAPI: SharpHound, Rubeus, Powershell, Procdump, BOF'ы, обычные C-программы, выполняющие операции записи файлов, и команда bupload_raw от CobaltStrike (которая позволяет оператору указать удалённое расположение файла). Безусловно, есть и другие инструменты, которые будут работать с MemFiles «из коробки»; другие окажутся несовместимыми.

На первый взгляд процесс создания файла в Windows выглядит прямолинейно: NtCreateFile->NtWriteFile->NtClose. Однако, углубившись в этот проект, я быстро обнаружил, что в нём задействован ряд других API, и, что самое интересное, набор этих API различается между программами. Некоторые программы в рамках своего процесса ввода-вывода вызывают Win32 API SetFilePointerEx, который, в свою очередь, вызывает NtAPI NtSetInformationFile. Другие, например .NET-программы, включая SharpHound, в конечном итоге вызывают NtFlushBuffersFile.

Отсутствие единой общей цепочки вызовов API для создания и записи файлов оставляет пространство для несовместимости в зависимости от инструмента, с которым используется MemFiles. Программа может вызвать другой NtAPI, который MemFiles не перехватывает, передав фиктивный handle, созданный MemFiles в функции замены NtCreateFile, что приведёт к ошибке «Invalid Handle» и остановке выполнения. Другие программы выполняют более сложные действия, которые MemFiles не в состоянии адекватно подменить/заменить. Одна из уже выявленных несовместимостей — ADExplorer.exe.

ADExplorer.exe — это подписанный бинарный файл Microsoft, используемый для перечисления Active Directory. Во время выполнения ADExplorer записывает данные в указанный выходной файл, а затем позже возвращается к нему и обращается к нему, прежде чем в конечном итоге записать окончательный вывод в файл в конце выполнения. Поскольку он использует выходной файл в качестве своего рода кэша, ему необходимо иметь возможность читать данные, которые он уже записал в файл, и крайне маловероятно, что он делает это каким-либо простым и предсказуемым образом.

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

ADExplorer создаёт ещё одну проблему — размер создаваемых файлов. В крупных корпоративных средах выходной файл может превышать 1 ГБ; хотя MemFiles программно должен справляться с этим, он определённо не предназначен для таких сценариев использования.

Несовместимые инструменты

Несомненно, сообщество обнаружит инструменты, с которыми MemFiles работает некорректно; я призываю вас открыть issue с описанием несовместимой программы/инструмента и обстоятельств, в которых вы его запускали, т.е. это BOF, запущенный через inline-executeAssembly, Inline-Execute-PE и т.д., чтобы я мог посмотреть, не удастся ли расширить MemFiles и заставить его работать.

IOC и AV/EDR

IOC, связанные с MemFiles, включают, но не ограничиваются:

Выделение памяти с помощью VirtualAlloc
Запись данных с помощью WriteProcessMemory
Изменение защиты памяти на выделенной памяти между RW и RX
Перезапись памяти в NTDLL.dll

AV/EDR

MemFiles не разрабатывался и не тестировался против полноценного EDR; в наличии был только Microsoft Defender. Тем не менее, я бы рискнул предположить, что инструмент/программа, которую Beacon запускает для создания файла, с большей вероятностью вызовет срабатывание, чем сам MemFiles, перехватывающий или хранящий этот файл в памяти. Перезапись памяти в NTDLL/перехват NtAPI, как мне кажется, может вызвать вопросы у некоторых продуктов, но у меня нет доказательств, подтверждающих это. Для вызовов перехваченных NtFunction, которые не касаются файлов, перехватываемых (или подлежащих перехвату) MemFiles, системный вызов по-прежнему выполняется из адресного пространства NTDLL.dll, поскольку продукты безопасности действительно обнаруживают и сигнализируют о системных вызовах, совершаемых из-за пределов этой области.

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

Технические детали, проектные решения и комментарии

С концепцией файловой системы в памяти меня впервые познакомил доклад на конференции несколько месяцев назад на Tradecraftcon от KFiveFour, где докладчик (@DexterGerig) продемонстрировал POC, создающий файловую систему в памяти на основе модели «клиент-сервер». Половина задуманного мной функционала такого проекта была реализована в моём предыдущем крупном релизе, Inline-Execute-PE. Вторая половина — идея перехватывать файлы, создаваемые инструментами, и хранить их в памяти, а не на диске — не была реализована в том проекте и оставалась крайне желаемой возможностью по очевидным причинам.

MemFiles был для меня невероятно сложной задачей, поскольку до этого проекта я проводил очень мало времени в отладчике, не понимал ассемблер и не разбирался в перехвате API. В ходе проекта я столкнулся с несколькими препятствиями, каждое из которых занимало по 10–20 часов, и, к счастью, благодаря упорству мне удалось их преодолеть. Хотя это, вероятно, не самый эффективный путь, я приобрёл большой опыт работы с отладчиками и лучше понял, как компьютеры работают «под капотом»: ассемблер, регистры, стек и соглашения о вызовах.

Далее следует технический разбор некоторых наиболее важных технических деталей и проектных решений, заложенных в MemFiles.

Файлы в Windows и как работает MemFiles

Создание файла в Windows начинается с NtCreateFile, которому передаётся путь к требуемому файлу; в ответ Windows создаёт файл по этому пути и возвращает handle. Возвращённый handle используется во всех последующих вызовах, связанных с файлом, например в NtWriteFile и NtClose.

Размышляя о том, как разделить вызовы всех этих API на те, которые мы хотим перехватывать и изменять, и те, которые мы хотим оставить в покое, я остановился на поиске ключевого слова в вызове NtCreateFile. Это было реализовано путём указания уникальной, несуществующей директории как части пути к файлу в вызове NtCreateFile. Когда наш хук перенаправляет выполнение на функцию замены NtCreateFile, путь к файлу, переданный в качестве аргумента в NtCreateFile, проверяется на наличие этого уникального «ключевого слова»; если оно найдено, MemFiles понимает, что этот вызов NtCreateFile относится к файлу, который должен быть помещён в память, а не на диск. В этом случае MemFiles инициализирует несколько переменных и выделяет начальный 1 МБ памяти для файла, но, что самое важное, связывает фиктивный handle с именем файла, указанным в структуре MemFiles, и возвращает этот фиктивный handle вызывающему коду.

Во всех остальных перехватываемых MemFiles NtAPI соответствующие функции замены NtFunction смотрят на переданный в качестве аргумента handle и проверяют, существует ли он в структуре MemFiles; если handle существует в структуре MemFiles (фиктивные handle, создаваемые MemFiles, достаточно ненастоящие, чтобы никогда не пересечься с реальным), MemFiles идентифицирует этот вызов как относящийся к файлу в памяти и действует соответствующим образом.### Теория перехвата и функции замены Чтобы записывать в память файлы, предназначенные для записи на диск, MemFiles должен перехватывать вызовы определённых API, которые программы совершают при попытке создать файл. Перехват API (API Hooking) существует уже очень давно и активно используется многими EDR-продуктами как ключевая часть их функциональности; вызовы определённых API перенаправляются в адресное пространство EDR, где выполняется анализ вызова API и переданных в него переменных. Если EDR определяет, что вызов вредоносный — например, является частью атакующего инструмента или цепочки kill chain, — он предотвращает завершение вызова и поднимает alert. Если EDR решает, что вызов безвреден, он возвращает исполнение обратно в ту точку, откуда оно было перенаправлено, и позволяет вызову API завершиться так, как и задумывалось. Простую аналогию можно найти в следующем: вы отправляете письмо другу, но прежде чем ваш друг получит его, третья сторона вскрывает письмо, читает его и решает, есть ли в нём что-то незаконное; в этом случае друг письмо так и не получает, а полиция получает уведомление.

MemFiles следует той же теории, но без уведомлений (и без теоретического участия полиции). Перехват API обычно реализуется на самом низком возможном уровне в userland — на уровне NtFunctions внутри NTDLL.dll. Давайте посмотрим на NtCreateFile до начала любого перехвата:

image

Все NtFunctions идентичны, за исключением номера syscall, который в данном примере равен 55. Номер syscall меняется между разными NtFunctions, и также следует отметить, что это число может меняться между версиями Windows; номер syscall для NtCreateFile в этой ОС (Windows 11) равен 55, однако в Windows 10 он может быть другим (и уж точно другим в Windows 7).

Стоит обратить внимание на инструкции TEST и JNE. Они существуют для определения того, должна ли NtFunction использовать обычную инструкцию syscall или устаревшую инструкцию INT 2E. Я процитирую пост Клезвируса SysWhispers is dead, long live SysWhispers!:

Теперь самое интересное: функция проверяет, установлено ли значение SharedUserData[0x308] (BYTE PTR DS:[7FFE0308]) в 1. SharedUserData — это символ, ссылающийся на структуру режима ядра KUSER_SHARED_DATA.

Структура KUSER_SHARED_DATA определяет фиксированное (или предопределённое) пространство памяти, используемое для обмена информацией с программами в user-mode. Это, разумеется, было сделано для того, чтобы определённая глобальная системная информация была готова к использованию пользовательским кодом без затрат на переключение между user-mode и kernel-mode при каждом обращении.

Значение по индексу 0x308 представляет собой инструкцию syscall, которая поддерживается во всех версиях Windows, начиная с 1511. Как можно догадаться, во всех версиях Windows до 1511 стандартным способом выполнения syscall был вызов прерывания int 2Eh.

...

Если вам интересно, почему этот int 2Eh всё ещё здесь, хотя Windows сейчас намного новее 1511, то причина в том, что эта инструкция всё ещё используется. Действительно, когда включена HVCI (Hypervisor-protected Code Integrity), SharedUserData[0x308] устанавливается в 0, и вместо инструкции syscall используется int 2Eh. Это сделано в основном из соображений производительности, из-за того, как выполняется переключение из Ring3 в Ring0 при использовании той или иной инструкции.

Я попросил дополнительных разъяснений по этой теме в Twitter, на что @yarden_shafir ответил следующее:

image

Короче говоря, каждая инструкция в NtFunction может когда-нибудь понадобиться (за возможным исключением многобайтового NOP в конце, который, судя по всему, недостижим), и если мы перезаписываем инструкции в NtFunction, нам нужно убедиться, что мы сохраняем их и выполняем в какой-то момент перед финальным syscall (или INT 2E, в зависимости от ситуации).

Стоит отметить, что перед выполнением syscall номер syscall перемещается в RAX (на скриншоте показан как EAX). Поскольку мы не видим, чтобы RAX перед этим сохранялся в стек, я (возможно, наивно) предположил, что значение, содержавшееся в RAX до перемещения туда номера syscall, не важно и не потребуется позже после выполнения syscall. Это хорошая новость, поскольку означает, что мы можем свободно использовать регистр RAX, если гарантируем, что перед выполнением syscall в нём будет находиться номер syscall.

Чтобы перенаправить исполнение на наш собственный код/заменяющую NtFunction, мы перезапишем часть оригинального NtAPI, переместив адрес нашей заменяющей NtFunction в RAX, а затем используя инструкцию JMP для перехода по этому адресу:

image

Для инструкций MOV и JMP требуется 12 байт; поскольку мы повреждаем другие инструкции, перезаписывая первые 12 байт NtAPI, эти инструкции заменяются NOP, чтобы сохранить правильные интервалы и выравнивание NtAPI.

Теперь, когда программа вызывает NtCreateFile, исполнение перепрыгнет к нашей заменяющей NtFunction.

Собственный код и заменяющие NtFunctions

MemFiles отклоняется от того, как EDR выполняют перехват, в отношении того, где располагаются заменяющие функции, на которые перенаправляются перехваченные API. Многие EDR загружают собственную DLL в процесс. Перехваченные API перенаправляются в адресное пространство этой загруженной DLL, где может проводиться анализ. Поскольку весь смысл этого проекта заключался в том, чтобы избегать записи чего-либо на диск, размещение DLL на диске и загрузка её нашим процессом Beacon для получения доступа к нашим заменяющим NtFunctions показались плохим путём. Существует POC нескольких лет давности, который позволяет загружать DLL из памяти; это жизнеспособная стратегия для наших нужд, но проект не поддерживается, и в нём, судя по всему, есть несколько проблем. Кроме того, это 1200 строк кода, и превратить его в формат BOF было бы той ещё задачей.

Небольшое отступление: наши заменяющие NtFunctions не могут находиться в BOF; CobaltStrike загружает, выполняет, а затем стирает BOF из памяти процесса после завершения их работы. Поскольку нам нужна постоянная функция (или функции) в памяти, которая может вызываться всякий раз, когда процесс вызывает один из перехваченных NtAPI, BOF не подойдут.

Ответом, к которому я пришёл, стал позиционно-независимый код (PIC). Как следует из названия, в отличие от обычных исполняемых файлов, которым требуется загрузка в определённое место/с определённым взаимным расположением частей, PIC можно разместить и запустить в любом месте памяти. Это открывает возможность писать наши заменяющие NtFunctions как PIC-исполняемые файлы, внедрять их в процесс Beacon и с помощью хуков перенаправлять исполнение на них при вызовах наших перехваченных NTAPI.

Шаблон для этих PIC NtFunctions взят из проекта Cracked5pider ShellcodeTemplate.

Одно заметное отклонение от базового проекта состоит в том, что базовый проект рассчитан на создание полноценного PIC exe; то есть в нём есть ASM для сохранения указателя стека, создания места в стеке, вызова назначенной заменяющей NtFunction, содержащейся внутри exe, и последующего восстановления указателя стека после возврата из этой функции. Инструкция call, выполняемая PIC exe, представляет проблему: она помещает адрес возврата туда, откуда был сделан вызов (в ASM PIC exe), в стек, что приведёт к тому, что встретившаяся позже инструкция ret вернёт исполнение обратно в PIC exe, а не вызывающему исходного NtAPI.

Чтобы устранить это, ASM-файл в проекте ShellcodeTemplate был отредактирован: убрана часть ASM, связанная с настройкой стека, вызовом функции и восстановлением указателя стека после завершения выполнения функции. В результате хук, установленный в NtAPI, теперь переводит исполнение напрямую в заменяющую NtFunction, при этом стек и регистры остаются в том состоянии, в котором они были при вызове исходного NtAPI программой (за исключением RAX, который используется для нашего JMP).

Оригинальный ASM ShellcodeTemplate:
image

ASM MemFiles:
image

Каждая перехваченная NtAPI имеет собственную PIC NtFunction, содержащую необходимую логику для:

A. Выполнения специфических для MemFiles действий, таких как создание фейкового хендла, запись данных в память, изменение переменных в структуре MemFiles и т.д.
или
B. Перенаправления исполнения на трамплин, который вернёт вызов API в нужное русло и возвратит его обратно в NTDLL, где будет выполнен syscall

Некоторые заменяющие NtFunctions сложнее других; когда вызов перехваченного NtAPI касается MemFiles, некоторые из них, например NtCreateFile и NtQueryVolumeInformationFile, изменяют переменные, переданные в качестве аргументов NtAPI, в соответствии с документацией MSDN, результатами тестирования и некоторыми догадками/здравым смыслом. Другие, такие как NtClose и NtReadFile, просто возвращают STATUS_SUCCESS исходному вызывающему, чтобы избежать неизбежной ошибки «Invalid Handle», которая иначе возникла бы при передаче фейкового хендла, созданного MemFiles.

Когда вызов перехваченного NtAPI НЕ касается MemFiles, нам нужно направить исполнение на трамплин, чтобы вернуть всё в нормальное русло:

image

Трамплины

Трамплин отвечает за выполнение всех инструкций, которые не были выполнены в оригинальном NtAPI из-за того, что этот API был перехвачен; это включает любые инструкции, частично или полностью перезаписанные первоначальным хуком. Перехват API может быстро привести к ситуациям, когда у нас недостаточно места для выполнения всех необходимых инструкций. Трамплины также помогают решить эту проблему, поскольку мы можем выполнить любое количество действий для настройки наших регистров и/или стека перед тем, как прыгнуть обратно в оригинальный NtAPI. Трамплин NtCreateFile можно увидеть ниже:

image

Самое очевидное — три инструкции, перезаписанные нашим первоначальным хуком, видны как первые три инструкции трамплина:

MOV R10, RCX
MOV EAX, 55
TEST BYTE PTR DS:[7FFE0308], 1

Как упоминалось ранее, главное требование во всех этих манипуляциях заключается в том, чтобы номер syscall (55 в приведённом выше примере) находился в RAX(EAX) перед выполнением syscall. Однако у нас есть проблема: нам всё ещё нужно использовать инструкцию JMP, чтобы вернуть исполнение в оригинальный NtAPI. Хотя, возможно, существует другой регистр, который не содержит важной информации и который можно было бы использовать для этого, я не нашёл стабильно безопасного варианта, учитывая количество перехватываемых NtAPI, каждый из которых может использовать регистры по-разному. Безопасный вариант — продолжать использовать RAX; для этого мы можем поместить номер syscall, хранящийся в RAX, в стек. Затем мы можем переместить адрес, по которому хотим прыгнуть обратно в оригинальный NtAPI, в RAX и использовать инструкцию JMP, чтобы вернуться в NTDLL:

image

В хук, установленный в NtAPI, включена инструкция POP RAX, и именно сюда мы прыгаем с помощью нашего трамплина. Выполнение этой инструкции восстанавливает номер syscall в RAX из вершины стека и подготавливает нас к выполнению syscall. Обратите внимание, что инструкция JNE из оригинального, не перехваченного NtAPI всё ещё здесь; соответствующая инструкция TEST, которая устанавливает флаг ZF и определяет, будет ли выполнен JNE (который перепрыгнет через syscall к INT 2E), была выполнена в трамплине. Такое устройство позволяет нам как успешно перехватывать NtAPI и перенаправлять исполнение на нашу заменяющую PIC NtFunction, так и гарантировать, что мы ничего не пропускаем и не теряем функциональность в результате перехвата.

Проблема «Телега впереди лошади»

Если кратко резюмировать то, что было описано выше: при инициализации MemFiles в памяти процесса Beacon создаётся структура, содержащая важную информацию для функционирования MemFiles. На информацию в этой структуре постоянно ссылаются на протяжении всего жизненного цикла MemFiles, включая каждую из заменяющих PIC NtFunction, а также BOF, используемые для запроса и получения файлов, хранящихся в памяти MemFiles. С этой целью при создании структуры адрес памяти, по которому она располагается, передаётся обратно на Teamserver:

image

После сохранения этого адреса в memtable последующие команды MemFiles (memlist, memfetch, memclean) отправляют этот адрес в качестве аргумента BOF, чтобы структуру можно было найти и ссылаться на неё. Но как PIC NtFunction находят структуру?

Очевидная проблема в том, что PIC NtFunction требуют адрес памяти структуры MemFiles, но они уже скомпилированы к моменту создания структуры. В ранней реализации MemFiles эта проблема решалась разделением инициализации MemFiles на два отдельных BOF. Первый создавал структуру и отправлял адрес обратно на Teamserver, который с помощью некоторой магии Aggressor-скриптов разбирал адрес, вставлял его в файл исходного кода каждой NtFunction, а затем перекомпилировал их в итоговую PIC NtFunction. Второй BOF затем передавал готовые PIC NtFunctions и выполнял собственно внедрение и перехват NtAPI.

Помимо уродливости и дополнительного времени, реальные проблемы могли возникнуть в ситуации, когда несколько Beacon пытаются инициализировать MemFiles одновременно. Если Beacon 2 выйдет на связь с адресом своей структуры MemFiles, пока Beacon 1 находится в процессе вставки и перекомпиляции файлов исходного кода NtFunction, всё может запутаться.

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

image

Эту переменную можно увидеть в скомпилированном коде с помощью такого инструмента, как xxd:

image

Когда выполняется команда meminit, каждая из PIC NtFunctions отправляется в Beacon вместе с BOF InstallHooks. Этот BOF отвечает за создание структуры MemFiles; после её создания он вызывает функцию patchAddr для каждой из PIC NtFunctions. patchAddr отвечает за поиск строки из букв A и замену её строковым представлением адреса структуры. По сути, используя адрес памяти из более раннего скриншота, переменная pFileInfoStr теперь выглядит так:

char* pFileInfoStr = GET_SYMBOL( "00000178BC106860" );

Это строковое представление затем можно преобразовать в фактическое шестнадцатеричное значение, которое и является нашим адресом памяти. BOF делают это довольно просто с помощью API _strtoi64:

image

Конечно, для PIC NtFunctions всё не могло быть настолько просто.

Этот короткий фрагмент ассемблера показывает конец одной из PIC NtFunctions. Обратите внимание на инструкцию JMP RAX, которая является вызовом трамплина из PIC NtFunction (то есть это вызов, в который MemFiles НЕ вмешивался/который MemFiles НЕ подделывал):

image

По неизвестным причинам, когда я пытался использовать _strtoi64 (или любые её родственные API, такие как stroull или atoll), эта инструкция JMP RAX превращалась в инструкцию CALL RAX. Я уверен, что для этого есть веская причина, связанная с каким-то глубоким уровнем «того, как работают компьютеры», но это, казалось бы, незначительное изменение ломает довольно много вещей. Потратив более 15 часов на поиски способа преобразовать строковое представление адреса структуры MemFiles в фактическое шестнадцатеричное значение, чтобы я мог использовать хранящиеся в нём значения, я наткнулся на этот пост на StackOverflow, в котором комментатор предоставил собственную процедуру, разработанную для микроконтроллеров, преобразующую строку в uint32; к счастью, она также работала с uint64 без изменений, и, что самое важное, сохраняла инструкцию JMP RAX дальше в PIC NtFunction, не превращая её в CALL. Финальный фрагмент:

image

Старый NtAPI против нового NtAPI

Ради простоты ранее об этом не упоминалось, но формат NtAPI менялся с годами. Более ранние версии, например те, что используются в Windows 7, значительно короче своих современных аналогов: всего 16 байт вместо 32:

image

Это требует изменений как в том, как MemFiles перехватывает NtAPI, так и в том, как строится трамплин. Чтобы MemFiles мог определить, с какой версией NtAPI он имеет дело, BOF InstallHooks сначала резолвит адрес интересующего NtAPI, а затем читает 32 байта из этого места. Из-за разницы в форматах инструкция syscall находится в разных местах между версиями NtAPI; проверяя наличие или отсутствие syscall по определённому смещению байта, MemFiles может определить, имеет ли он дело с современной или устаревшей реализацией NtAPI, и действовать соответствующим образом:

image

После перехвата устаревший API NtCreateFile выглядит так:

image

И трамплин, используемый для настройки регистров и последующего возврата в NtCreateFile:

image

В целом техника очень похожа, но всё гораздо плотнее и без особого пространства для манёвра. Стоит отметить, что для того, чтобы всё поместилось и работало должным образом, инструкцию syscall пришлось переместить внутри NtCreateFile; она по-прежнему находится в пространстве памяти API NtCreateFile в NTDLL, но сместилась с 9-го и 10-го байтов API на 14-й и 15-й байты API; многобайтовый NOP был принесён в жертву, чтобы должным образом всё втиснуть.

Поиск NtAPI, связанных с I/O

Некоторые API, связанные с файловыми операциями I/O, были интуитивно понятными, и их было легко идентифицировать и перехватить; другие оказались гораздо более неуловимыми и потребовали многих часов работы в WinDbg и x64dbg с пошаговым прохождением ассемблера, чтобы определить, какие API вызываются. Я должен верить, что существовал более эффективный способ выполнить эту задачу, но я учился по ходу дела.В качестве небольшого бонуса я решил описать процесс, который, как оказалось задним числом, должен был быть очень быстрым, — поиск NtAPI, мешавшего SharpHound работать в течение 20 часов. Будучи .NET-программой, SharpHound выдал очень некрасивый стек вызовов, целиком связанный с тем, что он определил: "The handle is invalid". Хотя .NET — дружелюбный к программистам язык, большая часть (или вся?) функциональности транслируется и в конечном счёте проходит через Win32 API (а значит, и NtAPI), где мы можем наблюдать и перехватывать её, как и всё остальное.

sharphounderror

Ошибка "Invalid Handle" была явной подсказкой, что SharpHound использует какой-то NtAPI, который я не перехватывал (в отличие от ситуации, когда одна из моих существующих PIC-функций Nt работала неправильно), поэтому я решил попытаться выяснить, что это за API. Методика, которую я использовал с другими инструментами до этого момента, заключалась в том, чтобы сначала установить точку останова на NtCreateFile и найти вызов, относящийся к моей "особой" директории. Затем я проходил программу по шагам (обычно несколько раз, потому что терялся) и смотрел, какие функции программа вызывает дальше. Именно следуя этой методике, я обнаружил, что NtQueryVolumeInformationFile, NtQueryInformationFile и NtSetInformationFile вызываются и нуждаются в перехвате.

SharpHound подкидывает дополнительные сложности тем, что выполняет многие свои задачи асинхронно. Из-за этого гораздо сложнее проследить линейные шаги, которые один файл проходит через вызовы API, поскольку одновременно через этот процесс проходит несколько файлов. Кроме того, я обнаружил, что при прохождении программы по шагам после вызова NtCreateFile поток, в котором был сделан этот вызов, в конечном счёте завершается; последующие вызовы NtWriteFile (и любые другие неизвестные вызовы API, которые были предметом этого поиска) происходят в другом потоке, что ещё больше запутывает процесс поиска.

Поскольку с .NET я имел дело лишь мельком, я не был силён в разборе стеков вызовов, порождаемых ошибками, тем более тех, которые становятся вдвое уродливее из-за асинхронности. В течение этих 20 часов, не видя прогресса от прежней стратегии, я раз за разом возвращался к этому стеку и постепенно, но верно начинал в нём разбираться. Примерно в третьем блоке сверху, отделённом строками "--- End of stack trace...", можно увидеть строку "at Sharphound.Writers.JsonDataWriter...". Это дало мне относительную отправную точку в реальном коде SharpHound, который открыт и доступен на Github. Как следовало из названия, функция SharpHound занималась записью JSON-вывода в файл; я уже знал, что мои данные не записываются в файл, так что это не было новостью. Поднявшись на один уровень вверх по стеку вызовов, я увидел следующую релевантную строку: "at System.IO.Streamwriter.". Префикс System.IO подсказал мне, что это встроенная функция .NET, а не специфичная для SharpHound. Глядя на самую верхнюю часть стека вызовов, я обратил внимание на строку "at System.IO.FileStream.FlushOSBuffer()". Я решил погуглить FlushOSBuffer и посмотреть, что удастся найти.

Это привело меня к документации Microsoft по .NET для filestream.cs. Там я нашёл определение FlushOSBuffer:

image

Похоже, она вызывает Win32 API FlushFileBuffers. Определение Win32Native.FlushFileBuffers можно найти в документации Win32Native:

image

Любой, кто работал с .NET и P/Invoke, узнает этот формат. Теперь у меня был Win32 API, который, как я знал, вызывает System.IO.Filestream.FlushOSBuffer() — моя проблемная функция .NET. Установка точки останова на KERNEL32!FlushFileBuffers и запуск SharpHound подтвердили это, и, пройдя по шагам, я быстро увидел, что под капотом FlushFileBuffers вызывает NtFlushBuffersFile. Перехват этого API устранил проблемы, с которыми сталкивался SharpHound, и позволил ему успешно работать, записывая свои выходные файлы в память.

Скачивание файлов из памяти

Критически важной частью этого проекта является возможность реально скачивать файлы на CobaltStrike Teamserver, когда они уже находятся в памяти. Как и следовало ожидать, обычная команда download в CobaltStrike не работает с файловым путём, которого на самом деле не существует. С учётом нынешних знаний решение, вероятно, заключается в доработке заменяющего PIC-кода NtReadFile, чтобы файлы в памяти можно было читать, а не только записывать. Не обладая этими знаниями заранее, я не мог реально получить файлы, что было серьёзным блокером.

Случайно я наткнулся на BOF, написанный EspressoCake, в котором была функция, сразу бросившаяся мне в глаза:

image

Просматривая код, я заметил, что в нём используется недокументированная опция Beacon CALLBACK:

image

Эта функция позволяет BOF инициировать скачивание файла на Teamserver с целевой системы, а не с клиента. Эта возможность (которая, как я узнал позже, была совместным результатом нескольких человек, включая @Cr0Eax, @EthicalChaos и @anthemtotheego) устранила существовавший ранее серьёзный блокер, поскольку теперь у меня был способ инициировать передачу файла из памяти с целевой системы. Огромное спасибо всем причастным за этот фрагмент кода, который, как я предвижу, пригодится и в будущем.

Структура данных MemFiles

Сложной частью этого проекта было обеспечение доступности функциональности MemFiles для всех клиентов CobaltStrike, подключённых к Team Server. Данные MemFiles хранятся в структурах, создаваемых MemFiles.cna, который должен быть загружен в каждый клиент, желающий использовать инструмент; в результате эти структуры данных живут внутри каждого клиента, а не на Team Server. Если бы эти данные находились в едином центральном месте (TS), их получение из каждого клиента было бы тривиальной задачей, и вся эта проблема просто исчезла бы; будь у команды CobaltStrike намерение официально интегрировать такую возможность, как MemFiles, в CobaltStrike, я уверен, они пошли бы именно этим путём. Но поскольку это дополнение от сообщества, мы обходимся тем, что имеем.

Есть несколько различных сценариев, о которых нам приходится беспокоиться, когда речь идёт о том, чтобы каждый клиент CobaltStrike имел актуальные и точные данные о состоянии MemFiles в Beacons и конфигурации:

Новые клиенты, подключающиеся к TS и нуждающиеся в текущей memtable
Случаи, когда к TS подключён только один клиент, и он перезапускает CobaltStrike (теряя тем самым memtable, хранящуюся в памяти клиента)
Клиент A вносит изменение в данные MemFiles, которое должно быть передано клиенту B

Для решения этих сценариев был применён многосторонний подход. Чтобы обработать случай, когда к TS подключён только один клиент CobaltStrike (и, следовательно, только он владеет данными memtable), каждый раз, когда клиент изменяет memtable (meminit, memclean), он также записывает содержимое своей memtable в локальный текстовый файл, расположенный в каталоге CobaltStrike. Если клиент завершает работу/перезапускается или когда MemFiles.cna перезагружается, он сначала попытается прочитать локальный файл memtable.txt, чтобы заполнить свою memtable в памяти.

Когда к TS подключено несколько клиентов и к ним присоединяется новый клиент (согласно Event Log), каждый клиент получает список всех пользователей, подключённых к TS, и сортирует его в алфавитном порядке. Клиент, который оказывается первым в этом списке, выбирается в качестве "Broadcast"-клиента и после ожидания 5 секунд (чтобы новый клиент успел инициализироваться и прочитать свой локальный memtable.txt) отправляет в Event Log сообщения (Actions) для каждой записи своей memtable. Все клиенты (кроме транслирующего) читают эти сообщения и обновляют свою memtable переданной информацией; это включает как обновление существующих записей, так и добавление дополнительных, которых в их соответствующей memtable нет.

Обычные операции с MemFiles также полагаются на отправку сообщений в Event Log. Когда клиент A запускает meminit, транслируется сообщение, содержащее всю необходимую информацию о memtable; ВСЕ клиенты обновляют свои memtable, разбирая эти транслируемые сообщения Event Log с помощью хука "on Event_Action". Изменения данных MemFiles также происходят, когда meminit завершает выполнение своего BOF; эти изменения передаются обратно через Beacon (например, после запуска meminit Beacon вызывает обратный вызов с адресом в памяти структуры pMemAddrs) и поэтому видны всем подключённым клиентам, которые обновляют свои memtable с помощью хука "on Beacon_Output".

Сочетание этих отдельных усилий позволяет MemFiles эффективно и надёжно синхронизировать критически важные данные между несколькими клиентами.

Благодарности и признания

Этот проект был бы невозможен без вклада следующих людей и проектов:

  1. x64-NTAPI-inline-hook от globalpolicy
  2. x64 Function Hooking by Example от Kyle Halladay
  3. ShellcodeTemplate от Cracked5pider AKA @C5pider
  4. @ilove2pwn_, через Cracked5pider
  5. DLL-Exports-Extraction-BOF от EspressoCake AKA @the_bit_diddler
  6. @anthemtotheego, через EspressoCake
  7. @Cr0Eax, через @anthemtotheego
  8. @EthicalChaos, через @anthemtotheego
  9. SysWhispers is dead, long live SysWhispers! от KlezVirus
  10. @yarden_shafir
  11. @DexterGerig
Скачать инструмент