19 смертных грехов, угрожающих безопасности программ
Шрифт:
Это и есть типичная атака с внедрением команды, когда введенные пользователем данные по какой–то причине интерпретируются как команда. Часто такая команда может наделять человека контролем над данными и вообще куда более широкими полномочиями, чем предполагалось.
Подверженные греху языки
Внедрение команд становится проблемой всякий раз, когда данные и команды хранятся вместе. Да, язык способен исключить наиболее примитивные способы внедрения команд за счет подходящего API (интерфейса прикладного программирования), осуществляющего тщательную проверку входных данных. Но всегда остается шанс, что новые API породят доселе неведомые варианты
Как происходит грехопадение
Внедрение команды происходит, когда непроверенные данные передаются тому или иному компилятору или интерпретатору, который может счесть, что это вовсе не данные.
Канонический пример – это передача аргументов системному командному интерпретатору без какого–либо контроля. Например, в старой версии IRIX вышеупомянутое окно регистрации содержало примерно такой код:
char buf[1024];
snprintf(buf, "system lpr -P %s", user_input, sizeof(buf) – 1);
system(buf);
В данном случае пользователь не имел никаких привилегий, это вообще мог быть любой человек, случайно оказавшийся рядом с рабочей станцией. И достаточно было ему ввести строку FRED; xterm&, как тут же появилось бы окно консоли, поскольку символ ; завершает предыдущую команду системного интерпретатора (shell), а команда xterm создает новое окно консоли, готовое для ввода команд. При этом символ & сообщает системе, что новый процесс нужно запустить, не блокируя текущий. (В Windows символ & имеет тот же смысл, что точка с запятой в UNIX.) А поскольку процесс, контролирующий вход в систему, имел привилегии администратора, то и созданная консоль обладала такими же привилегиями.
Как будет показано ниже, в разных языках есть немало функций, уязвимых для таких атак. Но для успеха атаки с внедрением команды обращение к системному интерпретатору необязательно. Противник мог бы вызвать также интерпретатор самого языка. Это довольно распространенный способ в таких языках высокого уровня, как Perl или Python. Рассмотрим, например, такой код на языке Python:
def call_func(user_input, system_data):
exec \'special_function_%s("%s")\' % (system_data, user_input)
Здесь встроенный в Python оператор % работает примерно так же, как спецификаторы формата в функциях семейства printf из стандартной библиотеки С. Он сопоставляет значения в скобках с шаблонами %s в форматной строке. Идея заключалась в том, чтобы вызвать выбранную системой функцию, передав ей введенный пользователем аргумент. Например, если бы строка system_data была равна sample, a user_input – f red, то программа выполнила бы такой код:
special_function_sample(«fred»)
Причем этот код работал бы в том же контексте, что и запустившая его функция exec.
Теперь противник, контролирующий переменную user_input, может выполнить произвольный код на Python. Для этого ему достаточно поставить кавычку, за ней правую скобку и точку с запятой, например так:
fred"); print("foo
Тогда программа выполнит следующий код:
special_function_sample(«fred»); print(«foo»)
В результате будет выполнено не только то, что хотел автор программы, но
и напечатана строка foo. Противник может сделать буквально все, что ему заблагорассудится: удалить файлы, к которым имеет доступ программа, или, скажем, создать новое сетевое соединение. Если такая гибкость позволяет противнику расширить свои привилегии, то это уже угроза безопасности.Многие проблемы такого рода возникают, когда данные и управляющие структуры перемешаны, и с помощью того или иного специального символа противник может переключить контекст с данных на команды. Если речь идет о командных интерпретаторах, то таких волшебных символов тьма–тьмущая. Например, в большинстве клонов UNIX противнику достаточно ввести точку с запятой (конец предложения), обратную кавычку (данные между двумя обратными кавычками исполняются как команда) или вертикальную черту (все следующее за ней относится уже к другому процессу, связанному с первым), и после этого он сможет исполнять произвольные команды. Есть и другие специальные символы, меняющие контекст; мы перечислили лишь самые очевидные.
Для устранения проблем, связанных с запуском команд, часто применяют вызов API, позволяющий запустить программу непосредственно, в обход командного интерпретатора. Например, в UNIX есть семейство функций execv, которым передается просто имя запускаемой программы и ее параметры.
Это неплохо, но полностью проблему не решает, потому что запущенная программа может сама поместить данные рядом с важными управляющими конструкциями. Предположим, к примеру, что execv используется для запуска Python–программы, которая затем передает список полученных аргументов функции exec. И мы снова оказываемся в исходной ситуации. Нам встречались программы, в которых через execv исполнялся файл /bin/sh (это и есть командный интерпретатор), при этом весь смысл execv теряется.
Родственные грехи
Есть несколько грехов, которые можно считать частными случаями внедрения команд. Ясно, что внедрение SQL – это особый вид атаки с внедрением команд. К той же категории можно отнести атаки на форматную строку, поскольку противник вставляет в переменную, которую программист рассматривал как данные, команды чтения и записи (например, спецификатор %п это команда записи). Но эти частные случаи столь широко распространены, что мы посвятили им отдельные главы.
Та же ошибка лежит в основе кросс–сайтовых сценариев (cross–site scripting), когда в отсутствие должного контроля противник может вставить в данные некоторые управляющие элементы разметки.
Где искать ошибку
Вот основные характерные признаки:
□ команды (или управляющая информация) и данные расположены друг за другом;
□ существует возможность, что данные будут интерпретироваться как команда, зачастую из–за наличия специальных символов, например кавычки и точки с запятой;
□ контроль над исполняемой командой может наделить противника более широкими привилегиями, чем он имел до этого.
Выявление ошибки на этапе анализа кода
Этой ошибке подвержены многочисленные вызовы API и конструкции, встречающиеся в самых разных языках программирования. В ходе анализа кода следует первым делом обращать внимание на те конструкции, которые потенциально можно использовать для вызова командного процессора (входящего в оболочку ОС, базы данных либо интерпретатора самого языка). Нужно выяснить, встречаются ли такие конструкции в программе. Если да, проверьте, предприняты ли адекватные защитные меры. Хотя защита зависит от природы греха (см., например, обсуждение внедрения SQL–команд в грехе 4), но в общем случае рекомендуется скептически относиться к подходу «все кроме», отдавая предпочтение подходу «только такие» (см. ниже раздел «Искупление греха»).