![]() |
Классификация уязвимостей
Ниже представлена классификация уязвимостей языка C/C++, немного подправленная и
дополненная, взятая из книги Джека Козиола, Дэвида Личфилда и др.: «Искусство взлома и защиты систем». Список не претендует на полноту, но даёт представление о различных типах уязвимостей которые наиболее часто встречаются. Всегда полезно знать, какие дефекты часто (или редко) встречаются в приложениях. Попытаемся отметить основные разновидности ошибок, встречающихся и современных приложениях. Каждые несколько лет обнаруживается новая разновидность дефектов, и почти сразу после этого хакеры выявляют целый пласт уязвимостей. Другие уязвимости быстро найти не удается, но в любом случае, чтобы выявите уязвимость, необходимо сначала опознать ее. 1 Общие логические ошибки Хотя класс общих логических ошибок составляет самую размытую категорию уязвимостей, именно ошибки этой категории лежат в основе многих проблем, Чтобы найти дефекты в логике программирования, создающие угрозу безопасности, необходимо достаточно хорошо понять само приложение. Разберитесь во внутренних структурах и классах, специфических для приложения, и попробуйте изобрести способы их некорректного использования. Например, если в приложении задействована стандартная буферная структура или строковый класс, хорошее понимание логики приложения поможет найти те места, в которых члены структуры или класса применяются некорректно или небезопасно. При анализе достаточно защищенных, хорошо протестированных приложений, поиск общих логических ошибок может стать вторым по эффективности методом анализа. 2 Пережитки прошлого Некоторые уязвимости, часто встречавшиеся в программах с открытыми исходными текстами еще пять лет назад, в настоящее время почти исчезли. Обычно они воплощаются в форме хорошо известных функций копирования содержимого памяти без проверки, таких как strcpy, sprintf и strcat Хотя эти функции можно вызывать и приложениях вполне безопасно, раньше они часто использовались неправильно, что приводило к переполнению буфера. Впрочем, в современных программах с открытыми текстами эти типы уязвимостей практически не встречаются. Функции strcpy, sprintf, strcat, gets и другие аналогичные функции не имеют никакой информации о размере приемных буферов. Если правильно выделить приемный буфер пли проверить размер данных перед копированием, большинство функции может использоваться без всякого риска, но в противном случае возникает угроза безопасности. Информация об этих проблемах широко распространена в сообществе разработчиков. Например, в man-страницах функций sprintf и strcpy упоминается об опасности вызова этих функций без предварительной проверки границ. 3 Форматные строки Уязвимости форматных строк привлекли к себе внимание примерно в 2000 году, и за последние несколько лет было открыто немало серьезных уязвимостей этого класса. Данные дефекты основаны на возможности нападающего контролировать форматную строку, которая передается функциям, получающим аргументы в стиле printf (syslog, *printf и их аналоги). Если нападающий получит контроль над форматной строкой, он сможет передать директивы, приводящие к порче содержимого памяти и выполнению произвольного кода. Эти уязвимости в значительной мере базируются на малоизвестной директиве %n, которая записывает количество уже выведенных байтов по указателю на целое число.Уязвимости форматных строк очень легко обнаруживаются в процессе анализа. Количество функций, получающих аргументы в стиле printf, относительно невелико; часто достаточно поочередно проверить вызовы этих функций на предмет того, может ли нападающий получить контроль над форматкой строкой. Например, следующие два вызола syslog заметно отличаются друг от друга: Код потенциальной уязвимостью: Цитата:
Самым распространенным местом поиска дефектов форматных строк является код ведения журналов. Часто приходится видеть, как константная форматная строка перелается журнальной функции, после чего вывод направляется в буфер и передается syslog с созданием уязвимости. Следующий гипотетический пример демонстрирует классическую уязвимость форматной строки в коде функции ведения журнала: Цитата:
4 Общие ошибки проверки границ Нередко приложения пытаются организовать проверку границ при выполнении небезопасных операции; впрочем, на практике эта процедура часто организуется неверно. Ошибки проверки границ отличаются от других классов уязвимостей, в которых проверка вообще отсутствует, однако в конечном счете результат оказывается одним и тем же. Оба типа уязвимостей объясняются логическими ошибками при реализации проверки. Без углубленного анализа кода проверки эти уязвимости часто остаются незамеченными. Другими словам, не стоит полагать, что некоторый фрагмент кода неуязвим только потому, что он пытается проверять границы. Прежде чем следовать дальше, убедитесь и там, что эта попытка делается правильно.Хорошим примером ошибки проверки границ является дефект препроцессора Snort RFC, обнаруженный группой ISS X-Force в начале 2003 г. Следующий фрагмент присутствует в уязвимых версиях Snort: Цитата:
5 Циклические конструкции Переполнение буфера очень часто обнаруживается в циклах — вероятно потому, что с точки зрения программирования они несколько сложнее линейного кода. Чем сложнее цикл, тем больше вероятность того, что ошибка программиста приведет к появлению уязвимости. Многие широко распространенные и критичные в плане безопасности приложения содержат крайне запутанные циклы, часть значений которых небезопасна. Нередко в программах встречаются циклы внутри циклов; так появляются сложные наборы команд, в которых вероятность ошибок весьма велика. Циклы синтаксического разбора и любые циклы обработки пользовательского ввода являются хорошей отправной точкой для анализа приложения. Сосредоточив внимание на этих областях, можно получить ценные результаты с минимальными усилиями.Интересный пример ошибки в сложном цикле дает уязвимость, которую Марк Дауд (Mark Dowd) обнаружил в функции crackaddr программы Sendmail. Этот цикл слишком велик, чтобы его можно было привести здесь; наверняка он входит в список самых сложных циклов, встречающихся в программах с открытыми исходными текстами. Вследствие сложности и огромного количества переменных, обрабатываемых в цикле, при некоторых комбинациях входных данных происходит переполнение буфера. Sendmail содержит многочисленные проверки для предотвращения переполнения, и все же цикл приводит к непредвиденным последствиям. Некоторые аналитики, в том числе польская группа исследователей в области безопасности «The Last Stages of Delirium», недооценили возможность практического использования этой ошибки просто потому, что не нашли комбинации данных, приводящей к переполнению. 6 Уязвимости единичного смещения Уязвимости единичного смещения (или на другое небольшое число) принадлежат к числу распространенных ошибок программирования, при которых совсем небольшое число байтов записывается за пределами выделенной памяти. Эти ошибки часто являются результатом некорректного завершения строк нулем, неправильной организации циклов или неудачного использования стандартный строковых функций. В прошлом такие уязвимости встречались в некоторых распространенных приложениях. Например, следующий фрагмент взят из кода Apache 2 (до 2.0.46); позднее эта ошибка была исправлена без особого шума:Цитата:
Любой цикл, после которого строка завершается нулем, следует дважды проверить на предмет ошибки смещения. Следующий фрагмент FTP-демона Open BSD демонстрирует проблему: Цитата:
Ошибка единичного смещения возникает также при неправильном использовании некоторых библиотечных функций. Например, функция strncat всегда завершает выходную строку нулем; если третий аргумент не будет соответствовать объему оставшегося места в выходном буфере за вычетом одного байта, то функция запишет нулевой байт за границами буфера. Пример неправильного вызова strncat: Цитата:
Цитата:
7 Ошибки некорректного завершения строк В общем случае строки должны завершаться нуль-символами; это позволяет легко определить их границы и корректно выполнять операции с ними. Отсутствие завершителей у строк может создать дефекты безопасности при выполнении программы. Например, если строка не завершена положенным символом, содержимое прилегающей памяти будет интерпретировано как продолжение строки. Это может привести к различным последствиям, от включения в строку лишних символов до порчи памяти за пределами строкового буфера операциями, изменяющими строку. Некоторые библиотечные функции являются источником проблем с завершением строк и требуют особого внимания при анализе исходного кода. Например, если у функции strncpy кончается свободное место в приемном буфере, она не завершает записываемую строку нулем. Программист должен явно записать завершитель, иначе в программе может возникнуть уязвимость. Например, следующий код небезопасен: Цитата:
Возможность эксплуатации этих дефектов несколько ограничивается состоянием прилегающих буферов, но во многих ситуациях некорректное завершение строк может привести к выполнению постороннего кода. 8 Пропуск завершителя Некоторые уязвимости в приложениях появляются в результате пропуска завершающего нулевого байта и продолжения обработки дальше в памяти. Если после пропуска нулевого байта произойдет операция записи, появляется потенциальная возможность порчи содержимого памяти и выполнения постороннего кода. Такие уязвимости обычно возникают в циклах, где строка обрабатывается по одному символу или делаются допущения относительно длины строки. Следующий фрагмент до недавнего времени присутствовал в модуле mod_rewrite Apache: Цитата:
Цитата:
9 Уязвимости знакового сравнения Многие программисты пытаются проверять длину вводимых пользователем данных, но при наличии знаковых спецификаторов проверка часто осуществляется неверно. Многие спецификаторы длины (такие, как size_t) являются беззнаковыми, и им не присущи проблемы знаковых спецификаторов вроде off_t. В ходе сравнения двух знаковых целых чисел при проверке длины можно упустить возможность того, что одно из чисел отрицательно, особенно при сравнении с константой.Правила сравнения разнотипных целых чисел не всегда очевидны по повелению откомпилированного кода. Согласно стандарту ISO для языка С, при сравнении двух целых разного типа или размера они предварительно преобразуются к знаковому типу int, а затем сравниваются. Если какое-либо из целых превышает по размеру знаковый тип int, оба числа преобразуются к большему типу, а затем сравниваются. При сравнении беззнакового и знакового чисел беззнаковый тип обладает большим приоритетом. Например, следующее сравнение будет беззнаковым: Цитата:
Цитата:
Цитата:
сравнением преобразуются в знаковые: Цитата:
Знаковые сравнения лежат в основе уязвимости Apache, обнаруженной в 2002 г. Причина кроется в следующем фрагменте: Цитата:
наименьшее значение из bufsiz и r->remaining, но если размер фрагмента отрицателен, эту проверку удается обойти. При передаче отрицательного размера фрагмента ap_bread значение преобразуется в очень большое положительное число, и происходит очень большая операция memcpy. Дефект просто и очевидно эксплуатировался в Win32 посредством замены SEH, а группа Gobbles Security Group доказала, что он также может использоваться в BSD из-за ошибки в реализации memcpy. Дефекты этого типа продолжают встречаться и в современных программах. Будьте внимательны везде, где знаковые целые числа используются в качестве спецификаторов длины. |
10 Целочисленное переполнение Похоже, термин «целочисленные переполнения» вошел в моду. Сейчас им часто обозначают широкий круг уязвимостей, многие из которых не имеют отношения к «настоящему» целочисленному переполнению. Первое четкое определение целочисленного переполнения было дано в докладе «Профессиональный анализ исходного кода» на конференции BlackHat USA в 2002 г., хотя эта проблема и ранее была известна и описана специалистами в области безопасности.Целочисленное переполнение происходит тогда, когда целое число превышает свое максимальное допустимое значение или надает ниже минимума. Максимальное и минимальное значения целого числа зависят от его типа и размера. 16-разрядное целое со знаком имеет максимальное значение 32 767 (0x7fff) и минимальное значение -32 768 (-0x8000). 32-разрядное целое без знака имеет максимальное значение 4 294 967 295 (Oxffffffff) и минимальное значение 0. Если 16-разрядное целое со знаком, равное 32 767, будет увеличено на 1, оно в результате целочисленного переполнения становится равным -32 768. Целочисленное переполнение может пригодиться для обхода проверки размеров или для выделения буферов, размер которых заведомо недостаточен для хранения копируемых в них данных. К категории целочисленного переполнения в общем случае относятся переполнение сложения/вычитания и переполнение умножения. Переполнение сложения/вычитания возникает при сложении или вычитании двух величин, в результате которого результат выходит за верхнюю или нижнюю границу целого типа. Например, следующий код создает потенциальную опасность целочисленного переполнения: Цитата:
Переполнение вычитания обычно возникает тогда, когда в программе предполагается некоторый минимальный размер вводимой пользователем величины. Так, в следующем фрагменте существует угроза целочисленного переполнения: Цитата:
Переполнение умножения возникает при умножении двух величин, результат которого превышает максимальное допустимое значение целого типа. Уязвимости этого типа были обнаружены в OpenSSH и библиотеке Sun RPC и 2002 г. Следующий фрагмент OpenSSH (до версии 3.4) является типичным примером переполнения умножения. Цитата:
Появляется возможность выделить очень маленький блок памяти и скопировать в него большое количество указателей на символы. Интересно заметить, что эта уязвимость могла реально эксплуатироваться в OpenBSD как раз из-за более защищенной реализации кучи, когда управляющие структуры не хранятся в самой куче. В реализациях кучи со встроенными управляющими структурами запись указателей ведет к сбоям при последующих попытках выделения памяти (как в packet_get_string). Целые числа меньшей разрядности в большей степени подвержены угрозе переполнения; для 16-разрядных целых типов целочисленное переполнение может быть вызвано стандартными функциями вроде strlen(). В частности, этот тип целочисленного переполнения имел место в функции RtlDosPathNameToNtPathName_U, ставшей причиной уязвимости IIS WebDAV, описанной в Microsoft Security Bulletin MS03-007. Дефекты целочисленного переполнения по-прежнему актуальны и часто встречаются на практике. Хотя многие программисты знают о потенциальных дефектах строковых операций, они хуже представляют себе риски, возникающие при манипуляциях целыми числами. Вероятно, эти уязвимости будут встречаться в программах еще много лет. 11 Преобразование целых чисел с разной разрядностью Преобразования между целыми числами разного размера порой приводят к интересным и неожиданным результатам. Такие преобразования могут быть небезопасными, если программист не продумает их последствия; встретив соответствующие команды в исходном коде, следует тщательно изучить их. Преобразования могут привести к усечению данных, смене знака или его распространению внутри числа. Иногда при этом возникают дефекты, которые можно реально эксплуатировать.Преобразование большего целого типа к меньшему (скажем, 32-разрядного к 16-разрядному, или 16-разрядного к 8-разрядному) может привести к усечению или смене знака. Скажем, если знаковое 32-разрядное целое с отрицательным значением -65 535 преобразуется в 16-разрядное целое, то результат окажется равным +1 из-за усечения старших 16 бит. Преобразования меньших целых типов к большим могут приводить к распространению знака в зависимости от исходного и приемного типов. Скажем, при преобразовании знакового 16-разрядного целого со значением -1 к 32-разядно-му целому без знака будет получен результат 4 Гбайт минус 1. В табл. 11.1 описаны последствия разных преобразований целочисленных типов. Представленная информация проверена для последних версий gcc. Таблица 11.1. Преобразования целочисленных типов Цитата:
Откровенно говоря, преобразования целых чисел разного размера довольно сложны. Если недостаточно глубоко продумать суть таких преобразований, они часто становятся источником ошибок. Кстати говоря, в современных приложениях не так уж много реальных причин для использования целых чисел разного размера; но если такие причины все же существуют, внимательно проанализируйте код преобразования. 12 Повторное освобождение памяти Хотя ошибка повторного освобождения одного и того же блока памяти на первый взгляд кажется вполне безопасной, она может принести к порче содержимого памяти и выполнению произвольного кода. Некоторые реализации кучи полностью или хорошо защищены от таких дефектов, поэтому их практическое применение возможно не на всех платформах.Как правило, программисты не делают подобных ошибок и не пытаются освобождать локальную переменную дважды (хотя мы сталкивались с такими примерами). Уязвимости повторного освобождения чаще всего встречаются тогда, когда буферы в куче хранятся в указателях с глобальной видимостью. Многие приложения при освобождении глобального указателя присваивают ему значение NULL, чтобы предотвратить его повторное использование. Если приложение не делает чего-нибудь в этом роде, желательно начать поиски мест, в которых фрагмент памяти может освобождаться дважды. Такие уязвимости также встречаются в коде на C++ при освобождении экземпляра класса, некоторые члены которого уже были освобождены. Недавно в zlib была обнаружена уязвимость, в которой некоторая ошибка в процессе разархивации приводила к двукратному освобождению глобальной переменной. Кроме того, недавняя уязвимость CVS-сервера также была результатом повторного освобождения. 13 Неосвобождение выделенной памяти Хоть применение функции освобождающей выделенный блок памяти, например free(), не является действительно необходимым, поскольку любая распределённая память автоматически освобождается по завершении программы, следует всё-таки уделить внимание и тут. В более сложных программах возможность, связанная с освобождением повторным использованием памяти, может иметь значение. Объём статистической памяти фиксируется во время компиляции; этот объём не изменяется при выполнении программы. В процессе выполнения программы объём памяти, выделяемый для автоматических переменных, изменяется в автоматическом режиме. Однако объём памяти, используемый для распределённой памяти, только возрастает, если не воспользоваться функцией free(). Например, предположим, что функция создаёт временную копию массива, как показано в следующем коде: Цитата:
Предположим, что функция free() не используется. Если функция завершается, указатель temp, являясь автоматической переменной, исчезает. Причём указанные 16 000 байтов памяти продолжают существовать. К этому объёму нельзя получить доступ, поскольку адрес отсутствует. Его нельзя использовать повторно, так как не вызывается функция free(). Вторично вызывается gobble(), создаётся снова указатель temp, функция malloc() опять применяется для распределения 16 000 байтов памяти. Первый блок из 16 000 байтов больше не является доступным, поэтому функция malloc() должна обнаруживать второй блок из 16 000 байтов. Если функция завершается, этот блок памяти также становится недоступным и не используется повторно. Однако цикл выполняется 1000 раз, поэтому ко времени завершения цикла из пула памяти удаляется 16 000 000 памяти байтов. Фактически, программа может превысить лимит выделяемой памяти. Проблема такого рода называется утечкой памяти. Чтобы предотвратить её появление, необходимо в конце выполняемой программы вызвать функцию free(). 14 Использование памяти вне области видимости Некоторые фрагменты памяти в приложении имеют область видимости, а также срок жизни, в течение которого они являются действительными. Любое использование этих фрагментов до того, как они станут действительными, или после того, как они станут недействительными, рискованно. Потенциальный результат — порча памяти, приводящая к выполнению произвольного кода.15 Использование неинициализированных переменных Хотя случаи использования неинициализированных переменных в программах попадаются относительно редко, иногда это все же случается, и тогда в приложениях могут возникнуть реально эксплуатируемые дефекты. Статическая память (в частности, секции .data и .bss) инициализируется нулями при запуске программы. Для переменных в стеке и куче гарантия такой инициализации отсутствует, поэтому для устойчивой работы программы они должны специально инициализироваться перед первой операцией чтения.Содержимое неинициализированной переменной по своей сути является неопределенным. Тем не менее, можно точно предсказать, какие данные будут содержаться в неинициализированной области памяти. Например, неинициализированная стековая переменная будет содержать данные, оставшиеся от предыдущих вызовов функций. В ней могут оказаться данные аргументов, сохраненные регистры или локальные переменные от предыдущих вызовов, в зависимости от ее местонахождения в стеке. Если благодаря везению нападающему удастся взять под контроль нужную область памяти, часто открывается возможность эксплуатации таких уязвимостей. Уязвимости неинициализированных переменных встречаются редко, потому что обычно ведут к немедленному аварийному завершению программы. Как правило, их можно встретить в редко выполняемом коде, скажем, в блоках, управление которым передается в результате маловероятных ошибок. Многие компьютеры пытаются выявить случаи обращения к неинициализированным переменным. В Microsoft Visual C++ предусмотрена логика выявления подобных состояний; то же делает и gcc, но ни один компилятор не справляется с этой работой идеально. Следовательно, ответственность возлагается в первую очередь на разработчика, которому не следует допускать таких ошибок. В следующем гипотетическом примере продемонстрирован упрошенный случай использования неициализированной переменной: Цитата:
Хотя такой тип уязвимостей неплохо поддается автоматическому обнаружению, дефекты использования неинициализированных переменных все еше встречаются в приложениях (например, дефект, обнаруженный Стефаном Эссером в PHP в 2002 г.). Несмотря на относительную редкость, эти дефекты бывают довольно неочевидными, и могут оставаться незамеченными в течение многих лет. 16 Использование памяти после освобождения Буферы в куче остаются действительными, начиная с момента выделения и до момента освобождения памяти вызовом free или realloc с нулевым размером. Любые попытки записи в буфер и куче после его освобождения приводят к порче содержимого памяти п открывают возможность выполнения постороннего кода.Уязвимости использования памяти после освобождения чаще всего встречаются тогда, когда программа освобождает один из нескольких существующих указателей на буфер. Подобные уязвимости вызывают непредвиденное повреждение кучи и обычно ликвидируются в процессе разработки. Как правило, они попадают л окончательную версию приложения лишь в редко выполняемых блоках кода. Примером может послужить уязвимость в функции psprintf программы Apache 2. опубликованная в мае 2003 года — программа случайно освобождала активный блок памяти, а затем передавала его функции выделения памяти Apache, которая являлась аналогом malloc. 17 Проблемы многопоточности и реентерабельности Приложения с открытыми исходными текстами в большинстве не являются многопоточными. С другой стороны, в немногочисленных многопоточных приложениях не всегда реализованы необходимые меры предосторожности. Любой многопоточный код, в котором разные программные потоки без блокировки работают с одними и теми же глобальными переменными, создает потенциальную угрозу для безопасности. Обычно такие дефекты обнаруживаются лишь после того, как приложение начинает эксплуатироваться под интенсивной нагрузкой, а иногда вообще остаются незамеченными или относятся к категории перемежающихся сбоев, которые не удается подтвердить.Как указал Михал Залевски (Michal Zalewski) в статье «Problems with Msktemp()» (август 2002), передача сигналов в Unix может привести к остановке программы, при которой глобальные переменные оказываются в неожиданном состоянии. Если в обработчиках сигналов используются библиотечные функции, небезопасные в плане реентерабельности, это может привести к порче памяти. Хотя у многих функций существуют версии, безопасные в отношении и программных потоков, и реентерабельности, используются они не всегда. При поиске таких уязвимостей необходимо представлять себе, что происходит при доступе со стороны нескольких потоков. Очень важно понимать, как работают базовые библиотечные функции, потому что проблемы могут крыться именно в них. Если помнить об этом, выявление дефектов многопоточности окажется не такой уж и сложной задачей. Приложение Хочу представить список наиболее опасных функций для C/C++ взятых из исходников программы Flawfinder, мною переведённых и дополненных. Список можно скачать тут. |
| Время: 22:07 |