В этой статье мы рассмотрим особенности переполнения стека в 64-битном Linux. Сделаем мы это на примере таска Bitterman с соревнования CAMP CTF 2015. С помощью модуля pwntools для Python мы построим эксплоит, в котором будут применены техники Return-oriented programming (для обмана запрета исполнения DEP/NX) и Return-to-PLT — для байпаса механизма рандомизации адресов ASLR без брутфорса.
В этом цикле статей мы изучаем разные аспекты атак типа «переполнение стека». Читай также:
Я составил целых три импровизированных кейса, поочередно изучив которые ты получишь необходимые знания для 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, который мы поставим чуть позже.
В прошлых статьях мы использовали PEDA в качестве основного обвеса для дебаггера, однако я знал, что уже есть более продвинутые тулзы для апгрейда GDB (к тому же PEDA больше не поддерживается разработчиком), а именно GEF и pwndbg. Изучая эти инструменты, я нашел изобретательный пост, в котором рассказывается, как одновременно установить эти софтины и переключаться между ними одним нажатием. Мне понравилась идея, но не реализация, поэтому я набросал свой скрипт, позволяющий в одно действие инсталлировать все три ассистента. Теперь каждый из них будет запускаться следующими командами соответственно.
$ gdb-peda [ELF-файл] $ gdb-gef [ELF-файл] $ gdb-pwndbg [ELF-файл]
Для этой статьи мы продолжим юзать PEDA, потому что с ним удобнее всего делать скриншоты.
Уязвимый исходный код.
/** * 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
, которая уже стала эталоном небезопасного кода.
Never use gets(). Because it is impossible to tell without knowing the data in advance how many characters gets() will read, and because gets() will continue to store characters past the end of the buffer, it is extremely dangerous to use. It has been used to break computer security. Use fgets() instead.
Как видишь, даже 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'
Получив порцию негодования от GCC из-за использования gets
, мы собрали 64-битный исполняемый файл classic
.
Скрипт checksec.py
, идущий в комплекте с модулем pwntools и доступный из командной строки, говорит о том, что бинарь никак не защищен. Это нам и нужно для демонстрации первого кейса.
Запустим отладчик и попробуем получить контроль над регистром RIP (он отвечает за хранение адреса возврата) в момент завершения работы функции vuln
.
Регистры процессора:
EAX->RAX
, EBX->RBX
, ECX->RCX
, EDX->RDX
, ESI->RSI
, EDI->RDI
, EBP->RBP
(база стекового кадра), ESP->RSP
(вершина стека);R8..R15
;EIP->RIP
.Память:
push
и pop
оперируют значениями размером восемь байт;0x00007FFFFFFFFFFF
(то есть, в сущности, используются только шесть наименьших значащих байт).Функции:
RDI, RSI, RDX, RCX, R8, R9
, последующие помещаются в стек.Хорошее чтиво по теме: What happened when it goes to 64 bit?
Как обычно, будем пользоваться pattern create
, чтобы сгенерировать циклический паттерн де Брёйна, который мы скормим программе.
Этим действием, как и планировалось, мы вышли за границы отведенного буфера.
Однако несмотря на то, что отрывки нашего паттерна можно наблюдать на стеке (синий), адрес возврата (красный) перезаписать не удалось. Всему виной каноническая форма виртуальной адресации (0x00007FFFFFFFFFFF
), где задействованы лишь младшие 48 бит (6 байт). В том случае, если процессор видит «неканонический» адрес (в котором первые два значащих байта отличны от нуля), будет вызвано исключение, и контроля над RIP мы точно не получим.
Чтобы перезапись удалась, посмотрим, что находится в RSP, и посчитаем смещение.
Нам нужно 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 поддается для перезаписи произвольным значением.
Чтобы не мучиться с вычислением адреса загрузки шелл-кода в стеке, воспользуемся техникой размещения полезной нагрузки в переменной окружения.
Идея вкратце: адрес любой переменной окружения может быть найден с помощью простой программы на C (функция getenv
), следовательно, если разместить в такой переменной шелл-код, то можно точно узнать его адрес, что избавляет хакера от необходимости возиться с NOP-срезами. Интересно то, что на расположение шелл-кода относительно стекового пространства программы влияет ее имя.
Подробнее об этой технике читай в книге Hacking: The Art of Exploitation, PDF, с. 142.
Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», увеличит личную накопительную скидку и позволит накапливать профессиональный рейтинг Xakep Score! Подробнее
1 год7690 р. |
1 месяц720 р. |
Я уже участник «Xakep.ru»
Читайте также
Последние новости