【问题标题】:Implementing a sandbox with custom stack on Windows 64-bit在 Windows 64 位上使用自定义堆栈实现沙箱
【发布时间】:2012-12-24 01:23:30
【问题描述】:

我目前正在研究如何实现一个沙盒(类似于 Google's NaCl project ),我可以在其中运行不受信任的 x86 代码(受限指令集),这样它就不会损害我的其余进程。

与 NaCl 不同,不受信任的代码不会在单独的进程中运行,而是在与宿主应用程序相同的进程中运行。因此,一个关键步骤是让 Windows 的结构化异常处理正确,以便在 Windows 杀死我的主机应用程序之前捕获错误(如无效的内存访问或 div 为 0)并优雅地终止沙箱。 (NaCl 不会遇到这些问题。沙箱是一个单独的进程,如果发生错误,就会被杀死。)

此外,沙盒代码不应使用主机应用程序堆栈,而应在我分配的某个单独的“堆栈”上运行。

正是这种组合(存在自定义分配堆栈时的异常处理)让我大吃一惊。我检查了 GoFactor 的语言实现,它们做了类似的事情,并在这个帮助下运行了一些东西。

但仍有一些悬而未决的问题和不确定性。所以我想我会利用 Stack Overflow 的奇妙知识来获得一些意见:-)

以下是一个工作代码 sn -p 减少到核心问题:

code.cpp

#include <Windows.h>
extern "C" void Sandbox();

// just a low level helper to print "msg"
extern "C" void Write(const char* msg)
{
    WriteFile(GetStdHandle(STD_OUTPUT_HANDLE),
              msg, (DWORD)strlen(msg), NULL, NULL);
}

// should be called first on error and continue exception handling
LONG __stdcall GlobalExceptionHandler(_EXCEPTION_POINTERS*)
{
    Write("GEH ");
    return EXCEPTION_CONTINUE_SEARCH;
}

// should be called afterwards on error and terminate the process
// of course this is just a stub to simplify the issue
// in real world it would just terminate the sandbox
extern "C" EXCEPTION_DISPOSITION __stdcall FrameExceptionHandler(
        PEXCEPTION_RECORD, ULONG64, PCONTEXT, PVOID)
{
    Write("FEH ");
    ExitProcess(42);
}

void main()
{
    AddVectoredExceptionHandler(1, GlobalExceptionHandler);
    Sandbox();
    // never reach this...
    ExitProcess(23);
}

code.asm

EXTERN FrameExceptionHandler:PROC
EXTERN malloc:PROC

.code

Handler:
    jmp FrameExceptionHandler

Sandbox PROC FRAME : Handler
    ; function prologue compliant with Windows x86_64 calling conventions
    ; saves rsp to the "frame-pointer" r15
    push r15
    .PUSHREG r15
    sub rsp, 20h
    .ALLOCSTACK(20h)
    mov r15, rsp
    .SETFRAME r15, 0h
    .ENDPROLOG

    ; set rsp to the top of a "heap allocated stack" of size 0x10000 bytes
    mov rcx, 10000h
    call malloc
    lea rsp, [rax+10000h]

    ; got this from implementation of the Go language runtime:
    ; while unwinding the stack, Windows sanity checks the values of
    ; RSP to be within stack-bounds. Of course RSP is set to our
    ; "heap allocated stack" and not within the bounds of what Windows
    ; thinks should be the stack.
    ; Fix this by adjusting StackBase and StackEnd in the TIB (thread
    ; information block), so that basically the stack is unbounded:
    ; StackBase = 0xffffffffffffffff, StackEnd = 0x0000000000000000
    mov rcx, 0FFFFFFFFFFFFFFFFh
    mov gs:[008h], rcx
    mov rcx, 0
    mov gs:[010h], rcx


    ; trigger an access error by reading invalid memory
    mov rax, 0DEADBEEFh
    mov rax, [rax]

    ; function epilogue - will never get here
    mov rax, 0
    add rsp, 28h
    ret
Sandbox ENDP

end

运行此程序将打印“GEH FEH”,然后以代码 42 优雅退出。

有没有人对这套StackBase &amp; StackEnd“hack”有更深入的了解? 我试图将堆栈限制缩小为:

    mov gs:[008h], rsp
    mov gs:[010h], rax    ; rax is the address returned by malloc

但它不起作用。它打印“GEH”,然后由于未处理的异常而崩溃。 FrameExceptionHandler() 永远不会被执行。

我还尝试了更宽松的边界,包括“堆分配的堆栈”以及 Windows 分配的堆栈。但这无济于事。

另一个问题是,你是否知道我可能遇到的任何其他陷阱。例如,如果 RSP 不均匀,我注意到 Windows 不喜欢它(我猜是因为你永远无法通过在 16 字节对齐的堆栈指针上执行 2/4/8 字节的 PUSH 和 POP 来达到不均匀的 RSP)。

谢谢, 乔纳斯

【问题讨论】:

  • 你玩过Sandboxie吗?如果您只需要一个方便的沙盒实用程序,这可能会有所帮助。
  • 不,不幸的是这没有帮助。我不是在谈论在我的操作系统中运行不受信任的应用程序,而是在我的应用程序中运行不受信任的 x86 代码。例如。视频播放器中新视频编解码器的插件。我不希望有错误的插件使我的应用程序崩溃。
  • 感谢您提供此示例,这帮助我弄清楚了我在这里处理的类似代码有什么问题。但是请注意,将实际堆栈边界设置为“沙盒”堆栈空间的开始和结束对我来说完全有效。我必须小心恢复 TIB 字段,否则你很快就会遇到其他崩溃(duh)!
  • 我不明白这样的事情怎么可能完全万无一失。例如,不受信任的插件可能会尝试访问随机内存地址。最好的情况是地址无效并且访问会被你的向量异常处理程序捕获,但最坏的情况地址可能实际上是有效的内存(你分配的堆、数据页、代码页等)并且插件可能会损坏该数据。
  • 您可以通过将内存访问限制为仅为插件代码保留的某个地址范围来处理此问题。为此,您必须仅以“安全”组合允许所有写入指令。您必须静态检查插件是否遵守这些限制。当然,你必须以一种特殊的方式编译你的插件。 Google 的 NaCl 项目中的技术涵盖了所有这些内容。一项非常有趣的技术:-)

标签: windows assembly exception-handling sandbox x86-64


【解决方案1】:

在同一进程中运行不受信任的第 3 方代码是自找麻烦。该代码可以以各种方式杀死您的进程。例如。它可以在失败时调用exit(),请求大量内存,或者写入线程分配的内存。

更安全但不那么难的解决方案是在不同的进程中运行此代码,类似于 Chrome 正在执行的操作。每个 Chrome 扩展程序都在不同的进程中运行,并且数据在进程之间传递。

您的应用程序可以启动一个单独的进程并通过管道、Windows 消息、共享内存(内存映射文件)等方式与它通信以共享大数据。

插件(通常)实现了一个接口,因此您需要编写一个代理对象来抽象插件驻留在另一个进程中的事实,并隐藏多进程应用程序带来的 IPC 复杂性。 gSoap 就是这样一个框架,还有其他的。

【讨论】:

    【解决方案2】:

    你想执行代码并检查它的有效性。您可以使用自己的沙箱来完成。请参阅 x86 处理器的虚拟盒实现。它可以提供帮助。 但是所有的虚拟机都是有代价的:模拟处理器运行底层代码比你的应用慢 5-10 倍。

    如果您只需要错误检查并在核心 cpu 上运行应用程序,那么您需要在线程中运行它。当线程挂起时,应用程序保持不变。你可以向线程注入一些代码来寻找它的执行。 但这种情况不太安全,因为恶意代码可能会破坏您的检查程序并利用它对您的系统进行后门/root。

    所以,我的回答是:为了安全 - 使用你自己的虚拟机,为了速度 - 在另一个线程中执行。

    最好的问候,祝你好运 弗拉基米尔

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-04-07
      • 2015-03-21
      • 2021-08-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-08-18
      • 1970-01-01
      相关资源
      最近更新 更多