Добавить в корзинуПозвонить
Найти в Дзене
Триалогия

Пишем операционную систему Триалогия - Подписанные (signed) программы - подписи ELF через ECDSA и SHA-256

Статья про загрузчик ELF закончилась деликатным вопросом. Модуль ядра работает в ring 0 с полным доступом к системе. Кто же тогда решает, какому файлу вообще можно в ring 0? Если бы любой произвольный файл .km просто грузился, система была бы беззащитна. Ответ - это криптографическая подпись, и изящный приём в том, что подпись решает не только можно ли, но и с какой привилегией работает программа. Подписи можно было бы сделать простым "да/нет" - подписано, значит можно, без подписи нельзя. У меня они несут больше информации. Неподписанный файл может работать, но только как обычный user-процесс в ring 3, запертый за стеной kcall. Подписанный файл несёт в подписи тип и он определяет, станет ли файл модулем ядра, драйвером, командой ядра или привилегированным сервисом userspace. То есть привилегия сидит в подписи, а не в имени файла и не в месте хранения. Подпись живёт в своей секции ELF под названием .note.trialogie, компактном 76-байтовом struct: struct elf_note_trialogger { uint32_t ve
Оглавление
Триалогия - Пишем операционную систему
Триалогия - Пишем операционную систему

Подписанные (signed) программы

Статья про загрузчик ELF закончилась деликатным вопросом. Модуль ядра работает в ring 0 с полным доступом к системе. Кто же тогда решает, какому файлу вообще можно в ring 0? Если бы любой произвольный файл .km просто грузился, система была бы беззащитна. Ответ - это криптографическая подпись, и изящный приём в том, что подпись решает не только можно ли, но и с какой привилегией работает программа.

Подпись, это ключ к привилегии

Подписи можно было бы сделать простым "да/нет" - подписано, значит можно, без подписи нельзя. У меня они несут больше информации. Неподписанный файл может работать, но только как обычный user-процесс в ring 3, запертый за стеной kcall. Подписанный файл несёт в подписи тип и он определяет, станет ли файл модулем ядра, драйвером, командой ядра или привилегированным сервисом userspace. То есть привилегия сидит в подписи, а не в имени файла и не в месте хранения.

Где находится подпись

Подпись в файле ELF - Триалогия
Подпись в файле ELF - Триалогия

Подпись живёт в своей секции ELF под названием .note.trialogie, компактном 76-байтовом struct:

struct elf_note_trialogger {

uint32_t version; // = 1

uint32_t sig_type; // NONE / KERNEL_COMMAND / DRIVER / MODULE / SERVICE

uint32_t mod_flags; // CAN_SLEEP, SYNC_NO_SLEEP, ... либо capability сервиса

uint8_t signature[64]; // ECDSA-P256: r || s, по 32 байта

} __attribute__((packed));

Здесь прописаны тип, пара флагов поведения и собственно 64-байтовая подпись. При проверке именно эта секция пропускается, иначе подписи пришлось бы подписать саму себя а это уже ситуация - курица и яйцо.

Проверка шаг за шагом

Поток проверки подписи ELF - Триалогия
Поток проверки подписи ELF - Триалогия

elf_verify_signature работает чёткими ступенями. Если секции .note.trialogie нет, файл не подписан и получает SIG_TYPE_NONE, ему можно только в ring 3. Если секция есть, сперва вычисляется хэш SHA-256 файла, причём с пропущенной секцией note.

// пропустить секцию .note.trialogie при хэшировании

sha256_init(&ctx);

sha256_update(&ctx, elf_data, section_start); // всё ДО note

sha256_update(&ctx, elf_data + section_end, after_size); // всё ПОСЛЕ note

sha256_final(&ctx, hash);

Затем идёт собственно криптографическая проверка.

extern const uint8_t kernel_public_key[64]; // вшит в ядро (x || y)

if (!ecdsa_verify_p256(hash, note->signature, kernel_public_key))

return SIG_TYPE_INVALID; // подпись не сходится ->отклонить

return note->sig_type; // действительна -> вписанный тип определяет привилегию

Почему это безопасно? Потому что ECDSA асимметрична. Есть пара ключей:

  • приватный ключ, которым подписывают при сборке и который лежит только у разработчика и никогда в системе
  • открытый ключ, которым ядро проверяет и который вшит (kernel_public_key, 64 байта) в систему.

Из открытого ключа нельзя вывести приватный. Только тот, у кого приватный ключ может создать подпись которую ядро примет. А поскольку хэш идёт по всему коду, любая последующая подделка всплывает. Измени хоть один байт и подпись больше не сходится.

bool ecdsa_verify_p256(const uint8_t hash[32], // SHA-256 файла

const uint8_t signature[64], // r || s

const uint8_t public_key[64]); // x || y (NIST P-256)

Выбор осознанно пал на ECDSA с кривой NIST P-256 вместо RSA. Равная или лучшая безопасность (128 бит) при крошечной 64-байтовой подписи, которая свободно влезает в тонкую секцию note. Сама математика кривой, это библиотека Intel TinyCrypt (BSD лицензия). Может кто то и может похвастаться, что он на завтрак пишет крипто-библиотеки, я поаплодирую ему стоя. Я же использую чужую... :-(

От типа к праву

Подпись и привилегия ELF - Триалогия
Подпись и привилегия ELF - Триалогия

Когда в конце установлен действительный тип, загрузчик переводит его в режим загрузки и тем самым в уровень привилегии. SIG_TYPE_NONE становится user-процессом (ring 3). SIG_TYPE_SERVICE может остаться в ring 3, но получает расширенные права через capability-флаги (доступ к сети, разделяемая память ядра, доступ к дисплею, это использует, например, оконный менеджер). SIG_TYPE_KERNEL_COMMAND, SIG_TYPE_DRIVER и SIG_TYPE_MODULE попадают в ring 0 каждый с чуть иным способом загрузки. А SIG_TYPE_INVALID, присутствующая но ошибочная подпись ведёт к жёсткому обрыву - не грузить вовсе. Так замыкается круг к прошлой статье, ring 0 доступен лишь в обмен на действительный ключ.

Честное место

Курьёз бросился в глаза при перечитывании собственного кода- над elf_verify_signature всё ещё стоит большой комментарий, называющий функцию "STUB", который якобы слепо доверяет подписи без проверки ECDSA. Это давно перестало быть правдой, код под ним очень даже вычисляет хэш и вызывает ecdsa_verify_p256. Документация отстаёт от кода, классический мелкий промах, который я тут же и исправил. Важнее однако серьёзная сторона дела - вся безопасность держится на том, что приватный ключ остаётся в тайне. У кого он есть, тот может подписать произвольный код ring 0. Есть ровно один ключ и живёт он вне системы, у разработчика.

Что дальше

На этом первая волна вокруг центральных интерфейсов ядра завершена: IPC, загрузчик ELF и теперь подпись. Дальше спускаемся на уровень ниже, к фундаменту устройств - как ядро вообще находит железо с которым должно говорить? Следующая часть начинается с шины PCI.

Было бы интересно увидеть ваши комментарии и улучшить статьи.

Предыдущая статья Содержание Следующая статья

*Система не стоит на месте, поэтому в дальнейшем тексты могут не совпадать с реальным положением