Getting Started Welcome Setting Up Game Tree GuiRoot Method
Writing a Memory Library Reading and Writing Page Iterator Pattern Scanning Buffer Strategies
Writing a Roblox Library C++ String

Game Tree

In this guide, we will be making a simple explorer in terminal

Alright now that we got our project set up, let's make a simple terminal explorer to see the game tree.

I will be using my Roblox and Memory libraries, I suggest you make your own.

First let's get a handle to a Roblox process:

const std = @import("std");
const Roblox = @import("Roblox");

pub fn main(init: std.process.Init) !void {
    // Open First Roblox Process
    const robloxProcess = try Roblox.Process.first();
    // Close once done
    defer robloxProcess.deinit();
    // Print Roblox process
    var stdout_buffer: [4096]u8 = undefined;
    var stdout_writer = std.Io.File.stdout().writer(init.io, &stdout_buffer);
    const stdout = &stdout_writer.interface;
    try stdout.print("{s}{X}{c}", .{ "Roblox process handle is: ", @intFromPtr(robloxProcess.getProcessHandle()), '\n' });
    try stdout.flush();
    // Read terminal input so it doesn't exit right away
    var tmp: [1]u8 = undefined;
    var reader = std.Io.File.stdin().reader(init.io, &.{});
    _ = try reader.interface.readSliceShort(&tmp);
}

Let's run Roblox and test it out!

Roblox process handle is: A8

It works! However, there is a problem.

Roblox is able to detect this and flag your client and you might get BANNED!

Roblox is able to do this by scanning for handles to the Roblox process in every running processes.

So what can we do? Just make it so Roblox can't by making it so that Roblox does not have permission to scan for handles in our process.

We can achieve this my running our process as the SYSTEM account. I will use Win2Sys for this.

...
pub fn main() !void {
    // Elevate to SYSTEM account
    if(Win2Sys.elevate() != .Success)
        return;
    // Open First Roblox Process
...

Once we obtained the process handle we can get our DataModel.

However, we must be careful as Roblox can detect Reads and Writes to itself using paging

What is paging?

Computers nowadays use virtual memory, they are divided into blocks called pages (around 4096 in bytes)

When pages aren't used, your OS may page out such pages to reduce memory usage, either by compressing it or put it on disk.

When programs try to access these pages, it will case a PAGE FAULT which then causes the OS to put these pages back in to the main memory

Roblox can utilize this by telling to OS to page out memory pages and explicitly telling the OS to page in when they need to use it.

Roblox can then detect memory regions that are paged in but they aren't supposed to be yet due to our exploit reading/writing to such memory pages and flag us!!!

Counter measures

The solution sounds simple, you could just tell the OS to page out the page again after using them however this solution has a problem: race coditions, that is what happens if you can't page out in time and Roblox detected it before you do so.

The solution? Just freeze Roblox so it can't, then page everything out and then resume!

Roblox can flag you if the pages are paged in when they aren't supposed to but they cannot do the same with paged out pages since the OS may do it as well!

Let's implement the solution above:

...
    // Bypass paging check
    // Freeze Roblox
    try robloxProcess.freeze();
    defer {
        // Page everything out
        robloxProcess.emptyWorkingSet() catch {};
        // Resume Roblox
        robloxProcess.thaw() catch {};
    }
    // TODO: Get datamodel
...

Now let's get datamodel! There are many ways to do so. I will use my library's Top Down method first and I'll show you the GuiRoot method

I'm gonna load config.hscl for offsets, here is the full code:

const std = @import("std");
const Roblox = @import("Roblox");
const Win2Sys = @import("Win2Sys");

pub fn main(init: std.process.Init) !void {
    // Elevate to SYSTEM account
    if(Win2Sys.elevate() != .Success)
        return;
    // Get allocator
    const allocator = init.gpa;
    // Load config.hscl
    try Roblox.DefaultConfig.Loader.loadFileMapped(allocator, "config.hscl");
    // deinit config once done
    defer Roblox.Config.deinit(allocator);
    // Open First Roblox Process
    const robloxProcess = try Roblox.Process.first();
    // Close once done
    defer robloxProcess.deinit();
    // Print Roblox process
    var stdout_buffer: [4096]u8 = undefined;
    var stdout_writer = std.Io.File.stdout().writer(init.io, &stdout_buffer);
    const stdout = &stdout_writer.interface;
    try stdout.print("{s}{X}{c}", .{ "Roblox process handle is: ", @intFromPtr(robloxProcess.getProcessHandle()), '\n' });
    // Bypass paging check
    // Freeze Roblox
    try robloxProcess.freeze();
    defer {
        // Page everything out
        robloxProcess.emptyWorkingSet() catch {};
        // Resume Roblox
        robloxProcess.thaw() catch {};
    }
    // Get memory accessor
    const memory_accessor = robloxProcess.getMemoryAccessor();
    // Get datamodel
    const datamodel = try Roblox.DataModelLocators.TopDown.locate(allocator, memory_accessor, {}, struct {
        pub fn filter(_: anytype, _: Roblox.Instance.View) bool {
            return true;
        }
    }.filter);
    // Print datamodel address
    try stdout.print("{s}{X}{c}", .{ "Datamodel is: ", datamodel.getPtr(), '\n' });
    // Get datamodel name
    var name_buffer: [64]u8 = undefined;
    const name = try datamodel.getName(memory_accessor, &name_buffer);
    // print datamodel name
    try stdout.print("{s}{s}{c}", .{ "Datamodel name is: ", name, '\n' });
    // flush stdout
    try stdout.flush();
    // Read terminal input so it doesn't exit right away
    var tmp: [1]u8 = undefined;
    var reader = std.Io.File.stdin().reader(init.io, &.{});
    _ = try reader.interface.readSliceShort(&tmp);
}

We got:

Roblox process handle is: 1BC
Datamodel is: 1E280A83E38
Datamodel name is: Ugc

Now that we got datamodel, let's explore the game tree!

Let's create a recursive function to print out the entire tree of a Roblox game:

pub fn printTree(allocator: std.mem.Allocator, memory_accessor: anytype, stdout: *std.Io.Writer, instance: Roblox.Instance.View, depth: u32) !void {
    var name_buffer: [256]u8 = undefined;
    const name = try instance.getName(memory_accessor, &name_buffer);
    try stdout.splatByteAll(' ', depth);
    try stdout.print("{s}{s}{c}", .{ "[+] ", name, '\n' });
    const children = try instance.getChildrenAlloc(allocator, memory_accessor) orelse return;
    defer allocator.free(children);
    for(children) |child|
        try printTree(allocator, memory_accessor, stdout, child.get(), depth + 1);
}

Don't forget to use the function:

...
    try stdout.print("{s}{s}{c}", .{ "Datamodel name is: ", name, '\n' });
    // print tree
    try printTree(allocator, memory_accessor, stdout, datamodel, 0);
    // flush stdout
...

The output is very large here's part of it:

         [+] CapturePromptItem
         [+] CaptureServicePrompts
         [+] PromptConnections
         [+] SaveCapturesPrompt
         [+] ShareCapturePrompt
        [+] CapturesGallery
         [+] CapturesGallery
         [+] GalleryActionContainer
         [+] GalleryGridView
         [+] GalleryItem
        [+] Common
         [+] DeleteWarningModal
         [+] MenuButton
         [+] PermissionModal
         [+] ShowMoreButton
         [+] ToastContainer
        [+] Composer
         [+] ComposerBottomOverlay
         [+] ComposerScreen
         [+] ComposerTextbox
        [+] CoreUI
         [+] CaptureConfigCTAs
         [+] CaptureControlsView
         [+] CaptureOverlay
         [+] CapturesCoreUI
         [+] CapturesPage
         [+] EntrypointButton
         [+] OnOffButton
         [+] RadialProgressBar
         [+] VideoCaptureControl
         [+] VideoCaptureCTA
         [+] VoiceChatIndicator
       [+] Constants
       [+] Context
        [+] CapturesContext
       [+] Enums
        [+] CapturesToastType
        [+] CaptureTriggerType
        [+] CaptureType
        [+] EntrypointType