Читать 6 мин

Идентичность исполняемого файла агента после замены символической ссылки

Проверьте идентичность исполняемого файла агента при замене символической ссылки в 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 рассматривает требования к коду как ограничения идентичности, а не как имена файлов. В документации сказано, что designated requirement содержит критерии, по которым определяется, является ли код тем же кодом, который уже встречался ранее. Обычно они выводятся из полномочий подписанта и встроенного идентификатора, если разработчик не задал требование явно. Это правильное направление, но есть важная оговорка: требование описывает идентичность кода, а не живую сессию процесса.

Одобрение получает запущенный процесс

Шлюз должен принимать решение об авторизации на основе действительной ссылки на процесс, полученной от операционной системы. Затем это решение нужно привязать к запуску процесса, а не к пути, заявленному клиентом, имени исполняемого файла или переменной окружения, которую можно использовать повторно.

В macOS хорошая проверка решает две задачи:

  1. Получает сведения о подписи фактического вызывающего процесса.
  2. Проверяет этот процесс до того, как использовать сведения об идентичности как основание для авторизации.

Это разные задачи. SecCodeCopyDesignatedRequirement может вернуть designated requirement для подписанного кода, но Apple прямо отмечает, что этот вызов не проверяет подпись. Код, измененный после подписания или подписанный неправильно, все равно может вернуть неполные или вводящие в заблуждение сведения. Поэтому шлюз должен отдельно проверять действительность подписи.

Запись о процессе должна содержать достаточно свидетельств, чтобы позже объяснить решение:

  • идентификатор процесса или, еще лучше, выданные ОС учетные данные процесса, защищенные от повторного использования PID;
  • путь к исполняемому файлу, зафиксированный во время проверки и сохраненный только как контекст;
  • идентификатор подписи и идентификатор команды, если они доступны;
  • полномочия подписанта и цепочку сертификатов, если применимо;
  • designated requirement;
  • cdhash или набор поддерживаемых значений cdhash;
  • результат проверки подписи и время проверки.

Не превращайте этот список в язык политик. Шлюзу нужен короткий ответ: какой запущенный процесс отправляет запрос и одобрил ли человек именно этот запуск? Нескольких фактов об идентичности достаточно. Набор правил по путям не поможет.

Сложнее всего обрабатывать обновления. Designated requirement часто сохраняется у легитимных версий. Поэтому macOS использует его для сохранения непрерывности, когда пользователь разрешает приложению доступ к защищенной службе. В технической записке Apple TN3127 приведен знакомый пример с обновлением приложения, которое снова получает доступ к микрофону: macOS сравнивает новую версию с сохраненным designated requirement.

Для долгосрочного системного разрешения это разумно. Но если молча перенести прежнее согласие человека на новый автономный запуск агента, правило окажется слишком широким. Шлюз может показывать оператору полномочия подписанта, чтобы тот узнал программу, и при этом снова спрашивать согласие при каждом новом запуске процесса. Подписант подтверждает происхождение кода. Граница процесса определяет срок действия согласия.

Статические проверки пути оставляют гонку

Статическая проверка отвечает на статический вопрос: «Какую подпись сейчас имеет файл по этому пути?» Сама по себе она не отвечает на вопрос: «Какой код отправил этот запрос?»

Разрыв важен даже тогда, когда статическая проверка технически безупречна. Допустим, шлюз получает запрос с указанием пути /Users/dev/bin/agent. Он выполняет:

codesign --verify --strict --verbose=2 /Users/dev/bin/agent
codesign -d -r- /Users/dev/bin/agent

Обе команды могут сообщить о действительной подписи и designated requirement. Затем шлюз сохраняет имя пути как одобренную идентичность. До следующего запроса злоумышленник может направить 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 сообщает, что у кода с ad hoc-подписью нет сертификатов и 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 для каждой сессии включена по умолчанию, а в карточке согласия сначала указаны полномочия подписанта кода процесса, а не изменяемое имя файла. Оператор получает более надежный факт для оценки, когда новый процесс агента просит выполнить действие.

Подписант, designated requirement и cdhash отвечают на разные вопросы

Команды часто сводят три разных понятия к фразе «подписано тем же приложением». Такое упрощение приводит либо к усталости от запросов согласия, либо к разрешениям, которые действуют слишком долго.

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

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

Cdhash идентифицирует конкретный подписанный CodeDirectory. Для авторизации это почти отпечаток сборки. Он отлично подходит для аудита, потому что позволяет отличить две версии с одним подписантом и идентификатором. Для постоянного разрешения инструментов разработчика это обычно плохое правило: обычные обновления будут менять cdhash.

Используйте свидетельства в соответствии с решением:

  • Применяйте учетные данные живого процесса, чтобы привязать сессию к вызывающему процессу.
  • Показывайте в интерфейсе согласия полномочия подписанта и идентификатор, чтобы оператор мог узнать источник.
  • Записывайте designated requirement и cdhash, чтобы последующее расследование различало непрерывность издателя и точную сборку.
  • Снова запрашивайте согласие для нового процесса агента, даже если подписант и designated requirement совпадают.

Последний пункт намеренно строже разрешений macOS. Шлюз агента может отправлять HTTP-запросы или SSH-команды с учетными данными, которые никогда не попадают в процесс агента. Это граница действия, а не одноразовый запрос доступа к микрофону. Перенос прежнего нажатия на другой будущий процесс ослабляет человеческий контроль, ради которого и создавался шлюз.

Не превращайте наблюдение за файлами в авторизацию

Проверяйте цепочку офлайн
Автономная команда sp audit verify проверяет зашифрованную хеш-цепочку без ключа хранилища.

Иногда предлагают следить за путем агента и отзывать разрешение при изменении файла. Решение кажется простым: сохранить исходный inode, подписаться на события файловой системы и отменять согласие после записи или переименования.

Это неправильная основа.

События файловой системы полезны как телеметрия, но не доказывают идентичность вызывающего процесса. У пути может быть несколько имен. Бинарный файл можно скопировать. Процесс может быть запущен из уже удаленного файла. Доставка событий может задерживаться или объединять несколько изменений. Злоумышленнику не нужно выигрывать зрелищную гонку, если ваша конструкция изначально авторизует путь, а не процесс.

Сравнение inode тоже не исправляет модель. Оно может обнаружить одну замену в одном каталоге, но ничего не говорит о процессе, запущенном через другую жесткую ссылку или скопированный объект. Кроме того, оно создает хрупкое поведение в обычной разработке, где инструменты постоянно пересобираются, переименовываются и заменяют файлы.

Считайте наблюдения за файлами поводом добавить полезный контекст в аудит. Если текущий путь ведет к другому объекту, чем во время согласия, запишите этот факт. Если шлюз видит новый вызывающий процесс, проверьте его и примените обычный поток авторизации. Граница процесса решает задачу безопасности без доверия к наблюдателю как к эталонному контроллеру.

Это правило предотвращает и более тонкую ошибку: одобрение оболочки по знакомой подписи без проверки того, что она запускает. Подписанная оболочка может выполнить неподписанный дочерний процесс, загрузить локальный скрипт или выбрать цель по окружению. Шлюз должен идентифицировать процесс, который действительно подключается и запрашивает привилегированное действие. Если вызывающим является оболочка, одобряется она. Если вызывающим является ее дочерний процесс, проверяйте его.

Привязывайте согласие к учетным данным ОС и отказывайте при неоднозначности

Одобряйте процессы, а не пути
Sallyport одобряет новый процесс агента, а не изменяемый путь к команде.

Практической реализации нужен предоставленный ОС дескриптор запрашивающей стороны. В macOS это обычно означает получение объекта кода гостя для вызывающего процесса через Code Signing Services, используя атрибуты процесса из доверенного контекста соединения, а не значения, скопированные из входных данных агента.

Безопасная последовательность выглядит так:

  1. Примите соединение и получите идентичность вызывающей стороны из механизма соединения.
  2. Свяжите эту идентичность с работающим объектом кода.
  3. Проверьте действительность кода до чтения сведений об идентичности для авторизации.
  4. Получите из работающего объекта полномочия подписанта, идентификатор, designated requirement и cdhash.
  5. Создайте запись сессии, связанную с учетными данными вызывающей стороны, и запросите согласие, если одобренной сессии нет.
  6. Перед каждым действием снова проверяйте, что учетные данные вызывающей стороны относятся к тому же живому процессу. Уничтожайте сессию после завершения процесса или отзыва оператором.

Детали первого шага зависят от транспорта. Локальный 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 и проверяйте их до выдачи сессии.

Что такое designated requirement в macOS?

Designated requirement описывает семейство кода, которое macOS считает той же издательской и прикладной идентичностью при легитимных обновлениях. Он не указывает единственную точную сборку, поэтому подходит для обеспечения непрерывности, но не должен быть единственной записью о том, что запускалось.

Стоит ли одобрять агента по cdhash?

Cdhash идентифицирует конкретный подписанный каталог кода и служит полезным свидетельством для определенной сборки. Это более строгий признак, чем издатель или designated requirement, но постоянная привязка к нему будет ломать обычные обновления, пока кто-то специально не одобрит новую сборку.

Должно ли согласие для агента сохраняться после перезапуска?

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

Почему одобрение по пути опасно для AI-агентов?

Опасный сценарий выглядит так: помощник одобряют один раз, а все будущие процессы, запущенные из этого пути, считают одобренными. Злоумышленнику достаточно заменить символическую ссылку, оболочку или бинарный файл после первоначального решения, не меняя знакомое имя команды.

Что должен записывать журнал аудита действий агента?

В журнале должны быть идентификатор процесса или audit token, наблюдавшийся путь к исполняемому файлу как контекст, издатель подписи, идентификатор, designated requirement, cdhash, результат проверки и время начала сессии. Путь полезен для диагностики, но не должен быть якорем доверия.

Что должно произойти, если замена не подписана или подписана иначе?

Неподписанная замена должна быть отклонена, если правило согласия требует действительной подписи. Замена, подписанная другим издателем, требует нового согласия. Легитимное обновление, подписанное идентичностью, которую вы сознательно разрешили, должно обрабатываться по выбранной вами политике обновлений.

Безопасно ли проверять замену символической ссылки на рабочем Mac?

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

Sallyport

Sallyport выполняет API-вызовы и SSH-команды за вашего ИИ-агента. Ключи остаются в локальном хранилище на вашем Mac; вы подтверждаете каждый запуск, и каждое действие попадает в запечатанный журнал.

© 2026 Sallyport · Открытый код по лицензии Apache-2.0 · Oleg Sotnikov