На red team операции против финтех-компании с парком в 200 MacBook Pro под управлением Jamf я перебрал четыре техники закрепления, прежде чем одна пережила и перезагрузку, и частичную remediation от SOC. LaunchAgent в
~/Library/LaunchAgents/ с именем com.apple.metadata.mdworker-helper.plist продержался 11 дней - выглядел как системный процесс Spotlight и не вызвал алерта ни в SentinelOne, ни в BlockBlock. Ниже - разбор persistence macOS техник закрепления с позиции оператора: что реально работает на Ventura и Sonoma, что детектируется сходу, и почему Apple Silicon + SIP изменили правила игры.Persistence в контексте macOS kill chain
Persistence - страховка от потери доступа после initial access. В типичной цепочке атаки на macOS-инфраструктуру закрепление следует за получением code execution (через фишинговый .dmg, supply chain или exploit) и предшествует lateral movement на другие хосты.[Применимо: внутренний пентест / red team, macOS 12 Monterey - macOS 15 Sequoia, Apple Silicon и Intel]
Зачем это нужно атакующему: в корпоративных средах macOS-хосты разработчиков нередко имеют доступ к репозиториям, CI/CD-секретам и внутренним сервисам. Разработчик закрывает крышку MacBook - beacon умирает. Persistence гарантирует автоматическое восстановление C2-канала при открытии крышки. По данным CrowdStrike Global Threat Report 2025, среднее время lateral movement после initial access - 62 минуты, рекорд - 51 секунда. За это время оператору нужно не только закрепиться, но и начать двигаться по сети. Один разрыв связи без persistence обнуляет всю работу.
Все техники ниже предполагают, что initial access уже получен - оператор имеет shell или C2-агент (Mythic Poseidon, Havoc) на целевом Mac.
LaunchAgents и LaunchDaemons - основа закрепления в macOS
Два самых распространённых механизма persistence на macOS, маппированные на MITRE ATT&CK: Launch Agent (T1543.001, Persistence / Privilege Escalation) и Launch Daemon (T1543.004, Persistence / Privilege Escalation). По данным Huntress, Launch Items составляют подавляющее большинство как легитимных, так и вредоносных точек автозагрузки. RustBucket (DPRK), Shlayer, CloudMensis - все используют этот вектор, согласно исследованию Elastic Security.
LaunchAgents - user-level persistence (T1543.001)
LaunchAgent - plist-файл, указывающийlaunchd запустить определённый бинарь при логине пользователя. Не требует root. Работает в контексте пользовательской сессии.Предусловия: code execution от имени целевого пользователя. SIP не мешает записи в
~/Library/LaunchAgents/. Работает на macOS 10.x–15.x, Intel и Apple Silicon.Расположение файлов:
~/Library/LaunchAgents/- пользовательские агенты, не требуют привилегий/Library/LaunchAgents/- системные агенты для всех пользователей, нужен root/System/Library/LaunchAgents/- защищены SIP, недоступны для записи
XML:
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.apple.metadata.mdworker-helper</string>
<key>ProgramArguments</key>
<array><string>/Users/dev/.config/helpers/mdworker</string></array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
</dict>
</plist>
Label имитирует Apple-сервис Spotlight (mdworker). RunAtLoad запускает бинарь при логине. KeepAlive заставляет launchd перезапускать процесс при падении - аналог Restart=always в systemd. Бинарь mdworker в ProgramArguments - скомпилированный Mach-O (C2-агент), не скрипт. Путь /Users/dev/.config/helpers/ - скрытая директория с dot-prefix, стандартный ls её не покажет.Загрузка без перезагрузки:
launchctl load ~/Library/LaunchAgents/com.apple.metadata.mdworker-helper.plist. На macOS 13+ Apple продвигает launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.apple.metadata.mdworker-helper.plist, но старый синтаксис load по-прежнему работает (помечен как legacy и может выдавать warning).OPSEC-правила именования: имя plist и Label обязаны мимикрировать под легитимные Apple-сервисы или установленный софт. Никаких
com.backdoor.shell - только com.apple.cfprefsd-helper или com.google.keystone.agent.helper. Перед выбором имени я всегда делаю ls ~/Library/LaunchAgents/ на целевом хосте и подбираю имя, стилистически похожее на то, что уже там стоит. Этот шаг решает, проживёт ваш persistence 2 часа или 2 недели.Когда техника НЕ работает:
- MDM-политика (Jamf, Kandji) запрещает загрузку неодобренных LaunchAgents - plist просто не загрузится
- BlockBlock (Objective-See) стоит на хосте - пользователь получит popup-уведомление о новом persistence-item
- TCC (Transparency, Consent, and Control) на macOS 13+ может запрашивать подтверждение для отдельных операций
LaunchDaemons - system-level persistence (T1543.004)
LaunchDaemon запускается при старте системы до логина пользователя, с root-привилегиями.Предусловия: root-доступ (privilege escalation уже выполнен). SIP не защищает
/Library/LaunchDaemons/.Plist аналогичен по структуре LaunchAgent, но кладётся в
/Library/LaunchDaemons/ и должен принадлежать root:wheel с правами 644. Демон работает без пользовательской сессии и без GUI - beacon жив даже когда никто не залогинен.OPSEC-приём: plist в
/Library/LaunchDaemons/ виден через launchctl list. Имя должно мимикрировать под существующий софт на конкретной машине. Если стоит Jamf - com.jamf.management.helper.plist вызовет меньше вопросов, чем com.service.daemon.plist.Продвинутый вектор - LaunchDaemon hijacking: вместо создания нового plist атакующий находит существующий демон, бинарь которого можно перезаписать. Команда
find /Library/LaunchDaemons -name "*.plist" -exec plutil -p {} \; покажет все пути к бинарям. Если бинарь по указанному пути имеет world-writable permissions - его можно заменить на payload без создания новых plist-файлов. Это обходит все правила детекта, которые ищут создание plist-файлов. На практике world-writable бинарь в LaunchDaemons - редкость, но я встречал такое у кривых установщиков VPN-клиентов и корпоративных агентов мониторинга.Что видят EDR при создании Launch Items
Главная головная боль оператора: любой plist в LaunchAgents/LaunchDaemons-директории - файловая операция, которую ESF (Endpoint Security Framework) логирует. EDR на macOS видят:
- Создание файла
.plistв целевых директориях - Процесс-создатель - подписанный installer vs
bashvscurl - Вызов
launchctl load/launchctl bootstrap - Запуск бинаря из
ProgramArguments- его подпись, путь, сетевое поведение
SentinelOne фиксирует
launchctl load и коррелирует с создателем plist. Если plist создан python, bash или osascript из /tmp/ - красный флаг.Elastic Defend (8.x+) использует опубликованное EQL-правило (severity: high, risk score: 73), которое ловит создание plist, если создатель - скриптовый интерпретатор (
python[I], osascript, bash, zsh, sh, curl, wget, java) или unsigned binary из временных директорий (/private/tmp/[/I], /var/folders/[I], /Users/Shared/[/I]), согласно документации Elastic Security.Наиболее надёжная комбинация для evasion: plist создаётся не из shell-сессии, а через
NSPropertyListSerialization из compiled Objective-C/Swift дроппера с ad-hoc code signature. Бинарь в ProgramArguments - тоже подписанный Mach-O. CrowdStrike Falcon по-прежнему увидит новый plist, но без аномальных маркеров (scripting interpreter, temp path) алерт будет ниже по приоритету. Разница между "critical alert в 3 ночи" и "medium в общей ленте" - разница между провалом и успешной операцией.Login Items - закрепление через штатный механизм macOS
Login Items - приложения, автоматически запускающиеся при логине пользователя. Видимы в System Settings -> General -> Login Items.Предусловия: code execution от имени пользователя. Admin не нужен.
На macOS Ventura (13) и Sonoma (14) Apple переработала Login Items. Старый
LSSharedFileListInsertItemURL API устарел. Теперь используется ServiceManagement framework с [URL='https://developer.apple.com/documentation/servicemanagement/smappservice']SMAppService[/URL]. Это критически важно: техники из старых туториалов не работают на современных macOS. Если вы тащите рецепт с поста 2019 года - он мёртв.Добавление через
osascript (работает на macOS 12; на macOS 13+ требует TCC-разрешения на Automation): osascript -e 'tell application "System Events" to make login item at end with properties {path:"/Users/dev/.config/helpers/mdworker", hidden:true}'. Параметр hidden:true скрывает приложение из Dock, но не из списка Login Items в System Settings. На macOS Ventura+ эта команда триггерит TCC-промпт "[App] wants to control System Events""[Приложение] хочет контролировать системные события" - видимый диалог пользователю. Техника непригодна для скрытного persistence без предварительного TCC-разрешения.На macOS 12 и ранее данные хранятся в
~/Library/Application Support/com.apple.backgroundtaskmanagementagent/backgrounditems.btm. На macOS 13+ хранилище перенесено в /var/db/com.apple.backgroundtaskmanagement/BackgroundItems-v*.btm.Когда техника НЕ работает:
- Login Items видны пользователю в System Settings - внимательный пользователь заметит незнакомую запись
- На macOS Ventura+ ОС отображает уведомление: "[App] was added to your Login Items""[Приложение] было добавлено в ваши объекты входа" - серьёзный OPSEC-риск
- CrowdStrike Falcon детектирует добавление Login Items через
osascriptкак подозрительную операцию
Dylib hijacking - persistence без plist-файлов
Dylib hijacking - одна из самых скрытных техник persistence на macOS. macOS-приложения загружают динамические библиотеки (.dylib) через линковщикdyld. Если приложение ссылается на библиотеку по пути, где её нет, - атакующий подкладывает свою. Никаких новых файлов в подозрительных директориях, никаких launchctl load в логах.Предусловия: приложение ссылается на dylib через
LC_LOAD_WEAK_DYLIB по пути, где файла нет. SIP не защищает целевое приложение. Работает на Intel и Apple Silicon (dylib должна быть скомпилирована под arm64).Разведка целей - поиск weak-linked библиотек:
Bash:
# Weak dylibs: если файла нет по указанному пути,
# приложение загрузит подставную dylib без ошибки
otool -l /Applications/TargetApp.app/Contents/MacOS/TargetApp \
| grep -A2 LC_LOAD_WEAK_DYLIB
# Альтернатива для всех linked libs:
otool -L /Applications/TargetApp.app/Contents/MacOS/TargetApp
otool -L по сторонним приложениям из /Applications/ и ~/Applications/ - так лучше чувствуешь, что именно стоит на хосте.Механика атаки: найти приложение с missing weak dylib через
otool, скомпилировать вредоносную dylib с payload, поместить по ожидаемому пути. При следующем запуске приложения код выполнится в его контексте.Почему это хорошо для OPSEC:
- Не создаётся новый plist - нет алертов на файловые операции в LaunchAgents
- Код выполняется от имени легитимного подписанного приложения
- Нет модификации самого приложения - code signing не нарушен
- Триггер привязан к запуску конкретного приложения - поведение выглядит естественно
DYLD_INSERT_LIBRARIES: принудительная загрузка dylib в любой процесс. На macOS с SIP работает только для non-SIP-protected бинарей. Для persistence прописывается в ~/.zshrc: export DYLD_INSERT_LIBRARIES=/path/to/lib.dylib. Выполнится при каждом открытии терминала. Минус: бесполезно для GUI-приложений вне терминала. Persistent-вариант через launchd environment (задаётся в plist ключом EnvironmentVariables) переживает перезагрузку.Когда техника НЕ работает:
- Hardened Runtime (введён в macOS 10.14, обязателен для notarization с 2020) - блокирует загрузку неподписанных dylib, если у приложения отсутствует entitlement
com.apple.security.cs.disable-library-validation. Все App Store приложения имеют Hardened Runtime - SIP-защищённые приложения (всё в
/System/и штатные Apple-приложения) - не hijackable - CrowdStrike Falcon отслеживает
DYLD_INSERT_LIBRARIESв переменных среды процессов - ESF логирует загрузку dylib - SentinelOne фиксирует unsigned dylib в подписанных приложениях
- Ручная проверка:
fs_usage -w | grep dylibпоказывает все загружаемые библиотеки в реальном времени
Legacy-техники: cron, periodic scripts и shell profiles
Cron (T1053.003, Persistence) по-прежнему работает на macOS, хотя Apple рекомендует
launchd. Crontab-файлы хранятся в /var/at/tabs/ (он же /private/var/at/tabs/). Предусловия: code execution от имени пользователя, root не нужен для пользовательского crontab. Sigma-правило proc_creation_macos_schedule_task_job_cron.yml из SigmaHQ покрывает создание cron-задач на macOS, а osquery запрос SELECT * FROM crontab; немедленно выявит аномальные записи. Честно? Я использую cron как decoy - прописываю задачу с безобидной командой, чтобы SOC нашёл и "вычистил" её, пока настоящий persistence в LaunchAgent продолжает работать. Шумная техника, но полезная как отвлекающий манёвр.Periodic scripts - директории
/etc/periodic/daily/, /etc/periodic/weekly/, /etc/periodic/monthly/. Скрипт выполняется соответствующим SIP-protected LaunchDaemon. Требует root. На macOS 13+ Apple отключила некоторые periodic-задачи по умолчанию - проверяйте через launchctl list | grep periodic.Shell profiles - macOS по умолчанию использует zsh (с Catalina).
~/.zshrc выполняется при каждом открытии Terminal.app или iTerm2. Для разработчиков - надёжный вариант, терминал открывается ежедневно, часто несколько раз в час. Для нетехнических пользователей - бесполезен, они терминал не открывают никогда. CrowdStrike Falcon мониторит модификации shell-profile файлов, SentinelOne в default policy на macOS обрабатывает это мягче.Login Hooks - устаревший механизм через plist
/Library/Preferences/com.apple.loginwindow.plist с ключом LoginHook. Устанавливается командой sudo defaults write com.apple.loginwindow LoginHook /path/to/script. Ограничение: только один Login Hook одновременно, требует root. Редко мониторится EDR, но требует привилегий.Overrides plist - на macOS 11+ overrides хранятся в
/var/db/com.apple.xpc.launchd/disabled.plist (system) и disabled.<uid>.plist (per-user); классический путь /var/db/launchd.db/com.apple.launchd/overrides.plist устарел (работал до ~macOS 10.10). Запись Disabled=false в overrides принудительно загрузит агент, даже если в plist задано Disabled=true. По данным Huntress, этот механизм используется редко, но даёт интересную возможность: скрыть активный persistence за "отключённым" plist. SOC-аналитик видит Disabled=true в plist, считает его неактивным, а overrides тихо его включает.Emond (event monitor daemon) - удалён начиная с macOS Ventura 13.0, по данным Huntress. На macOS 12 и ранее позволял создавать правила в
/etc/emond.d/ для запуска команд при системных событиях. На Ventura+ - мёртвая техника. Если встретите её в чьём-то "актуальном гайде" - гайд протух.Когда что использовать - decision tree по сценарию
📚 Часть контента скрыта. Этот материал доступен участникам сообщества с рангом One Level или выше
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
, RC-скрипты - мертвы. SIP + Signed System Volume + Hardened Runtime убили целый класс атак. Kext (Kernel Extensions) полностью deprecated в пользу System Extensions, а System Extensions требуют одобрения MDM и Apple Developer подписи. Что осталось стабильным: LaunchAgents в пользовательской директории и dylib hijacking для приложений без Hardened Runtime.
Русскоязычные источники по macOS persistence в основном пересказывают MITRE ATT&CK без конкретных plist-примеров и OPSEC-контекста. Англоязычное комьюнити даёт PoC, но не разбирает вендор-специфику EDR на macOS. По моему опыту работы с macOS-флотами: CrowdStrike Falcon - самый зрелый EDR на этой платформе, он напрямую использует ESF и ловит plist-creation + unsigned dylib load в default policy. SentinelOne на macOS слабее, чем на Windows - модификации shell profile в default policy проходят тише (наблюдение актуально на середину 2025 года, может устареть с обновлением движков). Elastic Defend набирает позиции - EQL-правила для Launch Items опубликованы и работают из коробки.
Вот что я редко вижу в разборах: на macOS persistence - не техническая проблема, а OPSEC-задача. Записать plist в LaunchAgents может любой скрипт-кидди с пятиминутным доступом к терминалу. Записать plist так, чтобы он не вызвал алерт в CrowdStrike, пережил проверку KnockKnock и выглядел легитимно при ручном review SOC-аналитика - это ремесло. Нужно понимание конкретной среды: какой софт стоит на хосте, какой EDR развёрнут, какие naming conventions приняты в организации. На HackerLab (https://hackerlab.pro) лежит сценарий, где persistence-примитивы нужно собрать в полную цепочку - от foothold до закрепления.
Последнее редактирование модератором: