Unity3D, Mono and invalid PE files, part 2

kao

In the first part of the series I explained how some cheat authors try to protect their work against other cheaters. It was a quick introduction to Unity3D and bugs in Mono that cheat authors exploit.

Last week someone emailed me another example of a game cheat. My tool from the previous article failed to fix invalid metadata, so I decided to look at it again.

Cheats by BlackMod.net

The cheat I received was made by Mod4U from BlackMod.net team. It appears that Mod4U is one of the most active members of the team, judging by number of the releases. His/her cheats use invalid PE file tricks and are encrypted, as you'll see later in the article.

After looking at different mods from other team members, I've confirmed that Rito, Aurora and Legend also are using invalid PE files for hiding their work. But none of their cheats encrypt Assembly-CSharp.dll.

So, let's look at the cheats and see what new tricks BlackMod team members have found!

Just like in my previous article, I'll be using Mono sources from commit 74be9b6, so that they would more or less match the code used by Unity3D.

Unchecked CLI header values

Mono ignores pretty much every field from CLI header in mono_image_load_cli_header:

...
	memcpy (&iinfo->cli_cli_header, image->raw_data + offset, sizeof (MonoCLIHeader));

#if G_BYTE_ORDER != G_LITTLE_ENDIAN
#define SWAP32(x) (x) = GUINT32_FROM_LE ((x))
#define SWAP16(x) (x) = GUINT16_FROM_LE ((x))
#define SWAPPDE(x) do { (x).rva = GUINT32_FROM_LE ((x).rva); (x).size = GUINT32_FROM_LE ((x).size);} while (0)
	SWAP32 (iinfo->cli_cli_header.ch_size);
	SWAP32 (iinfo->cli_cli_header.ch_flags);
	SWAP32 (iinfo->cli_cli_header.ch_entry_point);
	SWAP16 (iinfo->cli_cli_header.ch_runtime_major);
	SWAP16 (iinfo->cli_cli_header.ch_runtime_minor);
...

.NET runtime validates ch_size, ch_runtime_major, ch_runtime_minor fields. Other reversing tools validate some of the fields.

Unchecked MetaDataRoot values

Mono completely ignores md_version_major and other values of the MetaDataRoot in load_metadata_ptrs:

...
    image->md_version_major = read16 (ptr);
    ptr += 2;
    image->md_version_minor = read16 (ptr);
    ptr += 6;
...

.NET runtime and tools based on older dnLib validate md_version_major and md_version_minor fields.

How long is the version string?

ECMA-335 specification paragraph II.24.2.1 specifically says

Call the length of the string (including the terminator) m (we require m <= 255); the length x is m rounded up to a multiple of four.

Mono throws specification out the window and does it in another, completely broken way:

...
    version_string_len = read32 (ptr);
    ptr += 4;
    image->version = g_strndup (ptr, version_string_len);
    ptr += version_string_len;
    pad = ptr - image->raw_metadata;
    if (pad % 4)
        ptr += 4 - (pad % 4);
...

Mono accepts version_string_len that is not rounded to multiple of four and rounds it up automatically. It also accepts string length larger than necessary.

This is a very interesting bug, as it affects most of the commonly used .NET reversing tools.

  • .NET 2.0 runtime doesn't actually follow ECMA-335 specification. It doesn't require string length to be aligned and doesn't align it. But it does require that metadata streams start at dword boundary.
  • dnLib/dnSpy will accept unaligned strings but will not align them. It will accept extra long strings. dnLib doesn't require streams to start at dword boundary.
  • CFF Explorer will happily read file with unaligned string length and will align it automatically. It will also accept file with extra long strings.

Invalid HeapSizes/MDStreamFlags value

Mono processes only the lowest 3 bits of HeapSizes in load_tables:

...
    int heap_sizes;

    heap_sizes = heap_tables [6];
    image->idx_string_wide = ((heap_sizes & 0x01) == 1);
    image->idx_guid_wide   = ((heap_sizes & 0x02) == 2);
    image->idx_blob_wide   = ((heap_sizes & 0x04) == 4);
...

and completely ignores flag value 0x40 which is quite crucial.

See the dnLib sources:

    [Flags]
    public enum MDStreamFlags : byte {
        /// #Strings stream is big and requires 4 byte offsets
        BigStrings = 1,
        /// #GUID stream is big and requires 4 byte offsets
        BigGUID = 2,
        /// #Blob stream is big and requires 4 byte offsets
        BigBlob = 4,
        /// 
        Padding = 8,
        /// 
        DeltaOnly = 0x20,
        /// Extra data follows the row counts
        ExtraData = 0x40,
        /// Set if certain tables can contain deleted rows. The name column (if present) is set to "_Deleted"
        HasDelete = 0x80,
    }

When ExtraData flag is set, there is one more field present in .NET metadata::

if (HasExtraData)
    extraData = reader.ReadUInt32();

.NET runtime and tools based on dnLib respect the flag but Mono completely ignores it. D'oh!

Invalid size of #~ stream

Mono doesn't check size of #~ stream at all. Really. It accesses all the #~ fields with no validation whatsoever:

/*
 * Load representation of logical metadata tables, from the "#~" stream
 */
static gboolean
load_tables (MonoImage *image)
{
	const char *heap_tables = image->heap_tables.data;
	const guint32 *rows;
	guint64 valid_mask;
	int valid = 0, table;
	int heap_sizes;
	
	heap_sizes = heap_tables [6];     <--- unchecked!
	image->idx_string_wide = ((heap_sizes & 0x01) == 1);
	image->idx_guid_wide   = ((heap_sizes & 0x02) == 2);
	image->idx_blob_wide   = ((heap_sizes & 0x04) == 4);
	
	valid_mask = read64 (heap_tables + 8);     <--- unchecked!
	rows = (const guint32 *) (heap_tables + 24);     <--- unchecked!
	
	for (table = 0; table < 64; table++){
		if ((valid_mask & ((guint64) 1 << table)) == 0){
			if (table > MONO_TABLE_LAST)
				continue;
			image->tables [table].rows = 0;
			continue;
		}
		if (table > MONO_TABLE_LAST) {
			g_warning("bits in valid must be zero above 0x2d (II - 23.1.6)");
		} else {
			image->tables [table].rows = read32 (rows);     <--- unchecked!
...

So, you can set size of #~ stream to a random number and Mono will be happy. CFF will be happy as well. But any other tool will complain.

Invalid size of #Blob stream

Mono correctly reads offset and size for #Blob stream:

...
    else if (strncmp (ptr + 8, "#Blob", 6) == 0){
        image->heap_blob.data = image->raw_metadata + read32 (ptr);
        image->heap_blob.size = read32 (ptr + 4);
        ptr += 8 + 6;
    }
...

but doesn't check values for correctness. So, it will also accept insanely large values for heap_blob.size.

The only checks are done when reading from #Blob stream and only ensure that index is smaller than the declared #Blob size:

/**
 * mono_metadata_blob_heap:
 * @meta: metadata context
 * @index: index into the blob.
 *
 * Returns: an in-memory pointer to the @index in the Blob heap.
 */
const char *
mono_metadata_blob_heap (MonoImage *meta, guint32 index)
{
	g_assert (index < meta->heap_blob.size);
	g_return_val_if_fail (index < meta->heap_blob.size, "");/*FIXME shouldn't we return NULL and check for index == 0?*/
	return meta->heap_blob.data + index;
}

Assembly-CSharp.dll encryption

As I already mentioned, Mod4U is the only Blackmod team member who uses this type of protection. To learn how exactly the protection works, please read my previous post.

Today I'd like analyze different encryption methods used by Mod4U. Data show that Mod4U keeps stealing the protection mechanisms from others!

Not operation

On 24-Jun-2018, Mod4U released Dragon Spear v1.25 MOD. APK contains Assembly-CSharp.dll encrypted using simple NOT operation. It also contains libmono.so for armeabi-v7a architecture which is protected by some protector that I can't recognize.

Thanks to VirusTotal, we can track this libmono.so back to a Chinese game from year 2016, called Lance Town. Stolen! 🙂

Xor 0x71

On 12-Jul-2018, Mod4U uses different encryption in his/her Noblesse M Global v1.2.0 release. APK contains libmono.so for armeabi-v7a architecture and it's not protected at all. This is the exact same encryption that was used by AndroidThaiMod and described in my previous post.

Additional VirusTotal search shows that this protection has been used by different teams since August 2017. Stolen! 🙂

Sub 0x0D

Starting from 24-Jul-2018 release of Boxing Star v1.1.2, Mod4U uses yet another version of libmono.so. Now there are 2 libmono.so versions - one for armeabi-v7a and one for x86 architecture.

This version of libmono.so seems to be stolen from a different Chinese game! 🙂 How else can you explain presence of the exact same code described in Pediy post about Chinese game and reference to ThreeKindom.dll? And the function name rvlt_modify_data_with_name - a reference to RevolutionTeam who released the game.

void __fastcall rvlt_modify_data_with_name(_BYTE *a1, int a2, char *a3)
{
  const char *moduleName; // [sp+4h] [bp-18h]
  int dataLen; // [sp+8h] [bp-14h]
  _BYTE *dataPtr; // [sp+Ch] [bp-10h]
  int j; // [sp+10h] [bp-Ch]
  int i; // [sp+14h] [bp-8h]

  dataPtr = a1;
  dataLen = a2;
  moduleName = a3;
  if ( strstr(a3, "Assembly-CSharp.dll") )
  {
    for ( i = 0; i != dataLen; ++i )
      dataPtr -= 0xD;
  }
  else if ( strstr(moduleName, "ThreeKindom.dll") )
  {
    for ( j = 0; j != dataLen; ++j )
      dataPtr[j] += 0xB;
  }
}

VirusTotal search confirms the theory. Stolen again! 😀

Cheats by platinmods.com

Platinmods is another Android modding team using pretty much the same tricks.

I took just a quick look at their releases (eg. BLEACH Brave Souls v7.3.1) and found that they employ another encryption method (Xor 0xFB).

  if ( *datac != 0x4D || datac[1] != 0x5A || datac[2] != 0x90 || datac[3] )
  {
    for ( i = 0; i < data_lena; ++i )
    {
      datac[i] ^= 0xCBu;
      datac[i] ^= 0x30u;
    }
  }

According to VirusTotal, that particular version of libmono.so has been used by Vietnamese modding team W4VN.NET since April-2017.

Update for my tool

My original intention was to make a SIMPLE tool that helps you to fix the broken PE file and metadata. My tool does exactly that.

Most of the tricks that I described are trivial to fix. Check the value, if it's incorrect, overwrite it with correct value. Repeat until done. Something like this:

if (cor20Header.cb != 0x48 || cor20Header.MajorRuntimeVersion != 2 || cor20Header.MinorRuntimeVersion != 5)
{
    Console.WriteLine("[!] Fixing invalid COR20_HEADER values");
    cor20Header.cb = 0x48;
    cor20Header.MajorRuntimeVersion = 2;
    cor20Header.MinorRuntimeVersion = 5;
    stream.WriteStruct(cor20Position, cor20Header);
}

However, "Invalid #~/#Blob size" tricks are particularly difficult to fix. My tool cannot magically support all possible cases, not without writing full .NET metadata reader/writer. And I don't want to do that, sorry. 😉

So, if you really, really need to fix invalid stream sizes, try running de4dot on the decrypted Assembly-CSharp-fixed.dll. de4dot versions from years 2016-2017 work best, versions from 2018 most likely will complain about "invalid parameter" or something like that. Same advice applies for dnSpy - versions from 2017 will open most (or all) decrypted files, versions from year 2018 will not.

Download here:

Conclusion

As predicted in my previous article, cheat authors are willing to exploit any and all bugs in Mono to hide their work from others. Some of the bugs they've discovered are really surprising and affect more than just one tool. I expect cheat authors to continue finding new and innovative ways to hide their code in the future as well.

Next time I'll look into... well, I have no idea! 😀

P.S. If you still can't make some DLL work, please send me an email (or leave a comment below). I'll look at it and try to help you.

12 thoughts on “Unity3D, Mono and invalid PE files, part 2

  1. help me fix error:
    Unhandled Exception: System.IndexOutOfRangeException: Index was outside the bounds of the array.
    at FixUnity3D.Program.Main(String[] args)

      1. I used the latest version you provided. 2.1 but it probably does not work.
        APK link: {hidden link}

      2. sorry link hide. i have send you with email. please check email.. thanks you!

        1. Links are hidden for other people, I can see them, no problem.

          I answered to your email.

    1. I will look at that protection someday when I get some free time, but I cannot promise you anything.

  2. I wish Admin Kao will add xigncode3 for apk protection bypasser? this protection add in every games but dont know how other bypass it.

      1. Thanks for sharing this wonderful ideas. Please accept me as friend in your gmail or any messaging app . Thank again

Comments are closed.