?? 漫談兼容內核之十八:windows的lpc機制.txt
字號:
CRequest = (PEPORT_CONNECT_REQUEST_MESSAGE)&Request->Message;
memcpy(&Header, &Request->Message, sizeof(LPC_MESSAGE));
Header.DataSize = CRequest->ConnectDataLength;
Header.MessageSize = Header.DataSize + sizeof(LPC_MESSAGE);
Status = MmCopyToCaller(LpcMessage, &Header, sizeof(LPC_MESSAGE));
if (NT_SUCCESS(Status))
{
Status = MmCopyToCaller((PVOID)(LpcMessage + 1),
CRequest->ConnectData, CRequest->ConnectDataLength);
}
}
else
{
Status = MmCopyToCaller(LpcMessage, &Request->Message,
Request->Message.MessageSize);
}
. . . . . .
if (Request->Message.MessageType == LPC_CONNECTION_REQUEST)
{
KeAcquireSpinLock(&Port->Lock, &oldIrql);
EiEnqueueConnectMessagePort(Port, Request);
KeReleaseSpinLock(&Port->Lock, oldIrql);
}
else
{
ExFreePool(Request);
}
/* Dereference the port */
ObDereferenceObject(Port);
return(STATUS_SUCCESS);
}[/code]
在Port機制的實現中,NtReplyWaitReceivePortEx()是個經常用到的函數。剛才我們看到的是從NtListenPort()逐層調用下來的,所以沒有應答報文需要發送,直接就進入等待接收報文的階段了,但在多數情況下是有應答報文要發送的,服務線程運行的典型情景就是“接收-處理-應答-再接收”,所以把應答和再接收合并在一起。因此,我們也應看一下在有應答報文要發送時的操作,就是代碼中if (LpcReply != NULL && !Disconnected)語句里面的操作。
當然,如果端口已經處于斷開狀態、即Disconnected為TRUE,那就不發送應答報文了。從代碼中可以看出,應答報文的發送是通過EiReplyOrRequestPort()完成的。完成以后還要向對方端口的“信號量”執行一次V操作,即增量為1的KeReleaseSemaphore()操作,如果對方線程正在睡眠等待就把它喚醒。后面這一步屬于線程間同步,讀者想必已經熟悉了,這里看一下EiReplyOrRequestPort()的代碼:
[code][NtReplyWaitReceivePort() > NtReplyWaitReceivePortEx()> EiReplyOrRequestPort()]
NTSTATUS STDCALL
EiReplyOrRequestPort (IN PEPORT Port,
IN PLPC_MESSAGE LpcReply,
IN ULONG MessageType,
IN PEPORT Sender)
{
. . . . . .
MessageReply = ExAllocatePoolWithTag(NonPagedPool, sizeof(QUEUEDMESSAGE),
TAG_LPC_MESSAGE);
MessageReply->Sender = Sender;
if (LpcReply != NULL)
{
memcpy(&MessageReply->Message, LpcReply, LpcReply->MessageSize);
}
MessageReply->Message.ClientId.UniqueProcess = PsGetCurrentProcessId();
MessageReply->Message.ClientId.UniqueThread = PsGetCurrentThreadId();
MessageReply->Message.MessageType = MessageType;
MessageReply->Message.MessageId = InterlockedIncrementUL(&LpcpNextMessageId);
KeAcquireSpinLock(&Port->Lock, &oldIrql);
EiEnqueueMessagePort(Port, MessageReply);
KeReleaseSpinLock(&Port->Lock, oldIrql);
return(STATUS_SUCCESS);
}[/code]
參數LpcReply是從上面傳下來的指針,指向一個LPC_MESSAGE數據結構,這就是待發送的報文。下面是LPC_MESSAGE數據結構的格式定義:
[code]typedef struct _LPC_MESSAGE {
USHORT DataSize;
USHORT MessageSize;
USHORT MessageType;
USHORT VirtualRangesOffset;
CLIENT_ID ClientId;
ULONG MessageId;
ULONG SectionSize;
UCHAR Data[ANYSIZE_ARRAY];
} LPC_MESSAGE, *PLPC_MESSAGE;[/code]
實際上這只是報文的頭部,后面的不定長數組Data[ANYSIZE_ARRAY]才是具體的報文。這里ANYSIZE_ARRAY定義為1,其實定義成0也可以,只是表示從這兒開始才是具體的報文內容。具體報文的數據結構可以由使用者自行定義,接收方根據頭部的MessageType就可以知道收到的是什么報文。不過Port機制定義了兩種特殊的報文用于建立連接的過程:
[code]typedef struct _EPORT_CONNECT_REQUEST_MESSAGE
{
LPC_MESSAGE MessageHeader;
PEPROCESS ConnectingProcess;
struct _SECTION_OBJECT* SendSectionObject;
LARGE_INTEGER SendSectionOffset;
ULONG SendViewSize;
ULONG ConnectDataLength;
UCHAR ConnectData[0];
} EPORT_CONNECT_REQUEST_MESSAGE;[/code]
這就是“連接請求”報文,其第一個成分是一個LPC_MESSAGE數據結構,那就是報文的頭部。下面的“連接應答”報文也是一樣。前面的參數LpcReply表面上是LPC_MESSAGE指針,但是實際上卻可以是具體報文的指針。注意最后的ConnectData[0]表示在這個數據結構的后面還可以有不定長的數據,報文頭部的DataSize就反映了這部分數據的大小。所以,報文頭部的Data[ANYSIZE_ARRAY]實際上是具體報文的正身,而具體報文如“連接請求”中的ConnectData[0]則是隨同具體報文發送的數據。不過這是額外的數據,因為建立連接所必需的信息已經體現在報文正身的各個字段中了。
[code]typedef struct _EPORT_CONNECT_REPLY_MESSAGE
{
LPC_MESSAGE MessageHeader;
PVOID SendServerViewBase;
ULONG ReceiveClientViewSize;
PVOID ReceiveClientViewBase;
ULONG MaximumMessageSize;
ULONG ConnectDataLength;
UCHAR ConnectData[0];
} EPORT_CONNECT_REPLY_MESSAGE;[/code]
最后,包括額外數據在內的報文總長度是有限的(256字節),如果是大塊數據就要通過共享緩沖區發送。
再看上面EiReplyOrRequestPort()的代碼。它先在內核空間分配一個QUEUEDMESSAGE數據結構作為報文的“容器”,再把需要發送的報文拷貝到這個數據結構中。注意這里的LpcReply有可能是從用戶空間傳下來的,相應的緩沖區也在用戶空間,所以需要把它搬到內核空間的緩沖區中。然后,真正的“發送”操作其實只是把這個數據結構掛入目標端口的接收報文隊列中,這是由EiEnqueueMessagePort()完成的。注意這里的LpcpNextMessageId是報文的序號,每次遞增。還要說明,拷貝到QUEUEDMESSAGE數據結構中的只是報文本身,而不包括通過共享內存區傳遞的數據,這正是為什么要使用共享內存區的原因。
回到NtReplyWaitReceivePortEx()的代碼。由于沒有應答報文要發送,這里直接就通過KeWaitForSingleObject()進入了睡眠等待。當這個線程接收到了報文而被喚醒(在這里我們忽略超時的可能)時,端口的報文隊列里已經有了報文,所以通過EiDequeueMessagePort()從隊列中摘下一個報文的數據結構。如果這是個“連接請求”報文,就把它復制到用戶提供的緩沖區中,代碼中的兩次調用MmCopyToCaller()就是分別復制報文的正身及其所附帶的數據。其實不是“連接請求”報文也要復制,只不過此時只復制報文的正身。
然后,對于“連接請求”報文,這里還通過EiEnqueueConnectMessagePort()把它掛入端口的ConnectQueueListHead隊列,這是為隨后的“接受連接”操作、即系統調用NtAcceptConnectPort()留下一個參考,后面我們就會看到其作用。
不過,我們這兒講的只是如果接收到了連接請求就要做些什么操作,但是實際上此刻還沒有客戶方提出連接請求,所以服務方線程只是睡眠等待。
至此,服務方已經作好了準備,就等著客戶方的連接請求了。如前所述,客戶方通過系統調用NtConnectPort()向命名的連接端口請求連接。注意這里連接的目標就是一個連接端口,而并不指定某個具體的進程。哪一個進程在這個連接端口上執行了NtListenPort()并因此而受到阻塞正在等待,這請求就實際上是發給那個進程(線程)的。
請求連接的一方對于將來要發送多大的數據量應該是心里有數的。如果數據量不大,那就可以作為附加信息隨同報文(在同一個緩沖區中)一起發送。但是,要是數據量比較大(報文總長大于256字節),那就要通過共享內存區“發送”,因為否則的話一來傳輸效率低下,二來報文緩沖區的大小也不好靜態地安排確定。所以,在期望的發送數據量比較大時要準備好一個共享內存區(Section)用于數據發送。這是要由請求連接的一方、即客戶方做好準備,通過調用參數傳給NtConnectPort()的。為此客戶方要準備好兩個數據結構,就是LPC_SECTION_WRITE和LPC_SECTION_READ,把有關共享內存區的信息填寫在前一個數據結構中,再把指向這兩個數據結構的指針作為參數傳給NtConnectPort()。
這兩個數據結構的定義為:
[code]typedef struct _LPC_SECTION_WRITE {
ULONG Length;
HANDLE SectionHandle;
ULONG SectionOffset;
ULONG ViewSize;
PVOID ViewBase;
PVOID TargetViewBase;
} LPC_SECTION_WRITE;
typedef struct _LPC_SECTION_READ {
ULONG Length;
ULONG ViewSize;
PVOID ViewBase;
} LPC_SECTION_READ;[/code]
注意客戶方只提供(和映射)用于它寫入(發送)的共享內存區,而反方向的共享內存區則由服務方提供(和映射)。所以LPC_SECTION_READ數據結構只是用來獲取映射后的結果。
下面看NtConnectPort()的代碼。
[code]NTSTATUS STDCALL
NtConnectPort (PHANDLE UnsafeConnectedPortHandle,
PUNICODE_STRING PortName,
PSECURITY_QUALITY_OF_SERVICE Qos,
PLPC_SECTION_WRITE UnsafeWriteMap,
PLPC_SECTION_READ UnsafeReadMap,
PULONG UnsafeMaximumMessageSize,
PVOID UnsafeConnectData,
PULONG UnsafeConnectDataLength)
{
. . . . . .
/* Copy in write map and partially validate. */
. . . . . .
/* Handle connection data. */
. . . . . .
/* Reference the named port. */
Status = ObReferenceObjectByName (PortName, 0, NULL, PORT_ALL_ACCESS,
LpcPortObjectType, UserMode, NULL, (PVOID*)&NamedPort);
. . . . . .
/* Reference the send section object. */
if (WriteMap.SectionHandle != INVALID_HANDLE_VALUE)
{
Status = ObReferenceObjectByHandle(WriteMap.SectionHandle,
SECTION_MAP_READ | SECTION_MAP_WRITE,
MmSectionObjectType, UserMode, (PVOID*)&SectionObject, NULL);
. . . . . .
}
else
{
SectionObject = NULL;
}
/* Do the connection establishment. */
Status = EiConnectPort(&ConnectedPort, NamedPort, SectionObject, SectionOffset,
WriteMap.ViewSize, &WriteMap.ViewBase, &WriteMap.TargetViewBase,
&ReadMap.ViewSize, &ReadMap.ViewBase, &MaximumMessageSize,
ConnectData, &ConnectDataLength);
. . . . . .
/* Do some initial cleanup. */
if (SectionObject != NULL)
{
ObDereferenceObject(SectionObject);
SectionObject = NULL;
}
ObDereferenceObject(NamedPort);
NamedPort = NULL;
/* Copy the data back to the caller. */
. . . . . .
Status = ObInsertObject(ConnectedPort, NULL, PORT_ALL_ACCESS,
0, NULL, &ConnectedPortHandle);
. . . . . .
Status = MmCopyToCaller(UnsafeConnectedPortHandle,
&ConnectedPortHandle, sizeof(HANDLE));
. . . . . .
if (UnsafeWriteMap != NULL)
{
Status = MmCopyToCaller(UnsafeWriteMap,
&WriteMap, sizeof(LPC_SECTION_WRITE));
. . . . . .
}
if (UnsafeReadMap != NULL)
{
Status = MmCopyToCaller(UnsafeReadMap,
&ReadMap, sizeof(LPC_SECTION_READ));
. . . . . .
}
. . . . . .
return(STATUS_SUCCESS);
?? 快捷鍵說明
復制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -