GMKrypt - hpgDesigns/hpgdesigns-dev.io GitHub Wiki
GMKrypt is an encryption/obfuscation method developed for Game Maker 7, and was used to encrypt GM7 GMKs, EXEs, DATs, and GEXs.
The main aspect of the encryption is a #Substitution Cipher generated by a #Key. The key may be hidden in #Junk data, and underneath the cipher, the data may also be #Additively encrypted. GMKs use all of these together, while GEXs and DATs only use the key and cipher.
The key is a 32-bit integer usually stored next to the data which is
used to generate a cipher. When generating a key, GM typically uses the
formula random(25600) + 3328
, resulting in a range of [3328,28928),
although GM is perfectly fine reading in keys that are out of that
range, so user-generated keys (e.g. those produced by
LateralGM) are perfectly fine.
It is worth noting that all of the even keys (0, 2, 3328, etc) produce poor ciphers, where every lookup either maps to itself or the next closest character (so 'c' will map to 'b', 'c', or 'd'). In fact, every 250 keys, starting at key 248 (248, 498, 748... 3498, 3748... 28498, 29748 -- a total of 102 in-range keys) is an identity key, wherein every lookup maps to itself, so the data would essentially remain unencrypted, barring other encryption methods of course (although this is not indicative that every 250th table is identical. For instance, tables 249 and 499 are different). Despite this, GM still uses even keys and identity keys, provided they are in the aforementioned range. LateralGM uses key 248.
Since the key is somewhat sensitive information to just leave out in the open, it is sometimes surrounded by Junk data, especially in GMKs. In GEXs and DATs, it is not.
In order to hide the key, GMK surrounds it with Junk data, so that the casual hex editor would be looking for a needle in a haystack (or in this case, a key in a junkyard). This method does significantly increase filesize (anywhere from 1424 to 25416 extra bytes), which is probably one of the reasons GMKrypt was not used in subsequent versions of the GM format.
For those keen to guessing, the junk data itself is just a bunch of
32-bit integers, each generated using the formula random(3000)
. This
means the junk data remains in the range of [0,3000). This means the
maximum value of GM-generated junk is 2999, while the minimum value of a
GM-generated key is 3328. To keep with the needle-haystack analogy, this
would be like if the needle were painted red.
For those who are sane, the junkyard is prefixed by 2 pieces of data, which I affectionately call Bill and Fred, which determine the number of junk integers before and after the key, respectively. Using our documentation guidelines, explained in Format info, we can document the junkyard as such:
...(Header information)...
Bill
Fred
{
Junk
}
Key
{
Junk
}
...(Encrypted data)...
In GM, Bill and Fred are generated by the formula random(3000) + N
where N = 123
for Bill, and N = 231
for Fred.
Please notice that Fred and Fred's junk are not encrypted, even though they follow the key.
Also note, for your confusion, in GMKs, the first byte of useful data after the junkyard is not encrypted, but is left as-is. The remainder of the data thereafter (including the remaining 3 bytes of the first longint field) is, though.
The additive component of the encryption is a very simple additive combiner stream cipher where the key is simply the position in the file. For those who don't know cryptography terminology, that means, for every byte, we return the byte plus its position in the file, modulo 256 if necessary (to keep it as a byte). In C, this is written as:
fputc(fgetc(in) + ftell(in),out);
- fputc writes a character (first argument) to a stream (second argument)
- fgetc reads a character from a stream
- ftell returns the current position in a stream
Notice that, if a modulo 256 is necessary in your language (e.g.
typeless languages), consider using the more efficient bitwise and 255:
(val & 0xFF)
or (val & 255)
A recommended alternative, for languages where a call to ftell is expensive or unavailable, is to simply keep track of the stream position in your own variable (and since we're only using it to combine with another byte, we could store it in an unsigned byte and let overflow roll back to 0).
unsigned char pos = (unsigned char) ftell(in);
size_t len = fread(buf,BUFSIZ,sizeof(unsigned char),in);
for (int i = 0; i < len; i++) {
buf[i] = (unsigned char) (buf[i] + pos + i)
fwrite(buf,len,sizeof(unsigned char),out);
To reverse this, simply subtract the file position, instead of adding, and then roll the value back into the byte range (& 0xFF can be used again, if necessary).
The rest of this still needs documenting. If you would like to help, please first gain an understanding of the encryption, fully documented here: http://lateralgm.org/formats/gmkrypt1.html and then try to reword it in a way that is more wiki-friendly (since the document can be kinda confusing).
- Encrypt: Add, then cipher
- Decrypt: Reverse Cipher, then Subtract
Category:Formats