LPC 简单应用
概念性的东西就不介绍了。这里主要介绍LPC 函数以及一个代码实例。总的来说,LPC与套接字类似,对比着理解会比较容易。一个进程创建一个LPC 端口对象(服务器),然后等待其他进程(客户)连接过来,服务器接收连接后,服务器与客户端均建立一个通信端口对象,通信端口对象没有名称。理论上说,通信端口处理数据请求,连接端口处理连接请求,就像套接字中的监听套接字与通信套接字一样,但是通过实例代码和阅读wrk 中lpc 的相关代码我们发现事实并非如此,后面将介绍相关内容。
消息传递的类型
1. 短消息,最大消息长度< 256,要传递的消息直接跟在消息头后面。将进行两次数据拷贝:发送方--->内核---->接收方.
2. 长消息,利用内存区对象,在两个进程地址空间中映射同一块内存,需要注意的是,由于映射地址空间的时候,映射粒度通常为64KB,因此即使所需要的地址空间小于该值,系统依然会按照粒度进行映射。
消息传递的同步
LPC 使用消息队列来保存发送给一个端口对象的消息,使用信号量对象来同步发送和接收操作。
LPC相关的API
| NtCreatePort | 创建一个LPC 连接端口 |
| NtConnectPort | 连接服务器进程创建的连接端口 |
| NtListenPort | 服务器进程监听所有的连接请求 |
| NtAcceptConnectPort | 接收或者拒绝一个连接请求 |
| NtCompleteConnectPort | 调用NtAcceptConnectPort后需要调用该函数唤醒客户端线程 |
| NtRequestPort | 发送一个请求消息 |
| NtRequestWaitReplyPort | 发送一个请求消息并等待回复 |
| NtReplyPort | 发送一个应答消息 |
| NtReplyWaitReplyPort | 发送一个回复并等待对方回复 |
| NtReplyWaitReceivePort | (可选地)发送一个应答消息,等待接收一个客户消息 |
| NtSecureConnectPort | 客户进程可以通过名称来连接到一个服务器进程,允许指定服务器进程的安全标识符 |
| NtReplyWaitReceivePortEx | 同NtReplyWaitReceivePort,可以指定超时值 |
| NtCreateWaitablePort | 服务器用来创建一个LPC 连接端口,允许异步方式等待LPC 消息,即等待客户连接请求的到来 |
函数声明
NTSTATUS
NtReplyWaitReceivePortEx(
__in HANDLE PortHandle,
__out_opt PVOID *PortContext,
__in_opt PPORT_MESSAGE ReplyMessage,
__out PPORT_MESSAGE ReceiveMessage,
__in_opt PLARGE_INTEGER Timeout
)
NTSTATUS
NtReplyWaitReceivePort (
__in HANDLE PortHandle,
__out_opt PVOID *PortContext ,
__in_opt PPORT_MESSAGE ReplyMessage,
__out PPORT_MESSAGE ReceiveMessage
)
NTSTATUS
NtReplyPort (
__in HANDLE PortHandle,
__in PPORT_MESSAGE ReplyMessage
)
NTSTATUS
NtReplyWaitReplyPort (
__in HANDLE PortHandle,
__inout PPORT_MESSAGE ReplyMessage
)
NTSTATUS
NtRequestPort (
__in HANDLE PortHandle,
__in PPORT_MESSAGE RequestMessage
)
NTSTATUS
NtRequestPort (
__in HANDLE PortHandle,
__in PPORT_MESSAGE RequestMessage
)
NTSYSCALLAPI
NTSTATUS
NTAPI
NtCreatePort(
__out PHANDLE PortHandle,
__in POBJECT_ATTRIBUTES ObjectAttributes,
__in ULONG MaxConnectionInfoLength,
__in ULONG MaxMessageLength,
__in_opt ULONG MaxPoolUsage
)
NTSYSCALLAPI
NTSTATUS
NTAPI
NtCreateWaitablePort(
__out PHANDLE PortHandle,
__in POBJECT_ATTRIBUTES ObjectAttributes,
__in ULONG MaxConnectionInfoLength,
__in ULONG MaxMessageLength,
__in_opt ULONG MaxPoolUsage
);
NTSYSCALLAPI
NTSTATUS
NTAPI
NtConnectPort(
__out PHANDLE PortHandle,
__in PUNICODE_STRING PortName,
__in PSECURITY_QUALITY_OF_SERVICE SecurityQos,
__inout_opt PPORT_VIEW ClientView,
__inout_opt PREMOTE_PORT_VIEW ServerView,
__out_opt PULONG MaxMessageLength,
__inout_opt PVOID ConnectionInformation,
__inout_opt PULONG ConnectionInformationLength
);
NTSYSCALLAPI
NTSTATUS
NTAPI
NtSecureConnectPort(
__out PHANDLE PortHandle,
__in PUNICODE_STRING PortName,
__in PSECURITY_QUALITY_OF_SERVICE SecurityQos,
__inout_opt PPORT_VIEW ClientView,
__in_opt PSID RequiredServerSid,
__inout_opt PREMOTE_PORT_VIEW ServerView,
__out_opt PULONG MaxMessageLength,
__inout_opt PVOID ConnectionInformation,
__inout_opt PULONG ConnectionInformationLength
);
NTSYSCALLAPI
NTSTATUS
NTAPI
NtListenPort(
__in HANDLE PortHandle,
__out PPORT_MESSAGE ConnectionRequest
);
NTSYSCALLAPI
NTSTATUS
NTAPI
NtAcceptConnectPort(
__out PHANDLE PortHandle,
__in_opt PVOID PortContext,
__in PPORT_MESSAGE ConnectionRequest,
__in BOOLEAN AcceptConnection,
__inout_opt PPORT_VIEW ServerView,
__out_opt PREMOTE_PORT_VIEW ClientView
);
NTSYSCALLAPI
NTSTATUS
NTAPI
NtCompleteConnectPort(
__in HANDLE PortHandle
);
内核端口对象
typedef struct _LPCP_PORT_OBJECT {
struct _LPCP_PORT_OBJECT *ConnectionPort; // 连接端口对象
struct _LPCP_PORT_OBJECT *ConnectedPort; // 通信的对方
LPCP_PORT_QUEUE MsgQueue; // 端口对象的消息队列
CLIENT_ID Creator; // 创建线程的ID
PVOID ClientSectionBase;
PVOID ServerSectionBase; // 分别指的客户和服务器的内存区对象的视图基地址
PVOID PortContext; // 一个由服务进程管理和解释的指针域
PETHREAD ClientThread; // 服务器通信端口
SECURITY_QUALITY_OF_SERVICE SecurityQos;
SECURITY_CLIENT_CONTEXT StaticSecurity;
LIST_ENTRY LpcReplyChainHead; // 仅适用于通信端口
LIST_ENTRY LpcDataInfoChainHead; // 仅适用于通信端口-----用于通信端口存放应答消息
union {
PEPROCESS ServerProcess; // 服务器连接端口对象,服务器进程对象指针
PEPROCESS MappingProcess; // 适用于通信端口,存放映射内存区视图的进程,以便销毁端口对象时可以解除映射
};
USHORT MaxMessageLength;
USHORT MaxConnectionInfoLength;
ULONG Flags; // 用于标识四种端口对象
KEVENT WaitEvent; // 仅适用于可等待的端口,可等待的端口对象分配于非分页内存,否则分页内存
} LPCP_PORT_OBJECT, *PLPCP_PORT_OBJECT;
通信中用到的消息结构
typedef struct _LPCP_MESSAGE {
union {
LIST_ENTRY Entry; // 一个消息被插入消息队列时的链表节点对象
struct {
SINGLE_LIST_ENTRY FreeEntry; //
ULONG Reserved0;
};
};
PVOID SenderPort; // 指向发送方的端口对象
PETHREAD RepliedToThread; // 发送应答时填充,因而对方可以解除对该线程的引用
PVOID PortContext; // 从发送方的通信端口中获得,指向发送方端口对象的PortContext
PORT_MESSAGE Request; // 制订了消息的长度、类型、消息ID等
} LPCP_MESSAGE, *PLPCP_MESSAGE;
就像我们常见的Windows 的数据结构一样,这个结构是消息头,真正的数据跟在此结构后面。
通信过程
通信建立
1. 创建LPC 端口对象
NtCreatePort 或 NtCreateWaitablePort--->LpcpCreatePort(指定名称,连接端口对象,否则, 通信端口对象)
2. 服务器端等待连接
NtListenPort--->NtReplyWaitReceivePort(回复消息传空)---->NtReplyWaitReceivePortEx(超时为永久等待)---->等待ReceivePort->MsgQueue.Semaphore对象
NtReplyWaitReceivePortEx 检查ReplyMessage 和 PortHandle 参数。指定了ReplyMessage,从此消息中定位到目标线程,并调用KeReleaseSemaphore,唤醒正在等待的目标线程。
3. 客户端连接服务器
NtConnectPort---->NtSecureConnectPort---->根据名称找到服务器端口对象,调用ObCreateObject 创建一个新的LPC 端口对象,如果ClientView 指定了内存区对象,在当前进程中映射一个视图。然后初始化新建的LPC 端口对象,并构造一个请求连接的LPC 消息,将它插入到LPC 连接对象的消息队列中,调用KeReleaseSemaphore释放其信号量(即上面服务器端等待的连接端口消息队列中的信号量)并在刚生成的通信端口对象的LpcReplySemaphore信号量等待---->服务器等待线程被唤醒,从消息队列中得到LPC连接请求消息,NtListenPort 返回,此时服务器程序可以意识到一个连接请求,并判断是否接收该请求。
4. 服务器拒绝或者同意客户端连接
NtAcceptConnectPort---->AccepConnection 指示是否接收连接,ConnectionRequest 指示回复的对象,即NtListenPort返回的连接请求消息,ServerView 用于服务器向客户进程传送大数据消息。如果接收连接,函数创建一个通信端口对象,并与客户进程创建的通信端口对象链接起来(即相互保存对象指针),映射客户端指定的内存区对象的一个视图。如果拒绝,函数内部将直接调用KeReleaseSemaphore 使客户端返回失败。同意连接的话,NtAcceptConnectPort 并不唤醒客户线程。
5. NtCompleteConnectPort
PortHandle 指向服务器的通信端口对象,再次检查客户线程的状态和端口对象的一些状态,针对客户线程的LpcReplySemaphore 信号量成员调用KeReleaseSemaphore以唤醒客户线程。
通信过程
NtRequestPort--->目标端口--->放入消息队列---->KeReleaseSemaphore
NtRequestWaitReplyPort--->目标端口--->放入消息队列---->KeReleaseSemaphore--->等待应答
NtReplyWaitReceivePort/NtReplyWaitReceivePortEx,如果指定应答消息,首先根据应答消息向应答目标方传送应答消息,并唤醒对方。然后在端口对象的消息队列信号量上等待,以接收消息。
NtReplyPort--->发送一个应答消息,PortHandle 指定了原来接收到的请求消息的端口对象。ReplyMessage结构的消息类型设置为LPC_REPLY,ClientId和MessageId字段用于标识等待此应答的线程。 如果目标线程实际上等待此应答消息,则将应答消息复制到线程的消息缓冲器中,并且满足线程的等待。
如果线程不等待答复或正在等待对某个其他MessageId的答复,则该消息被放置在连接到由PortHandle参数指定的通信端口的端口的消息队列中,并返回STATUS_REPLY_MESSAGE_MISMATCH。
NtReplyWaitReplyPort---->发送一个应答消息,等待对此消息的应答。
另外需要注意的是
当客户端给服务器发送消息的时候,如果是NtRequest….函数,消息将会发送到服务器连接端口对应队列的信号量上。服务器给客户端发送消息不存在这种情况。
当客户端调用NtReply…给服务器恢复消息的时候,由于服务器实现调用了Nt…WaitReply,因此该线程在线程对象的LpcReplySemaphore 信号量上等待消息,消息可以正常接收。
例如如下程序:
服务器等待连接:
NtStatus = NtListenPort(Server->ListenPortHandle, &LPCRequertMessage.Header);
服务器等待已经连接的通信端口的消息
NtStatus = NtReplyWaitReceivePort(*ServerCommunicationPortHandle, NULL, NULL, &RecvMessage.Header);
客户端发送消息代码
NtStatus = NtRequestPort(ClientCommunicationPortHandle, &LPCSendMessage.Header);
由于NtListenPort 内部同样是等待的连接端口的消息队列的信号量。此时,就会出现客户端必须调用NtRequest 两次服务器才能接收到消息的情况。当把代码修改为:
服务器等待已经连接的通信端口的消息
NtStatus = NtRequestWaitReplyPort(*ServerCommunicationPortHandle, &SendMsg.Header, &RecvMessage.Header);
客户端发送消息
NtReplyWaitReceivePort(ClientCommunicationPortHandle, NULL, NULL, &LPCSendMessage.Header);// 得到服务器等待线程的CLIENT_ID以及MessageId
wcscpy_s(LPCSendMessage.MessageText, LPC_MESSAGE_LENGTH, wzSendMsg); //这里进行消息的拷贝
LPCSendMessage.Command = MsgType;
NtStatus = NtReplyPort(ClientCommunicationPortHandle, &LPCSendMessage.Header);// 给服务器等待线程回复而不是通信端口队列的信号量下一篇再给出一个比较完整的LPC实例
参考链接
潘爱民 windows 内核原理与实现
3502

被折叠的 条评论
为什么被折叠?



