From: Bob <ubob at mail.ru>
Newsgroups: email
Date: Fri, 2 Jul 2004 14:31:37 +0000 (UTC)
Subject: Разработка модуля Linux ядра, реализующего алгоритм криптозащиты ГОСТ 28147-89
РАЗРАБОТКА МОДУЛЯ ЯДРА ОПЕРАЦИОННОЙ СИСТЕМЫ LINUX, РЕАЛИЗУЮЩЕГО
АЛГОРИТМ КРИПТОГРАФИЧЕСКОЙ ЗАЩИТЫ ИНФОРМАЦИИ ГОСТ 28147-89
1. Введение
---------------------
Без использования криптографии сегодня немыслимо решение задач по обеспечению
безопасности информации, связанных с конфиденциальностью и целостностью,
аутентификацией и невозможностью отказа от авторства. Если до 1990 г.
криптография обеспечивала защиту исключительно государственных линий связи, то
в наши дни использование криптографических методов получило широкое
распространение благодаря развитию компьютерных сетей и электронного обмена
данными в различных областях: финансах, банковском деле, торговле и т.д.
2. Управление ключами
------------------------
Важнейшую роль в криптографии играет управление ключами. Это основа для
обеспечения конфиденциальности обмена информацией, идентификации и целостности
данных.
Целью управления ключами является нейтрализация таких угроз, как:
- компрометация конфиденциальности секретных ключей;
- компрометация аутентичности секретных ключей и открытых ключей. При этом под
аутентичностью понимается знание или возможность проверки идентичности
корреспондента, для обеспечения конфиденциальной связи с которым используется
данный ключ;
- несанкционированное использование секретных или открытых ключей, например
использование ключа, срок действия которого истек.
Для снижения риска компрометации конфиденциальности секретных ключей необходимо
ограничить круг лиц, у которых есть доступ к ключевой информации. Работать с
ключами должен специально подготовленный персонал, имеющий соответствующие
полномочия (допуск).
Исходя из этого требования, разработаем модель системы криптографической защиты
информации (СКЗИ), в которой доступ к ключам имеет только администратор системы.
3. Назначение, состав и алгоритм функционирования СКЗИ
----------------------------------------------------------------
Назначение СКЗИ - криптографическое преобразование (шифрование/расшифрование)
файлов пользователя в соответствии с алгоритмом ГОСТ 28147-89, режим
гаммирования. Описание стандарта ГОСТ 28147-89 можно взять здесь:
http://www.enlight.ru/crypto/articles/vinokurov/gost_i.htm
В состав СКЗИ входят следующие структурные элементы:
- криптографический драйвер (далее драйвер). Непосредственно
выполняет операцию криптографического преобразования информации в соответствии
с алгоритмом ГОСТ 28147-89. Содержит блок криптографического преобразования
(БКП) и блок хранения ключевой информации (БКИ);
- модуль ключевых данных (МКД). Выполняет генерацию ключевых данных и их
последующую запись в БКИ драйвера;
- модуль взаимодействия с драйвером (МВ). Представляет собой приложение
пользователя, и выполняет передачу драйверу информационных блоков, подлежащих
криптопреобразованию.
Алгоритм функционирования СКЗИ следующий:
- модуль МВ считывает из файла блок данных и вызывает системную функцию
sys_gost(), передав ей блок данных в качестве параметра. Но функция sys_gost()
никаких преобразований над данными не выполняет;
- драйвер после загрузки перехватывает системный вызов sys_gost(), и все обращения
МВ к функции sys_gost() будут обслуживаться драйвером. Если ключевые данные
не введены в БКИ, драйвер также не будет выполнять криптографических
преобразований, возвращая модулю МВ данные в неизмененном виде.
Только после того, как МКД выполнит генерацию ключевых данных и их запись в
БКИ драйвера, данные, передаваемые драйверу, будут поступать на вход БКП.
Сразу отметим, что операции загрузки драйвера и записи в него ключевых данных
являются привилегированными. Выполнить их может только администратор системы,
имеющий права root. Таким образом выполняется требование по ограничению доступа
персонала к ключевой информации.
4. Системный вызов sys_gost
------------------------------------
Как было сказано выше, в состав ядра Linux вводится новый системный вызов.
Методика добавления в ядро новых системных вызовов была рассмотрена здесь:
http://opennet.ru/base/dev/new_linux_syscall.txt.html
- добавляем запись о системном вызове sys_gost() в таблицу системных вызовов
sys_call_table (файл arch/i386/kernel/entry.S):
ENTRY(sys_call_table)
.long SYMBOL_NAME(sys_ni_syscall) /* 0 - old "setup()" system call*/
.
.
.
.long SYMBOL_NAME(sys_gost) /* sys_gost call 259 */
Системный вызов sys_gost() имеет порядковый номер 259 (ядро версии 2.4.26).
- в файл include/asm-i386/unistd.h добавляем запись:
#define __NR_gost 259
- в файл /usr/include/bits/syscall.h добавляем запись:
#define SYS_gost __NR_gost
- в файл fs/open.c добавим код, реализующий системный вызов sys_gost():
asmlinkage long sys_gost(unsigned char *block, size_t count, int mode)
{
return 0;
}
Системный вызов sys_gost принимает три параметра: указатель на блок с данными,
размер блока и флаг, информирующий о начале/завершении операции
криптопреобразования.
- в каталоге /usr/include создаем файл gost.h следующего содержания:
E_START и E_STOP - флаги, информирующие о начале/завершении операции
криптографического преобразования.
После внесения всех изменений ядро необходимо перекомпилировать.
5. Реализация СКЗИ
----------------------
5.1. Драйвер
Задача драйвера - перехватить все обращения к системной функции sys_gost,
сохранить в БКИ ключевые данные, переданные модулем МКД, и приступить к
выполнению операций криптографического преобразования блоков данных, поступающих
от модуля МВ. Драйвер является символьным устройством. Создадим для него файл
устройства следующей командой:
mknod /dev/gost c 69 0
Адреса всех системных вызовов хранятся в специальной таблице - таблице
системных вызовов (далее ТСВ). Для обращения к той или иной системной функции
ядро извлекает адрес этой функции из ТСВ, и передает управление по этому
адресу.
Для перехвата обращений к системной функции sys_gost() драйверу необходимо в
ТСВ произвести замену адреса функции sys_gost адресом нового обработчика.
Чтобы выполнить эту операцию, драйвер должен знать адрес самой ТСВ. Т.к. все
системные вызовы выполняются через программное прерывание int 0x80, то адрес
таблицы можно извлечь из обработчика этого прерывания - функции system_call.
Определение функции system_call находится в файле arch/i386/kernel/entry.S.
Рассмотрим, как выполняется обращение к системной функции. В функции system_call
это выглядит следующим образом:
В регистре EAX находится номер системного вызова. Управление передается по
адресу, который находится в ТСВ по смещению ([EAX] * 4) относительно базового
адреса таблицы sys_call_table, т.к. адрес каждого вызова занимает 4 байта.
Данный вызов равнозначен выражению на С:
/* вызов функции в массиве функций */
(sys_call_table[eax])()
Найдем опкод команды обращения к системной функции. Для этого загрузим ядро в
отладчик:
Таким образом, чтобы извлечь из функции system_call адрес ТСВ, достаточно найти
в теле функции сигнатуру xffx14x85. Следующие за ней 4 байта будут искомым
адресом таблицы.
Но для того, чтобы выполнить этот поиск, необходимо знать адрес самой функции
system_call. Эта задача решается просто: поскольку функция system_call
является обработчиком прерывания int 0x80, ее адрес находится в таблице
дескрипторов прерываний, Interrupt Descriptor Table (IDT). Адрес IDT хранит
специальный регистр IDTR, получить значение из которого можно при помощи
инструкции SIDT.
Алгоритм поиска адреса таблицы системных вызовов выглядит следующим образом:
- извлечь из регистра IDTR адрес таблицы IDT;
- из таблицы IDT извлечь адрес обработчика программного прерывания int 0x80.
Это будет адрес функции system_call;
- в теле функции system_call выполнить поиск сигнатуры xffx14x85 и считать
следующие за ней 4 байта - искомый адрес ТСВ.
/* Структура регистра IDTR */
struct {
__u16 limit;
__u32 base; // адрес таблицы IDT
} __attribute__ ((packed)) idtr;
__u32 get_sct_addr()
{
int i;
__u32 sct; // в этой переменной сохраним адрес таблицы
__u32 idt_addr = 0; // адрес таблицы IDT
__u32 sc_addr = 0; // адрес ф-ии system_call
__u8 sc[INT80_LEN]; // сюда скопируем первые 128 байт функции system_call
__u8 p[4]; // вспомогательный буфер
Определим дополнительно заголовочный файл gost_var.h, в котором разместим
вспомогательные переменные и структуры, необходимые для реализации алгоритма
ГОСТ 28147-89. Файл имеет следующее содержание:
/*
* Листинг 2. Файл gost_var.h
*/
#include <linux/types.h>
#ifndef C1
#define C1 0x01010104
#endif
#ifndef C2
#define C2 0x01010101
#endif
__u32 SM1, SM2, N3, N4;
int key_enable; // флаг наличия ключевой информации в БКИ
int key_init; // флаг инициализации ключей
/*
* Структура для хранения ключевой информации, переданной модулем МКД.
* В соответствии с алгоритмом (см. п. 2), данная структура является БКИ
*/
struct key_info {
__u8 key_d[64];
__u32 X[8];
__u8 sp[8];
} keys;
Значения C1 и C2 определены ГОСТ 28147-89. Переменные SM1 и SM2, в соответствии
с терминологией ГОСТ, назовем сумматорами, а N1, N2, N3, N4 - накопителями.
Поиск адреса таблицы системных вызовов будет выполнен во время инициализации
драйвера. Рассмотрим следующий листинг:
/*
* Листинг 3. Функция инициализации драйвер и загрузки ключей в БКИ
* (файл sys_call_gost.c)
*/
/*
* Прототипы функций поиска адреса ТСВ и нового обработчика системного
* вызова sys_gost
*/
__u32 get_sct_addr();
int new_gost(__u8 *, size_t, int);
__u32 old_sys_gost_addr = 0; // здесь сохраним старый адрес sys_gost
__u32 sct = 0; // адрес ТСВ
/*
* Функция записи в устройство. В процессе записи драйверу передается структура
* типа struct key_info (см. Листинг 2), содержащая ключевую информацию
*/
static ssize_t write_gost(struct file *file, const char *buf, size_t count, loff_t *ppos)
{
if(count != sizeof(struct key_info)) return -EINVAL;
copy_from_user((char *)&keys, buf, count);
/*
* Устанавливаем флаг наличия ключевой информации в БКИ
*/
key_enable = 1;
return count;
}
/*
* Функция открытия файла устройства
*/
static int open_gost(struct inode *inode, struct file *file)
{
if(MOD_IN_USE) return -EBUSY;
if (MINOR(inode->i_rdev) != 0) return -ENODEV;
/*
* Проверяем режим открытия файла устройства. Файл должен быть
* открыт в режиме "Только для записи"
*/
if ((file->f_flags & O_ACCMODE) != O_WRONLY) return -EACCES;
/*
* Функция закрытия файла устройства. Ничего не выполняет,
* просто уменьшает счетчик использования драйвера
*/
static int close_gost(struct inode *inode, struct file *file)
{
MOD_DEC_USE_COUNT;
return 0;
}
/*
* Функция инициализация драйвера. Выполняет регистрацию устройства
* в системе и производит замену адресов в ТСВ
*/
static int __init init_gost()
{
if(register_chrdev(GOST_MAJOR, "/dev/gost", &gost_fops)) return -EIO;
/*
* Определяем адрес ТСВ и заменяем адрес системного вызова sys_gost
* адресом нового обработчика - функции new_gost. Адрес "старого" вызова
* сохраняем в переменной old_sys_gost_addr
*/
sct = get_sct_addr();
old_sys_gost_addr = *(__u32 *)(sct + SYS_gost * 4);
*(__u32 *)(sct + SYS_gost * 4) = (__u32)&new_gost;
Рассмотрим функцию new_gost, которая заменит системный вызов sys_gost.
Эта функция непосредственно выполняет операции криптографического преобразования
блоков данных, переданных драйверу модулем МВ.
/*
* Листинг 4. Функция, выполняющая операции криптографического преобразования
* информации (файл gost.c)
*/
/*
* Функция new_gost принимает три параметра - указатель на блок данных,
* подлежащих криптопреобразованию, размер блока и флаг управления режимом
*/
int new_gost(__u8 *block, size_t count, int flag)
{
int i;
/*
* Если ключи не загружены, флаг key_enable установлен в 0. В этом
* случае драйвер никаких криптопреобразований не выполняет
*/
if(!key_enable) {
printk(KERN_INFO "Keys not loadedn");
return 1;
}
/*
* Проверяем значения флагов flag и key_enable. Если оба флага установлены,
* приступаем к выполнению операции криптопреобразования блока данных
*/
if((flag && key_enable)) {
/*
* Все дальнейшие операции выполняются в соответствии с алгоритмом
* ГОСТ 28147-89, режим гаммирования.
*
* Инициализируем ключи
*/
if(!key_init) init_key();
/*
* Суммируем значение накопителя N3 и константы C2 по модулю (2^32)
*/
N3 += C2;
nac.lg.N1 = N3;
nac.lg.N2 = N4;
/*
* Шифруем содержимое накопителей N1 и N2 в режиме простой замены
* алгоритма ГОСТ 28147-89
*/
simple_replace_crypt();
/*
* Выполняем криптопреобразование блока данных и возвращаем модулю МВ
* размер этого блока
*/
for(i = 0; i < count; i++) block[i] ^= nac.N[i];
return count;
}
/*
* Если установлен флаг завершения операции криптопреобразования
* (flag = E_STOP), сбрасываем флаг инициализации ключей и возвращаем
* нулевое значение модулю МВ
*/
key_init = 0;
return 0;
}
/*
* Функция simple_replace_crypt() выполняет операцию шифрования в режиме
* простой замены алгоритма ГОСТ 28147-89
*/
void simple_replace_crypt()
{
int i,n;
/*
* В соответствии с алгоритмом ГОСТ 28147-89, выполняем 31 цикл преобразования
*/
for(n = 1; n < 4; n++) {
for(i = 0; i < 8; i++) op(i);
}
for(i = 7; i > 0; i--) op(i);
/*
* Функция init_key() выполняет процедуру инициализации ключевых данных
*/
void init_key()
{
/*
* Копируем значение синхропосылки с накопители N1 и N2
*/
memset(&nac, 0, sizeof(nac));
memcpy(nac.N, keys.sp, 8);
/*
* Шифруем содержимое накопителей N1 и N2 в режиме простой замены
* алгоритма ГОСТ 28147-89
*/
simple_replace_crypt();
N3 = nac.lg.N1;
N4 = nac.lg.N2;
/*
* Устанавливаем значение флага инициализации ключей в 1
*/
key_init = 1;
return;
}
/*
* В функции block об`единены блок подстановки (главный структурный элемент
* алгоритма) и регистр сдвига. Параметры функции:
* - значение сумматора SM1
* - указатель на блок данных, содержащих долговременный ключ
* - значение накопителя N2
* Результат, возвращаемый функцией block, будет помещен в сумматор SM2
*/
__u32 block(__u32 SM1, __u8 *k, __u32 N2)
{
__u32 SM2;
5.2. Модуль ключевых данных
---------------------------
Модуль ключевых данных (МКД) выполняет две задачи:
- генерацию ключевых данных;
- запись ключевых данных в БКИ драйвера.
Ключевые данные представляют собой три файла фиксированного размера:
- файл долговременного ключа key_d длиной 64 байт;
- файл сеансового ключа key_s длиной 32 байт;
- файл синхропосылки key_sp длиной 8 байт.
В соответствии с выполняемыми функциями, в состав МКД будут входить:
- генератор ключевых данных;
- модуль записи ключевых данных в БКИ драйвера.
Код очень простой, поэтому приводится без комментариев. Для получения
случайной последовательности мы воспользовались файлом /dev/urandom.
/*
* Листинг 6. Модуль записи ключевых данных в БКИ драйвера (файл write_key.c)
*/
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#define KEYDIR "./" // ключи будут находиться в текущем каталоге
/*
* Структура struct key_struct будет передана драйверу для записи в БКИ
*/
struct key_struct {
char key_d[64];
unsigned long X[8];
char sp[8];
} __attribute ((packed)) keys;
char tmp_buff[104];
struct struct_key {
char *k_name;
int k_size;
} __attribute__ ((packed));
Для предотвращения компрометации ключевые данные целесообразно хранить на
отдельном носителе, например, на дискете. При этом администратору необходимо
обеспечить соответствующие условия хранения данного носителя.
5.3. Модуль взаимодействия с драйвером
--------------------------------------
Модуль взаимодействия с драйвером (МВ) представляет собой приложение
пользователя. Его задача - обмен информационными блоками с драйвером и
сохранение результатов в файле.
/*
* Листинг 7. Модуль взаимодействия с драйвером (файл test_gost.c)
*/
#include <stdio.h>
#include <gost.h>
#include <fcntl.h>
int main(int argc, char **argv)
{
int in, n;
unsigned char buff[8];
/*
* Параметром модуля МВ является имя файла, содержание которого подлежит
* криптографическому преобразованию
*/
if(argc != 2) {
printf("ntUsage: gost [input file]nn");
return 0;
}
/*
* Считываем из входного файла блоки данных длиной 8 байт
*/
while((n = read(in, buff, 8)) > 0) {
/*
* Вызываем системную функцию sys_gost для выполнения операции
* криптографического преобоазования. Результат записываем в тот же файл
*/
gost(buff, n, E_START);
lseek(in, -n, SEEK_CUR);
write(in, buff, n);
}
/*
* Информируем драйвер о завершении операции криптопреобразования
*/
gost(0, 0, E_STOP);
close(in);
return 0;
}
6. Порядок работы с СКЗИ
--------------------------
Для подготовки СКЗИ к работе администратор должен выполнить следующие
действия:
- загрузить криптографический драйвер
- сгенерировать ключевые данные и записать их в БКИ драйвера
После этого пользователь может приступить к выполнению операций
криптографической защиты информации. Для этого ему достаточно запустить на
выполнение модуль МВ (файл test_gost), указав в командной строке имя
преобразуемого файла. Результат преобразования будет записан в тот же файл.