Explaining TCoAaL's Obfuscation
In order to extend or modify the game engine (RPG Maker MV), developers write JavaScript plugins. Some plugins are made with the express purpose of being used by many developers in their own games, while other plugins are specifically made for one game. These plugins are accessible in the www/js/plugins/
folder, and the main RPG Maker code is in the parent directory, www/js/
.
The Coffin of Andy and Leyley has plugin code written specifically for it, in order to handle things like custom decryption. However, steps have been taken to obscure the code. This page details how exactly the game loads the custom code.
Game Flow
How the main game code is run.
- Buried in a plugin somewhere is the following call:
_();
- Buried elsewhere is the definition of that function, which has surrounding obfuscated code. The
_
function assembles a string from several chained function calls (i.e.const data = function1() + function2() + function3() ... etc
). - That data is a Base64 string which is converted to a buffer and then inflated via
zlib
. - The resulting code is the code that gets embedded into the web page.
(De)obfuscation Flow
How the obfuscation (or rather, the deobfuscation process) of the main game code works.
- At the top of the file, there is a big array full of strings, and a “shuffler” function that takes one number as an input and returns one of the strings. Research indicates this is a transformation done via obfuscator.io, though existing deobfuscators don’t seem to handle it.
- Those functions are used all over the place, but the bodies of functions tend to redeclare the shuffler (
var newDecode = decoder;
) as a local variable and use the redeclared version instead. The deobfuscation process involved converting those functions to their string equivalents. - Additionally, all integer numbers are stored in the hex format (i.e.
0x32
instead of 50) which can be changed back into the decimal form for better readability. You can also unminify things like![]
and!![]
, which represent boolean values.
En/decryption
In the obfuscated game code, various loader methods (DataManager.loadDataFile
, WebAudio.prototype._load
, and others) are overwritten. They mostly resemble the original code of those functions, but include calls to Crypto.decrypt
with the data to decrypt the game assets.
Crypto.decrypt
is called with the encrypted contents as aUint8Array
and the path to the file relative from thewww
folder, i.e.data/Actors.json
- A bitmask is derived from the capitalized filename of the asset. For example, if the path to the file is
data/Actors.json
, it would derive a mask fromACTORS
. - The file is checked for a valid signature at the top of the file by running a specific 16 byte string through the mask, then comparing it to the first 16 bytes of the file. If the signature does not match, the raw data is returned; this means that you can run the game using decrypted assets, and it will simply accept them.
- After those first 16 bytes, there is one byte that determines which bytes of the file are encrypted. If the byte is
0x00
, then, the entire file is encrypted. Otherwise, the numerical value of the byte determines how many bytes need to be decrypted; if it’s0x40
, or 64, then the first 64 bytes of the file after this byte are encrypted. The value of that byte seems to follow these rules:.json
files are all 0, so are all fully encrypted..png
files are from 100 to 120, so only the first 100 to 120 bytes are encrypted..ogg
files are from 200 to 220, so only the first 200 to 220 bytes are encrypted.
- Based on the above rules, the first x bytes (or all of them for
.json
) are decrypted.
At this point in the flow, the data has been fully decrypted and can be extracted and used.
- A signature check is performed for the last 8 bytes against a constant value. If it fails, it returns the data. However, this doesn’t appear to have adverse effects, as the data is already decrypted.
- The game’s “session,” consisting of an LZString-compressed JSON string including information such as the external IPv4 network addresses on the computer and the connected Steam users, is appended to the decrypted data.
- The fixed 8 byte signature in step 6 is also appended to the data.
- The data is returned.
I am not entirely sure how the game manages to successfully parse files with all of the extra “tracking metadata” tacked on.