Development/Windows

#2 MiniFilter - 미니 필터 만들기

geunyeong 2021. 11. 23. 23:18

Table of Contents

    Abstract

    앞서 미니 필터에 대해서 간략하게 알아봤다. 이번 글에서는 미니 필터를 작성하기 위한 최소한의 인사이트를 정리해보고자 한다. 본 글은 Visual Studio 2019를 사용해 프로젝트를 생성하는 것부터, 드라이버를 빌드하고 가상머신에서 테스트하는 과정을 담고 있다. 깃헙에 올린 미니 필터 예제를 보며 읽으면 큰 도움이 될 것이다.

    Develop the MiniFilter

    개발 환경은 Windows 11 Pro 21H2 64bit, IDE는 Visual Studio 2019, WDK(Windows Driver Kit)는 10.0.22000.1을 사용했다. 개발한 드라이버를 테스트한 가상 머신은 Windows 10 Pro 21H2 64bit다.

    Create the Windows Driver Project

    Visual Studio를 설치하고 WDK를 설치하면 Visual Studio 프로젝트 선택 항목에 Driver 타입이 나타난다. 이 중 Empty WDM Driver를 선택하고 프로젝트를 생성하면 된다. 아무런 포맷도 주어지지 않는 커널 드라이버 프로젝트를 만드는 템플릿이다. 만약 드라이버 관련 프로젝트 템플릿이 보이지 않는다면 WDK를 다시 설치해보길 바란다.

    Empty WDM Driver

    Write the Operation

    FLT_OPERATION_REGISTRATION

    I/O에서 각 Operation은 두가지 종류로 나뉜다. 요청이 디바이스 스택을 따라 외부 디바이스까지 전달되기 전을 Pre Operation, 전달되고 난 후 유저 모드 어플리케이션으로 돌아올 때는 Post Operation이라고 한다.

    콜백 루틴을 작성하기 위해서는 먼저 FLT_OPERATION_REGISTRATION 구조체 배열 변수를 선언해야 한다.

    //
    //  Structure used for registering operation callback routines
    //
    
    typedef struct _FLT_OPERATION_REGISTRATION {
    
        UCHAR MajorFunction;
        FLT_OPERATION_REGISTRATION_FLAGS Flags;
        PFLT_PRE_OPERATION_CALLBACK PreOperation;
        PFLT_POST_OPERATION_CALLBACK PostOperation;
    
        PVOID Reserved1;
    
    } FLT_OPERATION_REGISTRATION, *PFLT_OPERATION_REGISTRATION;
    FLT_OPERATION_REGISTRATION operations[] = {
    	{
    		IRP_MJ_CREATE,
    		0,
    		MinifltExampleCreatePreRoutine,
    		MinifltExampleCreatePostRoutine,
    		NULL
    	},
    	{
    		IRP_MJ_OPERATION_END
    	}
    };

    FLT_OPERATION_REGISTRATION은 작업(생성, 읽기, 쓰기 등)별 콜백 루틴을 필터 관리자에게 등록하기 위해 작업별로 콜백 루틴을 묶어주는 구조체다. 때문에  IRP Major Function Code를 담는 MajorFunction 멤버와 실제 콜백 루틴을 담는 PreOperation, PostOperation 멤버로 이루어져 있다.

    FLT_OPERATION_REGISTRATION 구조체 배열을 만들고 위와 같이 적으면 IRP_MJ_CREATE 작업 요청이 들어올 때 필터 관리자가 Pre-Operation 상태에서는 MinifltExampleCreatePreRoutine 함수를 호출하고, Post-Operation 상태에서는 MinifltExampleCreatePostRouinte을 호출한다. 구조체 배열의 끝은 IRP_MJ_OPERATION_END를 넣는 것으로 나타낸다.

    Write Operations

    이제 Pre-Operation과 Post-Operation 루틴을 작성한다.

    FLT_PREOP_CALLBACK_STATUS 
    MinifltExampleCreatePreRoutine(
    	_Inout_ PFLT_CALLBACK_DATA data,
    	_In_    PCFLT_RELATED_OBJECTS flt_object,
    	_Out_   PVOID* completion_context
    )
    {
    	UNREFERENCED_PARAMETER(data);
    	UNREFERENCED_PARAMETER(completion_context);
    
    	if (flt_object && flt_object->FileObject && flt_object->FileObject->FileName.Buffer) {
    		DbgPrint(
    			"[miniflt] " __FUNCTION__ "  [%u] Start to creat/open a file (%wZ)\n",
    			PtrToUint(PsGetCurrentProcessId()),
    			&(flt_object->FileObject->FileName)
    		);
    	}
    	
    	return FLT_PREOP_SUCCESS_WITH_CALLBACK;
    }
    
    
    
    FLT_POSTOP_CALLBACK_STATUS 
    MinifltExampleCreatePostRoutine(
    	_Inout_      PFLT_CALLBACK_DATA data,
    	_In_         PCFLT_RELATED_OBJECTS flt_object,
    	_In_opt_     PVOID completion_context,
    	_In_         FLT_POST_OPERATION_FLAGS flags
    )
    {
    	UNREFERENCED_PARAMETER(data);
    	UNREFERENCED_PARAMETER(completion_context);
    	UNREFERENCED_PARAMETER(flags);
    
    	if (flt_object && flt_object->FileObject && flt_object->FileObject->FileName.Buffer) {
    		DbgPrint(
    			"[miniflt] " __FUNCTION__ " [%u] Complete to creat/open a file (%wZ)\n",
    			PtrToUint(PsGetCurrentProcessId()),
    			&(flt_object->FileObject->FileName)
    		);
    	}
    
    	return FLT_POSTOP_FINISHED_PROCESSING;
    }

    본 예제에서는 간단하게 파일에 접근하는 어플리케이션의 PID와 접근하는 파일의 경로를 DbgPrint로 출력하도록 작성했다.

    flt_object에는 작업의 대상인 파일과 관련한 데이터가 존재한다. Windows의 커널에서 모든 문자열은 UNICODE_STRING이라는 구조체로 존재하며, DbgPrint의 %wZ를 이용하면 UNICODE_STRING 문자열을 출력할 수 있다. Pre-Operation에서 무언가를 기억하고 Post-Operation에서 이를 확인해 처리하고 싶다면 동적으로 메모리를 할당한 후 Pre-Operation의 completion_context에 넣으면 된다. Pre-Operation 과정이 모두 끝나고 Post-Operation이 호출될 때 해당 Context 데이터를 Post-Operation의 completion_context로 다시 전달해준다.

    리턴 값은 Pre-Operation의 경우 FLT_PREOP_SUCCESS_WITH_CALLBACK를, Post-Operation의 경우 FLT_POSTOP_FINISHED_PROCESSING을 반환하면 된다. FLT_PREOP_SUCCESS_WITH_CALLBACK은 작업을 마치고 다음 미니 필터 드라이버나 파일 시스템 드라이버에게 I/O 요청 작업을 넘기겠다는 의미다. 만약 파일 접근을 차단하고 싶다면 리턴 전에 data의 IoStatus에 STATUS_ACCESS_DENIED와 같은 NTSTATUS 에러 코드를 넣으면 된다.

    data->IoStatus.Status = STATUS_ACCESS_DENIED;
    return FLT_PREOP_COMPLETE;

    추가로 Pre-Operation에서 FLT_PREOP_COMPLETE를 반환하면 I/O 작업 자체가 미니 필터 선에서 끝나버리므로 주의해야 한다. 이는 하위 NTFS.sys 같은 파일 시스템 드라이버를 포함해, 볼륨 드라이버 및 디스크 드라이버까지 I/O 요청이 전달되지 않는다는 것을 의미한다. 만약 STATUS_ACCESS_DENIED로 작업을 마치고자 한다면 FLT_PREOP_COMPLETE를 반환해야 한다.

    Write the MiniFilter Unload Routine

    필터 관리자에 내 미니 필터를 등록할 때 미니 필터 언로드 루틴도 필요하다. 미니 필터 언로드 루틴은 미니 필터가 언로드될 때 불리는 콜백 함수다. 함수 원형은 fltkernel.h에 정의되어 있으며, 아래와 같다.

    typedef NTSTATUS (FLTAPI *PFLT_FILTER_UNLOAD_CALLBACK) (
        FLT_FILTER_UNLOAD_FLAGS Flags
    );

    미니 필터를 만들 때 특별한 초기화 작업을 하지 않았다면 해줄 일은 크게 없다. 만약 Context같이 동적 메모리를 할당했거나 리스트, 큐 같은 자료 구조를 만들었었다면 이를 Cleanup하는 코드를 넣어주면 된다. 본 예제에서는 아래와 같이 미니 필터를 등록 해제하는 코드를 작성했다.

    NTSTATUS
    MinifltExampleFilterUnloadRoutine(
    	_In_ FLT_FILTER_UNLOAD_FLAGS flags
    )
    {
    	UNREFERENCED_PARAMETER(flags);
    
    	if (flt_handle) {
    		FltUnregisterFilter(flt_handle);
    	}
    
    	return STATUS_SUCCESS;
    }

    Register the MiniFilter

    앞서 작성한 Operation Routine들을 필터 관리자에게 등록해야 한다. 필터 관리자에게 등록하기 위해서는 FLT_REGISTRATION이라는 구조체 변수를 선언하고 값을 채워야 한다. FLT_REGISTRATION은 fltkernel.h에 정의되어 있다.

    //
    //  Registration structure
    //
    
    typedef struct _FLT_REGISTRATION {
    
        USHORT Size;
        USHORT Version;
        FLT_REGISTRATION_FLAGS Flags;
        CONST FLT_CONTEXT_REGISTRATION *ContextRegistration;
    
        //
        //  Variable length array of routines used for processing pre- and post-
        //  file system operations.
        //
    
        CONST FLT_OPERATION_REGISTRATION *OperationRegistration;
    
        //
        //  This is called before a filter is unloaded.  If an ERROR or WARNING
        //  status is returned then the filter is NOT unloaded.  A mandatory unload
        //  can not be failed.
        //
        //  If a NULL is specified for this routine, then the filter can never be
        //  unloaded.
        //
    
        PFLT_FILTER_UNLOAD_CALLBACK FilterUnloadCallback;
    
        PFLT_INSTANCE_SETUP_CALLBACK InstanceSetupCallback;
        PFLT_INSTANCE_QUERY_TEARDOWN_CALLBACK InstanceQueryTeardownCallback;
        PFLT_INSTANCE_TEARDOWN_CALLBACK InstanceTeardownStartCallback;
        PFLT_INSTANCE_TEARDOWN_CALLBACK InstanceTeardownCompleteCallback;
        PFLT_GENERATE_FILE_NAME GenerateFileNameCallback;
        PFLT_NORMALIZE_NAME_COMPONENT NormalizeNameComponentCallback;
        PFLT_NORMALIZE_CONTEXT_CLEANUP NormalizeContextCleanupCallback;
    
    #if FLT_MGR_LONGHORN
        PFLT_TRANSACTION_NOTIFICATION_CALLBACK TransactionNotificationCallback;
        PFLT_NORMALIZE_NAME_COMPONENT_EX NormalizeNameComponentExCallback;
    #endif // FLT_MGR_LONGHORN
    
    #if FLT_MGR_WIN8
        PFLT_SECTION_CONFLICT_NOTIFICATION_CALLBACK SectionNotificationCallback;
    #endif // FLT_MGR_WIN8
    
    } FLT_REGISTRATION, *PFLT_REGISTRATION;

    주요 멤버는 아래와 같다.

    • Size: FLT_REGISTRATION의 크기이다. sizeof 키워드를 사용해 FLT_REGISTRATION 구조체의 크기를 넣어주면 된다.
    • Version: FLT_REGISTRATION 구조체의 버전이다. FLT_REGISTRATION_VERSION를 설정해주면 된다.
    • OperationRegistration: 앞서 작성한 Operation들이 저장된 FLT_OPERATION_REGISTRATION 구조체 변수의 주소를 넣어주면 된다.
    • FilterUnloadCallback: 미니 필터가 언로드될 때 호출될 콜백 함수다. 

    본 예제에서는 아래와 같이 작성했다.

    FLT_REGISTRATION registration = {
    	sizeof(registration),              // size
    	FLT_REGISTRATION_VERSION,          // version
    	0,                                 // flags
    	NULL,                              // context registration
    	operations,                        // operation registration
    	MinifltExampleFilterUnloadRoutine, // filter unload callback
    	NULL,                              // instance setup callback
    	NULL,                              // instance query teardown callback
    	NULL,                              // instance teardown start callback
    	NULL,                              // instance teardown complete callback
    	NULL,                              // generate file name callback
    	NULL,                              // normalize name component callback
    	NULL,                              // normalize context cleanup callback
    	NULL,                              // transaction notification callback
    	NULL,                              // normalize name component ex callback
    	NULL                               // section notification callback
    };

    Write the INF

    INF는  드라이버를 설치하는 데 사용되는 모든 정보를 포함하는 텍스트 파일이다. sys 파일을 어느 위치(C:\Windows\System32\drivers 등)로 복사할 것인지, 레지스트리의 어느 키에 어떤 값을 만들 것인지, 서비스 이름, 커맨드 라인, 시작 타입(수동/자동 등), 서비스 타입(커널 드라이버/파일 시스템 등) 등을 정의한다. INF에 대한 자세한 설명은 MSDN이나 다른 블로그에도 잘 되어 있으니 자세한 설명은 생략하겠다. 내용이 길기 때문에 본 예제에서 사용한 INF는 미니 필터 예제 깃헙을 참고하길 바란다.

    https://github.com/geun-yeong/minifilter-example/blob/main/minifilter/minifilter.inf

    Build

    미니 필터 드라이버는 FlgMgr.lib를 필요로 한다. 프로젝트 속성의 링커 메뉴에서 FlgMgr.lib를 빌드에 포함시킨다.

    프로젝트 속성의 링커 메뉴에서 FltMgr.lib 링킹 추가

    이후 빌드를 하면 된다. 만약 Inf2Cat(error code: -2) 에러가 발생한다면 INF 파일이 잘못되었거나 테스트 사이닝 과정에서 타임 스탬프가 맞지 않아 발생하는 문제일 수 있다. 오타를 찾거나 프로젝트 속성의 Inf2Cat 메뉴로 가서 "Use Local Time" 속성을 Yes로 해주면 된다.

    Test

    테스트할 가상머신에 INF 파일과 sys 파일을 복사한다. INF 파일을 우클릭하면 Install 항목이 있다. Install 항목을 클릭하면 INF 파일에 정의된 대로 sys 파일을 복사하고, 서비스를 생성하고, 레지스트리에 새 키나 값을 생성한다. INF로 드라이버를 설치하고 sc로 확인할 수 있다. qc 옵션은 서비스의 설정 정보를 보는 옵션이다. sc를 실행하기 위해선 관리자 권한으로 실행해야 한다.

    > sc qc <Service Name>

    sc qc로 확인한 미니필터 드라이버 서비스

    sc start로 서비스를 실행하면 미니 필터가 로드된다. fltmc 명령으로 현재 시스템에 로드된 미니 필터 목록을 볼 수 있다. 여기서 우리의 미니 필터 서비스 이름이 보이면 미니 필터로 로드된 것이다.

    > sc start <Service Name>

    sc start로 미니 필터 로드
    sc start후 fltmc로 MiniFilter-Example이라는 미니 필터가 등록된 것을 확인
    DebugView로 미니 필터가 내보내는 디버그 메시지를 볼 수 있음

    멈추고 싶다면 sc stop을 사용하면 된다.

    > sc stop <Service Name>

    sc stop으로 미니 필터 해제

    Close

    이상으로 간단한 미니 필터 만드는 법을 알아봤다. 본 글은 깃헙에 올린 미니 필터 예제를 기반으로 작성했다. 미니 필터 예제 코드와 함께 보며 읽으면 미니 필터에 다가서는 데 큰 어려움이 없을 것이다.

    다음은 Port를 사용해 유저 모드 어플리케이션과 통신하는 방법도 적어보려 한다. 

    Github

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