Содержание статьи
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()
имеет следующий прототип:
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()
и затем правильно расположить их на стеке. Функция будет выглядеть следующим образом:
Продолжение доступно только подписчикам
Вариант 1. Оформи подписку на «Хакер», чтобы читать все материалы на сайте
Подписка позволит тебе в течение указанного срока читать ВСЕ платные материалы сайта. Мы принимаем оплату банковскими картами, электронными деньгами и переводами со счетов мобильных операторов. Подробнее о подписке
Вариант 2. Купи один материал
Заинтересовала информация, но нет возможности оплатить подписку? Тогда этот вариант для тебя! Обрати внимание: этот способ покупки доступен только для материалов, опубликованных более двух месяцев назад.
Уже подписан?
Riketta
18.08.2016 at 13:33
Еще можно эффективно собирать бинарники с шеллами через ragg2 🙂
Это компонент radare2, не зря же его предлагается ставить в статье.
На примере шелла из статьи:
ragg2 -a x86 -b 64 -k linux -f elf -o shell -B 4831d25248b82f62696e2f2f7368504889e752574889e64831c0b03b0f05
После этой команды вводим ./shell, и если вы увидели заветный $ в начале новой строки (а может и # О_о), то все гуд.
Simon Uvarov
23.08.2016 at 19:17
Так немного нагляднее мне кажется)
Спасибо за инфу 🙂