На главную | Содержание | Назад | Вперёд
Наши друзья

 

 

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

0хба94бббб
0x7EfSi^ сложная полезная нагрузка должна успешно работать на разных аппарат-^й^лат(формах. Это очень удобно, если планируется использовать полезную на-
груП°дв> ^ет-ер охгенро(сйи;г^лОнрилегтель1еыйи^опакт • зОнлючпоттсебувктом,ичтомвигьолез-
может привести к значительному увеличению размера. Из-за ограничений в размерах полезная нагрузка для нескольких платформ обычно ограничена и относительно области применения, в основном служит для чего-то простого, например для вызова прерываний и останова системы.
В качестве примера представим, что у нас в зоне поражения используются четыре различные операционные среды. Три из них представляют собой устаревшие системы НР9000. Последняя система более новая и основана на платформе Intel x86. Для каждой из систем должен использоваться немного отличный вектор вторжения, но следует использовать одну и ту же полезную нагрузку, которая позволит завершить работу как систем HP, так и системы Intel.
Рассмотрим машинный язык для систем HP и Intel. Если мы планируем создать полезную нагрузку, которая будет осуществлять операцию перехода на одной платформе и продолжать исполнение на другой системе, то мы можем разделить полезную нагрузку на две части, как показано на 23.
Кроссплатформенный код должен или осуществлять переход, или продолжать исполнение, в зависимости от платформы. Для системы НР9000 следующий код яв

ляется условным переходом, который передает управление только на два слова вперед. На платформе Intel следующий код является командой jmp, которая передает управление на 64 байта вперед (т.е. 4 байта нужны для осуществления нашей кросс-платформенной операции перехода).
Рассмотрим другой пример, при котором атака проводится на компьютере, использующем платформы MIPS и Intel. Следующие байты представляют собой кроссплатформенный заголовочный код для платформ MIPS и Intel.
На платформе Intel первое слово 0x24OF обрабатывается как одна безопасная команда.
and al,0Fh
Второе слово 0x7350 обрабатывается как команда jmp на платформе Intel и осуществляет переход на 80 байт вперед. Поэтому мы можем начать наш код, специфический для платформы Intel, со смещением в 80 байт. С другой стороны, для платформы MIPS все 4 байт обрабатываются как безопасная команда li.
li register [15], 0x1750
Таким образом, код для платформы MIPS можно начинать непосредственно после общего заголовка. Это весьма полезные сведения для создания универсальных программ атаки.
Кроссплатформенные команды пор
При использовании команд пор, следует выбрать те из них, которые будут работать на нескольких платформах. Команда пор (0x90) для процессоров х86 преобразуется в безопасную команду на платформе HP. Таким образом, стандартная команда пор работает на обеих платформах. На платформе MIPS, поскольку используются 32-битовые команды, придется быть немного хитрее. Кроссплатформенная команда пор для платформ х86 и MIPS может представлять собой вариацию следующих байтов кода.
24
OF
90
90

Этот набор байтов на платформе MIPS загружает несколько раз в регистр 15 значение 0x9090, а на платформе Intel эти байты преобразуются в безопасную команду add, после которой следуют две команды пор. Очевидно, что создание универсальных команд пор для использования на разных платформах не составляет большой сложности.

Код пролога и эпилога для защиты функций
Несколько лет тому назад системные архитекторы, в том числе Криспин Коуэн (Crispin Cowan) и др., попытались решить проблему атак на переполнение буфера с помощью добавления кода, контролирующего стек программы. Во многих реализациях этой идеи использовался код пролога или эпилога функции. Во многих компиляторах существует возможность вызывать дополнительную конкретную функцию перед каждым вызовом любой функции. Обычно это использовалось для целей отладки. Однако при разумном использовании этой возможности вполне реально создать функцию, которая бы контролировала стек и гарантировала правильную работу всех остальных функций.
К сожалению, переполнение в буфере имеет множество непредвиденных последствий. Часто оно вызывает искажение данных в памяти, а память, как известно, является ключевым аспектом правильной работы программы. Очевидно, это означает, что любой дополнительный код, который предназначен для защиты программы от самой себя, теряет смысл. Установка дополнительных барьеров и ловушек в программе только усложняет создание средств для взлома программного обеспечения, но никоим образом не устраняет возможность создания этих средств (см. главу 2, "Шаблоны атак", в которой обсуждается, как в этом вопросе ошиблась компания Microsoft).
Кто-то может заявить, что подобные методы уменьшаю риск возникновения ошибок. С другой стороны, можно утверждать, что эти же методы создают ложное чувство безопасности, поскольку всегда найдется хакер, способный взломать эту защиту. Если при атаке на переполнение буфера предоставляется контроль над указателем, то переполнение буфера можно использовать для перезаписи других указателей функций и даже для непосредственного изменения кода (вспомните наши методы по созданию "трамплинов"). Существует и еще одна возможность путем переполнения буфера изменить какие-то критически важные структуры в памяти. Как мы уже продемонстрировали, значения в структурах памяти управляют правами доступа и параметрами вызова системных функций. Изменение любых этих данных может привести к возникновению бреши в системе безопасности и тогда останется очень мало шансов для оперативного блокирования подобных атак.
Устранение защиты с помощью сигнальных значений
Хорошо известным приемом для защиты от атак на переполнение буфера является применение сигнальных значений (canary value) в стеке. Этот прием открыл Крис-пин Коуэн (Crispin Cowan). При попытке организовать переполнение стека выполняется затирание сигнального значения. Если сигнальное значение не обнаруживается, то считается, что программа работает неправильно и выполняется немедленное завершение ее работы. Вообще, идея была хорошей. Однако проблема при защите стека состоит в том, что переполнение буфера не является по сути проблемой стека. При атаках на переполнение буфера используются указатели, но указатели могут находиться в куче, в стеке, в таблицах или в заголовках файлов. Успех атаки на переполнение буфера действительно зависит от получения контроля над указателем. Безусловно, что очень удобно получить непосредственный контроль над указателем команд, и это легко осуществляется с помощью стека. Но если "на пути стоит" сигнальное значение, то можно воспользоваться "другой дорогой". На самом деле, проблема переполнения буфера должна решаться путем создания более надежного кода,
а не добавлением дополнительных систем безопасности и ловушек в программу. Однако при наличии многочисленных уже существующих систем подобные решения, предназначенные для устранения проблем в готовых программах, представляют определенную ценность.
На 24 мы видим, что при переполнении буфера мы затираем сигнальное значение. Это означает провал атаки. Если мы не можем использовать буфер после сигнального значения, значит, в нашем распоряжении остаются только другие локальные переменные и указа­тель стека. Однако возможность контролировать какой-либо указатель, независимо от того, где он находится, уже гарантирует успех современных атак.
Рассмотрим функцию, в которой используется несколько локальных переменных. По крайней мере одна из них является указателем. Если мы способны провести переполнение по отношению к локальной переменной типа указатель, значит, у нас есть шансы на успех атаки.
Как видно на 25, если организовать переполнение в буфере В, можно исказить значение в указателе А. Управляя указателем, мы прошли только часть пути. Следующий вопрос в том, как указатель, который мы только что изменили, используется в коде? Если это указатель функции, значит, мы добились успеха. Эта функция может быть вызвана в дальнейшем, и если мы изменили ее адрес, то можем вызвать вместо нее свой код.
Другой вариант состоит в том, что указатель используется для обращения к данным (что более вероятно). Если в другой локальной переменной содержатся исходные данные для операции с указателем, то существует вероятность перезаписать интересующие данные по любому адресу в области памяти, выделенной для программы. Это можно использовать для "победы" над сигнальным значением, для получения контроля над адресом возврата или искажения значений указателей функций где-либо в программе. Для обхода сигнального значения, можно установить указатель А с указанием на стек и задать в исходном буфере адрес, который мы хотим разместить в стеке (26).
Идея искажения других указателей, а не адреса возврата, заслуживает наивысшей похвалы. Эта идея реализуется при проведении атак на переполнение буфера в куче и использовании объектов C+ + . Рассмотрим структуру, в которой хранятся указатели функций. Такие структуры существуют практически во всех областях системы. Используя наш предыдущий пример, мы можем указать на одну из этих структур и перезаписать в ней адрес. Затем вполне реально использовать одну из функций этой структуры для возврата назад в наш буфер. Если при вызове функции наш стек остается доступным, значит, мы получили полный контроль над ситуацией (28).
Безусловно, основная проблема этого метода заключается в том, чтобы гарантировать сохранение нашего буфера. Во многих программах используются таблицы переходов для вызова любой библиотечной функции. Если процедура, на которую проводится атака переполнения буфера, содержит библиотечные вызовы, то выбор цели атаки становится очевидным. Следует перезаписать указатель функции для любого библиотечного вызова, который используется после операции по переполнению буфера, но до возврата этой процедуры.

 

На главную | Содержание | Назад | Вперёд
 
Яндекс.Метрика