Цель: взломать шифр Хилла
Доброго времени суток, уважаемые читатели! Сегодня я хотел поделиться способом, который помог мне вскрыть текст, зашифрованный методом Хилла. Что такое метод Хилла описывать не буду: до меня уже постарались опытные умельцы донести особенности данного способа. Ссылка на пост.
Что имеем?
Скажу сразу, что на руках не имелось ни открытого текста, ни ключа. Было известно, что текст длинной 6286 символов был зашифрован матрицей 7 х 7. Поэтому для нашего же удобства, мы разобьем текст на 898 строчек по 7 символов. В тексте не содержатся буквы 'ё' и 'ъ'. В целях благоразумства, я не буду приводить весь зашифрованный текст, а лишь его часть:
тчгяцмекещэнлжсвтйджтчгсмнежздыщзяотьдрпюмимаадрх...
На вид бессмысленная ерунда, пока что…
Как будем ломать?
Рассмотрим атаку «грубой силой». Выше было оговорено, что из алфавита исключены две буквы, поэтому все линейные комбинации при шифровании (как и при дешифровке) берутся по mod 31 (учитывая, что это простое число, текст становится чуть более безопасным).
Если рассматривать перебор обратных матриц-ключей, то всего нам придется перебрать
К ак быть?
Предлагаю слегка «смягчить» нашу атаку и воспользовать
- Рассчитывается «критическая» точка
, где N — длина текста;
- Берется случайный текст, рассчитывается
, где F — сколько раз встречается буква в тексте;
- Узнаем, насколько лучше
аппроксимирует
.
Скорее вы подметите, что тут ничего нового-то и нет. Всё тоже самое можно проделать при взломе шифра Виженёра. Однако, это нам позволит в программе использовать только целочисленные типы, засчёт чего производительность пересчёта только ускорится.
Начинаем ломать...
Сразу оговорюсь: да, я
Скажу, что для нашего текста
int chast[898]; /*Номера букв исходного текста*/
int mass[31]; /*Массив количества каждой буквы*/
int c1;
int c2;
int c3;
register __int32 PHI_O; /*PHI(O)*/
for (int k1 = 0; k1 < 4; k1++) {
/*Здесь вложенные циклы for ...*/
for (int k7 = 0; k7 < 31; k7++) {
/*При новой комбинации ключа очищаем массив и PHI(o) */
memset(mass, 0, sizeof(mass));
PHI_O = 0;
/*Сгенерированный ключ применяем к зашифрованном тексту*/
for (int i = 0; i < 898; i++)
{
c1 = k1 * sym[7 * i] + k2 * sym[7 * i + 1];
c2 = k3 * sym[7 * i + 2] + k4 * sym[7 * i + 3];
c3 = k5 * sym[7 * i + 4] + k6 * sym[7 * i + 5] + k7* sym[7 * i + 6];
chast[i] = (c1 + c2 + c3) % 31; /*Получаем какой-то текст*/
mass[chast[i]]++; /*Сразу кладём в подсчет буквы*/
}
for (int i = 0; i < 31; i++)
PHI_O += mass[i] * (mass[i] - 1);
if(PHI_O > 39000)
{
cout << PHI_O << '\n';
printf("%d %d %d %d %d %d %d\n", k1, k2, k3, k4, k5, k6, k7);
}
}
/*Куча закрывающихся скобок от вложенных циклов */
}
Таким образом один поток перебирает
Данный ряд букв полностью удовлетворял критерию фи-теста, но частотность букв не являлась правдоподобной. Иными словами буква «в» могла встретиться так же часто, как в натуральном тексте встречается буква «о», а «о» могла и вовсе не встретиться, но при этом свойство ИС сохранялось. Мне не удалось дать этому математическое объяснение. Я исключаю, что это случайное совпадение. Итак, нам пришлось
- 3 16 5 24 17 20 25
- 6 30 5 1 13 12 3
- 7 11 18 12 17 27 15
- 12 17 0 15 5 16 1
- 15 19 27 14 21 10 2
- 25 14 27 20 22 21 22
- 29 3 1 10 30 28 26
Осталось-то всего ничего… Расположить их в нужной последовательности. Для этого мне пришлось создать матрицу-ключ, с помощью функции перестановки элементов менять между собой индексы строчек (т.е. перестановка шла только между индексами) и применять полученную матрицу к зашифрованному тексту, все это дело я выводил в текстовый файл (чудно) и пытался зацепиться хоть за какой-то осмысленный текст. Ищем… ищем… стоп! Кажется что-то получилось! Да!

Кстати найти среди 7! строк осмысленный текст очень легко, займет не больше 2 минут. Кажется, это всё!
Выводы
Вычисления на CPU на 8 потоках заняли у меня 10 часов. ЭВМ пыхтела как паровоз. Столкнулся с трудностями не раз. Во-первых, были кучи ошибок внутри кода, которые спустя 10 часов давали неверный результат. Во-вторых, было сложно подобрать подходящий критерий. В-третьих, программа не давалась хорошей оптимизации (в этой статье я привёл кусок неоптимизированного кода). Что касается теоретической стороны вопроса: Разделяй и властвуй.
Данная статья (по указанной ссылке) обязательна к прочтению всем, кто интересуется классическим криптоанализом. Техника отлично подходит для небольших текстов, когда широкодушно не применишь свои навыки частотного анализа. Метод «жестко» рекурсивен в том смысле, что сильно зависит от всех предыдущих результатов. По этой причине его невозможно распараллелить (кстати, его и не оптимизировать при всём желании), но метод динамичен и должен привести к верному результату (в теории).
Что же на практике? CUDA
Это всё! Надеюсь моя статья будет хоть чуточку полезной для вас! Спасибо за внимание.