Сервис по реверсу MD5

258
10 января 2018, 15:37

Передо мной встала задача - реверс md5 хеша 4 произвольных байт. То есть, другими словами, мне надо написать сервис из 1 метода - метод получает 16 байт хеша на входе и возвращает 4 байта на выходе.

Поскольку, насколько я помню, реверс хеша сам по себе неосуществим, я решил пойти другим путем и сгеренировать хеши для всех возможных значений. Я выделил сущность, назовем её запись, которая состоит из 16 байт хеша (назовем ключ) и 4 байт значения. Поскольку значений 4 байт ~4млд, то у меня получается 4млд записей, каждая размером 20 байт - это 80 гигабайт данных.

В качестве первого приближения я сгенерировал 80гиг, разбитых на сортированные отрезки (каждый отрезок - отдельный файл) и реализовал слияние файлов по принципу сортировки слиянием. Таким образом в итоге я получу один большой 80 гиговый файл, где будет 4 млд записей, сортированных по ключу. Я планирую по такому большому файлу создать второй файл - индексный, который запросто влезет в оперативку. После этого поиск записи по ключу будет выглядеть как двоичный поиск в большом файле с небольшой помощью индексного файла. Время поиска в общем случае должно быть логарифмическим, ограниченным максимумом в 32 прыжка.

Плюсы такого решения:

  • простота развертывания. Просто задеплоил сервис, дождался инициализации, и все работает.

Минусы:

  • Генерирование файла занимает очень много времени. Время измеряется часами.
  • Генерируемые данные занимают очень много места - 80 гиг

Исходя из минусов, у меня возникло несколько идей:

  • Первая, конечно, хранить данные в БД. Они там сразу индексирутся и сжимаются. Я пробовал MongoDB и SQL Server, однако скорость вставки и там и там была настолько низкая, что я устал ждать, и оно явно было медленней, чем простая генерация в файл. То есть я и так ради 1 сервиса БД ставить не хотел, и выходит, что это бы мне и не помогло особо.

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

Мне довольно сложно побрать максимально конкретный вопрос в моей ситуации, но я постараюсь: Если ли более общепринятые способы решения подобной задачи? У меня стойкое ощущение, что я делаю что то не так.

Answer 1

Оценим минимальный размер возможного индекса. При любом способе индексации ключ должен вывести нас на 4 байта значения. Для хранения самих значений необходимо 16 ГБайт. Учитывая, что в индексе значения расположены в порядке следования ключей то можно считать их набором случайных данных с полным использованием всех возможных бит. Без потерь сжать такие данные не способен ни один алгоритм.

И так, минимальный индекс составляет 16 ГБайт. Но искать по ним пока не возможно, так как нужен способ быстро найти нужную ячейку этого массива. Учитывая, что MD5 дает хорошее распределение, и обладает большим лавинным эффектом, можно утверждать, что первые 4 байта хеша почти однозначно должны приводить нас к 4 байтам значения. В то же время вычисление функции MD5 на таком небольшом объеме происходит очень быстро. значит нам для поиска достаточно найти от 0 до N значений подходящих для данных 4х первых байт хеша и вычислить их MD5 для окончательной сверки. Причем значение N очень мало.

Предлагаю использовать следующую структуру индекса: создаем 2 файла. В первом (16 Гб) лежат значения отсортированные в порядке первых 4-х байт их хеша, назовем его словарь. Во втором, в порядке ключей лежат 20 байтные блоки, по одному на каждые 16 ключей, в каждом из которых храниться 4 байта - номер стартового элемента в словаре и 16 байт - количество элементов в списке значений для каждого ключа (в предположении что на один ключ не может быть более 255 значений в словаре). Данный файл назовем индекс. При таком подходе размер индексного файла составит 5 гигабайт.

Для поиска берем первые 4 байта хеша (ключ), сбрасываем 4 младшие бита, умножаем полученное значение на 20 и по полученному смещению считываем 20 байт из индекса. Берем младшие 4 бита ключа (k) и убеждаемся, что k элемент в списке длин не 0 (нет такого хеша). После этого к базовому смещению из начала блока добавляем по очереди с 0 по k-1 элемент списка длин (до 16 операций сложения). По полученному смещению (номер элемента*4) получаем список значений в количестве k. Для каждого значения вычисляем MD5 и сравниваем с искомым.

Если по статистике на один ключ приходится не более 15 значений индекс можно еще сократить, храня по 4 бита в списке длин. Тогда объем индексного файла составит 3 Гбайта. Возможно его можно еще сократить, уменьшив длину хранимого базового смещения до 3х, а может даже до 2х байт (храня не номер элемента, а его относительную позицию (со знаком), относительно "идеальной" (если бы на каждый ключ было бы строго 1 значение. Сокращать сильнее список длин смысла не вижу, выиграем еще максимум 1 байт но битовые преобразования значительно усложняться. Так же можно пересмотреть количество ключей, индексируемых одним блоком индекса. Это несколько сократит хранимый объем, но увеличит количество потенциальных операций сложения при поиске элемента.

При предложенном способе поиска хранить в оперативной памяти какие либо индексы не обязательно. Для поиска нам понадобится выполнить 2 операции ввода-вывода (получить с диска блок индекса и блок словаря) и провести немного вычислений: пара сдвигов и and/or, до 16 сложений и N операций MD5, где N обычно не превышает 10.

Для уменьшения объема индекса или уменьшения объема необходимых расчётов мы можем выбрать другую длину ключа, отличную от 4х байт. Уменьшение длины ключа на 1 бит даст 2х кратное уменьшение объема индекса (но в какой то момент придется увеличивать размер ячеек под количества), но в то же время потребует в среднем в 2 раза больше операций MD5 при поиске. При дальнейшем увеличении длины ключа довольно скоро мы придем к ситуации, что на каждый ключ у нас будет приходится не более 1 значения, в этом случае нам фактически будет не нужно делать отдельно словарь и индекс и можно будет держать значения по определенным ключом смещениям в индексе. Необходимый объем памяти для этого подхода надо прикинуть, собрав более детальную статистику и поняв необходимую длину ключа для достижения эффекта однозначного соответствия

Дополнение: способ номер 2

Индекс вообще не нужен !!! Нам нужен только 16 Гб словарь, в котором будут лежать значения, отсортированные в порядке следования их MD5-хешей. При поиске мы на основе первых N бит хеша прикидываем примерный регион файла, в котором должно находиться искомое значение. Учитывая почти однозначное соответствие даже 32 битных ключей это даст достаточно точное попадание. Далее мы вычисляем MD5 от найденного значения и сверяем с искомым. И двигаемся прыжками в том или ином направлении по региону словаря, фактически двоичным поиском, но не по хранимым данным, а по вычисляемым на лету MD5 суммам. Так же можно комбинировать первый и второй способ, сделав небольшой индекс указывающий искомый регион, для ограничения двоичного поиска сразу в нем и сильного уменьшения количества необходимых "прыжков"

Answer 2

Один из вариантов: Храним файлы каталогах со следующей структурой: (первые 3 символа hex(md5))/(следующие 2 символа hex(md5)).db Таким образом папок будет 4096, файлов в папках по 256. Всего 1048576 нод. При таких параметрах файловая система тормозить не будет. Файл будет содержать в среднем 4096 хеша. Сами файл - записи из 16 байт хеша + 4 байта - искомые, расположеные друг за другом. При первоначальной генерации файлы просто дополняются записями, потом записи в каждом файле нужно будет отсортировать по хешу. Таким образом можно будет за минимальное время дойти до нужного файла и выполнить бинарный поиск, занимающий не больше чем 12 итераций открытия файла и чтения 20 байт по смещению.

Answer 3

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

В соответствии с идеей Mike берем у каждого хеша первые 2 байта - и именно в таком виде записываем в файл. Файл можно организовать двухуровневым способом: в начале файла таблица из 65 536 ячеек (каждая ячейка соответствует некоторой комбинации первых двух байт), в каждой ячейке - смещение по которому записаны данные и количество попаданий в эту ячейку. Если брать по 8 байт на каждое значение - то всего эта таблица займет будет 1 МБ, что довольно немного.

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

Во второй части файла будут записаны все данные хеши которых привели в эту ячейку. Их будет в сумме 16 ГБ.

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

Кстати, на 64х-битной системе можно использовать отображенный на память файл для ускорения работы. Но в 32х-битных системах преимущество такого подхода теряется: вы не сможете отобразить файл в память целиком.

READ ALSO
Вернуть исходную позицию анимации

Вернуть исходную позицию анимации

Есть объект, который меняет свой scale в анимации, после animationStop(); анимация останавливается с тем размером, с которым в этот момент проигрывалась,...

240
Домены приложений, время жизни

Домены приложений, время жизни

Рихтер пишет, что так как у типов в другом домене нету корней, то в CLR пошли на хитрость: прокси объекты живут ~5 минут с момента последнего обращения,...

229
Дополнительные ряды в InlineKeyboardButton в Telegram Bot

Дополнительные ряды в InlineKeyboardButton в Telegram Bot

Есть код который создает разметку клавиатуры в два рядаЧто-то такое:

245