Game Tree
In this guide, we will be making a simple explorer in terminal
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
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!!!
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