Введение в эксплуатацию двоичных файлов x64 Linux (часть 1)
Definitions
Начнем с некоторых относительных определений:
Атака повторного использования кода: Атака повторного использования кода - это техника эксплуатации, которая после получения контроля над указателем инструкций перенаправляет поток управления исполняемого файла на существующий код, который соответствует потребностям злоумышленника.
Возврат в библиотеку и предсуществующие последовательности инструкций (гаджеты) являются примерами данной техники.
Бит NX или XD: Бит NX (no-execute) или XD (eXecute Disabled) - это технология, используемая в процессорах для разделения областей памяти для использования либо для хранения инструкций процессора (кода), либо для хранения данных [1].
Как я показал в предыдущей части, используя BoF, можно вставить произвольный код в память программы. При маркировке этих областей памяти, явно предназначенных для хранения данных, попытка выполнить внедренный код вызовет исключение. Некоторые операционные системы Unix (например, OpenBSD, macOS) поставляются с защитой исполняемого пространства, а новые версии Microsoft Windows также поддерживают защиту исполняемого пространства, называемую Data Execution Prevention [2].
Return into libc
Конкретная атака использует код, который существует в всегда доступной библиотеке C (libc). Для простоты предположим, что у нас уже есть контроль над регистром RIP, тогда вместо того, чтобы перенаправить управление на некоторую область памяти в стеке, мы заставим вызвать тщательно выбранную существующую функцию, которая служит нашим потребностям как атакующего (например, для порождения оболочки).
Как уже упоминалось, libc является очень популярной мишенью для такого рода атак, поскольку содержит почти все функции языка Си, и все эти функции могут быть доступны, поскольку они экспортированы. Одной из таких функций является system(), которая имеет следующий синтаксис:
Эта функция библиотеки C устанавливает команду или имя программы, указанное параметром command, в среду хоста для выполнения командным процессором и возвращается после завершения команды. Таким образом, следующая программа просто порождает SHELL:
Если предположить, что с помощью уязвимости нам удастся перезаписать регистр RIP, нам все равно придется решить две проблемы:
Найти адрес функции system()
Передать аргументы этой функции
Найти адрес функции exit() для чистого закрытия программы.
Чтобы ответить на эти вопросы, давайте сначала посмотрим, как поток выполнения передается в функцию system() в контексте корректного вызова функции. Для этого скомпилируйте приведенную выше программу ($gcc system.c -o system) и загрузите ее в отладчик:
Обратите внимание на вызов функции system() (0x00000000000000001158) и предшествующую ей инструкцию lea rdi,[rip+0xeac] # 0x2004. Помните из части 1, что:
В вызывающем преобразовании языка C до шести аргументов будут помещены в регистры RDI, RSI, RDX, RCX, R8 и R9, а все дополнительное будет помещено в стек.
В конкретном вызове параметр "/bin/sh" будет передан в регистр RDI. Действительно, набрав x/s 0x2004 (или x/s 0x555555556004 в моем случае, поскольку я уже запустил программу) для просмотра строк в этой области памяти, мы получим следующий результат:
Или на лучшем виде:
Вернемся к нашей уязвимой программе из первой части:
После возврата из функции greet_me стек должен выглядеть так, как показано ниже:
1 → Поместите достаточное количество данных, чтобы переполнить буфер и перезаписать регистр $rbp
2 → Переписать регистр RIP так, чтобы он указывал на инструкцию POP RDI, за которой следует RET. Таким образом, то, что находится на вершине стека (в нашем случае это будет строка "/bin/sh"), будет передано в регистр RDI, а RET развернет стек, помещая адрес следующей инструкции в регистр RIP→3.
4 → В стеке находится адрес функции system(), который будет передан в RIP (благодаря RET из предыдущего шага).
5 → Когда функция system() вернется, регистр RIP будет указывать на функцию exit(), чтобы чисто завершить нашу программу.
Для тех, кто знаком с двоичной эксплуатацией, эта последовательность инструкций POP RDI, RET уже знакома
Достаточно точная визуализация концепции программирования, ориентированного на возврат, показана ниже:
A vulnerable program
Мы снова будем переполнять буфер имен, как и в первой части, с той лишь разницей, что на этот раз мы будем использовать функцию gets C, так как будем использовать '\00' байт на входе. Кроме того, мы опустим параметр -z execstack во время компиляции, чтобы сделать стек неисполнимым. Теперь команда компиляции должна выглядеть следующим образом:
Если вы помните предыдущее сообщение, мы использовали 208 байт для переполнения буфера имен + 8 байт для перезаписи RBP + 8 байт для перезаписи RIP. Инжектированный код был включен в эти 208 байт в дополнение к NOP салазкам из 30 байт.
Давайте удалим shellcode и NOP sled из сценария эксплуатации, поскольку мы не собираемся их использовать. Сценарий должен выглядеть следующим образом:
Crafting the exploit string
Используя карту стека из предыдущего параграфа, мы начнем с поиска адреса гаджета POP RDI, RET в libc:
Найдите файл библиотеки libc:
найти гаджет (в libc):
Существуют различные инструменты, которые можно использовать для этой цели, в данном случае мы будем использовать https://github.com/sashs/Ropper:
Запишите смещение 0x26b72 и перейдем к следующему шагу
Просканируйте весь файл libc на наличие строки "/bin/sh" и выведите расположение строки в базисе 16:
Запишите смещение 0x1b75aa и перейдем к следующему шагу
Найдите функцию system() с помощью readelf:
Запишите смещение 0x55410 и перейдем к следующему шагу.
Аналогично функции system() найдите функцию exit(), используя readelf:
Запомните смещение 0x49bc0 и перейдем к следующему шагу
Найдите базовый адрес функции libc:
Как вы понимаете, смещения, которые мы обнаружили выше, будут добавлены к адресу, по которому будет загружена функция libc. Поскольку ASLR отключена, этот адрес будет одним и тем же каждый раз, когда мы запускаем уязвимую программу, и его можно найти, изучив распределение памяти процесса:
Запомните смещение 0x00007ffff7dc5000.
Теперь давайте изменим скрипт эксплойта:
Пока проигнорируйте строки 5 и 13 и обратите внимание на переменную buf (строки с 11 по 17), которая соответствует карте стека, которую мы изначально планировали. Если мы попытаемся использовать этот эксплойт, мы все равно получим ошибку сегментации... но не shell:
The 16 Bytes Stack Alignment
Если мы отладим уязвимую программу, используя вышеупомянутый эксплойт, и установим точку останова на инструкции возврата функции greet_me, то получим следующее:
1 → RBP был перезаписан, 2 → следующая инструкция (ret) переместит адрес на вершину стека в RIP (3) и будет выполнена POP RDI, RET, помещающая "/bin/sh" (3) в регистр RDI. Пока все работает как ожидалось, позволив программе продолжить выполнение, мы переходим к следующей инструкции:
64-битная конвенция вызова требует, чтобы стек был выровнен на 16 байт перед инструкцией вызова, но это легко нарушить во время выполнения цепочки ROP, в результате чего все дальнейшие вызовы этой функции будут выполняться с неправильно выровненным стеком. movaps вызывает общий сбой защиты при работе с невыровненными данными, поэтому попробуйте дополнить свою цепочку ROP дополнительным ret перед возвратом в функцию или вернуться дальше в функцию, чтобы пропустить инструкцию push [4]. Мы можем найти адрес дополнительной инструкции RET в файле libc, следуя точно такому же процессу, как мы делали в гаджете POP RDI, RET (используя ropper или аналогичный инструмент). Окончательный сценарий эксплуатации будет выглядеть следующим образом:
Запустив уязвимую программу, используя вывод, сделанный скриптом выше, мы, наконец, получим Shell:
References
[1] https://en.wikipedia.org/wiki/NX_bit
[2] https://en.wikipedia.org/wiki/Buffer_overflow
[3] The Ghidra Book: The Definitive Guide, Chris Eagle, Kara Nance, September 2020
[4] https://ropemporium.com/guide.html#Appendix B
valsamaras.medium.com
Definitions
Начнем с некоторых относительных определений:
Атака повторного использования кода: Атака повторного использования кода - это техника эксплуатации, которая после получения контроля над указателем инструкций перенаправляет поток управления исполняемого файла на существующий код, который соответствует потребностям злоумышленника.
Возврат в библиотеку и предсуществующие последовательности инструкций (гаджеты) являются примерами данной техники.
Бит NX или XD: Бит NX (no-execute) или XD (eXecute Disabled) - это технология, используемая в процессорах для разделения областей памяти для использования либо для хранения инструкций процессора (кода), либо для хранения данных [1].
Как я показал в предыдущей части, используя BoF, можно вставить произвольный код в память программы. При маркировке этих областей памяти, явно предназначенных для хранения данных, попытка выполнить внедренный код вызовет исключение. Некоторые операционные системы Unix (например, OpenBSD, macOS) поставляются с защитой исполняемого пространства, а новые версии Microsoft Windows также поддерживают защиту исполняемого пространства, называемую Data Execution Prevention [2].
Return into libc
Конкретная атака использует код, который существует в всегда доступной библиотеке C (libc). Для простоты предположим, что у нас уже есть контроль над регистром RIP, тогда вместо того, чтобы перенаправить управление на некоторую область памяти в стеке, мы заставим вызвать тщательно выбранную существующую функцию, которая служит нашим потребностям как атакующего (например, для порождения оболочки).
Как уже упоминалось, libc является очень популярной мишенью для такого рода атак, поскольку содержит почти все функции языка Си, и все эти функции могут быть доступны, поскольку они экспортированы. Одной из таких функций является system(), которая имеет следующий синтаксис:
Код:
int system(const char *command)
Эта функция библиотеки C устанавливает команду или имя программы, указанное параметром command, в среду хоста для выполнения командным процессором и возвращается после завершения команды. Таким образом, следующая программа просто порождает SHELL:
Если предположить, что с помощью уязвимости нам удастся перезаписать регистр RIP, нам все равно придется решить две проблемы:
Найти адрес функции system()
Передать аргументы этой функции
Найти адрес функции exit() для чистого закрытия программы.
Чтобы ответить на эти вопросы, давайте сначала посмотрим, как поток выполнения передается в функцию system() в контексте корректного вызова функции. Для этого скомпилируйте приведенную выше программу ($gcc system.c -o system) и загрузите ее в отладчик:
Обратите внимание на вызов функции system() (0x00000000000000001158) и предшествующую ей инструкцию lea rdi,[rip+0xeac] # 0x2004. Помните из части 1, что:
В вызывающем преобразовании языка C до шести аргументов будут помещены в регистры RDI, RSI, RDX, RCX, R8 и R9, а все дополнительное будет помещено в стек.
В конкретном вызове параметр "/bin/sh" будет передан в регистр RDI. Действительно, набрав x/s 0x2004 (или x/s 0x555555556004 в моем случае, поскольку я уже запустил программу) для просмотра строк в этой области памяти, мы получим следующий результат:
Или на лучшем виде:
Вернемся к нашей уязвимой программе из первой части:
После возврата из функции greet_me стек должен выглядеть так, как показано ниже:
1 → Поместите достаточное количество данных, чтобы переполнить буфер и перезаписать регистр $rbp
2 → Переписать регистр RIP так, чтобы он указывал на инструкцию POP RDI, за которой следует RET. Таким образом, то, что находится на вершине стека (в нашем случае это будет строка "/bin/sh"), будет передано в регистр RDI, а RET развернет стек, помещая адрес следующей инструкции в регистр RIP→3.
4 → В стеке находится адрес функции system(), который будет передан в RIP (благодаря RET из предыдущего шага).
5 → Когда функция system() вернется, регистр RIP будет указывать на функцию exit(), чтобы чисто завершить нашу программу.
Для тех, кто знаком с двоичной эксплуатацией, эта последовательность инструкций POP RDI, RET уже знакома
Код:
POP RAX ; pop the next item on the stack into RAX
RET ; transfer control to the address contained in the next stack item
Достаточно точная визуализация концепции программирования, ориентированного на возврат, показана ниже:
A vulnerable program
Мы снова будем переполнять буфер имен, как и в первой части, с той лишь разницей, что на этот раз мы будем использовать функцию gets C, так как будем использовать '\00' байт на входе. Кроме того, мы опустим параметр -z execstack во время компиляции, чтобы сделать стек неисполнимым. Теперь команда компиляции должна выглядеть следующим образом:
Код:
$gcc -fno-stack-protector vuln.c -o vuln -D_FORTIFY_SOURCE=0
Если вы помните предыдущее сообщение, мы использовали 208 байт для переполнения буфера имен + 8 байт для перезаписи RBP + 8 байт для перезаписи RIP. Инжектированный код был включен в эти 208 байт в дополнение к NOP салазкам из 30 байт.
Давайте удалим shellcode и NOP sled из сценария эксплуатации, поскольку мы не собираемся их использовать. Сценарий должен выглядеть следующим образом:
Код:
import sys
import structbuf = b”A”* 208
buf += b”BBBBBBBB” #RBP overwrite
buf += struct.pack(‘<Q’,0x7fffffffde18) #RIP overwritesys.stdout.buffer.write(buf)
Crafting the exploit string
Используя карту стека из предыдущего параграфа, мы начнем с поиска адреса гаджета POP RDI, RET в libc:
Найдите файл библиотеки libc:
найти гаджет (в libc):
Существуют различные инструменты, которые можно использовать для этой цели, в данном случае мы будем использовать https://github.com/sashs/Ropper:
Запишите смещение 0x26b72 и перейдем к следующему шагу
Просканируйте весь файл libc на наличие строки "/bin/sh" и выведите расположение строки в базисе 16:
Код:
$ strings -a -t x /usr/lib/libc.so.6 | grep /bin/sh1b75aa /bin/sh
Запишите смещение 0x1b75aa и перейдем к следующему шагу
Найдите функцию system() с помощью readelf:
Код:
$ readelf -s libc-2.31.so | grep system
....
1427: 0000000000055410 45 FUNC WEAK DEFAULT 16 system@@GLIBC_2.2.5
Запишите смещение 0x55410 и перейдем к следующему шагу.
Аналогично функции system() найдите функцию exit(), используя readelf:
Код:
$ readelf -s libc-2.31.so | grep exit
...
135: 0000000000049bc0 32 FUNC GLOBAL DEFAULT 16 exit@@GLIBC_2.2.5
Запомните смещение 0x49bc0 и перейдем к следующему шагу
Найдите базовый адрес функции libc:
Как вы понимаете, смещения, которые мы обнаружили выше, будут добавлены к адресу, по которому будет загружена функция libc. Поскольку ASLR отключена, этот адрес будет одним и тем же каждый раз, когда мы запускаем уязвимую программу, и его можно найти, изучив распределение памяти процесса:
Запомните смещение 0x00007ffff7dc5000.
Теперь давайте изменим скрипт эксплойта:
Пока проигнорируйте строки 5 и 13 и обратите внимание на переменную buf (строки с 11 по 17), которая соответствует карте стека, которую мы изначально планировали. Если мы попытаемся использовать этот эксплойт, мы все равно получим ошибку сегментации... но не shell:
The 16 Bytes Stack Alignment
Если мы отладим уязвимую программу, используя вышеупомянутый эксплойт, и установим точку останова на инструкции возврата функции greet_me, то получим следующее:
1 → RBP был перезаписан, 2 → следующая инструкция (ret) переместит адрес на вершину стека в RIP (3) и будет выполнена POP RDI, RET, помещающая "/bin/sh" (3) в регистр RDI. Пока все работает как ожидалось, позволив программе продолжить выполнение, мы переходим к следующей инструкции:
64-битная конвенция вызова требует, чтобы стек был выровнен на 16 байт перед инструкцией вызова, но это легко нарушить во время выполнения цепочки ROP, в результате чего все дальнейшие вызовы этой функции будут выполняться с неправильно выровненным стеком. movaps вызывает общий сбой защиты при работе с невыровненными данными, поэтому попробуйте дополнить свою цепочку ROP дополнительным ret перед возвратом в функцию или вернуться дальше в функцию, чтобы пропустить инструкцию push [4]. Мы можем найти адрес дополнительной инструкции RET в файле libc, следуя точно такому же процессу, как мы делали в гаджете POP RDI, RET (используя ropper или аналогичный инструмент). Окончательный сценарий эксплуатации будет выглядеть следующим образом:
Код:
import sys
import structlibc_base_address = 0x7ffff7dc5000
ret = libc_base_address+0xc0533
pop_rdi = libc_base_address + 0x26b72
bin_sh = libc_base_address + 0x1b75aa
system_function = libc_base_address + 0x55410
exit_function = libc_base_address + 0x49bc0buf = b”A”* 208
buf += b”BBBBBBBB”
buf += struct.pack(‘<Q’,ret)
buf += struct.pack(‘<Q’,pop_rdi)
buf += struct.pack(‘<Q’,bin_sh)
buf += struct.pack(‘<Q’,system_function)
buf += struct.pack(‘<Q’,exit_function)sys.stdout.buffer.write(buf)
Запустив уязвимую программу, используя вывод, сделанный скриптом выше, мы, наконец, получим Shell:
References
[1] https://en.wikipedia.org/wiki/NX_bit
[2] https://en.wikipedia.org/wiki/Buffer_overflow
[3] The Ghidra Book: The Definitive Guide, Chris Eagle, Kara Nance, September 2020
[4] https://ropemporium.com/guide.html#Appendix B
Introduction to x64 Linux Binary Exploitation (Part 2)—return into libc
This post is the second one of a series of articles, where I describe some basic x64 Linux Binary Exploitation techniques. In Part 1 I…
valsamaras.medium.com