Чтение онлайн

ЖАНРЫ

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

Виега Джон

Шрифт:

Поразрядные операции

Поразрядные операции AND, OR и XOR (исключающее или) вроде бы должны работать, но и тут расширение со знаком путает все карты. Рассмотрим пример:

...

int flags = 0x7f;

char LowByte = 0x80;

if ((char)flags ^ LowByte == 0xff)

return ItWorked;

Вам кажется, что результатом операции должно быть 0xff, именно с этим значением вы и сравниваете, но настырный компилятор решает все сделать по–своему и приводит оба операнда к типу int. Вспомните, мы же говорили, что даже для поразрядных операций выполняется приведение к int, если операнды имеют более узкий тип. Поэтому flags расширяется до 0x0000007f, и тут ничего плохого нет, зато LowByte расширяется до 0xffffff80, в результате операции мы получаем 0xffffffff!

Греховность С#

С# во

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

...

byte a, b;

a = 255;

b = 1;

byte c = (b + a);

error CS0029: Cannot implicitly convert type \'int\' to \'byte\'

(ошибка CS0029: Не могу неявно преобразовать тип \'int\' в \'byte\')

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

...

byte с = (byte) (Ь + а) ;

Безопаснее воспользоваться классом Convert:

...

byte d = Convert.ToByte(a + b);

Поняв, что пытается сказать компилятор, вы хотя бы задумаетесь, есть ли в вашем коде реальная проблема. К сожалению, возможности компилятора ограничены. Если бы в предыдущем примере вы избавились от ошибки, объявив a, b и с как целые со знаком, то появилась бы возможность переполнения, а компилятор ничего не сказал бы.

Еще одна приятная особенность С# состоит в том, что он по мере необходимости пользуется 64–разрядными целыми числами. Например, следующий код дал бы неверный результат на С, но правильно работает на С#:

...

int i = -1;

uint j = 0xffffffff; // наибольшее положительное 32-разрядное целое

if(i == j)

Console.WriteLine("Отлично!");

Причина в том, что С# приведет операнды к типу long (64–разрядное целое со знаком), который позволяет точно сохранить оба числа. Если вы решите пойти дальше и проделать то же самое с числами типа long и ulong (в С# оба занимают 64 разряда), то компилятор сообщит, что необходимо явно преобразовать их к одному типу. По мнению авторов, стандарт C/C++ следует уточнить: если компилятор поддерживает операции над 64–разрядными значениями, то он должен в этом отношении вести себя так же, как С#.

Ключевые слова checked и unchecked

В языке С# есть ключевые слова checked и unchecked. Можно объявить checked–блок:

...

byte a = 1;

byte b = 255;

checked

{

byte c = (byte)(a + b);

byte d = Convert.ToByte(a + b);

Console.Write("{0} {1}\n", b+1, c);

}

В данном примере приведение а + b от int к byte возбуждает исключение. В следующей строке, где вызывается Convert.ToByte, исключение возникло бы и без ключевого слова checked, но его наличие приводит к возбуждению исключения еще и при вычислении аргументов метода Console.Write. Поскольку иногда переполнение целого допускается намеренно, то имеется также ключевое слово unchecked, отключающее контроль на переполнение.

Слова checked и unchecked можно также использовать для включения или отключения контроля в одном выражении:

...

checked (с = (byte) (Ь + а));

И наконец, включить контроль можно с помощью флага /checked компилятора. Если этот флаг присутствует, то нужно явно помечать словом unchecked участки кода или отдельные предложения, в которых переполнение допустимо.

Греховность Visual Basic и Visual Basic .NET

Visual Basic регулярно претерпевает кардинальные модификации, а переход от Visual Basic 6.0 к Visual Basic .NET стал самым значительным шагом со времен введения объектной ориентированности в Visual Basic 3.0. Одно из самых фундаментальных изменений связано с целочисленными типами (см. табл. 3.1).

Вообще говоря, и Visual Basic 6.0, и Visual Basic .NET не подвержены угрозе исполнения произвольного кода из–за переполнения целых чисел. В Visual Basic 6.0 генерируется ошибка, если при выполнении какого–либо

оператора или функции преобразования, например CInt, возникает переполнение. В Visual Basic .NET в этом случае возбуждается исключение типа System.OverflowException. Как показано в табл. 3.1, программа на Visual Basic .NET имеет доступ ко всем целочисленным типам, определенным в каркасе .NET Framework.

Таблица 3.1. Целочисленные типы, поддерживаемые Visual Basic 6.0 и Visual Basic .NET

Хотя операции в самом языке Visual Basic, может быть, и неуязвимы для переполнения целого, но потенциальная проблема состоит в том, что вызовы Win32 API обычно принимают в качестве параметров 32–разрядные целые без знака (DWORD). Если ваша программа передает системному вызову 32–разрядное целое со знаком, то в ответ может получить отрицательное число. Аналогично вполне допустимо выполнить такую операцию, как 2 – 8046, над числами со знаком, но что, если указать в качестве одного из операндов такое число без знака, чтобы возникло переполнение? Если системный вызов возвращает некоторое значение, а потом вы выполняете манипуляции над этим значением и величиной, прямо или косвенно (после тех или иных вычислений) полученной от пользователя, и напоследок обращаетесь к другим системным вызовам, то можете оказаться в угрожаемой ситуации. Переходы от знаковых чисел к беззнаковым и обратно чреваты опасностью. Даже если переполнение целого и не приведет к исполнению произвольного кода, необработанные исключения станут причиной отказа от обслуживания. Неработающее приложение не приносит доходов заказчику.

Греховность Java

В отличие от Visual Basic и С#, в язык Java не встроена защита от переполнений. Вот цитата из спецификации языка «Java Language Specification*, размещенной по адресу http://java.sun.com/docs/books/jls/second_edition/html/typeValues.doc.html#9151:

При выполнении встроенных операций над целыми типами переполнение или потеря значимости не индицируются. Единственные арифметические операторы, которые могут возбудить исключение (§11), – это оператор целочисленного деления / (§15.17.2) и оператор вычисления остатка % (§15.17.3). Они возбуждают исключение ArithmeticException, если правый операнд равен нулю.

В отличие от Visual Basic, Java поддерживает лишь подмножество всего диапазона целочисленных типов. Хотя 64–разрядные целые поддерживаются, но единственным беззнаковым типом является char, и он представляется в виде 16–разрядного значения без знака.

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

Греховность Perl

По крайней мере, два автора этой книги являются горячими сторонниками Perl. Но, несмотря на это, следует признать, что работа с целыми числами реализована в Perl странно. Внутри они представляются в виде чисел с плавающей точкой двойной точности, но тестирование позволяет выявить некоторые любопытные вещи. Рассмотрим следующий код:

...

$h = 4294967295;

$i = 0xffffffff;

$k = 0x80000000;

print "$h = 4294967295 – $h + 1 = ".($h + 1)."\n";

print "$i = 0xffffffff – $i + 1 = ".($i + 1)."\n";

printf("\nИспользуется printf со спецификатором %%d\n");

printf("\\$i = %d, \$i + 1 = %d\n\n", $i, $i + 1);

printf("\nТестируется граничный случай деления\n");

printf("0x80000000/-1 = %d\n", $k/-1);

print "0x80000000/-1 = ".($k/-1)."\n";

В результате печатается следующее:

...

[e:\projects\19_sins\perl foo.pl

4294967295 = 4294967295 – 4294967295 + 1 = 4294967296

4294967295 = 0xffffffff – 4294967295 + 1 = 4294967296

Используется printf со спецификатором %d

$i = -1, $i + 1 = -1

Тестируется граничный случай деления

0x80000000/-1 = -2147483648

0x80000000/-1 = -2147483648

На первый взгляд, результат выглядит странно, особенно когда используется printf с форматной строкой (в отличие от обычной функции print). Первым делом в глаза бросается то, что мы можем присвоить переменной максимально возможное значение без знака, но после прибавления к нему 1 она либо увеличивается на единицу, либо – если печатать с помощью %d – вообще не изменяется. Загвоздка в том, что на самом деле вы оперируете числами с плавающей точкой, а спецификатор %d заставляет Perl преобразовать double в int. В действительности никакого переполнения нет, но при печати результатов создается впечатление, будто оно произошло.

Поделиться с друзьями: