Самый маленький шелл-код. Создаем 44-байтовый Linux x86 bind shellcode
Shell-код представляет собой набор машинных команд, позволяющий получить доступ к командному интерпретатору (cmd.exe в Windows и shell в Linux, от чего, собственно, и происходит его название). В более широком смысле shell-код — это любой код, который используется как payload (полезная нагрузка для эксплоита) и представляет собой последовательность машинных команд, которую выполняет уязвимое приложение (этим кодом может быть также простая системная команда, вроде chmod 777 /etc/shadow) :
Немного теории
Уверен, что многие наши читатели и так знают те истины, которые я хочу описать в теоретическом разделе, но не будем забывать про недавно присоединившихся к нам хакеров и постараемся облегчить их вхождение в наше непростое дело.
Системные вызовыСистемные вызовы обеспечивают связь между пространством пользователя (user mode) и пространством ядра (kernel mode) и используются для множества задач, таких, например, как запуск файлов, операции ввода-вывода, чтения и записи файлов.
Для описания системного вызова через ассемблер используется соответствующий номер, который вместе с аргументами необходимо вносить в соответствующие регистры.
РегистрыРегистры — специальные ячейки памяти в процессоре, доступ к которым осуществляется по именам (в отличие от основной памяти). Используются для хранения данных и адресов. Нас будут интересовать регистры общего назначения: EAX, EBX, ECX, EDX, ESI, EDI, EBP и ESP.
Стеком называется область памяти программы для временного хранения произвольных данных. Важно помнить, что данные из стека извлекаются в обратном порядке (что сохранено последним — извлекается первым). Переполнение стека — достаточно распространенная уязвимость, при которой у атакующего появляется возможность перезаписать адрес возврата функции на адрес, содержащий shell-код.
Проблема нулевого байтаМногие функции для работы со строками используют нулевой байт для завершения строки.
Таким образом, если нулевой байт встретится в shell-коде, то все последующие за ним байты проигнорируются и код не сработает, что нужно учитывать.
Необходимые нам инструменты
- Linux Debian x86/x86_64 (хотя мы и будем писать код под x86, сборка на машине x86_64 проблем вызвать не должна);
- NASM — свободный (LGPL и лицензия BSD) ассемблер для архитектуры Intel x86;
- LD — компоновщик;
- objdump — утилита для работы с файлами, которая понадобится нам для извлечения байт-кода из бинарного файла;
- GCC — компилятор;
- strace — утилита для трассировки системных вызовов.
Если бы мы создавали bind shell классическим способом, то для этого нам пришлось бы несколько раз дергать сетевой системный вызов socketcall() :
- net.h/SYS_SOCKET — чтобы создать структуру сокета;
- net.h/SYS_BIND — привязать дескриптор сокета к IP и порту;
- net.h/SYS_LISTEN — начать слушать сеть;
- net.h/SYS_ACCEPT — начать принимать соединения.
И в конечном итоге наш shell-код получился бы достаточно большим. В зависимости от реализации в среднем выходит 70 байт, что относительно немного. Но не будем забывать нашу цель — написать максимально компактный shell-код, что мы и сделаем, прибегнув к помощи netcat!
Почему размер так важен для shell-кода?
Ты, наверное, слышал, что при эксплуатации уязвимостей на переполнение буфера используется принцип перехвата управления, когда атакующий перезаписывает адрес возврата функции на адрес, где лежит shell-код. Размер shell-кода при этом ограничен и не может превышать определенного значения.
Shell-код мы будем писать на чистом ассемблере, тестировать — в программе на С. Наша заготовка bind_shell_1.nasm , разбитая для удобства на блоки, выглядит следующим образом:
Сохраним ее как super_small_bind_shell_1.nasm и далее скомпилируем:
а затем слинкуем наш код:
и запустим получившуюся программу через трассировщик (strace), чтобы посмотреть, что она делает:
Запуск bind shell через трассировщик
Введение в AssemblerКак видишь, никакой магии. Через системный вызов execve() запускается netcat , который начинает слушать на порте 12345, открывая удаленный шелл на машине. В нашем случае мы использовали системный вызов execve() для запуска бинарного файла /bin/nc с нужными параметрами ( -le/bin/sh -vp12345 ).
execve() имеет следующий прототип:
- filename обычно указывает путь к исполняемому бинарному файлу — /bin/nc ;
- argv[] служит указателем на массив с аргументами, включая имя исполняемого файла, — ["/bin//nc", "-le//bin//sh", "-vp12345"] ;
- envp[] указывает на массив, описывающий окружение. В нашем случае это NULL, так как мы не используем его.
Синтаксис нашего системного вызова (функции) выглядит следующим образом:
Описываем системные вызовы через ассемблер
Как было сказано в начале статьи, для указания системного вызова используется соответствующий номер (номера системных вызовов для x86 можно посмотреть здесь: /usr/include/x86_64-linux-gnu/asm/unistd_32.h ), который необходимо поместить в регистр EAX (в нашем случае в регистр EAX, а точнее в его младшую часть AL было занесено значение 11, что соответствует системному вызову execve() ).
Аргументы функции должны быть помещены в регистры EBX, ECX, EDX:
- EBX — должен содержать адрес строки с filename — /bin//nc ;
- ECX — должен содержать адрес строки с argv[] — "/bin//nc" "-le//bin//sh" "-vp12345" ;
- EDX — должен содержать null-байт для envp[] .
Регистры ESI и EDI мы использовали как временное хранилище для сохранения аргументов execve() в нужной последовательности в стек, чтобы в блоке 5 (см. код выше) перенести в регистр ECX указатель (указатель указателя, если быть более точным) на массив argv[] .
Ныряем в код
Разберем код по блокам.
Блок 1 говорит сам за себя и предназначен для определения секции, содержащей исполняемый код и указание линкеру точки входа в программу.
Блок 2
Обнуляем регистр EDX, значение которого (NULL) будет использоваться для envp[] , а также как символ конца строки для вносимых в стек строк. Обнуляем регистр через XOR, так как инструкция mov edx, 0 привела бы к появлению null-байтов в shell-коде, что недопустимо.
Важно!Аргументы для execve() мы отправляем в стек, предварительно перевернув их справа налево, так как стек растет от старших адресов к младшим, а данные из него извлекаются наоборот — от младших адресов к старшим.
Для того чтобы перевернуть строку и перевести ее в hex, можно воспользоваться следующей Linux-командой:
Блок 3
Ты, наверное, заметил странноватый путь к бинарнику с двойными слешами. Это делается специально, чтобы число вносимых байтов было кратным четырем, что позволит не использовать нулевой байт (Linux игнорирует слеши, так что /bin/nc и /bin//nc — это одно и то же).
Блок 4
Блок 5
Почему в AL, а не в EAX? Регистр EAX имеет разрядность 32 бита. К его младшим 16 битам можно обратиться через регистр AX. AX, в свою очередь, можно разделить на две части: младший байт (AL) и старший байт (AH). Отправляя значение в AL, мы избегаем появления нулевых байтов, которые бы автоматически появились при добавлении 11 в EAX.
Извлекаем shell-код
Чтобы наконец получить заветный shell-код из файла, воспользуемся следующей командой Linux:
и получаем на выходе вот такой вот симпатичный shell-код:
Тестируем
Для теста будем использовать следующую программу на С:
Компилируем. NB! Если у тебя x86_64 система, то может понадобиться установка g++-multilib :
Проверяем bind shell
Хех, видим, что наш shell-код работает: его размер — 58 байт, netcat открывает шелл на порте 12345.
Оптимизируем размер
58 байт — это довольно неплохо, но если посмотреть в shellcode-раздел exploit-db.com, то можно найти и поменьше, например вот этот размером в 56 байт.
Можно ли сделать наш код существенно компактнее?
Можно. Убрав блок, описывающий номер порта. При таком раскладе netcat все равно будет исправно слушать сеть и даст нам шелл. Правда, номер порта нам теперь придется найти с помощью nmap . Наш новый код будет выглядеть следующим образом:
А теперь попробуем подключиться и получить удаленный шелл-доступ. С помощью Nmap узнаем, на каком порте висит наш шелл, после чего успешно подключаемся к нему все тем же netcat :
И снова проверяем bind shell
Bingo! Цель достигнута: мы написали один из самых компактных Linux x86 bind shellcode. Как видишь, ничего сложного ;).