Hooking LuaJIT

If you've been around the gaming industry even a little, you've almost definitely heard of Lua. This potent scripting language has found itself embedded in thousands of video games, acting as an API for engineers to easily add functionality to game clients and servers alike.

Sidebar: You'll see me talking about this again and again: the best way to make your hacks portable, reliable, and time-resilient is to attack engines, not games. Hooking into a ton of functions and locating a plethora of addresses is well and good, but it means you're going to need to update your offsets everytime the game is updated. If you, instead, hook into the libraries used by the game, these problems mostly vanish.

The ubiquity of Lua makes it an amazing target for hooking. Moreover, since Lua is used by game developers to add content and features, a game's Lua environment is a well-oiled machine with a ton of functionality.

Because of performance requirements, it is quite common to see LuaJIT used in place of vanilla Lua. For this reason, I'm going to talk about how to attack it. Small variations on this attack can be used for vanilla Lua.

Injecting Lua Code

A Lua environment is created by grabbing a lua_State from a call to luaL_newstate, which is then followed by a call to luaL_openlibs with the lua_State as a parameter. It might seem like a good point at which to intercept execution and inject code is luaL_newstate, but that's incorrect. Because the libs haven't been opened by this point, any scripts loaded would be completely crippled. Instead, we should intercept luaopen_jit, which is the final function called when opening libs (here).

When LuaJIT is linked dynamically, finding this function is a matter of walking the export table:

luaexports

When linked statically, there are a handful of identifiable strings which can be searched for:

lua_open_jit_strings

Once the function is found, hooking it is quite easy. However, before we can do that, we need to find two more functions: luaL_loadfilex to load our Lua script, and lua_pcall to execute it. When dynamically linked, the export table has us covered. Otherwise, the former can be found by the string =stdin (here):

luastdin

The latter is a bit trickier, as it doesn't have any identifiable strings. Luckily, however, it is called internally (here), following the string =(debug command):

lua_pcall

This also shows the address of luaL_loadbuffer. Keep this in mind for later.

With the addresses identified, we can write our hook:

typedef void* lua_State;

typedef int (*_luaL_loadfilex)(lua_State *L, const char *filename, const char *mode);
_luaL_loadfilex luaL_loadfilex;

typedef int (*_luaopen_jit)(lua_State *L);
_luaopen_jit luaopen_jit_original;

typedef int (*_lua_pcall)(lua_State *L, int nargs, int nresults, int errfunc);
_lua_pcall lua_pcall;

int luaopen_jit_hook(lua_State *L)
{
    int ret_val = luaopen_jit_original(L);
    luaL_loadfilex(L, "C:\\test.lua", NULL) || lua_pcall(L, 0, -1, 0);
    return ret_val;
}

BOOL APIENTRY DllMain(HMODULE mod, DWORD reason, LPVOID res)
{
    switch (reason) {
    case DLL_PROCESS_ATTACH: {
            luaL_loadfilex = (_luaL_loadfilex)LOADFILEEX_ADDR;
            lua_pcall = (_lua_pcall)PCALL_ADDR;
            HookCode(OPENJIT_ADDR, luaopen_jit_hook, (void**)&luaopen_jit_original);
            break;
        }
    }
    return TRUE;
}

This is how my hook looks, but I'm using a proprietary hooking engine. You can use Detours or your own engine. Remember, this hook should be in a DLL which is injected into the process.

Now, when a new Lua environment is created, this will load C:\test.lua into the environment. Typically, I'll first inject code which uses debug.sethook to intercept all calls to Lua functions, as well as their parameters, for later analysis:

jit.off()

FILE_PATH = "C:\\LuaJitHookLogs\\"
STARTING_TIME = os.clock()
GDUMPED = false

function dumpGlobals()
    local fname = FILE_PATH .. "globals_" .. STARTING_TIME .. ".txt"
    local globalsFile = io.open(fname, "w")
    globalsFile:write(table.show(_G, "_G"))
    globalsFile:flush()
    globalsFile:close()
end

function trace(event, line)
    local info = debug.getinfo(2)

    if not info then return end
    if not info.name then return end
    if string.len(info.name) <= 1 then return end

    if (not GDUMPED) then
        dumpGlobals()
        GDUMPED = true
    end
    
    local fname = FILE_PATH .. "trace_" .. STARTING_TIME .. ".txt"
    local traceFile = io.open(fname, "a")
    traceFile:write(info.name .. "()\n")

    local a = 1
    while true do
        local name, value = debug.getlocal(2, a)
        if not name then break end
        if not value then break end
        traceFile:write(tostring(name) .. ": " .. tostring(value) .. "\n")
        a = a + 1
    end

    traceFile:flush()
    traceFile:close()
end
debug.sethook(trace, "c")

This will drop a bunch of useful global and tracing information to C:\LuaJitHookLogs\.

Lua Breakdown

If you're familiar with Lua, you can probably skip this part. If you're not, stick around while I walk through the details of this script.

I start by calling jit.off because the debug library cannot intercept calls to code which has been just-in-time compiled by the jit engine.

Within dumpGlobals, I print out a table named _G. This is the global object table, and is used by Lua to track everything that is within global scope in the form of key, value pairs. As you can imagine, this is extremely useful. Depending on your use-case, you might want to call dumpGlobals a bit later in the life of the environment, as the game might not have all of it's globals configured upon it's first function call.

I use debug.sethook(trace, "c") to tell Lua that trace should be called before each function call is completed. Within trace, I call debug.getinfo(2) to obtain the name of the function call being intercepted. This is because the trace function is the current function, which is at stack level 1, meaning that the intercepted function is at stack level 2. You'll then see that I call debug.getlocal(2, a) in a loop with with a incrementing from 1 until a nil value is returned. This is simply looping through the stack at stack level 2 until there are no remaining locals, which is a way to find the parameters in the form of key, value pairs. Here's an example I captured from a game which you can probably guess, but will remain nameless:

type()
(*temporary): table: 074FA7D0
{
    IsDestroyed = false,
    NumOfSpawnDisables = 0,
    SpawnOrderMinionNames = 
    {
        "Super",
        "Melee",
        "Cannon",
        "Caster",
    },
    WillSpawnSuperMinion = 0,
}

This is a simple call to type to determine the type of an object, but the object itself leaks some interesting information about the game.

If we were so inclined, we could change parameters to calls using debug.setlocal.

Intercepting Lua Code

In many cases, it can be beneficial to intercept entire Lua scripts. This allows you to inspect a game's scripts and understand how they function without trying to piece it together from tracing code you've injected. Lua code can be loaded into the LuaJIT environment either as a file or a buffer. Because files can be easily inspected on-disk, we'll focus on buffers and luaL_loadbuffer.

There are actually two flavors of this function: luaL_loadbuffer and luaL_loadbufferex. They're almost identical, where the former just calls the latter with NULL as a final parameter (here). You might think this means we can hook luaL_loadbufferex to catch both calls, but that's not the case: LuaJIT is built for performance so luaL_loadbufferex is typically inlined. Both functions, however, end up calling of this function (here):

int lua_loadx(lua_State *L, lua_Reader reader, void *data, const char *chunkname, const char *mode);

The aggressive inlining leads to this also being inlined. However, the main abstraction at work here is lua_Reader reader, a callback which knows how to turn void *data into a buffer containing Lua code. When loading code which is already in buffer, reader will be the address of reader_string (here), and the Lua code is a char* pointed to by void *data.

reader_string is not inlined, as it's address is passed around as a callback pointer, so it can be found inside of luaL_loadbuffer:

reader_string

Using the address, we can design a hook to intercept and display all Lua buffers which are loaded:

typedef const char* (*_reader_string)(lua_State *L, void *ud, size_t *size);
_reader_string reader_string_original;

const char* reader_string_hook(lua_State *L, void *ud, size_t *size)
{
    if (((size_t*)ud)[1] > 0)
        MessageBoxA(NULL, ((char**)ud)[0], "LuaJITHook", MB_OK);
    return reader_string_original(L, ud, size);
}

// from DllMain DLL_PROCESS_ATTACH
HookCode(READERSTRING_ADDR, reader_string_hook, (void**)&reader_string_original);

A message box probably isn't the best way to accomplish this, but you get the idea.

Wrap Up

This technique can be extremely powerful. Many games expose functionality to Lua which can be used for automation, heads-up displays, and ESP hacks. Different games may have different flavors of Lua, but all of them will work very similar to this.

You can build upon this hook by adding scanning functionality which can automatically locate these functions, perhaps using a library like XenoScan.

Feel free to give feedback in the comments, and follow me on Twitter for updates on my blog and other projects.