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

Имя файла указывает на объект, но не идентифицирует его. Это различие кажется придиркой, пока агент получает согласие, работая как ~/bin/agent, кто-то заменяет этот путь другой программой, а шлюз считает следующего вызывающего доверенным только потому, что знакомая метка никуда не делась.
Символические ссылки помогают легко воспроизвести ошибку, но они не являются ее корнем. Ее вызывают любые изменяемые промежуточные звенья: оболочка, запись в PATH, скопированный бинарный файл, рабочая копия проекта или запускатель, который разрешает путь при каждом запросе. Шлюз, одобряющий имя пути, одобряет объект, который непривилегированный пользователь часто может заменить.
В macOS полезной единицей считается запущенный процесс и его идентичность кода. Apple Code Signing Services может описать и проверить код, связанный с процессом. Путь может присутствовать в записи для диагностики, но он не должен определять решение об авторизации.
Символическая ссылка это служба имен, а не программа
Символическая ссылка хранит строку, которую ядро разрешает, когда программа открывает или запускает ее. Замена этой строки меняет результат будущего вызова exec. Она не изменяет образ исполняемого файла уже запущенного процесса.
Эта деталь создает ложное чувство безопасности. Люди проверяют замену ссылки, видят, что исходный процесс продолжает работать, и заключают, что атака безвредна. Но такой тест показывает лишь обычное поведение процесса. Он не проверяет главное: распознает ли шлюз следующий процесс, запущенный через тот же псевдоним, как процесс, который ранее получил согласие.
Представим агента для работы с кодом, запущенного через такой путь:
~/bin/build-agent -> ~/work/approved-agent
Шлюз получает первый запрос, видит ~/bin/build-agent и просит оператора дать разрешение. Затем атакующий меняет ссылку:
~/bin/build-agent -> ~/Downloads/replacement-agent
Если одобренный процесс продолжает отправлять запросы, шлюз может правильно продолжать этот сеанс до завершения процесса. Это не ошибка. Ошибка возникает, когда через то же имя запускается другой процесс, а шлюз говорит: «Я уже одобрил ~/bin/build-agent».
Второй процесс является новым субъектом безопасности. Его нужно заново проверить и, если он не соответствует специально определенному правилу обновления, заново одобрить.
Та же ошибка появляется и без символических ссылок. Шлюз может выполнить codesign для пути, переданного клиентом, проверить один файл, а принять запрос от другого процесса. Клиент может изменить путь после проверки или просто указать стабильный псевдоним, ведущий к меняющимся целям. Красивый путь в карточке согласия не исправляет такую архитектуру.
Apple рассматривает требования к коду как ограничения идентичности, а не как имена файлов. В документации сказано, что назначенное требование содержит критерии, по которым определяется, является ли код тем же кодом, который уже видели раньше. Обычно оно выводится из издателя подписи и встроенного идентификатора, если разработчик не объявил собственное требование. Это правильное направление, но с важной оговоркой: требование описывает идентичность кода, а не живой сеанс процесса.
Согласие получил именно запущенный процесс
Шлюз должен принимать решение об авторизации на основе ссылки на живой процесс, полученной от операционной системы. Затем это решение нужно привязать к запуску процесса, а не к пути, заявленному клиентом, имени исполняемого файла или изменяемой переменной окружения.
В macOS хорошая проверка решает две задачи:
- Получает сведения о подписи фактического процесса-вызывающего.
- Проверяет этот процесс до того, как использовать сведения о его идентичности как основание для авторизации.
Это разные задачи. SecCodeCopyDesignatedRequirement может вернуть назначенное требование для подписанного кода, но Apple отдельно отмечает, что этот вызов не проверяет подпись. Измененный после подписи или неправильно подписанный код все еще может выдавать неполные или вводящие в заблуждение сведения. Шлюз должен выполнять и проверку действительности.
Запись о процессе должна содержать достаточно данных, чтобы позже объяснить решение:
- идентификатор процесса или, еще лучше, выданные ОС учетные данные процесса, устойчивые к повторному использованию PID
- путь к исполняемому файлу, наблюдавшийся во время проверки, только как контекст
- идентификатор подписи и идентификатор команды, если они доступны
- издатель подписи и цепочку сертификатов, если применимо
- назначенное требование
- cdhash или набор поддерживаемых значений cdhash
- результат проверки подписи и время проверки
Не принимайте этот список за язык политики. Шлюзу нужен короткий ответ: какой запущенный процесс отправляет запрос и одобрил ли человек именно этот запуск. Несколько фактов об идентичности помогают ответить. Набор правил для путей не помогает.
Сложнее всего обрабатывать обновления. Назначенное требование часто остается неизменным между легитимными версиями. Поэтому macOS использует его для сохранения разрешения, когда пользователь разрешает приложению доступ к защищенной службе. В технической заметке Apple TN3127 приведен привычный пример с обновлением приложения, которое снова обращается к микрофону: macOS сравнивает новую версию с сохраненным назначенным требованием.
Для постоянного системного разрешения это разумно. Но такой подход слишком широк, если молча использовать прежнее согласие человека для нового автономного запуска агента. Шлюз может показывать издателя подписи, чтобы оператор узнал программу, и все равно снова запрашивать согласие при запуске нового процесса. Издатель подтверждает происхождение. Граница процесса определяет срок действия согласия.
Статические проверки пути оставляют необъяснимую гонку
Статическая проверка отвечает на статический вопрос: «Какова подпись этого файла по данному пути прямо сейчас?» Сама по себе она не отвечает на вопрос: «Какой код отправил этот запрос?»
Этот пробел важен даже тогда, когда статическая проверка технически безупречна. Допустим, шлюз получает запрос, в котором указан путь /Users/dev/bin/agent. Он выполняет:
codesign --verify --strict --verbose=2 /Users/dev/bin/agent
codesign -d -r- /Users/dev/bin/agent
Обе команды могут сообщить о действительной подписи и назначенном требовании. Затем шлюз сохраняет имя пути как одобренную идентичность. Между проверкой и следующим запросом атакующий может перенаправить agent на другой файл. При следующем запросе шлюз снова выполнит команды и получит сведения о замене. Ни одна команда не доказывает связь с процессом, отправившим запрос.
Есть и вторая гонка, которую команды часто упускают. Помощник может проверить путь перед запуском агента, а затем получить соединение от дочернего процесса. Проверка пути описывает родительский файл в момент запуска. Соединение описывает процесс в момент запроса. Если помощник не связывает эти события с помощью учетных данных операционной системы, он просто принимает предположение за происхождение.
Именно поэтому API Apple для подписи кода разделяют разные категории сведений. kSecCSSigningInformation запрашивает данные сертификата и CMS, kSecCSRequirementInformation запрашивает требования, а kSecCSDynamicInformation запрашивает сведения о динамической действительности запущенного кода. Набор API сам по себе не превращает эти части в готовую архитектуру шлюза, но ясно показывает различие: статическая проверка подписи и состояние запущенного кода являются разными входными данными.
Имя файла все равно должно отображаться в карточке согласия. Оператору важно видеть, что запрос пришел из рабочей копии в ~/work/demo, а не от установленного инструмента в /Applications. Считайте это контекстом интерфейса. Запись об авторизации должна оставаться привязанной к вызывающему процессу, который идентифицировала операционная система.
Воспроизведите замену без настоящего агента
Проблему с путем можно показать с помощью двух небольших бинарных файлов во временной папке. Для этого не нужны производственный токен, ключ SSH или измененный пакет приложения.
Создайте рабочую папку и скомпилируйте две программы, которые печатают разные маркеры. Код намеренно простой: проверяется замена, а не логика программы.
work="$(mktemp -d /tmp/agent-identity.XXXXXX)"
cd "$work"
cat > approved.c <<'EOF'
#include <cstdio.h>
#include <unistd.h>
int main(void) {
printf("approved process pid=%d\n", getpid());
fflush(stdout);
sleep(60);
return 0;
}
EOF
cat > replacement.c <<'EOF'
#include <cstdio.h>
#include <unistd.h>
int main(void) {
printf("replacement process pid=%d\n", getpid());
fflush(stdout);
sleep(60);
return 0;
}
EOF
clang approved.c -o approved-agent
clang replacement.c -o replacement-agent
codesign --force --sign - --identifier com.example.approved approved-agent
codesign --force --sign - --identifier com.example.replacement replacement-agent
ln -s "$work/approved-agent" agent
Для локальной проверки механики достаточно специальной подписи, но у нее нет цепочки сертификатов. Apple отмечает, что код со специальной подписью не имеет сертификатов и содержит пустые данные CMS. Не используйте такую подпись для имитации издателя дистрибутива или для определения того, что принимает производственный шлюз.
Запишите, куда ведет псевдоним, проверьте его подпись и запустите первый процесс:
printf 'alias before: %s\n' "$(readlink agent)"
codesign -d -r- agent 2>&1 | sed -n '1,8p'
./agent &
first_pid=$!
printf 'first pid: %s\n' "$first_pid"
ps -p "$first_pid" -o pid=,comm=,args=
Форма вывода должна быть примерно такой:
alias before: /tmp/agent-identity.xxxxxx/approved-agent
Executable=/tmp/agent-identity.xxxxxx/agent
designated => identifier "com.example.approved"
approved process pid=48291
first pid: 48291
48291 ... ./agent
Теперь замените ссылку, пока первый процесс спит:
rm agent
ln -s "$work/replacement-agent" agent
printf 'alias after: %s\n' "$(readlink agent)"
codesign -d -r- agent 2>&1 | sed -n '1,8p'
ps -p "$first_pid" -o pid=,comm=,args=
При проверке agent вы должны увидеть com.example.replacement, а first_pid все еще будет работать. Запустите псевдоним во второй раз:
./agent &
second_pid=$!
printf 'second pid: %s\n' "$second_pid"
wait "$first_pid" "$second_pid"
Второй процесс напечатает replacement process. Два PID выполняли разные образы программ, хотя оба были запущены как ./agent.
Этот результат должен изменить план тестирования шлюза. Проверка, которая лишь спрашивает, продолжил ли работать старый PID, почти ничего не доказывает. Полезная проверка спрашивает, считает ли шлюз второй PID новым вызывающим и показывает ли его наблюдаемую идентичность до того, как разрешит одобренное действие.
Полезная проверка согласия включает три разных запуска
Не ограничивайтесь одним успешным согласованием и одной командой с символической ссылкой. Нужны три запуска, потому что каждый доказывает отдельное свойство.
Сначала запустите одобренную цель через псевдоним и отправьте запрос на действие. Шлюз должен создать запись сеанса и показать согласие, в котором фактический вызывающий описан понятными оператору терминами. Запишите идентификатор сеанса, PID, наблюдаемый путь и сведения о подписи кода.
Затем измените псевдоним, но оставьте первый процесс работать. Пусть первый процесс отправит еще один запрос. Если ваша модель сеанса одобряет один запуск процесса до его завершения, шлюз может разрешить этот запрос. Образ исполняемого файла не менялся. Не называйте ожидаемый результат уязвимостью.
Наконец, запустите новый процесс через замененный псевдоним и отправьте тот же запрос. Шлюз не должен наследовать согласие первого процесса только потому, что совпадают имя исполняемого файла, командная строка, рабочая папка или действие. Он должен создать отдельный сеанс и потребовать обычную авторизацию.
Используйте таблицу результатов:
| Запуск | Цель псевдонима при запуске | Ожидаемый результат |
|---|---|---|
| Первый процесс | approved-agent | Новое согласие, затем разрешение для этого запуска |
| Первый процесс после замены | На диске replacement-agent, но старый образ остается в памяти | Работа существующего сеанса продолжается до завершения |
| Второй процесс | replacement-agent | Новое согласие или отказ, но никогда не унаследованное согласие |
Стоит проверить еще один случай. Верните псевдоним к исходному бинарному файлу и запустите третий процесс. Шлюз все равно должен создать новый сеанс. Та же идентичность кода не означает тот же процесс. Если продукт намеренно предлагает постоянные отношения доверия к подписанному издателю, это должно быть отдельным явным решением оператора. Не прячьте его внутри функции согласия для отдельного процесса.
Авторизация для каждого сеанса в Sallyport включена по умолчанию, а карточка согласия сначала показывает издателя подписи кода процесса, а не изменяемое имя файла. Это дает оператору более надежный факт для оценки, когда новый процесс агента хочет выполнить действие.
Издатель, назначенное требование и cdhash отвечают на разные вопросы
Команды часто сводят три разных понятия к фразе «подписано тем же приложением». Такое упрощение приводит либо к усталости от согласий, либо к разрешениям, которые действуют слишком долго.
Издатель подписи отвечает на вопрос, кто подписал код. Для распространяемого программного обеспечения это может включать цепочку сертификатов и идентификатор команды. Он помогает оператору отличить известного поставщика от случайного исполняемого файла, но не указывает конкретную сборку.
Назначенное требование отвечает на вопрос, должна ли macOS считать код той же идентичностью после обновлений. Apple говорит, что у всего подписанного кода есть назначенное требование, явное или созданное автоматически. Обычно оно включает издателя подписи и встроенный идентификатор. Это делает его подходящим для преемственности, но оно может принять более поздние версии, которые вы лично не проверяли.
Cdhash идентифицирует конкретный подписанный CodeDirectory. Для авторизации это почти отпечаток сборки. Он отлично подходит для аудита, поскольку позволяет расследователю отличить две версии с одинаковым издателем и идентификатором. Для инструментов разработки это обычно плохое постоянное правило одобрения, потому что обычные обновления будут его менять.
Используйте сведения в зависимости от решения:
- Используйте учетные данные живого процесса, чтобы привязать сеанс к вызывающему.
- Показывайте в интерфейсе согласия издателя подписи и идентификатор, чтобы оператор мог узнать источник.
- Записывайте назначенное требование и cdhash, чтобы при расследовании отличать преемственность издателя от конкретной сборки.
- Снова запрашивайте согласие для нового процесса агента, даже если издатель и назначенное требование совпадают.
Последний пункт намеренно строже разрешений macOS. Шлюз агента может отправлять HTTP-запросы или команды SSH с учетными данными, которые никогда не попадают в процесс агента. Это граница действия, а не одноразовый запрос доступа к микрофону. Перенос прежнего нажатия на другой будущий процесс ослабляет человеческий контроль, ради которого и создавался шлюз.
Не превращайте наблюдатель файлов в средство авторизации
Можно попытаться следить за путем агента и отзывать разрешение при изменении файла. Это кажется простым: сохранить исходный inode, подписаться на события файловой системы и отменять согласие после записи или переименования.
Но это неверная основа.
События файловой системы полезны как телеметрия, но не доказывают идентичность вызывающего. У путей может быть несколько имен. Бинарный файл можно скопировать. Процесс можно запустить из уже удаленного файла. Доставка событий может задерживаться или объединять несколько изменений. Атакующему не нужно выигрывать эффектную гонку, если архитектура уже авторизует путь вместо процесса.
Сравнение inode тоже не исправляет модель. Оно может обнаружить одну замену в одной папке, но ничего не говорит о процессе, запущенном через другую жесткую ссылку или скопированный объект. Кроме того, оно создает хрупкое поведение в обычных рабочих процессах, где инструменты постоянно пересобираются, переименовываются и заменяют файлы.
Считайте сведения о файлах полезным контекстом для аудита. Если текущий путь ведет к другой цели, чем во время согласия, запишите это. Если шлюз видит новый процесс-вызывающий, проверьте его и примените обычный поток авторизации. Граница процесса принимает решение безопасности, не заставляя наблюдатель работать как эталонный монитор.
Это правило также предотвращает более тонкую ошибку: одобрение оболочки только потому, что ее подпись знакома, при игнорировании того, что она запускает. Подписанная оболочка может выполнить неподписанный дочерний процесс, загрузить локальный скрипт или выбрать цель по окружению. Шлюз должен идентифицировать процесс, который фактически подключается и запрашивает привилегированное действие. Если вызывающим является оболочка, одобряется она. Если вызывающим является ее дочерний процесс, проверяйте его.
Привязывайте согласие к учетным данным ОС и закрывайте доступ при неоднозначности
Практической реализации нужен предоставленный ОС дескриптор запрашивающей стороны. В macOS это обычно означает получение объекта кода гостевого процесса для вызывающего процесса через Code Signing Services, используя атрибуты процесса из доверенного контекста соединения, а не значения, скопированные из входных данных агента.
Безопасная последовательность выглядит так:
- Примите соединение и получите идентичность вызывающего от механизма соединения.
- Свяжите эту идентичность с работающим объектом кода.
- Проверьте действительность кода до чтения сведений об идентичности для авторизации.
- Получите из этого объекта издателя подписи, идентификатор, назначенное требование и cdhash.
- Создайте запись сеанса, привязанную к учетным данным вызывающего, и запросите согласие, если одобренного сеанса нет.
- Перед каждым действием проверяйте, что учетные данные вызывающего все еще относятся к тому же живому процессу, а после завершения процесса или отзыва оператором уничтожайте сеанс.
Детали первого шага зависят от транспорта. Локальный Unix-сокет, XPC-соединение и канал дочернего процесса предоставляют разные учетные данные. Опасный запасной вариант одинаков во всех случаях: принимать PID, путь, идентификатор пакета или сводку подписи, которые агент прислал в теле запроса. Агент управляет этим телом. Оно ничего не доказывает.
Повторное использование PID требует особого внимания. Один PID уникален только пока процесс существует. После завершения ОС может назначить тот же номер другому процессу. Если API предоставляет более надежные учетные данные, храните их. Если транспорт не дает долговременной привязки вызывающего, ограничьте сеанс временем соединения и повторно авторизуйте его после подключения. Закрыть доступ при неоднозначности менее удобно, чем принять неясного вызывающего, но именно в неоднозначности возникают ошибки замены пути.
Карточки согласия должны быть честными. Если шлюз видит специальную подпись, так и напишите. Если действительной подписи нет, укажите это. Если у программы знакомое отображаемое имя, но новый издатель подписи, не прячьте издателя за дополнительным раскрытием. В первой строке должно быть ясно, кто подписал процесс и какое действие он хочет выполнить.
В журнале нужно сохранять решение, а не только запрос
Запись POST /deploy выполнен не объяснит инцидент с заменой символической ссылки. Она говорит, что произошло, но не почему шлюз разрешил это действие вызывающему.
Для каждого одобренного сеанса сохраняйте снимок сведений об идентичности, наблюдавшихся при согласии. Для каждого действия сохраняйте ссылку на сеанс и результат. В записи действия не обязательно повторять данные сертификата, но она должна вести к точной записи согласия, где эти данные есть.
Компактная запись может выглядеть так:
{
"session": "6F2A...",
"caller": {
"process": "OS-issued caller credential",
"path_observed": "/private/tmp/demo/agent",
"signing_identifier": "com.example.approved",
"designated_requirement": "identifier com.example.approved",
"cdhash": "<observed digest>",
"validity": "valid"
},
"approval": "granted",
"action": "HTTP POST /deploy",
"result": "success"
}
Путь остается полезным. Он может показать, что процесс запущен из временной папки или через псевдоним оболочки. Но его недостаточно, чтобы связать более поздний запрос с этим сеансом.
Sallyport хранит отдельные журналы Sessions для запусков агентов и Activity для отдельных вызовов. Они строятся из одного зашифрованного журнала с хеш-цепочкой, а офлайн-команда sp audit verify проверяет цепочку без ключа хранилища. Благодаря этому тест замены пути проще анализировать: событие согласия и последующее действие остаются отдельными фактами, а не сливаются в одно расплывчатое сообщение об успехе.
Проведите упражнение с символической ссылкой до того, как напишете правило согласия, которое потом не сможете объяснить. Если недавно запущенная замена может использовать согласие первого процесса, перестаньте считать имя исполняемого файла просто удобным полем. Оно уже проникло в решение о доверии.
सामान्य प्रश्न
Делает ли подпись кода путь к исполняемому файлу безопасным для доверия?
Подписанный исполняемый файл все равно может быть доступен через символическую или жесткую ссылку, скопированный путь или скрипт-запускатель. Имя показывает лишь, где был найден процесс. Оно не доказывает, какой код выполняется и кто его подписал.
Может ли замена символической ссылки изменить уже работающий процесс?
Нет. Запущенный процесс продолжает выполнять образ программы, загруженный командой exec, а замена символической ссылки влияет на разрешение пути при следующих запусках. Проблема безопасности возникает, когда шлюз снова обращается к изменяемому пути и принимает этот результат за сведения о ранее одобренном процессе.
Как приложению macOS идентифицировать процесс агента?
Проверяйте сам запущенный процесс, а не путь, который передал клиент. В macOS получайте сведения о подписи процесса через Code Signing Services и проверяйте их до предоставления сеанса.
Что такое назначенное требование в macOS?
Назначенное требование идентифицирует семейство кода, которое macOS считает той же издательской и прикладной идентичностью после легитимных обновлений. Оно не идентифицирует единственную конкретную сборку, поэтому используйте его для преемственности, а не как единственную запись о запущенном коде.
Стоит ли одобрять агента по cdhash?
Cdhash идентифицирует конкретный подписанный CodeDirectory и служит полезным свидетельством для определенной сборки. Он строже, чем издатель или назначенное требование, но постоянная привязка к нему будет ломать обычные обновления, пока кто-то намеренно не одобрит новую сборку.
Должно ли согласие для агента сохраняться после перезапуска?
Нет. Согласие должно относиться к запуску процесса, а не к имени файла и не к постоянной идентичности разработчика. Новый процесс должен создавать новое событие согласия, даже если он происходит из того же подписанного приложения.
Почему одобрение по пути опасно для ИИ-агентов?
Опасный шаблон заключается в том, что помощник одобряют один раз, а затем считают одобренным каждый будущий процесс, запущенный из этого пути. Атакующему достаточно заменить символическую ссылку, оболочку или бинарный файл после первоначального решения, не меняя знакомое имя команды.
Что должен записывать журнал действий агента?
В журнале должны быть идентификатор процесса или audit token, наблюдавшийся путь к исполняемому файлу как контекст, издатель подписи, идентификатор, назначенное требование, cdhash, результат проверки и время начала сеанса. Путь полезен для диагностики, но не должен быть основой доверия.
Что делать, если заменивший файл исполняемый код не подписан или подписан иначе?
Неподписанная замена должна быть отклонена, если правило одобрения требует действительной подписи. Замена, подписанная другим издателем, требует нового согласия. Легитимное обновление, подписанное разрешенной вами идентичностью, должно обрабатываться по выбранной вами политике обновлений.
Безопасно ли проверять замену символической ссылки на рабочем Mac?
Проведите замену во временной папке с бинарными файлами, которые вы скомпилировали сами, и направьте ссылку на них. Не заменяйте файлы внутри приложения, которого вы не создавали, не отключайте защиту платформы и не проверяйте производственные учетные данные.