Not long ago, I used to play a real-time multiplayer browser game called TagPro. The architecture is fairly standard for real-time multiplayer games. The browser opens a websocket connection to a server and user input is sent over the open connection. The server runs all the game logic and sends the game state to each browser several times a second.
There are two separate versions of the JavaScript bundle that powers the TagPro client: a competitive version with client-side security and a casual version without. A toggle in private games enables the competitive version. The JavaScript for TagPro's competitive scene has a few security measures in place to prevent third-party script execution. The first security measure is not exposing the game object globally by wrapping the game.js
bundle in a self-executing function:
(function init() {
// game.js
var tagpro = {
...
// etc.
})();
(This is not implemented in the casual version because many third-party mods for the game depend on window.tagpro
.)
The second security measure consisted of sending the stringified length of the wrapped function to the server for validation:
(function init() {
// game.js
var tagpro = {
...
// Ensure init function hasn't been tampered with
ws.send("6941e8c29f:" + init.toString().length);
6941e8c29f
is a generated socket message key. It's something that changes every time the bundle changes, most likely a hash of the bundle.
These security measures can effectively stop someone from tampering with the bundle and enforce everyone is running the same game.
The first thing I did was play the game with logging in place. I was able to gather logs of all websocket messages being sent by monkey patching Websocket.prototype.send
and playing the game normally. I then checked the logs for anything unique or unexpected which lead me to the discovery of the second security measure. I didn't want to set off any alarms by testing on TagPro's server so I reimplimented the security measures locally.
My goal was to expose the tagpro
object globally to allow any third-party script to be run in competitive games. I couldn't just replace var tagpro =
with window.tagpro =
, that would change the init
function length. Also keeping a modified static copy of the bundle around would require manual updates when TagPro updates.
Solving the first issue was fairly simple. I added a three letter alias for window
and replaced var tagpro
:
window.win = window;
(function init() {
win.tagpro = {
...
})();
This kept the length check the same. The next thing I needed to do was replace the bundle on the fly. I redirected the request to point at a local proxy. The proxy downloaded the original script from TagPro's server, added window.win = window;
to the top, and replaced var tagpro
with win.tagpro
. This seemed to work just fine and I was able to use any third-party mod without being detected. Unfortuanatly, I couldn't stop other players from becoming suspicious of my play style and was eventually banned from competetive play.
Hey, do you have an example script I could look at as reference?