Переполнение буфера на системах с неисполняемым стеком

       

Атака на DEP


Microsoft подтвердила возможность обхода DEP еще в январе 2005, когда на maxpatrol'е появилась статья Александра Анисимова "Defeating Microsoft Windows XP SP2 Heap protection and DEP bypass", однако, не придала этому большего значения, заявив, что реализовать удаленную атаку все равно не удастся: "An attacker cannot use this method by itself to attempt to run malicious code on a user's system. There is no attack that utilizes this, and customers are not at risk from the situation" (Атакующий не может использовать этот метод для запуска зловредного кода на целевой системе. До сих пор не было продемонстрировано ни одной таки, использующей этот трюк и потребители находятся вне риска — http://www.eweek.com/article2/0,1759,1757786,00.asp).

Это — наглая ложь! Механизм DEP легко пробивается с любого расстояния, хоть из Антарктики. Результатом атаки становится переданный и успешно выполненный shell-код, выполняющий команды заранее подготовленные злоумышленником. Чтобы понять как это делается, сначала необходимо разобраться с классическими методами переполнения, подробно разобранных в статье "ошибки переполнения буфера извне и изнутри как обобщенный опыт реальных атак".

Напомним читателю основные положения: отсутствие контроля границ локальных буферов позволяет затирать адрес возврата из функции, помещая сюда указатель на shell-код, который находится здесь же, в стеке. Другой тип переполнения связан с кучей. С его помощью хакер может модифицировать любую writable ячейку в адресном пространстве уязвимого процесса (например, подменить указатель на виртуальную функцию или подделать адрес возврата). Имеются и другие возможности (в частности, атакующий может изменить обычный порядок выделения блоков памяти из кучи, разместив следующий выделяемый блок поверх ключевых структур данных), но для простоты изложения мы ограничимся модификацией адреса возврата.

Рисунок 10 классическая удаленная атака — засылка shell-кода в стек с последующей передачей на него управления путем модификации адреса возврата.


Существует множество защитных механизмов, контролирующих целость кучи и адреса возврата, но… со своей задачей они не справляются. Это отдельная большая тема, никак не связанная ни с NX/XD-битами, ни с технологией DEP. В следующих статьях мы подробно рассмотрим ее, но… это будет потом. Сейчас же ограничимся тем, что DEP никак не препятствует модификации адреса возврата и существует множество программ действительно позволяющих это делать (Internet Explorer, FireFox и др.).

Манипулируя адресом возврата, хакер может вызывать произвольные функции уязвимой программы (в том числе и API-функции операционной системы) передавая необходимые параметры через стек. Конечно, при этом он будет очень ограничен в своих возможностях, поскольку среди готовых функций полезных для хакерствования не так уж и много, однако, если покурить хорошей травы и как следует подумать головой, попутно напрягая и другие части тела, проблему удается решить путем вызова API-функции CreateProcess или функции System из библиотеки CRT, запустив штатную программу типа tftp.exe и закачав на атакуемый компьютер двоичный файл, который можно исполнить с помощью CreateProcess/System (атака типа return-to-libc). Механизм DEP этому сценарию никак не препятствует, поскольку в этом случае shell-код передается не через стек/кучу, а через легальный исполняемый файл. На стеке содержаться лишь аргументы вызываемых функций и "поддельные" адреса возврата, указывающие на них.

Таким образом, даже при включенном DEP у хакера сохраняется возможность забрасывать передать на атакуемую машину свой код и передавать на него управление. Наличие tftp.exe не является необходимым условием для атаки. Даже если его удалить, хакер может вызвать cmd.exe и… нет, удалять все файлы с диска необязательно (хотя и возможно), достаточно перенаправить вывод в файл и с помощью ECHO создать крохотный com, делающий что-то "полезное". Да и в самой уязвимой программе наверняка содержаться функции, через которые можно загрузить файл из Интернета… Словом, возможностей — море! Атаки этого типа хорошо изучены хакерами и описаны в литературе.


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

Некоторые могут сказать, что это "не настоящая" атака, поскольку в ней отсутствует явная передача shell-кода через стек, а ведь именно на это DEP и нацеленрассчитан! Мысль, конечно, умная, но забавная. И какой это хакер броситься на амбразуру если можно сходить в обход? Неужели в Microsoft всерьез считают, что атакующий играет по "правилам" и идет по пути наибольшего сопротивления, карабкаясь по извилистой горной тропинке, напичканной патрулями, когда столбовая дорога никем не охраняется?!



В качестве разминки для мозгов рассмотрим альтернативный сценарий атаки, передающий shell-код через стек. Засунуть shell-код в локальный буфер — не проблема, подменить адрес возврата тоже, но… при попытке передачи управления на shell-код при активном DEP будет возникать исключение, ведь x-атрибута у нас нет, хотя… это смотря, что мы переполняем. Как уже говорилось выше, некоторые приложения (в том числе и браузеры) нуждаются в исполняемом стеке, в который они складывают откомпилированный java-код. Но не будем смягчать себе условия. Давайте считать, что никаких исполняемых страниц в нашем стеке нет. Чтобы их заполучить, необходимо либо сбросить NX/XD-бит (но это можно делать только система), либо… вызывать функцию VirtualProtect, назначая атрибуты защиты по своему желанию! Но как же мы вызовем VirtualProtect без возможности выполнения shell-кода? Да очень просто — скорректируем адрес возврата так, чтобы он указывал на VirtualProtect, тогда при выполнении команды return она передаст на нее управление!



На самом деле, это только идея. До практической реализации ей еще далеко. В жизни все происходит намного сложнее и… элегантнее. Допустим, вызвали мы VirtualProtect. А дальше что? Куда она возвратит управление? И как узнать адрес выделенного блока? Это великолепная головоломка на решение которой мыщъх потратил целый день и был очень разочарован, когда узнал, что он не один такой умный, и все загадки уже разгадали еще до него. Взгляните на рис. 11. Специалистамы

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



Рисунок 11 подготовка стека для реализации атаки типа commit-n-copy

Итак, все по порядку. Начнем с ответа на вопрос: куда возвращает управление VirtualProtect? Ответ очевиден: по указателю, который лежит за модифицированным адресом возврата! В момент выхода из VirtualProtect, процессор стакливает текущий адрес возврата с вершины стека и удаляет переданные ей аргументы. Так происходит потому, что VirtualProtect (как и все API-функции) придерживается соглашения о передаче параметров типа stdcall, при котором аргументы удаляются самой вызываемой функцией.

Таким образом, мы можем вызывать столько stdcall-функций, сколько захотим. Процессор будет послушно стягивать их со стека, поступательно двигаясь от вершины вглубь. Техника вызова cdecl-функций выглядит чуть сложнее. Они не очищают аргументы при выходе, и это атакующему приходится это делать самостоятельно. Проще всего перенаправить адрес возврата на код типа ADD ESP,n/RET, расположенный где-то внутри уязвимой программы, где n – количество байт, занятых аргументами. Такую комбинацию можно встретить практически в любом оптимизированном эпилоге. Ну а нужное n подобрать совсем не сложно!

Теперь мы знаем как вызывать функцию для изменения атрибутов доступа и вернуться обратно в shell-код, но это еще не все. Функция VirtualProtect требует, чтобы ей передали адрес уже выделенного региона, а shell-коду он неизвестен. К тому же, по слухам Microsoft собирается встроить в VirtualProtect дополнительную проверку, запрещающую назначать x-атрибут, если он не был присвоен еще при выделении.


Тупик? Не совсем, ведь мы можем выделить новый регион, назначить нужные права, скопировать туда свой shell-код и передать ему управление. С выделением никаких проблем нет — берем VirtualAlloc и вперед. Как прочитать возвращенный указатель — вот в чем вопрос! Функция передает его в регистре EAX, и заранее предсказать его значение невозможно. А ведь хакер должен сформировать указатель и затолкать его в стек еще на стадии проектирования shell-кода, то есть задолго до вызова VirtualAlloc. Ну теперь уж точно тупик… А вот и нет! Внимательное чтение SDK (или Рихтера) показывает, что Windows позволяет COMMIT'ить (то есть передавать) уже переданную память по заданному адресу и хотя последствия такого выделения могут быть очень печальными (для кучи) — кого это волнует?! Главное, чтобы shell-код получил управление и он его получит! Оторвать мыщъх'у хвост, если это не так! Выбираем произвольный адрес, который с высокой степенью вероятности не будет занят ничем полезным (например, 191000h), и передаем его функции VirtualAlloc вместе с флагом MEM_COMMIT и атрибутами PAGE_EXECUTE_READWRITE. Все! Первая стадия атаки благополучно завершилась и теперь моджно курнуть травы или глотнуть пива.

На втором шаге мы вызываем функцию memcpy и копируем shell-код в только что выделенный регион памяти, целевой адрес которого заранее известен. После этой операции, shell-код оказывается в области памяти, где разрешено выполнение и нам остается только заснуть в стек еще один подложный адрес возврата, который будет указывать на него (внимание! функция memcpy в отличии от VirtualAlloc придерживается cdecl соглашения, поэтому после нее мы уже не можем выполнять никакие другие функции, предварительно не удалив аргументы со стека).

Последовательность вызовов, реализующих атаку выглядит так (напоминаем: это не shell-код, это именно последовательность вызовов API-функций, осуществляемая путем подмены адресов возврата):

VirtualAlloc(REMOTE_BASE, SHELLCODE_LENGTH, MEM_COMMIT, PAGE_EXECUTE_READWRITE);



memcpy(REMOTE_BASE, SHELLCODE_BASE, SHELLCODE_LENGTH);

GOTO shell_code;

Листинг 1  последовательность вызова функций, реализующих атаку типа commit-n-copy

Маленький нюанс — 64-битные редакции NT передают аргументы API-функциям через регистры и потому вызывать VirtualAlloc на них уже не удастся (разумеется, речь идет только о 64-битных приложениях). То есть, вызывать-то удастся, а вот передать аргументы — нет, поэтому этот сценарий уже не сработает, однако, вызов функций уязвимой программы через подмену адреса возврата будет действовать по-прежнему (то есть, запустить tftp.exe мы все-таки сможем, вызывая ее через функцию System, которая по-прежнему принимает аргументы через стек).

Подведем итог: стечение ряда неблагоприятных для DEP обстоятельств делает эту технологию практически полностью бесполезной. Да, она отсекает целый класс атак, основанных на переполнении, однако, дает пишу для новых и в целом ситуация никак не меняется. Забавно, но большинство людей (в том числе и администраторов!) совершенно не понимают, что такое DEP, зачем он нужен, какие цели преследует и в чем заключается взлом. Достаточно почитать дискуссию, развернувшуюся на http://www.mastropaolo.com/?p=13, чтобы убедится, что оба предложенных сценария обхода DEP не считаются взломом, поскольку x-атрибут присваивается "легальным" способом через вызов VirtualAlloc. На самом деле, суть взлома вовсе не в том, чтобы выполнить код в области памяти без x-атрибута (это действительно невозможно), а в том, чтобы использовать уязвимое ПО в своих хакерских целях. В идеале, DEP должен представлять собой целый комплекс защитных мер, предотвращающих это, но ничего подобного он не делает, ограничиваясь простой формальной поддержкой NX/XD-атрибутов. Перечислим те обстоятельства, которые ему мешают:

q       параметры API-функций передаются через стек;

q       адреса API-функций и положение вершины стека легко предсказуемы;

q       всякий процесс может пометить любой регион памяти как "исполняемый";

q       функция VituallAlloc позволяет выделять/передавать уже переданную память;


Содержание раздела