Ниже представлена классификация уязвимостей языка C/C++, немного подправленная и дополненная, взятая из книги Джека Козиола, Дэвида Личфилда и др.: «Искусство взлома и защиты систем». Список не претендует на полноту, но даёт представление о различных типах уязвимостей которые наиболее часто встречаются. Всегда полезно знать, какие дефекты часто (или редко) встречаются в приложениях. Попытаемся отметить основные разновидности ошибок, встречающихся и современных приложениях. Каждые несколько лет обнаруживается новая разновидность дефектов, и почти сразу после этого хакеры выявляют целый пласт уязвимостей. Другие уязвимости быстро найти не удается, но в любом случае, чтобы выявите уязвимость, необходимо сначала опознать ее. 1 Общие логические ошибкиХотя класс общих логических ошибок составляет самую размытую категорию уязвимостей, именно ошибки этой категории лежат в основе многих проблем, Чтобы найти дефекты в логике программирования, создающие угрозу безопасности, необходимо достаточно хорошо понять само приложение. Разберитесь во внутренних структурах и классах, специфических для приложения, и попробуйте изобрести способы их некорректного использования. Например, если в приложении задействована стандартная буферная структура или строковый класс, хорошее понимание логики приложения поможет найти те места, в которых члены структуры или класса применяются некорректно или небезопасно. При анализе достаточно защищенных, хорошо протестированных приложений, поиск общих логических ошибок может стать вторым по эффективности методом анализа. 2 Пережитки прошлогоНекоторые уязвимости, часто встречавшиеся в программах с открытыми исходными текстами еще пять лет назад, в настоящее время почти исчезли. Обычно они воплощаются в форме хорошо известных функций копирования содержимого памяти без проверки, таких как strcpy, sprintf и strcat Хотя эти функции можно вызывать и приложениях вполне безопасно, раньше они часто использовались неправильно, что приводило к переполнению буфера. Впрочем, в современных программах с открытыми текстами эти типы уязвимостей практически не встречаются. Функции strcpy, sprintf, strcat, gets и другие аналогичные функции не имеют никакой информации о размере приемных буферов. Если правильно выделить приемный буфер пли проверить размер данных перед копированием, большинство функции может использоваться без всякого риска, но в противном случае возникает угроза безопасности. Информация об этих проблемах широко распространена в сообществе разработчиков. Например, в man-страницах функций sprintf и strcpy упоминается об опасности вызова этих функций без предварительной проверки границ. 3 Форматные строкиУязвимости форматных строк привлекли к себе внимание примерно в 2000 году, и за последние несколько лет было открыто немало серьезных уязвимостей этого класса. Данные дефекты основаны на возможности нападающего контролировать форматную строку, которая передается функциям, получающим аргументы в стиле printf (syslog, *printf и их аналоги). Если нападающий получит контроль над форматной строкой, он сможет передать директивы, приводящие к порче содержимого памяти и выполнению произвольного кода. Эти уязвимости в значительной мере базируются на малоизвестной директиве %n, которая записывает количество уже выведенных байтов по указателю на целое число. Уязвимости форматных строк очень легко обнаруживаются в процессе анализа. Количество функций, получающих аргументы в стиле printf, относительно невелико; часто достаточно поочередно проверить вызовы этих функций на предмет того, может ли нападающий получить контроль над форматкой строкой. Например, следующие два вызола syslog заметно отличаются друг от друга: Код потенциальной уязвимостью: Если в первом примере строка string окажется под контролем нападающего, может возникнуть угроза безопасности. Чтобы убедиться в существовании уязвимости форматной строки, нередко приходится отслеживать поток данных на несколько функций назад. Некоторые приложения содержат собственные реализации аналогов printf, поэтому анализ не должен ограничиваться узким набором стандартных функции. Процедура выявления дефектов форматных строк достаточно стандартна, поэтому поиск таких дефектов может осуществляться автоматически. Самым распространенным местом поиска дефектов форматных строк является код ведения журналов. Часто приходится видеть, как константная форматная строка перелается журнальной функции, после чего вывод направляется в буфер и передается syslog с созданием уязвимости. Следующий гипотетический пример демонстрирует классическую уязвимость форматной строки в коде функции ведения журнала: Уязвимости форматных строк впервые были выявлены на сервере wu-ftpd, а в дальнейшем были обнаружены во многих приложениях. Но из-за того, что эти уязвимости очень легко выявляются в процессе анализа, они практически полностью исчезли во всех основных программных пакетах с открытыми текстами. 4 Общие ошибки проверки границНередко приложения пытаются организовать проверку границ при выполнении небезопасных операции; впрочем, на практике эта процедура часто организуется неверно. Ошибки проверки границ отличаются от других классов уязвимостей, в которых проверка вообще отсутствует, однако в конечном счете результат оказывается одним и тем же. Оба типа уязвимостей объясняются логическими ошибками при реализации проверки. Без углубленного анализа кода проверки эти уязвимости часто остаются незамеченными. Другими словам, не стоит полагать, что некоторый фрагмент кода неуязвим только потому, что он пытается проверять границы. Прежде чем следовать дальше, убедитесь и там, что эта попытка делается правильно. Хорошим примером ошибки проверки границ является дефект препроцессора Snort RFC, обнаруженный группой ISS X-Force в начале 2003 г. Следующий фрагмент присутствует в уязвимых версиях Snort: В контексте приложения length — длина одного фрагмента RPC, a size — размер всего пакета данных. Выходной буфер совпадает с входным, а для ссылок на него в двух разных местах используются переменные грс и index. Программа пытается восстановить фрагменты RPC, удаляя заголовки из потока данных. При каждой итерации цикла позиции rрс и index увеличиваются, a total_len представляет размер данных, записанных в буфер. Здесь сделана попытка организовать проверку границ, однако проверка выполняется неверно. Длина текущего фрагмента RPC сравнивается с общим размером данных, тогда как в действительности общая длина всех фрагментов RPC, включая текущий, должна сравниваться с размером буфера. При невнимательном просмотре кода легко предположить, что проверка выполняется правильно. Данный пример показывает, как важно проконтролировать все фрагменты, обеспечивающие проверку границ в важных местах программы. 5 Циклические конструкцииПереполнение буфера очень часто обнаруживается в циклах — вероятно потому, что с точки зрения программирования они несколько сложнее линейного кода. Чем сложнее цикл, тем больше вероятность того, что ошибка программиста приведет к появлению уязвимости. Многие широко распространенные и критичные в плане безопасности приложения содержат крайне запутанные циклы, часть значений которых небезопасна. Нередко в программах встречаются циклы внутри циклов; так появляются сложные наборы команд, в которых вероятность ошибок весьма велика. Циклы синтаксического разбора и любые циклы обработки пользовательского ввода являются хорошей отправной точкой для анализа приложения. Сосредоточив внимание на этих областях, можно получить ценные результаты с минимальными усилиями. Интересный пример ошибки в сложном цикле дает уязвимость, которую Марк Дауд (Mark Dowd) обнаружил в функции crackaddr программы Sendmail. Этот цикл слишком велик, чтобы его можно было привести здесь; наверняка он входит в список самых сложных циклов, встречающихся в программах с открытыми исходными текстами. Вследствие сложности и огромного количества переменных, обрабатываемых в цикле, при некоторых комбинациях входных данных происходит переполнение буфера. Sendmail содержит многочисленные проверки для предотвращения переполнения, и все же цикл приводит к непредвиденным последствиям. Некоторые аналитики, в том числе польская группа исследователей в области безопасности «The Last Stages of Delirium», недооценили возможность практического использования этой ошибки просто потому, что не нашли комбинации данных, приводящей к переполнению. 6 Уязвимости единичного смещенияУязвимости единичного смещения (или на другое небольшое число) принадлежат к числу распространенных ошибок программирования, при которых совсем небольшое число байтов записывается за пределами выделенной памяти. Эти ошибки часто являются результатом некорректного завершения строк нулем, неправильной организации циклов или неудачного использования стандартный строковых функций. В прошлом такие уязвимости встречались в некоторых распространенных приложениях. Например, следующий фрагмент взят из кода Apache 2 (до 2.0.46); позднее эта ошибка была исправлена без особого шума: Код обрабатывает MIME-заголовки, передаваемые как часть запроса веб-серверу. Если первые два условия if истинны, то длина выделенного буфера окажется на 1 меньше, чем следует, и завершающий вызов memcpy запишет пулевой байт за границей буфера. Использовать этот дефект на практике чрезвычайно трудно из-за нестандартной реализации кучи; тем не менее, перед вами несомненный случай ошибки единичного смещения. Любой цикл, после которого строка завершается нулем, следует дважды проверить на предмет ошибки смещения. Следующий фрагмент FTP-демона Open BSD демонстрирует проблему: Хотя программа пытается зарезервировать место для нулевого байта, если последним символом на границе выходного буфера является кавычка, происходит ошибка единичного смешения. Ошибка единичного смещения возникает также при неправильном использовании некоторых библиотечных функций. Например, функция strncat всегда завершает выходную строку нулем; если третий аргумент не будет соответствовать объему оставшегося места в выходном буфере за вычетом одного байта, то функция запишет нулевой байт за границами буфера. Пример неправильного вызова strncat: Безопасный вызов: 7 Ошибки некорректного завершения строкВ общем случае строки должны завершаться нуль-символами; это позволяет легко определить их границы и корректно выполнять операции с ними. Отсутствие завершителей у строк может создать дефекты безопасности при выполнении программы. Например, если строка не завершена положенным символом, содержимое прилегающей памяти будет интерпретировано как продолжение строки. Это может привести к различным последствиям, от включения в строку лишних символов до порчи памяти за пределами строкового буфера операциями, изменяющими строку. Некоторые библиотечные функции являются источником проблем с завершением строк и требуют особого внимания при анализе исходного кода. Например, если у функции strncpy кончается свободное место в приемном буфере, она не завершает записываемую строку нулем. Программист должен явно записать завершитель, иначе в программе может возникнуть уязвимость. Например, следующий код небезопасен: Так как вызов strncpy не завершает буфер non_term_buf, второй вызов strcpy небезопасен, хотя оба буфера имеют одинаковый размер. Если вставить следующую строку между strncpy и strcpy, угроза переполнения буфера исчезает: Возможность эксплуатации этих дефектов несколько ограничивается состоянием прилегающих буферов, но во многих ситуациях некорректное завершение строк может привести к выполнению постороннего кода. 8 Пропуск завершителяНекоторые уязвимости в приложениях появляются в результате пропуска завершающего нулевого байта и продолжения обработки дальше в памяти. Если после пропуска нулевого байта произойдет операция записи, появляется потенциальная возможность порчи содержимого памяти и выполнения постороннего кода. Такие уязвимости обычно возникают в циклах, где строка обрабатывается по одному символу или делаются допущения относительно длины строки. Следующий фрагмент до недавнего времени присутствовал в модуле mod_rewrite Apache: Проблема кроется и команде: В этой команде код обработки пытается обойти конструкцию :// в URI. Тем не менее, обратите внимание, что внутри is_absolute_uri не все схемы URI заверишаются символами ://. При запросе URI вида ldap:a программа пропустит нулевой завершающий байт. Дальнейшая обработка URI приведет к записи нулевого банта, в результате чего возникнет потенциальная уязвимость. В данном случае для этого должны быть установлены некоторые правила перезаписи, но подобные, проблемы все еще часто встречаются в программах с открытыми исходными текстами, и поэтому им следует уделять внимание в процессе анализа. 9 Уязвимости знакового сравненияМногие программисты пытаются проверять длину вводимых пользователем данных, но при наличии знаковых спецификаторов проверка часто осуществляется неверно. Многие спецификаторы длины (такие, как size_t) являются беззнаковыми, и им не присущи проблемы знаковых спецификаторов вроде off_t. В ходе сравнения двух знаковых целых чисел при проверке длины можно упустить возможность того, что одно из чисел отрицательно, особенно при сравнении с константой. Правила сравнения разнотипных целых чисел не всегда очевидны по повелению откомпилированного кода. Согласно стандарту ISO для языка С, при сравнении двух целых разного типа или размера они предварительно преобразуются к знаковому типу int, а затем сравниваются. Если какое-либо из целых превышает по размеру знаковый тип int, оба числа преобразуются к большему типу, а затем сравниваются. При сравнении беззнакового и знакового чисел беззнаковый тип обладает большим приоритетом. Например, следующее сравнение будет беззнаковым: С другой стороны, следующее сравнение выполняется как знаковое: Некоторые операторы (такие, как sizeof()) являются беззнаковыми. Показанное ниже сравнение выполняется как беззнаковое, несмотря на то, что результат оператора sizeof является константой: Однако следующее сравнение является знаковым, потому что оба коротких целых перед сравнением преобразуются в знаковые: В большинстве случаев, особенно при использовании 32-разрядных цел их, для обхода проверки размерен необходима возможность напрямую задать целое число. Например, на практике невозможно заставить функцию strlen() вернуть значение, которое может быть преобразовано в отрицательное число, но если целое напрямую читается из пакета, часто удается сделать его отрицательным. Знаковые сравнения лежат в основе уязвимости Apache, обнаруженной в 2002 г. Причина кроется в следующем фрагменте: Здесь bufsiz — знаковое целое число, определяющее объем свободного места в буфере, a r->remaining — знаковое число типа off_t. определяющее размер фрагмента и читаемое непосредственно из запроса. Предполагается, что переменной len_to_read будет присвоено наименьшее значение из 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. Целочисленное переполнение может пригодиться для обхода проверки размеров или для выделения буферов, размер которых заведомо недостаточен для хранения копируемых в них данных. К категории целочисленного переполнения в общем случае относятся переполнение сложения/вычитания и переполнение умножения. Переполнение сложения/вычитания возникает при сложении или вычитании двух величин, в результате которого результат выходит за верхнюю или нижнюю границу целого типа. Например, следующий код создает потенциальную опасность целочисленного переполнения: Если значение переменной attacker_defined_size лежит и диапазоне от -16 до-1, сложение вызовет целочисленное переполнение, и буфер, выделенный в результате вызова malloc(), окажется слишком мал для данных, копируемых вызовом memcpy(). Подобный код очень часто встречается в приложениях, распространяемых с открытыми исходными текстами. Эксплуатация подобных уязвимо-стей сопряжена с некоторыми трудностями, н все же такие дефекты сушествуют. Переполнение вычитания обычно возникает тогда, когда в программе предполагается некоторый минимальный размер вводимой пользователем величины. Так, в следующем фрагменте существует угроза целочисленного переполнения: В этом примере целочисленное переполнение произойдет, если размер прочитанных из сети данных меньше предполагаемого минимального размера (HEADER_SIZE). Переполнение умножения возникает при умножении двух величин, результат которого превышает максимальное допустимое значение целого типа. Уязвимости этого типа были обнаружены в OpenSSH и библиотеке Sun RPC и 2002 г. Следующий фрагмент OpenSSH (до версии 3.4) является типичным примером переполнения умножения. Здесь nresp представляет собой целое число, полученное непосредственно из SSH-пакета. Оно умножается па размер указателя на символ (в данном случае — 4), и программа выделяет приемный буфер соответствующего размера. Если значение nresp превышает 0x3fffffff, результат умножения выходит за верхнюю границу беззнакового целого, и происходит переполнение. Появляется возможность выделить очень маленький блок памяти и скопировать в него большое количество указателей на символы. Интересно заметить, что эта уязвимость могла реально эксплуатироваться в 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. Преобразования целочисленных типов Будем надеяться, что таблица поможет разобраться в тонкостях преобразований целых разных типов. Хорошим примером служит уязвимость, недавно обнаруженная в функции prescan программы Scndmail. Знаковый символ (8 разрядов) читался из входного буфера и преобразовывался в 32-разрядное число со знаком. Происходило расширение знака до 32-разрядиого значения -1, которое интерпретировалось как признак специальной ситуации NOCHAR. В результате в процедуре проверки ошибок происходил сбои, и появлялась возможность удаленного использования переполнения буфера. Откровенно говоря, преобразования целых чисел разного размера довольно сложны. Если недостаточно глубоко продумать суть таких преобразований, они часто становятся источником ошибок. Кстати говоря, в современных приложениях не так уж много реальных причин для использования целых чисел разного размера; но если такие причины все же существуют, внимательно проанализируйте код преобразования. 12 Повторное освобождение памятиХотя ошибка повторного освобождения одного и того же блока памяти на первый взгляд кажется вполне безопасной, она может принести к порче содержимого памяти и выполнению произвольного кода. Некоторые реализации кучи полностью или хорошо защищены от таких дефектов, поэтому их практическое применение возможно не на всех платформах. Как правило, программисты не делают подобных ошибок и не пытаются освобождать локальную переменную дважды (хотя мы сталкивались с такими примерами). Уязвимости повторного освобождения чаще всего встречаются тогда, когда буферы в куче хранятся в указателях с глобальной видимостью. Многие приложения при освобождении глобального указателя присваивают ему значение NULL, чтобы предотвратить его повторное использование. Если приложение не делает чего-нибудь в этом роде, желательно начать поиски мест, в которых фрагмент памяти может освобождаться дважды. Такие уязвимости также встречаются в коде на C++ при освобождении экземпляра класса, некоторые члены которого уже были освобождены. Недавно в zlib была обнаружена уязвимость, в которой некоторая ошибка в процессе разархивации приводила к двукратному освобождению глобальной переменной. Кроме того, недавняя уязвимость CVS-сервера также была результатом повторного освобождения. 13 Неосвобождение выделенной памятиХоть применение функции освобождающей выделенный блок памяти, например free(), не является действительно необходимым, поскольку любая распределённая память автоматически освобождается по завершении программы, следует всё-таки уделить внимание и тут. В более сложных программах возможность, связанная с освобождением повторным использованием памяти, может иметь значение. Объём статистической памяти фиксируется во время компиляции; этот объём не изменяется при выполнении программы. В процессе выполнения программы объём памяти, выделяемый для автоматических переменных, изменяется в автоматическом режиме. Однако объём памяти, используемый для распределённой памяти, только возрастает, если не воспользоваться функцией free(). Например, предположим, что функция создаёт временную копию массива, как показано в следующем коде: Сначала вызывается функция gobble(), создаётся указатель temp для распределения 16 000 байтов памяти (при условии, что тип double хранит 8 байтов), затем вызывается функция malloc(). Предположим, что функция 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, но ни один компилятор не справляется с этой работой идеально. Следовательно, ответственность возлагается в первую очередь на разработчика, которому не следует допускать таких ошибок. В следующем гипотетическом примере продемонстрирован упрошенный случай использования неициализированной переменной: Если аргумент data содержит null, то указатель test оказывается неинициализированным. Он продолжает оставаться в атом состоянии до конца функции, когда для него вызывается функция free. Обратите внимание: ни gcc, ни Visual C++ во время компиляции не предупреждают программиста об этой ошибке. Хотя такой тип уязвимостей неплохо поддается автоматическому обнаружению, дефекты использования неинициализированных переменных все еше встречаются в приложениях (например, дефект, обнаруженный Стефаном Эссером в PHP в 2002 г.). Несмотря на относительную редкость, эти дефекты бывают довольно неочевидными, и могут оставаться незамеченными в течение многих лет. 16 Использование памяти после освобожденияБуферы в куче остаются действительными, начиная с момента выделения и до момента освобождения памяти вызовом free или realloc с нулевым размером. Любые попытки записи в буфер и куче после его освобождения приводят к порче содержимого памяти п открывают возможность выполнения постороннего кода. Уязвимости использования памяти после освобождения чаще всего встречаются тогда, когда программа освобождает один из нескольких существующих указателей на буфер. Подобные уязвимости вызывают непредвиденное повреждение кучи и обычно ликвидируются в процессе разработки. Как правило, они попадают л окончательную версию приложения лишь в редко выполняемых блоках кода. Примером может послужить уязвимость в функции psprintf программы Apache 2. опубликованная в мае 2003 года — программа случайно освобождала активный блок памяти, а затем передавала его функции выделения памяти Apache, которая являлась аналогом malloc. 17 Проблемы многопоточности и реентерабельностиПриложения с открытыми исходными текстами в большинстве не являются многопоточными. С другой стороны, в немногочисленных многопоточных приложениях не всегда реализованы необходимые меры предосторожности. Любой многопоточный код, в котором разные программные потоки без блокировки работают с одними и теми же глобальными переменными, создает потенциальную угрозу для безопасности. Обычно такие дефекты обнаруживаются лишь после того, как приложение начинает эксплуатироваться под интенсивной нагрузкой, а иногда вообще остаются незамеченными или относятся к категории перемежающихся сбоев, которые не удается подтвердить. Как указал Михал Залевски (Michal Zalewski) в статье «Problems with Msktemp()» (август 2002), передача сигналов в Unix может привести к остановке программы, при которой глобальные переменные оказываются в неожиданном состоянии. Если в обработчиках сигналов используются библиотечные функции, небезопасные в плане реентерабельности, это может привести к порче памяти. Хотя у многих функций существуют версии, безопасные в отношении и программных потоков, и реентерабельности, используются они не всегда. При поиске таких уязвимостей необходимо представлять себе, что происходит при доступе со стороны нескольких потоков. Очень важно понимать, как работают базовые библиотечные функции, потому что проблемы могут крыться именно в них. Если помнить об этом, выявление дефектов многопоточности окажется не такой уж и сложной задачей. ПриложениеХочу представить список наиболее опасных функций для C/C++ взятых из исходников программы Flawfinder, мною переведённых и дополненных. Список можно скачать тут.