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.
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).
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
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). |
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.
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 );Parameters
Function returns an NTSTATUS containing the creation result.
NoteNtCreatePort checks whether (MaxConnectionInfoLength <= 0x104) and (MaxMessageLength <= 0x148).
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
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.
Copyright (c) Ladislav Zezula 2009