Everyone's Detected: Roblox, Part I — YARA Memory Scanning

In the first installment of the Everyone's Detected series, we begin our deep dive into Roblox's layered anti-cheat system. This post focuses on their top-level detection method: a memory scanning routine that uses VirusTotal's YARA engine. We examine how Roblox relies on custom YARA rules to detect known cheat signatures in executable memory and trigger detection packets when a match is found. This layer acts as the game's first line of defense. It is fast, aggressive, and surprisingly effective.

Background Knowledge

What is YARA?

You’ll see YARA mentioned a lot in this blog, so here’s a quick explanation.

YARA is a tool used to identify files, programs, or memory by looking for specific patterns. It is commonly used by antivirus engines, malware analysts, and security tools to detect threats. You can think of it like a search engine for code that uses rules to decide if something looks suspicious or not.

It is powerful because it can quickly scan large amounts of memory or files and match them against many known patterns. Roblox uses it in a similar way to detect common cheats or exploits.

If you want to learn more, you can read about it on the official YARA website.

Reverse Engineering The Process

Before proceeding, it's important to note that all the code presented here has been extensively reverse-engineered and, where applicable, deobfuscated. As a result, the code you encounter in the binary may appear slightly different from what is shown.

This analysis is based on the version-e1da58b32b1c4d64 build of Hyperion, which is the latest publicly accessible version at the time of writing.

Hyperion's Main Loop

These memory scans are triggered from within Hyperion's main loop, which is also responsible for various integrity checks. I did not spend much time analyzing this part in detail, so some of what is described here may not be fully accurate.

When Hyperion decides to run a memory scan, which happens every few seconds, it begins by looking for regions of executable memory. Specifically, it searches for regions that are at least ten pages in size (40 KB). However, once a region is found, it does not scan it immediately. Instead, Hyperion attempts to recover the full memory allocation that the region belongs to.

It is important to understand that a memory region and an allocation are not the same. A region is just a part of an allocation, which may span multiple regions. Hyperion wants to scan the full allocation so it can analyze the entire module, not just the .text section that contains executable code. This gives the scanner more context, including headers and data sections, which helps improve the accuracy of detection.

Once the full memory allocation is recovered and its address range is known, Hyperion calls a function named Hyperion::RunMemoryScan. This routine is responsible for triggering the main scanning and matching logic. You can see the function below:

Entry point for executing the memory scan and updating internal detection statistics.

You may have noticed that after RunMemoryScanInternal is called, Hyperion appears to collect and compute statistics about the scan. If the scan completes successfully and the allocation is classified, a global structure located at RobloxPlayerBeta.dll+0x2d7530 is updated with the results. This structure keeps track of the latest scan data. Below is a simplified version of the key fields:

struct MemoryScanStatistics
{
  uint32_t _0;
  uint32_t _1;
  uint32_t lastScanMs;
  uint32_t _2;
  uint32_t totalScanMs;
  uint32_t allocBadCertCount;
  uint32_t allocNeutralCount;
  uint32_t allocSuspiciousCount;
  uint32_t allocLikelyMaliciousCount;
  uint32_t allocMaliciousCount;
};

Structure used to store scan results and classification statistics for each memory allocation.

These fields are fairly straightforward. The allocXXXCount entries track how many memory allocations were classified under each scan result. For example, allocMaliciousCount records how many allocations were marked as malicious.

In the next section, we will take a closer look at each scan status when we explore the YARA engine. For now, we will continue focusing on Hyperion.

Initializes scan input and result buffers before passing them to the YARA engine.

Looking ahead, we can see that PrepareMemoryForScan collects all readable memory and stores it in a vector called outBuffer. After this step, two more structures are initialized: inputView and result. These are the two main inputs passed to the YARA scanner running inside the Roblox client. I have recreated both structures below as accurately as possible:

struct ScanInputView
{
    uint8_t* data;
    uint32_t size;
};

struct ScanResult
{
    uint32_t capacity;
    uint32_t status;
    uint32_t dataSize;
    uint8_t data[0x1000];
};

Structures used to pass memory data and store scan results during the YARA scan.

Scrolling further, we finally reach the point where the YARA scanning routine is called inside the Roblox client. Both inputView and result are passed in, and the scanner writes the matched rules to the result structure. We will break down what happens inside the scan in the next section.

Calls the main YARA scan function with prepared input and result buffers.

Once the scan is finished, the status code is checked and a packet payload is created. This only happens if the scan result is considered severe enough. Specifically, a payload is only generated if the status is SCAN_BAD_CERT (0x1) or falls under one of the suspicious or malicious classifications. If the scan result is SCAN_NEUTRAL (0x2), the system will not create or send a packet.

Creates and fills a payload packet with scan data when a suspicious or malicious allocation is detected.

I will not go into full detail about the structure of the payload, but in short, it includes key values from the scan result and embeds the entire input view. This data is later compressed before being sent out. The purpose of this payload is to give Roblox's anti-cheat team a complete snapshot of the detected memory. With it, they can analyze how the exploit worked and improve their YARA rules to better detect similar threats in the future.

I will not show it here, but if you go back to Hyperion's main loop where Hyperion::RunMemoryScan is called, you will see that Hyperion applies an extra layer of encryption to the payload before sending it through its internal RakNet networking system inside the Roblox client. The networking layer is quite complex, so I will cover that in a future blog post.

Breaking Down The YARA Engine

Now that we know what memory Roblox provides to its YARA engine, let's take a closer look at how the scanner itself works. By jumping to the function we observed being called in Hyperion, we land at the main scanning routine:

The scanner’s entry point, responsible for invoking ScanInternal and copying results to the output buffer.

There’s not much happening in this function directly—it simply forwards the input to an internal routine, RBX::Scanner::ScanInternal, which performs the actual scanning. The input data is passed along, and the function fills an output buffer that gets copied into the ScanResult structure.

The start of the internal YARA matching logic, where memory is classified and matched against known rulesets.

Looking deeper, we reach the internal scanning logic. This is where the input buffer is passed to MatchMemory, a function that compares it against a set of YARA-generated rulesets. The result contains a status code (indicating how the buffer was classified) and a list of matched ruleset IDs. This data is later used to determine whether the memory block looks suspicious, malicious, or clean.

enum ScanStatus
{
  SCAN_BAD_CERT = 1,
  SCAN_NEUTRAL,
  SCAN_SUSPICIOUS,
  SCAN_LIKELY_MALICIOUS,
  SCAN_MALICIOUS
};

struct MatchResult
{
  ScanStatus status;
  struct vector_t matches;
};

A C-style recreation of the structures mentioned above.

Next, we look at the second step in the ScanInternal routine. After matching rulesets, Roblox encodes the list of rule IDs and copies them into the output buffer. These encoded IDs are a lightly obfuscated form of the original rule strings. This extra step likely serves as a basic layer of protection, making it harder for monitoring tools or network hooks to inspect or tamper with the scan results.

Encodes matched ruleset IDs into an obfuscated output buffer for transmission.

The Ruleset Matching System

Now we’ll take a closer look at the actual matching logic behind Roblox’s anti-cheat. Interestingly, while Roblox has removed VMProtect virtualization from the MatchMemory function, it is now protected using layers of encryption and complex arithmetic. This makes reverse engineering significantly more challenging. Although this limits how much we can fully recover, there is still a great deal we can understand about how the system works.

Initial setup and dispatch call to the YARA matching backend (yr_scanner_scan_mem).

Here, we see the function preparing its input and calling yr_scanner_scan_mem, the core routine responsible for matching the input buffer against YARA rulesets. A derived hash key is passed to this function, used internally for identifying and dispatching the appropriate ruleset handlers. We’ll explore that logic in the next section.

Loops through YARA matches and appends obfuscated results to the match vector.

This loop iterates through each ruleset returned by the yr_scanner_scan_mem function, adding matched rules to a vector. Each MatchedRule entry contains two 64-bit values: the ruleset ID and a second value with metadata about the match. The rest of the logic involves heavily complex math, so we’ll skip ahead to where the match status is finalized.

Decrypts and assigns the highest match status based on obfuscated rule conditions.

If a complex condition is satisfied, a code is decrypted to determine the match status. The function keeps track of the highest severity level seen across all matched rules and updates the final status accordingly. The flags set here aren’t relevant to our analysis, so we’ll skip their meaning.

Let’s revisit the yr_scanner_scan_mem routine we saw earlier. This function is responsible for matching the input buffer to rulesets by dispatching them to the appropriate handlers. We can see that behavior in the code below.

Dispatches matched rulesets to their corresponding handler functions via a hashed jump table.

Each ruleset has an associated match handler that determines the next step in the matching logic. These handlers are stored in a hashmap and initialized by the subroutine sub_2B4B690. YARA uses this layered design to support ruleset dependencies—where one rule can invoke another during the matching process.

Unfortunately, because of this hashmap, it’s hard to tell what the engine is actually matching against (e.g., specific byte signatures or strings). If you’re curious, you can analyze the handler initialization subroutine to reverse how the hashmap is populated.

The Ruleset Encoding

Earlier, we saw RBX::Scanner::EncodeRulesets being used to convert ruleset IDs into human-readable strings with an added layer of encoding for security. Let’s briefly examine how this function works.

Initializes random values used for obfuscating ruleset strings in the output buffer.

At the start, we see some setup logic followed by the generation of two random values: randByte1 and randByte2. These values are used later to obfuscate the ruleset strings. The initial four bytes of the output buffer v9 store these random values, which form the basis of the encoding scheme applied further down in the function.

Extracts each matched ruleset's string using GetRulesetString.

This snippet shows the start of the ruleset string decoding loop. Each matched ruleset is processed, and its associated string is retrieved using GetRulesetString. Roblox maintains 64 custom YARA rulesets in total, many specifically crafted to detect well-known Roblox cheats.

I brute-forced all possible ruleset IDs through GetRulesetString and dumped the full list below. The position of each string in the JSON array corresponds directly to its ruleset ID—for example, index 0 maps to ID 0, index 1 to ID 1, and so on.

[
  "found_suspicious_libs",
  "mod_has_common_section_names",
  "p-vmp",
  "lib-font",
  "m-normal",
  "ma-hidden",
  "s-urlrepo",
  "m-tfuture",
  "m-pe",
  "e-electron",
  "m-hpe",
  "l-dec",
  "s-rbxkey",
  "ma-certsubj",
  "r-key",
  "s-apivm",
  "eval_legitimate",
  "f-lua",
  "static-imgui",
  "m-told",
  "ma-unsafepath",
  "eval_exploit",
  "e-awp",
  "lib-syxsdk",
  "e-celestial",
  "f-pck",
  "eval_suspicious",
  "e-wave",
  "e-krampus",
  "l-jit",
  "l-func",
  "s-urlsocial",
  "ma-ename",
  "e-sirhurt",
  "lib-lua",
  "m-trecent",
  "e-synapse",
  "eval_possible_exploit",
  "r-url",
  "m-mspe",
  "p-vmpbin",
  "l-key",
  "lib-curl",
  "s-urlexploits",
  "s-hmask",
  "w-hyp",
  "eval_neutral",
  "s-ename",
  "mod_has_dosstub_msg",
  "e-celery",
  "mod_has_optional_header_magic64",
  "e-vandal",
  "p-thembin",
  "ma-certissuer",
  "lib-imgui",
  "f-rbx",
  "ma-spath",
  "lib-kiero",
  "p-themida",
  "s-pipe",
  "l-ver",
  "s-estrings",
  "mod_has_optional_header_magic32",
  "m-notpe"
]

Full list of extracted ruleset strings used by Roblox’s YARA engine.

s you might have noticed, the naming is fairly intuitive—each prefix indicates the category of the ruleset (e.g., e- for exploits, s- for strings, lib- for libraries, etc.).

Now lets take a quick look at the ruleset string encoding we have been mentioning this whole time. Looking past the last screenshot we see the following.

Encrypts the ruleset string using XOR with two random bytes before storing it in the output buffer.

As mentioned, v9 holds the output buffer, and the first value written is the length of the ruleset string. Each byte of the string is then XOR-encrypted using the two random values we saw earlier (randByte1 and randByte2). To recover the original string from a RBX::Scanner::Scan call, this encryption would need to be reversed.

Testing The Effectiveness of RBX::Scanner

I was curious—just how effective is Roblox’s custom YARA-based engine? To find out, I created rrlog, a tool that logs ruleset matches during memory scans. If you're interested in the implementation, you can find the project on my GitHub here. Below, I’ll give a quick summary of how rrlog works and what it revealed.

Recreating The Scanning Logic

I had two main options. I could take the more complex and accurate route of intercepting ClientQoSItem packets at the network level by hooking into Roblox’s RakNet interface. Alternatively, I could replicate the scanning logic we analyzed earlier and call the RBX::Scanner:: functions directly.

In the end, I chose the second approach to save time. The ClientQoSItem packets are heavily encrypted, and I did not want this to turn into a weeklong project just to decode them.

Just like Hyperion, rrlog scans for executable memory regions that are larger than 10 pages in size (40 KB). When such a region is found, its AllocationBase is extracted and the entire memory allocation is recovered. This ensures that RBX::Scanner receives the full context of the allocation, including the PE header, data sections, and other relevant information.

The RBX::Scanner::MatchMemory function is called directly on the allocation, and the matched ruleset IDs are converted to their string representations using RBX::Scanner::GetRulesetString. I chose to call these functions directly to avoid dealing with additional encryption. The scan results are saved in a human-readable .json file.

Reviewing Our Findings

⚠️
Disclamer: Before we continue, please note that I do not condone cheating in any form. The exploits discussed here were executed by third parties in a controlled environment under professional supervision. This analysis is intended solely for educational and research purposes.

We ran rrlog with ten of the most common internal Roblox exploits injected into the game. The results were not surprising. The YARA engine flagged 90 percent of them as either malicious or suspicious, which triggered the creation and transmission of a ClientQoSItem packet back to the server. Unless these exploits are able to intercept and spoof outgoing detection packets, anyone using them is likely to be banned in an upcoming wave.

I have released all rrlog samples used in this analysis on the GitHub repository here. Feel free to take a look yourself.

Possible Evasion Techniques

At its core, this is a powerful and capable signature scanner. Roblox only logs memory that appears suspicious or contains an invalid certificate. If the scanner classifies an exploit's memory as SCAN_NEUTRAL, it will not be logged.

This leaves exploit developers with only two practical options for evasion. The first is to obfuscate strings and randomize code structure as much as possible to break known patterns. The second is to install a network hook, intercept outgoing ClientQoSItem packets, decrypt them, and send spoofed responses to avoid detection.

Conclusion

This analysis reveals that Roblox's YARA-based scanning system is highly effective at identifying unauthorized modifications in memory. In testing, it correctly flagged the majority of known internal exploits, making it a reliable first layer of defense. While the approach is not particularly subtle, its speed and accuracy make it well suited for large-scale detection.

In future posts, we will examine how Roblox handles detections after they are made, how data is transmitted to the server, and what further protections are in place beneath this top layer.

Read more