LPC Communication

LPC (Local Procedure Call) is a portion of Windows NT kernel, used for fast communication between threads or processes. It can be also used for communication between kernel mode and user mode components (e.g. between driver and user application). This article contains description and an example how to use LPC communication.

Note: This article is for advanced programmers. Reader should have knowledge of processes, threads and interprocess communication. An advantage (but not necessity) is at least partial knowledge of native NT API and experiences with programming usig NT DDK.

Using native API only

LPC is a part of NT-based Windows only (Windows NT 3.51, NT 4.0, Windows 2000, XP, 2003 Server and Longhorn) and even more, it is undocumented (yet). However, LPC APIs are compatible across all NT versions. It is a part of the native API only, which are exported by the ntdll.dll library (or ntoskrnl.exe in kernel mode). For building application using ntdll.dll, you'll need the ntdll.lib library (part of the NT DDK).

How it works

A communication using LPC means transferring data blocks (LPC messages) between client and server. Client and server can be different thread, different process and may also run in different processor mode (client may be a kernel mode driver, server may be an user application).

The communication runs using LPC port, created by the server. When creating port, the server specifies port name and security descriptor. After successful port create, the server awaits a connection from client(s). When a client requests LPC connection, the server can check the client by CLIENT_ID and decide whether to accept or refuse the connection. If the connection is accepted, the client can send a LPC message and optionally wait for a reply from the server.

Every communication between client and server goes with the LPC message header, represented by the PORT_MESSAGE structure in C language.

There are two ways how the data can be transferred between server and client

Short LPC message - the data are sent with the message header (and immediately follow the PORT_MESSAGE structure, which contains data size). Maximum size of data that can be transferred whis way is 0x130 bytes.

Larger data blocks must be sent using memory mapped section, created by NtCreateSection. When data is sent to a different process, the LPC facility ensures remapping to the target process address space. Maximum size of the data sent this way is limited by 32-bit memory space (maximum section size).

The following scheme shows the communication between client and server:

Client Server Server creates LPC port using the NtCreatePort function. When creating port, server specifies name which must be used by the client for establishing a connection. Optionally, SECURITY_DESCRIPTOR can be used to specify access rights to the created port object. Server awaits a connection from client(s) using NtListenPort. A client requests connection using the NtConnectPort function. When calling this function, the client specifies name of the port to be connected to. Server receives LPC message header (the MESSAGE_HEADER structure). The struct contains the CLIENT_ID, containing thread ID and process ID of the client. Using CLIENT_ID, server decides whenher the connection will be accepted or refused (using NtAcceptConnectPort). Server completes the connection process using NtCompleteConnectPort. After calling this function, the client will send the LPC message to the server (if the server previously accepted the connection). The NtConnectPort function returns result of the connection attempt. The return value can be STATUS_SUCCESS (connection established), STATUS_ACCESS_DENIED (the client does not have access to the port), STATUS_OBJECT_NAME_NOT_FOUND (port does not exist), STATUS_PORT_CONNECTION_REFUSED (server refused the connection), or another status code. If the connection has been established, the client received port handle. The server awaits data from the client (using NtReplyWaitReceivePort). The client sends data to the server using NtRequestPort API (does not wait for a reply) or NtRequestWaitReplyPort (waits for reply). The server processes data from the client. If the client awaits a reply, server sends it using NtReplyPort. This API sends the reply back to the client. The client finished communication by closing the handle returned when the connection has been established (NtClose). The communication is over. Server side LPC communication is complete, server awaits another connection request (NtListenPort).

Structures and functions

The following part describes some important structures and APIs using by LPC facility. The description is not complete, for complete LPC API description and structures refer to the LPC example.

typedef struct _PORT_MESSAGE { union { struct { USHORT DataLength; // Length of data following the header (bytes) USHORT TotalLength; // Length of data + sizeof(PORT_MESSAGE) } s1; ULONG Length; } u1; union { struct { USHORT Type; USHORT DataInfoOffset; } s2; ULONG ZeroInit; } u2; union { CLIENT_ID ClientId; double DoNotUseThisField; // Force quadword alignment }; ULONG MessageId; // Identifier of the particular message instance union { ULONG_PTR ClientViewSize; // Size of section created by the sender (in bytes) ULONG CallbackId; // }; } PORT_MESSAGE, *PPORT_MESSAGE;

The PORT_MESSAGE structure is used to describe data sent using LPC. Every communication always goes with this structure. Data may follow the structure, their size is limited to 0x130 bytes.

typedef struct _PORT_VIEW { ULONG Length; // Size of this structure HANDLE SectionHandle; // Handle to section object with // SECTION_MAP_WRITE and SECTION_MAP_READ ULONG SectionOffset; // The offset in the section to map a view for // the port data area. The offset must be aligned // with the allocation granularity of the system. ULONG ViewSize; // The size of the view (in bytes) PVOID ViewBase; // The base address of the view in the creator // PVOID ViewRemoteBase; // The base address of the view in the process // connected to the port. } PORT_VIEW, *PPORT_VIEW;

The PORT_VIEW structure is used to describe memory mapped section (created using NtCreateSection or CreateFileMapping), that will be sent to the other side through LPC. The section must be backed by the system pagefile (file handle must be INVALID_HANDLE_VALUE). The structure must be filled by the process that has created the section. The section size must be a multiplier of system allocation granularity.

typedef struct _REMOTE_PORT_VIEW { ULONG Length; // Size of this structure ULONG ViewSize; // The size of the view (bytes) PVOID ViewBase; // Base address of the view } REMOTE_PORT_VIEW, *PREMOTE_PORT_VIEW;

When LPC message sender sends data using memory mapped section, the LPC facility remaps the section to the address space of the target process and gives the memory view decription in the REMOTE_PORT_VIEW structure.

NtCreatePort

The NtCreatePort is used to create a LPC port.

NTSTATUS NTAPI NtCreatePort( OUT PHANDLE PortHandle, IN POBJECT_ATTRIBUTES ObjectAttributes, IN ULONG MaxConnectionInfoLength, IN ULONG MaxMessageLength, IN ULONG MaxPoolUsage );

PortHandle [out] Points to a variable that will receive the port object handle if the call is successful. ObjectAttributes [in] Name and a security descriptor for the port. The port name must begin with the backslash character (e.g. "\\MyTestPort"). When the LPC communication will run between two different processes, the security descriptor should contain an empty DACL. MaxConnectionInfoLength [in] Maximum size of the data that can be send as short LPC message. MaxMessageLength [in] Maximum size of the message that can be sent to the LPC port. MaxPoolUsage [in] Maximum size of non-paged memory that can be used for storing LPC message. Zero means default value.

Function returns an NTSTATUS containing the creation result.

NtCreatePort checks whether (MaxConnectionInfoLength <= 0x104) and (MaxMessageLength <= 0x148).

Rest of APIs and an example

I really don't want to rewrite description for all functions from Ntdll.h, as the Ntdll.h is also available in the LPC example. This ZIP file contains

The ntdll.h header containing the complete description of LPC data structures and APIs

header containing the complete description of LPC data structures and APIs An example how to send short LPC message (organized as MS Visual Studio.NET 2005 project)

An example of communication using memory mapped section

Note for 64-bit systems

If you are running 32-bit applications using LPC functions under 64-bit Windows, you will encounter various bad functionalty. As it turned out, the layer between 32-bit Ntdll.dll and 64-bit Ntdll.dll does not translate the layout of PORT_MESSAGE structure. As consequence, kernel API can't recognize format of the PORT_MESSAGE structure and usually returns STATUS_INVALID_PARAMETER (0xC000000D). For 64-bit systems, always use 64-bit build of the example.

References

Gary Nebbet: Native NT API

The Undocumented NT functions (http://www.ntinternals.net)

Copyright (c) Ladislav Zezula 2009