В настоящее время я изучаю, как реализовать песочницу (похожую на проект Google NaCl), где я могу запустить ненадежный код x86 (набор ограниченных инструкций) таким образом, чтобы это не может повредить остальную часть моего процесса.
В отличие от NaCl, ненадежный код не будет запускаться в отдельном процессе, но тот же процесс, что и хост-приложение. Итак, одним из важных шагов является получение правильной обработки структурированных исключений Windows для того, чтобы ловить ошибки (например, недопустимый доступ к памяти или div на 0) и изящно завершать песочницу до того, как Windows уничтожит мое хост-приложение. (NaCl не сталкивается с этими проблемами. Песочница - отдельный процесс и просто убивается в случае ошибки.)
Кроме того, изолированный код не должен использовать стек хостов приложений, а запускаться на какой-то отдельный "стек", который выделяется мной.
Именно эта комбинация (обработка исключений в присутствии пользовательского выделенного стека) искажает мой разум. Я проверил языковые реализации Go и Factor, которые делают подобные вещи и с этой помощью что-то работает.
Но есть еще некоторые открытые вопросы и неопределенности. Поэтому я подумал, что буду использовать фантастические знания Stack Overflow, чтобы получить некоторые мнения: -)
Ниже приведен фрагмент рабочего кода, сокращенный до основных проблем:
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 & StackEnd
"взломать"?
Я попытался сузить границы стека на что-то вроде:
mov gs:[008h], rsp
mov gs:[010h], rax ; rax is the address returned by malloc
Но это не сработает. Он печатает "GEH", а затем сбой из-за необработанных исключений.
FrameExceptionHandler()
никогда не будет выполнен.
Я также попробовал более расслабленные границы, которые включают "куча выделенного стека", а также стек, выделенный Windows. Но это не помогает.
Другой вопрос: знаете ли вы какие-либо другие ловушки, с которыми я могу столкнуться. Например, я заметил, что Windows не нравится, если RSP неравномерен (я думаю, потому что вы никогда не сможете попасть в неравномерный RSP, выполнив 2/4/8 байт PUSHes и POPs с помощью указателя указателя на 16 байт).
Спасибо, Jonas