IoT — самый настоящий тренд последнего времени. Почти везде в нем используется ядро Linux. Однако статей по вирусописательству и шелл-кодингу под эту платформу сравнительно мало. Думаешь, писать шелл-код под Linux — только для избранных? Давай выясним как написать вирус для Linux!
БАЗА ДЛЯ НАПИСАНИЯ ВИРУСА ДЛЯ LINUX
Что нужно для работы?
Для компиляции шелл-кода нам понадобится компилятор и линковщик. Мы будем использовать nasm и ld. Для проверки работы шелл-кода мы напишем небольшую программку на С. Для ее компиляции нам понадобится gcc. Для некоторых проверок будет нужен rasm2 (часть фреймворка radare2). Для написания вспомогательных функций мы будем использовать Python.
Что нового в x64?
x64 является расширением архитектуры IA-32. Основная отличительная ее особенность — поддержка 64-битных регистров общего назначения, 64-битных арифметических и логических операций над целыми числами и 64-битных виртуальных адресов.
[ad name=»Responbl»]
Если говорить более конкретно, то все 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 байт.
[ad name=»Responbl»]
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 как разделители (зависит от платформы и функции). Поэтому лучше такие значения не использовать. В противном случае функция может скопировать шелл-код не полностью. Рассмотрим следующий пример:
$ rasm2 -a x86 -b 64 'push 0x00' 6a00
Как видно, код push 0x00 скомпилируется в следующие байты 6a 00. Если бы мы использовали такой код, наш шелл-код бы не сработал. Функция скопировала бы все, что находится до байта со значением 0x00.
В шелл-коде нельзя использовать «захардкоженные» адреса, потому что мы заранее эти самые адреса не знаем. По этой причине все строки в шелл-коде получаются динамически и хранятся в стеке.
Вот вроде бы и все.
[ad name=»Responbl»]
JUST DO IT!
Если ты дочитал до этого места, то уже должна сложиться картина, как будет работать наш шелл-код.
Первым делом необходимо подготовить параметры для функции execve() и затем правильно расположить их на стеке. Функция будет выглядеть следующим образом:
Второй параметр представляет собой массив argv[]. Первый элемент этого массива содержит путь к исполняемому файлу.
Третий параметр представляет собой информацию об окружении, нам он не нужен, поэтому будет иметь значение null.
Сначала получим нулевой байт. Мы не можем использовать структуру вида mov eax, 0x00, поскольку это приведет к появлению null-байтов в коде, так что мы будем использовать следующую инструкцию:
xor rdx, rdx
Оставим это значение в регистре rdx — оно еще понадобится в качестве символа конца строки и значения третьего параметра (которое будет null).
Так как стек растет от старших адресов к младшим, а функция execve() будет читать входные параметры от младших к старшим (то есть стек работает с памятью в обратном порядке), то на стек мы будем класть перевернутые значения.
Для того чтобы перевернуть строку и перевести ее в hex, можно использовать следующую функцию на Python:
Вызовем эту функцию для /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
В итоге получили такой файл:
Компилируем и линкуем под x64. Для этого:
$ nasm -f elf64 example.asm $ ld -m elf_x86_64 -s -o example example.o
Теперь можем использовать objdump -d example для того, чтобы посмотреть получившийся файл:
Чтобы получить шелл-код вида x11x22… из бинарника, можем воспользоваться следующим кодом:
В результате получаем:
x48x31xd2x52x48xb8x2fx62x69x6ex2fx2fx73x68x50x48x89 xe7x52x57x48x89xe6x48x31xc0xb0x3bx0fx05
Тестируем шелл-код
Для теста используем следующую программу на С (вместо SHELLCODE нужно вставить получившийся шелл-код):
Затем компилируем:
gcc -m64 -fno-stack-protector -z execstack -o shellcode_test shellcode_test.c
В результате получаем программу shellcode_test. Запускаем программу и попадаем в интерпретатор sh. Для выхода вводим exit.
[ad name=»Responbl»]
ЗАКЛЮЧЕНИЕ
Вот мы и написали свой первый шелл-код под Linux x64. На первый взгляд — ничего сложного, труднее всего сократить размеры шелл-кода. И нельзя забывать, что это лишь «проба пера», наш шелл-код не справится с DEP и ASLR, но полученные навыки пригодятся для написания более сложных вещей.
Ссылки:
3 comments On Вирус для Linux. Учимся писать шелл-коды.
нихуя не понятно короче
Все очень хорошо работает,и немного тормозит
Segmentation fault: (core dumped)