R0 CREW

(Phrack 65): Stealth Hooking: another way to subvert the Windows kernel

Переводчики: ALiEN Assault & Izg0y
Источник: xaknotdie.org

  1. Введение в технологии anti-rookit и обхода
    1.1 - Руткиты и технологии anti-rootkit
    1.2 - О защите на уровне ядра
    1.3 - Ключевая концепция: используем код ядра против самого себя
  2. Введение в скрытный перехват на IDT.
    2.1 - Как Windows управляет аппаратными прерываниями
    [INDENT2]2.1.1 - Распределение аппаратных прерываний в Windows
    2.1.2 - Перехватываем аппаратные IT, как ниндзя
    2.1.3 - Приложение 1 : кейлоггер на уровне ядра
    2.1.4 - Приложение 2 : NDIS-сниффер входящих пакетов[/INDENT2]2.2 - Заключение о скрытном перехвате на IDT
  3. Захват NonPaged pool с помощью скрытного перехвата
    3.1 - Обзор распределения памяти ядром
    [INDENT2]3.1.1 - Различия между Paged и NonPaged pool
    3.1.2 - Таблицы NonPaged pool
    3.1.3 - Алгоритмы распределения и высвобождения[/INDENT2]3.2 - Исполнение кода путем внедрения в код распределения
    [INDENT2]3.2.1 - Нарушение целостности данных в MmNonPagedPoolFreeListHead
    3.2.2 - Расширяем для всех объёмов[/INDENT2]3.3 - Используем нашу позицию
    [INDENT2]3.3.1 - Перенаправление стэка
    3.3.2 - Внедрение кода в пользовательские процессы[/INDENT2]1. Обнаружение
  4. Заключение
  5. Ссылки

—[ - 1 - Введение в технологии anti-rookits и обхода

Сегодня руткиты и анти-руткиты значат всё больше и больше на ландшафте IT-безопасности. Любовь одних и ненависть других сделали руткиты своего рода Святым Граалем среди бэкдоров: скрытные, маленькие, плотно работающие с “железом”, гениальные, злобные… Уровень контроля, который дают удаленные или локальные руткиты, делает их лучшим выбором атакующих.

Антируткиты пытаются обнаружить и уничтожить эти злобные программы. Сложность Rk-технологии и их быстрая эволюция делают разработку rk или anti-rk очень сложной задачей.

Этот текст рассказывает о rootkit’ах под Windows платформы. Точнее, о новой технологии перехвата, которая может быть применена к ядру Windows… Предполагается, что читатель знаком с rootkit-технологиями под Windows.

----[ 1.1 - Rootkit и anti-rootkit техники

Rootkit управляет поведением операционной системы. Для достижения этой цели можно просто изменить бинарный код ОС, но это слишком палевно. Большинство rk используют перехваты основных функций и изменяют их поведение. Простой перехват перенаправляет поток выполнения, заменяя функции запуска или указатель на функцию, но нет никакого “единого” пути для перехвата в программе. Самый общий пример - SSDT (System Service Descriptor Table), это таблица содержит список системмных вызовов, который содержит указатели на функции. Если ты можешь модифицровать указатели в этой таблице, то у тебя есть контроль над функциями. Существует много критических областей, с помощью которых можно эффективно контролировать работу системы.

Anti-rootkit’ы пытаются проверять критические области, но задача эта достаточно сложна. Большинство свего времени Anti-rootkit’ы проводят, сравнивая между собой код в памяти и на жёстком диске, или проверяя различные указатели на функции в таблице на изменения.

Это как война между rk- и anti-rk-разработчиками за лучшие способы перехата критических областей операционной системы.

В Windows rootkit’ами часто используются следующие области:

  • SSDT (таблица системных вызовов ядра) и shadow SSDT (таблица системных вызовов
  • win32k ), самое простое решение.
  • MSR (Model Specific Registers) может изменяться rootkit’ом. В Windows MSR_SYSENTER_EIP используется в инструкции ‘sysenter’ для перехода в ring0 - режим ядра. Изменение MSR позволяет атакующему получить контроль над системой.
  • MajorFunctions функции используются для общения с драйвером I/O или другими устройствами, перехват этих функций может быть полезен для rootkit’a.
  • IDT (Interrupt Descriptor Table), эта таблица используется системой для обработки исключений и прерываний.

Есть и другие техники. Получая доступ к ядру, объекты руткита могут легко изменить информацию о процессах, нитях, загружаемых модулях и так далее. Такая техника называется DKOM (Direct Kernel Object Manipulation). Например, ядро Windows поддерживает двойной связный список, называемый PsActiveProcessList (EPROCESS структура), содержащий информацию о запущенных процессах. После исключения процесса из этого списка, он пропадёт из списка процессов в Диспетчере задач, но процесс будет существовать и продолжать работу.

Чтобы блокировать этот метод, anti-rk проверяют другие секции. Для процессов они используют чтение PspCidTable, которая имеет таблицу PID (Process IDentifier) и TID (Thread IDentifier). Сравнение между этой таблицей и PsActiveProcessList может выявить скрытые процессы. Чтобы обойти anti-rk, атакующий ищет другие пути и решения, которые не обнаруживаются на данное время.

Один из первых документов о скрытной работе с Windows был написан Holy Father, “Invisibility on NT boxes” [1]. С этим документом пришёл 1 из первых вариантов публичного rootkit’a с ring0 драйвером, Hacker Defender [2], написанный Holy Father и Ratter из знаменитого VX-зина 29A [3]. Этот драйвер мог поднять права процесса, мспользовав манипуляции с “кольцами”. Остальная часть rootkit’a использует перехваты для выполнения файлов и сокрытия ключей реестра, внедрение dll в чужой процесс. Хоршим примером rootkit’a полностью на уровне ring0 является NT Rootkit от Greg Hoglund [4], этот драйвер использует перехват SSDT для скрытых операций. Он регистрирует Filter Device Object выше системы NTFS и клавиатуры для фильтрации IRP (I/O Request Packets). Он также рреализует драйвер протокола NDIS для сокрытия работы с сетью Несмотря на то, что он был написан для NT 4.0 и Win2K, это отличный пример для начинающих.

После него пришёл более продвинутый ring0 rk FU [5], написанный Fuzen_op и усовершенствованный FUto, опубликованный в знаменитом техническом журнале Uninformed [6]. Усовершенствования в Vista (проверка подписи драйвера) по большей части основываются на аппаратных особенностях. Однако BootRoot [7] и Pixie [8] написанные eЕye начинают свою работу ещё перед загрузкой защиты. В завершении Joanna Rutkowska со своей Blue Pill [9] использует технологию виртуализации для создания “прослойки” между операционной системой и железом.

В реальных условиях rk используют для спама или организации ботнетов. Большинство rk используют как правило старые и хорошо известные техники, однако встречаются и те, которые вызывают реальный интерес. Например: серия Rustock [10] или StormWorm [11] и MBR rootkit [12]. Они содержат много иннтересного, например ADS (Alternate Data Stream), “запутывание” кода, anti-debug, anti-VM или полиморфный код. Их целью является не только проникновение в ядро, но так же и замедление их анализа и обнаружения.

Технологии, используемые в rootkit’ах постоянно совершенствуются, андерграунд разрабатывает POCs для совершенствования существующих технологий. Unreal [13] и AK992 [14] содержат отличные примеры. Первый использует ADS и NTFS MajorFunctions перехват для сокрытия, второй проверяет завершение IRP при работе с дисками. Можно найти множество самых различных примеров на rootkit.com.

Завершение этой части было бы не полным, если не упомнятуть о anti-rk. Самыми известными являются Rk Unhooker написанный MP_ART и EP_X0FF из команды UG North. Другие anti-rk DarkSpy [15] написанный CardMagic, IceSword [16] написанный pjf и конечно Gmer [17].

----[ 1.2 - О защите на уровне ядра

Если мы говорим о защите, то должны обратить внимание на то, что она представляет собой в рамках системы. Защита будет иметь преимущества только в том случае, если она осуществляется на более высоком уровне, нежели атака. Такие защиты как PaX или Exec Shield эффективны, потому что реализованы на уровне ядра.

PatchGuard и другие HIPS также защищают систему, но атакующий может найти лазейку в их собственной защите и получить те же привелегии, что и они сами (в этом случаи эти защиты станут бесполезными). Защита является эффективной, только когда она НЕ может быть “повреждена” при атаке. Если злоумышленник нашел способ внедрить код в защиту, то она мертва.

Вот почему PatchGuard оказался не настолько эффективен [18]. Но мы знаем, что отключение или уничтожение защиты слишком палевно. Нет, лучше решение это “лететь” под “радаром” работая с особыми обьектами и событиями, которые невозможно проверить из-за их неустойчивости.

В июне 2006, Greg Hoglund представил разработку KOH (Kernel Object Hooking) [19]. Новый путь исполнения кода в обход, вам не придётся изменять статическую кодовую секцию, но ваша работа на динамически выделяемых структурах/коде как DPC (Deferred Procedure Calls). Для защит, это очень сложно обнаружить и проверить из-за их нестабильности.

Другие объекты IRP. Они используются в ядре Windows для операций I/O с менеджерами устройств. Каждая операция I/O для оборудования генерирует IRP, sycalls отправляет IRP для драйвера через его устройство. В общем драйвер владеет несколькими устройствами; одно из них используется для комуникации с режимом пользователя, используя IOCTL и другие устройства, фильтруя IRP можно решить поставленную задачу. IRP отправляется драйверу, используя свою таблицу MajorFunctions. Эта таблица содержит различные функциональные возможности драйвера. Вы можете проверить результат возвращаемый MajorFunction, установив подпрограмму для мониторинга IRP. Они являются очень неустойчивыми объектами; контроль и их проверка очень сложна.

Фактически, если вы захотите проверить всё что только можно, придётся перепроективровать архитектуру операционной системы. Так, что имейте ввиду - невозможно находится везде и постоянно, и мы продемонстрируем это в следующей части.

----[ 1.3 - Ключевая концепция: используем код ядра против самого себя

Основная идея этого документа заключается в использовании кода ядра. Это возможно т.к. ввод определяет поведение кода. Отправка специфического кода приводит к его исполнению в уязвимом ПО. “Опасный” ввод, разумеется, определяется вашей целью. Пространство ядра содержит море сценариев эксплуатации т.к. среду исполнения постоянно изменяют. А rootkit не может изменить базовые принципы работы. Но он может изменить среду вокруг своего кода. Методы эксплуатации динамической памяти - отличный пример. Заменяя структуру блока памяти, вы можете записать поверх 4 байта. Некоторые техники могут даже изменить адрес блока [20]. Это работает потому, что программа “доверяет” этой информации. В ядре, у тебя есть полный контроль над средой. Также, полная проверка ядра скажется на производительности и не представляется возможной.

Замена среды кода, была успешно использована в phide2 rootkit [21]. Этот rootkit может скрыть поток без перехвата. Этот метод требует серьёзных знаний в области обратного проектирования (это расширяет концепцию поведения операционной системы). Универсальная защита основана на универсальных предположениях, таких, как проверка только “образа” драйвера на перехваты кода. В наши дни защита операционной системы требует использования современных rootkit технологий.

—[ 2 - Введени в stealth hooking на IDT

Нашу концепцию скрытного перехвата лучше всего проиллюстрирует пример работы с IDT. Сначала определимся с IDT и целями. Потом мы обсудим аппаратные прерывания и тем, как Windows обрабатывает их.

IDT (Interrupt Descriptor Table) - это специфическая таблица, находящаяся в ядре. IDT может быть прочитан и в ring3, но чтобы вписать в неё, у вас должен быть доступ к ring0. IDT состоит из 256 записей в структуре KIDTENTRY и вы можете, используя Kernel Debugger (KD) в Debugging Tools for Windows [22] видеть её сожержимое.

kd> dt nt!_KIDTENTRY
   +0x000 Offset           : Uint2B
   +0x002 Selector         : Uint2B
   +0x004 Access           : Uint2B
   +0x006 ExtendedOffset   : Uint2B

Здесь мы не будем обсуждать структуру/содержимое IDT. Все, кто хочет узнать подробнее, могут заглянуть в Phrack 59, и понять работу IDT [23].

Первые 32 записи в IDT зарезервированы CPU для исключений. Другие - используются для обработки аппаратных прерываний и специальных системных событий.

Вот дамп первых 64 записей для Windows IDT:

kd> !idt -a
Dumping IDT:

00: 804df350 nt!KiTrap00
01: 804df4cb nt!KiTrap01
02: Task Selector = 0x0058
03: 804df89d nt!KiTrap03
04: 804dfa20 nt!KiTrap04
05: 804dfb81 nt!KiTrap05
06: 804dfd02 nt!KiTrap06
07: 804e036a nt!KiTrap07
08: Task Selector = 0x0050
09: 804e078f nt!KiTrap09
0a: 804e08ac nt!KiTrap0A
0b: 804e09e9 nt!KiTrap0B
0c: 804e0c42 nt!KiTrap0C
0d: 804e0f38 nt!KiTrap0D
0e: 804e164f nt!KiTrap0E
0f: 804e197c nt!KiTrap0F
10: 804e1a99 nt!KiTrap10
11: 804e1bce nt!KiTrap11
12: 804e197c nt!KiTrap0F
13: 804e1d34 nt!KiTrap13
14: 804e197c nt!KiTrap0F
15: 804e197c nt!KiTrap0F
16: 804e197c nt!KiTrap0F
17: 804e197c nt!KiTrap0F
18: 804e197c nt!KiTrap0F
19: 804e197c nt!KiTrap0F
1a: 804e197c nt!KiTrap0F
1b: 804e197c nt!KiTrap0F
1c: 804e197c nt!KiTrap0F
1d: 804e197c nt!KiTrap0F
1e: 804e197c nt!KiTrap0F
1f: 804e197c nt!KiTrap0F

20: 00000000 
21: 00000000 
22: 00000000 
23: 00000000 
24: 00000000 
25: 00000000 
26: 00000000 
27: 00000000 
28: 00000000 
29: 00000000 
2a: 804deb92 nt!KiGetTickCount
2b: 804dec95 nt!KiCallbackReturn
2c: 804dee34 nt!KiSetLowWaitHighThread
2d: 804df77c nt!KiDebugService
2e: 804de631 nt!KiSystemService
2f: 804e197c nt!KiTrap0F
30: 806f3d48 hal!HalpClockInterrupt
31: 80dd816c i8042prt!I8042KeyboardInterruptService (KINTERRUPT 80dd8130)
32: 804ddd04 nt!KiUnexpectedInterrupt2
33: 80dd3224 serial!SerialCIsrSw (KINTERRUPT 80dd31e8)
34: 804ddd18 nt!KiUnexpectedInterrupt4
35: 804ddd22 nt!KiUnexpectedInterrupt5
36: 804ddd2c nt!KiUnexpectedInterrupt6
37: 804ddd36 nt!KiUnexpectedInterrupt7
38: 806edef0 hal!HalpProfileInterrupt
39: 80f0827c ACPI!ACPIInterruptServiceRoutine (KINTERRUPT 80f08240)
3a: 80dc67cc vmsrvc+0x1C16 (KINTERRUPT 80dc6790)
3b: 80df6414 NDIS!ndisMIsr (KINTERRUPT 80df63d8)
3c: 80de040c i8042prt!I8042MouseInterruptService (KINTERRUPT 80de03d0)
3d: 804ddd72 nt!KiUnexpectedInterrupt13
3e: 80ed78a4 atapi!IdePortInterrupt (KINTERRUPT 80ed7868)
3f: 80f01dd4 atapi!IdePortInterrupt (KINTERRUPT 80f01d98)
40: 804ddd90 nt!KiUnexpectedInterrupt16
[...]

Этот дамп типичен для Windows IDT, вы можете видеть список записей IDT, их адрес и название. В первых 32 записях расположены KiTrap* функции, которые обрабатывают исключения. Остальную часть таблицы составляют системные прерывания, такие как KiSystemService и KiCallbackReturn и используемые драйверами I8042KeyboardInterruptService или I8042MouseInterruptService.

----[ 2.1 - Как Windows управляет аппаратными прерываниями

При разговоре о прерываниях мы должны ввести понятие IRQL (Interrupt ReQuest Level). Ядро представляет несколько IRQL, от 0 до 31 для x86, чем больше номер, тем выше “уровень” прерывания. Хотя ядро предоставляет свой набор IRQL для програмных прерываний, HAL (Hardware Abstraction Layer) соотносит аппаратные прерывания и номера IRQL.

   +----------------+
31 |    Высшие      | \
to |    IRQLs       | | Часы, сбой системы.
27 |                | /
   +----------------+
26 |                | \
to |  DEVICE_IRQL   | | Аппаратные прерывания.          
3  |                | /
   +----------------+
2  | DISPATCH_LEVEL | Планировщик, DPC.
   +----------------+ 
1  |    APC_LEVEL   | Распределение APC.
   +----------------+ 
0  |  PASSIVE_LEVEL | IRQL нитей. 
   +----------------+    

Каждый процессор имеет свой IRQL. Ты можешь иметь IRQL=DISPATCH_LEVEL, то есть работать на PASSIVE_LEVEL. В действительности IRQL представляет собой возможность маскировки текущего исполнения кода.

Некоторые компоненты системы недоступны когда уровень IRQL>=DISPATH_LEVEL. Доступ к paged-памяти (память, которая может находится на диске), и уйма других возможностей ядра.

Аппаратные прерывания являются асинхронными и приходят от периферийных устройств. Например, когда ты нажимаешь на клавишу, клавиатура посылает IRQ (Interrupt ReQuest) запрос на южный мост [24] на твоё прерывание контроллера северного моста [25]. Южный мост - это чип, который реализует I/O узла. Эта микросхема получает все запросы I/O внешних прерываний и посылает их ан северный мост. Северный мост непосредственно подключён к памяти и высокоскоростной графической шине. Эта микросхема также известна как контроллер памяти узла.

На большинстве x86 систем мы найдём микросхему i82489, Advanced Programmable Interrupt Controller (APIC). APIC сочетает 2 основных компонента, I/O APIC и LAPIC (Local APIC) на каждом ядре. I/O APIC используется для “отсылки” прерывания для более приспособленного ядра. Согласно принципу местоположения, I/O APIC доставит аппаратное прерывание ядру которое обрабатывало его в прошлый раз [26].

После того, как LAPIC переводит IRQ в 8-битовое значение, вектор прерывания. Этот вектор прерывания представляет собой список записей IDT, связанных с обработчиком. Когда ядро готово выполнить прерывание, поток инструкций перенаправляется по адресу в IDT.

   IDT            IDT            IDT           IDT 
    1              2              3             4 
  +---+          +---+          +---+         +---+         
  |   |          |   |          |   |         |   |
  |---|          |---|          |---|         |---|
  |   |          |   |          |   |         |   |
  |---|          |---|          |---|         |---|
  |   |          |   |          |   |         |   |
  +---+          +---+          +---+         +---+    
    |              |              |             |
+--------+     +--------+     +--------+    +--------+
|        |     |        |     |        |    |        | 
| ядро 1 |     | ядро 2 |     | ядро 3 |    | ядро 4 |
|        |     |        |     |        |    |        |
+--------+     +--------+     +--------+    +--------+ 
| LAPIC  |     | LAPIC  |     | LAPIC  |    | LAPIC  |
+---+----+     +---+----+     +---+----+    +---+----+
    |              |              |             |   
    |              |              |             |
<---+--------------+------+-------+-------------+-----> 
               Сообщения  |         Системная шина процессора
              прерываний  |
                          |
                          |
    Внешние        +------+------+
    прерывания     |             |
    --------------->   I/O APIC  |  
                   |             |
                   +-------------+

-----[ 2.3.1 Распределение аппаратных прерываний в Windows

В Windows обработчик прерываний не выполняется сразу же, есть шаблон кода в первую очередь. Этот шаблон реализуется в функции KiInterruptTemplate и делает две вещи. Во первых, сохраняет текущее состояние ядрв в стэке и отправляет поток кода на “распределитель прерываений”.

Когда прерывание “поднято”, после того, как ядро основного состояния сохранено, поток кода направляется на обработку прерываний, как это определено в IDT. Фактически каждый обработчик прерывания в IDT на KiInterruptTemplate [27]. KiInterruptTemplate обращается к KiInterruptDispatch, который выполняет следующие операции:

  • Принимает spinlock системной подпрограммы.
  • Поднимает IRQL у DEVICE_IRQL, IRQL данного вектора прерываний вычисляется путём вычитания вектора прерываний из 27d.
  • Обращается к обработчику прерывания, ISR (Interrupt Service Routine).
  • Понижает IRQL.
  • Высвобождает spinlock системной подпрограммы.

Например, ISR клавиатуры - I8042KeyboardInterruptService. ISR - подпрограммы, обрабатывающие прерывания подобно top-halves ядра Linux. Согласно WDK (Windows Driver Kit), ISR должен сделать всё для устройства, чтобы высвободить прерывание. Затем ISR должен сделать лишь необходимое для сохранения состояния и постановки DFC в очередь. Это подразумевает, что управление прерыванием будет на низшем уровне IRQL, чем во время выполнения ISR. Процесс I/O производится в DPC.

DPC (Deferred Procedure Call) является аналогом bottom-halves в Linux. DPC работает на IRQL DISPATCH_LEVEL, ниже чем ISR IRQL. Фактически в ISR будет очередь DPC для обработки прерываний на более низком IRQL, во избежание основного приоритетного прерывания, занимающего слишком большое время. Для клавиатуры DPC - I8042KeyboardIsrDpc. Ниже представлен рисунок, для более наглядного понимания:

                                              +-------------------------+
Аппаратное прерывание                    /----> Здесь мы находимся на   |  
       |                                 |    | IRQL=DEVICE_LEVEL       |
       |                                 |    | KiInterruptDispatch   |
       /---> IDT ---\                    |    | обращается к ISR.   |
                    |                    |    |                         |
                    |                    |    | ISR обрабатывает      |
            +-----------------------+    |    | прерывание и ставит DPC |
            |  KiInterruptTemplate ------/    | для дальнейшей          |
            +-----------------------+         | обработки               |
                                              +-------------------------+

KiInterruptDispatch получает основной параметр от KiInterruptTemplate, указатель на объект прерывания, сохранённый в регистре EDI. Обьект прерывания определяется структурой KINTERRUPT:

kd> dt nt!_KINTERRUPT
   +0x000 Type             : Int2B
   +0x002 Size             : Int2B
   +0x004 InterruptListEntry : _LIST_ENTRY
   +0x00c ServiceRoutine   : Ptr32     unsigned char 
   +0x010 ServiceContext   : Ptr32 Void
   +0x014 SpinLock         : Uint4B
   +0x018 TickCount        : Uint4B
   +0x01c ActualLock       : Ptr32 Uint4B
   +0x020 DispatchAddress  : Ptr32     void 
   +0x024 Vector           : Uint4B
   +0x028 Irql             : UChar
   +0x029 SynchronizeIrql  : UChar
   +0x02a FloatingSave     : UChar
   +0x02b Connected        : UChar
   +0x02c Number           : Char
   +0x02d ShareVector      : UChar
   +0x030 Mode             : _KINTERRUPT_MODE
   +0x034 ServiceCount     : Uint4B
   +0x038 DispatchCount    : Uint4B
   +0x03c DispatchCode     : [106] Uint4B

Мы получаем в этой структуре SpinLock и ServiceRoutine. Обратите внимание, что SynchronizeIrql содержит IRQL, когда ISR будет выполняться.

Для каждой записи в IDT, которая обрабатывает аппаратное прерывание, KiInterruptTemplate содержится в таблице DispatchCode структуры KINTERRUPT.

Для клавиатуры у нас есть KINTERRUPT:

kd> dt nt!_KINTERRUPT 80dd8130
   +0x000 Type             : 22
   +0x002 Size             : 484
   +0x004 InterruptListEntry : _LIST_ENTRY [ 0x80dd8134 - 0x80dd8134 ]
   +0x00c ServiceRoutine   : 0xfa815495     unsigned char  
   ->i8042prt!I8042KeyboardInterruptService+0
   +0x010 ServiceContext   : 0x80e2ec88 
   +0x014 SpinLock         : 0
   +0x018 TickCount        : 0xffffffff
   +0x01c ActualLock       : 0x80e2ed48  -> 0
   +0x020 DispatchAddress  : 0x804da8d8     void  nt!KiInterruptDispatch+0
   +0x024 Vector           : 0x31
   +0x028 Irql             : 0x1a ''
   +0x029 SynchronizeIrql  : 0x1a ''
   +0x02a FloatingSave     : 0 ''
   +0x02b Connected        : 0x1 ''
   +0x02c Number           : 0 ''
   +0x02d ShareVector      : 0 ''
   +0x030 Mode             : 1 ( Latched )
   +0x034 ServiceCount     : 0
   +0x038 DispatchCount    : 0xffffffff
   +0x03c DispatchCode     : [106] 0x56535554

Давай посмотрим на начало KiInterruptTemplate :

nt!KiInterruptTemplate:
804da972 54              push    esp
804da973 55              push    ebp
804da974 53              push    ebx
804da975 56              push    esi
804da976 57              push    edi
804da977 83ec54          sub     esp,54h
804da97a 8bec            mov     ebp,esp
804da97c 89442444        mov     dword ptr [esp+44h],eax
804da980 894c2440        mov     dword ptr [esp+40h],ecx
804da984 8954243c        mov     dword ptr [esp+3Ch],edx
804da988 f744247000000200 test    dword ptr [esp+70h],20000h
804da990 0f852a010000    jne     nt!V86_kit_a (804daac0)
804da996 66837c246c08    cmp     word ptr [esp+6Ch],8
804da99c 7423            je      nt!KiInterruptTemplate+0x4f (804da9c1)
804da99e 8c642450        mov     word ptr [esp+50h],fs
804da9a2 8c5c2438        mov     word ptr [esp+38h],ds
804da9a6 8c442434        mov     word ptr [esp+34h],es
804da9aa 8c6c2430        mov     word ptr [esp+30h],gs
804da9ae bb30000000      mov     ebx,30h
804da9b3 b823000000      mov     eax,23h
804da9b8 668ee3          mov     fs,bx
804da9bb 668ed8          mov     ds,ax
804da9be 668ec0          mov     es,ax
804da9c1 648b1d00000000  mov     ebx,dword ptr fs:[0]
804da9c8 64c70500000000ffffffff mov dword ptr fs:[0],0FFFFFFFFh
804da9d3 895c244c        mov     dword ptr [esp+4Ch],ebx
804da9d7 81fc00000100    cmp     esp,10000h
804da9dd 0f82b5000000    jb      nt!Abios_kit_a (804daa98)
804da9e3 c744246400000000 mov     dword ptr [esp+64h],0
804da9eb fc              cld
804da9ec 8b5d60          mov     ebx,dword ptr [ebp+60h]
804da9ef 8b7d68          mov     edi,dword ptr [ebp+68h]
804da9f2 89550c          mov     dword ptr [ebp+0Ch],edx
804da9f5 c74508000ddbba  mov     dword ptr [ebp+8],0BADB0D00h
804da9fc 895d00          mov     dword ptr [ebp],ebx
804da9ff 897d04          mov     dword ptr [ebp+4],edi
804daa02 f60550f0dfffff  test    byte ptr ds:[0FFDFF050h],0FFh
804daa09 750d            jne     nt!Dr_kit_a (804daa18)

nt!KiInterruptTemplate2ndDispatch:
804daa0b bf00000000      mov     edi,0
nt!KiInterruptTemplateObject:
804daa10 e9c3fcffff      jmp     nt!KeSynchronizeExecution+0x2 (804da6d8)
[...]

Запомни, этот код уникален для каждого KINTERRUPT. Мы говорили о том, что KiInterruptDispatch получает свои аргументы из регистра EDI (указатель на KINTERRUPT прерывания). В KiInterruptTemplate мы видим этот кусок кода:

[...] 
nt!KiInterruptTemplate2ndDispatch:
804daa0b bf00000000      mov     edi,0
nt!KiInterruptTemplateObject:
804daa10 e9c3fcffff      jmp     nt!KeSynchronizeExecution+0x2 (804da6d8)
[...]

Здесь мы видим mov “edi, 0” и jmp, но если мы смотрим на код KiInterruptTemplate содержащийся в KINTERRUPT клавиатуры, то мы видим :

ffb72525 bf5024b7ff      mov     edi,0FFB72450h ; Keyboard KINTERRUPT
ffb7252a e9a9839680      jmp     nt!KiInterruptDispatch (804da8d8)

Ух ты, инструкции меняются! Ядро динамически изменяет те 2 команды в коде KiInterruptTemplate. В EDI ы находим объект KINTERRUPT и jmp переход на KiInterruptDispatch.

Зачем это сделано? Потому что мы можем легко изменить обработчик Даже если мы часто имеем KiInterruptDispatch, мы можем найти KiFloatingDispatch или KiChainDispatch. KiChainedDispatch для векторов, распределяется между несколькими объектами и прерываниями KiFloatingDispatch похож на KiInterruptDispatch, но он сохраняет “плавающие” состояние.

В Windows реализованы API для соединения прерываний в IDT. IoConnectInterrupt и IoConnectInterruptEx, описанные в WDK:

NTSTATUS IoConnectInterrupt(
    OUT PKINTERRUPT  *InterruptObject,
    IN PKSERVICE_ROUTINE  ServiceRoutine,
    IN PVOID  ServiceContext,
    IN PKSPIN_LOCK  SpinLock  OPTIONAL,
    IN ULONG  Vector,
    IN KIRQL  Irql,
    IN KIRQL  SynchronizeIrql,
    IN KINTERRUPT_MODE    InterruptMode,
    IN BOOLEAN  ShareVector,
    IN KAFFINITY  ProcessorEnableMask,
    IN BOOLEAN  FloatingSave
    );

Как видно, IoConnectInterrupt возвращает в InterruptObject параметр KINTERRUPT структуры, тот же что и в IDT. Ранее мы видели в KiInterruptTemplate две таблицы, KiInterruptTemplateObject и KiInterruptTemplate2ndDispatch. Эти две таблицы используют функции ядра, чтобы найти две инструкции KiInterruptTemplateRoutine. KeInitializeInterrupt использует KiInterruptTemplateObject “метку” для обновления “jmp Ki*Dispatch” и KiConnectVectorAndInterruptObject для использования KiInterruptTemplate2ndDispatch для изменения “mov edi, <&Kinterrupt>”.

-----[ 2.3.2 Перехватываем аппаратные IT, как ниндзя

Теперь давайте подумаем. Мы хотим перехватить IDT в скрытном режиме, мы знаем, что непосредственно изменить запись - далеко не лучшее решение. Anti-rooktit’ы не проверяют динамически выделяемые KiInterruptTemplate. Так что мы будем изменять их, как и хотели. Есть три варианта:

  • Перенаправить “jmp Ki*Dispatch” нашей подпрограмме.
  • Изменить адрес в EDI на инструкцию “mov edi, <&Kinterrupt>”. Новый KINTERRUPT будет таким же как предыдущий, только ServiceRoutine будет изменён нами.
  • Создание собственного KiInterruptTemplate, сложно …

В этом документе мы рассмотрим простой способ. Мы изменим “mov edi, <&kinterrupt>” на “mov edi, <&OurKinterrupt>” и мы реализуем наш ServiceRoutine. Мы знаем, что эта инструкция следует за jmp, так при помощи дизассемблирования мы сможем отыскать команду перед jmp nt!KiInterruptDispatch и изменить её. Следует помнить, что когда ServiceRoutine выполняется, прерывания не обрабатываются, и мы работаем как DEVICE_IRQL IRQL. Это не справедливо, потому что многие функции ядра Windows недоступны. Известно, что многие ISR поставили в очередь DFC, так что после выполнения ISR последняя запись в текущей очереди DFC ядра должна содержать подпрограмму DFC нашего прерывания.

Если мы хотим получит доступ к данным, генерируемым прерыванием, мы должны работать подобно ISR. Заменить оригинальный ISR нашим ISR очень сложно, т. к. это зависит от целой кучи железок. Но мы знаем, что реальный I/O выполняется в DPC, когда KiInterruptTemplate обратится к нашему ServiceRoutine, сначала мы вызываем оригинальный ServiceRoutine и модифицируем последнюю запись DPC.

DPC представлен как KDPC структура :

kd> dt nt!_KDPC
   +0x000 Type             : Int2B
   +0x002 Number           : UChar
   +0x003 Importance       : UChar
   +0x004 DpcListEntry     : _LIST_ENTRY
   +0x00c DeferredRoutine  : Ptr32     void 
   +0x010 DeferredContext  : Ptr32 Void
   +0x014 SystemArgument1  : Ptr32 Void
   +0x018 SystemArgument2  : Ptr32 Void
   +0x01c Lock             : Ptr32 Uint4B

DPC список можно найти в KPRCB (Kernel Processor Control Region Block) структуре текущего процессора. KPRCB предшествует KPCR (Kernel Processor Control Block) структуре, которая находится по адресу FS:[0x1C] текущего процессора. KPRCB это 0x120 байт из начала KPCR структуры.

dt nt!_KPRCB
[...]
   +0x860 DpcListHead      : _LIST_ENTRY
   +0x868 DpcStack         : Ptr32 Void ; аргументы DPC
   +0x86c DpcCount         : Uint4B ; счетчик ядра DPC
   +0x870 DpcQueueDepth    : Uint4B ; Число DPC в списке
   +0x874 DpcRoutineActive : Uint4B
   +0x878 DpcInterruptRequested : Uint4B
   +0x87c DpcLastCount     : Uint4B
   +0x880 DpcRequestRate   : Uint4B
   +0x884 MaximumDpcQueueDepth : Uint4B
   +0x888 MinimumDpcRate   : Uint4B

Сейчас мы знаем, как отыскать DPC нашего прерывания, мы можем легко изменить его и сами обработать данные.

В случае с клавиатурой, DPC поставлен в очередь KeInsertQueueDpc в подпрограмме I8xQueueCurrentKeyboardInput, вызванной ISR клавиатуры.

kd> dt nt!_KDPC 80e3461c
+0x000 Type             : 19 ; 19=DpcObject
+0x002 Number           : 0 ''
+0x003 Importance       : 0x1 ''
+0x004 DpcListEntry     : _LIST_ENTRY [ 0xffdff980 - 0x80559684 ]
+0x00c DeferredRoutine  : 0xfa815650    void  i8042prt!I8042KeyboardIsrDpc
+0x010 DeferredContext  : 0x80e343b8 
+0x014 SystemArgument1  : (null) 
+0x018 SystemArgument2  : (null) 
+0x01c Lock             : 0xffdff9c0  -> 0

Демонстрация атаки :

                                            Структура MyKinterrupt
                                            +---------------------+
Аппаратное прерывание                  /---->  MyServiceRoutine   |
       |                               |    |  Вызов        |
       |                               |    |  оригинального ISR  ---\
       \---> IDT ---\                  |    |  Изменение DPC    |  |  
                    |                  |    |  очереди.           |  |
                    |                  |    +---------------------+  |
            +---------------------+    |                             |
            | KiInterruptTemplate -----/      Исходный Kinterrupt    |
            +---------------------+         +---------------------+  |
     Ядро                                   |                     |  |
+------------+                              |   ServiceRoutine <-----/
|            |                              | Чередует ISR's DPC  |
|DpcListHead |--\                           +---------------------+       
|            |  |
+------------+  |
                | +-----+     +-----+     +-----+     +-----+
                \-> DPC |---->| DPC |---->| DPC |---->| DPC |-->DpcListHead
   DpcListHead<---|     |<----|     |<----|     |<----|     |
                  +-----+     +-----+     +-----+     +-----+  
                                                        /\     
                                                        ||
                                                  Последняя запись DPC
                                                  Изменённая после вызова
                                                  ServiceRoutine.

-----[ 2.3.3 - Приложение 1 : кейлоггер на уровне ядра

Пришло время написать POC. В этом примере мы покажем как “подсмотреть” нажатия клавиш клавиатуры. Как вы поняли, мы можем контролировать DPC, генерируемый прерыванием. Для клавиатуры мы будем перехватывать I8042KeyboardIsrDpc, которая устанавливается в DPC прерывания клавиатуры. В нашем DPC мы будем воспроизводить оригинальную работу, к сожалению подобное достаточно сложно написать, поэтому мы скопипастим немного кода, используя обратную инженерию (заметьте, это стиль ленивых хакеров).

В нашем DPC мы должны вызвать KeyboardClassServiceCallback [28] , эта процедура осуществляется Kbdclass драйвером. Этот вызов передаёт ввод данных в буфер устройства. Драйвер клавиатуры должен вызвать класс, обслуживающий DPC. Вот прототип KeyboardClassServiceCallback:

VOID KeyboardClassServiceCallback (
    IN PDEVICE_OBJECT  DeviceObject,
    IN PKEYBOARD_INPUT_DATA  InputDataStart,
    IN PKEYBOARD_INPUT_DATA  InputDataEnd,
    IN OUT PULONG  InputDataConsumed
    );

Параметры :

  • DeviceObject : Указатель на класс устройства объекта.
  • InputDataStart : Указатель на первый пакет данных в буфере клавиатуры из порта устройства.
  • InputDataEnd : Указатель на последний пакет данных в буфере клавиатуры из порта устройства.
  • InputDataConsumed : Указатель на число переданных пакетов.

KEYBOARD_INPUT_DATA определяется так:

typedef struct _KEYBOARD_INPUT_DATA {
  USHORT  UnitId;
  USHORT  MakeCode;
  USHORT  Flags;
  USHORT  Reserved;
  ULONG  ExtraInformation;
} KEYBOARD_INPUT_DATA, *PKEYBOARD_INPUT_DATA;

И так, в нешем DPC мы только должны проверить MakeCode из структуры KEYBOARD_INPUT_DATA. MakeCode (или сканкод) представляет собой данные, посланные клавиатурой системе, когда вы нажимаете или отпускаете клавишу, система получает код и преобразует его в соответствии с кодовой таблицей. Например, сканкод 19d для классической US клавиатуры переводится в символ ‘e’.

Для того чтобы узнать состояние CAPSLOCK, мы посылаем IOCTL запрос клавиатуре, но он посылается только на уровне IOCTL PASSIVE_LEVEL. Для этого мы используем системные потоки, посылая IOCTL запросы к ядерным API IoBuildDeviceIoControlRequest. Фактически, очередь сканкодов является списком, закрытым spinlock’ом и нитью, синхронизированными semaphore. Поток “слушает” нажатия клавиш и преобразует сканкоды в коды знаков. Что и делает кейлогер на уровне ядра Klog [29].

-----[ 2.3.4 - Приложение 2 : NDIS-сниффер входящих пакетов

Здесь точно так же, прерывание “поднято”, когда сетевая карточка получает пакет. Когда тип этого прерывания “поднят”, NDIS ISR handler, подпрограмма обрабочика (ndisMIsr) запускает miniport ISR.Подпрограмма NdisMIsr используется в качестве обертки miniport ISR и DPC. Вы можете увидеть в IDT следующую запись:

3b: 80df6414 NDIS!ndisMIsr (KINTERRUPT 80df63d8) 

Это означает, что к вашему ISR не обращаются напрямую, когда происходит прерывание, выполняется ndisMIsr . MndisMIsr вызывает ISR минипорта, и DPC минипорта также становится в очередь этой подпрограммой. В завершении, NDIS является обёрткой вокруг ndisMIsr и ndisMDpc, в Windows XP используется NDIS 5.1. Мы не знаем, осуществляется ли всё описанное в Windows Vista с NDIS 6.0.

Мы знаем, что можем блокировать ndisMDpc собственным обработчиком. С NDIS мы будем действовать таким же образом, но мы не будем перехватывать MiniportDpc запросы, а напрямаю перехватим подпрограмму ndisMDpc. Почему? Потому что мы знаем, что ndisMDpc обёртка MiniportDpc и в сущности MiniportDpc слишком сильно зависит от устройства минипорта. Каждое устройство представлено в структуре NDIS_MINIPORT_BLOCK [30], в ней мы находим ссылку на NDIS_MINIPORT_INTERRUP, вот её описание:

kd> dt ndis!_NDIS_MINIPORT_INTERRUPT
   +0x000 InterruptObject  : Ptr32 _KINTERRUPT
   +0x004 DpcCountLock     : Uint4B
   +0x008 Reserved         : Ptr32 Void
   +0x00c MiniportIsr      : Ptr32 Void 
   +0x010 MiniportDpc      : Ptr32 Void 
   +0x014 InterruptDpc     : _KDPC
   +0x034 Miniport         : Ptr32 _NDIS_MINIPORT_BLOCK
   +0x038 DpcCount         : UChar
   +0x039 Filler1          : UChar
   +0x03c DpcsCompletedEvent : _KEVENT
   +0x04c SharedInterrupt  : UChar
   +0x04d IsrRequested     : UChar

Если посмотреть на ndisMDpc, то мы заметим, что только первый параметр используется и этот параметр ссылается на структуру NDIS_MINIPORT_INTERRUPT. NdisMDpc обращается к MiniportDpc этой структуры. Нам нужно только подменить этот указатель нашей подпрограммой для того, чтобы управлять входящими пакетами на системе.

В документации к NDIS видно, что miniport DPC должна уведомить драйвер протокола, что массив полученных пакетов доступен для вызова функцией NdisMIndicateReceivePacket [31].

VOID NdisMIndicateReceivePacket(
    IN NDIS_HANDLE  MiniportAdapterHandle,
    IN PPNDIS_PACKET  ReceivePackets,
    IN UINT  NumberOfPackets
    ); 

In the ndis.h header we have :
#define NdisMIndicateReceivePacket(_H, _P, _N)              \
{                                                           \
    (*((PNDIS_MINIPORT_BLOCK)(_H))->PacketIndicateHandler)( \
                        _H,                                 \
                        _P,                                 \
                        _N);                                \
}

И так, в нашеи MiniportDpc мы будем перехватывать PacketIndicateHandler, которая зачастую является ethFilterDprIndicateReceivePacket в обычной NDIS_MINIPORT_BLOCK структуре, для фильтрации входящих пакетов на miniport. После перехвата мы вызываем оригинальный MiniportDpc для обработки. После чего востанавливаем указатель PacketIndicateHandler в NDIS_MINIPORT_BLOCK для реализации скрытности. Подводим итог, мы должны:

  • Перехватить подпрограмму в DPC, поставленную в очередь подпрограммой ndisMIsr.
  • Изменить PacketIndicateHandler в NDIS_MINIPORT_BLOCK из miniport.
  • Вызвать оригинальный ndisMDpc.
  • В MiniportDpc вызываем NdisMIndicateReceivePacket. И делаем свою работу.
  • Когда ndisMDpc выполнится, мы востанавливаем оригинал PacketIndicateHandler в NDIS_MINIPORT_BLOCK из miniport.

С помощью этого фильтра мы можем контролировать или изменять входящие пакеты. Например, ваш PacketIndicateHandler может перехватывать и искать во входящих пакетах определённую метку, после её нахождения выполнять определённую функцию.

—[ 2.2 - Заключение о скрытном перехвате на IDT

В этой части мы увидели как Windows управляет аппаратными прерываниями, используя глобальную функцию шаблона, назначенную для всех прерываний. По сути, подпрограмма шаблона создающаяся для каждого прерывания, и есть главная цеьл атаки - мы можем подделать подпрограмму шаблона, которую будет невозможно обнаружить напрямую. Скрытность нашей атаки зависит от двух факторов:

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

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

–[ 3 - Захват NonPaged pool с помощью скрытного перехвата

Изощрённость Rootkit’а зависит от того, насколько сильно он изменяет ядро. Более сложные методы будут появляться, т. к. ядро и железо постоянно эволюционируют. В настоящее время существует много путей захвата ядра, соответсвенно повышается обороноспособность защиты. Мы собираемся представить разные средства для получения контроля. Следующий метод применяет этот подход к программе распределения памяти ядром.

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

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

—[ 3.1 - Обзор распределения памяти ядром

В каждой операционной системе, ядро Windows использует различные функции для передачи или высвобождения памяти. Вирутальная память состоит из блоков памяти, называемых страницами. В архитектуре Intel x86 размер страницы - 4096 байт и большинство запросов на распределение меньшего объёма. Поэтому, функции ядра, такие как ExAllocatePoolWithTag и ExFreePoolWithTag резервируют неиспользуемую память для последующего распределения. Внутренние функции напрямую взаимодействуют с железом каждый раз, когда страница нужна. Все эти процедуры достаточно сложны и деликатны, вот почему они реализованы в ядре.

-----[ 3.1.1 - Различия между Paged и NonPaged pool

Память ядра системы делится на два различных пула. Отдельно выделены наиболее часто используемые блоки. Система должна знать, какие страницы наиболее востребованны и от каких можно временно отказаться. “Недоступные” страницы восстанавливаются только тогда, когда IRQL уровень ниже DPC или DISPATCH. Пул страниц может быть сохранен внутри или вне системы. Блок памяти, сохраненный вне системы, будет сохранен в файловой системе, таким образом неиспользуемая память не будет резидентной. NonPaged pool существует для каждого уровня IRQL и испольуется для важных задач.

Файл pagefile.sys содержит paged-память. Он становился жертвой атаки, в ходе которой неподписанный код внедрялся в ядро Vista [32]. Среди обсуждаемых решений было и предложение отключить paged-память. Джоанна Рутковска защищала такое решение, как более безопасное по сравнению с другими, хотя и влекующее за собой небольшую потерю физической памяти. Microsoft отказывается от прямого доступа к диску, что подтверждает значимость таких возможностей ядра Windows как Paged и NonPaged пулы [33].

Эта статья написана с упором на NonPaged pool, так как обработка PagedPool происходит совершенно иначе. NonPaged pool можно рассматривать как более имли менее типичную реализацию heap. Подробная информация о системных пулах доступна в Microsoft Windows Internals [34].

-----[ 3.1.2 - Таблица NonPaged pool

Алгоритм распределения должен быстро распределть наиболее часто используемые объёмы. Поэтому существуют три разные таблицы, каждая из которых выделена для определенного диапазона объёмов. Такую структуру мы обнаружиили в большинстве алгоритмов управления памятью. Считывание блоков памяти с устройств занимает некоторое время. Windows балансирует между скоростью ответа и оптимальным распределением памяти. Время ответа сокращается, если блоки памяти сохраняются для последующего распределения. С другой стороны, избыточное резервирование памяти может сказаться на производительности.

Каждая таблица представляет собой отдельный способ хранения блоков памяти. Мы рассмотрим каждую таблицу и её местоположение.

NonPaged lookaside - таблица, назначаемая каждому процессору, работающая с объёмами, равными или менее 256 байт. У каждого процессора есть контрольный реестр (PCR), хранящий служебные данные процессора - уровень IRQL, GDT, IDT. Расширение нреестра называется контрольным регионом (PCRB) и содержит lookaside-таблицы. Следующий дамп windbg представляет структуру такой таблицы:

kd> !pcr
KPCR for Processor 0 at ffdff000:
    Major 1 Minor 1
  NtTib.ExceptionList: 805486b0
      NtTib.StackBase: 80548ef0
     NtTib.StackLimit: 80546100
   NtTib.SubSystemTib: 00000000
        NtTib.Version: 00000000
    NtTib.UserPointer: 00000000
        NtTib.SelfTib: 00000000

              SelfPcr: ffdff000
                 Prcb: ffdff120
                 Irql: 00000000
                  IRR: 00000000
                  IDR: ffffffff
        InterruptMode: 00000000
                  IDT: 8003f400
                  GDT: 8003f000
                  TSS: 80042000

        CurrentThread: 80551920
           NextThread: 00000000
           IdleThread: 80551920

            DpcQueue:  0x80551f80 0x804ff29c
kd> dt nt!_KPRCB ffdff120
[...]
   +0x5a0 PPNPagedLookasideList : [32]
      +0x000 P                : 0x819c6000 _GENERAL_LOOKASIDE
      +0x004 L                : 0x8054dd00 _GENERAL_LOOKASIDE
[...]
kd> dt nt!_GENERAL_LOOKASIDE
   +0x000 ListHead         : _SLIST_HEADER
   +0x008 Depth            : Uint2B
   +0x00a MaximumDepth     : Uint2B
   +0x00c TotalAllocates   : Uint4B
   +0x010 AllocateMisses   : Uint4B
   +0x010 AllocateHits     : Uint4B
   +0x014 TotalFrees       : Uint4B
   +0x018 FreeMisses       : Uint4B
   +0x018 FreeHits         : Uint4B
   +0x01c Type             : _POOL_TYPE
   +0x020 Tag              : Uint4B
   +0x024 Size             : Uint4B
   +0x028 Allocate         : Ptr32     void*
   +0x02c Free             : Ptr32     void
   +0x030 ListEntry        : _LIST_ENTRY
   +0x038 LastTotalAllocates : Uint4B
   +0x03c LastAllocateMisses : Uint4B
   +0x03c LastAllocateHits : Uint4B
   +0x040 Future           : [2] Uint4B

Lookaside-таблицы предоставляют более быстрое считывание блоков, по сравнению с типичным списком с двойными ссылками. Для такой оптимизации очень важно время задержки, и список с одиночными ссылками эффективнее, чем программная задержка. Фунция ExInterlockedPopEntrySList используется для выбора записи из списка, с ипользованием аппаратной инструкции “lock”.

PPNPagedLookasideList - Lookaside-таблица, о которой мы говорили выше. Она содержит два Lookaside-списка P и L. Поле “depth” структуры GENERAL_LOOKASIDE определяет, как много записей может находиться в списке ListHead. Система регулярно обновляет этот параметр, используя различные счетчики. Алгоритм обновления основан на номере процессора и не одинаков для P и L. В случае P поле “depth” обновляется чаще, чем у списка L, потому что оно оптимизирует производительность очень маленьких блоков.

Вторая таблица зависит от числа процессоров и того, как ими управляет система. Система арспрределения задействуется, если объём равен или менее 4080 байт, или если lookaside-поиск не дал результатов. Даже если целевая таблица можт измениться, у неёё будет та же структура POOL_DESCRIPTOR. В случае единственного процессора используется переменная PoolVector для считывания указателя NonPagedPoolDescriptor. В случае многих процессоров, таблица ExpNonPagedPoolDescriptor содержит 16 слотов с описаниями пулов. PCRB каждого процессора указывает на структуру KNODE. Узел может быть связан с более чем одним процессором и содержит поле “color”, используемое как список для ExpNonPagedPoolDescriptor. Следующие схемы иллюстрируют этот алгоритм:

          PoolVector
        +------------+
        |  NonPaged  | --------------> NonPagedPoolDescriptor
        |------------+
        |   Paged    |
        +------------+

         [ Схема 1 - описание пула при одном процессоре ]

         Процессор #1
        +------------+
        |            |                  ExpNonPagedPoolDescriptor
        |    PRCB  ------\                +-------------------+
        |            |   |          /---------> SLOT #01      |
        +------------+   |          |     |     SLOT #02      |
               /---------/          |     |     SLOT #03      |
               |         KNODE      |     |     SLOT #04      |
               |---> +------------+ |     |     SLOT #05      |
               |     | Proc mask  | |     |     SLOT #06      |
               |     | color (01) --/     |     SLOT #07      |
               |     | ...        |       |     SLOT #08      |
               |     +------------+       |     SLOT #09      |
               |                          |     SLOT #10      |
               \---------\                |     SLOT #11      |
         Процессор #2    |                |     SLOT #12      |
        +------------+   |                |     SLOT #13      |
        |            |   |                |     SLOT #14      |
        |    PRCB  ------/                |     SLOT #15      |
        |            |                    |     SLOT #16      |
        +------------+                    +-------------------+

        [ Схема 2 - описание пула при нескольких процессорах ]

Глобальная переменная ExpNumberOfNonPagedPools определяет, имеют ли место несколько процессоров.Она должна содержать количество процессоров, но меняется в разных версиях операционной системы.

Следующий дамп windbg отображает структуру POOL_DESCRIPTOR:

kd> dt nt!_POOL_DESCRIPTOR
   +0x000 PoolType         : _POOL_TYPE
   +0x004 PoolIndex        : Uint4B
   +0x008 RunningAllocs    : Uint4B
   +0x00c RunningDeAllocs  : Uint4B
   +0x010 TotalPages       : Uint4B
   +0x014 TotalBigPages    : Uint4B
   +0x018 Threshold        : Uint4B
   +0x01c LockAddress      : Ptr32 Void
   +0x020 PendingFrees     : Ptr32 Void
   +0x024 PendingFreeDepth : Int4B
   +0x028 ListHeads        : [512] _LIST_ENTRY

Чередуемая синхронизация spinlock’ов, часть библиотеки HAL, используется для предотвращения конфликтов в дескрипторе пула. Эта процедура позволяет только одному процессору и одной нити одновременный доступ к записи из дескриптора пула. Библиотека HAL различается на разных архитектурах, и то, что в случае единственного процессора является просто поднятием IRQL, становится сложной чередуемой системой в случае нескольких процессоров. Для дескриптора по умолчанию, главный NonPaged spinlock замкнут (LockQueueNonPagedPoolLock). В противном случае создается отдельный чередуемый spinlock.

Третья, и последняя, таблица разделяется процессорами для объёмов свыше 4080 байт. MmNonPagedPoolFreeListHead также используется при нехватки памяти в остальных таблицах. Она состоит из четырех LIST_ENTRY, каждая из которых соотносится с номером страницы, кроме последней, содержащей все последующие страницы, хранящиеся системой. Доступ к этой таблице охраняется главным чередуемым non paged spinlock’ом, также именуемым LockQueueNonPagedPoolLock. В ходе процедуры высвобождения меньшего по объему блока ExFreePoolWithTag объединяет его с предыдущим и следующим свободными блоками. Так может быть создан блок размером в страницу и более. В этом случае блок добавляется в таблицу MmNonPagedPoolFreeListHead.

-----[ 3.1.3 - Алгоритмы распределения и высвобождения

Распределение памяти ядром не очень меняется в разных версиях ОС, но этот алгоритм не менее сложен, чем heap пользовательских процессов. В жтой части статьи мы хотим проиллюстрировать основы поведения таблиц в ходе процедур распределения и высвобождения. Многие детали, такие как механизмы синхронизации, опущены. Эти алгоритмы помогут в объяснении метода и понимании основ распределения в ядре. Несмотря на то, что эксплуатация ядра не рассматривается в этом материале, обзор пулов - интересная тема, которая требует понимамия части алгоритма.

Алгоритм распределения в NonPaged pool (ExAllocatePoolWithTag):

    IF [ Объем > 4080 байт ]
      [
        - Вызвать функцию MiAllocatePoolPages
          - Обратиться к таблице MmNonPagedPoolFreeListHead LIST_ENTRY
          - Получить память у устройств (если необходимо) 
          - Вернуть объединенную страницу памяти (без заголовка).
      ]

    IF [ Объем <= 256 байт ]
      [
        - Выбрать запись из таблицы PPNPagedLookasideList .
        - Если что-либо найдено, вернуть блок памяти.
      ]

    IF [ ExpNumberOfNonPagedPools > 1 ]
      - PoolDescriptor из ExpNumberOfNonPagedPools и использованный список
        считываются из значения "color" в PRCB KNODE .
    ELSE
      - PoolDescriptor - первая запись в PoolVector, созданная символом как
        NonPagedPoolDescriptor.

    FOREACH [ >= Размер записи в PoolDescriptor.ListHeads ]
      [
        IF [ Запись не пуста ]
          [
            - Отсоединить запись и разделить её, если необходимо 
            - Вернуть блок памяти.
          ]
      ]

    - Вызвать функцию MiAllocatePoolPages
      - Обратиться к таблице MmNonPagedPoolFreeListHead LIST_ENTRY..
      - Правильно разбить её по объёмам 
      - Вернуть новый блок памяти.

Алгоритм высвобождения NonPaged pool (ExFreePoolWithTag):

    IF [ MemoryBlock связан со страницей ]
      [
        - Вызвать функцию MiFreePoolPages
          - Определить тип блока (Paged или NonPaged)
          - В зависимости от количества хранимых в MmNonPagedPoolFreeListHead блоков,
            предоставить их железу.
      ]
    ELSE
      [
        - Соединить с предыдущим и следующим блоками, если возможно 

        IF [ объём NewMemoryBlock <= 256 bytes ]
          [
            - Обратиться к значению "depth" в PPNPagedLookasideList и определить, хранить ли блок
            - Вернуть, если блок включен в список lookaside            
          ]

        IF [ объём NewMemoryBlock <= 4080 bytes ]
          [
            - Использовать переменную PoolIndex POOL_HEADER чтобы определить,
              который PoolDescriptor должен быть использован.
            - Вставить его в верный диапазон записей LIST_ENTRY
            - Если всё получилось, вернуться
          ]

        - В зависимости от количества хранимых в MmNonPagedPoolFreeListHead блоков,
          предоставить их железу.
      ]

Алгоритм Paged pool весьма отличается, особенно в случае связанныхз со страницей блоков. Управление меньшими объёмами не должно быть сильно далеко от NonPaged pool, но в ассемблерном коде мы видим, что NonPaged и Paged пулы сильно различаются. Когда вы узнаете больше о распределении в NonPaged пуле, мы сможем поговорить об эксплуатации.

—[ 3.2 - Исполнение кода путем внедрения в код распределения

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

Мы начнем с того, что код будет выполняться при распределении объёмов, равных странице или более. Как было сказано ранее, это затрагивает только третью и последнюю таблицу.

-----[ 3.2.1 - Нарушение целостности данных в MmNonPagedPoolFreeListHead

MmNonPagedPoolFreeListHead сохраняет связанные со страницей блоки памяти, чтобы ускорить распределение памяти. Он связывает блоки памяти с помощью структуры LIST_ENTRY. Эта структура общеупотребительна, и используется, к примеру, в библиотеке Windows heap.

kd> dt nt!_LIST_ENTRY
   +0x000 Flink            : Ptr32 _LIST_ENTRY
   +0x004 Blink            : Ptr32 _LIST_ENTRY

Доступ к MmNonPagedPoolFreeListHead охраняется главным чередуемым NonPaged spinlock’ом LockQueueNonPagedPoolLock, который обеспечивает доступ и модификацию структуры только для одного процессора и одной нити одновременно.

Итак, нам нужен отличный способ получения контроля над процедурами распределения и отсоединения. Мы можем “отравить” связанный список поддельной записью, максимально возможного объема, отсоединение которой вызовет изменения текущего исполняемого кода. На уровне ядра вы можете модифицировать код как данные без каких-либо проблем с защитой. Отсоединение использовалось для эксплуатации heap [20], но изменение кода было невозможно из пользовательских процессов. Spinlock обеспечивает эксклюзивность, так что риск изменения условий можно не принимать в расчет. Созданный hook будет динамическим и код будет напрямую восстановлен. Реверсинг защиты страниц [18] показывает, что код проверяется каждые 5 минут. Когда модификация обнаруживается, “настоящий” код просто заменяется.

У этого метода есть много преимуществ, но есть и недостатки. Перечислим препятствия:

  • При основной реализации отсоединения список становится недоступен. Это делает невозможным большинство способов работы с таблицей.
  • Нужно задействовать методы очистки страницы и блок должен быть первым в списке, иначе мы можем пропустить какой-либо вызов.
  • Мы нарушаем путь когда, и рано или поздно должны вернуть всё на место, как если бы ничего не было
  • Предварительная обработка процессором делает опасным само-модифицирующийся код

Отсоединение предоставляет нам перезапись 4 байт для создания опкода и перенаправления. В нашем случае, мы влияем на текущий контекст и регистр должен указывать на отсоединенную запись. Мы сказали - “должен указывать”, без определения конкретного регистра, который меняется в разных версиях ОС и сервис-паков. Так как мы обсуждаем контекст, то будем придерживаться наиболее распространенных ситуаций. Сделаем jmp [reg+XX], что в 16-ричном виде выглядит как FF60XX.

Эффективность этого методжа зависит от доступности MmNonPagedPoolFreeListHead. Список с двойными связями, как LIST_ENTRY, доступен при верном Flink. Исходя из этого, мы выберем адрес для Flink - 0xXXXX60FF и Blink будет указывать на адрес кода. На архитектуре Intel x86 little endian наш адрес найти легко, мы просто проверим оффсет опкода и отменим слишком близкие возможности. Следующая схема показывает “отравленную” запись:

MmNonPagedPoolFreeListHead[i]
   /------> +--------------------+
   |        |       Flink        | ---\
   |        |--------------------|    |
   |  <---- |       Blink        |    |
   |        +--------------------+    |
   |        |        ...         |    |
   |        +--------------------+    |
   |  /-------------------------------/
   |  |
   |  |       Отравленная запись
   |  |     +--------------------+
   |  |     | PreviousSize : -   |
   |  |     +--------------------+
   |  |     |   PoolIndex : -    |
   |  |     +--------------------+
   |  |     | PoolType: NonPaged |
   |  |     +--------------------+
   |  |     |   BlockSize : i    |
   |  |     +--------------------+
   |  |     |    PoolTag : -     |
   |  \---> +--------------------+
   |        | Flink : 0xYYXX60FF | <--\
   |        |--------------------|    |
   |   X--- | Blink : 0x80YYYYYY |    |
   |        +--------------------+    |
   |                                  |
   |  /-------------------------------/
   |  |    Ложная запись (0xYYXX60FF)
   |  |     +--------------------+
   |  |     | PreviousSize : -   |
   |  |     +--------------------+
   |  |     |   PoolIndex : -    |
   |  |     +--------------------+
   |  |     | PoolType: NonPaged |
   |  |     +--------------------+
   |  |     |   BlockSize : < i  |
   |  |     +--------------------+
   |  |     |    PoolTag : -     |
   |  |---> +--------------------+
   |  |     | Flink : 0x80.....  | ---\
   |  |     |--------------------|    |
   |  \---- | Blink : Poisoned   |    |
   |        +--------------------+    |
   \--------------- [...] ------------/

   
    Инструкция отсоединения       : mov [0x80YYYYYY], 0xYYXX60FF
    Новый опкод после отсоединения: jmp [reg+XX] (FF 60 XX)

   [ Схема 3 - Отравленный список с двойными связями ]

Эта схема показывает запись в MmNonPagedPoolFreeListHead, подтверждающую предсказанное отсоединение и выполнение кода. Мы должны придерживаться этой схемы, чтобы не утратить свою позицию. Блоки NonPaged приходят из двух разных вирутальных диапазонов памяти. Начало второго региона памяти хранится в MmNonPagedPoolExpansionStart. Функция очистки иногда вызывается для высвобождения блоков из расширения NonPaged pool. Чтобы избежать этой очистки, мы можем использовать замкнутый блок из Paged pool. Замкнуть блок памяти можно с помощью функции MmProbeAndLockPages, что сделает указанный регион памяти резидентным. Другой способ - переразметка NonPaged блока функцией MmMapLockedPagesSpecifyCache. Это более дискретно, так как разметка находится перед диапазоном памяти расширения NonPaged pool. Использование замкнутого Paged блока создает адрес совершенно иначе. Различия между адресами видны невооруженным взглядом. Так как виртуальная память очень велика, поиск адреса вроде 0xYYXX60FF не займет слишком много времени. Мы не будем высвобождать используемые страницы во время выполнения атаки.

Чтобы обойти проблемы с путём кода, мы выделим два различных состояния. В первом случае наш блок выбран. Во втором - он отсоединен. Если мы сможем возвращаться к первому шагу с выбранной следующей поддельной записью, то доступ к коду будет осуществляться как обычно. Достигается это следующим образом: при IRQL равном DISPATCH_LEVEL мы изменим запись в MmNonPagedPoolFreeListHead, добавив неверные указатели. С hook’ом на обработчике ошибок страницы мы сможем отследить первое и второе состояния, восстановить верный контекст в каждом случае и сохранить разницу контекста между состояниями.

Ассемблерный дамп MiAllocatePoolPages:

  lea     eax, [esi+8] ; Состояние #1 esi - выбранный блок, esi+8 - его объем
  cmp     [eax], ebx   ; Проверка требуемого объема
  mov     ecx, esi
  jnb     loc_47014B
  [...]
  
loc_47014B:
  sub     [esi+8], ebx
  mov     eax, [esi+8]
  shl     eax, 0Ch
  add     eax, esi
  cmp     _MmProtectFreedNonPagedPool, 0 ; Защищенный режим
  mov     [ebp+arg_4], eax
  jnz     short loc_47016E
  mov     eax, [esi]      ; \ Состояние #2
  mov     ecx, [esi+4]    ; | Процедура
  mov     [ecx], eax      ; | отсоединения
  mov     [eax+4], ecx    ; /
  jmp     short loc_470174

Теперь взглянем, как это работает - протестируем с хуком на обработчике ошибок прерываний (int 0xE):

  lea     eax, [esi+8]
                       ; Состояние #1 - Проверка требуемого объема
  cmp     [eax], ebx   ; ----> СБОЙ СТРАНИЦЫ esi = 0xAAAAAAAA | eax = esi + 8
                       ;     - Держим EIP и все регистры
                       ;     - Сканируем все регистры на 0xAAAAAAAA +/- 8
                       ;       и правим под текущий контекст. Продолжаем.
  mov     ecx, esi
  jnb     loc_47014B
  [...]
  
loc_47014B:
  sub     [esi+8], ebx
  mov     eax, [esi+8]
  shl     eax, 0Ch
  add     eax, esi
  cmp     _MmProtectFreedNonPagedPool, 0 ; Защищенный режим
  mov     [ebp+arg_4], eax
  jnz     short loc_47016E
  mov     eax, [esi]      ; \ Stage #2 - Unlinking procedure
  mov     ecx, [esi+4]    ; |
  mov     [ecx], eax      ; | ------> СБОЙ СТРАНИЦЫ ecx = 0xBBBBBBBB
                          ; |                       eax = 0xCCCCCCCC
                          ; |     - Держим EIP и sub этого контекста от
                          ; |       сохраненного контекста состояния #1
                          ; |     - Изменяем регистры сбоя и
                          ; |       указатели структуры. Продолжаем.
  mov     [eax+4], ecx    ; /
  jmp     short loc_470174

Адреса сбоя 0xAAAAAAA, 0xBBBBBBBB и 0xCCCCCCCC должны указывать на неверные адреса, чтобы вызвать сбой страницы. Этот тест проводится один раз, когда мы располагаем эксклюзивностью на всех процессорах. Обработчик int 0xE (сбой страницы) восстанавливается сразу после теста.

Этот метод позволяет нам восстановить верный контекст сразу перед проверкой объёма выбранного блока. После выполнения кода мы применяем разницу в контексте, изменяем регистр текущего блока и возвращаемся к адресу первого состояния. Всё это хорошо работает, так как оба наших состояния расположены близко; как только выбранный блок проверен, отсоединение происходит напрямую.

Приведенные примеры основаны на одной записи LIST_ENTRY в таблице MmNonPagedPoolFreeListHead, но отравить нужно все записи. Если отдельная запись пуста, (за исключением наших поддельных блоков), алгоритм обращается к следующей. Это означает, что на наши блоки придётся более одного обращения за процедуру распределения. Мы создали механизм управления множественными обращениями при одной процедуре распределения. Если первая запись пуста, используется вторая, и так далее. Заием последует два обращения, или более. С помощью проверки текущей таблицы, мы можем предсказать выполнение кода в этой же процедуре распределения и избежать лишних выполнений кода более чем один раз в ответ на запрос на распределение.

Prefetch - возможность процессора, которая считывает более чем одну инструкцию из памяти перед выполнением. Некоторые процессоры используют сложные алгоритмы предсказания с ветвлением для обработки максимально возможного количества инструкций. После нескольких тестов мы заметили, что процессоры проверяют кэш кода, когда модификация происходит в кэшированном диапазоне адресов памяти. В нашшем драйвере учтена ситуация, когда модификация кода происходит сразу после текущей инструкции. Чтобы добиться этого, мы создали подпрограмму, которая обсчитывает объем prefetch-кэша и учитывает его в дальнейших действиях. Также мы можем обнаружить специфические инструкции, очищающие prefetch-кэш, но это будет использоваться лишь опционально.

Этот метод позволяет выполнять код в контексте NonPaged распределения, равного по объёму 1 странице или более. Осуществить это помогает скрытный перехват, выполняемый кодом на уровне ядра м сразу после этого удаляемый нашей подрограммой. Конечно, метод далек от совершенства, так как подобные объемы распределяются не слишком часто. Следующая часть статьи описывает расширение возможностей метода для получения контроля над всеми NonPaged распределениями.

-----[ 3.2.2 - Расширяем для всех объёмов

Другие списки не могут перехвачены таким же образом, так как механизмы синхронизации не исключительны. Изменение некоторого ассемблерного кода может стать сложной задачей, если он будет выполняться одновременно более чем одной нитью. Наш подход заключается в проверке выполнения вышеописанной техники на всех процедурах распределения. Контролируя эти процедуры, мы сможем восстановить контекст ExAllocatePoolWithTag верным значением возврата. Мы должны добиться этого, не меняя ни строчки в обработчике распределений. Существует и возможность создания собственного обработчика, но отлично справится обработчик Windows.

В ходе распределения в первую очередь проверяется список lookaside. При этом выбирается запись, и при значении, отличном от NULL, используется. Эта запись берется из поля ListHeader в GENERAL_LOOKASIDE. Структура поля - SLIST_HEADER.

kd> dt nt!_SLIST_HEADER .
   +0x000 Alignment        : Uint8B
   +0x000 Next             :
      +0x000 Next             : Ptr32 _SINGLE_LIST_ENTRY
   +0x004 Depth            : Uint2B
   +0x006 Sequence         : Uint2B

Функция ExInterlockedPopEntrySList выбирает запись из структуры SLIST_HEADER. Поле Next указывает на следующий узел SLIST (список с одинарными связями). ExFreePoolWithTag сравнивает оптимальную глубину GENERAL_LOOKASIDE с текущей глубиной SLIST_HEADER. ExAllocatePoolWithTag не проверяет это поле, а просто удостоверивается в том, что Next указывает на какую-либо запись. Для вмешательства в процедуры распределения и высвобождения в таблице NonPaged lookaside, мы устанавливаем значение Next = NULL и Depth = 0xFFFF. Это состояние будет сохранено, а таблица более использоваться не будет.

Расширение нашего метода полностью зависит от получения контроля над способом использования таблицы ExpNonPagedPoolDescriptor. В предыдущей части статьи мы рассмотрели участие переменной ExpNumberOfNonPagedPools в этом процессе. Существует возможность увелечить количество NonPaged пулов и затем поиграть со значением color в KNODE. Это значение определяет, который дескриптор пула будет использован во время распределения. В ходе высвобождения оно сохраняется в поле PoolIndex POOL_HEADER.

Эта замечательная возможность даёт нам преимущество. По умолчанию значение color в KNODE на каждом процессоре указывает на пустые дескрипторы пулов. Используя наш основной метод, мы добьёмся выполнения кода. Если адрес возврата функции MiAllocatePoolPages не входит в число используемых про классическом постраничном распределении, это означает, что выполняется распределение меньшего объёма. Всё, что нам нужно сделать - переключить указатель PRCB KNODE на копию с заданным значением color и заново вызвать ExAllocatePoolWithTag. Всё, что касается распределения и управления блокамы, должно быть реализовано верно, несмотря на отличия между версиями операционной системы. PoolIndex возвращенных блоков будет указывать на наш дескриптор пула и процедуру высвбождения, что сработает превосходно. Взглянем, как это работает при единственном процессоре:

ExpNonPagedPoolDescriptor
        +-------------------+  
        |   ПРЕД. POOLDESC  | <--- Осталось для совместимости (0)
        |  ПУСТОЙ POOLDESC  | <--- По умолчанию KNODE->color (1)
        |        --         |  
        |        --         |  
        |        --         |  
        |        --         |  
        |        --         |  
        |        --         |  
        |        --         |  
        |        --         |  
        |        --         |  
        |        --         |  
        |        --         |  
        |        --         |  
        |        --         |  
        |   НАШ  POOLDESC   | <--- Используется для наших распределений (16)
        +-------------------+  

      [ Схема 4 - Подмена ExpNonPagedPoolDescriptor ]
      [            на единственном процессоре       ]

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

Когда у нас есть свеже-распределенный блок, мы вернемся к адресу возврата ExAllocatePoolWithTag. MiAllocatePoolPages вызывается для считывания новой страницы и внесения её в новый дескриптор. Естественно, что мы не можем выполнить нормальный возврат и допустить распределение этой страницы. На архитектуре Intel x86 стэк используется для хранения локальных переменных, аргументов и регистров. Компилятор Windows запускается резервированием локальной переменной и затем обрабатывает каждый регистр до его изменения. Следующая схема демонстрирует конфигурацию стэка при выполнении нашего кода:

    верх
      +-----------------------+
      |  Наши элементы стэка  |      Пример восстановления ассемблера
      +-----------------------+ <------ /---------------\
      |                       |         | pop ecx       |
      | Сохраненные регистры  |         | pop ebx       |
      |                       |         | pop esi       |
      +-----------------------+         | leave         |
      |                       |         | retn 0Ch      |
      |                       |         \---------------/
      |                       |                |
      |                       |                |
      |    Переменные стэка   |                |
      |                       |                |
      |                       |                |
      |                       |                |
      +-----------------------+       [новый уровень стэка]
      |  Сохраненные EBP      |                |
      +-----------------------+                |
      |     Адрес возврата    |                |
      +-----------------------+                |
      |                       |                |
      |   Аргументы функций   |                |
      |                       |                |
      +-----------------------+ <--------------/
    низ

      [ Схема 5 - Контекст стэка после выполнения кода ]
      [            ~ случай маленьких блоков -         ]

Часть с восстановлением ассемблера показывает верный код в текущей функции, который отлично восстанавливает контекст. Он не связан с первой серией инструкций до возврата. Существует немалый риск того, что какой-либо из регистров ещё не было обработан. Возможно получить номера обработанных регистров из пролога функции, где хранятся переменные стэка. В компиляторе Windows это несложно, и мы можем вычислить номер регистра. Простой дизассемблерный анализ нужного номера регистра вполне подходит для этой цели. Он должен быть проведен для MiAllocatePoolPages и ExAllocatePoolWithTag. Мы изменяем адрес возврата, хранящийся в стэке, и переходим к нужному адресу MiAllocatePoolPages. Последним шагом мы устанавливаем регистр eax на значение возврата. Обе функции вернут значение и сохранят значение eax. Наш анализатор работает динамически, и регистрирует каждый pop и его регистр, именно поэтому мы и можем восстановить верный контекст, несмотря на различия между версиями.

Компилятор Windows легко предсказуем и не создает особо странных ассемблерных структур. Наш метод теоретически возможен к использованию на любом ассемблерном коде, следующем спецификации stdcall. Подход может меняться в случае других компиляторов.

—[ 3.3 Используем нашу позицию

Эта статья представляют путь к получению контроля над ядром Windows, путем изменения только данных. Нет указателей на функцию, нет статического перехвата или других классических техник. Это освобождает нас от более пространных объяснений. Но без некоторых конкретных примеров тема не будет раскрыта полно. Я лично полагаю, что единственным ограничением здесь может быть только воображение.

-----[ 3.3.1 Перенаправление стэка

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

Первый способ - вызов обработчика, если обратная трассировка стэка выявляет опредеделенную функцию. Обратная трассировка стэка показывает только адреса возврата, но не функции. Дебаггеры выявляют эти функции путем глубокого анализа проверки символов. Реализация этих функций отнимет слишком много времени, так что проще выбрать специфический адрес возврата во фрэйме стэка ExAllocatePoolWithTag. Таким образом мы существенно повысим скорость проверки. Дл яэтого мы должны указать нашему API перенаправления стэка, что нас интересует конкретная функция. Затем осуществить обычный вызов или процедуру, которые приведут нас к функции. Каждое распределение в течение этого времени даст важные результаты обратной трассировки стэка.

Допустим, нам нужен IRP и мы определили, какая функция обратывает его, посмотрев на таблицу направлений IRP. Путём реверсинга мы узнаем, что он распределяет NonPaged блок. При выполнении I/O запроса наш API зарегистрирует вызов в области NonPaged и позже распознает.

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

-----[ 3.3.2 Внедрение кода в пользовательские процессы

Распределение в NonPaged преимущественно осуществеляется в режиме ядра, и это относится ко всем процессам. Некоторые драйверы ядра, например win32k.sys, обращаются к пользовательским процессам многократно. Эти обращения выполняются функцией KeUserModeCallback [35], которая модифицирует пользовательский стэк, чтобы временно переключиться на обращение к пользовательским процессам. Доступные функции ограничены таблицей.

Внедрение в пользовательские процессы из ядра не должно быть резидентным и касается только доверенных приложений, таких как браузеры. Можно внедриться в explorer.exe, или создать скрытую копию доверенной программы. Алгоритм KeUserModeCallback может быть легко воспроизведен или скопирован, затем размещен иначе. Можно контролировать таблицу перенаправлений, чтобы перенаправить обращение. Также можно подумать об экплуатации обращений пользовательских процессов. Нет никакого смысла добавлять проверки к доступным функциям.

–[ 4 - Обнаружение.

Данная статья не пытается убедить Вас в том, что перехват IDT или механизмов распределения является передовой техникой будущего. Многие утилиты могут только идентфицировать наличие или отсутствие rootkit’a. Они не могут определить, какой модуль отвечает за это. Они обнаруживают антивирус или файрволл как rootkit. Защита также может указать, что сама является rootkit’ом, т. к. сама реализует многие технологии rootkit’ов, тем не менее она не просит удалить себя. Технологии Rootkit демонстрируют множество лёгких путей обхода защит. Но мы не видим этих технологий в реальности потому, что большинство в них не нуждается.

Определение изменения поведения программ может быть частью защиты OS [36]. Она должна проверять базовые структуры на изменение, контролировать целостность структуры LIST_ENTRY и восстанавливать её при необходимости. Мы можем винить защиты от rootkit’ов, но определение rootkits в закрытой OS практически невозможно. Более подробная информация об ядре и его модулюх даст больше информации для нападения. С другой стороны, это уменьшит площадь, уязвимую для атаки. Следующие улучшения защиты должны исходить от операционной системы.

Теперь, когда есть аппаратные усовершенствования для виртуализации, такие как hypervisors, появится расширение аппаратных средств для обнаружения и защиты против rootkits. Оно даст реальный контроль над поведением операционной системы без ппродвинутого исследования ядра. Некоторые методы защиты невозможно осуществить в среде Windows, люди могут полагаться только на аппаратные возможности. Наши методы могут быть обнаружены путем регистрации и мониторинга некоторых конкретных событий на процессоре. Вполне возможно сегодня это сделать, но вопросы производительности имеют более важное значение.

–[ 5 - Заключение

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

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

–[ 6 - Ссылки

[1] Holy Father, Invisibility on NT boxes, How to become unseen on Windows NT (Version: 1.2)
[2] Holy Father, Hacker Defender
[3] 29A
[4] Greg Hoglund, NT Rootkit
[5] fuzen_op, FU
[6] Peter Silberman, C.H.A.O.S, FUto
[7] Eeye, Bootroot
[8] Eeye, Pixie
[9] Joanna Rutkowska and Alexander Tereshkin, Blue Pill project
[10] Frank Boldewin, A Journey to the Center of the Rustock.B Rootkit
[11] Frank Boldewin, Peacomm.C - Cracking the nutshell
[12] Stealth MBR rootkit
[13] EP_X0FF and MP_ART, Unreal.A, bypassing modern Antirootkits
[14] AK922 : Bypassing Disk Low Level Scanning to Hide File
[15] CardMagic and wowocock, DarkSpy
[16] pjf, IceSword
[17] Gmer
[18] Pageguard papers (Uniformed):

[19] Greg Hoglund, Kernel Object Hooking Rootkits (KOH Rootkits)
[20] Windows Heap Overflows - David Litchfield
[21] Bypassing Klister 0.4 With No Hooks or Running a Controlled Thread Scheduler by 90210 - 29A
[22] Microsoft, Debugging Tools for Windows
[23] Kad, Phrack 59, Handling Interrupt Descriptor Table for fun and profit
[24] Wikipedia, Southbridge
[25] Wikipedia, Northbridge
[26] The NT Insider, Stop Interrupting Me – Of PICs and APICs
[27] Russinovich, Solomon, Microsoft Windows Internals, Fourth Edition (Chapter 3. System Mechanisms -> Trap Dispatching)
[28] MSDN, KeyboardClassServiceCallback
[29] Clandestiny, Klog
[30] Alexander Tereshkin, Rootkits: Attacking Personal Firewalls
[31] MSDN, NdisMIndicateReceivePacket
[32] Subverting VistaTM Kernel For Fun And Profit by Joanna Rutkowska
[33] Vista RC2 vs. pagefile attack by Joanna Rutkowska
[34] Russinovich, Solomon, Microsoft Windows Internals, Fourth Edition (Chapter 7. Memory Management -> System Memory Pools)
[35] KeUserModCallback ref - “Ring0 under WinNT/2k/XP” by Ratter - 29A
[36] Joanna Rutkowska - Towards Verifiable Operating Systems

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

Интернет не постоянная штука, так что нечему удивляться. На момент публикации статьи все ссылки были живы.

Суть материала, в общем, ясна. Просто хотел посетить пару ресурсов, а они не открылись. Ладно, попробую пережить это.

Журнал 2008 года .