📄
字号:
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 + -