extensions/sandbox_test: Search for exploits in latest real sandbox environments and a test sandbox environment (KEY_F10)

This commit is contained in:
Perttu Ahola 2014-09-28 10:55:42 +03:00
parent 4e5babfa0b
commit 1148decb2a
6 changed files with 427 additions and 16 deletions

View File

@ -1,7 +1,7 @@
-- Buildat: client/sandbox.lua
-- http://www.apache.org/licenses/LICENSE-2.0
-- Copyright 2014 Perttu Ahola <celeron55@gmail.com>
local log = buildat.Logger("__client/sandbox")
local log = buildat.Logger("sandbox")
local dump = buildat.dump
--
@ -73,6 +73,29 @@ __buildat_sandbox_environment.require = function(name)
error("require: \""..name.."\" not found in sandbox")
end
--
-- Sandbox environment debugging
--
-- For debugging purposes. Used by extensions/sandbox_test.
__buildat_latest_sandbox_global_wrapper_number = 0 -- Incremented every time
__buildat_latest_sandbox_global_wrapper = nil
-- Save a number of old wrappers for debugging purposes
__buildat_old_sandbox_global_wrappers = {}
local function debug_new_wrapper(sandbox)
if __buildat_latest_sandbox_global_wrapper then
table.insert(__buildat_old_sandbox_global_wrappers, __buildat_latest_sandbox_global_wrapper)
-- Keep a number of old wrappers.
-- These wrappers are created at quite a fast pace due to Update events.
if #__buildat_old_sandbox_global_wrappers > 60*5 then
table.remove(__buildat_old_sandbox_global_wrappers, 1)
end
end
__buildat_latest_sandbox_global_wrapper_number = __buildat_latest_sandbox_global_wrapper_number + 1
__buildat_latest_sandbox_global_wrapper = sandbox
end
--
-- Running code in sandbox
--
@ -80,6 +103,16 @@ end
local function wrap_globals(base_sandbox)
local sandbox = {}
local sandbox_declared_globals = {}
-- Sandbox special functions
sandbox.sandbox = {}
function sandbox.sandbox.make_global(t)
for k, v in pairs(t) do
if sandbox[k] == nil then
rawset(sandbox, k, v)
end
end
end
-- Prevent setting sandbox globals from functions (only from the main chunk)
setmetatable(sandbox, {
__index = function(t, k)
local v = rawget(sandbox, k)
@ -100,13 +133,7 @@ local function wrap_globals(base_sandbox)
rawset(sandbox, k, v)
end
})
function sandbox.buildat.make_global(t)
for k, v in pairs(t) do
if sandbox[k] == nil then
rawset(sandbox, k, v)
end
end
end
debug_new_wrapper(sandbox)
return sandbox
end
@ -125,15 +152,15 @@ function __buildat_run_function_in_sandbox(untrusted_function)
return true
end
local function run_code_in_sandbox(untrusted_code, sandbox)
local function run_code_in_sandbox(untrusted_code, sandbox, chunkname)
if untrusted_code:byte(1) == 27 then return false, "binary bytecode prohibited" end
local untrusted_function, message = loadstring(untrusted_code)
local untrusted_function, message = loadstring(untrusted_code, chunkname)
if not untrusted_function then return false, message end
return run_function_in_sandbox(untrusted_function, sandbox)
end
function __buildat_run_code_in_sandbox(untrusted_code)
local status, err = run_code_in_sandbox(untrusted_code, __buildat_sandbox_environment)
function __buildat_run_code_in_sandbox(untrusted_code, chunkname)
local status, err = run_code_in_sandbox(untrusted_code, __buildat_sandbox_environment, chunkname)
if status == false then
log:error("Failed to run script:\n"..err)
return false
@ -148,7 +175,7 @@ function buildat.run_script_file(name)
return false
end
log:info("buildat.run_script_file("..name.."): code length: "..#code)
return __buildat_run_code_in_sandbox(code)
return __buildat_run_code_in_sandbox(code, name)
end
buildat.safe.run_script_file = buildat.run_script_file

View File

@ -55,7 +55,7 @@ The sandbox environment
All code sent by the server to the client is run in the sandbox environment.
buildat.make_global(table)
sandbox.make_global(table)
- Copies contents table into the current global sandbox environemnt. It will
still not leak into the scope of other files running in the sandbox. Useful if
you want to remove the "namespace table" of an extension.
@ -72,8 +72,8 @@ Use extensions in this way:
cereal = require("buildat/extension/cereal")
cereal.binary_output(...)
You can use buildat.make_global like this:
buildat.make_global(require("buildat/extension/urho3d"))
You can use sandbox.make_global like this:
sandbox.make_global(require("buildat/extension/urho3d"))
local scene = Scene()
cereal

View File

@ -0,0 +1,62 @@
-- Buildat: extension/sandbox_test/init.lua
-- http://www.apache.org/licenses/LICENSE-2.0
-- Copyright 2014 Perttu Ahola <celeron55@gmail.com>
local log = buildat.Logger("sandbox_test")
local dump = buildat.dump
local try_exploit = dofile(buildat.extension_path("sandbox_test").."/try_exploit.lua")
local M = {}
local function get_file_content(path)
local f = io.open(path, "rb")
if not f then
log:error("Could not open file "..dump(path))
return nil
end
local content = f:read("*all")
f:close()
return content
end
local function run_in_sandbox(content, chunkname)
local sandbox_status = nil
local f = function()
sandbox_status = __buildat_run_code_in_sandbox(content, chunkname)
end
local status, err = __buildat_pcall(f)
if err then
log:verbose(err)
end
return sandbox_status
end
function M.run()
log:info("sandbox_test(): Begin")
local ext_path = buildat.extension_path("sandbox_test")
local tmp_path = buildat.extension_path("sandbox_test")
-- Check that running safe code works
log:info("sandbox_test(): Testing safe code")
local safe_content = get_file_content(ext_path.."/tests/safe.lua")
assert(safe_content)
local success = run_in_sandbox(safe_content, "=safe.lua")
assert(success)
-- Check that running the safe code as bytecode doesn't work
log:info("sandbox_test(): Testing bytecode")
local f, err = loadstring(safe_content)
if f == nil then
error("Could not load bytecode source: "..err)
end
local bytecode = string.dump(f)
local success = run_in_sandbox(bytecode)
assert(success == false)
-- Run the exploit search
log:info("sandbox_test(): Trying to find an exploit")
try_exploit.run()
log:info("sandbox_test(): Finished")
end
return M
-- vim: set noet ts=4 sw=4:

View File

@ -0,0 +1,14 @@
-- Buildat: extension/sandbox_test/tests/safe.lua
-- http://www.apache.org/licenses/LICENSE-2.0
-- Copyright 2014 Perttu Ahola <celeron55@gmail.com>
local log = buildat.Logger("safe.lua")
log:verbose("Valid test case running")
-- This is only available in the sandbox, so if this doesn't fail, we're in it
sandbox.make_global({global_foo = "bar"})
assert(global_foo == "bar")
-- This too
assert(buildat.is_in_sandbox)

View File

@ -0,0 +1,295 @@
-- Buildat: extension/sandbox_test/try_exploit.lua
-- http://www.apache.org/licenses/LICENSE-2.0
-- Copyright 2014 Perttu Ahola <celeron55@gmail.com>
local log = buildat.Logger("try_exploit.lua")
local magic = require("buildat/extension/urho3d")
local uistack = require("buildat/extension/uistack")
local dump = buildat.dump
local M = {}
-- Have a list of things to search for
local bad_names = {
-- Maybe someone is dumb enough to put these with their real names in a table?
"getmetatable",
"setmetatable",
"getfenv",
"setfenv",
-- Used in metatables
"__index",
"__newindex",
"__eq",
"__lt",
"__add",
"__sub",
"__mul",
"__div",
"__gc",
"__call",
-- Tolua++ or something metatable content
".get",
".call",
".set",
"tolua_ubox",
-- Used by buildat itself in metatables
"unsafe",
-- Dunno, but sounds unsafe
"internal",
}
local bad_values = {
-- Sandbox shouldn't be able to iterate through this
__buildat_sandbox_environment,
-- If these are leaked, sandbox is doomed
getmetatable,
setmetatable,
getfenv,
setfenv,
io,
io.open,
io.popen,
}
-- Have a list of field names to check in case of metatables
local interesting_field_names = {
"meta",
"class_meta",
}
for _, v in ipairs(bad_names) do -- All bad names are interesting
table.insert(interesting_field_names, v)
end
local bad_names_set = {}
for _, v in ipairs(bad_names) do bad_names_set[v] = true end
local bad_values_set = {}
for _, v in ipairs(bad_values) do bad_values_set[v] = true end
local interesting_field_names_set = {}
for _, v in ipairs(interesting_field_names) do interesting_field_names_set[v] = true end
local function path_str(path)
local result = ""
for _, v in ipairs(path) do
if result ~= "" then result = result.."." end
if type(v) == 'string' then
result = result..v
else
result = result..dump(v)
end
end
return result
end
local function search(value, checked_values_set, result_list, current_path)
if value == nil then
return
end
if checked_values_set[value] then
return
end
log:verbose("search: "..path_str(current_path))
checked_values_set[value] = true
if #current_path >= 20 then
table.insert(result_list, "Path too long: "..
path_str(current_path))
return
end
-- The root is never bad (it is the environment itself, which would be bad
-- as a value in any other position)
if #current_path ~= 1 then
-- Check if this value is bad
if bad_values_set[value] then
table.insert(result_list, "Bad value found: "..
path_str(current_path))
end
end
-- Try static list of field names first
for _, field_name in ipairs(interesting_field_names) do
function f()
local v = value[field_name]
if v ~= nil then
table.insert(current_path, field_name)
if bad_names_set[field_name] then
table.insert(result_list, "Bad name found: "..
path_str(current_path))
end
search(v, checked_values_set, result_list, current_path)
table.remove(current_path)
end
end
pcall(f)
end
-- Iterate through the value
if type(value) == 'table' then
-- Use meta.__next
for field_name, v in pairs(value) do
if v ~= nil then
table.insert(current_path, field_name)
if bad_names_set[field_name] then
table.insert(result_list, "Bad name found: "..
path_str(current_path))
end
search(v, checked_values_set, result_list, current_path)
table.remove(current_path)
end
end
-- Don't use meta.__next
local rawpairs = function(t)
return next, t, nil
end
for field_name, v in rawpairs(value) do
if v ~= nil then
table.insert(current_path, field_name)
if bad_names_set[field_name] then
table.insert(result_list, "Bad name found: "..
path_str(current_path))
end
search(v, checked_values_set, result_list, current_path)
table.remove(current_path)
end
end
end
end
function M.run()
-- Prepare result tables
local checked_values_set = {}
local result_list = {}
--
-- Search recursively through the base sandbox environment
-- Note that the sandbox actually can't even index its own environment
--
local start_path = {"__buildat_sandbox_environment"}
search(__buildat_sandbox_environment,
checked_values_set, result_list, start_path)
--
-- Search past global sandbox wrapper environments so we can see what kind
-- of stuff the server environment has pulled in
--
local start_path = {"__buildat_old_sandbox_global_wrappers"}
search(__buildat_old_sandbox_global_wrappers,
checked_values_set, result_list, start_path)
--
-- Create a sandbox, run a function in it which grabs its environment, and
-- run a search through it
--
local wrapper_number_was = __buildat_latest_sandbox_global_wrapper_number
local local_getfenv = getfenv
local local_pcall = pcall
local new_sandbox = {}
function f()
-- Require all the things
-- TODO: Get this list automatically from the filesystem
local extnames = {
"cereal",
"experimental",
"magic_sandbox",
"__menu",
"sandbox_test",
"test",
"uistack",
"urho3d"
}
local loaded_extensions = {}
local function try_require_extension(n)
local function g()
loaded_extensions[n] = require("buildat/extension/"..n)
end
-- There can be errors for example when the extension doesn't return
-- a safe interface
pcall(g)
end
for _, extname in ipairs(extnames) do
try_require_extension(extname)
end
-- Make this global so it stays in the environment for checking through
sandbox.make_global({loaded_extensions = loaded_extensions})
-- Get the environment (which isn't available for iteration normally)
new_sandbox.env = local_getfenv(1)
end
__buildat_run_function_in_sandbox(f)
-- Check that one wrapper was made, and include it in the search
local wrapper_number_is = __buildat_latest_sandbox_global_wrapper_number
assert(wrapper_number_is == wrapper_number_was + 1)
assert(__buildat_latest_sandbox_global_wrapper)
new_sandbox.wrapper = __buildat_latest_sandbox_global_wrapper
-- Search
local start_path = {"new_sandbox"}
search(new_sandbox, checked_values_set, result_list, start_path)
--
-- Handle results
--
local num_checked_values = 0
for k, v in pairs(checked_values_set) do
num_checked_values = num_checked_values + 1
end
for _, result in ipairs(result_list) do
log:error("Result: "..result)
end
local message = ""
if #result_list == 0 then
message = message.."No exploit found."
else
message = message..#result_list.." exploits found."
end
message = message.." Checked "..num_checked_values.." values."
M.show_result(message)
end
function M.show_result(message)
log:info("Result: "..message)
local root = uistack.main:push({desc="show_result"})
local style = magic.cache:GetResource("XMLFile", "__menu/res/main_style.xml")
root.defaultStyle = style
local window = root:CreateChild("Window")
window:SetStyleAuto()
window:SetName("show_result window")
window:SetLayout(LM_VERTICAL, 10, magic.IntRect(10, 10, 10, 10))
window:SetAlignment(HA_LEFT, VA_CENTER)
local message_text = window:CreateChild("Text")
message_text:SetName("message_text")
message_text:SetStyleAuto()
message_text.text = message
message_text:SetTextAlignment(HA_CENTER)
local ok_button = window:CreateChild("Button")
ok_button:SetStyleAuto()
ok_button:SetName("Button")
ok_button:SetLayout(LM_VERTICAL, 10, magic.IntRect(0, 0, 0, 0))
ok_button.minHeight = 20
local ok_button_text = ok_button:CreateChild("Text")
ok_button_text:SetName("ButtonText")
ok_button_text:SetStyleAuto()
ok_button_text.text = "Ok"
ok_button_text:SetTextAlignment(HA_CENTER)
ok_button:SetFocus(true)
magic.SubscribeToEvent(ok_button, "Released",
function(self, event_type, event_data)
log:info("ok_button: Released")
uistack.main:pop(root)
end)
root:SubscribeToStackEvent("KeyDown", function(event_type, event_data)
local key = event_data:GetInt("Key")
if key == KEY_ESC then
log:info("KEY_ESC pressed at show_result level")
uistack.main:pop(root)
end
end)
end
return M
-- vim: set noet ts=4 sw=4:

View File

@ -187,6 +187,7 @@ struct CApp: public App, public magic::Application
{
log_v(MODULE, "run_script_no_sandbox():\n%s", cs(script));
// TODO: Use lua_load() so that chunkname can be set
if(luaL_loadstring(L, script.c_str())){
ss_ error = lua_tocppstring(L, -1);
log_e("%s", cs(error));
@ -318,6 +319,18 @@ struct CApp: public App, public magic::Application
m_options.graphics.apply(magic_graphics);
}
}
if(key == Urho3D::KEY_F10){
ss_ extname = "sandbox_test";
ss_ script = ss_()+
"local m = require('buildat/extension/"+extname+"')\n"
"if type(m) ~= 'table' then\n"
" error('Failed to load extension "+extname+"')\n"
"end\n"
"m.run()\n";
if(!run_script_no_sandbox(script)){
log_e(MODULE, "Failed to load and run extension %s", cs(extname));
}
}
}
void on_screenmode(magic::StringHash event_type, magic::VariantMap &event_data)