Microsoft MVP성태의 닷넷 이야기
DDK: 5. NT Legacy 드라이버: 프로세스(EXE) 생성/제거 모니터링 [링크 복사], [링크+제목 복사],
조회: 32005
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

(시리즈 글이 5개 있습니다.)
DDK: 3. NT Legacy 드라이버를 이용하여 C#에서 Port 입출력
; https://www.sysnet.pe.kr/2/0/932

DDK: 5. NT Legacy 드라이버: 프로세스(EXE) 생성/제거 모니터링
; https://www.sysnet.pe.kr/2/0/937

DDK: 6. ZwTerminateProcess로 프로세스를 종료하는 Device Driver 프로그램
; https://www.sysnet.pe.kr/2/0/2904

DDK: 8. Visual Studio 2019 + WDK Legacy Driver 제작- Hello World 예제
; https://www.sysnet.pe.kr/2/0/12103

DDK: 9. 커널 메모리를 읽고 쓰는 NT Legacy driver와 C# 클라이언트 프로그램
; https://www.sysnet.pe.kr/2/0/12104




NT Legacy 드라이버: 프로세스(EXE) 생성/제거 모니터링

프로세스 생성/소멸에 대한 알림은 사용자 모드 프로그램에서도 쉽게 구현이 가능합니다. 일례로 WMI에서도 알림을 받을 수 있도록 폴링 이벤트를 걸어둘 수도 있고, 그 외에 DLL 후킹을 하거나, Appinit_Dlls로 구현할 수도 있을 것입니다.

그렇지만, 역시 프로그램을 해봐야 좀 실력이 늘기 때문에, 실습 겸 해서 이에 대한 기능을 드라이버 단에서 구현을 해보았습니다. 다행히도 힌트는 다음의 책에서 얻을 수 있었는데요.

Windows 구조와 원리: OS를 관통하는 프로그래밍의 원리 
정덕영 저 | 한빛미디어 
; http://www.yes24.com/24/goods/1956142

187페이지에서 알려준 "PsSetCreateProcessNotifyRoutine"을 이용해서 프로세스(EXE) 생성 또는 제거 시마다 알림을 받을 수 있는 커널 드라이버를 제작해 보았습니다.

지난번, Port IO 예제보다 나름 한 단계 더 나아간 예제인데요. ^^ 다음과 같은 코드들이 더 추가되었습니다.

  • CreateFile 시마다 컨텍스트 정보를 보관할 수 있도록 FileObject 사용
  • LIST_ENTRY / ExAllocatePool / ExFreePool를 이용하여 내부 자료 저장
  • ObReferenceObjectByHandle / ObDereferenceObject 및 KeSetEvent를 이용하여 사용자 모드 프로그램에 이벤트 전달

그 외에, 프로젝트 빌드 스크립트를 변경했습니다. 실제 예제를 작성하다 보니, 지난번에 작성했던 빌드 스크립트들이 터무니 없다는 생각이 들었는데요. 그래서, 이전에 써 놓았던 아래의 글을,

Device Driver 응용 프로그램의 빌드 스크립트
; https://www.sysnet.pe.kr/2/0/921

보완하는 내용을 실었습니다.

Device Driver 응용 프로그램의 빌드 스크립트 - 두 번째 이야기
; https://www.sysnet.pe.kr/2/0/936

충분히 현실적으로 사용할 수 있도록! ^^




코드를 잠깐 살펴보면, DriverEntry에서는 PsSetCreateProcessNotifyRoutine 호출과 FileObject 컨텍스트 정보를 연결해 두기 위한 LIST_ENTRY를 초기화합니다.

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegistryPath)
{
    PsSetCreateProcessNotifyRoutine(ProcessCreateCallback, FALSE); // IRQL: PASSIVE_LEVEL
    InitializeListHead(&g_processListHead); // IRQL: Any level

    ... [생략: 나머지 코드는 이전의 Port IO 예제와 유사]...

    return STATUS_SUCCESS;
}

DriverUnload에서는 당연히 이때 설정한 정보를 해제하고,

VOID DriverUnload( PDRIVER_OBJECT pDriverObject )
{
    PsSetCreateProcessNotifyRoutine(ProcessCreateCallback, TRUE);
    ... [생략]...
}

IRP_MJ_CREATE Dispatch 메서드에서는 FileObject 컨텍스트 정보에 KEVENT 개체와 생성/소멸되는 프로세스 정보를 연결 리스트로 보관할 수 있도록 메모리를 할당하고 초기화합니다.

NTSTATUS MajorCreate(IN PDEVICE_OBJECT pDeviceObject, IN PIRP pIrp)
{
    PIO_STACK_LOCATION pStack = ::IoGetCurrentIrpStackLocation(pIrp); // IRQL: Any level
    PFILE_OBJECT pFileObject = pStack->FileObject;

    pFileObject->FsContext = ExAllocatePool(NonPagedPool, sizeof(ProcessListEntry)); // IRQL: <= DISPATCH_LEVEL
    ProcessListEntry *pProcessEntry = (ProcessListEntry *)(pFileObject->FsContext);
    pProcessEntry->pEvent = NULL;
    ::InitializeListHead(&(pProcessEntry->listHead));

    InsertHeadList(&g_processListHead, pProcessEntry);  // IRQL: Any level

    ... [생략]...
    return STATUS_SUCCESS;
}

쌍을 이루는 IRP_MJ_CLOSE Dispatch 메서드에서는 Create에서 생성했던 문맥 정보와, 그 안에 보관되어 있던 프로세스 생성/소멸 정보에 할당된 메모리를 해제합니다.

NTSTATUS MajorClose(IN PDEVICE_OBJECT pDeviceObject, IN PIRP pIrp)
{
    PIO_STACK_LOCATION pStack = ::IoGetCurrentIrpStackLocation(pIrp);
    PFILE_OBJECT pFileObject = pStack->FileObject;

    ProcessListEntry *pProcessEntry = (ProcessListEntry *)(pFileObject->FsContext);

    if (pProcessEntry->pEvent != NULL) // 파일 문맥 정보에 담긴 이벤트 개체에 대한 참조 횟수를 감소
    {
        ObDereferenceObject(pProcessEntry->pEvent);  // IRQL: <= DISPATCH_LEVEL
        pProcessEntry->pEvent = NULL;
    }

    // 응용 프로그램이 가져가지 못해 보관된 프로세스 생성/소멸 정보를 삭제
    while (!IsListEmpty(&pProcessEntry->listHead))  // IRQL: Any level
    {
        ProcessItemListEntry *pItem = (ProcessItemListEntry *)::RemoveHeadList(&pProcessEntry->listHead);
        ExFreePool(pItem);  // IRQL: <= DISPATCH_LEVEL
    }

    RemoveEntryList(pProcessEntry);  // IRQL: Any level
    ExFreePool(pProcessEntry);

    ... [생략]...
    return ntStatus;
}

이제 프로세스 생성/소멸 시마다 불려지는 콜백 메서드의 구현입니다.

VOID ProcessCreateCallback(IN HANDLE parentId, IN HANDLE processId, IN BOOLEAN Create)
{
    PEPROCESS pParentProcess, pChildProcess;

    PsLookupProcessByProcessId(parentId, &pParentProcess);  // IRQL: <= APC_LEVEL
    PsLookupProcessByProcessId(processId, &pChildProcess);

    PLIST_ENTRY pListPtr, pListHead;
    pListPtr = pListHead = &g_processListHead;

    // CreateFile.FileObject마다 새롭게 전달된 프로세스 정보를 넣어둔다.
    while (pListPtr->Flink != pListHead)
    {
        pListPtr = pListPtr->Flink;

        int entrySize = sizeof(ProcessItemListEntry);
        ProcessItemListEntry *pItem = (ProcessItemListEntry *)ExAllocatePool(NonPagedPool, entrySize);
        ...[생략: 프로세스 정보 구조체의 내용을 채운다.]...
        RtlStringCbCopyA(pItem->ChildName, ..., pChildProcess + PSOFFSET_IMAGEFILENAME); // IRQL: PASSIVE_LEVEL

        ProcessListEntry *pEntry = (ProcessListEntry *)pListPtr;
        ::InsertHeadList(&pEntry->listHead, pItem);
    }

    // 이벤트 Set
    pListPtr = pListHead = &g_processListHead;
    while (pListPtr->Flink != pListHead)
    {
        pListPtr = pListPtr->Flink;

        ProcessListEntry *pEntry = (ProcessListEntry *)pListPtr;
        ::KeSetEvent(pEntry->pEvent, 0, FALSE);  // Wait == FALSE, IRQL <= DISPATCH_LEVEL
    }
}

IRP_MJ_DEVICE_CONTROL Dispatch 메서드에서는 IOCTL_SET_EVENT_HANDLE, IOCTL_GET_PROCESS_ENTRY 2가지의 명령어를 제공합니다. IOCTL_SET_EVENT_HANDLE에서는 사용자 모드의 Win32 EVENT 개체를 전달받아서 ObReferenceObjectByHandle 함수를 이용하여 커널모드로 변환하여, 이후에 ProcessCreateCallback 메서드가 불릴 때마다 사용자 모드 응용 프로그램에 이 사실을 알리기 위해 보관해 둡니다.

case IOCTL_SET_EVENT_HANDLE:
    {
        ProcessListEntry *pProcessEntry = (ProcessListEntry *)irpSp->FileObject->FsContext;

        HANDLE userEventHandle = *(HANDLE *)pIrp->AssociatedIrp.SystemBuffer;
        ntStatus = ObReferenceObjectByHandle(userEventHandle, EVENT_ALL_ACCESS, *ExEventObjectType, UserMode,
            (PVOID *)&(pProcessEntry->pEvent), NULL);  // IRQL: PASSIVE_LEVEL
    }
    pIrp->IoStatus.Information = 0;
    ntStatus = STATUS_SUCCESS;
    break;

IRP_MJ_DEVICE_CONTROL Dispatch 메서드 자체는 IRQL <= DISPATCH_LEVEL에서 호출될 수 있지만, 여기서는 당연히 IOCTL_SET_EVENT_HANDLE 명령어가 사용자 모드에서 전달하는 것이므로, IRQL == PASSIVE_LEVEL에서만 호출 가능한 ObReferenceObjectByHandle 함수를 사용했습니다.

위와 같이 사용자 모드에서 Win32 EVENT 개체를 받아서, 이후에 프로세스 생성/소멸 시점이 되면 이벤트 개체를 Signal 상태로 만들어 사용자 모드의 대기 상태를 해제합니다. 활성화된 응용 프로그램은 다시 IOCTL_GET_PROCESS_ENTRY 명령어를 이용하여 프로세스 생성/소멸 정보를 한 번에 하나씩 가져갈 수 있습니다.

case IOCTL_GET_PROCESS_ENTRY:
    {
        // 한 번 호출할 때마다 하나의 목록을 가져간다.
        ProcessListEntry *pProcessEntry = (ProcessListEntry *)irpSp->FileObject->FsContext;
        LIST_ENTRY *pList = (LIST_ENTRY *)&pProcessEntry->listHead;

        BOOLEAN isEmpty = IsListEmpty(pList);
        int entrySize = sizeof(ProcessItemListEntry) - sizeof(LIST_ENTRY);

        if (isEmpty == FALSE)
        {
            // 버퍼의 크기가 entrySize 크기보다 커야 함.
            if (outBufLength < entrySize)
            {
                ntStatus = STATUS_BUFFER_TOO_SMALL;
                pIrp->IoStatus.Information = 0;
            }
            else
            {
                ProcessItemListEntry *pItem = (ProcessItemListEntry *)::RemoveTailList(pList);  // IRQL: Any level 
                memcpy(ioBuffer, &pItem->cbSize, entrySize);
                pIrp->IoStatus.Information = entrySize;
                ::ExFreePool(pItem);
            }
        }
    }

개인적으로, 위에서 궁금한 점이 하나 있는데요. 보통 버퍼 용량이 모자라면 반환되는 ntStatus 값을 STATUS_BUFFER_TOO_SMALL로 설정하는 경우 Win32 응용 프로그램에서 GetLastError() 함수의 반환값으로 122(The data area passed to a system call is too small.) 오류 코드가 담깁니다. 그런데, 윈도우즈에서 제공되는 메서드들은 그런 경우에 out 파라미터로 필요한 버퍼 용량을 넘겨주게 되는데, 위에서 그 처리를 어떻게 할 수 있는지 궁금합니다. pIrp->IoStatus.Information에 필요한 버퍼 바이트 용량을 채워서 반환해도 Win32 응용 프로그램에서는 여전히 0 값만이 나오는데요... 음... 혹시 방법을 아시는 분은 댓글 부탁드립니다. ^^

자, 이렇게 해서 커널 드라이버 구현은 완료되었고, C# 응용 프로그램에서는 Port I/O 예제에서 했던 것처럼 손쉽게 Device Driver와 통신을 해서 프로세스 생성/소멸 정보를 받아보게 됩니다.

아래는 실제로 실행된 화면입니다.

process_event_ddk_sample_1.png

보시는 것처럼 탐색기에서 메모장을 실행한 후, 종료한 것입니다.




참고로, 이 예제에서는 어떠한 동기화 개체도 사용하지 않았습니다. 공부가 목적이었기 때문에 너무 자세히 구현하려니 시간이 너무 소모되어서 그냥 동기화는 포기했습니다. 그리고, 테스트는 Windows 2003 x86, Windows 2008 R2에서만 해봤는데 일단 잠시 동안의 테스트였지만 블루 스크린 없이 잘 실행이 되었습니다. ^^v

첨부된 파일은 C# 사용자 프로그램과 VC++ 드라이버 소스입니다. 혹시나 블루스크린이 뜰 만한 여지가 있는 곳을 발견하시면 지적을 부탁드립니다. ^^



[이 토픽에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 6/27/2021]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2011-03-30 01시45분
유저 레벨과의 통신을 이벤트가 아닌 APC로 하는 방법을 아래의 글에서 소개해 놓았네요. ^^ 언제 시간 나면 한번 바꿔봐야겠습니다.

APC를 통해 커널레벨 코드에서 유저레벨 코드로 콜백 흉내내기
; http://blog.naver.com/gloryo/110105853413
; https://web.archive.org/web/20230404004205/https://blog.naver.com/gloryo/110105853413
정성태
2015-04-17 01시07분
정성태
2023-04-04 09시34분
정성태

... 91  92  93  94  95  96  97  98  99  100  [101]  102  103  104  105  ...
NoWriterDateCnt.TitleFile(s)
11438정성태1/17/201820176오류 유형: 446. Error CS0234 The type or namespace name 'ITuple' does not exist in the namespace
11437정성태1/17/201819717VS.NET IDE: 124. Platform Toolset 설정에 따른 Visual C++의 헤더 파일 기본 디렉터리
11436정성태1/16/201822137개발 환경 구성: 352. ASP.NET Core (EXE) 프로세스가 IIS에서 호스팅되는 방법 - ASP.NET Core Module(AspNetCoreModule) [4]
11435정성태1/16/201823238개발 환경 구성: 351. OWIN 웹 서버(EXE)를 IIS에서 호스팅하는 방법 - HttpPlatformHandler (Reverse Proxy)파일 다운로드2
11434정성태1/15/201823885개발 환경 구성: 350. 사용자 정의 웹 서버(EXE)를 IIS에서 호스팅하는 방법 - HttpPlatformHandler (Reverse Proxy)파일 다운로드2
11433정성태1/15/201822033개발 환경 구성: 349. dotnet ef 명령어 사용을 위한 준비
11432정성태1/11/201828451.NET Framework: 726. WPF + Direct2D + SharpDX 출력 C# 예제파일 다운로드2
11431정성태1/11/201825581.NET Framework: 725. C# - 동기 방식이면서 비동기 메서드(awaitable)처럼 구현한 사례 [9]
11430정성태1/10/201829076.NET Framework: 724. WPF + Direct2D 출력 C# 예제 [2]파일 다운로드1
11429정성태1/9/201819647개발 환경 구성: 348. ASP.NET Core 2.1 Preview 버전 적용 방법
11428정성태1/6/201822857개발 환경 구성: 347. WinForm 프로젝트를 WPF 프로젝트 유형으로 변경하는 방법파일 다운로드1
11427정성태1/5/201820198오류 유형: 445. vcpkg 빌드 오류 - Starting the CLR failed with HRESULT 80040153
11426정성태1/5/201830421오류 유형: 444. curl로 호출할 때 발생하는 오류 정리
11425정성태1/4/201821057개발 환경 구성: 346. ASP.NET Core Web Application을 IIS에서 호스팅하는 방법 (2)
11424정성태1/4/201820481개발 환경 구성: 345. ASP.NET Core 프로젝트를 명령행에서 빌드하는 방법
11423정성태1/3/201838944VC++: 123. 내가 만든 코드보다 OpenCV의 속도가 월등히 빠른 이유 [8]파일 다운로드2
11422정성태1/2/201829053.NET Framework: 723. C# - OpenCvSharp 사용 시 C/C++을 이용한 속도 향상 (for 루프 연산) [4]파일 다운로드1
11421정성태1/2/201821287오류 유형: 443. Visual Studio - nuget configuration is invalid
11420정성태12/30/201725376.NET Framework: 722. C# - Windows 10 운영체제의 데스크톱 앱에서 음성인식(SpeechRecognizer) 사용하는 방법 [3]파일 다운로드1
11419정성태12/23/201727543.NET Framework: 721. WebClient 타입의 ...Async 메서드 호출은 왜 await + 동기 호출 시 hang 현상이 발생할까요? [2]파일 다운로드1
11418정성태12/23/201737248.NET Framework: 720. 비동기 메서드 내에서 await 시 ConfigureAwait 호출 의미 [2]파일 다운로드1
11417정성태12/22/201723186.NET Framework: 719. Task를 포함하는 async 메서드의 동작 방식 [2]
11416정성태12/21/201720295.NET Framework: 718. AsyncTaskMethodBuilder.Create() 메서드 동작 방식 [2]
11415정성태12/21/201722163.NET Framework: 717. Task를 포함하지 않는 async 메서드의 동작 방식 [6]
11414정성태12/21/201729223.NET Framework: 716. async 메서드의 void 반환 타입 사용에 대하여파일 다운로드2
11413정성태12/20/201724029개발 환경 구성: 344. 윈도우 10 - TTS 및 음성 인식을 위한 환경 설정
... 91  92  93  94  95  96  97  98  99  100  [101]  102  103  104  105  ...