Analyzing Oaklands Anticheat: Part 1

It has come to my attention that the Roblox game Oaklands has very few, if any, publicly available scripts. This is surprising, as I was able to bypass it without much difficulty. However, let’s continue.

Oaklands has their own wrapper around Remote Events/Functions, which means simply spying on remotes to see which triggers chop events will not cut it;

Additionally, starting up Remote Spy or Infinite Yield will instantly detect you.

Before starting on the remote stuff, it would be nice to figure out why infinite yield is being detected, and it was easy enough to find this.

I have written a simple script that prevents scripts from being moved to nil, and surely enough, we can see that we have some “Animate” scripts being blocked.

Now when we see scripts being moved to nil on the game startup, it’s an immediate red flag because game developers do not move any other scripts to nil unless they are meant to be hidden.

Continuing, I will be using Oracle Decompiler to see the decompiled output because it is simply the best one.

If we look at the script “Animate5”, we can immediately see why infinite yield was being detected; they are using PreloadAsync from the ContentProvider service to list loaded assets in CoreGui.

local l_GuiService_0 = game:GetService("GuiService");
local l_ContentProvider_0 = game:GetService("ContentProvider");
local l_PreloadAsync_0 = l_ContentProvider_0.PreloadAsync;
local v13 = {
    game.CoreGui
};
local v14 = 236236326;
local _ = game:GetService("RunService").Heartbeat:Connect(function(v15)
    v14 = v14 + v15;
    if v14 > 30 then
        v14 = 0;
        if l_GuiService_0.MenuIsOpen then
            return;
        else
            local l_status_0, l_result_0 = pcall(function()
                l_PreloadAsync_0(l_ContentProvider_0, v13, v9);
            end);
            if not l_status_0 and string.find(l_result_0, "oldfuncteehee") then
                v1:FireServer({
                    val = 1255
                });
                if l_HAX_0 then
                    l_HAX_0:FireServer(1255);
                end;
            end;
        end;
    end;
end);

Here we can see the code responsible for detecting Infinite Yield and other CoreGui UIs that shouldn’t be there.

Here is a snippet of the code with edited variables.

local l_GuiService_0 = game:GetService("GuiService");
local l_ContentProvider_0 = game:GetService("ContentProvider");
local l_PreloadAsync_0 = l_ContentProvider_0.PreloadAsync;
local tableToCheck = {
    game.CoreGui
};
local secondsAfterLastCheck = 236236326;
local _ = game:GetService("RunService").Heartbeat:Connect(function(delta)
    secondsAfterLastCheck = secondsAfterLastCheck + delta;
    if secondsAfterLastCheck > 30 then
        secondsAfterLastCheck = 0;
        if l_GuiService_0.MenuIsOpen then
            return;
        else
            local l_status_0, l_result_0 = pcall(function()
                l_PreloadAsync_0(l_ContentProvider_0, tableToCheck, preloadAsyncCallback);
            end);
            if not l_status_0 and string.find(l_result_0, "oldfuncteehee") then
                v1:FireServer({
                    val = 1255
                });
                if l_HAX_0 then
                    l_HAX_0:FireServer(1255);
                end;
            end;
        end;
    end;
end);

Every heartbeat, the amount of seconds that has passed since the last heartbeat (so delta) is added to the variable “secondsAfterLastCheck”, and if that variable is larger than 30, the checks run and the variable is reset.

This effectively means the CoreGui check is run every 30 seconds.

In the check itself, they are using GuiService to check if the escape menu is open, and if it is, the CoreGui check will not run. This might be a possible attempt to confuse exploiters that run Infinite Yield and do not close the menu; however, I don’t think it is effective.

Now the check calls PreloadAsync in pcall; this is possibly to prevent exploiters from hooking PreloadAsync and making the function wait indefinitely. After PreloadAsync is called, it checks if it was successfully executed, and if not and the error message contains the string “oldfuncteehee”, a detection is sent to the server, and you are likely banned.

Now this itself doesn’t detect Infinite Yield being present; the real detection is being done in the callback, called here “preloadAsyncCallback”, and this is how the callback looks.

local function preloadAsyncCallback(assetId)
    if assetId then
        local numberAssetId = tonumber(string.match(assetId, "(%d+)")) or 0;
        if table.find(badIDs, numberAssetId) then
            task.spawn(function()
                task.wait(1);
                for _, v8 in pairs(game:GetService("ReplicatedStorage"):GetDescendants()) do
                    if v8:IsA("RemoteEvent") and v8.Name ~= "HAX" then
                        v8:FireServer({
                            val = true
                        });
                    end;
                end;
            end);
            v1:FireServer({
                val = 81
            });
            if l_HAX_0 then
                l_HAX_0:FireServer(81);
            end;
            game.ReplicatedStorage.HAX:FireServer(8);
            return;
        elseif assetId:find("rbxassetid://") then

        end;
    end;
end;

Now how PreloadAsync works: every loaded assetId that is present, the callback is called with that Id; however, the assetId contains the “rbxassetid://”, so the detection strips that from it and turns it only into the number id.

Once done, the ID is simply checked against the badIDs table, and if found, a detection is sent to the server, and you are banned.

There are two simple ways to prevent being detected; the first one would be hooking the game’s __index metamethod to make MenuIsOpen in GuiService always return true and stop the check from even running.

Now that might be easily detectable in the future due to the fact we will be hooking metamethods, and game developers can easily detect it by purposefully erroring on the __index metamethod and checking the call stack for any functions that shouldn’t be there.

However, an easier way is simply hooking the PreloadAsync function itself since they aren’t using name calls for that, so to successfully open Infinite Yield without being instantly banned, you can simply hook the PreloadAsync function like this:

hookfunction(game:GetService("ContentProvider").PreloadAsync, function(...)
    return nil
end)

Now it may be noted that if the developer of the game sees this post, they might change and add detections to make my posts useless; however, if you know how to bypass ACs on your own, that should be no problem for you.

So let’s test it; after executing the hook, we are no longer being banned for opening up Infinite Yield.

So today, we have managed to bypass the CoreGui Detection, but we are only getting started, as our goal is to automatically chop wood, so stay tuned for Part 2!