image

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

  1. Разместить строку «dir» по адресу, который известен на момент срабатывания уязвимости.
  2. Поместить в регистр rcx адрес этой строки.
  3. Передать управление в функцию 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 мы собираем самые разнообразные задания, требующие знаний из разных направлений ИБ и способные показать, чем чревато небрежное отношение к безопасности: слабые пароли, плохая реализация сервера (уязвимость в ней мы искали методом фаззинга и подробно описали в этой статье), нестойкие шифры. А на примере данного задания хорошо видно, как небрежности в программировании могут нанести критический урон безопасности информации.

Комментарии (7)


  1. arthur_veber
    10.08.2017 11:19

    Интересно, сколько лет надо изучать\программировать, чтобы досконально разбираться в том, что здесь написано? ))
    Кстати, было бы здорово увидеть на вашем сайте лабораторные (хоть и к прошедшим квестам).


    1. mike_y_k
      10.08.2017 14:53

      Тут много зависит от способностей и желания. Остальное уже рутина ;).


    1. San911mustday
      11.08.2017 09:55

      Полностью согласен! Читать интересно, но хотелось бы увидеть и простейшие задания, которые входят в данную тему…


  1. Yanis
    10.08.2017 11:47

    NWOcs, маленькая опечатка в формуле (0x328-0x198)/4 = 100


    1. NWOcs Автор
      10.08.2017 11:58
      +1

      Пересчитали, опечатки не нашли :)
      Дело в том, что в левой части формулы у нас значения HEX (0x328-0x198=0x190), а в правой части — десятичные значения!


  1. mike_y_k
    10.08.2017 14:53

    Отличный howto.


    1. NWOcs Автор
      10.08.2017 14:56

      Спасибо!