Jump to content
Sign in to follow this  
mr.save

Энкодеры msfvenom. Разбираемся с кодированием боевой нагрузки при бинарной эксплуатации

Recommended Posts

Генерация полезной нагрузки — неотъемлемая часть эксплуатации. При использовании модулей Metasploit пейлоад генерируется автоматически, достаточно выбрать тип и задать некоторые переменные. В этой статье мы попробуем разобраться с ролью энкодеров в бинарной эксплуатации и рассмотрим несколько реальных примеров энкодинга на боевых эксплоитах.

Обычно модули скрывают от пользователя детали реализации полезной нагрузки. Ситуация немного меняется, когда дело доходит до необходимости воспользоваться эксплоитом, для которого не существует модулей Metasploit, но есть, например, PoC на Python или любом другом языке.

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

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

 

Buffer Overflow

Поскольку тема шелл-кодинга неотделима от эксплуатации бинарных уязвимостей, давай рассмотрим небольшой фрагмент кода, содержащего в себе типичный Stack-based Buffer Overflow.

#include <stdio.h>
#include <string.h>

int main (int argc, char** argv) {
    char buffer[100];
    strcpy(buffer, argv[1]);

    return 0;
}

Уязвимость этого кода происходит из функции strcpy(char *destination, const char *source). Вот так выглядит описание этой функции в официальной документации:

Цитата

Copies the C string pointed by source into the array pointed by destination, including the terminating null character (and stopping at that point). To avoid overflows, the size of the array pointed by destination shall be long enough to contain the same C string as source (including the terminating null character), and should not overlap in memory with source.

В двух словах: если длина строки (то есть разница между адресом первого байта строки и адресом первого нулевого байта, именуемого NULL-терминатором), на которую указывает argv[1], больше размера буфера buffer, то произойдет тот самый Stack-based Buffer Overflow.

image2.png.8b0cec5a2a89c0d92dd8a9b056260ae5.png

Если рандомизация адреса стека отключена, атакующий может предсказать то место, куда будут записаны данные. Таким образом, манипуляциями с argv[1] можно записать полезную нагрузку в стек, а также подменить адрес возврата так, чтобы передать ей управление.

image5.png.214664e13f8bb749e20a46570c013078.png

Если защита от исполнения данных также отключена, то шелл-код будет исполнен.

Хорошим примером полезной нагрузки будет шелл-код, запускающий командную оболочку sh, который можно сгенерировать с помощью msfvenom.

image7.png.e6d7525d0f2cea9649428f058773d742.png

Здесь и начинается все интересное.

 

Зачем нужны энкодеры?

Если передать нашей программе строку вида argv[1] = shellcode + "BBBB" + shellcode_address, то переполнение не произойдет. Это связано с тем, что шестнадцатый байт шелл-кода равен \x00, так что копирование этого буфера в стек прекращается после 15 байт.

image10.png.f69870b446d71be7185b1d6cee526312.png

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

image11.png.84bd62c5733e7adbda52e0e927c023aa.png

Первое, что бросается в глаза, — присутствие инструкции call 0x25 при том, что ближайшая распознанная инструкция objdump начинается с 0x24. Такое часто встречается, когда требуется передать данные в теле самого шелл-кода. Инструкция call в таком случае очень полезна, поскольку помещает адрес следующей инструкции, то есть адрес последующих данных, в стек.

Подтвердить эту гипотезу можно, если изучить байты \x2f\x62\x69\x6e\x2f\x73\x68\x00, находящиеся в диапазоне 0x1d–0x25, — они представляют собой строку /bin/sh, которую мы указывали в качестве CMD при генерации полезной нагрузки.

image3.png.7a7428137f379b9ec14405ec93d436df.png

Заменив эти байты на NOP’ы в изначальном шелл-коде, получим более корректную картину.

image4.png.55c983e563d154a68cd7e3425ac90ded.png

Суть этого шелл-кода сводится к исполнению прерывания int 0x80 с кодом 0x0b, что соответствует системному вызову SYS_EXECVE. Системные вызовы в Linux — это своеобразные мосты между приложениями и функциями ядра. Они позволяют, например, открывать сокеты, читать и записывать файлы или, как в нашем шелл-коде, запускать приложения. В частности, он выполняет команду /bin/sh -c CMD.

Вернемся к байтам 0x00. Первый из них возникает при использовании инструкции push 0x68732f. Так как мы препарируем шелл-код для x86, то формально эта инструкция записывает в стек значение, равное 0x0068732f, что, если приглядеться, и указано в опкоде этого вызова — 68 2f 73 68 00. Кроме этого, целую кучу байтов 0x00 создает инструкция call 0x25 с опкодом e8 08 00 00 00.

Чтобы избавиться от нулевого байта в первом опкоде, можно заменить одну инструкцию push 0x68732f тремя — push edx (52), push 0x68 (6a 68) и pushw 0x732f (66 68 2f 73). Первая произведет запись значения edx (равного 0x00000000) в стек, вторая и третья же запишут нужную строку (-c). От нулей в call 0x25 можно избавиться, если поместить значение 0x25 в регистр dl (mov dl, 0x25 — b2 25), а потом вызвать call edx (ff d2).

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

image1.png.d5956a841555b84633619b9d64a07145.png

Кроме этого, изменение инструкций, скорее всего, приведет к изменению длины соответствующих участков кода, что сломает ветвление типа JMP/CALL, — поэтому придется заниматься еще и отслеживанием смещений.

Хорошая новость состоит в том, что такие вещи можно автоматизировать. Программа, выполняющая некоторое преобразование байтов шелл-кода так, чтобы избежать присутствия «плохих» байтов, называется энкодером.

Со времени появления концепции подходы к кодированию полезных нагрузок сильно менялись. Например, некоторые энкодеры позволяют не только избавиться от «плохих» байтов, мешающих эксплуатации, но и обойти средства защиты типа IPS и AV. При этом считать, что энкодеры созданы для того, чтобы обходить средства защиты, по меньшей мере неправильно — это полезное свойство очень сильно зависит от схемы работы конкретного энкодера.

 

MSFVenom Encoders

 

x86/xor_dynamic

Это энкодер, использующий x86 XOR с динамической длиной ключа.

Одна из самых распространенных схем работы энкодеров — это схема со Stub-декодером.

image8.png.9f7f37a78b4a4e625ed56e7586352210.png

Кодирование шелл-кода при такой схеме состоит из двух частей:

  • Преобразование изначального пейлоада таким образом, чтобы избежать присутствия «плохих» байтов.
  • Добавление кода, который выполняет обратное преобразование и передает управление оригинальному шелл-коду в оперативной памяти.

Соответственно, при исполнении закодированной полезной нагрузки первым делом выполняется Stub, который раскодирует оригинальный шелл-код и передает ему управление. Пример реализации такой схемы — это и есть x86/xor_dynamic. В качестве преобразования используется операция XOR с ключом переменной длины. При этом длина и значение ключа зависят от «плохих» байтов.

Чтобы лучше понять работу этого энкодера, давай дважды сгенерируем уже знакомый нам шелл-код linux/x86/exec с применением x86/xor_dynamic и посмотрим, чем отличаются результаты.

image9.thumb.png.247a8cc7ff7a01987f4d27e0cadda430.png

Синим цветом отмечен Stub — код, который расшифровывает шелл-код в памяти и исполняет его. Первый выделенный красным фрагмент кода загружает первоначальное значение ключа в регистр AL, второй и третий используются для определения начала и конца закодированных данных. Фактически все отличия в Stub сводятся к этим двум моментам.

При этом здесь используется тот же трюк для передачи указателя на данные, что мы видели на примере шелл-кода linux/x86/exec, — с помощью инструкции call указатель на данные помещается в стек в качестве адреса возврата, откуда его можно легко забрать с помощью инструкции pop. После того как оригинальный шелл-код декодирован, ему передается управление инструкцией jmp ecx.

Несмотря на простоту x86/xor_dynamic и на то, что добавляемый к шелл-коду объем данных невелик, этот метод тоже не лишен недостатков. В частности, если один из байтов Stub «плохой» (а общее число байтов Stub в x86/xor_dynamic гораздо больше, чем, скажем, в x86/add_sub) и недопустим для использования в полезной нагрузке, то с этим ничего не удастся сделать и придется использовать другой энкодер.

 

x86/add_sub

Кодирует пейлоад при помощи инструкций add и sub. Идея взята из боевого эксплоита HP NNM. Его фишка в том, что в полезной нагрузке разрешена только треть всего диапазона 0x00–0xFF. Это накладывает сильные ограничения на пейлоад — насколько нам известно, ни один энкодер, кроме этого (и еще пары alphanumeric, которые реализуют принципиально иной подход), не осилит маскировку плохих байтов в таких условиях.

Энкодер x86/add_sub делит исходный шелл-код на участки, а потом представляет каждый из них в виде суммы или разности нескольких значений. Так как операции add и sub в x86 работают с 32-битными блоками, то попытка применить этот энкодер к шелл-коду, имеющему длину, не кратную четырем, будет приводить к неудаче.

image6.png.dde618157c41fb6a3b7dfcc6ff65a2e3.png

Кроме того, этот энкодер не предусматривает передачу исполнения исходному шелл-коду ¯\_(ツ)_/¯. С другой стороны, если дело дошло до применения этого энкодера, то передача управления точно не составит труда.

Если мы дизассемблируем закодированный шелл-код linux/x86/exec, то увидим что-то подобное вот такому листингу.

image13.thumb.png.5ae103b05966a0f484bc4a032806dbbc.png

Здесь видно, что каждый отдельный четырехбайтовый блок исходного шелл-кода сначала собирается в регистре eax с помощью инструкций and и add, а потом размещается в стеке с помощью инструкции push. В случае если байт 0x05 (опкод инструкции add) «плохой», в качестве основной арифметической операции используется вычитание и соответствующая ему инструкция sub.

Фактически энкодер x86/add_sub позволяет представить шелл-код в виде последовательности четырех инструкций и их операндов, что делает его потенциально применимым даже в самых сложных случаях, когда набор разрешенных для использования байтов крайне ограничен.

 

x86/alpha_mixed

Этот энкодер кодирует полезную нагрузку как цифро-буквенную строку с разными регистром букв. Такие шелл-коды называют venetian shellcode.

Какое-то время назад фильтрация входящих данных по принципу соответствия символам какой-либо кодировки действительно рассматривалась как одна из эффективных мер противодействия бинарной эксплуатации, пока в 2002 году Крис Анли в своей культовой статье Creating Arbitrary Shellcode In Unicode Expanded Strings не показал, что это не так и что даже с помощью инструкций с опкодами, соответствующими символам Unicode, можно добиваться полноценного исполнения кода.

Чтобы обеспечить преобразование байтов шелл-кода в цифро-буквенную последовательность, энкодер x86/alpha_mixed использует движок Alpha2 за авторством SkyLined. Использование этого движка имеет одну побочную особенность: фактически x86/alpha_mixed генерирует самомодифицирующийся шелл-код, поэтому во время исполнения ему необходимо знать свой собственный адрес.

Если мы сгенерируем шелл-код без дополнительных параметров энкодера, то мы увидим следующую картину.

image14.thumb.png.3688d6c5660d9d82be92302366a67646.png

Перед понятной последовательностью ASCII-символов будет идти несколько непечатных. Эта часть как раз и выполняет определение позиции шелл-кода в памяти.

Довольно часто во время эксплуатации уязвимости на шелл-код указывает некоторый регистр CPU. Очевидно, что в таком случае нам не нужно проводить дополнительные операции, чтобы определить положение шелл-кода в памяти, и мы можем избавиться от части байтов, портящей нашу буквенно-цифровую картину.
 Энкодер x86/alpha_mixed поддерживает передачу такого регистра в виде параметра BufferRegister.

image15.thumb.png.55f8d517f2552b42278d2fc386a0bec4.png

В общих чертах энкодер x86/alpha_mixed использует ту же схему декодирования, что и x86/xor_dynamic, — в коде предусмотрен определенный Stub, который декодирует оригинальный шелл-код в памяти и передает ему управление.

image12.png.ccb100e58c16c1ca90c933274bf3cbdc.png

Тема venetian shellcode заслуживает отдельной статьи, поэтому всем заинтересованным рекомендую ознакомиться с публикацией автора.

 

Вывод

Msfvenom предлагает на выбор 23 энкодера полезных нагрузок для x86. Каждый из них имеет определенные ограничения и сферы применения, где они максимально эффективны.

Например, энкодер x86/add_sub нужно дополнительно кастомизировать для работы, хоть он и предлагает наибольшую гибкость, x86/alpha_mixed позволяет запустить полезную нагрузку в условиях, когда разрешены только буквы и цифры, но для полноценной работы может требовать указания BufferRegister, а x86/xor_dynamic не требует ничего, но бессилен против «плохих» байтов, которые есть в исходнике декодера.

Как было сказано в начале, авторы некоторых энкодеров ставили своей задачей не только сокрытие «плохих» байтов, но и обход средств защиты. Среди таких энкодеров, например, x86/shikata_ga_nai. Это полиморфный энкодер, который производит разные полезные нагрузки при каждом запуске так, что формирование единой сигнатуры для IPS или AV просто невозможно.

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

Share this post


Link to post

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Sign in to follow this  

×
×
  • Create New...