我正在考虑一些我必须在 C++ 中实现的 RPC 代码,我想知道通过网络将它发送到相同的二进制代码是否安全(以及在哪些假设下)(假设它完全相同并且它们运行相同)建筑学)。我想虚拟内存应该在这里有所作为。
我只是出于好奇才问它,因为无论如何它都是一个糟糕的设计,但我想知道它在理论上是否可行(以及它是否可以扩展到其他类型的指向静态数据的指针,而不是程序可能包含的函数)。
我正在考虑一些我必须在 C++ 中实现的 RPC 代码,我想知道通过网络将它发送到相同的二进制代码是否安全(以及在哪些假设下)(假设它完全相同并且它们运行相同)建筑学)。我想虚拟内存应该在这里有所作为。
我只是出于好奇才问它,因为无论如何它都是一个糟糕的设计,但我想知道它在理论上是否可行(以及它是否可以扩展到其他类型的指向静态数据的指针,而不是程序可能包含的函数)。
一般来说,由于许多原因,它并不安全,但在有限的情况下它会起作用。首先,我将假设您在协议中使用某种签名或加密来确保数据流的完整性;如果没有,那么您已经遇到了严重的安全问题,这些问题只能通过传递函数指针来复杂化。
如果完全相同的程序二进制文件在连接的两端运行,如果函数在主程序中(或在从静态库链接的代码中)而不是在共享库中,并且如果程序不是作为位置构建的-独立可执行文件(PIE),那么函数指针在两端将是相同的,并且通过网络传递它应该可以工作。请注意,这些是非常严格的条件,必须作为使用程序的一部分记录在案,而且它们非常脆弱;例如,如果有人升级了一侧的软件,同时忘记升级连接另一侧的版本,事情就会严重而危险地中断。
我会完全避免这种类型的低级 RPC 以支持更高级别的命令结构或抽象 RPC 框架,但如果你真的想这样做,一个稍微安全一点的方法是传递函数名并使用dlsym
或等效于查看他们起来。如果符号驻留在主程序二进制文件中而不是库中,则根据您的平台,您可能需要-rdynamic
(GCC) 或类似选项以使它们可用于dlsym
. libffi
也可能是抽象这一点的有用工具。
此外,如果您想避免依赖dlsym
or libffi
,您可以将自己的“符号表”硬编码在二进制文件中,作为static const
线性表或哈希表将符号名称映射到函数指针。ELF 中用于此目的的哈希表格式非常易于理解和实现,因此我可能会考虑基于此实现您的实现。
它指向什么?
它是指向一块静态程序内存的指针吗?如果是这样,请不要忘记它是地址,而不是偏移量,因此您首先需要相应地在两者之间进行转换。
其次,如果它不是一块静态内存(即:在构建时而不是运行时创建的静态分配数组),那根本不可能。
最后,您如何确保两段代码相同?两个二进制文件是否相同(例如:)diff -a binary1 binary2
。即使它们是位相同的,取决于每台机器上的虚拟内存管理,整个程序的程序内存段可能不存在于单个页面中,或者跨多个页面的对齐方式对于每个系统可能不同。
这真是个坏主意,不管你怎么切。这就是消息传递和 API 的用途。
这是高度依赖于系统的。在具有虚拟寻址的系统上,每个进程每次执行时都认为它在同一个地址上运行,这可能适用于可执行代码。Darren Kopp 关于 ASLR 的评论和链接很有趣——快速阅读 Wikipedia 文章表明 Linux 和 Windows 版本专注于数据而不是可执行代码,除了 Linux 上的“面向网络的守护进程”,而在 Windows 上,它仅适用于“特别链接为启用 ASLR”。
尽管如此,静态链接最好确保“相同的二进制代码”——如果加载了不同的共享对象/库,或者它们以不同的顺序加载(可能是由于动态加载——dlopen
由配置文件或命令行参数中的不同顺序驱动等等)你可能已经吃饱了。
我不知道有任何形式的 RPC 可以让你通过网络发送一个指针(至少不做类似强制转换之类的事情int
)。如果您确实int
在发送端转换为,并将其转换回远端的指针,则与将任何其他任意指针转换int
为指针几乎相同:如果您尝试取消引用它,则为未定义的行为。
通常,如果您传递一个指向 RPC 函数的指针,它将被编组——即,它指向的数据将被打包、发送、放入内存,以及指向传递给的数据的本地副本的指针另一端的功能。这就是 IDL 变得有点丑陋的原因/方式的一部分——您需要告诉它如何确定当/如果您传递指针时要通过线路发送多少数据。大多数人都知道以零结尾的字符串。对于其他类型的数组,您通常需要指定数据的大小(不知何故)。
通过网络发送指针通常是不安全的。两个主要原因是:
如果我是你,我会设计一些不同的东西。而且我会确保传输的数据是不重要的或加密的,并且接收部分在使用它之前对其进行必要的验证,因此没有缓冲区溢出或执行任意事情。
如果您正在寻找一些正式的保证,我无法帮助您。您必须查看您正在使用的编译器和操作系统的文档 - 但是我怀疑您会找到必要的保证 - 可能除了一些专门的嵌入式系统操作系统。
但是,我可以为您提供一种情况,我 99.99% 确信它可以正常工作:
如果要调用 DLL 中的函数,可能会遇到问题。根据上面的列表,模块(=DLL)可能没有重定位信息,这当然使得它不可能重定位(这是我们需要的)。不幸的是,这也意味着加载 DLL 将失败,如果“首选加载地址”被其他东西使用。所以这有点冒险。
但是,如果该函数驻留在 EXE 中,则应该没问题。32 位 EXE 不需要重定位信息,并且大多数不包括它(MSVC 默认设置)。顺便说一句:ASLR 在这里不是问题,因为 a) ASLR 仅移动标记为要移动的模块,并且 b) ASLR 无法在没有重定位信息的情况下移动 32 位 Windows 模块,即使它想要移动。
以上大部分内容只是确保该函数在双方都具有相同的地址。剩下的唯一问题——至少我能想到的——是:假设字节模式是如果我们获取了所需函数的地址,我们会得到相同的结果吗?这肯定是 C++ 标准不能保证的,但我不希望当前的实际编译器会出现任何实际问题。
话虽如此,我不建议这样做,除非安全性和稳健性真的不重要。