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

Некриптографические хеш-функции


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

Критерии, которые важны для некриптографических хеш-функций:

  • Выходные значения должны иметь равномерное распределение (uniform distribution). Этот критерий важен для построения хеш-таблиц, чтобы скорость поиска была оптимальна.
  • Каждый входной бит должен в 50% случаев влиять на каждый выходной бит (avalanche — лавинный критерий). Этот критерий важен для использования хеша в качестве уникального идентификатора документа. Также, иногда хеш может иметь равномерное распределение, но проваливать лавинный критерий — в этом случае будут ключи, для которых хеш не будет давать равномерное распределение.
  • Между парами бит на выходе не должно быть корреляции. Это рассматривать не будем, т.к. обычно с этим критерием проблемы нет.
  • Скорость. Все перечисленные хеши на порядок быстрее, чем криптографические.

Поскольку обычно пространство входных значений больше пространства выходных значений, то коллизии неизбежны. У идеальной функции выходные значения имеют равномерное распределение, поэтому вероятность коллизий минимальная. Это можно проверить хи-квадрат тестом (χ²).

FNV-1a

2003 год. Скорость: 4.5 циклов на байт.
Очень простая функция [1] (RFC [5]).
1. Равномерное распределение: плохое, может использоваться до 14 бит для хеш-таблицы (до 214 слотов хеш-таблицы). Чуть-чуть лучше в вариации FNV-1a, которая рекомендуется по умолчанию.
2. Лавинный критерий: плохо. Также плохо и в FNV-1a.
Оба критерия значительно улучшены в модифицированном FNV-1a [2], где к FNV-1a добавлены перемешивания в конце.

Bob Jenkins' Hash (lookup3)

2006 год, длина 32 и 64 бита, 32-битные инструкции, скорость: 2.62 цикла на байт (для сообщений длиной 16 байт).
1. Равномерное распределение: отличное для всех бит.
2. Лавинный критерий: хорошо. Есть небольшая проблема с верхними битами.
Первый вариант функции назывался One-at-a-Time.

CRC32

1975 год, длина: 32, 64 и 128 бит, скорость: 1.2–0.13 цикла на байт (для сообщений короче 64 бит, в зависимости от CPU).
1. Равномерное распределение: плохое распределение нижних бит. Используйте верхние биты для хеш-таблицы, тогда можно использовать все протестированные биты — до 16 верхних бит (до 216 слотов хеш-таблицы).
2. Лавинный критерий: очень плохо. Лавинный эффект отсутствует. Сказывается назначение хеша: его выходные биты спроектированы так, чтобы обнаруживать ошибки в канале передачи данных. Также, у CRC есть важное свойство: можно разбить сообщение на части, посчитать хеш для каждой части, потом объединить сообщение, и посчитать хеш всего сообщения из хешей его частей.

MurmurHash 2

2008 год, длина 32 и 64 бита, 32 и 64-битные инструкции.
Есть проблема с равномерностью распределения некоторых ключей [3].

MurmurHash 3

2011 год, длина 32 и 128 бит, 32 и 64-битные инструкции, скорость: 1.87 циклов на байт (для сообщений длиной 16 байт).
1. Равномерное распределение: хорошее (в одном случае из трёх «почти подозрительное»).
2. Лавинный критерий: отлично.



CityHash

2011 год, длина 32, 64, 128 и 256 бит, 64-битные ��нструкции. Достаточно сложная функция.
Для самого быстрого варианта нужны CRC32 инструкции процессора (SSE 4.2 — Intel Nehalem).
1. Равномерное распределение: хорошее (в одном случае из трёх «почти подозрительное»).
2. Лавинный критерий: хорошо. Есть небольшая проблема с нижними битами.

SpookyHash

2011 год, длина 32, 64 и 128 бит, скорость: 0.33 цикла на байт. Автор Bob Jenkins.
1. Равномерное распределение: среднее (в одном случае из трёх «подозрительное»).
2. Лавинный критерий: отлично.

Статистический анализ хеш-функций можно сделать при помощи инструментов [17], использовались результаты анализа [2], [4], [18].

CityHash более сложный и имеет бо́льшую пропускную способность (скорость на длинных сообщениях), чем MurmurHash. MurmurHash более простой, имеет низкую задержку для коротких сообщений (< 20 байт).

CityHash использует инструкции CRC у новых процессоров, обеспечивая до 0.17 циклов на байт, что быстрее всех остальных хешей; для сравнения, SHA-1 — 4.6 циклов на байт (Ivy Bridge). Без поддержки этих инструкций CityHash в два раза медленнее SpookyHash (по мнению автора последнего).

Для 32-битной платформы MurmurHash 3 может быть быстрее, чем SpookyHash и CityHash.

Идеальная хеш-функция

Если массив сообщений заранее известен, то можно использовать идеальную хеш-функцию: без коллизий и с поиском в одну операцию сравнения. Пример: gperf.

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

  • для 32 бит: с вероятностью 1 к 10 000 будет коллизия среди 927 хешей, с вероятностью 1 к 100 будет коллизия среди 9 292 хешей,
  • для 64 бит: с вероятностью 1 к 10 000 будет коллизия среди 60.7 миллионов хешей, с вероятностью 1 к 100 будет коллизия среди 609 миллионов хешей.



На сколько реальны коллизии? Для 32-битных хешей они встречаются регулярно. Например, CRC32 совпадает у «codding» и «gnu», «exhibiters» и «schlager», «завиток» и «естествен» и т.д. Это не вина статистических особенностей хеш-функции, а следствие ограниченного размера хеша — всего 4 байта.

Все перечисленные функции, кроме CRC32 и FNV-1, имеют хорошие статистические показатели. Для 32-битной системы, возможно, быстрее всех будет работать MurmurHash. Если система не может делать невыровненное чтение (unaligned memory read), например, ARM или SPARC, то MurmurHash, в��зможно, также будет работать быстрее всех. Скорость CityHash сильно зависит от компилятора, поскольку используется много сложного кода, она может быть как больше, так и меньше остальных.

Рекомендации

В качестве хеш-функции общего назначения можно применять MurmurHash 3, CityHash, SpookyHash V2, gperf.

DoS-атака на хеш-таблицу


Можно представить DoS атаку, когда атакующий скармливает в хеш-таблицу данные так, что они попадают все в один слот, и получается худшая скорость поиска одного элемента — O(n), вставки каждой новой коллизии — O(n²), растёт объём памяти (hash-flooding). Для осуществления атаки нужно иметь возможность помещать произвольные данные в хеш-таблицу, и найти коллизии хеш-функции.
Атака ещё называется hash DoS или, более обще, атака на алгоритмическую сложность (algorithmic complexity attack).

Коллизии можно создать несколькими способами:

  • если хеш-функция заранее известна и длина хеша невелика, то использовать метод перебора: вычисление и поиск в памяти meet-in-the-middle.
  • если хеш-функция не стойка к коллизиям, то коллизии можно вычислить математически. Некриптографические функции достаточно просты, и для поиска коллизии часто достаточно исполнить алгоритм в обратном порядке ([6], oCERT [7]).

Перечислим свойства криптографических хеш-функций (неприменимо к хеш-функциям общего назначения):

  • стойкость к восстановлению прообраза (дано m, нельзя найти X так, что H(X) = m),
  • стойкость к восстановлению второго прообраза (дано m1, нельзя найти m2 так, что H(m1) = H(m2)),
  • стойкость к поиску коллизий (нельзя найти m1 и m2 так, что H(m1) = H(m2)).

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

Обычно самая «лёгкая» атака — это поиск коллизий. Из-за парадокса «дней рождений» сложность этой атаки как минимум в два раза меньше, чем длина хеша. Именно эта атака существует для SHA-1: при теоретической сложности коллизии O(280), атака уменьшает сложность до O(261). Атаки восстановления первого или второго прообраза для SHA-1 нет.

Чтобы увеличить стойкость к коллизиям, можно:

  • использовать криптографическую хеш-функцию для вычисления хеша достаточной длины. Она будет стойка к коллизиям, но если коллизии будут однажды вычислены, они смогут быть применены для атаки любой системы.
  • использовать HMAC (MAC на базе криптографической хеш-функции).
  • использовать MAC на базе PRF или универсальной хеш-функции (universal one-way hash function).

MAC, построенный на базе криптографической функции, имеет следующие свойства:

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

Если мы решим применять криптографическую хеш-функцию (например, SHA-3) или MAC на её основе, то мы столкнёмся со следующими недостатками:

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

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

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

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

Поэтому, хотя этот подход может усложнить жизнь случайному атакующему, часто он не выдерживает простейшего криптоанализа (oCERT [8]). Было показано, что большинство хеш-функций общего назначения не стойки к криптографическим атакам:

  • xxHash не стойкий к восстановлению второго прообраза даже без знания ключа, зная хеш [12].
  • MurmurHash 2, MurmurHash 3, CityHash 1.0.3 не стойкие к коллизиям без знания ключа и хеша: был найден способ быстро находить коллизии, которые не зависят от выбора seed («universal» multicollisions) [13].
  • Python hash не стойкий к восстановлению ключа, зная результат хеш-функции, после чего можно провести атаку против 64-битного состояния с использованием meet-in-the-middle [13].
  • Python hash не стойкий к коллизиям без знания ключа и хеша [14].

До 2008-2010 года большинство языков и фреймворков использовало достаточно простые хеш-функции:

  • PHP 4, ASP.NET и JavaScript: DJBX33X,
  • PHP 5, Ruby 1.8 и Java: DJBX33A,
  • Rust: DJB (по 2012 год),
  • Python: модифицированный FNV,
  • JRuby: MurmurHash2,
  • Haskell: FNV-1,
  • Redis: djbhash, позже MurmurHash 2.

Исключение: Perl 4, использовал One-at-a-Time со случайным seed с 2003 года (в этом году обсуждали на конференции эту атаку), позже (5.17.6) использовал MurmurHash 3.

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

Пример MAC, стойких к коллизиям:

  • VMAC, UMAC, Poly1305-AES — используют AES-128, оптимальны для длинных сообщений.
  • SipHash — является PRF, не использует криптографических алгоритмов, оптимальна для коротких сообщений.
  • “Strongly universal string hashing is fast” [10].

SipHash

2012 год, длина 64 бита, ключ 128 бит.
Скорость: 15.50 циклов на байт для 8 байт сообщения, 3.66 циклов на байт для 64 байт сообщения, до 1.96 циклов на байт для длинных сообщений. Для сравнения наиболее быстрый финалист SHA-3 BLAKE-512: 134 цикла на байт для 8 байт, 20 циклов на байт для 64 байт.
Можно использовать любое число раундов compression и finalization. Например, SipHash-2-4 — 2 и 4 раунда соответственно.

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



Ситуация по языкам на текущий момент выглядит так:

  • Rust: SipHash 2-4,
  • Rails: SipHash 2-4,
  • Perl (5.8.1): SipHash 2-4 (64 бит) и One-at-a-time (32 бит),
  • Ruby (1.9.3-p327), JRuby: SipHash 2-4,
  • Python (2.7.3, 3.2.3): собственная хеш-функция, при включении -R добавляется случайный seed [11],
  • Haskell (1.2): SipHash (для строк),
  • Go: aeshash (при поддержке AES-NI) или простая собственная функция (вариация FNV-1),
  • PHP (5): DJBX33A, есть ограничение на количество элементов GET/POST (max_input_vars),
  • Java SE (7u6 и 8): при включении jdk.map.althashing.threshold используется MurmurHash со случайным seed (alternative string hashing),
  • Scala: Byteswap32 и MurmurHash 3 или hash от Java,
  • .NET (4.5): при включении UseRandomizedStringHashAlgorithm используется собственная разработка Marvin32 [9].

Открытые задачи:

  • Java: нет реакции на криптоанализ MurmurHash [16],
  • Python: признали, что ключ «-R» бесполезен, и размышляют, как лучше всего исправить атаку на хеш-таблицу [14]
  • Go: реализовали aeshash, но пока нет криптоанализа [15].

Другие методы защиты

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

  • использовать структуры данных с меньшей сложностью в худшем случае. Например, сбалансированное дерево — АВЛ-дерево или красно-чёрное дерево — гарантирует в любом случае O(log n). Здесь важно учитывать новые атаки, например, каскадный ребаланс дерева специально сформированными данными, но даже в этом случае мы получим лучше результат — O(n log n),
  • ограничить число обрабатываемых элементов (например, POST/GET),
  • поднимать исключение, если число коллизий больше лимита (или просто отбрасывать данные, если это возможно),
  • поднимать исключение, если CPU занято больше нормы на операции поиска (timeout).

Рекомендации

Для структур данных, в которых используются недоверенные данные, применять алгоритмы со сложностью O(log n), например, сбалансированное дерево. Если нет возможности, то использовать хеш-функцию, случайно выбираемую из семейства функций (MAC), например, VMAC или SipHash.

Источники
[1] www.isthe.com/chongo/tech/comp/fnv/index.html
[2] Pluto Scarab — Hash Functions
[3] code.google.com/p/smhasher/wiki/MurmurHash2Flaw
[4] «Choosing a Good Hash Function, Part 3 – AK Tech Blog» — blog.aggregateknowledge.com/2012/02/02/choosing-a-good-hash-function-part-3
[5] tools.ietf.org/html/draft-eastlake-fnv-05
[6] www.ocert.org/advisories/ocert-2011-003.html
[7] www.nruns.com/_downloads/advisory28122011.pdf
[8] www.ocert.org/advisories/ocert-2012-001.html
[9] github.com/floodyberry/Marvin32/blob/master/Marvin32.c
[10] «Strongly universal string hashing is fast» — arxiv.org/abs/1202.4961
[11] «Issue 13703: Hash collision security issue» — bugs.python.org/issue13703
[12] crypto.stackexchange.com/questions/6408/from-hash-to-cryptographic-hash
[13] 131002.net/siphash/#at
[14] «Issue 14621: Hash function is not randomized properly» — bugs.python.org/issue14621
[15] «Issue 4604 — go — runtime: switch to a fast, collision-resistant hash function» — code.google.com/p/go/issues/detail?id=4604
[16] «Bug 880705 – CVE-2012-5373 java: Murmur hash function collisions (oCERT-2012-001)» — bugzilla.redhat.com/show_bug.cgi?id=880705
[17] code.google.com/p/smhasher/wiki/SMHasher
[18] floodyberry.com/noncryptohashzoo