Подписанные (signed) программы
Статья про загрузчик ELF закончилась деликатным вопросом. Модуль ядра работает в ring 0 с полным доступом к системе. Кто же тогда решает, какому файлу вообще можно в ring 0? Если бы любой произвольный файл .km просто грузился, система была бы беззащитна. Ответ - это криптографическая подпись, и изящный приём в том, что подпись решает не только можно ли, но и с какой привилегией работает программа.
Подпись, это ключ к привилегии
Подписи можно было бы сделать простым "да/нет" - подписано, значит можно, без подписи нельзя. У меня они несут больше информации. Неподписанный файл может работать, но только как обычный user-процесс в ring 3, запертый за стеной kcall. Подписанный файл несёт в подписи тип и он определяет, станет ли файл модулем ядра, драйвером, командой ядра или привилегированным сервисом userspace. То есть привилегия сидит в подписи, а не в имени файла и не в месте хранения.
Где находится подпись
Подпись живёт в своей секции 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_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 лицензия). Может кто то и может похвастаться, что он на завтрак пишет крипто-библиотеки, я поаплодирую ему стоя. Я же использую чужую... :-(
От типа к праву
Когда в конце установлен действительный тип, загрузчик переводит его в режим загрузки и тем самым в уровень привилегии. 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.
Было бы интересно увидеть ваши комментарии и улучшить статьи.
◀ Предыдущая статья Содержание Следующая статья ▶
*Система не стоит на месте, поэтому в дальнейшем тексты могут не совпадать с реальным положением