Оценить:
 Рейтинг: 0

19 смертных грехов, угрожающих безопасности программ

Год написания книги
2011
<< 1 ... 8 9 10 11 12 13 14 15 16 ... 22 >>
На страницу:
12 из 22
Настройки чтения
Размер шрифта
Высота строк
Поля

THING* AllocThings(int a, int b, int c, int d)

{

int bufsize;

THING* ptr;

bufsize = IntegerOverflowsRUs(a, b, c, d);

ptr = (THING*)malloc(bufsize);

return ptr;

}

Ошибка скрывалась в функции, вычисляющей размер буфера, к тому же найти ее мешали загадочные, ничего не говорящие читателю имена переменных (и литералов, которые представлены типом signed int). Если у вас есть время на доскональный анализ, проследите порядок вызова всех ваших функций вплоть до обращений к низкоуровневым библиотечным функциям или системным вызовам. И напоследок выясните, откуда поступают данные. Можете ли вы утверждать, что аргументы функций не подвергались манипуляциям? Кто контролирует аргументы: вы или потенциальный противник?

По мнению автора языка Perl, величайшим достоинством программиста является лень! Так давайте пойдем простым путем – заставим потрудиться компилятор. Включите уровень диагностики /W4 (для Visual С++) или–Wall либо–Wsign–compare (для gcc) – и вы увидите, как много в вашей программе мест, где возможны проблемы с целыми числами. Обращайте внимание на все предупреждения, касающиеся целых чисел, особенно на те, в которых говорится о сравнении знаковых и беззнаковых величин, а также об усечении.

В Visual С++ самыми важными с этой точки зрения являются предупреждения С4018, С4389иС4244.

В gcc ищите предупреждения «warning: comparison between signed and unsigned integer expressions».

Относитесь с подозрением к директивам #pragma отключающим предупреждения, например:

#pragma warning(disable : 4244)

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

int ConcatBuffers(char *buf1, char *buf2,

size_t len1, size_t len2) {

char buf[0xFF];

if(len1 + len2) > 0xFF) return -1;

memcpy(buf, buf1, len1);

memcpy(buf + len1, buf2, len2);

// сделать что-то с buf

return 0;

}

Здесь проверяется, что суммарный размер двух входных буферов не превышает размера выходного буфера. Но если lenl равно 0x103, а 1еп2 равно 0xfffffffc, то сумма переполняет 32–разрядный регистр процессора и оказывается равной 255 (0xff), так что проверка успешно проходит. В результате memcpy попытается записать примерно 4 Гб в буфер размером всего 255 байтов!

Не вздумайте подавлять эти надоедливые предупреждения путем приведения типов. Теперь вы знаете, насколько это рискованно и как внимательно нужно все проверять. Найдите все приведения и убедитесь, что они безопасны. О приведениях и преобразованиях в языках С и С++ см. раздел «Операции приведения» выше.

Вот еще один пример:

int read(char* buf, size_t count) {

// Сделать что-то с памятью

}

...

while (true) {

BYTE buf[1024];

int skip = count – cbBytesRead;

if (skip > sizeof(buf))

skip = sizeof(buf);

if (read(buf, skip))

cbBytesRead += skip;

else

break;

...

В этом фрагменте значение skip сравнивается с 1024, и если оно меньше, то в буфер buf копируется skip байтов. Проблема в том, что если skip оказывается отрицательным (скажем, -2), то оно будет заведомо меньше 1024, так что функция read попытается скопировать–2 байта, а это значение, будучи представлено как целое без знака (тип size_t), равно без малого 4 Гб. Итак, read() копирует 4 Гб в буфер размером 1 Кб. Печально!

Еще один источник неприятностей – это оператор new в языке С++. Он сопряжен с неявным умножением:

Foo *p = new Foo(N);

Если N контролируется противником, то возможно переполнение внутри operator new при вычислении выражения N * sizeof(Foo);

С#

Хотя сам язык С# и не допускает прямых обращений к памяти, но иногда программа обращается к системным вызовам, помещенным в блок, помеченный ключевым словом unsafe (при этом ее еще надо компилировать с флагом /unsafe). Любые вычисления, результат которых передается системному вызову, нужно контролировать. Для этого полезно применить ключевое слово checked или – еще лучше – соответствующий флаг компилятора. Включите его и следите, не произойдет ли исключения. Напротив, ключевое слово unchecked используйте изредка, предварительно обдумав все последствия.

Java

В языке Java прямые обращения к памяти также запрещены, поэтому он не так опасен, как C/C++. Но проявлять беспечность все же не следует: как и в C/C++, в Java нет никакой защиты от переполнения целых, и легко можно допустить логические ошибки. О том, какие существуют программные решения, см. в разделе «Искупление греха».

Visual Basic и Visual Basic .NET
<< 1 ... 8 9 10 11 12 13 14 15 16 ... 22 >>
На страницу:
12 из 22