Файл: Debian Таненбаум Бос.pdf

ВУЗ: Не указан

Категория: Книга

Дисциплина: Операционные системы

Добавлен: 29.10.2018

Просмотров: 48135

Скачиваний: 190

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
background image

9.7. Взлом программного обеспечения   

711

01. void A (char *date) {
02.  int len;
03.  char B [128];
04.  char logMsg [256];
05.
06.  strcpy (logMsg, date);     /* сначала в строку сообщения копируется строка 
                                с данными */
07.  len = str len (date);      /* Определение количества символов в строке 
                                данных */
08.  gets (B);                  /* теперь получение настоящего сообщения */
09.  strcpy (logMsg+len, B);    /* и копирование его после данных в
                                сообщение журнала */
10.  writeLog (logMsg); /* и наконец, запись регистрационного сообщения 
                                на диск */
11. }

Как и в предыдущем примере, функция A считывает регистрационное сообщение со 
стандартного ввода, но теперь к нему явным образом добавляются текущие данные 
(предоставляемые в качестве строкового аргумента функции A). Сначала она копирует 
данные в регистрационное сообщение (строка 6). Строка данных может иметь разную 
длину в зависимости от дня недели, месяца и т. д. Например, в слове «среда» 5 букв, а в 
слове «суббота» — 7. То же самое относится к названиям месяцев. Поэтому вторым дей-
ствием определяется количество символов в строке данных (строка 7). Затем функция 
получает ввод пользователя (строка 8) и копирует его в регистрационное сообщение, 
начиная сразу же после строки данных. Это делается путем указания того, что полу-
чаемая копия должна начинаться с регистрационного сообщения плюс длина строки 
данных (строка 9). И наконец, функция, как и раньше, записывает регистрационное 
сообщение на диск.

Предположим, что система использует стековые «канарейки». Как можно изменить 
адрес возврата? Секрет в том, что при организации переполнения буфера B взломщик 
не пытается тут же попасть на адрес возврата. Вместо этого он изменяет значение 
переменной len, расположенное в стеке непосредственно над ним. В строке 9 len слу-
жит смещением, определяющим, куда будет записано содержимое буфера B. Замысел 
программиста заключается в пропуске только строки данных, а поскольку взломщик 
контролирует значение переменной len, он может воспользоваться этой переменной 
для обхода «канарейки» и перезаписи адреса возврата.

Более того, переполнения буферов не ограничиваются адресом возврата. Вполне 
подойдет любой указатель функции, доступный путем организации переполнения. 
Указатель функции похож на обычный указатель, но указывает не на данные, а на 
функцию. Например, в C и C++ программист может объявить переменную f в качестве 
указателя функции, которая получает строковый аргумент и возвращает управление 
без предоставления результата:

void (*f)(char*);

Синтаксис, наверное, немного загадочный, но он действительно является еще одним 
объявлением переменной. Поскольку функция A из предыдущего примера соответ-
ствует приведенной выше сигнатуре, теперь можно написать «f = A» и использовать 
в нашей программе f вместо A. В задачу книги не входит углубленное изучение под-
робностей указателей функций, но заверяю вас, что эти указатели встречаются в опе-
рационных системах довольно часто. Теперь предположим, что взломщик ухитрился 


background image

712  

 Глава 9. Безопасность 

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

Предотвращение выполнения данных

Возможно, сейчас вы воскликните: «Постойте! Проблема ведь не в том, что взломщик 
может переписывать указатели функций или адреса возврата управления, а в том, что 
он может внедрить код и заставить систему его выполнить. Почему бы не пресечь вы-
полнение байтов в куче и в стеке?» Если вы так и сделали, значит, наступило прозрение. 
Но совсем скоро мы увидим, что такие прозрения не всегда способны остановить атаку 
переполнения буфера. Но сама идея весьма толковая. Атаки внедрения кода (code 
injection attacks) не сработают, если байты, предоставленные взломщиком, не смогут 
быть выполнены в качестве легального кода.

У современных центральных процессоров есть свойство, которое в народе именуется 
NX-битом

 (NX bit), что означает «No-eXecute», то есть невыполняемый. Этот бит 

особенно полезен для того, чтобы различать сегменты данных (кучу, стек и глобальные 
переменные) и текстовые сегменты (которые содержат код). На самом деле многие со-
временные операционные системы старались обеспечить возможность записи сегмен-
тов данных без возможности выполнения их содержимого и возможность выполнения 
текстовых сегментов без возможности их записи. Эта политика известна в OpenBSD 
как WˆX (произносится «WExclusive-OR X» или «W XOR X»). Она означает, что 
память может быть либо записываемой, либо исполняемой, но не той и другой одно-
временно. Mac OS X, Linux и Windows имеют похожие схемы защиты. Обобщенно эта 
мера безопасности называется DEP (Data Execution Prevention), то есть предотвраще-
ние выполнения данных. Некоторое оборудование не поддерживает NX-бит. В таком 
случае DEP работает, но принуждение носит программный характер.

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

Атаки повторного использования кода

DEP делает невозможным выполнение кода в области данных. Стековые «канарейки» 
затрудняют (но не исключают) переписывание адресов возврата и указателей функций. 
К сожалению, это еще не конец рассказа, поскольку однажды на кого-то также сни-
зошло озарение. Он понял примерно следующее: «Зачем внедрять код, когда его уже 
вполне достаточно в двоичном виде?». Иными словами, вместо внедрения нового кода 
взломщик просто составляет нужную функциональность из существующих функций 
и инструкций в двоичном коде и библиотеках. Сначала мы рассмотрим простейшую 
из таких атак — атаку возврата в библиотеку (return to libc), а затем более сложную, 
но очень популярную технологию возвратно-ориентированного программирования 
(return-oriented programming).


background image

9.7. Взлом программного обеспечения   

713

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

Эта атака называется возвратом в библиотеку (return to libc) и имеет несколько ва-
риантов. System — не единственная функция, способная заинтересовать взломщика. 
Например, взломщик может также воспользоваться функцией mprotect, чтобы сделать 
часть сегмента данных выполняемой. Кроме того, вместо безусловного перехода непо-
средственно на функцию библиотеки libc взломщик может воспользоваться уровнем 
перенаправления. Например, в Linux взломщик может вместо этого вернуть управле-
ние в таблицу компоновки процедур (Procedure Linkage Table (PLT)). PLT является 
структурой, облегчающей динамическое связывание, и содержит фрагменты кода, 
при выполнении которых, в свою очередь, вызываются динамически связываемые 
библиотечные функции. Возвращение управления в этот код приводит к косвенному 
выполнению библиотечной функции.

Концепция  возвратно-ориентированного  программирования (Return-Oriented 
Programming (ROP)) доводит замысел повторного использования кода программы 
до крайности. Вместо возвращения управления в точки входа в библиотечные функ-
ции взломщик может передать управление любой инструкции в текстовом сегменте. 
Например, он может передать управление коду в середине, а не в начале функции. Вы-
полнение просто продолжится с этой точки, и инструкции будут выполняться одна за 
другой. Предположим, что после выполнения нескольких инструкций очередь дойдет 
до еще одной инструкции возврата. И теперь мы зададим все тот же вопрос: куда мож-
но будет передать управление? Поскольку у взломщика есть контроль над стеком, он 
опять может заставить код передать управление куда захочет. А после того как он это 
проделает дважды, он может сделать это в третий, в четвертый, в десятый раз и т. д.

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

На рис. 9.20, a показан пример того, как гаджеты связываются вместе путем адресов 
возврата в стеке. Гаджеты являются небольшими фрагментами кода, заканчивающими-


background image

714  

 Глава 9. Безопасность 

ся инструкцией возврата. Эта инструкция будет брать адрес из стека, чтобы передать 
управление на этот адрес и продолжить работу с него. В таком случае взломщик сна-
чала передаст управление гаджету A в некой функции X, затем гаджету B в функции Y 
и т. д. Задача взломщика состоит в сборе таких гаджетов в выполняемый двоичный код. 
Поскольку сам он гаджеты не создает, иногда ему приходится иметь дело с гаджетами, 
которые, возможно, далеки от идеала, но вполне подходят для намеченной им работы. 
Например, на рис. 9.20, б предполагается, что гаджет A входит в последовательность 
инструкций и имеет проверку. Взломщику эта проверка может быть вообще не нужна, 
но поскольку она там есть, ему придется с этим смириться. Для большинства целей 
было бы, наверное, неплохо поместить в регистр 1 любое неотрицательное число. Сле-
дующий гаджет помещает любое значение стека в регистр 2, а третий гаджет умножает 
значение регистра 1 на 4, помещает его значение в стек и складывает его со значением 
регистра 2. Объединение этих трех гаджетов приводит взломщика к чему-то, что может 
быть использовано для вычисления адреса элемента в массиве целых чисел. Индекс 
массива предоставляется первым значением данных в стеке, а базовый адрес массива 
должен быть во втором значении данных.

à

á

Äàííûå

Äàííûå

&Ãàäæåò Ñ

&Ãàäæåò B

&Ãàäæåò A

&Ãàäæåò Ñ

(÷àñòü ôóíêöèè Z)

&Ãàäæåò B

(÷àñòü ôóíêöèè Y)

&Ãàäæåò A

(÷àñòü ôóíêöèè X)

Èíñòðóêöèÿ 4

Èíñòðóêöèÿ 3

Èíñòðóêöèÿ 2

Èíñòðóêöèÿ 1

Èíñòðóêöèÿ 3

Èíñòðóêöèÿ 2

Èíñòðóêöèÿ 1

Èíñòðóêöèÿ 4

Èíñòðóêöèÿ 3

Èíñòðóêöèÿ 2

Èíñòðóêöèÿ 1

Òåêñòîâûé 

ñåãìåíò

Ñòåê

Ãàäæåòû ïðèìåðà:

Ãàäæåò A:

Ïîìåùåíèå îïåðàíäà èç ñòåêà â ðåãèñòð 1

Åñëè çíà÷åíèå îòðèöàòåëüíîå, ïåðåõîä 

ê îáðàáîò÷èêó îøèáîê

 ïðîòèâíîì ñëó÷àå âîçâðàò óïðàâëåíèÿ

Ãàäæåò B:

Ïîìåùåíèå îïåðàíäà èç ñòåêà â ðåãèñòð 2

Âîçâðàò

Ãàäæåò C:

Óìíîæåíèå çíà÷åíèÿ ðåãèñòðà 1 íà 4

Ïîìåùåíèå çíà÷åíèÿ ðåãèñòðà 1 â ñòåê

Ñëîæåíèå çíà÷åíèÿ ðåãèñòðà 2 

ñî çíà÷åíèåì íà âåðøèíå ñòåêà 

è ñîõðàíåíèå ðåçóëüòàòà â ðåãèñòðå 2

Рис. 9.20. Возвратно-ориентированное программирование: связывание гаджетов 

Возвратно-ориентированное программирование может выглядеть весьма сложным, и, 
наверное, так оно и есть. Но как и всегда, люди разработали инструментальные сред-
ства для автоматизации максимально возможного объема работы. В качестве примера 
можно привести сборщики гаджетов и даже ROP-компиляторы. В настоящее время 


background image

9.7. Взлом программного обеспечения   

715

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

Рандомизации распределения адресного пространства

Существует еще одна идея по предотвращению подобных атак. Кроме изменения адреса 
возврата и внедрения некой (ROP) программы взломщик должен иметь возможность 
передавать управление по абсолютно точным адресам, с технологией ROP номер с nop-
направляющими не пройдет. Когда адреса фиксированы, задачу решить несложно, а что, 
если они не фиксированы? Рандомизация распределения адресного пространства 
(Address Space Layout Randomization (ASLR)) нацелена на произвольное назначение 
адресов функций и данных при каждом запуске программы. В результате задача взлом-
щика по нанесению вреда системе существенно затрудняется. В частности, ASLR часто 
занимается произвольным выбором позиций исходного стека, кучи и библиотек.

Многие современные операционные системы наряду со стековыми «канарейками» 
и DEP поддерживают в той или иной степени также и ASLR. Большинство из них 
предоставляют эту технологию для пользовательских приложений, но лишь немногие 
применяют ее постоянно и к самому ядру операционной системы (Giuffrida et al., 2012). 
Объединенные усилия этих трех механизмов защиты существенно подняли барьер 
на пути взломщиков. Простой переход к внедренному коду или даже к какой-нибудь 
уже существующей в памяти функции стал весьма непростой работой. Все вместе эти 
механизмы формируют в современных операционных системах важную линию обо-
роны. Особенно привлекательным во всем этом является предоставление защиты по 
весьма разумной цене с точки зрения производительности.

Обход ASLR

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

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

01. void C() {
02.  int index;
03.  int prime [16] = { 1,2,3,5,7,11,13,17,19,23,29,31,37,41,43,47 };
04.  printf ("Какое простое число из показанных здесь вы хотели бы видеть?");
05.  index = read user input ();
06.  printf ("На позиции %d находится простое число %d\n", index, 
            prime[index]);
07. }