Часть 34
Last updated
Last updated
Давайте начнём с эксплуатации и возможного выполнения кода. Конечно, мы должны учитывать смягчения и учиться противостоять новым защитам, которые были добавлены. Иногда мы сможем избежать их, а иногда и нет. Основная идея состоит в том, чтобы обучаться постепенно. Давайте сначала рассмотрим некоторые важные определения.
ЧТО ТАКОЕ DEP ?
Предотвращение выполнения данных (DEP) представляет собой набор аппаратных и программных технологий, который выполняет дополнительные проверки в памяти, чтобы помочь избежать запуска в системе вредоносного кода. В SERVICE PACK 2 (SP2) MICROSOFT WINDOWS XP и в MICROSOFT WINDOWS XP TABLET PC EDITION 2005, как аппаратное, так программное обеспечение применяют DEP.
Основное преимущество DEP заключается в том, чтобы помочь избежать выполнения кода из страниц данных. Обычно, код по умолчанию не исполняется в куче или стеке. Аппаратное обеспечение DEP обнаруживает код, который запускается из этих областей и вызывает исключение, когда он пытается исполниться. Программное обеспечение DEP может помочь предотвратить то, что вредоносный код использует преимущество механизма обработки исключений WINDOWS.
Давайте оставим определение DEP компании MICROSOFT'у. На самом деле, существует несколько способов задействовать DEP. Один из таких способов, сделать это через Свойства Системы.
Сейчас я нахожусь в WINDOWS 10 и DEP включен для основных программ и сервисов. Это настройки по умолчанию. Это означает, что существуют программы, у которых DEP выключен по умолчанию.
Конечно, Вы можете изменить настройки на другую опцию, чтобы все программы имели включенный DEP, что, очевидно, помогает немного больше, чтобы избежать исполнение кода.
Помимо этой конфигурации системы, каждая программа можем активировать функцию DEP самостоятельно, используя API, который MICROSOFT предоставляет для этого.
В общем, скажем, что, защита DEP изменяет разрешения страниц, где хранятся данные, стек, куча и т.д. Чтобы избежать этого, мы можем исполнить код там.
Поскольку DEP управляется процессом, то у него есть несколько способов запуститься и это можно сделать прям во время выполнения. Мы можем увидеть список процессов с помощью утилиты PROCESS EXPLORER, у которой есть столбец, показывающий статус DEP для каждого процесса.
https://technet.microsoft.com/en-us/sysinternals/processexplorer.aspx
Вот нужная нам настройка. Мы должны запустить утилиту под пользователем Администратор. Мы добавляем столбец, сделав правый щелчок в панели и выбрав пункт SELECT COLUMNS.
Мы видим, что у большинства процессов режим DEP активирован, а у некоторых других процессов он отключен.
Очевидно, для системных процессов у процессов DEP всегда включен, у некоторых программ сторонних производителей защита так же включена, но есть процессы, где она отключена.
Хорошо, основной момент здесь такой, что даже если DEP включен, он не имеет большого значения, потому что его можно обойти. DEP усилится, когда он объединяется с другими защитами, которые мы увидим позже.
Один из основных способов обхода DEP является ROP или возвратно-ориентированное программирование.
Четверо исследователя из Калифорнийского Университета опубликовали статью под названием "RETURN-ORIENTED PROGRAMMING: SYSTEMS, LANGUAGES AND APPLICATIONS" где они показали способ обхода этой защиты. Грубо говоря, он заключается в выполнении фрагментов кода, которые уже существуют в самом коде программы. Таким образом нет необходимости инжектировать свой собственный код. Я постараюсь кратко описать технику и сделать пример приложения. Хотя я советую Вам прочитать оригинальную статью, где это все хорошо объясняется.
Необходимо получить маленькие фрагменты кода, оканчивающиеся инструкцией RET. В идеале, с одной ассемблерной инструкцией (в дополнении к RET), внутри программы, которую мы хотим использовать. Эти маленькие фрагменты кода называются гаджетами. С помощью этих фрагментов мы должны как с конструктором LEGO, создать код эксплоита или шеллкод. Как только эти фрагменты получены и правильно упорядочены, мы можем запустить их в установленном порядке. Как мы это сделаем? То, что эти гаджеты заканчиваются инструкцией RET не являются случайными. Этот метод заключается во вводе адресов гаджетов в стек в правильном порядком исполнения, так чтобы при выходе из исполняющейся функции адрес возврата возвращался в регистр %EIP, где эти гаджеты вызывались бы один за другим в правильном порядке.
Основной смысл заключается в том, что, когда DEP не включен, и например, мы перезаписываем адрес возврата при переполнении стека, то обычно мы переходим на инструкцию JMP ESP или CALL ESP, которая возвращает выполнение на стек и продолжает выполнять мой код, который расположен ниже инструкции JMP ESP.
Но основная идея ROP заключается в том, что вместо перехода на инструкцию JMP ESP, происходит переход на кусочки кода, называемые гаджетами, которые являются исполняемым кодом программы, которые завершаются инструкцией RET (гаджеты являются частью некоторого модуля, вот почему мы можем их запускать) и с помощью которых, вы можете по чуть-чуть делать вызовы некоторых API, например такие как VIRTUALPROTECT или VIRTUALALLOC, которые изменяют и дают разрешение на выполнения кода в стеке или куче, т.е там, где находится мой код и наконец происходит переходит на его выполнение.
Другими словами, если эксплоит, который перезаписывал адрес возврата для примера без DEP был:
"A" * 200 + АДРЕС_JMP_ESP + КОД ДЛЯ ВЫПОЛНЕНИЯ
То, теперь с DEP, в том же случае, код должен быть таким:
"A" * 200 + ROP + КОД ДЛЯ ВЫПОЛНЕНИЯ
Где ROP должен предоставить моему коду разрешение на выполнение.
Рассмотрим, пару примеров без защиты DEP.
Здесь у нас есть программа. Она имеет буфер длиной 30 десятичных байт, и получает строку в качестве аргумента, которая копируется с помощью функции STRCPY в буфер без проверки длины. Следовательно, аргумент вызывает переполнение буфера.
Также, программа загружает модуль под названием MYPEPE.DLL. Позже увидим, нужен ли программе этот модуль или нет.
Давайте откроем программу в ЗАГРУЗЧИКЕ IDA.
Мы видим, что программа имеет только два аргумента в функции MAIN - ARGC и ARGV. Мы знаем, что переменная ARGC - это количество аргументов, которые мы вводим через консоль. Поэтому, если их не два (имя исполняемого файла + второй аргумент после него через пробел) программа будет закрываться, так как условие не выполняется.
Если количество аргументов не равно 2, программа будет переходить в красный блок, и выводить сообщение об ошибке, и придёт к возврату без каких либо действий. А если количество аргументов корректно, т.е. равно 2, программа будет переходить в желтый блок, загрузит DLL и затем перейдёт в функцию SALUDA. Сейчас имена выглядят уродливо. Я иду в меню OPTIONS→DEMANGLENAMES→NAMES.
Если кто-то не помнит, переменная ARGC - это число аргументов, а переменная ARGV - это массив указателей. Каждый элемент указывает на строку, которая является аргументом. Другими словами, в случае:
По адресу 0x0040109C регистр ECX имеет значение переменной ARGV. Т.е. это массив указателей.
ARGV = [указатель_на_имя_исполняемого_файла, указатель_на_аргумент1, указатель_на_аргумент2…]
В IDA мы видим, что программа исполняет инструкцию SHL EAX, 0, другими словами сдвигает 0 байтов, оставляя регистр EAX равным, как и раньше, т.е. 4 в этом случае.
Затем выражение [ECX+EAX] будет возвращать указатель на аргумент. Если регистр EAX равен нулю, программа будет возвращать указатель на имя исполняемого файла. Если он будет равен 4, то поскольку каждый указатель имеет длину 4 байт, то программа будет читать указатель на следующий аргумент и так далее.
В этом случае, поскольку регистр EAX равен 4, то указатель на второй аргумент, который мы ввели, будет в регистре EDX, который передаётся как аргумент в функцию SALUDA.
В функции SALUDA, есть аргумент, который является указателем на аргумент и переменная, которая является буфером, куда будет скопирована строка.
Поскольку я скомпилировал программу с символами, IDA определяет текст как указатель, и что он указывает на строку (или массив символов).
Конечно, этот массив может иметь нужный размер, какой мы захотим, так как мы вводим его и нет никакого ограничения или проверки.
Давайте посмотрим буфер.
Поскольку я скомпилировал программу с символами, IDA обнаружила, что это буфер. Давайте посмотрим ссылки, где программа использует этот буфер.
Ссылками являются инструкции типа LEA, что является ещё одним ключом к разгадке, если мы чего-то не знаем. Кроме того, буфер используется в качестве назначения функции STRCPY, где он будет использоваться как целевой буфер. Затем буфер будет использоваться для печати его содержимого.
Поэтому давайте сделаем правый щелчок и выберем пункт ARRAY.
Здесь нет никаких сомнений. Ниже нет никаких переменных. Что есть ниже, так это переменные СОХРАНЕННЫЙ EBP и АДРЕС ВОЗВАРАТА. Таким образом, нет никаких сомнений, что буфер рассчитан IDA хорошо. (Кроме того, программа имеет символы, которые помогают IDA определить, что это буфер, даже без нашей помощи)
Поэтому, чтобы перезаписать адрес возврата, какого размера должны быть аргументы, которые мы должны ввести?
Я выбираю область, которую я собираюсь заполнить начиная с буфера, оставляя АДРЕС ВОЗВРАТА, делаю правый щелчок и выбираю пункт - ARRAY без соглашения, просто, чтобы увидеть размер, который должна иметь срока для переполнения.
Другими словами, если я введу 36 десятичных байт, я остановлюсь точно в нужном месте, чтобы перезаписать адрес возврата. Поэтому, если бы мой код был таким:
Предположительно, строка 0xCCCCCCCC просто должна перезаписать адрес возврата. Я мог бы попробовать эту строку. Для этого я устанавливаю IDA как JUST IN TIME DEBUGGER. Я открываю консоль с правами администратора и иду в папку, где находится исполняемый файл IDA.
-I# установит IDA как отладчик времени исполнения (0 - для выключения и 1 - для включения)
При запуске скрипта, я вижу, что он переходит к выполнению кода, по адресу 0xCCCCCCCC, который я помещаю в него же. Этим я перезаписываю адрес возврата.
Здесь, я вижу, что сейчас регистр ESP указывает чуть ниже значения 0xCCCCCCCC. Поэтому, если я добавлю больше кода ниже, и вместо перехода на адрес 0xCCCCCCCC, программа будет переходить на инструкцию JMP ESP, чтобы начать выполнять указанный код (Какие же хорошие были времена, когда не было DEP)
Я ищу в списке модулей библиотеку MYPEPE.DLL.
Мы сделаем щелчок правой кнопкой, чтобы проанализировать нашу библиотеку и загружаем символы для неё. Тем временем, в любом месте кода мы запускаем плагин KEYPATCHER и не принимая соглашение, мы видим, что инструкции JMP ESP соответствует последовательности байтов FF E4.
Когда анализ закончится, мы увидим, что в списке функций появляется функция MYPEPE. Я иду к одной из них.
Модуль выглядит хорошо. Давайте посмотрим, есть ли здесь инструкция JMP ESP?
Выбираем пункт SEARCH FOR→ SEQUENCE OF BYTES и вводим значение FF E4.
Здесь, по адресу 0x004010BA есть инструкция JMP ESP. Мы не можем использовать нуль, но поскольку система добавляет нуль в конце, когда программа исполняет функцию STRCPY, мы не будем добавлять нуль.
Проблема заключается в том, что инструкция JMP ESP служит только для перехода если мы добавим ещё код ниже. Но мы не можем добавить больше кода из-за того, что адрес инструкции JMP ESP заканчивается нулем. Так что мы будем переходить на инструкцию RET. Чуть ниже это указатель на нашу строку, которая была передана нами как аргумент.
Чуть ниже адреса возврата в стеке, у нас есть указатель на нашу текстовую строку. Поэтому, если мы перейдём к инструкции RET, программа будет возвращаться к моему коду, потому что эта инструкция RET вернет программу туда используя это указатель, как если бы это был снова адрес возврата.
Скажем так - "Это хороший RET". Давайте добавим его.
Давайте теперь попробуем с ним.
Я вижу, что программа уже совершает переход, чтобы выполнить мой код CCCCCCCC, который я добавил как шеллкод. Сейчас, я бы мог перестроить код, который я хотел бы туда поместить, и выполнить то, что захочу я захочу, потому что здесь нет DEP. Единственное, что у программы мало свободного места, потому что я выделил буфер только длиной 30 байт, который мешает мне делать большие вещи.
Я подготовил шеллкод, который запустит калькулятор:
import struct shellcode = "\xB8\x40\x50\x03\x78\xC7\x40\x04" + "calc" + "\x83\xC0\x04\x50\x68\x24\x98\x01\x78\x59\xFF\xD1"
fruta = shellcode + "A" * (36-len(shellcode)) + "\x3a\x10\x40"
#0x40103a ret
Программа будет "падать", но уже после запуска калькулятора, который является нашей целью.
В следующих частях, мы добавим немного больше упражнений. Некоторые из них будут с DEP. Мы также научимся работать с ROP и так будем идти шаг за шагом до самой победы.
До встрече в 35 части.
Автор оригинального текста — Рикардо Нарваха.
Перевод и адаптация на английский язык — IvinsonCLS.
Перевод и адаптация на русский язык — Яша Яшечкин.
Перевод специально для форума системного и низкоуровневого программирования - WASM.IN
12.03.2018