
Выполнение неуправляемых исполняемых файлов Windows в биконах CobaltStrike
Inline-Execute-PE — это набор BOF (Beacon Object Files) и сопутствующий скрипт Aggressor для CobaltStrike, позволяющий операторам загружать неуправляемые исполняемые файлы Windows в память Beacon и выполнять их, получая вывод и отображая его в консоли Beacon.
Это позволяет операторам использовать множество сторонних инструментов (Mimikatz, Dsquery, утилиты Sysinternals и т.д.) без необходимости сбрасывать их на диск, переформатировать в позиционно-независимый код с помощью таких инструментов, как Donut, или создавать новый процесс для их запуска.
Эти исполняемые файлы отображаются в памяти Beacon, чтобы их можно было запускать многократно без необходимости каждый раз передавать их по сети, выделять новую память и создавать новый процесс conhost.exe.
Исполняемые файлы, загруженные в Beacons, доступны и могут быть запущены всеми клиентами CobaltStrike, подключенными к Team Server CobaltStrike.
Inline-Execute-PE разработан для x64 Beacon и x64 исполняемых файлов Windows на C или C++, скомпилированных с помощью Mingw или Visual Studio. Этот проект не поддерживает x86 исполняемые файлы или x64 исполняемые файлы, написанные на другом языке или скомпилированные другим компилятором.

Клонируйте репозиторий и, при необходимости, выполните make для перекомпиляции BOF.
Загрузите Inline-Execute-PE.cna в клиент CobaltStrike. Убедитесь, что директория, из которой запускается CobaltStrike, доступна для записи вашим пользователем; Inline-Execute-PE создаёт там текстовый файл (petable.txt), чтобы обеспечить доступность данных, необходимых для работы Inline-Execute-PE.
Inline-Execute-PE состоит из 3 команд, взаимодействующих с целью и запускающих BOF, и 3 внутренних команд, управляющих структурой данных проекта:
Команды для цели:
Внутренняя структура данных:
peload — это начало работы с Inline-Execute-PE. Эта команда используется для загрузки PE в память Beacon. Она выполняет следующие основные действия:
perun — это второй шаг Inline-Execute-PE. Он выполняет следующие основные действия:
peunload вызывается для удаления PE из памяти Beacon, когда оператор закончил работу с ним или хочет загрузить другой PE. Он выполняет следующие основные действия:
petable используется для отображения информации о всех PE, загруженных в Beacons.
У каждого клиента CobaltStrike есть своя собственная petable; Inline-Execute-PE прилагает значительные усилия для обеспечения синхронизации данных между всеми подключенными клиентами CobaltStrike, чтобы PE могли использоваться всеми операторами. Подробнее об этом см. "Design Considerations and Commentary".

peconfig используется для настройки параметров, касающихся работы Inline-Execute-PE. Два текущих параметра, которые могут быть изменены:
pebroadcast можно использовать для ручной рассылки содержимого petable клиента всем остальным подключенным клиентам CobaltStrike.
Каждый другой клиент CobaltStrike обновит свою petable полученными данными. Это вряд ли когда-либо понадобится, но функция существует на всякий случай.
Используйте peload для загрузки PE в память Beacon

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

Вызовите perun, передав аргументы загруженному PE

Двойные кавычки в аргументах должны экранироваться обратной косой чертой

Если вы обнаружили, что PE вызывает проблемы при попытке освободить DLL во время выгрузки, используйте peconfig, чтобы установить unloadlibraries в false

Когда вы закончили использовать PE, вызовите peunload, чтобы очистить его из Beacon

Теперь в Beacon можно загрузить другой PE

Необходимо быть внимательным с аргументами командной строки, передаваемыми PE; некоторые PE сразу аварийно завершатся при неверных аргументах, в то время как другие будут работать бесконечно, из-за чего Beacon никогда не ответит, хотя процесс всё ещё работает.
Это можно наблюдать с Mimikatz.exe, когда в конце списка аргументов не указан 'exit'

...

Inline-Execute-PE завершит поток работающего PE после достижения указанного значения тайм-аута. Это позволяет Beacon возобновить нормальную связь (Beacon не отвечает до завершения выполнения BOF perun). Хотя обычные команды CobaltStrike и другие BOF всё ещё могут использоваться в этом Beacon, Inline-Execute-PE теперь отключен; при таком принудительном завершении работающего PE, похоже, нарушается работа stdout и stderr в процессе Beacon, и последующие загруженные PE функционируют некорректно.
PE может (и должен) быть выгружен из памяти Beacon, однако просмотр petable покажет, что в этот Beacon больше нельзя загружать дополнительные PE. 
Крайне важно протестировать PE, которые вы собираетесь запускать с помощью Inline-Execute-PE, и проявлять осторожность при передаче аргументов командной строки perun. Некоторые PE более снисходительны, чем другие.
Ниже в произвольном порядке приведены некоторые наблюдения, сделанные в ходе тестирования и разработки, касающиеся определённых PE, которые пользователи могут захотеть загрузить в Beacon.
IOC, связанные с Inline-Execute-PE, включают, но не ограничиваются:
Я не проводил полномасштабное тестирование против EDR во время разработки, отчасти из-за лени, отчасти из-за отсутствия тестовой среды. Однако он был протестирован с последней версией Windows Defender (которая, по моему опыту, является довольно хорошим антивирусным продуктом).
Mimikatz.exe, вероятно, является наиболее подозрительным и широко известным PE, который приходит на ум в качестве кандидата для использования с Inline-Execute-PE. Я обнаружил, что способность Windows Defender обнаруживать Mimikatz, запущенный с помощью Inline-Execute-PE, зависит от процесса, в котором работает Beacon.
Beacon, работающий в отдельном исполняемом файле (представьте beacon.exe с artifact kit, чтобы он мог нормально выполняться и работать, обходя Defender), будет обнаружен при использовании Mimikatz.exe с Inline-Execute-PE.
Beacon, работающий в процессе Windows (внедрённый в Explorer.exe, notepad.exe и т.д. или DLL, загруженная боковым способом в легитимный процесс), НЕ будет обнаружен при использовании Mimikatz.exe с Inline-Execute-PE.
Что касается EDR, выполняющих пользовательские перехваты (userland hooking), как я уже сказал, я не тестировал, но у меня есть следующие общие соображения:
Поскольку PE работает внутри процесса Beacon, в котором вы, предположительно, уже сняли перехваты/обновили NTDLL, я думаю, у вас не должно быть слишком много проблем с помеченными API-вызовами, сделанными PE. Те же проблемы, связанные с тем, что на самом деле делает PE (обращается к процессам, изменяет ключи реестра и т.д.), всё ещё актуальны.
Пару месяцев назад я наткнулся на RunPE-In-Memory и подумал попробовать преобразовать его в BOF для CobaltStrike. Последующее путешествие оказалось гораздо сложнее и заняло гораздо больше времени, чем предполагалось. Этот проект был особенно сложным, потому что это не самостоятельный инструмент, а инструмент для запуска других инструментов. Это требует большой гибкости и усилий для совместимости с широким спектром PE и всеми различными способами, которыми эти PE могут выполнять одну и ту же задачу (получать аргументы, завершаться и т.д.).
Изначально Inline-Execute-PE задумывался как универсальный BOF, отвечающий за загрузку, выполнение и освобождение PE в Beacon. Примерно через 3 недели работы над проектом, когда у меня был готов POC примерно на 75%, я нашёл Pezor, который был выпущен ~1.5 года назад и уже делал почти всё, что я пытался сделать; основное отличие заключалось в том, что Pezor вызывал Donut под капотом, чтобы превратить PE в шелл-код, а не вручную отображал исходный PE в память.
Это открытие было желанным в одном отношении и разочаровывающим в другом; было феноменально иметь зрелый проект, из которого можно черпать вдохновение и который помогает преодолеть некоторые затруднительные моменты в моём коде, но обескураживало то, что я фактически изобрёл велосипед заново, не зная об этом. После изучения Pezor и размышлений о его дизайне, некоторых вопросах, связанных с tradecraft, и операционных потребностях моей организации, я изменил курс Inline-Execute-PE до того, что вы видите сегодня. Это решение было продиктовано несколькими факторами, которые будут обсуждаться ниже, а также некоторые из более любопытных дизайнерских решений, которые могли вызвать удивление у тех, кто дочитал до этого места.
Анализируя свой операционный опыт, я вспомнил множество случаев и инструментов, где мне нужно было запускать инструмент многократно; с Pezor оператор должен был каждый раз отправлять PE по сети, создавать conhost.exe, выделять новую память в Beacon и т.д., что казалось мне потенциально нежелательным с точки зрения AV/EDR. Эта линия рассуждений привела к идее «загрузить» PE в Beacon, аналогично тому, как можно загрузить .PS1 в Beacon для многократного использования. conhost.exe создаётся при первой загрузке PE и сохраняется, пока PE загружен в память; аналогично, новая память выделяется для PE один раз при его первой загрузке, и, конечно, вы избегаете необходимости отправлять PE по сети каждый раз, когда хотите его использовать. Модель, принятая Inline-Execute-PE, не лишена недостатков, которые я пытался устранить с разной степенью успеха.
Дизайнерское решение, которое должно бросаться в глаза, — это тот факт, что Inline-Execute-PE отображает PE в Beacon ДВАЖДЫ. Это, безусловно, нежелательно и не было моим добровольным выбором, но родилось из необходимости. Как упоминалось ранее, Inline-Execute-PE должен перехватывать несколько функций, связанных с аргументами командной строки в PE. Поскольку отображённый PE работает внутри процесса Beacon, PE будет пытаться использовать аргументы командной строки, указанные в разделе PROCESS_PARAMETERS PEB; чтобы обойти это, когда PE вызывает одну из различных функций, извлекающих аргументы командной строки, мы должны направить PE к нашим собственным пользовательским функциям, где мы можем предоставить предполагаемые аргументы, переданные из CobaltStrike с помощью perun.
Это работает хорошо, но в процессе разработки я заметил нечто странное с несколькими различными PE. При первом запуске PE пользовательская функция, которую мы предоставили IAT PE, вызывалась правильно, однако при всех последующих запусках PE с другими аргументами PE не вызывал пользовательскую функцию и, следовательно, не получал аргументы, переданные из CobaltStrike. Я не уверен, что на самом деле происходит под капотом, но я склонен полагать, что после того, как PE выполняется один раз, он копирует аргументы командной строки куда-то в память, и при последующих запусках сначала ищет это место в памяти, прежде чем вызывать перехваченные функции для получения аргументов командной строки, как в первый раз. Я подтвердил эту теорию, получив местоположение в памяти, где находился указатель на другой указатель на массив указателей, содержащих аргументы, и вручную изменив это место в памяти, чтобы оно содержало правильный указатель при каждом запуске. Это сработало для функций __getmainargs и __wgetmainargs, но другие PE вызывают альтернативные функции, такие как __p___argv и __p___argc, для которых этот метод не работал.
Чтобы иметь возможность «сбросить» PE в состояние, в котором он действительно будет вызывать перехваченные функции для получения аргументов, я прибег к созданию второй копии PE в памяти во время peload. Эта копия также XOR-зашифрована и находится с защитой RX в течение всего жизненного цикла Inline-Execute-PE, просто используется для перезаписи копии PE, которая фактически выполняется с помощью perun. Как уже упоминалось, это не идеальное решение, но это универсальное решение, которое охватывает все PE, не зарываясь в дебри попыток найти решение для всех различных PE и разных API, которые они используют.
Учитывая, что одной из главных особенностей Inline-Execute-PE является возможность запускать инструменты без создания новых процессов, это серьёзный удар по самолюбию — необходимость... создавать новый процесс (conhost.exe) для этого. Это требование связано с тем, что стандартные потоки (stdin/stdout/stderr) не инициализируются в программах Windows, если не присутствует консоль. В нашем случае консоль нам вообще не нужна; стандартные потоки перенаправляются в анонимный канал и захватываются таким образом, но без conhost потоки не инициализируются и не могут быть перенаправлены.
Inline-Execute-PE подходит к проблеме conhost так же, как это делает Pezor: он вызывает AllocConsole, а затем сразу же скрывает её из виду с помощью ShowWindow. На виртуальной машине Windows 11 с 8 ГБ ОЗУ я никогда не вижу, как окно консоли мелькает и исчезает, но в зависимости от целевой системы это может быть по-разному.
Я разговаривал с разработчиком, работающим над очень продвинутым коммерческим C2, который недавно выпустил нативный эквивалент (ну, гораздо более продвинутую версию) Inline-Execute-PE, и он сказал мне, что они смогли избежать запуска conhost.exe, «обманув Windows, заставив её думать, что у неё есть консоль». С этой подсказкой я потратил около недели, прочёсывая интернет в поисках документации о том, как программы Windows взаимодействуют с conhost, пытаясь отследить API-вызовы, связанные с функциями записи и консолью в WinDBG, и даже изучил исходный код Windows Terminal, который, как ни удивительно, доступен на Github. Хотя я много узнал о PEB и вещах, связанных со стандартными потоками, из этого ничего не вышло. Подозреваю, что путь вперёд может включать патчинг определённых консольных функций в kernel32, но я не знаю. Честно говоря, я очень расстроен, что не смог найти решение здесь, но, будучи самоучкой с всего лишь несколькими годами карьеры за плечами, этого, вероятно, следовало ожидать.### Тайм-аут и спасение PE Все, кто когда-либо пытался написать BOF, знают, что при всех преимуществах, которые они дают, огромная опасность заключается в том, что ошибка или сбой в вашем BOF может и убьет ваш Beacon. Эта опасность усиливается в данном проекте из-за того, насколько большой контроль пользователи имеют над данными, передаваемыми в Inline-Execute-PE, и как мало мер безопасности я, разработчик, могу легко или надежно внедрить. Пользователи могут, например, обрушить свой Beacon, загрузив x86 PE в x64 Beacon, или, что гораздо чаще, передав неправильные аргументы отображенному PE, как я упоминал ранее. Хотя я не могу помешать пользователям обрушивать свои Beacons из-за неправильных аргументов их PE, я могу попытаться спасти их Beacon в случае бесконечно работающего PE, как в случае с Mimikatz, когда не указан 'exit'.
В идеале я мог бы остановить выполнение PE, позволив Beacon возобновить нормальную работу, а затем сразу же дать пользователю попробовать снова (надеюсь, на этот раз с правильными аргументами). На практике я обнаружил, что завершение PE, похоже, ломает FILE*, связанные с stdout/stderr, и даже полная выгрузка PE и последующая его новая загрузка не решают эту проблему; они сломаны на уровне всего процесса.
Чтобы завершить PE, который продолжает работать после истечения 'timeout', вызывается TerminateThread для дескриптора, возвращенного CreateThread. Это не позволяет потоку gracefully завершить что-либо, поэтому логично, что некоторые вещи могут сломаться. Я попытался смягчить эту проблему, реализовав thread hijacking с целью приостановить поток PE и перенаправить его выполнение на API ExitThread(). Надежда была в том, что если поток сам запустит процедуры выхода (в отличие от принудительного внешнего завершения), это может привести к тому, что stdout/stderr продолжат работать, но у меня возникла та же проблема (а также невозможность приостановить поток PE в случае с Mimikatz).
Не в силах смягчить эту проблему, я решил просто запретить пользователям продолжать запускать PE или загружать дополнительные PE в затронутый Beacon (что ПРИВЕЛО бы к сбою). Это еще один пример того, как Inline-Execute-PE не дотягивает до того уровня, который мне бы хотелось, но я смирился с тем, что Оператор по крайней мере все еще имеет свой Beacon и может использовать его для нормальной функциональности.
Сложной частью этого проекта было обеспечение доступности PE, загруженных в Beacons, для всех клиентов CobaltStrike, подключенных к Team Server. Данные Inline-Execute-PE хранятся в структурах, создаваемых Inline-Execute-PE.cna, которые должны быть загружены в каждый клиент, желающий использовать инструмент; в результате эти структуры данных живут внутри каждого клиента, а не на Team Server. Если бы эти данные находились в одном центральном месте (TS), было бы тривиально извлечь их из каждого клиента, и вся эта проблема не возникла бы; если бы команда CobaltStrike формально интегрировала такую возможность, как Inline-Execute-PE, в CobaltStrike, я уверен, они пошли бы именно этим путем. Но поскольку это дополнение сообщества, мы довольствуемся тем, что имеем.
Существует несколько различных сценариев, о которых нам нужно беспокоиться, чтобы гарантировать, что каждый клиент CobaltStrike имеет актуальные и точные данные о PE, загруженных в Beacons:
Для решения этих сценариев был применен многосторонний подход. Чтобы обработать случай, когда только один клиент CobaltStrike подключен к TS (и, следовательно, является единственным обладателем данных petable), каждый раз, когда клиент изменяет petable (peload, peconfig, peunload и т.д.), он также записывает содержимое своего petable в локальный текстовый файл, расположенный в каталоге CobaltStrike. Если клиент выйдет или перезапустится, или когда Inline-Execute-PE.cna будет перезагружен, он сначала попытается прочитать локальный файл petable.txt, чтобы заполнить свой petable в памяти.
Когда несколько клиентов подключены к TS и новый клиент подключается (как указано в журнале событий), каждый клиент получает список всех пользователей, подключенных к TS, и сортирует его в алфавитном порядке. Клиент, который первый в этом списке, выбирается как "Broadcast" клиент и после 5-секундного ожидания (чтобы дать новому клиенту инициализироваться и прочитать его локальный petable.txt) отправляет сообщения (Actions) в журнал событий для каждой записи в своем petable. Все клиенты (кроме широковещательного) читают эти сообщения и обновляют свои petables полученной информацией; это включает обновление существующих записей, а также добавление тех, которых нет в их соответствующих petables.
Обычные операции с Inline-Execute-PE также полагаются на отправку сообщений в журнал событий. Когда клиент A запускает peload, отправляется сообщение, содержащее всю соответствующую информацию petable; ВСЕ клиенты обновляют свои petables, анализируя эти транслируемые сообщения журнала событий с помощью хука "on Event_Action". Изменения также вносятся в данные Inline-Execute-PE, когда peload и peunload завершают выполнение своих BOF; эти изменения передаются обратно Beacon (например, после выполнения peload Beacon возвращает вызов с адресом памяти структуры pMemAddrs) и, таким образом, видны всем подключенным клиентам, которые обновляют свои petables с помощью хука "on Beacon_Output".
Эти отдельные усилия в совокупности позволяют Inline-Execute-PE эффективно и надежно синхронизировать критически важные данные между несколькими клиентами.
Этот проект не был бы возможен без следующих проектов и ресурсов, на которые я активно ссылался и из которых возникли основные части этого проекта. Огромное спасибо авторам за их код и их видение.