How We Got Free Chicken for a Year by Reverse Engineering a Mobile Game
A few days ago, my friend Dere published a blog post about how we reverse-engineered a fast-food company's mobile game to win free food for an entire year. It's a great read, and I recommend checking it out. Since it's his side of the story, I figured I'd write mine. There are a few details I want to add, and some parts I experienced differently.
For legal reasons, we'll keep calling the company "ChickenDonald."
How it started
My friend had been grinding a minigame inside ChickenDonald's app. High scores could win you free food, and he wanted to automate his way to the top. His approach? Build a pixel-perfect clone in PyGame and train an AI on it. He spent 36 hours on that. It didn't work.
When he showed me the repo, my first thought was: this is way too much effort. The game looked simple enough that you could just mess with it directly.
"Why don't we just cheat the real game?"
That's when things got interesting.

Spotting the webapp
The first thing I did was open the app and start playing the game myself. Almost immediately, something caught my eye: I noticed a brief loading flash, the kind you see when a WebView renders a page. If you've done any mobile development, you know what I'm talking about.
This was a big deal. If the game was just a webpage being displayed inside the app, it meant the entire thing (logic, score submission, everything) was running in a browser context. And browser contexts are something you can inspect, intercept, and manipulate.
Finding the game's subdomain
Before even touching the app's traffic, I wanted to see what was publicly exposed. I went to digger.tools, a tool that enumerates subdomains for a given domain, and ran it against ChickenDonald's main domain.
Among the results, one subdomain stood out. I opened it in a browser and, sure enough, the game loaded. But it required authentication to actually play, and the login flow wasn't working outside the app.
So I needed to get a valid token.
Intercepting the traffic
I set up a proxy on my phone using an iOS app called WebProxyTool. It lets you intercept HTTP/HTTPS traffic from apps and inspect the requests they make.
I launched the ChickenDonald app, started the game, and watched the network log. I found the authentication flow, grabbed the bearer token from the response, and sent it to my friend along with the game's URL.
He pasted the token into Chrome's localStorage, refreshed the page, and we were in. The full game, running in a regular browser tab, fully authenticated.
Understanding the score system
Now that we had the game running in Chrome, the first thing we tried was the obvious: just fake a high score and send it to the server.
No luck. The final score submission came as an enormous encrypted blob (multiple megabytes) containing timestamps, a full action history of every tap and input, collected point events, damage events, and a cryptographic hash of the entire payload. Every field depended on other fields, so changing a single value would break the hash and get rejected.
At first glance, this seemed like a solid defense. But there was a critical flaw: all of those values were being generated entirely on the client side. The browser built the payload, the browser hashed it, and the browser sent it. There was no server-side validation of the actual gameplay. The server just checked that the payload was internally consistent.
This meant we didn't need to forge a score. We needed to modify the game itself so that a legitimate run would naturally produce a ridiculous score. The proof system would validate whatever reality the game client created.
De-obfuscating the code
The real game logic lived inside a massive, heavily obfuscated script: variable names replaced with gibberish, control flow flattened, the kind of code that's designed to be unreadable.
We ran it through a de-obfuscator, which turned it from completely unreadable into something you could at least work with. From there, we started searching for anything related to damage calculation, health variables, collision detection. Anything that controlled whether the player could survive.
Eventually, we found the damage handler. The logic looked something like this:
if (playerHasShield) {
// ignore damage
} else {
playerHealth -= someAmount;
}
The game already had a shield mechanic, a power-up that temporarily made you invincible. All we had to do was force the code to always take the shield branch. One patch, and the player could never take damage.
The immortality problem
There was a catch. If you couldn't die, you couldn't end the game. And if you couldn't end the game, you couldn't submit your score. Closing the tab would just discard everything.
The solution was simple. We added a new variable:
stopCheating = false;
During the run, the cheat stayed active and you were invincible. When you were ready to submit, you'd open the browser console and type:
stopCheating = true;
Damage turned back on, your character died, and the score submission triggered normally with a perfectly valid payload.
The Sentry incident
This is the part where we almost blew everything.
After our very first successful immortal run, we were watching the network logs out of habit. And then we saw it: an outgoing request to Sentry, a crash and error reporting service that developers use to monitor their applications in production. The game had silently sent an error report, and inside that report was a snippet of the modified JavaScript.
Including the variable name stopCheating.
The game had literally told ChickenDonald's developers that someone injected a variable called stopCheating into their code. It couldn't have been more obvious if we'd left a signed confession.
We immediately renamed it to enableDebug, which could plausibly look like a forgotten development flag rather than evidence of foul play. We also deleted the account we had used for that run, just to be safe.
For the rest of the project, we were much more careful about monitoring outgoing requests. Any telemetry, any error logging, anything that might leak our modifications. It was a good lesson: when you're modifying a client-side application, it's not just the score submissions you need to worry about. Every analytics call, every error report, every background ping is a potential leak.
The tools we built
Once the core exploit was solid, the project grew. More friends joined (Toma and Mirko), and we started building infrastructure around the cheat.
We added a custom UI overlay to the game page itself, with toggles for enabling and disabling the modifications. No more typing in the console.
We also built a separate website that scraped and tracked the leaderboard over time. This was important for two reasons: we needed to know where our accounts sat relative to the prize cutoffs, and we were fairly sure other people were cheating too.
On top of that, we set up a Discord bot with webhook notifications that pinged us whenever an account moved suspiciously on the leaderboard. If someone jumped 50 positions overnight, we'd know about it.
All of this fed into our strategy: never climb too fast, keep our accounts in realistic positions, blend in with the legitimate players, and save the big push for the very end.
The final hour
The contest was ending, and this was the moment we'd been planning for.
We had long immortal sessions running, each one around two to three hours, timed so that the runs would end right before the submission deadline. Each of us had multiple Chrome tabs open, each running the game on a different account. We had timers set. We had the leaderboard tracker open on a second monitor. Everything was coordinated.
At five minutes before the deadline, we were watching the clock. At three minutes, the tension was real. At one minute, we opened the consoles and set enableDebug = true on all our accounts. The characters started taking damage and dying. The score submission payloads went out, huge requests (multiple megabytes each) because they contained every single event from hours of continuous gameplay.
We held our breath.
Then: HTTP error. The server rejected the submissions. ChickenDonald had shut down the score endpoint five minutes before the official deadline.
Three hours of perfectly executed runs, gone.
But we had planned for this. Our "sleeper" accounts, the ones that had been climbing slowly and steadily over the previous days, were already safely in prize range. Their scores had been submitted long before the cutoff. We were disappointed about the final push, but we hadn't lost everything.
The wait
The rules said winners would be contacted within 30 days. We waited. And waited. By day 25, I was convinced they had caught us. The Sentry report, the suspicious scores, something must have tipped them off.
On day 30, we got the email. We won. Free chicken for an entire year.
The aftermath
The contest was targeting ChickenDonald's Italian branch, and I live in Switzerland, so I never got to redeem the prize myself. But every now and then, my friends message me asking to use my account for free chicken. It's become a running joke at this point.
The whole project, from the initial idea to the reverse engineering to the tense final hour, was one of the most fun things I've worked on.
Big thanks to my friend Dere for writing his post first and for being the one who had the idea of exploiting the game in the first place. And to Toma and Mirko, who joined the effort and helped make the whole thing work.