#2 WFP - WFP Callout Driver 만들기
Table of Contents
Abstract
본 글에서는 앞서 설명한 WFP 어플리케이션을 직접 만들어보겠다. 본 예제 코드에서는 필터의 조건(Condition)을 설정하지 않고 모든 트래픽에 대해서 콜아웃을 호출하도록 했다. WFP 기능을 이용하기 위한 최소한의 코드만을 담고 있으니 본 예제 코드가 정석이라고 생각하진 말자. 전체 코드는 깃허브에 올려놨다.
WFP에 대한 개요는 이전 글을 참고하길 바란다.
- #1 WFP - 개요
https://geun-yeong.tistory.com/55
Create a WDF Device
WFP를 사용하기 위해서는 Device Object가 필요하다. MS가 모던한 드라이버 개발을 위해 만든 WDF를 이용해 디바이스 오브젝트를 만들어보자.
Create a WDF Driver
먼저 WDF Driver를 만든다. WDF Driver는 반드시 DriverEntry가 실행되는 과정에서 생성해야 한다. 본 예제에서는 WDF Driver의 언로드 루틴만 등록했다. DriverInitFlags에 WdfDriverInitNonPnpDriver 플래그를 활성화 하자. 그렇지 않으면 PNP 드라이버로 인식해 재부팅하지 않는 이상 드라이버가 언로드 되지 않는다.
WDF_DRIVER_CONFIG config = { 0, };
WDFDRIVER wdfdriver = NULL;
//
// create a wdf driver
//
WDF_DRIVER_CONFIG_INIT(&config, NULL);
config.EvtDriverUnload = WdfDriverUnload;
config.DriverInitFlags |= WdfDriverInitNonPnpDriver;
status = WdfDriverCreate(driver_object,
registry_path,
WDF_NO_OBJECT_ATTRIBUTES,
&config,
&wdfdriver);
IF_ERROR(WdfDriverCreate, EXIT_OF_INIT_DEVICE);
Create a WDF Device
다음으로 WDF Device Object를 만든다. WdfDeviceInitAssignName으로 WDF Device에 이름을 할당하고, WdfDeviceCreate로 WDF Device Object를 생성한다. 디바이스 오브젝트를 만들고 유저 모드 어플리케이션에서 접근하기 쉽도록 Dos Name을 심볼릭 링크로 걸어주었다.
UNICODE_STRING device_name = { 0, },
dos_device_name = { 0, };
PWDFDEVICE_INIT device_init = NULL;
WDFDEVICE wdfdevice = NULL;
//
// create a wdf device
//
device_init = WdfControlDeviceInitAllocate(wdfdriver, &SDDL_DEVOBJ_KERNEL_ONLY);
if (!device_init) {
status = STATUS_INSUFFICIENT_RESOURCES;
goto EXIT_OF_INIT_DEVICE;
}
RtlInitUnicodeString(&device_name, TEXT(WDFKM_NT_DEVICE_NAME));
status = WdfDeviceInitAssignName(device_init, &device_name);
IF_ERROR(WdfDeviceInitAssignName, EXIT_OF_INIT_DEVICE);
status = WdfDeviceCreate(&device_init, WDF_NO_OBJECT_ATTRIBUTES, &wdfdevice);
IF_ERROR(WdfDeviceCreate, EXIT_OF_INIT_DEVICE);
RtlInitUnicodeString(&dos_device_name, TEXT(WDFKM_DOS_DEVICE_NAME));
status = IoCreateSymbolicLink(&dos_device_name, &device_name);
if (status == STATUS_OBJECT_NAME_COLLISION) {
IoDeleteSymbolicLink(&dos_device_name);
status = IoCreateSymbolicLink(&dos_device_name, &device_name);
}
IF_ERROR(IoCreateSymbolicLink, EXIT_OF_INIT_DEVICE);
만들어진 WDF Device Object를 WFP에 전달할 수 있도록 리턴한다.
*device_object = WdfDeviceWdmGetDeviceObject(wdfdevice);
Unload Routine
WDF Driver를 만들면 DriverEntry의 파라메터로 넘어온 DriverObject에 언로드 루틴을 등록해도 불리지 않는다. 드라이버 언로드시 WDF Driver에 등록된 언로드 루틴이 대신 호출된다. 그러므로 드라이버 언로드시 Finalization 작업은 WDF Driver의 언로드 루틴에 작성해야 한다. 본 예제에서는 WFP Finalization 함수를 호출하고, 심볼릭 링크를 제거하는 코드를 넣었다.
VOID WdfDriverUnload(
_In_ WDFDRIVER wdfdriver
)
{
UNREFERENCED_PARAMETER(wdfdriver);
UNICODE_STRING dos_device_name = { 0, };
FinWfp();
RtlInitUnicodeString(&dos_device_name, TEXT(WDFKM_DOS_DEVICE_NAME));
IoDeleteSymbolicLink(&dos_device_name);
}
Create a Callout Driver
우리는 본 예제에서 콜아웃을 시스템에 등록해 시스템에서 나가고, 들어오는 모든 네트워크 이벤트를 감시할 것이다. 때문에 콜아웃 드라이버를 작성한다고 할 수 있다. 앞서 만든 WDF Device Object를 사용해 콜아웃을 만들고 필터에 등록한다.
본 설명에서 필터 엔진이란 커널 모드 필터 엔진으로써, TCPIP.sys같은 네트워크 드라이버와 WFP 어플리케이션의 중간에서 인터페이스를 제공하는 Windows 커널 컴포넌트이고, 필터란 WFP 어플리케이션이 필터 엔진에 등록하는 필터 오브젝트를 말한다. 이 용어들을 구분지어 사용했으니 헷갈리지 않길 바란다.
Prepare
먼저 Filter Engine에 대한 핸들을 얻어야 한다. FwpmEngineOpen이라는 API로 커널 모드의 필터 엔진에 대한 핸들을 얻을 수 있다. 본 예제에서는 시스템에 Provider를 등록했는데, 이는 Optional이다. Provider를 등록하면 FwpmFilterEnum이나 FwpmCalloutEnum같은 API로 시스템에 존재하는 모든 필터와 콜아웃 오브젝트 중 내가 등록한 Provider를 가진 애들만 필터링 해 가져올 수 있다. 본 예제에서도 후에 다양한 예제 코드를 만드는 데 사용할 것 같아 Provider를 등록했다.
Provider를 포함해 Filter, Callout 등을 추가, 제거하는 작업들은 모두 FwpmTransactionBegin을 호출한 후 진행되어야 하며, 모든 작업을 마친 후에는 FwpmTransactionCommit을 호출해 작업 내용을 적용시켜야 한다. 만약 에러가 발생했다면 FwpmTransactionAbort로 작업 내용을 모두 되돌릴 수 있다.
InitFilterList는 생성한 필터와 콜아웃 오브젝트의 ID 값을 저장할 리스트를 초기화하는 함수다. 본 예제를 위해 직접 만든 함수이기 때문에 리스트 관련 함수 코드는 깃허브를 참고하길 바란다.
static HANDLE kmfe_handle; // kernel mode filter engine handle
static PDEVICE_OBJECT wfpkm_device;
NTSTATUS InitWfp(
_In_ PDEVICE_OBJECT device_object
)
{
// ...(전략)...
FWPM_PROVIDER wfpkm_provider = { 0, };
//
// open a kernel mode filter engine
//
status = FwpmEngineOpen(NULL, RPC_C_AUTHN_DEFAULT, NULL, NULL, &kmfe_handle);
IF_ERROR(FwpmEngineOpen, EXIT_OF_INIT_WFP);
//
// add a provider of this module to kmfe
//
status = FwpmTransactionBegin(kmfe_handle, 0);
IF_ERROR(FwpmTransactionBegin, CLEANUP_OF_INIT_WFP);
wfpkm_provider.serviceName = (wchar_t*)L"wfpkm";
wfpkm_provider.displayData.name = (wchar_t*)L"wfpkm_example_provider";
wfpkm_provider.displayData.description = (wchar_t*)L"The provider object for wfp-example";
wfpkm_provider.providerKey = WFPKM_PROVIDER_KEY;
status = FwpmProviderAdd(kmfe_handle, &wfpkm_provider, NULL);
IF_ERROR(FwpmProviderAdd, CLEANUP_OF_INIT_WFP);
KdPrint(("[wfpkm] " __FUNCTION__ " - FwpmProviderAdd success"));
//
// create a list to store callout and filter id
//
InitFilterList();
//
// done
//
status = FwpmTransactionCommit(kmfe_handle);
wfpkm_device = device_object;
CLEANUP_OF_INIT_WFP:
if (!NT_SUCCESS(status)) FwpmTransactionAbort(kmfe_handle);
EXIT_OF_INIT_WFP:
return status;
}
Make a Callout Object
콜아웃은 FWPS_CALLOUT과 FWPM_CALLOUT 구조체로 나뉜다. S는 콜아웃 오브젝트의 콜백 함수들을 가지고 있고, M은 콜아웃 키, displayData, Provider 키, applicableLayer 등 속성과 관련한 데이터들을 갖고 있다. S는 FwpsCalloutRegister로 넘겨 해당 콜아웃을 필터 엔진에 등록하고, M은 FwpmCalloutAdd로 해당 콜아웃을 시스템에 추가한다.
특히 S의 classifyFn은 필터 엔진이 콜아웃을 참조해 호출하는 콜백 함수다. 즉 필터에 등록한 Filtering Layer Identifier에 해당하는 트래픽이 발생했을 때 필터 엔진이 콜아웃의 classifyFn을 호출한다는 의미다. notifyFn은 필터에 대한 변경사항이 생겼을 때 호출되는 콜백 함수다.
- 콜아웃 키: 콜아웃 오브젝트를 고유하게 식별할 수 있는 GUID
- displayData: 해당 오브젝트에 대한 주석 용도의 문자열. 필수 값임.
- Provider 키: 옵셔널한 값으로, 이 콜아웃을 만들고 등록한 WFP 어플리케이션의 Provider(쉽게 말해 개발자)를 의미.
- applicableLayer: 해당 콜아웃 오브젝트가 동작할 수 있는 Filtering Layer Identifier.
FWPS_CALLOUT fwps_callout = { 0, };
FWPM_CALLOUT fwpm_callout = { 0, };
UINT32 fwps_callout_id = 0;
UINT32 fwpm_callout_id = 0;
//
// create and register a callout object, add a filter object that has callout
//
status = FwpmTransactionBegin(kmfe_handle, 0);
IF_ERROR(FwpmTransactionBegin, EXIT_OF_INIT_WFP);
// register a callout object to system
fwps_callout.classifyFn = ClassifyFunctionRoutine;
fwps_callout.notifyFn = NotifyFunctionRoutine;
fwps_callout.flowDeleteFn = FlowDeleteFunctionRoutine;
do { status = ExUuidCreate(&fwps_callout.calloutKey); } while (status == STATUS_RETRY);
status = FwpsCalloutRegister(wfpkm_device, &fwps_callout, &fwps_callout_id);
IF_ERROR(FwpsCalloutRegister, CLEANUP_OF_ADD_CALLOUT_TO_LAYER);
KdPrint(("[wfpkm] " __FUNCTION__ " - FwpmCalloutAdd success (callout id = %u)",
fwps_callout_id));
// add a callout object to filter engine
fwpm_callout.calloutKey = fwps_callout.calloutKey;
fwpm_callout.displayData.name = (wchar_t*)L"wfpkm_example_callout";
fwpm_callout.displayData.description = (wchar_t*)L"The callout object for wfp-example";
fwpm_callout.providerKey = (GUID*)&WFPKM_PROVIDER_KEY;
fwpm_callout.applicableLayer = *layer_key;
status = FwpmCalloutAdd(kmfe_handle, &fwpm_callout, NULL, &fwpm_callout_id);
IF_ERROR(FwpmCalloutAdd, CLEANUP_OF_ADD_CALLOUT_TO_LAYER);
KdPrint(("[wfpkm] " __FUNCTION__ " - FwpmCalloutAdd success (callout id = %u)",
fwpm_callout_id));
Make a Filter Object
필터는 FWPM_FILTER 구조체로 표현된다. 필터는 filterKey 멤버를 모두 0으로 채우면 FwpmFilterAdd 함수에서 GUID를 자동으로 생성해 할당한다. 본 예제에서는 Condition을 설정하지 않고 모든 트래픽에 대해서 콜아웃으로 호출하도록 하겠다.
- layerKey: 콜아웃의 applicableLayer처럼 해당 필터가 동작할 Filtering Layer Identifier를 의미한다.
- action.type: 필터의 조건(Condition)에 부합하는 네트워크 이벤트일 경우 취할 행동의 유형(PERMIT/BLOCK 등)
- action.calloutKey: action.type이 콜아웃 참조인 경우 참조할 콜아웃의 콜아웃 키 값.
FWPM_FILTER fwpm_filter = { 0, };
UINT64 fwpm_filter_id = 0;
// fwpm filter key was automatically created by FwpmFilterAdd
fwpm_filter.displayData.name = (wchar_t*)L"wfpkm_example_filter";
fwpm_filter.displayData.description = (wchar_t*)L"The filter object for wfp-example";
fwpm_filter.layerKey = *layer_key;
fwpm_filter.action.type = FWP_ACTION_CALLOUT_UNKNOWN;
fwpm_filter.action.calloutKey = fwps_callout.calloutKey;
status = FwpmFilterAdd(kmfe_handle, &fwpm_filter, NULL, &fwpm_filter_id);
IF_ERROR(FwpmFilterAdd, CLEANUP_OF_ADD_CALLOUT_TO_LAYER);
KdPrint(("[wfpkm] " __FUNCTION__ " - FwpmFilterAdd success (filter id = %llu)",
fwpm_filter_id));
Write the Classify Function
classifyFn은 필터의 조건에 부합되는 트래픽이 발생했을 때 필터 엔진이 호출해주는 콜백 함수다. 코드가 길기 때문에 전체 코드는 깃허브에 올린 예제 코드를 참고하길 바란다. 본 예제의 classifyFn은 들어오고 나가는 트래픽의 로컬 IP 및 포트, 원격 IP 및 포트, PID를 디버그 메시지로 출력하도록 작성했다.
파라메터 중 fixed_values는 해당 트래픽에 대한 실제 데이터가 들어있다. fixed_values의 layerId는 트래픽에 대한 Filtering Layer를 의미하며 FWPS_LAYER를 접두사로 하는 Enum 타입 값이다. 때문에 switch-case를 사용해 어떤 Layer인지 구분할 수 있다. incomingValue는 트래픽에 대한 데이터가 배열로 나열되어 있다.
물론 MS가 몇번째 요소에 어떤 값이 들어있는지 알 수 있도록 각 Layer 별로 incomingValue에 대한 Enum을 만들어뒀다. FWPS_FIELD를 접두사로 사용하며 그 뒤로는 Layer에 해당하는 문장(INBOUND_TRANSPORT_V4 등)이 나오고 마지막엔 IP_LOCAL_ADDRESS 같이 데이터가 의미하는 바를 나타낸다. 때문에 INBOUND TRANSPORT V4에 대한 fixed_values에서 로컬 IP 주소를 보고싶다면 FWPS_FIELD_INBOUND_TRANSPORT_V4_IP_LOCAL_ADDRESS를 incomingValue의 인덱스로 사용하면 된다.
참고로 WFP에서는 Source, Destination 구분이 아니라 Local, Remote로 구분한다. Inbound 트래픽이라면 Source는 Remote, Destination은 Local이 되고, Outbound 트래픽이라면 Source는 Local, Destination은 Remote가 된다.
void NTAPI ClassifyFunctionRoutine(
_In_ const FWPS_INCOMING_VALUES0* fixed_values,
_In_ const FWPS_INCOMING_METADATA_VALUES0* meta_values,
_Inout_opt_ void* layer_data,
_In_opt_ const void* classify_context,
_In_ const FWPS_FILTER3* filter,
_In_ UINT64 flow_context,
_Inout_ FWPS_CLASSIFY_OUT0* classify_out
)
{
switch (fixed_values->layerId) {
//
// inbound ipv4
//
case FWPS_LAYER_INBOUND_TRANSPORT_V4:
local_ipstr = ConvertIpv4ToString(
fixed_values->incomingValue[
FWPS_FIELD_INBOUND_TRANSPORT_V4_IP_LOCAL_ADDRESS
].value.uint32,
_local_ipstr,
64
);
remote_ipstr = ConvertIpv4ToString(
fixed_values->incomingValue[
FWPS_FIELD_INBOUND_TRANSPORT_V4_IP_REMOTE_ADDRESS
].value.uint32,
_remote_ipstr,
64
);
local_port = fixed_values->incomingValue[
FWPS_FIELD_INBOUND_TRANSPORT_V4_IP_LOCAL_PORT
].value.uint16;
remote_port = fixed_values->incomingValue[
FWPS_FIELD_INBOUND_TRANSPORT_V4_IP_REMOTE_PORT
].value.uint16;
direction = "<-";
break;
// ... (중략)...
//
// done
//
KdPrint((
"[wfpkm] " __FUNCTION__ " [%-5u] %s:%u %s %s:%u",
pid,
local_ipstr, local_port,
direction,
remote_ipstr, remote_port
));
}
Write a Function of Finalization of WFP
FinWfp는 WDF Driver의 언로드 루틴에서 호출되는 WFP 클린업 함수다. 시스템과 필터 엔진에 등록했던 모든 콜아웃과 필터 오브젝트, Provider를 제거한다.
NTSTATUS FinWfp()
{
//
// get callout and filter id from list and delete/unregister them
//
status = FwpmTransactionBegin(kmfe_handle, 0);
IF_ERROR(FwpmTransactionBegin, EXIT_OF_FIN_WFP);
while (NT_SUCCESS(TakeFilterItem(&item))) {
KdPrint(("[wfpkm] " __FUNCTION__ " - FwpmFilterDeleteById(%llu)",
item.filter_id));
status = FwpmFilterDeleteById(kmfe_handle, item.filter_id);
IF_ERROR(FwpmFilterDeleteById, CLEANUP_OF_FIN_WFP);
KdPrint(("[wfpkm] " __FUNCTION__ " - FwpmCalloutDeleteById(%u)",
item.fwpm_callout_id));
status = FwpmCalloutDeleteById(kmfe_handle, item.fwpm_callout_id);
IF_ERROR(FwpmCalloutDeleteById, CLEANUP_OF_FIN_WFP);
KdPrint(("[wfpkm] " __FUNCTION__ " - FwpsCalloutUnregisterById(%u)",
item.fwps_callout_id));
status = FwpsCalloutUnregisterById(item.fwps_callout_id);
IF_ERROR(FwpsCalloutUnregisterById, CLEANUP_OF_FIN_WFP);
KdPrint(("[wfpkm] " __FUNCTION__ " - RemoveFilterItem"));
status = RemoveFilterItem(&item);
IF_ERROR(RemoveFilterItem, CLEANUP_OF_FIN_WFP);
}
//
// done
//
status = FwpmTransactionCommit(kmfe_handle);
FwpmProviderDeleteByKey(kmfe_handle, &WFPKM_PROVIDER_KEY);
FwpmEngineClose(kmfe_handle);
CLEANUP_OF_FIN_WFP:
if (!NT_SUCCESS(status)) FwpmTransactionAbort(kmfe_handle);
EXIT_OF_FIN_WFP:
return status;
}
Run
cmd를 관리자 권한으로 실행하고 sc 명령으로 드라이버 서비스를 생성한다.
> sc create wfp-example binpath= C:\test\wfpkm.sys displayname= wfp-example start= demand type= kernel
DebugView를 실행시켜 커널 디버그 아웃풋 캡처를 활성화하고 sc start로 서비스를 실행시키면 시스템에서 나가고 들어오는 모든 트래픽의 로컬 IP 및 포트, 원격 IP 및 포트, 해당 트래픽을 발생시킨 프로세스의 PID를 볼 수 있다. 본 예제에서 "->"는 Outbound, "<-"는 Inbound를 의미한다.
Close
이상으로 WFP 어플리케이션을 만드는 데 필요한 가장 기본적이고 최소한의 제작법을 알아봤다. 본 예제의 실행 결과를 보면 알겠지만 생각보다 PID가 0으로 뜨는 트래픽이 많다. 때문에 실제 업무에서 WFP 어플리케이션을 제작할 때는 PID가 0인 트래픽을 예외처리 할 필요가 있어보인다.
다음 글에서는 필터의 조건을 사용해 방화벽 어플리케이션을 만드는 예제에 대해서 작성해보려 한다.
Github
main 브랜치에 코드가 안 보인다면 dev 브랜치를 찾아보길 바란다.
https://github.com/geun-yeong/wfp-example