Атака методом перебора MD5 — эффективная многопоточная реализация

Я хочу реализовать многопоточный алгоритм атаки грубой силы MD5 (на С++). Я знаю о таблицах и словарях Rainbow, но я не собираюсь реализовывать самый эффективный взломщик MD5, просто интересуюсь алгоритмом перебора

Проблема в том, как распределить между потоками все варианты паролей всех доступных длин. Например, чтобы восстановить пароль, содержащий только строчные буквы от 4 до 6 символов, нужно перебрать N=26^4+26^5+26^6=321254128 комбинаций (согласно формуле вариации с повторениями, Vnk = n^ к)

Чтобы распределить все перестановки поровну между, например, 8 потоками, мы должны знать каждую (N/8)*t вариацию, где t=(1..7). И обратите внимание, эти варианты имеют разную длину (4,5,6), и варианты из 4-5 символов могут быть отправлены в один и тот же поток с некоторым количеством вариантов из 6 символов.

Кто-нибудь знает, как этот алгоритм реализован в «реальных» приложениях грубой силы? Может быть, какой-то пул потоков?


person Community    schedule 20.08.2014    source источник


Ответы (3)


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

void thread_fn() {
    PASSWORD_BLOCK block;
    while (get_next_password_block(&block) {
        for (PASSWORD password in block) {
            if (verify_password(password)) set_password_found(password);
        }
    }
}

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

get_next_password_block() — это место, где выполняются все блокировки и синхронизация. Эта функция отвечает за отслеживание списка/диапазона паролей, увеличение пароля и т. д.

Зачем использовать PASSWORD_BLOCK, а не один пароль? Что ж, MD5 — очень быстрый алгоритм, поэтому, если мы будем вызывать get_next_password_block() для каждого пароля, то накладные расходы на блокировку/увеличение будут экстремальными. Кроме того, SIMD-инструкции позволяют нам выполнять массовые вычисления MD5 (4 пароля одновременно), поэтому нам нужен быстрый и эффективный способ получить значительную часть паролей, чтобы уменьшить накладные расходы.

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

person Andrey    schedule 20.08.2014
comment
Итак, вы советуете генерировать блок паролей для каждого потока? Разве это не приводит к высокому потреблению памяти при большом количестве выделений? - person ; 20.08.2014
comment
Вы можете (и должны) выделять память только один раз (для каждого потока), а затем повторно использовать ее. Кроме того, для атак грубой силы PASSWORD_BLOCK может быть очень простым и небольшим, например. хранить только начальный пароль и количество паролей (т. е. не хранить все пароли). - person Andrey; 20.08.2014
comment
Разумно, будем считать - person ; 20.08.2014
comment
В любом случае, есть ли способ проверить N паролей на поток? Я думаю о создании потоков для некоторых вариантов низкого порядка, т.е. для пароля N символов создайте отдельный поток для просмотра последних 4 вариантов символов. Например: AAAA____: 1-й поток, AAAB____: 2-й поток, ... - person ; 20.08.2014
comment
Создание потока — дорогостоящая операция, поэтому, если важна производительность, вам следует повторно использовать существующие. Обратите внимание, что вы можете добиться поведения, похожего на то, что вы предлагаете, если get_next_password_block() вернет AAAA____, AAAB____, AAAC____ и т. д. - person Andrey; 20.08.2014
comment
Я нашел лучший способ. Мне нужно просто преобразовать десятичное число вариаций в систему счисления, сгенерированную кодировкой, т.е. для кодировки 'abc': 0 = a, 1 = b, 2 = c, 3 = ba,... Это не требует создания большого количества потоков и перераспределение памяти - person ; 20.08.2014

«Правильный» способ сделать это - иметь пул рабочих (равный количеству ядер ЦП, либо не считая ядер гиперпотока, либо считая их все как «одно») и незаблокированную очередь FIFO, в которую вы отправляете группы из ста тысяч или около того задач. Это дает приемлемый баланс между накладными расходами на синхронизацию и балансировкой нагрузки.
Хитрость заключается в том, чтобы разделить работу на относительно небольшие группы, чтобы время, когда только один поток выполняет последнюю группу, было не слишком долгим (никакого параллелизма!), но в то же время не делайте группы слишком маленькими, чтобы вы были связаны синхронизацией/конкуренцией за шину. MD5 довольно быстр, поэтому от нескольких десятков до сотен тысяч рабочих элементов должно хватить.

Однако, учитывая конкретную проблему, это на самом деле излишество. Слишком сложно.

5-буквенных паролей в 26 раз больше, чем 4-буквенных, 6-буквенных паролей в 26 раз больше, чем 5-буквенных, и так далее. Другими словами, самый длинный пароль имеет значительно наибольшую долю в общем количестве комбинаций. Все 4,5,6-значные комбинации вместе составляют только около 3,9% комбинаций всех 7-значных комбинаций. Другими словами, они совершенно ничтожны. 96% всего времени выполнения находится в пределах 7-значных комбинаций, независимо от того, что вы делаете с остальными. Это еще более экстремально, если вы рассматриваете буквы и цифры или заглавные буквы.

Таким образом, вы можете просто запустить столько потоков, сколько у вас ядер процессора, и запускать все 4-значные комбинации в одном потоке, все 5-значные комбинации в другом и так далее. Это не очень хорошо, но этого достаточно, поскольку никто все равно не заметит разницы.
Затем просто разбейте возможные 7-значные комбинации на num_thread диапазонов одинакового размера, и пусть каждый поток, закончивший свой начальный диапазон, продолжит работу с этим. один.
Работа не всегда будет идеально сбалансирована, но так будет в течение 96% времени выполнения. И он работает с абсолютным минимумом управления задачами (нет) и синхронизации (нужно просто установить глобальный флаг для выхода при обнаружении совпадения).

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

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

person Damon    schedule 20.08.2014

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

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

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

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

void check_from(std::string pass) {
    check_password(pass);
    if(pass.size() < MAX_SIZE)
        cilk_for(int i = 0; i < syms; i++)
            check_from(pass+sym[i]);
}
person Anton    schedule 20.08.2014