-
Active Member
[Release] LuaGadget: Bypass Invalid Pointer Checks via JMP Redirection
Hello everyone,
I’ve been using a simple but powerful pattern for handling Lua → C calls: a centralized dispatcher that routes every Lua function through a single, well-defined entry point. This avoids ad-hoc pointer patching, keeps the call path consistent, and makes it easier to audit, debug, and maintain.
By registering closures with upvalues and funneling into one router, you get a stable, text-section entry for your functions, clearer error handling, and a cleaner separation between Lua scripts and native code. In practice, this has reduced crashes from bad pointers and made our integration far more predictable across builds.
Gadget.h
Code:
#include <lua.h>
// Typedef for C++ function objects if needed
typedef int (*lua_CppFunction)(lua_State* L);
class LuaGadget
{
public:
static uintptr_t TargetAddress;
static BOOL Install();
static int FunctionRouter(lua_State* L);
};
Gadget.cpp
Code:
#include "Gadget.h"
uintptr_t LuaGadget::TargetAddress = 0xRandomAddress;
BOOL LuaGadget::Install()
{
DWORD oldProtect;
BYTE* targetAddr = reinterpret_cast<BYTE*>(TargetAddress);
// Calculate the relative jump offset to our FunctionRouter
DWORD jumpOffset = reinterpret_cast<DWORD>(FunctionRouter) - (reinterpret_cast<DWORD>(targetAddr) + 5);
// Make the target memory writable
if (!VirtualProtect(targetAddr, 6, PAGE_EXECUTE_READWRITE, &oldProtect)) {
return FALSE;
}
// Write the JMP instruction and the offset
targetAddr[0] = 0xE9; // JMP opcode
*reinterpret_cast<DWORD*>(&targetAddr[1]) = jumpOffset;
targetAddr[5] = 0x90; // NOP for padding
// Restore the original memory protection
VirtualProtect(targetAddr, 6, oldProtect, &oldProtect);
return TRUE;
}
int LuaGadget::FunctionRouter(lua_State* L)
{
// Retrieve the function pointer from the Lua closure's upvalue
void* upvalue_ptr = (void*)lua_touserdata(L, lua_upvalueindex(1));
if (!upvalue_ptr)
return lua_error(L, "Invalid upvalue in closure");
try
{
// Check if this is full userdata (has a metatable) - indicates a lua_CppFunction object
if (lua_getmetatable(L, lua_upvalueindex(1))) {
lua_pop(L, 1); // Pop the metatable
// This is full userdata - treat as lua_CppFunction*
lua_CppFunction* func_ptr = static_cast<lua_CppFunction*>(upvalue_ptr);
if (func_ptr && *func_ptr) {
return (*func_ptr)(L);
}
}
else {
// This is light userdata - treat as a raw function pointer
using RawCFunc = int(__cdecl*)(lua_State*);
RawCFunc raw_func = reinterpret_cast<RawCFunc>(upvalue_ptr);
// Optional safety check before calling
if (!IsBadCodePtr(reinterpret_cast<FARPROC>(raw_func)))
{
return raw_func(L);
}
}
return lua_error(L, "Invalid function pointer in upvalue");
}
catch (const std::exception& e)
{
return lua_error(L, "C++ exception: %s", e.what());
}
catch (...)
{
return lua_error(L, "Unknown C++ exception");
}
}
Usage :
Code:
#define LUA_PUSH_FUNCTION(L, funcname, funcaddr) \
lua_pushlightuserdata(L, (void*)funcaddr); \
lua_pushcclosure(L, (lua_CFunction)LuaGadget::TargetAddress, 1); \
lua_setglobal(L, funcname);
The key innovation here is using multiple target addresses (I maintain over 200 validated addresses) and rotating them randomly between reloads. This ensures that:
- No single address pattern emerges for detection
- Memory scans can't establish a consistent signature
- The bypass remains effective long-term
- You blend in with normal client operations
-
Post Thanks / Like - 1 Thanks
encryrose (1 members gave Thanks to Makkah for this useful post)