TyblitZ.exe
Bored in the college lab, losing a typing game to my friend, so I opened Cheat Engine. That spiraled into me sitting on top of a global leaderboard with -1001.3% accuracy. Here's the whole beginner friendly rabbit hole.
Okay so this one started because I was bored and losing.
Me and my friend Rajveer were in the comps lab at college, tasks done, nothing to do, just burning time. We remembered this game our senior Anmol Rai (he ships stuff under Raian Games) had shown us a while back, a little typing game called TyblitZ. It had also been floating around some of the college group chats. So I did a bit of digging, found it, and we downloaded it.
And honestly? The game is sick. It’s this tight little typing survival loop where words fly at you, you’ve got a score counter and a streak multiplier that climbs every time you nail a word, and it just keeps ramping. Zero intro, zero handholding, it throws you straight into Level 1 and lets you cook. Rajveer and I immediately started going head to head, and he kept beating me.
So I did the most reasonable thing a person can do when they’re losing to the guy sitting next to them. I opened Cheat Engine and started messing with the game’s memory, lmao.
That petty little move turned into a whole afternoon of me pulling the game’s leaderboard completely apart, and by the end I was sitting at rank 1 with a -1001.3% accuracy, which, spoiler, is a number the game is physically incapable of producing. This post is me walking you through every step of how I got there, beginner friendly, because it’s genuinely one of the cleanest little starter hacks I’ve ever done. Anmol knows, I had his blessing, and I cleaned up after myself. Let’s go.
Quick heads up: I did this on my own machine, on my senior’s game, with his permission, and I deleted my junk entry and told him everything before writing this. I’ve also masked the live database key and project address throughout. This is a “here’s how it works and how to fix it” story, not a “go grief a live game” guide.
The game itself
First thing I noticed: TyblitZ is a Unity game, and it ships for Linux, Mac, and Windows. I was on Windows.
You launch it, it drops you into Level 1 with no menu, no tutorial. There’s a score in the top left, a streak multiplier (that little x1, x3, x5 that goes up as you chain correct words) in the bottom left, and words scrolling in that you have to type. Simple, clean, addictive.
Score up top, streak multiplier bottom left. no intro, just vibes
After you finish Level 1, it asks you to pick a username.
ALSO, before anyone comes at me: yes, Ik this is super basic. if this post is old news to you, pls ignore it; you ARE NOT the target audience.
Round one: Cheat Engine and the score in memory
Here’s the core idea behind Cheat Engine, explained like you’ve never touched it. Your score isn’t magic. While the game runs, that number is sitting somewhere in your computer’s memory (RAM) as plain data. You don’t know where, but you can find it by playing a guessing game with the tool.
The workflow is called First Scan / Next Scan:
- You attach Cheat Engine to the game’s process. In the process list it showed up as
TyblitZ.exe. - You tell it your current score (say,
2) and hit First Scan. It searches all of the game’s memory for the number2and comes back with… a ton of matches. Mine had 322,668. Way too many, because the number 2 is everywhere in memory. - You go play, your score changes to
7, and you hit Next Scan for7. Now it only keeps the addresses that used to be 2 and are now 7. That cut me from 322k down to 211. - Repeat a couple more times and you narrow it to a tiny handful. I ended on 2 addresses both holding my score (probably because two different bits of the game code read the score, like the on screen counter and the thing that eventually submits it).
Attaching Cheat Engine to the TyblitZ process
First Scan for my current score. value type 4 Bytes, and 322,668 hits to narrow down
A few rounds of Next Scan later, down to the two addresses holding my score
Once you’ve got the address, you just double click it and type a new number. The score is stored as a 4 byte integer, which caps out around 2.1 billion, and the game’s input let me go up to 9 digits. So I set my score to 613613613, then 999999999, just to floor it.
The edited value showing up live in the game
For fun I poked around and found I could do the same to the streak multiplier and even the level number (though messing with the level kinda broke the game visually, the UI didn’t like it). Rajveer looked over at my screen, saw a 900 million score, and went “how?? wtf how did you do that.” Which is exactly when it clicked that this would make a great little writeup, because it’s a friendly starting point for anyone getting into this stuff. I showed him how too, obviously.
This is lesson zero of game hacking: anything the game stores in memory on your machine, you own. The game trusting that number to stay honest is the whole weakness. This works because the score lives on your side, where you’re in charge.
there’s an online leaderboard?!
At this point I assumed the game was fully offline. Then I found the global leaderboard, and things got interesting.
The real leaderboard. that’s Rajveer sitting at a legit 4387
So the leaderboard has three columns: Name, Score, Accuracy. And I figured, cool, my Cheat Engine’d score would just get pushed up here the next time I set a high score. Easy rank 1.
But that accuracy column bugged me. Accuracy is obviously some calculation the game does, and it almost certainly works like this:
1
accuracy = (correct words / total words typed) * 100
Which means by its very nature it can only ever land somewhere between 0 and 100. You can’t get more words right than you typed. So even if I Cheat Engine’d the pieces of that formula in memory, the math would probably still clamp me into that 0 to 100 range through normal play. I didn’t just want a high score. I wanted an impossible one. A number that could not exist if you played honestly. That would be the real proof that nothing was checking my work.
To do that, I needed to stop thinking about the game’s memory and start thinking about where that score actually goes when it leaves my computer.
Something is talking to a server
A global leaderboard means my score has to travel over the internet to some database, and everyone else’s scores travel back to me. That happens through a web request (a little message your app sends to a server saying “here’s some data” or “give me some data”). If I could see that message, I could learn exactly how the game talks to its backend, and maybe talk to it myself.
To watch the game’s traffic I used Fiddler Classic, a free tool that acts as an intercepting proxy; kinda like Burp Suite.
Normally when the game talks to a server, the conversation is wrapped in HTTPS encryption so nobody in the middle can read it. Fiddler plants itself in the middle, installs a certificate your machine trusts, and basically pretends to be the server to your game and pretends to be your game to the server. So it decrypts the message, shows you everything in plaintext, then encrypts it again and forwards it on. Neither side notices. Modern Unity automatically routes through the Windows system proxy, so Fiddler catches its traffic with no extra setup.
A passive tool like Wireshark can’t do this: it can only hear that a call happened, not what was said, because it never pretends to be the other side to decrypt it.
no new high score, no request
I fired up Fiddler, opened the game, and… barely anything leaderboard related showed up when I played.
Here’s why, and it’s a great little puzzle. The game only bothers sending your score to the server when you actually beat your own previous high score. No new record, no write request. And I’d already Cheat Engine’d my score to 999999999. There is no beating 999999999. So the game looked at my run, went “eh, not a new high score, nothing to submit,” and stayed quiet. My own cheating had locked me out of seeing the very request I wanted to capture.
I needed the game to treat me like a brand new player with no high score at all, so that my next run would definitely be a record and would fire off a fresh submit request.
registry rabbit hole
My first instinct was to just reinstall the game. Didn’t work. It still knew who I was, still had my inflated high score. So where the heck is it storing my identity if not in the game folder?
Turns out Unity has a thing called PlayerPrefs for saving small bits of player data, and on Windows it writes those into the Windows Registry, not into a file next to the game. The registry is basically a giant system wide settings database that lives completely separate from any game folder. So reinstalling, redownloading, or moving the game never touches it. Your data just sits there, waiting.
I opened regedit and found the game’s little corner:
1
2
3
4
HKEY_CURRENT_USER\SOFTWARE\Raian Games\TyblitZ
PLAYER_NAME_h######## = my username
PLAYER_ID_h######## = a GUID that is my identity on the leaderboard
BEST_SCORE_1, BEST_ACCURACY_1, MAX_UNLOCKED_WAVE, ...
The game’s PlayerPrefs living in the registry. name, ID, best scores, all of it
That PLAYER_ID is the key one. It’s a GUID (a long unique id string) that ties this machine to its row on the leaderboard. Delete these values and the game forgets I ever existed. So I nuked the whole key.
Deleting the whole key. clean slate
1
reg delete "HKCU\SOFTWARE\Raian Games\TyblitZ" /f
Rebooted the game, and bingo, it prompted me for a username again like a fresh install. (Fun side effect: this is also the answer to “why is there no way to change your name in the game?” The game never deletes that identity itself, so it just sticks forever unless you go into the registry.)
Catching the request in the wild
Now, with a clean identity, I watched Fiddler again. Two useful things showed up.
First, every single time I opened the leaderboard screen, the game fired a GET request (a “please give me data” request) to read the board. This one always fires, no high score needed. And when I inspected its headers, the whole secret spilled out:
The GET request to read the leaderboard, with the key sitting right in the headers
Let me break down what we’re looking at, because the headers (the metadata attached to a web request) tell you everything:
1
2
3
4
5
GET /rest/v1/leaderboard_entries?select=*&order=score.desc,accuracy.desc,created_at.desc&limit=100 HTTP/1.1
Host: <project>.supabase.co
User-Agent: UnityPlayer/6000.4.7f1 (UnityWebRequest/1.0, libcurl/8.10.1-DEV)
apikey: sb_publishable_REDACTED
Authorization: Bearer sb_publishable_REDACTED
- The
Hostis a Supabase address. Supabase is a popular “backend in a box” for indie devs. It’s basically a hosted PostgreSQL database with a ready made web API stuck on the front, so you can get an online leaderboard going without writing your own server. - The path
/rest/v1/leaderboard_entriesis that API. Supabase uses a tool called PostgREST that turns a database table directly into web addresses. Soleaderboard_entriesis literally a table in the database (think of it like one sheet in a spreadsheet, where each player is a row and Score/Accuracy are columns), and PostgREST lets you read and write it with plain web requests. - The
apikeyandAuthorization: Bearerheaders are the API key, the credential that says “I’m allowed to talk to this database.” It’s sitting right there in plaintext, shipped inside the game to every single player.
And here’s the thing that makes this the whole ballgame: there’s no server of Anmol’s own in the middle. The game talks straight to the database. Which means whatever the game is allowed to do to that leaderboard, anyone holding this key can do too. And Fiddler had just handed me the key.
flowchart LR
subgraph anyone["Anyone holding the shipped key"]
G["GET (read)"]
P["POST (create)"]
U["PATCH (update)"]
D["DELETE (wipe)"]
end
anyone -- "apikey + Bearer token<br/>captured straight off Fiddler" --> API["Supabase PostgREST<br/>/rest/v1/leaderboard_entries"]
API --> DB[("PostgreSQL<br/>leaderboard_entries table")]
That empty space in the middle where the developer’s own server should be? That’s the vulnerability.
Then I beat my (now reset) high score on purpose, and sure enough, the POST request fired, the one that actually creates my leaderboard entry:
The POST that writes a new row. 201 Created, and the whole body in plaintext
The body of that request, the actual data being sent, was just a chunk of JSON (a simple text format for structured data):
1
{"player_id":"2d85ac19-6acc-44b8-81f9-33c9a95fab1d","player_name":"him","score":1000000020,"accuracy":100.0,"game_version":"1.0"}
Look at that. The score and the accuracy are just… values in the message. The game computed them on my machine (where I’m in charge) and shipped them off, and the server took accuracy: 100.0 at face value. Nothing on the server side is recomputing anything. Whatever I put in that JSON is what lands on the board. (There was also a little POST /rest/v1/rpc/get_player_rank helper that fetches your rank, but the write is the interesting one.)
From the game to my own terminal
Now I didn’t need the game at all. I had the address, the key, and the exact shape of the request. So I rebuilt it as a curl command (curl is a command line tool for firing off web requests by hand) and started poking the database directly.
This is where CRUD comes in. It’s the four things you can do to data: Create, Read, Update, Delete, which map to the web verbs POST, GET, PATCH, DELETE. On a leaderboard, the public should really only be allowed to Read. Let’s see what I actually had.
My first attempt to update my row I got lazy and left off the filter, and PostgREST smacked me for it:
“UPDATE requires a WHERE clause”. a nice safety rail that stops you nuking every row at once
1
{"code":"21000","details":null,"hint":null,"message":"UPDATE requires a WHERE clause"}
That’s actually a built in safety feature: the database refuses to update everything unless you specifically say which rows. So I added the filter, targeting my own row by its player_id:
Targeting my own row with player_id=eq.<guid> and it just works
1
2
3
4
5
6
7
KEY="<publishable-key>" # redacted
BASE="https://<project>.supabase.co/rest/v1/leaderboard_entries" # redacted
curl -X PATCH "$BASE?player_id=eq.2d85ac19-6acc-44b8-81f9-33c9a95fab1d" \
-H "apikey: $KEY" -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" -H "Prefer: return=representation" \
-d '{"player_name":"him","score":1,"accuracy":100.0,"game_version":"1.0"}'
It worked. My row updated to a score of 1 and dropped down the board, purely from my terminal, no game running. Full control confirmed.
So then I went for the whole point of this exercise. I sent one more request, setting my score to 123456789 (sequential digits, because I think it’s funny) and my accuracy to -1001.3:
1
2
3
4
curl -X PATCH "$BASE?player_id=eq.2d85ac19-6acc-44b8-81f9-33c9a95fab1d" \
-H "apikey: $KEY" -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" -H "Prefer: return=representation" \
-d '{"player_name":"him","score":123456789,"accuracy":-1001.3,"game_version":"1.0"}'
Opened the leaderboard in the game, and there I was. Rank 1. Score 123456789. Accuracy -1001.3%.
Rank 1 with an accuracy the game literally cannot produce
-1001.3%
Remember how I said accuracy is correct / total * 100, so it can only ever be between 0 and 100? That’s the whole punchline. There is no way to legitimately produce a negative accuracy, or one over 100, by playing the game. The formula won’t do it.
So a value like -1001.3% isn’t just cheating a number up, it’s proof that the value never went through the game’s logic at all. It could only exist if someone wrote it straight to the database, bypassing every check the game does. An impossible value on the board is a fingerprint that says, loud and clear: nothing on the server is validating this data. One tiny database rule (“accuracy must be between 0 and 100”) would have rejected my request instantly. There wasn’t one.
My DB now gng
Since that key can do all four CRUD operations, here’s everything that’s actually on the table. The public should only ever get Read:
| Action | Verb | What it lets you do |
|---|---|---|
| Read | GET | Pull the whole board (fine, the game does this) |
| Update | PATCH | Set any score/accuracy/name to anything |
| Create | POST | Invent brand new players and stuff the board with fakes |
| Delete | DELETE | Erase rows, up to the entire leaderboard |
And the nastiest part isn’t editing my own row. Look at that filter again: ?player_id=eq.<id>. I choose the id. Nothing checks that I own the row I’m editing. So you could point it at someone else’s player_id and rewrite or delete their score. It’s not “edit your entry,” it’s “edit anybody’s entry.” Same deal with names: the game’s rules about valid usernames live in the game, so a direct request can set literally any name it wants.
This is exactly where I stopped and stayed on the right side of the line. I did not flood the board with fake rows, delete other people’s scores, or run anything destructive against the live database. That stuff isn’t a demo, it is an attack: it could cost Anmol money, wrecks the board for real players, and leaves a mess. Proving I could write one impossible row to my own entry makes the point with zero collateral. And when I was done, I deleted my junk entry.
Damage?
Let me be fair, because there’s genuinely good news here.
The key that leaked is a publishable key, not a secret admin key (Supabase calls the scary one the service_role key). A publishable key is actually designed to be public and handed out to clients. It’s supposed to be safely fenced in by database rules. So an attacker with this key can’t dump private user data or take over the whole project. The damage is limited to the leaderboard table.
The bad news is that the leaderboard table has basically no rules turned on, so that “safe to be public” key has full run of it. On top of the impossible value stuff, someone could flood it with millions of junk rows, which eats storage and bandwidth. On a free plan that just gets the project paused (a denial of service for real players), and on a paid plan it could run up a bill.
The fix
Every single thing above comes from one root problem: the server trusts the client completely. No login, no ownership check, no validation. Here’s the fix, in the order that matters, because step 1 alone stops the bleeding today:
- Turn on Row Level Security (RLS) on the leaderboard table and give the public role read only access. RLS is the per table bouncer inside the database that decides what the anonymous public is allowed to do. Right now it’s off, so the public key can do everything. Flip it on, allow Read, block Write.
- Add CHECK constraints as a cheap backstop:
accuracy BETWEEN 0 AND 100, a sane max onscore, a name length limit. This kills my -1001.3% row instantly. - Route writes through a server side function (a Supabase Edge Function) so the client never writes the table directly, and the server can validate and clamp everything on a machine the player doesn’t control.
- Tie each write to an identity so
player_idcan’t just be picked by whoever’s sending the request.
And the mindset that ties it all together:
You can’t stop memory editing on the client, that’s what my Cheat Engine run proved. Any number the game shows you, you can forge. So the fix is never “hide it better,” it’s move the trust to the server. Validate, check, and recompute on a machine the player can’t reach. That’s the real defense.
TL;DR
the whole ride: bored in a lab losing to my friend, opened Cheat Engine out of spite, and followed the thread all the way to sitting on top of a global leaderboard with an accuracy that can’t exist. Cheat Engine for the memory side, Fiddler to catch the request, a registry detour to reset my identity, and curl to talk straight to the database.
The takeaways, if you skimmed:
- Never trust the client. Anything computed on the player’s machine is a suggestion, not a fact.
- Nothing shipped is secret. Keys, addresses, formulas, they’re all sitting in the game or on the wire.
- A public key is only as safe as your database rules. The bug wasn’t “the key leaked,” it was “the key could write.”
- Impossible values are a gift. -1001.3% told me the whole write path was unchecked, at a single glance.
None of this makes TyblitZ a worse game, by the way. It’s a great game that shipped one extremely common backend mistake, the same one a hundred other indie leaderboards are shipping right this second. The fix is small and the game still slaps.
Huge respect to Anmol / Raian Games for building something this fun and being cool enough to let me tear into it and write the whole thing up. I told him everything before touching anything shared, and cleaned up my mess after. If you’re just getting into this stuff, this is honestly a perfect first playground: a real game, a real backend, and every step is beginner doable. Go play TyblitZ, and go (respectfully, with permission) break your friends’ stuff.