Интеллектуальные развлечения. Интересные иллюзии, логические игры и загадки.

Добро пожаловать В МИР ЗАГАДОК, ОПТИЧЕСКИХ
ИЛЛЮЗИЙ И ИНТЕЛЛЕКТУАЛЬНЫХ РАЗВЛЕЧЕНИЙ
Стоит ли доверять всему, что вы видите? Можно ли увидеть то, что никто не видел? Правда ли, что неподвижные предметы могут двигаться? Почему взрослые и дети видят один и тот же предмет по разному? На этом сайте вы найдете ответы на эти и многие другие вопросы.

Log-in.ru© - мир необычных и интеллектуальных развлечений. Интересные оптические иллюзии, обманы зрения, логические флеш-игры.

Привет! Хочешь стать одним из нас? Определись…    
Если ты уже один из нас, то вход тут.

 

 

Амнезия?   Я новичок 
Это факт...

Интересно

Погонофобия - это боязнь бород.

Еще   [X]

 0 

19 смертных грехов, угрожающих безопасности программ (Ховард Майкл)

Эта книга необходима всем разработчикам программного обеспечения, независимо от платформы, языка или вида приложений. В ней рассмотрены 19 грехов, угрожающих безопасности программ, и показано, как от них избавиться. Рассмотрены уязвимости на языках C/C++, C#, Java, Visual Basic, Visual Basic.NET, Perl, Python в операционных системах Windows, Unix, Linux, Mac OS, Novell Netware. Авторы издания, Майкл Ховард и Дэвид Лебланк, обучают программистов, как писать безопасный код в компании Microsoft. На различных примерах продемонстрированы как сами ошибки, так и способы их исправления и защиты от них. Если вы программист, то вам просто необходимо прочесть эту книгу.

Год издания: 2011

Цена: 150 руб.



С книгой «19 смертных грехов, угрожающих безопасности программ» также читают:

Предпросмотр книги «19 смертных грехов, угрожающих безопасности программ»

19 смертных грехов, угрожающих безопасности программ

   Эта книга необходима всем разработчикам программного обеспечения, независимо от платформы, языка или вида приложений. В ней рассмотрены 19 грехов, угрожающих безопасности программ, и показано, как от них избавиться. Рассмотрены уязвимости на языках C/C++, C#, Java, Visual Basic, Visual Basic.NET, Perl, Python в операционных системах Windows, Unix, Linux, Mac OS, Novell Netware. Авторы издания, Майкл Ховард и Дэвид Лебланк, обучают программистов, как писать безопасный код в компании Microsoft. На различных примерах продемонстрированы как сами ошибки, так и способы их исправления и защиты от них. Если вы программист, то вам просто необходимо прочесть эту книгу.


Майкл Ховард, Дэвид Лебланк, Джон Виега 19 смертных грехов, угрожающих безопасности программ. Как не допустить типичных ошибок

   Моей потрясающей семье. Ничто не может сравниться с ощущением, которое испытываешь, когда приходишь домой и в ответ на вопрос «Кто дома, ребятки?» слышишь, как два голоска хором кричат: «Папа!»
– Майкл
   Моему отцу, который объяснил мне, почему надо постоянно учиться и принимать новые вызовы.
– Дэвид
   Маме. Она привила мне интеллектуальное любопытство и всегда была со мной рядом.
-Джон

Об авторах

   Майкл Ховард работает старшим менеджером по безопасности программного обеспечения в группе по обеспечению безопасности в Microsoft Corp. Является соавтором удостоенной различных наград книги «Writing Secure Code» (Разработка безопасного кода). Он также совместно с коллегами ведет колонку «Basic Training» в журнале «ШЕЕ Security & Privacy Magazine» и является одним из авторов документа «Processes to Produce Secure Software» («Процессы в производстве безопасного программного обеспечения»), выпущенного организацией National Cyber Security Partnership для Министерства национальной безопасности (Department of Homeland Security). Будучи архитектором «Жизненного цикла разработки безопасного программного обеспечения» в Microsoft, Майкл посвящает большую часть времени выработке и внедрению передового опыта создания безопасных программ, которыми в конечном итоге будут пользоваться обычные люди.
   Дэвид Лебланк, доктор философии, в настоящее время работает главным архитектором программ в компании Webroot Software. До этого он занимал должность архитектора подсистемы безопасности в подразделении Microsoft, занимающемся разработкой Microsoft Office, стоял у истоков инициативы Trustworthy Computing и работал «белым хакером» в группе безопасности сетей в Microsoft. Дэвид является соавтором книг «Writing Secure Code» и «Assessing Network Secu–rity» («Оценка безопасности сети»), а также многочисленных статей. В погожие дни он любит конные прогулки вместе со своей женой Дженнифер.
   Джон Виега первым дал описание 19 серьезных просчетов при написании программ. Этот труд привлек внимание средств массовой информации и лег в основу настоящей книги. Джон является основателем и техническим директором компании Secure Software (www.securesoftware.com). Он один из авторов первой книги по безопасности программного обеспечения «Building Secure Software» («Создание безопасного программного обеспечения»), а также книг «Network Security and Cryptography with OpenSSL» («Безопасность и криптографические методы в сетях. Подход на основе библиотеки OpenSSL») и «Secure Programming СоокЬоок» («Рецепты для написания безопасных программ»). Он является основным автором процесса CLASP, призванного включить элементы безопасности в цикл разработки программ. Джон написал и сопровождает несколько относящихся к безопасности программ с открытыми исходными текстами. Раньше Джон занимал должности адъюнкт–профессора в техническом колледже штата Вирджиния и старшего научного сотрудника в Институте стратегии кибепространства (Cyberspace Policy Institute). Джон хорошо известен своими работами в области безопасности программ и криптографии, а в настоящее время он трудится над стандартами безопасности для сетей и программ.

О научных редакторах

   Алан Крассовски работает главным инженером по безопасности программного обеспечения в компании Symantec Corporation. Он возглавляет группу по безопасности продуктов, в задачу которой входит оказание помощи другим группам разработчиков в плане внедрения безопасных технологий, которые сокращают риски и способствуют завоеванию доверия со стороны клиентов. За последние 20 лет Алан работал над многими коммерческими программными проектами. До присоединения к Symantec он руководил разработками, был инженером–программистом и оказывал консультативные услуги многим компаниям, занимающим лидирующее положение в отрасли, в частности Microsoft, IBM, Tektronix, Step Technologies. Screenplay Systems, Quark и Continental Insurance. Он получил научную степень бакалавра в области вычислительной техники в Рочестерском технологическом институте, штат Нью–Йорк.
   Дэвид А. Уилер много лет занимается совершествованием практических методов разработки программ для систем с повышенным риском, в том числе особо крупных и нуждающихся в высокой степени безопасности. Он соавтор и соредактор книги «Software Inspection: An Industry Best Practice» («Инспекция программ: передовой опыт»), а также книг «Ada95: The Lovelace Tutorial» и «Secure Programming for Linux and UNIX HOWTO» («Рецепты безопасного программирования для Linux и UNIX»). Проживает в Северной Вирджинии.

Предисловие

   В основе теории компьютеров лежит предположение о детерминированном поведении машин. Обычно мы ожидаем, что компьютер будет вести себя так, как мы его запрограммировали. На самом деле это лишь приближенное допущение. Современные компьютеры общего назначения и их программное обеспечение стали настолько сложными, что между щелчком по кнопке мыши и видимым результатом лежит множество программных слоев. И мы вынуждены полагаться на то, что все они работают правильно.
   Любой слой программного обеспечения может содержать ошибки, из–за которых оно работает не так, как хотел автор, или, по крайней мере, не соответствует ожиданиям пользователя. Эти ошибки вносят в систему неопределенность, что может приводить к серьезным последствиям с точки зрения безопасности. Проявляться они могут по–разному: от простого краха системы, и тогда ошибку можно использовать, чтобы вызвать отказ от обслуживания, до переполнения буфера, позволяющего противнику выполнить в системе произвольный код.
   Коль скоро поведение программных систем недетерминировано из–за ошибок, то самые лучшие идеи по их защите – не более чем гипотезы. Мы можем воздвигать межсетевые экраны, реализовывать технологии защиты от переполнения буфера на уровне ОС, применять самые разнообразные методики, но все это никоим образом не изменит фундаментальную парадигму безопасности. И лишь за счет радикального улучшения качества программ и сокращения числа ошибок мы можем надеяться на успешность попыток обеспечить безопасность программного обеспечения.
   Устранение всех рисков, относящихся к безопасности, – нереальная задача при современном уровне развития систем разработки. У этой проблемы так много аспектов, что, даже для того чтобы просто оставаться в курсе дел, нужно посвящать этому все свое время. Что уж говорить о владении предметом в совершенстве!
   Если мы хотим добиться прогресса в битве против ошибок, связанных с безопасностью, то должны облегчить процесс их идентификации и устранения организациям, занимающимся разработкой, и при этом учесть реальные ограничения. О безопасности программного обеспечения написано немало отличных книг, в том числе и авторами настоящего издания. Но я полагаю необходимым не углубляться в разного рода сложности, а предложить разработчикам небольшой набор критически важных советов, следуя которым они смогут повысить качество своих программ с минимальными усилиями. Идея в том, чтобы осветить наиболее типичные проблемы, которые нетрудно устранить, а не ставить нереалистичную задачу достижения полной безопасности.
   В бытность начальником отдела в Министерстве национальной безопасности я попросил Джона Виегу составить перечень 19 «грехов» программиста. Первоначальный список был призван поставить корпоративный мир в известность о тех ошибках, которые чаще всего угрожают безопасности, но он не был составлен в форме рецептов. А эта книга именно такова. В ней приводится список проблем, от которых организации–разработчики должны защищаться в первую очередь, и даются рекомендации, как не допустить самого возникновения этих проблем. В книге также показано, как выявить подобные ошибки: посредством анализа кода или тестирования. Описание приемов и методик краткое и точное, авторы четко формулируют, что надо, а чего никогда не надо делать. Авторы проделали огромную работу, чтобы представить вашему вниманию список наиболее распространенных дефектов, от которых страдает безопасность современных программ. Надеюсь, что сообщество разработчиков оценит эту книгу и воспользуется ей для устранения недетерминизма и рисков, с которыми мы постоянно сталкиваемся.
   Амит Йоран,
   бывший начальник
   отдела национальной кибербезопасности
   Министерства национальной безопасности
   Грейт Фоллс, Вирджиния,
   21 мая 2005 г.

Благодарности

   Эта книга – косвенный результат дальновидности Амита Йорана. Мы благодарны ему за то, что во время работы в Министерстве национальной безопасности (и позже) он делал все возможное, чтобы привлечь внимание к проблемам безопасности программного обеспечения. Мы также выражаем признательность следующим специалистам в области безопасности за усердие, с которым они рецензировали черновики отдельных глав, за их мудрость и за откровенные комментарии: Дэвиду Рафаэлю (David Raphael), Марку Кэрфи (Mark Curphy), Рудольфу Араю (Rudolph Arauj), Алану Крассовски (Alan Krassowski), Дэвиду Уилеру (David Wheeler) и Биллу Хильфу (Bill Hilf). Эта книга не состоялась бы без настойчивости сотрудников издательства McGraw–Hill. Большое спасибо трем «Дж»: Джейн Браунлоу (Jane Brownlow), Дженнифер Хауш (Jennifer Housh) и Джоди Маккензи Qody McKenzie).

Введение

   В 2004 году Амит Йоран, тогда начальник отдела национальной кибербезопасности Министерства национальной безопасности США, объявил, что около 95% всех дефектов программ, относящихся к безопасности, проистекают из 19 типичных ошибок, природа которых вполне понятна. Мы не станем подвергать сомнению ваши интеллектуальные способности и объяснять важность безопасного программного обеспечения в современном взаимосвязанном мире, вы и так все понимаете, но приведем основные принципы поиска и исправления наиболее распространенных ошибок в вашем собственном коде.
   Неприятная особенность ошибок, касающихся безопасности, состоит в том, что допустить их очень легко, а результаты одной неправильно написанной строчки могут быть поистине катастрофическими. Червь Blaster смог распространиться из–за ошибки всего в двух строках кода.
   Если попытаться выразить весь накопленный опыт одной фразой, то, наверное, она звучала бы так: «Никакой язык программирования, никакая платформа не способны сделать программу безопасной, это можете сделать только вы». Существует масса литературы о том, как создавать безопасное программное обеспечение, да и авторы настоящей книги написали на эту тему немало текстов, к которым прислушиваются. И все же есть потребность в небольшой, простой и прагматической книге, в которой рассматривались бы все основные проблемы.
   Работая над этой книгой, мы старались придерживаться следующих правил, которые не позволили бы оторваться от земли.
   □ Простота. Мы не тратили место на пустую болтовню. Здесь вы не найдете ни репортажей с поля боя, ни забавных анекдотов – только голые факты. Скорее всего, вы просто хотите сделать свою работу качественно и в кратчайшие сроки. Поэтому мы стремились к тому, чтобы найти нужную информацию можно было просто и быстро.
   □ Краткость. Это следствие предыдущего правила: сосредоточившись исключительно на фактах, мы смогли сделать книгу небольшой по объему. Это введение тоже не будет многословным.
   □ Кроссплатформенность. Интернет – это среда, связывающая между собой мириады вычислительных устройств, работающих под управлением разных операционных систем и программ, написанных на разных языках. Мы хотели, чтобы эта книга была полезна всем разработчикам, поэтому представленные примеры относятся к большинству имеющихся операционных систем.
   □ Многоязычие. Следствие предыдущего правила: мы приводим примеры ошибок в программах, которые составлены на разных языках.

Структура книги

   □ «В чем состоит грех» – краткое введение, в котором объясняется, почему данное деяние считается грехом;
   □ «Как происходит грехопадение» – описывается суть проблемы; принципиальная ошибка, которая доводит до греха;
   □ «Подверженные греху языки» – перечень языков, подверженных данному греху;
   □ «Примеры ошибочного кода» – конкретные примеры ошибок в программах, написанных на разных языках и работающих на разных платформах;
   □ «Где искать ошибку» – на что нужно прежде всего обращать внимание при поиске в программе подобных ошибок;
   □ «Выявление ошибки на этапе анализа кода» тут все понятно: как найти грехи в своем коде. Мы понимаем, что разработчики – люди занятые, поэтому старались писать этот раздел коротко и по делу;
   □ «Тестирование» – описываются инструменты и методики тестирования, которые позволят обнаружить признаки рассматриваемого греха;
   □ «Примеры из реальной жизни» – реальные примеры данного греха, взятые из базы данных типичных уязвимостей и брешей (Common Vulnerabilities and Exposures – CVE) (www.cve.mitre.org). с сайта BugTraq (www.securityfocus.com) или базы данных уязвимостей в программах с открытыми исходными текстами (Open Source Vulnerability Database) (www.osvdb.org). В каждом случае мы приводим свои комментарии. Примечание: пока мы работали над этой книгой, рассматривался вопрос об отказе с 15 октября 2005 года от номеров CAN в базе данных CVE и переходе исключительно на номера CVE. Если это случится, то все ссылки на номер ошибки «CAN…» следует заменить ссылкой на соответствующий номер CVE. Например, если вы не сможете найти статью CAN–2004–0029 (ошибка Lotus Notes для Linux), попробуйте поискать CVE–2004–0029;
   □ «Искупление греха» – как исправить ошибку, чтобы избавиться от греха. И в этом случае мы демонстрируем варианты для разных языков;
   □ «Дополнительные защитные меры» – другие меры, которые можно предпринять. Они не исправляют ошибку, но мешают противнику воспользоваться потенциальным дефектом, если вы ее все–таки допустите;
   □ «Другие ресурсы» – это небольшая книжка, поэтому мы даем ссылки на другие источники информации: главы книг, статьи и сайты;
   □ «Резюме» – это неотъемлемая часть главы, предполагается, что вы будете к ней часто обращаться. Здесь приводятся списки рекомендуемых, нерекомендуемых и возможных действий при написании нового или анализе существующего кода. Не следует недооценивать важность этого раздела! Содержание всех Резюме сведено воедино в Приложении В.

Кому предназначена эта книга

   Эта книга адресована всем разработчикам программного обеспечения. В ней описаны наиболее распространенные ошибки, приводящие к печальным последствиям, а равно способы их устранения до того, как программа будет передана заказчику. Вы найдете здесь полезный материал вне зависимости от того, на каком языке пишете, будь то С, С++, Java, С#, ASP, ASP.NET, Visual Basic, PHP, Perl или JSP. Она применима к операционным системам Windows, Linux, Apple Mac OS X, OpenBSD и Solaris, а равно к самым разнообразным платформам: «толстым» клиентам, «тонким» клиентам или пользователям Web. Честно говоря, безопасность не зависит ни от языка, ни от операционной системы, ни от платформы. Если ваш код небезопасен, то пользователи беззащитны перед атакой.

Какие главы следует прочитать

   Но все же есть грехи, которым подвержены лишь некоторые языки и некоторые среды, поэтому важно, чтобы в первую очередь вы прочли о тех, что специфичны именно для вашего языка программирования, вашей ОС и вашей среды исполнения (Web и т. п.).
   Вот минимум, с которым надо ознакомиться при различных предположениях о специфике вашей работы.
   □ Всем рекомендуется ознакомиться с грехами 6, 12 и 13.
   □ Если вы программируете наязыках C/C++, то обязаны прочесть о грехах 1, 2 и З.
   □ Если вы программируете для Web с использованием таких технологий, как JSP, ASP, ASP.NET, PHP, CGI или Perl, то познакомьтесь с грехами 7 и 9.
   □ Если вы создаете приложения для работы с базами данных, например Oracle, MySQL, DB2 или SQL Server, прочтите о грехе 4.
   □ Если вы разрабатываете сетевые системы (клиент–серверные, через Web и прочие), не проходите мимо грехов 5, 8, 10, 14 и 15.
   □ Если в вашем приложении каким–то образом используется криптография или пароли, обратите внимание на грехи 8, 10, 11, 17 и 18.
   □ Если ваша программа работает в ОС Linux, Mac OS X или UNIX, следует прочесть о грехе 16.
   □ Если с вашим приложением будут работать неопытные пользователи, взгляните на описание греха 19.
   Мы полагаем, что эта книга важна, поскольку в работе над ней приняли участие трое наиболее авторитетных на сегодняшний день специалистов–практиков в сфере безопасности, а также потому, что она охватывает все распространенные языки и платформы для развертывания программ. Надеемся, что вы найдете здесь немало полезной информации.
   Майкл Ховард, Дэвид Лебланк, Джон Виега, июль 2005 г.

Грех 1.
Переполнение буфера

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

   Строго говоря, переполнение возникает, когда программа пытается писать в память, не принадлежащую выделенному буферу, но есть и ряд других ошибок, приводящих к тому же эффекту. Одна из наиболее интересных связана с форматной строкой, мы рассмотрим ее в описании греха 2. Еще одно проявление той же проблемы встречается, когда противнику разрешено писать в произвольную область памяти за пределами некоторого массива. И хотя формально это не есть классическое переполнение буфера, мы рассмотрим здесь и этот случай.
   Результатом переполнения буфера может стать что угодно – от краха программы до получения противником полного контроля над приложением, а если приложение запущено от имени пользователя с высоким уровнем доступа (root, Administrator или System), то и над всей операционной системой и другими пользователями. Если рассматриваемое приложение – это сетевая служба, то ошибка может привести к распространению червя. Первый получивший широкую известность Интернет–червь эксплуатировал ошибку в сервере finger, он так и назывался – «finger–червь Роберта Т. Морриса» (или просто «червь Морриса»). Казалось бы, что после того как в 1988 году Интернет был поставлен на колени, мы уже должны научиться избегать переполнения буфера, но и сейчас нередко появляются сообщения о такого рода ошибках в самых разных программах.
   Быть может, кто–то думает, что такие ошибки свойственны лишь небрежным и беззаботным программистам. Однако на самом деле эта проблема сложна, решения не всегда тривиальны, и всякий, кто достаточно часто программировал на С или С++, почти наверняка хоть раз да допускал нечто подобное. Автор этой главы, который учит других разработчиков, как писать безопасный код, сам однажды передал заказчику программу, в которой было переполнение на одну позицию (off–by–one overflow). Даже самые лучшие, самые внимательные программисты допускают ошибки, но при этом они знают, насколько важно тщательно тестировать программу, чтобы эти ошибки не остались незамеченными.

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

   Чаще всего переполнение буфера встречается в программах, написанных на С, недалеко от него отстает и С++. Совсем просто переполнить буфер в ассемблерной программе, поскольку тут нет вообще никаких предохранительных механизмов. По существу, С++ так же небезопасен, как и С, поскольку основан на этом языке. Но использование стандартной библиотеки шаблонов STL позволяет свести риск некорректной работы со строками к минимуму, а более строгий компилятор С++ помогает программисту избегать некоторых ошибок. Даже если ваша программа составлена на чистом С, мы все же рекомендуем использовать компилятор С++, чтобы выловить как можно больше ошибок.
   В языках более высокого уровня, появившихся позже, программист уже не имеет прямого доступа к памяти, хотя за это и приходится расплачиваться производительностью. В такие языки, как Java, С# и Visual Basic, уже встроены строковый тип, массивы с контролем выхода за границы и запрет на прямой доступ к памяти (в стандартном режиме). Кто–то может сказать, что в таких языках переполнение буфера невозможно, но правильнее было бы считать, что оно лишь гораздо менее вероятно. Ведь в большинстве своем эти языки реализованы на С или С++, а ошибка в реализации может стать причиной переполнения буфера. Еще один потенциальный источник проблемы заключается в том, что на какой–то стадии все эти высокоуровневые языки должны обращаться к операционной системе, а уж она–то почти наверняка написана на С или С++. Язык С# позволяет обойти стандартные механизмы .NET, объявив небезопасный участок с помощью ключевого слова unsafe. Да, это упрощает взаимодействие с операционной системой и библиотеками, написанными на C/C++, но одновременно открывает возможность допустить обычные для C/C++ ошибки. Даже если вы программируете преимущественно на языках высокого уровня, не отказывайтесь от тщательного контроля данных, передаваемых внешним библиотекам, если не хотите пасть жертвой содержащихся в них ошибок.
   Мы не станем приводить исчерпывающий список языков, подверженных ошибкам из–за переполнения буфера, скажем лишь, что к их числу относится большинство старых языков.

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

   Классическое проявление переполнения буфера – это затирание стека. В откомпилированной программе стек используется для хранения управляющей информации (например, аргументов). Здесь находится также адрес возврата из функции и, поскольку число регистров в процессорах семейства х86 невелико, сюда же перед входом в функцию помещаются регистры для временного хранения. Увы, в стеке же выделяется память для локальных переменных. Иногда их неправильно называют статически распределенными в противоположность динамической памяти, выделенной из кучи. Когда кто–то говорит о переполнении статического буфера, он чаще всего имеет в виду переполнение буфера в стеке. Суть проблемы в том, что если приложение пытается писать за границей массива, распределенного в стеке, то противник получает возможность изменить управляющую информацию. А это уже половина успеха, ведь цель противника – модифицировать управляющие данные по своему усмотрению.
   Возникает вопрос: почему мы продолжаем пользоваться столь очевидно опасной системой? Избежать проблемы, по крайней мере частично, можно было бы, перейдя на 64–разрядный процессор Intel Itanium, где адрес возврата хранится в регистре. Но тогда пришлось бы смириться с утратой обратной совместимости, хотя на момент работы над этой книгой представляется, что процессор х64 в конце концов станет популярным.
   Можно также спросить, почему мы не переходим на языки, осуществляющие строгий контроль массивов и запрещающие прямую работу с памятью. Дело в том, что для многих приложений производительность высокоуровневых языков недостаточно высока. Возможен компромисс: писать интерфейсные части программ, с которыми взаимодействуют пользователи, на языке высокого уровня, а основную часть кода – на низкоуровневом языке. Другое решение–в полной мере задействовать возможности С++ и пользоваться написанными для него библиотеками для работы со строками и контейнерными классами. Например, в Web–сервере Internet Information Server (IIS) 6.0 обработка всех входных данных переписана с использованием строковых классов; один отважный разработчик даже заявил, что даст отрезать себе мизинец, если в его коде отыщется хотя бы одно переполнение буфера. Пока что мизинец остался при нем, и за два года после выхода этого сервера не было опубликовано ни одного сообщения о проблемах с его безопасностью. Поскольку современные компиляторы умеют работать с шаблонными классами, на С++ теперь можно создавать очень эффективный код.
   Но довольно теории, рассмотрим пример.
   tinclude <stdio.h>
   void DontDoIhis (char* input)
   {
   char buf[16];
   strcpy(buf, input);
   printf("%s\n» , buf);
   }
   int main(int argc, char* argv[])
   {
   // мы не проверяем аргументы
   // а чего еще ожидать от программы, в которой используется
   // функция strcpy?
   DontDoThis(argv[l]);
   return 0;
   }
   Откомпилируем эту программу и посмотрим, что произойдет. Для демонстрации автор собрал приложение, включив отладочные символы и отключив контроль стека. Хороший компилятор предпочел бы встроить такую короткую функцию, как DontDoThis, особенно если она вызывается только один раз, поэтому оптимизация также была отключена. Вот как выглядит стек непосредственно перед вызовом strcpy:
   0x0012FEC0  с8  fe 12 00 .. <– адрес аргумента buf
   0x0012FEC4  с4 18 32 00 .2. <– адрес аргумента input
   0x0012FEC8  d0 fe 12 00 .. <– начало буфера buf
   0x0012FECC  04 80 40 00  .<<Unicode: 80>>@.
   0x0012FED0  el 02 3f 4f     .?0
   0x0012FED4  66 00 00 00    f… <– конец buf
   0x0012FED8  e4 fe 12 00     .. <– содержимое регистра EBP
   0x0012FEDC  3f 10 40 00  ?.@. <– адрес возврата
   0x0012FEE0  c4 18 32 00    .2. <– адрес аргумента DontDoThis
   0x0012FEE4  cO ff 12 00     ..
   0x0012FEE8  10 13 40 00  ..@. <– адрес, куда вернется main()
   Напомним, что стек растет сверху вниз (от старших адресов к младшим). Этот пример выполнялся на процессоре Intel со схемой адресации «little–endian». Это означает, что младший байт хранится в памяти первым, так что адрес возврата «3f104000» на самом деле означает 0x0040103f.
   А теперь посмотрим, что происходит, когда буфер buf переполняется. Сразу вслед за buf находится сохраненное значение регистра EBP (Extended Base Pointer – расширенный указатель на базу). ЕВР содержит указатель кадра стека; при ошибке на одну позицию его значение будет затерто. Если противник сможет получить контроль над областью памяти, начинающейся с адреса 0x0012fe00 (последний байт вследствие ошибки обнулен), то программа перейдет по этому адресу и выполнит помещенный туда противником код.
   Если не ограничиваться переполнением на один байт, то следующим будет затерт адрес возврата. Коль скоро противник сумеет получить контроль над этим значением и записать в буфер, адрес которого известен, достаточное число байтов ассемблерного кода, то мы будем иметь классический пример переполнения буфера, допускающего написание эксплойта. Отметим, что ассемблерный код (его обычно называют shell–кодом, потому что чаще всего задача эксплойта – получить доступ к оболочке (shell)) необязательно размещать именно в перезаписываемом буфере. Это типичный случай, но, вообще говоря, код можно внедрить в любое место вашей программы. Не обольщайтесь, полагая, что переполнению подвержен только очень небольшой участок.
   После того как адрес возврата переписан, в распоряжении противника оказываются аргументы атакуемой функции. Если функция перед возвратом каким–то образом модифицирует переданные ей аргументы, то открываются новые соблазнительные возможности. Это следует иметь в виду, оценивая эффективность таких средств борьбы с переполнением стека, как программа Stackguard Криспина Коуэена (Crispin Cowan), программа ProPolice, распространяемая IBM, и флаг /GS в компиляторе Microsoft.
   Как видите, мы предоставили противнику как минимум три возможности получить контроль над нашим приложением, а это ведь была очень простая функция. Если в стеке объявлен объект класса С++ с виртуальными функциями, то станет доступна таблица указателей на виртуальные функции; такая ошибка тоже легко эксплуатируется. Если одним из аргументов функции является указатель на функцию, что часто бывает в оконных системах (например, в X Window System или Microsoft Windows), то перезапись этого указателя перед использованием–очевидный способ получить контроль над приложением.
   Есть множество хитроумных способов перехватить управление программой, гораздо больше, чем способен измыслить наш слабый ум. Существует несоответствие между возможностями и ресурами, доступными разработчику и хакеру. В своей работе вы ограничены сроками, тогда как противник может тратить все свое свободное время на то, чтобы придумать, как заставить вашу программу делать то, что нужно ему. Ваша программа может защищать ресурс, достаточно ценный, чтобы потратить на ее взлом несколько месяцев. Хакер тратит массу времени на то, чтобы быть в курсе последних достижений в области взлома. К его услугам – такие ресурсы, как www.metasploit.com, позволяющие в несколько «кликов» создать shell–код, который будет делать что угодно и при этом включать только символы из ограниченного набора.
   Если вы попытаетесь выяснить, можно ли создать эксплойт для какой–то программы, то, скорее всего, полученный ответ будет неполным. В большинстве случае можно лишь доказать, что программа либо уязвима, либо вы недостаточно хитроумны (или потратили на поиск решения недостаточно времени), чтобы написать для нее эксплойт. Очень редко можно с уверенностью утверждать, что для некоторого переполнения эксплойт невозможен.
   Мораль, стало быть, в том, что самое правильное – исправить ошибки! Сколько раз случалось, что модификации с целью «повысить качество кода» заодно приводили и к исправлению ошибок, связанных с безопасностью. Автор как–то битых три часа убеждал команду разработчиков исправить некую ошибку. В переписке приняло участие восемь человек, и мы потратили 20 человекочасов (половина рабочей недели одного программиста), споря, нужно ли исправлять ошибку, поскольку разработчики жаждали получить доказательства того, что для нее можно написать эксплойт. Когда эксперты по безопасности доказали, что проблема действительно есть, для исправления потребовался час работы программиста и еще четыре часа на Тестирование. Сколько же времени ушло впустую!
   Заниматься анализом надо непосредственно перед поставкой программы. На завершающих стадиях разработки хорошо бы иметь обоснованное предположение о том, достаточно ли велика опасность написания эксплойта для ошибки, чтобы оправдать риск, связанный с переделками и, как следствие, нестабильностью продукта.
   Распространено заблуждение, будто переполнение буфера в куче не так опасно, как буфера в стеке. Это совершенно неправильно. Большинство реализаций кучи страдают тем же фундаментальным пороком, что и стек, – пользовательские и управляющие данные хранятся вместе. Часто можно заставить менеджер кучи поместить четыре указанных противником байта по выбранному им же адресу. Детали атаки на кучу довольно сложны. Недавно Matthew «shok» Conover и Oded Horovitz подготовили очень ясную презентацию на эту тему под названием «Re–liable Windows Heap Exploits» («Надежный эксплойт переполнения кучи в Win–dows»), которую можно найти на странице http://cansecwest.com/csw04/csw04–Oded+Connover.ppt. Даже если сам менеджер кучи не поддается взломщику, в соседних участках памяти могут находиться указатели на функции или на переменные, в которые записывается информация. Когда–то эксплуатация переполнений кучи считалась экзотическим и трудным делом, теперь же это одна из самых распространенных атакуемых ошибок.

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

   char buf[20] ;
   gets (buf) ;
   Не существует никакого способа вызвать gets для чтения из стандартного ввода без риска переполнить буфер. Используйте вместо этого fgets. Наверное, второй по популярности способ вызвать переполнение – это воспользоваться функцией strcpy (см. предыдущий пример). А вот как еще можно напроситься на неприятности:
   char buf[20];
   char prefix[] = "http://";
   strcpy(buf, prefix);
   strncat(buf, path, sizeof(buf));
   Что здесь не так? Проблема в неудачном интерфейсе функции strncat. Ей нужно указать, сколько символов свободно в буфере, а не общую длину буфера. Вот еще один распространенный код, приводящий к переполнению:
   char buf[MAX_PATH];
   sprintf(buf, "%s – %d\n", path, errno);
   Если не считать нескольких граничных случаев, функцию sprintf почти невозможно использовать безопасно. Для Microsoft Windows было выпущено извещение о критической ошибке, связанной с применением sprintf для отладочного протоколирования. Подробности см. в бюллетене MS04–011 (точная ссылка приведена в разделе «Другие ресурсы»).
   А вот еще пример:
   char buf [ 32] ;
   strncpy(buf, data, strlen(data));
   Что неверно? В последнем аргументе передана длина входного буфера, а не размер целевого буфера!
   Еще один способ столкнуться с проблемой – по ошибке считать байты вместо символов. Если вы работаете с кодировкой ASCII, то между ними нет разницы, но в кодировке Unicode один символ представляется двумя байтами. Вот пример:
   _snwprintf(wbuf, sizeof(wbuf), «%s\n», input);
   Следующее переполнение несколько интереснее:
   bool CopyStructs(InputFile* pInFile, unsigned long count)
   {
   unsigned long i;
   m_pStructs = new Structs[count];
   for(i = 0; i < count; i++)
   {
   if(!ReadFromFile(pInFile, &(m_pStructs[i])))
   break;
   }
   }
   Как здесь может возникнуть ошибка? Оператор new[] в языке С++ делает примерно то же, что такой код:
   ptr = malloc(sizeof(type) * count);
   Если значение count может поступать от пользователя, то нетрудно задать его так, чтобы при умножении возникло переполнение. Тогда будет выделен буфер гораздо меньшего размера, чем необходимо, и противник сможет его переполнить. В компиляторе С++, который будет поставляться в составе Microsoft Visual Studio 2005, реализована внутренняя проверка для недопущения такого рода ошибок. Аналогичная проблема может возникнуть во многих реализациях функции calloc, которая выполняет примерно такую же операцию. В этом и состоит коварство многих ошибок, связанных с переполнением целых чисел: опасно не само это переполнение, а вызванное им переполнение буфера. Но подробнее об этом мы расскажем в грехе 3.
   Вот как еще может возникать переполнение буфера:
   #define MAX_BUF 256
   void BadCode(char* input)
   {
   short len;
   char buf[MAX_BUF];
   len = strlen(input);
   // конечно, мы можем использовать strcpy безопасно
   if(len < MAX_BUF)
   strcpy(buf, input);
   }
   На первый взгляд, все хорошо, не так ли? Но на самом деле здесь ошибка на ошибке. Детали мы отложим до обсуждения переполнения целых числе в грехе 3, а пока заметим, что литералы всегда имеют тип signed int. Если длина входных данных (строка input) превышает 32К, то переменная len станет отрицательна, она будет расширена до типа int с сохранением знака и окажется меньше MAX_BUF, что приведет к переполнению. Еще одна ошибка возникнет, если длина строки превосходит 64К. В этом случае мы имеем ошибку усечения: len оказывается маленьким положительным числом. Основной способ исправления – объявлять переменные для хранения размеров как имеющие тип size_t. Еще одна скрытая проблема заключается в том, что входные данные могут не заканчиваться нулем. Вот как может выглядеть исправленный код:
   const size_t MAX_BUF = 256;
   void LessBadCode(char* input)
   {
   size_t len;
   char buf[MAX_BUF];
   len = strlen(input);
   // конечно, мы можем использовать strcpy безопасно
   if(len < MAX_BUF)
   strcpy(buf, input);
   }

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

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

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

   □ любые входные данные, будь то из сети, из файла или из командной строки;
   □ передача данных из вышеупомянутых источников входных данных во внутренние структуры;
   □ использование небезопасных функций работы со строками;
   □ использование арифметических операций для вычисления размера буфера или числа свободных байтов в нем.

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

   Обнаружить присутствие этого греха во время анализа кода может быть как совсем легко, так и очень сложно. Проще всего проанализировать все случаи употребления функций работы со строками. Надо иметь в виду, что вы можете найти много мест, где функции вызываются безопасно, но наш опыт показывает, что ошибки могут скрываться даже в правильных вызовах. Коэффициент регрессии, характерный для модификации кода с целью перехода исключительно на безопасные функции, обычно очень мал (от одной десятой до одной сотой величины, типичной для исправления ошибки), зато это позволит устранить возможность некоторых видов эксплойтов.
   Добиться этого можно, например, поручив выполнение задачи компилятору. Если вы исключите объявления функций strcpy, strcat, sprintf и им подобных из заголовочных файлов, то компилятор укажет все места в коде, где они встречаются. Но имейте в виду, что некоторые приложения полностью или частично переопределяют библиотеку времени исполнения для языка С.
   Сложнее отыскать переполнение кучи. Чтобы решить эту задачу, нужно помнить о возможности переполнения целых, о чем пойдет речь в грехе 3. Начать нужно с выявления всех мест, где производится выделение памяти, а затем проверить, с помощью каких арифметических операций вычислялся размер буфера.
   Наилучший подход состоит в том, чтобы проследить, как используются все поступающие от пользователя данные, начиная с точки входа в приложение и далее по всем функциям. Очень важно знать, что именно может контролировать противник.

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

   Одной из наиболее эффективных методик является рандомизированное тестирование (fuzz testing), когда на вход подаются полуслучайные данные. Попробуйте увеличить длину входных строк и понаблюдайте за поведением приложения. Обратите внимание на одну особенность: иногда множество неправильных значений входных данных довольно мало. Например, в одном месте программы проверяется, что длина входной строки должна быть меньше 260 байтов, а в другом месте выделяется буфер длиной 256. Если вы подадите на вход очень длинную строку, то она, конечно, будет отвергнута, но стоит попасть точно в неконтролируемый интервал–и можно писать эксплойт. Часто проблему можно найти, используя при тестировании степени двойки или степени двойки плюс–минус единица.
   Стоит также поискать те места, где пользователь может задать длину чего–либо. Измените длину так, чтобы она не соответствовала строке, и особое внимание обращайте на возможность переполнения целого: опасность представляют случаи, когда длина +1 = 0.
   Для рандомизированного тестирования нужно собрать специальную тестовую версию программы. В отладочные версии часто вставляют утверждения, которые изменяют поток выполнения программы и могут помешать обнаружить условия, при которых возможен экплойт. С другой стороны, современные компиляторы включают в отладочные версии хитроумный код для обнаружения порчи стека. В зависимости от используемого распределителя памяти и операционной системы вы можете также включить более строгую проверку целостности кучи.
   Если вы используете утверждения для контроля входных данных, то имеет смысл перейти от такой формы:
   assert(len < MAX_PATH);
   к следующей
   if(len >= MAX_PATH)
   {
   assert(false);
   return false;
   }
   Всегда следует тестировать программу с помощью какой–либо утилиты обнаружения ошибок при работе с памятью, например AppVerifier для Windows (см. ссылку в разделе «Другие ресурсы»). Это позволит выявить ошибки, связанные с небольшим или трудноуловимым переполнением буфера.

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

   Ниже приведены некоторые примеры переполнения буфера, взятые из базы данных типичных уязвимостей и брешей (CVE) на сайте http://cve.mitre.org. Интересно, что когда мы работали над этой книгой, в базе CVE по запросу «buffer overrim» находилось 1734 записи. Поиск по бюллетеням CERT, в которых документируются самые широко распространенные и серьезные уязвимости, по тому же запросу дал 107 документов.

CVE–1999–0042

   Цитата из описания ошибки: «Переполнение буфера в реализации серверов IMAP и POP Вашингтонского университета». Эта же ошибка очень подробно документирована в бюллетене CERT за номером СА–1997–09. Переполнение происходит во время аутентификации для доступа к серверам, реализующим протоколы Post Office Protocol (POP) и Internet Message Access Protocol (IMAP). Связанная с ней уязвимость состоит в том, что сервер электронной почты не мог отказаться от избыточных привилегий, поэтому эксплойт давал противнику права пользователя root. Это переполнение поставило под удар довольно много систем.
   Контрольная программа, созданная для поиска уязвимых версий этого сервера, обнаружила аналогичные дефекты в программе SLMail 2.5 производства Seattle Labs, о чем помещен отчет на странице www.winnetmag.com/Article/ArticleID/9223/ 9223.html.

CVE–2000–0389 – CVE–2000–0392

   Из CVE–2000–0390: «Переполнение буфера в функции krb425_conv_principal в Kerberos 5 позволяет удаленному противнику получить привилегии root».
   Из CVE–2000–0391: «Переполнение буфера в программе krshd, входящей в состав Kerberos 5, позволяет удаленному противнику получить привилегии root».
   Из CVE–2000–0392: «Переполнение буфера в программе krshd, входящей в состав Kerberos 5, позволяет удаленному противнику получить привилегии root».
   Эти ошибки в реализации системы Kerberos производства МТИ документированы в бюллетене CERT СА–2000–06 по адресу www.cert.org/advisories/CA–2000–06.html. Хотя исходные тексты открыты уже несколько лет и проблема коренится в использовании опасных функций работы со строками (strcat), отчет о ней появился только в 2000 году.

CVE–2002–0842, CVE–2003–0095, CAN–2003–0096

   Ошибка при работе с форматной строкой в одной модификации функции mod_dav, применяемой для протоколирования сообщений о плохом шлюзе (например, Oracle9i Application Server 9.0.2), позволяет удаленному противнику выполнить произвольный код, обратившись к URI, для которого сервер возвращает ответ «502 BadGateway». В результате функция dav_lookup_uri() в файле mod_dav.c возвращает спецификаторы форматной строки, которые потом используются при вызове ap_log_rerror().

   Из CVE–2003–0095:
   Переполнение буфера в программе ORACLE.EXE для Oracle Database Server 9i, 8i, 8.1.7 и 8.0.6 позволяет удаленному противнику выполнить произвольный код, задав при входе длинное имя пользователя. Ошибкой можно воспользоваться из клиентских приложений, самостоятельно выполняющих аутентификацию, что и продемонстрировано на примере LOADPSP.

   Из CAN–2003–0096:
   Многочисленные ошибки переполнения буфера в Oracle 9i Database Release 2, Release 1, 8i, 8.1.7 и 8.0.6 позволяют удаленному противнику выполнить произвольный код, (1) задав длинную строку преобразования в качестве аргумента функции TO_TIMESTAMP_TZ, (2) задав длинное название часового пояса в качестве аргумента функции TZ_OFFSET и (3) задав длинную строку в качестве аргумента DIRECTORY функции BFILENAME.

   Эти ошибки документированы в бюллетене CERT СА–2003–05 по адресу www.cert.org/advisories/CA–2003–05.html. Их – в числе прочих – обнаружил Дэвид Литчфилд со своими сотрудниками из компании Next Generation Security Software Ltd. Попутно отметим, что не стоит объявлять свои приложения «не–взламываемыми», если за дело берется г–н Литчфилд.

CAN–2003–0352

   Переполнение буфера в одном интерфейсе DCOM, используемом в системе RPC, применяемой в Microsoft Windows NT 4.0,2000, ХР и Server 2003, позволяет удаленному противнику выполнить произвольный код, сформировав некорректное сообщение. Этой ошибкой воспользовались черви Blaster/MSblast/LovSAN and Nachi/Welchia.

   Это переполнение интересно тем, что привело к широкому распространению двух весьма разрушительных червей, что повлекло за собой глобальные неприятности в сети Интернет. Переполнялся буфер в куче, доказательством возможности эксплуатации ошибки стало появление очень стабильного червя. Ко всему прочему еще был нарушен принцип предоставления наименьших привилегий, этот интерфейс не должен был быть доступен анонимным пользователям. Интересно еще отметить, что предпринятые в Windows 2003 контрмеры свели последствия атаки к отказу от обслуживания вместо эскалации привилегий.
   Подробнее об этой проблеме можно прочитать на страницах www.cert.org/ advisories/CA–2003–23.html и www.microsoft.com/technet/security/bulletin/MS03–039.asp.

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

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

Замена опасных функций работы со строками

   Как минимум вы должны заменить небезопасные функции типа strcpy, strcat и sprintf их аналогами со счетчиком. Замену можно выбрать несколькими способами. Имейте в виду, что интерфейс старых вариантов функций со счетчиком оставляет желать лучшего, а кроме того, для задания параметров часто приходится заниматься арифметическими вычислениями. В грехе 3 вы увидите, что компьютеры не так хорошо справляются с математикой, как могло бы показаться. Из новых разработок стоит отметить функцию strsafe, Safe CRT (библиотеку времени исполнения для С), которая войдет в состав Microsoft Visual Studio (и очень скоро станет частью стандарта ANSI C/C++), и функции strlcat/strlcpy для *nix. Обращайте внимание на то, как каждая функция обрабатывает конец строки и усечение. Некоторые функции гарантируют, что строка будет завершаться нулем, но большинство старых функций со счетчиком таких гарантий не дают. Опыт группы разработки Microsoft Office по замене небезопасных функций работы со строками в Office 2003 показал, что коэффициент регресии (число новых ошибок в расчете на одно исправление) очень мал, так что пусть страх внести новые ошибки вас не останавливает.

Следите за выделениями памяти

Проверьте циклы и доступ к массивам

Пользуйтесь строками в стиле С++, а не С

   Это эффективнее простой замены стандартных С–функций, но может повлечь за собой кардинальную переработку кода, особенно если программа собиралась не компилятором С++. Вы должны хорошо представлять себе характеристики производительности STL–контейнеров. Вполне возможно написать очень эффективный код с использованием STL, но, как всегда, нежелание читать руководство (RTFM – Read The Fine Manual) может привести к коду, далекому от оптимального. Наиболее типичное усовершенствование такого рода – переход к использованию шаблонных классов std::string или std::wstring.

Пользуйтесь STL–контейнерами вместо статических массивов

Пользуйтесь инструментами анализа

   На рынке появляются прекрасные инструменты для анализа кода на языках C/C++ на предмет нарушения безопасности, в том числе Coverity, PREfast и Kloc–work. В разделе «Другие ресурсы» приведены ссылки. В состав Visual Studio .NET 2005 войдет программа PREfast и еще один инструмент анализа кода под названием Source code Annotation Language (SAL – язык аннотирования исходного текста), позволяющий обнаружить такие дефекты, как переполнение буфера. Проще всего проиллюстрировать SAL на примере. В показанном ниже фрагменте (примитивном) вы знаете, как соотносятся аргументы data и count: длина data составляет count байтов. Но компилятору об этом ничего не известно, он видит только типы char * and a size_t.
   void *DoStuff(char *data, size_t count) {
   static char buf[32];
   return memcpy(buf, data, count);
   }
   На первый взгляд, код не содержит ничего плохого (если не считать, что мы возвращаем указатель на статический буфер, ну посмейтесь над нами). Однако если count больше 32, то возникает переполнение буфера. Если бы этот код был аннотирован с помощью SAL, то компилятор мог бы отловить эту ошибку:
   void *DoStuff(__in_ecount(count) char *data, size_t count) {
   static char buf[32];
   return memcpy(buf, data, count);
   }
   Объясняется это тем, что теперь компилятор и/или PREfast знают о тесной связи аргументов data и count.

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

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

Защита стека

   Защиту стека первым применил Криспин Коуэн в своей программе Stack–guard, затем она была независимо реализована Microsoft с помощью флага компилятора /GS. В самом простом варианте суть ее состоит в том, что в стек между локальными переменными и адресом возврата записывается некое значение. В более современных реализациях для повышения эффективности может также изменяться порядок переменных. Достоинство этого подхода в том, что он легко реализуется и почти не снижает производительности, а кроме того, облегчает отладку в случае порчи стека. Другой пример – это программа ProPolice, созданная компанией IBM как расширение компилятора Gnu Compiler Collection (GCC). Любой современный продукт должен включать в себя защиту стека.

Запрет исполнения в стеке и куче

   Эта контрмера существенно усложняет задачу хакера, но может сказаться на совместимости. Некоторые приложения считают допустимым компилировать и исполнять код на лету. Например, это относится к языкам Java и С#. Важно также отметить, что если противник сумеет заставить ваше приложение пасть жертвой атаки с возвратом в libc, когда для достижения неблаговидных целей выполняется вызов стандартной функции, то он сможет снять защиту со страницы памяти.
   К сожалению, современная аппаратура по большей части не поддерживает эту возможность, а имеющаяся подержка зависит от процессора, операционной системы и даже ее конкретной версии. Поэтому рассчитывать на наличие такой защиты в реальных условиях не стоит, но все равно следует протестировать свою программу и убедиться, что она будет работать, когда стек и куча защищены от исполнения. Для этого нужно запустить ее на том процессоре и операционной системе, которые поддерживают аппаратную защиту. Например, если целевой платформой является Windows ХР, то прогоните тесты на машине с процессором AMD Athlon 64 FX под управлением Windows ХР SP2. В Windows эта технология называется Data Execution Protection (DEP – защита от исполнения данных), а раньше носила имя No eXecute (NX).
   ОС Windows Server 2003 SP1 также поддерживает эту возможность, равно как подсистема РаХ для Linux и OpenBSD.

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

   □ «Defeating the Stack Based Buffer Overflow Prevention Mechanism of Microsoft Windows Server 2003» by David Litchfield: www.ngssoftware.com/papers/ defeating–w2k3–stack–protection.pdf
   □ «Non–stack Based Exploitation of Buffer Overrun Vulnerabilities on Windows NT/2000/ХР» by David Litchfield: www.ngssoftware.com/papers/non–stack–bo–windows.pdf
   □ «Blind Exploitation of Stack Overflow Vulnerabilities» by Peter Winter–Smith: www.ngssoftware.com/papers/NISR.BlindExploitation.pdf
   □ «Creating Arbitrary Shellcode In Unicode Expanded Strings: The 'Venetian' Exploit» by Chris Anley: www.ngssoftware.com/papers/unicodebo.pdf
   □ «Smashing The Stack For Fun And Profit» by Alephl (Elias Levy): www.insecure.org/stf/smashstack.txt
   □ «The Tao of Windows Buffer Overflow» by Dildog: www.cultdeadcow.com/ cDc_files/cDc–351/
   □ Microsoft Security Bulletin MS04–011/Security Update for Microsoft Windows (835732): www.microsoft.com/technet/security/Bulletin/MS04–011 .mspx
   □ Microsoft Application Compatibility Analyzer: www.microsoft.com/windows/ appcompatibility/analyzer.mspx
   □ Using the Strsafe.h Functions: http://msdn.microsoft.com/library/en–us/winui/ winui/windowsuserinterface/resources/strings/usingstrsafefunctions.asp
   □ More Secure Buffer Function Calls: AUTOMATICALLY!: http://blogs.msdn.com/michael_howard /archive/2005/2/3.aspx
   □ Repel Attacks on Your Code with the Visual Studio 2005 Safe С and С++ Libraries: http://msdn.microsoft.com/msdnmag/issues/05/05/SafeCand С / default.aspx
   □ «strlcpy and strlcat – Consistent, Safe, String Copy and Concatenation by Todd C. Miller and Theo de Raadt: www.usenix.org/events/usenix99/millert.html
   □ GCC extension for protecting applications from stack–smashing attacks: www.trl. ibm.com/projects/security/ssp/
   □ PaX: http://pax.grsecurity.net/
   □ OpenBSD Security: www.openbsd.org/security.html
   □ Static Source Code Analysis Tools for C: http://spinroot.com/static/

Резюме

   □ Тщательно проверяйте любой доступ к буферу за счет использования безопасных функций для работы со строками и областями памяти.
   □ Пользуйтесь встраиваемыми в компилятор средствами защиты, например флагом /GS и программой ProPolice.
   □ Применяйте механизмы защиты от переполнения буфера на уровне операционной системы, например DEP и РаХ.
   □ Уясните, какие данные контролирует противник, и обрабатывайте их безопасным образом.
   Не рекомендуется
   □ Не думайте, что компилятор и ОС все сделают за вас, это всего лишь дополнительные средства защиты.
   □ Не используйте небезопасные функции в новых программах.
   Стоит подумать
   □ Об установке последней версии компилятора C/C++, поскольку разработчики включают в генерируемый код новые механизмы защиты.
   □ О постепенном удалении небезопасных функций из старых программ.
   □ Об использовании строк и контейнерных классов из библиотеки С++ вместо применения низкоуровневых функций С для работы со строками.

Грех 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_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_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) хранится в переменных окружения и определяет, в частности, каталог, где находятся файлы на нужном языке. Иногда противник может даже заставить приложение искать файлы в произвольных каталогах.

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

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

   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

   Полный текст оригинального бюллетеня можно найти по адресу 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);

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

   #include <iostream>
   //...
   std::cout << user_input
   //...

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

   □ 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

Резюме

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

Грех 3.
Переполнение целых чисел

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

   Суть проблемы в том, что какой бы формат для представления чисел ни выбрать, существуют операции, которые при выполнении компьютером дают не тот же результат, что при вычислениях на бумаге. Существуют, правда, исключения–в некоторых языках реализованы целочисленные типы переменной длины, но это встречается редко и обходится не даром.
   В других языках, например в Ada реализованы целочисленные типы с проверкой диапазона, и если ими пользоваться всюду, то вероятность ошибки снижается. Вот пример:
   type Age is new Integer range 0..200;
   Нюансы разнятся в языках. В С и С++ применяются настоящие целые типы. В современных версиях Visual Basic все числа представляются типом Variant, где хранятся как числа с плавающей точкой; если объявить переменную типа int и записать в нее результат деления 5 на 4, то получится не 1, а 1.25. У Perl свой подход. В С# проблема усугубляется тем, что в ряде случаев этот язык настаивает на использовании целых со знаком, но затем спохватывается и улучшает ситуацию за счет использования ключевого слова «checked» (подробности в разделе «Греховный С#»).

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

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

   Результатом переполнения целого может быть все, что угодно: логическая ошибка, аварийный останов программы, эскалация привилегий или исполнение произвольного кода. Большинство современных атак направлены на то, чтобы заставить приложение допустить ошибку при выделении памяти, после чего противник сможет воспользоваться переполнением кучи. Если вы работаете на языках, отличных от C/C++, то, возможно, считаете себя защищенным от переполнений целого. Заблуждение! Логический просчет, возникший в результате усечения целого, стал причиной ошибки в сетевой файловой системе NFS (Network File System), из–за которого любой пользователь мог получить доступ к файлам от имени root.

Греховность С и С++

   Даже если вы не пишете на С и С++, полезно взглянуть, какие грязные шутки могут сыграть с вами эти языки. Будучи языком сравнительно низкого уровня, С приносит безопасность в жертву быстродействию и готов преподнести целый ряд сюрпризов при работе с целыми числами. Большинство других языков на такое не способны, а некоторые, в частности С#, проделывают небезопасные вещи, только если им явно разрешить. Если вы понимаете, что можно делать с целыми в C/C++, то, наверное, отдаете себе отчет в том, что делаете нечто потенциально опасное, и не удивляетесь, почему написанное на Visual Basic .NET–приложение досаждает всякими исключениями. Даже если вы программируете только на языке высокого уровня, то все равно приходится обращаться к системным вызовам и внешним объектам, написанным на С или С++. Поэтому ваши ошибки могут проявиться как ошибки в вызываемых программах.

Операции приведения

   const long MAX_LEN = 0x7fff;
   short len = strlen(input);
   if (len < MAX_LEN)
   // что-то сделать
   Если даже не обращать внимания на усечение, то вот вопрос: в каком порядке производятся приведения типов при сравнении len и MAX_LEN? Стандарт языка гласит, что повышающее приведение следует выполнять перед сравнением; следовательно, len будет преобразовано из 16–разрядного целого со знаком в 32–разрядное целое со знаком. Это простое приведение, так как оба типа знаковые. Чтобы сохранить значение числа, оно расширяется с сохранением знака до более широкого типа. В данном случае мог бы получиться такой результат:
   len = 0x100;
   (long)len = 0x00000100;
   ИЛИ
   len = 0xffff;
   (long)len = 0xfffffffff;
   Поэтому если противник сумеет добиться того, чтобы len превысило 32К, то len станет отрицательным и останется таковым после расширения до 32 битов. Следовательно, после сравнения с MAX_LEN программа пойдет по неверному пути.
   Вот как формулируются правила преобразования в С и С++:
   Целое со знаком в более широкое целое со знаком. Меньшее значение расширяется со знаком, например приведение (char)0x7f к int дает 0x0000007f, но (char)0x80 становится равно 0xffffff80.
   Целое со знаком в целое без знака того же размера. Комбинация битов сохраняется, значение может измениться или остаться неизменным. Так, (char)0xff (-1) после приведения к типу unsigned char становится равно 0xff, но ясно, что–1 и 255 – это не одно и то же.
   Целое со знаком в более широкое целое без знака. Здесь сочетаются два предыдущих правила. Сначала производится расширение со знаком до знакового типа нужного размера, а затем приведение с сохранением комбинации битов. Это означает, что положительные числа ведут себя ожидаемым образом, а отрицательные могут дать неожиданный результат. Например, (char) -1 (0xff) после приведения к типу unsigned long становится равно 4 294 967 295 (0xffffffff).
   Целое без знака в более широкое целое без знака. Это простейший случай: новое число дополняется нулями, чего вы обычно и ожидаете. Следовательно, (unsigned char)0xff после приведения к типу unsigned long становится равно
   0x000000ff.
   Целое без знака в целое со знаком того же размера. Так же как при приведении целого со знаком к целому без знака, комбинация битов сохраняется, а значение может измениться в зависимости от того, был ли старший (знаковый) бит равен 1 или 0.
   Целое без знака в более широкое целое со знаком. Так же как при приведении целого без знака к более широкому целому без знака, значение сначала дополняется нулями до нужного беззнакового типа, а затем приводится к знаковому типу. Значение не изменяется, так что никаких сюрпризов в этом случае не бывает.
   Понижающее приведение. Если в исходном числе хотя бы один из старших битов был отличен от нуля, то мы имеем усечение, что вполне может привести к печальным последствиям. Возможно, что число без знака станет отрицательным или произойдет потеря информации. Если речь не идет о битовых масках, всегда проверяйте, не было ли усечения.

Преобразования при вызове операторов

   template <typename T>
   void WhatIsIt(T value)
   {
   if((T)-1 < 0)
   printf("Со знаком");
   else
   printf("Без знака");
   printf(" – %d бит\n", sizeof(T)*8);
   }
   Для простоты оставим в стороне случай смешанных операций над целыми и числами с плавающей точкой. Правила формулируются так:
   □ если хотя бы один операнд имеет тип unsigned long, то оба операнда приводятся к типу unsigned long. Строго говоря, long и int – это два разных типа, но на современных машинах тот и другой имеют длину 32 бита, поэтому компилятор считает их эквивалентными;
   □ во всех остальных случаях, когда длина операнда составляет 32 бита или меньше, операнды расширяются до типа int, и результатом является значение типа int.
   Как правило, ничего неожиданного при этом не происходит, и неявное приведение в результате применения операторов может даже помочь избежать некоторых переполнений. Но бывают и сюрпризы. Во–первых, в системах, где имеется тип 64–разрядного целого, было бы логично ожидать, что коль скоро unsigned short и signed short приводятся к int, а операторное приведение не нарушает корректность результата (по крайней мере, если вы потом не выполняете понижающего приведения до 16 битов), то unsigned int и signed int будут приводиться к 64–разрядному типу (_int64). Если вы думаете, что все так и работает, то вынуждены вас разочаровать – по крайней мере, до той поры, когда стандарт C/C++ не станет трактовать 64–разрядные целые так же, как остальные.
   Вторая неожиданность заключается в том, что поведение изменяется еще и в зависимости от оператора. Все арифметические операторы (+, – ,*,/,%) подчиняются приведенным выше правилам. Но им же подчиняются и поразрядные бинарные операторы (&,|,А); поэтому (unsigned short) | (unsigned short) дает int! Те же правила в языке С распространяются на булевские операторы (&&,|| и !), тогда как в С++ возвращается значение встроенного типа bool. Дополнительную путаницу вносит тот факт, что одни унарные операторы модифицируют тип, а другие–нет. Оператор дополнения до единицы (~) изменяет тип результата, поэтому -((unsigned short)0) дает int, тогда как операторы префиксного и постфиксного инкремента и декремента (++, – ) типа не меняют.
   Один программист с многолетним стажем работы предложил следующий код для проверки того, возникнет ли переполнение при сложении двух 16–разрядных целых без знака:
   bool IsValidAddition(unsigned short x, unsigned short y)
   {
   if(x + y < x)
   return false;
   return true;
   }
   Вроде бы должно работать. Если результат сложения двух положительных чисел оказывается меньше какого–то слагаемого, очевидно, что–то не в порядке. Точно такой же код должен работать и для чисел типа unsigned long. Увы, программист не учел, что компилятор оптимизирует всю функцию так, что она будет возвращать true.
   Вспомним из предыдущего обсуждения, какой тип имеет результат операции unsigned short + unsigned short. Это int. Каковы бы ни были значения целых без знака, результат никогда не может переполнить тип int, поэтому сложение всегда выполняется корректно. Далее int сравнивается с unsigned short. Значение х приводится к типу int и, стало быть, никогда не будет больше х + у. Чтобы исправить код, нужно лишь привести результат обратно к unsigned short:
   if((unsigned short)(x + y) < x)
   Этот код был показан хакеру, специализирующемуся на поиске ошибок, связанных с переполнением целых, и он тоже не заметил ошибки, так что наш опытный программист не одинок!

Арифметические операции

   Не упускайте из виду последствия приведений типов и применения операторов, размышляя над корректностью той или иной строки кода, – в результате неявных приведений может возникнуть переполнение. Вообще говоря, нужно рассмотреть четыре основных случая: операции только над знаковыми типами, только над беззнаковыми типами и смешанные операции. Проще всего операции над беззнаковыми типами одного размера, затем идут операции над знаковыми типами, а когда встречаются смешанные операции, нужно принять во внимание правила приведения. В следующих разделах мы обсудим возможные ошибки и способы их исправления для каждого случая.
   Сложение и вычитание. Очевидная проблема при выполнении этих операций – возможность перехода через верхнюю и нижнюю границы объявленного типа. Например, если речь идет о 8–разрядных числах без знака, то 255 + 1 = 0. Или: 2 – 3 = 255. В случае 8–разрядных чисел со знаком 127 + 1 = -128. Менее очевидная ошибка возникает, когда числа со знаком используются для представления размеров. Если кто–то подсунет вам число–20, вы прибавите его к 50, получите 30, выделите буфер длиной 30 байтов, а затем попытаетесь скопировать в него 50 байтов. Все, вы стали жертвой хакера. Помните, особенно при программировании на языке, где переполнить целое трудно или невозможно, – что вычитание из положительного числа, в результате которого получается число, меньшее исходного, – это допустимая операция, и никакого исключения вследствие переполнения не будет, но поток исполнения программы может отличаться от ожидаемого. Если вы предварительно не проверили, что входные данные попадают в положенный диапазон, и не уверены на сто процентов, что переполнение невозможно, контролируйте каждую операцию.
   Умножение, деление и вычисление остатка. Умножение чисел без знака не вызывает трудностей: любая операция, где а * b > MAX_INT, дает некорректный результат. Правильный, но не очень эффективный способ контроля заключается в том, чтобы проверить, что b > MAX_INT/a. Эффективнее сохранить результат в следующем по ширине целочисленном типе (если такой существует) и посмотреть, не возникло ли переполнение. Для небольших целых чисел это сделает за вас компилятор. Напомним, что short * short дает int. При умножении чисел со знаком нужно еще проверить, не оказался ли результат отрицательным вследствие переполнения.
   Ну а может ли вызвать проблемы операция деления, помимо, конечно, деления на нуль? Рассмотрим 8–разрядное целое со знаком: MIN_INT = -128. Разделим его на–1. Это то же самое, что написать -(-128). Операцию дополнения можно записать в виде ~х+1. Дополнение–128 (0x80) до единицы равно 127 или 0x7f. Прибавим 1 и получим 0x80! Итак, минус–128 снова равно–128! То же верно для деления на–1 минимального целого любого знакового типа. Если вы еще не уверены, что контролировать операции над числами без знака проще, надеемся, что этот пример вас убедил.
   Оператор деления по модулю возвращает остаток от деления одного числа на другое, поэтому мы никогда не получим результат, который по абсолютной величине больше числителя. Ну и как тут может возникнуть переполнение? Переполнения как такового и не возникает, но результат может оказаться неожиданным из–за правил приведения. Рассмотрим 32–разрядное целое без знака, равное MAX_INT, то есть 0xffffffff, и 8–разрядное целое со знаком, равное–1. Остаток от деления–1 на 4 294 967 295 равен 1, не так ли? Не торопитесь. Компилятор желает работать с похожими числами, поэтому приведет–1 к типу unsigned int. Напомним, как это происходит. Сначала число расширяется со знаком до 32 битов, поэтому из 0xff получится 0xffffffff. Затем (int)(0xffffffff) преобразуется в (unsigned int)(0xffffffff). Как видите, остаток от деления–1 на 4 млрд равен нулю, по крайней мере, на нашем компьютере! Аналогичная проблема возникает при смешанной операции над любыми 32–или 64–разрядными целыми без знака и отрицательными целыми со знаком, причем это относится также и к делению, так что–1/4 294 967 295 равно 1, что весьма странно, ведь вы ожидали получить 0.

Операции сравнения

   Операции сравнения могут преподнести и другую неожиданность – когда максимальный размер сравнивается с числом со знаком. Противник может найти способ сделать это число отрицательным, а тогда оно заведомо будет меньше верхнего предела. Либо пользуйтесь числами без знака (это рекомендуемый способ), либо делайте две проверки: сначала проверяйте, что число больше или равно нулю, а потом – что оно меньше верхнего предела.

Поразрядные операции

   int flags = 0x7f;
   char LowByte = 0x80;
   if ((char)flags ^ LowByte == 0xff)
   return ItWorked;
   Вам кажется, что результатом операции должно быть 0xff, именно с этим значением вы и сравниваете, но настырный компилятор решает все сделать по–своему и приводит оба операнда к типу int. Вспомните, мы же говорили, что даже для поразрядных операций выполняется приведение к int, если операнды имеют более узкий тип. Поэтому flags расширяется до 0x0000007f, и тут ничего плохого нет, зато LowByte расширяется до 0xffffff80, в результате операции мы получаем 0xffffffff!

Греховность С#

   byte a, b;
   a = 255;
   b = 1;
   byte c = (b + a);
   error CS0029: Cannot implicitly convert type 'int' to 'byte'
   (ошибка CS0029: Не могу неявно преобразовать тип 'int' в 'byte')
   Если вы понимаете, о чем говорит это сообщение, то подумайте о возможных последствиях такого способа исправления ошибки:
   byte с = (byte) (Ь + а) ;
   Безопаснее воспользоваться классом Convert:
   byte d = Convert.ToByte(a + b);
   Поняв, что пытается сказать компилятор, вы хотя бы задумаетесь, есть ли в вашем коде реальная проблема. К сожалению, возможности компилятора ограничены. Если бы в предыдущем примере вы избавились от ошибки, объявив a, b и с как целые со знаком, то появилась бы возможность переполнения, а компилятор ничего не сказал бы.
   Еще одна приятная особенность С# состоит в том, что он по мере необходимости пользуется 64–разрядными целыми числами. Например, следующий код дал бы неверный результат на С, но правильно работает на С#:
   int i = -1;
   uint j = 0xffffffff; // наибольшее положительное 32-разрядное целое
   if(i == j)
   Console.WriteLine("Отлично!");
   Причина в том, что С# приведет операнды к типу long (64–разрядное целое со знаком), который позволяет точно сохранить оба числа. Если вы решите пойти дальше и проделать то же самое с числами типа long и ulong (в С# оба занимают 64 разряда), то компилятор сообщит, что необходимо явно преобразовать их к одному типу. По мнению авторов, стандарт C/C++ следует уточнить: если компилятор поддерживает операции над 64–разрядными значениями, то он должен в этом отношении вести себя так же, как С#.

Ключевые слова checked и unchecked

   byte a = 1;
   byte b = 255;
   checked
   {
   byte c = (byte)(a + b);
   byte d = Convert.ToByte(a + b);
   Console.Write("{0} {1}\n", b+1, c);
   }
   В данном примере приведение а + b от int к byte возбуждает исключение. В следующей строке, где вызывается Convert.ToByte, исключение возникло бы и без ключевого слова checked, но его наличие приводит к возбуждению исключения еще и при вычислении аргументов метода Console.Write(). Поскольку иногда переполнение целого допускается намеренно, то имеется также ключевое слово unchecked, отключающее контроль на переполнение.
   Слова checked и unchecked можно также использовать для включения или отключения контроля в одном выражении:
   checked (с = (byte) (Ь + а));
   И наконец, включить контроль можно с помощью флага /checked компилятора. Если этот флаг присутствует, то нужно явно помечать словом unchecked участки кода или отдельные предложения, в которых переполнение допустимо.

Греховность Visual Basic и Visual Basic .NET

   Вообще говоря, и Visual Basic 6.0, и Visual Basic .NET не подвержены угрозе исполнения произвольного кода из–за переполнения целых чисел. В Visual Basic 6.0 генерируется ошибка, если при выполнении какого–либо оператора или функции преобразования, например CInt(), возникает переполнение. В Visual Basic .NET в этом случае возбуждается исключение типа System.OverflowException. Как показано в табл. 3.1, программа на Visual Basic .NET имеет доступ ко всем целочисленным типам, определенным в каркасе .NET Framework.

   Таблица 3.1. Целочисленные типы, поддерживаемые Visual Basic 6.0 и Visual Basic .NET

   Хотя операции в самом языке Visual Basic, может быть, и неуязвимы для переполнения целого, но потенциальная проблема состоит в том, что вызовы Win32 API обычно принимают в качестве параметров 32–разрядные целые без знака (DWORD). Если ваша программа передает системному вызову 32–разрядное целое со знаком, то в ответ может получить отрицательное число. Аналогично вполне допустимо выполнить такую операцию, как 2 – 8046, над числами со знаком, но что, если указать в качестве одного из операндов такое число без знака, чтобы возникло переполнение? Если системный вызов возвращает некоторое значение, а потом вы выполняете манипуляции над этим значением и величиной, прямо или косвенно (после тех или иных вычислений) полученной от пользователя, и напоследок обращаетесь к другим системным вызовам, то можете оказаться в угрожаемой ситуации. Переходы от знаковых чисел к беззнаковым и обратно чреваты опасностью. Даже если переполнение целого и не приведет к исполнению произвольного кода, необработанные исключения станут причиной отказа от обслуживания. Неработающее приложение не приносит доходов заказчику.

Греховность Java


   При выполнении встроенных операций над целыми типами переполнение или потеря значимости не индицируются. Единственные арифметические операторы, которые могут возбудить исключение (§11), – это оператор целочисленного деления / (§15.17.2) и оператор вычисления остатка % (§15.17.3). Они возбуждают исключение ArithmeticException, если правый операнд равен нулю.
   В отличие от Visual Basic, Java поддерживает лишь подмножество всего диапазона целочисленных типов. Хотя 64–разрядные целые поддерживаются, но единственным беззнаковым типом является char, и он представляется в виде 16–разрядного значения без знака.
   Поскольку в Java есть только знаковые типы, проверка переполнения становится непростым делом, и по сравнению с C/C++ удается лишь избежать затруднений, связанных со смешанными операциями над знаковыми и беззнаковыми величинами.

Греховность Perl

   $h = 4294967295;
   $i = 0xffffffff;
   $k = 0x80000000;
   print "$h = 4294967295 – $h + 1 = ".($h + 1)."\n";
   print "$i = 0xffffffff – $i + 1 = ".($i + 1)."\n";
   printf("\nИспользуется printf со спецификатором %%d\n");
   printf("$i = %d, $i + 1 = %d\n\n", $i, $i + 1);
   printf("\nТестируется граничный случай деления\n");
   printf("0x80000000/-1 = %d\n", $k/-1);
   print "0x80000000/-1 = ".($k/-1)."\n";
   В результате печатается следующее:
   [e:\projects_sins\perl foo.pl
   4294967295 = 4294967295 – 4294967295 + 1 = 4294967296
   4294967295 = 0xffffffff – 4294967295 + 1 = 4294967296
   Используется printf со спецификатором %d
   $i = -1, $i + 1 = -1
   Тестируется граничный случай деления
   0x80000000/-1 = -2147483648
   0x80000000/-1 = -2147483648
   На первый взгляд, результат выглядит странно, особенно когда используется printf с форматной строкой (в отличие от обычной функции print). Первым делом в глаза бросается то, что мы можем присвоить переменной максимально возможное значение без знака, но после прибавления к нему 1 она либо увеличивается на единицу, либо – если печатать с помощью %d – вообще не изменяется. Загвоздка в том, что на самом деле вы оперируете числами с плавающей точкой, а спецификатор %d заставляет Perl преобразовать double в int. В действительности никакого переполнения нет, но при печати результатов создается впечатление, будто оно произошло.
   В силу особенностей работы с числами в Perl мы рекомендуем быть очень осторожными при написании на этом языке приложений, в которых много математических операций. Если вы не разбираетесь досконально в арифметике с плавающей точкой, то можете столкнуться с весьма поучительными сюрпризами. Другие языки высокого уровня, например Visual Basic, тоже иногда производят преобразования в числа с плавающей точкой. Следующий код иллюстрирует, что в таком случае происходит:
   print (5/4)."\n";
   1.25
   Для большинства обычных приложений Perl ведет себя предсказуемо и работает великолепно. Но не забывайте, что вы имеете дело не с целыми числами, а с числами с плавающей точкой, – а это «две большие разницы».

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

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

   При написании программ на языках C/C++ надо обращать самое пристальное внимание на возможность переполнения целого числа. Теперь, когда многие разработчики осознали важность проверок размеров при прямых манипуляциях с памятью, атаки направлены нате арифметические операции, с помощью которых эти проверки выполняются. Следующими на очереди стоят С# и Java. Прямые манипуляции с памятью в этих языках запрещены, но тем не менее можно допустить почти все ошибки, которые характерны для С и С++.
   Ко всем языкам относится следующее замечание: проверяйте входные данные прежде, чем каким–либо образом их использовать! В Web–серверах Microsoft IIS 4.0 и 5.0 очень серьезная ошибка имела место из–за того, что программист сначала прибавил к переменной 1, а затем проверил, не оказался ли размер слишком большим. При тех типах, что он использовал, 64К–1 + 1 оказалось равно нулю! На соответствующее извещение есть ссылка в разделе «Другие ресурсы».

C/C++

   THING* AllocThings(int a, int b, int c, int d)
   {
   int bufsize;
   THING* ptr;
   bufsize = IntegerOverflowsRUs(a, b, c, d);
   ptr = (THING*)malloc(bufsize);
   return ptr;
   }
   Ошибка скрывалась в функции, вычисляющей размер буфера, к тому же найти ее мешали загадочные, ничего не говорящие читателю имена переменных (и литералов, которые представлены типом signed int). Если у вас есть время на доскональный анализ, проследите порядок вызова всех ваших функций вплоть до обращений к низкоуровневым библиотечным функциям или системным вызовам. И напоследок выясните, откуда поступают данные. Можете ли вы утверждать, что аргументы функций не подвергались манипуляциям? Кто контролирует аргументы: вы или потенциальный противник?
   По мнению автора языка Perl, величайшим достоинством программиста является лень! Так давайте пойдем простым путем – заставим потрудиться компилятор. Включите уровень диагностики /W4 (для Visual С++) или–Wall либо–Wsign–compare (для gcc) – и вы увидите, как много в вашей программе мест, где возможны проблемы с целыми числами. Обращайте внимание на все предупреждения, касающиеся целых чисел, особенно на те, в которых говорится о сравнении знаковых и беззнаковых величин, а также об усечении.
   В Visual С++ самыми важными с этой точки зрения являются предупреждения С4018, С4389иС4244.
   В gcc ищите предупреждения «warning: comparison between signed and unsigned integer expressions».
   Относитесь с подозрением к директивам #pragma отключающим предупреждения, например:
   #pragma warning(disable : 4244)
   Во вторую очередь следует искать места, где вы пытаетесь защититься от переполнения буфера (в стеке или в куче) путем проверки выхода за границы. Убедитесь, что все арифметические вычисления корректны. В следующем примере показано, как может возникнуть ошибка:
   int ConcatBuffers(char *buf1, char *buf2,
   size_t len1, size_t len2) {
   char buf[0xFF];
   if(len1 + len2) > 0xFF) return -1;
   memcpy(buf, buf1, len1);
   memcpy(buf + len1, buf2, len2);
   // сделать что-то с buf
   return 0;
   }
   Здесь проверяется, что суммарный размер двух входных буферов не превышает размера выходного буфера. Но если lenl равно 0x103, а 1еп2 равно 0xfffffffc, то сумма переполняет 32–разрядный регистр процессора и оказывается равной 255 (0xff), так что проверка успешно проходит. В результате memcpy попытается записать примерно 4 Гб в буфер размером всего 255 байтов!
   Не вздумайте подавлять эти надоедливые предупреждения путем приведения типов. Теперь вы знаете, насколько это рискованно и как внимательно нужно все проверять. Найдите все приведения и убедитесь, что они безопасны. О приведениях и преобразованиях в языках С и С++ см. раздел «Операции приведения» выше.
   Вот еще один пример:
   int read(char* buf, size_t count) {
   // Сделать что-то с памятью
   }
   ...
   while (true) {
   BYTE buf[1024];
   int skip = count – cbBytesRead;
   if (skip > sizeof(buf))
   skip = sizeof(buf);
   if (read(buf, skip))
   cbBytesRead += skip;
   else
   break;
   ...
   В этом фрагменте значение skip сравнивается с 1024, и если оно меньше, то в буфер buf копируется skip байтов. Проблема в том, что если skip оказывается отрицательным (скажем, -2), то оно будет заведомо меньше 1024, так что функция read попытается скопировать–2 байта, а это значение, будучи представлено как целое без знака (тип size_t), равно без малого 4 Гб. Итак, read() копирует 4 Гб в буфер размером 1 Кб. Печально!
   Еще один источник неприятностей – это оператор new в языке С++. Он сопряжен с неявным умножением:
   Foo *p = new Foo(N);
   Если N контролируется противником, то возможно переполнение внутри operator new при вычислении выражения N * sizeof(Foo);

С#

   Хотя сам язык С# и не допускает прямых обращений к памяти, но иногда программа обращается к системным вызовам, помещенным в блок, помеченный ключевым словом unsafe (при этом ее еще надо компилировать с флагом /unsafe). Любые вычисления, результат которых передается системному вызову, нужно контролировать. Для этого полезно применить ключевое слово checked или – еще лучше – соответствующий флаг компилятора. Включите его и следите, не произойдет ли исключения. Напротив, ключевое слово unchecked используйте изредка, предварительно обдумав все последствия.

Java

Visual Basic и Visual Basic .NET

   Visual Basic ухитрился превратить проблему переполнения целого в проблему отказа от обслуживания – примерно такую же ситуацию мы имеем при использовании ключевого слова checked в С#. Подозрение должны вызывать те места, где программист применяет механизм перехвата для игнорирования ошибок при работе с целыми. Убедитесь, что ошибки обрабатываются правильно. Следующее предложение в программе на Visual Basic (не Visual Basic .NET) свидетельствует о лености программиста, не пожелавшего обрабатывать ошибки, возникающие во время работы программы. Нехорошо это.
   On Error Continue

Perl

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

   Если программа допускает прямой ввод числовых данных, например в структурированный документ, попробуйте очень большие числа и обращайте особое внимание на граничные случаи.

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

   Поиск по запросу «integer overflow» в базе данных уязвимостей на сайте Security Focus дает больше 50 документов, а в базе CVE – 65 документов. Вот лишь несколько из них.

Ошибка в интерпретаторе Windows Script позволяет выполнить произвольный код


   Переполнение целого в функции JsArrayFimctionHeapSort, используемой в Windows Script Engine для JScript (JScript.dll), в различных операционных системах Windows позволяет противнику выполнить произвольный код путем подготовки Web–страницы или электронного письма в формате HTML, в котором используется большой индекс массива. При этом возникает переполнение размещенного в куче буфера и открывается возможность для атаки.

   Интересная особенность этой ошибки в том, что переполнение возникает в языке сценариев, не допускающем прямого обращения к памяти. Выпущенный по этому поводу бюллетень Microsoft можно найти на странице www.microsoft.com/ technet/security/bulletin/MS03–008.mspx.

Переполнение целого в конструкторе объекта SOAPParameter


   Zen Parse сообщил о некорректном контроле входных данных в конструкторе объекта SOAPParameter, что приводит к переполнению целого с последующей порчей кучи. Можно написать вредоносную программу на языке JavaScript, которая воспользуется этой ошибкой для выполнения произвольного кода.

   В том же отчете читаем далее:

   Во время аудита исходных текстов Крис Эванс (Chris Evans) обнаружил переполнение буфера и переполнение целого в библиотеке libpng, используемой в браузере Mozilla. Противник может создать специальный PNG–файл, при просмотре которого в этом браузере либо произойдет аварийный останов, либо будет выполнен произвольный код.

Переполнение кучи в HTR–документе, передаваемом поблочно, может скомпрометировать Web–сервер

   Вскоре после того, как об этой ошибке стало известно (в июне 2002 года), последовали многочисленные атаки на уязвимые серверы IIS. Подробности можно найти на странице www.microsoft.com/technet/security/bulletin/MS02–028.mspx, но суть проблемы в том, что обработчик HTR–документов принимал от пользователя данные длиной 64К – 1, прибавлял 1 (нам ведь нужно место для завершающего нуля), после чего запрашивал буфер длиной 0 байтов. Неизвестно, то ли Билл Гейтс сказал, что 64К хватит любому, то ли это интернетовская легенда, но любой хакер сможет уместить в 64К такой shell–код, что мало не покажется!

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

   По–настоящему избавиться от ошибок переполнения целого можно, только если вы хорошо понимаете суть проблемы. Но все же опишем несколько шагов, которые помогут не допустить такой ошибки. Прежде всего пользуйтесь всюду, где возможно, числами без знака. В стандарте C/C++ описан тип size_t для представления размеров, и разумные программисты им пользуются. Контролировать беззнаковые целые гораздо проще, чем знаковые. Ну нет же смысла применять число со знаком для задания размера выделяемой памяти!
   Избегайте «хитроумного» кода – контроль целых должен быть прост и понятен. Вот пример чересчур заумного кода для контроля переполнения при сложении:
   int a, b, c;
   c = a + b;
   if(a ^ b ^ c < 0)
   return BAD_INPUT;
   В этом фрагменте масса проблем. Многим из нас потребуется несколько минут на то, чтобы понять, что же автор хотел сделать. А кроме того, код дает ложные срабатывания – как позитивные, так и негативные, то есть работает не всегда. Вот другой пример проверки, дающей правильный результат не во всех случаях:
   int a, b, c;
   c = a * b;
   if(c < 0)
   return BAD_INPUT;
   Даже если на входе допустимы только положительные числа, этот код все равно пропускает некоторые переполнения. Возьмем, к примеру, выражение (2 А 30 + + 1) * 8, то есть 2 А 33 + 8. После отбрасывания битов, вышедших за пределы 32 разрядов, получается 8. Это число положительно, а ошибка тем не менее есть. Безопаснее решить эту задачу, сохранив результат умножения 32–разрядных чисел в 64–разрядном, а затем проверить, равен ли хотя бы один из старших битов единице. Это и будет свидетельством переполнения.
   Когда встречается подобный код:
   unsigned a, b;
   ...
   if (a * b < MAX) {
   ...
   }
   проще ограничить а и b значениями, произведение которых заведомо меньше МАХ. Например:
   #include «limits.h»
   #define MAX_A 10000
   #define MAX_A 250
   assert(UINT_MAX / MAX_A >= MAX_B); // проверим, что MAX_A и MAX_B
                                                         // достаточно малы
   if (a < MAX_A && b < MAX_B) {
   ...
   }
   Если вы хотите надежно защитить свой код от переполнений целого, можете воспользоваться классом Safelnt, который написал Дэвид Лебланк (подробности в разделе «Другие ресурсы»). Но имейте в виду, что, не перехватывая исключения, возбуждаемые этим классом, вы обмениваете возможность выполнения произвольного кода на отказ от обслуживания. Вот пример использования класса Safelnt:
   size_t CalcAllocSize(int HowMany, int Size, int HeaderLen)
   {
   try{
   SafeInt<size_t> tmp(HowMany);
   return tmp * Size + SafeInt<size_t>(HeaderLen);
   }
   catch(SafeIntException)
   {
   return (size_t)~0;
   }
   }
   Целые со знаком используются в этом фрагменте только для иллюстрации; такую функцию следовало бы писать, пользуясь одним лишь типом size_t. Посмотрим, что происходит «под капотом». Прежде всего проверяется, что значение HowMany неотрицательно. Попытка присвоить отрицательное значение беззнаковой переменной типа Safelnt вызовет исключение. Затем Safelnt умножается на переменную Size типа int, при этом проверяется как переполнение, так и попадание в допустимый диапазон. Результат умножения Safelnt * int – это снова Safelnt, поэтому далее выполняется операция контролируемого сложения. Обратите внимание на приведение входного параметра типа int к типу Safelnt – отрицательная длина заголовка с точки зрения математики, может быть, и допустима, но с точки зрения здравого смысла – нет, поэтому размеры лучше представлять числами без знака. Наконец, перед возвратом величина типа Safelnt<size_t> преобразуется обратно в size_t, это пустая операция. Внутри класса Safelnt выполняется много сложных проверок, но ваш код остается простым и понятным.
   Если вы программируете на С#, включайте флаг /checked и применяйте unchecked–блоки только для того, чтобы отключить контроль в отдельных предложениях.

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

   Компилятор Microsoft Visual С++ 2005 автоматически обнаруживает переполнение при вызове оператора new. Ваша программа должна перехватывать исключения std::bad_alloc, иначе произойдет аварийный останов.

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

   □ «Another Look at the Safelnt Class», David LeBlanc, http://msdn.microsoft.com/ library/default.asp?url=/library/en–us/dncode/html/secure05052005.asp
   □ «Reviewing Code for Integer Manipulation Vulnerabilities*, Michael Howard, http://msdn. microsoft, com/library/default.asp?url=/library/en–us/dncode/ html/ secure04102003.asp
   □ «An Overlooked Construct and an Integer Overflow Redux», Michael Howard, http://msdn. microsoft, com/library/default.asp?url=/library/en–us/dncode/ html/ secure09112003.asp
   □ «Expert Tips for Finding Security Defects in Your Code», Michael Howard, http://msdn.microsoft.eom/msdnmag/issues/03/l 1/SecurityCodeReview/ default.aspx
   □ «Integer overflows – the next big threat», Ravind Ramesh, central.com/tech/story.asp?file=/2004/10/26/itfeature/9170256&sec=itfeature
   □ DOS against Java JNDI/DNS, http://archives.neophasis.com/archives/ bugtraq/2004–11 / 0092.html

Резюме

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

Грех 4.
Внедрение SQL–команд

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

   Уязвимость для внедрения SQL–команд (или просто «внедрение SQL») – это широко распространенный дефект, который может привести к компрометации машины и раскрытию секретных данных. А печальнее всего то, что от этой ошибки часто страдают приложения электронной коммерции и программы, обрабатывающие конфиденциальные данные и персональную информацию. Опыт авторов показывает, что многие приложения, работающие с базами данных, которые создавались для внутреннего использования или обмена информацией с партнерами по бизнесу, подвержены внедрению SQL.
   Никогда не интересовались, как хакеры воруют номера кредитных карточек с Web–сайтов? Одним из двух способов: либо внедряя SQL, либо заходя через парадный вход, который вы распахиваете перед ними, открывая порт сервера базы данных (ТСР/1433 для Microsoft SQL Server, TCP/1521 для Oracle, TCP/523 для IBM/DB2 и TCP/3306 для MySQL) для доступа из Интернет и оставляя без изменения принимаемый по умолчанию пароль администратора базы данных.
   Быть может, самая серьезная опасность, связанная с внедрением SQL, – это получение противником персональных или секретных данных. В некоторых странах, штатах и отраслях промышленности вас за это могут привлечь к суду. Например, в штате Калифорния можно сесть в тюрьму по закону о защите тайны личной жизни в сети, если из управляемой вами базы данных была похищена конфиденциальная или персональная информация. В Германии §9 DBSG (Федеральный закон о защите данных) требует, чтобы были предприняты должные организационные и технические меры для защиты систем, в которых хранится персональная информация. Не забывайте также о действующем в США Акте Сарбанеса–Оксли от 2002 года, и прежде всего о параграфе 404, который обязывает защищать данные, на основе которых формируется финансовая отчетность компании. Система, уязвимая для атак с внедрением SQL, очевидно, имеет неэффективные средства контроля доступа, а значит, не соответствует всем этим установлениям.
   Напомним, что ущерб не ограничивается данными, хранящимися в базе. Внедрение SQL может скомпрометировать сам сервер, а не исключено, что и сеть целиком. Для противника скомпрометированный сервер базы данных – это лишь ступень к новым великим свершениям.

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

   Все языки программирования, применяемые для организации интерфейса с базой данных, уязвимы! Но прежде всего это относится к таким языкам высокого уровня, как Perl, Python, Java, технологии «серверных страниц» (ASP, ASP.NET, JSP и PHP), С# и VB.NET. Иногда оказываются скомпрометированными также и языки низкого уровня, например библиотеки функций или классов, написанные на С или С++ (к примеру, библиотека c–tree компании FairCom или Microsoft Foundation Classes). Наконец, не свободен от греха и сам язык SQL.

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

   Самый распространенный вариант греха совсем прост – атакующий подсовывает приложению специально подготовленные данные, которые тот использует для построения SQL–предложения путем конкатенации строк. Это позволяет противнику изменить семантику запроса. Разработчики продолжают использовать конкатенацию, потому что не знают о существовании других, более безопасных методов. А если и знают, то не применяют их, так как, говоря откровенно, конкатенация – это так просто, а для вызова других функций еще подумать надо. Мы могли бы назвать таких программистов лентяями, но не станем.
   Реже встречается другой вариант атаки, заключающийся в использовании хранимой процедуры, имя которой передается извне. А иногда приложение принимает параметр, конкатенирует его с именем процедуры и исполняет получившуюся строку.

Греховность С#

   using System.Data;
   using System.Data.SqlClient;
   ...
   string ccnum = "None";
   try {
   SqlConnection sql = new SqlConnection(
   @"data source=localhost;" +
   "user id=sa;password=pAs$w0rd;");
   sql.Open();
   string sqlstring="SELECT ccnum" +
   " FROM cust WHERE id=" + Id;
   SqlCommand cmd = new SqlCommand(sqlstring,sql);
   try {
   ccnum = (string)cmd.ExecuteScalar();
   } catch (SqlException se) {
   Status = sqlstring + " failed\n\r";
   foreach (SqlError e in se.Errors) {
   Status += e.Message + "\n\r";
   }
   } catch (SqlException e) {
   // Ой!
   }
   Ниже приведен по существу такой же код, но SQL–предложение строится с помощью замены подстроки, а не конкатенации. Это тоже ошибка.
   using System.Data;
   using System.Data.SqlClient;
   ...
   string ccnum = "None";
   try {
   SqlConnection sql = new SqlConnection(
   @"data source=localhost;" +
   "user id=sa;password=pAs$w0rd;");
   sql.Open();
   string sqlstring="SELECT ccnum" +
   " FROM cust WHERE id=%ID%";
   String sqlstring2 = sqlstring.Replace("%ID", id);
   SqlCommand cmd = new SqlCommand(sqlstring2,sql);
   try {
   ccnum = (string)cmd.ExecuteScalar();
   } catch (SqlException se) {
   Status = sqlstring + " failed\n\r";
   foreach (SqlError e in se.Errors) {
   Status += e.Message + "\n\r";
   }
   } catch (SqlException e) {
   // Ой!
   }

Греховность PHP

   <?php
   $db = mysql_connect("localhost","root","$$sshhh...!");
   mysql_select_db("Shipping",$db);
   $id = $HTTP_GET_VARS["id"];
   $qry = "SELECT ccnum FROM cust WHERE id = %$id%";
   $result = mysql_query($qry,$db);
   if ($result) {
   echo mysql_result($result,0,"ccnum");
   } else {
   echo "No result! " . mysql_error();
   }
   ?>

Греховность Perl/CGI

   #!/usr/bin/perl
   use DBI;
   use CGI;
   print CGI::header();
   $cgi = new CGI;
   $id = $cgi->param('id');
   $dbh = DBI->connect('DBI:mysql:Shipping:localhost',
   'root',
   'cre+')
   or print "Ошибка connect : $DBI::errstr";
   $sql = "SELECT ccnum FROM cust WHERE id = " . $id;
   $sth = $dbh->prepare($sql)
   or print "Ошибка prepare : $DBI::errstr";
   $sth->execute()
   or print "Ошибка execute : $DBI::errstr";
   # Вывести данные
   while (@row = $sth->fetchrow_array ) {
   print "@row<br>";
   }

   $dbh->disconnect;
   print "</body></html>";

   exit;

Греховность Java

   import java.*;
   import java.sql.*;
   ...
   public static boolean doQuery(String Id) {
   Connection con = null;
   try
   {
   Class.forName("com.microsoft.jdbc.sqlserver.SQLServerDriver");
   con = DriverManager.getConnection("jdbc:microsoft:sqlserver: " +
                                              "//localhost:1433", "sa", "cre+");
   Statement st = con.createStatement();
   ResultSet rs = st.executeQuery("SELECT ccnum FROM cust WHERE id=" +
                                      Id);
   while (rx.next()) {
   // Полюбоваться на результаты запроса
   }

   rs.close();
   st.close();
   }
   catch (SQLException e)
   {
   // Ой!
   return false;
   }
   catch (ClassNotFoundException e)
   {
   // Не найден класс
   return false;
   }
   finally
   {
   try
   {
   con.close();
   } catch(SQLException e) {}
   }
   return true;
   }

Греховность SQL

   CREATE PROCEDURE dbo.doQuery(@query nchar(128))
   AS
   exec(@query)
   RETURN
   А вот следующий код распространен куда шире и не менее опасен:
   CREATE PROCEDURE dbo.doQuery(@id nchar(128))
   AS
   DECLARE @query nchar(256)
   SELECT @query = 'select ccnum from cust where id = ''' + @id + ''''
   EXEC @query
   RETURN
   Здесь опасная конкатенация строк выполняется внутри процедуры. То есть вы по–прежнему совершаете постыдный грех, даже если процедура вызвана из корректного кода на языке высокого уровня.
   Стоит поискать и другие операторы конкатенации, имеющиеся в SQL, а именно «+» и «||», а также функции CONCAT() и CONCATENATE().
   Во всех этих примерах противник контролирует переменную Id. Важно всегда представлять себе, что именно контролирует атакующий, это поможет понять, есть реальная ошибка или нет. В данном случае противник может задать любое значение переменной Id, участвующей в запросе, и тем самым управлять видом строки запроса. Последствия могут оказаться катастрофическими.
   Классическая атака состоит в том, чтобы видоизменить SQL–запрос, добавив лишние части и закомментарив «ненужные». Например, если противник контролирует переменную Id, то может задать в качестве ее значения строку 1 or 2>1 – – , тогда запрос примет такой вид:
   SELECT ccnum FROM cust WHERE id=1 or 2>1 – –
   Условие 2>1 истинно для всех строк таблицы, поэтому запрос возвращает все строки из таблицы cust, другими словами, номера всех кредитных карточек. Можно было бы воспользоваться классической атакой «1 = 1», но сетевые администраторы часто включают поиск такой строки в системы обнаружения вторжений (IDS), поэтому мы применили условие «2>1», которое столь же эффективно, но «проходит под лучом радара».
   Оператор комментария – – убирает из поля зрения сервера все последующие символы запроса, которые могла бы добавить программа. В одних базах данных для комментирования применяются символы —, в других – #. Проверьте, что воспринимает в качестве комментария ваша база.
   Различных вариантов атак слишком много, чтобы перечислять их здесь, дополнительный материал вы найдете в разделе «Другие ресурсы».

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

   □ соединение от имени учетной записи с высоким уровнем доступа;
   □ включение пароля в текст программы;
   □ сообщение противнику излишне подробной информации в случае ошибки.
   Рассмотрим их по порядку. Везде соединение устанавливается от имени административного или высокопривилегированного пользователя, хотя достаточно было бы пользователя, имеющего доступ только к одной базе данных. Это означает, что противник потенциально сможет манипулировать и другой информацией, а то и всем сервером. Короче говоря, соединение с базой данных от имени пользователя с высоким уровнем доступа – скорее всего, ошибка и нарушение принципа наименьших привилегий.
   «Зашивание» паролей в код – почти всегда порочная идея. Подробнее см. грех 11 и 12 и предлагаемые там «лекарства».
   Наконец, в случае ошибки противник получает слишком много информации. Воспользовавшись ей, он сможет получить представление о структуре запроса и, быть может, даже узнать имена объектов базы. Более подробную информацию и рекомендации см. в грехе 6.

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

   □ принимает данные от пользователя;
   □ не проверяет корректность входных данных;
   □ использует введенные пользователем данные для запроса к базе;
   □ применяет конкатенацию или замену подстроки для построения SQL–запроса либо пользуется командой SQL exec (или ей подобной).

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


   Выяснив, что в программе есть обращения к базе данных[1], нужно определить, где выполняются запросы и насколько можно доверять данным, участвующим в запросе. Самое простое – найти все места, где выполняются предложения SQL, и посмотреть, производится ли конкатенация или подстановка небезопасных данных, взятых, например, из строки Web–запроса, из Web–формы или аргумента SOAP. Вообще, любых поступающих от пользователя данных!

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

   Прежде всего определите все точки входа в приложение, где формируются SQL–запросы. Затем создайте тестовую программу–клиент, которая будет посылать в эти точки частично некорректные данные. Например, если тестируется Web–приложение, которое строит запрос на основе одного или нескольких полей формы, попробуйте вставить в них произвольные ключевые слова языка SQL. Следующий пример на Perl показывает, как это можно сделать.
   #!/usr/bin/perl
   use strict;
   use HTTP::Request::Common qw(POST GET);
   use HTTP::Headers;
   use LWP::UserAgent;
   srand time;
   # Приостановить исполнение, если найдена ошибка
   my $pause = 1;
   # Тестируемый URL
   my $url = 'http://mywebserver.xyzzy123.com/cgi-bin/post.cgi';
   # Максимально допустимый размер HTTP-ответа
   my $max_response = 1000;

   # Допустимые города
   my @cities = qw(Auckland Seattle London Portland Manchester Redmond
   Brisbane Ndola);

   while (1) {
   my $city = randomSQL($cities[rand @cities]);
   my $zip = randomSQL(10000 + int(rand 89999));

   print «Пробую [$city] и [zip]\n»;
   my $ua = LWP::UserAgent->new();
   my $req = POST $url,
   [ City => $city,
   ZipCode => $zip,
   ];
   # Послать запрос, получить ответ и поискать в нем признаки ошибки
   my $res = $ua->request($req);
   $_ = $res->as_string;
   die "Хост недостижим\n" if /bad hostname/ig;
   if ($res->status_line != 200
   || /error/ig
   || length($_) > $max_response) {
   print "\nПотенциальная возможность внедрения SQL\n";
   print;
   getc if $pause;
   }
   }

   # Выбрать случайное ключевое слово SQL, в 50% случаев перевести
   # его в верхний регистр
   sub randomSQL() {
   $_ = shift;

   return $_ if ($rand > .75);

   my @sqlchars = qw(1=1 2>1 «fred=»fre" + "d" or and select union
                            drop update insert into dbo < > = ( ) ' .. – #);

   my $sql = $sqlchars[rand @sqlchars];
   $sql = uc($sql) id rand > .5;

   return $_ . ' ' . $sql if rand > .9;
   return $sql . ' ' . $_ if rand > .9;
   return $sql;
   }
   Этот код обнаружит возможность внедрения SQL только в случае, когда приложение возвращает сообщение об ошибке. Как мы уже сказали, нет ничего лучше тщательного анализа кода. Другой способ тестирования заключается в том, чтобы воспользоваться приведенной выше Perl–программой, заранее выяснить, как выглядит нормальный ответ, а затем анализировать, на какие запросы получен ответ, отличающийся от правильного, или вообще нет никакого ответа.
   Имеются также инструменты третьих фирм, например AppScan компании Sanctum (теперь Watchfire) (www.watchfire.com), Weblnspect компании SPI Dynamics (www.spidynamics.com) и ScanDo компании Kavado (www.kavado.com).
   Для оценки инструмента рекомендуем создать небольшое тестовое приложение с известными ошибками, допускающими внедрение SQL, и посмотреть, какие ошибки этот инструмент сумеет найти.

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

   Следующие примеры внедрения SQL–команд взяты из базы данных CVE (http://cve.mitre.org).

CAN–2004–0348

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

CAN–2002–0554

   Модуль Web DataBlade для базы данных Informix SQL динамически генерирует HTML–страницу на основе данных. Уязвимость отмечена в нескольких версиях Web DataBlade. Можно внедрить SQL–команды в любой запрос, обрабатываемый этим модулем. Результатом может стать раскрытие секретной информации или повышение уровня доступа к базе данных.

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

   Простейший и наиболее безопасный способ искупления – никогда не доверять входным данным, на основе которых формируется SQL–запрос, и пользоваться подготовленными или параметризованными предложениями (prepared statements).

Проверяйте все входные данные

Никогда не применяйте конкатенацию для построения SQL–предложений

   Следующая рекомендация состоит в том, чтобы никогда не строить SQL–предложения посредством конкатенации или замены подстроки. Никогда! Пользуйтесь только подготовленными или параметризованными запросами. В некоторых технологиях это называется связыванием параметров (binding). В примерах ниже продемонстрированы некоторые из таких безопасных конструкций.

   Примечание. Во всех примерах информация о параметрах соединения не хранится в тексте программы; мы вызываем специальные функции, которые считывают данные из пространства приложения.

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

   string ccnum;
   string sqlstring = "";
   // пропускаем только корректные ID (от 1 до 8 цифр)
   Regex r = new Regex(@"^\d{1,8}$");
   if (!r.Match(Id).Success)
   throw new Exception("Неверный ID. Попробуйте еще раз.");
   try {
   SqlConnection sqlConn = new SqlConnection(GetConnection);
   string str = "sp_GetCreditCard";
   cmd = new SqlCommand(str, sqlConn);
   cmd.CommandType = CommandType.StoredProcedure;
   cmd.Parameters.Add("@ID", Id);
   cmd.Connection.Open();
   SqlDataReader read = myCommand.ExecuteReader();
   ccnum = read.GetString(0);
   }
   catch (SqlException se) {
   throw new Exception("Ошибка – попробуйте еще раз.");
   }
   }

Искупление греха в PHP 5.0 и MySQL версии 4.1 и старше

   $db = mysqli_connect(getServer(), getUid(), getPwd());
   $stmt = mysqli_prepare($link, "SELECT ccnum FROM cust WHERE id=?");
   $id = $HTTP_GET_VARS["id"];
   // пропускаем только корректные ID (от 1 до 8 цифр)
   if (preg_match('\d{1,8}$/',$id);
   mysqli_stmt_bind_param($stmt, "s", $id);
   mysqli_stmt_execute($stmt);
   mysqli_stmt_bind_result($stmt, $result);
   mysqli_stmt_fetch($stmt);
   if (empty($name)) {
   echo "Результата нет!";
   } else {
   echo $result;
   }
   else {
   echo "Неверный ID. Попробуйте еще раз.");
   }
   ?>

Искупление греха в Perl/CGI

   use DBI;
   use CGI;
   print CGI::header();
   $cgi = new CGI;
   $id = $cgi->param('id');
   // пропускаем только корректные ID (от 1 до 8 цифр)
   exit unless ($id =~ /^[\d]{1,8}$);
   print "<html><body>";
   // Параметры соединения получаем извне
   $dbh = DBI->connect(conn(),
                             conn_name(),
                             conn_pwd())
   or print "Ошибка connect";
                             # детальная информация об ошибке в $DBI::errstr
   $sql = "SELECT ccnum FROM cust WHERE id = ?";
   $sth = $dbh->prepare($sql)
   or print "Ошибка prepare";

   $sth->bind_param(1,$id);
   $sth->execute()
   or print "Ошибка execute";

   # Вывести данные
   while (@row = $sth->fetchrow_array ) {
   print "@row<br>";
   }
   $dbh->disconnect;
   print "</body></html>";

   exit;

Искупление греха в Java с использованием JDBC

   // пропускаем только корректные ID (от 1 до 8 цифр)
   Pattern p = Pattern.compile("^\d{1,8}$");
   if (!p.matcher(arg).find())
   return false;
   Connection con = null;
   try
   {
   Class.forName("com.microsoft.jdbc.sqlserver.SQLServerDriver");
   con = DriverManager.getConnection("jdbc:microsoft:sqlserver: " +
                                                 "//localhost:1433", "sa", "cre+");
   PreparedStatement st = con.prepareStatement(
   "exec pubs..sp_GetCreditCard ?");
   st.setString(1, arg);
   ResultSet rs = st.executeQuery();
   while (rx.next()) {
      // Получить данные из rs.getString(1)
   }
   rs.close();
   st.close();
   }
   catch (SQLException e)
   {
   System.out.println("Ошибка SQL");
   return false;
   }
   catch (ClassNotFoundException e)
   {
   System.out.println("Ошибка во время исполнения");
   return false;
   }
   finally
   {
   try
   {
   con.close();
   } catch(SQLException e) {}
   }
   return true;
   }

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

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

   Не следует исполнять в хранимой процедуре строку, полученную из не заслуживающего доверия источника, как процедуру. В качестве одного из механизмов глубоко эшелонированной обороны можно воспользоваться некоторыми функциями для проверки корректности строкового параметра. В примере ниже проверяется, что входной параметр содержит ровно четыре цифры. Заметим, что длина параметра заметно уменьшена, чтобы усложнить передачу любой другой входной информации.
   CREATE PROCEDURE dbo.doQuery(@id nchar(4))
   AS
   DECLARE @query nchar(64)
   IF RTRIM(@id) LIKE '[0-9][0-9][0-9][0-9]'
   BEGIN
   SELECT @query = 'select ccnum from cust where id = ''' + @id + ''''
   EXEC @query
   END
   RETURN
   Или еще лучше – потребуйте, чтобы параметр был целым числом:
   CREATE PROCEDURE dbo.doQuery(@id smallint)
   В Oracle lOg, как и в Microsoft SQL Server 2005, добавлены совместимые со стандартом POSIX регулярные выражения. Поддержка регулярных выражений реализована также для DB2 и Microsoft SQL Server 2000. В MySQL регулярные выражения поддерживаются с помощью оператора REGEXP. Ссылки на все эти решения вы найдете в разделе «Другие ресурсы».
   

notes

Примечания

1

комментариев нет  

Отпишись
Ваш лимит — 2000 букв

Включите отображение картинок в браузере  →