a deep dive into my HWID ban system

August 22, 2022 (3y ago)

0 views

this was a creation of a hwid (hardware id) ban system for a harry potter game on roblox. back when exploiting was the norm, i needed a solid plan to keep my game fair and fun.

the spark for this project came from a github repo, which was all about tracking users across accounts on roblox. the core idea? use os.clock() to get a unique time count since the cpu started, basically a fingerprint for each machine.

let's dive into the rongoData script, the heartbeat of our system. this script was like the central hub, managing our mongoDB database interactions. when a player joined, the serverData event would fire up, kicking our script into action.

Player Hook

serverData.Event:Connect(function(Player : Player)
-- [...]
-- this bit handled loading player data and checking their fingerprint
local userFingerprint = clientDetails.Fingerprint;
-- [...]
end)
serverData.Event:Connect(function(Player : Player)
-- [...]
-- this bit handled loading player data and checking their fingerprint
local userFingerprint = clientDetails.Fingerprint;
-- [...]
end)

this little snippet starts the whole process. when a player hops into the game, it fetches their unique fingerprint—a mix of the hardware id and other data points.

all the data points
  • CPU Start: tick() - os.clock()
  • Timezone: os.date("%Z")
  • Is Daylight Savings Time: os.date("*t").isdst
  • Has Accelerometer: UserInputService.AccelerometerEnabled
  • Has Touchscreen: UserInputService.TouchEnabled

next, we checked this fingerprint against our database. if the player had any linked accounts (ones with the same fingerprint), we updated the account lists for all those users. the kicker? changing your hardware id later wouldn't help; once an account was linked, it stays that way.

Document Fetch

local function getAllRelatedDocuments(userId : number, fingerprint : string?) : any
-- this function fetched all related documents based on the fingerprint
-- [...]
end
local function getAllRelatedDocuments(userId : number, fingerprint : string?) : any
-- this function fetched all related documents based on the fingerprint
-- [...]
end

Document Updation

local function updateAllRelatedDocuments(
userIds : { [number] : number },
shouldBeFlagged : boolean,
userFingerprint : string,
accountList : { [string] : string }
)
-- and this one updated all related documents if any changes were detected
-- [...]
end
local function updateAllRelatedDocuments(
userIds : { [number] : number },
shouldBeFlagged : boolean,
userFingerprint : string,
accountList : { [string] : string }
)
-- and this one updated all related documents if any changes were detected
-- [...]
end

Aggregation and Analysis

local allDocuments = collection:Aggregate(pipeline);
-- Processing the aggregated data to find patterns and links
local allDocuments = collection:Aggregate(pipeline);
-- Processing the aggregated data to find patterns and links

this bit aggregated all data relevant to a player's fingerprint. it was like putting together a puzzle, finding which accounts were connected.

Data Structure

the database was structured to store detailed information about each player, including their unique hardware fingerprint and any linked accounts. here's a simplified view:

  • userId: unique identifier for each player.
  • username: the player's roblox username.
  • fingerprint: unique hardware id, a blend of various hardware and system characteristics.
  • flagged: boolean indicating if the account is flagged (for exploiting, alt account, etc.).
  • accountList: a collection of linked accounts (identified through the fingerprint).

this structure allowed me to keep track of individual players and any potential alternate accounts they might be using.

Aggregation Pipeline

the aggregation pipeline was where the heavy lifting happened. it was designed to process and analyze player data to find linked accounts. here's a glimpse into the steps involved:

  1. Matching Stage: using the $match operator, we filtered documents based on the user's userId or fingerprint. this was the initial step to narrow down relevant documents.

  2. Projection Stage: with $project, we transformed the data to include the number of accounts linked (numAccounts), along with other relevant fields like flagged, username, and the accountList.

  3. Sorting Stage: next, $sort helped us order the documents by the number of linked accounts in descending order - the idea was to prioritize documents with more links.

  4. Limiting Stage: finally, $limit was used to restrict the number of documents processed, ensuring efficiency and focus on the most relevant data.

local pipeline : mongoPipeline = {
{ ["$match"] = {
["$or"] = {
{ ["UserId"] = userId };
{ ["Fingerprint"] = fingerprint or "NO_FINGERPRINT" };
};
} };
{ ["$project"] = {
["numAccounts"] = {
["$size"] = { ["$objectToArray"] = "$AccountList" }
};
-- other fields...
} };
{ ["$sort"] = {
["numAccounts"] = -1
} };
{ ["$limit"] = 50 };
};
local pipeline : mongoPipeline = {
{ ["$match"] = {
["$or"] = {
{ ["UserId"] = userId };
{ ["Fingerprint"] = fingerprint or "NO_FINGERPRINT" };
};
} };
{ ["$project"] = {
["numAccounts"] = {
["$size"] = { ["$objectToArray"] = "$AccountList" }
};
-- other fields...
} };
{ ["$sort"] = {
["numAccounts"] = -1
} };
{ ["$limit"] = 50 };
};

this pipeline was key to identifying and linking accounts. by processing data this way, we could efficiently flag exploiters and alt accounts and update linked accounts.

the journey of building this system was a rollercoaster.

Current State

fast forward to now, and this system seems like overkill. roblox's security game is strong, and exploiting isn't what it used to be. plus, with big players like synapse exiting the stage, the landscape's really changed.

if you want to see the current state of exploiting, take a look at this.