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

da1f3b36126dc31803d8e17f60e4e0eb.jpg

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

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

Кроме подводных камней типа псевдонимов указателей, у языка C фундаментальные неисправленные ошибки, которые ждут своих жертв. Именно эти уязвимости Кейс Кук, инженер по безопасности ядра Google Linux, рассмотрел на конференции по безопасности Linux в Ванкувере.
«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 почти удалили из ядра, они ещё остались в некотором коде. К счастью, VLA легко найти с помощью флага компилятора -Wvla.

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

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

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

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


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

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

© Habrahabr.ru