Getting the FastClusterEntity VTable
Make sure you've got IDA 9.1+ and a decrypted .bin for whatever Roblox version you're on.
Finding the offset
Open the .bin into IDA and wait for auto-analysis to finish.
Open the Strings subview with Shift+F12 and search
for FastClusterTrace. You'll land on something like
.rdata:0000000005E83CA8 00000011 C FastClusterTrace.
Take that address and subtract 0x40 — that's your
FastClusterEntity VTable offset.
Applying chams at runtime
With the VTable offset, now walk every committed
memory region in the roblox process, and overwrite the render queue ID with 13
to force the chams.
POC:
namespace chams {
constexpr uintptr_t vtable_offset = 0x5E83C68;
constexpr uintptr_t rq_id_offset = 0x10;
constexpr uint32_t rq_id_chams = 13;
constexpr size_t max_region = 512 << 20;
static bool scannable(const MEMORY_BASIC_INFORMATION& region) {
if (region.State != MEM_COMMIT || region.Type == MEM_IMAGE)
return false;
return region.Protect == PAGE_READWRITE
|| region.Protect == PAGE_EXECUTE_READWRITE;
}
static void scan_region(HANDLE proc, const MEMORY_BASIC_INFORMATION& region,
uintptr_t vtable, std::vector<uint8_t>& scratch) {
if (scratch.size() < region.RegionSize)
scratch.resize(region.RegionSize);
SIZE_T bytes_read = 0;
if (!ReadProcessMemory(proc, region.BaseAddress,
scratch.data(), region.RegionSize, &bytes_read) || bytes_read < 16)
return;
auto* data = reinterpret_cast<const uintptr_t*>(scratch.data());
size_t count = bytes_read / sizeof(uintptr_t) - 1;
for (size_t i = 0; i < count; ++i) {
if (data[i] != vtable)
continue;
if (data[i + 1] < 0x10000 || data[i + 1] >= 0x7FFFFFFEFFFFuLL)
continue;
uintptr_t entity = reinterpret_cast<uintptr_t>(region.BaseAddress)
+ i * sizeof(uintptr_t);
memory->write<uint32_t>(entity + rq_id_offset, rq_id_chams);
}
}
void run() {
HANDLE proc = memory->get_handle();
uintptr_t base = memory->get_base("RobloxPlayerBeta.exe");
SYSTEM_INFO si;
GetSystemInfo(&si);
uintptr_t lo = reinterpret_cast<uintptr_t>(si.lpMinimumApplicationAddress);
uintptr_t hi = reinterpret_cast<uintptr_t>(si.lpMaximumApplicationAddress);
uintptr_t vtable = base + vtable_offset;
std::vector<uint8_t> scratch;
MEMORY_BASIC_INFORMATION region;
for (;;) {
if (globals::engine_chams::enabled) {
for (uintptr_t cursor = lo; cursor < hi;) {
if (!VirtualQueryEx(proc, reinterpret_cast<LPCVOID>(cursor),
®ion, sizeof(region)))
break;
if (scannable(region) && region.RegionSize <= max_region)
scan_region(proc, region, vtable, scratch);
cursor = reinterpret_cast<uintptr_t>(region.BaseAddress)
+ region.RegionSize;
}
}
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
}
Enjoy skidding Engine Chams.
For any help @not284 on Discord