windows 内核原理与实现读书笔记之LPC

LPC(本地过程调用)服务

本地过程调用(LPC,Local Procedure Call),主要用于操作系统各个组件之间进行通信,或者用户模式程序与系统组件之间通信。

LPC工作方式时消息传递,允许两个进程进行双向通信,在Windows的主要应用如下:

  1. Windows 应用程序与系统进程,包括Windows环境子系统之间的通信。
  2. 用户模式程序与内核模式组件之间的通信。
  3. 当RPC(远程过程调用,Remote Procecure Call)的两端在同一个系统时,RPC通信转化位LPC通信。

1  LPC结构模型

LPC 通信模型,如图:

LPC允许一对多通信模型。LPC连接端口只接受连接请求,通信端口只接受数据请求和服务。

LPC进程双方建立连接后,可以给对方发送消息。每个端口对象都有一个消息队列,用来保存发送给端口对象的消息。LPC允许使用两种方法来传输数据:

  1. 当最大消息长度不超过256字节时,要传输的数据之间在消息头后面。发送进程将数据拷贝到系统地址空间中,然后接收进程将数据拷贝到它的进程地址空间中。
  2. 若最大消息长度超过256字节,那么客户在连接到端口对象时,可以指定一个内存区对象,并且在客户进程中映射一个视图,以用于向服务器发送大数据。服务器也可以指定一个内存区对象,用于向客户发送大数据。

Windows的LPC使用消息队列来保存发送给一个端口对象的消息,使用信号量对象来同步发送和接收操作。

LPC系统服务(适用于Windows 2000以后的版本)

系统服务

简要说明

NtAcceptConnectPort

服务器进程利用该服务来接受或拒绝一个连接请求

NtCompleteConnectPort

服务器进程在调用了NtAcceptConnectProt后,在调用该服务以便唤醒客户线程

NtConnectPort

通过该服务,客户进程可通过名称来连接一个服务器进程

NtCreatPort

服务器进程利用该服务创建一个LPC连接端口

NtCreateWaitablePort

服务器进程利用该服务来创建一个LPC连接端口,它允许异步方式等待LPC消息,即等待客户连接请求的到来

NtListenProt

利用NtReplyWaitReceivePort服务来等待来自客户端的连接请求

NtReplyPort

发送一个应答消息

NtReplyWaitReceivePort

发送一个应答消息,等待接收一个客户消息

NtReplyWaitReceivePortEx

功能同NtReplyWaitReceivePort,可指定超时值

NtReplyWaitReplyPort

发送一个应答信息,并等待此应答消息的应答消息

NtRequestPort

发送一个请求消息

NtRequestWaitReplyPort

发送一个请求信息,并等待此请求的应答消息

NtSecureConnectPort

通过此服务,客户进程可通过名称来连接到一个服务器进程,它允许指定服务器进程的安全标识符

2  LPC端口和LPC消息

typedef struct _LPCP_PORT_OBJECT
{
    struct _LPCP_PORT_OBJECT *ConnectionPort;
    struct _LPCP_PORT_OBJECT *ConnectedPort;
    LPCP_PORT_QUEUE MsgQueue;
    CLIENT_ID Creator;
    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;//仅适用于通信端口
    };
    ULONG MaxMessageLength;
    ULONG MaxConnectionInfoLength;
    ULONG Flags;
    KEVENT WaitEvent;//仅适用于可等待的端口
} LPCP_PORT_OBJECT, *PLPCP_PORT_OBJECT;

ConnectionPort : 指向当前端口对象的连接对象,对于服务器通信端口或客户通信端口,指向相关联的连接端口对象;连接端口的ConnectionPort 指向其自身。
ConnectedPort: 指向通信的对方。
MsgQueue: 表示端口对象的消息队列。
Creator : 表示创建线程的ID。
ClientSectionBase: 指向客户内存区对象的视图基地址。
ServerSectionBase : 指向服务器内存区对象的视图基地址
PortContext : 是一个由服务器进程管理和解释的指针域。
ClientThread : 仅用于服务器通信端口对象,在建立连接过程中用于记录客户线程对象。
SecurityQos 、StaticSecurity : 用于建立端口对象的安全环境。
LpcReplayChainHead 、LpcDataInfoChainHead : 两个链表头,用于通信端口存放应答消息。
ServerProcess : 仅用于连接端口,以便销毁端口对象时可以解除映射。
MaxMessageLength 、MaxConnectionInfoLength :表示该端口的最大消息长度和最大连接信息长度。
Flags : 表示该端口对象的类型和状态标志,其最低4位标识端口的类型。
WaitEvent : 仅用于可等待的端口,此事件的信号状态标识该端口是否有消息到达。

LPC子系统在初始化时,创建了名称为”Port”和“WaitablePort”的两种LPC端口对象类型。两者的主要区别是,后者包含了LPCP_PORT_OBJECT 的WaitEvent,并且从非换页内存池分配,前者从换页内存池分配。

LPC通信的消息结构:

typedef struct _LPCP_MESSAGE
{
     union
     {
          LIST_ENTRY Entry;
          struct
          {
               SINGLE_LIST_ENTRY FreeEntry;
               ULONG Reserved0;
          };
     };

     PVOID SenderPort;
     PETHREAD RepliedToThread; //发送应答时填充,因而接收方可以解除对该线程的引用
     PVOID PortContext; //从发送方的通信端口中获得
     PORT_MESSAGE Request;
} LPCP_MESSAGE, *PLPCP_MESSAGE;

Entry : 是一个消息被插入消息队列时的链表节点对象。
SenderPort : 指向发送方的端口对象。
Request : 一个PORT_MESSAGE 结构,指定了消息的长度、类型、消息ID等信息。

3  LPC通讯模型的实现

LPC通信过程分为两个阶段: 建立连接阶段和数据传输阶段。

在建立连接阶段,首先服务器进程调用NtCreateProt 或NtCreateWaitablePort 创建一个LPC端口,原型如下:

NTSYSAPI
NTSTATUS
NTAPI
NtCreatePort(
  OUT PHANDLE             PortHandle,
  IN POBJECT_ATTRIBUTES   ObjectAttributes,
  IN ULONG                MaxConnectInfoLength,
  IN ULONG                MaxDataLength,
  IN OUT PULONG           Reserved OPTIONAL );


NTSYSAPI
NTSTATUS
NTAPI
NtCreateWaitablePort(
      __out PHANDLE PortHandle,
      __in POBJECT_ATTRIBUTES ObjectAttributes,
      __in ULONG MaxConnectionInfoLength,
      __in ULONG MaxMessageLength,
      __in_opt ULONG MaxPoolUsage
  );

两个函数都是调用LpcpCreatePort函数。LpcpCreatePort根据参数名称,创建一个LPC端口对象。它首先检查参数,然后调用ObCreateObject 创建一个LpcPortObjectType 或LpcWaitablePortObjectType 类型的内核对象。然后初始化对象中的域,包括最大消息长度和最大连接信息长度。最大消息长度不得超过PORT_MAXIMUM_MESSAGE_LENGTH,即256字节。如果调用者指定了名称,则此端口对象为连接端口对象(SERVER_CONNECTION_PORT),否则是非连接得通信端口对象(UNCONNECTED_COMMUNICATION_PORT)。最后,调用ObInsertObject 将新建得端口对象插入到当前进程得句柄表中。

服务器进程调用NtListenPort 监听连接请求,NtListen的监听是一个同步过程,它调用NtReplyWaitReceivePort ,直至该函数接收到一个LPC连接请求,或者返回不成功。函数原型如下:

NTSYSAPI
NTSTATUS
NTAPI
NtListenPort(
  IN HANDLE PortHandle,
  OUT PLPC_MESSAGE ConnectionRequest );

NTSYSAPI
NTSTATUS
NTAPI
NtReplyWaitReceivePort(
  IN HANDLE               PortHandle,
  OUT PHANDLE             ReceivePortHandle OPTIONAL,
  IN PLPC_MESSAGE         Reply OPTIONAL,
  OUT PLPC_MESSAGE        IncomingRequest );

LPC服务器和客户进程都可以使用NtReplyWaitReceivePort来接收消息。

NtReplyWaitReceivePort调用NtReplyWaitReceivePortEx ,NtReplyWaitReceivePortEx 首先检查其参数,尤其是ReplyMessage 和PortHandle。然后确定接收者的端口对象(即ReceivePort)。如果指定了ReplyMessage参数,则从此消息中定位到目标线程,并调用KeReleaseSemaphore,唤醒正在等待的目标线程。NtListenPort 将ReplyMessage 参数设置为0,所以,服务器监听连接请求时不需要处理ReplyMessage 参数。待完成参数处理后,NtReplyWaitReceivePortEx 在接收端口对象的消息队列的信号量对象上等待,即

ReceivePort->MsgQueue.Semaphore对象。等待成功以后从ReceivePort 的消息队列中读取消息,并复制到参数ReceiveMessage指定的消息对象中。

NtListen 直接在LPC连接端口对象的消息队列信号量上等待,直到有客户向此端口对象发送连接请求信息。在客户进程中,通过NtConnectPort 来完成。

NTSYSAPI
NTSTATUS
NTAPI
NtConnectPort(
  OUT PHANDLE             ClientPortHandle,
  IN PUNICODE_STRING      ServerPortName,
  IN PSECURITY_QUALITY_OF_SERVICE SecurityQos,
  IN OUT PLPC_SECTION_OWNER_MEMORY ClientSharedMemory OPTIONAL,
  OUT PLPC_SECTION_MEMORY ServerSharedMemory OPTIONAL,
  OUT PULONG              MaximumMessageLength OPTIONAL,
  IN                      ConnectionInfo OPTIONAL,
  IN PULONG               ConnectionInfoLength OPTIONAL );

NtConnectPort 调用NtSecureConnectPort。NtSecureConnectPort 首先检查参数。然后根据PortName 获得目标连接对象,然后调用ObCreateObject 创建一个新的LPC端口对象。如果ClientView 指定了内存区对象,则在当前进程中映射一个视图。然后初始化新的LPC端口对象,并构造一个请求连接的LPC消息,将它插入到LPC连接对象的消息队列中。服务器连接对象的消息队列的信号量对象调用KeReleaseSemaphore,唤醒服务器进程正在监听连接对象的线程。然后再当前线程的LpcReplySemaphore 信号量上等待。

服务器进程会向客户线程的LpcReplySemaphore 信号量发送信号。

NtSecureConnectPort 的到服务器对象的信号通知后,将客户的通信端口对象插入到进程的句柄表中。

服务器进程接下来调用NtAcceptConnectPort 和NtCompleteConnectPort ,接受或拒绝客户的连接请求。原型如下:

NTSTATUS
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
  );



NTSYSAPI
NTSTATUS
NTAPI
NtCompleteConnectPort(
  IN HANDLE               PortHandle );

ConnectionRequest 是NtListenPort 返回的连接请求消息。

AcceptConnection 表明应该接受还是拒绝此请求。

ServerView 和ClientView 用于内存区对象的视图映射。

至此,客户进程与服务器进程之间的LPC连接已经建立起来。建立LPC连接的交互过程,如图:

两个进程建立连接后,双方可以发送或接收应答消息。LPC通信的函数简要说明:

  1. NtRequestPort ,向指定的端口发送一个请求消息。该函数首先找到目标端口,将消息插入到该端口的消息队列中,然后调用KeReleaseSemaphore 使消息队列的信号量计数器增1。
  2. NtRequestWaitReplyPort,向指定的端口发送一个请求消息,然后等待应答。该函数首先检查要发送的消息,并利用消息中的信息找到要唤醒的线程,将请求消息告诉对方,并唤醒它,然后等待对方的应答。
  3. LpcRequestPort,直接使用端口对象的地址
  4. LpcRequestWaitReplyPort/LpcRequestWaitReplyPortEx 直接使用端口对象的地址
  5. NtReplyWaitReceivePort/NtReplyWaitReceivePortEx , 如果参数中指定了应答消息,则首先根据应答消息中的信息,向应答目标方传送应答消息,并唤醒对方。然后再端口对象的消息队列信号量上等待,以接收消息。NtListenPort 使用此函数来监听客户进程的连接请求。
  6. NtReplyPort ,发送一个应答消息。参数PortHandle 指定了原来接收到请求消息的端口对象,参数ReplyMessage 指定了要送回的应答消息。
  7. NtReplyWaitReplyPort, 发送一个应答消息,并且等待对此应答消息的应答。

以下几点请留意:

  1. 在传输消息时,不管是请求消息还是应答消息,都要进行消息拷贝,是通过LpcpMoveMessage 完成的。
  2. 应答消息必定是针对前一个已经传输的消息,线程对象ETHREAD的LpcReplyMessageId 记录了该消息的ID。
  3. 消息ID通过一个简单的计数器来产生,参加LpcpGenerateMessageId。
  4. 如果一个函数要等待应答消息,则在等待之前,要将当前线程的LpcReplyChain 节点加入到适当端口对象的LpcReplyChanHead链表中。
  5. LPC使用了全局锁LpcpLock。
  6. 如果这些函数的调用者是用户模式代码,则必须严格检查参数的有效性。

LpcpClosePort 关闭端口连接,LpcpDeletePort 删除端口对象。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值