Как устроен перехват

Как устроен перехват#

astra-shim решает две задачи: переписывает пути, зашитые в программу, и подменяет команды, которых в песочнице нет. Обе задачи описываются одной таблицей правил, а применяются двумя независимыми слоями перехвата.

Таблица правил#

Правила лежат в одном файле формата ключ = значение. Путь к нему задаёт переменная окружения ASTRA_SHIM_CONFIG; при сборке через aft-app это /app/etc/astra-shim.conf.

debug = 1

/usr/lib/myapp = /app/lib/myapp
/etc/myapp     = /app/etc/myapp

apt       = flatpak-spawn --host apt
systemctl = flatpak-spawn --host systemctl

Ремап пути срабатывает по префиксу и границе компонента. Правило /usr/lib/myapp поймает /usr/lib/myapp/data.bin, но не /usr/lib/myapp-other. Так правило не расползается на соседние каталоги.

Подмена команды сопоставляется по имени исполняемого файла без каталога. Исходные аргументы дописываются после команды-замены, поэтому запись apt = flatpak-spawn --host apt превращает вызов apt install foo в flatpak-spawn --host apt install foo.

Строка debug = 1 включает журнал перехвата в поток ошибок. Она нужна при подборе правил и в готовой сборке не оставляется.

Слой LD_PRELOAD#

Основной слой, работает всегда. Библиотека подключается к процессу через LD_PRELOAD и перехватывает функции libc. Уже подключённые программой библиотеки в LD_PRELOAD сохраняются.

Перехватываются обращения к файловой системе: open, openat, creat, fopen, freopen, stat, lstat, fstatat, statx, access, faccessat, opendir, readlink, readlinkat, realpath, canonicalize_file_name, mkdir, mkdirat, rmdir, unlink, unlinkat, rename, link, symlink, chmod, chown. Для старых сборок дополнительно перехватывается семейство __xstat, через которое до glibc 2.33 проходили вызовы stat.

Перехватывается запуск процессов: execve, execv, execvp, execvpe, posix_spawn, posix_spawnp, system, popen.

Этого хватает обычным программам, собранным против libc, включая приложения на Electron.

Слой seccomp#

Дополнительный слой, включается переменной ASTRA_SHIM_SECCOMP=1. Он ловит то, что LD_PRELOAD физически не видит:

  • загрузку разделяемых библиотек динамическим загрузчиком ld.so, который зовёт openat напрямую, минуя перехватываемые обёртки;

  • статически слинкованные программы и программы на Go, которые обращаются к ядру, не проходя через libc.

Слой построен на механизме ядра SECCOMP_RET_USER_NOTIF. Лаунчер ставит фильтр на openat, openat2, newfstatat, statx, faccessat, faccessat2 и readlinkat, а супервизор по совпавшему правилу сам открывает файл по новому пути и передаёт дескриптор в целевой процесс либо отвечает на запрос о свойствах файла. Пути, не совпавшие ни с одним правилом, ядро выполняет как есть, поэтому накладные расходы ложатся только на системные вызовы с путями.

Композиция слоёв#

Слои используют одну таблицу правил и не мешают друг другу. Для обычного вызова через libc путь переписывает LD_PRELOAD, и до ядра доходит уже /app/..., который ни с одним правилом не совпадает и проходит без вмешательства. Для ld.so и статических программ до ядра доходит исходный путь, и его переписывает seccomp. Двойного применения правила не происходит.