2.2. Распространенные ошибки при работе со строками 67
0 1 # include < string . h>
0 2 # i nclude < s tdi o . h>
0 3 # include < s tdlib . h>
04
0 5 i n t rnai n ( vo i d ) {
О б char sl [ ] = " 012345678" ;
0 7 char s 2 [ ] = " 0 1 2 34 5 6 7 8 9 " ;
0 8 cha r * de s t ;
0 9 int i ;
10
1 1 s t rcpy_s ( s l , s i z e o f ( s2 ) , s 2 ) ;
1 2 de s t = ( char * ) ma l l o c ( s t r l en ( s l ) ) ;
13 for ( i=l ; i <= 1 1 ; i++ ) {
1 4 des t [ i ] = s l [ i ] ;
15
1 6 de s t [ i ] = ' \ 0 ' ;
1 7 p r i n t f ( "de s t % s " , de s t ) ;
18 /* . . . */;
19
Многие из этих ошибок являются ошибками новичка, но иногда их допускают и опыт
ные программисты. Очень легко разработать и внедрить программу, подобную приведен
ной, которая на большинстве систем будет компилироваться и работать без ошибок.
Ошибки, связанные с нулевым завершающим символом
Еще одна распространенная проблема, связанная со строками, заключается в их не
верном завершении нулевым символом. Строка корректно завершена нулевым символом,
если нулевой символ находится в последнем элементе массива или до него. Если в строке
отсутствует завершающий нулевой символ, программа может быть обманута чтением или
записью данных за пределами границ массива.
Чтобы быть безопасно переданными в качестве аргументов стандартным функциям ра
боты со строками, таким как s trcpy ( ) или strl en ( ) , строки должны содержать нулевой
з авершающий символ по адресу последнего элемента массива или до него. Завершающий
нулевой символ необходим, поскольку эти функции , как и другие функции, определенные
стандартом С, полагаются на наличие указателя конца строки. Аналогично строки должны
иметь завершающий нулевой символ при выполнении программой итераций по массиву
символов, когда условие завершения цикла зависит от существования завершающего ну
левого символа в пределах выделенной строке памяти.
1 s i ze-t i ;
2 cha r n tb s [ 1 6 ] ;
3 /* . . . */
4 for ( i = О ; i < s i z e o f ( n tb s ) ; + + i )
5 if (ntbs [ i ] == ' \ О ' ) break;
6 /* . . . */
7
Приведенная далее программа при уровне предупреждений /WЗ компилируется
Microsoft Visual С++ 20 1 0 с предупреждениями об использовании функций s t rncpy ( )
и s t r cpy ( ) . Она также сообщает об ошибках ( во время выполнения ) , будучи скомпили
рованной GCC в операционной системе Linux при определенном с ненулевым значением
идентификаторе препроцессора _FORT I FY_SOURCE.
68 Строки
1 int ma in ( void) { sizeof (a) ) ;
2 char а [ l б ] ; sizeof (b) ) ;
3 char Ь [ l б ] ;
4 char с [ l б ] ;
5 s t rncpy ( a , " 0 1 2 3 4 5 6 7 8 9abcde f " ,
6 s t rncpy ( b , " 0 1 2 3 4 5 6 7 8 9abcde f " ,
7 s trcpy ( c , а ) ;
8 /* . . . */
9
В этой программе каждый из трех массивов символов - а [ ] , Ь [ ] и с [ ] - объявлен
как имеющий размер 1 6 байт. Хотя копирование функцией s trncpy ( ) в а оrраничено
значением s i ze o f ( а ) ( 1 6 байт), получающаяся в результате строка не является строкой с
завершающим нулевым символом в силу исторических причин и стандартного поведения
функции strncpy ( ) .
В соответствии со стандартом С функция s t rn cp y ( ) копирует не больше n симво
лов (символы, следующие за нулевым, не копируются) из исходного массива в целевой .
Следовательно , если среди первых n символов исходного массива, как в данном примере,
не имеется нулевого, получающаяся в результате строка не будет завершена нулевым
сим вол о м .
Применение функции s trncpy ( ) для копирования в строку Ь дает аналогичный ре
зультат. В зависимости от того, как компилятор выделяет память, после массива а [ ] мо
жет случайно оказаться нулевой символ, но это не определено компилятором и вряд ли
осуществится в приведенном примере, в частности, если память тесно упакована. В ре
зультате применение функции s trcpy ( ) для копирования в с может привести к записи за
пределы массива, поскольку строка, хранящаяся в а [ ] , не является корректно завершен
ной нулевым символом.
В книге The CERT С Secure Coding Standard [ 1 86 ] имеется рекомендация "STR32-C. Не
обходимо корректно завершать строки нулевыми символами". Заметим , что это правило
не исключает использования массивов символов. Например, нет ничего некорректного в
следующем фрагменте программы, несмотря на то что строка , хранящаяся в массиве сим
волов ntbs, может не быть корректно завершена нулевым символом после копирования с
ПОМОЩЬЮ функции strncpy ( ) .
1 char ntbs [ NTBS_S I Z E ] ;
2
3 strncpy ( ntbs , source , s i zeof ( ntbs ) - 1 ) ;
4 ntbs [ s i zeof (ntbs ) - 1 ] = ' \ 0 ' ;
Ошибки, связанные с нулевым завершающим символом , подобно другим ошибкам ,
связанным со строками и описанным в этом разделе, трудны для выявления и могут
оставаться незамеченными в программе до тех пор, пока не проявятся при каком-то кон
кретном наборе входных данных. Код не должен зависеть от того , к ак происходит выде
ление памяти компилятором, которое может изменяться от одной версии компилятора
к другой.
Усечение строк
Усечение строки может происходить, когда целевой массив символов недостаточно ве
лик для того, чтобы хранить содержимое строки. Усечение строки может осуществлять
ся при чтении программой пользовательского ввода или копировании строки и зачастую
2.3. Уязвимости, связанные со строками, и их использование 69
является результатом попытки программиста предотвратить переполнение буфера. Хотя
усечение и не столь опасно, как переполнение буфера , в результате усечения происходит
потеря информации и в ряде случаев могут возникнуть уязвимости программного обеспе
чения.
Строковые ошибки без функций
Большинство функций , определенных в стандартной библиотеке обработки строк
<str ing . h>, включая s t rcpy ( ) , s t rcat ( ) , s t rncpy ( ) , s t rncat ( ) и s trtok ( ) , склонны
к ошибкам. Например, Microsoft Visual Studio не рекомендует использовать многие из этих
функций.
Однако, поскольку строки с завершающим нулевым байтом реализованы как массивы
символов, оказывается возможным выполнить небезопасную операцию со строкой даже
без вызова функции. Приведенная далее программа содержит дефект, связанный с опера
цией копирования строки, но при этом не вызывает ни одну из функций строковой биб
лиотеки.
0 1 int ma i n ( i nt argc , cha r * a rgv [ ] ) {
02 int i = О ;
03 char buff [ l2 8 ] ;
0 4 char * argl = argv [ l ] ;
0 5 i f ( a rgc == 0 ) {
0 6 рut s ( "Аргументов нет " ) ;
0 7 return EX I T_FAI LURE ;
08
1 0 while (argl [ i ] ! = 1 \ 0 1 )
1 1 bu ff [ i ] = argl [ i ] ;
12 i++;
13
1 4 buff [ i ] = 1 \О ' ;
1 5 print f ( "buff = % s \n " , buf f ) ;
1 6 exit ( EXIT_SUCCES S ) ;
17
Дефектная программа получает строковый аргумент, копирует его в массив символов
bu f f и выводит содержимое в буфер. Переменная bu f f объявлена как фиксированный
массив из 1 28 символов. Если первый аргумент программы содержит 1 28 или более симво
лов (помните о завершающем нулевом символе) , программа выполняет запись за предела
ми массива фиксированного размера.
Очевидно, что устранение необходимости использовать опасные функции не гаранти
рует, что ваша программа свободна от недостатков безопасности. В следующих разделах
вы увидите, как эти недостатки безопасности могут привести к уязвимостям, которыми
смогут воспользоваться злоумышленники.
• 2.3. Уязвимости, связанные со строками,
и их использование
В предыдущих разделах описаны распространенные ошибки при работе со строками в
С и С++ . Эти ошибки становятся опасными, когда код работает с ненадежными данными