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