Why leveling if you can just use Lua? Hacking Lua inside Games

Scripting Engines

In the last post, we saw how to properly hook and use AngelScript from HPL Engine to manipulate a game. If you haven’t read it yet, I really recommend you to do it, it was an interesting case, and there isn’t much information about hooking AngelScript on the Internet.

This time we go for more, we are going to see how to hook Lua inside a well-known game (Vermintide 2) to practically do anything we want, boost our experience, spawn items on your inventory or on the floor, heal yourself, send chat messages, kill everything? Why not?

TL;DR

After an introduction to Lua, we are going to analyze how we can hook Lua inside any game and then we are going to explain each step to write your own LuaHook. In the end, we will be executing our own Lua code inside the game context without any limitation.

Boosting our experience to reach MAX lvl

Lua

As we have seen with AngelScript, when Lua is used in games, it has the main goal to add client site features to the game. It is an extremely powerful, flexible and easy to integrate scripting language, which allows game developers to add functionalities to game client in a much more readily way. Lua is distributed in packages and can be built in almost all the platforms that have a stander C compiler. It runs in multiple operating systems such as Windows, Unix, Android, iOS, etc. I let you here an interesting post about Lua and games here.

LuaJIT

It is highly possible that you find something called LuaJIT inside games, this is an independent implementation of Lua using a Just-in-time compiler which runs even with more speed than the normal Lua. This is actually what we are going to see today, a variation of Lua, which is being used by Vermintide 2 to add content and features to the game.

What we are going to do with LuaJIT, can be done with the normal version of Lua too. Especially because you can use common methods that Lua and LuaJIT have in common to create the hook we are going to see now.

How to hook Lua?

LuaState

In order to hook Lua we need to introduce the concept of LuaState. LuaState is the universe where all the code will run.  Games can run multiple States and it will depend on us to identify the State we need to hook. Usually, each call to a Lua method requires a reference to a valid State structure, if we are able to obtain this reference, we can invoke those methods by ourselves.

Of course, this process may change depending on the Lua version the game is using, and also if it is using vanilla Lua or LuaJIT. Identify if they use LuaJIT is quite easy, I usually check the methods defined inside luaXX.dll to see if there is any function like luaJIT_XXXX:

 

Actually, you will have a method called luaJIT_version_X_X_X, which will display the current version in use. I truly recommend you to validate the version before trying to hook the script engine since you may need some slight modifications to work well depending on the version.

Obtaining a valid reference to a LuaState

There are multiple functions that we can hook to obtain the reference we need. Usually, a pointer to a LuaState will be the first argument on each function. We can confirm it by looking at one example: lua_gettop()

int lua_gettop (lua_State *L);

As the documentation says: “Returns the index of the top element in the stack. Because indices start at 1, this result is equal to the number of elements in the stack (and so 0 means an empty stack)”. And the most important aspect is that this call is usually called multiple times and in a very constant way, which makes it perfect to be hooked. We can be sure that this method will be called in the future and our hook will be invoked.

Let’s start coding:

typedef void* lua_State;

// LUA STATE HOOK DEFINITIONS
lua_State * lua_State_ptr = 0;
lua_State * new_lua_State_ptr = 0;
typedef int(__cdecl *gettop)(int);

DWORD _gettop(int state)
{
	if (lua_State_ptr == 0) {
		lua_State_ptr = (lua_State *)state;
		std::cout << "[+] State Obtained: \t" << std::hex << lua_State_ptr << std::endl;
	}
	return (*(DWORD *)(state + 24) - *(DWORD *)(state + 16)) >> 3;
}

What do we have here? On the first line, we have the definition of a void pointer called lua_State, by doing this we are declaring our pointer to a LuaState structure. Then, we are creating two variables to store the reference we need. Why two? I will explain it later.

And finally, we will define the existing function lua_gettop as gettop and we will create our own version called _gettop. If you paid enough attention, you may be wondering where all this came from: (*(DWORD *)(state + 24) - *(DWORD *)(state + 16)) >> 3.

Simple, instead of looking for the original address of lua_gettop and defining a new function on that address to call it before returning, I decided to implement it by my self. Actually, what I did was open the lua51.dll that comes with the game with IDA PRO and then I used the decompiler to obtain an equivalent code in C++ of lua_gettop. This option, of course, is not the best one, but I wanted to show you that sometimes we can implement everything by ourselves without calling the original function. In this case, the function is quite simple and we can easily do it.

Last but not least, we will store the argument on our lua_State pointer.

Defining our Lua functions with typedef

Once we have our reference to LuaState, we won’t be able to call any Lua function without defining first the required functions we want to call. Again there are multiple functions and ways to execute code inside Lua. Some functions and macro examples are, but not limited to:

  • luaL_dostring: macro for luaL_loadstring and lua_pcall
  • luaL_dofile: macro for luaL_loadfile and lua_pcall
  • luaL_loadfilex and lua_pcall

Those functions are all built-in with the library and may vary from one version to another.

I decided to go for the last option:

// Mandatory functions
typedef int(*_luaL_loadfilex)(lua_State *L, const char *filename, const char *mode);
typedef int(*_lua_pcall)(lua_State *L, int nargs, int nresults, int errfunc);

// Additional (optionals)
typedef lua_State *(__cdecl *newThread)(lua_State *);
typedef void(__cdecl *pushinteger)(lua_State *, int);
typedef int(__cdecl *tointeger)(lua_State *, int);
typedef int(__cdecl *_luaStatus)(lua_State *);
typedef int(__cdecl *_luaL_loadstring)(lua_State *, const char *);

Original addresses and Detour

By looking at lua51.dll, we can obtain all the required offsets by subtracting the DLL base address to the address of the function in run time as we have seen before. As an example, the offset address of luaJIT_version_2_1_0_beta3 will be 7FF9905C4130 minus 7FF990590000 : 0x34130

struct values {
	uintptr_t lua51Module;
	uintptr_t getTop;
	uintptr_t newThread_f;
	gettop lua_gettop_p;
	newThread lua_newthread_p;
	pushinteger lua_pushinteger_p;
	uintptr_t lua_pushinteger_f;
	tointeger lua_tointeger_p;
	uintptr_t lua_tointeger_f;
	uintptr_t lua_status_f;
	_luaStatus lua_status_p;
	uintptr_t luaL_loadstring_f;
	_luaL_loadstring luaL_loadstring_p;
	uintptr_t lua_pcall_f;
	_lua_pcall lua_pcall_p;
	uintptr_t luaL_loadfilex_f;
	_luaL_loadfilex luaL_loadfilex_p;
}val;


void retrieveValues()
{
	val.lua51Module = (uintptr_t)GetModuleHandle("lua51.dll");
	val.getTop = (uintptr_t)(val.lua51Module + offsets.getTop);
	val.lua_gettop_p = (gettop)val.getTop;

	val.newThread_f = (uintptr_t)(val.lua51Module + offsets.newThread);
	val.lua_newthread_p = (newThread)val.newThread_f;

	val.lua_pushinteger_f = (uintptr_t)(val.lua51Module + offsets.pushinteger);
	val.lua_pushinteger_p = (pushinteger)val.lua_pushinteger_f;

	val.lua_tointeger_f = (uintptr_t)(val.lua51Module + offsets.tointeger);
	val.lua_tointeger_p = (tointeger)val.lua_tointeger_f;

	val.lua_status_f = (uintptr_t)(val.lua51Module + offsets.luaStatus);
	val.lua_status_p = (_luaStatus)val.lua_status_f;

	val.luaL_loadstring_f = (uintptr_t)(val.lua51Module + offsets.loadstring);
	val.luaL_loadstring_p = (_luaL_loadstring)val.luaL_loadstring_f;

	val.luaL_loadfilex_f = (uintptr_t)(val.lua51Module + offsets.loadfilex);
	val.luaL_loadfilex_p = (_luaL_loadfilex)val.luaL_loadfilex_f;

	val.lua_pcall_f = (uintptr_t)(val.lua51Module + offsets.pCall);
	val.lua_pcall_p = (_lua_pcall)val.lua_pcall_f;
}

 

 

Then, we detour lua_gettop:

void detourLuaState()
{
	std::cout << "[+] Calling LuaState Detour" << std::endl;
	DetourTransactionBegin();
	DetourUpdateThread(GetCurrentThread());
	DetourAttach(&(LPVOID&)val.lua_gettop_p, (PBYTE)_gettop); // Detours the original lua_gettop_p with our _gettop
	DetourTransactionCommit();
}

Cloning the LuaState

Sometimes we may encounter problems when trying to use the same luaState the game is using at the same time. If that’s the case Lua provides us with a solution: lua_newthread. This will create a new thread and return a pointer to a lua_State that represents this new thread. The new state returned by this function shares with the original state all global objects (such as tables) but has an independent execution stack.

Perfect, we can clone the luaState and use it to execute code in the game but with an independent execution stack. That’s why we had defined two pointers to luaState, in case we needed to do this.

lua_State * CreateThread()
{
	std::cout << "[+] Calling newThread" << std::endl;
	lua_State * thread = (val.lua_newthread_p)(lua_State_ptr);
	std::cout << "[+] New LuaState: " << thread << std::endl;

	return thread;
};

Writing your own Lua Script

We have been talking about executing Lua code in the game context, but how are we going to write our own scripts? As same as happened with AngelScript in the previous post, those scripts are usually compiled inside the game files. We are not going to cover how to extract these files in this post but I will provide some examples of code that we can use to manipulate the game.

Boosting your character experience:

local function setmylevel()
	local hero_name = "bright_wizard"
	local hero_attributes = Managers.backend:get_interface("hero_attributes")
	hero_attributes:set(hero_name, "experience", 55000)
end
 
setmylevel()

Spawning items:

local function spawnit()
	local backend_items = Managers.backend:get_interface("items")
	local backend_id = backend_items.award_item(backend_items, "we_h1_sword_1001")
	local backend_id = backend_items.award_item(backend_items, "we_dual_wield_sword_dagger_1001")
	local backend_id = backend_items.award_item(backend_items, "bw_skullstaff_beam_1001")
	local backend_id = backend_items.award_item(backend_items, "wh_hat_0011")

	return
end

spawnit()

These examples were crafted by observing and analyzing the existing Lua scripts inside the game. After a couple of minutes, you will be able to write anything you want by just modifying existing scripts.

Execute our code!

The final step would be to properly call our script and check if it works properly. Remember that we had defined previously all the required function and that we found the real address of each of them. Now we will just call those function and invoke them using the correct parameters:

while (true)
{
	if (GetAsyncKeyState(VK_F9) & 1)
	{
		val.luaL_loadfilex_p(lua_State_ptr, "main.lua", NULL) || val.lua_pcall_p(lua_State_ptr, 0, -1, 0);
		std::cout << "[!] Main.lua Executed" << std::endl;
		//std::cout << "[+] LuaStatus: " << val.lua_status_p(lua_State_ptr) << std::endl;
		//std::cout << "[+] New LuaStatus: " << val.lua_status_p(new_lua_State_ptr) << std::endl;
	}
	Sleep(2000);
}

Once injected the DLL, each time the player press F9, the cheat will look for the main.lua file inside the game executable folder and run it. If you are not sure where the game is looking for the file you can either hardcode a path or use ProcessMonitor to check all the system files calls:

 

Conclusion

Lua is a strong and powerful scripting engine and it is used by hundreds of games. Targeting a scripting engine like this will be definitely a good entry point to any game using it. Squirrel is another scripting engine in our TODO list, and I may write a post about it on a near future 🙂

I will be uploading the code to GitHub in coming days.