Вы здесь

19 смертных грехов, угрожающих безопасности программ. Грех 2.. Ошибки, связанные с форматной строкой (Дэвид Лебланк)

Грех 2.

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

В чем состоит грех

С форматной строкой связан новый класс атак, появившихся в последние годы. Одно из первых сообщений на эту тему прислал Ламагра Аграмал (Lamagra Arga–mal) 23 июня 2000 года (www.securityfocus.com/archive/1/66842). Месяцем позже Паскаль Бушарен (Pascal Bouchareine) дал более развернутое пояснение (www.securityfocus.eom/archive/l/70552). В более раннем сообщении Марка Слемко (Mark Slemko) (www.securityfocus.com/archive/1 /10383) были описаны основные признаки ошибки, но о возможности записывать в память речи не было.

Как и в случае многих других проблем, относящихся к безопасности, суть ошибки в форматной строке заключается в отсутствии контроля данных, поступающих от пользователя. В программе на C/C++ такая ошибка позволяет произвести запись по произвольному адресу в памяти, а опаснее всего то, что при этом необязательно затрагиваются соседние блоки памяти. В результате противник может обойти защиту стека и модифицировать очень небольшие участки памяти. Проблема может возникнуть и тогда, когда форматная строка читается из не заслуживающего доверия источника, контролируемого противником, но это свойственно скорее системам UNIX и Linux. В Windows таблицы строк обычно хранятся внутри исполняемого файла или в динамически загружаемых библиотеках ресурсов (ресурсных DLL). Если противник может изменить основной исполняемый файл или ресурсную DLL, то он способен провести прямолинейную атаку, и не эксплуатируя ошибки в форматной строке.

Но и в программах на других языках атаки на форматную строку могут стать источником серьезных неприятностей. Самая очевидная заключается в том, что пользователь не понимает, что происходит, однако при некоторых условиях противник может организовать атаку с кросс–сайтовым сценарием или внедрением SQL–команд, тем самым запортив или модифицировав данные.

Подверженные греху языки

Самыми опасными в этом отношении являются языки С и С++. Успешная атака приводит к исполнению произвольного кода и раскрытию информации. В программах на других языках произвольный код обычно выполнить не удается, но, как отмечено выше, возможны другие виды атак. С программой на Perl ничего не случится, если пользователь подсунет спецификаторы формата, но она может стать уязвимой, когда форматные строки считываются из ненадежного источника данных.

Как происходит грехопадение

Форматирование данных для вывода или хранения – это довольно сложное дело. Поэтому во многих языках программирования есть средства для решения этой задачи. Как правило, формат описывается так называемой форматной строкой. По существу, это мини–программа на очень специализированном языке, предназначенном исключительно для описания формата выходных данных. Однако многие разработчики допускают примитивную ошибку – позволяют задавать форматную строку пользователям, не заслуживающим доверия. В результате противник может подсунуть такую строку, при работе с которой возникнут серьезные проблемы.

В программах на языке C/C++ это особенно рискованно, поскольку обнаружить сомнительные места в форматной строке очень сложно, а кроме того, форматные строки в этих языках могут содержать некоторые опасные спецификаторы (и прежде всего %п), отсутствующие в других языках.

В C/C++ можно объявить функцию с переменным числом аргументов, указав в качестве последнего аргумента многоточие (…). Проблема в том, что при вызове такая функция не знает, сколько аргументов ей передано. К числу наиболее распространенных функций с переменным числом аргументов относятся функции семейства printf: printf, sprintf, snprintf, fprintf, vprintf и т. д. Та же проблема свойственна функциям для работы с широкими символами. Рассмотрим пример:

#include <stdio.h>

int main(int argc, char* argv[])

{

if(argc > 1)

printf(argv[1]);

return 0;

}

Исключительно простая программа. Однако посмотрим, что может произойти. Программист ожидает, что пользователь введет что–то безобидное, например Hello World. В ответ будет напечатано то же самое: Hello World. Но давайте передадим программе в качестве аргумента строку %х %х. Если запустить эту программу в стандартном окне команд (cmd.exe) под Windows ХР, то получим:

E:\projects\19_sins\format_bug>format_bug.exe «%x %x»

12ffc0 4011e5

В другой операционной системе или при использовании другого интерпретатора команд для ввода точно такой строки в качестве аргумента может потребоваться слегка изменить синтаксис, и результат, вероятно, тоже будет отличаться. Для удобства можете поместить аргументы в shell–сценарий или пакетный файл.

Что произошло□ Функции printf передана форматная строка, вместе с которой следовало бы передать еще два аргумента, то есть поместить их в стек перед вызовом функции. Встретив спецификатор %х, printf прочтет четыре байта из стека. Нетрудно представить себе, что при наличии более сложной функции, которая хранит в стеке некоторую секретную информацию, противник смог бы эту информацию распечатать. В данном же случае на выходе мы видим адрес кадра стека (0xl2ffc0), за которым следует адрес, по которому вернет управление функция main(). То и другое – важная информация, которую противник сумел несанционированно получить.

Теперь возникает вопрос: «Как противник может воспользоваться ошибкой при работе с форматной строкой для записи в память□» Существует довольно редко используемый спецификатор %п, который позволяет записать число выведенных к настоящему моменту байтов в переменную, адрес которой передан в качестве соответствующего ему аргумента. Вот предполагаемый способ его применения:

unsigned int bytes;

printf("%s%n\n", argv[1], &bytes);

printf("Длина входных составляла %d символов\n, bytes");

В результате было бы напечатано:

E:\projects\19_sins\format_bug>format_bug2.exe «Some random input»

Some random input

Длина входных составляла 17 символов

На платформе, где длина целого составляет четыре байта, спецификатор %п выводит четыре байта, а спецификатор %hn – два байта. Противнику осталось только вычислить, какой адрес должен быть помещен в нужную позицию стека, а потом, манипулируя спецификаторами ширины, добиться, чтобы число выведенных байтов равнялось числовому значению нужного адреса.


Примечание. Более подробная демонстрация шагов, которые нужно предпринять для реализации такого эксплойта, приведена в главе 5 книги Michael Howard и David С. LeBlanc «Writing Secure Code, Second Edition» (Microsoft Press, 2002) или в книге Holesby Jack Koziol, David Litchfield, Dave Artel, Chris Anley, Sinan «noir» Eren, Neel Mehta and Riley Hassell «The Shellcoder's Handbook» (Справочник no shell–кодам) (Wiley, 2004).


Пока достаточно принять за аксиому, что если вы позволите противнику контролировать форматную строку в программе на C/C++, то рано или поздно он придумает, как заставить эту программу выполнить нужный ему код. Особенно неприятно, что перед запуском такой атаки противник может изучить содержимое стека и изменить направление атаки на лету. На самом деле в первый раз, когда автор демонстрировал эту атаку публично, ему попался не тот интерпретатор команд, на котором эксплойт разрабатывался, поэтому атака не сработала. Но вследствие удивительной гибкости этой атаки удалось исправить ошибку и взломать уязвимое приложение на глазах аудитории.

В большинстве других языков эквивалент спецификатора формата %п не поддерживается, поэтому напрямую противник не сможет таким образом выполнить код по своему выбору. Тем не менее проблемы все равно остаются, поскольку существуют более тонкие варианты этой атаки, перед которыми уязвимы и другие языки. Если противник может задать форматную строку для вывода в файл протокола или в базу данных, то сумеет сформировать некорректный или сбивающий с толку протокол. Кроме того, приложение, читающее протоколы, может считать их заслуживающими доверия, а если это предположение нарушается, то ошибки в синтаксическом анализаторе могут все же привести к исполнению произвольного кода. С этим связана и другая проблема – запись в файл протокола управляющих символов. Так, символ забоя можно использовать для стирания данных, а символы конца строки могут скрыть или даже уничтожить следы атаки.

Без слов понятно, что если противник может задать форматную строку, передаваемую функции scanf и ей подобным, то беда неминуема.

Греховность C/C++

В отличие от многих других рассматриваемых нами ошибок, эту обнаружить довольно легко. Такой код неправилен:

printf(user_input);

а вот такой – правилен:

printf(«%s», user_input);

Многие программисты легкомысленно полагают, что ошибку достаточно исправить только в таких местах. Однако нередко встречаются ситуации, когда форматную строку с помощью sprintf помещают в буфер, а потом забывают об этом и пишут примерно такой код:

fprintf(STDOUT, err_msg);

Противнику нужно лишь подготовить входные данные так, чтобы спецификаторы формата экранировались, и обычно написать эксплойт для такой ошибки даже проще, потому что буфер err_msg часто выделяется в стеке. Получив возможность пройти вверх по стеку, противник сможет управлять тем, в какое место будет записана информация, определяемая поданными им на вход данными.

Родственные грехи

Хотя самая очевидная атака связана с дефектом в коде программы, нередко форматные строки помещают во внешние файлы, чтобы упростить локализацию. Если такой файл недостаточно защищен, то противник сможет просто подставить собственные форматные строки.

Еще один близкий грех – это недостаточный контроль входных данных. В некоторых системах информация о местных привязках (locale) хранится в переменных окружения и определяет, в частности, каталог, где находятся файлы на нужном языке. Иногда противник может даже заставить приложение искать файлы в произвольных каталогах.

Где искать ошибку

Любое приложение, которое принимает данные от пользователя и передает их функции форматирования, потенциально уязвимо. Очень часто этому греху подвержены приложения, записывающие полученные от пользователя данные в протокол. Кроме того, некоторые функции могут реализовывать форматирование самостоятельно.

Выявление ошибки на этапе анализа кода

В программе на C/C++ обращайте внимание на функции семейства printf, особенно на такие конструкции:

printf(user_input);

fprintf(STDOUT, user_input);

Если встретится что–то похожее на

fprintf(STDOUT, msg_format, arg1, arg2);

проверьте, где хранится строка, на которую указывает msg_format, и насколько хорошо она защищена.

Есть много других уязвимых системных вызовов и API, в частности функция syslog. Определение любой функции, в списке аргументов которой встречается многоточие (…), должно вас насторожить.

Многие сканеры исходных текстов, даже лексические типа RATS и flawfinder, способны обнаружить такие ошибки. Есть даже программа PScan (www.striker. ottawa.on.ca/~aland/pscan/), специально спроектированная для этой цели. Существуют и инструменты, которые можно встроить в процесс компиляции, например программа FormatGuard Криспина Коуэна (http://lists.nas.nasa.gov/archives/ ext/linux–security–audit/2001/05/msg00030.html).

Тестирование

Передайте приложению входную строку со спецификаторами формата и посмотрите, выводятся ли шестнадцатеричные значения. Например, если программа ожидает ввода имени файла и в случае, когда файл не найден, возвращает сообщение об ошибке, в которое входит введенное имя, попробуйте задать такое имя файла: NotLikely%x%x. txt. Если в ответ будет напечатано что–то типа «NotLikelyl2fd234104587.txt cannot be found», значит, вы нашли уязвимость, связанную с форматной строкой.

Ясно, что такая методика тестирования зависит от языка, – передавать имеет смысл только спецификаторы формата, поддерживаемые языком, на котором написана программа. Однако поскольку среды исполнения многих языков часто реализуются на C/C++, вы поступите мудро, если протестируете также и форматные строки для C/C++ – вдруг обнаружится опасная уязвимость библиотеки, использованной при реализации.

Отметим, что если речь идет о Web–приложении, которое отправляет назад данные, введенные пользователем, то существует также опасность атаки с кросс–сайтовым сценарием.

Примеры из реальной жизни

Следующие примеры взяты из базы данных CVE (http://cve.mitre.org). Это лишь небольшая выборка из 188 сообщений об ошибках при работе с форматной строкой.

CVE–2000–0573

Цитата из бюллетеня CVE: «Функция lreply в FTP–сервере wu–ftpd версии 2.6.0 и более ранних плохо контролирует форматную строку из не заслуживающего доверия источника, что позволяет противнику выполнить произвольный код с помощью команды SITE ЕХЕС». Это первый опубликованный эксплойт, направленный против ошибки в форматной строке. Заголовок сообщения в BugTraq подчеркивает серьезность проблемы: «Удаленное получение полномочий root по крайней мере с 1994 года».

CVE–2000–0844

Цитата из бюллетеня CVE: «Некоторые функции, используемые в подсистеме локализации UNIX, недостаточно контролируют внедренные пользователем форматные строки, что позволяет противнику выполнить произвольный код с помощью таких функций, как gettext и catopen».

Полный текст оригинального бюллетеня можно найти по адресу www.securityfocus.eom/archive/l/80154. Эта ошибка интересна тем, что затрагивает базовые API, применяемые в большинстве вариантов UNIX (в том числе и Linux), за исключением систем на базе BSD, в которых привилегированная suid–программа игнорирует значение переменной окружения NLSPATH. Как и многие бюллетени в разделе CORE SDI, этот прекрасно написан, информативен и содержит очень подробное объяснение проблемы в общем, но это предложение не только опасно, но еще и потребляет много процессорного времени.

Искупление греха

Прежде всего никогда не передавайте поступающие от пользователя данные функциям форматирования без проверки. За этим нужно следить на всех уровнях форматирования вывода. Отметим попутно, что функциям форматирования присущи заметные накладные расходы; загляните в исходный текст функции _output, если вам любопытно. Как бы ни удобно было писать просто:

fprintf(STDOUT, buf);

Во вторую очередь позаботьтесь о том, чтобы все используемые в программе форматные строки читались только из доверенного источника и чтобы противник не мог контролировать путь к этому источнику. Если вы пишете код для UNIX или Linux, имеет смысл последовать примеру BSD в плане игнорирования переменной NLSPATH, которая задает путь к файлу локализованных сообщений. Это повысит степень защиты.

Искупление греха в C/C++

Достаточно просто пользоваться функциями форматирования вот так:

printf(«%s», user_input);

Дополнительные защитные меры

Проверяйте локаль и разрешайте только корректные значения. Подробнее см. статью David Wheeler «Write It Secure: Format Strings and Locale Filtering», упомянутую в разделе «Другие ресурсы». Не пользуйтесь функциями семейства printf, если есть другие пути. Например, в С++ имеются операторы вывода в поток:

#include <iostream>

//...

std::cout << user_input

//...

Другие ресурсы

□ «format bugs, in addition to the wuftpd bug» by Lamagra Agramal: www.securityfocus.com/archive/1/66842

□ Writing Secure Code, Second Edition by Michael Howard and David C. LeBlanc (Microsoft Press, 2002), Chapter 5, «Public Enemy #1: Buffer Overruns»

□ «UNIX locale format string vulnerability, CORE SDI» by Ivan Arce: www.securityfocus.com/archive/1/80154

□ «Format String Attacks» by Tim Newsham: www.securityfocus.com/archive/ 1/81565

□ «Windows 2000 Format String Vulnerabilities» by David Litchfield: www.nextgenss.com/papers/win32format.doc

□ «Write It Secure: Format Strings and Locale Filtering» by David A Wheeler: www.dwheeler.com/essays/write_it_secure_l.html

Резюме

Рекомендуется

□ Пользуйтесь фиксированными форматными строками или считываемыми из заслуживающего доверия источника.

□ Проверяйте корректность всех запросов к локали.

Не рекомендуется

□ Не передавайте поступающие от пользователя форматные строки напрямую функциям форматирования.

Стоит подумать

□ О том, чтобы использовать языки высокого уровня, которые в меньшей степени уязвимы относительно этой ошибки.