SecureExec
Главная
Решения
ЦеныБлогО нас
ВойтиРегистрация
← Назад к блогу

BPF-LSM: детектирование в точке решения ядра

24 октября 2026 г.

Большинство Linux-EDR смотрят на syscall'ы. SecureExec тоже — но вдобавок перехватывает уровень Linux Security Module (LSM) через eBPF, ровно там, где ядро решает, разрешить ли операцию. Syscall-tracepoint видит аргументы так, как их передал userspace: голый файловый дескриптор, пустой путь, указатель, который ядро ещё не разыменовало. BPF-LSM-хук срабатывает несколькими инструкциями позже — после того как ядро разрешило fd в реальный файл, прошло путь и закоммитило привилегии. Он видит разрешённый объект — то, о чём операция на самом деле.

Именно в этом зазоре живёт целый класс техник обхода, и его закрытие — это то, что даёт BPF-LSM. В статье: что уровень LSM видит такого, чего не видят tracepoints; какие детекты это открывает; и как мы сделали это production-grade, а не демо.

Слепое пятно syscall-tracepoints

Syscall-tracepoint подключён на входной границе. Он читает сырые аргументы из регистров и ничего больше — потому что в этот момент ничего больше и нет, ядро ещё не сделало свою работу. Для большинства событий этого достаточно. Для операций, которые интересуют атакующего, это ровно неподходящий момент, чтобы смотреть:

  • Выполнение по дескриптору. execveat(fd, "", AT_EMPTY_PATH) запускает то, на что указывает fd — memfd, удалённый файл, пайп. Tracepoint видит path = "". Бинарь, который реально запускается, в аргументах не появляется.
  • W^X-память. Классический паттерн shellcode: mmap(PROT_READ|PROT_WRITE), запись полезной нагрузки, затем mprotect(…, PROT_EXEC). Хук на mmap видит безобидный writable-маппинг; момент, когда страница становится исполняемой — собственно сигнал — происходит в последующем mprotect, за которым он не наблюдает.
  • Пути, которые он не может разрешить. openat(dirfd, "../../etc/shadow"), symlink, путь через /proc/self/fd/N. У tracepoint есть строка, но не файл. Разрешать её в userspace — это гонка, и для короткоживущих процессов — проигрышная.

Каждое из этих мест можно залатать обогащением в userspace — прочитать /proc/<pid>/exe, заново пройти путь — но вы постфактум реконструируете информацию, которая у ядра уже была и которую оно выбросило. Под нагрузкой, против процесса, который завершается за миллисекунды, реконструкция проигрывает.

BPF-LSM: точка принятия решения ядром

Фреймворк LSM — это набор хуков, которые ядро вызывает, чтобы спросить «разрешить ли это?» — те же хуки, на которых построены SELinux и AppArmor. С ядра 5.7 BPF_PROG_TYPE_LSM позволяет подключить к ним eBPF-программу. К моменту срабатывания LSM-хука ядро уже сделало то разрешение, которое было недоступно tracepoint'у:

            execveat(fd=7, "", AT_EMPTY_PATH)
                        │
        userspace ──────┤  входит в ядро
                        │
   ┌────────────────────▼──────────────────────┐
   │  sys_enter_execveat   (syscall tracepoint) │   видит АРГУМЕНТЫ как переданы:
   │  → path = ""   fd = 7   → цель НЕИЗВЕСТНА  │   голый fd, пустая строка
   └────────────────────┬──────────────────────┘
                        │  ядро резолвит fd → файл,
                        │  проходит путь, коммитит creds
   ┌────────────────────▼──────────────────────┐
   │  bprm_check_security   (BPF-LSM-хук)       │   видит РАЗРЕШЁННЫЙ объект:
   │  → /usr/sbin/sshd   → реальный бинарь      │   файл, который ядро запустит
   └────────────────────┬──────────────────────┘
                        │
                   allow / deny

LSM-хуки SecureExec — observe-only: каждый возвращает 0 (allow). Они ничего не энфорсят, не добавляют задержку к решению, не рискуют заблокировать легитимную операцию. Их задача — прочитать разрешённый объект и выпустить событие. Детекция остаётся там, где ей место — на сервере, в движке алертов — тот же инвариант, которому следует любой сенсор SecureExec.

Какие LSM-хуки мы ставим

Семь хуков, каждый читает то, что разрешило ядро и чего не мог получить эквивалентный tracepoint:

LSM-хукЧто разрешило ядроЧто ловит
bprm_check_securityреальный запускаемый бинарь (exe_resolved)запуск memfd/по fd, гонки exec-then-exit, подмена пути под нагрузкой, которую теряет /proc/<pid>/exe
file_mprotectстраница, ставшая writable и executablein-memory инъекция / shellcode — флип mprotect W→X, который путь mmap не видит
file_openразрешённый путь чтениячтение секретов по symlink / relative / fd путям, которые openat-tracepoint не резолвит
socket_connect / socket_bindадресат прямо из sockaddr, протокол из сокетапокрытие UDP-connect без хрупких офсетов sock_common, на которые опирались kprobes
task_fix_setuidзакоммиченные uid / euidсмена привилегий по успешному коммиту — без ложняка от неудавшегося setuid
ptrace_access_checkодин процесс лезет в память другогоинъекция / дамп кредов — ptrace attach, process_vm_*, /proc/<pid>/mem

Большинство из них переиспользуют существующие типы событий, поэтому серверные правила и proto-схема не менялись — события просто приходят с разрешённой правдой вместо userspace-приближения.

Что это открывает

Смысл видеть разрешённый объект — в детектах, которые становятся надёжными:

  • Гонка-устойчивый lineage выполнения. bprm_check захватывает цель exec прямо во время execve и прикрепляет её к событию процесса как exe_resolved — индексируется как keyword-поле. Аналитики и правила могут сравнить её с путём из procfs, что ловит запуск memfd и по fd, а также гонки exec-then-exit, которые под нагрузкой мис-атрибутируют бинарь.
  • W^X-память (memory_wx). file_mprotect ловит момент, когда страница переводится в writable-и-executable — код записан, затем сделан исполняемым на месте. Это форма reflective loader'а или пакера, и это ровно тот переход, который tracepoint на mmap наблюдать не может.
  • Чтение секретов по реальному пути. file_open применяет матчинг чувствительного чтения к пути, который разрешило ядро, поэтому чтение SSH-ключа или /etc/shadow через symlink или относительный путь репортится один раз и с истинной целью — а не теряется из-за того, что строка аргумента не выглядела чувствительной.
  • Сетевой egress из самого сокета. socket_connect читает адресата прямо из sockaddr, а протокол — из объекта сокета, что добавляет покрытие UDP-connect, которого не было у TCP-kprobes, и убирает хрупкие чтения офсетов sock_common/socket->sk, о которых всегда предупреждал наш conventions-документ.
  • Смена привилегий, которая действительно случилась. task_fix_setuid срабатывает на закоммиченных привилегиях, поэтому репортит реальный переход в root и молчит на setuid, который не удался — в отличие от syscall-tracepoint, который видит попытку в любом случае.

Подключается best-effort, дедуплицируется и честно об этом сообщает

Хуки уровня ядра стоит ставить, только если их безопасно включать везде. Это обеспечивают три вещи.

Они не заменяют tracepoints — они их уточняют. Каждый LSM-хук подключается best-effort. На ядре без BPF-LSM или без нужного поля структуры хук просто не подключается, и источником остаётся существующий tracepoint или kprobe. Включение BPF-LSM только углубляет сбор; сломать его оно не может.

Нет CO-RE, поэтому офсеты валидируются по работающему ядру. eBPF агента собран на Aya, чей тулчейн поставляет непрозрачные структуры ядра — CO-RE-релокаций нет, а bpf_d_path из Rust-LSM-хука неприменим. Поэтому путь-резолвящий хук проходит цепочку dentry вручную, читая dentry->d_name, ->d_parent и прочее по офсетам, разрешённым из собственного BTF ядра при загрузке. Если BTF не описывает поле — хук отключает себя сам, а не читает неверный офсет:

#[lsm(hook = "bprm_check_security")]
pub fn bprm_check(ctx: LsmContext) -> i32 {
    // Ядро уже разрешило бинарь. Читаем путь из linux_binprm->file,
    // проходя цепочку dentry по офсетам, которые загрузчик проверил
    // по BTF ядра, затем сохраняем по tgid.
    let _ = resolve_bprm_path(&ctx);
    0 // observe-only: 0 = allow. Детекция остаётся на сервере.
}

Один источник, никогда не два. Там, где LSM-хук и tracepoint/kprobe покрывают одно и то же — connect сокета, чувствительное чтение — агент использует флаг «prefer-LSM». Когда подключается более богатый LSM-хук, он поднимает флаг, который проверяет fallback, и fallback уходит в сторону — одна операция никогда не репортится дважды:

// Kprobe tcp_v4_connect — это fallback. Как только socket_connect (LSM)
// подключился, он становится единственным источником, и kprobe уступает —
// флаг поднимается, только если хук реально подключился, так что connect
// никогда не теряется.
if crate::lsm::off(LSM_OFF_CONNECT_ACTIVE) {
    return 0;
}

Вы всегда знаете свой уровень покрытия

BPF-LSM мощный, но на большинстве дистрибутивов он выключен по умолчанию. Сток Ubuntu идёт с CONFIG_BPF_LSM=y, но не добавляет bpf в активный список LSM (/sys/kernel/security/lsm), пока администратор не пропишет его в командную строку ядра. Инструмент, который здесь молча откатывается на tracepoints, оставляет вас гадать о собственной глубине покрытия.

SecureExec об этом сообщает. Агент отправляет статус BPF-LSM в каждом heartbeat — active, degraded (ядро разрешает, но в BTF нет нужных хукам полей) или off — и страница эндпоинта в консоли показывает его вместе с точным способом исправления, когда он off:

GRUB_CMDLINE_LINUX="lsm=lockdown,capability,landlock,yama,apparmor,bpf"
update-grub && reboot

Так что глубокая аналитика — это не загадка. Вы видите по каждому хосту, живы ли хуки уровня ядра, и включаете их там, где они ещё не включены.

Как включить BPF-LSM на Linux-хостах

BPF-LSM нужно от ядра две вещи: оно должно быть собрано с CONFIG_BPF_LSM=y — так у любого mainline-ядра с 5.7 и у всех актуальных ядер Ubuntu, Debian, RHEL и Amazon Linux — и bpf должен быть в активном списке LSM ядра. Обычно не хватает именно второго: сток-дистрибутивы компилируют BPF-LSM, но не включают его.

Проверить текущий список:

cat /sys/kernel/security/lsm
# lockdown,capability,landlock,yama,apparmor   ← нет "bpf" — значит выключено

Если bpf там нет, добавьте его в параметр lsm= командной строки ядра. Этот параметр заменяет активный набор, поэтому перечислите всё, что уже есть, и допишите bpf — выкинуть существующий модуль (AppArmor, SELinux, …) значит его отключить.

Debian / Ubuntu. Отредактируйте /etc/default/grub, чтобы GRUB_CMDLINE_LINUX нёс ваш текущий список плюс bpf, затем пересоберите GRUB и перезагрузитесь:

# /etc/default/grub
GRUB_CMDLINE_LINUX="... lsm=lockdown,capability,landlock,yama,apparmor,bpf"
sudo update-grub && sudo reboot

RHEL / Fedora / Amazon Linux. Здесь вместо AppArmor — SELinux, поэтому берите значение из своего /sys/kernel/security/lsm:

sudo grubby --update-kernel=ALL --args="lsm=lockdown,capability,yama,selinux,bpf"
sudo reboot

После перезагрузки убедитесь, что включилось:

cat /sys/kernel/security/lsm
# lockdown,capability,landlock,yama,apparmor,bpf   ← bpf активен

Агент SecureExec подхватит это на следующем heartbeat — без переустановки и смены конфигурации — и статус BPF-LSM у эндпоинта в консоли переключится с off на active. Если в ядре действительно нет CONFIG_BPF_LSM, агент остаётся на tracepoints, а статус показывает off; все остальные детекты продолжают работать.

Что это значит для вашего SOC

Syscall-tracepoints говорят, что процесс попросил ядро сделать. BPF-LSM говорит, что ядро собиралось сделать — с уже разрешёнными бинарём, путём, привилегиями и адресатом. Такая телеметрия труднее для обхода (техники обхода живут в зазоре между этими двумя точками), точнее (серверные правила работают по ground truth, а не по userspace-догадке) и закрывает слепые пятна — запуск по fd, флипы W→X, чтение секретов по symlink — которые EDR «только на syscall'ах» структурно увидеть не может.

Оно работает на обычных ядрах, ничего не стоит, когда не может подключиться, и точно сообщает, где оно активно. Это та глубина, которую endpoint-платформа должна давать по умолчанию.

Для guided walkthrough на вашей инфраструктуре — включая включение BPF-LSM и наблюдение за тем, как приходит дополнительный сигнал — запросите демо. Проходит на реальном Linux-хосте, без слайдов.

SecureExec

SecureExec — Linux-first EDR для детекции, расследования и реагирования на инциденты на серверах. Cloud или self-hosted.

Продукт
  • Цены
Компания
  • О нас
  • Блог
  • Связаться с отделом продаж
  • Поддержка
Аккаунт
  • Войти
  • Регистрация
Правовая информация
  • Политика конфиденциальности
  • Условия использования

© 2026 SecureExec. Все права защищены.

Built with Rust & Next.js