Bypassing Hyperion with Serenity

SOURCE CODE

For the following lesson we will be talking about the source code. You will be able to find the source code here.

Introduction

Hello everyone, today I will be showing you a Hyperion bypass I’ve created which powers my proof-of-concept cheat named Serenity. You may have previously heard of me in 2022 releasing Headhunter a cheat I open sourced and publically maintained for a few months, which helped many people learn how game hacking works. I’m sharing this with you so you can learn how it works, and learn more about computers. Do not use this to produce cheats. I am not responsible for what you do with this source. I hope everyone has a wonderful time learning what we have here, as you will not typically come across this kind of injection technique. This guide is designed for beginners who are willing to take a step out of their comfort zone, and learn more about their system. Although many of these topics aren’t beginner friendly, the idea itself for this cheat is. You may think it’s dumb for me to leak a Hyperion bypass but this was just something I made overnight while I was bored. To make a real bypass you will need to put much more time and development and consideration into your cheat, and I couldn’t care less if this gets patched. Sorry if this post is rushed as it is currently Thanksgiving and I plan to hang out with my family, so I am rushing through this (creating the cheat & writing the blog). Please note that the cheat doesn’t follow the best coding practices and simply is the way I code. It is in no way the best way to go about things, and I encourage you learn your own style. At the bottom of this thread I have posted resources to learn the various concepts we talk about. I highly recommend you read them if you are interested. I have also provided various links throughout the reading to describe the related topic.

Credits

All the work was created by me, (Fish-Sticks / Fishy / Birdy). You can contact me on Discord @ goatthegb. Please don’t ask me to fix your cheat. I’ll help you learn and reverse engineer, but I won’t fix your problems for you, and I may come off very rude but remember it’s nothing personal.

Credits to my communities for supporting me, and making learning computer science a great experience.

GDO a great server which has an upcoming forum, and is designed to get you from beginner at game hacking to expert, along with learning other fields of computer science. Highly recommend you join this one if you are looking for any help or want to talk with like minded people.

Background Knowledge

Background knowledge - Basics

Before we can start talking about how the bypass works please make sure you are familiar with assembly language. In this case we will be using amd64 assembly, and using FelixCloutier as a reference, as it goes into good detail and contains a psuedocode example of most instructions. We won’t be using many complicated assembly examples here, but if you don’t understand the fundamentals of stacks you may find yourself lost, as we create our own stack in one of the examples. Please make sure you are also comfortable with bitwise operations as I will be going slightly into some basic bitmath.

Background knowledge - Hyperion

Since we are creating a bypass for Hyperion we should also have some background on what Hyperion is. Hyperion is a user-mode anti-cheat developed by Byfron (a company under Roblox) which specializes in anti-tamper (blocking cheats from being injected / running), and the anti-cheat (issues bans and detects cheaters). We will be circumventing their anti-tamper techniques to get unsigned code execution inside the client. If you don’t understand an example, you can re-read until you understand it, or even better try debugging it. By debugging you can see exactly how the assembly code affects the registers, and these shellcode examples I’ve made I had to use a debugger for, which let me see any issues with the assembly code.

Background Knowledge - Executable Memory Scanner

One of the anti-tamper methods Hyperion has made which has stopped many cheaters is their executable memory scanner. It will scan over memory regions and check the page protections of this memory. If the memory is marked with an executable flag (PAGE_EXECUTE / PAGE_EXECUTE_READ / PAGE_EXECUTE_READWRITE, etc) then it goes onto it’s second check. It will check if you are whitelisted inside one of Hyperion’s maps. It encrypts the address of the virtual page number (VPN) by removing the offset of the page (shifting right 12). If you have trouble understanding how pages are stored for all you need to know is that let’s say we have page 0x7FF665DB3F94. Hyperion needs to track each whitelisted page, so instead of encrypting this specific address, it will instead drop the last 3 hex digits (which are offset into the current page), to grab the page base, being 0x7FF665DB3. It will take this VPN and XOR it by a constant which is generated and hardcoded for each build on compile time, so it is changing each update. By using a stupidly simple signature, you can actually find this encryption and easily whitelist your own pages but in this case we won’t even be doing that, we will be doing something very simple instead. Once Hyperion has found a memory allocation it will determine if this memory is allowed to be executable or not. This may be a game page, a DLL such as user32.dll, or your cheat. Hyperion will hash and look up the VPN of the executable page inside its whitelisted page map. If it sees that you aren’t in the whitelisted page map then it will remove executable permissions. PAGE_EXECUTE_READ will become PAGE_READONLY, PAGE_EXECUTE_READWRITE will become PAGE_READWRITE, etc. If Hyperion sees you inside the whitelisted page map, then it will continue on and not touch the page.

Background Knowledge - Thread Blocking

So we know how Hyperion determines if memory can be executable or not, but how does it detect threads? Hyperion detects thread injection attempts through various methods. For example if you try to use SetThreadContext Hyperion will crash your game. If you try to call CreateRemoteThread your thread will seemingly not run at all. Hyperion also has protection against threads with APCs in their queue. For blocking CreateRemoteThread, Hyperion has a special mechanism called the instrumentation callback which will catch all kernel to usermode transitions. You may ask how this is possible from usermode, however Windows implemented a secret technology that isn’t documented. By calling a couple NT API functions with the correct arguments you can register an instrumentation callback, so you too can intercept kernel -> usermode transitions. Hyperion uses this to detect when LdrInitializeThunk is being called (This is called on new initializing threads), and inserts it’s own hook to check the thread start IP. They go through a list of whitelisted thread start addresses, and if you are whitelisted then you are allowed to create your thread. However, we don’t plan on having to whitelist ourselves here either as the technique we will be describing in Serenity is much more clever.

Background Knowledge - Tamper Proof Pages

Although not as important as the other two checks, this is a very important check too. You may have tried to write data to the games code region, and noticed all your writes are failing. You may wonder how Hyperion has stopped you from writing to the game, after all they are blocking the kernel from writing the memory? Well, the answer is actually a bit more complicated. You will notice that these writes are blocked way too fast for it to be them simply replacing the data back, so what’s really going on? Hyperion has again abused Windows internally, just like they have with catching kernel -> usermode transitions. They mark their memory section as SEC_NO_CHANGE. This is a special flag which basically tells Windows no matter what, don’t let the protection of this page to change. You cannot undo this protection without directly manipulating the VAD, or unmapping the section. I highly suggest you read about the VAD, but for a simple explanation think about how Hyperion finds all those pages. They simply go over all the pages used by the game. These are stored in the VAD from when you call VirtualAlloc. It holds information about each page, such as the protections and region sizes. If you have heard of PML4 paging structures, the VAD is like a higher level of it for the Windows operating system to have a simpler and more abstracted interface. Unlike the paging structures which must be in memory if they are defined, the VAD can described paged out sections too. I won’t go too in depth about PxE structures or VAD but you can read about them with the provided links. All for now you need to know is that Hyperion has a way to prevent us from simply writing memory to the game, and overwriting the game’s code. They also have an integrity checker which hashes each page with BLAKE3 and checks for any tampering, which if it’s found then you will be crashed. One more idea I want you to know before we continue on is that Hyperion does have the ability to write to these pages, even though they are seemingly write proof. Hyperion creates a special view in another virtual address which points to the same memory. This second view has write permissions, unlike the original, but doesn’t have execute permissions. If you are new to learning about memory management you might be shocked that two virtual addresses can point to the same exact memory, and I was too at first but it’s actually all over if you look closer! For example, Windows shares the memory of most loaded DLLs between processes, as long as they don’t modify anything. If they modify a page then it invokes COW (copy on write) and Windows creates that specific page just for your process, but the rest of the processes share the original copy. This is a core concept in Windows which allows your computer to keep your memory nicely managed. To prevent this section from getting too lengthy we will have to continue, but let me know if you would like a post just on paging & memory in general.

The Bypass Theories

Theory A

Okay so we need to bypass both their thread creation to get our code to run, and we must bypass their memory protection to have our cheat code be able to have execute permissions. We don’t have to worry too much about their tamper protection, unless we plan on overwriting another already signed DLL. By simply whitelisting ourselves in the thread start check, and adding ourselves to the memory whitelist map we can bypass Hyperion right? Theoretically yes we can, and I have done it before, however it is a lot of effort to reverse engineer all that and have to update and maintain it. Let’s think of alternatives that have been talked about before.

Theory B

Many public internal sources right now have many different methods but one of the methods that was presented was preventing Hyperion from changing your protections. After all, they protected their code which can’t have its protections changed, so why can’t we? This was one of the early Hyperion bypass methods, and one of the first publically known ways to do it. You simply set your code to have SEC_NO_CHANGE and then Hyperion can’t revert your page protections, so no matter what you will have execute permissions! Although this method sounds decent, it’s still very much suboptimal. Let’s go over some oversights this method has, and think of another way. The first oversight is assuming that you are safe just because Hyperion hasn’t crashed you. Hyperion will simply see that it can’t set your page protections, and it will have a couple of options. It can either remove your memory, scan your memory and upload it to the game for being suspicious, silently flag you, or do nothing!

Theory C

Considering Hyperion tightly secures the game, what if we just go external? This is what many cheats have done once Hyperion was deployed. Although being external is an alright option, it presents many issues. One of these issues is synchronization. Since your thread isn’t running in queue with the game, like it would normally be on the task scheduler you face a high risk of falling out of sync with the game. This can create issues such as reading variables at the wrong times, writing them when they are already used, etc. Writing externals is suboptimal due to their external checks with Deleter2 if you have heard of it. If you haven’t then don’t worry about it, as I will not be going in depth on it as it is out of the scope of this post.

Theory D

Okay so we must be whitelisted by Hyperion. One of these cheats thought of another clever idea, which was to inject the DLL like a normal program would such as Discord. Discord injects its overlay into other processes, what if your cheat could do this too? While unfortunately the idea seems simple, there’s more than a challenge presented. Hyperion ensures the DLL cannot be loaded by checking the certificate of the file, to make sure it is a legitimate program by a company. Buying certificates costs lots of money, and is an easy footprint for you if your cheat is using one. One exploit decided on another idea, what if we could lie to Hyperion about being signed? This would certainly allow Hyperion to load our cheat for us. By hooking WinVerifyTrust you could spoof the result. While this method had lasted a while, it is patched now (Note: It was patched on a test channel but is currently working once again), and the Hyperion developers are well aware of it. Again please remember, having injection does NOT mean you are undetected. Hyperion anti-cheat is still very well aware of you, and you have only bypassed their anti-tamper.

Theory Behind Serenity

What if there was a way to be whitelisted without whitelisting ourselves? This would certainly help out, and speed up development times. Let’s think about this harder, and realize that we can actually patch signed DLLs! Hyperion has already whitelisted these DLLs, and then all we are left with is having to deal with injection, so this is an optimal idea for getting unsigned code running inside the game. For this bypass we have decided on win32u.dll since it has a nice chunk of space before its first function, which allows for perfect room for a nice shellcode injection. Compilers generate some blank space in between each function to ensure each function is properly aligned for optimal execution. You can abuse this to place shellcode in between. You may have seen this in your reverse engineering tool, by seeing INT3 (0xCC) in between functions. Now all we must deal with is injection. We will be hijacking threads in a special way! Since calling SetThreadContext will crash our games we must find another way to hijack threads. I didn’t want to hook any Hyperion functions since this would require updating, and also make this post longer, so I’ve instead developed another idea. All we need to do is query the game thread, we don’t technically need to set the context. You may wonder, but then how will we hijack the thread, or control the registers? This is where our assembly knowledge will come in. We must suspend a thread before we can retrieve its thread context. Since we have the thread suspended it must have somewhere to call to when we decide to unsuspend the thread. This is where our special injection method comes in, what if we simply swap the return value with our own. When we unsuspend the thread it will simply return to our desired code, and voila we won’t have to use SetThreadContext or create a new thread! While this idea is easy to explain, we still must understand how to write the assembly for it. This is where our assembly knowledge comes in. For those that don’t know, the stack grows DOWN. This means subtracting RSP will actually allocate stack space (you can see this when an assembly routine creates space for locals). Adding RSP will remove stack space. This concept is very critical so please make sure you understand it. Here’s another example, when you push rbp (to allocate a stack frame) here’s what’s going on under the hood.

IF StackAddrSize = 64
    THEN
        IF OperandSize = 64
            THEN
                RSP := RSP  8;
                Memory[SS:RSP] := SRC;
                    (* push quadword *)
        ELSE IF OperandSize = 32
            THEN
                RSP := RSP  4;
                Memory[SS:RSP] := SRC;
                    (* push dword *)
            ELSE (* OperandSize = 16 *)
                RSP := RSP  2;
                Memory[SS:RSP] := SRC;
                    (* push word *)
        FI;

If we look closely, pushing RBP (a 64 bit operand) will first subtract the stack, and then set the current pointer of RSP to RBP. This means the stack subtracts before it writes. If we wanted to replace the return value of the thread we suspend, we will simply write to RSP as we don’t want to push anything, but overwrite what’s at the top. Before we overwrite this return we will want to save it so we can have our shellcode return back to the real return value after, this way the thread can continue execution past our control.

// Retrieve old return value off stack (remember the thread is suspended so it has to have a return here)
std::uintptr_t oldReturnValue = 0;
ReadProcessMemory(robloxInfo.hRoblox, (PVOID)threadCtx.Rsp, &oldReturnValue, sizeof(oldReturnValue), nullptr);

// Replace return to our hook
WriteProcessMemory(robloxInfo.hRoblox, (PVOID)threadCtx.Rsp, &baseText, sizeof(baseText), nullptr);

baseText points to the beginning of our shellcode, so what this code is doing is basically telling the CPU that when it executes a ret instead of returning to the old value, we will return to ours. The CPU will read RSP when ret is executed, and see our value instead of the old value since we have written it. Since the RSP is stored in data, we don’t need to set any thread context or modify any registers to preform this sneaky injection. Now that we have control over execution of a thread, and signed memory we still have one more issue. How will our shellcode have all the necessary data to run, and properly return back in such a tight space? If you have seen in your preferred reverse engineering tool or debugger, you will notice there isn’t much space between functions. By using win32u.dll we have about 60 bytes of space before we hit the first function, which is plenty for tightly coded shellcode. This is where the magic of my shellcode comes in. I’ve created shellcode which can use 4 pointers (32 bytes of data), and still have an injection space of only 41. This means we are packing an extra 32 bytes that would otherwise be originally inside the shellcode, inside data by simply moving all our pointers over. For those who are confused I will present some examples. We must think about how assembly works. Assembly can have lots of pointers inside the code, or it can be moved into data, and use memory reads and writes. We will profit off this with a very efficient method, by combining our knowledge with the stack. If you remember, we cannot write registers since we don’t have SetThreadContext but once we are internal we can do anything we want to these registers. So the injector can simply set up one pointer, which holds ALL our data inside a format that the stack can read. This way we only take up 8 bytes of data in our shellcode, which allows for 90% code instead of putting pointers all over. So in other words, we construct our own pretend stack with all the data on it. This allows us to use pop which is a 1 byte instruction to read 8 bytes of data. Here is a more in depth example:

We could either have

     ASSEMBLY CODE          |       MACHINE CODE
movabs rax, 0xaabbccddeee11 | 48 b8 11 ee de cd bc ab 0a 00

Which takes up many bytes of data, and reduces the amount of shellcode logic we can have, or we could have a much smaller

ASSEMBLY CODE   |   MACHINE CODE
pop rax         |       58

Which takes up only a singular byte, and only requires the stack to be prepared right, which can be done externally. By utilizing this idea we save a TON of space.

I will now show the entire shellcode to you, and it is your job to carefully read it and understand it. Remember that the pointer value is a dummy value which is later filled in with the real value by the injector. Please also remember that the stack this dummy pointer holds is properly setup by the C++ code, which I will show here.

C++ code which sets up the virtual stack:

// We will store all the data our shellcode needs here in this custom stack space.
// This allows us to pack the code tighter by storing some of the information in data such as pointers
// On top of this if we use this storage as a stack we can turn an 8 byte moving a pointer into a register, into a 1 byte pop which saves a LOT of space.
void* VariableStorage = VirtualAllocEx(robloxInfo.hRoblox, 0, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

WriteProcessMemory(robloxInfo.hRoblox, VariableStorage, &oldReturnValue, sizeof(oldReturnValue), nullptr); // +0 = Return

WriteProcessMemory(robloxInfo.hRoblox, (PVOID)((std::uintptr_t)VariableStorage + 8), &myMessagePtr, sizeof(myMessagePtr), nullptr); // +8 = Message

WriteProcessMemory(robloxInfo.hRoblox, (PVOID)((std::uintptr_t)VariableStorage + 16), &myTitlePtr, sizeof(myTitlePtr), nullptr); // +16 = Title

WriteProcessMemory(robloxInfo.hRoblox, (PVOID)((std::uintptr_t)VariableStorage + 24), &MessageBoxAPtr, sizeof(MessageBoxAPtr), nullptr); // +24 = Func to call

// Since the stack GROWS down, popping the stack will actually raise the value of the stack pointer (which in this case reads the variable storage for us incrementally)
*(std::uintptr_t*)(&shellcode[4]) = (std::uintptr_t)VariableStorage;

WriteProcessMemory(robloxInfo.hRoblox, (PVOID)baseText, shellcode, sizeof(shellcode), nullptr); // Write the code payload for shellcode.

As you can see it will create the virtual stack space, fill the data inside it, and then replace the dummy pointer in the shellcode (shown below) with the real stack pointer. Then it writes it inside a legitimate module (win32u.dll) to have the executable code which is already whitelisted.

// X64 CALLING CONVENTION: rcx, rdx, r8, r9, stack (right to left)
// MESSAGEBOXA FUNCTION: rcx (HWND), rdx (message text), r8 (message title), r9 (icon and buttons flags)

Assembly shellcode which is injected into the game:

push r15 ; Preserve r15
mov r15, 0xAABBCCDDEEFF1122 ; Temporary hold stack
xchg r15, rsp ; Swap stack (WE WILL NOW BE USING OUR VIRTUAL STACK)
pop r14 ; Pop real return address
pop rdx ; Text argument
pop r8  ; Title argument
pop rax ; Function to call
xchg r15, rsp ; Restore stack (WE ARE BACK IN THE REAL STACK)
pop r15 ; Restore r15

xor rcx, rcx ; Clear RCX (HWND)
mov r9, 0x30 ; Warning icon
call rax ; Call MessageBoxA
jmp r14 ; Go back to original return value

You will notice that this snippet actually overwrites a couple registers. As long as the game doesn’t depend on these registers, we will be relatively fine, and if we stick to the volatile registers then we will be even more safe. By safe I mean safe from crashing due to bad assembly code, not because of Hyperion.

Upon combining our memory whitelist vulnerability with our thread hijack vulnerability, we have essentially bypassed Hyperion’s core anti-tamper goals. We now have code running inside Roblox which will trigger a message box, giving ourselves a nice success! Serenity showing a message box

Okay but what if we wanted to do something else, such as actually print inside Roblox and see our cheat in the developer console? Well since Serenity is internal we don’t have to worry about page decryption, as Hyperion will automatically decrypt any page we touch for us. Let’s change our shellcode up a bit, so instead of above, it is now this.

New shellcode for calling print inside Roblox:

mov rax, 0xAABBCCDDEEFF ; Print function
mov ecx, 1 ; Print format specifier
mov rdx, 0xAABBCCDDEEFF ; Print text
call rax ; Call print

mov rax, 0xAABBCCDDEEFF ; return back to real code
jmp rax

Unlike the other shellcode this one uses a lot more pointers and thus takes up much more space than it needed to. I’ll let you find out how to apply your own virtual stack method onto it to prevent taking up so much space, but it isn’t anything that will affect this shellcode since it’s small enough. Obviously as before, our injector will fill out these “template” values for our real values. The code is quite self explanatory so I won’t be doing too much explaining.

Serenity printing to console

If it isn’t clear enough, Serenity is now an internal and can do anything you want. This includes calling lua functions, serving as a base for hooking functions to spoof values, or even block APIs.

Conclusion

I hope you have enjoyed reading this, and hopefully have learned some new things or at least found it interesting. I don’t know if I will be uploading any more posts anytime soon as they require a lot of effort to make, but perhaps with some motivation I’d be willing to share more. I won’t be going in depth on how to further leverage Serenity to inject a whole DLL but this is enough information for you, the reader to do that! Experiment with Serenity all you want, and enjoy it until it gets patched. This is my Thanksgiving gift for you, consider it an early Christmas present! The entire Serenity source is available at the top of this post, just please don’t use it to actually cheat. Instead use it as an opportunity to learn more about how Windows or Hyperion works. Perhaps you may even come up with your own ideas and have your own secret cheat that works in a special way nobody knows about. Serenity isn’t my only cheat I’ve made that is unique, but I thought it was worth sharing since it is beginner friendly and doesn’t spill too many secrets. If you are confused about anything please read the resource links provided, as I have spent time gathering resources so you don’t have to!

Resources

Assembly:

Felix Cloutier - x86 and amd64 instruction reference

Intel - Intel® 64 and IA-32 Architectures Software Developer’s Manual Combined Volumes 2A, 2B, 2C, and 2D: Instruction Set Reference, A- Z

Stanford CS107 - Guide to x86-64

Memory:

Connor McGarr - Turning the Pages: Introduction to Memory Paging on Windows 10 x64

Justin Miller - Understanding x86_64 Paging

Fortra - Getting Physical: Extreme abuse of Intel based Paging Systems - Part 1

Microsoft - Virtual Memory Functions

TripleFault.io - Introduction to IA-32e hardware paging

TripleFault.io - Exploring Windows virtual memory management

Threads:

Bruno van Dooren - Understanding Windows Asynchronous Procedure Calls (APCs)

Pavel Yosifovich - What Can You Do with APCs?

Souhail Hammou - Windows Thread Suspension Internals Part 1

Tools:

Gogo1000 & Fishy - Static Decryptor for Roblox

Defuse Security - Online assembler & disassembler