Development/Windows

#1 ProcessNotify - 프로세스, 스레드, 이미지 콜백

geunyeong 2021. 12. 25. 14:22

Table of Contents

    Abstract

    Windows는 프로세스나 스레드의 생성/종료, 이미지 로드 같은 이벤트를 실시간으로 모니터링할 수 있는 기능을 제공한다. 본 글에서는 Windows 시스템에서 발생하는 모든 프로세스, 스레드, 이미지 로드 이벤트를 감시하는 기능의 사용법을 간략하게 알아본다.

    예제 코드는 깃허브에 올려져있다.

    Notifies for Process, Thread and Loaded Image

    Notify for Creating Process

    CreateProcessNotifyRoutine은 프로세스가 생성되거나 종료될 때 호출되는 콜백 함수다. 이 콜백 함수가 호출될 때는 아직 해당 프로세스의 이미지도 올라오지 않은 상태이다. 프로세스 Notify 콜백은 PsSetCreateProcessNotifyRoutine 함수로 등록하고 삭제할 수 있다.

    프로세스 Notify 콜백 등록

    PsSetCreateProcessNotifyRoutine 함수의 두번째 파라메터는 Remove로 FALSE를 넣으면 추가가 되고, TRUE를 넣으면 콜백을 제거한다.

    status = PsSetCreateProcessNotifyRoutine(MyCreateProcessNotifyRoutine,
                                             FALSE);
    IF_ERROR(PsSetCreateProcessNotifyRoutine, EXIT_OF_DRIVER_ENTRY);
    KdPrint(("[procnotify] " __FUNCTION__ " PsSetCreateProcessNotifyRoutine success\n"));

    프로세스 Notify 콜백 삭제

    PsSetCreateProcessNotifyRoutine(MyCreateProcessNotifyRoutine, TRUE);

    프로세스 Notify 콜백 루틴

    PsSetCreateProcessNotifyRoutine로 전달되는 콜백 함수의 원형은 아래와 같다. 첫번째 Argument인 ParentId는 생성 혹은 종료된 프로세스의 부모 프로세스 ID를 의미하고, 두번째 Argument인 ProcessId는 생성 혹은 종료된 프로세스의 ID를 의미한다. 세번째 Create는 이 콜백이 호출될 때 프로세스가 생성되면서 호출된 건지, 종료되면서 호출된 것인지를 나타내는 Bool 값이다. TRUE면 생성, FALSE면 종료될 때 호출된 것이다.

    타입이 HANDLE이라 헷갈릴 수 있는데, 작업 관리자에서 볼 수 있는 정수 형태의 PID들이다. OpenProcess로 얻은 프로세스 핸들이 아닌 정수형태의 PID이기 때문에 이 값을 유저 레벨 어플리케이션으로 전달해서 프로세스 핸들 값으로 사용할 수 없다.

    PCREATE_PROCESS_NOTIFY_ROUTINE PcreateProcessNotifyRoutine;
    
    void PcreateProcessNotifyRoutine(
        [in] HANDLE ParentId,
        [in] HANDLE ProcessId,
        [in] BOOLEAN Create
    );

    본 예제에서는 간단한 디버그 메시지만 출력하도록 작성했다.

    VOID MyCreateProcessNotifyRoutine(
        _In_ HANDLE parentPid,
        _In_ HANDLE processId,
        _In_ BOOLEAN isCreate
    )
    {
        if (isCreate) {
            KdPrint(("[procnotify] " __FUNCTION__ " PPID(%u) created PID(%u)\n",
                     PtrToUint(parentPid),
                     PtrToUint(processId)));
        }
        else {
            KdPrint(("[procnotify] " __FUNCTION__ " PID(%u) was terminated\n",
                     PtrToUint(processId)));
        }
    }

    Notify for Creating Thread

    CreateThreadNotifyRoutine은 새로운 스레드가 생성되거나 기존 스레드가 종료될 때 호출된다. 이는 프로세스마다 가지는 주 스레드 뿐만 아니라 CreateThread, CreateRemoteThread 등 새 스레드를 만드는 함수를 호출해 새로운 스레드가 만들어질 때마다 호출된다.

    스레드 Notify 콜백 등록

    스레드 Notify 콜백은 PsSetCreateThreadNotifyRoutine 함수로 등록할 수 있다.

    status = PsSetCreateThreadNotifyRoutine(MyCreateThreadNotifyRoutine);
    IF_ERROR(PsSetCreateThreadNotifyRoutine, EXIT_OF_DRIVER_ENTRY);
    KdPrint(("[procnotify] " __FUNCTION__ " PsSetCreateThreadNotifyRoutine success\n"));

    스레드 Notify 콜백 삭제

    프로세스 Notify 콜백과 달리 스레드 Notify 콜백은 PsRemoveCreateThreadNotifyRoutine 함수로 삭제한다.

    PsRemoveCreateThreadNotifyRoutine(MyCreateThreadNotifyRoutine);

    스레드 Notify 콜백 루틴

    스레드 Notify 콜백 루틴의 원형은 아래와 같다. ProcessId는 해당 스레드가 생성 혹은 종료된 프로세스의 ID이고 ThreadId는 생성되거나 종료된 스레드의 ID다. Create는 스레드가 생성되면서 호출된 것인지, 종료되면서 호출된 것인지를 나타낸다. 앞서 프로세스 Notify 콜백 루틴에서도 보았듯, 이 값들도 HANDLE 타입이지만 유저 모드 어플리케이션에서 사용하는 핸들이 아니다. 실제 PID와 TID다.

    PCREATE_THREAD_NOTIFY_ROUTINE PcreateThreadNotifyRoutine;
    
    void PcreateThreadNotifyRoutine(
        [in] HANDLE ProcessId,
        [in] HANDLE ThreadId,
        [in] BOOLEAN Create
    );

    본 예제에서는 간단한 디버그 메시지만 출력하도록 했다.

    VOID MyCreateThreadNotifyRoutine(
        _In_ HANDLE processId,
        _In_ HANDLE threadId,
        _In_ BOOLEAN isCreate
    )
    {
        if (isCreate) {
            KdPrint(("[procnotify] " __FUNCTION__ " TID(%u) was created in PID(%u) by PID(%u)\n",
                     PtrToUint(threadId),                  // made thread
                     PtrToUint(processId),                 // process that thread was made
                     PtrToUint(PsGetCurrentProcessId()))); // process that make this thread
        }
        else {
            KdPrint(("[procnotify] " __FUNCTION__ " TID(%u) in PID(%u) was terminated\n",
                     PtrToUint(threadId),
                     PtrToUint(processId)));
         }
    }

    Notify for Loading Image

    이미지 Notify 콜백은 프로세스가 자신의 가상 메모리 영역으로 새로운 이미지(DLL 등)를 로드할 때마다 호출되는 콜백 함수다. 때문에 처음 프로세스가 생성될 때는 프로세스의 IAT 영역에 명시된 모든 DLL에 대해 이미지 Notify 콜백이 호출되고, 런타임 중에 LoadLibrary에 의해 로드되는 DLL도 당연히 이미지 Notify 콜백이 호출되게 된다.

    이미지 Notify 콜백 등록

    이미지 Notify 콜백은 PsSetLoadImageNotifyRoutine으로 등록할 수 있다.

    status = PsSetLoadImageNotifyRoutine(MyLoadImageNotifyRoutine);
    IF_ERROR(PsSetLoadImageNotifyRoutine, EXIT_OF_DRIVER_ENTRY);
    KdPrint(("[procnotify] " __FUNCTION__ " PsSetLoadImageNotifyRoutine success\n"));

    이미지 Notify 콜백 삭제

    앞서 스레드와 마찬가지로 이미지 Notify 콜백 삭제는 함수가 따로 존재한다. PsRemoveLoadImageNotifyRoutine를 이용해 이미지 Notify 콜백을 삭제할 수 있다.

    PsRemoveLoadImageNotifyRoutine(MyLoadImageNotifyRoutine);

    이미지 Notify 콜백 루틴

    이미지 Notify 콜백 루틴의 원형은 아래와 같다. FullImageName에는 로드될 이미지의 전체 경로가 UNICODE_STRING 형태로 저장된다. 경로는 "C:\~"가 아닌 "\Device\HarddiskVolume#\~" 형태다. ProcessId는 로드될 이미지가 위치할 가상 메모리를 가진 프로세스의 ID이고, ImageInfo에는 ImageBase 등 로드될 이미지에 대한 여러 데이터가 들어있다.

    PLOAD_IMAGE_NOTIFY_ROUTINE PloadImageNotifyRoutine;
    
    void PloadImageNotifyRoutine(
        [in, optional] PUNICODE_STRING FullImageName,
        [in]           HANDLE ProcessId,
        [in]           PIMAGE_INFO ImageInfo
    );

    IMAGE_INFO 구조체는 ntddk.h에 정의되어 있다.

    typedef struct _IMAGE_INFO {
        union {
            ULONG Properties;
            struct {
                ULONG ImageAddressingMode  : 8;  // Code addressing mode
                ULONG SystemModeImage      : 1;  // System mode image
                ULONG ImageMappedToAllPids : 1;  // Image mapped into all processes
                ULONG ExtendedInfoPresent  : 1;  // IMAGE_INFO_EX available
                ULONG MachineTypeMismatch  : 1;  // Architecture type mismatch
                ULONG ImageSignatureLevel  : 4;  // Signature level
                ULONG ImageSignatureType   : 3;  // Signature type
                ULONG ImagePartialMap      : 1;  // Nonzero if entire image is not mapped
                ULONG Reserved             : 12;
            };
        };
        PVOID       ImageBase;
        ULONG       ImageSelector;
        SIZE_T      ImageSize;
        ULONG       ImageSectionNumber;
    } IMAGE_INFO, *PIMAGE_INFO;

    본 예제에서는 이미지 경로와 ImageBase 같은 간단한 디버그 메시지만 출력하도록 했다.

    VOID MyLoadImageNotifyRoutine(
        _In_opt_ PUNICODE_STRING imageName,
        _In_ HANDLE processId,
        _In_ PIMAGE_INFO imageInfo
    )
    {
        if (imageName) {
            KdPrint(("[procnotify] " __FUNCTION__ " PID(%u) loaded a/an %wZ at 0x%p\n",
                     PtrToUint(processId),
                     imageName,
                     imageInfo->ImageBase));
        }
    }

    Result

    빌드하고 실행하면 DebugView를 통해 시스템에서 발생하는 모든 프로세스, 스레드 생성/종료 이벤트와 이미지 로드 이벤트를 모니터링할 수 있다. 드라이버를 로드하고 Notepad.exe(1812)를 실행시키면 먼저 프로세스 Notify 콜백 함수가 호출되고, 직후 스레드 Notify 콜백이 호출된다. 뒤이어 Notepad.exe 이미지를 포함해 Notepad.exe의 IAT에 명시된 DLL들이 차례로 로드되는 것도 볼 수 있다.

    Close

    이상으로 Windows 시스템에서 프로세스, 스레드의 생성/종료 이벤트와 이미지 로드 이벤트를 실시간으로 감시할 수 있는 기능의 사용법을 알아봤다. 본 예제에서는 단순히 디버그 메시지를 출력하기만 했으나 엔드 포인트 보안 프로그램을 만드는 데 있어서 주요한 기능으로 사용할 수 있을 것이다.

    Github

    https://github.com/geun-yeong/procnotify-example