Если Вы уже успели прочитать одну из моих предыдущих записей о хэшировании, то Вы уже имеете базовое представление о теме сегодняшнего разговора. Одним из возможных способов применения хэшей является хранение аутентификационных данных пользователей интернет-приложения, об особенностях реализации формирования и проверки хэшей при регистрации и авторизации пользователей средствами PHP я и хотел бы с Вами поговорить. Сомневаюсь, что Вы услышите что-то новое, если я скажу, что в PHP даже в "стандартной комплектации" реализована масса алгоритмов хэширования, начиная с широкораспространенных md5(); и sha1(); и заканчивая модулями hash и mhash, в которых реализована еще целая масса алгоритмов. Все они давно уже стандартизованы и доступны для изучения всем желающим получить о них какую-либо информацию.

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

  • Первым делом он бы попытался определить, какой именно хэш перед ним находится - чаще всего это делается либо просто взглянув на длину хэша, либо если приложение широко распространено (популярная CMS скажем) - покопавшись в ее исходниках, еще есть вариант найти свой собственный аккаунт - и зная пароль попробовать на нем разные алгоритмы, способов можно придумать множество - все ограничивается лишь воображением. Узнав ответ на свой вопрос ему лишь останется набрать в Google фразу вроде "md5 decrypt", а дальше уже дело техники.
  • Еще один вариант решения задачи - взглянуть на список хэшей на предмет наличия совпадений. С очень высокой степенью вероятности за значительной группой одинаковых хэшей будет скрываться какой-либо банальный пароль вроде 123456.

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

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

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

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

<?php
function generateHash($input,$salt = false)
{
  if(!$salt)$salt=randomString(2);
  $hash=md5($input.$salt);
  return $salt.substr($hash,2);
}
?>

Как не трудно заметить - используется самодельная функция randomString();, которая возвращает случайную строку, состоящую из указанного количества шестнадцатеричных цифр (надеюсь Вы в состоянии написать ее своими силами). Именно этот момент и гарантирует элемент случайности при каждой новой генерации хэша. В том месте, где я прочитал про этот механизм (ссылку, к сожалению, привести не могу - в bookmark'ах не нашел), этот случайный компонент назывался словом salt, смысл его заключается в том, что он приписывается ко входным данным, передаваемым стандартной функции хэширования, а затем им же подменяется какая-либо фиксированная часть полученного хэша. Наверняка у Вас возник вопрос: а как же потом понять, что пользователь ввел верные данные, ведь для тех же исходных данных получится другой хэш и возможности их сравнить не будет? Ответ достаточно прост, его можно было увидеть даже в коде: при повторной инициализации хэша из базы данных достается заранее известная часть хранящегося там хэша, соответствующего конкретному пользователю - тот самый salt, и передается нашей функции. В этом случае в механизме будет использоваться именно он, а не новое случайное значение, и, как следствие, в случае правильности введенных данных на выходе получатся совпадающие хэши. Вот такой вот простенький, но иногда достаточно полезный трюк.

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

15 февраля 2008 |  Иван Блинков  |  PHP