ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Windows Minifilter 드라이버에서 발생한 BSOD 0x7F (Double Fault) 커널 스택 오버플로우 분석 및 해결
    윈도우/커널 덤프분석 2025. 12. 22. 00:00
    반응형

    개요

    Windows Minifilter 드라이버를 개발하던 중 특정 조건에서 BSOD(Blue Screen of Death)가 발생하는 심각한 버그를 발견했습니다. 이 글에서는 해당 문제의 원인 분석과 해결 과정을 공유합니다.

     

    증상

    시스템이 갑자기 블루스크린과 함께 크래시되었고, WinDbg를 통해 크래시 덤프를 분석한 결과 다음과 같은 정보를 확인할 수 있었습니다.

    UNEXPECTED_KERNEL_MODE_TRAP (7f)
    This means a trap occurred in kernel mode, and it's a trap of a kind
    that the kernel isn't allowed to have/catch (bound trap) or that
    is always instant death (double fault).
    
    Arguments:
    Arg1: 0000000000000008, EXCEPTION_DOUBLE_FAULT
    Arg2: 0000000080050033
    Arg3: 00000000000000ff
    Arg4: fffff80535ca3da1

    BSOD 0x7F는 커널 모드에서 예상치 못한 트랩이 발생했음을 의미하며, Arg1: 0x08 (EXCEPTION_DOUBLE_FAULT)Double Fault, 즉 예외 처리 중 또 다른 예외가 발생했음을 나타냅니다.

     

    스택 오버플로우 확인

    WinDbg 분석에서 스택 오버플로우가 발생했음이 명확히 드러났습니다:

    STACK_OVERFLOW_ON_CALLBACK:  nt!ExExpandEnvironmentStrings+0x16bb10
    STACK_DEPTH_ENTRIES:  53

    스택 깊이가 53단계로 매우 깊었고, 콜백 중 스택 오버플로우가 발생했습니다.

     

    Call Stack 분석

    크래시 시점의 전체 콜백 스택을 분석해보았습니다:

    fffffe8a`6b2fb060 fffff805`35ca3da1     : nt!ExExpandEnvironmentStrings+0x16bb10
    fffffe8a`6b2fb090 fffff805`35af6aa8     : nt!KiDoubleFaultAbort+0xb1
    fffffe8a`6b2fb130 fffff805`35afd129     : nt!KiPageFault+0x448
    fffffe8a`6b2fb2c8 fffff805`4e0f0e8c     : nt!RtlDecompressBufferEx+0xe9
    fffffe8a`6b2fb320 fffff805`4e0e54d9     : FLTMGR!TreeUnlink+0x7c
    fffffe8a`6b2fb370 fffff805`4e0e6df6     : FLTMGR!FltpSetCancelRoutine+0x39
    fffffe8a`6b2fb3a0 fffff805`4e0f8c9a     : FLTMGR!FltpSendMessageWaiter+0x66
    fffffe8a`6b2fb3f0 fffff805`4e0f8b48     : FLTMGR!FltpSendMessage+0x10e
    fffffe8a`6b2fb4a0 fffff805`5c3f6f57     : FLTMGR!FltSendMessage+0x38
    fffffe8a`6b2fb4f0 fffff805`5c3f7101     : MyDrv!RTSendEventWithQueue+0x97
    fffffe8a`6b2fb560 fffff805`5c3f1a14     : MyDrv!RTSendIoEvent+0x71
    fffffe8a`6b2fb5d0 fffff805`5c3f1ef9     : MyDrv!PreWrite+0x554
    fffffe8a`6b2fb9d0 fffff805`4e0e8ab1     : MyDrv!PreWriteCallback+0x69
    fffffe8a`6b2fba20 fffff805`4e0e89a0     : FLTMGR!FltpPerformPreCallbacks+0x301
    fffffe8a`6b2fbb30 fffff805`4e0da07c     : FLTMGR!FltpPassThroughInternal+0xd0
    fffffe8a`6b2fbb80 fffff805`35dbab95     : FLTMGR!FltpPassThrough+0x16c
    fffffe8a`6b2fbc00 fffff805`361086ae     : nt!IoSynchronousPageWrite+0x1d5
    fffffe8a`6b2fbcc0 fffff805`35d29d53     : nt!MiFlushSectionInternal+0xa0e
    fffffe8a`6b2fc1d0 fffff805`35d1ec1f     : nt!MmFlushSection+0xc3
    fffffe8a`6b2fc280 fffff805`35dcc49b     : nt!CcFlushCachePriv+0x46f
    fffffe8a`6b2fc420 fffff805`5b91d60f     : nt!CcFlushCache+0x3b
    fffffe8a`6b2fc490 fffff805`5b91ccab     : Ntfs!NtfsFlushUserStream+0xbf
    fffffe8a`6b2fc520 fffff805`5b91cb22     : Ntfs!NtfsFlushVolume+0x8b
    fffffe8a`6b2fc560 fffff805`5b9dfa5b     : Ntfs!NtfsCommonFlushBuffers+0x62
    fffffe8a`6b2fc5a0 fffff805`5b89e6a9     : Ntfs!NtfsCheckpointVolume+0x1bb
    fffffe8a`6b2fc7e0 fffff805`5b89e538     : Ntfs!NtfsCheckpointVolumeUntilDone+0x79
    fffffe8a`6b2fc860 fffff805`5b89e1ef     : Ntfs!NtfsCheckpointForLogFileFull+0xb8
    fffffe8a`6b2fc8e0 fffff805`5b89e0f2     : Ntfs!NtfsCheckForLargeLogFile+0xaf
    fffffe8a`6b2fc930 fffff805`5b7c9f1a     : Ntfs!NtfsWriteLog+0xc2
    fffffe8a`6b2fc9b0 fffff805`5b7ca52e     : Ntfs!NtfsSetEndOfFileRecordInformation+0x36
    fffffe8a`6b2fca10 fffff805`5b7c9bfe     : Ntfs!NtfsSetEndOfFileRecord+0x4e
    fffffe8a`6b2fca60 fffff805`5b7a5d2d     : Ntfs!NtfsAddAllocation+0x9a
    fffffe8a`6b2fcab0 fffff805`5b8c4dde     : Ntfs!NtfsCommonWrite+0x167d
    fffffe8a`6b2fce60 fffff805`4e0e8ab1     : Ntfs!NtfsFsdWrite+0x18e
    fffffe8a`6b2fcf00 fffff805`4e0e89a0     : FLTMGR!FltpPerformPreCallbacks+0x301
    fffffe8a`6b2fd010 fffff805`4e0dd91f     : FLTMGR!FltpPassThroughInternal+0xd0
    fffffe8a`6b2fd060 fffff805`35a44af5     : FLTMGR!FltpPassThrough+0x3ff
    fffffe8a`6b2fd0e0 fffff805`35a44697     : nt!IofCallDriver+0x55
    fffffe8a`6b2fd120 fffff805`35a43c3b     : nt!IopSynchronousServiceTail+0x1c7
    fffffe8a`6b2fd1d0 fffff805`35a4344f     : nt!IopWriteFile+0x9fb
    fffffe8a`6b2fd390 fffff805`35af4ae3     : nt!NtWriteFile+0x6f
    fffffe8a`6b2fd420 fffff805`35ae7fe0     : nt!KiSystemServiceCopyEnd+0x43
    fffffe8a`6b2fd628 fffff805`5c405bf4     : nt!KiServiceLinkage
    fffffe8a`6b2fd630 fffff805`5c405988     : MyDrv!RTBackupFile+0x2f4
    fffffe8a`6b2fdb60 fffff805`5c3f32b9     : MyDrv!RTBackupFile+0x88
    fffffe8a`6b2fdc00 fffff805`5c3f4063     : MyDrv!PreCreate+0x449
    fffffe8a`6b2fe0e0 fffff805`4e0e8ab1     : MyDrv!PreCreateCallback+0xf3
    fffffe8a`6b2fe160 fffff805`4e0e89a0     : FLTMGR!FltpPerformPreCallbacks+0x301
    fffffe8a`6b2fe270 fffff805`4e0dd91f     : FLTMGR!FltpPassThroughInternal+0xd0
    fffffe8a`6b2fe2c0 fffff805`4e0e63d6     : FLTMGR!FltpPassThrough+0x3ff
    fffffe8a`6b2fe340 fffff805`4e0e6110     : FLTMGR!FltpCreate+0x316
    fffffe8a`6b2fe3f0 fffff805`35a44af5     : FLTMGR!FltpCreateInternal+0x40
    fffffe8a`6b2fe440 fffff805`35fbf475     : nt!IofCallDriver+0x55
    fffffe8a`6b2fe480 fffff805`35fb0dc7     : nt!IopParseDevice+0x8e5
    fffffe8a`6b2fe660 fffff805`35faffdc     : nt!ObpLookupObjectName+0xa87
    fffffe8a`6b2fe850 fffff805`35ee3a0d     : nt!ObOpenObjectByNameEx+0x1fc
    fffffe8a`6b2fe990 fffff805`35ee1b3e     : nt!IopCreateFile+0x3fd
    fffffe8a`6b2fea40 fffff805`35af4ae3     : nt!NtCreateFile+0x6e
    fffffe8a`6b2fead0 fffff805`35ae7fe0     : nt!KiSystemServiceCopyEnd+0x43

     

    문제의 원인

    Call Stack을 자세히 보면 다음과 같은 재진입(Reentrant) 패턴이 나타납니다:

    사용자 요청: NtCreateFile (파일 열기)
        ↓
    MyDrv!PreCreate (파일 접근 감지)
        ↓
    MyDrv!RTBackupFile (파일 백업 시도)
        ↓
    NtWriteFile (백업 파일 쓰기)
        ↓
    NTFS!NtfsWriteLog → NtfsCheckpointVolume (NTFS 체크포인트 발생)
        ↓
    MmFlushSection → IoSynchronousPageWrite (Paging I/O 발생!)
        ↓
    FLTMGR → MyDrv!PreWrite (Paging I/O로 다시 콜백 진입)
        ↓
    MyDrv!RTSendIoEvent → RTSendEventWithQueue
        ↓
    FLTMGR!FltSendMessage (유저 모드 통신 시도)
        ↓
    💥 스택 오버플로우 → Double Fault → BSOD

     

    핵심 문제점

    1. 깊은 콜 스택: PreCreate에서 백업 작업을 수행하면서 이미 스택이 깊어진 상태
    2. NTFS 체크포인트: 백업 파일 쓰기 중 NTFS가 로그 파일 체크포인트를 트리거
    3. Paging I/O 재진입: 체크포인트 과정에서 Paging I/O가 발생하고, 이로 인해 PreWrite 콜백이 다시 호출됨
    4. FltSendMessage 호출: Paging I/O 컨텍스트에서 FltSendMessage를 호출하려 했으나, 이미 스택 한계에 도달

    Paging I/O는 메모리 관리자가 직접 발생시키는 I/O로, 일반 I/O와 다른 특수한 컨텍스트에서 실행됩니다. 이 상황에서 블로킹 호출인 FltSendMessage를 호출하는 것은 위험합니다.

     

    해결 방법

    1. Paging I/O에서 이벤트 전송 스킵

    Paging I/O 컨텍스트에서는 유저 모드 통신을 수행하지 않도록 수정했습니다:

    // PreWrite 콜백에서
    if (!RTIsMonitoredExtension(&pNameInfo->Extension)) {
        DbgPrint(RT_LOG_PREFIX "PREWRITE_SKIP: NotMonitored pid=%lu ext=%wZ paging=%d\n",
                 ulPid, &pNameInfo->Extension, bPagingIo);
    
        // Paging I/O가 아닐 때만 이벤트 전송
        if (!bPagingIo) {
            RTSendIoEvent(pFilter, pStreamContext->pszFilePath,
                          pStreamContext->ullFileId, ulPid, RT_FILE_OP_WRITE, pNameInfo);
        }
    
        goto Cleanup;
    }
    
    // 첫 수정 이벤트 전송 부분에서도 동일하게 적용
    if (bFirstModify && !bPagingIo) {
        RTSendIoEvent(pFilter, pStreamContext->pszFilePath,
                      pStreamContext->ullFileId, ulPid, RT_FILE_OP_WRITE, pNameInfo);
    }

     

    2. PreSetInfo에도 동일한 보호 적용

    파일 정보 설정 콜백에서도 같은 패턴을 적용했습니다:

    // PreSetInfo 콜백에서
    if (!bPagingIo) {
        RTSendIoEvent(pFilter, wszFilePath, ullFileId, ulPid, RT_FILE_OP_SETINFO, pNameInfo);
    }

     

    3. IRQL 안전성 체크 추가

    추가적인 안전장치로 IRQL 레벨 확인을 추가했습니다:

    VOID RTSendEventWithQueue(_In_ PRT_IO_EVENT Event)
    {
        // IRQL이 APC_LEVEL보다 높으면 FltSendMessage 호출 불가
        if (KeGetCurrentIrql() > APC_LEVEL) {
            DbgPrint(RT_LOG_PREFIX "RTSendEventWithQueue: IRQL too high (%d), queueing only\n",
                     KeGetCurrentIrql());
            (VOID)RtpEnqueueEvent(Event);
            return;
        }
    
        // 정상 처리 계속...
    }

     

    왜 이벤트를 버려도 되는가?

    Paging I/O에서 발생하는 이벤트를 버리는 것이 괜찮은 이유:

    1. 이벤트는 UI 알림 용도: 이벤트는 사용자 인터페이스에 파일 활동을 표시하기 위한 것이며, 핵심 보호 로직과는 분리되어 있습니다.
    2. MutationCount는 별도 관리: 랜섬웨어 탐지에 사용되는 변경 카운트는 이벤트 전송과 별개로 증가합니다.
    3. 백업도 독립적: 파일 복원에 사용되는 백업 파일은 이벤트 전송 여부와 관계없이 정상적으로 생성됩니다.
    4. Paging I/O의 특성: Paging I/O는 시스템이 내부적으로 발생시키는 것으로, 일반적인 사용자 파일 작업과는 다릅니다.

     

    교훈

    1. Minifilter 콜백에서 블로킹 호출 주의: FltSendMessage와 같은 블로킹 함수는 콜백 컨텍스트에서 신중하게 사용해야 합니다.
    2. Paging I/O 특별 처리: Paging I/O 컨텍스트에서는 가능한 최소한의 작업만 수행하고, 유저 모드 통신은 피해야 합니다.
    3. 재진입 시나리오 고려: 백업이나 로깅 같은 작업이 추가 I/O를 발생시킬 수 있고, 이로 인해 콜백이 재진입될 수 있음을 고려해야 합니다.
    4. 스택 깊이 모니터링: 커널 스택은 제한되어 있으므로(일반적으로 24KB), 깊은 콜 체인이 발생할 수 있는 상황을 인지해야 합니다.
    5. IRQL 확인: 커널 모드에서 함수를 호출하기 전에 현재 IRQL이 해당 함수의 요구 사항을 충족하는지 확인해야 합니다.

     

    결론

    Windows Minifilter 드라이버 개발은 다양한 엣지 케이스를 고려해야 하는 복잡한 작업입니다. 특히 Paging I/O, 재진입 콜백, 스택 깊이 등의 요소를 항상 염두에 두어야 합니다. 이번 문제는 테스트 환경에서 발견되어 다행이었지만, 프로덕션 환경에서 발생했다면 심각한 시스템 불안정을 초래할 수 있었습니다.


    이 글이 Windows 드라이버 개발자분들께 도움이 되길 바랍니다.

    반응형

    댓글

Designed by Tistory.