Development/Windows

#3 MiniFilter - Communication Port (user → minifilter)

geunyeong 2021. 11. 25. 23:49

Table of Contents

    Abstract

    Communication Port는 미니 필터와 유저 모드 어플리케이션 간에 데이터를 주고 받기 위해 만들어진 기능이자 인터페이스다. 필터 관리자에 의해 제공되는 기능이다. 동기식, 비동기식 송수신도 가능하고 미니 필터에서 유저 모드로, 유저 모드에서 미니 필터로 데이터를 전송하고 이에 대해 응답을 받을 수 있는 양방향 통신 시스템이다. 드라이버와 유저 모드 어플리케이션 간의 데이터 송수신이 제법 까다로운 걸 생각하면 포트는 혁명과도 같다.

    본 글에서는 유저 모드 어플리케이션에서 미니 필터로 데이터를 보내고 이에 대한 응답을 동기식으로 받는 방법을 서술했다. 예제 코드를 담은 깃허브 주소를 본문 최하단에 적어놨으니 당장 예제 코드가 필요하다면 참고하길 바란다. main 브랜치에서 안 보인다면 dev 브랜치를 보면 된다.

    Port

    Port Overview

    Communication Port란 유저 모드 어플리케이션과 미니 필터 드라이버가 양방향 데이터 통신이 가능하도록 필터 관리자가 제공하는 기능이자 인터페이스를 의미한다. 소켓에서의 포트와는 다르다. Port는 서버와 클라이언트로 구성되는데, 미니 필터가 서버가 되어 Port를 생성하고 유저 모드 어플리케이션이 접근하기를 기다린다. 유저 모드 어플리케이션은 클라이언트가 되어 FilterConnectCommunicationPort API를 사용해 미니 필터가 생성한 Port에 연결한다. Port는 기본적으로 동기 I/O이나 Overlapped를 사용해 비동기 I/O로도 동작 가능하다.

    Communication Server Port in MiniFilter

    유저 모드 어플리케이션에서 미니 필터로 데이터 전송 시 API 및 콜백 함수 순서

    Create the Server Port

    포트를 사용하기 위해서는 먼저 미니 필터를 등록해 필터 핸들을 얻어야 한다. 미니 필터를 만드는 법은 이미 설명한 글이 있으니 자세한 내용은 참고하길 바란다.

    • #2 MiniFilter - 미니 필터 만들기
    https://geun-yeong.tistory.com/52
    // ...(전략)...
    
    NTSTATUS status = STATUS_SUCCESS;
    
    // register the minifilter to filter manager
    status = FltRegisterFilter(
        driver_object,
        &registration,
        &flt_handle
    );
    IF_ERROR(FltRegisterFilter, CLEANUP_DRIVER_ENTRY);
    
    // initialize the filter port
    status = MinifltPortInitialize(flt_handle);
    IF_ERROR(MinifltPortInitialize, CLEANUP_DRIVER_ENTRY);
    
    // start my minifilter filtering
    status = FltStartFiltering(
        flt_handle
    );
    IF_ERROR(FltStartFiltering, CLEANUP_DRIVER_ENTRY);
    
    // set a driver unload routine
    driver_object->DriverUnload = DriverUnload;
    
    return STATUS_SUCCESS;
    
    // ...(후략)...

    미니 필터를 등록해 필터 핸들을 얻었다면, FltCreateCommunicationPort API를 사용해 포트를 생성하면 된다. 서버 포트를 만들기 위해서는 Security Descriptor와 포트 이름을 지정해줘야 하는 등의 작업이 필요해 본 예제에서는 MinifltPortInitialize라는 별도 함수로 작성했다.

    과정은 간단하다. 먼저 FltBuildDefaultSecurityDescriptor로 PSECURITY_DESCRIPTOR를 ALL ACCESS 권한으로 초기화한다. 다음 포트의 이름으로 사용할 UNICODE_STRING을 만들어주고, InitializeObjectAttributes로 ALL ACCESS 권한을 가지고, UNICODE_STRING에 정의된 이름을 가진 오브젝트 속성 객체를 만든다. 그리고 이를 FltCreateCommunicationPort에 전달하면 된다. 단 포트의 이름은 '\'로 시작해야 한다. max connection 파라메터에서 짐작할 수 있듯 하나의 포트에 여러 클라이언트(유저 모드 어플리케이션)가 접근할 수 있다.

    #define MINIFLT_EXAMPLE_PORT_NAME "\\MiniFilterPort_Example"
    static PFLT_PORT flt_port;
    
    NTSTATUS
    MinifltPortInitialize(
    	_In_ PFLT_FILTER flt_handle
    )
    {
    	NTSTATUS status         = STATUS_SUCCESS;
    	UNICODE_STRING port_name; RtlZeroMemory(&port_name, sizeof(port_name));
    	PSECURITY_DESCRIPTOR sd;  RtlZeroMemory(&sd, sizeof(sd));
    	OBJECT_ATTRIBUTES oa;     RtlZeroMemory(&oa, sizeof(oa));
    	
    	status = FltBuildDefaultSecurityDescriptor(
    		&sd,
    		FLT_PORT_ALL_ACCESS
    	);
    	IF_ERROR(FltBuildDefaultSecurityDescriptor, CLEANUP_PORT_INITIALIZE);
    
    	RtlInitUnicodeString(&port_name, TEXT(MINIFLT_EXAMPLE_PORT_NAME));
    	InitializeObjectAttributes(
    		&oa, &port_name, OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE, NULL, sd
    	);
    
    	status = FltCreateCommunicationPort(
    		flt_handle,
    		&flt_port,
    		&oa,
    		NULL,
    		MinifltPortNotifyRoutine,
    		MinifltPortDisconnectRoutine,
    		MinifltPortMessageRoutine,
    		1 // max connection
    	);
    	IF_ERROR(FltCreateCommunicationPort, CLEANUP_PORT_INITIALIZE);
    
    	return STATUS_SUCCESS;
    
    CLEANUP_PORT_INITIALIZE:
    
    	MinifltPortFinalize();
    
    	return status;
    }

    Write Notify Routines

    FltCreateCommunicationPort를 호출할 때 3개의 함수 포인터를 넘겨주는 걸 볼 수 있다. 첫번째는 Notify Routine으로, 유저 모드 어플리케이션이 미니 필터가 생성한 포트에 연결할 때 호출되는 콜백 함수다. 두번째는 Disconnect Notify Routine으로, 유저 모드 어플리케이션이 포트 연결을 끊을 때 호출된다. 세번째는 Message Notify Routine으로 유저 모드 어플리케이션이 FilterSendMessage API를 사용해 미니 필터로 데이터를 전달할 때 호출되는 콜백 함수다. 사실상 recv 함수인 셈이다.

    Notify Routine에서는 파라메터로 클라이언트 측의 포트 정보가 담긴 PFLT_PORT를 넘겨주는데, 앞서 말했듯 하나의 포트에 여러 클라이언트가 연결될 수 있기 때문에 각 클라이언트를 구분하기 위해선 이 포트 정보를 잘 보관해야 한다. 특히 클라이언트 포트 정보는 미니 필터 측에서 FltSendMessage로 유저 모드 어플리케이션에 데이터를 전송할 때 필요하다.

    본 예제에서는 Notify Routine과 Disconnect Notify Routine은 별다른 작업을 하지 않고 자신이 호출됐다는 디버그 메시지만 출력한다. Notify Message Routine에서는 유저 모드 어플리케이션이 전송한 메시지를 디버그 메시지로 출력하고, 이에 대한 응답으로 "Hello, User"를 응답한다.

    //
    // This function will be call when
    // user mode application calls FilterConnectCommunicationPort
    //
    NTSTATUS
    MinifltPortNotifyRoutine(
    	_In_ PFLT_PORT client_port,
    	_In_ PVOID server_cookie,
    	_In_ PVOID connection_context,
    	_In_ ULONG connection_context_size,
    	_Out_ PVOID* connection_port_cookie
    )
    {
    	UNREFERENCED_PARAMETER(client_port);
    	UNREFERENCED_PARAMETER(server_cookie);
    	UNREFERENCED_PARAMETER(connection_context);
    	UNREFERENCED_PARAMETER(connection_context_size);
    	UNREFERENCED_PARAMETER(connection_port_cookie);
    
    	DbgPrint(
    		"[filterport] " __FUNCTION__ " User-mode application(%u) connect to this filter\n",
    		PtrToUint(PsGetCurrentProcessId())
    	);
    
    	return STATUS_SUCCESS;
    }
    
    //
    // This function will be call when
    // user mode handle count for the client port reaches zero or
    // when the minifilter driver is to be unloaded
    //
    VOID
    MinifltPortDisconnectRoutine(
    	_In_ PVOID connection_cookie
    )
    {
    	UNREFERENCED_PARAMETER(connection_cookie);
    
    	DbgPrint(
    		"[filterport] " __FUNCTION__ " User-mode application(%u) disconnect with this filter\n",
    		PtrToUint(PsGetCurrentProcessId())
    	);
    }
    
    //
    // This function will be call when
    // user mode application calls FilterSendMessage
    //
    NTSTATUS
    MinifltPortMessageRoutine(
    	_In_ PVOID port_cookie,
    	_In_opt_ PVOID input_buffer,
    	_In_ ULONG input_buffer_size,
    	_Out_opt_ PVOID output_buffer,
    	_In_ ULONG output_buffer_size,
    	_Out_ PULONG return_output_buffer_length
    )
    {
    	UNREFERENCED_PARAMETER(port_cookie);
    
    	DbgPrint(
    		"[filterport] " __FUNCTION__ " User-mode application(%u) send data to this filter\n",
    		PtrToUint(PsGetCurrentProcessId())
    	);
    
    	if (input_buffer && input_buffer_size == sizeof(USER_TO_FLT)) {
    		PUSER_TO_FLT sent = (PUSER_TO_FLT)input_buffer;
    		DbgPrint(
    			"[filterport] " __FUNCTION__ " Data: %ws\n",
    			sent->msg
    		);
    	}
    	
    	if (output_buffer && output_buffer_size == sizeof(USER_TO_FLT_REPLY)) {
    		PUSER_TO_FLT_REPLY reply = (PUSER_TO_FLT_REPLY)output_buffer;
    		wcscpy_s(reply->msg, ARRAYSIZE(reply->msg), L"Hello, User");
    		*return_output_buffer_length = (ULONG)(wcslen(L"Hello, User") * sizeof(wchar_t));
    	}
    	
    	return STATUS_SUCCESS;
    }

    Message Notify Routine에서 input buffer는 클라이언트가 보낸 메시지 데이터가 들어있는 포인터다. input buffer size는 보낸 메시지의 크기다. output buffer는 응답 데이터를 저장할 버퍼의 포인터, output buffer size는 그 버퍼의 크기, return output buffer length는 클라이언트에게 output buffer에 얼마만큼의 데이터를 썼는지 알려주는 값이다. 때문에 공용 구조체를 하나 만들어 미니 필터와 유저 모드 어플리케이션이 서로 동일한 구조의 데이터를 주고 받을 수 있도록 하는 것이 좋다.

    본 예제에서 사용한 유저 모드 어플리케이션 -> 미니 필터 간의 메시지 포맷은 아래와 같다.

    #define BUFFER_SIZE 4096 // page size
    
    // this structure will be used when user send message to user
    typedef struct _USER_TO_FLT {
    	
    	wchar_t msg[BUFFER_SIZE / sizeof(wchar_t)];
    
    } USER_TO_FLT, *PUSER_TO_FLT;
    
    // this structure will be used when filter reply to user
    typedef struct _USER_TO_FLT_REPLY {
    
    	wchar_t msg[BUFFER_SIZE / sizeof(wchar_t)];
    
    } USER_TO_FLT_REPLY, *PUSER_TO_FLT_REPLY;

    Connect to Communication Server Port from User-mode application

    Connect

    유저 모드 어플리케이션은 fltUser.h를 인클루드하고, FilterConnectCommunicationPort를 호출하면 미니 필터가 생성한 포트와 연결할 수 있다. FilterConnectCommunicationPort로 반환받은 포트 핸들을 사용해 FilterSendMessage로 데이터를 미니 필터에 전달하거나 FilterGetMessage로 데이터를 받을 수도 있다.

    본 예제에서는 FilterSendMessage로 "Hello, MiniFilter"라는 문자열을 전송하고, 그에 대한 응답을 받아 콘솔에 출력하도록 했다.

    #include <Windows.h>
    #include <fltUser.h>
    #include "../common/common.h"
    
    #include <iostream>
    #include <exception>
    using namespace std;
    
    int main() {
    	char exception_msg[128];
    
    	HANDLE port_handle;
    	HRESULT h_result;
    
    	USER_TO_FLT sent;        ZeroMemory(&sent, sizeof(sent));
    	USER_TO_FLT_REPLY reply; ZeroMemory(&reply, sizeof(reply));
    	DWORD returned_bytes = 0;
    
    	try {
    		// connect to minifilter communication port
    		h_result = FilterConnectCommunicationPort(
    			TEXT(MINIFLT_EXAMPLE_PORT_NAME),
    			0,
    			nullptr,
    			0,
    			nullptr,
    			&port_handle
    		);
    
    		if (IS_ERROR(h_result)) {
    			sprintf_s(
    				exception_msg, 128,
    				"FilterConnectCommunicationPort failed (HRESULT = 0x%x)", h_result
    			);
    			throw exception(exception_msg);
    		}
    		
    		
    
    		// send a message and receive a reply
    		wcscpy_s(sent.msg, ARRAYSIZE(sent.msg), L"Hello, MiniFilter");
    		h_result = FilterSendMessage(
    			port_handle,
    			&sent,
    			sizeof(sent),
    			&reply,
    			sizeof(reply),
    			&returned_bytes
    		);
    
    		if (IS_ERROR(h_result)) {
    			sprintf_s(
    				exception_msg, 128,
    				"FilterSendMessage failed (HRESULT = 0x%x)", h_result
    			);
    			throw exception(exception_msg);
    		}
    
    		// if minifilter send the reply
    		if (returned_bytes > 0) {
    			wcout << reply.msg << endl;
    		}
    		else {
    			cout << "No reply" << endl;
    		}
    	}
    	catch (const exception& e) {
    		cerr << e.what() << endl;
    	}
    	
    	
    
    	if (port_handle) {
    		FilterClose(port_handle);
    	}
    
    	return 0;
    }

    Build

    미니 필터를 빌드할 때와 마찬가지로 유저 모드 어플리케이션에서 포트 라이브러리를 사용하기 위해서는 lib 파일을 링크 목록에 포함시켜야 한다. 프로젝트 속성의 링커 메뉴로 가서 FltLib.lib를 추가해주면 된다.

    링킹 라이브러리 목록에 FltLib.lib 추가

    Result

    미니 필터를 먼저 실행하고 유저 모드 어플리케이션을 실행하면 아래 그림과 같이 미니 필터는 유저 모드 어플리케이션이 보낸 "Hello, MiniFilter"를 받아 디버그 메시지로 출력하고, 유저 모드 어플리케이션은 미니 필터가 응답한 "Hello, User"를 받아 출력한 것을 볼 수 있다.

    Finalize the Port

    사용을 끝마친 포트는 잘 닫아주자.

    VOID
    MinifltPortFinalize()
    {
    	if (flt_port) {
    		FltCloseCommunicationPort(flt_port);
    	}
    }
    VOID DriverUnload(
    	_In_ PDRIVER_OBJECT driver_object
    )
    {
    	UNREFERENCED_PARAMETER(driver_object);
    
    	MinifltPortFinalize();
    
    	if (flt_handle) {
    		FltUnregisterFilter(flt_handle);
    	}
    }

    Close

    이상으로 유저 모드 어플리케이션에서 미니 필터로의 기본적인 포트 통신 사용 방법을 알아봤다. 다음 글에선 미니 필터에서 유저 모드 어플리케이션으로 메시지를 전달하는 방법을 기술하겠다.

    Github

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