Язык C очень мощный и много где используется — особенно в ядре Linux — но при этом очень опасный. Один из разработчиков ядра Linux рассказал, как справиться с уязвимостями безопасности С.
Вы можете сделать практически любую вещь на С, но это не значит, что её нужно делать. Код C очень быстр, но несётся без ремней безопасности. Даже если вы эксперт, как большинство разработчиков ядра Linux, всё равно возможны убийственные ошибки.
Кроме подводных камней типа псевдонимов указателей, у языка C фундаментальные неисправленные ошибки, которые ждут своих жертв. Именно эти уязвимости Кейс Кук, инженер по безопасности ядра Google Linux, рассмотрел на конференции по безопасности Linux в Ванкувере.
«C — это своеобразный ассемблер. Это почти машинный код», — говорил Кук, обращаясь к аудитории из несколько сотен коллег, понимающих и ценящих скорость приложений на C. Но плохая новость в том, что «C поставляется с некоторым опасным багажом, неопределённым поведением и другими слабостями, которые ведут к дырам в безопасности и уязвимой инфраструктуре».
Если вы используете C в своих проектах, стоит обратить внимание на проблемы безопасности.
Защита ядра Linux
Со временем Кук с коллегами обнаружил многочисленные проблемы нативного С. Для их устранения был запущен Проект самозащиты ядра — 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: уроки
- C — зрелый и мощный язык, но создаёт технические трудности и проблемы безопасности.
- Разработчики Linux уделяют особое внимание тому, чтобы обезопасить C (не потеряв его мощь), потому что на нём написана бoльшая часть операционной системы.
- Инженер по безопасности ядра Google Linux определил конкретные уязвимости языка и объяснил, как их избежать.
Комментарии (19)
LynXzp
07.09.2018 18:27+1КДПВ соответствует статье. Привлекает внимание, но по существу ничего нет. А если ближе рассмотреть то посевы бактерий симметричные и складки на желтом тоже.
holomen
07.09.2018 21:21+2Вот интересно. Это свое выступление он закончил словами «Всегда Ваш, КЭП.»? :D
Или это статья такая?
Или перевод?
CodeRush
08.09.2018 01:19Лучший способ использования небезопасным инструментом — не пользоваться им, если это возможно.
Т.е. если у вас кроме C и ассемблера ничего нет и не будет, тогда нужны правильные процессы, анализаторы, верификаторы, санитайзеры и прочий остальной тулинг, а сами вы должны быть готовы писать по 50 строк в день и защищать на ревью каждую строку.
Не хотите? Используйте более безопасные инструменты, оставив С тем, кому от него деваться некуда.
iig
08.09.2018 15:06Си в опасности, это понятно. А как его обезопасить, про это в статье ни слова. Продолжение будет?
IBAH_II
08.09.2018 15:15-1Си это кросплатформенный ассемблер!
Проблемы «небезопасности» Си связаны с набежавшей в эту область школотой, которая не писала ни на одном ассемблере.
Отсюда основное правило написания кода на Си: «Пиши так, чтобы не сомневаться как конструкция будет преобразована машинный код. Если за строкой Си-кода не видишь машинных инструкций — не пиши такую строку!»
В нашем современном мире мы стали забывать что a=b[i]; означает — адрес «b» загрузить в индексный регистр, прибавить к содержимому регистра значение по адресу «i» умноженное (бинарно сдвинутое влево) на размер типа «b». Значение по адресу содержимого индексного регистра записать по адресу «a». Если вы будите видеть этот текст за «a=b[i];» проблем с безопасностью языка Си у вас не будет!
MacIn
08.09.2018 16:18+3что a=b[i]; означает — адрес «b» загрузить в индексный регистр, прибавить к содержимому регистра значение по адресу «i» умноженное (бинарно сдвинутое влево) на размер типа «b».
Муахаха. А на деле может быть что угодно — если вы итерируетесь по массиву, на каждом шаге мы можем просто прибавлять к указателю смещение. И оптимизирующий компилятор может делать что угодно, если результат — тот же.IBAH_II
08.09.2018 19:11Не надо цепляться к словам! Он сделает именно это, в той или иной форме. Может вместо индексного регистра будет использован супериндексный суперрегистр с суперинкрементом итп.
Оптимизирующий компилятор должен делать то, что я ему приказал. Иначе он не компилятор. Не умеешь приказывать, пиши на питоне.MacIn
08.09.2018 20:49+2Стоп-стоп-стоп, вы уж определитесь — то ли С — кроссплатформенный ассемблер и вы точно должны знать, что и как происходит, цитата:
Пиши так, чтобы не сомневаться как конструкция будет преобразована машинный код.
то ли «в той или иной форме». Форма может быть самая разная, гарантируется только абстрактный результат — на то он и ЯВУ.
Gizmich
08.09.2018 16:54-2Люди хотят математической простоты и работать с алгоритмами, а не думать правильно ли этот алгоритм будет трактоваться на конкретном железе. Раньше реализация была очень важна поскольку вашему отделу в институте выделили 2 часа работы с компьютером и ошибиться значит ждать еще неделю свои 2 часа.
AVI-crak
08.09.2018 17:14GCC в режиме агрессивной оптимизации вовсе берега теряет. Не спасают даже переменные с коротким жизненным циклом (одноразовые). По этому говорить о какой-либо безопасности просто глупо. GCC вполне может заоптимизировать до нуля код проверки условий — если посчитает что от его наличия или отсутствия ничего не измениться.
IBAH_II
08.09.2018 19:21-1Ничего он не теряет. А чтобы не оптимизировал условия использовать модификатор volatile.
Я всего один раз столкнулся с «небезопасностью», и то по собственному незнанию архитектуры процессора. Иар на Кортекс-М0 на высокий скоростных оптимизациях выравнивает переменные, в даже в структурах, на 4 байта. Решается через прагму.
fiftin
08.09.2018 18:23+1Как оказывается, видеть машынные инструкции уже не достаточно (а может и смысла не имеет): С — не низкоуровневый язык
gavrilovm
09.09.2018 13:09-1В 21 стандарте с++ собираются скрыть указатели, мне кажется язык от этого много потеряет.
GarryC
10.09.2018 09:58Наверное, я чего то не знаю, но как VLA может уменьшить производительность вообще, и по сравнению с malloc в частности, я даже представить не могу.
EndUser
«Ассемблер опасен».
«Как обезопасить» — долгий и трудный путь!
:-)
Holix
Спойлер!!