Reversing Hyperion's Deleter2 Mechanism
This blog post explores the inner workings of Roblox's Hyperion anti-tamper mechanism, specifically its Deleter2 routines designed to detect and prevent out-of-sync memory access in critical game structures. By leveraging forced exceptions, pointer encryption, and a dedicated memory pool, Deleter2 primarily targets external cheats while leaving internal operations largely unaffected. The post provides a comprehensive reverse engineering analysis, breaking down the initialization, guarded memory allocation, and protections employed by Deleter2 and highlighting potential weaknesses in its design.
Background Knowledge
Windows Paging
To understand Deleter2, we must first explore how Windows manages process memory. While this explanation simplifies a complex topic, it provides the necessary context for understanding the mechanism.
When you access a page in Windows, several things happen depending on the memory address:
- If the page isn’t in memory, the CPU triggers a page fault exception, signaling Windows that an invalid memory address was accessed.
- Windows checks the Virtual Address Descriptor (VAD), a high-level memory management structure, to determine if the memory should be allocated. If valid, the memory is loaded into RAM.
- For pages not currently accessed, Windows can:
- Page them out to disk, freeing up RAM for other processes.
- Free the memory entirely if it's no longer needed.
When an address isn’t part of an allocated or described memory region, Windows forwards the exception to user-mode exception handlers as an invalid memory access.
Hyperion’s Forced Exceptions
Hyperion employs exception-oriented code to signal key events, such as initialization. For example, when Hyperion calls the function 0xFFFFFFFFFFFFFFF4
with a pointer to a data structure, it triggers an invalid memory access exception. Windows sends this exception to Hyperion’s handler, which examines the exception record to determine the accessed address and origin.
If the origin is RobloxPlayerBeta.exe
and the address matches 0xFFFFFFFFFFFFFFF4
, Hyperion treats it as a valid exception event. It adjusts the RIP
(instruction pointer) in the _CONTEXT
structure to point to its handler function and resumes the thread, effectively invoking a Hyperion handler indirectly.
Key notes:
- Only the
RobloxPlayerBeta.exe
PE file is permitted to execute these functions. - Any attempt to force the exception from outside the game module results in a crash.
Hyperion’s Pointer Encryption
Hyperion uses pointer encryption extensively to obfuscate memory access and complicate reverse engineering efforts. Additional techniques like dead stores, opaque predicates, and MBA expressions further obscure operations. A detailed explanation of these concepts can be found in this blog post.
The pointer encryption mechanism is relatively simple but is updated frequently, altering the underlying mathematical operations.
Deleter2’s Target Audience
Deleter2 is one of Hyperion’s weaker checks, primarily targeting external cheats that rely on frequent, unsynchronized memory access. These include ESP and aimbot hacks. Internal cheats, which operate on the game’s engine scheduler, are generally unaffected because their memory operations remain synchronized with the game pipeline.
For inexperienced cheaters, Deleter2 effectively mitigates external cheats with high-frequency memory operations, demonstrating its utility despite being a less sophisticated protection.
Reverse Engineering The Mechanism
Deleter2's Initialization Routine
Now that we understand the core Windows features exploited by the Deleter2 mechanism to enable its functionality, we can begin our reversal. To start we will analyze a common instance that it protects, RBX::Players
. This instance is the service that manages all the players in the game making it vital for ESP and Aimbot cheats.

RBX::Players
in watched memory.The code snippet demonstrates the initialization process for the RBX::Players
object within a protected memory region managed by Hyperion's Deleter2 mechanism. First, it checks if the Allocate
function in Hyperion::Deleter::PlayersContext
is initialized. If not, it calls InitializePlayersContext
to set it up. Once the allocation function is available, it acquires a memory pool and uses it to construct the RBX::Players
object. This ensures that the object is securely allocated in protected memory, safeguarding it from unauthorized access or tampering.
Let us take a deeper look at Hyperion::Deleter::InitializePlayersContext
and see what it exactly does.

Hyperion::Deleter::InitializePlayersContext
routine.Here we see Hyperion utilizing its forced exception system. As previously discussed, this involves attempting to call an invalid memory location (0xFFFFFFFFFFFFFFF4
), which appears to initialize a structure: Hyperion::Deleter::PlayersContext
. After examining its definition, the structure resembles the following:

Hyperion::Deleter::PlayersContext
.The structure includes the following properties:
- Status (
uint8_t
): A flag indicating the structure's initialization state:0
: Not initialized.1
: Partially initialized.2
: Fully initialized.
- BeginChecks (
uintptr_t
): A pointer to a routine that starts the working set detection and pages the memory pool into the working set. - EndChecks (
uintptr_t
): A pointer to a routine that ends the working set detection and pages the memory pool out of the working set. - Allocate (
uintptr_t
): A pointer to a routine responsible for allocating instances in the protected memory region. - Deallocate (
uintptr_t
): A pointer to a routine responsible for deallocating instances from the protected memory region.
All of these function pointers reference routines within the Hyperion module which is why you don't see any definitions.
Deleter2's Scoped Detection
This routine is part of the function marshaller, an internal mechanism in Roblox that schedules events to run asynchronously on the game's main thread. This is where BeginChecks
and EndChecks
come into play.

When certain tasks are executed through this marshaller, BeginChecks
ensures access to a protected memory region by paging it into the working set and locking it there. The function or task is then executed, and once it's complete, EndChecks
pages the memory back out of the working set. This pattern allows the game to enforce controlled access to protected memory, ensuring that certain operations only run when the memory is actively available.
Hyperion's Deleter2 Exception Handler
Hyperion's ExceptionHandlers::ResolveInvalidMemoryCalls
is the structured exception handler responsible for resolving calls to invalid memory locations, as seen earlier in the Roblox Player. This routine determines the appropriate target routine based on the exception code and address.

Interestingly, this handler is not inlined or chained within Hyperion's Instrumentation callbacks like most others. Its standalone design makes reverse engineering and analysis much easier. The first switch case (-0xc
→ 0xFFFFFFFFFFFFFFF4
) handles the routine that initializes the Deleter2 context structures for protected instances, as discussed earlier.
Understanding Deleter2 Context Initialization
Like most routines in Hyperion, the Hyperion::Deleter::InitializeContext
routine is very complex and heavily obfuscated, so we will only examine parts of it. At the beginning, you will see something like this:

Hyperion::Deleter::InitializeContext
The pointer to the watched memory region is decrypted as part of Hyperion's obfuscation mechanism. If this pointer is uninitialized (i.e., set to NULL
), the stub invokes a syscall to allocate the watched memory region. Binary Ninja's high-level output might not provide clear details, so analyzing the assembly ensures accuracy.

NtAllocateVirtualMemory
stub.After decrypting the system syscall number (SSN), the following arguments are passed to the syscall: the process handle, allocation size, base address, alignment, and memory protection flags. Recreating this logic in C shows a direct call to NtAllocateVirtualMemory
, setting up a 2 MB memory region with the MEM_COMMIT | MEM_RESERVE
flags and PAGE_READWRITE
protection.
SIZE_T regionSize = 0x200000;
NTSTATUS status = NtAllocateVirtualMemory(
GetCurrentProcess(), // Process handle (current process)
NULL, // Base address (NULL means any address)
0, // ZeroBits (alignment, typically 0)
®ionSize,
MEM_COMMIT | MEM_RESERVE, // Memory allocation type
PAGE_READWRITE // Memory protection flags
);
Once the syscall completes successfully, the internal pointer is re-encrypted with the address of the allocated memory region, and the memory block is initialized for use. After this, the output info structure is constructed.

Hyperion::Deleter::Context
instance.Depending on the available memory, either a partial or complete context is created. If space is limited, only the detection routines (BeginDetection
and EndDetection
) are set, ensuring Hyperion can still monitor and protect the memory region. When sufficient space is available, the full context is initialized, including routines for allocation and deallocation of watched memory.
Breaking Down Deleter2 Memory Management
Contrary to what it might seem, AllocateWatchedMemory
and FreeWatchedMemory
do not modify the size of the watched memory pool. Instead, they provide a pointer to a block of free, pre-reserved memory within the pool. The watched memory pool is composed of smaller memory blocks linked together in an internal data structure. To understand this in more detail, let’s examine Hyperion::Deleter::AllocateWatchedMemory
.

Hyperion::Deleter::AllocateWatchedMemory
.The Hyperion::Deleter::AllocateWatchedMemory
routine takes two parameters: memoryPool
and size
. The memoryPool
parameter is assigned a pointer to the next memory block, while size
specifies the number of bytes the block needs to accommodate.
Hyperion uses pointer encryption to secure the watchedMemoryPool
pointer, adding an extra layer of obfuscation to its memory management. Further down, we can see the logic responsible for retrieving the next available memory block.

The watched memory pool is essentially a linked list of memory blocks. If there isn't enough free space in the pool (*(watchedMemoryPool + 0x208) - 0x20 < size
) or if no free blocks are available, the allocation is set to an invalid value (nullptr
or NULL
). Otherwise, the next free block is retrieved and initialized.

The code handles block linking by maintaining references to the previous and next blocks. If there are remaining blocks after the allocation, a new free block is created from the remaining space, linked into the pool, and updated in the metadata.
Looking At The Detection
We now have a general understanding of how Hyperion detects out-of-sync access to the watched memory pool. The theory is that BeginDetection
checks if the watched memory is in the working set and triggers an error if it isn't. It then forces the memory back into the working set. Conversely, EndDetection
likely performs the opposite action. To confirm this, let’s take a closer look at Hyperion::Deleter::BeginDetection
.

NtQueryVirtualMemory
call to initiate the detection check.As we examine the code, we see that the pointer to the watched memory pool is retrieved again, and a structure is populated with working set information. This structure contains the virtual addresses of each block in the memory pool. Once this setup is complete, a call to NtQueryVirtualMemory
is made to gather detailed information about the memory region.
NTSTATUS result = NtQueryVirtualMemory(
GetCurrentProcess(), // Process handle (current process).
0, // Base address (passed via memory info)
MemoryWorkingSetExInformation, // Information class (0x4)
workingSetInformation, // The memory information structure
0x2000, // The size of memory information
NULL
);
The syscall uses the MemoryWorkingSetExInformation
class (0x4
) to populate the workingSetInformation
structure with the virtual attributes of each block in the watched memory region. This process ensures that the memory's state is checked and updated.
As we explore further, we come across another decrypted pointer. This pointer references an address within the watched memory pool and contains flags and attributes related to the associated memory block.

watchedMemoryInformationPool
initialization.While the exact purpose of the following syscall is unclear, it appears a call to NtProtectVirtualMemory
is made to apply PAGE_READWRITE
access rights to the entire watched memory region. The syscall might resemble the following:
NTSTATUS result = NtProtectVirtualMemory(
GetCurrentProcess(), // Process handle (current process).
watchedMemoryPool, // Base address
0x20000, // The size of the region
PAGE_READWRITE, // The new protection
&oldProtection
);
The loop iterates through all the blocks in the information pool by updating the offset of the current block being processed. Within the loop, we find this notable check:

This condition checks whether the Valid
bit for the current page is set. The Valid
bit is extracted from the virtual attributes using the workingSetInformation
pointer:
- If the bit is set: The page is in the working set, indicating it was accessed out-of-sync, which triggers a detection.
- If the bit is not set: There is no detection, and the process continues.
When the Valid
bit is set, a detection almost always occurs—you just might not realize it immediately. While this flag alone may not cause a crash or any noticeable impact, it’s likely being logged or tracked internally. Hyperion uses this information to monitor out-of-sync access, even if there are no immediate consequences.
In the deeper checks that follow, specific conditions evaluate the memory block’s attributes and redirect control flow accordingly.

If the checks pass, the process jumps to a location that skips setting the return value of 7
(which would typically occur if the checks fail) but continues executing the regular code. Similar to a regular detection where the checks fail, Hyperion still updates a value in the watchedMemoryInformationPool
to indicate that a memory block was accessed out-of-sync with the engine. This means that even though the return value remains 0
and the program appears to run normally, Hyperion is still logging the out-of-sync access.

Once this value is set, execution reaches label_7fffdb8c747a
. If the Valid bit was not set, execution jumps to this label and continues as usual.

Hyperion::Deleter::BeginDetection
routine.The currentBlockOffset
is then incremented, and a loop iterates through each block. If the offset remains within 0x1ff
, the loop continues processing. Otherwise, once currentBlockOffset
exceeds 0x1ff
(which is 511 in decimal), the loop terminates.
Each block corresponds to a memory page (0x1000 bytes), and since the comparison includes 512 blocks, the total size covered is 0x200000 (2MB).
Conclusion
Through this deep dive into Roblox's Hyperion Deleter2 mechanism, we’ve explored how it enforces memory protection using forced exceptions, pointer encryption, and working set checks to detect out-of-sync memory access. By managing a protected memory pool and monitoring access through NtQueryVirtualMemory
, Deleter2 is primarily designed to detect external cheats that frequently access game data asynchronously.
Our analysis shows that while Deleter2 effectively flags unauthorized access, it does not always cause an immediate crash. Instead, it tracks and logs detections, allowing Hyperion to build a history of suspicious activity before taking action. Since it relies on working set manipulations, it is more effective against external tools than internal cheats that integrate directly with the game’s engine.
With the information provided, at least five different bypasses for the Deleter2 mechanism could be developed. One has already been published here. The rest are left for you to figure out.
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, making it far more difficult for attackers to analyze and tamper with your binaries.