私は現在、信頼されていない x86 コード (制限された命令セット) を実行できるサンドボックス ( Google の NaCl プロジェクトに似ています) を実装する方法を調査しています。
NaCl とは異なり、信頼できないコードは別のプロセスではなく、ホスト アプリケーションと同じプロセスで実行されます。したがって、重要なステップの 1 つは、エラー (無効なメモリ アクセスや 0 による div など) をキャッチし、Windows がホスト アプリケーションを強制終了する前にサンドボックスを適切に終了するために、Windows の構造化例外処理を正しく行うことです。(NaCl はこれらの問題に直面していません。サンドボックスは別のプロセスであり、エラーが発生した場合は単に強制終了されます。)
さらに、サンドボックス化されたコードは、ホスト アプリケーション スタックを使用するのではなく、自分で割り当てた別の「スタック」で実行する必要があります。
まさにこの組み合わせ (カスタム割り当てスタックが存在する場合の例外処理) は、私の心をねじ曲げています。同様のことを行うGoとFactorの言語実装を確認しましたが、この助けを借りて何かが実行されました。
しかし、未解決の問題や不確実性がまだいくつかあります。そこで、Stack Overflow の素晴らしい知識を使って意見を集めようと思いました :-)
以下は、コアの問題に切り詰められた実用的なコード スニペットです。
コード.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);
}
コード.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 & StackEnd
このセットの「ハック」についてもっと洞察を持っている人はいますか? スタック制限を次のように狭めようとしました:
mov gs:[008h], rsp
mov gs:[010h], rax ; rax is the address returned by malloc
しかし、うまくいきません。「GEH」を出力し、未処理の例外が原因でクラッシュします。
FrameExceptionHandler()
実行されることはありません。
また、「ヒープ割り当てスタック」と Windows によって割り当てられたスタックを含む、より緩和された境界も試しました。しかし、それは役に立ちません。
もう 1 つの質問は、私が遭遇できる他のトラップを知っているかどうかです。たとえば、RSP が不均一な場合、Windows はそれを好まないことに気付きました (16 バイトでアラインされたスタック ポインターで 2/4/8 バイトの PUSH と POP を実行しても、RSP が不均一になることは決してないためだと思います)。
ありがとう、ジョナス