Civil &: строительная инженерия
Как написать безопасный код C, чтобы предотвратить переполнение буфера
Table of Contents
Буферные переливы остаются одной из самых устойчивых и опасных уязвимостей безопасности в программировании 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
}
Дополнительные лучшие практики
- Инициализировать буферы — Всегда нуль-инициализировать буферы, чтобы избежать утечки неинициализированной памяти.
- Избегайте рекурсии с неограниченной глубиной — переполнения стека могут происходить из глубокой рекурсии; используйте итерацию или ограничьте глубину.
- Использовать квалификатор — Помогает компилятору оптимизировать и может улавливать проблемы с псевдонимами, хотя и не напрямую предотвращая переполнения.
- Предпочитает -правильность — предотвращает случайную модификацию входных строк и обеспечивает намерение.
- Осуществлять обработку ошибок — Не игнорируйте значения возврата от таких функций, как , , и т. д.
Ресурсы для дальнейшего обучения
- SEI CERT C Coding Standard — Комплексные правила безопасного кодирования C.
- CWE-120: Буферная копия без проверки размера входа — классификация MITRE недостатков переполнения буфера.
- OWASP Buffer Overflow — практическое руководство по проекту Open Web Application Security Project.
- GNU C Library Manual: String and Array Utilities — Документация для безопасных струнных функций.
- AddressSanitizer — быстрый детектор ошибок памяти.
Заключение
Предотвращение переполнения буфера в C не является обязательным; это фундаментальная ответственность любого разработчика, работающего с языком. Понимая механизмы переполнения, заменяя опасные функции более безопасными альтернативами, строго проверяя вводы и размеры, обеспечивая защиту компилятора и используя статический анализ и тестирование, вы можете резко снизить риск этих уязвимостей. Ни одна техника не является достаточной; защита в глубине - объединение дисциплины кодирования, флагов компилятора, смягчения ОС и тщательного тестирования - обеспечивает самую сильную защиту. С помощью этих практик вы можете писать код C, который является одновременно мощным и безопасным, способным безопасно работать в критических системах. Помните: безопасное кодирование - это постоянная практика, а не одноразовое исправление. Будьте в курсе новых уязвимостей и постоянно пересматривайте и улучшайте свой код.