Чому так важко зробити C менш схильним до переповнення буфера?


23

Я проходжу курс в коледжі, де однією з лабораторій є виконання подвигів переповнення буфера за кодом, який вони нам дають. Це варіюється від простих подвигів, таких як зміна повернення функції для стека для повернення до іншої функції, аж до коду, який змінює стан реєстру програм / стан пам'яті, але потім повертається до функції, яку ви викликали, тобто Функція, яку ви назвали, повністю не забуває про подвиг.

Я провів деякі дослідження з цього питання, і подібні подвиги використовуються майже всюди навіть зараз, у таких речах, як запуск домашньої версії по Wii та непідвладна джейлбрейк для iOS 4.3.1

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

EDIT: Ще одна точка зору, на яку я хотів би розглянути відповіді - чому творці C не вирішують ці проблеми шляхом повторного втілення бібліотек?

Відповіді:


35

Вони все-таки виправили бібліотеки.

Будь-який сучасний C Стандартна бібліотека містить більш безпечні варіанти strcpy, strcat, sprintfі так далі.

У системах C99 - що є більшістю Unixes - ви знайдете їх з такими іменами, як strncatі snprintf, "n", що вказує на те, що він потребує аргументу розміром буфера або максимальної кількості елементів для копіювання.

Ці функції можна використовувати для більш безпечного керування багатьма операціями, але в перспективі їх зручність використання не велика. Наприклад, деякі snprintfреалізації не гарантують, що буфер припиняється з нуля. strncatпотрібно скопіювати декілька елементів, але багато людей помилково передають розмір буфера dest.

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

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

Ідея, що можна «просто виправити С», - це непорозуміння. Виправити 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 часто дуже точно співпадає з фактичним машинним кодом. Іншими словами, ++iзазвичай це лише incінструкція, і ви часто можете отримати загальне уявлення про те, що буде робити процесор під час виконання, переглянувши код C.

Але додавання в неявні межі перевірки додає багато зайвих накладних витрат, які програміст не просив і може не хотіти. Це накладні витрати виходять за рамки додаткового місця зберігання, необхідного для зберігання довжини кожного масиву, або додаткових інструкцій для перевірки меж масиву для кожного доступу до масиву. Що з арифметикою вказівника? Або що робити, якщо у вас є функція, яка бере вказівник? Середовище виконання не може знати, чи потрапляє цей покажчик у межі законно виділеного блоку пам'яті. Для того, щоб відслідковувати це, вам знадобиться серйозна архітектура виконання, яка може перевірити кожен покажчик на таблиці виділених в даний час блоків пам’яті, і тоді ми вже потрапляємо на територію виконання Java / C # -style, що управляється.


12
Чесно кажучи, коли люди запитують, чому C не є "безпечним", я змушує задуматися, чи не скаржиться вони на те, що збірка не є "безпечною".
Бен Брокка

5
Мова С дуже схожа на портативну збірку на машині Digital Equipment Corporation PDP-11. У той же час машини Берроуса перевіряли межі масиву в процесорі, тому їм було дуже легко отримати програми. Проверка масиву апаратних засобів в апараті Rockwell Collins (в основному використовується в авіації)
Tim Williscroft

15

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


3
Вам не потрібні "дуже хороші огляди коду". Вам просто потрібно заборонити sprintf або повторно визначити sprintf на те, що використовує sizeof () та помилки щодо розміру вказівника, тощо. Вам навіть не потрібні огляди коду, ви можете робити такі речі за допомогою SCM гачки і греп.

1
@JoeWreschnig: загалом sizeof(ptr)це 4 або 8. Це ще одне обмеження С: немає можливості визначити довжину масиву, задавши лише вказівник на нього.
MSalters

@MSalters: Так, масив int [1] або char [4] або все, що може бути помилковим позитивом, але на практиці ви ніколи не обробляєте буфери такого розміру з цими функціями. (Теоретично я не кажу тут - я працював над великою базою коду С протягом чотирьох років, що використовував такий підхід. Я ніколи не потрапляв на обмеження спринтерфінгу в знак знаку [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]))це торгування жменькою помилкових позитивів для деяких помилкових негативів - на жаль, це не даремно для char []. Ви можете використовувати різні розширення компілятора, щоб зробити його ще більш надійним, наприклад, blogs.msdn.com/b/ce_base/archive/2007/05/08/… .

7

Виправити переповнення буфера важко, оскільки C практично не містить корисних інструментів для вирішення проблеми. Це фундаментальна вада мови, рідні буфера не забезпечують ніякого захисту , і це практично, якщо не повністю, неможливо замінити їх продукт найвищого класу, як C ++ зробив з std::vectorі std::array, і це важко навіть в режимі налагодження , щоб знайти переповнення буфера.


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

Проблема не з C мови .

ІМО, єдина головна перешкода для подолання - це те, що С просто не вчить . Десятиліття поганої практики та неправильної інформації були закладені у довідкові посібники та конспекти лекцій, отруюючи розум кожного нового покоління програмістів. Студенти дають стислий опис "легких" функцій вводу / виводу, як gets1 або, scanfа потім залишаються на власних пристроях. Їм не кажуть, де і як ці інструменти можуть вийти з ладу, або як запобігти цим збоям. Їм не кажуть про використання fgetsтаstrtol/strtodтому що вони вважаються "передовими" інструментами. Потім вони розв’язані у професійному світі, щоб завдати хаосу. Мало того, що багато досвідченіших програмістів знають що-небудь краще, тому що вони отримали однакову освіту з мозком. Це божевільно. Я бачу так багато питань тут, і на Stack Overflow, і на інших сайтах, коли зрозуміло, що людину, яка задає питання, навчає той, хто просто не знає, про що вони говорять , і звичайно, ви не можете просто сказати "ваш професор помиляється", тому що він професор, а ви просто якийсь хлопець в Інтернеті.

І тоді у вас є натовп, який зневажає будь-яку відповідь, починаючи з "ну, відповідно до мовного стандарту ...", оскільки вони працюють у реальному світі, і відповідно до них стандарт не стосується реального світу . Я можу мати справу з тим, хто просто має погану освіту, але той, хто наполягає на тому, щоб бути неосвіченим, - це лише удар у галузі.

Проблем із переповненням буфера не було б, якби мова викладалася правильно з акцентом на написання захищеного коду. Це не "важко", це не "просунуто", це просто обережність.

Так, це була зухвала.


1 Що, на щастя, нарешті було вилучено з мовної специфікації, хоча воно буде ховатися у 40-річному віковому спадковому коді назавжди.


1
Хоча я з вами переважно згоден, я думаю, ти все ще трохи несправедливий. Те, що ми вважаємо "безпечним" - це також функція часу (і я бачу, ви були професійним розробником програмного забезпечення набагато довше мене, тому я впевнений, що ви з цим знайомі). Через десять років хтось буде вести цю саму розмову про те, чому, пекло, всі в 2012 році використовували реалізацію хеш-таблиць з можливістю DoS, хіба ми нічого не знали про безпеку? Якщо в навчанні є проблема, це проблема, яка ми занадто зосереджуємось на навчанні "найкращої" практики, а не в тому, що сама найкраща практика розвивається.

1
І будьмо чесними. Ви можете написати безпечний код просто sprintf, але це не означає, що мова не була хибною. C був зіпсований і має свої недоліки - як і будь-яка мова - і це важливо , що ми визнаємо ті недоліки , так що ми можемо продовжувати , щоб виправити їх.

@JoeWreschnig - Хоча я погоджуюсь із більшою точкою, я думаю, що між реалізацією хеш-таблиць хеш-таблиць та перекриттям буфера є якісна різниця. Перший можна віднести до обставин, що складаються навколо вас, але другий не має виправдань; Перевищення буфера - це помилки кодування, період. Так, C не має охоронців клинків і зріже вас, якщо ви недбалі; ми можемо сперечатися, чи є це недоліком у мові чи ні. Це ортогонально тому, що дуже мало учнів дають якісь інструкції з безпеки, коли вони вивчають мову.
Джон Боде

5

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

Проблема полягає в тому, що витрати на незахищеність або не стягуються з правого адресата (компанія, що продає додаток, майже ніколи не доведе повернути ціну покупки), або не чітко видно під час прийняття рішень ("Нам потрібно відправити у березні, незважаючи ні на що! »). Я досить впевнений, що якщо ви враховуєте довгострокові витрати та витрати для своїх користувачів, а не прибуток вашої компанії, письмовою мовою C або суміжними мовами буде набагато дорожче, напевно, настільки дорого, що, очевидно, неправильний вибір у багатьох сфери, де сьогодні звичайна мудрість говорить, що це необхідність. Але це не зміниться, якщо не буде введено значно більш жорсткої програмної відповідальності - чого ніхто в галузі не хоче.


-1: Звинувачення в управлінні як корені всього зла не особливо конструктивне. Ігнорування історії трохи менше. Відповідь майже викуплена останнім реченням.
mattnz

Суворішу відповідальність за програмне забезпечення можуть запровадити користувачі, зацікавлені у безпеці та готові платити за неї. Можливо, це може бути запроваджено суворими штрафами за порушення безпеки. Ринкове рішення працює, якщо користувачі будуть готові платити за безпеку, але вони ні.
Девід Торнлі

4

Однією з великих можливостей використання C є те, що він дозволяє вам маніпулювати пам'яттю будь-яким способом, який вважаєте за потрібне.

Однією з найважливіших недоліків використання C є те, що він дозволяє вам маніпулювати пам'яттю будь-яким способом, який вважаєте за потрібне.

Існують безпечні версії будь-яких небезпечних функцій. Однак програмісти та компілятор не суворо застосовують їх використання.


2

чому творці C не виправляють ці проблеми, повторно доповнюючи бібліотеки?

Можливо, тому, що C ++ вже це робив, і він сумісний із зворотним кодом. Отже, якщо ви хочете, щоб у вашому коді C був безпечний тип рядка, просто використовуйте std :: string та записуйте свій код C за допомогою компілятора C ++.

Підсистема основної пам’яті може допомогти запобігти переповненню буфера, ввівши захисні блоки та перевірку дійсності їх - тому всі розподіли додають 4 байти «fefefefe», коли ці блоки записуються, система може викидати воблер. Це не гарантує завади запису пам’яті, але це покаже, що щось пішло не так і потрібно виправити.

Я думаю, що проблема полягає в тому, що стара програма strcpy тощо. Якщо їх видалити на користь strncpy тощо, це допоможе.


1
Видалення strcpy тощо повністю зробить додаткові шляхи оновлення ще складнішими, що, в свою чергу, призведе до того, що люди зовсім не модернізуються. Так, як це зроблено зараз, ви можете перейти до компілятора C11, потім почати використовувати варіанти _s, потім заборонити не-_s варіанти, а потім виправити існуюче використання протягом будь-якого періоду часу, що практично є життєздатним.

-2

Зрозуміти, чому проблема переповнення не вирішена. C був недоліком у кількох областях. У той час ці вади розглядалися як терпимі або навіть як особливість. Тепер десятиліття через ці недоліки неможливо виправити.

Деякі частини спільноти програмування не хочуть, щоб ці діри були підключені. Подивіться на всі вогняні війни, які починаються над струнами, масивами, покажчиками, збиранням сміття ...


5
LOL, страшна і неправильна відповідь.
Хіт Ханнікутт

1
Щоб пояснити, чому це погана відповідь: С насправді має багато недоліків, але допускати переповнення буфера тощо має дуже мало спільного з ними, але з основними вимогами до мови. Не можна було б розробити мову, щоб виконувати завдання C і не допускати переповнення буфера. Частини спільноти не хочуть відмовлятися від можливостей, що дозволяє їм C, часто з поважних причин. Існують також розбіжності щодо того, як уникнути деяких із цих проблем, показуючи, що ми не маємо повного розуміння дизайну мови програмування, і нічого більше.
Девід Торнлі

1
@DavidThornley: Можна створити мову для виконання роботи C, але зробити так, щоб звичайні ідіоматичні способи виконання дій щонайменше дозволяли б компілятору перевірити переповнення буфера досить ефективно, якщо компілятор вирішить це зробити. Існує величезна різниця між наявністю memcpy()та наявністю лише стандартних засобів для ефективного копіювання сегмента масиву.
supercat
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.