- Статус
- Оффлайн
- Регистрация
- 21 Июн 2025
- Сообщения
- 186
- Реакции
- 74
Решил выпустить новую статью, делать нечего, заказов нет, команда чиллит, а делать что-то надо...
Статья сегодня на тему криптографии, а конкретно поговорим на тему алгоритма шифрования ChaCha20.
Сразу говорю, что должно быть понимание базовой школьной алгебры хотя бы на уровне базы, в статье много математический примером работы с алгоритмом.
Содержание статьи содержит в себе 3 главы введения, две из которых это теория, 3 практика с кодом.
4 глава же это полноценный разбор шифровальщики на примере LockBit 3.0, с объяснением того как рансом шифровал код.
Криптография изучает методы шифрования информации, обратимого преобразования открытого (исходного) текста на основе секретного
Шифрование это процесс обратимого преобразования открытого текста в шифротекст с использованием ключа.
Шифрование делится на симметричное и асимметричное, есть два вида шифрования:
1. Симметричное шифрование - это вид шифрования, когда для шифровки и расшифровки используется один и тот же ключ, этот ключ должен быть заранее известен обеим сторонам и передаваться по защищенному каналу так как его компрометация приводит к раскрытию всей информации
Само симметричное разделяется на виды: есть симметричное шифрование вида потокового, есть вида блока.
Потоковое шифрование - это когда данные обрабатывются последовательно, к примеру определенным количеством битов или байтов.
Ьлоковое шифрование - это когда данные сначала разбиваются на блоки фиксированного размера, после чего каждый блок шифруется отдельно с использованием одного и того же ключа, если последний блок оказывается меньше требуемого размера, применяется дополнение.
Пример криптографических алгоритмов с симметричным шифрованием: AES-256/ChaCha20.
Приведу пример, к примеру у нас есть функция:
расшифрование выполняется обратной операцией:
например пусть m = 7 и ключ k = 3 при n = 10 тогда получаем
В единой математической функции это можно записать так:
то есть шифрование это функция вида ek(m), расшифрование же Dk(C)
в кольце Z каждый элемент имеет аддитивный обратный −k, думаю хорошая демонстрация симметричного шифрования.
-
2. Асимметричное шифрование - это метод шифрованяи когда у вас используется пара ключей открытый и закрытый, открытый ключ может свободно передаваться и использоваться для шифрования данных тогда как закрытый ключ хранится в секрете и применяется для расшифрования.
То есть кто угодно может использовать открытый ключ для шифрования определенных данных, но для дешифровки нужно использовать закрытый ключ:
Пример такого алгоритма: RSA.
На примере возьмем RSA(пример учебный, но не безопасный!!!!)
1. Возьмем два простых числа p = 3 и q = 11, считаем n = p * q = 33
2. Считаем функцию эйлера
3. Выбираем открытый показатель e = 3 такой что он взаимно прост с 20
4. Находим закрытый показатель d такой что e * d mod 20 = 1, получаем d = 7
к примеру:
открытый ключ это (e, n) = (3, 33)
закрытый ключ это (d, n) = (7, 33)
шифрование выполняется по формуле
расшифрование:
например пусть сообщение m = 4
шифруем c = 4^3 mod 33 = 64 mod 33 = 31
расшифруем
plaintext - исходные данные шифрования
keystream - 64 байтовый блок псевдослучайных данных, полученный из состояния чачи и использумый для xor с plaintext;
nonce - уникальное значение, которое падается вместе с ключом и формирует начальное состояние алгоритма, не является секретным но обзяано быть уникальным при каждомс шифровании с одним и тем же ключом.
cчетчик counter - часть состояния, которая увеличивается на каждый 64-байтовый блок.
state - 16 * 32-bit, то есть 16 слов по 32 бита.
1. константы
2. ключ
3. счетчик
4. nonce
QuarterRound - ядро чачи, он перемешивает четыре 32-битных слова состояния, создавая диффузию. Диффузия - это свойство преобразования, при котором изменение одного бита входных данных приводит к распространению изменений на множество битов выходного результата.
После 20 раундов из этого состояния формируется блок keystream, который затем используется в операции XOR с данными
DoubleRound - пара проходов(CR/DR), обеспечивающая полное перемешивание всех 16 слов состояния(про нее тоже позже узнаете)
====================================================================================================================
Теперь переходим к сути данного треда, это разбор конкретноого алгоритма шифрования - чача20.
Chacha20 - это симметричный потоковый алгоритм шифрования который под капотом использует операцию xor но не с простым ключом а с псевдослучайным потоком байтов генерируемым на основе ключа nonce и счетчика.
Что значит потоковый? - это значит, что шифрование происходит не блоками фиксированного размера например как в AES, а последовательно по байтам, генерируется непрерывный поток псевдослучайных данных который затем поэлементно складывается с исходным сообщением через xor, благодаря этому можно шифровать данные произвольной длины без разбиения на блоки и обрабатывать поток данных в реальном времен.
Работает это так:
1. принимается 32 битное значение которое участвует в операции сложения по модулю 2^32, при этом результат всегда ограничен размером регистра и переполнение просто отбрасывается
2. далее результат проходит побитовое XOR преобразование с другим значением, что обеспечивает нелинейное смешивание битов и является обратимой операцией
3. затем применяется операция ROL, то есть циклический сдвиг битов влево внутри 32 битного слова, при котором вышедшие за старший бит значения не теряются а возвращаются в младшие разряды, это перераспределяет биты внутри слова и усиливает диффузию(диффузия это неравномерный процесс перешивания)
в результате этих трех операций формируется некое промежуточное состояние, которое все еще нельзя рассматривать как исходное значение, так как информация распределена по всему 32 битному пространству
4. далее это не шифрование само по себе, а часть процесса генерации ключевого потока, который в ChaCha20 формируется после многократного применения таких преобразований ко всей 16 словной матрице состояния
5. и уже только после получения ключевого потока выполняется финальный этап, где каждый байт данных поэлементно ксорится(xor) с байтом keystream, формируя шифротекст.
записываем алгоритм как функцию состояния:
далее применяем функцию раундов:
R - один дабл раунд, который состоит из операций:
а QR:
QR(a, b, c, d)
после всех этих операций нужно сформировать keystream, это значит, простыми словами, что м получаем новый 64-байтовый блок псевдослучайных данных, который зависит от всего внутреннего состояния, а не от отдельных его частей, после чего этот блок и используется как поток ключа:
В итоге получается, что блок кейстрима = KS = T.
То есть как прошла последовательность:
1. мы взяли алгоритм исходного состояние S0
2. прогнали его через серию обратимых преобразования согласно спецификации чачи(сложение по модулю, xor, rol)
3. получили новое состояние T = F(S0, key, nonce, counter)
Далее из T формируется 64-байтовый keystream блок, который уже используется в операции шифрования:
где P - открытый текст, KS - псевдослучаный поток байтов, C - непосредственно шифротекст.
в симметричных алгоритмах дешифровка тоже происходит довольно просто, восстановление обратно тривиально:
Думаю ничего трудного здесь нет, обычный ксор с перешиванием данных..
2. Практика, реализация и разбор на ассемблере FASM.На практике разберем небольшую библиотеку, чтобы все понять и применим ее для шифрования каких нибудь данных, например для шифрования определенного файла.
Первое обращаем внимание на то. что в r8 сохраняется значение из state это массив из 16 двойных слов(64 byte), который определен в секции .rdata:
в этот буфер процедура chacha20_setupkey32 поместит начальную матрицу 4x4, которая состоит из констант "expand 32-byte k", ключа, счетчика и нонса.
chacha20_setupkey32 логика такова, что он берёт указатель на state в r8 и указатель на ключ в r9. сначала она пишет в первые четыре позиции state константу "expand 32-byte k", которая лежит в chacha20_sigma:
применяется же это для спецификации чачи для версии с 256-битным ключом. потом копирует восемь слов ключа, начиная с индекса 4 и до 11 включительно. ключ должен лежать как массив из 8 двойных слов в little-endian. после этого
в state остаются незаполненными индексы 12,13,14,15. их потом заполнит chacha20_setupiv:
процедура сама по себе тупая, просто перекладывает байты из одного места в другое, без вычислений. возвращает управление через ret, ничего не меняя в регистрах кроме rax и rcx, которые использует как временные. на этом всё.
далее в коде вызывается метка chacha20_encrypt:
ее задача зашифровать данные потоком чачи.
на вход получается что она получает регистры r8(state) и уже настроенным ключом и iv, r9 - это же указатель на открый текст, r10 же это указатель на буфер куда класть шифротекст, r11 размер данных в байтах.
дальше резервирует место на стеке под три локальных массива:
первый это j, 16 слов по 4 байта, всего 64 байта, туда будет скопировано исходное состояние state перед началом обработки блока, чтобы потом после хеширования прибавить к нему результат и получить keystream. второй это x, ещё 64 байта под рабочую копию состояния для передачи в функцию хеша. третий это tmp, 64 байта под временный чбуфер на случай если последний блок данных меньше 64 байт.
То есть после тоо кеак исходное копируется в j, формируется рабочая копия x, которая передается в функцию хэширования:
Она берет рабочее состояние x и последовательно применяет к нему 10 двойных раундов, это называется DoubleRound, в сумме 20 раундов которые полноситью соотвествуют спецификации чачи.
Каждый дабл раунд состиот из чередования CR/DR, в ходе которых все 16 слов состояния перемешиваются с помощью трех базовых операций которые я описывал выше(сложение по модулю, ксор и циклический сдвиг).
CR/DR это два способа примененияя одной и той же операции, но к разным наобором слов состояния
CR - обрабатывает состяоние по 4x4 матрицы:
(0, 4, 8, 12)
(1, 5, 9, 13)
(2, 6, 10, 14)
(3, 7, 11, 15)
То есть берутся вертикальные группы слов и к каждой применяется QuarterRound, это просто перешивает данные внутри колонок
DR же затем берет те же 16 слов, но уже в диагональных комбинациях:
(0, 5, 10, 15)
(1, 6, 11, 12)
(2, 7, 8, 13)
(3, 4, 9, 14)
И также перешивает уже между разными колонками.
После 20 раундов получается рабочее состояние, которое затем поэлементарно складывается с исхходным состоянием j по модулю 2<32>, что является финальным этапом формирования ключа.
-
После обработки каждого 64 байтного блока величвается счетчик в состоянии, это нужно для того, чтобыдля каждого следующего блока генерировался уникальный keystream.
Если же счетик не увеличить, то при одинаковых ключе и nonce будет получаться один и тот же поток, а значит одинаковый keystream будет накладываться на разные данные.
nonce - это уникальное значение, которое вместе с ключом используется для генерации ключа.
Задача этой процедуры брать блок keystream-логики и обеспечивать корректную работу счетчика состояния для генерации уникального потока на каждый блок данных, после получения доступа к текущему состоянию выполняется обычный инкремент 32 битного счетчика, который йотвечает за номер блока в потоке шифрования.
Если происходит переполнение младшего слова счётчика, управление переносом переходит в старшее слово, что позволяет сохранить корректную непрерывность нумерации блоков в пределах 64-битного диапазона, после чего же вычисляется адрес текущего блока состояния, который используется для генерации keystream через функцию хеширования чачи. Полученный результат затем применяется к входным данным - каждый 32-битный элемент keystream поэлементно комбинируется с соответствующими байтами открытого текста через XOR, формируя шифротекст.
Кароче мне лень писать дальше, я надеюсь все понятно.
Работа этого алгоритма заключается в том, что просто берется определенное значение, ОНО складывается по модулю 2<32>, после чего это же значение XOR-ится и реализуется циклический сдвиг ROL, после всех этих манипуляций мы получаем новое значение, это значение просто XOR-ится и в итоге получаются расшифрованные данные.
Процесс шифрования:
Где:
формируется базовое внутреннее состояние алгоритма(4x4 матрица), state - это буфер 16 * 32 слов = 512 бит, записываются консатнта вида:
в итоге создается структура вида:
Далее процесс установки nonce и счетчика:
заполняются оставшиеся поля состояния
nonce - уникальный идентификатор потока, а counter номер блока(0 при старте)
Итоговое состояние:
Далее уже идет процесс подготовки данных:
r9 - указывает на шеллкод
r10 - буфер для
r11 - длина данных
после чего вызывается функция шифрования
1. копирование состояние
2. выполнение 20 раундом чачи
3. генерация кейстрим блока из 64 байта
4. xor входными данными:
в итоге вся выпочка детерминированное преобразование:
где безопасность определяется стойкостью генератора псевдослучайного потока, а XOR лишь применяется как финальная операция наложения
Локер разделяется на несколько заголовочных файлов и одним главным:
chacha.h - публичный криптографический интерфейс алгоритма.
внутри:
16 x 32 битных слова - 512 бит, этов нутреннее состояние чачи(4x4 матрицы), хранит:
1. константы
2. ключ
3. счетчик
4. nonce.
прям в точности как мы разбирали в статье:)
Также вызывается парочка функций:
Первая функция
Аналог инициализации: state[0..11]
Вторая функция
Третья функция
encrypt-machine.h - это слой адптации под железо и компилятор, разбирать не буду.
encrypt-sync.h - выполняет задачу стандартного интерфейса потокового шифра.
chacha.c - main, начнем разбор главного .C-шного файла.
В макросах определяется алгебра состояния:
Эта функия строит 512 битное состояние генератора:
В итоге мы получаем структуру вида:
То есть эта функция является начальной точки дитнамической системы.
ENCRYPT_ivsetup:
Суть этой функции в том, что онафиксирует параметр уникальности потока(nonce) и обнуляет счетчик блока внутри состояния.
Сброс счетчика генерации:
текущий номер блока = 0, 64 битный счетчик начинается заново, делается это для того, чтобы каждый новый поток шифрования стартовал с чистого состояния счетчика.
Далее задача nonce:
Это 64 битное значение nonce, которая интерпретируется как два 32 битных слова.
Далее ECRYPT_encrypt_bytes:
Тут буквально мозг алгоритма - QuarterRound, который мы разбирали чуть выше, разберем по частям кода:
Извлечение состояния:
перед началом обработки блока берется текущее внутреннее состояние шифра, это состояние включает в себя: ключ, nonce, счетчик блоков и фиксированные константы.
for (;;) { - это обработка потока данных по блокам 64 байта, т.к чача работает строго блоками по 64 байта чито равно 512 битам, каждая итерация цикла = один криптографический блок.
Далее идет обработка неполного блока:
если осталось меньше 64 байт данные копируются во временный буфер, обработка же идет как полного блока, результат соотвественно потом обрезается.
Тут создается локальная копия state, j - неизменяемое состояние, x - временная матрица для раундов
И 20 раундов перешивания:
Обычный цикл раундов согласно спецификации алгоритма чачи, каждый проход выполняет CR/DR которые были описаны выше
CR(работа по столбцам)
Матрица 4x4 обрабатывается по вертикали, в виде эффекта получаем перешивание внутри столбцов и разрушение локальной структуры данных, обычное смешивание.
Далее идет DR(диагонали):
Перешивание идет по диагоналям матрицы, в виде эффекта получаем связь между всеми элементами state и полную диффузию по структуре
Далее финализация раунда:
результат раундов складывается с исходным состоянием, это нужно для того, что функция стала необратимой
Далее превращение state в keystream через КСОР:
Сгенерированный блок состояния ксорится с plaintext-ом
все по формуле:
И далее обновляется счетчик блока:
После чего результат записывается в выходной буфер:
32 битные слова переводятся в байты
После чего блок завершается:
Думаю описал нормально, эта часть главы вышла для тех, кто не понял то, что было выше, или понял слабо.
Суть самого алгоритма шифрования я думаю понятно.
====================================================================================================================
Полезные материалы по криптографии:
1.
Описание книги вкратце:
3.
====================================================================================================================
На этом все, надеюсь статья была полезной, как по мне она немного поплыла, но вроде нормально, кто хоть как то разбирается поймет.
Писал под эпинефрином просто.
Пакааа
Статья сегодня на тему криптографии, а конкретно поговорим на тему алгоритма шифрования ChaCha20.
Сразу говорю, что должно быть понимание базовой школьной алгебры хотя бы на уровне базы, в статье много математический примером работы с алгоритмом.
Содержание статьи содержит в себе 3 главы введения, две из которых это теория, 3 практика с кодом.
4 глава же это полноценный разбор шифровальщики на примере LockBit 3.0, с объяснением того как рансом шифровал код.
Теория. Введение в криптографию
Криптография - это наука о математических методах обеспечения конфиденциальности, целостностиданных. Шифрование если простым языком.Криптография изучает методы шифрования информации, обратимого преобразования открытого (исходного) текста на основе секретного
Пожалуйста, авторизуйтесь для просмотра ссылки.
или
Пожалуйста, авторизуйтесь для просмотра ссылки.
в
Пожалуйста, авторизуйтесь для просмотра ссылки.
(
Пожалуйста, авторизуйтесь для просмотра ссылки.
).Шифрование это процесс обратимого преобразования открытого текста в шифротекст с использованием ключа.
Шифрование делится на симметричное и асимметричное, есть два вида шифрования:
1. Симметричное шифрование - это вид шифрования, когда для шифровки и расшифровки используется один и тот же ключ, этот ключ должен быть заранее известен обеим сторонам и передаваться по защищенному каналу так как его компрометация приводит к раскрытию всей информации
Само симметричное разделяется на виды: есть симметричное шифрование вида потокового, есть вида блока.
Потоковое шифрование - это когда данные обрабатывются последовательно, к примеру определенным количеством битов или байтов.
Ьлоковое шифрование - это когда данные сначала разбиваются на блоки фиксированного размера, после чего каждый блок шифруется отдельно с использованием одного и того же ключа, если последний блок оказывается меньше требуемого размера, применяется дополнение.
Пример криптографических алгоритмов с симметричным шифрованием: AES-256/ChaCha20.
Приведу пример, к примеру у нас есть функция:
c=(m+k)modn где m это исходное сообщение c шифротекст n размер алфавитарасшифрование выполняется обратной операцией:
m=(c−k)modnнапример пусть m = 7 и ключ k = 3 при n = 10 тогда получаем
(7 + 3) mod 10 = 0, в модульной арифметике отрицательные значения приводятся по модулю n, при расшифровании m = 0 - 3 mod 10 = 7, таким образом один и тот же ключ используется и для шифрования и для расшифрования.В единой математической функции это можно записать так:
Ek(m)=(m+k)modn,Dk(c)=(c−k)modn=(c+(−k))modnто есть шифрование это функция вида ek(m), расшифрование же Dk(C)
в кольце Z каждый элемент имеет аддитивный обратный −k, думаю хорошая демонстрация симметричного шифрования.
-
2. Асимметричное шифрование - это метод шифрованяи когда у вас используется пара ключей открытый и закрытый, открытый ключ может свободно передаваться и использоваться для шифрования данных тогда как закрытый ключ хранится в секрете и применяется для расшифрования.
То есть кто угодно может использовать открытый ключ для шифрования определенных данных, но для дешифровки нужно использовать закрытый ключ:
Пример такого алгоритма: RSA.
На примере возьмем RSA(пример учебный, но не безопасный!!!!)
1. Возьмем два простых числа p = 3 и q = 11, считаем n = p * q = 33
2. Считаем функцию эйлера
ф(n) = (p - 1)(q - 1) = 2 * 10 = 203. Выбираем открытый показатель e = 3 такой что он взаимно прост с 20
4. Находим закрытый показатель d такой что e * d mod 20 = 1, получаем d = 7
к примеру:
открытый ключ это (e, n) = (3, 33)
закрытый ключ это (d, n) = (7, 33)
шифрование выполняется по формуле
c = m^e mod nрасшифрование:
m = c^d mod nнапример пусть сообщение m = 4
шифруем c = 4^3 mod 33 = 64 mod 33 = 31
расшифруем
m = 31^7 mod 33 = 42. Теория. Разбираем работу ChaCha20.
Термины и их значения:
plaintext - исходные данные шифрования
keystream - 64 байтовый блок псевдослучайных данных, полученный из состояния чачи и использумый для xor с plaintext;
nonce - уникальное значение, которое падается вместе с ключом и формирует начальное состояние алгоритма, не является секретным но обзяано быть уникальным при каждомс шифровании с одним и тем же ключом.
cчетчик counter - часть состояния, которая увеличивается на каждый 64-байтовый блок.
state - 16 * 32-bit, то есть 16 слов по 32 бита.
1. константы
2. ключ
3. счетчик
4. nonce
QuarterRound - ядро чачи, он перемешивает четыре 32-битных слова состояния, создавая диффузию. Диффузия - это свойство преобразования, при котором изменение одного бита входных данных приводит к распространению изменений на множество битов выходного результата.
После 20 раундов из этого состояния формируется блок keystream, который затем используется в операции XOR с данными
DoubleRound - пара проходов(CR/DR), обеспечивающая полное перемешивание всех 16 слов состояния(про нее тоже позже узнаете)
====================================================================================================================
Теперь переходим к сути данного треда, это разбор конкретноого алгоритма шифрования - чача20.
Chacha20 - это симметричный потоковый алгоритм шифрования который под капотом использует операцию xor но не с простым ключом а с псевдослучайным потоком байтов генерируемым на основе ключа nonce и счетчика.
Что значит потоковый? - это значит, что шифрование происходит не блоками фиксированного размера например как в AES, а последовательно по байтам, генерируется непрерывный поток псевдослучайных данных который затем поэлементно складывается с исходным сообщением через xor, благодаря этому можно шифровать данные произвольной длины без разбиения на блоки и обрабатывать поток данных в реальном времен.
Работает это так:
1. принимается 32 битное значение которое участвует в операции сложения по модулю 2^32, при этом результат всегда ограничен размером регистра и переполнение просто отбрасывается
2. далее результат проходит побитовое XOR преобразование с другим значением, что обеспечивает нелинейное смешивание битов и является обратимой операцией
3. затем применяется операция ROL, то есть циклический сдвиг битов влево внутри 32 битного слова, при котором вышедшие за старший бит значения не теряются а возвращаются в младшие разряды, это перераспределяет биты внутри слова и усиливает диффузию(диффузия это неравномерный процесс перешивания)
в результате этих трех операций формируется некое промежуточное состояние, которое все еще нельзя рассматривать как исходное значение, так как информация распределена по всему 32 битному пространству
4. далее это не шифрование само по себе, а часть процесса генерации ключевого потока, который в ChaCha20 формируется после многократного применения таких преобразований ко всей 16 словной матрице состояния
5. и уже только после получения ключевого потока выполняется финальный этап, где каждый байт данных поэлементно ксорится(xor) с байтом keystream, формируя шифротекст.
записываем алгоритм как функцию состояния:
state = (constants[4], key[8], counter[1], nonce[3])далее применяем функцию раундов:
S20 = R"20"(S0)R - один дабл раунд, который состоит из операций:
(x, y, z, w) / QR(x, y, z, w)а QR:
QR(a, b, c, d)
a = a + b mod 2<32> - сложение по модулю 2<32>d = ( d ⊕ a) < 16 - нелинейное смешивание XOR + циклический сдвигc = c + d mod 2<32> - усиление зависимости состояния через сложениеb = (b ⊕ c) < 12 - диффузия через XOR и ротацию битовa = a + b mod 2<32> - повторное распространение влияния между словамиd = (d ⊕ a) < 8 - дополнительное нелинейное перемешивание состоянияc = c + d mod 2<32> - финальное связывание компонент состоянияb = (b ⊕ c) < 7 - завершающая стадия диффузии внутри четверки словпосле всех этих операций нужно сформировать keystream, это значит, простыми словами, что м получаем новый 64-байтовый блок псевдослучайных данных, который зависит от всего внутреннего состояния, а не от отдельных его частей, после чего этот блок и используется как поток ключа:
output_block = (state_after_20_rounds + original_state) mod 2^32В итоге получается, что блок кейстрима = KS = T.
То есть как прошла последовательность:
1. мы взяли алгоритм исходного состояние S0
2. прогнали его через серию обратимых преобразования согласно спецификации чачи(сложение по модулю, xor, rol)
3. получили новое состояние T = F(S0, key, nonce, counter)
Далее из T формируется 64-байтовый keystream блок, который уже используется в операции шифрования:
C = P⊕ KSгде P - открытый текст, KS - псевдослучаный поток байтов, C - непосредственно шифротекст.
в симметричных алгоритмах дешифровка тоже происходит довольно просто, восстановление обратно тривиально:
P = C⊕ KSДумаю ничего трудного здесь нет, обычный ксор с перешиванием данных..
2. Практика, реализация и разбор на ассемблере FASM.
asm:
mov r8, state
mov r9, key
call chacha20_setupkey32
mov r9, n
call chacha20_setupiv
mov r9, msg
mov r10, cmsg
movzx r11, byte [msize]
call chacha20_encrypt
asm:
section '.rdata' data readable
state rd 16
asm:
chacha20_setupkey32:
mov rax, r8
mov rcx, chacha20_sigma
mov edx, [rcx+4*0]
mov [rax+4*0], edx
mov edx, [rcx+4*1]
mov [rax+4*1], edx
mov edx, [rcx+4*2]
mov [rax+4*2], edx
mov edx, [rcx+4*3]
mov [rax+4*3], edx
mov rcx, r9
mov edx, [rcx+4*0]
mov [rax+4*4], edx
mov edx, [rcx+4*1]
mov [rax+4*5], edx
mov edx, [rcx+4*2]
mov [rax+4*6], edx
mov edx, [rcx+4*3]
mov [rax+4*7], edx
mov edx, [rcx+4*4]
mov [rax+4*8], edx
mov edx, [rcx+4*5]
mov [rax+4*9], edx
mov edx, [rcx+4*6]
mov [rax+4*10], edx
mov edx, [rcx+4*7]
mov [rax+4*11], edx
ret
asm:
chacha20_sigma: db "expand 32-byte k"
в state остаются незаполненными индексы 12,13,14,15. их потом заполнит chacha20_setupiv:
asm:
chacha20_setupiv:
mov rax, r8
mov rdx, r9
mov ecx, [rdx+4*0]
mov [rax+4*12], ecx
mov ecx, [rdx+4*1]
mov [rax+4*13], ecx
xor rcx,rcx
mov [rax+4*14], ecx
mov [rax+4*15], ecx
ret
далее в коде вызывается метка chacha20_encrypt:
C:
chacha20_encrypt:
push rbp
mov rbp, rsp
sub rsp, 16*4 ; j
sub rsp, 16*4 ; x
sub rsp, 64 ; tmp
mov rax, r11
test rax, rax
jz done
push rsi
push rdi
push rbx
mov rax, r8
mov rbx, rbp
sub rbx, 16*4
mov rcx, 15
.jloop:
mov edx, [rax+4*rcx]
mov [rbx+4*rcx], edx
dec rcx
jge .jloop
на вход получается что она получает регистры r8(state) и уже настроенным ключом и iv, r9 - это же указатель на открый текст, r10 же это указатель на буфер куда класть шифротекст, r11 размер данных в байтах.
дальше резервирует место на стеке под три локальных массива:
asm:
sub rsp, 16*4 ; j
sub rsp, 16*4 ; x
sub rsp, 64 ; tmp
То есть после тоо кеак исходное копируется в j, формируется рабочая копия x, которая передается в функцию хэширования:
asm:
chacha20_hash:
push rbp
push rsi
mov rbp, 10
.roundLoop:
call chacha20_DoubleRound
dec rbp
jnz .roundLoop
pop rsi
Каждый дабл раунд состиот из чередования CR/DR, в ходе которых все 16 слов состояния перемешиваются с помощью трех базовых операций которые я описывал выше(сложение по модулю, ксор и циклический сдвиг).
CR/DR это два способа примененияя одной и той же операции, но к разным наобором слов состояния
CR - обрабатывает состяоние по 4x4 матрицы:
(0, 4, 8, 12)
(1, 5, 9, 13)
(2, 6, 10, 14)
(3, 7, 11, 15)
То есть берутся вертикальные группы слов и к каждой применяется QuarterRound, это просто перешивает данные внутри колонок
DR же затем берет те же 16 слов, но уже в диагональных комбинациях:
(0, 5, 10, 15)
(1, 6, 11, 12)
(2, 7, 8, 13)
(3, 4, 9, 14)
И также перешивает уже между разными колонками.
После 20 раундов получается рабочее состояние, которое затем поэлементарно складывается с исхходным состоянием j по модулю 2<32>, что является финальным этапом формирования ключа.
-
После обработки каждого 64 байтного блока величвается счетчик в состоянии, это нужно для того, чтобыдля каждого следующего блока генерировался уникальный keystream.
Если же счетик не увеличить, то при одинаковых ключе и nonce будет получаться один и тот же поток, а значит одинаковый keystream будет накладываться на разные данные.
nonce - это уникальное значение, которое вместе с ключом используется для генерации ключа.
C:
sub rbp, 16*4
inc qword [rbp+4*12] ;// инкремент/увелечение счетчика блока
mov rax, [rbp+4*12
test rax, rax
jnz .noNoneOverflow
inc qword [rbp+4*14]
.noNoneOverflow:
add rbp, 16*4
mov rax, rbp
sub rax, 16*4+16*4
mov rbx, r10
mov rcx, 15
Если происходит переполнение младшего слова счётчика, управление переносом переходит в старшее слово, что позволяет сохранить корректную непрерывность нумерации блоков в пределах 64-битного диапазона, после чего же вычисляется адрес текущего блока состояния, который используется для генерации keystream через функцию хеширования чачи. Полученный результат затем применяется к входным данным - каждый 32-битный элемент keystream поэлементно комбинируется с соответствующими байтами открытого текста через XOR, формируя шифротекст.
Кароче мне лень писать дальше, я надеюсь все понятно.
Работа этого алгоритма заключается в том, что просто берется определенное значение, ОНО складывается по модулю 2<32>, после чего это же значение XOR-ится и реализуется циклический сдвиг ROL, после всех этих манипуляций мы получаем новое значение, это значение просто XOR-ится и в итоге получаются расшифрованные данные.
Процесс шифрования:
Где:
ASM:
mov r8, state
mov r9, key
call chacha20_setupkey32
c:
chacha20_sigma: db "expand 32-byte k"
S = (C,K,?,?)Далее процесс установки nonce и счетчика:
asm:
mov r9, n
call chacha20_setupiv
nonce - уникальный идентификатор потока, а counter номер блока(0 при старте)
Итоговое состояние:
S0 = (C, K, counter, nonceДалее уже идет процесс подготовки данных:
asm:
mov r9, shellcode
mov r10, cmsg
movzx r11, byte [msize]
r10 - буфер для
r11 - длина данных
после чего вызывается функция шифрования
call chacha20_encrypt, после которой происходит:1. копирование состояние
2. выполнение 20 раундом чачи
3. генерация кейстрим блока из 64 байта
4. xor входными данными:
C = P ⊕ KS
asm:
chacha20_encrypt:
push rbp
mov rbp, rsp
sub rsp, 16*4 ; j
sub rsp, 16*4 ; x
sub rsp, 64 ; tmp
mov rax, r11
test rax, rax
jz done
push rsi
push rdi
push rbx
mov rax, r8
mov rbx, rbp
sub rbx, 16*4
mov rcx, 15
.jloop:
mov edx, [rax+4*rcx]
mov [rbx+4*rcx], edx
dec rcx
jge .jloop
.mainLoop:
cmp r11, 64
jge .dontUseTmp
mov rbx, r9
mov rcx, r11
dec rcx
mov rdx, rbp
sub rdx, (16*4)+(16*4)+64
.tmploop:
mov al, byte [rbx+rcx]
mov [rdx+rcx], al
dec rcx
jge .tmploop
mov r9, rdx
mov rax, r10
mov r12, rax
mov r10, rdx
.dontUseTmp:
mov rax, rbp
sub rax, 16*4
mov rbx, rbp
sub rbx, (16*4)+(16*4)
mov rcx, 15
.xloop:
mov edx, [rax+4*rcx]
mov [rbx+4*rcx], edx
dec rcx
jge .xloop
mov rsi, rbp
sub rsi, 16*4
mov rdi, rbp
sub rdi, (16*4)+(16*4)
call chacha20_hash
mov rax, r9
mov rcx, 15
.hashloop:
mov edx, [rax+4*rcx]
xor [rdi+4*rcx], edx
dec rcx
jge .hashloop
sub rbp, 16*4
inc qword [rbp+4*12]
mov rax, [rbp+4*12]
test rax, rax
jnz .noNoneOverflow
inc qword [rbp+4*14]
.noNoneOverflow:
add rbp, 16*4
mov rax, rbp
sub rax, 16*4+16*4
mov rbx, r10
mov rcx, 15
.cipherloop:
mov edx, [rax+4*rcx]
mov [rbx+4*rcx], edx
dec rcx
jge .cipherloop
mov rax, r11
cmp rax, 64
jg .noLastBE
jge .noLastB
mov rax, r10
mov rbx, r12
mov rcx, r11
dec rcx
.copyloop:
mov dl, [rax+rcx]
mov [rbx+rcx], dl
dec rcx
jge .copyloop
.noLastB:
mov rdx, r8
sub rbp, 16*4
mov rax, [rbp+4*12]
mov [rdx+4*14], rax
mov rax, [rbp+4*14]
mov [rdx+4*15], rax
add rbp, 16*4
jmp cleanup
.noLastBE:
sub r11, 64
add r10, 64
add r9, 64
jmp .mainLoop
cleanup:
pop rbx
pop rdi
pop rsi
done:
add rsp, 64
add rsp, 16*4
add rsp, 16*4
pop rbp
ret
в итоге вся выпочка детерминированное преобразование:
(C,K,nonce,counter) -> KS -> Cгде безопасность определяется стойкостью генератора псевдослучайного потока, а XOR лишь применяется как финальная операция наложения
2. Технический анализ исходного кода алгоритма шифрования(LockBit3.0)
Эту часть статьи можно тоже отнести к практике, ведь мы будем разбирать реальный код, кароче интерпретируйте как хотите, не важно.Локер разделяется на несколько заголовочных файлов и одним главным:
chacha.h - публичный криптографический интерфейс алгоритма.
внутри:
C:
typedef struct {
uint32_t input[16];
} chacha_ctx;
1. константы
2. ключ
3. счетчик
4. nonce.
прям в точности как мы разбирали в статье:)
Также вызывается парочка функций:
Первая функция
chacha_keysetup загружает ключ, строит базовое состояние через(константа + ключ).Аналог инициализации: state[0..11]
Вторая функция
chacha_ivsetup загружает IV / nonce, после чего задает уникальность потока.Третья функция
chacha_encrypt стоит в роле основного потокового xor-движка, его задача это сделать генерацию keystream, ксор с plaintexto-м, это одинаковая функция для шифрования/дешифрования.encrypt-machine.h - это слой адптации под железо и компилятор, разбирать не буду.
encrypt-sync.h - выполняет задачу стандартного интерфейса потокового шифра.
chacha.c - main, начнем разбор главного .C-шного файла.
В макросах определяется алгебра состояния:
cc:
#define ROTATE(v,c) (ROTL32(v,c))
#define XOR(v,w) ((v) ^ (w))
#define PLUS(v,w) (U32V((v) + (w)))
PLUS - сложение по модулю 2<32>
XOR - операция векторного пространства GF(2)
ROTATE - перестановка битов
PLUSONE - инкремент счетичка битов.
Теперь разберу только самые важные функции, которые поймет читатель который понял базовый принцип работы Чачи.
[CODE lang="c" title="C"]void ECRYPT_keysetup(ECRYPT_ctx* x, const u8* k, u32 kbits, u32 ivbits)
C:
const char* constants;
x->input[4] = U8TO32_LITTLE(k + 0);
x->input[5] = U8TO32_LITTLE(k + 4);
x->input[6] = U8TO32_LITTLE(k + 8);
x->input[7] = U8TO32_LITTLE(k + 12);
if (kbits == 256) { /* recommended */
k += 16;
constants = sigma;
}
else { /* kbits == 128 */
constants = tau;
}
x->input[8] = U8TO32_LITTLE(k + 0);
x->input[9] = U8TO32_LITTLE(k + 4);
x->input[10] = U8TO32_LITTLE(k + 8);
x->input[11] = U8TO32_LITTLE(k + 12);
x->input[0] = U8TO32_LITTLE(constants + 0);
x->input[1] = U8TO32_LITTLE(constants + 4);
x->input[2] = U8TO32_LITTLE(constants + 8);
x->input[3] = U8TO32_LITTLE(constants + 12);
C:
STATE = [
CONSTANTS (128 bit),
KEY (256 or 128 bit),
COUNTER (64 bit),
NONCE (64 bit)
]
ENCRYPT_ivsetup:
C:
void ECRYPT_ivsetup(ECRYPT_ctx* x, const u8* iv)
{
x->input[12] = 0;
x->input[13] = 0;
x->input[14] = U8TO32_LITTLE(iv + 0);
x->input[15] = U8TO32_LITTLE(iv + 4);
}
Сброс счетчика генерации:
с:
x->input[12] = 0;
x->input[13] = 0;
Далее задача nonce:
C:
x->input[14] = U8TO32_LITTLE(iv + 0);
x->input[15] = U8TO32_LITTLE(iv + 4);
Далее ECRYPT_encrypt_bytes:
C:
void ECRYPT_encrypt_bytes(ECRYPT_ctx* x, const u8* m, u8* c, u32 bytes)
{
u32 x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15;
u32 j0, j1, j2, j3, j4, j5, j6, j7, j8, j9, j10, j11, j12, j13, j14, j15;
u8* ctarget = 0;
u8 tmp[64];
int i;
if (!bytes) return;
j0 = x->input[0];
j1 = x->input[1];
j2 = x->input[2];
j3 = x->input[3];
j4 = x->input[4];
j5 = x->input[5];
j6 = x->input[6];
j7 = x->input[7];
j8 = x->input[8];
j9 = x->input[9];
j10 = x->input[10];
j11 = x->input[11];
j12 = x->input[12];
j13 = x->input[13];
j14 = x->input[14];
j15 = x->input[15];
for (;;) {
if (bytes < 64) {
for (i = 0; i < bytes; ++i) tmp[i] = m[i];
m = tmp;
ctarget = c;
c = tmp;
}
x0 = j0;
x1 = j1;
x2 = j2;
x3 = j3;
x4 = j4;
x5 = j5;
x6 = j6;
x7 = j7;
x8 = j8;
x9 = j9;
x10 = j10;
x11 = j11;
x12 = j12;
x13 = j13;
x14 = j14;
x15 = j15;
for (i = 8; i > 0; i -= 2) {
QUARTERROUND(x0, x4, x8, x12)
QUARTERROUND(x1, x5, x9, x13)
QUARTERROUND(x2, x6, x10, x14)
QUARTERROUND(x3, x7, x11, x15)
QUARTERROUND(x0, x5, x10, x15)
QUARTERROUND(x1, x6, x11, x12)
QUARTERROUND(x2, x7, x8, x13)
QUARTERROUND(x3, x4, x9, x14)
}
x0 = PLUS(x0, j0);
x1 = PLUS(x1, j1);
x2 = PLUS(x2, j2);
x3 = PLUS(x3, j3);
x4 = PLUS(x4, j4);
x5 = PLUS(x5, j5);
x6 = PLUS(x6, j6);
x7 = PLUS(x7, j7);
x8 = PLUS(x8, j8);
x9 = PLUS(x9, j9);
x10 = PLUS(x10, j10);
x11 = PLUS(x11, j11);
x12 = PLUS(x12, j12);
x13 = PLUS(x13, j13);
x14 = PLUS(x14, j14);
x15 = PLUS(x15, j15);
x0 = XOR(x0, U8TO32_LITTLE(m + 0));
x1 = XOR(x1, U8TO32_LITTLE(m + 4));
x2 = XOR(x2, U8TO32_LITTLE(m + 8));
x3 = XOR(x3, U8TO32_LITTLE(m + 12));
x4 = XOR(x4, U8TO32_LITTLE(m + 16));
x5 = XOR(x5, U8TO32_LITTLE(m + 20));
x6 = XOR(x6, U8TO32_LITTLE(m + 24));
x7 = XOR(x7, U8TO32_LITTLE(m + 28));
x8 = XOR(x8, U8TO32_LITTLE(m + 32));
x9 = XOR(x9, U8TO32_LITTLE(m + 36));
x10 = XOR(x10, U8TO32_LITTLE(m + 40));
x11 = XOR(x11, U8TO32_LITTLE(m + 44));
x12 = XOR(x12, U8TO32_LITTLE(m + 48));
x13 = XOR(x13, U8TO32_LITTLE(m + 52));
x14 = XOR(x14, U8TO32_LITTLE(m + 56));
x15 = XOR(x15, U8TO32_LITTLE(m + 60));
j12 = PLUSONE(j12);
if (!j12) {
j13 = PLUSONE(j13);
/* stopping at 2^70 bytes per nonce is user's responsibility */
}
U32TO8_LITTLE(c + 0, x0);
U32TO8_LITTLE(c + 4, x1);
U32TO8_LITTLE(c + 8, x2);
U32TO8_LITTLE(c + 12, x3);
U32TO8_LITTLE(c + 16, x4);
U32TO8_LITTLE(c + 20, x5);
U32TO8_LITTLE(c + 24, x6);
U32TO8_LITTLE(c + 28, x7);
U32TO8_LITTLE(c + 32, x8);
U32TO8_LITTLE(c + 36, x9);
U32TO8_LITTLE(c + 40, x10);
U32TO8_LITTLE(c + 44, x11);
U32TO8_LITTLE(c + 48, x12);
U32TO8_LITTLE(c + 52, x13);
U32TO8_LITTLE(c + 56, x14);
U32TO8_LITTLE(c + 60, x15);
if (bytes <= 64) {
if (bytes < 64) {
for (i = 0; i < bytes; ++i) ctarget[i] = c[i];
}
x->input[12] = j12;
x->input[13] = j13;
return;
}
bytes -= 64;
c += 64;
m += 64;
}
}
Извлечение состояния:
C:
j0..j15 = x->input[0..15];
for (;;) { - это обработка потока данных по блокам 64 байта, т.к чача работает строго блоками по 64 байта чито равно 512 битам, каждая итерация цикла = один криптографический блок.
Далее идет обработка неполного блока:
C:
if (bytes < 64) {
for (i = 0; i < bytes; ++i) tmp[i] = m[i];
m = tmp;
ctarget = c;
c = tmp;
}
x0..x15 = j0..j15;Тут создается локальная копия state, j - неизменяемое состояние, x - временная матрица для раундов
И 20 раундов перешивания:
C:
for (i = 8; i > 0; i -= 2)
CR(работа по столбцам)
C:
QUARTERROUND(x0, x4, x8, x12)
QUARTERROUND(x1, x5, x9, x13)
QUARTERROUND(x2, x6, x10, x14)
QUARTERROUND(x3, x7, x11, x15)
Далее идет DR(диагонали):
C:
QUARTERROUND(x0, x5, x10, x15)
QUARTERROUND(x1, x6, x11, x12)
QUARTERROUND(x2, x7, x8, x13)
QUARTERROUND(x3, x4, x9, x14)
Далее финализация раунда:
c:
x0 = j0;
x1 = j1;
x2 = j2;
x3 = j3;
x4 = j4;
x5 = j5;
x6 = j6;
x7 = j7;
x8 = j8;
x9 = j9;
x10 = j10;
x11 = j11;
x12 = j12;
x13 = j13;
x14 = j14;
x15 = j15;
Далее превращение state в keystream через КСОР:
с:
x0 = XOR(x0, U8TO32_LITTLE(m + 0));
x1 = XOR(x1, U8TO32_LITTLE(m + 4));
x2 = XOR(x2, U8TO32_LITTLE(m + 8));
x3 = XOR(x3, U8TO32_LITTLE(m + 12));
x4 = XOR(x4, U8TO32_LITTLE(m + 16));
x5 = XOR(x5, U8TO32_LITTLE(m + 20));
x6 = XOR(x6, U8TO32_LITTLE(m + 24));
x7 = XOR(x7, U8TO32_LITTLE(m + 28));
x8 = XOR(x8, U8TO32_LITTLE(m + 32));
x9 = XOR(x9, U8TO32_LITTLE(m + 36));
x10 = XOR(x10, U8TO32_LITTLE(m + 40));
x11 = XOR(x11, U8TO32_LITTLE(m + 44));
x12 = XOR(x12, U8TO32_LITTLE(m + 48));
x13 = XOR(x13, U8TO32_LITTLE(m + 52));
x14 = XOR(x14, U8TO32_LITTLE(m + 56));
x15 = XOR(x15, U8TO32_LITTLE(m + 60));
все по формуле:
ciphertext = plaintext XOR keystreamИ далее обновляется счетчик блока:
C:
j12 = PLUSONE(j12);
if (!j12) j13 = PLUSONE(j13);
После чего результат записывается в выходной буфер:
c:
U32TO8_LITTLE(c + 0, x0);
// и еще снизу есть, символы занимают не буду добавлять
U32TO8_LITTLE(c + 60, x15);
После чего блок завершается:
C:
if (bytes <= 64) {
// еще и снизу есть, символы занимают опять же
x->input[12] = j12;
x->input[13] = j13;
return;
}
Думаю описал нормально, эта часть главы вышла для тех, кто не понял то, что было выше, или понял слабо.
Суть самого алгоритма шифрования я думаю понятно.
====================================================================================================================
Полезные материалы по криптографии:
1.
Пожалуйста, авторизуйтесь для просмотра ссылки.
Описание книги вкратце:
2.До появления настоящей монографии практикам приходилось тратить многие часы на поиск и изучение научной литературы, прежде чем они могли приступить к разработке криптографических приложений. Именно этот пробел восполняет книга "Прикладная криптография". Начав с целей засекречивания передачи данных и простейших примеров программ для достижения этих целей, Брюс Шнайер разворачивает перед читателем всю панораму практических результатов 20 лет исследований В книге Брюса Шнайера "Прикладная криптография. Протоколы, алгоритмы, исходные тексты на языке C(Си)" в деталях описаны принципы работы, реализации и примеры использования криптографических алгоритмов, является самой читаемой книгой по криптографии в мире!
Книга Брюса Шнайера Прикладная криптография: протоколы, алгоритмы, исходный код на языке C предназначена современным программистам и инженерам, которым необходимо использовать криптографию.
Пожалуйста, авторизуйтесь для просмотра ссылки.
.3.
Пожалуйста, авторизуйтесь для просмотра ссылки.
Книга реальная криптография автор Дэвид Вонг это практическое введение в современные криптографические протоколы где основное внимание уделяется не математическим доказательствам а реальным механизмам работы шифрования аутентификации и обмена ключами в сетевых системах, в книге объясняется почему простые схемы вроде базового симметричного шифрования недостаточны без правильного управления ключами и как ошибки реализации приводят к критическим уязвимостям рассматриваются базовые примитивы такие как хеш функции поточные и блочные шифры генераторы псевдослучайных чисел и протоколы публичного ключа, отдельно разбираются практические схемы вроде TLS и обмена ключами Диффи Хеллмана с акцентом на то как они устроены на уровне протокола а не только формул, главная идея книги в том что криптография это не просто математика а система инженерных решений где важны контекст использования ошибки интеграции и модель угроз.
====================================================================================================================
На этом все, надеюсь статья была полезной, как по мне она немного поплыла, но вроде нормально, кто хоть как то разбирается поймет.
Писал под эпинефрином просто.
Пакааа