Начала PAM.

Stanislav Ievlev, inger@linux.ru.net
Первая публикация произошла на linux.ru.net

0. О статье

Статья эта развивалась долго и нудно. Первая версия появилась на linux.ru.net и составляла только первую часть этого труда. Но время шло, знаний прибавлялось и захотелось прололжения. Систематизировать свои знания так интересно. Присоединяйтесь.

1. PAM c точки зрения пользователя

Как было раньше. Желая войти в систему вы были вынуждены пообшаться с программой-охранником гордо именуемой login. Она спрашивала ваше имя, затем пароль шифровала известным алгоритмом и сличала получивуюся абракадабру с записью в файле /etc/passwd. Если все совпадало, то вам разрешалось войти иначе предлагалось начать все с начала.

Через некоторое время наученные горьким опытом системы перестали хранить зашифрованные пароли в файле /etc/passwd открытом на всеобщее обозрение и переместили эту интимную информацию в файл /etc/shadow, читать который дозводено было только обладателям правами суперпользователя. Программа login была переписана заново: теперь она умела и читать из нового файла и шифровать по более серьезному алгоритму.

Неизвестно сколько еще раз пришлось бы переписывать эту программу, да и не только ее (с паролированием в системе связаны еще и passwd, и su), если бы не пришла кому-то в голову мысль отделить программы от механизма аутентификации. Эта система получила название PAM (Pluggable Authentication Modules), что по-русски означает Подгружаемые Модули Аутентификации (ПМА).

Теперь если программа (в частности login) жалает произвести аутентификацию пользователя она больше не занимается этим, а обращается к PAM'у с соответствующей просьбой. Последний выполняет все проверки и докладывает вызвавшему его о результатах-пускать или не пускать. Все заботы о выборе алгоритма и особенностях аутентификации теперь лежат на PAM'е программа и не догадывается через какие круги ада проходит пользователь чтобы его приняли.

Формально PAM выполнен в виде разделяемых библиотек-модулей комфортно расположившихся в каталоге /lib/security/. Каждый модуль по особому пропускает через себя пользователя, реализуя свой особенный механизм аутентификации. То есть, запуская тот или иной набор модулей, мы как из кирпичиков складываем сценарий аутентификации согласно нашим привычкам и желаниям.

Осталось только разобраться каким программам какой сценарий использовать. Оные разместились в каталоге /etc/pam.d . Имя каждого сценария в этом каталоге совпадает с именем программы для которого он предназначен. Например, сценарий для login находится по адресу /etc/pam.d/login.

Заглянем во внутрь этого файла. содержимое может выглядить, например, так:

#%PAM-1.0
auth     requisite      /lib/security/pam_unix.so       nullok #set_secrpc
auth     required       /lib/security/pam_securetty.so
auth     required       /lib/security/pam_env.so
auth     required       /lib/security/pam_mail.so
account  required       /lib/security/pam_unix.so
password required       /lib/security/pam_unix.so       strict=false
session  required       /lib/security/pam_unix.so       none # debug or trace
session  required       /lib/security/pam_limits.so

Каждая строчка означает, что для удачной аутентификации пользователь должен пройти через указанный модуль. Вообще формат записи:

Тип-Модуля Флаг-Контроля Путь-К-Модулю Параметры-Модуля

Тип-Модуля должен быть одним из следующих:

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

Путь-К-Модулю должен указывать полный адрес выбранного модуля на диске, а Параметры-Модуля зависят от выбора оного.

Помимо файлов-сценариев для некоторых модулей могут использоваться дополнительные файлы конфигурации. Все они расположены в каталоге /etc/security и каждый файл предназначен для конкретной группы настроек.

time.conf - Здесь вы сможете ограничить время доступа пользователей с различных терминалов к различным сервисам. Например, запретить входить в систему с первой виртуальной консоли администратору во время выходных. Эти настройки обслуживает модуль pam_time и, соответственно, если вы желаете, чтобы ваши ограничения возымели дествие модуль должен быть прописан в соответствующем сценарии.
pam_env.conf-А здесь вам под силу ограничить возможности в изменении отдельных переменных среды пользователями. Работает под руководством модуля pam_env.
limits.conf- Если вы злобный администратор или у вас недобросовестные пользователи, то этот файл для вас. Здесь вы можете индивидуально или для группы ограничить:размер core-файла, максимальный допустимый размер файла, максимальное количество открытых файлов, запущенных процессов, сколько раз можно одновременно зайти в систему и т.д. Руководящий модуль pam_limits.
access.conf-Так как PAM имеет средства аутентификации по сети, то подобные настройки являются полезными ибо контролируется не только кто может или не зайти, но и откуда. Контролируется pam_access.
group.conf- Можно указать какой группе будет принадлежать служба запущенная определенным пользователем, в определенное время с определенного терминала. Хозяева pam_time и pam_group.
console.perms-Несколько выбивается своим названием, но не функциями. Здесь определяются права, получаемые привилегированными пользователями к консоли во время входа в систему и возвращаемые при выходе. Модуль pam_console.

Итак, логика действий проста и понятна, осталось только разобраться какие могут быть модули.

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

2. Делаем собственный PAM-модуль

После теории посмотрим в глубь PAM. Попытаемся разобраться как же оно работает.

Пример1: Новый вход в систему.

Вы уже наверно во сне видите изрядно доставшие строки

login:***
Password:*******

Эта схема придуманная много лет назад продолжает служить и сейчас. Но не паролем единым жив человек. Давайте сделаем что-то новенькое, необычное. Будет и самому интересно и вредителей введет в заблуждение, ибо такого не будет не на одной другой машине.

До PAM'а нам пришлось бы переписывать весь login, а теперь мы просто сделаем новый модуль, соберем его как библиотеку и припишем в сценарий аутентификации /etc/pam.d/login. Модуль будет называться просто pam_test.

Но сначала о том, что же мы будем делать. Имеется следующая интересная схема аутентификации: система выдает пользователю несколько случайных чисел. Пользователь берет определенные из них, поставляет в ТОЛЬКО ЕМУ известный многочлен, например,

(дата)*x*x+3*x-(текущий час)*y+z

и вводит вместо пароля ответ. Компьютер тоже считает по этому алгоритму и если ответ правильный, пускает в систему.

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

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

Как вы прочитали ранее модули различаются по классам. Наш будет класса AUTH. То есть отвечать за аутентификацию.

Это его текст.

/*Это простейший модуль PAM*/

// Включаем необходимые заголовочные файлы.

#include <security/pam_modules.h>
#include <stdarg.h>
#include <time.h>

//Это определит тип нашего модуля

#define PAM_SM_AUTH
#define MAX_V 30

// Самая интересная функция.
// Именно она реализует наш неповторимый алгоритм аутентификации.
// Подобные внешние функции должны существовать во всех модулях данного класса.

PAM_EXTERN int pam_sm_authenticate(pam_handle_t * pamh, int flags
                                   ,int argc, const char **argv)
{
        unsigned int ctrl;
        int retval;
        const char *name, *p;
        char *right;
        /*special variables*/
        long x1,x2,x3,x4,y;

        time_t mytime;
        struct tm *mytm;

        /*готовимся к аутентификации*/
        mytime=time(0);
        mytm=localtime(&mytime);


        srandom(mytime);
        x1=random()%MAX_V;
        x2=random()%MAX_V;
        x3=random()%MAX_V;
        x4=random()%MAX_V;

        //завели несколько случайных величин, а заодно узнали и время.

        /* получим имя пользователя */

        // Получаем имя пользователя
        // Вся мудрость PAM в том, что приглашение "login: " появится если имя еще не известно,
        // иначе мы сразу получим ответ, сгенерированный предыдущими модулями.

        retval = pam_get_user(pamh, &name, "login: ");

        /*получим пароль используя диалог*/

        {
            struct pam_conv *conv;
            struct pam_message *pmsg[3],msg[3];
            struct pam_response *response;


        retval = pam_get_item( pamh, PAM_CONV, (const void **) &conv ) ;

// Сами мы не знаем как будет осущестляться диалог, это забота программы
   (в нашем случае этим займется login). Мы
// лишь только  укажем параметры, вид приглашения и более того, можем
   задать сразу несколько приглашений, если надо
// получить сразу несколько ответов

        pmsg[0] = &msg[0];
        msg[0].msg_style = PAM_PROMPT_ECHO_OFF;
        msg[0].msg=malloc(100);
        snprintf(msg[0].msg,60,"Second Password:%d:%d:%d:%d:",x1,x2,x3,x4);

        retval = conv->conv(1, ( const struct pam_message ** ) pmsg
                            , &response, conv->appdata_ptr);
 // Нам дали указатель на диалоговую функцию. Ее и запускаем.
        /*просчитаем правильный ответ*/
        y=2*x1*mytm->tm_mday+x3*mytm->tm_hour;
        right=malloc(100);
        snprintf(right,20,"%d",y);
 // Сравним с ответом пользователя. Ответ формируется диалоговой функцией в специальном формате.
        if (!(strcmp(right,response->resp))){
        return PAM_SUCCESS;
        }else{
        return PAM_AUTH_ERR;
        }
      }/*диалог*/
        return PAM_SUCCESS;
// Нашим результатом будет да или нет. Как прервать программу разберется основной модуль PAM.
}


/*
 * The only thing _pam_set_credentials_unix() does is initialization of
 * UNIX group IDs.
 *
 * Well, everybody but me on linux-pam is convinced that it should not
 * initialize group IDs, so I am not doing it but don't say that I haven't
 * warned you. -- AOY
 * Перевожу: Единственная вещь которую делает pam_set_cred это инициализация
   Идентификаторов групп... короче в данном случае это нам совершенно не нужно
 */

PAM_EXTERN int pam_sm_setcred(pam_handle_t * pamh, int flags
                              ,int argc, const char **argv)
{
        unsigned int ctrl;
        int retval;


        retval = PAM_SUCCESS;
//Чтобы никто не заметил, что мы ничего не делаем ответим, что все в порядке
        return retval;
}

// Это определение необходимо для статической линковки модулей PAM в приложениях.
#ifdef PAM_STATIC
struct pam_module _pam_unix_auth_modstruct = {
    "pam_test",
    pam_sm_authenticate,
    pam_sm_setcred,
    NULL,
    NULL,
    NULL,
    NULL,
};
#endif

Вот и все. Просто. Дело в том, что мы тут описываем процедуру аутентификации и только ее - положительный эффект от грамотного разделения обязанностей. Повторим все основные этапы. В модуле, понятное дело ,должны существовать экспортируемые функции. В нашем случае это pam_sm_authenticate и pam_sm_setcred. Для каждого класса модулей свой набор функций. Дальше, внутри функций находится основной алгоритм принятия решения ответом является только "да" или "нет". Если же для принятия столь ответственного решения необходимо узнать дополнительную информацию от пользователя, то можно узнать указатель на диалоговую функцию программы.

Работа с другими классами модулей абсолютно идентична. Так что попробуйте сделать еще что-нибудь свое.

3. Делаем собственное PAM-приложение

Давайте-ка теперь посмотрим как видят систему PAM-приложения. Нудную и скучную документацию вы сами потом прочитаете (или я все-таки сделаю это когда-нибудь сам), а сейчас обратимся к практике.

Пример2: избавляемся от SUID-программ.

Прочитали название? Страшно? Все в порядке, речь в этом разделе не пойдет о чем-то необычном. Просто несколько теоретических размышлений и пример их реализации.

Одним из основных архитектурных недостатков UNIX с точки зрения защищенности является наличие SUID-программ, то есть способность процесса изменять одного владельца на другого после запуска. По-умолчанию, процесс приобретает права того пользователя, который его запустил. Исключение - программы в правах которых выставлен бит SUID. После запуска такой процесс приобратает права не того пользователя,который запустил его, а права владельца данного файла.

Зачем это нужно. Дело в том, что иногда для доступа к тем или иным системным данным прав пользователя недостаточно. Так, например, программе passwd необходимо иметь права root для доступа к файлам /etc/passwd и /etc/shadow. Но будучи запущенной пользователем (ведь может же он поменять пароль самому себе), она не в состоянии это сделать. Тут то и происходит фокус со сменой владельца. Запускаясь, процесс passwd получает права своего владельца (root) и теперь может спокойно работать с ранее запретными данными. Получается, что пользователь как-бы не получает прав администратора, а программа спокойно с ними работает. Все гениально, просто и безопасно... Стоп, вот тут мы и не правы. Большинство атак, направленных на "переполнение буфера" как раз и пользуются этой возможностью процессов расширять свои возможности. Ну и что, что права получает только процесс, если его надлежащим образом накачать, то он запустит нам оболочку. Последняя унаследует права процесса - права администратора. Была у тебя оболочка с правами пользователя - стала от имени администратора.

Что же делать. Давайте обратимся к опыту других операционных систем, не наступивших на грабли UNIX. В Windows NT нет SUID-программ. А что же делать, если кому-то нужно, скажем, поменять себе пароль? Очень просто. Есть программа пользователя, желающая поменять пароль и работающая с правами оного. А есть сервер, работающий с правами администратора и готовый помочь всякому правильному клиенту. Вот пользовательский процесс делает запрос, сервер проверяет тот ли это пользователь и радостно выполняет поручение. При таком решении неизбежно возникают две проблемы: Клиент-серверное взаимодействие должно быть грамотно написано (что бы не напороться на грабли несанкционированного превишения полномочий) и падает производительность системы на всех подобных операциях (в Windows это решается применением кеширования запросов). Но с другой стороны серверов будет поменьше чем SUID-программ (следовательно, вероятность допустить ошибку меньше) и при хорошем проектировании системы повышение прав потребуется не столь часто.

А что если и в UNIX попытаться сделать , например, клиент-серверную авторизацию. Я понимаю, что для аккуратного решения этой проблемы надо очень крепко подумать и возможно даже изменить архитектуру, но разве это не интересно попробовать изменить как-то свою ОС?

Сразу оговорюсь, что в примерах не описан процесс аутентификации клиента. Понятно, что он должен быть достаточно хитроумным, желательно с применением криптографии. Кроме того, сервер обрабатывает только один запрос. В идеале он должен или постоянно висеть в системе как daemon, или вызываться через какой-нибудь wrapper. Все программы ниже будут выглядить максимально упрощенными. Лучше понимание, чем уважение без оного.

Итак, сказано-сделано. Пишем клиент (собственно passwd который будет вызывать пользователь).

//Всякие полезные включения. Право не знаю все ли они нужны?..
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <fcntl.h>
#include <netdb.h>

#define PORTNUM 1500

//Это порт по умолчанию, на котором расположится сервер в ожидании запросов.
//А также адрес по умолчанию-локальный.

#define DEFAULT_ADDR "0.0.0.0"

main(int argc, char *argv[])

//единственная и главная функция.

{
int s;
int pid;
int i,j;
struct sockaddr_in serv_addr;
struct hostent *hp;
char username[80],hostname[80]=DEFAULT_ADDR;
char *tmp=malloc(80);

if (argc<2){
printf("Usage: passwdc username host\n");
exit(1);
}

//Работать будем или с локальной машиной
  или с удаленной (а почему бы и нет, если силы позволяют, только аутентификация
//должна быть очень продуманной)

strncpy(username,argv[1],80);
printf("Changing password for user:%s \n",username);

if (argc>2) {
  strncpy(hostname,argv[2],80);
 }

printf("on host:%s\n",hostname);

if((hp=gethostbyname(hostname))==0)
{
perror("gethostbyname()");
exit(3);
}


bzero(&serv_addr, sizeof(serv_addr));
bcopy(hp->h_addr,&serv_addr.sin_addr,hp->h_length);

serv_addr.sin_family=hp->h_addrtype;
serv_addr.sin_port=htons(PORTNUM);

if((s=socket(AF_INET, SOCK_STREAM, 0))==-1){
perror("socket()");
exit(1);
}

if (connect(s, (struct sockaddr_in *)
&serv_addr, sizeof(serv_addr))==-1){
perror("connect()");
}

//где-то тут после успешного соединения клиент должен убедить сервер, что он
//действительно тот за кого себя выдает и что
  работает именно от имени того пользователя чей пароль так хочется изменить.

//посылаем имя пользователя

send(s, username, sizeof(username),0);

//считываем с локальной консоли и отсылаем пароль пользователя
//Тонкий момент: или соединение должно быть шифорванным или пароль
  посылается уже после применения к нему хеш-функции.

tmp=getpass("New UNIX password:");
strncpy(username,tmp);
send(s, username, sizeof(username),0);

//После этого работает уже сервер. Нас интересуют только результаты
//Возможно более сложный обмен сообщениями по определенному протоколу.
  Например если пароль оказался слишком прост, то надо
//Ввести новый.

if (recv(s, username, sizeof(username), 0)<0){
perror("recv()");
}

printf("Result:%s\n",username);
close(s);

printf("Client done...\n");
}

Ну вот и все. Все просто. Но так и должно быть. пример-то учебный. Для реального воплощения идеи надо не мало потрудиться. Важное замечание. Клиент нигде не использует никаких функций, связанных с аутентификацией в ОС - то чего мы и добивались.

А вот с сервером дела обстоят несколько хуже. Он использует библиотеку PAM, хотя может производить все изменения и вручную. Но только зачем нам изобретать велосипед?

//Масса полезных включений

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <netdb.h>

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <unistd.h>
#include <sys/types.h>

#include <security/pam_appl.h>
#include <security/pam_misc.h>

#define USERNAME "stas"
#define PASSWORD "1234"
#define PORTNUM 1500


// А это нужно для PAM. Так описывается функция диалога между программой и пользователем.
// Путем этого диалога определяется что и как спрашивать. Диалог задает программа.
   Это весьма разумно, ибо с РАМ могут
// работать как консольные программы, так и программы с графическим интерфейсом.

static struct pam_conv conv = {
    misc_conv,
    NULL
};

//Тут мы сохраним все самое дорогое
char username[80]       = USERNAME; /* имя пользователя */
char newPassword[80]    = PASSWORD; /* его пароль*/
int s, ns; /*идентификаторы сокетов*/
struct sockaddr_in serv_addr, clnt_addr; /*структуры описатели адресов*/

//Итак, сначала инициализируем серверную часть

server_init()
{
int pid;
int nport;
int nbytes;
int fout;
int addrlen;


struct hostent *hp;

char hname[80];

nport=PORTNUM;
nport=htons((u_short)nport);

if((s=socket(AF_INET, SOCK_STREAM, 0))==-1){
perror("socket()");
exit(1);
}

bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=INADDR_ANY;
serv_addr.sin_port=nport;

if( bind(s, (struct sockaddr_in *)&serv_addr, sizeof(serv_addr))==-1){
perror("bind()");
exit(1);
}


if(listen(s,5)==-1){
perror("listen()");
exit(1);
}

printf("server ready:%s\n",
        inet_ntoa(serv_addr.sin_addr));

}


server_read()
{

/*это в последствии и должно стать рабочим циклом при обмене с клиентом*/

int addrlen;
bzero(&clnt_addr,sizeof(clnt_addr));
addrlen=sizeof(clnt_addr);

if((ns=accept(s, (struct sockaddr_in *)&clnt_addr, &addrlen))==-1){
perror("accept()");
exit(1);
}

printf("Client: %s\n",
        inet_ntoa(clnt_addr.sin_addr));



/*Вот и пожаловал клиент*/
close(s);
printf("Receiving data ...\n");
recv(ns, username, sizeof(username), 0);
recv(ns, newPassword, sizeof(newPassword), 0);
//Приняли от него имя пользователя и пароль
}

//По окончании всей работы не забудьте выключить свет и сообщить клиенту результат
//обработки
server_done()
{
printf("Sending data...\n");
send(ns, username, sizeof(username), 0);
close(ns);
printf("Server done...\n");
}

// А это главная и хитрейшая функция
// Именно она олицетворяет собой диалог человека с машиной. Только у нас она вырожденная
// Просто копирует в формируемый специальный ответ имеющиесяф данные.
// А вообще-то она может помимо всего этого
// выводить всяуие радостные окна и приветствия

static int stdin_conv(int num_msg, const struct pam_message **msgm,
                      struct pam_response **response, void *appdata_ptr) {
    struct pam_response *reply;
    int count;

    if (num_msg <= 0)
        return PAM_CONV_ERR;

    reply = (struct pam_response *) calloc(num_msg,
                                           sizeof(struct pam_response));
    if (reply == NULL) {
        return PAM_CONV_ERR;
    }

    for (count=0; count < num_msg; ++count) {
        reply[count].resp_retcode = 0;
        reply[count].resp = strdup(appdata_ptr);
    }

    *response = reply;
    reply = NULL;

    return PAM_SUCCESS;
}

int main(int argc, char * const argv[])
// Последняя, но не по значимости функция

{
    int retval;
    pam_handle_t *pamh=NULL;


    int i;
    /*прочтем, то что нам хотел бы сказать клиент*/

    server_init();
    server_read();


    /*И не долго думая установим новые значения*/
 / Важно: PAM теперь работает совсем по другому.
//Так как раньше он не позволил бы простому пользователю ввести себе
//слабый пароль, а теперь нет проблем - администратору это можно.

    conv.conv = stdin_conv;
    conv.appdata_ptr = strdup(newPassword);
// Мы только что заполнили структуры данных, необзодимые для диалога.
// А именно Добавили пароль и указатель на функцию диалога.

// Запускаем PAM. Говорим, что нужно читать файл /etc/pam.d/passwd,
// указываем имя пользователя и функция для ведения
// диалога.

    retval = pam_start("passwd", username, &conv, &pamh);

    while (retval == PAM_SUCCESS) {
        retval = pam_chauthtok(pamh, 0);
// Вот именно здесь все и происходит. PAM вызывает функцию-диалог,
// анализирует ответ в месте с модулями заданными в
// /etc/pam.d/passwd и изменяет пароль, если получается.

        if (retval != PAM_SUCCESS)
            break;
        /* all done */
        retval = pam_end(pamh, PAM_SUCCESS);
        if (retval != PAM_SUCCESS)
            break;
        /* quit gracefully */
        sprintf(username, "all right!...\n");
//Поработали и хватит.
        server_done();
        exit(0);
    }

    if (retval != PAM_SUCCESS)
        sprintf(username, "passwd: %s\n", pam_strerror(pamh, retval));

    if (pamh != NULL) {
        (void) pam_end(pamh,PAM_SUCCESS);
        pamh = NULL;
    }

// Пусть нам сегодня не повезло, но клиента тоже надо огорчить.
    server_done();
    exit(1);
}

Вот и весь сервер. Функций аутентификации заметно прибавилось, хотя они и скрылись за тремя могучими буквами PAM. Итак, повторим увиденное.

Для приложения важно запустить PAM (pam_start) и получить некоторый дескриптор. Затем определиться с диалоговой функцией. Сам PAM она не волнует, а волнует только выдаваемый ею ответ в специальной форме. Сделав это, запускаем требуюмую функцию: аутентификации (pam_authentificate), изменения пользовательских данных (pam_chauthtok) или какую еще. Наконец, торжественно завершаем концерт функцией pam_end. По-моему все просто и понятно, а теперь вперед к компьютеру...

4. Выводы.

Не правда ли PAM это здорово... :))