- Прогрессивные методы разработки шеллкодов в ОС класса UNIX.
- 1. Введение.
- 2. Шеллкоды “As Is”.
- 2.1 Основные принципы разработки.
- 2.2 Избавляемся от нуль-байтов.
- 2.3 Адресация.
- 2.4 «Боевой» пример.
- 3. Advanced in shellcode development.
- 3.1 Шифрованное тело.
- 3.2 Замена NOP-ам. Полиморфный код.
- 3.3 Прямой поиск кода в памяти.
- Статья Hello world в виде шелл-кода: Особенности написания шелл-кодов
Прогрессивные методы разработки шеллкодов в ОС класса UNIX.
Причиной создания этой статьи послужило отсутствие достаточного количества информации по этой теме на русском языке, а так же собственные наработки автора в этой области. Первая часть статьи посвящена рассмотрению методов разработки шеллкодов в целом и решения проблем, возникающих при этом (к примеру, как эффективнее избавиться от нуль-байтов в коде). Во второй части статьи мы коснемся аспектов профессиональной разработки байт-кодов. Сюда входит решение таких проблем, как укрывание байт-кода от IDS, разделение шеллкода в памяти на несколько частей и прочие трюки, необходимые для более эффективной работы.
Автор: hr0nix (hr0nix 0 front.ru)
Содержание:
- Введение.
- Шеллкоды “As Is”.
- Основные принципы разработки.
- Избавляемся от нуль-байтов.
- Адресация.
- «Боевой» пример.
- Advanced in shellcode development.
- Шифрованное тело.
- Замена NOP-ам. Полиморфный код.
- Прямой поиск кода в памяти.
. ПРЕДУПРЕЖДЕНИЕ . Для того, чтобы понять все, что последует далее, от Вас потребуется знание ассемблера (под архитектуру x86), Си и ОС Linux. Впрочем, даже если чего-то из этого вы не знаете, читать дальше вам все равно никто не запретит. =)
1. Введение.
Причиной создания этой статьи послужило отсутствие достаточного количества информации по этой теме на русском языке, а так же собственные наработки автора в этой области. Первая часть статьи посвящена рассмотрению методов разработки шеллкодов в целом и решения проблем, возникающих при этом (к примеру, как эффективнее избавиться от нуль-байтов в коде). Во второй части статьи мы коснемся аспектов профессиональной разработки байт-кодов. Сюда входит решение таких проблем, как укрывание байт-кода от IDS, разделение шеллкода в памяти на несколько частей и прочие трюки, необходимые для более эффективной работы. Пока я писал эту статью, я создал небольшой программный пакет со скромным названием SH311G0d =), весьма полезный (по крайней мере, мне он часто был нужен) при разработке байт-кодов. По ходу статьи я буду рассказывать о его функциях и о том, как они были реализованы. Скачать сам пакет вы сможете с . ТУТ ССЫЛКА. Все примеры в статье реализованы для ОС Linux. Почему? Просто эта система – одна из самых распространенных и удобных на сегодняшний день. А, освоив технику написания байт-кода в ней, можно без труда писать шеллкоды для любой UNIX-системы.
2. Шеллкоды “As Is”.
Итак, понеслась. Для начала, что же такое шеллкод? В настоящее время шеллкодом принято называть последовательность опкодов процессора, представимую в виде строки символов и удовлетворяющую ряду свойств:
- Отсутствие в коде нуль-байтов (символов с кодом 0x00) – это свойство следует из требования того факта, что код должен быть представим в виде строки. Напоминаю, что в Си (и еще ряде языков) нуль-байт служит признаком конца строки, следовательно, если в шеллкоде будет нуль-байт, то при его копировании (или еще какой-нибудь строковой операции) обработается только часть строки до первого нуль-байта, что может вызвать некоторые неприятности.
- Шеллкод должен выполниться корректно в любой момент, когда на него будет передано управление (свойство вызвано спектром использования шеллкодов – в различных атаках, в ходе которых повреждается структура памяти), и его работа не должна зависеть от того, в какой области памяти находится байт-код.
- Шеллкод по возможности должен быть универсальным, то есть иметь работать корректно на всех системах одного типа (данное свойство наиболее критично в win32-системах, но речь у нас не о них).
- Чем меньше размер байт-кода, тем лучше (больше вероятность того, что мы успешно поместим его в какой-нибудь буфер). В принципе, это свойство можно отнести к категории универсальности.
Сами шеллкоды правильнее было бы назвать байт-кодами, название это прицепилось к ним потому, что коды, о которых пойдет речь, первоначально использовались для того, чтобы получить приглашение командного интерпретатора. Отсюда и название «шеллкоды».
2.1 Основные принципы разработки.
В некоторых системах метод вызова syscalls несколько отличается – это лучше уточнить в каком-нибудь специальном справочнике вашей системы или у разработчиков. Пример вызова sys_exit() – программа, выходящая из программы =) : strace – чрезвычайно полезная для отладки шеллкодов программа. Ее работа заключается в том, что она запускает программу, переданную вторым аргументом, и составляет подробный отчет обо всех системных вызовах, сделанных этой программой (а так же предоставляет список параметров этих вызовов). Я потерял очень много времени на отладке, не используя трэйсер системных вызовов. Итак, теперь мы знаем достаточно, чтобы приступать к самому главному.
2.2 Избавляемся от нуль-байтов.
Рассмотрим следующий ассемблерный код: Скомпилируем и выполним: На первый взгляд, все просто прекрасно. Однако вот досадный момент: Здесь надо заметить, что SG сперва компилирует исходник в объект-код, а потом уже работает с ним. Посмотрим, откуда взялся нуль-байт: Как можно заметить, первый нуль-байт встречается в инструкции mov edx,0xc. Откуда он берется? Регистр edx имеет размер 4 байта (подразумевается, что у вас 32-битный процессор архитектуры x86 =) ), однако мы в него пытаемся поместить константу, имеющую всего один значимый байт – младший. Остальные байты – нули. В конечном виде эта инструкция будет выглядеть как mov edx,0x0000000c. Отсюда и 3 нуль-байта дизассемблерного листинга. В принципе, нуль-байты в коде в 90% случаев берутся именно в результате компилирования подобных команд. Бороться с этим очень просто – необходимо предварительно обнулить весь регистр (при помощи xor), а потом записать в младший байт нашу константу. То есть наше mov edx,0x0000000c будет выглядеть так: В откомпилированном варианте, кстати, второй вариант даже будет короче.
Другая проблема – признаки конца строки. Если использовать в коде выражения вида msg db ‘hello world’,0x0 – ни до чего хорошего это не доведет. Самый простой способ решения этой проблемы – получить 0x0 в одном из регистров, а дальше сделать mov byte [ + ], . Пример использования (только с символом конца строки) – строка номер 4 вышеприведенного листинга. Так же рекомендуется всегда использовать минимально возможные регистры для хранения данных и явно указывать размеры операндов (push byte, jmp short и т.д.). Вот пример кода, содержащего большинство причин появления нуль-байтов: Вывод ndisasm –b 32 : Как мы видим, в шестнадцатеричном представлении каждой из команд присутствует нуль-байт. А теперь исправленный вариант: В придачу ко всему, исправленный код еще и оказался намного короче певоначального. Теперь мы готовы исправить hello.asm, превратив его в полноценный шелл-код: Ну что же, 0x0-байты нам теперь не помеха. Тут надо заметить, что существует еще один весьма эффективный способ избавиться от нуль-байтов – шифрование кода в памяти. Подробно об этом будет рассказано в соответствующем разделе.
2.3 Адресация.
Каждый раз, когда мы помещаем какие-либо данные в стек при помощи push, регистр ESP меняет свое значение на адрес этих данных в памяти. Мы уже использовали ESP для определения адреса строки в памяти (shello.asm): EIP
Когда мы используем инструкцию вида call _label, она заменяется парой При использовании ret EIP вынимается из стека, после чего на него происходит jmp. Этих двух фактов вполне достаточно для определения адреса любых наших данных в памяти. Вот многострадальный shello.asm, который теперь достает адрес строки, используя EIP:
2.4 «Боевой» пример.
3. Advanced in shellcode development.
Последнее время появляется все больше и больше систем обнаружения вторжения, способных обнаружить в анализируемых пакетах данных шеллкоды. Давайте разберемся, как это происходит и как от этого защититься.
В 95% случаев IDS использует для обнаружения вредоносных пакетов сигнатурный анализ. Вот список ключевых сигнатур, на основании наличия которых в данных пакета IDS делает вывод о степени его вредоносности по отношению к системе:
- “\xcd\x80” – классика. Считается, что шеллкод обязательно должен вызывать прерывание номер 0x80 (иначе как же он навредит?). Однако, как мы увидим в дальнейшем, можно написать любой код, не используя эту сигнатуру.
- Строки вида “/bin/sh”, “cmd.exe” и т.п. Очевидно, избавиться от подобных сигнатур очень просто вручную. Достаточно сперва положить в память несколько измененную строку, а потом придать ей вид нужной так же, как мы делали в shello.asm, добавляя в конец символ перевода строки.
- Наличие цепочек из байтов 0x90 – команд NOP. Обычно добавляют длинную цепочку NOP-ов в буфер перед шеллкодом, чтобы проще было попасть на него адресом возврата. О замене NOP-ам мы поговорим в соответствующем разделе.
В принципе, для обнаружения шеллкодов также можно было бы использовать эвристический анализ, однако в более-менее загруженных сетях обрабатывать большое количество пакетов подобным образом просто не представляется возможным.
3.1 Шифрованное тело.
Одно из наиболее красивых решений для скрытия шеллкодов – шифрование рабочего тела. Это происходит следующим образом:
- Само тело шеллкода шифруется при помощи какого-нибудь алгоритма шифрования, позволяющего осуществить однозначную расшифровку криптованного текста (далее, не заостряясь на деталях шифрования, будем использовать банальный XOR).
- Перед зашифрованным телом помещается компактный (по возможности) расшифровщик, который придает байт-коду в памяти его первоначальный вид.
- После расшифровки управление передается на расшифрованный байт-код.
Чаще всего для шифрования кода используется XOR (по одно-, двух- или четырехбайтовому ключу), как наиболее быстрый и просто реализуемый из алгоритмов шифрования. Вот пример реализации однобайтового XOR-расшифровщика: Поддержка шифрования предусмотрена и в моей программе. При указания ключа –c SH311G0d сделает из данного ему на входе байт-кода зашифрованную программную систему с расшифровщиком, которая сама по себе является полноценным шеллкодом.
Итак, какие плюсы мы получаем при шифровании?
- В зашифрованном коде почти наверняка будут отсутствовать различные сигнатуры наподобие “\xcd\x80”, а значит, обнаружить его при помощи IDS будет практически невозможно.
- Если в написанном вами коде очень сложно обойтись без нуль-байтов (или их использование дает серьезный выигрыш по размерам кода), то, зашифровав код так, чтобы в зашифрованном виде их не встречалось, мы навеки забудем про эту проблему (тут надо сказать, что SH311G0d так выбирает XOR-ключ, чтобы 0x0-байтов в зашифрованном байт-коде не возникло). В случае XOR-шифрования, для выполнения этого условия достаточно, чтобы значение ключа не совпадало ни с одним из байтов кода (т.к. a xor a = 0).
Однако есть и существенный минус: вредоносный пакет можно будет опознать по сигнатуре расшифровщика. О том, как избавиться от константной структуры вашего кода, сказано в следующем разделе.
3.2 Замена NOP-ам. Полиморфный код.
Но, как говорится, не NOP-ом единым жив человек. Вот пример инструкций, которые не имеют абсолютно никакого воздействия на процесс исполнения и, следовательно, могут послужить заменой NOP-у: Очевидно, в нашей псевдо-цепочке мы можем как угодно модифицировать содержимое пользовательских регистров (все равно в шеллкоде мы их обнулим), а так же переносить данные из любого регистра в самого себя. Однако здесь есть одна небольшая проблема – почти у всех этих команд опкоды занимают в памяти минимум два байта. Таким образом, нет никакой гарантии, что попадем мы именно на начало необходимой нам команды, а не на середину, получив в результате лаконичное Illegal Instruction. В принципе, это лечится довольно просто – каждый адрес возврата нужно пробовать использовать тремя способами: без сдвига, со сдвигом в один и в два байта.
Теперь что касательно полиморфизма кода. Избавляться от сигнатур можно по-разному, тут нужно не забывать, что запрограммировать один и тот же алгоритм можно довольно большим количеством способов. В придачу к этому, чтобы избавиться от постоянного вида нашего кода, достаточно добавить в случайные места нашего кода некоторое количество случайно (да, повсюду царит random) выбранных псевдо-NOP-ов. Здесь необходимо помнить, что мы уже не можем модифицировать содержимое пользовательских регистров, однако можем заменять команды вида “mov al, 5” на что-нибудь типа “mov al, 4; inc al” Применяя подобные преобразования к вашему шеллкоду (особенно вкупе с шифрованием), вы почти 100-процентно защищаете его от обнаружения IDS. SH311G0d также умеет случайным образом модифицировать данный на входе шеллкод, уменьшая вероятность его обнаружения. Для этого используется ключ –m. В случае использования ключа –m совместно с –c, программа сперва зашифрует байт-код, а потом модифицирует расшифровщик. Результат – каждый раз – новый код, что приводит к практически полному отсутствию сигнатур. В дальнейшем я буду работать над усовершенствованием эвристического модификатора исходного кода, т.к. эта идея меня весьма заинтересовала.
3.3 Прямой поиск кода в памяти.
Помещаем свой шеллкод в любое место адресного пространства программы (другой буфер, heap), а в уязвимый буфер кладем небольшой байт-код, который найдет по какой-либо сигнатуре «главный» код в памяти. Услышав слова «поиск в памяти», хочется сразу спросить: а как же SIGSEGV? Ведь всегда существует возможность наткнуться на область памяти, не принадлежащую программе и умереть с лаконичным и до боли знакомым сообщением.
Но и тут нас не оставят в беде. Оказывается, существует системный вызов chdir(), принимающий в качестве единственного аргумента указатель на строку – имя каталога. Если этот указатель указывает (и снова тавтология =( ) за пределы доступного адресного пространства, то вызов возвращает значение 0xfffffff2, иначе 0xfffffffe, чем мы и воспользуемся. Итак, общая структура данных в этом случае такова: Сам шеллкод для прямого поиска выглядит следующим образом: Вот пример программы на Си, использующей эту технологию на практике: При желании, можете запустить все это через strace и посмотреть, как вживую сканируется память.
Ну вот и все, что я хотел рассказать о современных методах разработки шеллкодов. Искренне надеюсь, моя статья поможет кому-нибудь из вас перестать использовать готовые решения, уподобляясь скрипт-кидди, и начать Творить, как и подобает человеку, что хочет называться Хакером.
Great tnx всем, кто помогал вылавливать баги в статье: ov3r, Nagatoky, virusman, [HEX] — ваша помощь была неоценима. Спасибо всем, кто не давал мне спать, пока я работал. Респект #m00 и #darkwired (просто потому, что там хорошие люди). И привет моему преподу по матану: еще раз завалите на экзамене, Дмитрий Валерьевич, будете страдать (да, это угроза)…
Источник
Статья Hello world в виде шелл-кода: Особенности написания шелл-кодов
Приветствую всех читателей этой статьи и посетителей Codeby.net >
Хочу рассказать о шелл-кодах и особенностях их написания вручную. Вам понадобятся знания ассемблера на базовом уровне. Рассмотрим как пишут шелл-коды без инструментов, которые могут их автоматически создать. Вредоносные шелл-коды писать не будем! Будем писать псевдо шелл-коды для простоты и понимания. Если эта статья и её формат вам понравиться, тогда расскажу о вредоносных шелл-кодах
Написание шелл-кода будет показано для архитектуры x86 . Алгоритм не сильно отличается для архитектуры x64. Для практики я рекомендую вам установить Linux в VirtualBox или VMware. Так же можно экспортировать готовый образ виртуальной машины.
План:
Теория: Что такое шелл-код и системные вызовы
Практика: Сравниваем программу на ассемблере и языке Си . Делаем hello world в виде шелл-кода
Что такое шелл-код и системные вызовы
Шелл-код — это двоичный исполняемый код, который выполняет определенную задачу. Например: Передать управление командной оболочке ( /bin/sh ) или даже выключить компьютер. Шелл-код пишут на языке ассемблер с помощью опкодов (Например: \x90 означает команду: nop ).
Программы взаимодействуют с операционной системой через функции. Функции расположены в библиотеках . Функция printf() , exit() в библиотеке libc . Помимо функций существуют системные вызовы. Системные вызовы находятся в ядре операционной системы . Взаимодействие с операционной системой происходит через системные вызовы . Функции используют системные вызовы.
Системные вызовы не зависят от версии какой-либо из библиотеки . Из-за универсальности системные вызовы используют в шелл-кодах.
У системных вызовов есть кода. Например, функция printf() использует системный вызов write() с кодом 4.
Машины с архитектурой x86: Системные вызовы определены в файле /usr/include/i386-linux-gnu/asm/unistd_32.h
Машины с архитектурой x64: Системные вызовы определены в файле /usr/include/x86_64-linux-gnu/asm/unistd_64.h
Справочник системных вызовов с объяснениями.
Проверим существование системных вызовов на практике
Напишем программу на языке Си, печатающую строку BUG .
Компиляция: gcc printf_prog.c -o printf_prog
Проверим наличие системных вызовов с помощью команды: strace ./printf_prog
В конце strace мы можем видеть системный вызов write(1, «BUG», 3BUG) . Количество кода для шелл-кода слишком много, если использовать функции . Старайтесь писать небольшие шелл-коды . Так они будут меньше обнаруживаться и вероятность их срабатывания будет больше.
Сравниваем программу на ассемблере и языке Си
Шелл-код можно написать, как программу на языке Си, скомпилировать, при необходимости отредактировать и перевести в байтовое представление. Такой способ подходит, если мы пишем сложный шелл-код.
Шелл-код можно написать на языке ассемблер. Этот способ я хочу рассмотреть более подробно. Для сравнения мы напишем 2 программы, печатающие сроку Hello world! . Первая будет написана на языке Си, а вторая на ассемблере.
Код на языке Си:
Компиляция: gcc hello_world_c.c -o hello_world_c
Код на ассемблере:
Получаем объектный файл с помощью nasm: nasm -f elf32 hello_world.asm -o hello_world.o
Объединяем объектный файл в один исполняемый: ld -m elf_i386 hello_world.o -o hello_world
Посмотрим на ассемблерный код получившихся файлов с помощью objdump .
Функция main в программе на языке Си:
Кажется, что больше кода в ассемблерном листинге, но это не так. В листинге языка Си я показал только функцию main , а она там не одна! В листинге ассемблера я показал программу целиком!
Делаем hello world в виде шелл-кода
Взгляните на листинг программы, написанной на ассемблере. Сначала идут адреса, затем байты, а далее инструкции ( 8049000: b8 04 00 00 00 mov eax, 0x4 ). Запишем опкоды инструкций в виде шелл-кода.
Вручную всё делать очень не удобно. Bash нам в помощь: objdump -d ./hello_world|grep ‘[0-9a-f]:’|grep -v ‘file’|cut -f2 -d:|cut -f1-6 -d’ ‘|tr -s ‘ ‘|tr ‘\t’ ‘ ‘|sed ‘s/ $//g’|sed ‘s/ /\\x/g’|paste -d » -s |sed ‘s/^/»/’|sed ‘s/$/»/g’ (вместо ./hello_world можно подставить любую другую программу ).
Опкоды ( представлены в читаемом виде )
Но работать этот шелл-код не будет , так как в нём присутствуют байты \x00 и строка hello_world указана по адресу ( «\xb9\x1f»\x90\x04\x08» — это инструкция mov ecx, 0x8040901f ), а в программе адрес может быть разный из-за механизма защиты ASLR. В шелл-коде точных адресов быть не должно . Решим проблему постепенно, начав заменять данные, расположенные по точному адресу, а затем уберём байты \x00 .
Убираем точные адреса
Строка, которую нам нужно напечатать — Hello, world! Представим её в виде байтов. Утилита xxd нам поможет: echo «Hello, World!» | xxd -pu
Байтовое представление строки Hello, world! : 48656c6c6f2c20576f726c64210a . Для удобства разделим по 4 всю последовательность байтов: 48656c6c 6f2c2057 6f726c64 210a . Байтов в конце недостаточно. Во всех отделённых нами наборов байтов, их по 4, а в последнем всего лишь 2. Добавим любые байты кроме \x00 , так как потом добавленные нами байты обрежутся программой. Я выберу байты \x90 . Нам нужно расположить байты в порядке: little-enidan ( в обратном порядке ). Получится такая последовательность байт: 90900a21 646c726f 57202c6f 6c6c6548 . Это просто байты строки.
Теперь превратим их в инструкции на ассемблере. Тут нам поможет фреймворк radare2 с утилитой rasm2.
Получаем опкоды инструкций
Флаг -a x86 -b 32 обозначают вывод для архитектуры x86.
Чтобы передать байты в стек нужна инструкция push . Регистр [/COLOR]esp[COLOR=rgb(97, 189, 109)] указывает на вершину стека . Переместим на значение вершине стека в регистр ecx .
В итоге получаем: 68210a9090 686f726c64 686f2c2057 6848656c6c 89e1 . Заменим точный адрес в нашем шелл-коде на новые инструкции.
Замена нулевых байтов
Для удобства мы представим эти инструкции в виде ассемблерных команд. Нам поможет утилита ndisasm . Первым делом запишем наши байты в файл, а затем применим утилиту ndisasm .
Нам нужно заменить инструкции с нулевыми байтами на другие. Нулевые байты образуются из-за того, что инструкция mov — двухбайтовая , а оставшиеся 2 байта из 4 компилятору нужно заменить нулями. Предлагаю заменить эти инструкции mov на сочетание двухбайтовых инструкций xor и mov .
Ассемблерные инструкции и их опкоды
Оформим весь этот набор байтов в виде программы на языке Си.
Компилируем: gcc hello_world_test.c -o hello_world_test -z execstack
Проверяем работоспособность: ./hello_world_test
Довольно долго это всё делать, если вы не хотите делать шелл-код для атаки на определённую компанию.
Существует замечательный инструменты Msfvenom и подобные ему. Msfvenom позволяет делать шелл-код по шаблону и даже закодировать его. Про этот инструмент и про сам metasploit на Codeby.net написано много информации. Про энкодеры информации в интернете тоже достаточно. Например: эта статья.
Хочу порекомендовать сайты: exploit-db и shell-storm. На этих сайтах вы сможете найти множество шелл-кодов.
Желаю вам удачи и здоровья. Не болейте и прокачивайте мозги
.
Источник