Development/Windows

#4 MiniFilter - Communication Port (minifilter → user)

geunyeong 2021. 11. 27. 00:53

Table of Contents

    Abstract

    앞선 글에서는 유저 모드 어플리케이션에서 미니 필터로 데이터를 전달하는 방법을 알아봤다. 본 글에서는 반대로 미니 필터에서 유저 모드 어플리케이션으로 데이터를 전달하는 방법을 알아본다.

    예제 코드는 깃헙에 있다.

    Port

    Communicate Server Port in MiniFilter

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

    Create the Server Port

    앞선 글을 참고하자.

    • #3 MiniFilter - Communication Port (user → minifilter)
    https://geun-yeong.tistory.com/53

    Common Structures

    유저 모드 어플리케이션에서 커널로 데이터를 보낼 때 처럼 둘 사이의 데이터를 주고 받을 포맷을 먼저 구상해야 한다. 본 예제에서는 미니필터는 클라이언트에게 접근하는 파일의 전체 경로를 보내고, 클라이언트는 이 파일의 접근 허용 여부를 반환하도록 했다.

    #define BUFFER_SIZE 4096 // page size
    
    // this structure will be used when filter send message to user
    typedef struct _FLT_TO_USER {
    
    	wchar_t path[BUFFER_SIZE / sizeof(wchar_t)];
    
    } FLT_TO_USER, *PFLT_TO_USER;
    
    // this structure will be used when user reply to filter
    typedef struct _FLT_TO_USER_REPLY {
    
    	unsigned __int32 block; // if 1, file access will be denied
    
    } FLT_TO_USER_REPLY, *PFLT_TO_USER_REPLY;

    Write the Send Function

    드라이버에서 포트를 사용해 유저 모드로 데이터를 전달할 땐 어느 클라이언트(유저 모드 어플리케이션)으로 보낼지를 나타낼 클라이언트 포트 값이 필요하다. 본 예제에서는 하나의 클라이언트에 대해서만 동작하도록 만들었기 때문에 큰 문제는 안 되지만 여러 클라이언트와 통신해야 한다면 여간 귀찮은 일이 아닐 것이다.좀 더 쓰기 편하게 클라이언트 포트는 static 변수로 선언해 port.c에서만 접근 가능하도록 하고 외부에서는 버퍼만 전달하면 되도록 작성했다.

    미니 필터에서 클라이언트로 데이터를 전달하는 함수는 FltSendMessage라는 API다. 이 함수는 DriverEntry에서 만들었던 미니 필터의 핸들과 메시지를 전달한 클라이언트의 포트 정보, 보낼 데이터 버퍼와 응답 데이터 버퍼를 파라메터로 받는다. 마지막 파라메터는 Timeout 값을 LARGE_INTEGER 구조체 형식으로 받는데, 조금 독특한 기준을 가진다. 우선 Timeout 값은 100 나노 초를 기준으로 한다. 즉 Timeout 값으로 10을 넣으면 1 마이크로 초가 되고, 1초를 만들고 싶다면 10'000'000을 넣어야 한다(1 나노 = 10^-9, 1 마이크로 = 10^-6, 1 밀리 = 10^-3)

    • Timeout 값이 양수라면 1601년 1월 1일부터 흐른 시간을 기다린다. 10을 넣었다고하면 현재 시간이 1601년 1월 1일 0시 0분 0.000001 (1 마이크로 초)가 지났을 때 현재 포트 I/O 작업을 종료한다. 즉 현재 시간을 가져와 +n을 한 후 넘겨주지 않으면 어지간히 큰 수가 아닌 이상 즉시 종료가 될 가능성이 크다.
    • Timeout 값이 음수라면 현재 시간을 기준으로 흐를 시간만큼 기다린다. -10을 넣었고, Send API를 호출한게 2021년 11월 27일 12시 0분 0초였다면 12시 0분 0.000001초까지만 현재 포트 I/O를 기다린다. 
    • NULL을 넣는다면 응답이 올 때 까지 무한 대기 상태에 들어간다. 그러므로 응답이 필요 없다면 NULL 포인터를 넣을 것이 아니라 LARGE_INTEGER 타입 변수에 0을 값으로 넣고 이 변수의 포인터를 전달하면 된다.
    static PFLT_FILTER flt_handle;
    static PFLT_PORT flt_port;
    static PFLT_PORT client_port;
    
    // ...(중략)...
    
    //
    // This function send data to user-mode application
    //
    NTSTATUS
    MinifltPortSendMessage(
    	_In_ PVOID send_data,
    	_In_ ULONG send_data_size,
    	_Out_opt_ PVOID recv_buffer,
    	_In_ ULONG recv_buffer_size,
    	_Out_ PULONG written_bytes_to_recv_buffer
    )
    {
    	NTSTATUS status = STATUS_SUCCESS;
    
    	if (recv_buffer) {
    		status = FltSendMessage( // receive a reply about sent data
    			flt_handle,
    			&client_port,
    			send_data,
    			send_data_size,
    			recv_buffer,
    			&recv_buffer_size,
    			NULL
    		);
    
    		*written_bytes_to_recv_buffer = recv_buffer_size;
    	}
    	else {
    		//
    		// timeout 값은
    		//   - 양수면 1601년 1월 1일을 기준으로 한 절대 시각이 될 때 까지 대기
    		//   - 음수면 현재 시간 기준으로 해당 시간 만큼 대기
    		//   - NULL이면 무한정 대기
    		// 
    		// 100 나노초 단위이므로
    		//   - 10 = 1 micro second
    		//   - 10'000 = 1 milli second
    		//   - 10'000'000 = 1 second
    		// 
    		LARGE_INTEGER timeout;
    		timeout.QuadPart = 0;
    
    		status = FltSendMessage( // just sending
    			flt_handle,
    			&client_port,
    			send_data,
    			send_data_size,
    			NULL,
    			NULL,
    			&timeout
    		);
    
    		*written_bytes_to_recv_buffer = 0;
    	}
    
    	return status;
    }

    Use the Send Function

    미니 필터의 Create Pre-Operation 루틴에서 MinifilterPortSendMessage 함수를 이용해 클라이언트에게 파일 경로를 전달하도록 코드를 추가했다. 그리고 클라이언트가 응답한 Block 값을 비교해 TRUE면 IoStatus 값을 STATUS_ACCESS_DENIED로 설정하고, Create Pre-Operation의 반환 값도 FLT_PREOP_COMPLETE로 설정하도록 수정했다. STATUS_ACCESS_DENIED를 반환하는 경우 반드시 FLT_PREOP_COMPLETE를 반환해야 한다.

    // ...(전략)...
    
    		FLT_TO_USER sent;        RtlZeroMemory(&sent, sizeof(sent));
    		FLT_TO_USER_REPLY reply; RtlZeroMemory(&reply, sizeof(reply));
    		ULONG returned_bytes = 0;
    
    		// send the file path to client
    		// if client reply to block accessing the file,
    		// the minifilter return STATUS_ACCESS_DENIED to filter manager
    		wcscpy_s(sent.path, ARRAYSIZE(sent.path), name_info->Name.Buffer);
    		
    		status = MinifltPortSendMessage(
    			&sent, 
    			sizeof(sent), 
    			&reply, 
    			sizeof(reply), 
    			&returned_bytes
    		);
    		if (NT_SUCCESS(status) && returned_bytes > 0 && reply.block) {
    			callback_data->IoStatus.Status = STATUS_ACCESS_DENIED;
    			ret = FLT_PREOP_COMPLETE;
    		}
    
    
    
    		// get the file name from the full path
    		LPWSTR file_name = wcsrchr(name_info->Name.Buffer, L'\\');
    		if (!file_name) {
    			file_name = name_info->Name.Buffer;
    		}
    
    		DbgPrint(
    			"[filterport] " __FUNCTION__ " [%u] %s to creat/open a file (%ws)\n",
    			PtrToUint(PsGetCurrentProcessId()),
    			(reply.block) ? "Blocked " : "Complete",
    			file_name
    		);
            
    // ...(후략)...

    Get and Reply the Message in User-mode application

    Connect to Communication Server Port

    이 역시 앞선 글을 참고하자.

    https://geun-yeong.tistory.com/53

    Get the Message

    미니 필터가 FltSendMessage로 보낸 데이터를 받기 위해서는 먼저 구조체를 새로 만들어야 한다.

    FilterGetMessage는 메시지를 받기만 할 뿐 FilterSendMessage처럼 응답을 받을 Output Buffer의 주소를 받지 않는다. 응답을 위해서는 FilterReplyMessage를 사용해야 하는데, 이 때 FilterReplyMessage로 응답하는 메시지가 미니 필터가 보낸 여러 메시지 중 어느 메시지의 응답인지를 구분하는 MessageId 값이 필요하다. FilterGetMessage는 이 MessageId 값을 FILTER_MESSAGE_HEADER 구조체에 담고, 미니 필터가 보낸 데이터를 이 HEADER 구조체의 바로 뒤부터 쓴다. 때문에 FilterGetMessage로 데이터를 받을 때 사용할 구조체에는 FILTER_MESSAGE_HEADER 멤버가 반드시 필요하고, 데이터를 주고 받을 포맷에 대한 구조체는 HEADER 바로 뒤에 위치해야 한다.

    앞서 FLT_TO_USER를 데이터를 주고 받을 포맷으로 작성했으니, FILTER_MESSAGE_HEADER 구조체를 포함한 FLT_TO_USER_WRAPPER 구조체를 만들고, FilterGetMessage에 HEADER 구조체의 포인터 값을 전달한다. 주소 값은 HEADER의 주소값이지만 버퍼의 크기는 구조체 전체 크기를 전달하면 된다.

    typedef struct _FLT_TO_USER_WRAPPER {
    
    	FILTER_MESSAGE_HEADER hdr;
    	FLT_TO_USER data;
    
    } FLT_TO_USER_WRAPPER, *PFLT_TO_USER_WRAPPER;
    
    // ...(중략)...
    
    h_result = FilterGetMessage(
    	port_handle,
    	&recv.hdr,
    	sizeof(recv),
    	nullptr
    );
    
    if (IS_ERROR(h_result)) {
    	sprintf_s(
    		exception_msg, 128,
    		"FilterGetMessage failed (HRESULT = 0x%x)", h_result
    	);
    	throw exception(exception_msg);
    }
    
    // if there is "test.txt" in path, block it
    ZeroMemory(&recv_reply, sizeof(recv_reply));
    if (wcsstr(recv.data.path, L"test.txt")) {
    	recv_reply.data.block = TRUE;
    	wcout << recv.data.path << L" will be blocked" << endl;
    }
    
    // ...(후략)...

    Reply the Message

    FilterGetMessage로 받은 메시지는 FilterReplyMessage로 응답할 수 있다. 하지만 FilterReplyMessage 역시 구조체를 새로 만들어야 한다. Reply에 사용되는 메시지 헤더는 FITLER_REPLY_HEADER다.

    FILTER_REPLY_HEADER 구조체가 포함된 FTL_TO_USER_REPLY_WRAPPER 구조체를 만들고, REPLY HEADER의 MessageId 값을 앞서 FilterGetMessage로 받았던 HEADER의 MessageId 값을 넣어준다. 이렇게 하면 필터 관리자가 알아서 적절한 FltSendMessage의 Output Buffer에 복사해 반환해준다. 때문에 무엇보다도 이 작없이 가장 중요하다.

    그리고 응답할 때는 FilterReplyMessage API에 REPLY HEADER 주소를 넘겨주면 된다. 이 때 주의해야할 점이 있다. 단순히 sizeof(recv_reply)를 사용하면 FilterReplyMessage가 MORE DATA 에러를 띄우며 함수 수행에 실패하기 때문이다. 이 에러는 소스 코드만 보고는 찾기 힘든 매우 귀찮은 버그다.

    윈도우는 변수의 크기에 따라 변수가 위치할 메모리 주소의 배수가 달라진다. 4바이트 크기의 변수는 4의 배수 위치에 맞추려 하고, 8바이트 크기 변수는 8의 배수 위치에 놓이게 한다. 그래서 하나의 구조체에 4바이트와 8바이트 크기 멤버가 있다면 오프셋 0엔 4바이트가, 오프셋 8엔 8바이트 멤버가 놓여 총 16바이트 크기가 된다. 즉 sizeof(struct)와 sizeof(4) + sizeof(8)의 결과가 달라진다.

    MORE DATA 에러는 이러한 패딩 정책때문에 발생한다. sizeof로 넘겨준 구조체 크기는 16인데 메시지 헤더 역할을 하는 4바이트를 뺀 12바이트가 미니 필터의 FltSendMessage의 Output Buffer로 들어가게 된다. 근데 FltSendMessage를 호출할 때 넘겨준 Output Buffer의 크기는 sizeof(8)이니 버퍼 크기보다 더 큰 데이터가 들어왔다는 MORE DATA 에러가 발생한 것이다. 여기선 예시를 들기 위해 메시지 헤더 구조체의 크기 등을 4, 8, 12, 16이라고 했지만 실제 크기는 당연히 다르다.

    이러한 이유로 FilterReplyMessage에서는 sizeof(struct)보다는 sizeof(REPLY HEADER)와 sizeof(FLT_TO_USER_REPLY)를 더하는 방법으로 Reply 데이터 사이즈를 명시하는 게 좋다. 본 예제에서는 접근하려는 파일 경로에 "test.txt"라는 문자열이 들어 있으면 차단 메시지를 응답하게 했다.

    typedef struct _FLT_TO_USER_REPLY_WRAPPER {
    
    	FILTER_REPLY_HEADER hdr;
    	FLT_TO_USER_REPLY data;
    
    } FLT_TO_USER_REPLY_WRAPPER, * PFLT_TO_USER_REPLY_WRAPPER;
    
    // ...(중략)...
    
    // if there is "test.txt" in path, block it
    ZeroMemory(&recv_reply, sizeof(recv_reply));
    if (wcsstr(recv.data.path, L"test.txt")) {
    	recv_reply.data.block = TRUE;
    	wcout << recv.data.path << L" will be blocked" << endl;
    }
    recv_reply.hdr.MessageId = recv.hdr.MessageId; // IMPORTANT!
    
    // send the reply
    h_result = FilterReplyMessage(
    	port_handle,
    	&recv_reply.hdr,
    	sizeof(recv_reply.hdr) + sizeof(recv_reply.data)
    );
    
    if (IS_ERROR(h_result)) {
    	sprintf_s(
    		exception_msg, 128,
    		"FilterReplyMessage failed (HRESULT = 0x%x)", h_result
    	);
    	throw exception(exception_msg);
    }
    
    // ...(후략)...

    Result

    이제 예제를 실행시키면 다른 파일들은 정상 접근이 가능하지만 test.txt라는 문자열이 들어간 파일은 "You do not have permission" 에러가 발생하며 정상적으로 열리지 않는 것을 볼 수 있다.

    test.txt를 열려고 하면 Permission 에러가 발생하는 모습

    Close

    이상으로 미니 필터에서 유저 모드 어플리케이션으로 '먼저' 데이터를 보내는 포트 통신 방법을 알아봤다. 그와 동시에 파일 접근을 막고 싶으면 어떻게 해야하는 지도 알아봤다. 미니 필터에서 알아둬야 할 유용한 기능은 거의 다뤘으니 아마 미니 필터 글은 본 글이 마지막이 될 수도 있다. 미니 필터와 유저 모드 어플리케이션 간 양방향 통신을 할 수 있다면 여러분이 원하는 기능을 만드는 데 큰 어려움이 없을 것이라고 생각한다.

    예제 코드는 깃헙에 있으며 보이지 않는다면 dev 브랜치를 확인하길 바란다.

    Github

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

    'Development > Windows' 카테고리의 다른 글

    #2 WFP - WFP Callout Driver 만들기  (3) 2021.12.05
    #1 WFP - 개요  (0) 2021.11.28
    #3 MiniFilter - Communication Port (user → minifilter)  (1) 2021.11.25
    #2 MiniFilter - 미니 필터 만들기  (0) 2021.11.23
    #1 MiniFilter - 개요  (0) 2021.11.20