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

Понимание переполнения буфера

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

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

Перелив на основе стека

Локальные переменные, включая буферы, объявленные внутри функций, хранятся на стеке. Стек также содержит обратный адрес, сохраненные указатели кадра и другие управляющие данные. Когда линейный буфер, такой как , переполнен, данные попадают в обратный адрес и за его пределы. Классические эксплойты, такие как червь Морриса (1988), использовали переполнение стека для получения несанкционированного доступа.

Перелив на основе кучи

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

Общие уязвимые функции и их безопасные альтернативы

Стандартная библиотека C предоставляет несколько функций, которые не выполняют проверку границ. Использование их является наиболее распространенной причиной переполнения буфера. Замена их более безопасными аналогами является фундаментальной лучшей практикой.

Струнная копия и конкатенация

  • — небезопасно: копии до нулевого терминатора, без ограничения длины.
    Безопасная альтернатива: — копии максимум n символов; обратите внимание, что она не прекращается, если источник длиннее n, поэтому всегда вручную прекращается.
  • Еще лучше: — доступен на BSD и многих системах Linux; всегда с нулевым окончанием и возвращает длину строки источника для обнаружения усечения.
  • — небезопасно: конкатенаты без границ.
    Безопасная альтернатива: — добавляется максимум n символов и всегда нулевых.

Форматированный выход и ввод

  • — Небезопасно: записывает отформатированный вывод в буфер без проверки размера.
    Безопасная альтернатива: — ограничивает вывод символами размера-1 плюс нулевой терминатор.
  • — аналогичный риск; используйте .
  • — чрезвычайно опасный; исключен из стандарта C11.
  • — Нет ограничений.Использовать или с указанием ширины поля.

Память копирует и перемещает

  • — Безопасен только в том случае, если n проверено на отсутствие превышения размера буфера назначения.
    Альтернатива: (перекрывающиеся ручки) и всегда обеспечивает n ≤ dest размер.
  • Некоторые платформы предоставляют из Приложения K (факультативно в C11), но принятие ограничено.

Валидация и управление размерами

Даже с безопасными функциями вы должны проверить длину входа, обеспечить правильные размеры буфера и изящно обрабатывать потенциальное усечение.

Проверьте входные длины

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

#define MAX_INPUT 255
char buffer[MAX_INPUT + 1]; // +1 for null
if (strlen(user_input) > MAX_INPUT) {
 // Handle error: reject or truncate
 fputs("Input too long", stderr);
 return -1;
}
strncpy(buffer, user_input, sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0';

Используйте буферы фиксированного размера с известными ограничениями

По возможности, определите буферы с постоянным размером и применяйте его по всему коду. Избегайте массивов переменной длины (VLA), которые могут вызвать переполнение стека, если поставляются большие размеры. Вместо этого динамически распределяйте с явными проверками размера.

Усечение рук явно

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

Флаги безопасности компилятора и защита от времени выполнения

Современные компиляторы предлагают флаги, которые добавляют обнаружение переполнения буфера и смягчение последствий без изменений кода.

  • — Вставляет канарейки стека (случайные значения) перед обратными адресами.Если переполнение буфера перезаписывает канарейку перед изменением обратного адреса, программа прерывается до завершения эксплойта.
  • — Заменяет вызовы небезопасных функций, таких как и , на проверенные версии, которые прерывают работу, если буфер назначения слишком мал. или требует более высокой оптимизации.
  • — Предупреждения об уязвимостях строк формата, которые могут привести к переполнению буфера или утечке информации.
  • — Код инструментов AddressSanitizer (ASan) для обнаружения переполнения буфера, ошибок использования после освобождения и других ошибок памяти во время выполнения. Замедляет выполнение, но неоценим для тестирования.
  • — Избегает оптимизации проверок переполнения (используется с осторожностью).

Защита операционной системы

Стековые канарейки - это всего лишь один слой. Технологии смягчения последствий эксплуатации в современных ОС включают:

  • Предотвращение выполнения данных (DEP) / NX бит — стек и куча меток как неисполняемые, предотвращающие выполнение кода оболочки.
  • Адресная рандомизация пространственного слоя (ASLR) — рандомизирует адреса памяти (стек, куча, общие библиотеки), чтобы затруднить прогнозирование целей.
  • Relocation Read-Only (RELRO) — Защищает GOT (Global Offset Table) от перезаписи.

Включение этих защит (обычно по умолчанию) повышает планку для использования, но не заменяет безопасное кодирование.

Аудит кода и статический анализ

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

  • Обзор управляемого кода — ищите использование небезопасных функций, пропущенных проверок размера и циклов, которые пишут за пределами буферных границ.
  • Статические инструменты анализа — Инструменты, такие как , , и , обнаруживают потенциальные перетоки, использование опасных функций и ошибки по отдельности.
  • Fuzzing — Используйте libFuzzer, AFL или другие fuzzer для автоматического тестирования обработки ввода с неожиданными данными, которые могут вызвать переполнение.

Примеры безопасного кода

Безопасная струнная копия с проверкой границ

#include <stdio.h>
#include <string.h>

int safe_string_copy(char *dest, size_t dest_size, const char *src) {
 if (!dest || !src || dest_size == 0) {
 return -1; // Invalid parameters
 }
 size_t src_len = strlen(src);
 if (src_len >= dest_size) {
 // Source too large; truncation or error
 // Option: copy what fits and null-terminate
 strncpy(dest, src, dest_size - 1);
 dest[dest_size - 1] = '\0';
 return 1; // Truncation occurred
 }
 strncpy(dest, src, dest_size);
 // strncpy fills remaining with null, so dest_size fits; no need to null-terminate if src shorter
 return 0; // Success, no truncation
}

Безопасное обращение с целым числом для размеров буфера

Перелив буфера также может быть результатом целых переливов при вычислении размеров. Всегда проверяйте арифметику перед распределением.

#include <stdlib.h>
#include <limits.h>
#include <errno.h>

void *safe_malloc_array(size_t nmemb, size_t size) {
 if (nmemb == 0 || size == 0) {
 return NULL; // Or handle zero-size allocation
 }
 if (nmemb > SIZE_MAX / size) {
 // Integer overflow would occur
 errno = ENOMEM;
 return NULL;
 }
 return malloc(nmemb * size);
}

Использование snprintf для форматированных строк

char log_message[256];
int ret = snprintf(log_message, sizeof(log_message),
 "User %s logged in from %s", username, ip_address);
if (ret < 0) {
 // Output error
} else if ((size_t)ret >= sizeof(log_message)) {
 // Truncation occurred; handle if needed
}

Дополнительные лучшие практики

  • Инициализировать буферы — Всегда нуль-инициализировать буферы, чтобы избежать утечки неинициализированной памяти.
  • Избегайте рекурсии с неограниченной глубиной — переполнения стека могут происходить из глубокой рекурсии; используйте итерацию или ограничьте глубину.
  • Использовать квалификатор — Помогает компилятору оптимизировать и может улавливать проблемы с псевдонимами, хотя и не напрямую предотвращая переполнения.
  • Предпочитает -правильность — предотвращает случайную модификацию входных строк и обеспечивает намерение.
  • Осуществлять обработку ошибок — Не игнорируйте значения возврата от таких функций, как , , и т. д.

Ресурсы для дальнейшего обучения

Заключение

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