Ловим эксплойты повышения привилегий в ядре Linux
Большинство детектов повышения привилегий смотрят на парадную дверь: вызов sudo, syscall setuid, SUID-бинарь в домашнем каталоге, pkexec с пустым argv. Это ловит мисконфиги и злоупотребление инструментами, и это нужно делать — у SecureExec есть правила на всё перечисленное. Но эксплойт ядра не делает ничего из этого. Use-after-free в netfilter, out-of-bounds запись в io_uring, баг в ksmbd — он портит память ядра, пока не сможет выполнить один примитив, commit_creds(&init_cred), и вызывающий процесс мгновенно становится root. Ни setuid, ни SUID-бинаря, ни sudo. Детекты парадной двери не срабатывают — атакующий к двери и не подходил.
Поэтому мы смотрим на то, что эксплойт действительно меняет: учётные данные процесса в том виде, в каком их держит ядро. В статье — три отпечатка, которые оставляет LPE в ядре, почему нормальная активность их никогда не порождает и как мы провалидировали это на реальных ядрах.
Что на самом деле делает LPE в ядре
Как только у эксплойта есть примитив записи в память ядра, получение root — почти формальность. Техник немного, и все они заканчиваются одним и тем же небольшим набором состояний:
- Перезаписать учётные данные текущей задачи. Классический payload —
commit_creds(prepare_kernel_cred(NULL))или, короче, указатьcredзадачи прямо на глобальныйinit_credядра. Все id становятся 0. Процесс — root, и он не выполнил ни одного syscall привилегий. - Перезаписать строку, которую ядро запускает как root.
modprobe_pathиcore_pattern— это строки, которые ядро исполняет (как root, из контекста ядра), когда автозагружается модуль или процесс дампит core. Перезапиши строку, спровоцируй событие — и ядро запустит твой бинарь за тебя. - Злоупотребить
call_usermodehelperнапрямую. Ряд недавних LPE (ksmbd, серия багов в netfilter) заставляют ядро породить helper по выбору атакующего. Результат тот же: root-процесс, родитель которого — само ядро.
Ни один из этих путей не порождает syscall setuid, SUID-exec или процесс sudo. EDR, смотрящему только на syscall'ы, не на что реагировать. Но каждый оставляет структурный след, которого на нормальной системе не бывает.
Отпечаток 1: указатель cred, который смотрит на init_cred
Легитимный userspace-процесс никогда не разделяет init_cred. На каждом fork ядро вызывает prepare_creds() — отдаёт потомку свежую копию учётных данных, а не глобальный объект. &init_cred держит только idle-задача, и твой shell она не запускает.
Поэтому проверка точная: на execve читаем указатель cred текущей задачи и сравниваем с init_cred ядра. Если они равны — что-то перезаписало учётные данные этой задачи, отпечаток commit_creds(&init_cred). Агент читает адрес init_cred из /proc/kallsyms, а смещение task->cred — из собственного BTF ядра, так что захардкоженных смещений нет; это работает между версиями ядра и само отключается (а не гадает) на ядре, где нет ни того, ни другого. Это правило cred_tamper, срабатывает critical.
Отпечаток 2: root, которым нельзя было стать
Проверка указателя точная, но узкая. Более широкий сигнал — поведенческий: процесс, который UID 0 на момент execve, но
- не запускал setuid-root бинарь (ядро фиксирует setuid-бит на диске, и его нет), и
- не делал syscall
setuid/setreuid/setresuid, и - имеет родителя, которого SecureExec зафиксировал работающим под не-root пользователем.
Root-shell, появившийся под не-root родителем, без setuid-бинаря и без setuid-вызова между ними, не получил root ни одним механизмом, который предлагает система. Он получил его в ядре. Это silent_privilege_escalation — оно ловит перезапись учётных данных, которую cred_tamper пропустит (эксплойт, обнуляющий cred->uid на месте, а не подменяющий весь указатель), и опирается на родословную процессов, которую SecureExec и так отслеживает, так что лаунчеры sudo, su, pkexec и контейнерные рантаймы исключены по построению.
Отпечаток 3: ядро запускает программу, которую не должно
Техники modprobe_path / core_pattern / call_usermodehelper объединяет один родитель — ядро. Родитель usermodehelper'а — это kthreadd (PID 2) или kworker, а не init, не shell и не сервис.
Ядро легитимно порождает лишь небольшой фиксированный набор helper'ов: modprobe, request-key, key.dns_resolver, обработчик coredump и ещё несколько — всегда из системного каталога. kernel_spawned_process срабатывает, когда ядро запускает что-то ещё: бинарь из /tmp, shell, helper из пользовательского каталога. Это ровно та форма, которую оставляет перезапись modprobe_path, и LPE в ksmbd и netfilter последних лет заканчиваются именно здесь.
Почему нормальная активность это не триггерит
Для правила с severity critical значим именно false positive, поэтому каждое из них построено на инварианте ядра, а не на эвристике:
cred_tamper— это точное равенство указателей сinit_cred, а ядро структурно никогда не отдаёт этот указатель userspace-процессу.silent_privilege_escalationтребует отсутствия сразу всех легитимных путей к root — setuid-бинаря, setuid-syscall, привилегированного родителя — что совпадает только когда ничего из этого не было.kernel_spawned_process— это allowlist собственных helper'ов ядра; всё вне него по определению не то, что ядро поставляет.
Мы не принимаем это на веру. Мы прогнали агент на реальном хосте Ubuntu 24.04 (ядро 6.8) под несколькими сотнями обычных запусков процессов — shell'ы, id, date, пакетные инструменты, всё подряд — и проверка учётных данных сработала ноль раз на 353 execve. Она загорается только на том, чего само по себе случиться не может. И поскольку вся проверка идёт от tracepoint на execve, ей не нужен BPF-LSM — она работает на стоковых дистрибутивах, где bpf нет в активном списке LSM, а это большинство из них.
Что это даёт вашему SOC
Детекты повышения привилегий «с парадного входа» — злоупотребление sudo, SUID-дропы, PwnKit, GTFOBins — говорят, когда кто-то прошёл по собственным путям эскалации системы. Эти три правила говорят, когда кто-то их полностью обошёл и взял root в ядре. Это разница между тем, чтобы поймать мисконфиг, и тем, чтобы поймать 0-day, применённый против вас.
Они ничего не стоят, когда не могут разрешить свои смещения (тихо остаются выключенными), не дают false positive на нагруженном хосте и превращают самый тихий и самый опасный шаг вторжения — тот, к которому большинство endpoint-инструментов слепы, — в critical-алерт с полным деревом процессов.
Разбор на вашей инфраструктуре — запросите демо. Мы запускаем на реальном Linux-хосте, без слайдов.