Хабрахабр

[Перевод] Как обезопасить C

Один из разработчиков ядра Linux рассказал, как справиться с уязвимостями безопасности С. Язык C очень мощный и много где используется — особенно в ядре Linux — но при этом очень опасный.

Код C очень быстр, но несётся без ремней безопасности. Вы можете сделать практически любую вещь на С, но это не значит, что её нужно делать. Даже если вы эксперт, как большинство разработчиков ядра Linux, всё равно возможны убийственные ошибки.

Именно эти уязвимости Кейс Кук, инженер по безопасности ядра Google Linux, рассмотрел на конференции по безопасности Linux в Ванкувере.
«C — это своеобразный ассемблер. Кроме подводных камней типа псевдонимов указателей, у языка C фундаментальные неисправленные ошибки, которые ждут своих жертв. Но плохая новость в том, что «C поставляется с некоторым опасным багажом, неопределённым поведением и другими слабостями, которые ведут к дырам в безопасности и уязвимой инфраструктуре». Это почти машинный код», — говорил Кук, обращаясь к аудитории из несколько сотен коллег, понимающих и ценящих скорость приложений на C.

Если вы используете C в своих проектах, стоит обратить внимание на проблемы безопасности.

Со временем Кук с коллегами обнаружил многочисленные проблемы нативного С. Для их устранения был запущен Проект самозащиты ядра — Kernel Self Protection Project. Он медленно и неуклонно работает над защитой ядра Linux от атак, удаляя оттуда проблемный код.

Большое количество кода относится к специфическим задачам, которые нужно тщательно проверить. Это сложно, говорит Кук, потому что «ядру нужно делать очень специфичные для конкретной архитектуры вещи по управлению памятью, обработке прерываний, шедулингу и так далее». Например, «у C нет API для установки таблиц страниц или переключения в 64-битный режим», — сказал он.

Кук процитировал — и согласился — с со статьёй в блоге Рафа Левиена «С неопределённым поведением возможно всё». При такой нагрузке и со слабыми стандартными библиотеками в C слишком много неопределённого поведения.

Это всё, что было в памяти раньше! Кук привёл конкретные примеры: «Каково содержание “неинициализированных” переменных? Конечно! В указателях void нет типа, но можно через них вызывать типизированные функции? Почему у memcpy() нет аргумента ’max destination length'? Сборке всё равно: можно обратиться на любой адрес! Неважно, просто делай как сказано; все области памяти одинаковы!»

С некоторыми из этих особенностей относительно легко справиться. Кук прокомментировал: «Линусу [Торвальдсу] нравится идея всегда инициализировать локальные переменные. Так что просто делайте это».

Если вы инициализируете локальную переменную в switch, то получите предупреждение: «Оператор никогда не будет выполняться [-Wswitch-unreachable]» из-за того, как компилятор обрабатывает код. Но с оговоркой. Это предупреждение можно игнорировать.

«Массивы переменной длины всегда плохо», — сказал Кук. Но не все предупреждения можно игнорировать. Кроме того, Кук обратил внимание на медлительность VLA. Среди других проблем — исчерпание стека, линейное переполнение и нарушение страничной защиты. Улучшение и скорости, и безопасности — двойная выгода. Удаление всех VLA из ядра повысило производительность на 13%.

К счастью, VLA легко найти с помощью флага компилятора -Wvla. Хотя VLA почти удалили из ядра, они ещё остались в некотором коде.

Если в switch пропущен break, то что имел в виду программист? Другая проблема скрыта в семантике С. Пропуск break может привести к выполнению кода из нескольких условий; это хорошо известная проблема.

На самом деле это комментарий, но современные компиляторы его разбирают. Если вы ищете в существующем коде операторы break/switch, можно использовать -Wimplicit-fallthrough для добавления новой инструкции switch. Вы также можете явно помечать отсутствие break комментарием “fallthrough”.

Например, проверка strcpy()-family снижает производительность на 2%. Ещё Кук обнаружил снижение производительности при проверке границ для выделения памяти slab. Оказывается, Strncpy не всегда завершается нуль-символом. У альтернатив вроде strncpy() свои проблемы. Кук печально обратился к аудитории: «Где взять лучшие API?»

Тем не менее, Торвальдс отказался от этой идеи, аргументируя, что если какой-то API устарел, его следует полностью выбросить. Во время сессии вопросов и ответов один разработчик Linux спросил: «Можно ли избавиться от старых, плохих API?» Кук ответил, что некоторое время Linux поддерживал концепцию устаревших API. Так что пока мы с ними застряли. Однако навсегда выбрасывать API «политически опасно», добавил Кук.

Кук предвидит долгий и трудный путь. Когда-то казалась привлекательной идея создания диалекта Linux C, но этого не будет. Реальная проблема с опасным кодом заключается в том, что «люди не хотят выполнять работу по очистке кода — не только плохого кода, но и самого C», — сказал он. Как и во всех проектах с открытым исходным кодом, «нам нужно больше преданных разработчиков, рецензентов, тестировщиков и спецов по бэкпорту».

  • C — зрелый и мощный язык, но создаёт технические трудности и проблемы безопасности.
  • Разработчики Linux уделяют особое внимание тому, чтобы обезопасить C (не потеряв его мощь), потому что на нём написана бóльшая часть операционной системы.
  • Инженер по безопасности ядра Google Linux определил конкретные уязвимости языка и объяснил, как их избежать.
Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть