extensions/sandbox_test: Search for exploits in latest real sandbox environments and a test sandbox environment (KEY_F10)
This commit is contained in:
parent
4e5babfa0b
commit
1148decb2a
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
62
extensions/sandbox_test/init.lua
Normal file
62
extensions/sandbox_test/init.lua
Normal 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:
|
14
extensions/sandbox_test/tests/safe.lua
Normal file
14
extensions/sandbox_test/tests/safe.lua
Normal 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)
|
||||
|
295
extensions/sandbox_test/try_exploit.lua
Normal file
295
extensions/sandbox_test/try_exploit.lua
Normal 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:
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user