Bypassing __metatable [Luau stack manipulation]

Hello!

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.

Any help is appreciated!

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

3 Likes

I appreciate the help!

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:
image

More specifically, focus on this:
image

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.

Any ideas? :>

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))

image

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…

ty for ur feedback, ive gone as far as i can and accepted the limitations of luau…
anyway, it’s a resource now.

resource

Function getrawmetatable() written in pure Luau

Thanks for your suggestion!