Написание идиом Secure C и Secure C

"Средний человек не хочет быть свободным. Он просто хочет быть в безопасности". - HL Menken

Я пытаюсь написать очень безопасный C. Ниже я перечисляю некоторые из методов, которые я использую, и спрашиваю, насколько они безопасны, как мне кажется. Пожалуйста, не стесняйтесь разрывать мой код / ​​предубеждения в клочья. Любой ответ, который найдет даже самую тривиальную уязвимость или научит меня новой идее, будет высоко оценен.

Чтение из потока:

Согласно учебному пособию по программированию GNU C getline:

Функция getline будет автоматически увеличивать блок памяти по мере необходимости через функцию realloc, поэтому никогда не будет недостатка места - одна из причин, по которой getline так безопасна. [..] Обратите внимание, что getline может безопасно обрабатывать вашу строку ввода, независимо от того, насколько она длинна.

Я предполагаю, что getline должен, при всех входах, предотвращать переполнение буфера при чтении из потока.

  • Правильно ли мое предположение? Существуют ли схемы ввода и / или распределения, в соответствии с которыми это может привести к эксплойту? Например, что если первый символ в потоке является каким-то странным управляющим символом, может быть 0x08 BACKSPACE (ctl-H).
  • Была ли сделана какая-либо работа, чтобы математически доказать, что getline безопасен?

Маллок возвращает ноль при неудаче:

Если malloc обнаруживает ошибку, malloc возвращает NULL-указатель. Это создает угрозу безопасности, поскольку к указателю NULL (0x0) все равно можно применить арифметику указателя, поэтому википедия рекомендует

/* Allocate space for an array with ten elements of type int. */
int *ptr = (int*)malloc(10 * sizeof (int));
if (ptr == NULL) {
    /* Memory could not be allocated, the program should handle 
       the error here as appropriate. */
} 

Безопасный sscanf:

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

const char *inputStr = "a01234b4567c";
const char *formatStr = "a%[0-9]b%[0-9]c":
char *str1[strlen(inputStr)];
char *str2[strlen(inputStr)];

sscanf(inputStr, formatStr, str1, str2);

Поскольку str1 и str2 имеют размер inputStr и из inputStr не может быть прочитано больше символов, чем strlen (inputStr), кажется невозможным, учитывая все возможные значения для inputStr, вызывать переполнение буфера?

  • Я прав? Есть странные угловые случаи, о которых я не думал?
  • Есть ли лучшие способы написать это? Библиотеки, которые уже решили это?

Основные вопросы:

Хотя я опубликовал большое количество вопросов, я не ожидаю, что кто-нибудь ответит на все из них. Вопросы в большей степени ориентированы на те ответы, которые я ищу. Я действительно хочу изучить безопасное мышление.

  • Какие еще безопасные идиомы существуют?
  • Какие угловые случаи мне нужно всегда проверять?
  • Как я могу написать модульные тесты для обеспечения соблюдения этих правил?
  • Как я могу применить ограничения в тестируемости или доказуемо правильным способом?
  • Любая рекомендуемая техника статического / динамического анализа или инструменты для C?
  • Какими безопасными методами С вы следуете и как оправдываете себя и других?

Ресурсы:

Многие ресурсы были заимствованы из ответов.

7 ответов

Решение
  1. Чтение из потока

Дело в том, что getline() "автоматически увеличит блок памяти по мере необходимости" означает, что это может быть использовано как атака типа "отказ в обслуживании", поскольку было бы тривиально сгенерировать ввод, который был бы настолько длинным, что исчерпал бы доступную память для процесса (или хуже системы!). Как только возникает нехватка памяти, в игру могут вступать и другие уязвимости. Поведение кода в нехватке / отсутствии памяти редко бывает хорошим, и его очень трудно предсказать. ИМХО, безопаснее устанавливать разумные верхние границы для всего, особенно в чувствительных к безопасности приложениях.

Кроме того (как вы ожидаете, упоминая специальные символы), getline() только дает вам буфер; он не дает никаких гарантий относительно содержимого буфера (поскольку безопасность полностью зависит от приложения). Таким образом, санация входных данных все еще является важной частью обработки и проверки пользовательских данных.

  1. sscanf

Я бы предпочел использовать библиотеку регулярных выражений и иметь очень узкие регулярные выражения для пользовательских данных, а не использовать sscanf, Таким образом, вы можете выполнить много проверок во время ввода.

  1. Общие комментарии

    • Доступны инструменты фаззинга, которые генерируют случайный ввод (действительный и недействительный), который можно использовать для проверки вашей обработки ввода
    • Управление буфером имеет решающее значение: переполнение буфера, недополнение, нехватка памяти
    • Условия гонки могут быть использованы в другом безопасном коде
    • Двоичными файлами можно манипулировать, чтобы ввести в заголовки недопустимые значения или слишком большие значения, поэтому код формата файла должен быть надежным и не предполагать, что двоичные данные действительны
    • Временные файлы часто могут быть источником проблем безопасности, и должны тщательно управляться
    • Внедрение кода может использоваться для замены системных вызовов или вызовов библиотеки времени выполнения вредоносными версиями.
    • Плагины обеспечивают огромный вектор для атаки
    • В качестве общего принципа я хотел бы предложить иметь четко определенные интерфейсы, в которых пользовательские данные (или любые данные извне приложения) считаются недействительными и враждебными до тех пор, пока они не будут обработаны, очищены и проверены, и единственным способом доступа пользовательских данных к приложению

Я думаю, что твой пример с sscanf неверен. Это может все еще переполниться при использовании таким образом.

Попробуйте это, которое определяет максимальное количество байтов для чтения:

void main(int argc, char **argv)
{
  char buf[256];
  sscanf(argv[0], "%255s", &buf);
}

Взгляните на эту статью разработчика IBM о защите от переполнения буфера.

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

Хорошее место, чтобы начать смотреть на это - отличный сайт безопасного кодирования Дэвида Уилера.

Его бесплатная онлайн-книга " Безопасное программирование для Linux и Unix HOWTO" - отличный ресурс, который регулярно обновляется.

Вы также можете посмотреть на его превосходный статический анализатор FlawFinder, чтобы получить дополнительные подсказки. Но помните, никакой автоматизированный инструмент не может заменить хорошую пару опытных глаз, или, как выразился Дэвид так красочно...

Любой инструмент статического анализа, такой как Flawfinder, является просто инструментом. Никакой инструмент не может заменить человеческую мысль! Короче говоря, "дурак с инструментом все еще дурак". Ошибочно думать, что инструменты анализа (такие как дефектоскоп) заменяют обучение и знания в области безопасности.

Я лично использовал ресурсы Дэвида уже несколько лет и считаю их превосходными.

Янник Мой разработал наиболее слабую систему предварительных условий Хоара / Флойда для C во время своей кандидатской диссертации и применил ее к библиотеке управляемых строк CERT. Он нашел много ошибок (см. Стр. 197 его мемуаров). Хорошая новость заключается в том, что библиотека теперь безопаснее для его работы.

Вы также можете посмотреть на веб-сайте Les Hatton здесь и на его книгу Safer C, которую вы можете получить с Amazon.

Не использовать gets() для ввода используйте fgets(), Использовать fgets(), если ваш буфер автоматически выделяется (т. е. "в стеке"), то используйте эту идиому:

char buf[N];
...
if (fgets(buf, sizeof buf, fp) != NULL)

Это будет работать, если вы решите изменить размер buf, Я предпочитаю эту форму:

#define N whatever
char buf[N];
if (fgets(buf, N, fp) != NULL)

потому что первая форма использует buf определить второй аргумент и понятнее.


Проверьте возвращаемое значение fclose() ,


Другие вопросы по тегам