Простой контроль целостности процесса под Windows Суббота, 23. Март 2013 автор: Kaimi http://kaimi.ru/2013/03/simple-integrity-check-windows/ http://kaimi.ru Намедни решил попробовать написать драйвер под Windows. Варианты с "Hello world" показались унылыми, поэтому в качестве тренировки поставил перед собой следющую цель: написать драйвер, который будет контролировать целостность кода процесса по запросу. В общем, драйвер будет считывать данные о загруженных в память секциях, проверять атрибуты и считать простенькую контрольную сумму, а при повторном обращении - сверять её. Совсем детально описывать процесс я не буду, так как в интернете есть куча мануалов по самым основам, да и желающие могут просто посмотреть примеры из WDK, которые достаточно хорошо документированы. Код, описанный ниже, предполагается вызывать в обработчике IOCTL-запросов. Начнем с реализации основной функции, которая будет осуществлять проверку, и нескольких вспомогательных. Code: #include <Ntifs.h> #include <Ntddk.h> #include <ntstrsafe.h> #if 1 #define CKSMLOG DbgPrint #else #define CKSMLOG(...) #endif /* Переменная, которая будет хранить перечень модулей процесса, */ /* их контрольные суммы и некоторые другие данные. */ /* Данные хранятся в двусвязном списке. */ static LIST_ENTRY ChecksumList; /* Функция инициализации вышеописанной переменной */ VOID ChecksumInit() { InitList(&ChecksumList); } /* Очистка её же */ VOID ChecksumFinalize() { CleanList(&ChecksumList); } /* Функция-прослойка, возвращающая результат проверки целостности вызывающего процесса */ BOOLEAN CheckUserProcess() { return ProcessModules(&ChecksumList, (UINT32) PsGetCurrentProcessId()); } Функции работы с двусвязным списком я подробно разбирать не буду, так как логика работы довольно тривиальная и подробно описана в MSDN. Опишу лишь формат, в котором я храню данные о секциях модулей. Code: typedef struct { /* Идентификатор процесса */ UINT32 Pid; /* Адрес, по которому загружен модуль */ PVOID BaseAddr; /* Относительный виртуальный адрес секции */ UINT32 RVA; /* Имя секции */ UINT8 Name[8]; /* CRC секции */ UINT32 CRC32; /* Указатель на следующий элемент списка */ LIST_ENTRY Next; /* Вспомогательная переменная для очистки списка от выгруженных модулей */ UINT32 IsPresent; } SectionList, * pSectionList; Теперь рассмотрим основные функции, реализующие заявленный контроль целостности: Code: BOOLEAN ProcessModules(PLIST_ENTRY Sections, UINT32 Pid) { PPEB Peb; PEPROCESS Pep; PLDR_DATA_TABLE_ENTRY LdrEntry; PLIST_ENTRY Entry; BOOLEAN Status = FALSE, Error; /* Получаем указатель на структуру EPROCESS для заданного PID */ Pep = GetProcessPep(Pid); if(Pep == NULL) return Status; /* Из EPROCESS получаем PEB */ /* (http://en.wikipedia.org/wiki/Process_Environment_Block) */ Peb = GetProcessPeb(Pep); if(Peb == NULL) return Status; CKSMLOG("(%d) PEB: %p", __LINE__, Peb); /* Проходим по списку загруженных в память модулей */ for(Entry = Peb->Ldr->InMemoryOrderModuleList.Flink; &Peb->Ldr->InMemoryOrderModuleList != Entry; Entry = Entry->Flink) { LdrEntry = CONTAINING_RECORD(Entry, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks); /* Обрабатываем секции для каждого именованного модуля */ if(LdrEntry->FullDllName.Buffer != NULL) { CKSMLOG("(%d) %p - %u - %wZ", __LINE__, LdrEntry->DllBase, LdrEntry->SizeOfImage, &LdrEntry->FullDllName); ProcessSections(Sections, Pep, LdrEntry->DllBase, LdrEntry->SizeOfImage, Pid, &Error); if(Error) Status = TRUE; } } /* Удалим из списка все секции, связанные с текущим Pid, которые не были обработаны в цикле выше */ DeleteUnusedEntries(Sections, Pid); /* Сбросим метки присутствия для секций */ ZeroIsPresent(Sections, Pid); return Status; } PEPROCESS GetProcessPep(UINT32 Pid) { PEPROCESS Pep = NULL; NTSTATUS Status = PsLookupProcessByProcessId((HANDLE) Pid, &Pep); if(!NT_SUCCESS(Status)) CKSMLOG("(%d) PsLookupProcessByProcessId (PID=%u; Status=%08X)", __LINE__, Pid, Status); return Pep; } PPEB GetProcessPeb(PEPROCESS Pep) { /* Недокументированная (в MSDN) функция, которая просто возвращает элемент из структуры PEPROCESS */ PPEB Peb = PsGetProcessPeb(Pep); /* MSDN не рекомендует использование этой функции, но пусть будет */ if(!MmIsAddressValid(Peb)) { CKSMLOG("(%d) MmIsAddressValid (Addr=%p)", __LINE__, Peb); return NULL; } return Peb; } И, наконец, здоровенная функция, которая обрабатывает секции модулей: Code: VOID ProcessSections(PLIST_ENTRY Sections, PEPROCESS Pep, PVOID BaseAddr, ULONG ImageSize, UINT32 Pid, PBOOLEAN Error) { /* Указатель на память процесса, которая будет спроецированна в ядро */ PVOID mPtr; UINT32 i, FirstSection, IsMonitored, CRC32; /* http://msdn.microsoft.com/en-us/library/windows/hardware/ff565421%28v=vs.85%29.aspx */ PMDL pMdl = NULL; BOOLEAN IsX64 = FALSE; /* Структуры для разбора PE-файлов, взятые из библиотеки dx'a, но слегка адаптированные под ядро */ /* http://code.google.com/p/portable-executable-library/ */ pimage_dos_header DosHeader; pimage_nt_headers32 NtHeader32; pimage_nt_headers64 NtHeader64; pimage_file_header FileHeader; pimage_section_header SectionHeader; pSectionList SectList; *Error = FALSE; /* Проецируем память процесса в ядро */ mPtr = AllocVaPtr(Pep, BaseAddr, ImageSize, &pMdl); if(mPtr == NULL) { CKSMLOG("(%d) Can't map VA", __LINE__); return; } /* Получаем указатель на DOS-заголовок модуля и проверяем разрядность процесса */ DosHeader = (pimage_dos_header)mPtr; IsX64 = IsX64Process( (pimage_nt_headers32) ((UINT8 *)DosHeader + DosHeader->e_lfanew) ); /* Получаем указатель на FileHeader в зависимости от разрядности */ if(IsX64) { NtHeader64 = (pimage_nt_headers64) ((UINT8 *)DosHeader + DosHeader->e_lfanew); FileHeader = (pimage_file_header)&NtHeader64->FileHeader; } else { NtHeader32 = (pimage_nt_headers32) ((UINT8 *)DosHeader + DosHeader->e_lfanew); FileHeader = (pimage_file_header)&NtHeader32->FileHeader; } /* Получаем указатель на первую секцию */ FirstSection = DosHeader->e_lfanew + FileHeader->SizeOfOptionalHeader + sizeof(image_file_header) + sizeof(UINT32); /* Проходим по всем загруженным секциям модуля */ for(i = 0; i < FileHeader->NumberOfSections; i++) { SectionHeader = (pimage_section_header)((ULONG_PTR)mPtr + FirstSection + (i * sizeof(image_section_header))); /* Нас интересуют только секции с флагами Read, Execute, Contains code, Not Discardable */ IsMonitored = (SectionHeader->Characteristics & image_scn_mem_execute) && (SectionHeader->Characteristics & image_scn_mem_read) && (SectionHeader->Characteristics & image_scn_cnt_code) && !(SectionHeader->Characteristics & image_scn_mem_discardable) ; if(IsMonitored) { /* Вычисляем контрольную сумму секции и проверяем, нет ли записи соответствующей нашей секции в двусвязаном списке */ CRC32 = Crc32Buf((UINT8 *)((ULONG_PTR)mPtr + SectionHeader->VirtualAddress), SectionHeader->Misc.VirtualSize); SectList = FindEntry(Sections, Pid, BaseAddr, SectionHeader->VirtualAddress, SectionHeader->Name); /* Если такой записи нет, то добавим секцию в список */ if(SectList == NULL) { CKSMLOG("(%d) adding entry (PID=%u; BaseAddr=%08X; RVA=%08X; Name=%.8s; CRC32=%08X)", __LINE__, Pid, BaseAddr, SectionHeader->VirtualAddress, SectionHeader->Name, CRC32); AddEntry(sections, Pid, BaseAddr, SectionHeader->VirtualAddress, SectionHeader->Name, CRC32); } /* Если запись есть, то сверим CRC и пометим секцию, как присутствующую в памяти */ else if(SectList->CRC32 == CRC32) { CKSMLOG("(%d) checksum validated (PID=%u; BaseAddr=%08X; RVA=%08X; Name=%.8s; CRC32=%08X)", __LINE__, SectList->Pid, SectList->BaseAddr, SectList->RVA, SectList->Name, SectList->CRC32); SectList->IsPresent = 1; } /* В противном случае пометим секцию, но сообщим об ошибке проверки целостности */ else { CKSMLOG("(%d) erroneous checksum (PID=%u; BaseAddr=%08X; RVA=%08X; Name=%.8s; CRC32=%08X~%08X)", __LINE__, SectList->Pid, SectList->BaseAddr, SectList->RVA, SectList->Name, SectList->CRC32, CRC32); SectList->IsPresent = 1; *Error = TRUE; } } } /* Освободим указатель на user-mode память */ FreeVaPtr(pMdl, BaseAddr); } BOOLEAN IsX64Process(pimage_nt_headers32 NtHeader) { return NtHeader->OptionalHeader.Magic == image_nt_optional_hdr64_magic ? TRUE : FALSE; } /* Проецируем юзермодный виртуальный адрес в ядро */ PVOID AllocVaPtr(PEPROCESS Pep, PVOID VA, ULONG VaSize, PMDL * OutMdl) { KAPC_STATE kAPC; PMDL pMdl; PVOID Ptr = NULL; CKSMLOG("(%d) AllocVaPtr", __LINE__); /* "Подключаем" текущий поток к адресному пространству пользовательского процесса */ KeStackAttachProcess(Pep, &kAPC); /* Получаем указатель на список физических страниц для заданного диапазона виртуальных адресов */ pMdl = IoAllocateMdl(VA, VaSize, FALSE, FALSE, NULL); if(pMdl == NULL) { CKSMLOG("(%d) IoAllocateMdl", __LINE__); KeUnstackDetachProcess(&kAPC); return NULL; } /* Закрепляем страницы в памяти, а то вдруг их кто-нибудь, например, в своп скинуть попытается */ __try { MmProbeAndLockPages(pMdl, UserMode, IoReadAccess); } __except(EXCEPTION_EXECUTE_HANDLER) { CKSMLOG("(%d) MmProbeAndLockPages", __LINE__); IoFreeMdl(pMdl); KeUnstackDetachProcess(&kAPC); return NULL; } /* Проецируем физические страницы в память ядра по произвольному виртуальному адресу */ __try { Ptr = MmMapLockedPagesSpecifyCache(pMdl, UserMode, MmCached, NULL, FALSE, NormalPagePriority); } __except(EXCEPTION_EXECUTE_HANDLER) { CKSMLOG("(%d) MmMapLockedPagesSpecifyCache", __LINE__); MmUnlockPages(pMdl); IoFreeMdl(pMdl); KeUnstackDetachProcess(&kAPC); return NULL; } if(Ptr == NULL) { CKSMLOG("(%d) MmMapLockedPagesSpecifyCache (NULL)", __LINE__); MmUnlockPages(pMdl); IoFreeMdl(pMdl); KeUnstackDetachProcess(&kAPC); return NULL; } /* Отсоединяем текущий поток от пользовательского процесса */ KeUnstackDetachProcess(&kAPC); *OutMdl = pMdl; return Ptr; } /* Освобождаем спроецированную память и дескриптор физических страниц */ VOID FreeVaPtr(PMDL pMdl, PVOID VA) { CKSMLOG("(%d) FreeVaPtr", __LINE__); MmUnmapLockedPages(VA, pMdl); MmUnlockPages(pMdl); IoFreeMdl(pMdl); } Вот мы и рассмотрели основной код, позволяющий осуществить простой контроль целостности секций юзермодного процесса из ядра. Дополнительные процедуры и заголовоные файлы: для работы с двусвязными списками, структуры, описывающие заголовки PE-файла и простой табличный метод расчета CRC приведены в архиве ниже. Теперь возьмем этот код и добавим к какому-нибудь базовому прототипу драйвера. Результат будет выглядеть, например, следующим образом: Code: #include "..\ChecksumLib\ChecksumFunc.h" typedef struct _SampleDeviceExt { PDEVICE_OBJECT Fdo; UNICODE_STRING Symlink; KMUTEX Mutex; } SAMPLE_DEVICE_EXT, *PSAMPLE_DEVICE_EXT; #define DEV_NAME L"SampleDriver" #define SYMLINK_NAME L"\\??\\" DEV_NAME #define IOCTL_SAMPLE CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS) #pragma code_seg("INIT") VOID DrvUnload(PDRIVER_OBJECT pDriverObject); NTSTATUS DispatchRoutine(PDEVICE_OBJECT DeviceObject, PIRP Irp); NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { NTSTATUS Status = STATUS_SUCCESS; PDEVICE_OBJECT Fdo; UNICODE_STRING DevName; PSAMPLE_DEVICE_EXT Pdx; UNICODE_STRING Symlink; UINT16 i = 0; DbgPrint("(%d) DriverEntry", __LINE__); for (i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++) DriverObject->MajorFunction[i] = DispatchRoutine; DriverObject->MajorFunction[i] = NULL; DriverObject->DriverUnload = DrvUnload; RtlInitUnicodeString(&DevName, L"\\Device\\" DEV_NAME); Status = IoCreateDevice ( DriverObject, sizeof(SAMPLE_DEVICE_EXT), &DevName, FILE_DEVICE_UNKNOWN, 0, FALSE, &Fdo ); if(!NT_SUCCESS(Status)) return Status; Pdx = (PSAMPLE_DEVICE_EXT) Fdo->DeviceExtension; Pdx->Fdo = Fdo; DbgPrint("(%d) FDO=%08X, DevExt=%X", __LINE__, Fdo, Pdx); RtlInitUnicodeString(&Symlink, SYMLINK_NAME); Pdx->Symlink = Symlink; Status = IoCreateSymbolicLink(&Symlink, &DevName); if(!NT_SUCCESS(Status)) { IoDeleteDevice(Fdo); return Status; } KeInitializeMutex(&Pdx->Mutex, 0); ChecksumInit(); DbgPrint("(%d) Driver loaded", __LINE__); return Status; } #pragma code_seg() NTSTATUS DispatchRoutine(PDEVICE_OBJECT DeviceObject, PIRP Irp) { PSAMPLE_DEVICE_EXT Pdx; PIO_STACK_LOCATION pIrpStack = IoGetCurrentIrpStackLocation(Irp); Irp->IoStatus.Information = 0; Irp->IoStatus.Status = STATUS_SUCCESS; Pdx = (PSAMPLE_DEVICE_EXT) DeviceObject->DeviceExtension; KeWaitForMutexObject(&Pdx->Mutex, UserRequest, KernelMode, FALSE, NULL); switch (pIrpStack->MajorFunction) { case IRP_MJ_CREATE: DbgPrint("(%d) IRP_MJ_CREATE", __LINE__); break; case IRP_MJ_CLOSE: DbgPrint("(%d) IRP_MJ_CLOSE", __LINE__); break; case IRP_MJ_DEVICE_CONTROL: DbgPrint("(%d) IRP_MJ_DEVICE_CONTROL", __LINE__); switch (pIrpStack->Parameters.DeviceIoControl.IoControlCode) { case IOCTL_SAMPLE: { BOOLEAN Status; UINT32 OutSize = pIrpStack->Parameters.DeviceIoControl.OutputBufferLength; if(OutSize != sizeof(BOOLEAN)) { Irp->IoStatus.Status = STATUS_INVALID_PARAMETER; break; } Status = CheckUserProcess(); RtlCopyMemory(Irp->AssociatedIrp.SystemBuffer, &Status, sizeof(BOOLEAN)); Irp->IoStatus.Information = sizeof(BOOLEAN); } break; } break; default: DbgPrint("(%d) Not implemented major function call", __LINE__); Irp->IoStatus.Status = STATUS_NOT_IMPLEMENTED; break; } KeReleaseMutex(&Pdx->Mutex, FALSE); IoCompleteRequest(Irp, IO_NO_INCREMENT); return Irp->IoStatus.Status; } VOID DrvUnload(PDRIVER_OBJECT pDriverObject) { PDEVICE_OBJECT NextDevObj; int i; DbgPrint("(%d) DrvUnload", __LINE__); NextDevObj = pDriverObject->DeviceObject; for(i = 0; NextDevObj != NULL; i++) { PSAMPLE_DEVICE_EXT Dx = (PSAMPLE_DEVICE_EXT) NextDevObj->DeviceExtension; UNICODE_STRING * Link = &(Dx->Symlink); NextDevObj = NextDevObj->NextDevice; IoDeleteSymbolicLink(Link); IoDeleteDevice(Dx->Fdo); } } Скомпилируем и установим наш драйвер в систему (для этой цели я воспользовался удобной утилитой OSR Driver Loader). Теперь нам необходимо отослать драйверу IOCTL-запрос. Что ж, сделаем небольшую утилиту, которая нам в этом поможет: Code: #include <Windows.h> #include <stdio.h> #define SYMLINK_NAME L"\\\\.\\SampleDriver" #define IOCTL_SAMPLE CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS) int main() { HANDLE DrvHandle = CreateFile ( SYMLINK_NAME, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0 ); if(DrvHandle == INVALID_HANDLE_VALUE) { printf("Can't open handle\n"); return -1; } UCHAR Buffer = 0xFF; DWORD Size; BOOL Status; while(TRUE) { Status = DeviceIoControl(DrvHandle, IOCTL_SAMPLE, &Buffer, sizeof(UCHAR), &Buffer, sizeof(UCHAR), &Size, NULL); if(Status == FALSE) { CloseHandle(DrvHandle); printf("IOCTL failed\n"); return -1; } printf("Driver returned: %d\n", Buffer); Sleep(1000); } return 0; } Компилируем и запускаем нашу утилиту, а также запускаем DebugView, чтобы видеть отладочный вывод драйвера. Как мы видим, драйвер посчитал контрольные суммы интересующих нас секций и добавил в свой внутренний список. Теперь возьмем OllyDbg и изменим произвольный байт в секции кода. Драйвер незамедлительно сообщает о нарушении целостности секции и пишет об этом в логе: Надеюсь, пример окажется кому-нибудь полезен. Исходный код полностью: скачать