Существует два способа изменить идентификатор пользователя, и оба они используются ядром. Первый способ — с помощью исполняемого файла setuid, о котором рассказано в разделе 2.17. Второй способ — используя семейства системных вызовов setuid(). Есть несколько различных версий таких вызовов, которые охватывают всевозможные идентификаторы пользователей, связанные с процессами, как вы узнаете далее.
Ядро обладает набором правил относительно того, что дозволено процессу, а что — нет. Приведу три основных правила.
• Процесс, запущенный как корневой (userid 0), может использовать команду setuid(), чтобы стать любым другим пользователем.
• На процесс, запущенный не в качестве корневого, накладываются строгие ограничения по использованию команды setuid(); в большинстве случаев он не может ее использовать.
• Любой процесс может выполнить setuid-команду, если у него есть соответствующие права доступа к файлам.
примечание
Переключение между пользователями никак не затрагивает пароли или имена пользователей. Эти понятия относятся исключительно к пространству пользователя, как вы уже видели на примере файла /etc/passwd в подразделе 7.3.1. Дополнительные подробности о том, как это работает, — в разделе 7.9.
Принадлежность процессов, эффективный, реальный и сохраненный идентификатор пользователя. Наш рассказ об идентификаторах пользователя до сего момента был упрощенным. В действительности каждый процесс снабжен несколькими идентификаторами пользователя. Мы описали
В современных системах различие между эффективным и реальным идентификаторами пользователя приводит к такой путанице, что большая часть документации, посвященной принадлежности процессов, является неверной.
Представляйте себе эффективный идентификатор пользователя как
В обычных системах Linux у большинства процессов совпадают эффективный и реальный идентификаторы пользователя. По умолчанию команда ps и другие команды диагностики системы показывают эффективный идентификатор пользователя. Чтобы увидеть оба идентификатора в своей системе, попробуйте ввести приведенную ниже команду, но не удивляйтесь, если вы обнаружите, что для всех процессов окажутся одинаковыми два столбца с идентификаторами пользователя:
$ ps — eo pid,euser,ruser,comm
Чтобы создать исключение только для того, чтобы увидеть различные значения в столбцах, попробуйте поэкспериментировать: создайте setuid-копию команды sleep, запустите эту копию на несколько секунд, а затем выполните приведенную выше команду ps в другом окне, прежде чем копия прекратит работу.
Чтобы усугубить путаницу, в дополнение к реальному и эффективному идентификаторам пользователя есть также
Типичное поведение команды setuid. Идея, заложенная в реальный идентификатор пользователя, может противоречить вашему предшествующему опыту. Почему вам не приходится часто иметь дело с другими идентификаторами пользователя? Например, если после запуска процесса с помощью команды sudo вам необходимо его остановить, вы также используете команду sudo; обычный пользователь не может остановить процесс. Не должен ли в таком случае обычный пользователь обладать реальным идентификатором пользователя, чтобы получить правильные права доступа?
Причина такого поведения заключается в том, что команда sudo и многие другие setuid-команды явным образом изменяют эффективный и реальный идентификаторы пользователя с помощью системных вызовов команды setuid(). Эти команды поступают так потому, что зачастую возникают непреднамеренные побочные эффекты и проблемы с доступом, если не совпадают все идентификаторы пользователя.
примечание
Если вам интересны подробности и правила, относящиеся к переключению идентификаторов пользователя, прочитайте страницу руководства setuid(2), а также загляните на страницы, перечисленные в секции SEE ALSO («см. также»). Для различных ситуаций существует множество разных системных вызовов.
Некоторым командам не нравится корневой реальный идентификатор пользователя. Чтобы запретить команде sudo изменение реального идентификатора пользователя, добавьте следующую строку в файл /etc/sudoers (и остерегайтесь побочных эффектов на другие команды, которые вы желаете запускать с корневыми правами!):
Defaults stay_setuid
Привлечение функций безопасности. Поскольку ядро Linux обрабатывает все переключения между пользователями (и как результат права доступа к файлам) с помощью команд setuid и последующих системных вызовов, системные программисты и администраторы должны быть исключительно внимательны по отношению:
• к командам, у которых есть права доступа к setuid;
• к тому, что эти команды выполняют.
Если вы создадите копию оболочки bash, которая будет наделена корневыми правами setuid, любой локальный пользователь сможет получить полное управление системой. Это действительно просто. Более того, даже специальная команда, которой предоставлены корневые права setuid, способна создать риск, если в ней есть ошибки. Использование слабых мест программы, работающей с корневыми правами, является основным методом вторжения в систему, и количество попыток такого использования очень велико.
Поскольку существует множество способов проникновения в систему, предотвращение вторжения является многосторонней задачей. Один из самых важных способов избежать нежелательной активности в системе состоит в обязательной аутентификации пользователей с помощью имен пользователей и паролей.
7.9. Идентификация и аутентификация пользователей
Многопользовательская система должна обеспечивать базовую поддержку безопасности пользователей в терминах идентификации и аутентификации.
Когда дело доходит до идентификации пользователя, ядро системы Linux знает только численный идентификатор пользователя для процесса и владения файлами. Ядро знает о правилах авторизации, относящихся к тому, как запускать исполняемые файлы setuid и как совершать системные вызовы из семейства setuid(), чтобы выполнить переход от одного пользователя к другому. Однако ядро ничего не знает об аутентификации: именах пользователей, паролях и т. п. Практически все, что относится к аутентификации, происходит в пространстве пользователя.
В подразделе 7.3.1 мы рассматривали соответствие между идентификаторами пользователей и паролями. Сейчас я объясню, как пользовательские процессы получают доступ к этому соответствию. Начнем с предельно упрощенного случая, при котором пользовательский процесс желает узнать свое имя пользователя (имя, которое соответствует эффективному идентификатору пользователя). В традиционной системе Unix процесс мог бы выполнить для этого что-либо подобное.
1. Процесс спрашивает у ядра свой эффективный идентификатор пользователя с помощью системного вызова geteuid().
2. Процесс открывает файл /etc/passwd и начинает его чтение с самого начала.
3. Процесс читает строки в файле /etc/passwd. Если читать больше нечего, попытка поиска имени пользователя завершается неудачей.
4. Процесс выполняет анализ строки по полям (вытаскивая все, что находится между двоеточиями). Третье поле является идентификатором пользователя в текущей строке.
5. Процесс сравнивает идентификатор, полученный на четвертом шаге, с тем, который был получен на первом шаге. Если они совпадают, первое поле, найденное на четвертом шаге, является искомым именем пользователя; процесс может прекратить поиски и воспользоваться данным именем.
6. Процесс переходит к следующей строке файла /etc/passwd и возвращается к третьему шагу.
Процедура довольно длинная, а в реальности она обычно гораздо более сложная.
Применение библиотек для получения информации о пользователе. Если каждому разработчику, которому потребовалось узнать текущее имя пользователя, приходилось бы создавать весь код, который вы только что видели, система стала бы ужасающе несвязной, раздутой и совершенно неуправляемой. К счастью, мы можем использовать стандартные библиотеки, чтобы выполнять повторяющиеся задачи. Чтобы узнать имя пользователя, необходимо лишь вызвать функцию вроде getpwuid() из стандартной библиотеки после получения ответа от команды geteuid(). Обратитесь к страницам руководства по этим вызовам, чтобы узнать о том, как они работают.
Когда стандартная библиотека является используемой совместно, можно осуществить значительные изменения в реализации системы, не меняя никаких других команд. Например, можно полностью уйти от применения файла /etc/passwd для ваших пользователей и применять вместо него сетевую службу, подобную LDAP (Lightweight Directory Access Protocol, облегченный (упрощенный) протокол доступа к (сетевым) каталогам).
Такой подход прекрасно работает при идентификации имен пользователей, связанных с идентификаторами, однако для паролей дела обстоят сложнее. В подразделе 7.3.1 описано, каким обычно образом зашифрованный пароль становится частью файла /etc/passwd, поэтому, если бы вам понадобилось проверить введенный пользователем пароль, пришлось бы шифровать все, что вводит пользователь, и сравнивать с содержимым файла /etc/passwd.
Традиционная реализация обладает следующими ограничениями.
• Не устанавливается общесистемный стандарт на протокол шифрования.
• Предполагается, что у вас есть доступ к зашифрованному паролю.
• Предполагается, что пользователю будет предлагаться ввести пароль всякий раз, когда он будет пытаться получить доступ к чему-либо, требующему аутентификации.
• Предполагается, что вы намерены использовать именно пароли. Если вы желаете применять одноразовые жетоны, смарт-карты, биометрическую или какую-либо еще аутентификацию пользователя, вам придется добавлять такую поддержку самостоятельно.
Некоторые ограничения, посодействовавшие развитию пакета shadow-паролей, рассмотрены в подразделе 7.3.3. Этот файл сделал первый шаг к тому, чтобы разрешить конфигурирование паролей на уровне системы в целом. Однако решение большинства проблем пришло вместе с разработкой и реализацией стандарта PAM.
7.10. Стандарт PAM
Чтобы обеспечить гибкость при аутентификации пользователей, в 1995 году корпорация Sun Microsystems предложила новый стандарт под названием
Поскольку существуют различные типы аутентификации, стандарт PAM использует несколько динамически загружаемых
Занятие это довольно сложное. Программный интерфейс непростой, и неясно, действительно ли стандарт PAM справляется со всеми существующими проблемами. Однако поддержка стандарта PAM присутствует практически в каждой команде, для которой требуется аутентификация в системе Linux, и большинство версий системы используют этот стандарт. Поскольку он работает поверх интерфейса API для аутентификации, существующего в Unix, интеграция поддержки в клиент требует незначительной (а то и совсем никакой не требует) дополнительной работы.
7.10.1. Конфигурация PAM
Мы изучим основы стандарта PAM, рассмотрев его конфигурацию. Файлы конфигурации PAM обычно можно найти в каталоге /etc/pam.d (в старых системах может использоваться единственный файл /etc/pam.conf). Во многих версиях содержится несколько файлов, и может быть неясно, с чего начать. Определенные имена файлов должны соответствовать частям системы, которые вы уже знаете, например cron и passwd.
Поскольку специальная конфигурация в таких файлах может сильно различаться в разных версиях системы, трудно подыскать общий пример. Рассмотрим образец строки конфигурации, который вы можете обнаружить для команды chsh (команда смены оболочки):
auth requisite pam_shells.so
Эта строка говорит о том, что оболочка пользователя должна располагаться в каталоге /etc/shells, чтобы пользователь мог успешно пройти аутентификацию команды chsh. Посмотрим, каким образом. Каждая строка конфигурации содержит три поля в таком порядке: тип функции, управляющий аргумент и модуль. Вот что они означают для данного примера.
• Тип функции. Это функция, которую пользовательское приложение просит выполнить утилиту PAM. В данном случае это команда auth, задание аутентификации пользователя.
Управляющий аргумент. Этот параметр контролирует то, что будет делать PAM-приложение
Модуль. Это модуль аутентификации, который запускается для данной строки и определяет, что именно делает строка. Здесь модуль pam_shells.so проверяет, упоминается ли текущая оболочка пользователя в файле /etc/shells.
Конфигурация PAM детально описана на странице руководства pam.conf(5). Рассмотрим некоторые основные части.
Типы функций
Пользовательское приложение может попросить PAM-утилиту выполнить одну из четырех перечисленных ниже функций:
• auth — выполнить аутентификацию пользователя (проверить, является ли пользователь тем, кем он себя называет);
• account — проверить статус учетной записи пользователя (авторизован ли, например, пользователь на выполнение каких-либо действий);
• session — выполнить что-либо только для текущего сеанса пользователя (например, отобразить сообщение дня);
• password — изменить пароль пользователя или другую информацию в учетной записи.
Для любой строки конфигурации модуль и функция совместно определяют действие PAM-утилиты. У модуля может быть несколько типов функции, поэтому при определении назначения строки конфигурации всегда помните о том, что функцию и модуль следует рассматривать в виде пары. Например, модуль pam_unix.so проверяет пароль, когда выполняет функцию auth, но если он выполняет функцию password, то пароль устанавливается.
Управляющие аргументы и стек правил
Одним важным свойством стандарта PAM является то, что правила, которые определены в строках конфигурации,
Существуют два типа управляющих аргументов: с простым и более сложным синтаксисом. Приведу три главных управляющих аргумента с простым синтаксисом, которые вы можете найти в правиле.
• sufficient. Если данное правило выполняется успешно, аутентификация также проходит успешно и PAM-утилите не нужно проверять следующие правила. Если правило не выполняется, утилита PAM переходит к дополнительным правилам.
• requisite. Если правило выполняется успешно, PAM-утилита переходит к следующим правилам. Если правило не выполняется, аутентификация завершается неудачей и PAM-утилите не нужно проверять следующие правила.
• required. Если данное правило выполняется успешно, PAM-утилита переходит к следующим правилам. Если правило не выполняется, PAM-утилита переходит к следующим правилам, но всегда вернет отрицательный результат аутентификации, вне зависимости от результатов выполнения дополнительных правил.
Продолжая предыдущий пример, приведу образец стека для функции аутентификации chsh:
auth sufficient pam_rootok.so
auth requisite pam_shells.so
auth sufficient pam_unix.so
auth required pam_deny.so
В соответствии с этой конфигурацией, после того как команда chsh запрашивает PAM-утилиту о выполнении функции аутентификации, утилита выполняет следующее (см. блок-схему на рис. 7.4).
1. Модуль pam_rootok.so проверяет, не пытается ли пройти аутентификацию корневой пользователь. Если это так, то происходит немедленное успешное завершение и дальнейшая аутентификация не предпринимается. Это срабатывает, поскольку для управляющего аргумента установлено значение sufficient, которое означает, что успешного завершения данного действия достаточно, чтобы утилита PAM немедленно возвратила результат команде chsh. В противном случае она переходит ко второму шагу.
2. Модуль pam_shells.so проверяет, есть ли оболочка пользователя в файле /etc/shells. Если ее там нет, модуль возвращает отказ, а управляющий аргумент requisite говорит о том, что утилита PAM должна немедленно сообщить об отказе команде chsh и не пытаться продолжать аутентификацию. В противном случае, когда оболочка есть в файле /etc/shells, модуль возвращает результат, который удовлетворяет флагу required, переходя к третьему шагу.
3. Модуль pam_unix.so запрашивает у пользователя пароль и проверяет его. Для управляющего аргумента указано значение sufficient, поэтому успешной проверки (пароль верный) достаточно для утилиты PAM, чтобы она сообщила об этом команде chsh. Если пароль неверный, утилита PAM переходит к четвертому шагу.
4. Модуль pam_deny.so всегда вызывает отказ, а поскольку присутствует управляющий аргумент required, утилита PAM возвращает отказ команде chsh. Этот вариант применяется по умолчанию в тех случаях, когда больше нечего пробовать. Обратите внимание на то, что управляющий аргумент required не приводит к моментальному отказу функции PAM — она обработает все остальные строки стека, — однако в результате в приложение всегда вернется отказ.
примечание
Не смешивайте термины «функция» и «действие», когда работаете с утилитой PAM. Функция является целью верхнего уровня: что ожидает пользовательское приложение от утилиты PAM (например, выполнить аутентификацию пользователя). Действие является отдельным шагом, который выполняет утилита, чтобы достичь цели. Запомните лишь то, что сначала пользовательское приложение вызывает функцию, а утилита PAM заботится обо всех деталях, связанных с действиями.
Сложный синтаксис управляющего аргумента, помещаемый внутри квадратных скобок ([]), позволяет вручную контролировать реакцию, основываясь на специальном значении, которое возвращает модуль (не только успех или отказ). Подробности см. на странице pam.conf(5) руководства. Когда вы разберетесь с простым синтаксисом, у вас не возникнет затруднений и со сложным.
Рис. 7.4. Процесс исполнения правила PAM
Аргументы модуля
Модули PAM могут задействовать аргументы после имени модуля. Вам часто будет встречаться такой вариант для модуля pam_unix.so:
auth sufficient pam_unix.so nullok
Здесь аргумент nullok сообщает о том, что у пользователя может не быть пароля (по умолчанию в таком случае возник бы отказ).