Статья Persistence на macOS: техники закрепления для Red Team — от LaunchAgents до dylib hijacking

Крупный план логической платы MacBook Pro с чипом Apple Silicon под жёстким янтарным светом. Рядом с процессором — миниатюрный дисплей с текстом на синем фоне.


На 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​

1782072175561.webp

Два самых распространённых механизма 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, недоступны для записи
Пример plist для закрепления C2-агента (Plist Modification, T1547.011):
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​

1782072205627.webp

Главная головная боль оператора: любой plist в LaunchAgents/LaunchDaemons-директории - файловая операция, которую ESF (Endpoint Security Framework) логирует. EDR на macOS видят:
  1. Создание файла .plist в целевых директориях
  2. Процесс-создатель - подписанный installer vs bash vs curl
  3. Вызов launchctl load / launchctl bootstrap
  4. Запуск бинаря из ProgramArguments - его подпись, путь, сетевое поведение
CrowdStrike Falcon на macOS использует ESF напрямую и детектирует создание plist + запуск unsigned binary из нестандартного пути. В default policy - высокий алерт.

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 как подозрительную операцию
Когда использовать: операции против нетехнических пользователей (менеджеры, HR, бухгалтерия), которые не проверяют Login Items. Против разработчиков - рискованно. Разработчик увидит незнакомую запись и полезет разбираться.

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
DylibHijack Scanner от Objective-See автоматизирует поиск приложений с hijackable dylib. На операциях я начинаю с ручного 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​

1782072291286.webp

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 до закрепления.
 
Последнее редактирование модератором:
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab

Похожие темы

🚀 Первый раз на Codeby?
Гайд для новичков: что делать в первые 15 минут, ключевые разделы, правила
Начать здесь →
🧭 Навигатор · ИБ 2026
Не знаешь, какой трек твой?
5 направлений ИБ, реальные зарплаты и точка входа для каждого — в одном треде.
JuniorSenior+
100K → 600K+ ₽ /мес
Открыть навигатор →

Популярный контент

🔴 Свежие CVE, 0-day и инциденты
То, о чём ChatGPT ещё не знает — обсуждаем в реальном времени
Threat Intel →
💼 Вакансии и заказы в ИБ
Pentest, SOC, DevSecOps, bug bounty — работа и проекты от проверенных компаний
Карьера в ИБ →

HackerLab