Перейти к содержимому

Как посчитать хэш функцию

  • автор:

Использование Visual C# для вычисления и сравнения хэш-значений

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

Исходная версия продукта: Visual C #
Оригинальный номер базы знаний: 307020

Аннотация

В этой статье рассматриваются следующие пространства имен библиотеки классов Microsoft платформа .NET Framework:

  • System.Security.Cryptography
  • System.Text

Класс System.Security.Cryptography в платформа .NET Framework упрощает вычисление хэш-значения для исходных данных.

Вычисление хэш-значения

С помощью криптографических ресурсов, содержащихся в System.Security.Cryptography пространстве имен, легко создавать и сравнивать хэш-значения. Так как все хэш-функции принимают входные данные типа Byte[] , может потребоваться преобразовать источник в массив байтов, прежде чем он будет хэширован. Чтобы создать хэш для строкового значения, выполните следующие действия.

  1. Откройте Visual Studio .NET или Visual Studio.
  2. Создание нового консольного приложения в Visual C# .NET или Visual C# создает открытый класс вместе с пустым Main() методом.

Примечание. В Visual C#. NET, Class1.cs создается по умолчанию. В Visual C# файл Program.cs создается по умолчанию.

using System; using System.Security.Cryptography; using System.Text; 
string sSourceData; byte[] tmpSource; byte[] tmpHash; 
sSourceData = "MySourceData"; //Create a byte array from source data. tmpSource = ASCIIEncoding.ASCII.GetBytes(sSourceData); 

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

//Compute hash based on source data. tmpHash = new MD5CryptoServiceProvider().ComputeHash(tmpSource); 
Console.WriteLine(ByteArrayToString(tmpHash)); static string ByteArrayToString(byte[] arrInput) < int i; StringBuilder sOutput = new StringBuilder(arrInput.Length); for (i=0;i < arrInput.Length; i++) < sOutput.Append(arrInput[i].ToString("X2")); >return sOutput.ToString(); > 

Сравнение двух хэш-значений

Цели создания хэша на основе исходных данных:

  • Предоставление способа узнать, изменялись ли данные с течением времени.
  • Сравнение двух значений без работы с фактическими значениями.

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

    Сразу под созданием шестнадцатеричной строки создайте новое хэш-значение на основе новых исходных данных.

sSourceData = "NotMySourceData"; tmpSource = ASCIIEncoding.ASCII.GetBytes(sSourceData); byte[] tmpNewHash; tmpNewHash = new MD5CryptoServiceProvider().ComputeHash(tmpSource); 
bool bEqual = false; if (tmpNewHash.Length == tmpHash.Length) < int i=0; while ((i < tmpNewHash.Length) && (tmpNewHash[i] == tmpHash[i])) < i += 1; >if (i == tmpNewHash.Length) < bEqual = true; >> if (bEqual) Console.WriteLine("The two hash values are the same"); else Console.WriteLine("The two hash values are not the same"); Console.ReadLine(); 

Полный список кода

using System; using System.Security.Cryptography; using System.Text; namespace ComputeAHash_csharp < /// /// Summary description for Class1. /// class Class1 < static void Main(string[] args) < string sSourceData; byte[] tmpSource; byte[] tmpHash; sSourceData = "MySourceData"; //Create a byte array from source data tmpSource = ASCIIEncoding.ASCII.GetBytes(sSourceData); //Compute hash based on source data tmpHash = new MD5CryptoServiceProvider().ComputeHash(tmpSource); Console.WriteLine(ByteArrayToString(tmpHash)); sSourceData = "NotMySourceData"; tmpSource = ASCIIEncoding.ASCII.GetBytes(sSourceData); byte[] tmpNewHash; tmpNewHash = new MD5CryptoServiceProvider().ComputeHash(tmpSource); bool bEqual = false; if (tmpNewHash.Length == tmpHash.Length) < int i=0; while ((i < tmpNewHash.Length) && (tmpNewHash[i] == tmpHash[i])) < i += 1; >if (i == tmpNewHash.Length) < bEqual = true; >> if (bEqual) Console.WriteLine("The two hash values are the same"); else Console.WriteLine("The two hash values are not the same"); Console.ReadLine(); > static string ByteArrayToString(byte[] arrInput) < int i; StringBuilder sOutput = new StringBuilder(arrInput.Length); for (i=0;i < arrInput.Length -1; i++) < sOutput.Append(arrInput[i].ToString("X2")); >return sOutput.ToString(); > > > 

Ссылки

Дополнительные сведения об использовании криптографических функций платформа .NET Framework см. в разделе .NET.

HackWare.ru

Этичный хакинг и тестирование на проникновение, информационная безопасность

Хеши: определение типа, подсчёт контрольных сумм, нестандартные и итерированные хеши

Что такое хеши и как они используются

Хеш-сумма (хеш, хеш-код) — результат обработки неких данных хеш-функцией (хеширования).

Хеширование, реже хэширование (англ. hashing) — преобразование массива входных данных произвольной длины в (выходную) битовую строку фиксированной длины, выполняемое определённым алгоритмом. Функция, реализующая алгоритм и выполняющая преобразование, называется «хеш-функцией» или «функцией свёртки». Исходные данные называются входным массивом, «ключом» или «сообщением». Результат преобразования (выходные данные) называется «хешем», «хеш-кодом», «хеш-суммой», «сводкой сообщения».

Это свойство хеш-функций позволяет применять их в следующих случаях:

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

Одним из применений хешов является хранение паролей. Идея в следующем: когда вы придумываете пароль (для веб-сайта или операционной системы) сохраняется не сам пароль, а его хеш (результат обработки пароля хеш-функцией). Этим достигается то, что если система хранения паролей будет скомпрометирована (будет взломан веб-сайт и злоумышленник получит доступ к базе данных паролей), то он не сможет узнать пароли пользователей, поскольку они сохранены в виде хешей. Т.е. даже взломав базу данных паролей он не сможет зайти на сайт под учётными данными пользователей. Когда нужно проверить пароль пользователя, то для введённого значения также рассчитывается хеш и система сравнивает два хеша, а не сами пароли.

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

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

Как определить тип хеша

Существует большое количество хешей. Некоторые из них являются универсальными и применяются различными приложениями, например, MD5, SHA1, CRC8 и другие. Некоторые хеши применяются только в определённых приложениях (MySQL, vBulletin) и протоколами.

Кроме популярных хешей, разработчики могут использовать различные сочетания универсальных хешей (например, посчитать хеш с помощью MD5, а затем для полученной строки получить хеш SHA1), либо итерированные (с повторением) хеши (например, для пароля рассчитывается MD5 хеш, затем для полученной строки вновь рассчитывается MD5 хеш, затем для полученной строки вновь считается MD5 – и так тысячу раз).

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

Пример такой строки для WinZip: $zip2$*0*3*0*b5d2b7bf57ad5e86a55c400509c672bd*d218*0**ca3d736d03a34165cfa9*$/zip2$

Пример строки для взлома пароля файла PDF 1.7 Level 8 (Acrobat 10 — 11): $pdf$5*6*256*-4*1*16*381692e488413f5502fa7314a78c25db*48*e5bf81a2a23c88f3dccb44bc7da68bb5606b653b733bcf9adaa5eb2c8ccf53abba66539044eb1957eda68469b1d0b9b5*48*b222df06deb308bf919d13447e688775fdcab972faed2c866dc023a126cb4cd4bbffab3683ecde243cf8d88967184680

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

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

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

hashID

Эта программа по умолчанию уже установлена в Kali Linux. Она идентифицирует различные типы хешей, используемых для шифрования данных, в первую очередь, паролей.

hashID – это инструмент, написанный на Python 3, который поддерживает идентификацию более 220 уникальных типов хешей используя регулярные выражения.

Использование программы очень простое:

hashid хеш_для_идентификации

Пара важных замечаний:

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

Хеш режимы Hashcat – это условное обозначение типа хеша, которое необходимо указать с опцией -m, —hash-type.

Информацию о других опциях hashID вы найдёте здесь: https://kali.tools/?p=2772

К примеру, мне необходимо идентифицировать хеш $S$C33783772bRXEx1aCsvY.dqgaaSu76XmVlKrW9Qu8IQlvxHlmzLf:

hashid -m '$S$C33783772bRXEx1aCsvY.dqgaaSu76XmVlKrW9Qu8IQlvxHlmzLf'

Как можно увидеть по скриншоту, это Drupal > v7.x в Hashcat для взлома данного хеша необходимо указать режим 7900.

Идентифицируем хеш $1$VnG/6ABB$t6w9bQFxvI9tf0sFJf2TR.:

hashid -m '$1$VnG/6ABB$t6w9bQFxvI9tf0sFJf2TR.'

Получаем сразу несколько вариантов:

MD5cryp – это алгоритм, который вызывает тысячу раз стандартный MD5, для усложнения процесса.

Для справки: MD5 использовался для хеширования паролей. В системе UNIX каждый пользователь имеет свой пароль и его знает только пользователь. Для защиты паролей используется хеширование. Предполагалось, что получить настоящий пароль можно только полным перебором. При появлении UNIX единственным способом хеширования был DES (Data Encryption Standard), но им могли пользоваться только жители США, потому что исходные коды DES нельзя было вывозить из страны. Во FreeBSD решили эту проблему. Пользователи США могли использовать библиотеку DES, а остальные пользователи имеют метод, разрешённый для экспорта. Поэтому в FreeBSD стали использовать MD5 по умолчанию. Некоторые Linux-системы также используют MD5 для хранения паролей.

Ещё один хеш $6$q8C1F6tv$zTP/eEVixqyQBEfsSbTidUJfnaE2ojNIpTwTHava/UhFORv3V4ehyTOGdQEoFo1dEVG6UcXwhG.UHvyQyERz01:

hashid -m '$6$q8C1F6tv$zTP/eEVixqyQBEfsSbTidUJfnaE2ojNIpTwTHava/UhFORv3V4ehyTOGdQEoFo1dEVG6UcXwhG.UHvyQyERz01'

Программа говорит, что это SHA-512 Crypt – т.е. SHA512 (Unix).

HashTag

HashTag – это инструмент на python, который разбирает и идентифицирует различные хеши паролей на основе их типа. HashTag поддерживает определение более 250 типов хешей и сопоставляет их с более чем 110 режимами hashcat. HashTag способен идентифицировать единичный хеш, разобрать единичный файл и определить хеши внутри него или обойти директорию и все поддиректории в поисках потенциальных файлов хешей и идентифицировать все найденные хеши.

Т.е. это аналогичная предыдущей программа.

По умолчанию в Kali Linux она отсутствует, поэтому требуется её скачать:

git clone https://github.com/SmeegeSec/HashTag.git cd HashTag/ python2 HashTag.py -h

Хеш для HashTag также нужно помещать в одинарные кавычки. Хеш нужно писать после опции -sh. Зато сразу, без дополнительных опций выводятся режимы. Информацию о других опциях HashTag вы найдёте здесь: https://kali.tools/?p=2777

Идентифицируем те же самые хеши:

python2 HashTag.py -sh '$S$C33783772bRXEx1aCsvY.dqgaaSu76XmVlKrW9Qu8IQlvxHlmzLf'

python2 HashTag.py -sh '$1$VnG/6ABB$t6w9bQFxvI9tf0sFJf2TR.'

python2 HashTag.py -sh '$6$q8C1F6tv$zTP/eEVixqyQBEfsSbTidUJfnaE2ojNIpTwTHava/UhFORv3V4ehyTOGdQEoFo1dEVG6UcXwhG.UHvyQyERz01'

Как видим, результаты аналогичны.

Примеры хешей

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

На той странице вы можете:

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

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

К примеру, меня интересует хеш c73d08de890479518ed60cf670d17faa26a4a71f995c1dcc978165399401a6c4:53743528:

hashid -m 'c73d08de890479518ed60cf670d17faa26a4a71f995c1dcc978165399401a6c4:53743528'

Это явно ошибочный результат, поскольку соль после двоеточия будто бы была отпрошена при идентификации хеша.

python2 HashTag.py -sh 'c73d08de890479518ed60cf670d17faa26a4a71f995c1dcc978165399401a6c4:5374'

Получаем более правильный результат:

В действительности это sha256($pass.$salt).

Как рассчитать хеш (контрольную сумму)

В Linux имеются программы для расчёта и сверки популярных хешей:

  • b2sum – вычисляет и проверяет криптографическую хеш-функцию BLAKE2 (512-бит)
  • cksum – печатает контрольную сумму CRC и количество байт
  • md5sum – печатает или проверяет контрольную сумму MD5 (128-бит)
  • sha1sum – печатает или проверяет контрольную сумму SHA1 (160-бит)
  • sha224sum – печатает или проверяет контрольную сумму SHA224 (224- бит)
  • sha256sum – печатает или проверяет контрольную сумму SHA256 (256- бит)
  • sha384sum – печатает или проверяет контрольную сумму SHA384 (384- бит)
  • sha512sum – печатает или проверяет контрольную сумму SHA512 (512- бит)

Информация о SHA-2 (безопасный алгоритм хеширования, версия 2) – семействе криптографических алгоритмов (SHA-224, SHA-256, SHA-384, SHA-512, SHA-512/256 и SHA-512/224.): https://ru.wikipedia.org/wiki/SHA-2

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

Применение всех этих программ похожее – нужно указать имя файла, либо передать по стандартному вводу строку.

Если для расчёта хеша строки вы используете echo, то крайне важно указывать опцию -n, которая предотвращает добавление символа новой строки – иначе каждый хеш для строки будет неверным!

Пример подсчёта хеша SHA1 для строки test:

echo -n 'test' | sha1sum a94a8fe5ccb19ba61c4c0873d391e987982fbbd3 -

Ещё один способ передачи строки без добавления конечного символа newline

printf '%s' 'test' | md5sum

Как можно заметить, после хеша следует пробел и имя файла (в случае стандартного ввода – указывается тире), чтобы показать только хеш, можно использовать к команде добавить | awk » или | cut -d» » -f1:

echo -n 'test' | sha1sum | awk ''

Этот же результат можно получить следующей конструкцией:

echo -n 'test' | sha1sum | cut -d" " -f1

Программы для вычисления различных хешей

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

Список некоторых популярных программ для вычисления хешей:

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

Последовательное хеширование с использованием трубы (|)

К примеру, нам нужно рассчитать sha256 хеш для строки ‘HackWare’; а затем для полученной строки (хеша), рассчитать хеш md5. Задача кажется очень тривиальной:

echo -n 'HackWare' | sha256sum | md5sum

Но это неправильный вариант. Поскольку результатом выполнения в любом случае является непонятная строка из случайных символов, трудно не только обнаружить ошибку, но даже понять, что она есть. А ошибок здесь сразу несколько! И каждая из них ведёт к получению абсолютно неправильных данных.

Даже очень бывалые пользователи командной строки Linux не сразу поймут в чём проблема, а обнаружив первую проблему не сразу поймут, что есть ещё одна.

Очень важно помнить, что в строке вместе с хешем всегда выводится имя файла, поэтому выполняя довольно очевидную команду вроде следующей:

echo -n 'HackWare' | sha256sum | md5sum

мы получим совсем не тот результат, который ожидаем. Мы предполагаем посчитать sha256 хеш строки ‘HackWare’, а затем для полученной строки (хеша) рассчитать новый хеш md5. На самом деле, md5sum рассчитывает хеш строки, к которой прибавлено « ». Т.е. получается совершенно другой результат.

Выше уже рассмотрено, как из вывода удалять « », кажется, теперь всё должно быть в порядке:

echo -n 'HackWare' | sha256sum | awk '' | md5sum

Давайте разобьём это действие на отдельные команды:

echo -n 'HackWare' | sha256sum | awk ''

Второй этап хеширования:

echo -n '353b717198496e369cff5fb17bc8be8a1d8e6e6e30be65d904cd000ebe394833' | md5sum 0fcc41fc5d3d7b09e35866cd6e831085 -

Это и есть правильный ответ.

echo -n 'HackWare' | sha256sum | awk '' | md5sum

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

Используя printf можно вывести результат без конечного символа новой строки:

printf '%s' `echo -n 'HackWare' | sha256sum | awk ''` | md5sum

Результат вновь правильный:

С printf не все дружат и проблематично использовать рассмотренную конструкцию если нужно хешировать более трёх раз, поэтому лучше использовать tr:

echo -n 'HackWare' | sha256sum | awk '' | tr -d '\n' | md5sum

Вновь правильный результат:

Или даже сделаем ещё лучше – с программой awk будем использовать printf вместо print (это самый удобный и короткий вариант):

echo -n 'HackWare' | sha256sum | awk '' | md5sum

Как посчитать итерированные хеши

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

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

Пример кода, который подсчитывает MD5 хеш с 1000 итераций:

#!/bin/bash text='HackWare' iterations=1000 function iterateMD5 < echo -n "$text" | md5sum >count=1 while [[ $count -le $iterations ]]; do text="$(iterateMD5 | awk '')" count=$((count + 1)) done echo "$text"
  • HackWare – строка для хеширования
  • 1000 – количество итераций
  • md5sum – используемая функция хеширования

Онлайн сервис определения хешей

Описанный выше способ идентификации типа хешей реализован в виде бесплатного онлайн сервиса на SuIP.biz: https://suip.biz/ru/?act=hashtag

Связанные статьи:

  • Как определить тип хеша (88.6%)
  • Полное руководство по John the Ripper. Ч.6: брут-форс нестандартных хешей (65.7%)
  • Как взломать пароль Windows (54.3%)
  • Расшифровка хранимых в Windows паролей с помощью mimikatz и DPAPI (54.3%)
  • Полное руководство по John the Ripper. Ч.7: Johnny — графический интерфейс для John the Ripper (54.3%)
  • Как взломать VNC пароль из захваченного трафика (challenge response) (RANDOM — 50%)

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

Алгоритм хеширования данных: просто о сложном

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

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

Что такое хеш (хэш, hash)?

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

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

Screenshot_1-1801-e37961.png

Убедиться в этом можно на любом онлайн-генераторе. Набрав слово «Otus» и воспользовавшись алгоритмом sha1 (Secure Hashing Algorithm), мы получим хеш 7576750f9d76fab50762b5987739c18d99d2aff7. При изменении любой буквы изменится и результат, причем изменится полностью. Мало того, если просто поменять регистр хотя бы одной буквы, итог тоже будет совершенно иным: если написать «otus», алгоритм хэш-функции отработает со следующим результатом: 1bbd70dc1b6fc84e5617ca8703c72c744b3b4fc1. Хотя общие моменты все же есть: строка всегда состоит из сорока символов.

В предыдущем примере речь шла о применении хэш-алгоритма для слова из 4 букв. Но с тем же успехом можно вставить слово из 1000 букв — все равно после обработки данных на выходе получится значение из 40 символов. Аналогичная ситуация будет и при обработке полного собрания сочинений Льва Толстого.

Screenshot_2-1801-1a2e3d.png

Криптостойкость функций хеширования

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

Проблемы хэшей

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

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

Если S = hash (x), то, в идеале, нахождение x должно быть практически невозможным.

Алгоритм MD5 и его подверженность взлому

MD5 hash — один из первых стандартов алгоритма, который применялся в целях проверки целостности файлов (контрольных сумм). Также с его помощью хранили пароли в базах данных web-приложений. Функциональность относительно проста — алгоритм выводит для каждого ввода данных фиксированную 128-битную строку, задействуя для вычисления детерминированного результата однонаправленные тривиальные операции в нескольких раундах. Особенность — простота операций и короткая выходная длина, в результате чего MD5 является относительно легким для взлома. А еще он обладает низкой степенью защиты к атаке типа «дня рождения».

Атака дня рождения

Если поместить 23 человека в одну комнату, можно дать 50%-ную вероятность того, что у двух человек день рождения будет в один и тот же день. Если же количество людей довести до 70-ти, вероятность совпадения по дню рождения приблизится к 99,9 %. Есть и другая интерпретация: если голубям дать возможность сесть в коробки, при условии, что число коробок меньше числа голубей, окажется, что хотя бы в одной из коробок находится более одного голубя.

Screenshot_3-1801-bf0263.png

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

Когда разговор идет о сопротивлении коллизиям, то алгоритм MD5 действительно очень слаб. Настолько слаб, что даже бытовой Pentium 2,4 ГГц сможет вычислить искусственные хеш-коллизии, затратив на это чуть более нескольких секунд. Всё это в ранние годы стало причиной утечки большого количества предварительных MD5-прообразов.

SHA1, SHA2, SHA3

Secure Hashing Algorithm (SHA1) — алгоритм, созданный Агентством национальной безопасности (NSA). Он создает 160-битные выходные данные фиксированной длины. На деле SHA1 лишь улучшил MD5 и увеличил длину вывода, а также увеличил число однонаправленных операций и их сложность. Однако каких-нибудь фундаментальных улучшений не произошло, особенно когда разговор шел о противодействии более мощным вычислительным машинам. Со временем появилась альтернатива — SHA2, а потом и SHA3. Последний алгоритм уже принципиально отличается по архитектуре и является частью большой схемы алгоритмов хеширования (известен как KECCAK — «Кетч-Ак»). Несмотря на схожесть названия, SHA3 имеет другой внутренний механизм, в котором используются случайные перестановки при обработке данных — «Впитывание» и «Выжимание» (конструкция «губки»).

Что в будущем?

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

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

Хэширование в строковых задачах

Хэш — это какая-то функция, сопоставляющая объектам какого-то множества числовые значения из ограниченного промежутка.

  • Быстро считается — за линейное от размера объекта время;
  • Имеет не очень большие значения — влезающие в 64 бита;
  • «Детерминированно-случайная» — если хэш может принимать \(n\) различных значений, то вероятность того, что хэши от двух случайных объектов совпадут, равна примерно \(\frac\) .

Обычно хэш-функция не является взаимно однозначной: одному хэшу может соответствовать много объектов. Такие функции называют сюръективными.

Для некоторых задач удобнее работать с хэшами, чем с самими объектами. Пусть даны \(n\) строк длины \(m\) , и нас просят \(q\) раз проверять произвольные две на равенство. Вместо наивной проверки за \(O(q \cdot n \cdot m)\) , мы можем посчитать хэши всех строк, сохранить, и во время ответа на запрос сравнивать два числа, а не две строки.

Применения в реальной жизни

  • Чек-суммы. Простой и быстрый способ проверить целостность большого передаваемого файла — посчитать хэш-функцию на стороне отправителя и на стороне получателя и сравнить.
  • Хэш-таблица. Класс unordered_set из STL можно реализовать так: заведём \(n\) изначально пустых односвязных списков. Возьмем какую-нибудь хэш-функцию \(f\) с областью значений \([0, n)\) . При обработке .insert(x) мы будем добавлять элемент \(x\) в \(f(x)\) -тый список. При ответе на .find(x) мы будем проверять, лежит ли \(x\) -тый элемент в \(f(x)\) -том списке. Благодаря «равномерности» хэш-функции, после \(k\) добавлений ожидаемое количество сравнений будет равно \(\frac\) = \(O(1)\) при правильном выборе \(n\) .
  • Мемоизация. В динамическом программировании нам иногда надо работать с состояниями, которые непонятно как кодировать, чтобы «разгладить» в массив. Пример: шахматные позиции. В таком случае нужно писать динамику рекурсивно и хранить подсчитанные значения в хэш-таблице, а для идентификации состояния использовать его хэш.
  • Проверка на изоморфизм. Если нам нужно проверить, что какие-нибудь сложные структуры (например, деревья) совпадают, то мы можем придумать для них хэш-функцию и сравнивать их хэши аналогично примеру со строками.
  • Криптография. Правильнее и безопаснее хранить хэши паролей в базе данных вместо самих паролей — хэш-функцию нельзя однозначно восстановить.
  • Поиск в многомерных пространствах. Детерминированный поиск ближайшей точки среди \(m\) точек в \(n\) -мерном пространстве быстро не решается. Однако можно придумать хэш-функцию, присваивающую лежащим рядом элементам одинаковые хэши, и делать поиск только среди элементов с тем же хэшом, что у запроса.

Хэшируемые объекты могут быть самыми разными: строки, изображения, графы, шахматные позиции, просто битовые файлы.

Сегодня же мы остановимся на строках.

Полиномиальное хэширование

Лайфхак: пока вы не выучили все детерминированные строковые алгоритмы, научитесь пользоваться хэшами.

Будем считать, что строка — это последовательность чисел от \(1\) до \(m\) (размер алфавита). В C++ char это на самом деле тоже число, поэтому можно вычитать из символов минимальный код и кастовать в число: int x = (int) (c — ‘a’ + 1) .

Определим прямой полиномиальный хэш строки как значение следующего многочлена:

\[ h_f = (s_0 + s_1 k + s_2 k^2 + \ldots + s_n k^n) \mod p \]

Здесь \(k\) — произвольное число больше размера алфавита, а \(p\) — достаточно большой модуль, вообще говоря, не обязательно простой.

Его можно посчитать за линейное время, поддерживая переменную, равную нужной в данный момент степени \(k\) :

 const int k = 31, mod = 1e9+7; string s = "abacabadaba"; long long h = 0, m = 1; for (char c : s)  int x = (int) (c - 'a' + 1); h = (h + m * x) % mod; m = (m * k) % mod; >

Можем ещё определить обратный полиномиальный хэш:

\[ h_b = (s_0 k^n + s_1 k^ + \ldots + s_n) \mod p \]

Его преимущество в том, что можно написать на одну строчку кода меньше:

 long long h = 0; for (char c : s)  int x = (int) (c - 'a' + 1); h = (h * k + x) % mod; >

Автору проще думать об обычных многочленах, поэтому он будет везде использовать прямой полиномиальный хэш и обозначать его просто буквой \(h\) .

Зачем это нужно?

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

Например, если нужно посчитать хэш от конкатенации строк \(a\) и \(b\) (т. е. \(b\) приписали в конец строки \(a\) ), то можно просто хэш \(b\) домножить на \(k^<|a|>\) и сложить с хэшом \(a\) :

Удалить префикс строки можно так:

А суффикс — ещё проще:

В задачах нам часто понадобится домножать \(k\) в какой-то степени, поэтому имеет смысл предпосчитать все нужные степени и сохранить в массиве:

 const int maxn = 1e5+5; int p[maxn]; p[0] = 1; for (int i = 1; i  maxn; i++) p[i] = (p[i-1] * k) % mod;

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

 int h[maxn]; h[0] = 0; // h[k] -- хэш префикса длины k // будем считать, что s это уже последовательность int-ов for (int i = 0; i  n; i++)  h[i+1] = (h[i] + p[i] * s[i]) % mod;

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

Деление по модулю возможно делать только при некоторых k и mod (а именно — при взаимно простых). В любом случае, писать его долго, и мы это делать не хотим.

Для нашей задачи не важно получать именно полиномиальный хэш — главное, чтобы наша функция возвращала одинаковый многочлен от одинаковых подстрок. Вместо приведения к нулевой степени приведём многочлен к какой-нибудь достаточно большой — например, к \(n\) -ной. Так проще — нужно будет домножать, а не делить.

 int hash_substring (int l, int r)  return (h[r+1] - h[l]) * p[n-l] % mod; >

Теперь мы можем просто вызывать эту функцию от двух отрезков и сравнивать числовое значение, отвечая на запрос за \(O(1)\) .

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

Лайфхак. Если взять обратный полиномиальный хэш короткой строки на небольшом алфавите с \(k=10\) , то числовое значение хэша строки будет наглядно соотноситься с самой строкой:

Этим удобно пользоваться при дебаге.

Примеры задач

Количество разных подстрок. Посчитаем хэши от всех подстрок за \(O(n^2)\) и добавим их все в std::set . Чтобы получить ответ, просто вызовем set.size() .

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

Сравнение строк (больше-меньше, а не только равенство). У любых двух строк есть какой-то общий префикс (возможно, пустой). Сделаем бинпоиск по его длине, а дальше сравним два символа, идущие за ним.

Палиндромность подстроки. Можно посчитать два массива — обратные хэши и прямые. Проверка на палиндром будет заключаться в сравнении значений hash_substring() на первом массиве и на втором.

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

Хранение строк в декартовом дереве

Если для вас всё вышеперечисленное тривиально: можно делать много клёвых вещей, если «оборачивать» строки в декартово дерево. В вершине дерева можно хранить символ, а также хэш подстроки, соответствующей её поддереву. Чтобы поддерживать хэш, нужно просто добавить в upd() пересчёт хэша от конкатенации трёх строк — левого сына, своего собственного символа и правого сына.

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

Если нам не нужно обрабатывать запросы вставки и удаления символов, а, например, только изменения, то можно использовать и дерево отрезков вместо декартова.

Вероятность ошибки и почему это всё вообще работает

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

Событие, когда два хэша совпали, а не должны, называется коллизией. Пусть мы решаем задачу определения количества различных подстрок — мы добавляем в set \(O(n^2)\) различных случайных значений в промежутке \([0, m)\) . Понятно, что если произойдет коллизия, то мы какую-то строку не учтем и получим WA. Насколько большим следует делать \(m\) , чтобы не бояться такого?

Выбор констант

Практическое правило: если вам нужно хранить \(n\) различных хэшей, то безопасный модуль — это число порядка \(10 \cdot n^2\) . Обоснование — см. парадокс дней рождений.

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

Можно также брать модуль \(2^\) . У него есть несколько преимуществ:

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

Всё с этим модулем было прекрасно, пока не придумали тест против него. Однако, его добавляют далеко не на все контесты — имейте это в виду.

В выборе же \(k\) ограничения не такие серьезные:

  • Она должна быть чуть больше размера словаря — иначе можно изменить две соседние буквы и получить коллизию.
  • Она должна быть взаимно проста с модулем — иначе в какой-то момент всё может занулиться.

Главное — чтобы значения \(k\) и модуля не знал человек, который генерирует тесты.

Парадокс дней рождений

В группе, состоящей из 23 или более человек, вероятность совпадения дней рождения хотя бы у двух людей превышает 50%.

Более общее утверждение: в мультимножество нужно добавить \(\Theta(\sqrt)\) случайных чисел от 1 до n, чтобы какие-то два совпали.

Первое доказательство (для любителей матана). Пусть \(f(n, d)\) это вероятность того, что в группе из \(n\) человек ни у кого не совпали дни рождения. Будем считать, что дни рождения распределены независимо и равномерно в промежутке от \(1\) до \(d\) .

\[ f(n, d) = (1-\frac) \times (1-\frac) \times . \times (1-\frac) \]

Попытаемся оценить \(f\) :

Из последнего выражения более-менее понятно, что вероятность \(\frac\) достигается при \(n \approx \sqrt\) и в этой точке изменяется очень быстро.

Второе доказательство (для любителей теорвера). Введем \(\frac\) индикаторов — по одному для каждой пары людей \((i, j)\) — каждый будет равен единице, если дни рождения совпали. Ожидание и вероятность каждого индикатора равна \(\frac\) .

Обозначим за \(X\) число совпавших дней рождений. Его ожидание равно сумме ожиданий этих индикаторов, то есть \(\frac \cdot \frac\) .

Отсюда понятно, что если \(d = \Theta(n^2)\) , то ожидание равно константе, а если \(d\) асимптотически больше или меньше, то \(X\) стремится нулю или бесконечности соответственно.

Примечание: формально, из этого явно не следует, что вероятности тоже стремятся к 0 и 1.

Бонус: «мета-задача»

Дана произвольная строка, по которой известным только авторам задачи способом генерируется ответ yes/no. В задаче 100 тестов. У вас есть 20 попыток отослать решение. В качестве фидбэка вам доступны вердикты на каждом тесте. Вердиктов всего два: OK (ответ совпал) и WA. Попытки поделить на ноль, выделить терабайт памяти и подобное тоже считаются как WA.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *