Нашумевшие события последних месяцев наглядно показывают, насколько актуальной является проблема критических уязвимостей в программном обеспечении. Массовые атаки на пользователей с помощью вирусов-шифровальщиков
WannaCry и
Petya осуществлялись путем удалённой эксплуатации уязвимостей нулевого дня в сетевых сервисах Windows – SMBv1 и SMBv2. Нахождение и эксплуатация уязвимостей для удаленного выполнения кода – задачка явно не из легких. Однако лучшим из лучших специалистов по информационной безопасности всё по зубам!
В одном из заданий
очного тура NeoQuest-2017 необходимо было найти уязвимости в доступной по сети программе-интерпретаторе команд, и с помощью их эксплуатации выполнить код на удаленном сервере. Для доказательства взлома нужно было прочитать содержимое файлового каталога на сервере – там, по условию, размещался ключ задания. Участникам был доступен бинарный файл интерпретатора. Итак, лёд тронулся!
Начинаем исследование
Для начала
запустим бинарник и посмотрим, что он собой представляет. Становится ясно, что исполняемый файл интерпретатора формата PE и предназначен для архитектуры Intel x64. Также ясно, что он скомпилирован с поддержкой
DEP/ASLR, но без
CFG. Сторонние библиотеки также не подгружаются.
Сама программа представляет собой простой интерпретатор команд работы с целочисленным вектором. Синтаксис поддерживаемых команд доступен при входе в программу. Вектор поддерживает запись и чтение значений из ячеек, запись в вектор целиком.
С поверхностным анализом закончено, пора реверсить – тур-то очный, время поджимает!
А что подавать на вход?
Первая промежуточная задача, которую необходимо решить – точное определение
поверхности атаки. Уточним количество и формат команд, загрузив бинарник в IDA Pro. Функция
main легко находится путем поиска выводимых на экран строк.
Анализ функции
main позволяет утверждать следующее:
1. 1. Сначала адрес массива
array на стеке функции
main заносится в переменную
array_base. В цикле ячейки массива инициализируются нулями. Условие выхода из цикла позволяет узнать размер массива – 100 (0x64) целочисленных ячеек.
2. 2. Происходит вывод на экран приветствия и считывание команды пользователя. Присутствие строк
«cls» и
«whoami» говорит о вызове функции
system (в дальнейшем это нам сильно пригодится).
3. Инициализируются переменные-регулярные выражения, для проверки корректности синтаксиса. Видно, что недокументированные команды отсутствуют. Кроме того, становится ясно множество допустимых параметров каждой инструкции.
4. Последовательно в бесконечном цикле производится проверка введенной команды на соответствие регулярным выражениям. Если соответствие какой-либо команде найдено – вычисляются аргументы у команды и вызывается её обработчик.
В результате анализа кода функции
main выяснено, что недокументированных команд нет. Возможно влиять на следующие параметры:
- индексы в командах set/get;
- помещаемое значение в команде set;
- размер и содержимое буфера для записи в массив.
В силу наличия защитных механизмов ASLR и DEP необходимо найти и реализовать 2 уязвимости:
- уязвимость раскрытия адреса в исполняемой области памяти — для обхода ASLR и построения ROP-цепи с обходом DEP;
- уязвимость перехвата потока управления по контролируемому нами адресу – для передачи управления на начало ROP-цепи и исполнения кода.
ROP (Return Oriented Programming) – современная техника эксплуатации, направленная на обход защитного механизма DEP. Суть этой техники заключается в переиспользовании маленьких фрагментов (гаджетов) исполняемой памяти перед инструкциями
ret. Выстроенные в цепочку, адреса гаджетов последовательно снимаются со стека, выполняя команды в гаджете вплоть до команды
ret. Она, в свою очередь, снимает со стека адрес следующего гаджета, и т.д.
Уязвимость №1
Теперь мы знаем, что целевой массив имеет константный размер и расположен на стеке функции
main. Попробуем прочитать ячейку с индексом, выходящим за границы массива.
Сначала при величинах, больших размера массива, мы наблюдаем нормальное поведение – выход с сообщением об ошибке. Однако при значениях индекса больших, чем 2147483647, программа то падает, то выдает какие-то значения.
Похоже, что индексы, которые трактуются как отрицательные числа, проходят проверку границ и выдают содержимое памяти по отрицательному индексу вне границ массива. Уязвимость найдена!
Почему так происходит? Уязвимость кроется в неверной обработке индекса в командах
set/get. Численный индекс в массиве считывается как параметр команды
set и переводится из строковой в численную форму с помощью вызова
stoul. Эта функция возвращает
беззнаковое целое.
Однако при передаче параметра в функции
SET/GET это же значение ошибочно приводится
к знаковому целому – оно сравнивается с размером массива и влияет на результат команды знакового сравнения
jl. Команда
GET выдает значение ячейки памяти, отсчитанное от начала массива.
Эту возможность можно использовать для чтения из памяти какого-либо исполняемого адреса. Поскольку массив расположен на стеке, чтением ячеек массива с отрицательными индексами мы можем просматривать содержимое стека до расположения в нем массива. Поскольку архитектура – x64, адреса в памяти занимают 8 байт. Чтение же из массива осуществляется по 4 байта. Поэтому для чтения адреса из памяти необходимо напечатать две подряд идущие ячейки.
С помощью отладчика экспериментально находим такие ячейки памяти перед буфером, где лежит исполняемый адрес – например, 4294967282 == -14 и 4294967283 == -13. Их значения мы далее используем при построении ROP-цепи.
Кроме того, в дальнейшем при эксплуатации нам понадобится адрес самого буфера на стеке. Как его найти? Взглянем на начало функции
main и увидим, что переменная
array_base хранит указатель на начало буфера.
На стеке эта переменная расположена по адресу rsp+0x358-0x328, а сам буфер начинается с адреса rsp+0x358-0x198.
Значит, необходимо отступить (0x328-0x198)/4 = 100 четырехбайтовых ячеек от начала массива, чтобы прочесть переменную
array_base. Искомые смещения: 4294967196, 4294967197.
Благодаря данной уязвимости мы раскрыли исполняемый адрес в памяти процесса, а также выяснили адрес искомого буфера для размещения параметров ROP-цепи. Теперь необходимо найти способ перехвата потока управления программы.
Уязвимость №2
До сего момента мы не исследовали функцию интерпретатора
load. Посмотрим на неё повнимательнее. Очень скоро здесь обнаруживается классическое переполнение буфера на стеке. Введенная с клавиатуры строка — аргумент команды
load — выделяется и передается как первый параметр функции-обработчика
LOAD. Вторым параметром идет адрес искомого буфера.
Функция LOAD осуществляет циклическую запись в этот буфер без каких-либо проверок его размера. Как видно, количество записанных байт зависит только от размера входной строки.
Передав достаточно большое количество символов на вход команде
load, возможно переписать адрес возврата на стеке и с выходом из интерпретатора передать управление по контролируемому нами адресу. С учетом размера и расположения буфера на стеке, адрес первичной передачи управления необходимо размещать со смещением 4 * (100 + 2) байта (дополнительные 8 байт переписывают сохраненное на стеке значение регистра
rbp).
Поскольку переполненный буфер расположен на стеке, а не в куче, то ROP-цепь будем располагать там же целиком – начиная с смещения 4 * (100 + 2) во входном параметре команды
load.
Теперь все ингредиенты на месте. Пришло время собирать из них боевой эксплойт!
Pwn it!
Для получения ключа задания необходимо прочитать и вывести на экран содержимое локального каталога на сервере. Ранее при анализе нами было замечено, что перед выводом приветствия содержимое экрана очищается, и в приветствии присутствует имя пользователя.
Очевидно, что функция, которая это делает и принимает в качестве параметров строки
«cls» и
«whoami», ведет себя подобно библиотечной функции system. Необходимо вызвать эту функцию, но в качестве выполняемой команды взять
«dir» – вывод на экран содержимого локального каталога на сервере.
Построим ROP-цепь, позволяющую это сделать. Последовательность операций следующая:
- Разместить строку «dir» по адресу, который известен на момент срабатывания уязвимости.
- Поместить в регистр rcx адрес этой строки.
- Передать управление в функцию system_func.
При размещении адресов в буфере необходимо вспомнить о наличии ASLR, и вычислять их динамически, с использованием считанных ранее адресов исполняемой памяти и начала буфера. Кроме того, их запись в буфер осуществляется в соответствие с нотацией
Little Endian, то есть, от младших байтов к старшим.
Теперь необходимо найти смещения нужных гаджетов в исполняемом файле интерпретатора. Первый необходим для помещения в
rcx адреса строки
«dir», второй — для вызова функции
system_func. По смещениям 0x24c00 и 0x16ce6 соответственно в секции кода исполняемого файла интерпретатора находятся нужные участки кода.
Строку
«dir» мы поместим после гаджетов. Её адрес, таким образом, будет на 4*(100 + 2) + 6*4 байт больше адреса буфера.
Ниже приведен наш вариант скрипта для генерации содержимого буфера:
from __future__ import print_function
import sys
n = 102
f=open("buf.txt", "w")
base_l = 0x2f150000 - 0x0 # get 4294967282
base_h = 0x7ff6 # get 4294967283
buffer_l = 0x0afcf6e0 # get 4294967196
buffer_h = 0xe8 # get 4294967197
def rev(x):
return ((x << 24) & 0xff000000 | (x << 8) & 0x00ff0000 | (x >> 8) & 0x0000ff00 | (x >> 24) & 0x000000ff)
arr = [
base_l + 0x24c00,
base_h, # pop rcx ; ret
buffer_l + n*0x4 + 6*0x4,
buffer_h, # buffer ptr - in rdi
base_l + 0x16ce6,
base_h # &system in our binary
]
for i in range(0,n): # dumb
print("%08x" % rev(0xdeadbeef), end='', file=f)
for i in arr:
print("%08x" % rev(i), end='', file=f)
# "dir\0"
print("%08x" % 0x64697200, end='', file=f)
f.close()
Всё готово, пора идти за ключом!
- Подключаемся к удалённому серверу, узнаем нужные адреса командой get.
- Генерируем уязвимый буфер на их основе.
- Выполняем команду load, переписываем адрес возврата из функции main.
- Выполняем команду exit, что завершает функцию main и запускает эксплойт на выполнение.
Искомый ключ:
fb520eb552747437c09f2770a9a282ea.
Что в итоге?
В
NeoQUEST мы собираем самые разнообразные задания, требующие знаний из разных направлений ИБ и способные показать, чем чревато небрежное отношение к безопасности: слабые пароли, плохая реализация сервера (уязвимость в ней мы искали методом фаззинга и подробно описали в этой
статье), нестойкие шифры. А на примере данного задания хорошо видно, как небрежности в программировании могут нанести критический урон безопасности информации.
https://habrahabr.ru/post/335046/