Minifilter知識總結


Minifilter注重功能實現,不注重更深層的IRP之類的操控

編寫Minifilter的第一件事是向過濾器宣告我們的微過濾器的存在。這里所謂的微過濾器是符合過濾器標准的過濾組件,它其實是一組回調函數,這組回調函數向過濾管理器注冊之后,在合適的時機(比如,要求的文件操作發生時)過濾管理器就會以合適的方式來調用某個回調函數。
如果我們編寫這個回調函數中的內容,就可以對文件系統加以過濾了。這比花很多精力去綁定各種設備要簡單得多,因為復雜的任務都在過濾管理器里面做了。

就基本很明白了,既然是一套回調函數,也就是過濾器,那就要先注冊,然后開啟,然后等等
一個是用FltRegisterFilter注冊一個微過濾器;另一個是用函數FltStartFiltering來開始過濾。

NTSTATUS
FLTAPI
FltRegisterFilter (
__in PDRIVER_OBJECT Driver,
__in CONST FLT_REGISTRATION *Registration,
__deref_out PFLT_FILTER *RetFilter
);
第1個參數是本驅動的驅動對象,是在入口函數DriverEntry中作為參數傳入的。
第2個參數就是一個宣告注冊信息的結構,這個結構內含描述這個過濾器的全部信息。在這里,稱為“微過濾器注冊結構”。
第3個參數(RetFilter)是一個返回參數。返回注冊成功的微過濾句柄。微過濾器句柄非常常用,一般都保存在全局變量中以備后用,在下面調用函數FltStartFiltering就需要這個句柄作為參數。顯而易見,調用FltRegisterFilter本身並不復雜,問題在於要填寫一個合法的FLT_REGISTRATION結構。這個結構在下一小節中介紹。

NTSTATUS
FLTAPI
FltStartFiltering (
__in PFLT_FILTER Filter
);
非常簡單,此函數只有一個參數,就是調用FltRegisterFilter時返回的微過濾器句柄。一般情況下,這個函數的調用會成功;如果失敗,除了放棄過濾,幾乎別無選擇。

 

微過濾器的數據結構
注冊微過濾器時,我們填寫了一個名為微過濾器注冊結構(FLT_REGISTRATION)的數據結構,定義如下:
typedef struct _FLT_REGISTRATION {
USHORT Size; //結構的大小
USHORT Version; //結構的版本
FLT_REGISTRATION_FLAGS Flags; //微過濾器的標志位
CONST FLT_CONTEXT_REGISTRATION *ContextRegistration;

CONST FLT_OPERATION_REGISTRATION *OperationRegistration; //操作回調函數,這是重點里面的重點
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; //格式化上下文清理回調
} FLT_REGISTRATION, *PFLT_REGISTRATION;
第1個域Size表示FLT_REGISTRTION結構的大小,當然大小就是sizeof(FLT_REGISTRATION)。微軟習慣在Windows內核的數據結構前面加上大小,以便容易排錯。

第2個域Version是FLT_REGISTRATION結構的版本號。對於這個域,讀者不需要多加考慮,直接按照慣例填寫FLT_REGISTRATION_VERSION即可。

第3個域Flags是標志位,標志是否要收到這一類的操作。但是有趣的是,這個域只有兩種設置方法:一種設置為NULL,不起任何作用;另一種則設置為FLT_REGISTRATION_DO_NOT_SUPPORT_SERVICE_STOP,代表當停止服務時Minifilter不會響應且不會調用到FilterUnloadCallback,即使FilterUnloadCallback並不是NULL。

第4個域Context Registration:上下文注冊,注冊處理上下文的函數。

第5個域OperationRegistration:操作回調函數集注冊。這是最重要的一個域,我們將要過濾的文件操作回調函數寫在其中,可以定義所有功能代碼對應的回調函數,舉例如下:
const FLT_OPERATION_REGISTRATION Callbacks[] = {
{ IRP_MJ_CREATE,0,NPPreCreate,XxxPostCreate },
//填寫要過濾的定義集合
{ IRP_MJ_OPERATION_END }
};
有關FLT_OPERATION_REGISTRATION這個結構,后面會做更詳細的解說。

第6個域FilterUnloadCallback:驅動卸載回調函數。在這個驅動被停止時,這個函數被調用,代表要釋放程序內的資源以結束過濾行為。這個域可以設置為NULL。

第7個域InstanceSetupCallback:實例安裝回調函數,當一個卷實例要加載時會通知此回調處理。這個域可以設置為NULL。

第8個域InstanceQueryTeardownCallback:控制實例銷毀函數,這個回調只有在一個手工解除綁定的請求時被調用。這個域可以設置成NULL。

第9個域InstanceTeardownStartCallback:實例解綁定函數,當調用時代表已經決定要解除綁定,這個域可以設置為NULL。

第10個域InstanceTeardownCompleteCallback:實例解綁定完成函數,當確定時調用解除綁定后的完成函數,這個域可以設置為NULL。
還有一些域因為使用不多,本書略去,有興趣的讀者可以自己參考相關文檔。筆者習慣將它們設置成NULL。

 

 

數據結構實例:
const FLT_REGISTRATION FilterRegistration = {
sizeof( FLT_REGISTRATION ), // Size
FLT_REGISTRATION_VERSION, // Version
0, // Flags
NULL, // Context
Callbacks, // Operation callbacks
NPUnload, // MiniFilterUnload
NPInstanceSetup, // InstanceSetup
NPInstanceQueryTeardown, // InstanceQueryTeardown
NPInstanceTeardownStart, // InstanceTeardownStart
NPInstanceTeardownComplete, // InstanceTeardownComplete
NULL, // GenerateFileName
NULL, // GenerateDestinationFileName
NULL // NormalizeNameComponent
};
其中,最重要的就是CallBacks。這是一個回調函數數組,在其中可以處理所有的請求。但是處理方式可以和以前請求過濾時有所不同,以前處理的是IRP,其實有兩種處理:一種是在請求完成之前就進行處理;另一種是用事件等待請求完成之后,或者在完成函數進行處理。前一種適合要攔截請求本身的情況,后一種適合要攔截請求之后返回結果的情況。在Minifilter中,這兩種過濾被截然地分在兩個回調函數中,一個稱作預操作回調(Pre-Operation Function),另一個稱為后操作回調(Post-Operation Function),下面是一個例子:
const FLT_OPERATION_REGISTRATION Callbacks[] =
{
{
IRP_MJ_CREATE,
0,
NPPreCreate, // 生成預操作回調函數
NPPostCreate // 生成后操作回調函數
},
{
IRP_MJ_WRITE,
FLT_OPERATION_REGISTRATION_SKIP_CACHED_IO,
NPPreWrite,
NPPostWrite
},
{IRP_MJ_OPERATION_END}
};


Callbacks數組內存儲的數據結構為 FLT_OPERATION_REGISTRATION 的數組,用意是把需要做過濾的請求一個一個聲明出來,每個都包括了預操作回調函數與后操作回調函數,宣告過后通過注冊就能使IRP包順利地通過這邊指定的函數來做處理了。當有多個微過濾器時,IRP會通過每一個微過濾器的預操作函數與后操作函數,除非IRP傳遞到中途被直接返回不再傳遞下去。
可以看到,這個數組的每個元素由4個部分組成。第1個域是請求的主功能號,這是我們熟知的。第2個域是一個標志位,有3種寫法:第1種是寫0,這個標志僅僅對讀/寫回調有用,所以對生成請求的處理直接寫0即可;第2種是寫FLT_OPERATION_REGISTRATION_SKIP_CACHED_IO,表示不過濾緩沖讀/寫請求;
第3種是寫FLT_OPERATION_REGISTRATION_SKIP_PAGING_IO,表示不過濾分頁讀寫請求。接下來的兩個域就是預操作回調函數和后操作回調函數。
請注意最后一個元素必須是{IRP_MJ_OPERATION_END},否則過濾器無法知道到底有多少個元素。
我們已經看到了上面有若干個回調函數,其中有一些回調函數在操作函數集Callbacks中,還有一些回調函數就直接在微過濾器注冊結構中。下面的任務就是逐個實現這些函數。

卸載回調函數 FltUnregisterFilter
NTSTATUS
NPUnload (
__in FLT_FILTER_UNLOAD_FLAGS Flags
)
{
UNREFERENCED_PARAMETER( Flags );
PAGED_CODE();

PT_DBG_PRINT( PTDBG_TRACE_ROUTINES,("NPminifilter!NPUnload: Entered\n") );

FltCloseCommunicationPort( gServerPort );
FltUnregisterFilter( gFilterHandle );
return STATUS_SUCCESS;
}
這個函數的主要工作是釋放資源,FltUnregisterFilter與FltRegisterFilter互相對應,FltUnregisterFilter是用來釋放已注冊的微過濾器在Windows內核內部所使用的資源。

 

預操作數與后操作數的講解

預操作回調函數
我們針對IRP_MJ_CREATE這個主功能號來設置預操作函數與后操作函數,當系統接收到標識為IRP_MJ_CREATE也就是試圖生成或者打開文件時,自然就會調用預操作函數與后操作函數。
NPPreCreate就是我們設置的預回調函數。這個函數有3個參數,其中第一個參數是一個PFLT_CALLBACK_DATA的指針,PFLT_CALLBACK_DATA稱為回調數據包,這個數據包內含有這個請求相關的全部信息。正是因為有了這個參數,所以不再直接讀取IRP的信息了。這個函數的參數中不再有IRP指針。
FLT_PREOP_CALLBACK_STATUS
NPPreCreate (
__inout PFLT_CALLBACK_DATA Data,
__in PCFLT_RELATED_OBJECTS FltObjects,
__deref_out_opt PVOID *CompletionContext
)
{
//緩沖區,用來獲得文件名
char FileName[260] = "X:";

NTSTATUS status;
PFLT_FILE_NAME_INFORMATION nameInfo;

//未使用的參數,用宏掩蓋使之不發生編譯警告
UNREFERENCED_PARAMETER( FltObjects );
UNREFERENCED_PARAMETER( CompletionContext );

//檢測可分頁代碼
PAGED_CODE();

__try
{
//獲取文件名信息,獲取文件名和解析文件名等幾個函數在本節內稍后的內容中介紹
status = FltGetFileNameInformation( Data,
FLT_FILE_NAME_NORMALIZED| FLT_FILE_NAME_QUERY_DEFAULT,
&nameInfo );

if (NT_SUCCESS( status ))
{
//判斷是否阻擋
if (gCommand == ENUM_BLOCK)
{
//如果成功了,解析文件名信息,然后比較其中是否含有NOTEPAD.EXE這個子字符串
FltParseFileNameInformation( nameInfo );

//將字符串轉換為CHAR大寫以利於比對字符串
if (NPUnicodeStringToChar(&nameInfo->Name, FileName))
{
if (strstr(FileName, "NOTEPAD.EXE") > 0)
{
//填寫拒絕
Data->IoStatus.Status = STATUS_ACCESS_DENIED;
Data->IoStatus.Information = 0;

FltReleaseFileNameInformation( nameInfo );

//返回請求已經結束,也就是說不用再下傳了
return FLT_PREOP_COMPLETE;
}
}
}

//釋放名字資源
FltReleaseFileNameInformation( nameInfo );
}
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
DbgPrint("NPPreCreate EXCEPTION_EXECUTE_HANDLER\n");
}

return FLT_PREOP_SUCCESS_WITH_CALLBACK;
}

這是一個很簡單的預操作函數,它的主要作用就是盡可能地解析目前的文件名稱,然后判斷這個名稱是否符合我們需要的條件。我們要做的目的是限制名為"notepad.exe"的文件被使用,任何此文件的操作比如說讀取、刪除、覆蓋、重命名、執行等,必定都會先調用到打開請求。因此,我們在這邊做個簡單的判斷,試圖去分辨出目前系統操作的文件是否就正符合我們所尋找的條件。

上面用到一個自定義函數NPUnicodeStringToChar。該函數將UNICODE_STRING轉換為全大寫的CHAR數組,一邊搜索子字符串“NOTEPAD.EXE”。其中使用了內核API函數RtlUpperChar轉換大小寫,請讀者試試自己實現這個函數。

下面是回調數據包的定義:
typedef struct _FLT_CALLBACK_DATA {
FLT_CALLBACK_DATA_FLAGS Flags;
PETHREAD CONST Thread;
PFLT_IO_PARAMETER_BLOCK CONST Iopb;
IO_STATUS_BLOCK IoStatus;
struct _FLT_TAG_DATA_BUFFER *TagData;

union {
struct {

LIST_ENTRY QueueLinks;
PVOID QueueContext[2];
};
PVOID FilterContext[4];
};

KPROCESSOR_MODE RequestorMode;
} FLT_CALLBACK_DATA, *PFLT_CALLBACK_DATA;
回調數據包結構代表了一個I/O操作。過濾管理器與微過濾驅動都使用這個結構來初始化與處理I/O操作,內含許多嵌套結構定義,這些定義可以在WDK標准頭文檔fltkernel.h中找到更多的數據。這個結構可以說是Minifilter的基礎。以前在sfilter中,我們從IRP指針及IRP的當前棧空間指針中得到許多信息,比如寫請求的長度等,現在我們如何讓得到這些信息呢?
請注意Iopb域,這是一個PFLT_IO_PARAMETER_BLOCK指針,這個數據結構的定義如下:
typedef struct _FLT_IO_PARAMETER_BLOCK {
ULONG IrpFlags;
UCHAR MajorFunction;
UCHAR MinorFunction;
UCHAR OperationFlags;
UCHAR Reserved;
PFILE_OBJECT TargetFileObject;
PFLT_INSTANCE TargetInstance;
FLT_PARAMETERS Parameters;
} FLT_IO_PARAMETER_BLOCK, *PFLT_IO_PARAMETER_BLOCK;
在這里讀者就可以找到以前熟悉的許多信息了,包括主功能號,次功能號和文件對象指針等。此外,其中還有一個結構為FLT_PARAMETERS的參數域,這個數據結構是一個聯合體,應用得域根據不同的主功能號而不同,數據結構如下:
typedef union _FLT_PARAMETERS {
省略…

//
// IRP_MJ_WRITE
//

struct {
ULONG Length; //Length of transfer
ULONG POINTER_ALIGNMENT Key;
LARGE_INTEGER ByteOffset; //Offset to write to

PVOID WriteBuffer; //Not in IO_STACK_LOCATION parameters list
PMDL MdlAddress; //Mdl address for the buffer (maybe NULL)
} Write;

省略…
} FLT_PARAMETERS, *PFLT_PARAMETERS;
從這里就很容易找到寫請求包括的寫入位置、長度和緩沖區等相關參數。
這里再介紹一下解析文件路徑所需要調用的函數。第一個函數是FltGetFileNameInformation,原型如下:
NTSTATUS
FLTAPI
FltGetFileNameInformation (
__in PFLT_CALLBACK_DATA CallbackData,
__in FLT_FILE_NAME_OPTIONS NameOptions,
__deref_out PFLT_FILE_NAME_INFORMATION *FileNameInformation
);
這個函數可以取得一個文件或目錄的文件名信息的結構,第二個函數名為FltParseFileNameInformation,原型如下:
NTSTATUS
FLTAPI
FltParseFileNameInformation (
__inout PFLT_FILE_NAME_INFORMATION FileNameInformation
);
通過FltParseFileNameInformation這個函數可以的到一個含有路徑名稱與文件名稱的字符串,我們再用字符串替換與對比便可以輕易的找出路徑內是否有NOTEPAD.EXE等字符串。在決定否決這個請求之后,我們采用常見的與填寫IRP的IoStatus域完全一樣的方法否決這次請求,相關代碼如下:
Data->IoStatus.Status = STATUS_ACCESS_DENIED;
Data->IoStatus.Information = 0;
FltReleaseFileNameInformation( nameInfo );
return FLT_PREOP_COMPLETE;
這段程序代碼主要是要告訴過濾器,這個請求要即刻返回失敗。即代表了這個IRP不會往下處理。

后操作回調函數
當IRP完成返回時就會通知后操作回調函數,例如,若不要讓文件新建成功,可以通過FltCancelFileOpen這個操作,這是因為我們在預操作函數內就已經過濾該行為且設定返回值的動作了,並不需要在這里重做一次。下面這個后處理回調函數對程序功能本身並沒有意義,僅僅作為后處理回調函數寫法的說明在這里展現給讀者。
FLT_POSTOP_CALLBACK_STATUS
NPPostCreate (
__inout PFLT_CALLBACK_DATA Data,
__in PCFLT_RELATED_OBJECTS FltObjects,
__in_opt PVOID CompletionContext,
__in FLT_POST_OPERATION_FLAGS Flags
)
{
FLT_POSTOP_CALLBACK_STATUS returnStatus = FLT_POSTOP_FINISHED_PROCESSING;
PFLT_FILE_NAME_INFORMATION nameInfo;
NTSTATUS status;

UNREFERENCED_PARAMETER( CompletionContext );
UNREFERENCED_PARAMETER( Flags );

//
// If this create was failing anyway, don't bother scanning now.
//

if (!NT_SUCCESS( Data->IoStatus.Status ) ||
(STATUS_REPARSE == Data->IoStatus.Status)) {

return FLT_POSTOP_FINISHED_PROCESSING;
}

//從回調數據包里面獲得名字信息
status = FltGetFileNameInformation( Data,
FLT_FILE_NAME_NORMALIZED | FLT_FILE_NAME_QUERY_DEFAULT,
&nameInfo );

if (!NT_SUCCESS( status )) {
return FLT_POSTOP_FINISHED_PROCESSING;
}

return returnStatus;
}
返回FLT_POSTOP_FINISHED_PROCESSING代表Minifilter已經完成對I/O的所有處理,並返回控制給過濾管理器。

 

通信方式

 

 

考慮到內核態與用戶態之間的互動,以前的做法是使用用戶態的API函數DeviceIOControl結合在內核模塊中的處理控制請求來實現雙方數據的傳遞。但是在Minifilter中卻不同,Minifilter有內建支持API提供給開發者來使用,這里就先針對這些API來作介紹。
這個方法有個稱呼叫“通信端口”(Communication Port),顧名思義,就是先定義一個通道名稱,通過雙邊已經定義好的通信端口來做數據上的溝通,使用上很像socket或管道(pipe)之類的通信程序設計。

 

PSECURITY_DESCRIPTOR sd;
OBJECT_ATTRIBUTES oa;

 

status = FltBuildDefaultSecurityDescriptor( &sd, FLT_PORT_ALL_ACCESS );

 

if (!NT_SUCCESS( status )) {
goto final;
}


RtlInitUnicodeString( &uniString, MINISPY_PORT_NAME );

 

InitializeObjectAttributes( &oa,
&uniString,
OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE,
NULL,
sd );

 

status = FltCreateCommunicationPort( gFilterHandle,
&gServerPort,
&oa,
NULL,
NPMiniConnect,
NPMiniDisconnect,
NPMiniMessage,
1 );
也就是利用FltBuildDefaultSecurityDescriptor先初始化安全描述符,再初始化對象,再以某個端口名進行通信,再RING3相應的函數對應相應的例如NPMiniConnect這樣的函數進行通信
FltBuildDefaultSecurityDescriptor以FLT_PORT_ALL_ACCESS權限來產生一個安全性的敘述子,MINISPY_PORT_NAME是剛剛所講的通信端口定義的名稱,通過InitializeObjectAttributes來初始化對象屬性(OBJECT_ATTRIBUTES),接下來便是注冊這個通信端口以及所需要使用到的函數。
這里必須提供3個回調函數,類似於以前我們為了實現通信所寫的控制請求的分發函數。這3個回調函數分別是NPMiniConnect、NPMiniDisconnect、NPMiniMessage。
NPMiniConnect是用戶態與內核態建立連接時內核會調用到的函數。
NPMiniDisconnect是用戶態與內核態連接結束時內核會調用到的函數。
NPMiniMessage是用戶態與內核態傳送數據時內核會調用到的函數。

 

用戶態不再使用CreateFile和DeviceIoControl這系列的API,Minifilter有專門的API提供給用戶態程序使用。
相關的API主要有兩個:FilterConnectCommunicationPort和 FilterSendMessage,
FilterConnectCommunicationPort可以調用到我們提供的NPMiniConnect函數,
FilterSendMessage調用到相對應的NPMiniMessage。
一對一關系很容易理解。至於參數都是PVOID的指針,開發時兩邊程序通過自定義的數據結構,傳入指針即可將數據傳入或者取出。
HRESULT
WINAPI
  FilterConnectCommunicationPort(
    IN LPCWSTR   lpPortName,
    IN DWORD   dwOptions,
    IN LPVOID   lpContext OPTIONAL,
    IN DWORD   dwSizeOfContext,
    IN LPSECURITY_ATTRIBUTES  lpSecurityAttributes OPTIONAL,
    OUT HANDLE  *hPort
);
各參數說明如下:
lpPortName:寬字符字符串,比如L"NPPort";
dwOptions:目前沒有使用,設為0;
lpContext:通過此參數可以傳入上下文數據給Minifilter的connect routine;
dwSizeOfContext:上下文的數據大小,單位為byte;
lpSecurityAttributes:通過此API,只要傳入已定義的Port名稱,就可以得到句柄;
此外,WDK定義的FilterSendMessage原型如下:
HRESULT
WINAPI
  FilterSendMessage(
    __in HANDLE   hPort,
    __in_bcount  LPVOID  lpInBuffer,
    __in DWORD   dwInBufferSize,
    __out_bcount_part_opt LPVOID  lpOutBuffer,
    __in DWORD   dwOutBufferSize,
    __out LPDWORD  lpBytesReturned
); 
各參數說明如下:
hPort:連接端口名稱,寬字符字符串;
lpInBuffer:輸入緩沖區,將定義好的結構用指針傳入;
dwInBufferSize:輸入緩沖區的大小;
lpOutBuffer:輸出緩沖區,既可以傳入數據也可以取得返回的數據;
dwOutBufferSize:輸出緩沖區的大小;
lpBytesReturned:當FilterSendMessage調用成功時會返回一個標志lpOutBuffer大小的值。

 


說了這么多具體怎么做呢,我們可以做一個dll作為Ring3和內核的通信,RING3調用就行了,但是必須得有FltUser.h fltLib.lib fltMgr.lib 記住的一點是初始化的完成在DLL_PROCESS_ATTACH中

以下是示例
示例:
/ dllmain.cpp : 定義 DLL 應用程序的入口點。
#include "stdafx.h"
#include <FltUser.h>
#pragma comment(lib, "fltLib.lib")
typedef enum _USER_COMMAND_
{
USER_PASS = 0,
USER_BLOCK
}USER_COMMAND;

 

#define MINI_FILTER_PORT_NAME L"\\MiniFilterPort"
HANDLE __PortHandle = INVALID_HANDLE_VALUE;

 


int InitialCommunicationPort(void);
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{

InitialCommunicationPort();
break;
}
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
{

 

if (__PortHandle != NULL)
{
CloseHandle(__PortHandle);
__PortHandle = NULL;
}
break;
}
}
return TRUE;
}
int InitialCommunicationPort(void)
{
//MiniFilter 的通信機制 --->??
DWORD Status = FilterConnectCommunicationPort(
MINI_FILTER_PORT_NAME, //監聽套接字
0,
NULL,
0,
NULL,
&__PortHandle);

 

if (Status != S_OK) {
return Status;
}
return 0;
}
int MiniFilterDeviceIoControl(USER_COMMAND UserCommand)
{
DWORD ReturnLength = 0;
DWORD Status = 0;
//同步還是異步 ?? ---> ??
Status = FilterSendMessage(
__PortHandle,
&UserCommand,
sizeof(USER_COMMAND),
NULL,
NULL,
&ReturnLength);

 

if (Status != S_OK)
{
return Status;
}
return 0;
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM