Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Практическая криптография алгоритмы и их программирование

.pdf
Скачиваний:
0
Добавлен:
08.09.2026
Размер:
2 Мб
Скачать
Глава 3. Дешифрование классических шифров 41
Листинг 3.13
#include <stdio.h> #include <stdlib.h> #include "unidef.h"
// биграммы typedef struct {
long values[40][40]; long total;
} BIGRAMM;
#include "etalon.h" #include "f_etalon.h" BIGRAMM current; long freq[40];
// подсчет биграмм void calc_gramm(unsigned char *buf, int len) {
for(inti=0;i<len-1; i++) {
current.values[ buf[i]-0xC0 ][ buf[i+1]-0xC0 ]++; current.total++;
}
}
// подсчет монограмм void calc_freq(unsigned char *buf, int len) {
for(inti=0;i<len; i++) {
freq[ buf[i]-0xC0 ]++;
}
}
int main(int argc, char* argv[]) {
FILE *f = fopen("collect.txt", "rb"); char buf[32768]; while (!feof(f)) {
int r = fread(buf, 1, sizeof(buf), f); for(inti=0;i<r;i++) if (buf[i] < 'À' || buf[i] > 'ß')
buf[i] = 0xE0;
calc_gramm(buf, r);
calc_freq(buf, r); } fclose(f);
f = fopen("f_etalon.h", "wt"); fprintf(f, "long etalon[] = {\n"); for(inti=0;i<40;i++)
fprintf(f, "%d, ", freq[i]); fprintf(f, "};\n"); fclose(f);
f = fopen("etalon.h", "wt"); fprintf(f, "BIGRAMM etalon = {\n"); for(inti=0;i<40;i++) {
for(intj=0;j<40;j++)
42 Глава 3. Дешифрование классических шифров
fprintf(f, "%d, ", current.values[i][j]);
fprintf(f, "\n"); } fprintf(f, "%d };\n", current.total); fclose(f); return 0;
}
Создав небольшой словарь из наиболее вероятных слов, которые могли бы встретиться в открытом тексте, мы становимся обладателями критерия, по кото­рому можем определить, насколько вероятен тот факт, что дешифрованное с за­данным нами ключом сообщение действительно является искомым открытым тек­стом. Собственно говоря, процесс криптоанализа заключается в том, чтобы сокра­тить работу человека и переложить ее на хрупкие плечи машины.
Реализуем алгоритм шифрования простой заменой (см. листинг 3.14) и по­пробуем применить к шифротексту первый критерий — проверку по словарю с помощью программы, реализованной в листинге 3.15.
Листинг 3.14
#include <stdio.h> #include <stdlib.h> #include "unidef.h"
// шифр простой замены void encrypt(unsigned char *buf, int len, char *key) {
for(inti=0;i<len; i++)
if (buf[i] >= 0xC0 && buf[i] <= 0xE0) buf[i] = key[buf[i]-0xC0];
}
void decrypt(unsigned char *buf, int len, char *key) {
for(inti=0;i<len; i++)
if (buf[i] >= 0xC0 && buf[i] <= 0xE0) buf[i] = unkey[buf[i]-0xC0];
}
Мы используем модуль unidef.h в каждой из программ, чтобы обеспечить уни­версальность подхода к обработке данных. В реализациях криптосистем универ­сальность обозначает верность реализации и отсутствие трудноуловимых логиче­ских ошибок в проектировании. Например, реализация RC4 на языке Cи с исполь­зованием типа char вместо unsigned char на одном компиляторе производит одну последовательность данных, а на компиляторе другого производителя — уже дру­гую. Естественно полагать, что и практическая криптостойкость у хорошего, но неверно реализованного алгоритма оставляет ждать лучшего.
Листинг 3.15
// осуществляем символьную замену void make_subst(char *buf, int len, int from, int to) {
for(inti=0;i<len; i++) if (buf[i] == from) buf[i] = to;
}
// автоматический анализ
Глава 3. Дешифрование классических шифров 43
void bruteforce_dict(char *buf, int len) {
// таблица временных замен
int sub[256], i;
// буфер для испытаний
char *temp = (char *) malloc(len);
// пройдемся в цикле по всему тексту
for (i = 0; i < len; i++) { // делаем локальную копию для испытаний
memcpy(temp, buf, len); // по всем словам из словаря
for(intw=0;w<dict_n; w++) {
// не рассматриваем слова, которые уже // не помещаются в границы текста if (i + strlen(dict[w]) >= len) continue; memset(sub, 0, sizeof(sub)); // по всем буквам текущего слова из словаря for(intk=0;k<strlen(dict[w]); k++) {
int from = temp[i+k];
int to = from - dict[w][k]; // сохраняем замену sub[from] = to;
} // осуществляем временную замену символов for (i = 0; i < 256; i++)
if (sub[i]) make_subst(temp, len, i, sub[i]);
// тестируем текущую временную замену test_subst(temp, len);
}
}
}
Алгоритм проверки по словарю (функция bruteforce_dict в листинге 3.15) сво­дится к проверке предположения о том, что в некоторой фиксированной позиции (на самом деле проверять следует начиная с первого и заканчивая последним сим­волом шифротекста) открытого текста перед зашифрованием находилось слово из заготовленного нами заранее словаря. Словарные элементы в листинге помещены в массив указателей на строки (char *) dict[] — каждая строка представляет со­бой словарное слово, а их количество задано переменно dict_n.
Вычитая слово из шифротекста, который нам необходимо раскрыть и о кото­ром сделали предположение, что он получен зашифрованием простой заменой, мы должны получить в каждой позиции символ, соответствующий номеру сдвига алфавита в случае, если это шифр Цезаря (как частный случай шифра замены) или целый набор замен (в более общем случае). Ориентируясь на общий случай, мы поступаем следующим образом.
Посчитав эти вычисленные замены временными и произведя их, подсчитыва-
7
ем частоты (по осуществленным заменам, конечно),
ищем запрещенные биграм­мы, триграммы или диграммы (с помощью функции test_subst). Дополнительно можем в расшифрованном тексте искать слова небольшой длины из нашего же словаря. Если хотя бы одно такое слово будет найдено, это будет означать доста­точно высокую вероятность верной замены.
44 Глава 3. Дешифрование классических шифров
Если запрещенных сочетаний символов не было найдено, программа дол­жна выдать найденную замену криптоаналитику. Аккумулируя такие «успеш­ные» замены, программа выдаст в конце криптоаналитику список возможных замен, корректных с ее точки зрения. Реализация проверки временных замен и принятия решения о необходимости анализа человеком представлена в лис­тинге 3.16.
Листинг 3.16
// вычисляем биграммы void calc_gramm(unsigned char *buf, int len) {
for(inti=0;i<len-1; i++) {
current.values[ buf[i]-0xC0 ][ buf[i+1]-0xC0 ]++; current.total++;
}
}
void calc_freq(unsigned char *buf, int len) {
for(inti=0;i<len; i++) {
freq[ buf[i]-0xC0 ]++;
}
}
// вычисляем "расстояние" от эталонных до полученных // экспериментально данных double calc_measure() {
double result = 0;
if (!current.total) current.total++;
for(inti=0;i<40;i++)
for(intj=0;j<40;j++)
result += fabs(((double) current.values[i][j]/current.total)
- ((double) etalon.values[i][j]/etalon.total));
return result;
}
// проверяем временные замены bool test_subst(char *buf, int len) {
// проведем проверку на запрещенные биграммы uint16_t *ofs = (uint16_t *) buf; // используем указатель для ускорения доступа к тексту for(intk=0;k<len-1; k++)
for(inti=0;i<deny_n; i++) {
if (*ofs == deny[i]) return false; // сдвигаем не на два байта (как слово) // а на один байт (как переменную в полслова = байт) ((uint8_t *) ofs)++;
7
Мы не можем подсчитывать частотное распределение по всем символам, поскольку не проводим замены всего алфавита, а только лишь его части. Мы можем учитывать лишь симво лы в тех местах, где такая замена нами проводилась. Метод малоэффективный на текстах небо льшой длины, но вполне может себя оправдать на текстах или структурах данных размером в несколько килобайтов.
-
-
Глава 3. Дешифрование классических шифров 45
}
// проверим, не найдется ли какое-нибудь слово // из нашего словаря в новом тексте for(intk=0;k<dict_n; k++)
if (strstr(buf, dict[k]) != NULL) return true;
// подсчитаем монограммы и/или диграммы // и определим, насколько сильно распределение их частот отличается // от эталонных значений для русского (английского, французского, ...) calc_gramm(buf, len); if (calc_measure() > gran)
return false;
else
return true;
}
Функция test_subst состоит из нескольких независимых частей: первая про­веряет наличие запрещенных биграмм в тексте, вторая осуществляет проверку слов из словаря. Если в тексте найдется хотя бы одно слово, алгоритм принимает решение о том, что дальнейший анализ будет проводить криптоаналитик. Заклю­чительная часть кода test_subst подсчитывает частотные характеристики текста и определяет разницу между подсчитанными и эталонными таблицами биграмм (или монограмм — по вкусу).
Вопрос о корректности названия «автоматический криптоанализ» уже дол­жен был возникнуть у читателя. В самом деле, создаваемые нами программы не выдают криптоаналитику восстановленный с точностью до буквы открытый текст. Вместо этого они выдают ему довольно много информации, которую соби­рают исходя опять же из предрассудков криптоаналитика и информации о шифре, которая может быть доступна ему заранее.
С другой стороны, мы рассматриваем случай, когда компьютер все равно бу­дет либо перебирать все возможные варианты ключа, либо осуществлять большое количество вычислений и оценок. И сейчас главное для нас — дать ему критерий, по которому он в качестве предварительной оценки отбросит самые невероятные открытые тексты. Остальные будет пытаться прочесть человек. И именно человек определит, какой из всех этих текстов является осмысленным, то есть открытым текстом.
Поэтому, чем большим количеством критериев мы наделим алгоритм и чем сильнее будут сами критерии, тем быстрее компьтер проверит все варианты и тем меньше из этих вариантов останется для анализа человеку.
Не имея верного ключа криптосистемы, криптоаналитик всегда задается во­просом: как определить, что выбранный для расшифрования ключ криптосистемы или эквивалентная ему последовательность преобразований шифротекста тако­вы, что дешифрованные данные представляют собой искомый открытый текст?
В каждом конкретном случае этот вопрос требует отдельного рассмотре­ния. В общем случае понятие открытого текста распространяется не только на естественные языки, но и на структуры данных, предсказуемые значения по­лей и их начальных значений. В таком случае криптоаналитик должен выде­лить набор критериев, при выполнении которых он может считать дешифро
-
46 Глава 3. Дешифрование классических шифров
ванные данные возможным кандидатом на место открытого текста. Например, обладая знанием того, что в передаваемом по электронной сети зашифрован­ном IP-пакете содержится адрес получателя, криптоаналитик может сущест­венно уменьшить пространство допустимых открытых текстов, рассматривая только те открытые тексты, в которых данный адрес присутствует в некотором (произвольном) месте.
Многие криптосистемы позволяют извлечь определенное количество так на­зываемой вторичной информации, которая может послужить дополнительным критерием отбора подходящих ключей.
К сожалению, полностью исключить активное участие человека в конечном процессе выбора открытого текста невозможно. Оптимальный подход состоит в организации интерактивной работы человек-машина, поскольку только человек достоверно решит, может ли представленная расшифровка данных быть носите­лем требуемой информации.
Быстрое раскрытие шифров простой замены
В 1995 году Томас Якобсен предложил автоматический метод раскрытия клю­ча простых (как моно-, так и полиалфавитных) шифров замены и в своей статье даже привел реализацию алгоритма его работы. Его работа является ярким при­мером множества подобных исследований, проведенных за последние несколько десятилетий.
В своем методе вскрытия шифров замены Якобсен использовал информацию о распределении диграмм и ее соответствие распределению открытых текстов. Простое решение не очень сложной задачи вылилось в весьма интересный алго­ритм. Его особенностью стало то, что изначально случайно выбранный ключ в процессе работы алгоритма последовательно приближается (своего рода аппрок­симируется или даже подбирается по частям) к настоящему ключу, использован­ному для зашифрования. Производя с ключом различные (может быть, даже слу­чайные) изменения, алгоритм определяет, продвинулись ли мы хоть сколь-нибудь к намеченной цели, если он сочтет изменения положительными, они будут испо­льзовать в дальнейшем. Изменения, которые либо бесполезны, либо, что называ­ется, «портят картину», просто отбрасываются и далее не принимаются к рас­смотрению.
Начальный ключ по возможности следует выбирать как можно более близ­ким к настоящему, например исходя из доступной вторичной информации о крип­тосистеме или абонентах, использующих ее. В случае, если такой информации нет, подойдет и случайный ключ — в алгоритме лишь увеличится количество ите­раций, необходимых для поиска настоящего ключа. В случае простой замены (мы позволим себе показатель упростить до моноалфавитной замены) ключ представ­ляет собой таблицу следующего характера (рис. 3.4). В соответствии с ней буква «А» заменяется на «Г», «Б» на «Д» и т. д. Нижняя строка таблицы, вообще говоря, переставляется произвольным образом и представляет секретную компоненту, раскрытие которой ведет к раскрытию шифра.
Глава 3. Дешифрование классических шифров 47
АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ_ ГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ_АБВ
Ðèñ. 3.4
Для того чтобы получить некоторое формальное описание открытого текста, в алгоритме раскрытия шифра предварительно выбирается фиксированная функ­ция от нескольких параметров. Эта функция и характеризует открытый текст, ко­торый был зашифрован с помощью данной криптосистемы и искомого ключа. Ре­зультатом функции должно быть значение корреляции подаваемого ей на вход от­крытого текста и некоторого эталонного открытого текста, заданного заранее и изначально фиксированного. Данную функцию называют целевой функцией,по­скольку, чем ближе ее значение к некоторой величине (например, единице для нормированных целевых функций), тем лучше данный ключ был аппроксимиро­ван и тем больше он «похож» на действительно использованный при зашифрова­нии.
Сам алгоритм состоит из нескольких шагов, повторяемых в каждой итерации последовательно, один за другим. Сначала рабочий ключ модифицируется случай­ным или заранее определенным способом, в зависимости от количества и успеха прошлых итераций (корректность этого описания мы обсудим чуть позже). Затем шифротекст расшифровывается с помощью нового модифицированного ключа и проверяется значение нормированной целевой функции. Если оно ближе к единице (из-за нормированности), чем вычисленное во время прошлой итерации алгоритма, то измененный ключ сохраняется в качестве рабочего и алгоритм повторяется.
Томас Якобсен в предложил использовать в качестве целевой функции сумму разностей по модулю между вычисленными и заранее посчитанным количеством встреченных в тексте различных диграмм. Позволим себе немного упростить фун­кцию, чтобы упростить понимание излагаемого метода, и будем подсчитывать не диграммы, а всего лишь биграммы (в реальной криптоаналитической практике ис­пользуются именно диграммы, а также приближения более высоких порядков). Если считать значение D
матрицы D равным количеству встретившейся биграм-
ij
мы, состоящей из символов i è j, то целевая функция примет вид
ãäå E
Wt D E
(),=−
— частоты биграмм, подсчитанные заранее и зафиксированные в алгорит-
ij
ij
t
ij
,
ij
ме в качестве эталонных. Понятно, что W(t) нормированной не является. Однако это можно легко поправить, воспользовавшись теорией больших чисел, которая позволит нам заменить значения частоты биграмм в матрицах (E
)è(Dij)назна-
ij
чения вероятностей. Дополнительно умножив функцию на нормирующий множи­тель, получим нормированную целевую функцию W*(t). Однако мы все еще не можем ее использовать, поскольку близость W*(t) к единице означает противопо­ложную искомой ситуацию, — в этом случае, почти все элементы (D удалены от соответствующих элементов (E
). Чтобы достичь желаемого теорети-
ij
) наиболее
ij
ческого эффекта нам придется ввести еще одну функцию и использовать в конце концов именно ее.
На псевдокоде реализация алгоритма показана в листинге 3.17.
48 Глава 3. Дешифрование классических шифров
Листинг 3.17
1. Готовим начальный рабочий ключ K исходя из предположений об открытом
тексте и настоящем ключе.
2. ПустьV=W(D(C,K) ).
3. Присвоим K' = K.
4. Модифицируем K', изменяя его незначительно.
5. Пусть V' = W( D(C,K) ).
6. Если V' < V, то теперьV=V'иК=K'.
7. Переходим к шагу 4.
Условием выхода из алгоритма может являться либо количество набранных вариантов ключей, либо некоторый критерий осмысленности открытого текста. В последнем случае, на каждом следующем ключе дешифруется шифртекст и к дешифровке применяется тест «на осмысленность». Если данные выглядят бо­лее-менее осмысленно, алгоритм прекращает работу.
Обратите внимание, что мы используем функцию W(t),àíåW*(t) èëè
W
*(t) — она удобнее с практической точки зрения. По этой причине успешность проведенных изменений ключа K¢ оценивается знаком меньше <, а не больше >, как это было сделано выше в теоретических выкладках. Если изменения в ключе на шаге 4 сказались на расстоянии между характеристиками дешифровки и эта­лона в меньшую сторону, то на шаге 6 они будут сохранены и использованы в да­льнейшем. Иначе изменения будут отброшены и начнется следующая итерация алгоритма.
Модификация ключа Kна шаге 4 в случае простой замены заключается в пе-
рестановке элементов последней строки таблицы, показанной на рис. 3.4. Всего таких перестановок N!, что при больших N делает тотальное опробование всех возможных комбинаций ключа очень трудоемкой задачей. Тем не менее, исполь­зуя статистические особенности открытого текста, зашифрованного простой за­меной, можно значительно сократить время перебора, чем, собственно, и занима­ется предложенный Якобсеном алгоритм.
Рассмотрим теперь реализацию предложенного алгоритма (cм. листинг 3.18).
Листинг 3.18
#include <stdio.h> #include <stdlib.h>* #include <string.h> #include <math.h> #include "unidef.h"
// биграммы typedef struct {
long values[40][40];
long total;
} BIGRAMM;
#include "etalon.h" BIGRAMM current;
Глава 3. Дешифрование классических шифров 49
// использованный алфавит (укороченный в целях упрощения) // "а" маленькое играет роль пробела и заменителя всех остальных знаков unsigned char alpha[33] = "АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯа";
// ключ-таблица для зашифрования unsigned char key[33] = "БВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯаА"; // ключ-таблица для расшифрования unsigned char right_unkey[33] = "ЯаАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮ"; unsigned char unkey[33] = "ЕПРСТЯаАБВГДЖЗИЙКЛМНОУФХЦЧШЩЪЫЬЭЮ";
// шифр простой замены void encrypt(unsigned char *buf, int len, char *key) {
for(inti=0;i<len; i++)
if (buf[i] >= 0xC0 && buf[i] <= 0xE0) buf[i] = key[buf[i]-0xC0];
}
void decrypt(unsigned char *buf, int len, char *key) {
for(inti=0;i<len; i++)
if (buf[i] >= 0xC0 && buf[i] <= 0xE0) buf[i] = unkey[buf[i]-0xC0];
}
// вычисляем биграммы void calc_gramm(unsigned char *buf, int len) {
for(inti=0;i<len-1; i++) {
current.values[ buf[i]-0xC0 ][ buf[i+1]-0xC0 ]++; current.total++;
}
}
// вычисляем "расстояние" рабочего ключа до настоящего double calc_measure() {
double result = 0;
if (!current.total) current.total++;
for(inti=0;i<40;i++)
for(intj=0;j<40;j++)
result += fabs(((double) current.values[i][j]/current.total) -
((double) etalon.values[i][j]/etalon.total));
return result;
}
// поиск ключа void findkey_jakobsen(char *buf, int len, char *guesskey) {
char work_key[33];
double V, V_; int c, pos, sym;
char *temp, s;
temp = (char *) malloc(len);
memcpy(work_key, guesskey, sizeof(work_key)); strncpy(temp, buf, len); memset(¤t, 0, sizeof(current)); calc_gramm(temp, len);
V = calc_measure(); randomize();
50 Глава 3. Дешифрование классических шифров
c=0;
for (;;) {
// переставляем символы в ключе
pos = rand() % 33; sym=(pos+1+rand()) % 33; s = work_key[pos]; work_key[pos] = work_key[sym]; work_key[sym] = s;
// дешифруем текст strncpy(temp, buf, len); decrypt(temp, len, work_key); memset(¤t, 0, sizeof(current)); calc_gramm(temp, len);
// считаем расстояние до эталона V_ = calc_measure(); if (++c % 1000 == 0) printf("%f < %f, %d, %d\n", V_, V, pos, sym); if(V_<V){
// ура! улучшили результат!
printf("WORK_KEY == %s, KEY = %s (V == %f, V_ == %f)",
work_key, guesskey, V, V_);
printf("!!!\n", work_key);
memcpy(guesskey, work_key, sizeof(work_key));
V = V_; }
else
// оставляем все без изменений
memcpy(work_key, guesskey, sizeof(work_key));
}
printf("%f", V);
}
#define MAKE_ETALON 0 int main(int argc, char* argv[]) {
char text[] = "САМ АЛГОРИТМ СОСТОИТ ИЗ НЕСКОЛЬКИХ ШАГОВ, ПОВТОРЯЕМЫХ В" "КАЖДОЙ ИТЕРАЦИИ ПОСЛЕДОВАТЕЛЬНО, ОДИН ЗА ДРУГИМ. СНАЧАЛА РАБОЧИЙ КЛЮЧ"
"МОДИФИЦИРУЕТСЯ СЛУЧАЙНЫМ ИЛИ ЗАРАНЕЕ ОПРЕДЕЛЕННЫМ СПОСОБОМ, В" "ЗАВИСИМОСТИ ОТ КОЛИЧЕСТВА И УСПЕХА ПРОШЛЫХ ИТЕРАЦИЙ (КОРРЕКТНОСТЬ" "ЭТОГО ОПИСАНИЯ МЫ ОБСУДИМ ЧУТЬ ПОЗЖЕ). ЗАТЕМ ШИФРОТЕКСТ" "РАСШИФРОВЫВАЕТСЯ С ПОМОЩЬЮ НОВОГО МОДИФИЦИРОВАННОГО КЛЮЧА И" "ПРОВЕРЯЕТСЯ ЗНАЧЕНИЕ НОРМИРОВАННОЙ ЦЕЛЕВОЙ ФУНКЦИИ. ЕСЛИ ОНО БЛИЖЕ" "К ЕДИНИЦЕ (ИЗ-ЗА НОРМИРОВАННОСТИ), ЧЕМ ВЫЧИСЛЕННОЕ ВО ВРЕМЯ ПРОШЛОЙ" "ИТЕРАЦИИ АЛГОРИТМА, ТО ИЗМЕНЕННЫЙ КЛЮЧ СОХРАНЯЕТСЯ В КАЧЕСТВЕ" "РАБОЧЕГО И АЛГОРИТМ ПОВТОРЯЕТСЯ.";
int len = strlen(text);
for(inti=0;i<len; i++)
if (text[i] < 'À' || text[i] > 'ß') text[i] = 0xE0;
// создание эталона if (MAKE_ETALON) {
FILE *f = fopen("collect.txt", "rb"); char buf[32768];
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]