Дедупликация страницы исполняемого кода драйверов OC Windows (1187398), страница 6
Текст из файла (страница 6)
выше), происходит "встройка" в его код. То есть внутрифункции NotifyRoutine нужно "встроиться" в функию DriverInit инициализации драйвера.То есть снять запрет на запись в страницы драйвера; в первые несколько байт функииDriverInit инициализации драйвера поставить безусловный переход на мою новуюфункцию, MyNewDrEntry, которая будучи вызванной, вернет DriverInit в ее исходноесостояние, вызовет ее, и получит от нее в соответствующем поле структуры DriverObjectадрес функции DriverUnload драйвера, который, прежде чем передать эту структуру28менеджеру памяти, она подменит на адрес моей новой функции MyNewDrUnload, котораятеперь будет вызываться вместо страной функции DriverUnload драйвера.Таким образом, способ состоит в следующем: необходимо заменить роднуюфункцию отгрузки драйвера DriverUnload на мою новую функцию MyNewDrUnload,которая теперь будет вызываться вместо нее.
Для этого необходимо "встроиться" вфункию DriverInit инициализации драйвера и подменить значение в соответствующемполе структуры DriverObject.Теперь, при отгрузки драйвера вместо DriverUnload запустится MyNewDrUnload,которая сначала позволит отработать родной функции отгрузки драйвера, а потомвыполнить "разделение" страниц.2.2.2.2. Анализ секций исполняемого файлаТут речь идет о секциях, так как загрузчик исполняемых образов в Windows вовремя загрузки драйвера в память устанавливает атрибуты его страниц в соответствии стеми, которые имеет секция, содержащая эту страницу.Когда в систему грузится первый экземпляр драйвера, необходимо найти средивсех его страниц страницы, подходящие для слияния, то есть страницы кода исполняемые.Для этого в образе драйвера, загруженном в память, нужно найти все секции, сподходящими свойствами (атрибутами), и "запомнить"(например, записать в массив) всестраницы из этих секций.
Именно страницы из массива и будут потом "объединяться" саналогичными страницами других экземпляров.2.2.2.3. Создание общего набора страницНеобходимость создания нулевого набора описана в пункте 2.1.2.5. Здесь жерассмотрим, как он реализуется. На этом этапе уже известно количество и содержимоевсех страниц, которые подлежат объединению.Выделение страницСначала необходимо выделить нужное количество страниц. Документированногоспособа сделать это нет. Поэтому было произведено детальное исследование работыподсистемы памяти в ядре ОС Windows, с упором на механизмы выделения страниц призагрузке драйвера в систему. Был обнаружен набор функций, решающий эту задачу:MiReserveSystemPtes - для того, чтобы зарезервировать системные PTE (то есть получить29непрерывный набор адресов); MiAllocatePfn - выделить физическую страницу дляданного PTE; MiGetVirtualAddressMappedByPte - узнать, какой виртуальный адрессоответствует данной странице.Атрибуты и содержимое страницПосле этого для каждой страницы записать в нее соответствующее содержимое.
Атак же выставить атрибуты и права доступа так, чтобы они были аналогичны тем, которыеимеют страницы драйвера изначально. Кроме того writable страницы нужно превратить вCopy on Write.Удаление нулевого набораВ конце работы, после того как все экземпляры были выгружены, необходимоудалить из памяти страницы нулевого набора. Эта задача так же не решаетсядокументированными средствами ОС Windows. Было произведено исследование, в ходекоторого были найдены необходимые функции: MiDeleteSystemPageableVm - удаляетобласть в подкачиваемой памяти (pageable system address space (paged pool or driverpageable sections)); MiReleaseSystemPtes - освобождает PTE в неподкачиваемом пулепамяти (non paged portion of system space).Эти же функции могут быть использованы и для возвращения в систему"освободженных" страниц, то есть тех, с которыми драйвер больше не работает из-затого, что они были подменены страницами из общего набора.2.2.2.4.
Встройка в обработчик прерывания Page FaultПервым делом, необходимо найти, по какому адресу ядро загружено в память документированная функция ZwQuerySystemInformation.Работа обработчика прерывания выглядит так: сначала вызывается функцияKiTrap0E, которая выполняет некоторые проверки и вызывает функцию MmAccessFault,которая обрабатывает прерывание. Т.к. это 64 битная система, нельзя просто подменитьвызов функции MmAccessFault - на вызов своей новой функции. Но можно найти"пустое" пространство в коде ядра (несколько байт, заполненных нулями в следствиевыравнивания на страницу), и туда записать небольшой "stub", то есть код, который будетвызывать мой обработчик. И подменить вызов функции MmAccessFault - на передачууправления в этот "stub".При попытке записи - исключение Page Fault и «разъединение» страниц.30Рис. 9.Таким образом, теперь, при возникновении исключения ошибки страницы (PageFault) будет вызываться мой новый обработчик, который должен будет "разъединять"ранее "объединенные" страницы при попытки записи в них (если, конечно, изначальнодрайвер имел на это право).Внутри новой функции обработчикаВнутри новой функции обработчика необходимо поставить поверку, и если адрес,на котором произошла ошибка страницы не является адресом (виртуальным) какой-либоиз "объединенных" страниц, то вызывать старую функцию MmAccessFault.
А еслиявляется и ошибка произошла из-за того, что один из экземпляров драйвера захотел что-тозаписать в свою страницу (которая была уже объединена с другими и помеченакопируемой при записи (Copy on Write)), то необходимо обработать эту ситуацию.Выделить новую физическую страницу; записать на нее содержимоесоответствующей страницы из нулевого набора; выставить для нее "правильные"атрибуты, и сделать ее доступной для записи; "разъединить" страницы, то есть сделатьтак, чтобы этот виртуальный адрес ссылался теперь на эту вновь предоставленнуюстраницу, а не на страницу из нулевого набора.
(Подробнее см. пункт 2.2.3."Имплементация механизма копирования при записи".)312.2.2.5. Проверка идентичности экземпляровВ тот момент, когда новый экземпляр грузится в систему, необходимо проверитьего идентичность всем остальным, т.к.
любое несоответствие в коде может привести кнепредсказуемым последствиям. Основным критерием служит имя драйвера, его версия,однако, можно использовать и более сложные методы, например, как в KSM подсчитывать функцию ....... и сравнивать значения, полученные для двух "одинаковых"страниц.2.2.2.6. Подмена страниц драйвера страницами общего набораКогда создан общий набор необходим подменить "родные" страницы драйвра наидентичные им страницы из общего набора страниц.
Драйвер обращается к своимстраницам по их виртуальным адресам. Дальше для перевода виртуального адреса вфизический используется структура PTE (Page Table Enry, см. пункт 1.4. "Озорлитературы"), которая содержит номер нужной страницы в базе данных PFN (Page FrameNumber, см. пункт 1.4.
"Озор литературы"). Нам необходимо сделать так, чтобы драйверобращался к странице из общего набора, то есть виртуальный адрес страницы долженостаться старым, а вот номер физической страницы, на которую он ссылается долженизмениться. Следовательно, необходимо изменить значение PTE, соответствующей этомувиртуальному адресу. И прописать в него новый номер физической страницы и ееатрибуты. Подробнее, как это делается, см. пункт 2.2.3. "Имплементация копирования призаписи".2.2.2.7. Writable страницы сделать Copy On Write.Для этого достаточно сбросить бит "Writable" и взвести бит "Copy On Write" ватрибутах в PTE страницы.
(Подробнее см. пункт 1.4. "Обзор литературы" и 2.2.3."Имплементация копирования при записи" про PTE и ее строение.)2.2.2.8. Создание и работа с резервным буфером; подкачивающая нитьПри "разъединении" страниц при записи необходимо предоставить новуюфизическую страницу, копию той страницы из обзего набора, на которую драйвер хотелчто-то записать. Так как это происходит в обработчике прерывания Page Fault, то сделатьэто необходимо быстро. Однако, не всегда есть возможность выделить новую страницу, тоесть запросить ее у системы.
В случае работы на высоких уровнях прерываний (IRQL)сделать это невозможно, так как выставлена блокировка базы данных PFN.В этом случае предлагается создать резервный буфер заранее выделенных страниц,и при необходимости (когда нельзя выделить новую) брать сттраницу из него.32Поскольку во время работы, буфер будет опустошаться, необходимо завестидополнительную нить, которая будет добавлять в него страницы.