Bypassing Hyperion's Working Set Detections
External cheat providers often rely on continuous data manipulation within Roblox, from reading and writing to memory, to enable their functionality. Hyperion counters these cheats by exploiting the process's working set, allowing it to detect external processes that access or modify a designated memory pool containing player data.
In this blog, we will reverse-engineer Hyperion's detection mechanisms and craft a bypass for educational purposes.
Getting Started
Before we can begin, we need to acquire a decrypted version of the Roblox Player. You may use any tool you like, but I recommend vulkan, a tool I designed specifically for processes protected by Hyperion (this includes Roblox).
For this reversal, I used the version-37cf60402a5648b4
build of Roblox.
Tools Used
- IDA 7.7: Used for reversing the Player with the ClassInformer plugin.
- Binary Ninja: Used for analyzing the Hyperion module (
RobloxPlayerBeta.dll
).
Initial Impressions
After loading the Player into IDA and resolving RTTI, you may notice that all instances being watched have a unique deleter type, RBX::Creatable::Deleter2
.

What's unique about these instances is that Roblox accesses them infrequently. As a result, moving the memory they're allocated in and out of the working set doesn't significantly impact the client's performance.
After searching for cross-references to the virtual function table of the std::_Ref_count_resource
with the unique deleter, you will see exactly where the instance is allocated.

As you can see, first the Player queries Hyperion for the custom allocation routine, which will return a block of memory in the watched memory pool. If the global Hyperion::AllocateWatchedMemory
function pointer isn't set, Roblox will attempt to acquire it again.

This pattern (Hyperion::Acquire::
) you see is actually quite common throughout the Player. This is how the Player communicates with the Hyperion module. If you look at Hyperion::Acquire::AllocateWatchedMemory
, you can see that a function call to an invalid memory location is invoked.

Otherwise, a default allocator is used and a smart pointer with the regular deleter (RBX::Creatable::Deleter
) is created.

Diving Deeper
If you take a look, Hyperion::AllocateWatchedMemory
is just a pointer to a virtual address. This address resides in the Hyperion module's memory.

Hyperion::AllocateWatchedMemory
function pointer.As you can see, it points to an absolute address: 0x7FFFDB7EB150
. Now that the Hyperion module has been dumped from memory, we can search for that absolute address within the dumped memory.
As expected, the routine takes two parameters: a pointer to a memory pool (this is where the allocated memory is passed) and a size, which corresponds to the size of the object we're allocating memory for.

You will initially notice a lot of complex code with tons of nested if-statments. These are part of Hyperion's lazy importer, which they use to obscure system calls and Windows API calls. This function is hard to trace, so let's see what it returns and try to trace it back.

Hyperion::AllocateWatchedMemory
As you can see above, the routine sets the memoryPool
parameter with the pointer to the newly reserved memory, and a return value is set. If no memory is available (or something went wrong) a return value of 5
is passed. This forces the Roblox Player to use an alternative method of allocation.
After searching for references to allocation
in the routine, you will find an interesting check.

The watched memory pool stores a size that represents available space in bytes. If the number of bytes we're trying to allocate exceeds this value, allocation
is set to null and an error arises.
Now a theory arises: Can we trick Hyperion into thinking that the watched memory pool is out of memory and force the Roblox Player to use alternative allocators?
Testing The Theory
To proceed, we must first locate the watched memory pool. Conveniently, it is the only private, committed memory region with read and write permissions and a fixed size of 0x200000
, making it straightforward to identify.
Furthermore, this will only work at the initialization of Roblox, since that's when the instances get allocated. Here are the steps we must follow:
- Wait for the Roblox Player to load.
- Find the region containing the watched memory pool.
- Set the number of free bytes to
0x20
(minimum number of bytes).
This was actually incredibly easy to implement, and I did it in 50 lines of C++ code using wincpp. You can check out the GitHub repository here.
Conclusion
Through careful reverse engineering of both the Roblox Player and Hyperion, we were able to craft a simple program that forces the Player to use the default allocator for all instances, effectively disabling the working set detections entirely.
If you're looking to safeguard your native applications and make reverse engineering significantly more challenging, check out Nestra. Our web-based PE obfuscation service provides robust protection tailored to your needs.