I wrote about Moonton game protection in November 2018. It was a pretty boring protection, so I quickly forgot about that. In January 2019 Moonton devs decided to change their protection. I'm not sure if it's a coincidence or not - but here's the update anyway.
This analysis covers:
specifically versions from 1.3.37 upto 1.3.47 (latest at the time of writing). All other games that I mentioned in my previous post haven't been updated, or are still using the old protection mechanism.
Simple ELF cryptor
I am planning write a separate blog post about ELF cryptors in general, so he're just a quick summary - 11 methods are encrypted:
- sub_1D1308
- sub_1D1309
- sub_1D1320
- sub_1D1321
- loc_1D0FEC
- loc_1D0FED
- DecodeInt
- DecodeHeaderFlag
- mono_method_get_header_summary
- mono_metadata_parse_mh_full
- load_metadata_ptrs
These names might look a little bit weird but those are the actual names of methods, as contained in ELF symbol table. Cryptor code obtains method addresses and sizes on runtime by parsing ELF file headers, decryption is simple RC4 using a hardcoded key:
32 14 28 30 70 03 AE 1B 52 98 57 04 07 3A 60 E1
Fun fact - this list also tells you where exactly to look for the good stuff. No more boring comparison of original libmono.so and a modified one. Thanks guys! 🙂
Changed DOS signature
Just like in previous version MSDOS signature is changed from "MZ" to "ER". However, the check is now implemented in the verify_msdos_header:
static void verify_msdos_header (VerifyContext *ctx) { guint32 lfanew; if (ctx->size < 128) ADD_ERROR (ctx, g_strdup ("Not enough space for the MS-DOS header")); if (ctx->data [0] != 0x45 || ctx->data [1] != 0x52) //"ER" ADD_ERROR (ctx, g_strdup ("Invalid MS-DOS watermark")); lfanew = pe_signature_offset (ctx); if (lfanew > ctx->size - 4) ADD_ERROR (ctx, g_strdup ("MS-DOS lfanew offset points to outside of the file")); }
and the check from mono_image_load_pe_data is removed altogether.
Encrypted PE header
This is a big change from the previous version. If you open some DLLs in hex editor, you'll see that PE header is encrypted:
The decryption is done do_mono_image_load:
static MonoImage * do_mono_image_load (MonoImage *image, MonoImageOpenStatus *status, gboolean care_about_cli, gboolean care_about_pecoff) { MonoCLIImageInfo *iinfo; MonoDotNetHeader *header; GSList *errors = NULL; if (sub_1D1308(image)) // decryption is done here { mono_profiler_module_event (image, MONO_PROFILE_START_LOAD); ... } }
If you look into sub_1D1308, you'll see that it decrypts first 0x1000 bytes of .NET assembly. Decryption algorithm is slightly modified AES-128 in ECB mode with another hardcoded key. Very secure! 😀
int __cdecl sub1D1308(void *image) { unsigned __int8 buffer[32]; unsigned __int8 aesKey[100]; unsigned __int8 aesContext[516]; unsigned __int8 msdos_header[128]; unsigned __int8 *dest; unsigned __int8 *src; if ( image ) { if ( image->raw_data ) { qmemcpy(msdos_header, image->raw_data, sizeof(msdos_header)); if ( msdos_header[0x7F] == 0x13 ) // this is a signature for AES-encrypted files. { memset(aesKey, 0, sizeof(aesKey)); aesKey[0] = 0x9A; aesKey[1] = 0x05; aesKey[2] = 0xB8; aesKey[3] = 0x14; aesKey[4] = 0xDE; aesKey[5] = 0x5E; aesKey[6] = 0xC3; aesKey[7] = 0xC7; aesKey[8] = 0x04; aesKey[9] = 0xDB; aesKey[10] = 0x23; aesKey[11] = 0x0B; aesKey[12] = 0x18; aesKey[13] = 0x96; aesKey[14] = 0xEE; aesKey[15] = 0x46; if ( sub1D1309(&aesContext, aesKey, 128) ) // aes_set_key return 0; .... // copy first 0x80 bytes as-is .... for ( int i = 0x80; i < 0x1000 && i < image->raw_data_len - 16; i += 16 ) { memset(buffer, 0, 0x20); memcpy(buffer, src, 0x10); sub1D1321(&aesContext, buffer, buffer); // aes_decrypt memcpy(dest, buffer, 0x10); src += 16; dest += 16; } ....
As for AES implementation, we can find a very similar opensource code, for example, here. Comparing decompiled code with the original, you can see that aes_decrypt has been modified in a way that it includes additional XOR encryption with another hardcoded key:
void aes_decrypt( aes_context *ctx, uint8 input[16], uint8 output[16] ) { uint32 *RK, X0, X1, X2, X3, Y0, Y1, Y2, Y3; RK = ctx->drk; for ( i = 0; i <= 15; ++i ) // this was added for "extra security" :) input[i] ^= PATTERN[i]; GET_UINT32( X0, input, 0 ); X0 ^= RK[0]; GET_UINT32( X1, input, 4 ); X1 ^= RK[1]; GET_UINT32( X2, input, 8 ); X2 ^= RK[2]; GET_UINT32( X3, input, 12 ); X3 ^= RK[3]; .... } uint8 PATTERN[] = { 0x4C, 0x7B, 0x95, 0x32, 0xA6, 0x54, 0xC3, 0x3D, 0xEE, 0x42, 0x94, 0x0B, 0xC2, 0xFA, 0x23, 0x4E };
Now we can decrypt the assembly and take a look at .NET metadata. 🙂
Obfuscated .NET metadata streams
Opening decrypted assembly in CFF Explorer, we can see that stream headers look like a mess.
Apparently, stream names were changed to a more random strings in load_metadata_ptrs. Also some fake streams have been added (Mono happily ignores them):
for (i = 0; i < streams; i++){ if (strncmp (ptr + 8, "?-", 3) == 0){ // this used to be "#~" .... } else if (strncmp (ptr + 8, ":ZFC", 5) == 0){ // "#Strings" .... } else if (strncmp (ptr + 8, "GUID", 6) == 0){ // "#GUID" ....
Previously stream offsets and sizes were obscured using simple "not" operation:
for (i = 0; i < streams; i++){ if (strncmp (ptr + 8, "$~", 3) == 0){ image->heap_tables.data = ~read32 (ptr) + image->raw_metadata; image->heap_tables.size = ~read32 (ptr + 4); ptr += 11; } else if (strncmp (ptr + 8, "$Strings", 9) == 0){ ....
Now size and data fields are swapped around and it's using a special method DecodeInt to decode stream offset/size:
for (i = 0; i < streams; i++){ if (strncmp (ptr + 8, "?-", 3) == 0){ // this used to be "#~" image->heap_tables.data = DecodeInt(read32 (ptr + 4)) + image->raw_metadata; image->heap_tables.size = DecodeInt(read32 (ptr)); ptr += 11; } else if (strncmp (ptr + 8, ":ZFC", 5) == 0){ // "#Strings" ....
The new method DecodeInt looks like this:
unsigned int __cdecl DecodeInt(unsigned int a1) { unsigned int valShr10; // [esp+18h] [ebp-8h] valShr10 = (a1 & 0xFF0000) >> 16; if ( a1 & 3 ) valShr10 = (unsigned __int8)~((a1 & 0xFF0000) >> 16); return (unsigned __int16)((unsigned __int16)(a1 & 0xFF00) >> 8 << 8) | ((unsigned __int8)~HIBYTE(a1) << 24) | (valShr10 << 16) & 0xFF0000 | (unsigned __int8)a1; }
That decompiled code looks scary, doesn't it? 🙂
Here's a more readable version which replaces "binary NOT" and bitshifting with XOR operations:
unsigned int __cdecl DecodeInt(unsigned int a1) { if (a1 & 3) return (a1 ^ 0xFFFF0000); else return (a1 ^ 0xFF000000); }
We can now fix stream headers and look at the methods and IL code.
Modified method headers
This is another change from previous version.
The magic happens in mono_metadata_parse_mh_full. There are 3 small changes in the code:
MonoMethodHeader * mono_metadata_parse_mh_full (MonoImage *m, MonoGenericContainer *container, const char *ptr) { MonoMethodHeader *mh = NULL; unsigned char flags = DecodeHeaderFlag(*(const unsigned char *) ptr); // (1) unsigned char format = flags & METHOD_HEADER_FORMAT_MASK; .... switch (format) { case METHOD_HEADER_TINY_FORMAT: mh = g_malloc0 (MONO_SIZEOF_METHOD_HEADER); ptr++; mh->max_stack = 8; mh->is_transient = TRUE; local_var_sig_tok = 0; mh->code_size = flags >> 2; ptr += *ptr + 1; // (2) mh->code = (unsigned char*)ptr; return mh; case METHOD_HEADER_FAT_FORMAT: fat_flags = read16 (ptr); ptr += 2; max_stack = read16 (ptr); ptr += 2; code_size = DecodeInt(read32 (ptr)); // (3) ptr += 4; local_var_sig_tok = read32 (ptr); ptr += 4; ....
DecodeHeaderFlag
First change is related to method header. Method's header flag is encoded and there's a new function DecodeHeaderFlag. Decompiled code of DecodeHeaderFlag looks like crap:
unsigned __int8 __cdecl DecodeHeaderFlag(unsigned __int8 a1) { return a1 & 3 | (unsigned __int8)((32 * ~(a1 >> 5)) | (4 * ((a1 & 0x1C) >> 2))); }
When rewritten using XOR operation it looks much nicer:
unsigned __int8 __cdecl DecodeHeaderFlag(unsigned __int8 a1) { return a1 ^ 0xE0; }
Tiny method header
Second change is related to tiny method headers. Instead of one line in original code
mh->code = (unsigned char*)ptr;
we now have 2 lines
ptr += *ptr + 1; // (2) mh->code = (unsigned char*)ptr;
To help you understand that weird calculation, here's a picture:
In .NET framework, tiny method header is immediately followed by IL code. In the Moonton version, there are some junk bytes inserted between tiny method header and IL code. To fix that, you need to process method header, find IL code size, find number of junk bytes and move IL code to the proper place. It's not hard, just... annoying.
Fat method header
Third and final change is in the fat header processing. Size of IL code gets decrypted using the same DecodeInt function we saw earlier. It's a very simple thing to fix, much easier than the tiny header.
Fix all that and observe the decompiled code:
Conclusion
Moonton protection has been significantly improved since I looked at it last time. However, there are plenty of MODs out there, even for the games protected with this latest protection which indicates that protection has been bypassed by some MOD teams already.
As explained earlier, I will no more provide a ready-made executable that can remove the protection. But here you have all the information you need to make your own tool.
Have fun! 🙂
Thanks to Sonny for bringing my attention to the updated version of Moonton.
SHARE FOR U A DEBUG TOOL: {hidden link}
The link in previous comment was: https://bbs.pediy.com/thread-249266.htm
Unfortunately I can't read Chinese, so I am not able to download the tool and try it. 🙁For all non-Chinese users, I've mirrored files to https://mega.nz/#F!A9JwDSZb!nZFMfS9P0j1Ic07bn0fKoQ
Please note that I have not tested any of them, just downloaded from Baidu and uploaded to Mega!
Thanks for the amazing tutorial, I just took your old source of fixer anf implemented all that you mentioned and It is working you are awesome.
That's the right kind of attitude! 🙂 Great job!
Hmm... Where can I get atlas battleground.unity3d? Bcoz I can't fine it anywhere can u tell me plzz
Really nice article. I had just figured out the AES module cryption before i stumbled on this, and thank god I did, figuring out the internal .NET fuckery would've taken me ages! I took your old application and modified it to fix up the changes you mentioned (parsing the metadata tables upto the method headers was a bit of a pain in the ass!). However, certain TypeDef and Field entries seem to be messed up, and references to locals in .ctor() functions are null'd out (FAT header messed up?). I'm failing to find any changes in their libmono.so to suggest they're doing more than what you've discovered. Do you think I've just made an error in my fixup code? I promise I've stared at it for hours before making this post.
Turns out I was running AES decrypt upto file offset 1000h instead of 1080h. Oops.
I realized another mistake I was making was fixing up method bodies multiple times, as I didn't realize different method definitions could have the same body rva.
After getting passed the .NET protection it might interest you to know that this game also employs a modified Lua VM for their Lua scripts (which seems to be the chunk of game functionality) that's "hiding" in the library at comlibs/arch/libmoba.bytes. The compiled lua scripts have a basic aes & gzip applied, and if you follow luaU_undump in libmoba you'll find that some opcodes are modified as well. Interesting stuff!
It was just some opcode swapping thankfully. Here's a funny snippet where they check some hardcoded package names 🙂
{hidden link}
They are now using il2cpp. Their global-metadata.dat is invalid. I bet its a placeholder and they fill it with bytes when the game loads. I will try investigating more!
thanks mr. kao you still alive, can you share more link or ebook about protecting moonton games
xia xia
Hello Koclok, if I learn more about Moonton games, I'll share it here. But at the moment I don't have anything new.
liblogic.so it's the true libil2cpp.so of the game, remember they create a process called UnityKillsMe, its true?