Введение в реверсинг с нуля, используя IDA PRO. Часть 56.

Дата публикации 28 окт 2018 | Редактировалось 6 ноя 2018
Давайте теперь рассмотрим драйвер, который запрограммирован с различными уязвимостями, чтобы понять, как их эксплуатировать. Как и всегда мы будем использовать WINDOWS 7 SP1 без какого-либо патча безопасности. Мы знаем, что здесь всё будет работать. Затем мы увидим, какие изменения есть ниже и какие другие возможности существуют в новых системах. Но мы будем идти потихоньку и никуда не будем спешить.

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

https://github.com/hacksysteam/HackSysExtremeVulnerableDriver

Реализованные уязвимости

  • Двойная выборка
  • Переполнение пула
  • Использование после освобождения
  • Путаница в типах
  • Переполнение стека
  • Целочисленное переполнение
  • Переполнение стека с флагом GS(Buffer Security Check)
  • Перезапись арбитражного кода
  • Разыменование нулевого указателя
  • Неинициализированная переменная кучи
  • Неинициализированная переменная стека
  • Небезопасный доступ к ресурсам ядра

Мы будем начинать потихоньку. Сначала сделаем анализ переполнения стека.

Конечно, Вам нужно скопировать драйвер на целевую машину и загрузить его с помощью OSR DRIVER LOADER.

Мы копируем его с его IDB в локальную папку и открываем его в IDA чтобы начать анализ.

Хорошо. Как мы уже знаем здесь, у нас есть символы, которые облегчают нам много вещи. Но первое, что мы должны найти и что почти всегда распознаётся с символами или без, это структура _DRIVER_OBJECT, которая передается как аргумент в функцию DRIVERENTRY.

В этом случае у нас не так много проблем.

1.png

Здесь хорошо видна точка входа, и ее аргументы хорошо обнаружились.

Мы видим, что драйвер использует, как и в предыдущих примерах, API функцию RTLINITUNICODESTRING для инициализации структур.

2.png

Напомним, что первым аргументом был указатель на структуру UNICODE_STRING. Здесь мы видим PUNICODE_STRING, т.е. указатель на структуру UNICODE_STRING.

3.png

Т.е. это буфер, куда драйвер сохранит длину строки в переменную типа WORD, Максимальная длина в следующем поле также имеет тип WORD, а указатель будет скопирован в строку юникода, которую мы передали как источник в третьем поле.

4.png

Сначала инициализируется в нуль структура DOSDEVICENAME, которая также имеет тип UNICODE_STRING.

5.png

Драйвер помещает в поле LENGTH нуль, помещая в него значение регистра AX, которое равно здесь 0. А затем идет инструкция STOSD, которая копирует значение из регистра EAX, т.е. помещает нуль в адрес, куда указывает регистр EDI, т.е. в поле MAXIMUMLENGHT. А затем следующая инструкция STOSW копирует из регистра AX значение, т.е. нуль в два следующих байтах, т.е. помещая нуль в 6 байтов, т. е. инициализирует два оставшихся поля структуры, которые занимают 6 байтов (1 WORD и DWORD).

Компилятор только инициализирует структуру DOSDEVICENAME. Другая переменная, которая называется DEVICENAME, не равна нулю. Драйвер использует ее напрямую.

6.png

Другими словами, DEVICENAME - это строка, которая преобразуется в тип структуры UNICODE_STRING. Другими словами, в её трех полях будет длина, максимальная длина и указатель, который мы передаем в строку источник. Он будет скопирован в третье поле.

7.png

В моей машине указатель находится по адресу 0x00016938. Это смещение скопирует его в третье поле структуры.

8.png

В DOSDEVICENAME вы будете создавать другой указатель UNICODE_STRING. Я использую как источник эту другую строку.

Затем идёт вызов функции IOCREATEDEVICE. Мы помним, что вам нужно было создать DEVICE OBJECT, чтобы иметь возможность общаться с программами из пользовательского режима.

9.png

10.png

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

11.png

Последний аргумент, это указатель на вновь созданную структуру DEVICE_OBJECT.

12.png

Мы видим, что если результат создания устройства отрицательный, который проверяется в этом условном знаковом переходе, драйвер переходит к оранжевым блокам ошибок, а объект устройства удаляется с помощью функции IODELETEDEVICE.

13.png

Затем структура DRIVEROBJECT будет инициализироваться из регистра ESI + 38. Поскольку ESI указывает на DRIVEROBJECT, я нажимаю T. Я могу посмотреть, какое это поле (но это DRIVER_OBJECT идём в LOCAL TYPES и синхронизируем типы)

14.png

15.png

Т.е. это указатель на структуру MAJORFUNCTION, про которую мы помним, что он представляет собой таблицу указателей, которая, согласно позиции, приведет меня к разным функциям в зависимости от случая. Вспомните это, например.

16.png

Первый указатель, т.е. тот, который находится в позиции 0, является IRP_MJ_CREATE и драйвер будет переходить на него, когда вы вызываете функцию CREATEFILE, чтобы открыть дескриптор устройства. Второй указатель, т.е. со значением 0x1 находится в положении 4, так как он являются DWORD и т.д. Это означает, что обратно пропорционально, если у меня есть поле этой структуры по её смещению, чтобы знать, какой указатель нам нужен надо делить его на четыре. Из примера, который мы использовали в предыдущих драйверах давайте вспомним.

17.png

Это соответствует значению 0x38/4, т.е.

Python>hex(0x38/4)
0xE

18.png

Т.е. 0xE соответствует IRP_MJ_DEVICE_CONTROL, когда мы передали код IOCTL из режима пользователя. Этот указатель мы перезаписали с помощью обработчика, так что в соответствии с тем, какой код IOCTL, различные действия будут выполняться с помощью конструкции SWITCH. Например так.

В текущем случае, мы видим, что драйвер инициализирует значения начиная с указателя на начало таблицы MAJORFUNCTION. Он копирует значение регистра EAX, в которое помещается смещение функции, которая называется _IRP_NOTIMPLEMENTEDHANDLERS. Копирование происходит 0x1C раз. Это значение передаётся в регистре ECX. Оно равно количеству указателей, которые нужно инициализировать.

19.png

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

20.png

Поскольку регистр EDX хранит указатель на начало таблицы MAJORFUNCTION, его содержимым является позиция 0, т.е.

+#define IRP_MJ_CREATE 0x00
+#define IRP_MJ_CREATE_NAMED_PIPE 0x01
+#define IRP_MJ_CLOSE 0x02
+#define IRP_MJ_READ 0x03
+#define IRP_MJ_WRITE 0x04
+#define IRP_MJ_QUERY_INFORMATION 0x05
+#define IRP_MJ_SET_INFORMATION 0x06
+#define IRP_MJ_QUERY_EA 0x07
+#define IRP_MJ_SET_EA 0x08
+#define IRP_MJ_FLUSH_BUFFERS 0x09
+#define IRP_MJ_QUERY_VOLUME_INFORMATION 0x0a
+#define IRP_MJ_SET_VOLUME_INFORMATION 0x0b
+#define IRP_MJ_DIRECTORY_CONTROL 0x0c
+#define IRP_MJ_FILE_SYSTEM_CONTROL 0x0d
+#define IRP_MJ_DEVICE_CONTROL 0x0e
+#define IRP_MJ_INTERNAL_DEVICE_CONTROL 0x0f
+#define IRP_MJ_SCSI 0x0f
+#define IRP_MJ_SHUTDOWN 0x10
+#define IRP_MJ_LOCK_CONTROL 0x11
+#define IRP_MJ_CLEANUP 0x12
+#define IRP_MJ_CREATE_MAILSLOT 0x13
+#define IRP_MJ_QUERY_SECURITY 0x14
+#define IRP_MJ_SET_SECURITY 0x15
+#define IRP_MJ_POWER 0x16
+#define IRP_MJ_SYSTEM_CONTROL 0x17
+#define IRP_MJ_DEVICE_CHANGE 0x18
+#define IRP_MJ_QUERY_QUOTA 0x19
+#define IRP_MJ_SET_QUOTA 0x1a
+#define IRP_MJ_PNP 0x1b
+#define IRP_MJ_PNP_POWER 0x1b
+#define IRP_MJ_MAXIMUM_FUNCTION 0x1b


Мы будем создавать структуру MAJORFUNCTION.

struct __MajorFunction{
unsigned int _MJ_CREATE;
unsigned int _MJ_CREATE_NAMED_PIPE;
unsigned int _MJ_CLOSE;
unsigned int _MJ_READ;
unsigned int _MJ_WRITE;
unsigned int _MJ_QUERY_INFORMATION;
unsigned int _MJ_SET_INFORMATION;
unsigned int _MJ_QUERY_EA;
unsigned int _MJ_SET_EA;
unsigned int _MJ_FLUSH_BUFFERS;
unsigned int _MJ_QUERY_VOLUME_INFORMATION;
unsigned int _MJ_SET_VOLUME_INFORMATION;
unsigned int _MJ_DIRECTORY_CONTROL;
unsigned int _MJ_FILE_SYSTEM_CONTROL;
unsigned int _MJ_DEVICE_CONTROL;
unsigned int _MJ_INTERNAL_DEVICE_CONTROL;
unsigned int _MJ_SCSI;
unsigned int _MJ_SHUTDOWN;
unsigned int _MJ_LOCK_CONTROL;
unsigned int _MJ_CLEANUP;
unsigned int _MJ_CREATE_MAILSLOT;
unsigned int _MJ_QUERY_SECURITY;
unsigned int _MJ_SET_SECURITY;
unsigned int _MJ_POWER;
unsigned int _MJ_SYSTEM_CONTROL;
unsigned int _MJ_DEVICE_CHANGE;
unsigned int _MJ_QUERY_QUOTA;
unsigned int _MJ_SET_QUOTA;
unsigned int _MJ_PNP;
unsigned int _MJ_PNP_POWER;
unsigned int _MJ_MAXIMUM_FUNCTION;
};

Я знаю, что это указатели, но для нашего случая я буду использовать тип UNSIGNED INT и это будет работать. Проблема состоит в том, что это локальные типы. При использовании операции INSERT, IDA не принимают структуру. Поэтому я буду экспортировать её. Я добавляю структуру и перезагружаю ее с помощью FILE→ LOAD FILE→ PARSE C HEADER FILE

21.png

Я добавил структуру в ЗАГОЛОВОЧНЫЙ ФАЙЛ.

22.png

Теперь возникает вопрос

23.png

Я позволял себе редактировать внутри структуры DRIVER_OBJECT, тип MAJORFUNCTION в LOCAL TIPES ?

24.png

Мы видим, что я изменил структуре определение поля MAJORFUNCTION. Внутри структуры DRIVER_OBJECT для того, чтобы она была типа __MAJORFUNCTION, который я определил.

25.png

26.png

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

Когда мы нажимаем T, мы не можем выбрать структуру DRIVER_OBJECT, потому что регистр EDX указывает на таблицу MAJORFUNCTION, поэтому я выбираю последнюю.

27.png

28.png

Сейчас стало намного лучше. Все в порядке. Я определил функции, которые будут использоваться, т. е. _MJ_CREATE, _MJ_CLOSE, _MJ_DEVICE_CONTROL и те, которые будет вызываться, когда драйвер останавливается через функцию DRIVERUNLOAD.

Очевидно, когда из режима пользователя мы вызвали функцию CREATEFILE, вызывается функция, которая перезаписывает поле _MJ_CREATE. Когда мы передаем IOCTL код в функцию DEVICEIOCONTROL, вызывается _MJ_DEVICE_CONTROL. Когда вызывается функция CLOSEHANDLE, драйвер вызывает ту, которая перезаписывает поле _MJ_CLOSE. И когда драйвер останавливается, вызывается та, которая перезаписывает функцию DRIVERUNLOAD.

Мы будем смотреть на функцию, которая будет вызываться при передаче IOCTL.

Мы синхронизируем структуру IRP из вкладки LOCAL TYPES.

Как мы видели в части 53, поле 60 из IRP указывает на структуру _IO_STACK_LOCATION.

29.png

Регистр ESI указывает на _IO_STACK_LOCATION, поэтому все, что равно ESI + XXX, будет полем вышеупомянутой структуры. После синхронизации из вкладки LOCAL TYPES.

Напомним, что у структуры _IO_STACK_LOCATION есть несколько опций. Я выберу ту, которая соответствует IOCONTROLCODE.

30.png

Мы видим, что в соответствии с кодом IOCTL, SWITCH отправляет нас в разные блоки и что они помечены типом уязвимости, который имеет каждый путь.

31.png

32.png

Здесь есть один блок, который говорит нам, что он имеет STACKOVERFLOW. Поэтому вам не нужно слишком сильно себя утруждать и искать переполнение.

33.png

Мы видим, что есть два аргумента, которые передают в регистре EDI структуру IRP а в регистре ESI IRPSP - это имя переменной типа _IO_STACK_LOCATION, которая была в регистре ESI.

34.png

35.png

Это указатель на входной буфер. Также в этой же субструктуре находится IOCONTROLCODE и длина входного и выходного буфера. Предположительно эти значения передаются то же. Давайте посмотрим, что с ними делать.

36.png

Мы видим, что этот SIZE и этот буфер передаются в функцию _TRIGGERSTACKOVERFLOW.

37.png

Мы видим, что драйвер помещает нуль с помощью регистра ESI в первый DWORD буфера KERNELBUFFER и затем с помощью функции MEMSET помещает нуль в следующий DWORD, так как происходит сложение KERNELBUFFER + 4, и получается размер 0x7FC.

38.png

Вышеупомянутый буфер имеет длину 512 * 4, так как это массив DWORD (DD), поэтому общая длина в десятичной системе равна:

512 * 4
Out[64]: 2048


В HEX это

hex(2048)
Out[65]: '0x800'


Поэтому, поместив в первый DWORD нуль, а затем в оставшиеся 0x7FC байт. Действительно, драйвер заполнит весь буфер размером 0x800 нулями. (0x7FC + 4 = 0x800)

Затем драйвер вызовет функцию PROBEFORREAD, которая проверит, выровнен ли входной буфер в пользовательском режиме и находится ли он в пользовательском пространстве.

39.png

40.png

Затем драйвер печатает указатели буферов и их размеры.

41.png

Здесь мы ясно видим переполнение стека, поскольку драйвер использует размер, который я передаю ему как данные для копирования из входного буфера в пользовательский, в буфер в ядре, который является назначением.

42.png

43.png

Здесь мы видим, что при печати размера буфера ядра, драйвер используйте тот, который находится в регистре ESI, который является константой 0x800, но при выполнении функции MEMCPY он используйте аргумент SIZE, который я передал ему, без каких-либо проверок, которые будут приводить к переполнению стека и поскольку здесь нет COOKIE, он будет легко переполняться.

В следующей части мы будем делать скрипт с эксплуатацией. На этом здесь, мы закончим анализ.

=======================================================
Автор текста: Рикардо Нарваха - Ricardo Narvaja (@ricnar456)
Перевод на русский с испанского: Яша_Добрый_Хакер(Ростовский фанат Нарвахи).
Перевод специально для форума системного и низкоуровневого программирования — WASM.IN
06.11.2018
Версия 1.0

1 5.128
yashechka

yashechka
Ростовский фанат Нарвахи

Регистрация:
2 янв 2012
Публикаций:
90