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

Так какие же еще способы можно предложить, кроме перехвата функций? Пожалуй, этот вопрос поставит в тупик две трети программистов. Более-менее подкованные кодеры, почесав затылок, смогут вспомнить о таком неочевидном способе, как скан PDE/ PTE-таблиц интересующего нас процесса или, в случае проще, о простом сканировании адресного пространства процесса. На этом способе, кстати, основаны многие программы типа ArtMoney и схожие с ней.
А еще способы есть? Да такие, чтобы не трогать напрямую адресное пространство процесса? Наверное, есть. Будем искать :).

 

Организация памяти в Windows

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

 

Захватываем память в Windows

Если ты хоть раз сталкивался с написанием драйверов под Windows, тебе должно быть знакомо разделение памяти на под качиваемую (paged) и неподкачиваемую (nonpaged).

Разница между ними вполне ясна — если ядро выделяет память в подкачиваемой памяти, то со временем, чтобы не захламлять оперативную память компьютера, она просто сбрасывается на жесткий диск и существует там в виде файла pagefile.sys. В нем, как правило, хранятся paged-секции драйверов и куча прочей шняги.
Nonpaged-память на диск никогда не сбрасывается и постоянно держится в оперативной памяти. Поэтому при ее выделении нужно быть крайне экономным и не использовать ее без особой нужды. К примеру, в nonpaged пуле хранятся ключевые секции драйверов и самого ядра. Не буду долго расписывать все прелести использования подкачиваемой и неподкачиваемой памяти, материала эту тему полно в Сети. Да и твой любимый журнал ][ не раз писал об организации памяти в Windows. Добавлю лишь, что обработка подкачиваемой и неподкачиваемой памяти ядром принципиально отличается одна от другой.

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

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

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

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

Первая таблица, PPNPagedLookasideList — это ассоциативная таблица (LookasideList), существующая отдельно для каждого процессора и описывающая выделенные куски nonpaged-памяти размером <= 256 байт. Каждый процессор обладает так называемым PCR — «регистром контроля процессора» (processor control register), который хранит информацию о таких вещах, как уровень IRQL, GDT, IDT и т.д. Расширение этого регистра, названное PCRB — «регион контроля процессора» (processor control region), хранит в себе указатель на эту очень любопытную таблицу.

В Windows Semerka внутренняя структура KPRCB немного изменилась, но, думаю, общая концепция понятна — KPRCB содержит в себе данные об использовании текущим процессором выделенной памяти:

typedef struct _KPRCB {
...
/*0x5A0*/ struct
_PP_LOOKASIDE_LIST PPLookasideList[16];
/*0x620*/ struct _GENERAL_LOOKASIDE_POOL
PPNPagedLookasideList[32];
/*0xF20*/ struct _GENERAL_LOOKASIDE_POOL
PPPagedLookasideList[32];
...
} KPRCB, *PKPRCB;

По сравнению с простыми двусторонними таблицами, такие ассоциативные таблицы позволяют процессору быстрее выделять память.

Для работы с этими таблицами используют внутреннюю функцию ExInterlockedPopEntrySList. Вторая таблица зависит от того, сколько процессоров используется, и как система управляет ими. Система выделения памяти использует эту таблицу, если размер выделяемой памяти <= 4080 байт. Или же в том случае, если ассоциативный поиск заканчивается ничем. Описатель такой таблицы имеет такую же структуру, как и POOL_DESCRIPTOR:

typedef struct _POOL_DESCRIPTOR
{
enum _POOL_TYPE PoolType;
union {
struct _KGUARDED_MUTEX PagedLock;
ULONG32 NonPagedLock;
};
LONG32 RunningAllocs;
LONG32 RunningDeAllocs;
LONG32 TotalBigPages;
LONG32 ThreadsProcessingDeferrals;
ULONG32 TotalBytes;
UINT8 _PADDING0_[0x2C];
ULONG32 PoolIndex;
UINT8 _PADDING1_[0x3C];
LONG32 TotalPages;
UINT8 _PADDING2_[0x3C];
VOID** PendingFrees;
LONG32 PendingFreeDepth;
UINT8 _PADDING3_[0x38];
struct _LIST_ENTRY ListHeads[512];
} POOL_DESCRIPTOR, *PPOOL_DESCRIPTOR;

Если на машине имеется лишь один процессор, то для получения переменной PoolVector используется указатель на NonPagedPoolDescriptor.
Если же процессоров много, то используется таблица ExpNonPagedPoolDescriptor, которая содержит 16 слотов, занятых под описатели пула.

При этом структура PRCB каждого процессора указывает на структуру KNODE, которая может быть слинкована между несколькими процессорами и может содержать несколько полей, используемых таблицей ExpNonPagedPoolDescriptor.

Третья и последняя таблица, нареченная MmNonPagedPoolFreeListHead, используется всеми процессорами в том случае, если нужно выделить блоки памяти больше, чем 4080 байт. Она также используется ядром, если у первых двух таблиц свободных ресурсов не осталось совсем. Эта таблица состоит из четырех списков LIST_ENTRY, каждый из которых представляет собой номер страницы, за исключением самого последнего списка, в котором хранится список (извини за тавтологию) страниц, занятых системой.

Доступ к этой таблице защищен спинлоком, который вызывается внутренней функцией ядра LockQueueNonPagedPoolLock.
Во время процедуры освобождения мелких блоков и очистки памяти функция ExFreePoolWithTag «склеивает» между собой такие блоки, после чего они попадают в таблицу MmNonPagedPoolFreeListHead уже в увеличенном размере.

Уфф, пожалуй, я увлекся. Понимаю, что все вышеописанное сразу понять трудно, но запасись терпением — и все встанет на свои места :). Так или иначе, думаю, пора двигаться к завершению.

 

И напоследок...

Итак, подведем итоги вышесказанного. Механизм выделения и освобождения памяти, по сути своей, зиждется на трех таблицах ядра, которые во многом похожи между собой. При этом самое примечательное то, что описание выделенных блоков памяти в этих таблицах основано на широко известных ассоциативных или просто двусвязных списках. Найти эти таблицы в ядре не представляет особого труда. Правда, для этого нужно засандалить драйвер, что в Windows Vista/7 сделать проблематично. Но речь сейчас не об этом.

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

Примерно вот таким образом. Помимо этого есть еще много способов поиздеваться над организацией памяти (таблицами) в Windows. Например, перезаписать указатель Next в ассоциативной таблице Lookaside, вызвать PoolOverflow путем перезаписи указателей в простых (односторонних) списках PendingFrees (они находятся в хидере пула) и т.д.

Вариантов много, и все они достаточно сложны для реализации. Однако при должных усилиях, прямых руках и умении копаться в отладчике — вырисовывается очень даже неплохая перспектива.

Мне, правда, неизвестны факты использования описанной техники в дикой природе, однако… кто знает, кто знает...

 

Заключение

Описанная в статье техника сводится к той удивительной категории, когда для того, чтобы поставить систему на колени, достаточно лишь переписать пару байт в памяти, при этом не используя какиелибо хуки. Используя эту технику, ты сможешь обойти самые неглупые механизмы обеспечения защиты операционной системы — такие, как сравнение кода или хэширование. Не хочу утверждать, что эта техника эксплуатации памяти в Windows всесторонне прекрасна и замечательна, однако при правильном и грамотном «употреблении» она способна вогнать в уныние разработчиков антивирусных и проактивных систем защиты. Удачного компилирования и да пребудет с тобой Сила!

 

Links

  • hackinthebox.org — на этом сайте ты сможешь найти много всяких вкусностей на тему программирования и безопасности.

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии