MAATRIX / Блог / Что такое setuid и почему обычная программа выполняется от имени root

Что такое setuid и почему обычная программа выполняется от имени root

MAATRIX

Открываете ls -l /usr/bin/passwd и видите на месте привычного x в правах владельца странную букву s. Программа принадлежит root, но запускает её любой пользователь — и почему-то ей это разрешено лезть в системные файлы, которые обычному аккаунту недоступны на запись. Это не баг — это специальный бит setuid, один из самых старых и до сих пор рабочих механизмов Unix-подобных систем. Разберёмся, как он устроен на уровне ядра, зачем нужен и почему каждый такой бинарник на сервере — осознанный компромисс между удобством и риском.

Что вообще происходит, когда вы запускаете программу

В обычной ситуации любой процесс в Linux наследует права того, кто его запустил. Если вы вошли под пользователем alice и запустили cat /etc/shadow, ядро проверит, может ли alice читать этот файл, увидит права -rw-r----- с владельцем root и группой shadow, и откажет — Permission denied. Так и должно быть: /etc/shadow хранит хеши паролей всех пользователей системы, и доступ к нему на чтение или запись есть только у root.

Но здесь возникает практическая проблема. Пользователь должен уметь менять свой собственный пароль — это базовая операция, которая не должна требовать звонка администратору. А смена пароля физически означает запись новой строки в /etc/shadow. Обычными правами доступа эту задачу не решить: либо /etc/shadow открыт на запись всем (катастрофа безопасности — любой подменит чужой хеш), либо пользователь физически не может поменять свой пароль без root.

Именно для таких ситуаций и придумали setuid.

Бит setuid: как он работает на уровне ядра

У каждого процесса в Linux есть не один, а несколько идентификаторов пользователя:

  • real UID (ruid) — кто на самом деле запустил процесс;
  • effective UID (euid) — от чьего имени процесс сейчас действует при проверке прав доступа к файлам, портам и другим ресурсам;
  • saved UID (suid) — сохранённое значение, к которому процесс может вернуться, если временно понижал привилегии.

В обычном процессе ruid и euid совпадают. Но если исполняемый файл помечен битом setuid, ядро при запуске (execve()) устанавливает effective UID процесса равным владельцу файла, а не тому, кто его запустил. Real UID при этом не меняется — система по-прежнему помнит, что физически программу запустила alice, но при любой проверке прав (открыть файл, забиндить порт, отправить сигнал) используется именно effective UID.

Если файл принадлежит root и на нём выставлен setuid, процесс на время работы получает effective UID 0 — права root — для всех операций, проверяющих владельца. Программа сама решает, что делать с этой властью: временно использовать для узкой операции, а затем явно сбросить вызовом setuid()/seteuid() перед тем, как отдать управление пользователю.

Посмотреть эти идентификаторы у живого процесса можно через /proc:

cat /proc/self/status | grep -E '^(Uid|Gid):'

Строка Uid: покажет четыре числа — real, effective, saved и filesystem UID. У обычного шелла все четыре совпадают с вашим UID. У процесса, запущенного через setuid-бинарник в момент повышенных прав, effective и saved будут равны 0, а real — вашему исходному UID.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Классический пример: как работает passwd

Лучше всего механизм виден на утилите passwd, с которой сталкивался каждый, кто администрировал Linux-сервер.

ls -l /usr/bin/passwd
# -rwsr-xr-x 1 root root 68208 авг 12 09:14 /usr/bin/passwd

Обратите внимание на s вместо x в блоке прав владельца (rws вместо rwx). Это и есть setuid — визуально он всегда виден именно там, где обычно стоит x у владельца файла.

Файл принадлежит root, и когда пользователь alice запускает passwd, происходит следующее:

  1. Ядро запускает бинарник и, увидев setuid-бит, выставляет effective UID процесса в 0 (root), хотя real UID остаётся UID пользователя alice.
  2. Программа passwd спрашивает текущий пароль и проверяет его — это операция, для которой правами root не нужно, но нужно прочитать хеш из /etc/shadow, что обычному пользователю недоступно. Effective UID = root это разрешает.
  3. После успешной проверки passwd формирует новый хеш и с правами root дописывает обновлённую строку в /etc/shadow — файл, к которому у alice в принципе нет прав на запись.
  4. Программа завершает работу, effective UID процесса перестаёт иметь значение — новый процесс (например, шелл, из которого вы её вызвали) как ни в чём не бывало продолжает работать с исходными правами alice.

Ключевой момент: alice в итоге поменяла только свой собственный пароль. Программа passwd написана так, что даже обладая root внутри себя, она разрешает пользователю менять запись именно о нём самом (проверяет соответствие UID вызывающего процесса и целевого пользователя), а не любую строку в /etc/shadow. Setuid дал доступ к ресурсу, а логика самой программы ограничила, что именно с этим доступом можно сделать: файл системного уровня остаётся закрытым для прямого доступа, а узкая программа-посредник выполняет ровно одну легитимную операцию от имени root и ничего больше.

По похожей схеме в системе работают sudo, su, mount (для обычных пользователей в некоторых конфигурациях), ping в старых системах без capabilities (нужен raw-сокет) и ряд других утилит с точечным доступом к ресурсам уровня ядра.

Как выставить и снять setuid

Бит устанавливается через chmod, символически или в восьмеричной записи. В восьмеричной записи setuid — это старшая цифра 4 перед обычными правами:

# символически
chmod u+s /path/to/program

# восьмеричным способом: 4 (setuid) + 755 (rwxr-xr-x)
chmod 4755 /path/to/program

# снять setuid
chmod u-s /path/to/program
# или явной восьмеричной формой без 4
chmod 0755 /path/to/program

Важный нюанс: современные ядра Linux по умолчанию игнорируют setuid на скриптах с shebang (#!/bin/bash и подобные) — намеренно, потому что setuid-скрипты исторически были источником уязвимостей через гонки (race conditions) между открытием интерпретатора и самого скрипта. Если нужно, чтобы скрипт выполнялся с повышенными правами, правильный путь — обёртка: небольшой C-бинарник с setuid, вызывающий скрипт, либо sudo с точечным правилом в /etc/sudoers.

Найти все setuid-бинарники в системе можно так:

find / -xdev -perm -4000 -type f 2>/dev/null

На свежей установке этот список обычно короткий — passwd, su, sudo, mount/umount, иногда ping (на многих современных дистрибутивах он уже переведён на capabilities вместо setuid). Если после установки софта или самописных скриптов список вырос — стоит разобраться, зачем каждому новому пункту нужен именно setuid, а не более узкий механизм.

setgid и sticky bit: соседние биты той же природы

Setuid — не единственный специальный бит прав доступа, и полезно понимать разницу, чтобы не путать их в логах и выводе ls -l.

setgid (восьмеричная 2, символ s в позиции группы) на исполняемом файле работает аналогично setuid, но подставляет effective GID — группу владельца файла — вместо UID. На директории setgid ведёт себя иначе: файлы, создаваемые внутри, автоматически наследуют её группу, а не основную группу создающего пользователя. Удобно для общих рабочих каталогов команды:

chmod g+s /srv/shared-project
# теперь все новые файлы в каталоге получают группу /srv/shared-project,
# а не личную группу пользователя, который их создал

Sticky bit (восьмеричная 1, символ t в позиции "прочие") исторически означал совсем другое — на старых Unix-системах он указывал ядру держать образ программы в свопе для более быстрого запуска. Сейчас на файлах он ни на что не влияет, а вот на директориях остаётся актуален: запрещает пользователю удалять или переименовывать чужие файлы внутри общей директории, даже если у него есть право записи в саму директорию. Классический пример — /tmp:

ls -ld /tmp
# drwxrwxrwt 15 root root 4096 авг 28 11:02 /tmp

Права 777 дают всем писать в /tmp, но t в конце гарантирует, что удалить файл там сможет только его владелец, владелец каталога или root.

БитВосьмеричное значениеНа файлеНа директории
setuid4000процесс получает euid владельца файлане действует
setgid2000процесс получает egid владельца файлановые файлы наследуют группу каталога
sticky1000не действует (устарело)удалять файлы может только владелец

Почему setuid — любимая цель атакующих

Если в программе с setuid-битом есть уязвимость — переполнение буфера, некорректная обработка переменных окружения, race condition между проверкой прав и использованием файла (TOCTOU, time-of-check to time-of-use) — результатом эксплуатации становится не просто крах процесса, а прямой путь к повышению привилегий (privilege escalation) с обычного пользователя до root. Это принципиально отличает баг в setuid-бинарнике от бага в обычной пользовательской программе: там ошибка максимум роняет процесс, здесь — потенциально отдаёт атакующему всю систему.

Отсюда несколько практических следствий, видных в том, как пишутся такие утилиты:

  • Минимальный код с root-правами. Хорошо написанная setuid-программа старается как можно раньше сделать всё, что требует повышенных прав, а затем явно вызвать setuid(getuid()), необратимо сбросив effective UID до реального пользователя, прежде чем выполнять что-то более сложное.
  • Недоверие к переменным окружения. PATH, LD_PRELOAD и другие переменные окружения атакующий полностью контролирует, если сам запускает setuid-программу. Классическая атака — подсунуть в PATH свою версию системной утилиты, которую setuid-программа вызывает без полного пути. Поэтому такие программы либо жёстко прописывают пути к вызываемым бинарникам, либо явно очищают окружение при старте.
  • Минимизация числа таких бинарников. Каждый новый setuid-файл — новая поверхность атаки. Список стараются держать коротким и периодически пересматривать через find / -perm -4000 — лишний setuid-бинарник, оставшийся после удалённого пакета, легко забывается и годами висит незамеченным риском.
  • Аудит вместо доверия. Раз баг в setuid-программе даёт root, такие программы традиционно проходят более тщательный review, чем обычные утилиты — именно passwd, su, sudo были и остаются самой очевидной целью для тех, кто ищет способ выйти из-под ограниченного пользователя.

Здесь стоит зайти почитать про антипаттерн, когда всё в системе просто запускают под root — setuid в этом смысле его прямая противоположность: не «дать root целиком», а «дать root на одну узкую операцию, реализованную в проверяемом коде».

Современная альтернатива: capabilities вместо setuid

Классический setuid — это бинарный выбор: либо процесс получает полный набор прав root, либо не получает вообще ничего особенного. Для большинства задач это избыточно: ping нужен не весь root, а конкретно право открыть raw-сокет; веб-серверу на 80-м порту — не всё, а право забиндить порт ниже 1024.

Для таких случаев современные ядра Linux предлагают capabilities — разбитие власти root на отдельные, независимо выдаваемые привилегии (CAP_NET_BIND_SERVICE, CAP_NET_RAW, CAP_SYS_ADMIN и другие). Вместо setuid-бита на бинарник можно выдать ровно ту capability, которая нужна:

# вместо setuid дать бинарнику только право открывать привилегированные порты
setcap 'cap_net_bind_service=+ep' /usr/bin/my-server

# посмотреть, какие capabilities выставлены на файле
getcap /usr/bin/my-server

Это снижает ущерб от возможной уязвимости: даже если атакующий полностью скомпрометирует такую программу, он получит не root целиком, а лишь ту узкую привилегию, что была выдана явно. Мы отдельно разбирали эту тему в статье про то, почему root внутри контейнера — не то же самое, что root на хосте — там capabilities разобраны подробнее, включая работу в Docker.

На практике имеет смысл придерживаться такого порядка приоритетов при выборе механизма повышения прав:

  1. Разово выполнить административную команду человеком — sudo с точечным правилом в sudoers, не setuid и не постоянный root-доступ. Мы отдельно писали, как выдать команде доступ к серверу без раздачи root целиком.
  2. Дать сервису конкретную привилегию ядра (открыть порт, работать с сырыми сокетами) — capabilities, а не setuid.
  3. Дать обычному пользователю возможность безопасно менять свои же данные в системном файле (как с паролем) — классический setuid на узкой, проверяемой программе-посреднике, то есть именно та модель, для которой бит и был придуман изначально.
  4. Управление самими пользователями и группами, для которых всё это настраивается, — тема соседняя; если нужно освежить useradd, usermod и работу с группами, у нас есть отдельный разбор управления пользователями и группами в Linux.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Как понять, что у файла установлен именно setuid, а не просто права на исполнение?

Смотрите на символ в позиции владельца в выводе ls -l: обычное право на исполнение — x, setuid — s (если право на исполнение у владельца тоже есть) или S (если setuid выставлен, а права на исполнение почему-то нет — на практике это почти всегда ошибка конфигурации).

Можно ли поставить setuid на shell-скрипт?

Технически бит выставится через chmod, но современные ядра Linux игнорируют setuid у файлов с shebang (#!) из соображений безопасности — там классически возникали race condition между открытием интерпретатора и содержимого скрипта. Если нужна такая логика — пишите компактную C-обёртку с setuid, которая вызывает скрипт, либо используйте sudo с ограниченным правилом.

Если процесс работает под setuid-правами, значит ли это, что весь дальнейший запущенный из него код тоже получит root?

Нет, если программа написана аккуратно: effective UID действует, пока процесс явно не сбросит его или не завершится, и хорошо написанные утилиты сбрасывают повышенные права как можно раньше. Но если разработчик про это не подумал, дочерний процесс унаследует effective UID родителя — это одна из типичных ошибок, которые превращаются в уязвимость.

Чем setuid отличается от sudo?

Setuid — свойство файла, встроенное в модель прав доступа ядра: любой, кто может выполнить файл, получает права его владельца без дополнительной аутентификации в моменте. sudo — отдельная программа с системой правил (/etc/sudoers), которая проверяет личность вызывающего, спрашивает пароль и логирует каждый вызов. Сам бинарник /usr/bin/sudo, кстати, реализован через setuid на root — sudo использует setuid как строительный блок, добавляя поверх аутентификацию и аудит.

Опасно ли, что на сервере вообще есть setuid-бинарники?

Само по себе — нет, это штатный механизм базовой системы. Опасность возникает, когда список таких файлов бесконтрольно растёт — из-за пакетов, самописных утилит или сброшенных вручную битов — и никто не проверяет, зачем каждому из них root. Периодический find / -perm -4000 -type f и ревизия результата — простая практика гигиены сервера.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →