Почему так трудно сделать C менее подверженным переполнению буфера?

23

Я провожу курс в колледже, где одна из лабораторий заключается в выполнении эксплойтов по переполнению буфера для кода, который они нам дают. Это варьируется от простых эксплойтов, таких как изменение адреса возврата для функции в стеке для возврата к другой функции, вплоть до кода, который изменяет состояние регистра / памяти программы, но затем возвращает к функции, которую вы вызвали, что означает, что вызванная вами функция совершенно не обращает внимания на эксплойт.

Я провел некоторое исследование в этой области, и подобные виды эксплойтов даже сейчас используются повсеместно, например, для запуска homebrew на Wii и отвязанного джейлбрейка для iOS 4.3.1.

Мой вопрос: почему эту проблему так трудно решить? Очевидно, что это один из основных эксплойтов, используемых для взлома сотен вещей, но кажется, что это будет довольно легко исправить, просто обрезав любой ввод за допустимую длину и просто очистив весь ввод, который вы берете.

РЕДАКТИРОВАТЬ: Еще одна точка зрения, которую я хотел бы рассмотреть ответы - почему создатели C не исправить эти проблемы путем переопределения библиотек?

Анкит Сони
источник

Ответы:

35

Они исправили библиотеки.

Любой современный C Стандартная библиотека содержит более безопасные варианты strcpy, strcat, sprintfи так далее.

В системах C99, а это большинство Unix-систем, вы найдете их с такими именами, как strncatи snprintf, где «n» означает, что он принимает аргумент, равный размеру буфера или максимальному количеству элементов для копирования.

Эти функции могут использоваться для более безопасной обработки многих операций, но в ретроспективе их удобство использования невелико. Например, некоторые snprintfреализации не гарантируют, что буфер завершен нулем. strncatкопирует несколько элементов, но многие люди ошибочно передают размер буфера dest.

В Windows, один часто находит strcat_s, sprintf_sсуффикс «_s» указывает на «безопасные». Они также нашли свое применение в стандартной библиотеке C в C11 и обеспечивают больший контроль над тем, что происходит в случае переполнения (например, усечение или утверждение).

Многие поставщики предоставляют еще больше нестандартных альтернатив, таких как asprintfв GNU libc, которые автоматически выделяют буфер соответствующего размера.

Идея, что вы можете "просто исправить C", является недоразумением. Исправление C не является проблемой - и уже было сделано. Проблема заключается в исправлении десятилетий кода C, написанного невежественными, уставшими или спешащими программистами, или кода, перенесенного из контекстов, где безопасность не имеет значения, в контексты, где безопасность имеет значение. Никакие изменения в стандартной библиотеке не могут исправить этот код, хотя миграция на новые компиляторы и стандартные библиотеки часто помогает автоматически определить проблемы.


источник
11
+1 за нацеливание задачи на программистов, а не на язык.
Николь Болас
8
@Nicol: Говорить «проблема программистов» - это несправедливо редукционистский подход. Проблема заключается в том, что в течение многих лет (десятилетий) C облегчал написание небезопасного кода, а не безопасного кода, особенно потому, что наше определение «безопасного» развивалось быстрее, чем любой языковой стандарт, и что этот код все еще существует. Если вы хотите попытаться свести это к одному существительному, проблема заключается в «1970-1999 libc», а не в «программистах».
1
Программисты по-прежнему обязаны использовать инструменты, которые у них есть сейчас, для решения этих проблем. Возьмите полдня или около того и поищите в этом исходный код.
Николь Болас
1
@Nicol: Хотя это тривиально, чтобы обнаружить потенциальное переполнение буфера, часто нетривиально, чтобы быть уверенным, что это реальная угроза, и менее тривиально выяснить, что должно произойти, если буфер когда-либо переполнится. Обработка ошибок / часто не учитывалась, невозможно «быстро» реализовать улучшение, так как вы можете изменить поведение модуля непредвиденными способами. Мы только что сделали это на основе унаследованного кода, состоящего из нескольких миллионов строк, и хотя это стоит того, чтобы потратить много времени (и денег).
Mattnz
4
@NicolBolas: Не уверен, в каком магазине вы работаете, но последнее место, где я написал C для производственного использования, потребовало внесения поправок в детальный проектный документ, его просмотра, изменения кода, внесения изменений в план тестирования, проверки плана тестирования, выполнения полного плана. тестирование системы, просмотр результатов теста, а затем повторная сертификация системы на сайте заказчика. Это для телекоммуникационной системы на другом континенте, написанной для компании, которая больше не существует. Последнее, что я знал, источник находился в архиве RCS на ленте QIC, которая должна быть читаемой, если вы можете найти подходящий стример.
TMN
19

Это не совсем неверно говорить , что C на самом деле «подверженные ошибкам» по дизайну . Помимо некоторых серьезных ошибок, таких как gets, язык C не может быть по-другому, не теряя при этом основной функции, которая в первую очередь привлекает людей к C.

C был разработан как системный язык, чтобы действовать как своего рода «переносимая сборка». Главная особенность языка C состоит в том, что в отличие от языков более высокого уровня, код C часто очень близко соответствует реальному машинному коду. Другими словами, ++iобычно это просто incинструкция, и вы часто можете получить общее представление о том, что процессор будет делать во время выполнения, взглянув на код Си.

Но добавление неявной проверки границ добавляет много дополнительных накладных расходов, которые программист не просил и мог не захотеть. Эти издержки выходят далеко за рамки дополнительного хранилища, необходимого для хранения длины каждого массива, или дополнительных инструкций для проверки границ массива при каждом доступе к массиву. А как насчет арифметики указателей? Или что, если у вас есть функция, которая принимает указатель? Среда выполнения не может знать, попадает ли этот указатель в пределы законно выделенного блока памяти. Чтобы отслеживать это, вам понадобится серьезная архитектура времени выполнения, которая может проверять каждый указатель на таблицу выделенных в данный момент блоков памяти, и в этот момент мы уже попадаем на территорию управляемого времени выполнения в стиле Java / C #.

Чарльз Сальвия
источник
12
Честно говоря, когда люди спрашивают, почему C не «безопасен», я задаюсь вопросом, будут ли они жаловаться, что сборка не «безопасна».
Бен Брока
5
Язык C во многом похож на переносную сборку на машине Digital Equipment Corporation PDP-11. В то же время машины Берроуза проверяли границы массивов в ЦП, поэтому им было действительно легко получить доступ к программам. Проверка массивов в аппаратном обеспечении работает на оборудовании Rockwell Collins (в основном используется в авиации).
Тим Виллискрофт,
15

Я думаю , что реальная проблема заключается не в том , что эти виды ошибок , которые трудно исправить, но то , что они так легко сделать: Если вы используете strcpy, sprintfи друзья в (казалось бы) простой способ , который может работать, то вы , вероятно , открыл дверь для переполнения буфера. И никто не заметит, пока кто-нибудь не воспользуется им (если у вас нет очень хороших обзоров кода). Теперь добавьте тот факт , что есть много посредственных программистов , и что они находятся под давлением времени большую часть времени - и у вас есть рецепт для кода , который настолько пронизана с переполнением буфера , что это будет трудно исправить их все просто потому , что есть их так много, и они так хорошо прячутся.

nikie
источник
3
Вам не нужны "очень хорошие обзоры кода". Вам просто нужно запретить sprintf или переопределить sprintf для чего-то, что использует sizeof () и ошибки по размеру указателя, и т. Д. Вам даже не нужны проверки кода, вы можете делать такие вещи с фиксацией SCM крючки и грэп.
1
@JoeWreschnig: обычно sizeof(ptr)4 или 8. Это еще одно ограничение C: нет способа определить длину массива, учитывая только указатель на него.
MSalters
@MSalters: Да, массив int [1] или char [4] или любой другой может быть ложно-положительным, но на практике вы никогда не обрабатываете буферы такого размера с этими функциями. (Я не говорю здесь теоретически - я работал над большой базой кода C в течение четырех лет, которые использовали этот подход. Я никогда не сталкивался с ограничением спринтинга в символ [4].)
5
@ BlackJack: Большинство программистов не глупы - если вы заставите их передать размер, они передадут правильный. Просто большинство также не пройдет размер, если не принужден к. Вы можете написать макрос, который будет возвращать длину массива, если он статический или с автоматическим размером, но с ошибками, если указан указатель. Затем вы # определяете sprintf для вызова snprintf с этим макросом, дающим размер. Теперь у вас есть версия sprintf, которая работает только с массивами с известными размерами и в противном случае вынуждает программиста вызывать snprintf с указанным вручную размером.
1
Одним из простых примеров такого макроса будет #define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]) / (sizeof(a) != sizeof(void *))запуск деления на ноль во время компиляции. Еще один умный пример, который я впервые увидел в Chromium, - #define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]) / !(sizeof(a) % sizeof((a)[0]))это обмен горстки ложных срабатываний на некоторые ложные отрицания - к сожалению, это бесполезно для символа []. Вы можете использовать различные расширения компилятора, чтобы сделать его еще более надежным, например blogs.msdn.com/b/ce_base/archive/2007/05/08/… .
7

Трудно устранить переполнение буфера, потому что C практически не предоставляет полезных инструментов для решения проблемы. Это фундаментальный изъян языка, родные буфера не обеспечивают никакой защиты , и это практически, если не полностью, невозможно заменить их продукт высочайшего класса, как C ++ сделал с std::vectorи std::array, и это трудно даже в режиме отладки , чтобы найти переполнение буфера.

DeadMG
источник
13
«Языковой недостаток» - ужасно необъективное требование. То, что библиотеки не обеспечивали проверку границ, было недостатком; то, что язык не сделал, является сознательным выбором, чтобы избежать накладных расходов. Этот выбор является частью того, что позволяет std::vectorэффективно реализовывать конструкции более высокого уровня . И vector::operator[]делает тот же выбор для скорости над безопасностью. Безопасность vectorдостигается за счет упрощения перемещения по размеру, что является тем же подходом, который используют современные библиотеки Си.
1
@Charles: «C просто не предоставляет никаких динамически расширяющихся буферов как часть стандартной библиотеки». Нет, это не имеет к этому никакого отношения. Во-первых, C предоставляет их через realloc(C99 также позволяет определять размеры массивов стека, используя определенный во время выполнения, но постоянный размер через любую автоматическую переменную, почти всегда предпочтительнее char buf[1024]). Во-вторых, проблема не имеет ничего общего с расширением буферов, она связана с тем, несут ли буферы размер с ними и проверяют этот размер при доступе к ним.
5
@Joe: проблема не столько в том, что нативные массивы сломаны. Это то, что их невозможно заменить. Для начала, vector::operator[]выполняет проверку границ в режиме отладки - чего не могут сделать нативные массивы - и, во-вторых, в C нет способа поменять местный тип массива на тот, который может выполнять проверку границ, потому что нет шаблонов и операторов перегрузки. В C ++, если вы хотите перейти от T[]к std::array, вы можете просто поменять typedef. В C нет способа достичь этого, и нет способа написать класс с эквивалентной функциональностью, не говоря уже об интерфейсе.
DeadMG
3
@Joe: за исключением того, что он никогда не может быть статического размера, и вы никогда не можете сделать его общим. Невозможно написать какую-либо библиотеку в C, которая выполняет ту же роль, что std::vector<T>и std::array<T, N>в C ++. Не было бы никакого способа спроектировать и указать какую-либо библиотеку, даже стандартную, которая могла бы сделать это.
DeadMG
1
Я не уверен, что вы подразумеваете под «статическим размером». Поскольку я использовал бы этот термин, std::vectorтакже никогда не может быть статического размера. Что касается универсального, вы можете сделать его настолько универсальным, насколько это требуется хорошему C - небольшое количество фундаментальных операций над void * (добавление, удаление, изменение размера) и все остальное написано специально. Если вы собираетесь жаловаться, что C не имеет обобщений в стиле C ++, это выходит за рамки безопасной обработки буфера.
7

Проблема не в языке Си .

ИМО, единственное серьезное препятствие, которое нужно преодолеть, заключается в том, что Си просто плохо учат . Десятилетия плохой практики и неверной информации были закреплены в справочных руководствах и примечаниях к лекциям, отравляя умы каждого нового поколения программистов с самого начала. Студентам дается краткое описание из «простых» функций ввода / вывода , как gets1 или , scanfа затем оставил их собственные устройства. Им не сказано, где и как эти инструменты могут выйти из строя, или как предотвратить эти ошибки. Им не говорят об использовании fgetsиstrtol/strtodпотому что они считаются «продвинутыми» инструментами. Затем они обрушиваются на профессиональный мир, чтобы нанести ущерб. Не так уж много из более опытных программистов знают лучше, потому что они получили такое же образование с повреждением мозга. Это сводит с ума. Я вижу так много вопросов здесь, в Stack Overflow и на других сайтах, где ясно, что человека, задающего вопрос, обучает тот, кто просто не знает, о чем говорит , и, конечно, вы не можете просто сказать «Ваш профессор не прав», потому что он профессор, а вы просто какой-то парень в Интернете.

И затем у вас есть толпа, которая презирает любой ответ, начинающийся с «ну, в соответствии с языковым стандартом ...», потому что они работают в реальном мире, и в соответствии с ними стандарт не применим к реальному миру . Я могу иметь дело с кем-то, у кого просто плохое образование, но любой, кто настаивает на том , чтобы быть невежественным, - это просто упадок в отрасли.

Не было бы проблем переполнения буфера, если бы язык преподавался правильно с акцентом на написание безопасного кода. Это не "сложно", это не "продвинутый", это просто быть осторожным.

Да, это была напыщенная речь.


1 Который, к счастью, был окончательно извлечен из языковой спецификации, хотя он навсегда просуществует через 40 лет унаследованного кода.

Джон Боде
источник
1
Хотя я в основном согласен с вами, я думаю, что вы все еще немного несправедливы. То, что мы считаем «безопасным», также является функцией времени (и я вижу, что вы были профессиональным разработчиком программного обеспечения гораздо дольше, чем я, поэтому я уверен, что вы знакомы с этим). Через десять лет кто-то будет вести этот же разговор о том, почему, черт возьми, все в 2012 году использовали DoS-совместимые реализации хеш-таблиц, разве мы ничего не знали о безопасности? Если есть проблема в обучении, это проблема в том, что мы слишком фокусируемся на обучении «лучшей» практике, а не на том, что сама передовая практика развивается.
1
И давайте будем честными. Вы можете написать безопасный код просто sprintf, но это не значит, что язык не был ошибочным. C был ошибочным и имеет недостатки - как и любой язык - и важно, чтобы мы признали эти недостатки, чтобы мы могли продолжать их исправлять.
@JoeWreschnig - Хотя я согласен с более широким вопросом, я думаю, что есть качественная разница между реализациями хэш-таблиц с поддержкой DoS и переполнением буфера. Первое может быть связано с обстоятельствами, развивающимися вокруг вас, но второе не имеет оправданий; переполнения буфера - ошибки кодирования, точка. Да, у C нет защиты лезвия, и он порежет вас, если вы неосторожны; мы можем спорить о том, является ли это недостатком языка или нет. Это ортогонально тому факту, что очень немногие студенты получают какие-либо инструкции по технике безопасности при изучении языка.
Джон Боде
5

Проблема не столько в некомпетентности программистов, сколько в управленческой близорукости. Помните, что приложению на 90 000 строк требуется только одна небезопасная операция, чтобы быть полностью незащищенной. Практически за пределами вероятности, что любое приложение, написанное поверх принципиально небезопасной обработки строк, будет на 100% совершенным, что означает, что оно будет небезопасным.

Проблема заключается в том, что расходы, связанные с отсутствием безопасности, либо не взимаются с правильного адресата (компании, продающей приложение, почти никогда не придется возвращать покупную цену), либо неясно видны в момент принятия решения («Мы должны отправить в марте не смотря ни на что! "). Я вполне уверен, что если бы вы учитывали долгосрочные затраты и затраты для своих пользователей, а не для прибыли вашей компании, то писать на C или родственных языках было бы намного дороже, возможно, настолько дорого, что во многих случаях это был бы неправильный выбор. области, где в наше время общепринятая мудрость говорит, что это необходимость. Но это не изменится, если не будет введена более строгая ответственность за программное обеспечение, чего никто в отрасли не хочет.

Килиан Фот
источник
-1: обвинять менеджмент как корень всего зла не особо конструктивно. Игнорирование истории чуть менее так. Ответ почти искуплен последним предложением.
Mattnz
Более строгая ответственность за программное обеспечение может быть введена пользователями, заинтересованными в безопасности и желающими заплатить за нее. Можно утверждать, что это может быть введено в виде суровых наказаний за нарушения безопасности. Рыночное решение будет работать, если пользователи будут готовы платить за безопасность, но это не так.
Дэвид Торнли
4

Одна из великих возможностей использования C заключается в том, что он позволяет вам манипулировать памятью любым удобным для вас способом.

Одним из больших недостатков использования C является то, что он позволяет вам манипулировать памятью любым удобным для вас способом.

Есть безопасные версии любых небезопасных функций. Тем не менее, программисты и компилятор не строго предписывают их использование.

Сардатрион - Восстановить Монику
источник
2

почему создатели C не решают эти проблемы путем переопределения библиотек?

Возможно, потому что C ++ уже сделал это и обратно совместим с кодом C. Поэтому, если вам нужен безопасный строковый тип в вашем C-коде, вы просто используете std :: string и пишете C-код, используя компилятор C ++.

Базовая подсистема памяти может помочь предотвратить переполнение буфера путем введения защитных блоков и проверки их корректности - поэтому во все выделения добавляется 4 байта «fefefefe», когда при записи в эти блоки система может генерировать воблер. Не гарантируется предотвращение записи в память, но это покажет, что что-то пошло не так и нуждается в исправлении.

Я думаю, что проблема в том, что старые процедуры strcpy и т. Д. Все еще присутствуют. Если бы они были удалены в пользу strncpy и т. Д., Это помогло бы.

gbjbaanb
источник
1
Полное удаление strcpy и т. Д. Сделало бы дополнительные пути обновления еще более трудными, что, в свою очередь, привело бы к тому, что люди вообще не обновлялись. Теперь вы можете переключиться на компилятор C11, затем начать использовать варианты _s, затем запретить варианты, отличные от _s, а затем исправить существующее использование в течение любого периода времени, который практически жизнеспособен.
-2

Просто понять, почему проблема переполнения не устранена. С был несовершенен в нескольких областях. В то время эти недостатки считались терпимыми или даже как особенность. Теперь, десятилетия спустя, эти недостатки не устраняются.

Некоторые части сообщества программистов не хотят, чтобы эти дыры были закрыты. Просто посмотрите на все пламенные войны, которые начинаются с строк, массивов, указателей, сборки мусора ...

mhoran_psprep
источник
5
LOL, ужасный и неправильный ответ.
Хит Ханникутт
1
Чтобы объяснить, почему это плохой ответ: C действительно имеет много недостатков, но разрешение переполнения буфера и т. Д. Имеет мало общего с ними, но с базовыми требованиями к языку. Было бы невозможно спроектировать язык для выполнения работы C и не допустить переполнения буфера. Части сообщества не хотят отказываться от возможностей, которые им позволяет С, часто по уважительной причине. Есть также разногласия относительно того, как избежать некоторых из этих проблем, показывая, что у нас нет полного понимания дизайна языка программирования, и ничего более.
Дэвид Торнли
1
@DavidThornley: Можно разработать язык для работы C, но сделать так, чтобы обычные идиоматические способы работы по крайней мере позволяли компилятору достаточно эффективно проверять переполнение буфера, если компилятор решит это сделать. Существует огромная разница между memcpy()доступностью и наличием единственного стандартного средства эффективного копирования сегмента массива.
суперкат