X

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

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

Теория

Чем больше вы пытаетесь разобраться в том, как работают компьютеры, тем чаще вы слышите термины «ядро» и «пользовательское пространство». Если поискать, что обозначает слово «ядро», можно найти следующее определение:

Ядро — это центральная часть операционной системы, которая контролирует абсолютно всё (процессор, память и т. д.) в системе. Всякий раз, когда программе нужно что-то сделать (например записать данные в файл, открыть сетевое подключение), она временно передает ядру управление работой, которой занималось пользовательское пространство.

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

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

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

Этот вид разделения привилегий широко используется в компьютерах. Однако, в то время как Бэтмен и Лига Справедливости имеют только две категории привилегий, у компьютеров четыре отдельных «кольца».

Прим. пер. Количество колец зависит от архитектуры процессора.

Кольца защиты

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

Проще всего рассматривать кольца защиты как уровни в видеоигре:

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

Марио потерпел неудачу с ошибкой «падение в яму»

Когда непривилегированное приложение попытается получить доступ к привилегированным данным, оно завершится с ошибкой и выдаст о ней отчёт. В дополнение к попытке доступа к привилегированной памяти или функциям из непривилегированной (пользовательской) среды, программы могут завершаться с ошибкой по множеству причин. Например, если программа пытается разделить что-то на ноль (что, как мы все знаем, не работает), программа завершится с ошибкой ArithmeticException.

Что на самом деле происходит, когда программа так завершается? Вот тут-то и появляются структурированные обработчики исключений (SEH). Прежде чем говорить о том, что такое структурированные обработчики исключений и как они работают, давайте в качестве примера рассмотрим серию фильмов Индиана Джонс:

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

В «Индиана Джонс: В поисках утраченного ковчега» ловушки расставлены по всем подземельям и пещерам, по которым Индиане и его команде приходится перемещаться. Эти ловушки были заложены за годы до того, как Индиана Джонс родился, и служат для защиты Золотой статуи, которая является целью приключения. Точно так же программы имеют свои собственные ловушки, от несоответствия привилегий и отказа в доступе до логических и арифметических ошибок, и у программ есть много способов предотвратить (обработать) ошибки или неправильные операции. Кроме того, программисты могут реализовывать пользовательские обработчики исключений с помощью блоков try-catch, которые пробуют выполнить блок кода и отлавливают любые ошибки или исключения для их обработки.

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

Структурная схема обработки исключений

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

Защита: стек с «канарейкой»

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

Такое название пошло из-за аналогии, которая используется для описания данного механизма защиты — канарейка в угольной шахте.

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

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

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

Стек с канарейкой

Такой способ защиты кажется довольно логичным, но он далёк от совершенства.

Атака: уязвимость форматированной строки

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

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

В многих языках программирования (здесь речь пойдёт о С) есть функция вывода данных — printf() (или что-то похожее). Эта функция используется для форматирования и вывода данных:

printf("formatting", variables)

Например, если нужно вывести целочисленную переменную intvar:

printf("%i", intvar)

В приведённом выше примере %i называется спецификатором типа, он определяет формат выводимой переменной. Существует много полезных спецификаторов, например %s, который задаёт строковое значение (просто текст, как hello, 123 и так далее). Однако для нас наиболее важными будут следующие:

  • %x — спецификатор типа для шестнадцатеричных значений;
  • %n — специальный спецификатор типа, который позволяет записывать данные в переменную, а не считывать из неё.

Кто-то, возможно, уже догадался, к чему это ведёт, но давайте рассмотрим небольшой пример:

Боб только что зарегистрировался в системе онлайн-банкинга UHB (UnHackable Bank). Когда Боб заходит на сайт UHB, его встречает форма «Введите номер вашего счёта». Когда Боб вводит номер своего счёта, банк отображает информацию о его счёте, и он может свободно проверять и переводить свои деньги. Однажды Боб заходит на сайт своего банка, но в поле «Введите номер вашего счёта» Боб случайно делает опечатку. Вместо того, чтобы получить сообщение «Неверный счёт», Боб попадает на счёт другого клиента и может просматривать и переводить деньги этого клиента.

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

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

Более того, даже просто возможность посмотреть произвольные данные из памяти является серьёзной проблемой — если злоумышленник имеет полный доступ к вызову printf(), он также может перезаписать данные, используя спецификатор формата %n. Это означает, что злоумышленник может скопировать и перезаписать канареек стека с правильным значением, а затем выполнить атаку ret2libc (описанную в первой статье) или перезаписать указатель возврата функции на какую-либо другую вредоносную программу.

Давайте посмотрим на небольшой пример:

Использование уязвимости форматированной строки

Примечание Стек в приведённом выше примере сокращён.

Теперь разберём диаграмму.

Во-первых, код, подверженный уязвимости:

char * inputvar = "";
char * stackvar = "STVR";
readuserinput (inputvar);
printf("%s", inputvar);

Сначала мы задаём две переменные строкового типа — inputvar и stackvar.

Затем мы считываем введённую пользователем информацию с помощью функции readuserinput() и сохраняем её в переменной inputvar. Потом мы используем функцию printf() со спецификатором формата %s и передаём inputvar.

Уязвимость в приведённом выше коде связана с тем, что пользователь может напрямую влиять на функцию printf() без каких-либо проверок спецификаторов типа.

Теперь взгляните на приведённую выше схему всей атаки. Когда пользователь вводит обычный текст, ничего особенного не происходит. Однако, когда пользователь вводит «%x %x %x», программа выводит содержимое стека.

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

Кроме того, если пользователь использует %n для перезаписи файла куки стека и указателя возврата, он сможет перенаправить программу на вредоносный код.

Атака: Перезапись SEH

Подобно атакам ret2libc, перезапись SEH (структурированного обработчика исключений) — это атака, связанная с перезаписью указателя так, чтобы он вёл на другую функцию. Однако в то время как атаки ret2libc были основаны на перезаписи указателя возврата для перенаправления программы на определенную функцию libc, перезапись SEH влияет на функцию, изменяя указатель обработчика исключения, а затем вызывает исключение, чтобы программа перешла к выполнению нашей вредоносной функции.

Вы можете провести аналогию с махинациями со страховками:

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

Подобно мошенничеству со страховкой Боба, перезапись SEH заставляет обработчик исключений переходить к программе злоумышленника, а не обрабатывать исключение должным образом. Такая атака может быть направлена на уязвимости форматированной строки или на переполнение буфера стека, поскольку указатель обработчика исключений — это просто адрес, который хранится в стеке. Чтобы воспользоваться уязвимостью, злоумышленнику потребуется перезаписать указатель обработчика исключения, указав на вредоносную программу, а не на фактический обработчик исключения. Затем пользователю потребуется вызвать исключение. Как только это исключение будет перехвачено, оно будет перенаправлено на вредоносную функцию, которая сможет отбросить исключение и выполнить своё назначение.

Перезапись SEH

Защита: DEP / NX

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

Принцип защиты DEP / NX можно объяснить на примере старой игры в палочки.

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

Теперь представьте игровое поле в своей голове (или посмотрите картинку ниже с wikihow):

Игра в палочки

Вместо точек и квадратов давайте представим, что каждый квадрат является областью памяти на вашем компьютере. Красные области — это области, которые позволяют выполнять данные, а зелёные области не позволяют выполнять данные.

Это именно то, что делают DEP / NX. Это просто механизмы, которые блокируют выполнение вредоносного кода в разных областях памяти. Например, в большинстве современных программ в стеке включена поддержка DEP, поэтому злоумышленники не могут выполнять вредоносную нагрузку через стек.

Заключение

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

Перевод статьи «Binary Exploitation ELI5 — Part 2»

Варвара Николаева

, последний центурион

Не смешно? А здесь смешно: @ithumor

Также рекомендуем:

Рубрика: Переводы