Новости из Блогов Простой контроль целостности процесса под Windows

Discussion in 'Мировые новости. Обсуждения.' started by Suicide, 24 Mar 2013.

  1. Suicide

    Suicide Super Moderator
    Staff Member

    Joined:
    24 Apr 2009
    Messages:
    2,482
    Likes Received:
    7,062
    Reputations:
    693
    Простой контроль целостности процесса под 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, чтобы видеть отладочный вывод драйвера.

    [​IMG]

    Как мы видим, драйвер посчитал контрольные суммы интересующих нас секций и добавил в свой внутренний список. Теперь возьмем OllyDbg и изменим произвольный байт в секции кода. Драйвер незамедлительно сообщает о нарушении целостности секции и пишет об этом в логе:

    [​IMG]

    Надеюсь, пример окажется кому-нибудь полезен.

    Исходный код полностью: скачать
     
    _________________________
  2. Zory

    Zory Banned

    Joined:
    19 Feb 2013
    Messages:
    2
    Likes Received:
    0
    Reputations:
    0
    Спасибо, полезная информация.
     
Loading...