Друзья, коллеги, всех приветствую! В этой статье мы напишем шифратор на грязном "макро-чистом" Ассемблере и в качестве элементарного шифра будем использовать Шифр Цезаря (Шифр сдвига с ключом k = n). Статья написана для новичков, которые могут погрузиться в мир "первой" криптографии. З.Ы. Код можно доработать, как вы захотите (например, реализовать другой шифр), и использовать, например, в курсовой (в ВУЗе). Благо, в профильных ВУЗах еще уделяют какое-то время языку Ассемблера :)

Итак, шифратор мы будем писать на MASM (вы можете использовать FASM, etc. - любой ассемблер синтаксис которого вы знаете).

Немного теории...

Шифр Цезаря — это один из самых простых и известных методов шифрования, который был использован в Древнем Риме. Он назван в честь Юлия Цезаря, хотя нет точных доказательств того, что именно он его использовал.

Принцип работы шифра Цезаря очень прост: каждая буква в открытом тексте заменяется на букву, находящуюся на некотором фиксированном числе позиций вперед или назад по алфавиту. Это число, на которое сдвигаются буквы, называется ключом шифрования (мы для простоты понимания будем использовать ключ k = 1).

Начнем с объявления директив:

.386
.model flat, stdcall
option casemap:none
  1. .386: Это директива, указывающая ассемблеру использовать инструкции для процессоров с архитектурой x86, начиная с 80386 (или просто 386). Это означает, что программа будет использовать инструкции, которые были введены с появлением процессоров 80386 и выше.

  2. .model flat, stdcall: Это также директива, которая определяет модель памяти и вызова функций для программы. В данном случае:

    • flat указывает на использование плоской модели памяти, где все адреса имеют одинаковый размер и пространство адресов представляется как единое, линейное пространство.

    • stdcall указывает на использование соглашения о вызове функций stdcall, которое определяет порядок размещения аргументов в стеке. По соглашению stdcall вызывающая функция очищает стек после вызова, что отличает его от соглашения cdecl, где чистка стека лежит на вызываемой функции.

  3. option casemap:none: Заставляет компилятор языка ассемблера различать большие и маленькие буквы в метках и именах процедур.

    Подключаем библиотеки:

include C:\masm32\include\kernel32.inc
include C:\masm32\include\windows.inc
includelib C:\masm32\lib\user32.lib
includelib C:\masm32\lib\kernel32.lib

Эти директивы позволяют использовать функции и структуры данных из библиотек Windows и взаимодействовать с операционной системой Windows при разработке программ.

Объявляем константу BSIZE:

BSIZE equ 128

Когда компилятор встречает в коде BSIZE, он заменяет эту константу на 128.

Далее определим секцию данных (.data) и объявим несколько переменных:

.data
buf db BSIZE dup(?)
result db BSIZE dup(?)
stdin dd ?
stdout dd ?
cRead dd ?
cWritten dd ?

buf db BSIZE dup(?): Эта строка объявляет массив байтов (byte array) под названием buf. Его размер определяется с помощью BSIZE, который, как мы определили ранее, эквивалентен 128. Каждый элемент массива будет состоять из одного байта (db), и все элементы будут инициализированы значением ?. Вопросительный знак (?) означает резервирование памяти без инициализации (присвоения начального значения).

Далее:

.code
start:
invoke GetStdHandle, STD_INPUT_HANDLE
mov stdin, eax
invoke ReadConsole, stdin, ADDR buf, BSIZE, ADDR cRead, NULL
  1. code: Эта директива обозначает начало секции кода программы. Все инструкции после этой директивы будут интерпретироваться как инструкции процессора, а не как данные.

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

  3. invoke GetStdHandle, STD_INPUT_HANDLE: Эта инструкция вызывает функцию WinAPI GetStdHandle, которая возвращает дескриптор (handle) стандартного ввода. После вызова этой функции регистр eax содержит возвращаемое значение. Дескриптор мы сохраняем в переменной stdin (mov stdin, eax).

  4. invoke ReadConsole, stdin, ADDR buf, BSIZE, ADDR cRead, NULL: Эта инструкция вызывает функцию WinAPI ReadConsole, которая читает данные из стандартного ввода (который был определен нами ранее как stdin).

Описание функции ReadConsole на сайте MS
Описание функции ReadConsole на сайте MS

Последний параметр по умолчанию требует ввода Юникода. Для режима ANSI установим для этого параметра значение NULL.

Теперь пришло время написания нашего алгоритма шифрования. Так как с помощью ReadConsole наш ввод с клавиатуры поместился в buf, мы скопируем адреса начала массивов buf и result в регистры esi и edi соответственно (в result мы будем переносить наши "обработанные" символы):

mov esi, offset buf
mov edi, offset result

Приступим к написанию цикла, в котором будет происходить "обработка" нашего ввода:

loop1:
mov bl, [esi]
cmp bl, 13
je endloop
add bl, 1
mov [EDI], bl
inc esi
inc edi
loop loop1

Вот как работает каждая инструкция:

  1. loop1:: Это метка, обозначающая начало цикла.

  2. mov bl, [esi]: копирует байт данных из памяти, адрес которой хранится в регистре esi, в регистр bl. Таким образом, содержимое однобайтовой ячейки памяти, адрес которой указан в регистре esi, будет скопировано в младший байт регистра bl (первый символ, который мы ввели с клавиатуры).

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

  4. je endloop: Если результат сравнения указывает на то, что значение в bl равно 13, программа переходит к метке endloop, прерывая выполнение цикла.

  5. add bl, 1: Если значение в bl не равно 13, то к нему добавляется 1 (это как раз и есть наш ключ k = 1).

  6. mov [EDI], bl: Значение, хранящееся в регистре bl, копируется в память по адресу, хранящемуся в регистре edi (сохраняем "обработанный" символ).

  7. inc esi: Регистр esi увеличивается на 1, чтобы указать на следующий элемент массива.

  8. inc edi: Регистр edi также увеличивается на 1, чтобы указать на следующий элемент во втором массиве, в который копируются "обработанные" символы.

  9. loop loop1: Эта инструкция уменьшает значение регистра ecx на 1 и, если оно не равно 0, выполняет переход к метке loop1. Это обеспечивает повторение цикла для каждого элемента массива до тех пор, пока значение ecx не достигнет 0.

Напишем код после метки endloop:

endloop:
invoke GetStdHandle, STD_OUTPUT_HANDLE
mov stdout, eax
invoke WriteConsoleA, stdout, ADDR result, cRead, ADDR cWritten, NULL
invoke ExitProcess, 0
end start
  1. invoke GetStdHandle, STD_OUTPUT_HANDLE: Эта инструкция вызывает функцию WinAPI GetStdHandle, чтобы получить дескриптор стандартного вывода (stdout). Результат (дескриптор стандартного вывода) сохраняем в переменной stdout (mov stdout, eax).

  2. invoke WriteConsoleA, stdout, ADDR result, cRead, ADDR cWritten, NULL: Эта инструкция вызывает функцию WinAPI WriteConsoleA, чтобы вывести данные из массива result в стандартный вывод (в result у нас уже находится "обработанная" строка).

  3. invoke ExitProcess, 0: Эта инструкция вызывает функцию WinAPI ExitProcess для завершения процесса. Параметр 0 указывает на успешное завершение программы.

  4. end start: Это директива, обозначающая конец программы.

Посмотрим на работу программы:

Полный листинг программы:

.386
.model flat, stdcall
option casemap:none

include C:\masm32\include\kernel32.inc
include C:\masm32\include\windows.inc
includelib C:\masm32\lib\user32.lib
includelib C:\masm32\lib\kernel32.lib
BSIZE equ 128
.data
buf db BSIZE dup(?)
result db BSIZE dup(?)
stdin dd ?
stdout dd ?
cRead dd ?
cWritten dd ?
.code
start:

invoke GetStdHandle, STD_INPUT_HANDLE
mov stdin, eax
invoke ReadConsole, stdin, ADDR buf, BSIZE, ADDR cRead, NULL

mov esi, offset buf
mov edi, offset result

mov ebx, 0

loop1:
mov bl, [esi]
cmp bl, 13
je endloop
add bl, 1
mov [EDI], bl
inc esi
inc edi
loop loop1

endloop:
invoke GetStdHandle, STD_OUTPUT_HANDLE
mov stdout, eax
invoke WriteConsoleA, stdout, ADDR result, cRead, ADDR cWritten, NULL

invoke ExitProcess, 0

end start

З.Ы. Осталось только сделать переход с последнего печатного символа на первый. Я думаю, если вы разобрались с этим кодом, то для вас это не будет проблемой! :)

Всем спасибо за внимание!

Комментарии (5)


  1. kekoz
    11.04.2024 14:54
    +3

    mov bl, [esi]: Эта инструкция загружает байт (8 бит), который хранится в регистре esi

    Срочно исправьте, это серьёзная ошибка в описании инструкции.


    1. CyberDen Автор
      11.04.2024 14:54
      +2

      Поправил, благодарю!


  1. MasterIT75
    11.04.2024 14:54

    День добрый. Спасибо за статью. Как то делал шифр Цезаря на Python,. Интересно посмотреть реализацию на другом языке программирования.


  1. nameisBegemot
    11.04.2024 14:54

    Существуют ли кодеры, которые не любят ассемблер?


  1. Chupaka
    11.04.2024 14:54

    loop loop1: Эта инструкция уменьшает значение регистра ecx на 1 и, если оно не равно 0, выполняет переход к метке loop1

    А что у нас в ecx? Туда, как будто, ничего не ложили...