39

我想构建一个 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;
}

我意识到执行这些检查会影响模块性能,但我怀疑它仍然会胜过高级或 VM 语言来解决它打算解决的任务。我并不是想直接阻止模块做危险的事情,我只是想强迫那些危险的事情以可控的方式发生(比如通过用户反馈)。即:“模块 X 已超出其内存分配,继续还是中止?”。

更新

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

对于我正在寻找的赏金:

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

可以与 GCC 一起工作的方法(即预处理器或小型GCC 补丁)的额外功劳。

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

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

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

4

13 回答 13

15

由于 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。

于 2009-06-14T18:26:55.940 回答
10

我想你会从阅读 Google 在设计Native Client时所做的一些实现问题和选择中获得很多信息,Native Client是一个在浏览器中执行 x86 代码(我们希望是安全的)的系统。如果代码不安全,您可能需要进行一些源代码重写或源代码到源代码编译以确保代码安全,但如果 NaCL 沙箱试图做任何太时髦的事情,您应该能够依靠它来捕获生成的汇编代码.

于 2009-06-12T06:25:42.197 回答
5

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

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

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

于 2009-06-11T12:59:22.290 回答
5

我偶然发现了Tiny C Compiler (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 的性能。

不过还是想听听其他的想法。

于 2009-06-11T14:10:27.873 回答
5

这不是微不足道的,但也不是那么难。

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

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

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

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

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

想要强加内存限制?签入malloc。想要限制分配多少堆栈?限制堆栈段。

操作系统整天使用它们的虚拟内存管理器创建这些类型的受限环境,因此您可以轻松地在现代操作系统上做这些事情。

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

于 2009-06-12T04:14:10.687 回答
3

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

于 2009-06-11T09:44:58.260 回答
3

我没有对此进行非常仔细的调查,但是在 Chromium(又名 Google Chrome)上工作的人已经在开发一个几乎像这样的沙盒,这可能值得研究。

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

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

于 2009-06-14T18:57:46.660 回答
3

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

于 2017-03-12T01:11:57.230 回答
2

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

当然,如果您的主管代码在较低的环级别上运行或者是一种解释语言(即模拟机器资源),那么这一点是没有意义的。

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

于 2009-06-12T06:34:27.527 回答
2

Liran 在上面的评论中指出了codepad.org。它不适合,因为它依赖于一个非常繁重的环境(由 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 -pedantic-errors -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-depth-128 -fno-merge-constants -fno-nonansi-builtins -fno-gnu-keywords -fno-elide-constructors - fstrict-aliasing -fstack-protector-all -Winvalid-pch

GCC 手册中解释了这些选项

真正引起我注意的是堆栈保护标志。我相信这是 IBM 研究项目(Stack-Smashing Protector)与官方 GCC 的合并。

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

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

于 2009-06-17T01:56:37.507 回答
0

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

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

于 2009-06-11T09:36:48.247 回答
0

如果您想真正确定,我认为最好的,也许是唯一的方法是沿着单独的流程走下去,让 O/S 处理访问控制。编写一个通用的线程加载器并没有那么痛苦,一旦你拥有它,你可以重写一些函数来加载特定的库。

于 2009-06-11T09:41:45.537 回答
0

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

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

于 2009-06-11T12:23:02.750 回答