Почему этот код уязвим для атак с переполнением буфера?

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, но я не уверен.

Любые идеи?

Ответ 1

В большинстве компиляторов максимальное значение 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

Проблема здесь:

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

Если строка больше длины целевого буфера, strncpy все равно скопирует ее. Вы определяете количество символов строки в качестве числа для копирования вместо размера буфера. Правильный способ сделать это:

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

То, что это делает, ограничивает объем данных, скопированных на фактический размер буфера минус один для символа с нулевым завершающим символом. Затем мы устанавливаем последний байт в буфере нулевым символом в качестве дополнительной защиты. Причина этого заключается в том, что strncpy будет копировать до n байтов, включая завершающий нуль, если strlen (str) < len - 1. Если нет, то нуль не копируется, и у вас есть сценарий сбоя, потому что теперь ваш буфер имеет неисчерпаемую строку.

Надеюсь, что это поможет.

РЕДАКТИРОВАТЬ: после дальнейшего изучения и ввода от других, возможно следующее кодирование функции:

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 байт.

РЕДАКТ. Хотя ответ о коротком без знака обычно правилен, так как максимальная длина, которая может быть представлена, составляет 65 535 символов, это не имеет большого значения, потому что если строка длиннее, значение будет обтекаться. Это похоже на 75 231 (что составляет 0x000125DF) и маскирование верхних 16 бит, давая вам 9695 (0x000025DF). Единственная проблема, с которой я вижу, - это первые 100 символов прошлых 65,535, поскольку проверка длины позволит копировать , но она будет копировать только до 100 символов строки во всех случаях, а null завершает строку. Таким образом, даже с проблемой обхода, буфер все равно не будет переполнен.

Это может или не может само по себе представлять угрозу безопасности в зависимости от содержимого строки и того, для чего вы ее используете. Если это просто прямой текст, который является читаемым человеком, тогда, как правило, нет проблем. Вы просто получаете усеченную строку. Однако, если это похоже на URL-адрес или даже последовательность команд SQL, у вас может быть проблема.

Ответ 3

Даже если вы используете 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;
}

Ответ 4

Ответ с упаковкой правильный. Но есть проблема, о которой я думаю не упоминался если (len >= 100)

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

Проблемная строка из C является ИМХО неразрешимой. У вас всегда было бы больше ограничений перед вызовом, но даже это не поможет. Проверка границ не происходит, и поэтому переполнение буфера всегда может и, к сожалению, произойдет....

Ответ 5

Помимо проблем безопасности, связанных с вызовом 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 байты, но другой поток должен был изменить исходную строку, результатом может быть конечная строка с конечным номером.