О хешах и безопасном хранении паролей
Опубликовано 10.02.11 / Автор перевода: Иван Саломатин
Комментариев 0
Время от времени мы слышим сообщения о том, что был взломан какой-то сервер или чья-то база данных… Осознавая такую опасность, очень важно обеспечить защиту ключевых данных пользователей, таких как пароли. В связи с этим, речь сегодня пойдёт об основных мерах, которые следует предпринять, чтобы обеспечить защиту паролей в ваших веб-приложениях.
1. Оговорочка
Криптология – это многогранный и комплексный предмет, и я отнюдь не считаю себя экспертом-криптографом. Многие институты и агентства по информационной безопасности регулярно проводят исследования в этой области. Я же лишь постараюсь максимально доступно рассказать об основных рациональных методах защиты паролей, которые следует использовать в веб-приложениях.2. Как работает Хеширование
Хеширование конвертирует кусочки данных (и большие и малые) в относительно короткие фрагменты, такие как «Строка» или «Целое число»
Это реализуется посредством использования односторонней (необратимой) функции. «Односторонней» означает, что крайне сложно (зачастую практически невозможно) выполнить реверс такого преобразования (привести результат действия функции к исходному значению).
Наиболее распространённый пример односторонней хеш-функции – это md5(), которая очень популярна среди программистов и используется во множестве языков и систем.
$data = "Hello World"; $hash = md5($data); echo $hash; // b10a8db164e0754105b7a99be72e3fe5
В результате обработки значения переменной функцией md5() результатом всегда будет строка длинной в 32 символа. Но такая строка содержит только шестнадцатеричные символы; технически, тот же результат может быть представлен 128-битным (16-байтным) целым числом. Не зависимо от размера данных в значении переменной, результатом действия функции md5() всегда будет хеш одинаковой длинны. Уже один этот факт может подсказать вам, почему это считается "односторонней" функцией.
3. Использование хеш-функции для хранения паролей
Типичный процесс регистрации пользователя включает в себя следующие действия:
- Пользователь заполняет регистрационную форму, включая поле «Пароль»;
- Сценарий записывает всю информацию в базу данных.
В то же время, введённый пароль обрабатывается хеш-функцией, и уже результат обработки помещается в базу. Исходная версия пароля нигде не хранится.
Теперь рассмотрим процесс «входа на сайт»:
- Пользователь вводит имя (или e-mail) и пароль;
- Сценарий обрабатывает пароль той же хеш-функцией;
- Затем, сценарий ищет данные пользователя в базе и извлекает хранимый хеш пароля.
- Хранимый хеш сравнивается с результатом обработки введённого пароля, и, если оба хеша совпадают, пользователю предоставляется доступ.
Поскольку оригинальный пароль нигде не хранится, даже если злоумышленник получит доступ к вашей базе данных, данные пользователей не могут быть скомпрометированы, верно? Ну, пожалуй, правильно будет ответить: «Это зависит от…». Давайте рассмотрим некоторые потенциальные угрозы.
4. Проблема #1: Коллизия хеш-функции
Коллизия хеш-функции возникает, когда результатом обработки разных данных является один и тот же хеш. Вероятность наступления такого события зависит от того, какую функцию для хеширования данных вы используете.
Как это может быть использовано?
Например, я встречал некоторые устаревшие скрипты, которые используют для хеширования паролей функцию crc32(). Результатом обработки данных этой функцией является 32-битное целое число. Это значит, что функция может возвращать 2 в 32-й степени (или 4 294 967 296) различных результатов.
Теперь давайте получим хеш какого-нибудь пароля:
echo crc32('supersecretpassword');
// результат: 323322056
Теперь давайте представим, как бы действовал злоумышленник, получивший доступ к вашей базе данных. Вероятнее всего он не сможет преобразовать хеш «323322056» в пароль «supersecretpassword», тем не менее, он сможет подобрать другой пароль, который в результате обработки даст такое же значение хеша. Сделать это можно с помощью простенького сценария:
set_time_limit(0);
$i = 0;
while (true) {
if (crc32(base64_encode($i)) == 323322056) { // обрабатываем функцией хеширования переменную $i
echo base64_encode($i);
// если результат обработки совпадает со значением хеша от искомого пароля, выводим значение переменной
exit;
}
$i++; // в противном случае увеличиваем переменную на единицу
}
Результат выполнения такой функции может быть успешно использован вместо пароля, поскольку хеши в данном случае эквивалентны.
Например, после запуска этого сценария на своём компьютере, я через некоторое время получил значение «MTIxMjY5MTAwNg==». Давайте его проверим:
echo crc32('supersecretpassword');
// возвращает: 323322056
echo crc32('MTIxMjY5MTAwNg==');
// возвращает: 323322056
Как защититься?
Сегодня, очень мощные домашние компьютеры способны выполнять функцию хеширования почти миллион раз в секунду! Следовательно, нам требуется хеш-функция, возвращающая значение, состоящее из как можно большего количества символов.
Например, нас может устроить md5(), генерирующая 128-битный хеш, и, следовательно, способная возвратить 340 282 366 920 938 463 463 374 607 431 768 211 456 различных результатов. Практически невозможно перебрать такое количество значений, чтобы обнаружить коллизию. Тем не менее, некоторые находят способы.
Лучшей альтернативой md5() является хеш-функция sha1(), генерирующая 160-битный хеш.
5. Проблема #2: Радужные таблицы
Даже если мы убережём свои хеши от возможных коллизий, риск взлома всё равно остаётся.
Радужные таблицы состоят из хешей значений наиболее распространённых паролей.
Эти таблицы включают миллионы и даже триллионы строк.
Например, вы можете открыть словарь и сгенерировать хеш для каждого слова. Также, вы можете расширить таблицу хешами словосочетаний. И даже это ещё не всё! Вы можете добавить хеши слов, дополненных цифрами, спец. символами и т.д.
Принимая во внимание тот факт, что дисковое пространство сегодня как никогда дёшево, радужные таблицы могут достигать гигантских размеров!
Как это может быть использовано?
Давайте представим, что злоумышленник получил доступ к огромной базе данных, хранящей миллион хешей паролей. Конечно, не составляет труда проверить наличие совпадений хешей из базы данных и хешей из таблицы. Далеко не все значения будут найдены, но некоторые, вероятно, найдутся.
Как защититься?
Наиболее распространённым и эффективным способом защиты от радужных таблиц является добавление «соли» - некоторого символьного мусора к каждому паролю. Например:
$password = "easypassword"; // простейший пароль, вводимый пользователем и, вероятно, имеющийся в радужной таблице echo sha1($password); // Хеш такого пароля при обработке функцией sha1() будет следующим: 6c94d3b42518febd4ad747801d50a8972022f956 $salt = "f#@V)Hu^%Hgfds"; // используя случайный набор символов, мы можем изменить значение хеша echo sha1($salt . $password); // а вот хеш для пароля, сдобренного солью: cd56a16759623378628c0d9336af69b74d9d71a5 // такой хеш не найдётся ни в одной радужной таблице
Всё, что мы делаем в данном случае – выполняем конкатенацию пароля и «соли», и полученный результат обрабатываем функцией хеширования.
Но даже теперь мы не достаточно защищены!
6. Проблема #3: И снова радужные таблицы
Не забывайте, что радужные таблицы могут быть созданы с нуля уже после того, как была скомпрометирована база данных.
Как это может быть использовано?
Даже если мы «подсолили» наши пароли, хеши всё-равно уязвимы. Всё что нужно сделать злоумышленнику в таком случае – создать новую таблицу, дополнив этой самой «солью» слова из словаря и сгенерировав хеши для полученных значений.
Так, например, очень вероятно, что хеш для «easypassword» имеется в существующей таблице, следовательно, в заново сгенерированной таблице найдётся и хеш для «f#@V)Hu^%Hgfdseasypassword»… Когда злоумышленник «пройдётся» по 10-и миллионам одинаково «подсоленных» хешей, он, наверняка, встретит несколько совпадений.
Как защититься?
Мы можем использовать уникальную динамически-генерируемую «соль» для каждого пароля, которая бы менялась для каждого пользователя.
Основой для генерации такой «соли» может быть, например, ID пользователя:
$hash = sha1($user_id . $password);
Мы также можем генерировать и строковое значение «соли» для каждого пользователя, только в этом случае нам следует убедиться, что это значение действительно уникально.
// генерируем случайную строку длинной в 22 символа
function unique_salt() {
return substr(sha1(mt_rand()),0,22);
}
$unique_salt = unique_salt();
$hash = sha1($unique_salt . $password); // формируем хеш пароля
// и затем сохраняем $unique_salt для каждого пользователя
Этот метод убережёт нас от радужных таблиц, поскольку теперь для каждого пароля существует уникальная «соль». Злоумышленнику придётся сгенерировать порядка 10-и миллионов различных радужных таблиц, что на практике фактически нереализуемо.
7. Проблема #4: Скорость хеширования
Большинство хеш-функций разработаны таким образом, чтобы время исполнения функции было минимальным, поскольку они часто используются для формирования так называемой «контрольной суммы», служащей индикатором при проверке целостности больших массивов данных.
Как это может быть использовано?
Как я уже упоминал выше, современные ПК с мощными GPU (да, именно видеокартами) могут обладать достаточными мощностями, чтобы рассчитывать миллиарды хешей в секунду. А это даёт возможность для брутфорс-атаки на каждый пароль.
Вы можете быть уверены, что пароль длинной в 8 символов достаточно безопасен, чтобы оградить от брутфорс-атаки, но давайте на основе элементарного расчёта определим, так ли это на самом деле:
Если пароль состоит из символов в нижнем и верхнем регистрах и цифр, то длина ряда символьных значений составит 62 (26+26+10) символа.
Для пароля длинной в 8 символов из допустимого символьного ряда можно составить 62 в восьмой степени комбинаций, что немногим более 218 триллионов.
Со скоростью близкой к 1 миллиарду хешей в секунду, задача подбора может быть решена приблизительно за 60 часов.
А для пароля в 6 символов, который также довольно распространён, такой подбор займёт немногим более 1 минуты!
Иначе говоря, требуйте от пользователей использовать пароли длинной 9-10 символов, и тогда, может быть, некоторых вы сможете от взлома уберечь...
Как защититься?
Использовать медленные функции хеширования.
Представьте, что ваша хеш-функция способна выполняться всего лишь миллион раз в секунду вместо миллиарда. Следовательно, для подбора пароля злоумышленнику потребуется в 1000 раз больше времени, а значит, решение задачи подбора восьмисимвольного пароля вместо прежних 60 часов займёт порядка 7 лет!
Единственный способ заставить функцию работать медленнее, не изменяя конфигурацию оборудования, – это программный:
function myhash($password, $unique_salt) {
$salt = "f#@V)Hu^%Hgfds";
$hash = sha1($unique_salt . $password);
// увеличиваем время исполнения функции в 1000 раз, заставив функцию сперва выполниться 1000 раз, и только затем возвратить результат
for ($i = 0; $i < 1000; $i++) {
$hash = sha1($hash);
}
return $hash;
}
Также, вы можете воспользоваться алгоритмом, позволяющим использовать «стоимостной показатель» функции, таким как BLOWFISH. В PHP это может быть реализовано посредством функции crypt().
function myhash($password, $unique_salt) {
// соль для blowfish должна быть длинной в 22 символа
return crypt($password, '$2a$10$'.$unique_salt);
}
Второй параметр функции crypt() состоит из значений, разделённых знаком доллара ($).
Первое значение «$2a», означает, что должен быть использован алгоритм BLOWFISH.
Второй параметр «$10» - это «стоимость функции». Этот параметр определяет сколько итераций цикла должна выполнить функция, прежде чем вывести результат. В данном случае 10 – это два в десятой степени итераций или 1024 итерации. Этот параметр может принимать значения от 04 до 31.
Давайте рассмотрим пример:
function myhash($password, $unique_salt) {
return crypt($password, '$2a$10$'.$unique_salt);
}
function unique_salt() {
return substr(sha1(mt_rand()),0,22);
}
$password = "verysecret";
echo myhash($password, unique_salt());
// результат: $2a$10$dfda807d832b094184faeu1elwhtR2Xhtuvs3R9J1nfRGBCudCCzC
В результате, хеш будет состоять из значения алгоритма ($2a), значения «стоимости функции» ($10) и 22-х символов «соли».
Протестируем эту функцию:
// допустим, хеш был извлечён из скомпрометированной базы данных
$hash = '$2a$10$dfda807d832b094184faeu1elwhtR2Xhtuvs3R9J1nfRGBCudCCzC';
// допустим, что исходное значение пароля для такого хеша будет
$password = "verysecret";
if (check_password($hash, $password)) {
echo "Доступ разрешён!";
} else {
echo "Доступ запрещён!";
}
function check_password($hash, $password) {
// первые 29 символов хеша, включая алгоритм, «стоимость функции» и оригинальную «соль» поместим в переменную $full_salt
$full_salt = substr($hash, 0, 29);
// выполним хеш-функцию для переменной $password
$new_hash = crypt($password, $full_salt);
// возвращаем результат («истина» или «ложь»)
return ($hash == $new_hash);
}
Исполнив такой сценарий для заданных переменных, мы увидим «Доступ разрешён!».
8. Собираем воедино
Принимая во внимание всё вышесказанное, давайте напишем класс, объединяющий в себе все приведённые сценарии:
class PassHash {
// blowfish
private static $algo = '$2a';
// стоимость функции
private static $cost = '$10';
// в основном для внутреннего использования
public static function unique_salt() {
return substr(sha1(mt_rand()),0,22);
}
// следующая функция будет использоваться для генерации хеша
public static function hash($password) {
return crypt($password,
self::$algo .
self::$cost .
'$' . self::unique_salt());
}
// эта функция будет использоваться для сравнения хешей при входе пользователя
public static function check_password($hash, $password) {
$full_salt = substr($hash, 0, 29);
$new_hash = crypt($password, $full_salt);
return ($hash == $new_hash);
}
}
Вот пример использования класса в процессе регистрации пользователя:
// подключаем класс
require ("PassHash.php");
// считываем данные из массива, содержащегося в переменной $_POST
// ...
// далее используем разнообразные механизмы проверки введённых данных
// ...
// формируем хеш пароля
$pass_hash = PassHash::hash($_POST['password']);
// помещаем все данные пользователя, за исключением значения ключа ['password'] массива $_POST ($_POST['password']), в базу данных
// а вместо значения $_POST['password'] помещаем значение переменной $pass_hash
// ...
И теперь используем класс в процессе авторизации пользователя:
// подключаем класс
require ("PassHash.php");
// считываем данные из массива, содержащегося в переменной $_POST
// ...
// на основании значения $_POST['username'] или $_POST['email'] извлекаем из базы данные пользователя
// ...
// проверяем эквивалентность хеша пароля, который ввёл пользователь, хешу, хранимому в базе
if (PassHash::check_password($user['pass_hash'], $_POST['password']) {
// разрешаем авторизацию
// ...
} else {
// отклоняем авторизацию
// ...
}
9. Пара слов о Blowfish
Не смотря на то, что алгоритм Blowfish сегодня довольно популярен, он может быть реализован не во всех системах. Определить наличие алгоритма для вашей системы вы можете с помощью следующего кода:
if (CRYPT_BLOWFISH == 1) {
echo "Да";
} else {
echo "Нет";
}
Если же вы используете PHP 5.3, то нет поводов для беспокойства, поскольку в эту версию BLOWFISH уже включён.
Заключение
Приведённый метод хеширования достаточно надёжен для многих веб-приложений. Тем не менее, не забывайте напоминать пользователям о необходимости использовать надёжные пароли – достаточно длинные и включающие как цифры, так и спец. символы.
И напоследок, вопрос вам, уважаемые читатели: а какие сценарии хеширования паролей используете вы? Быть может, вы сможете порекомендовать какие-нибудь усовершенствования приведённого метода? Буду очень признателен!










Добавь в закладки
или поделись с друзьями: