Pull to refresh
VK
Building the Internet

Риски и проблемы хеширования паролей

Reading time 11 min
Views 37K
Original author: Miguel Ibarra Romero
Безопасность всегда была неоднозначной темой, провоцирующей многочисленные горячие споры. И всё благодаря обилию самых разных точек зрения и «идеальных решений», которые устраивают одних и совершенно не подходят другим. Я считаю, что взлом системы безопасности приложения всего лишь вопрос времени. Из-за быстрого роста вычислительных мощностей и увеличения сложности безопасные сегодня приложения перестанут завтра быть таковыми.

Прим. перев.: для более полной картины здесь вас также будет ждать перевод Hashing Passwords with the PHP 5.5 Password Hashing API, на которую автор ссылается в статье.

Если вы не изучали алгоритмы хэширования, то, вероятнее всего, воспринимаете их как одностороннюю функцию, которая конвертирует данные переменной длины в данные фиксированной длины. Проанализируем это определение:
  • Односторонняя функция: из хэша невозможно восстановить исходные данные с помощью какого-либо эффективного алгоритма.
  • Конвертация данных переменной длины в данные фиксированной длины: входное значение может быть «бесконечной» длины, а выходное — нет. Это подразумевает, что два или более входных значения могут иметь одинаковые хэши. Чем меньше длина хэша, тем выше вероятность коллизии.

Алгоритмы MD5 и SHA-1 уже не обеспечивают достаточно высокой надёжности с точки зрения вероятности возникновения коллизий (см. Парадокс дней рождения). Поэтому рекомендуется использовать алгоритмы, генерирующие более длинные хэши (SHA-256, SHA-512, whirlpool и др.), что делает вероятность возникновения коллизии пренебрежимо малой. Такие алгоритмы ещё называют «псевдослучайными функциями», т. е. результаты их работы неотличимы от результатов работы полноценного генератора случайных чисел (true random number generator, TRNG).

Недостатки простого хэширования


Тот факт, что с помощью эффективного алгоритма невозможно провести операцию, обратную хэшированию, и восстановить исходные данные, не означает, что вас не могут взломать. Если хорошо поискать, то можно найти базы данных с хэшами распространённых слов и коротких фраз. Кроме того, простые пароли можно быстро и легко брутфорсить или взламывать перебором по словарю.

Вот небольшая демонстрация, как инструмент sqlmap через внедрение SQL-кода взламывает пароли с помощью брутфорса хэшей, сгенерированных алгоритмом MD5.



Злоумышленники могут поступить ещё проще — нагуглить конкретные хэши в онлайновых БД:

Также нужно понимать, что если два и более одинаковых пароля имеют одинаковые хэши, то, взломав один хэш, мы получаем доступ ко всем аккаунтам, где используется тот же пароль. Для примера: пусть у нас несколько тысяч пользователей, наверняка несколько из них используют пароль 123456 (если настройки сайта не заставляют усложнять пароль). MD5-хэш для этого пароля e10adc3949ba59abbe56e057f20f883e. Так что если вы заполучите этот хэш и поищете в базе данных по этому значению, то найдёте всех пользователей с таким паролем.

Почему небезопасны хэши с применением соли


Чтобы затруднить атаки описанного вида, применяется так называемая соль. Это стандартное средство, но в условиях современных вычислительных мощностей его уже недостаточно, особенно если длина соли невелика.

В общем виде функцию с использованием соли можно представить так:

f(password, salt) = hash(password + salt)

Для затруднения брутфорс-атаки соль должна быть длиной не менее 64 символов. Но проблема в том, что для дальнейшей аутентификации пользователей соль должна храниться в БД в виде простого текста.

if (hash([введённый пароль] + [соль]) == [хэш]) тогда пользователь аутентифицирован

Благодаря уникальности соли для каждого пользователя мы можем решить проблему коллизий простых хэшей. Теперь все хэши будут разными. Также уже не сработают подходы с гугленьем хэшей и брутфорсом. Но если злоумышленник через внедрение SQL-кода получит доступ к соли или БД, то сможет успешно атаковать брутфорсом или перебором по словарю, особенно если пользователи выбирают распространённые пароли (а-ля 123456).

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

Момент случайности


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

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

Когда от компьютера хотят случайное число, то обычно он берёт данные из нескольких источников (например, переменные среды: дату, время, количество записанных/считанных байтов и т. д.), а затем производит над ними вычисления для получения «случайных» данных. Поэтому такие данные называют псевдослучайными. А значит, если каким-то образом воссоздать набор исходных состояний на момент исполнения псевдослучайной функции, то мы сможем сгенерировать то же самое число.

Если псевдослучайный генератор ещё и реализован неправильно, то в генерируемых им данных можно обнаружить паттерны, а с их помощью предсказать результат генерирования. Взгляните на эту картинку, представляющую собой результат работы PHP-функции rand():

image

А теперь сравните с данными, сгенерированными полноценным генератором случайных чисел:



К сожалению, ни rand(), ни mt_rand() нельзя считать подходящими инструментами для обеспечения высокого уровня безопасности.

Если вам нужно получить случайные данные, воспользуйтесь функцией openssl_random_pseudo_bytes(), которая доступна начиная с версии 5.3.0. У неё даже есть флаг crypto_strong, который сообщит о достаточном уровне безопасности.

Пример использования:
<?php

function getRandomBytes ($byteLength)
{
    /*
     * Проверка доступности openssl_random_pseudo_bytes
     */
    if (function_exists('openssl_random_pseudo_bytes')) {
        $randomBytes = openssl_random_pseudo_bytes($byteLength, $cryptoStrong);
        if ($cryptoStrong)
            return $randomBytes;
    } 

    /*
     * Если openssl_random_pseudo_bytes недоступен или результат его работы слишком
     * слабый, то задействуется менее безопасный генератор
     */
    $hash = '';
    $randomBytes = '';

    /*
     * На Linux/UNIX-системах /dev/urandom является прекрасным источником энтропии, 
     * используйте его для получения начального значения $hash
     */
    if (file_exists('/dev/urandom')) {
        $fp = fopen('/dev/urandom', 'rb');
        if ($fp) {
            if (function_exists('stream_set_read_buffer')) {
                stream_set_read_buffer($fp, 0);
            }
            $hash = fread($fp, $byteLength);
            fclose($fp);
        }
    }

    /*
     * Используйте менее безопасную функцию mt_rand(), но только не rand()!
     */
    for ($i = 0; $i < $byteLength; $i ++) {
        $hash = hash('sha256', $hash . mt_rand());
        $char = mt_rand(0, 62);
        $randomBytes .= chr(hexdec($hash[$char] . $hash[$char + 1]));
    }
    return $randomBytes;
}

Растяжение пароля


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



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

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

Это делает атаку очень маловероятной… но не невозможной. Чтобы преодолеть секундную задержку, атакующий должен использовать более производительный компьютер, чем тот, под который был настроен алгоритм хэширования. Следовательно, процесс взлома может потребовать дополнительных расходов.

Для растяжения пароля можно использовать стандартные алгоритмы, например PBDKDF2, представляющий собой функцию формирования ключа:
<?php

/*
 * Количество итераций можно увеличить, чтобы перекрыть дальнейший рост 
 * производительности CPU/GPU. Используйте разные соли для каждого пароля
 * (можно класть их вместе со сгенерированным паролем). Данная функция
 * работает медленно, это сделано преднамеренно! Больше информации здесь: -
 * http://ru.wikipedia.org/wiki/PBKDF2 - http://www.ietf.org/rfc/rfc2898.txt
 */
function pbkdf2 ($password, $salt, $rounds = 15000, $keyLength = 32, 
        $hashAlgorithm = 'sha256', $start = 0)
{
    // Key blocks to compute
    $keyBlocks = $start + $keyLength;

    // Derived key
    $derivedKey = '';

    // Create key
    for ($block = 1; $block <= $keyBlocks; $block ++) {
        // Initial hash for this block
        $iteratedBlock = $hash = hash_hmac($hashAlgorithm, 
                $salt . pack('N', $block), $password, true);

        // Perform block iterations
        for ($i = 1; $i < $rounds; $i ++) {
            // XOR each iteration
            $iteratedBlock ^= ($hash = hash_hmac($hashAlgorithm, $hash, 
                    $password, true));
        }

        // Append iterated block
        $derivedKey .= $iteratedBlock;
    }

    // Return derived key of correct length
    return base64_encode(substr($derivedKey, $start, $keyLength));
}

Есть и более затратные по времени и памяти алгоритмы, например bcrypt (о нём мы поговорим ниже) или scrypt:
<?php
// bcrypt внедрён в функцию crypt()
$hash = crypt($pasword, '$2a$' . $cost . '$' . $salt);

  • $cost — коэффициент трудоёмкости;
  • $salt — случайная строка. Её можно генерировать, например, с помощью описанной выше функции secure_rand().

Коэффициент трудоёмкости целиком зависит от машины, на которой выполняется хэширование. Можете начать со значения 09 и постепенно увеличивать, пока длительность операции не достигнет одной секунды. Начиная с версии 5.5 можно пользоваться функцией password_hash(), об этом мы поговорим дальше.

На данный момент в PHP не реализована поддержка алгоритма scrypt, но можно воспользоваться реализацией от Domblack.

Применение технологий шифрования


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

Есть и ещё одно важное отличие шифрования от хэширования: размер пространства выходного сообщения не ограничен и зависит от размера входных данных в соотношении 1:1. Поэтому нет риска возникновения коллизий.

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

Некоторое время назад у Adobe была мощная утечка пользовательской БД из-за неправильно реализованного шифрования. Давайте разберём, что у них произошло.

image

Предположим, что в таблице хранятся следующие данные в виде обычного текста:



Кто-то в Adobe решил зашифровать пароли, но при этом совершил две большие ошибки:
  1. использовал один и тот же криптоключ;
  2. оставил поля passwordHint незашифрованными.

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

image

Мы не знаем, какой применялся криптоключ. Но если проанализировать данные, то можно заметить, что в строках 2 и 7 используется один и тот же пароль, так же как и в строках 3 и 6.

Пришло время обратиться к подсказке пароля. В строке 6 это «I’m one!», что совершенно неинформативно. Зато благодаря строке 3 мы можем предположить, что пароль — queen. Строки 2 и 7 по отдельности не позволяют вычислить пароль, но если проанализировать их вместе, то можно предположить, что это halloween.

Ради снижения риска утечки данных лучше использовать разные способы хэширования. А если вам нужно шифровать пароли, то обратите внимание на настраиваемое шифрование:



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

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

f(key, primaryKey) = key + primaryKey

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

Если мы применим к нашей таблице настраиваемое шифрование, то она будет выглядеть так:

image

Конечно, нужно будет ещё что-то сделать с подсказками паролей, но всё-таки уже получилось хоть что-то адекватное.

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

PHP 5.5


Сегодня оптимальным способом хэширования паролей считается использование bcrypt. Но многие разработчики всё ещё предпочитают старые и более слабые алгоритмы вроде MD5 и SHA-1. А некоторые при хэшировании даже не пользуются солью. В PHP 5.5 был представлен новый API для хэширования, который не только поощряет применение bcrypt, но и существенно облегчает работу с ним. Давайте разберём основы использования этого нового API.

Здесь применяются четыре простые функции:
  • password_hash() — хэширование пароля;
  • password_verify() — сравнение пароля с хэшем;
  • password_needs_rehash() — перехэширование пароля;
  • password_get_info() — возвращение названия алгоритма хэширования и применявшихся в ходе хэширования опций.

password_hash()


Несмотря на высокий уровень безопасности, обеспечиваемый функцией crypt(), многие считают её слишком сложной, из-за чего программисты часто допускают ошибки. Вместо неё некоторые разработчики используют для генерирования хэшей комбинации слабых алгоритмов и слабых солей:
<?php
$hash = md5($password . $salt); // работает, но уровень безопасности невысок

Функция password_hash() существенно облегчает разработчику жизнь и повышает безопасность кода. Для хэширования пароля достаточно скормить его функции, и она вернёт хэш, который можно поместить в БД:
<?php
$hash = password_hash($passwod, PASSWORD_DEFAULT);

И всё! Первый аргумент — пароль в виде строки, второй аргумент задаёт алгоритм генерирования хэша. По умолчанию используется bcrypt, но при необходимости можно добавить и более сильный алгоритм, который позволит генерировать строки большей длины. Если в своём проекте вы используете PASSWORD_DEFAULT, то удостоверьтесь, что ширина колонки для хранения хэшей не менее 60 символов. Лучше сразу задать 255 знаков. В качестве второго аргумента можно использовать PASSWORD_BCRYPT. В этом случае хэш всегда будет длиной в 60 символов.

Обратите внимание, что вам не нужно задавать значение соли или стоимостного параметра. Новый API всё сделает за вас. Поскольку соль является частью хэша, то вам не придётся хранить её отдельно. Если вам всё-таки нужно задать своё значение соли (или стоимости), то это можно сделать с помощью третьего аргумента:
<?php
$options = [
    'salt' => custom_function_for_salt(), // Напишите собственный код генерирования соли
    'cost' => 12 // По умолчанию стоимость равна 10
];
$hash = password_hash($password, PASSWORD_DEFAULT, $options);

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

password_verify()


Теперь рассмотрим функцию сравнения пароля с хэшем. Первый вводится пользователем, а второй мы берём из БД. Пароль и хэш используются в качестве двух аргументов функции password_verify(). Если хэш соответствует паролю, то функция возвращает true.
<?php
if (password_verify($password, $hash)) {
    // Успешно!
}
else {
    // Неверные данные
}

Помните, что соль является частью хэша, поэтому она не задаётся здесь отдельно.

password_needs_rehash()


Если вы захотите повысить уровень безопасности, добавив более сильную соль или увеличив стоимостный параметр, или изменится алгоритм хэширования по умолчанию, то вы, вероятно, захотите перехэшировать все имеющиеся пароли. Данная функция поможет проверить каждый хэш на предмет того, какой алгоритм и параметры использовались при его создании:
<?php
if (password_needs_rehash($hash, PASSWORD_DEFAULT, ['cost' => 12])) {
    // Пароль надо перехэшировать, поскольку использовался не текущий
    // алгоритм по умолчанию либо стоимостный параметр не был равен 12
    $hash = password_hash($password, PASSWORD_DEFAULT, ['cost' => 12]);

    // Не забудьте сохранить новый хэш!
}

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

password_get_info()


Данная функция берёт хэш и возвращает ассоциативный массив из трёх элементов:
  • algo — константа, позволяющая идентифицировать алгоритм;
  • algoName — название использовавшегося алгоритма;
  • options — значения разных опций, применявшихся при хэшировании.

Более ранние версии PHP


Как видите, работать с новым API не в пример легче, чем с неуклюжей функцией crypt(). Если же вы используете более ранние версии PHP, то рекомендую обратить внимание на библиотеку password_compact. Она эмулирует данный API и автоматически отключается, когда вы обновляетесь до версии 5.5.

Заключение


К сожалению, до сих пор не существует идеального решения для защиты данных. К тому же всегда есть риск взлома вашей системы безопасности. Однако борьба снаряда и брони не прекращается. Например, наш арсенал средств защиты относительно недавно пополнился так называемыми функциями губки.
Tags:
Hubs:
+31
Comments 42
Comments Comments 42

Articles

Information

Website
vk.com
Registered
Founded
Employees
5,001–10,000 employees
Location
Россия
Representative
Миша Берггрен