LXF121:PAM

Материал из Linuxformat.

Перейти к: навигация, поиск
Хаки и трюки Несколько полезных приемов улучшат ваши Linux-приложения

Содержание

Кодируем: Цвета для паролей

В очередном выпуске сборника советов для программистов Андрей Боровский затронет выполнение программ с правами root и управляющие последовательности терминала.

Едва ли в наших программистских (и администраторских) рядах сыщется человек, незнакомый с командой su. Вопреки распространенному заблуждению, эта аббревиатура означает не «super user» (что в английском языке вообще одно слово), а «switch user», хотя используется данная утилита, в основном, как раз для временного получения привилегий root. Но сегодня нас будет интересовать не этимология, а ее внутреннее устройство: выполнение какой-либо операции «руками суперпользователя» – столь распространенная задача, что не грех научиться решать ее более-менее стандартным образом.

Помимо безопасности и надежности, наши приложения (пусть даже консольные) должны быть по возможности красивыми. В завершение этого урока мы еще раз коснемся вывода на экран цветного текста, на сей раз – не используя ничего, кроме чистого C.

Как root

Все вы, конечно, имели дело с программами, которые позволяют получить доступ к некоторым возможностям root, не покидая учетной записи обычного пользователя: взять ту же su. Примерами таких приложений могут также служить утилиты для настройки оборудования и установки программного обеспечения. Внешне все выглядит так, как будто пользователь запускает обычную программу, вводит пароль root и становится (внутри этой программы) суперпользователем. На самом деле, все обстоит немного иначе.

Владельцем любого процесса (экземпляра программы) по умолчанию является запустивший его пользователь. Права процесса на доступ к файлам, привилегированным функциям API и другим объектам системы определяются правами владельца процесса. Соответственно, любая программа, запущенная пользователем root, получает максимальные полномочия. Разработчики Unix довольно быстро почувствовали, что переходить из одной учетной записи в другую каждый раз, когда пользователю требуется выполнить особые действия, неудобно, и придумали расширенные флаги прав доступа setuid и setgid. Если эти биты установлены, владельцем процесса считается владелец файла программы (а не пользователь, который запустил процесс). Иначе говоря, если некий исполняемый файл принадлежит пользователю root и для него установлен флаг setuid, то независимо от того, какой пользователь создаст из этого исполняемого файла процесс, он получит такие же права, как если бы его запустил root. При этом система не забывает, кто запустил процесс на самом деле. Для всех процессов система хранит два набора идентификаторов пользователя и группы – действительные (effective) и фактические (real). Если пользователь vpupkin запустит программу с установленным битом setuid, причем владельцем файла программы является root, действительным хозяином процесса окажется root, а фактическим – vpupkin. В своем взаимодействии с системой программа будет использовать права действительного владельца.

Проблема, которую нам предстоит решить, заключается в том, что мы хотим предоставлять право запуска программ с установленным флагом setuid не каждому пользователю, а только тому, кто знает пароль root. Возникает вопрос: зачем такому пользователю вообще нужны setuid-программы, ведь он может просто зайти в систему как root? На самом деле, возможность запускать setuid-программу и удобна (не нужно специально каждый раз регистрироваться в системе), и безопасна – пользователь проводит в режиме root минимум времени.

Пароль под контролем

Функция read_pwd() играет в нашей программе служебную роль; тем не менее, она гораздо длиннее, чем auth_root(). Функция, предлагающая ввести пароль, зачастую становится первым объектом атаки злоумышленников. Если взломщику удастся обмануть ее, он получит доступ к программе, не зная пароля. Хуже того, в некоторых случаях взломщик, сломавший функцию ввода пароля, может получить даже более широкие права, чем те, которые предоставляет данная программа по умолчанию: например, командную строку суперпользователя. По этой причине очень важно, чтобы suid-программы максимально ограничивали свободу действий даже тех пользователей, которые прошли процедуру аутентификации. Разумеется, ограничивать свободу действий пользователя, который действительно знает пароль root, бессмысленно, но никогда не следует исключать возможность проникновения «пользователя с черного хода».

Функция read_pwd() делает все возможное для того, чтобы пользователь не мог ее обойти. Сколько бы символов ни ввели с клавиатуры, буфер password будет заполнен не более чем buf_size символами. Вводимые символы, естественно, не отображаются на экране.

Таким образом, классическая схема авторизации пользователя в setuid-программе выглядит так: приложение, запущенное неким пользователем с действительными правами root, запрашивает у пользователя пароль root. Если введен правильный пароль, программа предоставляет пользователю возможность выполнить требуемые действия, в противном случае ему предлагается повторить попытку ввода пароля. Как видим, программы, запрашивающие у пользователя пароль root, не становятся программами суперпользователя в результате ввода пароля – они являются таковыми с самого начала. Поскольку пользователь, запустивший программу с установленным флагом setuid root, уже фактически имеет те права, которые он хочет получить, программы, предназначенные для подобного применения, должны быть написаны максимально аккуратно, чтобы запускающий их человек не мог обойти механизм аутентификации и получить привилегии root, не зная пароля (см. врезку). Отметим также, что новейшие средства аутентификации пользователей (PolicyKit) физически разделяют код, выполняющий аутентификацию, и код, выполняющий привилегированные действия. Впрочем, этот вопрос выходит за рамки данной статьи.

Время кодировать

За время существования Unix и Linux способов аутентификации пользователя было придумано немало, но в основе всего попрежнему лежит старый добрый файл зашифрованных паролей. Именно с работы с ним мы и начнем.

Ниже приводится листинг программы goroot. Она запрашивает у пользователя, запустившего ее, пароль root, и если он правильный, создает файл rootfile, владельцем которого будет root (а не пользователь, запустивший программу). Потом программа временно отказывается от прав root и создает файл userfile, владельцем которого будет пользователь, запустивший программу (во всем этом можно убедиться с помощью команды ls -al). Затем программа возвращает себе полномочия root и создает файл rootfile2, владельцем которого снова оказывается root.

#define _XOPEN_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#include <termios.h>
#include <unistd.h>
#include <sys/types.h>
#include <shadow.h>
#include <sys/types.h>
#include <pwd.h>
#include <sys/stat.h>
#include <fcntl.h>
void read_pwd (char * password, int buf_size)
{
   char ch;
   int i;
   if (!isatty(fileno(stdin))) {
       password[0] = 0;
       return;
   }
   struct termios oldsettings, newsettings;
   tcgetattr(fileno(stdin), &oldsettings);
   newsettings = oldsettings;
   newsettings.c_lflag &= ~(ECHO|ICANON|ISIG);
   newsettings.c_cc[VMIN] = 0;
   newsettings.c_cc[VTIME] = 0;
   tcsetattr(fileno(stdin), TCSANOW, &newsettings);
   i = 0;
   printf(“Введи те пароль\n”);
   while((ch = getchar()) != '\n') {
       if ((i<buf_size-1) && (ch != EOF)) {
           password[i] = ch;
           i++;
       }
   }
   password[i] = 0;
   tcsetattr(fileno(stdin), TCSANOW, &oldsettings);
}
int auth_root(char * password)
{
   char * epasswd;
   struct spwd *spwd;
   spwd = getspnam(“root”);
   epasswd = crypt(password, spwd->sp_pwdp);
   return !strcmp(epasswd, spwd->sp_pwdp);
}
#define BUF_SIZE 16
int main(void)
{
   char password[BUF_SIZE];
   int result = 0;
   if (geteuid() != getuid()) {
     read_pwd(password, BUF_SIZE);
     result = auth_root(password);
  }
     else result = 1;
  if (result) {
    int rootuid = geteuid();
    printf(“Теперь Вы root\n”);
    open(“rootfile”, O_CREAT|O_EXCL);
    seteuid(getuid());
    setegid(getgid());
    printf(“А теперь - нет\n”);
    open(“userfile”, O_CREAT|O_EXCL);
    seteuid(rootuid);
    printf(“А теперь - снова root\n”);
    open(“rootfile2”, O_CREAT|O_EXCL);
  } else {
      printf(“Неверный пароль\n”);
  }
  return EXIT_SUCCESS;
}

Давайте начнем разбор программы с функции main(). Прежде всего надо убедиться, что пользователь, запустивший программу – не root (действительно, зачем спрашивать пароль root у самого root?). Мы проверяем этот факт очень просто – сравнением значений функций getuid() и geteuid(). Первая функция возвращает фактический идентификатор пользователя, вторая – действительный. Поскольку у программы с установленным setuid root действительный идентификатор всегда соответствует идентификатору root, его совпадение с фактическим означает, что программу запустил root.

Интереснее всего, конечно, бывает, если пользователь, запустивший программу – не root. В этом случае мы предлагаем ему ввести пароль (функция read_pwd()) и проверяем корректность предоставленных им сведений в функции auth_root().

Аутентификатор-1

Чтобы понять, как сравнивать введенный пароль с паролем пользователя root, следует знать, каким образом последний хранится в Unix-системах. Пароль каждого пользователя записан на диске в зашифрованном виде, причем алгоритм, используемый для шифрования, является однонаправленным, то есть не предусматривает расшифровки. Чтобы сравнить введенный пароль с хранимым, его нужно зашифровать тем же способом, что и хранимый пароль, а затем сравнить результаты. Таким образом, даже получив файл паролей, злоумышленник не сможет воспользоваться им напрямую: ведь у него будут одни только шифры. При вводе такого «пароля» в программу он будет зашифрован еще раз, и результат, естественно, не совпадет с ожидаемым. Первоначально эта схема считалась настолько безопасной, что файл, хранящий зашифрованные пароли пользователей, был доступен для чтения всем желающим. Вскоре, однако, выяснилось, что, имея на руках файл с паролями и достаточно мощный компьютер, можно довольно быстро найти пароли методом целенаправленного подбора. В результате были приняты некоторые дополнительные меры предосторожности, вдаваться в подробности которых мы здесь не будем. Для нас алгоритм аутентификации пользователя выглядит так: получить зашифрованный пароль пользователя root; зашифровать пароль, введенный пользователем программы; сравнить результаты.

Зашифрованный пароль любого пользователя системы можно получить с помощью функции getspnam(). Ее аргументом является имя пользователя, а возвращаемым значением – указатель на структуру spwd, которая, помимо прочего, содержит зашифрованный пароль пользователя (поле sp_pwdp). Следует иметь в виду, что сама функция getspnam() сработает только в том случае, если владельцем вызывающего ее процесса является привилегированный пользователь – root или член группы shadow. Если владелец процесса не удовлетворяет этим требованиям, функция возвращает значение NULL. Таким образом, наша программа goroot должна обладать действительными правами root, чтобы как минимум получить доступ к базе паролей.

Шифрование введенного пользователем пароля выполняется функцией crypt(). Она принимает два аргумента: строку пароля с нулевым окончанием и «затравку» – строку, которая указывает параметры алгоритма, используемого для шифрования пароля. Раньше вторым аргументом функции crypt() были байты (крупицы?) «соли» (salt) – случайные символы, которые добавлялись к выбранному пользователем паролю и затрудняли «атаку со словарем». Теперь вторым аргументом crypt() может быть любая информация о том, как зашифрован пароль. Откуда нам взять эту информацию, если учесть, что введенный пользователем пароль должен быть зашифрован точно так же, как и хранящийся в системе пароль root? Оказывается, первые байты строки spwd->sp_pwdp как раз и содержат требуемые сведения, так что в качестве второго аргумента crypt() мы используем строку spwd->sp_pwdp. Зашифровав полученный от пользователя пароль, остается только сравнить его с сохраненным паролем root с помощью функции strcmp().

Если пользователь прошел аутентификацию, мы поздравляем его с этим фактом, а затем создаем файл rootfile. Поскольку действительным владельцем процесса является root, владельцем созданного файла будет тоже root. Допустим, что теперь мы хотим, чтобы программа временно отказалась от полномочий root. Самый простой способ сделать это – установить действительные идентификаторы пользователя и группы равными фактическим идентификаторам, которые соответствуют идентификаторам пользователя, запустившего процесс. Именно это мы и делаем:

seteuid(getuid());
setegid(getgid());

Теперь действительным владельцем процесса числится запустивший его пользователь, и привилегии процесса соответствуют привилегиям этого пользователя. С помощью функции seteuid() мы можем снова сделать пользователя root эффективным владельцем процесса, когда нам это понадобится.

Программу goroot следует компилировать с ключом -lcrypt:

gcc goroot.c -o goroot -lcrypt

Помня, что программа будет работать, только если ее владелец – root, да еще и установлен флаг suid, в режиме root скомандуем:

chown root goroot
chmod ug+s goroot

Теперь наше первое приложение готово к работе.

Аутентификатор-2

Пароль в наше время – не единственное средство аутентификации пользователя. Соответствие между именем учетной записи и реальным человеком можно установить многими способами, в том числе путем сканирования отпечатков пальцев или радужной оболочки глаза (некоторые уверяют, что можно использовать вырванное око своего врага, но я все же думаю, что врага придется тащить целиком). Учитывая многообразие средств аутентификации, в современных системах этот процесс абстрагирован от конкретной программы, для чего применяются подключаемые модули аутентификации (Pluggable Authentication Modules, PAM). Вот как выглядит аналог программы goroot, написанный с применением PAM:

#include <security/pam_appl.h>
#include <security/pam_misc.h>
#include <stdio.h>
int main ()
{
  pam_handle_t* pamh;
  struct pam_conv pamc;
  pamc.conv = &misc_conv;
  pamc.appdata_ptr = NULL;
  pam_start (“common-auth”, “root”, &pamc, &pamh);
  if (pam_authenticate (pamh, 0) != PAM_SUCCESS) {
    printf(“Неверный пароль\n”);
    pam_end (pamh, 0);
    return 1;
  }
  printf (“Теперь Вы - root.\n”);
  open(“rootfile”, O_CREAT|O_EXCL);
  pam_end (pamh, 0);
  return 0;
}

Программа взаимодействует с подсистемой PAM в режиме транзакций. Транзакция начинается с помощью функции pam_start(), первый аргумент которой – это имя сервиса PAM. Для серьезной программы в процессе настройки системы будет создан свой сервис (описания сервисов обычно «живут» в директории /etc/pam.d). Мы используем сервис common-auth, который является базовым, предназначенным для аутентификации пользователей. Второй аргумент функции pam_start() – имя пользователя, в нашем случае – root. Далее следуют указатели на две структуры, описывающие состояние процесса аутентификации. Сама аутентификация выполняется с помощью функции pam_authenticate(), которая использует метод аутентификации, принятый в вашей системе (скорее всего, это будет все то же приглашение для ввода пароля). По крайней мере, теперь мы можем переложить ответственность за создание функции чтения пароля на разработчиков системы.

В случае, если аутентификация прошла успешно, функция pam_authenticate() возвращает значение PAM_SUCCESS, с чем мы пользователя и поздравляем. Компилировать программу следует с ключами -lpam и -lpam_misc. После сборки исполняемый файл программы должен быть передан во владение root, и для него должен быть установлен флаг setuid, как и в предыдущем случае. Между прочим, программе, использующей PAM, по-прежнему необходим доступ к файлу паролей, а значит, она не сможет выполнить аутентификацию пользователя, если не будет запущена с действительными правами root.

Да будет цвет!

Разноцветный текст без ncurses — это возможно.

Все мы видели консольные программы, в которых различные важные надписи выделяются цветом. Один из способов раскрасить экран – использовать библиотеку ncurses. Однако использование ncurses только для вывода разноцветных надписей – это, что называется, из пушки по воробьям. Помимо прочего, библиотека заменяет стандартные параметры консоли своими собственными, и это может быть неприемлемо для нас. Между тем, у нас есть простой способ выводить на экран выделенные цветом надписи (а также проделывать многие другие манипуляции с экраном), не прибегая к помощи ncurses. С тех пор как терминалы обзавелись расширенными возможностями, был придуман способ управления этими возможностями, которым могла воспользоваться любая программа, выводящая данные на терминал: специальным командам терминала соответствовали специальные последовательности символов – Esc-последовательности. Рассмотрим пример программы:

#include <stdio.h>
int main() {
   int i;
   printf(“\033[30mЧерный \033[0m\n”);
   printf(“\033[31mКрасный \033[0m\n”);
   printf(“\033[32mЗеленый \033[0m\n”);
   printf(“\033[33mКоричневый \033[0m\n”);
   printf(“\033[34mСиний \033[0m\n”);
   printf(“\033[35mФиолетовый \033[0m\n”);
   printf(“\033[36mГолубой \033[0m\n”);
   printf(“\033[37mСерый \033[0m\n”);
   printf(“\033[37;1mБелый \033[0m\n”);
   printf(“\033[33;1mЖелтый \033[0m\n”);
  return 0;
}

Эта программа распечатывает на экране терминала надписи, цвет каждой из которых соответствует напечатанному слову (на самом деле, цвет, который вы увидите на экране, будет соответствовать выбранной вами цветовой схеме). Командная последовательность начинается с префикса \033[ (\033 – код символа Esc). Формат команды для смены цветов выглядит так:

Esc[код_цвета<;дополнительный_атрибут>m

Команда заканчивается символом m. Кодам 8‑ми базовых цветов соответствуют числа 30–37. Команда со специальным кодом цвета 0 возвращает терминал в стандартное состояние. Дополнительный (необязательный) атрибут позволяет установить такие параметры, как яркость или мерцание (с помощью бита яркости из 8 базовых цветов можно получить 7 дополнительных). Esc-последовательности позволяют не только изменять цвет символов и фона. Вот как, например, выглядит функция, перемещающая курсор в позицию, заданную координатами x (столбец) и y (строка):

void move(int x, int y) {
   printf(“\033[%i;%iH”, y, x);
}

Ладно, скажете вы, но где гарантия, что эти командные последовательности будут работать так же и в других системах? Когда создавалась библиотека ncurses, такой гарантии действительно не было, но теперь стандартизация сделала нашу жизнь проще. Приведенные выше последовательности стандартизированы в рамках спецификации ECMA-48 (можете ознакомиться с полным текстом стандарта, если хотите узнать об управ-ляющих последовательностях терминала побольше). Стандарт ECMA-48 поддерживают все эмуляторы терминалов Linux и других Unix-систем.LXF

Личные инструменты
  • Купить электронную версию
  • Подписаться на бумажную версию