2014-09-28 10:55:42 +03:00
|
|
|
-- 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")
|
2014-09-28 12:56:14 +03:00
|
|
|
local magic = require("buildat/extension/urho3d").safe
|
2014-09-28 10:55:42 +03:00
|
|
|
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",
|
2014-10-02 13:42:04 +03:00
|
|
|
"userdata",
|
2014-09-28 10:55:42 +03:00
|
|
|
-- Used by buildat itself in metatables
|
|
|
|
"unsafe",
|
|
|
|
-- Dunno, but sounds unsafe
|
|
|
|
"internal",
|
|
|
|
}
|
2014-09-28 15:25:08 +03:00
|
|
|
local bad_values_set = {
|
|
|
|
-- NOTE: Make sure you use [] for the keys
|
2014-09-28 10:55:42 +03:00
|
|
|
-- Sandbox shouldn't be able to iterate through this
|
2014-09-28 15:25:08 +03:00
|
|
|
[__buildat_sandbox_environment] = "__buildat_sandbox_environment",
|
2014-09-28 10:55:42 +03:00
|
|
|
-- If these are leaked, sandbox is doomed
|
2014-09-28 15:25:08 +03:00
|
|
|
[getmetatable] = "getmetatable",
|
|
|
|
[setmetatable] = "setmetatable",
|
|
|
|
[getfenv] = "getfenv",
|
|
|
|
[setfenv] = "setfenv",
|
|
|
|
[io] = "io",
|
|
|
|
[io.open] = "io.open",
|
|
|
|
[io.popen] = "io.popen",
|
2014-09-28 10:55:42 +03:00
|
|
|
}
|
|
|
|
|
2014-09-28 14:00:17 +03:00
|
|
|
-- Have a list of field names to check in case of disabled iteration
|
2014-09-28 10:55:42 +03:00
|
|
|
local interesting_field_names = {
|
|
|
|
"meta",
|
|
|
|
"class_meta",
|
2014-09-28 14:00:17 +03:00
|
|
|
"CreateChild",
|
|
|
|
"root",
|
|
|
|
"text",
|
|
|
|
"x",
|
2014-09-28 10:55:42 +03:00
|
|
|
}
|
|
|
|
for _, v in ipairs(bad_names) do -- All bad names are interesting
|
|
|
|
table.insert(interesting_field_names, v)
|
|
|
|
end
|
|
|
|
|
2014-09-28 14:00:17 +03:00
|
|
|
-- Functions that are known to shutdown the whole application if called without
|
|
|
|
-- parameters
|
|
|
|
local function_blacklist = {
|
|
|
|
"run_script_file",
|
|
|
|
"send_packet",
|
|
|
|
"disconnect",
|
|
|
|
"sub_tick",
|
2014-10-02 13:42:04 +03:00
|
|
|
"__gc",
|
2014-09-28 14:00:17 +03:00
|
|
|
}
|
|
|
|
|
2014-09-28 10:55:42 +03:00
|
|
|
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
|
2014-09-28 14:00:17 +03:00
|
|
|
local function_blacklist_set = {}
|
|
|
|
for _, v in ipairs(function_blacklist) do function_blacklist_set[v] = true end
|
2014-09-28 10:55:42 +03:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2014-09-28 15:25:08 +03:00
|
|
|
function M.search(value, checked_values_set, result_list, current_path, opts)
|
|
|
|
opts = opts or {}
|
2014-09-28 10:55:42 +03:00
|
|
|
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)
|
2014-09-28 15:25:08 +03:00
|
|
|
if not opts.root_always_good or #current_path ~= 1 then
|
2014-09-28 10:55:42 +03:00
|
|
|
-- Check if this value is bad
|
|
|
|
if bad_values_set[value] then
|
|
|
|
table.insert(result_list, "Bad value found: "..
|
2014-09-28 15:25:08 +03:00
|
|
|
path_str(current_path).." = "..bad_values_set[value])
|
2014-09-28 10:55:42 +03:00
|
|
|
end
|
|
|
|
end
|
|
|
|
-- Try static list of field names first
|
|
|
|
for _, field_name in ipairs(interesting_field_names) do
|
2014-09-28 15:25:08 +03:00
|
|
|
-- Try getting value with disabled error handling
|
|
|
|
local v = nil
|
2014-09-28 10:55:42 +03:00
|
|
|
function f()
|
2014-09-28 15:25:08 +03:00
|
|
|
v = value[field_name]
|
2014-09-28 10:55:42 +03:00
|
|
|
end
|
2014-09-28 14:00:17 +03:00
|
|
|
pcall(f) -- Ignore errors
|
2014-09-28 15:25:08 +03:00
|
|
|
-- 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
|
2014-09-28 10:55:42 +03:00
|
|
|
end
|
2014-09-28 14:00:17 +03:00
|
|
|
-- Iterate using __next
|
|
|
|
if getmetatable(value) and getmetatable(value).__next then
|
2014-09-28 10:55:42 +03:00
|
|
|
-- Use meta.__next
|
2014-09-28 14:00:17 +03:00
|
|
|
local metapairs = function(t)
|
|
|
|
return getmetatable(value).__next, t, nil
|
|
|
|
end
|
|
|
|
for field_name, v in metapairs(value) do
|
2014-09-28 10:55:42 +03:00
|
|
|
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
|
2014-09-28 15:25:08 +03:00
|
|
|
M.search(v, checked_values_set, result_list, current_path)
|
2014-09-28 10:55:42 +03:00
|
|
|
table.remove(current_path)
|
|
|
|
end
|
|
|
|
end
|
2014-09-28 14:00:17 +03:00
|
|
|
end
|
|
|
|
-- Iterate through raw table value
|
|
|
|
if type(value) == 'table' then
|
2014-09-28 10:55:42 +03:00
|
|
|
-- 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
|
2014-09-28 15:25:08 +03:00
|
|
|
M.search(v, checked_values_set, result_list, current_path)
|
2014-09-28 10:55:42 +03:00
|
|
|
table.remove(current_path)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2014-09-28 14:00:17 +03:00
|
|
|
-- 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)")
|
2014-09-28 15:25:08 +03:00
|
|
|
local ret = nil
|
2014-09-28 14:00:17 +03:00
|
|
|
function f()
|
2014-09-28 15:25:08 +03:00
|
|
|
-- 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
|
2014-09-28 14:00:17 +03:00
|
|
|
for i = 1, table.getn(ret) do
|
|
|
|
local ret_v = ret[i]
|
|
|
|
table.insert(current_path, "()["..i.."]")
|
2014-09-28 15:25:08 +03:00
|
|
|
M.search(ret_v, checked_values_set, result_list, current_path)
|
2014-09-28 14:00:17 +03:00
|
|
|
table.remove(current_path)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2014-09-28 10:55:42 +03:00
|
|
|
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"}
|
2014-09-28 15:25:08 +03:00
|
|
|
M.search(__buildat_sandbox_environment, checked_values_set,
|
|
|
|
result_list, start_path, {root_always_good=true})
|
2014-09-28 10:55:42 +03:00
|
|
|
|
|
|
|
--
|
|
|
|
-- 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"}
|
2014-09-28 15:25:08 +03:00
|
|
|
M.search(__buildat_old_sandbox_global_wrappers,
|
2014-09-28 10:55:42 +03:00
|
|
|
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
|
2014-09-28 14:00:17 +03:00
|
|
|
|
|
|
|
-- Make results global so they stay in the environment for checking
|
|
|
|
sandbox.make_global({
|
|
|
|
loaded_extensions = loaded_extensions,
|
|
|
|
})
|
|
|
|
|
2014-09-28 10:55:42 +03:00
|
|
|
-- 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"}
|
2014-09-28 15:25:08 +03:00
|
|
|
M.search(new_sandbox, checked_values_set, result_list, start_path)
|
2014-09-28 10:55:42 +03:00
|
|
|
|
|
|
|
--
|
|
|
|
-- 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."
|
2014-09-28 15:25:08 +03:00
|
|
|
log:info("Result: "..message)
|
|
|
|
M.show_result_dialog(message)
|
2014-09-28 10:55:42 +03:00
|
|
|
end
|
|
|
|
|
2014-09-28 15:25:08 +03:00
|
|
|
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
|
|
|
|
--
|
2014-09-28 10:55:42 +03:00
|
|
|
|
2014-09-28 15:25:08 +03:00
|
|
|
-- 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)
|
|
|
|
M.show_result_dialog(message)
|
|
|
|
|
|
|
|
error("Value is exploitable")
|
|
|
|
end
|
|
|
|
|
|
|
|
local result_dialog_handle = nil
|
|
|
|
|
|
|
|
function M.show_result_dialog(message)
|
|
|
|
-- Don't stack multiple dialogs
|
|
|
|
if result_dialog_handle then
|
|
|
|
result_dialog_handle.append(message)
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
local root = uistack.main:push({desc="show_result_dialog"})
|
2014-09-28 10:55:42 +03:00
|
|
|
|
|
|
|
local style = magic.cache:GetResource("XMLFile", "__menu/res/main_style.xml")
|
|
|
|
root.defaultStyle = style
|
|
|
|
|
|
|
|
local window = root:CreateChild("Window")
|
|
|
|
window:SetStyleAuto()
|
2014-09-28 15:25:08 +03:00
|
|
|
window:SetName("show_result_dialog window")
|
2014-09-28 10:55:42 +03:00
|
|
|
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
|
2014-09-28 15:25:08 +03:00
|
|
|
message_text:SetTextAlignment(HA_LEFT)
|
2014-09-28 10:55:42 +03:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2014-09-28 15:25:08 +03:00
|
|
|
result_dialog_handle = {
|
|
|
|
append = function(text)
|
|
|
|
if #message_text.text < 1000 then
|
|
|
|
message_text.text = message_text.text.."\n"..text
|
|
|
|
end
|
|
|
|
end,
|
|
|
|
}
|
|
|
|
|
2014-09-28 10:55:42 +03:00
|
|
|
magic.SubscribeToEvent(ok_button, "Released",
|
|
|
|
function(self, event_type, event_data)
|
|
|
|
log:info("ok_button: Released")
|
|
|
|
uistack.main:pop(root)
|
2014-09-28 15:25:08 +03:00
|
|
|
result_dialog_handle = nil
|
2014-09-28 10:55:42 +03:00
|
|
|
end)
|
|
|
|
|
|
|
|
root:SubscribeToStackEvent("KeyDown", function(event_type, event_data)
|
|
|
|
local key = event_data:GetInt("Key")
|
|
|
|
if key == KEY_ESC then
|
2014-09-28 15:25:08 +03:00
|
|
|
log:info("KEY_ESC pressed at show_result_dialog level")
|
2014-09-28 10:55:42 +03:00
|
|
|
uistack.main:pop(root)
|
2014-09-28 15:25:08 +03:00
|
|
|
result_dialog_handle = nil
|
2014-09-28 10:55:42 +03:00
|
|
|
end
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
|
|
|
return M
|
|
|
|
-- vim: set noet ts=4 sw=4:
|