如何创build一个轻量级的C代码沙箱?

我想build立一个C预处理器/编译器,允许从本地和在线源收集function。 即:

#fetch MP3FileBuilder http://scripts.com/MP3Builder.gz #fetch IpodDeviceReader http://apple.com/modules/MP3Builder.gz void mymodule_main() { MP3FileBuilder(&some_data); } 

这是很容易的部分。

困难的部分是我需要一个可靠的方法来“直接”或“无限制地访问磁盘或系统资源(包括内存分配和堆栈)”导入代码 。 我想要一种安全的方式来运行不受信任的C代码 (模块)的小片段,而不需要把它们放在单独的进程,虚拟机或解释器(尽pipe单独的线程可以接受)的开销。

要求

  • 我需要在其访问数据和资源,包括CPU时间配额。
  • 我将阻止直接访问标准库
  • 我想停止创build无限recursion的恶意代码
  • 我想限制静态和dynamic分配到特定的限制
  • 我想抓住模块可能会引起的所有exception(如除以0)。
  • 模块只能通过核心接口与其他模块交互
  • 模块只能通过核心接口与系统(I / O等)进行交互
  • 模块必须允许位操作,math,数组,枚举,循环和分支。
  • 模块不能使用ASM
  • 我想限制指针和数组访问为模块保留的内存(通过自定义safe_malloc())
  • 必须支持ANSI C或一个子集(见下文)
  • 系统必须是轻量级和跨平台的(包括embedded式系统)。
  • 系统必须是GPL或LGPL兼容的。

我很乐意解决C的子集。我不需要像模板或类的东西。 我主要对高级语言不擅长的事情感兴趣,比如快速math,位操作,二进制数据的search和处理。

现有的C代码无需修改就可以重新使用来创build模块。 其目的是要求模块符合一组规则和限制,这些规则和限制旨在将模块限制为基本逻辑和转换操作(例如video转码或压缩操作)。

对这样的编译器/预处理器的理论input将是具有module_main函数的单个ANSI C文件(或安全子集),不包括或预处理器指令,不包括ASM,它将允许循环,分支,函数调用,指针math(限于分配给模块的范围),位移,位域,强制转换,枚举,数组,整数,浮点数,string和math。 其他任何东西都是可选的。

示例实现

这里有一个伪代码片段来解释这个更好。 这里一个模块超过了它的内存分配配额,也创build了无限recursion。

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

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

 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; } 

我意识到执行这些检查会影响模块的性能,但是我怀疑它仍然会胜过高级语言或虚拟机语言。 我不是试图阻止模块直接做危险的事情,我只是试图强迫这些危险的事情以受控的方式发生(如通过用户反馈)。 即:“模块X已超出内存分配,继续还是中止?”。

UPDATE

到目前为止,我所得到的最好的结果是使用自定义编译器(就像一个被黑客入侵的TCC),边界检查和一些自定义函数和循环代码来捕获recursion。 我还想听听我还需要检查什么或者有什么解决scheme。 我想在使用之前删除ASM和检查指针解决了以前答案中expression的许多问题。 我添加了一个赏金,从SO社区中撬出更多的反馈。

对于我正在寻找的赏金:

  • 对上述定义的理论体系的潜在利用的细节
  • 可能优化检查每个访问指针
  • 这些概念的实验性开源实现(如Google Native Client)
  • 支持多种操作系统和设备的解决scheme(不含基于OS /硬件的解决scheme)
  • 支持大多数C操作的解决scheme,甚至C ++(如果可能的话)

可以使用GCC(即,预处理器或 GCC补丁)的方法的额外功劳。

我也会考虑任何能够确定地certificate我所尝试的东西根本无法完成的事情。 你需要相当有说服力,因为迄今为止没有任何反对意见确实指出了为什么他们认为这是不可能的技术方面。 为了防范那些说这个问题最初是作为安全运行C ++的方式提出的。 现在我已经将需求缩减到了C的有限子集

我对C的理解可以归类为“中级”,我对PC硬件的理解也许是“先进”下面的一步。 如果可以的话,尽量指导你的答案。 由于我不是C专家,所以我将主要基于给出答案的投票以及答案对我的要求的密切程度。 您可以通过提供足够的证据来帮助您的投诉(受访者)并投票(其他人)。 一旦赏金倒计时达到6小时,我会分配一个答案。

最后,我相信解决这个问题将是维持C在越来越networking化和偏执狂世界中的相关性的一个重要步骤。 随着其他语言在性能方面的差距越来越大,计算能力也在不断增加,certificateC开发的风险越来越大(就像现在的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)

所有这些指针访问都不能在编译时检查。 这与向编译器提供“程序中的所有缺陷”需要解决暂停问题的复杂性相同。

既然你希望这个函数能够在同一个进程中运行(可能在不同的线程中),你可以在应用程序和“安全”模块之间共享内存,因为这是线程的共同点:共享数据以加快访问速度。 但是,这也意味着两个线程都可以读写相同的内存。

而且既然你不能certificate指针最终的编译时间,你必须在运行时这样做。 这意味着像'a [10]'这样的代码必须被翻译成'get_byte(a + 10)'这样的东西,在这一点上我不会再把它叫做C了。

Google Native Client

所以,如果这是真的,谷歌如何做呢? 那么,与这里的要求(跨平台(包括embedded式系统))相比,Google专注于x86,除了页面保护和分段寄存器以外,还有x86分页。 这允许它创build一个沙箱,其他线程不会以相同的方式共享相同的内存:沙箱是通过分段限制在只改变其自己的内存范围。 此外:

  • 一个安全的x86汇编结构列表被组装
  • gcc被改变发出那些安全的结构
  • 这个清单的构build方式是可以validation的。
  • 加载一个模块后,这个validation完成

所以这是平台特定的,不是一个“简单”的解决scheme,虽然是一个工作。 阅读更多的研究论文 。

结论

所以,无论你走到哪条路线,你都需要从可以validation的新东西开始,只有这样,你才能开始适应现有的编译器或者生成一个新的编译器。 但是,试图模仿ANSI C需要考虑指针问题。 Google将他们的沙盒build模为不是在ANSI C上,而是在x86的一个子集上,这使得他们可以很好地利用现有的编译器,而且与x86绑定在一起。

我想你会在阅读关于Googledevise本机客户端时所做的一些实现方面的关注和select,这个系统在浏览器中执行x86代码(我们希望是安全的)。 您可能需要做一些源代码重写或源代码编译,以确保代码的安全(如果不是的话),但是如果它尝试做任何事情,那么您应该能够依靠NaCL沙箱来捕获您生成的汇编代码。

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

  • 使用CERN的CINT在解释器中运行沙盒代码,并查看有关限制解释器允许的内容。 这可能不会带来非常好的performance。
  • 使用LLVM创buildC ++代码的中间表示,然后查看在沙盒Java风格虚拟机中运行该字节代码是否可行。

不过,我同意其他人的看法,这可能是一个可怕的项目。 看看网页浏览器有bug或挂起插件不稳定整个浏览器的问题。 或者查看Wireshark项目的发行说明; 几乎每个发行版似乎都包含了解决其中一个协议parsing器中的问题的安全修复,然后影响整个程序。 如果一个C / C ++沙箱是可行的,我希望这些项目现在已经locking到一个。

我偶然发现了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. 

这是一个非常小的程序,使黑客入侵一个可行的select(黑客GCC?,而不是在这一辈子!)。 我怀疑它会成为一个很好的基础来build立我自己的限制编译器。 我将删除对语言function的支持,我无法保证安全并包装或replace内存分配和循环处理。

TCC已经可以对内存访问进行边界检查了 ,这是我的要求之一。

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

我并不期望这很容易,但是这让我希望能够以更低的风险获得接近C的性能。

尽pipe如此,仍然想听到其他的想法。

这不是微不足道的,但并不难。

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

他们将不得不使用你的标准库(而不是通用的C库)。 您的标准库将强制执行您想强加的任何控制。

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

如果你的编译器以encryption方式签名它的可执行文件,你的运行时将能够检测到被篡改的二进制文件,并且不加载它们。 这可以防止他们“戳穿”你不想让它们拥有的二进制文件。

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

想强加内存限制吗? 把一个检查到malloc。 想要限制多less堆栈被分配? 限制堆栈段。

操作系统整天使用虚拟内存pipe理器创build这些受限制的环境,因此您可以在现代操作系统上轻松完成这些任务。

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

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

我还没有仔细研究过这个问题,但是从事Chromium(又名Google Chrome)工作的人现在已经在这个沙盒上工作了,这可能是值得研究的。

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

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

如果一个静态代码validation器能够确定对于所有可能的代码,如果该语言是图灵完整的,那么一组代码是安全的或者不安全的是不可能的。 这相当于停止问题。

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

最好的办法是在另一个进程中启动代码(ipc并没有那么糟糕),并像Linux中的Ptrace一样捕获系统调用http://linux.die.net/man/2/ptrace

Liran在上面的评论中指出了codepad.org 。 这是不合适的,因为它依赖于一个非常沉重的环境(包括ptrace,chroot和出站防火墙),但是我发现有几个g ++安全开关,我想我会在这里分享:

gcc 4.1.2 flags:-O -fmessage-length = 0 -fno-merge-constants -fstrict-aliasing -fstack-protector-all

g ++ 4.1.2 flags:-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的合并。

通过缓冲区溢出检测和variables重sortingfunction实现保护,避免指针破坏。 缓冲区溢出检测的基本思想来自StackGuard系统。

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

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

不错的想法,但我相当确定你想要做什么是不可能的C或C + +。 如果你放弃沙箱的想法,它可能会工作。

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

如果你想确定一下,我认为做到这一点的最好也许是唯一的办法就是走下单独的进程,让O / S处理访问控制。 编写一个通用的线程加载器并不是那么痛苦,一旦拥有它,你可以重载一些函数来加载特定的库。

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

你似乎build议的是与C ++不同的,更有限的语言。 这是你当然可以追求的东西,但正如其他人已经注意到,你将不得不编写一个编译器 – 简单的文本处理不会给你你想要的。