我想构建一个 C 预处理器/编译器,允许从本地和在线源收集函数。IE:

#fetch MP3FileBuilder http://scripts.com/MP3Builder.gz
#fetch IpodDeviceReader http://apple.com/modules/MP3Builder.gz

void mymodule_main() {
  MP3FileBuilder(&some_data);
}

这是最简单的部分。

困难的部分是 我需要一种可靠的方法来“沙箱”导入的代码,以便直接或不受限制地访问磁盘或系统资源(包括内存分配和堆栈). 。我想要一种安全运行的方法 不受信任的 C 代码小片段 (模块),无需将它们放入单独的进程、VM 或解释器中(尽管单独的线程是可以接受的)。

要求

  • 我需要对其数据和资源(包括 CPU 时间)的访问设置配额。
  • 我将阻止对标准库的直接访问
  • 我想阻止产生无限递归的恶意代码
  • 我想将静态和动态分配限制在特定限制内
  • 我想捕获模块可能引发的所有异常(例如除以 0)。
  • 模块只能通过核心接口与其他模块交互
  • 模块只能通过核心接口与系统(I/O 等)交互
  • 模块必须允许位操作、数学、数组、枚举、循环和分支。
  • 模块不能使用ASM
  • 我想限制指针和数组对为模块保留的内存的访问(通过自定义 safe_malloc())
  • 必须支持 ANSI C 或其子集(见下文)
  • 系统必须是轻量级的、跨平台的(包括嵌入式系统)。
  • 系统必须兼容 GPL 或 LGPL。

我很高兴选择 C ​​的一个子集。我不需要模板或类之类的东西。我主要对高级语言不太擅长的事情感兴趣,比如快速数学、位运算以及二进制数据的搜索和处理。

这是 不是 其目的是无需修改即可重用现有 C 代码来创建模块。目的是要求模块遵守一组规则和限制,旨在将模块限制为基本逻辑和转换操作(例如视频转码或压缩操作)。

这种编译器/预处理器的理论输入将是带有 module_main 函数的单个 ANSI C 文件(或安全子集),无包含或预处理器指令,无 ASM,它将允许循环、分支、函数调用、指针数学(仅限于分配给模块的范围)、位移位、位域、强制转换、枚举、数组、整数、浮点数、字符串和数学。其他任何事情都是可选的。

实施示例

这是一个伪代码片段,可以更好地解释这一点。这里的模块超出了它的内存分配配额,并且还创建了无限递归。

buffer* transcodeToAVI_main( &in_buffer ) {
    int buffer[1000000000]; // allocation exceeding quota
    while(true) {} // infinite loop
    return buffer;
}

这是一个转换后的版本,其中我们的预处理器添加了观察点来检查内存使用情况和递归,并将整个过程包装在异常处理程序中。

buffer* transcodeToAVI_main( &in_buffer ) {
    try {
        core_funcStart(__FILE__,__FUNC__); // tell core we're executing this function
        buffer = core_newArray(1000000000, __FILE__, __FUNC__); // memory allocation from quota
        while(true) {
           core_checkLoop(__FILE__, __FUNC__, __LINE__) && break; // break loop on recursion limit
        } 
        core_moduleEnd(__FILE__,__FUNC__);
    } catch {
        core_exceptionHandler(__FILE__, __FUNC__);
    }
    return buffer;
}

我意识到执行这些检查会影响模块性能,但我怀疑对于它要解决的任务,它仍然会优于高级语言或虚拟机语言。我并不是想完全阻止模块做危险的事情,我只是想强制这些危险的事情以受控的方式发生(比如通过用户反馈)。IE:“模块 X 已超出其内存分配,继续还是中止?”。

更新

到目前为止,我得到的最好的方法是使用带有边界检查和一些自定义函数和循环代码的自定义编译器(例如黑客TCC)来捕获递归。我仍然想听听关于我还需要检查什么或有哪些解决方案的想法。我想在使用之前删除 ASM 并检查指针可以解决下面之前的答案中表达的许多问题。我添加了一笔赏金,以从 SO 社区获取更多反馈。

对于我正在寻找的赏金:

  • 针对上述理论系统的潜在利用的详细信息
  • 对每次访问检查指针的可能优化
  • 概念的实验性开源实现(例如 Google Native Client)
  • 支持各种操作系统和设备的解决方案(无基于操作系统/硬件的解决方案)
  • 支持大多数 C 操作甚至 C++ 的解决方案(如果可能的话)

可以与 GCC 一起使用的方法(即预处理器或 小的 海湾合作委员会补丁)。

我也会考虑任何能够最终证明我正在尝试的事情根本无法完成的人。不过,你需要非常有说服力,因为到目前为止,没有任何反对意见能够真正阐明他们认为这是不可能的技术方面的原因。为了给那些说不的人辩护,这个问题最初是作为一种安全运行 C++ 的方法提出的。我现在已将需求缩减到 C 的有限子集。

我对C的理解可以归类为“中级”,我对PC硬件的理解可能比“高级”低一级。如果可以的话,尝试指导您的答案达到该级别。由于我不是 C 专家,所以我将主要基于对答案的投票以及答案与我的要求的接近程度。您可以通过为您的主张提供充分的证据(受访者)和投票(其他人)来提供帮助。赏金倒计时达到 6 小时后我将分配答案。

最后,我相信解决这个问题将是在日益网络化和偏执的世界中保持 C 语言相关性的重要一步。随着其他语言在性能方面缩小差距并且计算能力不断增强,证明 C 开发的额外风险是合理的(就像现在的 ASM 一样)。我相信您的答案将比获得一些 SO 点具有更大的相关性,因此请尽您所能,即使赏金已过期。

有帮助吗?

解决方案

由于 C 标准太宽泛而无法被允许,因此您需要采取相反的方式:指定您需要的 C 的最小子集,并尝试实现它。即使 ANSI C 也已经太复杂并且允许出现不需要的行为。

C 中最有问题的方面是指针:C语言需要指针算术,而这些不被检查。例如:

char a[100];
printf("%p %p\n", a[10], 10[a]);

都会打印相同的地址。自从 a[10] == 10[a] == *(10 + a) == *(a + 10).

所有这些指针访问都无法在编译时检查。这与向编译器询问“程序中的所有错误”的复杂性相同,这需要解决停止问题。

由于您希望此函数能够在同一进程中(可能在不同的线程中)运行,因此您在应用程序和“安全”模块之间共享内存,因为这就是拥有线程的全部意义:共享数据以加快访问速度。然而,这也意味着两个线程可以读取和写入相同的内存。

由于您无法证明编译时指针最终位于何处,因此您必须在运行时执行此操作。这意味着像“a[10]”这样的代码必须翻译成像“get_byte(a + 10)”这样的代码,此时我不会再称它为C。

谷歌原生客户端

那么如果这是真的,那么谷歌是如何做到的呢?嗯,与这里的要求(跨平台(包括嵌入式系统))相反,Google 专注于 x86,除了具有页面保护的分页之外,还具有分段寄存器。这允许它创建一个沙箱,其中另一个线程不会以相同的方式共享相同的内存:沙箱通过分段仅限于更改其自己的内存范围。此外:

  • 已组装安全 x86 组装结构列表
  • gcc 被更改为发出那些安全结构
  • 该列表是以可验证的方式构建的。
  • 加载模块后,验证完成

因此,这是特定于平台的,并不是一个“简单”的解决方案,尽管是一个有效的解决方案。阅读更多内容 研究论文.

结论

因此,无论您走什么路线,您都需要从可验证的新事物开始,只有这样,您才能从适应现有的编译器或生成新的编译器开始。然而,尝试模仿 ANSI C 需要考虑指针问题。Google 不是在 ANSI C 上建模他们的沙箱,而是在 x86 的子集上建模,这使得他们能够在很大程度上使用现有的编译器,但缺点是与 x86 绑定。

其他提示

我认为您会从阅读 Google 在设计时所做的一些实施问题和选择中获益匪浅 本地客户端, ,一个在浏览器中执行 x86 代码(我们希望安全)的系统。您可能需要进行一些源代码重写或源到源编译 制作 如果代码不安全,那么代码是安全的,但是如果 NaCL 沙箱尝试执行任何过于时髦的操作,您应该能够依靠 NaCL 沙箱来捕获生成的汇编代码。

如果我要这样做,我会研究以下两种方法之一:

  • 使用 CERN 的 中央情报局 在解释器中运行沙盒代码并查看限制解释器允许的内容。这可能不会提供非常好的性能。
  • 使用 LLVM 创建 C++ 代码的中间表示,然后查看在沙盒 Java 风格的 VM 中运行该字节码是否可行。

然而,我同意其他人的观点,即这可能是一个非常复杂的项目。查看网络浏览器因有错误或挂起的插件而导致整个浏览器不稳定的问题。或者查看发行说明 Wireshark 项目;似乎几乎每个版本都包含针对其中一个协议解析器中的问题的安全修复程序,这些问题随后会影响整个程序。如果 C/C++ 沙箱可行,我希望这些项目现在已经锁定其中一个。

这不是一件小事,但也不是那么难。

您可以在沙箱中运行二进制代码。每个操作系统整天都在这样做。

他们将不得不使用您的标准库(相对于通用 C 库)。您的标准库将强制执行您想要施加的任何控制。

接下来,您需要确保它们无法在运行时创建“可运行代码”。也就是说,堆栈不可执行,它们无法分配任何可执行的内存,等等。这意味着只有编译器(您的编译器)生成的代码才是可执行的。

如果您的编译器以加密方式对其可执行文件进行签名,您的运行时将能够检测到被篡改的二进制文件,并且不会加载它们。这可以防止他们将您不希望他们拥有的东西“插入”二进制文件中。

通过生成“安全”代码的受控编译器和受控系统库,即使使用实际的机器语言代码,也应该提供合理受控的沙箱。

想要施加内存限制吗?检查 malloc。想要限制分配多少堆栈?限制堆栈段。

操作系统整天使用其虚拟内存管理器创建此类受限环境,因此您可以轻松地在现代操作系统上执行这些操作。

我不能说与使用现成的虚拟机和字节代码运行时相比,这样做是否值得。

我偶然发现 Tiny C 编译器 (TCC). 。这可能就是我需要的:

*  SMALL! You can compile and execute C code everywhere, for example on rescue disks (about 100KB for x86 TCC executable, including C preprocessor, C compiler, assembler and linker).
* FAST! tcc generates x86 code. No byte code overhead. Compile, assemble and link several times faster than GCC.
* UNLIMITED! Any C dynamic library can be used directly. TCC is heading torward full ISOC99 compliance. TCC can of course compile itself.
* SAFE! tcc includes an optional memory and bound checker. Bound checked code can be mixed freely with standard code.
* Compile and execute C source directly. No linking or assembly necessary. Full C preprocessor and GNU-like assembler included.
* C script supported : just add '#!/usr/local/bin/tcc -run' at the first line of your C source, and execute it directly from the command line.
* With libtcc, you can use TCC as a backend for dynamic code generation.

这是一个非常小的程序,这使得对其进行黑客攻击成为一个可行的选择(黑客 GCC?,这辈子都不会!)。我怀疑它将为构建我自己的受限编译器奠定良好的基础。我将删除对无法保证安全的语言功能的支持,并包装或替换内存分配和循环处理。

TCC已经可以做到 内存访问的边界检查, ,这是我的要求之一。

libtcc 也是一个很棒的功能,因为我可以在内部管理代码编译。

我并不认为这很容易,但它给了我希望,我可以以更少的风险获得接近 C 的性能。

但仍然想听听其他想法。

完全不可能。语言不是这样运作的。类的概念在大多数编译器(包括 GCC)中很早就消失了。即使是这样,也无法将每个内存分配与活动对象关联起来,更不用说“模块”了。

我还没有仔细研究过这个问题,但是从事 Chromium(又名 Google Chrome)工作的人已经在开发几乎像这样的沙箱了,这可能值得研究一下。

http://dev.chromium.org/developers/design-documents/sandbox/Sandbox-FAQ

它是开源的,所以应该可以使用它。

如果语言是图灵完备的,那么不可能制作一个静态代码验证器来确定所有可能的代码,一组代码是安全的还是不安全的。这相当于停机问题。

当然,如果您的主管代码运行在较低的环级别或是解释语言(即,模拟机器资源)。

最好的方法是在另一个进程中启动代码(ipc 还不错),并捕获像 linux 中的 Ptrace 这样的系统调用 http://linux.die.net/man/2/ptrace

李然指出 键盘网站 在上面的评论中。它不合适,因为它依赖于非常繁重的环境(由 ptrace、chroot 和出站防火墙组成),但是我发现有一些 g++ 安全开关,我想我会在这里分享:

GCC 4.1.2标志:-O -​​fmessage-length=0 -fno-merge-constants -fstrict-aliasing -fstack-protector-all

G ++ 4.1.2标志:-O -​​std=c++98 -迂腐错误 -Wfatal-errors -Werror -Wall -Wextra -Wno-missing-field-initializers -Wwrite-strings -Wno-deprecated -Wno-unused -Wno-non-virtual- dtor -Wno-variadic-macros -fmessage-length = 0 -ftemplate-深度-128 -fno-merge-constants -fno-nonansi-builtins -fno-gnu-keywords -fno-elide-constructors -fstrict-aliasing -fstack-保护器-all-Winvalid-pch

这些选项的解释见 海湾合作委员会手册

真正引起我注意的是堆栈保护标志。我相信这是 IBM 研究项目的合并(堆栈粉碎保护器)与官方海湾合作委员会。

该保护是通过缓冲区溢出检测和变量重新排序功能来实现的,以避免指针损坏。缓冲区溢出检测的基本思想来自于 堆栈卫士 系统。

新颖的功能是(1)对局部变量进行重新排序,将缓冲区放置在指针之后,以避免指针损坏,从而进一步损坏任意内存位置,(2)将函数参数中的指针复制到局部变量之前的区域缓冲区以防止指针损坏,指针损坏可用于进一步损坏任意内存位置,以及 (3) 从某些函数中省略检测代码以降低性能开销。

8 年后,我发现了一个可以满足我最初所有要求的新平台。 网络组装 允许您在浏览器中安全地运行 C/C++ 子集,并具有与我的要求类似的安全限制,例如限制内存访问并防止操作系统和父进程上的不安全操作。它已在 Firefox 52 中实现,并且有迹象表明其他浏览器将来也会支持它。

好主意,但我相当确定你想要做的事情用 C 或 C++ 是不可能的。如果你放弃了沙盒的想法,它可能会起作用。

Java 在 Maven2 中已经有了一个类似的系统(如在大型第三方代码库中)

如果你想真正确定的话,我认为最好的、也许唯一的方法就是沿着单独的进程进行,让操作系统处理访问控制。它不是 编写一个通用的线程加载器是很痛苦的,一旦拥有它,您可以重写一些函数来加载特定的库。

你似乎正在尝试解决两个非问题。在我自己的代码中,我没有内存分配问题或递归或无限循环问题。

您似乎提议的是一种与 C++ 不同的、更受限制的语言。当然,这是你可以追求的东西,但正如其他人指出的那样,你必须为其编写一个编译器 - 简单的文本处理不会给你你想要的东西。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top