Линуксовый IoT под прицелом: пишем шелл-код под *nix x64

Линуксовый IoT под прицелом: пишем шелл-код под *nix x64

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 и через регистры xmm0 — xmm3 для типов с плавающей точкой;
  • остальные параметры передаются через стек;
  • для параметров, передаваемых через регистры, все равно резервируется место в стеке;
  • результат работы функции возвращается через регистр 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() имеет следующий прототип:

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

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

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

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

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

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

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

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

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

Just do it!

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Заключение

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

📎📎📎📎📎📎📎📎📎📎