CppGuide社区 CppGuide社区
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Windows Native API编程
  • 🔥Windows x64 ShellCode入门教程
  • 🔥Windows Shellcode实战
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
  • Linux内核

    • 心中的内核 —— 在阅读内核代码之前先理解内核
    • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
    • TCP源码实现超详细注释版.pdf (opens new window)
GitHub (opens new window)
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Windows Native API编程
  • 🔥Windows x64 ShellCode入门教程
  • 🔥Windows Shellcode实战
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
  • Linux内核

    • 心中的内核 —— 在阅读内核代码之前先理解内核
    • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
    • TCP源码实现超详细注释版.pdf (opens new window)
GitHub (opens new window)
  • Windows Native API编程 专栏说明
  • 第1章 原生API(Native API)开发入门
  • 第2章 原生API(Native API)基础
  • 第3章 原生应用程序(Native Applications)
  • 第4章:系统信息
  • 第5章:进程
  • 第6章:线程
  • 第7章:对象与句柄
  • 第 8 章:内存(第一部分)
  • 第9章:I/O
  • 第10章:ALPC
    • 10.1 ALPC 概念
    • 10.2 简单客户端/服务器(Simple Client/Server)
    • 10.3 创建服务器端口(Creating Server Ports)
    • 10.4 连接到端口(Connecting to Ports)
    • 10.5 消息属性(Message Attributes)
      • 10.5.1 安全属性(Security Attribute,ALPCFLGMSGSECATTR)
      • 10.5.2 上下文属性(Context Attribute,ALPCFLGMSGCONTEXTATTR)
      • 10.5.3 句柄属性(Handle Attribute,ALPCFLGMSGHANDLEATTR)
      • 10.5.4 令牌属性(Token Attribute,ALPCFLGMSGTOKENATTR)
      • 10.5.5 数据视图属性(Data View Attribute,ALPCFLGMSGDATAVIEWATTR)
    • 10.6 发送和接收消息(Sending and Receiving Messages)
      • 10.6.1 服务器操作(Server Operation)
      • 10.6.2 客户端操作(Client Operation)
    • 10.7 总结
  • 第11章 安全性(Security)
  • 第12章 内存(第二部分)
  • 第13章 注册表
目录

第10章:ALPC

# 第10章:ALPC(Advanced Local Procedure Calls)

Windows 拥有多种进程间通信机制(inter-process communication mechanisms),允许进程通过传递数据进行相互通信。例如窗口消息(window messages)、共享内存(shared memory)、管道(pipes)、邮槽(mailslots)和组件对象模型(COM)。高级(或异步)本地过程调用(Local Procedure Calls,ALPC)是另一种此类机制,Windows 组件使用它进行跨进程边界(process boundaries)的通信。与其他提到的机制不同,ALPC 完全没有官方文档说明。本章概述了 ALPC API 及其在进程间通信中的使用方法。

ALPC 是一个庞大的主题,我对它的研究尚未完成,因此本章并未涵盖 ALPC 的所有内容。本专栏的后续版本将涵盖更多 ALPC 相关内容。

本章包含以下内容:

• ALPC 概念

• 简单客户端/服务器

• 创建服务器端口

• 连接到端口

• 消息属性

• 发送和接收消息

# 10.1 ALPC 概念

ALPC 涉及消息的发送和接收。消息(message)是一种包含消息头(header)和正文(body)的数据结构。消息头包含有关消息的信息,例如消息大小、发送方(sender)和接收方(receiver)。

ALPC 中的核心实体是端口(Port)。端口是一个通信端点(communication endpoint),从概念上讲与网络端口(network ports)类似。它是一种名为 ALPC 端口(ALPC Port)的内核对象类型(kernel object type)。端口分为 3 种类型:

• 服务器连接端口(Server connection port)—— 一种命名端口(named port),用于监听传入的连接(incoming connections)。

• 服务器通信端口(Server communication port)—— 一种未命名端口(unnamed port),用于与客户端通信。

• 客户端端口(Client port)—— 一种未命名端口,客户端使用它与服务器通信。

由于服务器连接端口是命名的,因此可以通过 WinObj、Object Explorer、Process Explorer 等工具查看。图 10-1 显示了在 WinObj 中查看的 RPC Control 对象管理器目录(object manager directory)中的多个 ALPC 端口对象。

图 10-1:WinObj 中的 ALPC 端口

使用 Object Explorer,可以查看每个端口对象的句柄(handles)(图 10-2)。双击某个对象可查看其属性(properties)。

图 10-2:ObjExp 中的 ALPC 端口句柄

使用 Process Explorer 和 Object Explorer,可以检查特定进程中的 ALPC 端口句柄(图 10-3)。

图 10-3:ObjExp 中的 ALPC 端口句柄

内核调试器(kernel debugger)可以提供有关 ALPC 端口、消息和连接的更多详细信息。例如,!alpc /lpp 命令加上进程地址,可以显示指定进程使用的所有 ALPC 端口:

lkd> !alpc /lpp ffff968f2ff62300
Ports created by the process ffff968f2ff62300:
ffff968f2ff90aa0('umpo') 0, 32 connections
ffff968f2ca9ed70 0 ->ffff968f306a2d70 0 ffff968f2ff130c0('services.exe')
ffff968f3094bb30 0 ->ffff968f30998df0 0 ffff968f2ff62300('svchost.exe')
ffff968f2c15bc60 0 ->ffff968f2c15ba00 0 ffff968f3143d080('svchost.exe')
ffff968f2c73e6e0 0 ->ffff968f2c73e070 0 ffff968f3150e080('svchost.exe')
ffff968f3534f070 0 ->ffff968f3534f530 0 ffff968f34f6e080('esif_uf.exe')
ffff968f2479add0 0 ->ffff968f243b6dd0 0 ffff968f3066c080('WUDFHost.exe')
ffff968f35fbe070 0 ->ffff968f354edce0 0 ffff968f423d0080('NVDisplay.Cont')
ffff968f45bee580 0 ->ffff968f351e4c30 0 ffff968f31260300('sihost.exe')
ffff968f24af4a80 0 ->ffff968f24af3ce0 0 ffff968f47af41c0('explorer.exe')
...
ffff968f21a18090('actkernel') 0, 19 connections
ffff968f30711de0 0 ->ffff968f3074edf0 0 ffff968f3074a340('svchost.exe')
ffff968f30736990 0 ->ffff968f30736730 0 ffff968f2ff62300('svchost.exe')
ffff968f30bbbdd0 0 ->ffff968f30b45a80 0 ffff968f30b11080('svchost.exe')
...
ffff968f307382f0('LRPC-a5e17d5e766d3850d2') 0, 5 connections
ffff968f243f9b80 0 ->ffff968f243f9de0 0 ffff968f31260300('sihost.exe')
ffff968f5d985510 0 ->ffff968f4bd482c0 0 ffff968f4f25e280('AcrobatNotific')
...
Ports the process ffff968f2ff62300 is connected to:
ffff968f2ff8cdf0 0 -> ffff968f2d1417e0 ('ApiPort') 0 ffff968f2cacd140 ('csrss.ex\
e')
ffff968f2ff51a80 0 -> ffff968f2ff6cd00 ('ntsvcs') 178 ffff968f2ff130c0 ('service\
s.exe')
ffff968f2ff4adc0 0 -> ffff968f02ea8070 ('PowerPort') 0 ffff968f02f02080 ('System\
')
ffff968f30730d90 0 -> ffff968f0a15f4a0 ('PdcPort') 0 ffff968f02f02080 ('System')
ffff968f217ce3a0 0 -> ffff968f2fef8c90 ('lsasspirpc') 0 ffff968f2ff55080 ('lsass\
.exe')
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

使用端口地址(上述输出左侧的第一个和第二个值)可以获取有关该端口的更多详细信息:

kd> !alpc /p ffff968f2a572b50
Port ffff968f2a572b50
Type : ALPC_CLIENT_COMMUNICATION_PORT
CommunicationInfo : ffffba0de0cd1460
ConnectionPort : ffff968f3072faa0 (LRPC-68623205695bc7d55a), Connections
ClientCommunicationPort : ffff968f2a572b50
ServerCommunicationPort : ffff968f30b09530
OwnerProcess : ffff968f2ff62300 (svchost.exe), Connections
SequenceNo : 0x00000004 (4)
CompletionPort : 0000000000000000
CompletionList : 0000000000000000
ConnectionPending : No
ConnectionRefused : No
Disconnected : No
Closed : No
FlushOnClose : Yes
ReturnExtendedInfo : No
Waitable : No
Chapter 10: ALPC 267
Security : Dynamic
Wow64CompletionList : No
Main queue is empty.
Direct message queue is empty.
Large message queue is empty.
Pending queue is empty.
Canceled queue is empty.
lkd> !alpc /p ffff968f93806a20
Port ffff968f93806a20
Type : ALPC_CONNECTION_PORT
CommunicationInfo : ffffba0f1e24c7b0
ConnectionPort : ffff968f93806a20 (OLE474475C7BFB330BCB066F923BC52), Co\
nnections
ClientCommunicationPort : 0000000000000000
ServerCommunicationPort : 0000000000000000
OwnerProcess : ffff968f61ae1080 (WhatsApp.exe), Connections
SequenceNo : 0x0000002A (42)
CompletionPort : ffff968fbc5bcf40
CompletionList : 0000000000000000
ConnectionPending : No
ConnectionRefused : No
Disconnected : No
Closed : No
FlushOnClose : Yes
ReturnExtendedInfo : No
Waitable : No
Security : Static
Wow64CompletionList : No
8 thread(s) are registered with port IO completion object:
THREAD ffff968f8923b040 Cid a0a4.1a3b8 Teb: 000000a170052000 Win32Thread: ffff\
968f9441f910 WAIT
THREAD ffff968f4d1e8080 Cid a0a4.1aa00 Teb: 000000a170056000 Win32Thread: ffff\
968fe1046be0 WAIT
THREAD ffff968f4eb61080 Cid a0a4.1235c Teb: 000000a17005a000 Win32Thread: ffff\
Chapter 10: ALPC 268
968fe1048580 WAIT
THREAD ffff968f4ee19080 Cid a0a4.c494 Teb: 000000a170060000 Win32Thread: 00000\
00000000000 WAIT
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

有关 !alpc 命令的更多详细信息,请查看相关文档。

有关 ALPC 的更多信息,可以在《Windows 内部原理(Windows Internals)》第 7 版第 2 卷的第 8 章中找到。

# 10.2 简单客户端/服务器(Simple Client/Server)

首先,我们将使用ALPC(本地过程调用,Advanced Local Procedure Call)以同步模式(synchronous mode)构建客户端进程与服务器进程之间的简单通信。

服务器的操作步骤如下:

• 使用端口创建函数(NtAlpcCreatePort)创建一个命名服务器监听端口(named server listening port)。

• 使用发送等待接收端口函数(NtAlpcSendWaitReceivePort)等待通信请求。

• 收到连接请求后,使用连接端口接受函数(NtAlpcAcceptConnectPort)接受该请求。

• 继续通过发送等待接收端口函数(NtAlpcSendWaitReceivePort)等待通信请求,对每个到达的请求进行处理。这可能包括来自新客户端的新连接请求。

客户端进程的操作步骤如下:

• 使用端口连接函数(NtAlpcConnectPort)通过名称打开服务器端口。

• 使用发送等待接收端口函数(NtAlpcSendWaitReceivePort)向服务器发送请求。

我们先从客户端开始,因为它相对简单。第一步是通过名称连接到服务器端口。为了让示例更具实用性,客户端最多尝试10次连接,每次尝试之间间隔1秒:

HANDLE hPort = NULL;  // 初始化句柄,避免野指针
UNICODE_STRING portName;
RtlInitUnicodeString(&portName, L"\\RPC Control\\SimpleServerPort"); 
NTSTATUS status;

// 循环10次尝试连接ALPC端口
for (int i = 0; i < 10; i++)
{
    status = NtAlpcConnectPort(
        &hPort,
        &portName,
        nullptr,
        nullptr,
        ALPC_MSGFLG_SYNC_REQUEST,
        nullptr,
        nullptr,
        nullptr,
        nullptr,
        nullptr,
        nullptr);

    if (NT_SUCCESS(status))
    {
        break;  // 连接成功则退出循环
    }

    printf("NtAlpcConnectPort failed: 0x%X\n", status);
    Delay(1); 
}

// 检查最终连接状态
if (!NT_SUCCESS(status))
{
    return status;
}

printf("Client port connected: 0x%p\n", hPort);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

端口连接函数(NtAlpcConnectPort)用于连接到名称为\\RPC Control\\SimpleServerPort的服务器——我们的服务器在注册端口时会使用该名称。并不要求必须使用“RPC Control”对象管理器目录(object manager directory),但它是一个不错的选择。同步请求标志(ALPC_MSGFLG_SYNC_REQUEST)表示客户端希望向服务器发送同步请求。该函数会在hPort中返回服务器端口的句柄(handle)。

我们将在下一节详细介绍所有参数。

延迟函数(Delay)是一个简单的函数,用于按指定的秒数休眠:

void  Delay(int  seconds)  {
    LARGE_INTEGER  time;
    time.QuadPart  =  -10000000LL  *  seconds;
    NtDelayExecution(FALSE,  &time); 
}
1
2
3
4
5

假设连接成功,客户端现在可以向服务器发送请求了。该客户端会在一个无限循环中每秒发送一条包含当前时间的字符串,直到发送失败。每个ALPC消息都必须以端口消息结构(PORT_MESSAGE)开头。我们将创建一个简单的结构,在该结构的基础上扩展一些文本内容:

struct Message : PORT_MESSAGE
{
    char Text[64];
};
1
2
3
4

首先,我们构建消息:

LARGE_INTEGER time;
TIME_FIELDS tf;

// 无限循环封装时间消息(ALPC通信)
for (;;)
{
    Message msg{};  // 初始化自定义ALPC消息结构体
    
    // 1. 获取系统时间并转换为本地时间
    NtQuerySystemTime(&time);
    RtlSystemTimeToLocalTime(&time, &time);
    RtlTimeToTimeFields(&time, &tf);

    // 2. 格式化时间字符串到消息文本缓冲区
    sprintf_s(
        msg.Text,
        "The Time is %02d:%02d:%02d.%03d",  // 移除多余空格,规范格式
        tf.Hour,
        tf.Minute,
        tf.Second,
        tf.Milliseconds);

    // 3. 设置ALPC消息的长度字段(PORT_MESSAGE核心字段)
    msg.u1.s1.DataLength = sizeof(msg.Text);  // 有效数据长度(仅文本部分)
    msg.u1.s1.TotalLength = sizeof(msg);      // 消息总长度(含PORT_MESSAGE头部)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

我们使用之前遇到过的一些函数来获取当前的本地时间,并将其转换为易于阅读的时间字段结构(TIME_FIELDS),然后将其格式化为字符串。数据长度(DataLength)必须设置为消息体的长度(即不包含端口消息头(PORT_MESSAGE header)),总长度(TotalLength)必须设置为消息的总长度(包含端口消息头)。

现在我们可以发送消息并等待可能的回复:

    Message reply;
    SIZE_T msgLen = sizeof(reply);
    status = NtAlpcSendWaitReceivePort(hPort, ALPC_MSGFLG_SYNC_REQUEST, &msg, nullptr, &reply, &msgLen, nullptr, nullptr);
    if (!NT_SUCCESS(status))
    {
        printf("NtAlpcSendWaitReceivePort  failed:  0x%X\n " ,  status);
        break;
    }
    printf("Sent  message  %s\n " ,  msg.Text);
    printf("Received  reply  from  PID:  %u  TID:  %u\n " ,
    HandleToULong(reply.ClientId.UniqueProcess),
    HandleToULong(reply.ClientId.UniqueThread));
    Delay(1);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

发送等待接收端口函数(NtAlpcSendWaitReceivePort)集成了多种功能,但上述代码仅发起同步请求(通过同步请求标志(ALPC_MSGFLG_SYNC_REQUEST)),并打印回复发送者的进程ID(PID)和线程ID(TID)。

完整的源代码位于SimpleClient项目中。

服务器相对复杂一些。首先,我们使用端口创建函数(NtAlpcCreatePort)创建一个命名端口:

HANDLE hServerPort;
UNICODE_STRING portName;
RtlInitUnicodeString(&portName, L"\\RPC  Control\\SimpleServerPort");
OBJECT_ATTRIBUTES portAttr = RTL_CONSTANT_OBJECT_ATTRIBUTES(&portName, 0);
auto status = NtAlpcCreatePort(&hServerPort, &portAttr, nullptr);
if (!NT_SUCCESS(status))
{
    printf("NtAlpcCreatePort  failed:  0x%X\n ",  status);
    return  1;
}
printf("Server  port  created:  0x%p\n ",  hServerPort);
1
2
3
4
5
6
7
8
9
10
11

并不要求必须在“RPC Control”对象管理器目录中创建ALPC端口,但该目录对非管理员调用者可访问。默认情况下,仅管理员级别的调用者可以在根对象管理器命名空间(root object manager namespace)中创建ALPC端口。

接下来,我们需要准备一个消息结构(Message)来接收来自客户端的消息,并可选择发送回复消息:

Message  msg;
SIZE_T  size  =  sizeof(msg);
PPORT_MESSAGE  sendMessage  =  nullptr;
PPORT_MESSAGE  receiveMessage  =  &msg;
1
2
3
4

服务器最初没有要发送的消息(发送消息(sendMessage)为NULL)。现在我们可以启动循环,等待调用:

for (;;)
{
    status = NtAlpcSendWaitReceivePort(hServerPort, ALPC_MSGFLG_RELEASE_MESSAGE, sendMessage, nullptr, receiveMessage, &size, nullptr, nullptr);
    if (!NT_SUCCESS(status))
        break;

    printf("Received  msg  type:  0x%X  (ID:  0x%X)\n " ,
    receiveMessage->u2.s2.Type,  receiveMessage->MessageId);
}
1
2
3
4
5
6
7
8
9

服务器使用的API与客户端相同。它发送发送消息(sendMessage,最初为NULL)并等待接收消息(receiveMessage)中的回复。释放消息标志(ALPC_MSGFLG_RELEASE_MESSAGE)表示(如果发送消息(sendMessage)非NULL)服务器不期望从客户端收到回复,因此消息发送后可以释放。

收到回复消息后,服务器需要通过检查消息类型来处理它。我们将仅处理两种类型的消息。第一种是来自客户端的连接请求:

switch (receiveMessage->u2.s2.Type & 0xff)
{
    case LPC_CONNECTION_REQUEST:
        printf("Connection  request  received  from  PID:  %u  TID:  %u\n " ,
        HandleToULong(receiveMessage->ClientId.UniqueProcess),
        HandleToULong(receiveMessage->ClientId.UniqueThread));

1
2
3
4
5
6
7

消息类型可能包含以0x100开头的标志,因此我们使用掩码过滤掉这些标志。

服务器会接收客户端的详细信息(进程ID(PID)和线程ID(TID))以及客户端发送的任何自定义数据。在本示例中,服务器忽略所有这些信息,并允许任何客户端连接:

            HANDLE  hCommPort;
            status  =  NtAlpcAcceptConnectPort(&hCommPort,  hServerPort,  0 ,
            nullptr ,  nullptr ,  nullptr ,  receiveMessage,  nullptr,  TRUE);
            if   ( !NT_SUCCESS(status))  {
                printf("NtAlpcAcceptConnectPort  failed:  0x%X\n " ,  status); 
            }
            else  {
                printf("Client  port  connected:  0x%p\n " ,  hCommPort); 
            }
            sendMessage  =  nullptr;
            break;
1
2
3
4
5
6
7
8
9
10
11

连接端口接受函数(NtAlpcAcceptConnectPort)接受或拒绝连接请求(基于最后一个参数),并创建一个用于与该特定客户端通信的端口(通信端口(hCommPort))。尽管该端口用于与该客户端通信,但服务器仍会在原始的命名服务器端口(服务器端口(hServerPort))上继续监听。发送给该客户端的任何消息都会在底层使用正确的未命名端口。

第二种处理的消息是来自客户端的“普通”同步消息:

        case  LPC_REQUEST:
            printf( "\t%s\n " ,  msg.Text);
            sendMessage  =  receiveMessage;
            sendMessage->u1 .s1 .DataLength  =  0;
            sendMessage->u1 .s1 .TotalLength  =  sizeof(PORT_MESSAGE);
            break;
1
2
3
4
5
6

这里没有复杂的操作——服务器仅打印消息,并准备一个“空”消息以回复客户端(在下一次循环迭代中),以确认已收到消息并标记该消息已处理。

完整的源代码位于SimpleServer项目中。

运行服务器进程和客户端进程后,输出将类似于以下内容:

(服务器)

Server  port  created:  0x00000000000000A8
Received  msg  type:  0x200A   (ID:  0xDC30)
Connection  request  received  from  PID:  47360  TID:  38892
Client  port  connected:  0x00000000000000A0
Received  msg  type:  0x2001   (ID:  0xDC30)
    The  Time  is  20:25:29 .439
Received  msg  type:  0x2001   (ID:  0xAA94)
    The  Time  is  20:25:30 .455
Received  msg  type:  0x2001   (ID:  0x10334)
    The  Time  is  20:25:31 .468
Received  msg  type:  0x2001   (ID:  0xAA94)
    The  Time  is  20:25:32 .483
1
2
3
4
5
6
7
8
9
10
11
12

(客户端)

Client  port  connected:  0x0000000000000094
Sent  message  The  Time  is  20:25:29 .439 .
Received  reply  from  PID:  48168  TID:  113376
Sent  message  The  Time  is  20:25:30 .455 .
Received  reply  from  PID:  48168  TID:  113376
Sent  message  The  Time  is  20:25:31 .468 .
Received  reply  from  PID:  48168  TID:  113376
Sent  message  The  Time  is  20:25:32 .483 .
Received  reply  from  PID:  48168  TID:  113376
1
2
3
4
5
6
7
8
9

# 10.3 创建服务器端口(Creating Server Ports)

显然,上一节省略了很多细节。以下各节将更详细地介绍ALPC API。

创建服务器端口通过端口创建函数(NtAlpcCreatePort)完成:

NTSTATUS  NtAlpcCreatePort(
    _Out_  PHANDLE  PortHandle,
    _In_opt_  POBJECT_ATTRIBUTES  ObjectAttributes,
    _In_opt_  PALPC_PORT_ATTRIBUTES  PortAttributes);
1
2
3
4

与其他创建类API不同,该函数无法打开现有命名端口的句柄——它只能创建新的端口。

对象属性(ObjectAttributes)包含端口的名称,而可选的端口属性(PortAttributes)可以指向一个ALPC端口属性结构(ALPC_PORT_ATTRIBUTES),该结构包含有关端口的附加信息:

typedef  struct  _ALPC_PORT_ATTRIBUTES  { 
    ULONG  Flags;
    SECURITY_QUALITY_OF_SERVICE  SecurityQos;
    SIZE_T  MaxMessageLength;
    SIZE_T  MemoryBandwidth;  //  not  used
    SIZE_T  MaxPoolUsage;         //  not  used  directly
    SIZE_T  MaxSectionSize;
    SIZE_T  MaxViewSize;  //  0=unlimited
    SIZE_T  MaxTotalSectionSize;
    ULONG  DupObjectTypes;
#ifdef  _WIN64
    ULONG  Reserved;
#endif
}  ALPC_PORT_ATTRIBUTES,  *PALPC_PORT_ATTRIBUTES;
1
2
3
4
5
6
7
8
9
10
11
12
13
14

标志(Flags)字段可以包含以下标志的组合:

typedef  enum  _ALPC_PORT_FLAGS  {
    ALPC_PORT_ FLAG_ALLOW_IMPERSONATION           =   0x00010000 ,
    ALPC_PORT_ FLAG_ACCEPT_REQUESTS                    =   0x00020000 ,
    ALPC_PORT_ FLAG_WAITABLE_PORT                         =  0x00040000 ,
    ALPC_PORT_ FLAG_ACCEPT_DUP_HANDLES              =  0x00080000 ,
    ALPC_PORT_ FLAG_SYSTEM_PROCESS                       =  0x00100000 ,
    ALPC_PORT_ FLAG_SUPPRESS_WAKE                         =   0x00200000 ,
    ALPC_PORT_ FLAG_ALWAYS_WAKE                              =  0x00400000 ,
    ALPC_PORT_ FLAG_DO_NOT_DISTURB                      =   0x00800000 ,
    ALPC_PORT_ FLAG_NO_SHARED_SECTION                =  0x01000000 ,
    ALPC_PORT_ FLAG_ACCEPT_INDIRECT_HANDLES  =  0x02000000 
}  ALPC_PORT_FLAGS;
1
2
3
4
5
6
7
8
9
10
11
12

安全服务质量(SecurityQos)是一个安全服务质量结构(SECURITY_QUALITY_OF_SERVICE),我们将在第11章中讨论。最大消息长度(MaxMessageLength)是通过该端口允许的最大消息长度,限制约为64KB。

如果端口属性(PortAttributes)为NULL,则端口的默认属性将初始化为以下值:

• 标志(Flags)设置为0。 • 安全服务质量(SecurityQos)设置为匿名安全(SecurityAnonymous)、动态跟踪安全(SECURITY_DYNAMIC_TRACKING),且仅有效标志(EffectiveOnly)设置为TRUE。 • 最大消息长度(MaxMessageLength)在64位系统上设置为512字节,在32位系统上设置为256字节。 • 最大池使用率(MaxPoolUsage)和最大节大小(MaxSectionSize)设置为0x4000(16KB)。 • 最大总节大小(MaxTotalSectionSize)设置为128KB。 • 其余成员设置为0。

复制对象类型(DupObjectTypes)是一个位掩码,指示哪些对象类型的句柄可以被编组(复制)到另一个进程:

typedef  enum  _ALPC_OBJECT_TYPE  {
    ALPC_ FILE_OBJECT_TYPE                 =  0x00000001 ,
    ALPC_THREAD_OBJECT_TYPE             =  0x00000004 ,
    ALPC_SEMAPHORE_OBJECT_TYPE       =  0x00000008 ,
    ALPC_EVENT_OBJECT_TYPE                =  0x00000010 ,
    ALPC_PROCESS_OBJECT_TYPE           =   0x00000020 ,
    ALPC_MUTANT_OBJECT_TYPE             =   0x00000040 ,
    ALPC_SECTION_OBJECT_TYPE           =  0x00000080 ,
    ALPC_REG_KEY_OBJECT_TYPE           =   0x00000100 ,
    ALPC_TOKEN_OBJECT_TYPE                =  0x00000200 ,
    ALPC_COMPOSITION_OBJECT_TYPE  =  0x00000400 ,
    ALPC_JOB_OBJECT_TYPE                    =  0x00000800 ,
    ALPC_ALL_OBJECT_TYPES                  =  0x00000FFD ,
}  ALPC_OBJECT_TYPE;
1
2
3
4
5
6
7
8
9
10
11
12
13
14

创建端口时,会设置端口的进程所有者。如果指定了系统进程标志(ALPC_PORT_FLAG_SYSTEM_PROCESS),或者调用者来自内核模式(kernel mode),则系统进程(System process)将成为所有者。否则,调用者进程将成为所有者。只有所有者进程可以监听客户端连接。

# 10.4 连接到端口(Connecting to Ports)

服务器创建命名端口(named port)后,客户端可通过NtAlpcConnectPort或NtAlpcConnectPortEx函数连接到该端口:

NTSTATUS  NtAlpcConnectPort(
    _Out_  PHANDLE  PortHandle,
    _In_  PUNICODE_STRING  PortName,
    _In_opt_  POBJECT_ATTRIBUTES  ObjectAttributes,
    _In_opt_  PALPC_PORT_ATTRIBUTES  PortAttributes,
    _In_  ULONG  Flags,
    _In_opt_  PSID  RequiredServerSid,
    _Inout_updates_bytes_to_opt_ (*Length,  *Length)  PPORT_MESSAGE  ConnectionMessage,
    _Inout_opt_  PULONG  Length,
    _Inout_opt_  PALPC_MESSAGE_ATTRIBUTES  OutMessageAttributes,
    _Inout_opt_  PALPC_MESSAGE_ATTRIBUTES  InMessageAttributes,
    _In_opt_  PLARGE_INTEGER  Timeout);

NTSTATUS  NtAlpcConnectPortEx(      //  Win  8+
    _Out_  PHANDLE  PortHandle,
    _In_  POBJECT_ATTRIBUTES  ConnectionPortObjectAttributes,
    _In_opt_  POBJECT_ATTRIBUTES  ClientPortObjectAttributes,
    _In_opt_  PALPC_PORT_ATTRIBUTES  PortAttributes,
    _In_  ULONG  Flags,
    _In_opt_  PSECURITY_DESCRIPTOR  ServerSecurityRequirements,
    _Inout_updates_bytes_to_opt_ (*Length,  *Length)  PPORT_MESSAGE  ConnectionMessage,
    _Inout_opt_  PSIZE_T  Length,
    _Inout_opt_  PALPC_MESSAGE_ATTRIBUTES  OutMessageAttributes,
    _Inout_opt_  PALPC_MESSAGE_ATTRIBUTES  InMessageAttributes,
    _In_opt_  PLARGE_INTEGER  Timeout);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

如果调用成功,端口句柄(PortHandle)将返回一个有效句柄。端口名称(PortName)是端口的完整名称(例如\RPC Control\MyPort)。对于NtAlpcConnectPortEx函数,连接端口对象属性(ConnectionPortObjectAttributes)通过标准的对象属性结构(OBJECT_ATTRIBUTES)提供端口名称。端口属性(PortAttributes,适用于NtAlpcConnectPort)和客户端端口属性(ClientPortAttributes,适用于NtAlpcConnectPortEx)提供可选的客户端端口属性配置。

端口属性(PortAttributes)是一个可选的ALPC端口属性对象(ALPC_PORT_ATTRIBUTES),用于指定待创建端口的ALPC端口特定属性。标志参数(Flags)可以是0,也可以是以下标志的组合:

  • ALPC_MSGFLG_SYNC_REQUEST(0x2000)——同步连接(connect synchronously)。
  • ALPC_MSGFLG_RETURN_EXTENDED_INFO(0x80000)——若连接出错,在输出消息中返回扩展信息(extended information)。

所需服务器安全标识符(RequiredServerSid)是一个可选的安全标识符(SID),必须是服务器进程令牌(server process token)的一部分,否则连接将失败。如果指定为NULL,则任何服务器进程的安全标识符(SID)都可生效。服务器安全要求(ServerSecurityRequirements)是一个可选的安全描述符(security descriptor),服务器将根据该描述符评估是否允许建立连接。

连接消息(ConnectionMessage)是客户端发送给服务器的可选消息,可包含任意内容。服务器可将其用于任意用途,例如以某种方式识别客户端、获取密码等。长度(Length)是一个地址,输入时存储连接消息(ConnectionMessage)的字节大小,若服务器向客户端返回某些信息,可能会覆盖该值。

输出消息属性(OutMessageAttributes)是一个可选的ALPC消息属性结构(ALPC_MESSAGE_ATTRIBUTES),包含出站消息的属性。同样,输入消息属性(InMessageAttributes,若不为NULL)将接收来自服务器的回复。ALPC消息属性结构(ALPC_MESSAGE_ATTRIBUTES)是一个变长结构,格式如下:

typedef  struct  _ALPC_MESSAGE_ATTRIBUTES  {
    ULONG  AllocatedAttributes;                //  define  the  structure  layout
    ULONG  ValidAttributes;                         //  which  attributes  are  valid

    //  message  attributes  follow
}  ALPC_MESSAGE_ATTRIBUTES,  *PALPC_MESSAGE_ATTRIBUTES;
1
2
3
4
5
6

我们将在下一小节详细介绍消息属性(message attributes)。

最后,超时时间(Timeout)是等待同步连接被接受的时长。如果超时时间已过仍未建立连接,将返回超时状态(STATUS_TIMEOUT)。传入NULL表示客户端愿意无限期等待。

以下示例展示了客户端如何发送包含连接消息的初始连接请求:

struct  Message   :  PORT_MESSAGE
{
    char  Text[64];
};

//
//  build  simple  connection  message
//
Message  connMsg{};
strcpy_s(connMsg.Text,  "Abracadabra");
connMsg .u1 .s1 .DataLength  =  sizeof(connMsg.Text);
connMsg .u1 .s1 .TotalLength  =  sizeof(connMsg);

//  request  to  connect  to  the  server
status  =  NtAlpcConnectPort(&hPort,  &portName,  nullptr ,  nullptr ,
    ALPC_MSGFLG_SYNC_REQUEST,  nullptr ,  &connMsg,  nullptr ,
    nullptr ,  nullptr ,  nullptr);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 10.5 消息属性(Message Attributes)

任何ALPC消息都可附带元数据(metadata)形式的消息属性(message attributes)。该属性结构看似简单(从之前的小节复制而来):

typedef  struct  _ALPC_MESSAGE_ATTRIBUTES  {
    ULONG  AllocatedAttributes;                //  define  the  structure  layout
    ULONG  ValidAttributes;                         //  which  attributes  are  valid
}  ALPC_MESSAGE_ATTRIBUTES,  *PALPC_MESSAGE_ATTRIBUTES;
1
2
3
4

实际的属性按特定顺序紧跟在该结构之后,且必须严格遵循此顺序。提供的API为开发者隐藏了这一复杂性。以下是按顺序排列的属性标志:

#define  ALPC_FLG_MSG_SEC_ATTR                                 0x80000000
#define  ALPC_FLG_MSG_DATAVIEW_ATTR                      0x40000000
#define  ALPC_FLG_MSG_CONTEXT_ATTR                        0x20000000
#define  ALPC_FLG_MSG_HANDLE_ATTR                          0x10000000
#define  ALPC_FLG_MSG_TOKEN_ATTR                            0x08000000
#define  ALPC_FLG_MSG_DIRECT_ATTR                          0x04000000
#define  ALPC_FLG_MSG_WORK_ON_BEHALF_ATTR         0x02000000
1
2
3
4
5
6
7

可按上述顺序指定任意组合的属性。每个属性都有其对应的结构来表示其数据。AlpcInitializeMessageAttribute函数可在缓冲区初始化后初始化一组属性,或返回所需的缓冲区大小:

NTSTATUS  AlpcInitializeMessageAttribute(
    _In_  ULONG  AttributeFlags,
    _Out_opt_  PALPC_MESSAGE_ATTRIBUTES  Buffer,
    _In_  SIZE_T  BufferSize,
    _Out_  PSIZE_T  RequiredBufferSize);
1
2
3
4
5

你可以通过累加所需属性对应的结构大小来手动计算所需缓冲区大小,但如果按以下示例调用两次AlpcInitializeMessageAttribute函数,它将为你自动计算:

ULONG  size;
//
//  assume  ALPC_FLG_MSG_CONTEXT_ATTR   and  ALPC_ FLG_MSG_HANDLE_ATTR   needed
//
auto  status  =  AlpcInitializeMessageAttribute(
    ALPC_FLG_MSG_CONTEXT_ATTR   |   ALPC_FLG_MSG_HANDLE_ATTR,
    nullptr ,  0 ,  &size);
assert(status  ==  STATUS_BUFFER_TOO_SMALL);

auto  buffer  =  std::make_unique<BYTE[]>(size);
auto  msgAttr  =   (PALPC_MESSAGE_ATTRIBUTES)buffer .get();
status  =  AlpcInitializeMessageAttribute(
    ALPC_FLG_MSG_CONTEXT_ATTR   |   ALPC_FLG_MSG_HANDLE_ATTR,
    msgAttr,  size,  &size);
1
2
3
4
5
6
7
8
9
10
11
12
13
14

AlpcInitializeMessageAttribute函数会将已分配属性(AllocatedAttributes)初始化为请求的属性标志,并将缓冲区的其余部分置零。

获取正确缓冲区大小的另一种方法是调用AlpcGetHeaderSize函数:

ULONG  AlpcGetHeaderSize(_In_  ULONG  Flags);
1

要获取缓冲区中特定属性对应的指针,可调用AlpcGetMessageAttribute函数:

PVOID  AlpcGetMessageAttribute(
    _In_  PALPC_MESSAGE_ATTRIBUTES   Buffer,
    _In_  ULONG  AttributeFlag);
1
2
3

缓冲区(Buffer)是属性缓冲区指针,属性标志(AttributeFlag)是目标属性对应的特定标志。例如:

auto  contextAttr  =  AlpcGetMessageAttribute(msgAttr,  ALPC_FLG_MSG_CONTEXT_ATTR);
auto  handleAttr  =  AlpcGetMessageAttribute(msgAttr,  ALPC_FLG_MSG_HANDLE_ATTR);
1
2

请勿指定多个标志,否则函数将失败并返回NULL。

# 10.5.1 安全属性(Security Attribute,ALPC_FLG_MSG_SEC_ATTR)

安全上下文属性(security context attributes)可通过NtAlpcCreateSecurityContext函数初始化:

#define  ALPC_SEC_CURRENT   ((ALPC_HANDLE)(ULONG_PTR)(-2))

typedef  struct  _ALPC_SECURITY_ATTR  {
    ULONG  Flags;
    PSECURITY_QUALITY_OF_SERVICE  QoS;    //  in,  optional
    ALPC_HANDLE  ContextHandle;                 //  in/out
}  ALPC_SECURITY_ATTR,  *PALPC_SECURITY_ATTR;

NTSTATUS  NtAlpcCreateSecurityContext(
    _In_  HANDLE  PortHandle,
    _Reserved_  ULONG  Flags,
    _Inout_  PALPC_SECURITY_ATTR  SecurityAttribute);
1
2
3
4
5
6
7
8
9
10
11
12

当安全上下文信息对象不再需要时,应通过NtAlpcDeleteSecurityContext函数销毁:

NTSTATUS  NtAlpcDeleteSecurityContext(
    _In_  HANDLE  PortHandle,
    _Reserved_  ULONG  Flags,
    _In_  ALPC_HANDLE  ContextHandle);
1
2
3
4

上下文句柄(ContextHandle)是ALPC安全属性结构(ALPC_SECURITY_ATTR)中的上下文句柄(ContextHandle)成员。这些结构作为每个端口的私有“句柄表”的一部分在内部进行管理。也可通过撤销(revoking)安全上下文对象使其失效:

NTSTATUS  NtAlpcRevokeSecurityContext(
    _In_  HANDLE  PortHandle,
    _Reserved_  ULONG  Flags,
    _In_  ALPC_HANDLE  ContextHandle);
1
2
3
4

# 10.5.2 上下文属性(Context Attribute,ALPC_FLG_MSG_CONTEXT_ATTR)

该属性通过以下结构表示:

typedef  struct  _ALPC_CONTEXT_ATTR  {
    PVOID  PortContext;
    PVOID  MessageContext;
    ULONG  Sequence;
    ULONG  MessageId;
    ULONG  CallbackId;
}  ALPC_CONTEXT_ATTR,  *PALPC_CONTEXT_ATTR;
1
2
3
4
5
6
7

可通过调用NtAlpcCancelMessage函数,使用该属性取消消息:

NTSTATUS  NtAlpcCancelMessage(
    _In_  HANDLE  PortHandle,
    _In_  ULONG  Flags,
    _In_  PALPC_CONTEXT_ATTR  MessageContext);
1
2
3
4

# 10.5.3 句柄属性(Handle Attribute,ALPC_FLG_MSG_HANDLE_ATTR)

使用的结构是ALPC句柄属性结构(ALPC_HANDLE_ATTR):

//  may  be  used  on  64-bit  systems  to  represent  arrays  of  handles
typedef  struct  _ALPC_HANDLE_ATTR32  {
    union
    {
        ULONG  Flags;
        struct
        {
            ULONG  Reserved0 :  16;
            ULONG  SameAccess:  1;
            ULONG  SameAttributes:  1;
            ULONG  Indirect:  1;
            ULONG  Inherit:  1;
            ULONG  Reserved1 :  12;
        };
    };

    ULONG  Handle;
    ULONG  ObjectType;
    union
    {
        ULONG  DesiredAccess;
        ULONG  GrantedAccess;
    };
}  ALPC_HANDLE_ATTR32,  *PALPC_HANDLE_ATTR32;

//  the  structure
typedef  struct  _ALPC_HANDLE_ATTR  {
    union
    {
        ULONG  Flags;
        struct
        {
            ULONG  Reserved0 :  16;
            ULONG  SameAccess:  1;
            ULONG  SameAttributes:  1;
            ULONG  Indirect:1;
            ULONG  Inherit   :1;
            ULONG  Reserved1 :  12;
        };
    };
    union
    {
        HANDLE  Handle;
        ALPC_HANDLE_ATTR32*  HandleAttrArray;
    };
    union
    {
        ULONG  ObjectType;
        ULONG  HandleCount;

    };
    union
    {
        ACCESS_MASK  DesiredAccess;
        ACCESS_MASK  GrantedAccess;
    };
}  ALPC_HANDLE_ATTR,  *PALPC_HANDLE_ATTR;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

截至撰写本文时,phnt中尚未正确定义该结构。

该结构可用于在通信双方之间传递句柄。句柄(Handle)是待共享(复制)的句柄,对象类型(ObjectType)必须设置为对应的对象类型(常量ALPC_OBJ_*)。或者,可通过填充句柄计数(HandleCount)和句柄属性数组(HandleAttrArray)来传递句柄数组。

以下示例展示了如何将事件句柄(event handle)封装到消息属性中(省略了错误处理):

auto  msgAttr  =  CreateMessageAttributes(ALPC_FLG_MSG_HANDLE_ATTR);
msgAttr->ValidAttributes  =  ALPC_FLG_MSG_HANDLE_ATTR;

//
//  create  an  event  to  share  with  the  server
//
HANDLE  hEvent;
NtCreateEvent(&hEvent,  EVENT_ALL_ACCESS,  nullptr,  SynchronizationEvent,  FALSE);
//
//  put  the  event  handle  information
//
auto  handleAttr  =   (PALPC_HANDLE_ATTR)AlpcGetMessageAttribute(msgAttr,
    ALPC_FLG_MSG_HANDLE_ATTR);
handleAttr->Flags  =  0;
handleAttr->Handle  =  hEvent;
handleAttr->ObjectType  =  OB_EVENT_OBJECT_TYPE;
handleAttr->DesiredAccess  =  EVENT_MODIFY_STATE   |    SYNCHRONIZE;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 10.5.4 令牌属性(Token Attribute,ALPC_FLG_MSG_TOKEN_ATTR)

可通过以下结构体提供令牌信息(token information):

typedef struct _ALPC_TOKEN_ATTR {
    LUID TokenId;
    LUID AuthenticationId;
    LUID ModifiedId;
} ALPC_TOKEN_ATTR, *PALPC_TOKEN_ATTR;
1
2
3
4
5

如果服务器(server)请求,这些信息会自动提供给服务器。结构体中的字段含义与令牌对象(token object)的对应属性一致。例如,AuthenticationId 是客户端(client)的登录会话ID(Logon Session Id)。

# 10.5.5 数据视图属性(Data View Attribute,ALPC_FLG_MSG_DATAVIEW_ATTR)

该属性允许在客户端和服务器之间共享一个区段(section)。需填充的结构体如下:

#define ALPC_VIEWFLG_UNMAP_EXISTING    0x00010000
#define ALPC_VIEWFLG_AUTO_RELEASE         0x00020000
#define ALPC_VIEWFLG_SECURED_ACCESS    0x00040000

typedef struct _ALPC_DATA_VIEW_ATTR {
    ULONG Flags;
    ALPC_HANDLE SectionHandle;
    PVOID    ViewBase;
    SIZE_T ViewSize;
} ALPC_DATA_VIEW_ATTR, *PALPC_DATA_VIEW_ATTR;
1
2
3
4
5
6
7
8
9
10

需在创建区段和视图(view)后填充结构体成员。第一步是调用 NtAlpcCreatePortSection 函数:

NTSTATUS NtAlpcCreatePortSection(
    _In_ HANDLE PortHandle,
    _In_ ULONG Flags,
    _In_opt_ HANDLE SectionHandle,
    _In_ SIZE_T SectionSize,
    _Out_ PALPC_HANDLE AlpcSectionHandle,
    _Out_ PSIZE_T ActualSectionSize);
1
2
3
4
5
6
7

客户端(client)通常调用此函数时,PortHandle 参数为 NtAlpcConnectPort 函数返回的已打开端口(opened port)。Flags 参数为 0 或 ALPC_VIEWFLG_SECURED_ACCESS(后者会使共享区段的访问仅限于相关进程)。SectionHandle 参数可以是现有区段对象(section object)的句柄(有关区段对象的更多信息,请参见第12章),也可以是 NULL——若为 NULL,该函数会创建一个大小为 SectionSize 的区段对象。函数的返回值包括 *AlpcSectionHandle 中的内部句柄(后续会存储到 ALPC_DATA_VIEW_ATTR 结构体中),以及区段的实际大小(可能比请求的大小向上取整)。

接下来,填充消息属性(message attributes)中的 ALPC_DATA_VIEW_ATTR 部分,然后客户端需通过 NtAlpcCreateSectionView 函数创建视图:

NTSTATUS NtAlpcCreateSectionView(
    _In_ HANDLE PortHandle,
    _Reserved_ ULONG Flags,
    _Inout_ PALPC_DATA_VIEW_ATTR ViewAttributes);
1
2
3
4

以下示例演示了这些步骤(省略错误处理):

ALPC_HANDLE hPortSection;
SIZE_T actualSize;

//
// 创建一个 4KB 的区段
//
NtAlpcCreatePortSection(hPort, 0, nullptr,
    1 << 12, &hPortSection, &actualSize);

auto dataView = (PALPC_DATA_VIEW_ATTR)AlpcGetMessageAttribute(
    msgAttr, ALPC_FLG_MSG_DATAVIEW_ATTR);

//
// 初始化区段视图属性
//
dataView->Flags = 0;
dataView->SectionHandle = hPortSection;
dataView->ViewSize = actualSize;
dataView->ViewBase = nullptr;      // 必须为 NULL

//
// 创建视图
//
NtAlpcCreateSectionView(hPort, 0, dataView);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

相应地,服务器可以提取这些信息并使用共享区段:

auto recvMsgAttr = CreateMessageAttributes(ALPC_FLG_MSG_ALL_ATTR);
NtAlpcSendWaitReceivePort(hServerPort, ..., recvMsgAttr, nullptr);
if (recvMsgAttr->ValidAttributes & ALPC_FLG_MSG_DATAVIEW_ATTR)
{
    auto dataView = (PALPC_DATA_VIEW_ATTR)AlpcGetMessageAttribute(
        recvMsgAttr, ALPC_FLG_MSG_DATAVIEW_ATTR);
    printf("Section map base address: 0x%p\n", dataView->ViewBase);
    // 使用 dataView->ViewBase ...
}
1
2
3
4
5
6
7
8
9

# 10.6 发送和接收消息(Sending and Receiving Messages)

ALPC(高级本地过程调用,Advanced Local Procedure Call)的核心函数是 NtAlpcSendWaitReceivePort:

NTSTATUS NtAlpcSendWaitReceivePort(
    _In_ HANDLE PortHandle,
    _In_ ULONG Flags,
    _In_reads_bytes_opt_(SendMessage->u1.s1.TotalLength) PPORT_MESSAGE SendMessage,
    _Inout_opt_ PALPC_MESSAGE_ATTRIBUTES SendMessageAttributes,
    _Out_writes_bytes_to_opt_(*Length, *Length) PPORT_MESSAGE ReceiveMessage,
    _Inout_opt_ PSIZE_T Length,
    _Inout_opt_ PALPC_MESSAGE_ATTRIBUTES ReceiveMessageAttributes,
    _In_opt_ PLARGE_INTEGER Timeout);
1
2
3
4
5
6
7
8
9

客户端和服务器都会使用该函数。它结合了发送(send)和接收(receive)两种操作(中间可选择等待),以最大限度地减少用户态/内核态切换(user/kernel transitions)。

PortHandle 是用于通信的端口句柄(port handle),可以是客户端或服务器端的句柄。Flags 参数为 0 或以下标志的组合:

  • ALPC_MSGFLG_SYNC_REQUEST(0x20000)——请求是同步的(synchronous)。函数会在消息处理完成或出现错误时返回。
  • ALPC_MSGFLG_RELEASE_MESSAGE(0x10000)——消息不需要对方回复。
  • ALPC_MSGFLG_DIRECT_RECEIVE(0x1000000)——直接消息传递(仅异步,async only)——需进一步研究。

SendMessage 是要发送给接收方的消息,其开头必须是 PORT_MESSAGE 类型的头部(header):

typedef struct _PORT_MESSAGE {
    union {
        struct {
            CSHORT DataLength;
            CSHORT TotalLength;
        } s1;
        ULONG Length;
    } u1;
    union {
        struct {
            CSHORT Type;
            CSHORT DataInfoOffset;
        } s2;
        ULONG ZeroInit;
    } u2;
    union {
        CLIENT_ID ClientId;
        double DoNotUseThisField;
    };
    ULONG MessageId;
    union {
        SIZE_T ClientViewSize;  // 对 LPC_CONNECTION_REQUEST 有效
        ULONG CallbackId;             // 对 LPC_REQUEST 有效
    };
    // 后续为数据(data follows)
} PORT_MESSAGE, *PPORT_MESSAGE;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

一种典型的用法是扩展该结构体以包含消息特定的详情。在 C++ 中可编写如下代码:

struct Message : PORT_MESSAGE {
    // 消息成员(示例)
    int SomeData;
    char TextData[32];
};
1
2
3
4
5

如果仅使用 C 语言,思路相同:

typedef struct _Message {
    PORT_MESSAGE Header;
    // 消息成员(示例)
    int SomeData;
    char TextData[32];
} Message;
1
2
3
4
5
6

当然,在某些场景下可能需要更具动态性的结构体,只要内存块的起始部分被视为 PORT_MESSAGE 即可。

大多数成员无需手动填充,只需填充 DataLength 和 TotalLength。以下是基于上述定义的示例:

Message msg;
msg.u1.s1.DataLength = sizeof(msg) - sizeof(PORT_HEADER);
msg.u1.s1.TotalLength = sizeof(msg);
1
2
3

其他详情会在消息到达接收方之前由函数自动填充。

接下来是 SendMessageAttributes 参数,它是指向所发送消息属性的可选指针(详见本章前文“消息属性(Message Attributes)”部分)。不提供任何属性也是完全合法的。

然后是 ReceiveMessage 参数,它是指向接收方返回消息的可选指针,其最大大小由 *BufferLength 指定。如果返回消息大于提供的缓冲区(buffer),则不会检索该消息,函数会返回错误(STATUS_BUFFER_TOO_SMALL),调用方必须分配更大的缓冲区(所需大小会存储在 *Length 中),然后再次调用 NtAlpcSendWaitReceivePort。

再之后是 ReceiveMessageAttributes 参数,它是指向与返回消息相关联的消息属性(若有)的可选指针。最后,Timeout 参数是同步调用(synchronous calls)中函数返回前的可选等待时间。若指定为 NULL,调用方会一直等待,直到操作完成或出现错误。

# 10.6.1 服务器操作(Server Operation)

服务器接收到消息后,必须通过检查 u2.s2.Type 成员(仅低8位)的消息类型来处理消息。可能的消息类型定义如下:

#define LPC_REQUEST                                1
#define LPC_REPLY                                    2
#define LPC_DATAGRAM                             3
#define LPC_LOST_REPLY                         4
#define LPC_PORT_CLOSED                      5
#define LPC_CLIENT_DIED                      6
#define LPC_EXCEPTION                           7
#define LPC_DEBUG_EVENT                      8
#define LPC_ERROR_EVENT                      9

#define LPC_CONNECTION_REQUEST    10
#define LPC_CONNECTION_REPLY         11
#define LPC_CANCELED                          12
#define LPC_UNREGISTER_PROCESS    13

// 标志(flags)
#define LPC_KERNELMODE_MESSAGE   (CSHORT)0x8000
#define LPC_NO_IMPERSONATE   (CSHORT)0x4000
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

有些消息的含义不言自明,例如 LPC_PORT_CLOSED(端口已关闭)和 LPC_CLIENT_DIED(客户端已终止)。

LPC_CONNECTION_REQUEST 消息表示客户端调用了 NtAlpcConnectPort 函数,希望与服务器的端口建立连接。服务器可以检查传入的消息,获取有助于决定是否允许连接请求的特定信息。做出决定后,服务器调用 NtAlpcAcceptConnectPort 函数:

NTSTATUS NtAlpcAcceptConnectPort(
    _Out_ PHANDLE PortHandle,
    _In_ HANDLE ConnectionPortHandle,
    _In_ ULONG Flags,
    _In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
    _In_opt_ PALPC_PORT_ATTRIBUTES PortAttributes,
    _In_opt_ PVOID PortContext,
    _In_reads_bytes_(ConnRequest->u1.s1.TotalLength) PPORT_MESSAGE ConnRequest,
    _Inout_opt_ PALPC_MESSAGE_ATTRIBUTES ConnectionMessageAttributes,
    _In_ BOOLEAN AcceptConnection);
1
2
3
4
5
6
7
8
9
10

最后一个参数 AcceptConnection 用于决定是否接受连接。即使拒绝连接,也可以通过 ConnRequest 传递一条消息,提供更多信息。PortHandle 是服务器内部用于与该客户端通信的未命名端口(unnamed port)的返回句柄(如果连接被接受)。ConnectionPortHandle 是 NtAlpcCreatePort 函数返回的服务器端口句柄。

Flags 参数通常为 0,但也可以是 ALPC_PORFLG_WOW64_CALL(0x80000000),强制将句柄视为32位,以避免句柄指针(HANDLE pointers)被覆盖。ObjectAttributes 和 PortAttributes 通常为 NULL(详见本章前文“端口属性(port attributes)”部分)。PortContext 是用户定义的值,存储为端口对象的一部分。它会在任何接收到的消息属性的 ALPC_CONTEXT_ATTR(若使用)中返回。最后,ConnectionMessageAttributes 是要返回的可选连接消息属性。

LPC_REQUEST 消息是一种可能需要回复的“普通”请求消息。而 LPC_DATAGRAM 消息则是一种“即发即弃”(fire and forget)的消息,不需要也不期望回复。当进行异步调用(未设置 ALPC_MSGFLG_SYNC_REQUEST)但设置了 ALPC_MSGFLG_RELEASE_MESSAGE 且无回复消息时,会使用这种消息类型。

# 10.6.2 客户端操作(Client Operation)

客户端通过 NtAlpcConnectPort 建立连接后,即可使用 NtAlpcSendWaitReceivePort 发送和接收消息。

要在异步请求(asynchronous request)完成时收到通知,一种方法是使用I/O完成端口(I/O completion port)。可通过 NtAlpcSetInformation 函数将I/O完成端口与ALPC端口关联:

NTSTATUS NtAlpcSetInformation(
    _In_ HANDLE PortHandle,
    _In_ ALPC_PORT_INFORMATION_CLASS PortInformationClass,
    _In_reads_bytes_opt_(Length) PVOID PortInformation,
    _In_ ULONG Length);
1
2
3
4
5

设置操作支持多种信息类(information class),其中之一是 AlpcAssociateCompletionPortInformation(值为2),该信息类对应的结构体如下:

typedef struct _ALPC_PORT_ASSOCIATE_COMPLETION_PORT {
    PVOID CompletionKey;
    HANDLE CompletionPort;
} ALPC_PORT_ASSOCIATE_COMPLETION_PORT, *PALPC_PORT_ASSOCIATE_COMPLETION_PORT;
1
2
3
4

CompletionKey 是要存储的任意值,CompletionPort 是有效的I/O完成端口句柄。可通过原生API NtCreateIoCompletion 或Windows API CreateIoCompletionPort 创建I/O完成端口对象。以下示例创建了一个I/O完成端口,并将其与ALPC端口关联:

ALPC_PORT_ASSOCIATE_COMPLETION_PORT iocp{};
NtCreateIoCompletion(&iocp.CompletionPort, IO_COMPLETION_ALL_ACCESS, nullptr, 0);
// 或使用 Windows API:
//iocp.CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, nullptr, 0, 0);

NtAlpcSetInformation(hPort, AlpcAssociateCompletionPortInformation,
    &iocp, sizeof(iocp));
1
2
3
4
5
6
7

异步操作不会指定回复消息或长度。相反,客户端会在I/O完成端口被触发后进行第二次调用。以下是概念性代码:

// 异步调用 - 传入消息,无接收消息
// 标志中不包含 ALPC_MSGFLG_SYNC_REQUEST
NtAlpcSendWaitReceivePort(hPort, 0, &msg,
    msgAttr, nullptr, nullptr, nullptr, nullptr);

// 执行其他工作...

// 在某个时刻(可能在另一个线程上)...
DWORD bytes;        // 无需使用,但必须提供
ULONG_PTR key;
OVERLAPPED* ov;

// 调用 Windows API GetQueuedCompletionStatus(Ex)
// 或原生 API NtRemoveIoCompletion(Ex) - 本专栏未描述
GetQueuedCompletionStatus(iocp.CompletionPort, &bytes, &key, &ov, INFINITE);

NtAlpcSendWaitReceivePort(hPort, 0, nullptr,
    nullptr, recvMsg, &recvMsgLen, nullptr, nullptr);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 10.7 总结

本章探讨了ALPC的部分特性。ALPC功能丰富,这既增加了它的复杂性,也赋予了它高度的灵活性。本章的介绍绝非详尽无遗,仍有许多内容需要进一步研究。希望本专栏的后续版本能包含更多关于ALPC的细节。

第9章:I/O
第11章 安全性(Security)

← 第9章:I/O 第11章 安全性(Security)→

最近更新
01
C++语言面试问题集锦 目录与说明
03-27
02
第四章 Lambda函数
03-27
03
第二章 关键字static及其不同用法
03-27
更多文章>
Copyright © 2024-2026 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式