Операционные системы - статьи

         

Делаем собственное 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. По-моему все просто и понятно, а теперь вперед к компьютеру...


Содержание раздела