29 Dec 2018

Unity3D protection in “AU2” dance games

Today's story is about dancing games. Specifically, about

These games employ some tricks in the APK file structure as well as modified libmono.so. I will go through each of the protection mechanisms step-by-step and explain how it works. In the end, you will have all the necessary information to implement your own decryption tool that can decrypt AU2 protected DLL files.

Since this research builds on my previous work, I strongly suggest that you read at least Part 1 and Part 2 of my series about Unity3D protections.

Let's get started!

Modified APK/ZIP file

As you probably know, APK file is actually a ZIP file. Game authors have taken the ZIP file and modified some of ZIP internal structures. Depending on which tool you use to decompress the APK, it may or may not cause problems.

For example, the very popular ICSharpCode.SharpZipLib library fails with an error:

This is an easy thing to fix, since SharpZipLib is an open-source project. Just find the offending check and comment it out.

Unpack libmono.so

Once the APK is unpacked and you open Assembly-CSharp.dll in the hex editor, you'll see that DOS and PE signatures are changed to lowercase "mz" and "pe". And the entire PE section table is encrypted.

So, it's very likely that libmono.so was modified. But you can't analyze it right away because it's encrypted with some protection I can't recognize.

I'll be very honest - I don't have a good solution for this part. The cryptor itself is not particularly difficult. However, these games also use libjiagu.so which employs some anti-debug protection. In the end I just made a memory dump from running process and did static analysis of dumped libmono.so in IDA. If I figure out a better way, I'll update the post.

Modifications of libmono.so

Once the libmono.so is unpacked, you can get to the good stuff. Authors of this protection did not go the usual way of changing mono_image_open_from_data_with_name, they changed several methods instead.

First change is in mono_image_load_pe_data - there is an additional check for "mz" signature:

Similarly, do_load_header is modified to support "pe" signature:

And the section headers are decrypted in the load_section_tables:

The decryption method is an overly-complicated XOR:

Broken IL method headers

Now we can open file in dnSpy. However, all methods have empty method bodies. In fact, all the method headers are invalid, both header size and code size is shown as zero.

So, there is still something missing. smile

This looks like a result of a JIT-hooking protection. But there is no JIT hooking in Mono framework! So, the protection authors must have invented another trick..

It took me some time to figure this one out.

First, there is some additional code in load_tables. It generates a decryption key from 2 fields in the #~ stream header. See ECMA-335, II.24.2.6 #~ stream for the description of the structure.

Second, the decryption of method header is done in mono_metadata_parse_mh_full. Tiny and fat headers are handled separately but the idea is the same.

The decryption routine is just a simple XOR again:

To fix this problem, you'll need parser for most .NET metadata structures. I used Asmex source as a base for my tool.

More IL code encryption

You make a tool that fixes method headers and decrypts IL code and run it - but the result still looks like junk:

That's because there's another XOR encryption hidden inside mini_method_compile and mono_method_get_header. Remember, how highest bit of IL code size was set during previous decryption routine? Now this bit is checked and decryption loop is executed. Something like this:

Why? Probably because 2 XOR encryptions are much stronger than just one. Oh, wait, what?! bigsmile Adding one extra line to the tool is an easy fix.

Changed IL opcodes.

Now the decrypted IL code looks makes some sense. There is a ret instruction in the end and some methods actually look correct. Not this one, however:

That's because protection authors have swapped around some IL opcodes. Normally, CIL opcode table looks like this:

  • 0x02 - ldarg.0
  • 0x03 - ldarg.1
  • 0x04 - ldarg.2
  • 0x05 - ldarg.3
  • 0x06 - ldloc.0
  • 0x07 - ldloc.1
  • 0x08 - ldloc.2
  • 0x09 - ldloc.3
  • 0x24 - UNUSED
  • 0x28 - call

In this modified version, ldarg.* and ldloc.* opcodes have been switched around and the call opcode is changed from 0x28 to 0x24:

  • 0x02 - ldarg.3
  • 0x03 - ldarg.2
  • 0x04 - ldarg.1
  • 0x05 - ldarg.0
  • 0x06 - ldloc.3
  • 0x07 - ldloc.2
  • 0x08 - ldloc.1
  • 0x09 - ldloc.0
  • 0x24 - call
  • 0x28 - call

The change is done in the methods mini_method_compile and inline_method. There's an extra check which tells if the module is protected. For protected module, an alternative implementation of mono_method_to_ri is called. If you're interested in the finer details, I suggest you study the code yourself.

To fix that, you'll need to make a simple .NET disassembler. You don't need a full-featured engine, simple length disassembler with lookup tables is more than enough for task in hand. I reused almost 10-years old code stored in my "old projects" folder - and now it is available on my bitbucket repo as well. smile

The code for fixing the opcodes is very simple:

The end result looks like this:


At the time of writing, if you search for modified versions of these 2 games, you'll find out that there are zero mods or cheats available. Zero!

But there are also Thai and Indonesian versions of the same game, as well as an older English version of the game. They do not use custom libmono.so:

There are plenty of cheats and mods available for all of them. From the quick Google search:

Such large difference in number of available mods suggests that this protection is good enough to stop most wanna-be-hackers of the Android MOD groups. Nice work!

10 thoughts on “Unity3D protection in “AU2” dance games

  1. Hi, great article!
    With all of these custom modification to the mono library, it kind of hard to see what inside without knowing the structure of the .NET binary.
    I think it more important for me to learn it so I could know what is wrong with the file structure. Could you recommend where I should learn it? is the one from ntcore good enough? or do you have article about it that easy to read? :)


    • Every person is different and learns in a different way. So, for me it's hard to guess which is the best way for you. :)

      NTCore has a good article, it's a little bit out of date but definitely worth reading. "Coding with Spike" has good series about making a disassembler. He starts from the very beginning end explains how the .NET structures work together. But it was never finished. Some people also like Vijay Mukhi's book.

      When you start digging deeper into .NET structures, you will need to read ECMA-335 specification. It is big and boring - but it is a definitive reference for everything.

  2. Do you mind explaining how to do memory dump on Android?

    I discovered other game that have encrypted libmono.so

    • It depends on what tools you prefer. :) Personally, I'm trying to use IDA as much as possible. IDA's debugger can take a process memory snapshot which contains the unpacked library as well.

      As for other tools... If you are most comfortable with Android, GameGuardian should work. If you are comfortable with command-line, then pmdump or memdump should work.

      Of course, if game uses some anti-debug or anti-dump protection, you'll need to bypass that somehow. But from what I've seen so far, it's not that common.

      You can always send me a link to that game and I'll take a look. No promises, however.

    • I looked at it, and it's using the tricks I've mentioned in previous articles. Please read Part 1
      and Part 2 for details.

      Here's a short rundown and which bytes you need to change to fix it. I will not post a public link to fixed file, it might cause your account to be banned. ;)

      1) Invalid PE signature

      2) Invalid NumberOfRvasAndSizes value

      3) Invalid values ch_size, ch_runtime_major, ch_runtime_minor in CLI header

      4) Invalid values md_version_major and md_version_minor in MetaDataRoot

      5) Invalid length of version string in MetaDataRoot

      6) Invalid number of .NET streams

      7) Invalid sizes of .NET streams

      Once you fix all that, your file will open in dnSpy just fine:

Leave a Reply

  • Be nice to me and everyone else.
  • If you are reporting a problem in my tool, please upload the file which causes the problem.
    I can`t help you without seeing the file.
  • Links in comments are visible only to me. Other visitors cannot see them.

Your email address will not be published.

7  −   =  3