Финансы Сайт на котором знают все про финансы

Простой контроль целостности процесса под Windows

Намедни решил попробовать написать драйвер под Windows. Варианты с «Hello world» показались унылыми, поэтому в качестве тренировки поставил перед собой следющую цель: написать драйвер, который будет контролировать целостность кода процесса по запросу. В общем, драйвер будет считывать данные о загруженных в память секциях, проверять атрибуты и считать простенькую контрольную сумму, а при повторном обращении — сверять её. Совсем детально описывать процесс я не буду, так как в интернете есть куча мануалов по самым основам, да и желающие могут просто посмотреть примеры из WDK, которые достаточно хорошо документированы.

Код, описанный ниже, предполагается вызывать в обработчике IOCTL-запросов. Начнем с реализации основной функции, которая будет осуществлять проверку, и нескольких вспомогательных.

#include 
#include 
#include 

#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. Опишу лишь формат, в котором я храню данные о секциях модулей.

typedef struct
{
    /* Идентификатор процесса */
    UINT32 Pid;
    /* Адрес, по которому загружен модуль */
    PVOID BaseAddr;
    /* Относительный виртуальный адрес секции */
    UINT32 RVA;
    /* Имя секции */
    UINT8 Name[8];
    /* CRC секции */
    UINT32 CRC32;
    /* Указатель на следующий элемент списка */
    LIST_ENTRY Next;
    /* Вспомогательная переменная для очистки списка от выгруженных модулей */
    UINT32 IsPresent;
} SectionList, * pSectionList;

Теперь рассмотрим основные функции, реализующие заявленный контроль целостности:

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;
}

И, наконец, здоровенная функция, которая обрабатывает секции модулей:

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 приведены в архиве ниже.

Теперь возьмем этот код и добавим к какому-нибудь базовому прототипу драйвера. Результат будет выглядеть, например, следующим образом:

#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-запрос. Что ж, сделаем небольшую утилиту, которая нам в этом поможет:

#include 
#include 

#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 и изменим произвольный байт в секции кода. Драйвер незамедлительно сообщает о нарушении целостности секции и пишет об этом в логе:

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

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