Здесь я продемонстрирую возможность, которая по своей сути — самый настоящий хак. Вопрос, зачем это может понадобиться? На самом деле целей для этого может быть огромное множество. Итак наша задача — изменить код библиотеки mscorlib таким образом, чтобы все программы, которые ей пользуются, получили эти изменения. Не рантайм, конечно, а во время старта (для runtime необходимо проделать другие вещи, и тут надо оговориться что изменения эти не должны поломать текущие состояния библиотеки). Mscorlib я взял как пример, потому что она есть у всех на компьютере. Но можно хакать любую другую.
Все мы знаем, что для того, чтобы не было «ада dll», Microsoft помимо обычных версий и названия библиотек, дали возможность подписывать сборки ключом, public key которой гарантирует что конкретная сборка «пришла» от конкретного разработчика, а не от какого-то другого. Поэтому, если мы хотим по какой-то вполне добросовестной причине изменить код существующей библиотеки т.о., чтобы она загрузилась в чужой процесс и при этом ключик public key остался тем же, у нас этого не получится. Потому что мы не сможем ее подписать, у нас нет закрытого ключа.
Наша мини цель, чтобы программа вывела на консоль текст:
![](https://habrastorage.org/r/w1560/files/8c6/0f0/e3d/8c60f0e3d36248ce86ae82a76f5d6997.png)
Краткое содержание:
Сегодня речь пойдет о сборках, находящихся в GAC и в NAC. Я выяснил, что .NET Framework очень чутко относится к структуре GAC. Он доверяет ей настолько, что загружает оттуда сборки, не особо проверяя номер версии. Потому для того чтобы сделать подмену нам надо не так много: нам необходимо написать драйвер уровня ядра для того чтобы перенаправлять обращения к определенным файлам в другое место. Я для этого воспользовался возможностью filesystem filters. Суть проста: для того чтобы не писать много кода в самом драйвере, пишется протокол между Kernel-space и user-space. Со стороны user-space выступает windows — service приложение. Оно общается с драйвером, дает ему команды, что куда перенаправлять. А тот в свою очередь перенаправляет. Для того чтобы менять код существующей библиотеки, например, mscorlib, можно воспользоваться либо Reflexil, либо Mono::Cecil.
Порядок действий:
И мы получаем при последующих запусках всех приложений на .net логгирования системной библиотеки. Для порядка, необходимо чтобы драйвер фильтровал также по номерам процессов, кому конкретно давать «левак», а кому — оригинальную библиотеку.
Для начала устанавливаем VirtualBox. Он нам понадобится чтобы отлаживать драйвер. Нам же не хочется перезапускаться каждый раз, когда наш драйвер будет обращаться не по тем адресам и вылетать с ошибкой (естественно, с BSOD'ом). На виртуалки накатываем образы Windows XP и Windows 7, x86 и x64 и снимаем снапшоты, чтобы было куда откатываться.
Далее устанавливаем на девелоперскую машину VisualDDK, WinDDK, и прочую инфраструктуру для разработки драйверов.
Далее пишем сам драйвер. Я не буду выкладывать полные листинги, а только важные их части. Оговорюсь только что пишем мы Minifilter Filesystem Driver.
1) Регистрация фильтра:
Тут все просто. Вводим структуры для регистрации драйвера фильтра. Фильтр будет работать на всех volumes, и перехватывать операцию CreateFile (создание/открытие файла)
Инициализация драйвера также не должна вызывать вопросов у опытных людей. Просто перечислю, что тут происходит:
Выход из фильтра также прост:
Скажу только что DeregisterFilter занимается освобождением всех ресурсов. основываясь на выставленных флагах в DRIVER_DATA.Initialized (см. код выше)
Далее. Теперь нам необходимо знать что и куда перенаправлять. Какой файл и куда. Для этого я сделал небольшой протокольчик между kernel-mode и user-mode по communication port, оторый мы подняли в DriverEntry
Определяем набор команд:
Определяем структуру базы всех команд:
Определяем структуру, посылаемую сервису Windows для уведомления о новом процессе в системе:
Опеделяем структуру-ответ:
Также вводим структуры для хранения данных в драйвере:
Теперь, когда все определено, необходимо при старте нового процесса (а мы уже его перехватываем) оповестить об этом сервис, который в качестве ответа отдаст нам набор правил редиректа файлов и папок в зависимости от этого процесса.
Я не буду включать код функций, очищающих память, т.к. это не так интересно.
При вызове функции ей передается создавший процесс, созданный и флаг, создан ли процесс либо он умирает. Т.е. функция оповещает нас и о создании и о смерти процесса.
Внутри ее, если процесс создается, мы оповещаем об этом сервис Windows, передавая PID процесса. По этому PID сервис находит у себя список правил редиректа и отдает их нам в качестве ответа:
Код, представленный ниже просто инициализирует внутренние структуры относительно тех данных, которые пришли от сервиса Windows:
И, последнее, функция поиска правила редиректа. Если правило найдено, возвращает STATUS_SUCCESS и видоизмененный путь:
У нас есть драйвер, который оповещает внешний сервис о создании новых процессов в операционной системе. Сервис получая информацию о процессе принимает решение о том, включать на нем мониторинг или нет. Если да, передает также список правил редиректов на файловой системе.
Опять же, я не буду вдаваться в особые подробности. Это обычный сервис. Единственная особенность заключается в том что он должен ожидать от драйвера команд и отвечать на них. Ожидать мы будем при помощи IoCompletionPort. Этот механизм требует (в общем понятно для чего), чтобы на ожидании завершения ввода/вывода по порту висело несколько потоков. Когда придут данные, одна из задач просыпается и может эти данные обработать. Мы в этой задаче будем отсылать список редиректов.
Код может быть немного нечистый, ну да ладно. Главное делает что надо:
И, наконец, класс обслуживания потока:
Настройка происходит следующим не хитрым образом:
Это значит что любое обращение к сборке mscorlib.dll в GAC приведет к редиректу в папку «C:\temp\GAC32\mscorlib.dll» для 32-х разрядной системы. Для 64-х разрядной проделайте тоже самое, но для другой папки.
Для модификации mscorlib можно воспользоваться .Net Reflector + Reflexil, старыми freeware версиями.
В них я заменил System.Environment.get_Version() так, чтобы номер версии был «5.0.40930.0»
Для тестирования создадим .Net Framework 4.0 приложение и назовем его testredir, поскольку наш драйвер ожидает именно такое имя процесса.
Текст приложения прост до невозможности
Вывод программы:
Заметка:
Изменять код системных библиотек необходимо аккуратно. Например, наш пример валит часть программного обеспечения, поскольку оно проверяет номер версии, на которой запускается. Хорошим примером может послужить создание логгера внутренних событий, чем я сейчас и занимаюсь
Источники:
Все мы знаем, что для того, чтобы не было «ада dll», Microsoft помимо обычных версий и названия библиотек, дали возможность подписывать сборки ключом, public key которой гарантирует что конкретная сборка «пришла» от конкретного разработчика, а не от какого-то другого. Поэтому, если мы хотим по какой-то вполне добросовестной причине изменить код существующей библиотеки т.о., чтобы она загрузилась в чужой процесс и при этом ключик public key остался тем же, у нас этого не получится. Потому что мы не сможем ее подписать, у нас нет закрытого ключа.
Наша мини цель, чтобы программа вывела на консоль текст:
![](https://habrastorage.org/files/8c6/0f0/e3d/8c60f0e3d36248ce86ae82a76f5d6997.png)
Для начала, полный список выложенных на Хабре статей данного цикла
Делаем отгружаемые сборки: взаимодействуем между доменами без маршаллинга
Получение указателя на объект .Net
Ручное клонирование потока. Когда Assembler + C# или Java = Love
Изменение кода системных сборок или «утечка» .Net Framework 5.0
Как работает декомпиляция в .Net или Java на примере .Net
Продолжаем кромсать CLR: пул объектов .Net вне куч SOH/LOH
Снимаем дамп объектов с памяти .Net приложения
![](https://habrastorage.org/files/7d1/9d7/8ab/7d19d78ab896445fb5573b5f5e807ba0.png)
![](https://habrastorage.org/files/7d1/9d7/8ab/7d19d78ab896445fb5573b5f5e807ba0.png)
![](https://habrastorage.org/files/7d1/9d7/8ab/7d19d78ab896445fb5573b5f5e807ba0.png)
![](https://habrastorage.org/files/7d1/9d7/8ab/7d19d78ab896445fb5573b5f5e807ba0.png)
![](https://habrastorage.org/files/7d1/9d7/8ab/7d19d78ab896445fb5573b5f5e807ba0.png)
![](https://habrastorage.org/files/7d1/9d7/8ab/7d19d78ab896445fb5573b5f5e807ba0.png)
![](https://habrastorage.org/files/7d1/9d7/8ab/7d19d78ab896445fb5573b5f5e807ba0.png)
Краткое содержание:
- Введение
- Разработка драйвера
- Выводы по разработке драйвера
- Написание сервиса Windows
- Изменение кода mscorlib.dll
- Результаты исследования
Введение
Сегодня речь пойдет о сборках, находящихся в GAC и в NAC. Я выяснил, что .NET Framework очень чутко относится к структуре GAC. Он доверяет ей настолько, что загружает оттуда сборки, не особо проверяя номер версии. Потому для того чтобы сделать подмену нам надо не так много: нам необходимо написать драйвер уровня ядра для того чтобы перенаправлять обращения к определенным файлам в другое место. Я для этого воспользовался возможностью filesystem filters. Суть проста: для того чтобы не писать много кода в самом драйвере, пишется протокол между Kernel-space и user-space. Со стороны user-space выступает windows — service приложение. Оно общается с драйвером, дает ему команды, что куда перенаправлять. А тот в свою очередь перенаправляет. Для того чтобы менять код существующей библиотеки, например, mscorlib, можно воспользоваться либо Reflexil, либо Mono::Cecil.
Порядок действий:
- Меняем сборку (Reflexil или Mono::Cecil) по сборке mscorlib и получаем видоизмененную сборку. Например, встраиваем в нее логгирование;
- Кладем результат в tmp каталог;
- Добавляем redirection «C:\Windows\Microsoft.NET\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll» на, например, «C:\tmp\mscorlib64.dll» — видоизмененная сборка.
И мы получаем при последующих запусках всех приложений на .net логгирования системной библиотеки. Для порядка, необходимо чтобы драйвер фильтровал также по номерам процессов, кому конкретно давать «левак», а кому — оригинальную библиотеку.
Разработка драйвера
Для начала устанавливаем VirtualBox. Он нам понадобится чтобы отлаживать драйвер. Нам же не хочется перезапускаться каждый раз, когда наш драйвер будет обращаться не по тем адресам и вылетать с ошибкой (естественно, с BSOD'ом). На виртуалки накатываем образы Windows XP и Windows 7, x86 и x64 и снимаем снапшоты, чтобы было куда откатываться.
Далее устанавливаем на девелоперскую машину VisualDDK, WinDDK, и прочую инфраструктуру для разработки драйверов.
Далее пишем сам драйвер. Я не буду выкладывать полные листинги, а только важные их части. Оговорюсь только что пишем мы Minifilter Filesystem Driver.
1) Регистрация фильтра:
const FLT_OPERATION_REGISTRATION Callbacks[] = {
{ IRP_MJ_CREATE,
0,
PbPreOperationCreateCallback,
PbPostOperationCreateCallback },
{ IRP_MJ_NETWORK_QUERY_OPEN,
0,
PbPreOperationNetworkQueryOpenCallback,
NULL },
{ IRP_MJ_OPERATION_END }
};
CONST FLT_REGISTRATION FilterRegistration = {
sizeof( FLT_REGISTRATION ), // Size
FLT_REGISTRATION_VERSION, // Version
0, // Flags
NULL, // Context
Callbacks, // Operation callbacks
PtFilterUnload, // FilterUnload
PtInstanceSetup, // InstanceSetup
PtInstanceQueryTeardown, // InstanceQueryTeardown
PtInstanceTeardownStart, // InstanceTeardownStart
PtInstanceTeardownComplete, // InstanceTeardownComplete
PtGenerateFileName, // GenerateFileName
PtNormalizeNameComponent // NormalizeNameComponent
};
Тут все просто. Вводим структуры для регистрации драйвера фильтра. Фильтр будет работать на всех volumes, и перехватывать операцию CreateFile (создание/открытие файла)
Инициализация драйвера также не должна вызывать вопросов у опытных людей. Просто перечислю, что тут происходит:
- драйвер регистрируется в системе
- делается нотификация, которая будет нас уведомлять о новых процессах в системе
- поднимается Communitation Port для общения с пользовательским уровнем (3 кольцо защиты, приложения Windows), где нас ожидает сервис Windows, который будет описан ниже
CPP_DRIVER_ENTRY (
__in PDRIVER_OBJECT DriverObject,
__in PUNICODE_STRING RegistryPath
)
{
/* locals */
NTSTATUS status;
OBJECT_ATTRIBUTES attr;
UNICODE_STRING portName;
PSECURITY_DESCRIPTOR securityDescriptor;
/* unused */
UNREFERENCED_PARAMETER( RegistryPath );
/* code */
__try {
// Set DRIVER_DATA to zeroes
memset(&DRIVER_DATA, 0, sizeof(DRIVER_DATA));
/* Setup process creation callback */
status = Xu(&PsProcessNotify, FALSE);
if( !NT_SUCCESS( status )) __leave;
SetFlag(DRIVER_DATA.initialized, FILTER_SUBSYSTEM_PROCESS);
// Get exported OS functions
DECLARE_CONST_UNICODE_STRING( Func1, L"IoReplaceFileObjectName" ); // Win7+
DRIVER_DATA.pfnIoReplaceFileObjectName = (PIOREPLACEFILEOBJECTNAME) MmGetSystemRoutineAddress((PUNICODE_STRING) &Func1 );
// Register filter
status = FltRegisterFilter( DriverObject,
&FilterRegistration,
&DRIVER_DATA.fltHandle );
if ( !NT_SUCCESS( status )) __leave;
SetFlag(DRIVER_DATA.initialized, FILTER_SUBSYSTEM_DRIVER);
FltInitializePushLock(&DRIVER_DATA.Sync);
SetFlag(DRIVER_DATA.initialized, FILTER_SUBSYSTEM_LOCK);
// Setup security descriptor
status = FltBuildDefaultSecurityDescriptor( &securityDescriptor, FLT_PORT_ALL_ACCESS );
if ( !NT_SUCCESS( status )) __leave;
RtlInitUnicodeString( &portName, PbCommPortName );
InitializeObjectAttributes( &attr, &portName, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, securityDescriptor );
status = FltCreateCommunicationPort( DRIVER_DATA.fltHandle, &DRIVER_DATA.Connection.ServerPort, &attr, NULL, CommPortConnect, CommPortDisconnect, CommPortMessageNotify, 1 );
if ( !NT_SUCCESS( status )) __leave;
SetFlag(DRIVER_DATA.initialized, FILTER_SUBSYSTEM_COMPORT);
// Free the security descriptor in all cases. It is not needed once the call to FltCreateCommunicationPort( ) is made.
FltFreeSecurityDescriptor( securityDescriptor );
if ( !NT_SUCCESS( status )) __leave;
// Start filtering i/o
status = FltStartFiltering( DRIVER_DATA.fltHandle );
if ( !NT_SUCCESS( status )) __leave;
ASSERT( NT_SUCCESS( status ) );
DRIVER_DATA.State = DRIVER_STATE_STARTED;
} __finally
{
if(!NT_SUCCESS( status ))
{
DeregisterFilter();
}
}
return status;
}
Выход из фильтра также прост:
Скажу только что DeregisterFilter занимается освобождением всех ресурсов. основываясь на выставленных флагах в DRIVER_DATA.Initialized (см. код выше)
NTSTATUS
PtFilterUnload(
__in FLT_FILTER_UNLOAD_FLAGS Flags
)
{
UNREFERENCED_PARAMETER( Flags );
PAGED_CODE();
DeregisterFilter();
DbgPrint("PoliciesSandbox!PtFilterUnload: Entered\n");
return STATUS_SUCCESS;
}
Далее. Теперь нам необходимо знать что и куда перенаправлять. Какой файл и куда. Для этого я сделал небольшой протокольчик между kernel-mode и user-mode по communication port, оторый мы подняли в DriverEntry
Определяем набор команд:
typedef enum {
// From verson = v1.0
// From User-space to Kernel-space
GetVersion,
GetFilesystemRedirections,
AddFilesystemByPIDRedirection,
RemoveFilesystemByPIDRedirection,
GetRegistryRedirections,
AddRegistryByPIDRedirection,
RemoveRegistryByPIDRedirection,
// From Kernel-space to User-space
ProcessAttached,
CancelIOCompletionPort
// Version v2.0 is here
} COMM_COMMAND;
Определяем структуру базы всех команд:
typedef struct {
COMM_COMMAND Command;
USHORT Data[];
} COMM_MESSAGE, *PCOMM_MESSAGE;
Определяем структуру, посылаемую сервису Windows для уведомления о новом процессе в системе:
typedef struct
{
ULONG Pid;
} COMM_PROCESS_ATTACHED, *PCOMM_PROCESS_ATTACHED;
Опеделяем структуру-ответ:
typedef struct
{
USHORT NeedToMonitor;
USHORT IsCompleted; // true, if this packet contains all redirections, needed by driver. Otherwice, driver needs to ask more
USHORT PairsCount; // redirections count. Actually, count of null-terminated strings in Data
struct { // positions of redirections to make searching fast
USHORT From;
USHORT To;
} Positions[64];
WCHAR Data[];
} COMM_FS_REDIRECTIONS, *PCOMM_FS_REDIRECTIONS;
Также вводим структуры для хранения данных в драйвере:
typedef struct _MAPPING_ENTRY {
UNICODE_STRING OldName;
UNICODE_STRING NewName;
_MAPPING_ENTRY *Next;
} MAPPING_ENTRY, *PMAPPING_ENTRY;
typedef struct _PROCESSES_MAP_ENTRY {
ULONG Pid;
PMAPPING_ENTRY entries;
_PROCESSES_MAP_ENTRY *Prev, *Next;
} PROCESSES_MAP_ENTRY, *PPROCESSES_MAP_ENTRY;
Теперь, когда все определено, необходимо при старте нового процесса (а мы уже его перехватываем) оповестить об этом сервис, который в качестве ответа отдаст нам набор правил редиректа файлов и папок в зависимости от этого процесса.
Я не буду включать код функций, очищающих память, т.к. это не так интересно.
При вызове функции ей передается создавший процесс, созданный и флаг, создан ли процесс либо он умирает. Т.е. функция оповещает нас и о создании и о смерти процесса.
Внутри ее, если процесс создается, мы оповещаем об этом сервис Windows, передавая PID процесса. По этому PID сервис находит у себя список правил редиректа и отдает их нам в качестве ответа:
VOID PsProcessNotify (
IN HANDLE ParentId,
IN HANDLE ProcessId,
IN BOOLEAN Create
)
{
NTSTATUS status;
if( HandleToULong( ProcessId ) <= 4) return;
/* Check exsisting data */
if(Create)
{
LARGE_INTEGER liTimeout;
DbgPrint("Process created: %x", ProcessId);
liTimeout.QuadPart = -((LONGLONG)1 * 10 * 1000 * 1000);
ULONG SenderBufferLength = MESSAGE_BUFFER_SIZE;
ULONG ReplyLength = MESSAGE_BUFFER_SIZE;
PCOMM_MESSAGE message = (PCOMM_MESSAGE)myNonPagedAlloc(MESSAGE_BUFFER_SIZE);
message->Command = ProcessAttached;
((PCOMM_PROCESS_ATTACHED)(&message->Data[0]))->Pid = HandleToULong(ProcessId);
status = FltSendMessage(DRIVER_DATA.fltHandle, &DRIVER_DATA.Connection.ClientPort,
message, SenderBufferLength, message, &ReplyLength,
&liTimeout);
if(ReplyLength > 0)
{
PCOMM_FS_REDIRECTIONS Redirections = (PCOMM_FS_REDIRECTIONS)&message->Data[0];
DbgPrint("Recieved reply from user-mode: NeedToMonitor = %s\n", Redirections->NeedToMonitor ? "TRUE" : "FALSE" );
if(Redirections->NeedToMonitor)
{
PbRepInitializeMapping(HandleToULong(ProcessId), Redirections);
} else {
DbgPrint("Thread %d not needed to be monitored. Skipping.", HandleToULong(ProcessId));
}
}
if(!message) myFree(message);
}
else
{
DbgPrint("Process destroyed: %x\n", ProcessId);
PPROCESSES_MAP_ENTRY entry = DRIVER_DATA.Mapping;
while( entry != NULL )
{
if(entry->Pid == HandleToULong(ProcessId))
{
PbRepDeleteMapping(entry);
break;
}
}
}
}
Код, представленный ниже просто инициализирует внутренние структуры относительно тех данных, которые пришли от сервиса Windows:
NTSTATUS PbRepInitializeMapping( __in ULONG pid, __in PCOMM_FS_REDIRECTIONS Redirections )
{
NTSTATUS status = STATUS_SUCCESS;
FltAcquirePushLockExclusive( &DRIVER_DATA.Sync );
__try
{
DbgPrint("PlociesSandbox!PbRepInitializeMapping: redirections count: %d\n", Redirections->PairsCount);
PMAPPING_ENTRY current = NULL;
// Lookup PID in map
PPROCESSES_MAP_ENTRY currentProcess = DRIVER_DATA.Mapping;
while(currentProcess != NULL)
{
if(currentProcess->Pid == pid)
{
DbgPrint("PlociesSandbox!PbRepInitializeMapping: Already initialized; skipping");
return STATUS_SUCCESS;
}
currentProcess = currentProcess->Next;
}
currentProcess = (PPROCESSES_MAP_ENTRY)myNonPagedAlloc(sizeof(PROCESSES_MAP_ENTRY));
currentProcess->Pid = pid;
currentProcess->Next = DRIVER_DATA.Mapping;
currentProcess->Prev = NULL;
if(DRIVER_DATA.Mapping != NULL) DRIVER_DATA.Mapping->Prev = currentProcess;
DRIVER_DATA.Mapping = currentProcess;
for(int i=0; i < Redirections->PairsCount; i++)
{
// Copying a pair of pathes From->To to internal mapping structure
int FromLen = wcslen(&Redirections->Data[Redirections->Positions[i].From]);
int ToLen = wcslen(&Redirections->Data[Redirections->Positions[i].To]);
PMAPPING_ENTRY mappingEntry = (PMAPPING_ENTRY)myAlloc(NonPagedPool, sizeof(MAPPING_ENTRY));
mappingEntry->OldName.Buffer = (WCHAR*)myAlloc(NonPagedPool, (FromLen + 1) * sizeof(WCHAR));
wcscpy(mappingEntry->OldName.Buffer, &Redirections->Data[Redirections->Positions[i].From]);
mappingEntry->OldName.Length = mappingEntry->OldName.MaximumLength = wcslen(mappingEntry->OldName.Buffer) * sizeof(WCHAR);
mappingEntry->NewName.Buffer = (WCHAR*)myAlloc(NonPagedPool, (ToLen + 1) * sizeof(WCHAR));
wcscpy(mappingEntry->NewName.Buffer, &Redirections->Data[Redirections->Positions[i].To]);
mappingEntry->NewName.Length = mappingEntry->NewName.MaximumLength = wcslen(mappingEntry->NewName.Buffer) * sizeof(WCHAR);
if(current == NULL)
{
current = mappingEntry;
currentProcess->entries = current;
} else {
current->Next = mappingEntry;
current = mappingEntry;
}
current->Next = NULL;
}
} __finally {
FltReleasePushLock( &DRIVER_DATA.Sync );
}
DbgPrint("PlociesSandbox!PbRepInitializeMapping: done\n");
return status;
}
И, последнее, функция поиска правила редиректа. Если правило найдено, возвращает STATUS_SUCCESS и видоизмененный путь:
bool PbIsFolder(PUNICODE_STRING path)
{
return path->Buffer[path->Length/2 - 1] == L'\\';
}
NTSTATUS PbLookupRedirection(__in PUNICODE_STRING FilePath, __out PUNICODE_STRING *FileFound)
{
// Possible redirections:
// Full change: \Device\HarddiskVolume2\InternalPath\Path\To\Some\File.Ext -> \Device\.\Temporary\File.ext
// Partial change: \Device\HarddiskVolume2\InternalPath\ -> \Device\HarddiskVolume2\Temporary\
// in this case all pathes, starts with ..\InternalPath should be changed. For ex.:
// \Device\HarddiskVolume2\InternalPath\Some\Path\To\File.Ext -> \Device\HarddiskVolume2\Temporary\Some\Path\To\File.Ext
ULONG Pid = HandleToULong(PsGetCurrentProcessId());
FltAcquirePushLockShared( &DRIVER_DATA.Sync );
__try
{
PPROCESSES_MAP_ENTRY currentProcess = DRIVER_DATA.Mapping;
while(currentProcess != NULL)
{
if(currentProcess->Pid == Pid)
{
PMAPPING_ENTRY current = currentProcess->entries;
while(current != NULL)
{
if(PbIsFolder(¤t->OldName))
{
// Folders prefixes are identical, please note that all lengthes are double-sized
if(wcsncmp(current->OldName.Buffer, FilePath->Buffer, current->OldName.Length / 2) == NULL)
{
int newlength = (FilePath->Length - current->OldName.Length) + current->NewName.Length;
PUNICODE_STRING ret = PbAllocUnicodeString(newlength + 2);
RtlCopyUnicodeString(ret, ¤t->NewName);
RtlCopyMemory( Add2Ptr(ret->Buffer, ret->Length),
Add2Ptr(FilePath->Buffer, current->OldName.Length),
(FilePath->Length - current->OldName.Length) + 2);
ret->Length = wcslen(ret->Buffer) * 2;
*FileFound = ret;
return STATUS_SUCCESS;
}
} else {
if(wcscmp(current->OldName.Buffer, FilePath->Buffer) == NULL)
{
PUNICODE_STRING ret = PbAllocUnicodeString(current->NewName.Length + 2);
RtlCopyUnicodeString(ret, ¤t->NewName);
*FileFound = ret;
return STATUS_SUCCESS;
}
}
current = current->Next;
}
}
currentProcess = currentProcess->Next;
}
return STATUS_NOT_FOUND;
} __finally
{
FltReleasePushLock( &DRIVER_DATA.Sync );
}
}
Выводы по разработке драйвера
У нас есть драйвер, который оповещает внешний сервис о создании новых процессов в операционной системе. Сервис получая информацию о процессе принимает решение о том, включать на нем мониторинг или нет. Если да, передает также список правил редиректов на файловой системе.
Написание сервиса Windows
Опять же, я не буду вдаваться в особые подробности. Это обычный сервис. Единственная особенность заключается в том что он должен ожидать от драйвера команд и отвечать на них. Ожидать мы будем при помощи IoCompletionPort. Этот механизм требует (в общем понятно для чего), чтобы на ожидании завершения ввода/вывода по порту висело несколько потоков. Когда придут данные, одна из задач просыпается и может эти данные обработать. Мы в этой задаче будем отсылать список редиректов.
Код может быть немного нечистый, ну да ладно. Главное делает что надо:
- FilterConnectCommunicationPort соединяет нас с драйвером
- CreateIoCompletionPort создает i/o completion port, все ссылки, где о нем почитать есть в коде
- MessagingManagerThread — класс, управляющий потоком обработки сообщений от драйвера
- Остановка (Stop) посылает всем потокам команду завершения через io completion port, иначе все будет висеть
public unsafe class MessagingManager
{
private const string FilterCommunicationPortName = "\\PoliciesInjectorCom0";
private IntPtr m_filterPortHandle;
private IntPtr m_ioCompletionPortHandle;
private IntPtr[] m_buffers;
private MessagingManagerThread[] m_threads;
private CancellationTokenSource m_cancellationToken;
public unsafe MessagingManager(Dictionary<string,string> redirections)
{
uint hr = WinApi.FilterConnectCommunicationPort(FilterCommunicationPortName, 0, IntPtr.Zero, 0, IntPtr.Zero, out m_filterPortHandle);
if(hr != 0x0)
{
throw WinApi.CreateWin32Exception(String.Format("Cannot connect to driver via '{0}' port", FilterCommunicationPortName));
}
Console.WriteLine("Connected to {0}", FilterCommunicationPortName);
// For more info, ENG: http://msdn.microsoft.com/en-us/library/windows/desktop/aa365198(v=vs.85).aspx
// For more info, RUS: http://www.rsdn.ru/article/baseserv/threadpool.xml
int threadsCount = Environment.ProcessorCount * 2;
m_ioCompletionPortHandle = WinApi.CreateIoCompletionPort(m_filterPortHandle, IntPtr.Zero, IntPtr.Zero, threadsCount);
if(m_ioCompletionPortHandle == IntPtr.Zero)
{
throw WinApi.CreateWin32Exception("Cannot create I/O Completeon port");
}
// Make thread for each processor and threads cancellation token
m_threads = new MessagingManagerThread[threadsCount];
m_buffers = new IntPtr[threadsCount];
m_cancellationToken = new CancellationTokenSource();
Console.WriteLine("Number of threads to monitor: {0}", threadsCount);
for(int i=0; i<m_threads.Length; i++)
{
m_threads[i] = new MessagingManagerThread(m_ioCompletionPortHandle, m_cancellationToken.Token, m_filterPortHandle, redirections);
unsafe
{
m_buffers[i] = Marshal.AllocHGlobal( sizeof(F2U.IncomingMessagePacket) );
WinApi.RtlZeroMemory( m_buffers[i], sizeof(F2U.IncomingMessagePacket) );
var buffer = (F2U.IncomingMessagePacket *) m_buffers[i];
// Make street magic (see DDK Examples->mini filters->scanner)
hr = WinApi.FilterGetMessage( m_filterPortHandle, out buffer->Header, Marshal.SizeOf(typeof(F2U.IncomingMessagePacket)),
ref buffer->Overlapped );
if ( hr != WinApi.HRESULT_FROM_WIN32( WinApi.ERROR_IO_PENDING ) )
{
throw WinApi.CreateWin32Exception( String.Format("Cannot get filter message. 0x{0:X}", hr ));
}
}
}
}
public unsafe bool Stop()
{
bool successfull = true;
m_cancellationToken.Cancel();
Console.WriteLine("Starting to cancel I/O.");
if (!WinApi.CancelIoEx(m_filterPortHandle, IntPtr.Zero))
{
var errstr = String.Format("Cannot cancel I/O operations (0x{0:X}).", Marshal.GetLastWin32Error());
Console.WriteLine(errstr);
successfull = false;
// throw WinApi.CreateWin32Exception(errstr);
}
foreach(var thread in m_threads.AsEnumerable())
{
var overlapped = new F2U.IncomingMessagePacket();
IntPtr *completionKey;
overlapped.Message.Command = Command.CancelIOCompletionPort;
WinApi.PostQueuedCompletionStatus(m_ioCompletionPortHandle, (uint)sizeof(F2U.IncomingMessagePacket), out completionKey, (NativeOverlapped *) &overlapped);
}
foreach(var thread in m_threads.AsEnumerable())
{
if(!thread.WaitHandle.WaitOne(2000))
{
Console.WriteLine("Failed while waiting for thread {0} is stopped", thread.ThreadId);
successfull = false;
// TODO: kill thread and report confusing bug
}
}
return successfull;
}
}
И, наконец, класс обслуживания потока:
public class MessagingManagerThread
{
private ManualResetEvent m_resetEvent;
private IntPtr m_ioCompletionPortHandle;
private IntPtr m_filterPortHandle;
private CancellationToken m_cancelToken;
private Thread m_thread;
private Int32 m_threadId;
private Dictionary<string,string> m_redirections;
private const int timeoutCompletionStatus = 5000;
/// <summary>
/// Builds MessagingManagerThread object
/// </summary>
/// <param name="handle">I/O Completion Port Handle (Win32)</param>
public MessagingManagerThread(IntPtr ioCompletionPortHandle, CancellationToken token, IntPtr filterPortHandle, Dictionary<string,string> redirections)
{
m_ioCompletionPortHandle = ioCompletionPortHandle;
m_filterPortHandle = filterPortHandle;
m_cancelToken = token;
m_redirections = redirections;
m_resetEvent = new ManualResetEvent(false);
m_thread = new Thread( Start );
m_thread.Start();
}
public WaitHandle WaitHandle
{
get { return m_resetEvent; }
}
public Int32 ThreadId
{
get { return m_threadId; }
}
public unsafe void Start()
{
try
{
// Get current thread id (unmanaged)
m_threadId = WinApi.GetCurrentThreadId();
Console.WriteLine("Monitoring thread {0} is started", m_threadId);
// Messages processing queue
while(true)
{
// If cancellation requested, we should to leave thread
if(m_cancelToken.IsCancellationRequested)
{
Console.WriteLine("Cancellation on thread {0} is requested", m_threadId);
return;
}
// Otherwise, we should read completion port
uint numberOfBytesTransferred;
UIntPtr lpCompletionKey;
NativeOverlapped* lpOverlapped;
Console.WriteLine("Starting waiting for message at {0}... ", m_threadId);
if(!WinApi.GetQueuedCompletionStatus(m_ioCompletionPortHandle, out numberOfBytesTransferred, out lpCompletionKey, out lpOverlapped, -1))
{
// Something wrong happend
var error = Marshal.GetLastWin32Error();
if(error == WinApi.ERROR_OPERATION_ABORTED)
{
return;
}
if(error == WinApi.WAIT_TIMEOUT)
{
Console.WriteLine("Time out {0}", m_threadId);
continue;
}
Console.WriteLine("WinApi.GetQueuedCompletionStatus returns error code: {0:X}", error);
throw WinApi.CreateWin32Exception("GetQueuedCompletionStatus", (uint)error);
}
Console.WriteLine("GetQueuedCompletionStatus finished successfully, Message is recieved");
// Message recieved
var request = (F2U.IncomingMessagePacket *)lpOverlapped;
if(request->Message.Command == Command.ProcessAttached)
{
Run_ProcessAttached(request);
} else
if(request->Message.Command == Command.CancelIOCompletionPort)
{
Console.WriteLine("Thread destroying requested");
}
// Queue a new request completion.
WinApi.RtlZeroMemory( (IntPtr) request, Marshal.SizeOf(request->GetType()));
uint hr = WinApi.FilterGetMessage( m_filterPortHandle, out request->Header, Marshal.SizeOf(request->GetType()),
ref request->Overlapped );
if (hr != WinApi.HRESULT_FROM_WIN32(WinApi.ERROR_IO_PENDING))
{
throw WinApi.CreateWin32Exception( "Cannot get filter message", hr );
}
}
} finally
{
// Send signal to owner
m_resetEvent.Set();
}
}
private unsafe void Run_ProcessAttached(F2U.IncomingMessagePacket *data)
{
var pid = ((F2U.ProcessAttached *)data->Message.Data)->ProcessId;
Console.WriteLine("Incoming request for PID = {0}", pid);
var reply = new IncomingMessagePacketReply();
reply.Header.NtStatus = 0;
reply.Header.MessageId = data->Header.MessageId;
reply.Message.Command = Command.ProcessAttached;
int size= Message.MAX_DATA_SIZE;
var pAttachedReply = ((ProcessAttachedReply *)(&reply.Message.Data[0]));
pAttachedReply->NeedToMonitor=1;
Process process = null;
try {
process = Process.GetProcessById(pid);
Console.WriteLine("Retrieved name by pid: {0}; Managed: ???", process.ProcessName);
} catch(ArgumentException ex)
{
pAttachedReply->NeedToMonitor=0;
}
if(!process.ProcessName.Contains("testredir"))
pAttachedReply->NeedToMonitor = 0;
if(pAttachedReply->NeedToMonitor==1)
{
int pos = 0, index = 0;
Console.WriteLine("Redirections registered: {0}", m_redirections.Count);
foreach(var redirection in m_redirections )
{
Console.WriteLine(" -- Trying to add redirection: \n {0}\n {1}", redirection.Key, redirection.Value);
unchecked {
((ProcessAttachedReply_Positions *)&pAttachedReply->Positions[index])->From = (ushort)pos;
AppendStringToArray(&pAttachedReply->Data, redirection.Key, ref pos);
((ProcessAttachedReply_Positions *)&pAttachedReply->Positions[index])->To = (ushort)pos;
AppendStringToArray(&pAttachedReply->Data, redirection.Value, ref pos);
index++;
}
}
pAttachedReply->PairsCount = (ushort)m_redirections.Count;
}
uint status = WinApi.FilterReplyMessage(m_filterPortHandle, ref reply.Header, (int)size);
Console.WriteLine("Making reply: Command = ProcessAttached, NeedToMonitor = True, Size = {0}, ReplyStat=0x{1:X}", size, status);
}
private unsafe void AppendStringToArray(ushort *arr, string str, ref int position)
{
foreach(var ch in str)
{
arr[position] = ch;
position++;
}
arr[position]=0;
position++;
}
}
Настройка происходит следующим не хитрым образом:
var redir = new RedirectionsManager();
redir.Add(typeof(Environment).Assembly.GetName(), "\\temp\\GAC32\\mscorlib.dll");
Это значит что любое обращение к сборке mscorlib.dll в GAC приведет к редиректу в папку «C:\temp\GAC32\mscorlib.dll» для 32-х разрядной системы. Для 64-х разрядной проделайте тоже самое, но для другой папки.
Модифицируем mscorlib.dll
Для модификации mscorlib можно воспользоваться .Net Reflector + Reflexil, старыми freeware версиями.
В них я заменил System.Environment.get_Version() так, чтобы номер версии был «5.0.40930.0»
Проверка результатов
Для тестирования создадим .Net Framework 4.0 приложение и назовем его testredir, поскольку наш драйвер ожидает именно такое имя процесса.
Текст приложения прост до невозможности
using System;
namespace testredir
{
public static class testredir
{
public static void Main(string[] args)
{
Console.WriteLine("Ver: {0}!", System.Environment.Version);
Console.Write("Press any key to continue . . . ");
Console.ReadKey(true);
}
}
}
Вывод программы:
Ver: 5.0.40930.0!
Заметка:
Изменять код системных библиотек необходимо аккуратно. Например, наш пример валит часть программного обеспечения, поскольку оно проверяет номер версии, на которой запускается. Хорошим примером может послужить создание логгера внутренних событий, чем я сейчас и занимаюсь
Источники:
- http://fsfilters.blogspot.com/
- DDK Examples->mini filters->scanner
- msdn.microsoft.com/en-us/library/windows/desktop/aa365198(v=vs.85).aspx
- www.rsdn.ru/article/baseserv/threadpool.xml