-- Buildat: extension/sandbox_test/try_exploit.lua -- http://www.apache.org/licenses/LICENSE-2.0 -- Copyright 2014 Perttu Ahola local log = buildat.Logger("try_exploit.lua") local dump = buildat.dump local magic = require("buildat/extension/urho3d").safe local uistack = require("buildat/extension/uistack") local ui_utils = require("buildat/extension/ui_utils").safe 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", "userdata", -- Used by buildat itself in metatables "unsafe", -- Dunno, but sounds unsafe "internal", } local bad_values_set = { -- NOTE: Make sure you use [] for the keys -- Sandbox shouldn't be able to iterate through this [__buildat_sandbox_environment] = "__buildat_sandbox_environment", -- If these are leaked, sandbox is doomed [getmetatable] = "getmetatable", [setmetatable] = "setmetatable", [getfenv] = "getfenv", [setfenv] = "setfenv", [io] = "io", [io.open] = "io.open", [io.popen] = "io.popen", } -- Have a list of field names to check in case of disabled iteration local interesting_field_names = { "meta", "class_meta", "CreateChild", "root", "text", "x", } for _, v in ipairs(bad_names) do -- All bad names are interesting table.insert(interesting_field_names, v) end -- Functions that are known to shutdown the whole application if called without -- parameters local function_blacklist = { "run_script_file", "send_packet", "disconnect", "sub_tick", "__gc", } local bad_names_set = {} for _, v in ipairs(bad_names) do bad_names_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_blacklist_set = {} for _, v in ipairs(function_blacklist) do function_blacklist_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 function M.search(value, checked_values_set, result_list, current_path, opts) opts = opts or {} 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 not opts.root_always_good or #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).." = "..bad_values_set[value]) end end -- Try static list of field names first for _, field_name in ipairs(interesting_field_names) do -- Try getting value with disabled error handling local v = nil function f() v = value[field_name] end pcall(f) -- Ignore errors -- Handle value without disabled error handling 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 M.search(v, checked_values_set, result_list, current_path) table.remove(current_path) end end -- Iterate using __next if getmetatable(value) and getmetatable(value).__next then -- Use meta.__next local metapairs = function(t) return getmetatable(value).__next, t, nil end for field_name, v in metapairs(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 M.search(v, checked_values_set, result_list, current_path) table.remove(current_path) end end end -- Iterate through raw table value if type(value) == 'table' then -- 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 M.search(v, checked_values_set, result_list, current_path) table.remove(current_path) end end end -- If it's a function or callable, call it and check the results if type(value) == 'function' or (getmetatable(value) and getmetatable(value).__call) then -- Don't call if blacklisted if not function_blacklist_set[current_path[#current_path]] then log:verbose("search: "..path_str(current_path).."(self)") local ret = nil function f() -- Call with itself as parameter; that should give the most bang -- for the buck ret = {value(value)} end pcall(f) -- Ignore errors if ret ~= nil then for i = 1, table.getn(ret) do local ret_v = ret[i] table.insert(current_path, "()["..i.."]") M.search(ret_v, checked_values_set, result_list, current_path) table.remove(current_path) end 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"} M.search(__buildat_sandbox_environment, checked_values_set, result_list, start_path, {root_always_good=true}) -- -- 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"} M.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 results global so they stay in the environment for checking 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"} M.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." log:info("Result: "..message) ui_utils.show_message_dialog(message) end function M.search_single_value(value) -- Prepare result tables local checked_values_set = {} local result_list = {} -- -- Search value -- local start_path = {"value"} M.search(value, checked_values_set, result_list, start_path) -- -- Handle results -- -- Don't show anything if nothing is bad if #result_list == 0 then return end 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 = #result_list.." exploits found.".. " Checked "..num_checked_values.." values." log:warning("Result: "..message) ui_utils.show_message_dialog(message) error("Value is exploitable") end return M -- vim: set noet ts=4 sw=4: