Добавил:
ivanov666
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз:
Предмет:
Файл:Практическая криптография алгоритмы и их программирование
.pdf
Глава 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];
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]
