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

Статья Введение в эксплуатацию двоичных файлов x64 Linux (часть 1)

timeshout

RAID-массив
Пользователь
Регистрация
29.06.2022
Сообщения
62
Реакции
83
Basic Buffer Overflow (BoF)
Эта статья является первой из серии статей, в которых я опишу некоторые основные методы эксплуатации бинарных файлов x64 Linux. Начиная с отключения всех относительных механизмов защиты, таких как ASLR, DEP/NX, Stack Canaries и т.д., мы начнем с создания простого BoF и постепенно включим все защиты одну за другой, чтобы понять, как их можно обойти, но самое главное - как они работают.

Tools
Мы будем проводить большинство наших экспериментов на системе Linux ubuntu и будем использовать компилятор (я использую gcc), gef (GEF - GDB Enhanced Features) и шестнадцатеричный редактор. Вы можете использовать тот компилятор и отладчик, который вам больше нравится.

Basic concepts
Ниже приведены некоторые базовые понятия, которые помогут вам понять эти уроки. Если вы уже знаете, что такое стек, регистр, буфер или как происходит вызов функции, вы можете использовать JMP в следующих параграфах или просто освежить свою память.

The application’s memory

Как большинство из вас, вероятно, знает, при выполнении программы операционная система создает для нее адресное пространство памяти. Это пространство делится на сегменты, которые, в свою очередь, включают инструкции программы и данные, необходимые для ее выполнения. Более конкретно, сегмент под названием .text содержит инструкции программы, а сегменты .bss и .data содержат глобальные переменные. Точнее, bss содержит объявленные, но неинициализированные глобальные переменные, а data содержит (глобальные) инициализированные.

Кроме упомянутых выше сегментов, в этом пространстве памяти находятся разделяемые библиотеки (C, cap, dl и т.д.), куча и (наконец) стек. Все, что выделяется динамически (например, переменные с помощью malloc или оператора new в C++ или Java), попадает в кучу, а стек - это структура данных LIFO (last in first out), используемая для статического распределения памяти. Чтобы получить визуальное представление виртуальной памяти приложения, можно посмотреть на файл /proc/<process id>/maps [1]:

1657506727439.png


На рисунке выше поле адреса - это адресное пространство в процессе, которое
занимает отображение. Поле perms - это набор разрешений (read (r), write (w), execute (x), shared (s) и private (p)). Поле offset - это смещение в файле, dev - устройство (major:minor), inode - inode на этом устройстве) и, наконец, поле pathname обычно представляет собой файл, на который опирается отображение.

The Registers
Регистр процессора - это быстро доступное место в процессоре компьютера. Если вы уже знакомы с архитектурой x86 (32-битной), вы, вероятно, знаете, что она использует, помимо прочего, восемь регистров общего назначения, шесть сегментных регистров, регистр EFLAG и регистр EIP, который указывает на адрес, где сохраняется следующая выполняемая инструкция.

Архитектура .x64 расширяет 8 регистров общего назначения x86 до 64-битных и добавляет 8 новых 64-битных регистров. 64-битные регистры имеют имена, начинающиеся с "r", поэтому, например, 64-битное расширение eax называется rax. Новые регистры имеют имена с r8 по r15:

1657506801331.png



The Buffer

Буфер определяется как ограниченный, смежно выделенный набор памяти. Наиболее распространенным буфером в языке C является массив [3]. Для таких языков, как C или C++, которые не имеют встроенного механизма проверки размера данных, копируемых из одного места памяти в другое, существует вероятность того, что эти данные превысят объем буфера, и именно в этом случае возникают серьезные проблемы.

Код:
    “Sins of the father”

    The C programming language has many “dangerous” functions that do not check bounds. These functions must be avoided, while in the unlikely event that they can’t, then the programmer must ensure that the bounds will never get exceeded. Some of these functions are the following:

    strcpy, strcat, sprintf, vsprintf, gets

    These should be replaced with functions such as strncpy, strncat, snprintf, and fgets respectively. The function strlen should be avoided unless you can ensure that there will be a terminating NIL character to find. The scanf family (scanf, fscanf, sscanf, vscanf, vsscanf, and vfscanf) is often dangerous to use [8].

Посмотрите на пример ниже:

1657506882784.png


В строке 13 мы определяем массив с 3 элементами, а в строке 14 присваиваем значение нераспределенному адресу памяти. Компилятор не выдает никакого предупреждения, но при выполнении программы мы получаем следующую ошибку:

*** stack smashing detected ***: terminated

Если программист разрабатывает код, который копирует пользовательский ввод в буфер, то пользователь может намеренно поместить в буфер больше ввода, чем он может вместить. Это может иметь ряд различных последствий, начиная от сбоя программы и заканчивая принуждением программы к выполнению пользовательских инструкций.


Function Calls

Когда вызывается функция, компилятор использует стековый фрейм (выделенный в стеке времени выполнения программы) для хранения всей временной информации, необходимой функции для работы. В зависимости от конвенции вызова вызывающая функция помещает вышеупомянутую информацию в определенные регистры или в стек программы, или в оба регистра. Например, для конвенции вызова C (cdecl) в операционной системе Linux, до шести аргументов будут помещены в регистры RDI, RSI, RDX, RCX, R8 и R9, а все дополнительное будет помещено в стек. Давайте рассмотрим простой пример, чтобы понять это. Приведенная ниже функция myfunc принимает 8 параметров:

1657506974246.png


В соответствии с тем, что мы говорили ранее (предполагая конвенцию вызова на языке C), когда функция будет вызвана, ее стековая рамка будет выглядеть следующим образом:

1657507001418.png


Как и ожидалось, первые шесть аргументов передаются с помощью упомянутых выше регистров, последние два h и g "высыпаются" в стек, как и адрес возврата после вызова функции, RBP, локальные переменные и загадочная красная зона, которая, согласно формальному определению из AMD64 ABI, определяется следующим образом:

128-байтовая область за пределами места, на которое указывает %rsp, считается зарезервированной и не должна изменяться обработчиками сигналов или прерываний. Поэтому функции могут использовать эту область для временных данных, которые не требуются во всех вызовах функций. В частности, функции листа могут использовать эту область для всего своего стекового кадра, а не корректировать указатель стека в прологе и эпилоге. Эта область известна как красная зона.

Существует множество конвенций вызова (stdcall, fastcall, thiscall), каждая из которых определяет уникальный способ, куда вызывающая сторона должна поместить параметры, которые требует вызываемая функция [2], однако для нашей цели наиболее важным является тот факт, что помимо параметров стековый фрейм содержит адрес возврата, по которому управление выполнением продолжится после выхода вызываемой функции.

При определенных условиях можно переопределить адрес возврата и контролировать выполнение программы.


Canonical Addresses
Хотя 64-битные процессоры имеют регистры шириной 64 бита, системы обычно не реализуют все 64 бита для адресации. Таким образом, большинство архитектур определяют нереализованную область адресного пространства, которую процессор будет считать недоступной для использования. В документации Intel и AMD говорится, что для 64-битного режима только 48 бит реально доступны для виртуальных адресов, а биты с 48 по 63 должны повторять бит 47 (расширение знака). Посмотрите на отображение памяти, приведенное ниже, чтобы увидеть, как применяется эта концепция:

1657507112635.png





Перезаписываемый адрес должен быть каноническим, иначе он будет считаться недействительным, и перенаправление будет неудачным.

Для тех, кто знаком с эксплуатацией x86 BoF, это означает, что перезапись типа 0x4141414141414141414141 (0x41 - это ascii значение "A") будет просто неудачной.


A vulnerable program

наша уязвимая программа имеет разрешение SUID (Set owner User ID up on execution), она принадлежит пользователю root и уязвима к переполнению буфера. Если говорить о функциональности, то она не делает ничего особенного, кроме печати "Hi there <name>", где имя задается как параметр командной строки:

1657507219859.png





Disabling Canary, ASLR, NX, FORTIFY_SOURCE

Прежде чем приступить к эксплуатации этого фрагмента кода, его необходимо скомпилировать, для чего мы воспользуемся следующей командой:

Код:
$gcc -fno-stack-protector vuln.c -o vuln -z execstack -D_FORTIFY_SOURCE=0

В приведенной выше команде vuln.c - это файл c, в котором мы написали наш код, vuln - это имя конечного исполняемого файла, -fno-stack-protector отключит защиту canary, -z execstack отключит защиту NX (No eXecute code from stack) и, наконец, D_FORTIFY_SOURCE=0 отключит обнаружение ошибок переполнения буфера для функций, выполняющих операции с памятью и строками[7]. Для отключения ASLR выполните следующую команду:

Код:
$sudo bash -c 'echo 0 > /proc/sys/kernel/randomize_va_space'


Наконец, используйте chown и chmod, чтобы изменить владельца на root и установить разрешение SUID:

Код:
#chown root vuln; chmod +s vuln


Smashing the stack

Почему эта программа уязвима?

Потому что она "слепо" копирует пользовательский ввод в буфер, определенный в функции greet_me. Под слепым копированием я подразумеваю, что программа просто не выполняет никаких проверок размера, в то время как, поскольку мы используем strcpy, наша обязанность (как разработчиков) убедиться, что размер целевой строки достаточно велик для хранения скопированной строки. Наш буфер имеет длину 200 байт, поэтому давайте посмотрим, как поведет себя программа при таком диапазоне размеров. Немного кунг-фу из linux в сочетании с python сделают свое дело:

Код:
$ for i in {200..210}; do echo using $i bytes; python -c “print(‘A’ * $i)” > payload; ./vuln $(cat payload) > /dev/null; done;


Приведенное выше однострочное предложение передает строку длиной $i (от 200 до 210) нашей уязвимой программе, и поскольку нас не волнует фактический вывод, мы отправляем его в /dev/null (как удобно). Однако вывод ошибок все равно будет отображен в стандартном выводе, поэтому после нескольких итераций мы получим следующий результат:

1657507438195.png



Это означает, что 209 байт (если включить новую строку '\x0a') было достаточно, чтобы переполнить буфер имен и вызвать ошибку сегментации. Полезная нагрузка, вызвавшая эту ошибку, выглядит следующим образом:

1657507469219.png



Exploiting the vulnerability


До этого момента мы определили, что программа имеет уязвимость переполнения буфера. Наш следующий шаг - использовать эту уязвимость, и для этого нам нужно сделать следующее:

1. Найти точный размер полезной нагрузки: Помните из параграфа "Вызовы функций", что стековая рамка уязвимой функции должна быть похожа на приведенную ниже. Адрес возврата" идет после регистра EBP:

1657507526259.png



Таким образом, мы должны найти точный размер полезной нагрузки, которая будет перезаписывать регистр RBP.

2. Вставить некоторый шеллкод, который "подходит" для наших нужд (обычно запускается shell).

3. Перенаправить выполнение кода в то место, где в памяти сохранен наш шеллкод.

Мы снова будем использовать расширенные возможности GDB для выполнения этих шагов, поэтому, начиная с (1), загрузите двоичный файл и добавьте точку останова в нашу главную функцию:


1657507580416.png




Мы собираемся использовать gef's pattern create для создания специально созданного входа, который поможет нам определить точный размер входа, который перезаписывает RBP:

1657507605369.png



Сохраните этот шаблон в нашем файле полезной нагрузки и (используйте echo -n, чтобы пропустить новую строку), и запустите программу (внутри gdb) с помощью команды:

Код:
gef> r $(cat payload)

Позвольте gdb установить нашу точку останова и введите "disas greet_me", чтобы разобрать функцию greet_me:

1657507676689.png



Установите новую точку останова на инструкции "ret" с помощью команды b *address (в моем случае это будет gef> b *0x00005555555551d5) и продолжите выполнение, выдав команду 'c'.

Когда выполнение достигнет второй точки останова, вы должны увидеть что-то вроде этого:


1657507713845.png


Регистр RBP (выделенный белым цветом) на рисунке выше был перезаписан значением "baaaaaab".

1657507777827.png


Таким образом, для перезаписи RBP потребуется 208 + 8 байт. Следующим шагом будет внедрение шеллкода, и хотя вы можете написать свой собственный, для этого руководства вы можете просто найти его в Интернете или взять тот, что приведен ниже:

Код:
\x48\x31\xff\xb0\x69\x0f\x05\x48\x31\xd2\x48\xbb\xff\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x48\x31\xc0\x50\x57\x48\x89\xe6\xb0\x3b\x0f\x05\x6a\x01\x5f\x6a\x3c\x58\x0f\x05

Приведенный выше шеллкод порождает оболочку, которая является функциональностью, которая нам нужна, поскольку мы имеем дело с программой SUID. Теперь давайте напишем простой скрипт на python, который "запишет" то, что мы имеем на данный момент:

1657507821293.png


точки зрения ассемблера nops означает просто "нет операции", поэтому, когда CPU сталкивается с ними, он просто ничего не делает. Они будут использоваться для "плавной" передачи исполнения следующему массиву байтов - Shellcode. Обратите внимание на следующее:

nops + shellcode + buf = 216 байт
rip = 6 байт (которые пойдут на перезапись регистра RIP).

Запустите программу еще раз с помощью gdb и, когда она достигнет второй точки останова, выполните следующую команду (для изучения стека):


1657507880563.png



Наш NOP начинается с адреса 0x7fffffffde18.
Указатель стека указывает на последовательность CCCCC:

1657507907151.png



Указатель инструкции, а также наша точка останова находится по адресу 0x00005555555551d5

1657507928548.png


На следующем шаге произойдет следующее: значение, на которое указывает указатель стека, будет вытолкнуто из стека и вставлено в указатель инструкции. Проверьте это, выполнив один шаг:

1657507957559.png



Осталось только перезаписать $rip по нужному адресу, который будет нашим nop sled (0x7fffffffde18), и для этого мы воспользуемся функцией struct библиотеки struct python. Наш финальный эксплойт будет выглядеть следующим образом:

1657507982679.png



Achieving root privileges is just a command away:

1657507997335.png



References
[1] https://man7.org/linux/man-pages/man5/proc.5.html

[2] The Ghidra Book: The Definitive Guide, Chris Eagle, Kara Nance, September 2020

[3] The Shellcoder’s Handbook and Exploiting Security Holes 2nd Edition, Chris Anley September 2008 John Wiley & Sons

[4] https://en.wikipedia.org/wiki/Processor_register

[5] https://cs.brown.edu/courses/cs033/docs/guides/x64_cheatsheet.pdf

[6] https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/x64-architecture

[7] https://www.redhat.com/en/blog/enhance-application-security-fortifysource

[8] https://dwheeler.com/secure-programs/Secure-Programs-HOWTO/dangers-c.html



 


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