Чому цей код вразливий до атак переповнення буфера?


148
int func(char* str)
{
   char buffer[100];
   unsigned short len = strlen(str);

   if(len >= 100)
   {
        return (-1);
   }

   strncpy(buffer,str,strlen(str));
   return 0;
}

Цей код вразливий до атаки переповнення буфера, і я намагаюся з'ясувати, чому. Я думаю, що це стосується того, lenщоб оголосити shortзамість цього int, але я не дуже впевнений.

Будь-які ідеї?


3
З цим кодом існує кілька проблем. Нагадаємо, що рядки C недійсні.
Дмитро Чубаров

4
@DmitriChubarov, недійсне завершення рядка буде проблемою лише в тому випадку, якщо рядок використовується після виклику до strncpy. У цьому випадку це не так.
R Sahu

43
Проблеми в цьому коді випливають безпосередньо з того факту, який strlenобчислюється, використовується для перевірки валідності, а потім він знову абсурдно обчислюється - це несправність DRY. Якби другу strlen(str)замінили len, не було б можливості переповнення буфера, незалежно від типу len. Відповіді не стосуються цього питання, вони просто встигають цього уникнути.
Джим Балтер

3
@CiaPan: Wenn, передаючи ненульовий рядок до нього, strlen буде демонструвати невизначене поведінку.
Кайзерлуді

3
@JimBalter Не, я думаю, я залишу їх там. Можливо, хтось інший матиме таке ж нерозумне уявлення і вчитися на цьому. Сміливо позначте їх, якщо вони вас дратують, хтось може підійти і видалити їх.
Асад Саедюддін

Відповіді:


192

Для більшості компіляторів максимальне значення an unsigned shortстановить 65535.

Будь-яке значення вище, яке обертається навколо, тому 65536 стає 0, а 65600 стає 65.

Це означає, що довгі рядки потрібної довжини (наприклад, 65600) пройдуть перевірку та переповнюють буфер.


Використовуйте size_tдля зберігання результату strlen(), а не для unsigned shortпорівняння lenз виразом, що безпосередньо кодує розмір buffer. Так, наприклад:

char buffer[100];
size_t len = strlen(str);
if (len >= sizeof(buffer) / sizeof(buffer[0]))  return -1;
memcpy(buffer, str, len + 1);

2
@PatrickRoberts Теоретично, так. Але ви повинні пам’ятати, що 10% коду відповідає за 90% часу виконання, тому не слід відпускати продуктивність перед безпекою. І майте на увазі, що з часом код змінюється, що може раптом означати, що попередня перевірка пропала.
orlp

3
Щоб запобігти переповненню буфера, просто використовуйте lenяк третій аргумент strncpy. Повторне використання strlen є німим в будь-якому випадку.
Джим Балтер

15
/ sizeof(buffer[0])- зауважте, що sizeof(char)в C завжди 1 (навіть якщо значок містить газильйон біт), тому це зайве, коли немає можливості використовувати інший тип даних. Все-таки ... кудо за повну відповідь (і дякую, що відповідали на коментарі).
Джим Балтер

3
@ rr-: char[]і char*це не те саме. Існує безліч ситуацій, коли char[]алімент неявно перетворюється на a char*. Наприклад, char[]точно такий же, як і char*при використанні типу для аргументів функції. Однак перетворення не відбувається для sizeof().
Дітріх Епп

4
@Controll Тому що якщо ви зміните розмір bufferв якийсь момент, вираз автоматично оновлюється. Це критично важливо для безпеки, оскільки декларація bufferможе бути в деяких рядках від перевірки фактичного коду. Тож легко змінити розмір буфера, але забудьте оновити в кожному місці, де використовується розмір.
orlp

28

Проблема тут:

strncpy(buffer,str,strlen(str));
                   ^^^^^^^^^^^

Якщо рядок перевищує довжину цільового буфера, strncpy все одно скопіює його. Ви базуєте кількість символів рядка як число, яке потрібно скопіювати, а не розмір буфера. Правильний спосіб зробити це наступним чином:

strncpy(buffer,str, sizeof(buff) - 1);
buffer[sizeof(buff) - 1] = '\0';

Це робить обмеження кількості скопійованих даних фактичним розміром буфера мінус один для нульового завершального символу. Тоді ми встановлюємо останній байт у буфері до нульового символу як додатковий захист. Причиною цього є те, що strncpy буде копіювати до n байтів, включаючи закінчуючий null, якщо strlen (str) <len - 1. Якщо ні, то null не скопіюється, і у вас є сценарій збою, тому що тепер у вашому буфері немає невиконаного рядок.

Сподіваюся, це допомагає.

EDIT: Після подальшого вивчення та введення даних від інших можливе кодування функції:

int func (char *str)
  {
    char buffer[100];
    unsigned short size = sizeof(buffer);
    unsigned short len = strlen(str);

    if (len > size - 1) return(-1);
    memcpy(buffer, str, len + 1);
    buffer[size - 1] = '\0';
    return(0);
  }

Оскільки ми вже знаємо довжину рядка, ми можемо використовувати memcpy для копіювання рядка з місця, на яке посилається str у буфер. Зауважте, що на сторінці керівництва для strlen (3) (у системі FreeBSD 9.3) зазначено наступне:

 The strlen() function returns the number of characters that precede the
 terminating NUL character.  The strnlen() function returns either the
 same result as strlen() or maxlen, whichever is smaller.

Я трактую так, що довжина рядка не включає нуль. Ось чому я копіюю len + 1 байт, щоб включити нуль, а тест перевіряє, щоб довжина <розмір буфера - 2. Мінус один, тому що буфер починається в положенні 0, а мінус ще один, щоб переконатися, що є місце для нуля.

EDIT: Виявляється, розмір чогось починається з 1, а доступ починається з 0, тому -2 раніше був неправильним, оскільки він повертав би помилку за що-небудь> 98 байт, але він повинен бути> 99 байт.

EDIT: Хоча відповідь про неподписаний короткий, як правило, правильна, оскільки максимальна довжина, яку можна представити, становить 65 555 символів, це насправді не має значення, оскільки якщо рядок довший за це, значення буде обгортатися. Це як взяти 75,231 (що становить 0x000125DF) і замаскувати 16 кращих бітів, що дає 9695 (0x000025DF). Єдиною проблемою, яку я бачу в цьому, є перші 100 знаків минулого 65535, оскільки перевірка довжини дозволить копіювати, але вона копіюватиме лише до перших 100 символів рядка у всіх випадках і нульове завершення рядка . Тож навіть із проблемою обгортання, буфер все ще не буде переповнений.

Це само по собі може або не може становити загрозу безпеці залежно від вмісту рядка та того, для чого ви його використовуєте. Якщо це просто прямий текст, який читається людиною, то взагалі проблем немає. Ви просто отримуєте усічену рядок. Однак, якщо це щось на зразок URL-адреси або навіть послідовності команд SQL, у вас може виникнути проблема.


2
Щоправда, але це виходить за рамки питання. Код чітко показує функцію, що передається знаком покажчика. Поза межами функції, нас не хвилює.
Даніель Руді

"буфер, в якому зберігається str" - це не переповнення буфера , в чому полягає проблема. І кожна відповідь має ту "проблему", яка неминуча з огляду на підпис func... та будь-яку іншу C-функцію, коли-небудь написану, яка бере аргументи, що закінчуються NUL. Запропонувати можливість введення даних, що не закінчуються NUL, повністю зрозуміло.
Джим Балтер

"що виходить за рамки питання" - що, на жаль, виходить за межі здатності деяких людей до розуміння.
Джим Балтер

"Проблема тут" - ви праві, але ви все одно не вистачаєте ключової проблеми, яка полягає в тому, що тест ( len >= 100) був зроблений проти одного значення, але довжині копії було надано інше значення ... це є порушенням принципу DRY. Просто виклик strncpy(buffer, str, len)дозволяє уникнути можливості переповнення буфера і виконує менше роботи, ніж strncpy(buffer,str,sizeof(buffer) - 1)... хоча тут це просто повільніший еквівалент memcpy(buffer, str, len).
Джим Балтер

@JimBalter Це поза питанням, але я відхиляюсь. Я розумію, що значення, використовувані тестом, і те, що використовується у strncpy, є двома різними. Однак загальна практика кодування говорить, що межа копії повинна бути sizeof (буфер) - 1, тому не має значення, яка довжина str на копії. strncpy перестане копіювати байти, коли вона потрапляє в нуль або копіює n байтів. Наступний рядок гарантує, що останній байт у буфері - це нульовий знак. Код безпечний, я стою біля свого попереднього твердження.
Даніель Руді

11

Незважаючи на те, що ви використовуєте strncpy, довжина обрізу все ще залежить від переданого рядкового вказівника. Ви поняття не маєте, як довгий цей рядок (розташування нульового термінатора відносно вказівника, тобто). Таким чином, strlenлише дзвінки відкривають вам уразливість. Якщо ви хочете бути більш захищеними, використовуйте strnlen(str, 100).

Повний код буде виправлено:

int func(char *str) {
   char buffer[100];
   unsigned short len = strnlen(str, 100); // sizeof buffer

   if (len >= 100) {
     return -1;
   }

   strcpy(buffer, str); // this is safe since null terminator is less than 100th index
   return 0;
}

@ user3386109 Чи не буде strlenтакож доступ до кінця буфера?
Патрік Робертс

2
@ user3386109 те, що ви вказуєте, робить відповідь orlp настільки ж недійсною, як і моя. Я не розумію, чому strnlenце не вирішує проблему, якщо те, що пропонує Orlp, як нібито є правильним.
Патрік Робертс

1
"Я не думаю, що strnlen тут нічого не вирішує" - звичайно, це робиться; це запобігає переповненню buffer. "оскільки str може вказувати на буфер у 2 байти, жоден з яких не NUL." - це не має значення, оскільки це стосується будь-якої реалізації func. Питання тут стосується переповнення буфера, а не UB, оскільки вхід не закінчується NUL.
Джим Балтер

1
"Другий параметр, переданий strnlen, повинен бути розміром об'єкта, на який вказує перший параметр, або strnlen є нічим не потрібним" - це повна і сутня дурниця. Якщо другим аргументом strnlen є довжина вхідного рядка, то strnlen еквівалентний strlen. Як би ви навіть отримали цей номер, і якщо у вас його було, то навіщо вам зателефонувати на str [n] len? Це зовсім не для чого strnlen.
Джим Балтер

1
+1 Хоча ця відповідь недосконалий , тому що це не відповідає коду ФП в - strncpy NUL подушечки і не NUL закінчити, тоді як STRCPY NUL звільняє і не NUL-панелі, це дійсно вирішити цю проблему, в відміну від смішні, неосвічені коментарі вище.
Джим Балтер

4

Відповідь з обгортанням правильна. Але є проблема, яку я думаю, не згадували, якщо (len> = 100)

Добре, якщо Лен буде 100, ми б скопіювали 100 елементів, у нас не було б слідів \ 0. Це однозначно означало б, що будь-яка інша функція залежно від належного закінченого рядка пішла б за вихідний масив.

Рядок, проблемний з C, є IMHO нерозв'язним. Ви завжди завжди матимете певні обмеження перед викликом, але навіть це не допоможе. Немає перевірки меж, тому переповнення буфера завжди може, і на жаль, станеться ....


Рядок проблематичною є вирішуваною: просто використовувати відповідні функції. І. е. не strncpy() і друзі, але пам'ять, що виділяє функції, такі як strdup()і друзі. Вони відповідають стандарту POSIX-2008, тому вони досить портативні, хоча не доступні в деяких фірмових системах.
cmaster - відновити моніку

"будь-яка інша функція залежно від належного закінченого рядка" - bufferлокальна для цієї функції і не використовується в іншому місці. У реальній програмі нам слід було б вивчити, як вона використовується ... іноді не NUL-закінчення є правильним (первісне використання strncpy полягало у створенні 14-ти байтних записів каталогу UNIX - NUL-padded, а не NUL-завершене). "Рядок, проблемний з C, - це нерозв'язний IMHO" - в той час, як C - це чітка мова, яка була перевершена набагато кращою технологією, в ній можна записати безпечний код, якщо буде використана достатня дисципліна.
Джим Балтер

Ваше спостереження здається мені помилковим. if (len >= 100)є умовою, коли перевірка не відповідає , а не тоді, коли вона проходить, а це означає, що не існує випадків, коли копіюється точно 100 байтів без термінала NUL, оскільки ця довжина включена в стан відмови.
Патрік Робертс

@ cmaster. У цьому випадку ви помиляєтесь. Це не вирішується, тому що завжди можна писати межі межі. Так, це невизначена поведінка, але немає способу її повністю запобігти.
Фрідріх

@Jim Balter Не важливо. Я потенційно можу писати за межі цього локального буфера, тому завжди можна буде пошкодити деякі інші структури даних.
Фрідріх

3

Крім проблем із безпекою, пов'язаних із викликом strlenне один раз, зазвичай не слід застосовувати рядкові методи для рядків, довжина яких точно відома [для більшості рядкових функцій існує лише дійсно вузький випадок, коли вони повинні використовуватися - для рядків, для яких максимум довжина може бути гарантована, але точна довжина не відома]. Після того, як буде відома довжина вхідного рядка і відома довжина вихідного буфера, слід визначити, наскільки велика область повинна бути скопійована, а потім використовувати memcpy()для фактичного виконання відповідної копії. Хоча цілком можливо , що strcpyможе випереджати memcpy()при копіюванні рядки тільки 1-3 байт або близько того , на багатьох платформах memcpy(), ймовірно, буде більш ніж в два рази швидше при роботі з великими рядками.

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

char *strdupe(char const *src)
{
  size_t len = strlen(src);
  char *dest = malloc(len+1);
  // Calculation can't wrap if string is in valid-size memory block
  if (!dest) return (OUT_OF_MEMORY(),(char*)0); 
  // OUT_OF_MEMORY is expected to halt; the return guards if it doesn't
  memcpy(dest, src, len);      
  dest[len]=0;
  return dest;
}

Зауважте, що останнє твердження, як правило, може бути пропущено, якщо memcpy обробляв len+1байти, але це інший потік, щоб змінити вихідний рядок, результатом може бути рядок призначення, що не закінчується NUL.


3
Чи можете ви пояснити проблеми безпеки, пов’язані з дзвінками strlenне один раз ?
Богдан Олександру

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