Раздел: Документация
0 ... 50 51 52 53 54 55 56 ... 102 гркрета проектирования shell-кода 167 FILE *psw: Char buff[32]. char user[16]: char pass[16]: char pass[16]: printf("printf bug demo\n"); if (!(psw=fopen("buff.psw". V))) return: fgets(& pass[0],8.psw): printf("Login:"): fgets(&user[0].lP.stdin): printf("Passw:"):fgets(&passL0].12.stdin): if (strcmp(&pass[0].& pass[OD) spnntf(&Duff[0]. "Invalid password: %S".&pass[0]): else spnntf (Sbuf f [0]."Password ok\n"): printf(&boff[01): Неуловимость лопушенной ошибки объясняется психологической инерцией мышления: вместо тщательного анализа кода к нему последовательно примеряются типовые штампы и шаблоны. Если ни один из них не подходит — программа считается защищенной. Комизм ситуации заключается в том, что некоторые вещи настолько привычны, что перестают обращать на себя внимание, и мысль проверить их просто не приходит в голову. Один из недостатков языка Си заключается в отсутствии штатных механизмов подсчета количества аргументов, переданных функции. Поэтому функциям с переменным числом аргументов приходится самостоятельно определять, сколько параметров находится в их распоряжении. Для решения этой задачи функция printf использует специальную управляющую строку, которая состоит из служебных комбинаций символов — спецификаторов. Спецификаторы описывают тип и количество аргументов. Каждому из спецификаторов должна соответствовать «своя» переменная, но что произойдет, если такое равновесие нарушится? Когда спецификаторов меньше, чем переменных, ничего скверного не происходит, поскольку в языке Си аргументы удаляются из стека не самой функцией, а вызывающим ее кодом (который уж наверняка знает, сколько аргументов было передано). Поэтому разбалансировки стека не происходит и все работает нормально, за исключением того, что отображаются не все указанные переменные Но если спецификаторов окажется больше, чем переданных переменных, то при попытке извлечь из стека очередной аргумент произойдет обращение к «чужим» данным, находящимся в этой области стека! "Здесь и далеснсрвый аргумент функции printf называется «строкой спецификаторов», а все последующие <• I юре.м енным и » Такую ситуацию позволяет продемонстрировать следующий нрцМе "main(){int a=0xa;int b=0xb:printf("2x 2х\п".а):}", в котором присутствует одИ) «беспарный» спецификатор "2х". Поскольку содержимое стека па моментвц зова функции printf зависит от используемого компилятора, поведение данного кода неопределенно. Например, результат работы программы, полученной с помощью Microsoft Visual С++ 6.0, выглядит так: "а Ь". Функция вывела два числа, несмотря на то, что ей передавали всего одну пере, менную а. Каким же образом она сумела получить содержимое переменной Ь? Ответ на этот вопрос дает дезассемблирование машинного кода программы в результате которого удастся установить содержимое стека на момент вызова функции printf (листинг 4.41). Листинг 4.41. Содержимое стека на момент вызова printf off аХХ ("*х %х") (строка спецификаторов) var 4 ("а") (аргумент функции printf) var 8 ("b") (локальная переменная) var 4 ("а") (локальная переменная) Жирным шрифтом выделены аргументы, переданные функции. Но сама функ ция не может определить их точное количество, поэтому она извлекает из верхушки стека указатель на строку спецификаторов и приступает к ее анализу. Встретив соответствующую комбинацию символов, функция извлекает из стека очередной аргумент, и так продолжается до тех пор, пока не исчерпаются все спецификаторы. Для поддержки функций с переменным количеством аргументов в языке Си был принят обратный порядок заталкивания параметров в стек, то есть самый левый аргумент заносится в последнюю очередь и оказывается на верхушке стека. Было бы замечательно, если бы компилятор напоследок передавал функции число используемых аргументов или, но крайней мере, сообщал их суммарный размер (тем более что технически в этом нет ничего затруднительного). Но увы! Разработчики языка не реализовали такой механизм, и отсюда следует неутешительное заключение о принципиальной невозможности защиты содержимого стека материнской функции. Дочерняя функция может беспрепятственно обращаться к любой ячейке стека — от верхушки до самого низа, читая каь «свои», так и «чужие» данные. При вызове "printf("*x #х\п",а)" функция извлекает из стека на одно слов" больше, чем ей было реально передано, в результате чего происходит втор*е ние в область памяти, запятой локальными переменными материнской фУ11К ции. Переменная b принимается за аргумент функции и выводится на экран-(В зависимости от используемого компилятора в заданном месте стека М0ЖеГ оказаться все что угодно, например, неременная а, значения регистров обШеГ< назначения, «черная дыра» — область памяти, отведенная для выравнивай"51 данных и т. д.) По идее программист должен следить за тем, чтобы каждому спецификатору соответствовала «своя» переменная, однако в некоторых ситуациях отсут8 проектирования shell-кода 169 нз аргументов не приводит к нарушению работоспособности нрограм- Эт<> происходит в тех случаях, когда пропущенная переменная оказывает-М1>на верхушке стека, что не так уж и маловероятно. По ошибка может неожи-С"нНО проявиться при переходе на другой компилятор, поскольку порядок 13 10пожет1ня локальных неременных нигде не задекларирован и каждый ком-Р ,,ятор группирует их по-своему (вовсе не факт, что переменные всегда располагаются в памяти в порядке их объявления в программе). В тех случаях, когда функция printf используется для вывода единственной символьной строки, строку спецификаторов обычно опускают, то есть вместо -printf ("*s". &buff[0])" пишут "printf(&buff[0])". На первый взгляд обе формы записи равносильны, но это не так! Самый левый аргумент всегда проверяется функцией printf на наличие спецификаторов, даже если он нередан функции в единственном числе. Поэтому использовать его для вывода строки можно в том н только втом случае, когда она гарантированно не содержит никаких «внеплановых» спецификаторов, в противном случае работа приложения окажется нестабильной. Особенно опасно полагаться на отсутствие спецификаторов в данных, введенных пользователем, и недопустимо передавать их функции printf в первом слева аргументе. Возможные последствия такого подхода позволяет продемонстрировать программа, приведенная в начале главы: если злоумышленник введет вместо пароля один или несколько спецификаторов, на экране появится содержимое локальных переменных, в том числе и буфера, хранящего эталонный пароль. Компилятор Microsoft Visual С++ 6.0 располагает этот буфер на вершине стека и просмотреть его можно следующим образом (предполагается, что файл "buff.psw" содержит строку "K98PN*") (листинг 4.42). Листинг 4.42. Подглядывание секретного пароля 2rrtf bug demo L3Sin:kpnc rval1d oassword: 5038394b a2a4e 2f4968 - Расшифровки ответа программы необходимо перевернуть каждое двойное л°во, поскольку в микропроцессорах Intel младшие байты располагаются по меньшг--- им адресам. В результате этого получается следующее (рис. 4.5). ШЩВ н2н4Е 4 ч Декодирование пароля Не т рюк для соревнований в «магическом программировании»? 0 ... 50 51 52 53 54 55 56 ... 102
|