天天看点

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

LPC(本地过程调用)服务

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

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

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

1  LPC结构模型

LPC 通信模型,如图:

windows 内核原理与实现读书笔记之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连接的交互过程,如图:

windows 内核原理与实现读书笔记之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 删除端口对象。