В королевстве PWN. ROP-цепочки и атака Return-to-PLT в CTF Bitterman

В этой статье мы рассмотрим особенности переполнения стека в 64-битном Linux. Сделаем мы это на примере таска Bitterman с соревнования CAMP CTF 2015. С помощью модуля pwntools для Python мы построим эксплоит, в котором будут применены техники Return-oriented programming (для обмана запрета исполнения DEP/NX) и Return-to-PLT — для байпаса механизма рандомизации адресов ASLR без брутфорса.

 

В королевстве PWN

В этом цикле статей мы изучаем разные аспекты атак типа «переполнение стека». Читай также:

  • «Препарируем классику переполнения буфера в современных условиях»
  • «Обходим DEP и брутфорсим ASLR на виртуалке с Hack The Box»

 

Ликбез по срыву стека на x86-64

Я составил целых три импровизированных кейса, поочередно изучив которые ты получишь необходимые знания для PWN’а бинарника Bitterman.

Первый кейс покажет отличия эксплуатации Stack Smashing от этой же атаки в 32-битной ОС (о которой мы говорили в первой части цикла) в случае, когда у нарушителя есть возможность разместить и выполнить шелл-код в адресном пространстве стека, — то есть с отключенными защитами DEP/NX и ASLR.

Второй кейс поможет разобраться в проведении атаки ret2libc на x86-64 (ее 32-битный аналог был рассмотрен во второй части). Здесь мы обсудим, какие регистры использует 64-битный ассемблер Linux при формировании стековых кадров, а также посмотрим, что собой представляет концепция Return-oriented programming (ROP). Механизм DEP/NX активен, ASLR — нет.

В третьем кейсе я покажу вариацию ROP-атаки, цель которой — стриггерить утечку адреса загрузки разделяемой библиотеки libc (методика Return-to-PLT, или ret2plt) для обхода ASLR без необходимости запускать перебор. DEP/NX и ASLR активны.

От последнего этапа мы перейдем непосредственно к исследованию Bitterman, который к этому моменту уже не будет представлять для тебя сложности.

 

Стенд

Для этой статьи я установил свежую 64-битную Ubuntu 19.10 с GCC версии 8.3.0.

$ uname -a
Linux pwn-3 5.0.0-31-generic #33-Ubuntu SMP Mon Sep 30 18:51:59 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux

Из дополнительного ПО я взял интерпретатор Python 2.7, который перестали поставлять по умолчанию с дистрибутивом (все переходят на третью версию Python).

$ sudo apt install python2.7 -y
$ sudo update-alternatives —install /usr/bin/python2 python2 /usr/bin/python2.7 1

Вторая версия пригодится нам для модуля pwntools, который мы поставим чуть позже.

 

Вооружение GDB

В прошлых статьях мы использовали PEDA в качестве основного обвеса для дебаггера, однако я знал, что уже есть более продвинутые тулзы для апгрейда GDB (к тому же PEDA больше не поддерживается разработчиком), а именно GEF и pwndbg. Изучая эти инструменты, я нашел изобретательный пост, в котором рассказывается, как одновременно установить эти софтины и переключаться между ними одним нажатием. Мне понравилась идея, но не реализация, поэтому я набросал свой скрипт, позволяющий в одно действие инсталлировать все три ассистента. Теперь каждый из них будет запускаться следующими командами соответственно.

$ gdb-peda [ELF-файл]
$ gdb-gef [ELF-файл]
$ gdb-pwndbg [ELF-файл]

Для этой статьи мы продолжим юзать PEDA, потому что с ним удобнее всего делать скриншоты.

 

Кейс 1. Классический срыв стека

Уязвимый исходный код.

/**
 * Buffer Overflow (64-bit). Case 1: Classic Stack Smashing
 * Compile: gcc -g -fno-stack-protector -z execstack -no-pie -o classic classic.c
 * ASLR: Off (sudo sh -c 'echo 0 > /proc/sys/kernel/randomize_va_space')
 */

#include <stdio.h>

void vuln() {
  char buffer[100];
  gets(buffer);
}

int main(int argc, char* argv[]) {
  puts("Buffer Overflow (64-bit). Case 1: Classic Stack Smashingn");
  vuln();

  return 0;
}

В наших изысканиях всему виной будет функция vuln, содержащая вызов уязвимой процедуры чтения из буфера gets, которая уже стала эталоном небезопасного кода.

Как видишь, даже man кричит о том, что ни в каких случаях не следует использовать gets, ведь этой функции наплевать на размер переданного ей буфера — она прочитает из него все, пока содержимое не кончится.

Скомпилируем программу без запрета исполнения данных в стеке и отключим ASLR.

$ gcc -g -fno-stack-protector -z execstack -no-pie -o classic classic.c
$ sudo sh -c ‘echo 0 > /proc/sys/kernel/randomize_va_space’

Подготовка исполняемого файла classic для первого кейса

Получив порцию негодования от GCC из-за использования gets, мы собрали 64-битный исполняемый файл classic.

Проверка безопасности исполняемого файла classic

Скрипт checksec.py, идущий в комплекте с модулем pwntools и доступный из командной строки, говорит о том, что бинарь никак не защищен. Это нам и нужно для демонстрации первого кейса.

Запустим отладчик и попробуем получить контроль над регистром RIP (он отвечает за хранение адреса возврата) в момент завершения работы функции vuln.

 

Некоторые изменения в логике x86-64

Регистры процессора:

  • все регистры общего назначения расширены до 64 бит: EAX->RAX, EBX->RBX, ECX->RCX, EDX->RDX, ESI->RSI, EDI->RDI, EBP->RBP (база стекового кадра), ESP->RSP (вершина стека);
  • введено восемь дополнительных регистров общего назначения: R8..R15;
  • служебный регистр — указатель на текущую исполняемую команду также расширен до 64 бит: EIP->RIP.

Память:

  • размер указателя стал равен восьми байтам;
  • инструкции работы со стеком push и pop оперируют значениями размером восемь байт;
  • каноническая форма адреса виртуальной памяти имеет вид 0x00007FFFFFFFFFFF (то есть, в сущности, используются только шесть наименьших значащих байт).

Функции:

  • аргументы для функций теперь размещаются в регистрах и в стеке. Первые шесть аргументов подаются через регистры в порядке RDI, RSI, RDX, RCX, R8, R9, последующие помещаются в стек.

Хорошее чтиво по теме: What happened when it goes to 64 bit?

Proof-of-concept

Как обычно, будем пользоваться pattern create, чтобы сгенерировать циклический паттерн де Брёйна, который мы скормим программе.

Создание циклического паттерна

Этим действием, как и планировалось, мы вышли за границы отведенного буфера.

Переполнение буфера и вызов процессорного исключения

Однако несмотря на то, что отрывки нашего паттерна можно наблюдать на стеке (синий), адрес возврата (красный) перезаписать не удалось. Всему виной каноническая форма виртуальной адресации (0x00007FFFFFFFFFFF), где задействованы лишь младшие 48 бит (6 байт). В том случае, если процессор видит «неканонический» адрес (в котором первые два значащих байта отличны от нуля), будет вызвано исключение, и контроля над RIP мы точно не получим.

Чтобы перезапись удалась, посмотрим, что находится в RSP, и посчитаем смещение.

Расчет смещения до RIP

Нам нужно 120 байт, чтобы добраться до RIP. Исходя из этого, напишем небольшой PoC-скрипт на Python, демонстрирующий возможность перезаписи адреса возврата.

#!/usr/bin/env python2
## -*- coding: utf-8 -*-

## Использование: python pwn-classic-poc.py

import struct

def little_endian(num):
  """Упаковка адреса в формат little-endian (x64)."""
  return struct.pack('<Q', num)

junk = 'A' * 120
ret_addr = little_endian(0xd34dc0d3)
payload = junk + ret_addr

with open('payload.bin', 'wb') as f:
  f.write(payload)

Квалификатор <Q упакует нужный адрес в 64-битный формат little-endian.

Успешная перезапись RIP «мертвым кодом»

Таким образом, RIP поддается для перезаписи произвольным значением.

Боевой пейлоад

Чтобы не мучиться с вычислением адреса загрузки шелл-кода в стеке, воспользуемся техникой размещения полезной нагрузки в переменной окружения.

Идея вкратце: адрес любой переменной окружения может быть найден с помощью простой программы на C (функция getenv), следовательно, если разместить в такой переменной шелл-код, то можно точно узнать его адрес, что избавляет хакера от необходимости возиться с NOP-срезами. Интересно то, что на расположение шелл-кода относительно стекового пространства программы влияет ее имя.

Источник: xakep.ru

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *