Техника сетевых атак
Шрифт:
· F:\TPNA\src»buff.demo.exe
· Buffer Overflows Demo
· Login:¬1234f¦Б^P с12345l ^R
· Passw:
· Password ok
Таким образом, ошибка программиста привела к возможности передачи управления на код злоумышленника и позволила ему проникнуть в систему еще на стадии аутентификации! Кстати, некоторые версии UNIX содержали ошибку переполнения буфера при вводе имени пользователя или пароля, поэтому рассмотренный выше пример трудно назвать надуманным.
Поскольку, при запуске программы из-под Windows 98, буфер имени пользователя располагается по другому адресу, то необходимо скорректировать адрес возврата с 0x12FF6C на 0x63FDE4 (кстати, в Windows 98 не работает клавиша Alt и единственный
· 00000000: B8 34 33 32 31 66 B9 81 ¦ 10 FF E1 66 66 66 66 66 ¬4321f¦Б> сfffff
· 00000010: E4 FD 63 0D 0A 0D 0A ¦ l ¦d0d0
Однако при попытке ввода такой строки происходит аварийное закрытие приложения. Отладчик позволяет установить, что управление получает не требуемый код, а какой-то непонятный мусор. Оказывается, операционная система Windows 98 портит содержимое стека, расположенное выше указателя (т.е. в младших адресах). Такое поведение является вполне нормальным, поскольку сохранность памяти, лежащей выше указателя стека не гарантируется. Экспериментально удается установить, с адреса 0x63FDE8 начинается неиспорченный «кусочек» стека, который пригоден для размещения кода.
Одина из возможных реализаций атаки, работающей под управлением 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”):
· #include «string.h»
·
· void main
· {
· FILE *psw;
· char buff[32];
· char user[16];
· char pass[16];
· char _pass[16];
·
· printf("printf bug demo\n");
· if (!(psw=fopen("buff.psw","r"))) return;
· fgets( amp;_pass[0],8,psw);
·
· printf("Login:");fgets( amp;user[0],12,stdin);
· printf("Passw:");fgets( amp;pass[0],12,stdin);
·
· if (strcmp( amp;pass[0], amp;_pass[0]))
· sprintf( amp;buff[0],"Invalid password: %s", amp;pass[0]);
· else
· sprintf( amp;buff[0],"Password ok\n");
·
· printf( amp;buff[0]);
·
·}
Все строки, читаемые как с клавиатуры, так и из файла паролей, гарантированно влезают в отведенный им буфер и ни при каких обстоятельствах не могут выйти за его границы. При условии, что у злоумышленника нет доступа к файлу “buff.psw”, содержащего пароли пользователей [313], он никак не сможет обойти защиту [314]. Кажется, в десятке строк трудно ошибиться, и никаких дыр тут нет.
Психологическая инерция подводит и на этот раз. И, видимо, не только разработчиков, но, в том числе, и злоумышленников, поскольку тип атаки, описанный ниже, не получил большого распространения. Поэтому, многие из приложений, считающиеся защищенными, все же содержат грубые ошибки, позволяющие легко и незаметно проникнуть в систему.
Речь идет о «большой дыре» в функции “printf”, вернее дыра находится не в одной конкретной функции (тогда бы она могла бы быть безболезненно устранена), а в самом языке Си. Одни из его недостатков заключается в том, что функция не может определить сколько ей было передано параметров. Поэтому, функциям с переменным количеством аргументов, приходится каким-то образом передавать и число этих самых аргументов.
Функция “printf” использует для этой цели строку спецификаторов, и ее вызов может выглядеть, например, так: “printf(“Name: %s\nAge: %d\nIndex: %x\n”, amp;s[0],age,index)”. Количество спецификаторов должно быть равно количеству передаваемых функции переменных. Но что произойдет, если равновесие нарушится?
Возможно два варианта - переменных больше, чем спецификаторов и переменных меньше, чем спецификаторов. Пока количество спецификаторов не превышает количества переданных параметров, не происходит ничего интересного, поскольку, из стека аргументы удаляются не самой функцией, а вызывающим ее кодом (который уж наверняка знает, сколько аргументов было передано) разбалансировки стека не происходит и все работает нормально. Но если количество спецификаторов превышает количество требуемых аргументов, функция, пытаясь прочитать очередной аргумент, обратится к «чужим» данным! Конкретное поведение кода зависит от компилятора и содержимого стека на момент вызова функции “printf”.
Сказанное будет рассмотрено ниже на примере следующей программы (на диске, прилагаемом к книге, она находится в файле “/SRC/printf.bug”):
· #include «stdio.h»
·
· main
· {
· int a=0x666;
· int b=0x777;
· printf("%x %x\n",a);
·
·}
·
Если ее откомпилировать с помощью Microsoft Visual Studio 5.0-6.0, результат работы окажется следующий:
· 666 777
Программа выдала два числа, несмотря на то, что ей передавали всего одну переменную ‘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