65.9K
CodeProject 正在变化。 阅读更多。
Home

一种自主且隐藏的 IPC 机制

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.75/5 (12投票s)

2017 年 12 月 12 日

CPOL

12分钟阅读

viewsIcon

14038

downloadIcon

211

一个非常小巧的 IPC 机制,不依赖任何现有的系统调用或 API。

下载 AIPC.zip

引言

如果您有一个或多个模块在系统中独立运行,并且它们需要相互协作,该怎么办?好吧,它们将不得不能够交换数据,这些数据被组织为事件、消息或某种形式的数据包。通过发送和接收这些数据,这两个(或更多)应用程序可以处理请求,或者识别其对应程序所处的状态,并因此做出反应(例如,当它等待命令到达时)。IPC(进程间通信)是操作系统为运行在其之上的应用程序提供的一项基本服务。

IPC 本身通常是一项功能,已经在网上大量的文章中得到了充分的文档记录和介绍。虽然这在一方面极大地帮助了应用程序开发人员,但这些机制也被各种监控应用程序和恶意软件所了解,它们会扫描提供的 API/句柄(或执行挂钩)以检测 IPC 元素并识别它们。如果您想要使用一些不常用的东西,该怎么办?如果您需要在多个进程/模块之间进行通信而不触发即时检测(例如,通过防火墙应用程序)怎么办?

在这篇短文中,我提出了一种使用现有机制进行进程间通信的替代方法。该软件不依赖任何系统调用,也不依赖 Windows 官方认可的任何已知 IPC 机制。这种方法应该提供一种足够隐藏的数据交换方式,因为它不会创建任何可以通过使用 OS API 检测到的系统/命名对象。

提供的原型适用于 Windows 10 的 32 位和 64 位版本,并且可以理想地导出到任何其他操作系统,因为它基于一种通常已包含在操作系统本身中的基本机制。

背景

读者应熟悉 C 编程语言。建议了解 Windows 子系统,特别是内存和同步。我将在文章中尽量降低技术细节的水平,将进一步的研究留给读者。

Windows 中的 IPC

Windows 已经提供了许多允许属于不同地址空间的进程之间进行通信的方式。由于现代操作系统中的内存是虚拟化的,进程生活在自己的“宇宙”中,这可能会与另一个进程的“宇宙”重叠。在底层,操作系统内存管理子系统会执行所有必要的技巧和转换,将地址映射到不同的内存物理区域,以避免覆盖另一个进程控制下的现有区域。

这意味着地址 0x00007c00 可以同时存在于两个进程中(并且将包含不同的代码/数据)。这两个应用程序没有意识到操作系统为它们各自映射了这个地址到不同的物理位置,而且它们应该(并且不会)关心。

IPC 允许数据跨越这些“宇宙”并在两侧之间传递。这个操作通常是不可能的,因为进程看不到另一个进程的“外部宇宙”(地址空间),并且需要一个具有扩展内存视图(操作系统)的实体来为其执行操作。

Windows 中现有的 IPC 机制包括:

  • 剪贴板
  • COM
  • 数据复制
  • DDE
  • 文件映射
  • 邮槽
  • 管道
  • RPC
  • Windows 套接字

以上每种方法都需要系统提供的实用程序来执行通信。

那么,我们如何在不使用现有方法和不调用系统/子系统的情况下,组织一个或多个进程(以及线程)之间的通信呢?请记住,应用程序无法看到其“可观察宇宙”之外的事物。

DLL

DLL 是动态链接库,它们提供了可以被多个进程同时调用的通用功能。MSDN 在这里提供了关于它们是什么以及如何使用的良好描述。DLL 本质上提供了一种一次导出模块,该模块可以被多个其他元素(通常是应用程序和服务)同时使用。

这样做是为了避免为使用该功能的每个应用程序在内存中复制相同的代码。例如,默认情况下,应用程序会在其地址空间内的特定地址自动加载 **ntdll.dll** 和 **kernel32.dll**。如果不修改 Windows 加载器,这种行为就无法改变,而且 Windows 加载器不容易修改(因为它是一个非常敏感的 OS 部分),至少我做不到。DLL 机制允许这些例程在内存中只有 **一个副本**,否则会消耗大量的内存。

现在,基于这个假设,人们很容易认为在 DLL 中定义数据可以使共享该库的应用程序之间有一个公共池,但这 **并非** 如此。实际上,数据节是库中为导入它们的每个进程个性化的一部分。您不希望其他进程在您运行服务时干扰您的状态机的状态,而数据变量通常包含仅绑定到特定应用程序的元素。

死胡同?完全不是!

神奇的共享节

共享节是内存页面的特殊标记,表示它们同时存在于多个地址空间中。共享节是进程空间中包含与其他(或更多)地址空间相同元素的那些部分,并且这些页面的虚拟位置被映射到相同的物理页面(这意味着如果一个进程中的内存发生变化,那么所有进程都会发生变化)。这可能让人想起 Windows API 提供的 **文件映射** IPC 机制(并且其背后的技术是相同的),但 **如何** 实现它则完全不同(实际上这里没有调用 CreateFileMapping/MapViewOfFile)。

AIPC DLL 中所做的是将特定变量集标记为驻留在标记为共享的节中。在 Visual Studio 和 MSBuild 中,这是使用以下语法完成的:

typedef struct __AIPCEntry {
        /* Lock for this entry resources */
        volatile long lock;

        /* Head index for the data */
        int           head;
        /* Tail index */
        int           tail;

        /* Data stored, organized as a circular buffer */
        BYTE          pool[AIPC_POOL_SIZE];
} AIPCEntry;

#pragma data_seg(".aipcs")

AIPCEntry AIPCEntries[AIPC_NOF_POOLS] = { 0 };

#pragma comment(linker, "/section:.aipcs,RWS")
#pragma data_seg()

一个名为 AIPCEntry 的结构(在代码文件的开头定义)组织了该区域内的内存。在该数组的每个条目中,我们有一些索引(用于通过循环缓冲区策略处理读/写)以及数据本身的区域(池变量)。如果您注意到该区域是静态分配的,因此不会调用内存保护或内存分配例程(这是一个期望的功能)。

当 Windows 加载器映射 DLL 时(使用 LoadLibrary 例程或在应用程序导入表中引用它),它将神奇地为我们处理我们地址空间内的一切。同样,这里不再需要系统调用,因为所有这些都已由系统应用程序加载器考虑在内。

由于它是共享的,因此 **任何** 导入该库的应用程序都可以访问该节。

临界区和并发

仅仅将内存暴露给应用程序并不能解决问题。它们将不得不协调对这些资源的访问,否则数据最终会被损坏。这通常会发生,如果两个进程尝试同时写入,或者如果其中一个进程在操作期间被抢占(暂停),导致一半的数据已写入,一半待写入。

幸运的是,Windows 提供了一些原始 API 来帮助您组织临界区:**Interlocked*** 操作。Interlocked 程序提供了一种安全访问共享变量的方法。这可以保证在并发 Interlocked 操作之间是有效的。

这意味着,只要您仅使用 Interlocked 调用来访问变量,该操作就是原子的,不会弄乱变量的状态。这足以让我们准备一个基本的锁定机制来访问我们的资源!

**AIPCRead** 和 **AIPCWrite** 是 AIPC DLL 导出的唯一两个过程,它们使用数据共享区域中定义的 **lock** 变量来同步对 IPC 机制使用的缓冲区的访问。任何试图修改数据的尝试都受到以下条件的保护:

while (InterlockedCompareExchange(
        &AIPCEntries[id].lock, 
        AIPC_LOCKED, 
        AIPC_UNLOCKED) == AIPC_LOCKED) {
                
        if (flags & AIPC_FLAGS_NOBLOCK) {
                return -ERROR_RETRY;
        }

        /* Schedule to processes with similar priority */
        Sleep(0);
}

此循环默认等待 lock 被释放后再继续。请注意,如果您在 AIPC* 例程标志中指定了 1(AIPC_FLAGS_NOBLOCK 宏的值),则表示您请求了 NOBLOCK 操作,并且执行将立即返回一个负错误代码 ERROR_RETRY(如果 lock 被其他人持有)。如果您想避免线程被系统调度,请使用非阻塞操作。

在每次操作结束时,将释放相对于条目的 lock,允许其他进程请求 lock 并检查/修改数据。

/* Release the Lock */
InterlockedExchange(&AIPCEntries[id].lock, AIPC_UNLOCKED);

测试机制

AIPCTest 实用程序是一个小型应用程序,用于测试 DLL 提供的功能。如您所见,该程序没什么特别之处,它可以执行的测试有限,但表明了正确的行为。您可以通过修改代码来自定义应用程序的工作方式,例如,删除等待在共享区域写入新数据的操作,该操作通常设置为 1 秒。

因此,注释掉此行,使应用程序运行尽可能快(但使用阻塞调用)

#define WAIT

如果您想测试非阻塞机制,则需要将预处理器声明的标志设置为 1,如下所示:

#define FLAGS   1

第一次测试将在未修改代码的情况下进行,仅用于演示 IPC 机制正在工作。构建项目并导航到 Bin\Debug\Win32 文件夹(请注意 Debug 和 Win32 取决于您如何构建项目)。按住 Shift 右键单击,然后在同一文件夹中启动两个 PowerShell 实例,以进行实验。

在第一个控制台中运行命令:

.\AIPCTest.exe 1 0 1 10 0

您将看到实例以 ID 1(第一个参数)运行,并等待发生某些事情。它将读取 IPC 插槽 0,写入 IPC 插槽 1,共 10 次,并且不会以写入操作开头(最后一个参数为 0)。

然后在第二个控制台中运行以下命令:

.\AIPCTest.exe 2 1 0 10 1

此实例的 ID 将为 2,从插槽 1 读取,写入插槽 0,共 10 次,并将以写入触发等待实例 1 的数据交换。您最终将看到以下内容,在 PS 1 上:

PS E:\Projects\AIPC\Bin\Debug\Win32> .\AIPCTest.exe 1 0 1 10 0
Running instance 1...
    Hello from 2...
    Hello from 2...
    Hello from 2...
    Hello from 2...
    Hello from 2...
    Hello from 2...
    Hello from 2...
    Hello from 2...
    Hello from 2...
    Hello from 2...

而在第二个实例中,跟踪将显示:

PS E:\Projects\AIPC\Bin\Debug\Win32> .\AIPCTest.exe 2 1 0 10 1
Running instance 2...
    Hello from 1...
    Hello from 1...
    Hello from 1...
    Hello from 1...
    Hello from 1...
    Hello from 1...
    Hello from 1...
    Hello from 1...
    Hello from 1...
    Hello from 1...

我们做到了!

两个独立的进程已成功地相互交换了数据,而无需使用任何现有的 IPC 机制或任何类型的系统调用。

对于下一次测试,您可以通过注释掉 WAIT 预处理器定义来禁用等待行为并重复测试。您将看到一个非常快速的跟踪,与上一次完全相同。或者,您可以尝试在同一个读/写插槽上运行两个实例,这样就可以模拟一个具有更高并发性的设置。

用于此类测试的命令是(输出包含在内):

.\AIPCTest.exe 1 1 1 10 0
Running instance 1...
    Hello from 2...
    Hello from 1...
    Hello from 1...
    Hello from 1...
    Hello from 2...
    Hello from 2...
    Hello from 1...
    Hello from 1...
    Hello from 1...
    Hello from 1...

以及(同样,输出包含在内):

.\AIPCTest.exe 2 1 1 10 1
Running instance 2...
    Hello from 2...
    Hello from 1...
    Hello from 2...
    Hello from 1...
    Hello from 2...
    Hello from 2...
    Hello from 2...
    Hello from 2...
    Hello from 2...
    Hello from 2...

同样,如果您想建立更复杂的设置,您可以尝试增加争夺 IPC 资源访问的进程数量,如下所示(您需要其他控制台来继续测试):

控制台 1

.\AIPCTest.exe 1 1 1 50 0

控制台 2

.\AIPCTest.exe 2 1 1 50 0

控制台 3

.\AIPCTest.exe 3 1 1 50 0

控制台 4

.\AIPCTest.exe 4 1 1 50 1

输出将取决于您的操作系统如何调度进程,但要点是看到的输出**从未损坏**,因为它受到这些原始(但有效)锁的保护。

结论

本文介绍了一种以离散方式执行 IPC 的方法,该方法不依赖任何现有的官方 IPC 机制。该机制是自主的,因为它不需要操作系统的任何支持(除了 DLL 的首次加载),并且保证了交换数据的安全性。

它不对给定数据做任何假设。您写入一个插槽,用数据加载它,并通过消耗这些数据来读取它。协议和使用的同步取决于交换这些信息的应用程序。由于交换的是通用字节,应用程序也可以写入加密的消息,从而确保免受可能嗅探共享节并窥视数据的外部应用程序的侵害。

安全注意事项

我实际上并不想在这个方法中涉及安全方面,因为文章只关注 IPC 机制,但由于一些评论(谢谢 Randor 的反馈)促使我,所以我们开始了……

该机制是安全的,但**不**是安全的;这意味着它能防止并发,但不能防止恶意方法。任何加载 DLL 的人都可以访问共享节(好吧,这就是目的),并获得对包含数据的内存区域的访问权。由于 API 很小,逆向工程代码的工作量微不足道。一旦您检测到数据所在的地址,您就可以窥视其内容,甚至弄乱和损坏它。

我没有设计这个 API 来防范恶意访问,但有些方面可以为使用这种方法提供额外的安全性

  • 请考虑该机制是不安全的,并考虑任何人都可能持有锁并且不再归还它;就像任何网络通信一样,您不希望您的应用程序仅仅因为没有网络连接而崩溃。
     
  • 通过加密在共享缓冲区中流动的数据来提供额外的安全性;仍然可以窥视它,但不太可能解密。
     
  • 为了提供额外的完整性,您还可以附加一个签名(例如 SHA)来检测数据在此过程中是否已损坏(某些外部应用程序随机更改缓冲区字节以导致 IPC 启用实用程序崩溃)。

如果您想要额外的安全性,您可以选择 Windows 的官方 API,但这**不是**本文的**范围**。我在第一章提供了一个常见且经过调试的机制列表,这些机制可以被利用;请查看它们。

我想强调的是,适用于此机制的许多顾虑也适用于其他官方 IPC 机制。例如,您始终可以窥探命名管道,或篡改内部通信(例如,拦截本地套接字数据包或注入一些东西)。不同之处在于,操作系统为您提供了一个可以用来最小化安全问题的安全环境。

如果您决定在应用程序中使用这种方法,您必须**知道您在做什么**。它可能是一个强大的工具,但也是一个可怕的头痛(在我看来,就像任何其他方法一样),所以由您来选择正确的药丸;红色还是蓝色?

历史

2017 年 12 月 12 日 - 文章初次发布

© . All rights reserved.