Шаг 15 - Работа с паролями системы с помощью функции crypt()

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

За генерацию паролей отвечает функция crypt(). Подключить ее к программе можно так:

#define _XOPEN_SOURCE
#include <unistd.h>

char *crypt(const char *key, const char *salt);

Вместо заголовочного файла unistd.h для подключения функций шифрования можно использовать другой файл - crypt.h. В принципе без разницы, они оба имеют одинаковые определения функции crypt().

Для работы функции требуется два параметра:

Если же с паролем все ясно, то salt может заставить задуматься. Давайте посмотрим на что влияет этот параметр. Напишем тестовую программку crypt.c:

#include <stdlib.h>
#include <crypt.h>

int main(){
    printf("crypt(\"password\",\"ab\") = \"%s\"\n",crypt("password","ab"));
    printf("crypt(\"password\",\"ab12\") = \"%s\"\n",crypt("password","ab12"));
    printf("crypt(\"password\",\"ac\") = \"%s\"\n",crypt("password","ac"));
    printf("crypt(\"password\",\"ac123\") = \"%s\"\n",crypt("password","ac123"));
    printf("crypt(\"password\",\"1a\") = \"%s\"\n",crypt("password","1a"));
    printf("crypt(\"password\",\"1a.\") = \"%s\"\n",crypt("password","1a."));
    return 0;
};

Данная программа пытается выработать хеш с разным сальтом для пароля "password". Компилируем программу:

dron~# gcc crypt.c -o crypt
/tmp/ccBXde1R.o: In function `main':
/tmp/ccBXde1R.o(.text+0x17): undefined reference to `crypt'
/tmp/ccBXde1R.o(.text+0x3f): undefined reference to `crypt'
/tmp/ccBXde1R.o(.text+0x67): undefined reference to `crypt'
/tmp/ccBXde1R.o(.text+0x8f): undefined reference to `crypt'
/tmp/ccBXde1R.o(.text+0xb7): undefined reference to `crypt'
/tmp/ccBXde1R.o(.text+0xdf): more undefined references to `crypt' follow
collect2: ld returned 1 exit status

Не так быстро. Просто так откомпилировать программу нельзя, надо обязательно подключить библиотеку libcrypt.a, делается это такой командой:

dron~# gcc crypt.c -o crypt -lcrypt
dron~# ./crypt
crypt("password","ab") = "abJnggxhB/yWI"
crypt("password","ab12") = "abJnggxhB/yWI"
crypt("password","ac") = "acBxUIBkuWIZE"
crypt("password","ac123") = "acBxUIBkuWIZE"
crypt("password","1a") = "1abtv8E0hkEd6"
crypt("password","1a.") = "1abtv8E0hkEd6"

Посмотрите на результат работы функции. Хоть я и задавал разные salt некоторые хеши получились совершенно одинаковыми. Из этого можно сделать вывод и том, что в нем играют роль только первые два символа. Значения этих символов используются для шифрования пароля алгоритмом DES. Заметьте также, что salt входит в хеш как его начальная часть. И это правильно, ведь если его удалить, то неизвестно, что надо использовать для проверки достоверности пароля. На самом деле, как мы узнаем дальше, это сделано специально для того, чтобы можно было без особых мучений с паролем из /etc/shadow сразу же отправлять его в функцию crypt().

Хорошо. Нам теперь, думаю, стало понятно как работает алгоритм DES, а как же MD5 ? Ведь, как я уже говорил, пароли сегодня чаще всего хранятся в виде MD5. Хороший вопрос. Ответ на него я искал некоторое время. Исходя из того, что совершенно точно функция crypt() должна работать с MD5 я начал рыскать в исходнике gnu-pop3d, который совсем недавно чуть ли не переписал заново :) Пролистав все и не обнаружив ни #define ни хитрых #include я попробовал залезть в файл библиотеки /usr/lib/libcrypt.a. Получить список функций из нее можно командой nm:

dron~# nm /usr/lib/libcrypt.a

crypt-entry.o:
0000000000000117 t Letext
0000000000000000 T __crypt_r
                 U __md5_crypt
                 U __md5_crypt_r
                 U _ufc_dofinalperm_r
                 U _ufc_doit_r
                 U _ufc_foobar
                 U _ufc_mk_keytab_r
                 U _ufc_output_conversion_r
                 U _ufc_setup_salt_r
00000000000000d0 T crypt
0000000000000000 W crypt_r
00000000000000d0 W fcrypt
0000000000000000 r md5_salt_prefix
                 U strncmp
                 U strncpy

md5-crypt.o:
000000000000070a t Letext
                 U __assert_fail
                 U __errno_location
0000000000000690 T __md5_crypt
0000000000000000 T __md5_crypt_r
                 U __md5_finish_ctx
                 U __md5_init_ctx
...............

Посмотрите, список функций очень большой, поэтому не привожу весь, но видно определенно, что libcrypt.a содержит функции для работы с MD5. Но тогда как ?! Ведь нет никаких параметров дополнительных. А все оказалось куда проще %) Посмотрите на листинг, видите имя md5_salt_prefix. Не правда ли очень говорящее название ?! А теперь посмотрите типичный пароль закодированный с помощью MD5:

$1$/DrNy/Cv$ZBydbOBsEvdI5u5sib2X/0
$1$02p9xyDo$gnkh4vts/rArhJselceTV1

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

$1$..salt..$.........hash.........

Именно по этой структуре функция crypt() определяет каким методом ей шифровать пароль. Не поленимся однако и посмотрим исходники crypt() в библиотеке libc. Вот к примеру строки из файла crypt-entry.c:

/* Define our magic string to mark salt for MD5 encryption
replacement.  This is meant to be the same as for other MD5 based
encryption implementations.  */

static const char md5_salt_prefix[] = "$1$";

.......
  
/* Try to find out whether we have to use MD5 encryption replacement.*/
if (strncmp (md5_salt_prefix, salt, sizeof (md5_salt_prefix) - 1) == 0)
	return __md5_crypt_r (key, salt, (char *) data, sizeof (struct crypt_data));

Помоему классно :) Именно "магическая строчка" $1$ и является тем методом переключения между различными алгоритмами. Тут еще интересен вопрос о том, какой длины должен быть этот salt, изучая исходниках дальше Вы сможете найти в файле md5-crypt.c строчки:

/* Find beginning of salt string.  The prefix should normally always
be present.  Just in case it is not.  */
if (strncmp (md5_salt_prefix, salt, sizeof (md5_salt_prefix) - 1) == 0)
	/* Skip salt prefix.  */
	salt += sizeof (md5_salt_prefix) - 1;

salt_len = MIN (strcspn (salt, "$"), 8);
key_len = strlen (key);

Тут не вооруженным глазом видно, что после $1$ ищется второй символ $ и берется длина строки ограниченная этими признаками. Далее выбирается минимум между длиной строки и 8, т.е. получается что salt в алгоритме MD5 может быть любой длины не больше 8-ми. Это и требовалось доказать, теперь давайте попробуем :)

#include <stdlib.h>
#include <crypt.h>

int main(){
    printf("crypt(\"12345678\",\"$1$abasdlkasl123$\") = \"%s\"\n",
    	crypt("password","$1$abasdlkasl123$"));
    printf("crypt(\"12345678\",\"$1$dfg$\") = \"%s\"\n",
    	crypt("password","$1$dfg$"));
    return 0;
};

Снова компилируем, и не забываем про библиотеку crypt:

dron~#  gcc crypt1.c -o crypt1 -lcrypt
dron~# ./crypt1
crypt("12345678","$1$abasdlkasl123$") = "$1$abasdlka$z9aVWR2l14E3WngLCABSt1"
crypt("12345678","$1$dfg$") = "$1$dfg$fF0Vo9cC5CyBY827ltEdn0"

Все получилось :) А Вы как думали ?! И обратите внимание на то, что длинный salt в первом случае обрезался до 8-ми символов. Кстати, помоему длина 8 символов куда лучше, чем два. Это еще раз говорит о том, что метод MD5 лучше DES. И раз вообще заговорили про размер, то сравните длину получающихся хешей от работы этих алгоритмов.

Теперь, собственно говоря, сам процесс проверки пароля. Как Вы уже наверно поняли он сводится к простому сравнению, смотрим код:

#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <pwd.h>
#include <shadow.h>

int main(int argc,int *argv){
	struct passwd *userinfo;
	struct spwd *passw;
	uid_t userid;

	if (argc<2) {
		printf("Try to use: %s uin password\n",argv[0]);
		return 1;
	};

	userid = (uid_t)atoi(argv[1]);
	userinfo = getpwuid(userid);

	if (userinfo != NULL){
		passw = getspnam(userinfo->pw_name);
		if (passw != NULL){
			printf("Try to test password for \"%s\": ",userinfo->pw_name);
			if (strcmp(passw->sp_pwdp,crypt(argv[2],passw->sp_pwdp))==0)
				printf ("Ok...\n");
			else
				printf ("Failed...\n");
		} else
			printf("Can't find password for user with UIN = %s\n",argv[1]);
	} else
		printf("Can't find user with UIN = %s\n",argv[1]);

	return 0;
};

Теперь компилируем и запускаем:

dron~# ./testpasswd
Try to use: ./testpasswd uin password

dron~# ./testpasswd 1000 12345678
Try to test password for "dron": Ok...

dron~# ./testpasswd 1000 1234
Try to test password for "dron": Failed...

Помоему мы научились проверять правильность паролей для пользователей :) Только не забывайте про то, что пароли из /etc/shadow доступны только из под root, но об этом мы не однократно говорили раньше.

И еще я все время пытаюсь Вам привить то, что исходники не только для того, чтобы их компилировать. Они нужны для того, чтобы изучать программирование и мы будем продолжать их просматривать :) Мало ли может глюки найдем %)


Предыдущий Шаг | Следующий Шаг | Оглавление
Автор Кузин Андрей.