Статья ASM – Брут и шифрование Цезарем

Среди огромного кол-ва алгоритмов шифрования имеется и шифр "Цезаря", который по-сути и открыл эпоху крипта в целом (Рим, 50-ые годы до нашей эры). Не нужно искать в нём скрытый смысл – его там попросту нет, а скрытие текста от посторонних глаз осуществляется обычной заменой одного символа, другим. Такие алго называют ещё "шифр с подстановкой". Ключ представляет собой обычное число, кредитный портфель которого ограничен диапазоном алфавита текущей кодировки текста (для кириллицы это Win-1251, с макс.ключом =32). Например если ключ равен единице, то на место символа "А" подставляется следующий "Б" и т.д. На рисунке ниже представлена общая схема, где в качестве ключа я выбрал значение(4):

Algo.png


Здесь видно, что алгоритм примитивен как амёба и в наше время источает уже запах давно непроветриваемых помещений. Однако в начальных учебных заведениях этот "труп" по-прежнему ворошат, делая ставку именно на простоту его реализации. Нужно отметить, что на основе алгоритма "Цезаря" 15-столетий спустя был придуман шифр "Виженера", роль ключа в котором играет не обычное число, а уже осмысленное слово типа "Привет" (или вообще целое предложение) с подстановочной таблицей. Но Виженер пока нам не интересен – остановимся на Цезаре..
------------------------------------------------

Пролог

Если в реальной жизни строить намного труднее, чем махая кувалдой крушить всё на своём пути, то с расшифровкой текста всё в точности, да наоборот. Попробуйте вооружившись ручкой и листом бумаги "сломать" зашифрованный даже ключом(8) текст Цезаря, не говоря уже о более "длинном" ключе типа 29. Перебор займёт у вас как-минимум пару часов, причём если в тексте не будет коротких слов как: "Я", "ОН", "ОНИ" (обычно выбирают одно, самое короткое слово из всего текста), то время увеличивается в геометрической прогрессии, и тогда вообще пиши-пропало. Для наглядности, вот так выглядит текст, закодированный ключом(4):


crypt_1.png


Как видим, текст попал в миксер и вернуть его в читабельный вид если и не составит труда, то займёт уйму времени. Чтобы не пыхтеть над этой элементарной задачей, попробуем автоматизировать процесс, написав универсальную программу для крипта и сразу декрипта алгоритма Цезаря, в зависимости от выбора пользователя (как на рис.выше, зашифровать по клавише(1), или расшифровать по двойке). Тут имеются несколько нюансов – остановимся на них подробнее..

1. Поскольку код носит демонстрационный характер, привяжем его исключительно к кириллице Win-1251. То-есть все латинские символы и знаки-препинания будем отсеивать, выбирая только русские буквы. Для этого нужно обратиться к таблице ASCII, причём обязательно к её кодировке СР-1251 (Code-Page), т.к. русский текст может быть набран и в досовском стиле ОЕМ-866 (Original Equipment Manufacturer).

Посмотрите на
– диапазон кириллицы в ней занимает ровно 40h байт, от C0h (заглавная "А") и вплоть до последнего FFh (прописная "я"). Пара ёжиков "Ё/ё" с кодами A8/В8h отбилась от стада, и чтобы не усложнять программный фильтр, мы просто оставим их как-есть и будем считать знаками-препинания (они всё-равно встречаются редко):

ascii_1251.png


Данная таблица отображает, в каком виде будет храниться русский текст в памяти нашей программы. К примеру слово "Цезарь" примет 16-тиричный вид: D6.E5.E7.E0.F0.FC. Что получится, если мы закодируем эту строку ключом(8)? Поскольку нужно будет прибавлять к каждому коду-символа значение(8), то большинство из этих букв останутся внутри данного 40h-байтного блока кириллицы. А вот последний символ "ь" улетит в штрафбат и его ASCII-код примет запрещённое значение(4), т.к. FCh+08h=04h. Вообще-то получим 104h, но мы имеем дело с байтами (макс.FFh), поэтому не учитываем перенос.

Значит при шифровании нужно обязательно следить за диапазоном кириллицы C0h..FFh, и если символ вылетает за его пределы, то прибавляя к нему код первой "А"=С0h, отправлять обратно внутрь данного блока. Вот пример такого фильтра:


C-подобный:
       mov      bl,[key]    ;// BL = ключ шифрования
@@:    lodsb                ;// AL = очередной байт из буфера
       cmp      al,'А'      ;// проверить его на первую букву кирилицы
       jb       @next       ;// если меньше, значит это знак-препинания – игнорируем
       add      al,bl       ;// иначе: шифрование и прибавить к символу ключ
       cmp      al,'А'      ;// проверить на переполнение
       ja       @next       ;// норма, если больше (в диапазоне А..я) Jump-Above
       add      al,'А'      ;// иначе: прибавить код символа "А" (коррекция)
@next: stosb                ;// перезаписать зашифрованный символ в буфер!
       loop     @b          ;// прогнать цикл ECX-раз..

Будем считать, что строку зашифровали. А как быть с обратным преобразованием в исходный вид, ведь софт планируем писать универсальный? В этом случае, на каждой итерации цикла будем осуществлять перебор, отнимания от текущего значения единицу, поскольку при шифровании мы прибавляли неизвестное число. Здесь так-же требуется коррекция при выходе символа за границы 40h-байтного блока кириллицы. Только теперь нужно будет прибавлять не код символа "А", а просто константу 40h по такому алго:

C-подобный:
@@:    lodsb                ;// AL = очередной/зашифрованный байт из буфера
       cmp      al,'А'      ;// проверить на знак-препинания,
       jb       @skip       ;//   ..пропустить, если он.
       dec      al          ;// иначе: AL-1 (брут в обратную сторону)
       cmp      al,'А'      ;// проверить выход символа за пределы блока
       jae      @skip       ;// пропустить, если больше/равно (Jump-Above-Equal)
       add      al,40h      ;// иначе: отправить символ в диапазон кирилицы
@skip: stosb                ;// перезаписать его в буфере!
       loop     @b          ;// крутим цикл по всей строке..

Метаморфозы символов (переход из одной формы в другую) удобно наблюдать в отладчике, который является глазами и руками любого реверсера. Кроме того, нужно предусмотреть и выход из цикла расшифровки по какой-нибудь клавише. Задачи такого характера решает функция GetAsyncKeyState(). На входе она принимает всего один аргумент с кодом виртуальной клавиши (для пробела "Space" это 20h), а на выходе возвращает в регистре EAX значение, в котором старший бит информирует о нажатом состоянии указанной клавиши, а младший – о нажатии и отпускании. Если на момент вызова этой функции юзер вообще не тискал клаву (её код запоминается в системной очереди-сообщений клавиатуры), то в регистре EAX получим нуль:

C-подобный:
       invoke   GetAsyncKeyState,0x20  ;// парсим клавишу "Space"
       or       eax,eax                ;// проверить EAX на нуль
       jnz      @stop                  ;// если No-Zero, значит был нажат пробел.

2. Другим немало/важным моментом является кириллица в консоли Win, поскольку изначально она заточена под досовскую кодировку OEM-866. Если не принять каких-либо мер, то загрузив файл 1251 в окно консоли, вместо символов получим непонятный мусор (крякозябры). На своём-то узле мы сможем заточить её под кириллицу, а как быть с удалённой машиной, куда попадёт наш код? Вот две ветки реестра, в которых скрыты рычаги управления дефолтным видом командной строки:

HKCU\Console – глобальные свойства окна интерпретатора cmd.exe;
HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Console\TrueTypeFont – добавление своих шрифтов.

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


C-подобный:
       invoke   SetConsoleOutputCP,1251  ;// для вывода на консоль "STD_OUTPUT_HANDLE"
       invoke   SetConsoleCP,1251        ;// для ввода с клавы "STD_INPUT_HANDLE"

Оказывается эти функции требуют консольного "True-Type" шрифта с кириллицей, а по-умолчанию, для всех запускаемых программ там установлен "точечный шрифт" без поддержки русского языка. Таким образом мало сменить кодировку на Win-1251, так ещё нужно поменять и шрифт ком.строки. А ведь редиска Microsoft этого даже не обозначила в своей документации на MSDN, в результате чего подобные глюки приходится отлавливать опытным путём.

На системах Win-XP включительно, смена шрифта из своего приложения представляло собой нерешаемую проблему. Зато начиная с Висты, в библиотеку Kernel32.dll была добавлена функция SetCurrentConsoleFontEx(), которая в качестве аргумента ждёт указатель на структуру "CONSOLE_FONT_INFOEX", где и задаются свойства необходимого шрифта. Пример этой структуры с поддерживающим кириллицу векторным шрифтом "Lucida Console", представлен ниже:


C-подобный:
struct CONSOLE_FONT_INFOEX
  cbSize        dd  sizeof.CONSOLE_FONT_INFOEX
  nFont         dd  5                   ;// индекс шрифта в таблице шрифтов
  FontWidth     dw  7                   ;// ширина символов
  FontHight     dw  12                  ;// высота, итого = 12x7 пикселей
  FontFamily    dd  54                  ;// семейство шрифтов
  FontWeight    dd  600                 ;// толщина 100...1000 (попугаев)
  FaceName      du  'Lucida Console',0  ;// Unicode имя шрифта
                rw  32-15               ;// байты выравнивания (резерв)
ends

Размер этой структуры должен быть строго 54h байтов (указывается в первом члене), иначе функция будет возвращать ошибку 0x57 "Неверный аргумент". Чтобы получить все эти данные, мне пришлось сначала мышью настроить консоль через свойства её окна, после чего вызвать функцию GetCurrentConsoleFontEx(), которая заполнила эту структуру текущими настройками. Кстати эти функции читают и пишут в реестр, по указанной ветке [HKCU\Console].

Console_Reg.png


Ну и теперь немного о самом алгоритме программы..
Значит предлагаем юзеру выбрать файл для шифрования или обратного процесса расшифровки. Получаем его размер и выделив память, читаем данные в буфер. Дальше запрашиваем действие с файлом, и в зависимости от выбора, зовём свои процедуры "Crypt" или "DeCrypt". Если это шифрование, то дополнительно запрашиваем у юзера ключ в диапазоне алфавита кириллицы 1..32, после чего шифруем до самого подвала и сбрасываем результат в лог-файл Crypt.txt.

На всякий/пожарный, при вводе ключа нужно предусмотреть "защиту от дурака" на случай, если юзер введёт ключ больше чем 32, например 100. Поскольку 32 кратно двум, мы просто берём остаток от деления на 32 логикой AND EAX,31 и получаем значение в нужном диапазоне.

Позже можно будет проверить функционал, обратно подсунув программе этот-же зашифрованный файл Crypt.txt, чтобы прожка попыталась его расшифровать. В процессе декрипта, даём юзеру возможность осмотреться и в случае попадания в яблочко, нажать пробел для останова. Реализуем это посредством скважности вывода данных на консоль = 5-сек через Sleep(). Остальные мелочи прокомментированы в коде, надеюсь разберётесь – вот пример:


C-подобный:
format   pe console
include 'win32ax.inc'
entry    start
;//--------
.data
struct CONSOLE_FONT_INFOEX
  cbSize        dd  sizeof.CONSOLE_FONT_INFOEX
  nFont         dd  5         ;// индекс шрифта в таблице шрифтов
  FontWidth     dw  7         ;// ширина символов
  FontHight     dw  12        ;// высота = 12x7
  FontFamily    dd  54        ;// семейство шрифтов
  FontWeight    dd  600       ;// толщина 100...1000
  FaceName      du  'Lucida Console',0  ;// Unicode имя шрифта
                rw  32-15     ;// байты выравнивания (резерв)
ends
cfi    CONSOLE_FONT_INFOEX

capt       db  '"Caesar cipher" ver 0.1',0
hello      db  " (\___/)    ",10
           db  " (='.'=)    ",10
           db  " E[:]|||[:]З",10
           db  ' (")_(")    ',10
           db  10
           db  ' Открыть файл: ',0
error      db  ' Ошибка! Не правильный выбор!',0
info       db  ' OK! Размер..: %d байт'    ,10
           db  ' -------------------------',10,0
job        db  10,' ----------------------'
           db  10,' Выбор задачи:'
           db  10,'   * Зашифровать  = 1'
           db  10,'   * Расшифровать = 2',10
           db  10,'   * Команда      = ',0
key        db     '   * Ключ [1..32] = ',0
line       db  10,' ----------------------',10,0
writeOk    db  10,' ----------------------'
           db  10,' OK! Смотри файл "Crypt.txt"',0
brute      db  10,' ==== Ключ: %02d. Жми пробел для останова ======='
           db  10,' ==============================================',10,0
fName      db  'Crypt.txt',0

frmtS      db  '%s',0  ;// спецификатор строки для scanf()
frmtD      db  '%d',0  ;// спецификатор числа DEC

fileBuff   dd  0       ;// под адрес выделенной памяти
jobInp     dd  0       ;// флаг выбора задачи
keyInp     dd  0       ;// ключ шифрования
counter    dd  1       ;// длина перебора (алфавита кирицы)
inHndl     dd  0       ;// хэндл открытого файла
outHndl    dd  0       ;// хэндл файла для записи
fSize      dd  0       ;// размер файла с данными
buff       db  0
;//--------

.code
start:
;//--- Ставим шрифт: "Lucida Console", 12x7, Normal
       invoke   GetStdHandle,STD_OUTPUT_HANDLE
       invoke   SetCurrentConsoleFontEx,eax,0,cfi

;//--- Обзываем консоль и кодировка Win-1251 (кирилица)
       invoke   SetConsoleTitle,capt
       invoke   SetConsoleOutputCP,1251  ;// для вывода на консоль
       invoke   SetConsoleCP,1251        ;// для ввода с клавы

;//--- Запрос на выбор файла
      cinvoke   printf,hello
      cinvoke   scanf,frmtS,buff

;//--- Пытаемся открыть выбранный файл
       invoke  _lopen,buff,OF_READWRITE
       mov      [inHndl],eax       ;// дескриптор
       cmp      eax,-1             ;// ошибка?
       jne      @ok                ;// нет..
      cinvoke   printf,error       ;// иначе: мессага,
       jmp      @exit              ;//  ..и на выход.

;//--- ОК! Размер и выделение памяти под файл
@ok:   invoke   GetFileSize,eax,0
       mov      [fSize],eax
       invoke   VirtualAlloc,0,eax,MEM_COMMIT,PAGE_READWRITE
       mov      [fileBuff],eax
      cinvoke   printf,info,[fSize]

;//--- Чтение открытого файла в буфер, и вывод его на консоль
       invoke  _lread,[inHndl],[fileBuff],[fSize]
      cinvoke   printf,[fileBuff]

;//--- Запрашиваем действие с файлом: Crypt(1), DeCrypt(2)
@job: cinvoke   printf,job
      cinvoke   scanf,frmtD,jobInp

;//--- Проверяем выбор действия
       cmp      [jobInp],1         ;//
       je       @01                ;// если нажата клавиша #1
       cmp      [jobInp],2         ;//
       je       @02                ;// если #2
      cinvoke   printf,error       ;// иначе: ошибка,
       jmp      @exit              ;//   ...и на выход.

@01:   stdcall  GetCrypt           ;// зовём процедуру шифрования!
       jmp      @exit              ;//   ...после чего на выход.
@02:   stdcall  GetDeCrypt         ;// зовём процедуру брута!

@exit:
      cinvoke   scanf,frmtS,buff   ;//<--- КОНЕЦ ПРОГРАММЫ -------
      cinvoke   exit,0             ;//

;//ннннннннннннннннннннннннннннннннннннннннннннннннннннннннннннннн
;//--- Юзер выбрал(1) = шифрование
proc  GetCrypt
      cinvoke   printf,key          ;//
      cinvoke   scanf,frmtD,keyInp  ;// запросить ключ 1..32
       mov      ebx,[keyInp]        ;// EBX = ключ
       and      ebx,31              ;// взять остаток от 32 (рус.алфавит)
       mov      ecx,[fSize]         ;// ECX = длина файла (цикла)
       mov      esi,[fileBuff]      ;// ESI = источник данных
       mov      edi,esi             ;// EDI = приёмник (он-же)
@@:    lodsb                        ;// AL = очередной байт из ESI
       cmp      al,'А'              ;// проверить его на первую букву кирилицы
       jb       @next               ;// если меньше, значит это знак-препинания
       add      al,bl               ;// иначе: прибавить к символу ключ
       cmp      al,'А'              ;// проверить на переполнение
       ja       @next               ;// норма, если больше (в диапазоне А..я)
       add      al,'А'              ;// иначе: коррекция
@next: stosb                        ;// перезаписать символ в буфере
       loop     @b                  ;// прогнать цикл ECX-раз..

      cinvoke   printf,line             ;// (линия-разделитель)
      cinvoke   printf,frmtS,[fileBuff] ;// Показать результат из буфера!
;//--- Сбрасываем зашифрованные данные в файл "Crypt.txt"
       invoke  _lcreat,fName,0          ;// создать его
       push     eax                     ;// дескриптор
       invoke  _lwrite,eax,[fileBuff],[fSize] ;// записать в него данные
       pop      eax                     ;//
       invoke  _lclose,eax              ;// закрыть файл.
      cinvoke   printf,frmtS,writeOk    ;// мессага ОК!
       ret                              ;// на выход
endp
;//ннннннннннннннннннннннннннннннннннннннннннннннннннннннннннннннн
;//--- Юзер выбрал(2) = брут зашифрованного файла
proc   GetDeCrypt
@find:
      cinvoke   flushall      ;// очистить буферы всех сообщений
       mov      ecx,[fSize]   ;// длина файла и цикла
       mov      esi,[fileBuff];// источник,
       mov      edi,esi       ;//   ..он-же приёмник.
@@:    lodsb                  ;// AL = очередной байт из ESI
       cmp      al,'А'        ;// проверить на знак-препинания,
       jb       @skip         ;//   ..пропустить, если он.
       dec      al            ;// иначе: AL-1 (брут в обратную сторону)
       cmp      al,'А'        ;// проверить выход символа за пределы
       jae      @skip         ;// пропустить, если больше/равно
       add      al,40h        ;// иначе: отправить символ в диапазон кирилицы
@skip: stosb                  ;// перезаписать его в буфере!
       loop     @b            ;// крутим цикл ECX-раз..

      cinvoke   printf,brute,[counter]  ;// покажем текущий ключ,
      cinvoke   printf,frmtS,[fileBuff] ;//   ...и что получилось.

       invoke   GetAsyncKeyState,0x20   ;// проверить на клавишу "Space"
       or       eax,eax       ;//
       jnz      @stop         ;// если в EAX не нуль, значит пробел.
       invoke   Sleep,5000    ;// пауза, чтобы осмотреться..
       inc      [counter]     ;// следующий ключ
       cmp      [counter],33  ;// по всему алфавиту прошлись?
       jnz      @find         ;//    ...повторить, если нет.
@stop: ret                    ;// выход из процедуры.
endp
;//---------
section '.idata' import data readable writeable
library  msvcrt,'msvcrt.dll',kernel32,'kernel32.dll',user32,'user32.dll'
import   msvcrt, printf,'printf',scanf,'scanf',\
                 gets,'gets',exit,'exit',flushall,"_flushall"
include 'api\kernel32.inc'
include 'api\user32.inc'


Brut.png


Эпилог

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

Вложения

  • caesar.zip
    1,2 КБ · Просмотры: 544
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!