Не следует напрямую использовать устаревшие функции для копирования строк и ввода данных, такие как strcpy
, strcat
, sprintf
, wcscpy
и mbscpy
. Эти функции могут выводить длинные строки без ограничения их длины. Если возможно, рекомендуется использовать безопасные версии этих функций с суффиксом _s
, или использовать версии функций с префиксом n
, такие как snprintf
или vsnprintf
.
При использовании функций типа sscanf
для обработки строковых входных данных необходимо строго ограничивать длину строки с помощью формата, такого как %10s
, и убедиться, что строка заканчивается символом \0
. Если возможно, также рекомендуется использовать безопасную версию этих функций.
Однако стоит отметить, что хотя MSVC 2015 по умолчанию включает функции с завершающим нулём, такие как snprintf
, более ранние версии MSVC могут использовать макрос _snprintf
, который не гарантирует завершение строки нулём (см. вторую половину этого раздела).
Поэтому при использовании функций копирования с префиксом n
необходимо правильно рассчитать размер буфера и, если вы не уверены в том, что код обеспечивает завершение строки нулём на всех компиляторах, рекомендуется увеличить размер буфера на один байт и установить его значение равным нулю, чтобы гарантировать наличие завершающего нуля в строке.
Некоторые функции, такие как strncpy
и _snprintf
, могут быть небезопасными. strncpy
не следует рассматривать как безопасную версию strcpy
. strncpy
может не добавлять завершающий ноль при копировании строки, если длина превышает указанный предел.
Аналогично, MSVC _snprintf
также может не завершать строку нулём при превышении указанного предела. Это может привести к утечке соседних данных или сбою программы.
В следующем примере кода показано, как правильно использовать функцию _snprintf
:
// Good
char a[4] = {0};
_snprintf(a, sizeof(a), "%s", "AAAA");
a[sizeof(a) - 1] = '\0';
foo = strlen(a);
Рекомендуется использовать более высокоуровневые компоненты, такие как строки (string
) и векторы (vector
), в C++, чтобы повысить читаемость и безопасность кода. Перевод текста на русский язык:
1.2 Создание функций класса процесса: стандарты безопасности
Функции запуска процессов, такие как system, WinExec, CreateProcess и ShellExecute, требуют строгой проверки их параметров.
Для запуска процесса необходимо добавить двойные кавычки. Пример неправильного использования:
// Bad
WinExec("D:\\program files\\my folder\\foobar.exe", SW_SHOW);
Если существует файл «D:\program files\my.exe», то запустится my.exe, а foobar.exe не запустится.
Пример правильного использования:
// Good
WinExec("\"D:\\program files\\my folder\\foobar.exe\"", SW_SHOW);
Кроме того, если при запуске из пользовательского ввода или чтения командной строки из переменных среды используется комбинированная команда, необходимо также учитывать возможность внедрения команды.
Пример неправильного использования:
// Bad
std::string cmdline = "calc ";
cmdline += user_input;
system(cmdline.c_str());
Например, если пользователь введёт «1+1 && ls», то фактически будут выполнены команды calc 1+1 и ls, что приведёт к внедрению команды.
Необходимо проверять ввод пользователя на наличие недопустимых данных.
Пример правильного использования:
// Good
std::string cmdline = "ls ";
cmdline += user_input;
if (cmdline.find_first_not_of("1234567890.+-*/e ") == std::string::npos)
system(cmdline.c_str());
else
warning(...);
Связанные уязвимости:
1.3 Минимизация использования _alloca и массивов переменной длины
Объём памяти, используемый функциями _alloca и массивами переменной длины, неизвестен во время компиляции. Особенно это опасно при использовании в циклах, так как в зависимости от реализации компилятора это может привести к:
Пример неправильного использования:
// Bad
for (int i = 0; i < 100000; i++) {
char* foo = (char *)_alloca(0x10000);
..do something with foo ..;
}
void Foo(int size) {
char msg[size]; // Неконтролируемый риск переполнения стека!
}
Пример правильного использования:
// Good
// Использование динамически выделяемой памяти кучи
for (int i = 0; i < 100000; i++) {
char * foo = (char *)malloc(0x10000);
..do something with foo ..;
if (foo_is_no_longer_needed) {
free(foo);
foo = NULL;
}
}
void Foo(int size) {
std::string msg(size, '\0'); // C++
char* msg = malloc(size); // C
}
Связанные уязвимости:
1.4 Соответствие параметров функциям printf
Все функции printf, такие как sprintf, snprintf и vprintf, должны соответствовать управляющим символам и параметрам.
Пример неправильного использования:
// Bad
const int buf_size = 1000;
char buffer_send_to_remote_client[buf_size] = {0};
snprintf(buffer_send_to_remote_client, buf_size, "%d: %p", id, some_string); // %p должен быть заменён на %s
buffer_send_to_remote_client[buf_size - 1] = '\0';
send_to_remote(buffer_send_to_remote_client);
В этом случае клиентский злоумышленник может получить часть исходного адреса указателя сервера, что можно использовать для обхода защиты ASLR.
Пример правильного использования:
// Good
const int buf_size = 1000;
char buffer_send_to_remote_client[buf_size] = {0};
snprintf(buffer_send_to_remote_client, buf_size, "%d: %s", id, some_string);
buffer_send_to_remote_client[buf_size - 1] = '\0';
send_to_remote(buffer_send_to_remote_client);
Предыдущее использование может позволить злоумышленнику получить часть исходного указателя сервера и использовать его для нарушения защиты ASLR, облегчая атаку на программу.
Связанная уязвимость:
1.5 Предотвращение утечки значений указателей (включая %p)
Все функции семейства printf должны предотвращать утечку строковых значений, форматированных с помощью этих функций, которые могут содержать информацию о структуре программы. Например, если строка с %p будет передана программе, это может нарушить защиту ASLR и облегчить атаку на программу.
Значение %p должно использоваться только внутри программы и не должно выводиться наружу или получаться извне каким-либо образом.
Пример неправильного использования:
// Bad
// Если это открытый API:
uint64_t GetUniqueObjectId(const Foo* pobject) {
return (uint64_t)pobject;
}
Пример правильного использования:
// Good
uint64_t g_object_id = 0;
void Foo::Foo() {
this->object_id_ = g_object_id++;
}
// Если это открытый API:
uint64_t GetUniqueObjectId(const Foo* object) {
if (object)
return object->object_id_;
else
error(...);
}
Связанное уязвимость:
1.6 Не следует использовать строки, контролируемые пользователем, в качестве параметров формата функций printf
Если строка контролируется пользователем, она может быть использована для выполнения произвольного вредоносного кода через %n %p и другие подобные параметры.
Особенно важно учитывать это в следующих случаях: имена Wi-Fi, имена устройств и т. д.
Неправильное использование:
snprintf(buf, sizeof(buf), wifi_name);
Правильное использование:
snprinf(buf, sizeof(buf), "%s", wifi_name);
Связанная уязвимость:
1.7 Удаление массивов: использование delete[] вместо delete
Оператор delete используется для удаления объектов, не являющихся массивами, а оператор delete[] — для удаления массивов. Они вызывают соответственно operator delete[] и operator delete.
Неправильное использование:
// Bad
Foo* b = new Foo[5];
delete b; // trigger assert in DEBUG mode
Поведение, зависящее от неопределённого поведения компилятора, может возникнуть при вызове delete для указателя, возвращаемого оператором new[]. Код, который зависит от такого неопределённого поведения, является ошибочным.
Правильное использование:
// Good
Foo* b = new Foo[5];
delete[] b;
Использование строк, векторов и интеллектуальных указателей, таких как std::unique_ptr<T[]> в C++, может устранить большинство случаев использования delete[], делая код более понятным.
Связанная уязвимость:
1.8 Внимание к неявным преобразованиям знаков
Когда два беззнаковых числа вычитаются и результат становится отрицательным числом, он должен быть большим беззнаковым числом, но может произойти неожиданное неявное преобразование знака, когда маленькое беззнаковое число используется в арифметических операциях.
Пример неправильного использования:
// 1
unsigned char a = 1;
unsigned char b = 2;
if (a - b < 0) // a - b = -1 (signed int)
a = 6;
else
a = 8;
// 2
unsigned char a = 1;
unsigned short b = 2;
if (a - b < 0) // a
``` ```
b = -1 (signed int)
a = 6;
else
a = 8;
В данном фрагменте кода переменная a
всегда будет равна 8.
// 3
unsigned int a = 1;
unsigned short b = 2;
if (a - b < 0) // a - b = 0xffffffff (unsigned int)
a = 6;
else
a = 8;
// 4
unsigned int a = 1;
unsigned int b = 2;
if (a - b < 0) // a - b = 0xffffffff (unsigned int)
a = 6;
else
a = 8;
Здесь переменная a
, равная 1, преобразуется в тип unsigned int
, и затем происходит вычитание из неё переменной b
, равной 2. Результат операции сравнения помещается в переменную a
. Так как результат вычитания не может быть меньше нуля для типа unsigned
, то выполняется блок else
, где переменной a
присваивается значение 8. В итоге значение переменной a
будет равно 8.
Если ожидается, что значение переменной а
должно быть равно 8, то код содержит ошибку:
// Bad
unsigned short a = 1;
unsigned short b = 2;
if (a - b < 0) // a - b = -1 (signed int)
a = 6;
else
a = 8;
Здесь переменные a
и b
имеют тип unsigned short
, который является знаковым целочисленным типом. Поэтому при вычитании двух переменных получается значение -1
. Это значение сравнивается с нулём, и так как оно меньше нуля, выполняется блок if
, где переменной a
присваивается значение 6
. В результате значение переменной a
равно 6
, а не 8
.
Правильный код:
// Good
unsigned short a = 1;
unsigned short b = 2;
if ((unsigned int)a - (unsigned int)b < 0) // a - b = 0xffff (unsigned short)
a = 6;
else
a = 8;
Здесь значения переменных a
и b
приводятся к типу unsigned int
перед выполнением операции вычитания. Это позволяет избежать переполнения знакового целочисленного типа. Затем результат операции сравнивается с нулем, и если он меньше нуля, то переменной a
присваивается значение 6
. Иначе переменной a
присваивается значение 8
. В итоге значение переменной a
будет равно 8
. ### Перевод текста на русский язык
void foo_thread1() {
__sync_fetch_and_add(&g_somechar, 3);
}
void foo_thread2() {
__sync_fetch_and_add(&g_somechar, 1);
}
Для кода C после C11 рекомендуется использовать стандартную библиотеку atomic. Для кода C++ после C++11 рекомендуется использовать std::atomic.
Условия гонки часто возникают в обработчиках сигналов, поскольку обработчики сигналов поддерживают асинхронные операции. Злоумышленник может использовать условия гонки, вызванные обработчиками сигналов, чтобы нарушить работу программного обеспечения и даже вызвать отказ в обслуживании или выполнение кода.
// Bad
char *log_message;
void Handler(int signum) {
syslog(LOG_NOTICE, "%s\n", log_m_essage);
free(log_message);
sleep(10);
exit(0);
}
int main (int argc, char* argv[]) {
log_message = strdup(argv[1]);
signal(SIGHUP, Handler);
signal(SIGTERM, Handler);
sleep(10);
}
Проблемы можно избежать следующими способами:
Связанные уязвимости:
TOCTOU: программное обеспечение проверяет состояние ресурса перед его использованием, но состояние ресурса может измениться между проверкой и использованием, делая результат проверки недействительным. Если ресурс находится в неожиданном состоянии, это может привести к неправильным действиям со стороны программного обеспечения.
Если злоумышленник может повлиять на состояние ресурса между проверкой и использованием, эта проблема может быть связана с безопасностью. Это может произойти с общими ресурсами, такими как файлы, память и даже переменные в многопоточных программах. Необходимо учитывать предотвращение проблем TOCTOU при программировании.
Например, в следующем примере файл мог быть обновлён после lstat и до printf, особенно из-за задержки в printf.
struct stat *st;
lstat("...", st);
printf("foo");
if (st->st_mtimespec == ...) {
printf("Now updating things\n");
UpdateThings();
}
Хотя TOCTOU трудно исправить, существуют следующие смягчающие меры:
Связанная уязвимость:
Пароли пользователей должны быть хешированы с использованием алгоритмов, таких как Argon2, scrypt, bcrypt или pbkdf2, перед сохранением в системе хранения.
Пользовательские данные, требующие конфиденциальности, должны шифроваться во время передачи и храниться в зашифрованном виде. Шифрование во время передачи может осуществляться с помощью протоколов аутентифицированного шифрования, таких как HTTPS.
Данные, хранящиеся в зашифрованном виде, могут быть защищены с помощью решений, подобных SQLCipher.
Даже временные данные, такие как пароли, должны быть полностью очищены после использования.
Ошибка:
#include <openssl/crypto.h>
#include <unistd.h>
{
...
string user_password(100, '\0');
snprintf(&user_password, "password: %s", user_password.size(), password_from_input);
...
}
Правильно:
{
...
string user_password(100, '\0');
snprintf(&user_password, "password: %s", user_password.size(), password_from_input);
...
OPENSSL_cleanse(&user_password[0], user_password.size());
}
Связанная уязвимость:
Функции rand() не обеспечивают высокую степень случайности. Перед использованием необходимо инициализировать их с помощью srand(). Неинициализированные случайные числа могут сделать некоторые данные предсказуемыми.
В следующем коде значение foo фиксировано после выполнения. Оно эквивалентно srand(1); rand();
// Bad
int main() {
int foo = rand();
return 0;
}
После правильной инициализации значение foo будет случайным.
// Good
int main() {
srand(time(0));
int foo = rand();
return 0;
}
Связанная уязвимость:
При генерации ключей/IV/Nonce для алгоритмов, таких как AES/SM1/HMAC, или закрытых ключей для RSA/ECDSA/ECDH, где требуется высокая безопасность, необходимо использовать криптографически безопасные генераторы случайных чисел (CSPRNG), а не обычные генераторы случайных чисел без криптографической безопасности, такие как rand().
Рекомендуемые CSPRNG включают:
Текст запроса представляет собой фрагмент кода на языке C++, в котором обсуждаются проблемы безопасности, связанные с использованием функций генерации случайных чисел. В тексте также приводятся примеры неправильного использования таких функций и рекомендации по их безопасному использованию.
{
unsigned char key[16];
if (1 != RAND_bytes(&key[0], sizeof(key))) { //... 错误处理
return -1;
}
AES_KEY aes_key;
if (0 != AES_set_encrypt_key(&key[0], sizeof(key) * 8, &aes_key)) {
// ... 错误处理
return -1;
}
...
OPENSSL_cleanse(&key[0], sizeof(key));
}
В этом фрагменте кода используется функция RAND_bytes
для генерации случайного массива байтов. Затем эти байты используются для создания ключа шифрования с помощью функции AES_set_encrypt_key
. Наконец, массив байтов очищается с помощью функции OPENSSL_cleanse
.
Перевод:
#include <unistd.h>
{
unsigned char key[16]; // Массив из 16 байт
if (1 != RAND_bytes(&key[0], sizeof(key))) { // Если генерация случайных байтов не удалась
return -1; // Завершить программу с ошибкой
}
AES_KEY aes_key; // Ключ шифрования
if (0 != AES_set_encrypt_key(&key[0], sizeof(key) * 8, &aes_key)) { // Установить ключ шифрования
// Если установка ключа шифрования не удалась, завершить программу с ошибкой
return -1;
}
...
OPENSSL_cleanse(&key[0], sizeof(key)); // Очистить массив байтов
}
Далее в тексте обсуждается проблема использования функции rand()
для генерации случайных чисел в контексте безопасности. Автор отмечает, что функция rand()
не является криптографически безопасной и может использоваться только в некритичных приложениях. Вместо неё рекомендуется использовать более надёжные методы генерации случайных чисел, такие как random_device
в C++11 или библиотеки, предоставляющие криптографически безопасные генераторы случайных чисел, например OpenSSL.
Также в тексте приводятся примеры неправильного использования функций генерации случайных чисел и обсуждаются связанные с этим проблемы безопасности. Например, автор указывает на то, что использование простых линейных конгруэнтных алгоритмов для генерации псевдослучайных чисел может привести к предсказуемости результатов и, как следствие, к уязвимостям в системе безопасности.
Автор также подчёркивает важность правильного использования функций генерации случайных чисел при разработке программного обеспечения, особенно в контексте обеспечения безопасности данных. Текст на языке C++:
payload, sizeof(payload));
}
int main() {
MyStruct dst_stuct;
dst_struct.buf = (char*)user_controlled_value;
Write(dst_struct);
return 0;
}
Текст на русском языке:
Связанные уязвимости:
При расчётах необходимо учитывать возможность переполнения целых чисел, особенно при операциях с памятью, где требуется проверка размеров при выделении, копировании и других операциях, чтобы предотвратить переполнение целых чисел и связанные с этим уязвимости.
Пример ошибки (этот пример приводит к переполнению целых чисел при расчёте):
const int kMicLen = 4;
// Переполнение целых чисел
void Foo() {
int len = 1;
char payload[10] = { 0 };
char dst[10] = { 0 };
// Bad, из-за того, что len меньше 4, это приводит к вычислению длины копирования, которая вызывает переполнение целых чисел
// len - kMicLen == 0xfffffffd
memcpy(dst, payload, len - kMicLen);
}
Правильный пример:
void Foo() {
int len = 1;
char payload[10] = { 0 };
char dst[10] = { 0 };
int size = len - kMicLen;
// Копирование перед проверкой размера
if (size > 0 && size < 10) {
memcpy(dst, payload, size);
printf("memcpy good\n");
}
}
Связанная уязвимость:
«Высокий риск — повреждение памяти»
При выполнении вычислений или операций, если используются максимальные или минимальные значения, которые не являются правильными, и это значение отличается от правильного на 1, это может привести к рискам безопасности.
Ошибка:
char firstname[20];
char lastname[20];
char fullname[40];
fullname[0] = '\0';
strncat(fullname, firstname, 20);
// Второй вызов strncat() может добавить ещё 20 символов. Если эти 20 символов не заканчиваются нулевым символом, существует проблема безопасности
strncat(fullname, lastname, 20);
Правильно:
char firstname[20];
char lastname[20];
char fullname[40];
fullname[0] = '\0';
// При использовании таких функций, как strncat(), необходимо оставить один нулевой символ в конце буфера для предотвращения проблем безопасности
strncat(fullname, firstname, sizeof(fullname) - strlen(fullname) - 1);
strncat(fullname, lastname, sizeof(fullname) - strlen(fullname) - 1);
Для кода C++ настоятельно рекомендуется использовать такие компоненты, как string
, vector
и другие, вместо использования исходных указателей и массивов.
Связанная уязвимость:
«Высокий риск — повреждение памяти»
В некоторых сценариях, связанных с обработкой данных с разным порядком байтов, необходимо проводить проверку порядка байтов. Например, при извлечении значений из больших конечных устройств, они должны обрабатываться с использованием большого порядка байтов, чтобы избежать ошибок, связанных с порядком байтов.
Связанная уязвимость:
«Средний риск — логическая ошибка»
При проведении операций деления необходимо проверять делитель на равенство нулю, чтобы предотвратить неожиданные результаты или сбои программы.
Ошибка:
int divide(int x, int y) {
return x / y;
}
Правильно:
int divide(int x, int y) {
if (y == 0) {
throw DivideByZero;
}
return x / y;
}
Связанная уязвимость:
«Низкий риск — отказ в обслуживании»
При наличии знаковых и беззнаковых чисел в вычислениях необходимо обращать внимание на возможные логические ошибки, вызванные преобразованием типов, и рекомендуется явно указывать тип данных при выполнении вычислений или использовать унифицированный тип для всех вычислений.
Пример ошибочного кода:
int Foo() {
int len = 1;
unsigned int size = 9;
// 1 < 9 - 10 ? Из-за смешивания беззнакового и знакового чисел в операции, результат будет рассчитан как беззнаковое число
if (len < size - 10) {
printf("Bad\n");
} else {
printf("Good\n");
}
}
Пример правильного кода:
void Foo() {
// Унификация обоих чисел для вычислений со знаком
int len = 1;
int size = 9;
if (len < size - 10) {
printf("Bad\n");
} else {
printf("Good\n");
}
}
Связанная уязвимость:
«Высокий риск — повреждение памяти», «Средний риск — логическая ошибка».
При сравнении размеров данных во время операций необходимо разумно проверять диапазон данных, основываясь на типе данных, и определять максимальные и минимальные значения для предотвращения неожиданных ошибок.
Ошибка:
void Foo(int index) {
int a[30] = {0};
// Здесь index является целым числом, и только рассматривается случай, когда оно меньше размера массива, но не проверяется, является ли оно больше или равно нулю
if (index < 30) {
// Если index отрицательное, произойдёт выход за границы массива
a[index] = 1;
}
}
Правильно:
void Foo(int index) {
int a[30] = {0};
// Проверка максимального и минимального значений индекса
if (index >= 0 && index < 30) {
a[index] = 1;
}
}
Связанная уязвимость:
«Высокий риск — повреждение памяти»
За исключением тестирования текущей длины указателя, использование sizeof на указателе обычно не требуется.
Правильно:
size_t pointer_length = sizeof(void*);
Возможно ошибочное использование:
size_t structure_length = sizeof(Foo*);
Может быть:
size_t structure_length = sizeof(Foo);
Связанная уязвимость:
«Средний риск — логическая ошибка»
Ошибочный код:
int a[3];
...;
if (a > 0)
...;
Это сравнение всегда будет истинным и эквивалентно:
int a[3];
...;
if (&a[0])
...;
Можно включить достаточно строгие предупреждения компилятора (например, -Waddress
в GCC, которое уже включено в -Wall
), и настроить их как ошибки, чтобы обнаруживать такие проблемы во время компиляции.
Связанная уязвимость:
«Средний риск — логическая ошибка»
Особые случаи могут требовать особого подхода (например, разработка аппаратного обеспечения, где может потребоваться фиксированный адрес), но в контексте системных драйверов и подобных проектов, фиксированные адреса могут привести к будущим проблемам.
Связанная уязвимость:
«Высокий риск — повреждение памяти»
Ошибка:
*foo = 100;
if (!foo) {
ERROR("foobar");
}
Правильно:
if (!foo) {
ERROR("foobar");
}
*foo = 100;
Ошибка:
void Foo(char* bar) {
*bar = '\0';
}
Правильно:
void Foo(char* bar) {
if(bar)
*bar = '\0';
else
...;
}
Связанная уязвимость:
«Низкий риск — отказ в обслуживании»
После освобождения указателя необходимо установить его в NULL, чтобы предотвратить неправильное использование освобождённого указателя и возможные проблемы с повреждением памяти, особенно в структурах данных и классах, содержащих необработанные указатели.
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )