Добро пожаловать в первую часть серии статей о разработке эксплоитов Windows. В этой первой части я расскажу только об основах, необходимых для понимания содержания будущих публикаций, включая некоторый синтаксис ассемблера, структуру памяти Windows и использование отладчика. Это не будет всестороннее обсуждение по любой из этих тем, поэтому, если вы не знакомы с ассемблером или если что-то неясно после прочтения этого первого поста, я призываю вас взглянуть на различные ссылки на ресурсы, которые я предоставил на протяжении курса.
Мой план для оставшейся части этой серии состоит в том, чтобы пройтись по различным темам эксплоитов, от простых (прямая перезапись EIP) до более сложных (юникод, поиск яиц, обход ASLR, распыление кучи, эксплоиты драйверов устройств и так далее), используя реальные эксплоиты чтобы продемонстрировать каждый. Я действительно не знаю когда закончу этот курс, поэтому, когда я думаю о других темах, я просто буду продолжать писать статьи.
Назначение
Моя цель этой серии публикаций - представить концепции поиска и написания эксплоитов для приложений Windows в надежде, что специалисты по безопасности и ИТ, которые не имели большого технического знакомства с этими концепциями, могут заинтересоваться безопасностью программного обеспечения и применить свои навыки чтобы сделать программное обеспечение частного и общественного достояния более безопасным. Отказ от ответственности: Если вы человек, который хочет создавать эксплоиты для участия в незаконной или аморальной деятельности, покиньте эту страницу.
Я должен также упомянуть, что эти посты не предназначены для конкуренции с другими великолепными туториалами, такими как Corelan Team (https://www.corelan.be), The Grey Corner (http://www.thegreycorner.com/) и Fuzzy Security (http://www.fuzzysecurity.com/tutorials.html). Вместо этого, они призваны дополнить их и предоставить еще один ресурс для объяснений и примеров - если вы похожи на меня, у вас никогда не будет слишком много примеров. Я настоятельно рекомендую вам проверить эти другие замечательные сайты.
Что нам нужно?
Вот что вам нужно, если вы хотите следовать курсу:
Начало работы с Immunity Debugger
Давайте начнем с рассмотрения отладчика, поскольку мы потратим немало времени на его использование в этих туториалах. Я собираюсь в первую очередь использовать отладчик Immunity, потому что он бесплатный и имеет некоторые плагины и настраиваемые возможности сценариев, которые я планирую выделить по мере продвижения.
Я буду использовать Windows Media Player в качестве примера программы для представления отладчика Immunity. Если вы хотите продолжить, откройте Windows Media Player и отладчик Immunity. В Immunity нажмите File -> Attach и выберите имя приложения/процесса (в моем примере, wmplayer). Примечание: вы также можете запустить WMP напрямую из Immunity, щелкнув File -> Open и выбрав исполняемый файл.
После того, как вы запустили исполняемый файл или присоединились к процессу в Immunity, вы должны перейти в представление центрального процессора (если его нет, нажмите Alt + C), которое выглядит так:
Когда вы запускаете/присоединяетесь к программе с помощью Immunity, она запускается в состоянии паузы (смотри правый нижний угол). Для запуска программы вы можете нажать F9 (или кнопку воспроизведения на панели инструментов). Чтобы перейти к следующей инструкции (но приостановить выполнение программы), нажмите F7. Вы можете использовать F7 для пошагового выполнения каждой инструкции. Если в любой момент вы хотите перезапустить программу, нажмите Ctrl + F2. Я не буду предоставлять полное руководство по использованию Immunity, но я постараюсь упомянуть любые соответствующие ярлыки и горячие клавиши, поскольку я представляю новые концепции в этой и будущих публикациях.
Как вы можете видеть, окно центрального процессора разбито на четыре панели, отображающие следующую информацию:
Давайте рассмотрим каждый из них более подробно, начиная с регистров.
Регистры центрального процессора
Регистры центрального процессора служат небольшими областями хранения, используемыми для быстрого доступа к данным. В архитектуре x86 (32-разрядной) имеется 8 регистров общего назначения: EAX, EBX, ECX, EDX, EDI, ESI, EBP и ESP. Технически они могут использоваться для хранения любых данных, хотя изначально они были спроектированы для выполнения конкретных задач, и во многих случаях до сих пор используются таким образом сегодня.
Вот немного подробной информации про каждый
EAX — Регистр аккумулятор
Он называется регистром аккумулятора, потому что это основной регистр, используемый для общих вычислений (таких как ADD и SUB). В то время как другие регистры могут использоваться для расчетов, регистру EAX был присвоен статус привилегированного, назначив ему более эффективные однобайтовые коды операций. Такая эффективность может быть важна, когда речь идет о написании эксплоита шеллкода для ограниченного доступного буферного пространства (подробнее об этом в будущих уроках!). В дополнение к его использованию в вычислениях, регистр EAX также используется для хранения возвращаемого значения функции.
На этот регистр общего назначения можно ссылаться полностью или частично следующим образом: регистр EAX относится к 32-битному регистру в целом. Регистр AX относится к наименее значимым 16 битам, которые могут быть дополнительно разбиты на AH (8 старших значащих бит AX) и AL (8 младших значащих бит).
Вот базовое визуальное представление:
Такое же полное/частичное 32-, 16- и 8-битное обращение также относится к следующим трем регистрам (EBX, ECX и EDX)
EBX — Регистр Базы
В 32-битной архитектуре, у регистра EBX нет особой цели, поэтому просто представьте, что это универсальное решение для доступного хранилища. Как и регистра EAX, на него можно ссылаться полностью (EBX) или частично (BX, BH, BL).
ECX - Регистр Счетчик
Как следует из названия, регистр счетчика часто используется в качестве счетчика повторений цикла и функции, хотя его также можно использовать для хранения любых данных. Как и регистр EAX, на него можно ссылаться полностью (ECX) или частично (CX, CH, CL).
EDX - Регистр Данных
EDX напоминает регистр регистр EAX. Он часто используется в математических операциях, таких как деление и умножение, чтобы справиться с переполнением, когда самые старшие биты будут храниться в регистре EDX, а наименее значимые - в регистре EAX. Он также обычно используется для хранения переменных функций. Как и регистр EAX, на него можно ссылаться полностью (EDX) или частично (DX, DH, DL).
ESI - Регистр Индекса Источника
В отличие от регистра EDI, регистр ESI часто используется для хранения указателя на место чтения. Например, если функция предназначена для чтения строки, регистр ESI будет содержать указатель на местоположение этой строки.
EDI - Регистр Индекса Назначения
Хотя он может быть (и используется) для общего хранения данных, регистр EDI был в первую очередь предназначен для хранения указателей хранения функций, таких как адрес записи строковой операции.
EBP - Указатель Базы
Регистр EBP используется для отслеживания базы/дна стека. Он часто используется для ссылки на переменные, расположенные в стеке, используя смещение к текущему значению регистра EBP, хотя, если на параметры ссылается только регистр, вы можете использовать регистр EBP для общих целей.
ESP - указатель стека
Регистр ESP используется для отслеживания вершины стека. По мере перемещения данных в стек и из стека регистр ESP соответственно увеличивается/уменьшается. Из всех регистров общего назначения регистр ESP редко/никогда не используется ни для чего, кроме его предназначения.
Указатель инструкций (EIP)
Не регистр общего назначения, но подходящий для этого, регистр EIP указывает на адрес памяти следующей инструкции, которая будет выполнена центральным процессором. Как вы увидите в следующих уроках, управляя значением регистра EIP, и вы сможете контролировать ход выполнения приложения (для выполнения кода по вашему выбору).
Сегментные регистры и регистр EFLAGS
Есть два дополнительных регистра, которые вы увидите на панели регистров: регистр сегментов и регистр EFLAGS. Я не буду подробно останавливаться на этом, но упомяну, что регистр EFLAGS состоит из серии флагов, представляющих логические значения, полученные в результате вычислений и сравнений, и может использоваться для определения того, когда и следует ли выполнять условные переходы (подробнее об этом позже).
Для получения дополнительной информации о регистрах центрального процессора, проверьте эти ресурсы:
Дамп памяти
Если перейти к панели Memory Dump в представлении центрального процессора, это просто место, где вы можете просмотреть содержимое ячейки памяти. Например, допустим, вы хотели просмотреть содержимое памяти регистра ESP, которое на следующем снимке экрана указывает на адрес 0007FF0C. Щелкните правой кнопкой мыши на ESP, выберите "Follow in Dump", и на панели Memory Dump отобразится это место.
Инструкции процессора
Как вы, наверное, знаете, большинство приложений сегодня написаны на языке высокого уровня (C, C++ и так далее). Когда приложение компилируется, эти инструкции языка высокого уровня переводятся в ассемблер, который имеет соответствующий опкод, чтобы помочь в дальнейшем преобразовать инструкцию в то, что машина может понять (машинный код). В отладчике вы можете просмотреть каждую ассемблерную инструкцию (и соответствующий опкод), обрабатываемый центральным процессором. Примечание: Для серии эксплойтов Windows я буду использовать синтаксис Intel на языке ассемблера x86 (http://en.wikipedia.org/wiki/X86_assembly_language#Syntax).
Вы можете пошагово проходить последовательность выполнения программы (F7) и видеть результат каждой инструкции процессора. Давайте посмотрим на первый набор инструкций для Windows Media Player. Программа начинается с паузы. Нажмите F7 несколько раз, чтобы выполнить первые несколько инструкций, пока не дойдете до второй инструкции MOV DWORD PTR SS: (выделено на скриншоте ниже). Инструкция MOV копирует элемент данных из одного места в другое.
Эта инструкция собирается переместить содержимое регистра EBX в область адреса памяти, на которую указывает регистр EBP - 18 (помните, что с синтаксисом Intel x86 это MOV [DST] [SRC]). Обратите внимание, что регистр EBP (указатель базы стека) указывает на адрес 0007FFC0. Используя калькулятор Windows или Mac (в научном/программном режиме), рассчитайте адрес 0007FFC0 - 0x18. Результат должен быть 0x7FFA8, что означает, что содержимое регистра EBP будет помещено в расположение адреса 0007FFA8. На самом деле, вам не нужно рассчитывать это вне Immunity. Обратите внимание на подокно в нижней части панели команд процессора. Оно уже сообщает вам значение регистра EBX, а также значение 0007FFC0 - 0x18 и текущее содержимое этой ячейки памяти (F4C47D04). Вы можете щелкнуть правой кнопкой мыши по строке "Stack" в этом подокне и выбрать "Follow address in Dump", чтобы проверить содержимое этой ячейки памяти.
Теперь снова нажмите F7, чтобы выполнить инструкцию. Обратите внимание, что ячейка памяти 0007FFA8 теперь имеет значение 00000000, поскольку содержимое регистра EBX было перемещено туда.
Это был лишь быстрый пример того, как вы можете следить за выполнением каждой инструкции центрального процессора в Immunity. Вот еще несколько общих инструкций по ассемблеру и синтаксису, с которыми вы можете столкнуться:
Я, конечно, не эксперт, но когда дело доходит до понимания и, в конечном итоге, разработки собственного кода эксплоТта, вы должны иметь довольно твердое понимание ассемблера. По мере продвижения я буду обсуждать еще несколько инструкций ассемблера, но я не планирую углубленно освещать язык ассемблера, поэтому, если вам понадобится переподготовка, есть множество хороших онлайн-ресурсов, включая:
Если вы хотите приобрести книгу, вы можете подумать об этой- Hacking: The Art of Exploitation, которая не только охватывает основы ассемблера, но и помогает в написании эксплоитов (хотя в основном в среде Linux).
В этой серии постов я постараюсь объяснить все примеры кода, которые я использую, поэтому, если у вас есть хотя бы какое-то базовое понимание ассемблера, вам будет хорошо.
Раскладка памяти Windows
Прежде чем мы поговорим о стеке, я хочу кратко рассказать о структуре памяти процесса Win32. Я должен сказать заранее, что это будет введение очень высокого уровня и не будет охватывать такие понятия, как рандомизация адресного пространства (ASLR), трансляция виртуальных адресов в физические, пейджинг, расширение физических адресов и так далее. Я планирую осветить некоторые из этих тем в следующей части, но сейчас я хочу, чтобы все было очень просто.
Во-первых, с отладчиком Immunity, подключенным к Windows Media Player, взгляните на карту памяти, нажав ALT + M (в качестве альтернативы вы можете выбрать View->Memory или щелкнуть значок "M" на панели инструментов).
Вам должно быть представлено что-то похожее на следующее (точные записи могут отличаться):
Это раскладка памяти wmplayer.exe, включая стек, кучу, загруженные модули (DLL) и сам исполняемый файл.Я представлю каждый из этих элементов более подробно, используя слегка упрощенную версию карты памяти, которую можно найти в великолепном вводном учебном пособии Корелана по переполнению стека, которое я сопоставил с картой памяти отладчика Immunity проигрывателя Windows Media.
Давайте продолжим работу снизу, начиная с части памяти от 0xFFFFFFFF до 0x7FFFFFFF, которую часто называют "уровнем ядра".
Уровень Ядра
Эта часть памяти зарезервирована ОС для драйверов устройств, системного кэша, выгружаемого/невыгружаемого пула, HAL и так далее. У пользователя нетт доступа к этой части памяти. Примечание: для подробного объяснения управления памятью в Windows вам следует ознакомиться с книгами по внутренним компонентам Windows (в настоящее время это два тома).
PEB и TEB
Когда вы запускаете программу/приложение, запускается экземпляр этого исполняемого файла, известного как процесс. Каждый процесс предоставляет ресурсы, необходимые для запуска экземпляра этой программы. Каждый процесс Windows имеет структуру исполнительного процесса (EPROCESS), которая содержит атрибуты процесса и указатели на связанные структуры данных. Хотя большинство этих структур EPROCESS находятся в ядре, блок Process Environment Block (PEB) находится в доступной для пользователя памяти. PEB содержит различные параметры пользовательского режима о запущенном процессе. Вы можете использовать WinDbg, чтобы легко изучить содержимое PEB, введя команду !peb.
Как видите, PEB включает в себя такую информацию, как базовый адрес образа (исполняемый файл), расположение кучи, загруженные модули (DLL) и переменные среды (операционная система, соответствующие пути и так далее). Посмотрите на ImageBaseAddress на приведенном выше скриншоте WinDbg. Обратите внимание на адрес 01000000. Теперь вернитесь к предыдущей диаграмме карты памяти Win32 и обратите внимание, что это значение совпадает с самым первым адресом в callout отладчика Immunity в блоке "Образа программы". Вы можете сделать то же самое для адреса кучи и связанных DLL.
Небольшое примечание о файлах символов ... особенно полезно загружать соответствующие файлы символов при отладке приложений Windows, поскольку они предоставляют полезную описательную информацию для функций, переменных и так далее. Вы можете сделать это в WinDbg, перейдя в "File –> Symbol File Path…". Следуйте инструкциям, найденным здесь: http://support.microsoft.com/kb/311503. Вы также можете загрузить файлы символов в Immunity, перейдя в "Debug –> Debugging Symbol Options".
Более подробную информацию обо всей структуре PEB можно найти здесь (http://msdn.microsoft.com/en-us/library/windows/desktop/aa813706(v=vs.85).aspx).
Программа или процесс может иметь один или несколько потоков, которые служат базовым модулем, которому операционная система выделяет процессорное время. Каждый процесс начинается с одного потока (основного потока), но может создавать дополнительные потоки по мере необходимости. Все потоки используют одно и то же виртуальное адресное пространство и системные ресурсы, выделенные родительскому процессу. Каждый поток также имеет свои собственные ресурсы, включая обработчики исключений, приоритеты, локальное хранилище и так далее. Как и у каждой программы/процесса есть PEB, так и у каждого потока есть блок среды потока (TEB). TEB хранит контекстную информацию для загрузчика образа и различных библиотек DLL Windows, а также расположение списка обработчиков исключений (о чем мы подробно расскажем в следующем посте). Как и PEB, TEB находится в адресном пространстве процесса, поскольку компоненты пользовательского режима требуют доступа с возможностью записи.
Вы также можете просматривать TEB с помощью WinDbg.
Более подробную информацию о всей структуре TEB можно найти здесь (http://msdn.microsoft.com/en-us/library/windows/desktop/ms686708(v=vs.85).aspx), а более подробную информацию о процессах и потоках можно найти здесь (http://msdn.microsoft.com/en-us/library/windows/desktop/ms681917(v=vs.85).aspx).
DLL
Программы Windows используют преимущества библиотек с общим кодом, которые называются динамическими библиотеками (DLL), что обеспечивает эффективное повторное использование кода и распределение памяти. Эти библиотеки DLL (также известные как модули или исполняемые модули) занимают часть пространства памяти. Как показано на скриншоте карты памяти, вы можете просмотреть их в Immunity в представлении Memory (Alt + M) или, если вы хотите просматривать только DLL, вы можете выбрать представление Executable Module (Alt + E). Существуют модули ОС/системы (ntdll, user32 и так далее), А также модули для конкретных приложений, и последние часто полезны при создании эксплоитов с переполнением (будет рассмотрено в будущих публикациях).
Вот скриншот представления "Памяти" в "Immunity":
Образ программы
Часть памяти образа программы находится там, где находится исполняемый файл. Это включает в себя секцию .text (содержащий исполняемый код/инструкции центрального процессора), секцию .data (содержащий глобальные данные программы) и секцию .rsrc (содержит неисполняемые ресурсы, включая значки, изображения и строки).
Куча
Куча - это динамически выделяемая (например c помощью функции malloc()) часть памяти, которую программа использует для хранения глобальных переменных. В отличие от стека, выделение кучи памяти должно управляться приложением. Другими словами, эта память будет выделяться до тех пор, пока она не будет освобождена программой или сама программа не завершит работу. Вы можете думать о куче как об общем пуле памяти, тогда как стек, о котором мы поговорим далее, более организован и разделен. Я пока не буду слишком углубляться в кучу, но планирую рассказать об этом позже в статье о переполнении кучи.
Стек
В отличие от кучи, где выделение памяти для глобальных переменных является относительно произвольным и постоянным, стек используется для выделения краткосрочного хранилища для локальных (функций/методов) переменных упорядоченным образом, и эта память впоследствии освобождается по окончании заданного функция. Вспомните, как данный процесс может иметь несколько потоков. Каждому потоку/функции выделяется свой собственный кадр стека. Размер этого стекового фрейма фиксируется после создания, а стековый фрейм удаляется при завершении функции.
PUSH и POP
Прежде чем мы рассмотрим, как функции назначается кадр стека, давайте кратко рассмотрим некоторые простые инструкции PUSH и POP, чтобы вы могли увидеть, как данные помещаются в стек и удаляются из него. Стек - это структура "первым пришел - первым вышел" (LIFO), то есть последний элемент, который вы положили в стек, - это первый элемент, который вы снимаете. Вы "кладете" предметы на вершину стека и "выталкиваете" предметы с верха стека.
Давайте посмотрим на это в действии …
На следующем снимке экрана вы увидите ряд инструкций PUSH на панели инструкций ЦП (вверху слева), каждая из которых будет принимать значение из одного из регистров (верхняя правая панель) и помещать это значение поверх стека (нижняя правая панель).
Давайте начнем с первой инструкции PUSH (PUSH ECX).
Обратите внимание на значение регистра ECX, а также адрес и значение вершины стека (нижний правый угол предыдущего снимка экрана). Теперь инструкция PUSH ECX выполняется …
После первой инструкции PUSH ECX значение из регистра ECX (адрес 0012E6FC) было помещено в верхнюю часть стека (как показано на снимке экрана выше). Обратите внимание, как адрес вершины стека уменьшился на 4 байта (с 0012E650 до 0012E64C). Это иллюстрирует, как стек растет вверх к более низким адресам, когда элементы помещаются в него. Также обратите внимание, что регистр ESP указывает на вершину стека, а регистр EBP указывает на основание этого кадра стека. На следующих снимках экрана вы заметите, что регистр EBP (базовый указатель) остается постоянным, в то время как регистр ESP (указатель стека) смещается по мере роста и сжатия стека. Теперь будет выполнена вторая инструкция PUSH ECX ...
Еще раз, значение из регистра ECX (0012E6FC) было помещено в вершину стека, регистр ESP скорректировал его значение еще на 4 байта, и, как вы можете видеть на скриншоте выше, последняя инструкция PUSH (PUSH EDI) вот-вот будет выполнена.
Теперь значение из регистра EDI (41414139) было перенесено в верхнюю часть стека, и вскоре должна быть выполнена следующая инструкция в списке (инструкция MOV), а значение регистра EDI изменилось. Давайте перейдем к инструкции POP EDI, чтобы показать, как элементы удаляются из стека. В этом случае текущее значение на вершине стека (41414139) будет вытолкнуто и помещено в EDI.
Как видите, значение EDI изменилось обратно на 41414139 после инструкции POP. Теперь, когда у вас есть представление о том, как манипулировать стеком, давайте посмотрим, как создаются фреймы стека для функций и как локальные переменные помещаются в стек. Понимание этого будет иметь решающее значение, когда мы перейдем к переполнению на основе стека во второй части этой серии.
Фреймы стека и функции
Когда выполняется программная функция, создается стековый фрейм для хранения ее локальных переменных. Каждая функция получает свой собственный кадр стека, который помещается поверх текущего стека и заставляет стек увеличиваться вверх до более низких адресов.
Каждый раз, когда создается кадр стека, выполняется серия инструкций для сохранения аргументов функции и адреса возврата (чтобы программа знала, куда идти после завершения функции), сохранения базового указателя текущего кадра стека и резервирования места для любые локальные переменные функции. [примечание: я намеренно опускаю обработчики исключений для этого базового обсуждения, но расскажу о них в следующем посте].
Давайте посмотрим на создание стекового фрейма с помощью одной из самых простых функций, которые я смог найти (из Википедии):
Этот код просто вызывает функцию foo(), передавая ей один параметр аргумента командной строки (argv [1]). Затем функция foo() объявляет переменную c длиной 12, которая резервирует необходимое место в стеке для хранения argv [1]. Затем он вызывает функцию strcpy(), которая копирует значение argv[1] в переменную c. Как говорится в комментарии, проверка границ не выполняется, поэтому использование strcpy может привести к переполнению буфера, что я продемонстрирую во второй части этой серии. А пока давайте просто сосредоточимся на том, как эта функция влияет на стек.
Я скомпилировал эту программу c (как stack_demo.exe), используя командную строку Visual Studio (2010), чтобы точно показать, как она выглядит при выполнении в отладчике. Вы можете запустить программу с аргументами командной строки непосредственно из отладчика Immunity, выбрав File–> Open (или просто нажав F3), выбрав свой исполняемый файл и введя аргументы командной строки в данное поле.
Для этого примера я просто использовал 11 символов А для argv[1].[Мы рассмотрим, что происходит, когда вы используете более 11 символов во второй части!]
Поскольку адреса могут меняться, лучший способ найти наш соответствующий программный код - выбрать "Просмотр" -> "Исполняемые модули" (или Alt + E). Затем дважды щелкните по модулю stack_demo.exe (или как вы назвали свой .exe).
Это должно привести вас к следующему:
Первая строка, которую вы видите - это начало функции foo(), но сначала мы рассмотрим main().Я установил несколько точек останова, чтобы помочь пройти по коду (обозначен голубым цветом), и вы можете сделать то же самое, выбрав нужный адрес и нажав F2. Давайте посмотрим на main () …
Хотя main () не делает ничего, кроме вызова функции foo(), есть пара вещей, которые должны произойти в первую очередь, как вы увидите в отладчике. Во-первых, он помещает содержимое Argv [1] (AAAAAAAAAAAAA) в стек. Затем, когда вызывается функция foo(), адрес возврата сохраняется в стеке, поэтому выполнение программы может возобновиться в нужном месте после завершения функции foo().
Посмотрите на скриншот с Immunity, который я прокомментировал соответствующим образом - просто обратите внимание на то, что сейчас в красной рамке; Я расскажу о некоторых других инструкциях в ближайшее время. Вы увидите, что указатель на argv[1] помещается в стек непосредственно перед вызовом функции foo(). Затем выполняется инструкция CALL, и адрес возврата следующей инструкции (EIP + 4) также помещается в стек.
Если вы хотите доказать, что адрес 00332FD4 содержит 0033301C, который является указателем на argv [1], обратитесь к содержимому дампа этого адреса:
Вы увидите содержимое, написанное задом наперед как 1C303300. Позвольте мне воспользоваться этой возможностью, чтобы быстро охватить нотацию Little Endian. "Endianness" относится к порядку, в котором байты хранятся в памяти. Системы на базе Intel x86 используют нотацию Little Endian, в которой младший байт значения хранится по наименьшему адресу памяти (поэтому адрес хранится в обратном порядке). В качестве примера смотри приведенный выше снимок экрана с шестнадцатеричным дампом: адрес вверху (00332FD4) самый маленький, а адрес внизу (00333034) самый большой. Таким образом, байт в верхнем левом углу (в настоящее время занятый 1C) занимает наименьшее расположение адреса, и адреса увеличиваются при перемещении слева направо и сверху вниз. Когда вы смотрите на адрес, такой как 0033301C, младший байт - это байт (1C). Чтобы преобразовать его в нотацию с прямым порядком байтов, вы переупорядочиваете его по одному байту справа налево. Вот изображение:
Итак, argv [1] и адрес возврата теперь помещены в стек, и была вызвана функция foo(). Вот посмотрите на стек с выделенными соответствующими частями.
Обратите внимание на указатель на argv[1] по адресу 0012FF74 и прямо над ним сохраненное значение RETURN. Если вы вернетесь к предыдущему снимку экрана main(), вы заметите, что адрес RETURN 0040103F является следующей инструкцией после CALL foo(), где выполнение программы будет возобновлено после завершения foo().
Теперь давайте посмотрим на функцию foo():
Когда вызывается функция foo(), первое, что происходит - текущий базовый указатель (EBP) сохраняется в стеке с помощью инструкции PUSH EBP, чтобы после завершения функции можно было восстановить базу стека для main().
Затем регистр EBP устанавливается равным регистру ESP (через инструкцию MOV EBP, ESP), делая верх и низ стека фрейма равными. Отсюда, регистр EBP останется постоянным (на весь срок действия функции foo), а регистр ESP увеличится до более низкого адреса, когда данные будут добавлены в кадр стека функции. Вот просмотр регистров до и после, показывающий, что регистр EBP теперь равен регистр ESP.
Далее, место зарезервировано для локальной переменной c (char c [12]) с помощью следующей инструкции: SUB ESP, 10.
Вот просмотр стека после этой серии инструкций:
Обратите внимание, как вершина стека (и, как следствие, ESP) изменилась с 0012FF6C на 0012FF5C.
Давайте перейдем к вызову strcpy(), который скопирует содержимое argv [1](AAAAAAAAAAAAA) в пространство, которое было только что зарезервировано в стеке для переменной c. Посмотрите на функцию в отладчике. Я выделил только ту часть, которая выполняет запись в стек.
На следующих снимках экрана вы заметите, что он продолжает перебирать значение argv[1], записывая в зарезервированное пространство в стеке (сверху вниз зарезервированное пространство), пока не будет записан весь argv [1] в стек.
Прежде чем мы рассмотрим, что происходит со стеком, когда функция завершается, вот еще один пошаговый наглядный пример, который подкрепляет шаги, выполняемые при вызове функции foo().
После того, как функция strcpy() завершена и функция foo() готова к завершению, в стеке должна произойти некоторая очистка. Давайте посмотрим на стек, поскольку функция foo() готовится к завершению, и выполнение программы возвращается к main ().
Как видите, первая выполняемая инструкция - это MOV ESP, EBP, которая помещает значение регистр EBP в регистр ESP, так что теперь он указывает на 0012FF6C, и эффективно удаляет переменную c (AAAAAAAAAAA) из стека. Вершина стека теперь содержит сохраненный EBP:
Когда следующая инструкция, POP EBP, будет выполнена, она восстановит предыдущий базовый указатель стека из main() и увеличит ESP на 4. Указатель стека теперь указывает на значение RETURN, помещенное в стек непосредственно перед вызовом foo (). Когда инструкция RETN выполнена, она вернет поток выполнения программы к следующей инструкции в main() сразу после инструкции CALL foo(), как показано на снимке экрана ниже.
Функция main() выполнит свою собственную очистку, переместив указатель стека вниз по стеку (увеличив его значение на 4) и очистив стек argv[1]. Затем он очистит регистр, который использовался для хранения argv [1] (EAX) через XOR, восстановления сохраненного EBP и возврата к сохраненному обратному адресу.
Этого должно быть достаточно, чтобы понять, как создается/удаляется кадр стека функций и как локальные переменные хранятся в стеке. Если вы хотите больше примеров, я рекомендую вам ознакомиться с некоторыми другими замечательными учебниками (особенно теми, которые опубликованы Кореланом).
Заключение
Это конец первой части серии статей об эксплоитах Windows. Надеемся, что вы уже знакомы с использованием отладчика, можете узнать некоторые основные инструкции ассемблера и понять (на высоком уровне), как Windows управляет памятью, а также как работает стек. В следующем посте мы рассмотрим ту же базовую функцию foo(), чтобы представить концепцию переполнения на основе стека. Затем я сразу же напишу реальный пример использования уязвимости для реального уязвимого программного продукта.
Я надеюсь, что этот первый пост был ясным, точным и полезным. Если у вас есть какие-либо вопросы, комментарии, исправления или предложения по улучшению, не стесняйтесь оставлять мне отзывы в разделе "Комментарии".
Источник: http://www.securitysift.com/windows-exploit-development-part-1-basics/
Автор перевода: yashechka
Переведено специально для портала xss.pro (c)
Мой план для оставшейся части этой серии состоит в том, чтобы пройтись по различным темам эксплоитов, от простых (прямая перезапись EIP) до более сложных (юникод, поиск яиц, обход ASLR, распыление кучи, эксплоиты драйверов устройств и так далее), используя реальные эксплоиты чтобы продемонстрировать каждый. Я действительно не знаю когда закончу этот курс, поэтому, когда я думаю о других темах, я просто буду продолжать писать статьи.
Назначение
Моя цель этой серии публикаций - представить концепции поиска и написания эксплоитов для приложений Windows в надежде, что специалисты по безопасности и ИТ, которые не имели большого технического знакомства с этими концепциями, могут заинтересоваться безопасностью программного обеспечения и применить свои навыки чтобы сделать программное обеспечение частного и общественного достояния более безопасным. Отказ от ответственности: Если вы человек, который хочет создавать эксплоиты для участия в незаконной или аморальной деятельности, покиньте эту страницу.
Я должен также упомянуть, что эти посты не предназначены для конкуренции с другими великолепными туториалами, такими как Corelan Team (https://www.corelan.be), The Grey Corner (http://www.thegreycorner.com/) и Fuzzy Security (http://www.fuzzysecurity.com/tutorials.html). Вместо этого, они призваны дополнить их и предоставить еще один ресурс для объяснений и примеров - если вы похожи на меня, у вас никогда не будет слишком много примеров. Я настоятельно рекомендую вам проверить эти другие замечательные сайты.
Что нам нужно?
Вот что вам нужно, если вы хотите следовать курсу:
- Установка Windows: Я планирую начать с Windows XP с пакетом обновления 3 (SP3), но по мере продвижения и освещения различных тем/эксплоитов я также могу использовать другие версии, включая Windows 7 и Windows Server 2003/2008.
- Отладчик: На хосте Windows вам также понадобится отладчик. В первую очередь я буду использовать Immunity Debugger, который вы можете скачать здесь (http://debugger.immunityinc.com/ID_register.py). Вы также должны получить плагин Мона, который можно найти здесь (http://redmine.corelan.be/projects/mona). Я также буду использовать WinDbg для некоторых моих примеров. Инструкции по загрузке можно найти здесь (https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/?redirectedfrom=MSDN) (прокрутите страницу вниз для более ранних версий Windows).
- Хост Backtrack/Kali (необязательно): я использую Kali для всех своих сценариев, а также планирую использовать его в качестве "атакующей машины' во всех примерах удаленноЙ эксплуатации, которые я использую. Я планирую использовать Perl и Python для большинства моих сценариев, поэтому вы можете вместо этого установить любую языковую среду на хост Windows.
Начало работы с Immunity Debugger
Давайте начнем с рассмотрения отладчика, поскольку мы потратим немало времени на его использование в этих туториалах. Я собираюсь в первую очередь использовать отладчик Immunity, потому что он бесплатный и имеет некоторые плагины и настраиваемые возможности сценариев, которые я планирую выделить по мере продвижения.
Я буду использовать Windows Media Player в качестве примера программы для представления отладчика Immunity. Если вы хотите продолжить, откройте Windows Media Player и отладчик Immunity. В Immunity нажмите File -> Attach и выберите имя приложения/процесса (в моем примере, wmplayer). Примечание: вы также можете запустить WMP напрямую из Immunity, щелкнув File -> Open и выбрав исполняемый файл.
После того, как вы запустили исполняемый файл или присоединились к процессу в Immunity, вы должны перейти в представление центрального процессора (если его нет, нажмите Alt + C), которое выглядит так:
Когда вы запускаете/присоединяетесь к программе с помощью Immunity, она запускается в состоянии паузы (смотри правый нижний угол). Для запуска программы вы можете нажать F9 (или кнопку воспроизведения на панели инструментов). Чтобы перейти к следующей инструкции (но приостановить выполнение программы), нажмите F7. Вы можете использовать F7 для пошагового выполнения каждой инструкции. Если в любой момент вы хотите перезапустить программу, нажмите Ctrl + F2. Я не буду предоставлять полное руководство по использованию Immunity, но я постараюсь упомянуть любые соответствующие ярлыки и горячие клавиши, поскольку я представляю новые концепции в этой и будущих публикациях.
Как вы можете видеть, окно центрального процессора разбито на четыре панели, отображающие следующую информацию:
- Инструкции центрального процессора - отображает адрес памяти, опкоды и ассемблерные инструкции, дополнительные комментарии, названия функций и другую информацию, связанную с инструкциями центрального процессора.
- Регистры - отображает содержимое регистров общего назначения, указатель команд и флаги, связанные с текущим состоянием приложения.
- Стек - показывает содержимое текущего стека
- Дамп памяти - показывает содержимое памяти приложения
Давайте рассмотрим каждый из них более подробно, начиная с регистров.
Регистры центрального процессора
Регистры центрального процессора служат небольшими областями хранения, используемыми для быстрого доступа к данным. В архитектуре x86 (32-разрядной) имеется 8 регистров общего назначения: EAX, EBX, ECX, EDX, EDI, ESI, EBP и ESP. Технически они могут использоваться для хранения любых данных, хотя изначально они были спроектированы для выполнения конкретных задач, и во многих случаях до сих пор используются таким образом сегодня.
Вот немного подробной информации про каждый
EAX — Регистр аккумулятор
Он называется регистром аккумулятора, потому что это основной регистр, используемый для общих вычислений (таких как ADD и SUB). В то время как другие регистры могут использоваться для расчетов, регистру EAX был присвоен статус привилегированного, назначив ему более эффективные однобайтовые коды операций. Такая эффективность может быть важна, когда речь идет о написании эксплоита шеллкода для ограниченного доступного буферного пространства (подробнее об этом в будущих уроках!). В дополнение к его использованию в вычислениях, регистр EAX также используется для хранения возвращаемого значения функции.
На этот регистр общего назначения можно ссылаться полностью или частично следующим образом: регистр EAX относится к 32-битному регистру в целом. Регистр AX относится к наименее значимым 16 битам, которые могут быть дополнительно разбиты на AH (8 старших значащих бит AX) и AL (8 младших значащих бит).
Вот базовое визуальное представление:
Такое же полное/частичное 32-, 16- и 8-битное обращение также относится к следующим трем регистрам (EBX, ECX и EDX)
EBX — Регистр Базы
В 32-битной архитектуре, у регистра EBX нет особой цели, поэтому просто представьте, что это универсальное решение для доступного хранилища. Как и регистра EAX, на него можно ссылаться полностью (EBX) или частично (BX, BH, BL).
ECX - Регистр Счетчик
Как следует из названия, регистр счетчика часто используется в качестве счетчика повторений цикла и функции, хотя его также можно использовать для хранения любых данных. Как и регистр EAX, на него можно ссылаться полностью (ECX) или частично (CX, CH, CL).
EDX - Регистр Данных
EDX напоминает регистр регистр EAX. Он часто используется в математических операциях, таких как деление и умножение, чтобы справиться с переполнением, когда самые старшие биты будут храниться в регистре EDX, а наименее значимые - в регистре EAX. Он также обычно используется для хранения переменных функций. Как и регистр EAX, на него можно ссылаться полностью (EDX) или частично (DX, DH, DL).
ESI - Регистр Индекса Источника
В отличие от регистра EDI, регистр ESI часто используется для хранения указателя на место чтения. Например, если функция предназначена для чтения строки, регистр ESI будет содержать указатель на местоположение этой строки.
EDI - Регистр Индекса Назначения
Хотя он может быть (и используется) для общего хранения данных, регистр EDI был в первую очередь предназначен для хранения указателей хранения функций, таких как адрес записи строковой операции.
EBP - Указатель Базы
Регистр EBP используется для отслеживания базы/дна стека. Он часто используется для ссылки на переменные, расположенные в стеке, используя смещение к текущему значению регистра EBP, хотя, если на параметры ссылается только регистр, вы можете использовать регистр EBP для общих целей.
ESP - указатель стека
Регистр ESP используется для отслеживания вершины стека. По мере перемещения данных в стек и из стека регистр ESP соответственно увеличивается/уменьшается. Из всех регистров общего назначения регистр ESP редко/никогда не используется ни для чего, кроме его предназначения.
Указатель инструкций (EIP)
Не регистр общего назначения, но подходящий для этого, регистр EIP указывает на адрес памяти следующей инструкции, которая будет выполнена центральным процессором. Как вы увидите в следующих уроках, управляя значением регистра EIP, и вы сможете контролировать ход выполнения приложения (для выполнения кода по вашему выбору).
Сегментные регистры и регистр EFLAGS
Есть два дополнительных регистра, которые вы увидите на панели регистров: регистр сегментов и регистр EFLAGS. Я не буду подробно останавливаться на этом, но упомяну, что регистр EFLAGS состоит из серии флагов, представляющих логические значения, полученные в результате вычислений и сравнений, и может использоваться для определения того, когда и следует ли выполнять условные переходы (подробнее об этом позже).
Для получения дополнительной информации о регистрах центрального процессора, проверьте эти ресурсы:
Дамп памяти
Если перейти к панели Memory Dump в представлении центрального процессора, это просто место, где вы можете просмотреть содержимое ячейки памяти. Например, допустим, вы хотели просмотреть содержимое памяти регистра ESP, которое на следующем снимке экрана указывает на адрес 0007FF0C. Щелкните правой кнопкой мыши на ESP, выберите "Follow in Dump", и на панели Memory Dump отобразится это место.
Инструкции процессора
Как вы, наверное, знаете, большинство приложений сегодня написаны на языке высокого уровня (C, C++ и так далее). Когда приложение компилируется, эти инструкции языка высокого уровня переводятся в ассемблер, который имеет соответствующий опкод, чтобы помочь в дальнейшем преобразовать инструкцию в то, что машина может понять (машинный код). В отладчике вы можете просмотреть каждую ассемблерную инструкцию (и соответствующий опкод), обрабатываемый центральным процессором. Примечание: Для серии эксплойтов Windows я буду использовать синтаксис Intel на языке ассемблера x86 (http://en.wikipedia.org/wiki/X86_assembly_language#Syntax).
Вы можете пошагово проходить последовательность выполнения программы (F7) и видеть результат каждой инструкции процессора. Давайте посмотрим на первый набор инструкций для Windows Media Player. Программа начинается с паузы. Нажмите F7 несколько раз, чтобы выполнить первые несколько инструкций, пока не дойдете до второй инструкции MOV DWORD PTR SS: (выделено на скриншоте ниже). Инструкция MOV копирует элемент данных из одного места в другое.
Эта инструкция собирается переместить содержимое регистра EBX в область адреса памяти, на которую указывает регистр EBP - 18 (помните, что с синтаксисом Intel x86 это MOV [DST] [SRC]). Обратите внимание, что регистр EBP (указатель базы стека) указывает на адрес 0007FFC0. Используя калькулятор Windows или Mac (в научном/программном режиме), рассчитайте адрес 0007FFC0 - 0x18. Результат должен быть 0x7FFA8, что означает, что содержимое регистра EBP будет помещено в расположение адреса 0007FFA8. На самом деле, вам не нужно рассчитывать это вне Immunity. Обратите внимание на подокно в нижней части панели команд процессора. Оно уже сообщает вам значение регистра EBX, а также значение 0007FFC0 - 0x18 и текущее содержимое этой ячейки памяти (F4C47D04). Вы можете щелкнуть правой кнопкой мыши по строке "Stack" в этом подокне и выбрать "Follow address in Dump", чтобы проверить содержимое этой ячейки памяти.
Теперь снова нажмите F7, чтобы выполнить инструкцию. Обратите внимание, что ячейка памяти 0007FFA8 теперь имеет значение 00000000, поскольку содержимое регистра EBX было перемещено туда.
Это был лишь быстрый пример того, как вы можете следить за выполнением каждой инструкции центрального процессора в Immunity. Вот еще несколько общих инструкций по ассемблеру и синтаксису, с которыми вы можете столкнуться:
- ADD/SUB OP1, OP2 - добавить или вычесть два операнда, сохраняя результат в первом операнде. Это могут быть регистры, ячейки памяти (не более одного) или константы. Например, ADD EAX, 10 означает добавить 10 к значению EAX и сохранить результат в EAX
- XOR EAX, EAX - выполняет "исключающее или" регистра с самим собой - устанавливает его значение в ноль; простой способ очистки содержимого реестра
- INC/DEC OP1– увеличивать или уменьшать значение операнда на единицу
- CMP OP1, OP2 - сравнить значение двух операндов (регистр/адрес памяти/ константа) и установить соответствующее значение EFLAGS.
- Переход (JMP) и условный переход (JE, JZ и так далее) - как следует из названия, эти инструкции позволяют переходить в другое место в потоке выполнения/наборе команд. Инструкция JMP просто переходит в определенное место, тогда как условные переходы (JE, JZ и так далее) выполняются только при соблюдении определенных критериев (с использованием значений регистра EFLAGS, упомянутых ранее). Например, вы можете сравнить значения двух регистров и перейти к месту, если они оба равны (использует инструкцию JE и нулевой флаг (ZF) = 1).
- Когда вы видите значение в скобках, такое как ADD DWORD PTR [X] или MOV EAX, [EBX], оно ссылается на значение, сохраненное по адресу памяти X. Другими словами, EBX относится к содержимому EBX, тогда как [EBX] относится к значению, хранящемуся по адресу памяти в EBX.
- Соответствующие ключевые слова размера: BYTE = 1 байт, WORD = 2 байта, DWORD = 4 байта.
Я, конечно, не эксперт, но когда дело доходит до понимания и, в конечном итоге, разработки собственного кода эксплоТта, вы должны иметь довольно твердое понимание ассемблера. По мере продвижения я буду обсуждать еще несколько инструкций ассемблера, но я не планирую углубленно освещать язык ассемблера, поэтому, если вам понадобится переподготовка, есть множество хороших онлайн-ресурсов, включая:
- x86 Assembly Guide
- Sandpile.org
- The Art of Assembly Language Programming
- Windows Assembly Language Megaprimer
Если вы хотите приобрести книгу, вы можете подумать об этой- Hacking: The Art of Exploitation, которая не только охватывает основы ассемблера, но и помогает в написании эксплоитов (хотя в основном в среде Linux).
В этой серии постов я постараюсь объяснить все примеры кода, которые я использую, поэтому, если у вас есть хотя бы какое-то базовое понимание ассемблера, вам будет хорошо.
Раскладка памяти Windows
Прежде чем мы поговорим о стеке, я хочу кратко рассказать о структуре памяти процесса Win32. Я должен сказать заранее, что это будет введение очень высокого уровня и не будет охватывать такие понятия, как рандомизация адресного пространства (ASLR), трансляция виртуальных адресов в физические, пейджинг, расширение физических адресов и так далее. Я планирую осветить некоторые из этих тем в следующей части, но сейчас я хочу, чтобы все было очень просто.
Во-первых, с отладчиком Immunity, подключенным к Windows Media Player, взгляните на карту памяти, нажав ALT + M (в качестве альтернативы вы можете выбрать View->Memory или щелкнуть значок "M" на панели инструментов).
Вам должно быть представлено что-то похожее на следующее (точные записи могут отличаться):
Это раскладка памяти wmplayer.exe, включая стек, кучу, загруженные модули (DLL) и сам исполняемый файл.Я представлю каждый из этих элементов более подробно, используя слегка упрощенную версию карты памяти, которую можно найти в великолепном вводном учебном пособии Корелана по переполнению стека, которое я сопоставил с картой памяти отладчика Immunity проигрывателя Windows Media.
Давайте продолжим работу снизу, начиная с части памяти от 0xFFFFFFFF до 0x7FFFFFFF, которую часто называют "уровнем ядра".
Уровень Ядра
Эта часть памяти зарезервирована ОС для драйверов устройств, системного кэша, выгружаемого/невыгружаемого пула, HAL и так далее. У пользователя нетт доступа к этой части памяти. Примечание: для подробного объяснения управления памятью в Windows вам следует ознакомиться с книгами по внутренним компонентам Windows (в настоящее время это два тома).
PEB и TEB
Когда вы запускаете программу/приложение, запускается экземпляр этого исполняемого файла, известного как процесс. Каждый процесс предоставляет ресурсы, необходимые для запуска экземпляра этой программы. Каждый процесс Windows имеет структуру исполнительного процесса (EPROCESS), которая содержит атрибуты процесса и указатели на связанные структуры данных. Хотя большинство этих структур EPROCESS находятся в ядре, блок Process Environment Block (PEB) находится в доступной для пользователя памяти. PEB содержит различные параметры пользовательского режима о запущенном процессе. Вы можете использовать WinDbg, чтобы легко изучить содержимое PEB, введя команду !peb.
Как видите, PEB включает в себя такую информацию, как базовый адрес образа (исполняемый файл), расположение кучи, загруженные модули (DLL) и переменные среды (операционная система, соответствующие пути и так далее). Посмотрите на ImageBaseAddress на приведенном выше скриншоте WinDbg. Обратите внимание на адрес 01000000. Теперь вернитесь к предыдущей диаграмме карты памяти Win32 и обратите внимание, что это значение совпадает с самым первым адресом в callout отладчика Immunity в блоке "Образа программы". Вы можете сделать то же самое для адреса кучи и связанных DLL.
Небольшое примечание о файлах символов ... особенно полезно загружать соответствующие файлы символов при отладке приложений Windows, поскольку они предоставляют полезную описательную информацию для функций, переменных и так далее. Вы можете сделать это в WinDbg, перейдя в "File –> Symbol File Path…". Следуйте инструкциям, найденным здесь: http://support.microsoft.com/kb/311503. Вы также можете загрузить файлы символов в Immunity, перейдя в "Debug –> Debugging Symbol Options".
Более подробную информацию обо всей структуре PEB можно найти здесь (http://msdn.microsoft.com/en-us/library/windows/desktop/aa813706(v=vs.85).aspx).
Программа или процесс может иметь один или несколько потоков, которые служат базовым модулем, которому операционная система выделяет процессорное время. Каждый процесс начинается с одного потока (основного потока), но может создавать дополнительные потоки по мере необходимости. Все потоки используют одно и то же виртуальное адресное пространство и системные ресурсы, выделенные родительскому процессу. Каждый поток также имеет свои собственные ресурсы, включая обработчики исключений, приоритеты, локальное хранилище и так далее. Как и у каждой программы/процесса есть PEB, так и у каждого потока есть блок среды потока (TEB). TEB хранит контекстную информацию для загрузчика образа и различных библиотек DLL Windows, а также расположение списка обработчиков исключений (о чем мы подробно расскажем в следующем посте). Как и PEB, TEB находится в адресном пространстве процесса, поскольку компоненты пользовательского режима требуют доступа с возможностью записи.
Вы также можете просматривать TEB с помощью WinDbg.
Более подробную информацию о всей структуре TEB можно найти здесь (http://msdn.microsoft.com/en-us/library/windows/desktop/ms686708(v=vs.85).aspx), а более подробную информацию о процессах и потоках можно найти здесь (http://msdn.microsoft.com/en-us/library/windows/desktop/ms681917(v=vs.85).aspx).
DLL
Программы Windows используют преимущества библиотек с общим кодом, которые называются динамическими библиотеками (DLL), что обеспечивает эффективное повторное использование кода и распределение памяти. Эти библиотеки DLL (также известные как модули или исполняемые модули) занимают часть пространства памяти. Как показано на скриншоте карты памяти, вы можете просмотреть их в Immunity в представлении Memory (Alt + M) или, если вы хотите просматривать только DLL, вы можете выбрать представление Executable Module (Alt + E). Существуют модули ОС/системы (ntdll, user32 и так далее), А также модули для конкретных приложений, и последние часто полезны при создании эксплоитов с переполнением (будет рассмотрено в будущих публикациях).
Вот скриншот представления "Памяти" в "Immunity":
Образ программы
Часть памяти образа программы находится там, где находится исполняемый файл. Это включает в себя секцию .text (содержащий исполняемый код/инструкции центрального процессора), секцию .data (содержащий глобальные данные программы) и секцию .rsrc (содержит неисполняемые ресурсы, включая значки, изображения и строки).
Куча
Куча - это динамически выделяемая (например c помощью функции malloc()) часть памяти, которую программа использует для хранения глобальных переменных. В отличие от стека, выделение кучи памяти должно управляться приложением. Другими словами, эта память будет выделяться до тех пор, пока она не будет освобождена программой или сама программа не завершит работу. Вы можете думать о куче как об общем пуле памяти, тогда как стек, о котором мы поговорим далее, более организован и разделен. Я пока не буду слишком углубляться в кучу, но планирую рассказать об этом позже в статье о переполнении кучи.
Стек
В отличие от кучи, где выделение памяти для глобальных переменных является относительно произвольным и постоянным, стек используется для выделения краткосрочного хранилища для локальных (функций/методов) переменных упорядоченным образом, и эта память впоследствии освобождается по окончании заданного функция. Вспомните, как данный процесс может иметь несколько потоков. Каждому потоку/функции выделяется свой собственный кадр стека. Размер этого стекового фрейма фиксируется после создания, а стековый фрейм удаляется при завершении функции.
PUSH и POP
Прежде чем мы рассмотрим, как функции назначается кадр стека, давайте кратко рассмотрим некоторые простые инструкции PUSH и POP, чтобы вы могли увидеть, как данные помещаются в стек и удаляются из него. Стек - это структура "первым пришел - первым вышел" (LIFO), то есть последний элемент, который вы положили в стек, - это первый элемент, который вы снимаете. Вы "кладете" предметы на вершину стека и "выталкиваете" предметы с верха стека.
Давайте посмотрим на это в действии …
На следующем снимке экрана вы увидите ряд инструкций PUSH на панели инструкций ЦП (вверху слева), каждая из которых будет принимать значение из одного из регистров (верхняя правая панель) и помещать это значение поверх стека (нижняя правая панель).
Давайте начнем с первой инструкции PUSH (PUSH ECX).
Обратите внимание на значение регистра ECX, а также адрес и значение вершины стека (нижний правый угол предыдущего снимка экрана). Теперь инструкция PUSH ECX выполняется …
После первой инструкции PUSH ECX значение из регистра ECX (адрес 0012E6FC) было помещено в верхнюю часть стека (как показано на снимке экрана выше). Обратите внимание, как адрес вершины стека уменьшился на 4 байта (с 0012E650 до 0012E64C). Это иллюстрирует, как стек растет вверх к более низким адресам, когда элементы помещаются в него. Также обратите внимание, что регистр ESP указывает на вершину стека, а регистр EBP указывает на основание этого кадра стека. На следующих снимках экрана вы заметите, что регистр EBP (базовый указатель) остается постоянным, в то время как регистр ESP (указатель стека) смещается по мере роста и сжатия стека. Теперь будет выполнена вторая инструкция PUSH ECX ...
Еще раз, значение из регистра ECX (0012E6FC) было помещено в вершину стека, регистр ESP скорректировал его значение еще на 4 байта, и, как вы можете видеть на скриншоте выше, последняя инструкция PUSH (PUSH EDI) вот-вот будет выполнена.
Теперь значение из регистра EDI (41414139) было перенесено в верхнюю часть стека, и вскоре должна быть выполнена следующая инструкция в списке (инструкция MOV), а значение регистра EDI изменилось. Давайте перейдем к инструкции POP EDI, чтобы показать, как элементы удаляются из стека. В этом случае текущее значение на вершине стека (41414139) будет вытолкнуто и помещено в EDI.
Как видите, значение EDI изменилось обратно на 41414139 после инструкции POP. Теперь, когда у вас есть представление о том, как манипулировать стеком, давайте посмотрим, как создаются фреймы стека для функций и как локальные переменные помещаются в стек. Понимание этого будет иметь решающее значение, когда мы перейдем к переполнению на основе стека во второй части этой серии.
Фреймы стека и функции
Когда выполняется программная функция, создается стековый фрейм для хранения ее локальных переменных. Каждая функция получает свой собственный кадр стека, который помещается поверх текущего стека и заставляет стек увеличиваться вверх до более низких адресов.
Каждый раз, когда создается кадр стека, выполняется серия инструкций для сохранения аргументов функции и адреса возврата (чтобы программа знала, куда идти после завершения функции), сохранения базового указателя текущего кадра стека и резервирования места для любые локальные переменные функции. [примечание: я намеренно опускаю обработчики исключений для этого базового обсуждения, но расскажу о них в следующем посте].
Давайте посмотрим на создание стекового фрейма с помощью одной из самых простых функций, которые я смог найти (из Википедии):
Этот код просто вызывает функцию foo(), передавая ей один параметр аргумента командной строки (argv [1]). Затем функция foo() объявляет переменную c длиной 12, которая резервирует необходимое место в стеке для хранения argv [1]. Затем он вызывает функцию strcpy(), которая копирует значение argv[1] в переменную c. Как говорится в комментарии, проверка границ не выполняется, поэтому использование strcpy может привести к переполнению буфера, что я продемонстрирую во второй части этой серии. А пока давайте просто сосредоточимся на том, как эта функция влияет на стек.
Я скомпилировал эту программу c (как stack_demo.exe), используя командную строку Visual Studio (2010), чтобы точно показать, как она выглядит при выполнении в отладчике. Вы можете запустить программу с аргументами командной строки непосредственно из отладчика Immunity, выбрав File–> Open (или просто нажав F3), выбрав свой исполняемый файл и введя аргументы командной строки в данное поле.
Для этого примера я просто использовал 11 символов А для argv[1].[Мы рассмотрим, что происходит, когда вы используете более 11 символов во второй части!]
Поскольку адреса могут меняться, лучший способ найти наш соответствующий программный код - выбрать "Просмотр" -> "Исполняемые модули" (или Alt + E). Затем дважды щелкните по модулю stack_demo.exe (или как вы назвали свой .exe).
Это должно привести вас к следующему:
Первая строка, которую вы видите - это начало функции foo(), но сначала мы рассмотрим main().Я установил несколько точек останова, чтобы помочь пройти по коду (обозначен голубым цветом), и вы можете сделать то же самое, выбрав нужный адрес и нажав F2. Давайте посмотрим на main () …
Хотя main () не делает ничего, кроме вызова функции foo(), есть пара вещей, которые должны произойти в первую очередь, как вы увидите в отладчике. Во-первых, он помещает содержимое Argv [1] (AAAAAAAAAAAAA) в стек. Затем, когда вызывается функция foo(), адрес возврата сохраняется в стеке, поэтому выполнение программы может возобновиться в нужном месте после завершения функции foo().
Посмотрите на скриншот с Immunity, который я прокомментировал соответствующим образом - просто обратите внимание на то, что сейчас в красной рамке; Я расскажу о некоторых других инструкциях в ближайшее время. Вы увидите, что указатель на argv[1] помещается в стек непосредственно перед вызовом функции foo(). Затем выполняется инструкция CALL, и адрес возврата следующей инструкции (EIP + 4) также помещается в стек.
Если вы хотите доказать, что адрес 00332FD4 содержит 0033301C, который является указателем на argv [1], обратитесь к содержимому дампа этого адреса:
Вы увидите содержимое, написанное задом наперед как 1C303300. Позвольте мне воспользоваться этой возможностью, чтобы быстро охватить нотацию Little Endian. "Endianness" относится к порядку, в котором байты хранятся в памяти. Системы на базе Intel x86 используют нотацию Little Endian, в которой младший байт значения хранится по наименьшему адресу памяти (поэтому адрес хранится в обратном порядке). В качестве примера смотри приведенный выше снимок экрана с шестнадцатеричным дампом: адрес вверху (00332FD4) самый маленький, а адрес внизу (00333034) самый большой. Таким образом, байт в верхнем левом углу (в настоящее время занятый 1C) занимает наименьшее расположение адреса, и адреса увеличиваются при перемещении слева направо и сверху вниз. Когда вы смотрите на адрес, такой как 0033301C, младший байт - это байт (1C). Чтобы преобразовать его в нотацию с прямым порядком байтов, вы переупорядочиваете его по одному байту справа налево. Вот изображение:
Итак, argv [1] и адрес возврата теперь помещены в стек, и была вызвана функция foo(). Вот посмотрите на стек с выделенными соответствующими частями.
Обратите внимание на указатель на argv[1] по адресу 0012FF74 и прямо над ним сохраненное значение RETURN. Если вы вернетесь к предыдущему снимку экрана main(), вы заметите, что адрес RETURN 0040103F является следующей инструкцией после CALL foo(), где выполнение программы будет возобновлено после завершения foo().
Теперь давайте посмотрим на функцию foo():
Когда вызывается функция foo(), первое, что происходит - текущий базовый указатель (EBP) сохраняется в стеке с помощью инструкции PUSH EBP, чтобы после завершения функции можно было восстановить базу стека для main().
Затем регистр EBP устанавливается равным регистру ESP (через инструкцию MOV EBP, ESP), делая верх и низ стека фрейма равными. Отсюда, регистр EBP останется постоянным (на весь срок действия функции foo), а регистр ESP увеличится до более низкого адреса, когда данные будут добавлены в кадр стека функции. Вот просмотр регистров до и после, показывающий, что регистр EBP теперь равен регистр ESP.
Далее, место зарезервировано для локальной переменной c (char c [12]) с помощью следующей инструкции: SUB ESP, 10.
Вот просмотр стека после этой серии инструкций:
Обратите внимание, как вершина стека (и, как следствие, ESP) изменилась с 0012FF6C на 0012FF5C.
Давайте перейдем к вызову strcpy(), который скопирует содержимое argv [1](AAAAAAAAAAAAA) в пространство, которое было только что зарезервировано в стеке для переменной c. Посмотрите на функцию в отладчике. Я выделил только ту часть, которая выполняет запись в стек.
На следующих снимках экрана вы заметите, что он продолжает перебирать значение argv[1], записывая в зарезервированное пространство в стеке (сверху вниз зарезервированное пространство), пока не будет записан весь argv [1] в стек.
Прежде чем мы рассмотрим, что происходит со стеком, когда функция завершается, вот еще один пошаговый наглядный пример, который подкрепляет шаги, выполняемые при вызове функции foo().
После того, как функция strcpy() завершена и функция foo() готова к завершению, в стеке должна произойти некоторая очистка. Давайте посмотрим на стек, поскольку функция foo() готовится к завершению, и выполнение программы возвращается к main ().
Как видите, первая выполняемая инструкция - это MOV ESP, EBP, которая помещает значение регистр EBP в регистр ESP, так что теперь он указывает на 0012FF6C, и эффективно удаляет переменную c (AAAAAAAAAAA) из стека. Вершина стека теперь содержит сохраненный EBP:
Когда следующая инструкция, POP EBP, будет выполнена, она восстановит предыдущий базовый указатель стека из main() и увеличит ESP на 4. Указатель стека теперь указывает на значение RETURN, помещенное в стек непосредственно перед вызовом foo (). Когда инструкция RETN выполнена, она вернет поток выполнения программы к следующей инструкции в main() сразу после инструкции CALL foo(), как показано на снимке экрана ниже.
Функция main() выполнит свою собственную очистку, переместив указатель стека вниз по стеку (увеличив его значение на 4) и очистив стек argv[1]. Затем он очистит регистр, который использовался для хранения argv [1] (EAX) через XOR, восстановления сохраненного EBP и возврата к сохраненному обратному адресу.
Этого должно быть достаточно, чтобы понять, как создается/удаляется кадр стека функций и как локальные переменные хранятся в стеке. Если вы хотите больше примеров, я рекомендую вам ознакомиться с некоторыми другими замечательными учебниками (особенно теми, которые опубликованы Кореланом).
Заключение
Это конец первой части серии статей об эксплоитах Windows. Надеемся, что вы уже знакомы с использованием отладчика, можете узнать некоторые основные инструкции ассемблера и понять (на высоком уровне), как Windows управляет памятью, а также как работает стек. В следующем посте мы рассмотрим ту же базовую функцию foo(), чтобы представить концепцию переполнения на основе стека. Затем я сразу же напишу реальный пример использования уязвимости для реального уязвимого программного продукта.
Я надеюсь, что этот первый пост был ясным, точным и полезным. Если у вас есть какие-либо вопросы, комментарии, исправления или предложения по улучшению, не стесняйтесь оставлять мне отзывы в разделе "Комментарии".
Источник: http://www.securitysift.com/windows-exploit-development-part-1-basics/
Автор перевода: yashechka
Переведено специально для портала xss.pro (c)
Последнее редактирование: