I’m trying to rewrite getrawmetatable() in pure Luau. For those who don’t know, this is a function commonly used in exploits to retrieve an item’s metatable bypassing it’s __metatable field, if set.
I found out I can use stack manipulation to do this - where possible, i can force a metamethod to error, and handle that in xpcall - the metamethod is available on the stack to retrieve with debug.info.
Tested example
local getrawmetatable = require("./getrawmetatable")
local tbl = setmetatable({someVal = "hi"}, {
__concat = function(self, other)
return self.someVal..other
end
})
local concat = getrawmetatable(tbl).__concat --this works by passing an inconcatenateable value, such as a function
--it's handled in an xpcall where the function then retrieves the metamethod from the stack
print(concat({someVal = "string 1 "}, "string2")) --> "string 1 string 2"
Now, here comes the issue - metamethods. Luau works in such a way so that metamethods are only left on the stack for the duration of their execution. This is a big problem, because it means I have no way to retrieve the metamethod from the stack because it’s already finished executing and popped off before I have a chance to.
So, I guess my question is - Is there anyway to “pause” a metamethod’s execution and examine the stack? The closest I have gotten so far is with debug.traceback on the return value, which
revealed the metamethod’s source code.
i don’t think you can fully instrumentate luau methods like this but you might be able to use a combination of getfenv and setfenv to make a wrapper metamethod for getting things in the environment ( only globals :c ) and yielding for every __index, __newindex, __call, etc until you’ve done what you needed to do
i don’t think you can do any more than this natively, and it’s also not exactly what you want so i don’t think it is possible to retrieve the stack in this manner.
x = 1
local function test()
local _print = print
x += 5
_print(x)
end
local function wait(seconds)
local start = os.clock()
while os.clock() - start < seconds do end
end
setfenv(test, setmetatable({}, {
__index = function(_, i)
local item = getfenv()[i]
print("global retrieved", i)
wait(1)
if type(item) == "function" then
local wrapper = function(...)
print("global called", i, ...)
wait(1)
return item(...)
end
return wrapper
end
return item
end,
__newindex = function(_, i, v)
print("global set", i, "=", v)
wait(1)
getfenv()[i] = v
end
}))
test()
if you do manage to make something cool, i’d love to see it and you should dm me (bio) :3
Yeah, that wouldnt really help for metamethods which dont try to access global functions…
but this gave me a GREAT IDEA!!! It’ll need refining but it allows me to start detecting non-erroring metamethods.
and this gave me some nice results.
--**I ran this in studio command bar as a quick test so please ignore bad code practices here :D**--
--original test metatable
local meta = {}
meta.__metatable = "hi"
function meta:__add(other) --using this syntax to make sure it still works
return self.val + other
end
local tbl = setmetatable({val = 3}, meta)
local proxy = newproxy(true)
getmetatable(proxy).__add = function(other) --+ operator overload on the proxy
for level = 1, 20, 1 do --check at 20 stack levels because __add should be there
local s, funcAtLevel = pcall(debug.info, level, "f") --get func
local s2, funcName = pcall(debug.info, funcAtLevel, "n") --get name
if (s and s2) then
print(funcName, funcAtLevel)
if (funcName == "__add") then --if it's the metamethod
--see if its the same metamethod
print(funcAtLevel == require(game.ServerScriptService.getrawmetatable)(tbl).__add)
end
end
end
return 3
end
print(tbl + proxy)
and this was the output:
More specifically, focus on this:
It’s the same metamethod as in the table!!!
This means i dont need to force an error and it optimises!!!
Thanks so much for your suggestion, tweaking it a bit added this much functionality. I’ll add this to my function at a later time (is late for me now, prob the morning). For now, I need to figure out how on earth I’m meant to account for custom metamethod logic to avoid invoking the proxy metamethod. More importantly, how to account for metamethods which dont require additional parameters like __tostring.
update: I’ve tweaked my method to now focus on using a userdata object with metamethods to retrieve other metamethods like __index and __newindex. Still having trouble with metamethods that don’t interact with the proxy object at all or just don’t take another parameter. Any help is appreciated!
updated
local getrawmetatable = require("./getrawmetatable")
local meta = {}
meta.__metatable = "hi"
function meta:__tostring()
error("!!!")
end
function meta:__concat(other)
return self..other
end
function meta:__newindex(k, v)
print(k, v)
end
function meta:__index(k, v)
print(k, v)
end
function meta:__call(...)
print(...)
end
local tbl = setmetatable({}, meta)
print(getrawmetatable(tbl))
What’s also interesting is this appears to also retrieve the Luau default metamethod fallbacks for when metamethods aren’t set.
EDIT:
Might have reached its pinnacle now. It’s gotten to the point where I can retrieve metamethods that either error or interact with the proxy in some way. If anyone knows how to get metamethods which do neither of these, suggestions are appreciated…