Reverse engineering – Supercell – chapter 9

“Cool! it’s crazy how much people I see in this room”. That’s what I probably said on stage, or at least that crossed my mind, 1 month ago, when I been invited once again to Finland, first at the Supercell HQ and later at the annual Disobey 2020 event in which I spoke about our latest development on Dwarf, showcasing what we want to achieve and the next public release.

Welcome to the chapter 9 of the awesome journey that kicked my ass in the awesome world of reverse engineering (and here and there around the world).

I want to start this blog post by shooting an huge thank you <3 to Supercell which sponsored me the trip to Finland and all the inspirational discussions we shared in front of a cold beer. As usual, I met a lot of awesome people and old friends already met a couple of years before in Finland.

In this blog post, I will mainly speak about 2 things:

  • An abstract of my recent researches and tricks.
    I finally managed to achieve an universal solution able to dump the public server key (pks) in all the games, including the new HayDayPop
  • The videobot i built 1 year ago, which is still rocking and providing non-stop automated content to the company i work for, with details about the sick solution i’ve coded to make it happens

An universal pks grabbing solution

That’s something that really tortured my mind for months. Until a couple of months ago I had to barely spend from 2 to 4/5 hours in order to reach a good offset in memory, jumping here and there to skip Arxan memory checks and tricks.
The path to follow was mostly 2:

  • Code a static solution which pull the values from the opaque predicate blocks of asm and hash them back, as the client code, to get the pks (which is pretty hard to achieve as the code changes at each recompilation and is different for all the 5 games)
  • Code a chain of hooks, without any module dependency (this mean, no static offset) to reach one or more dynamic offset in memory which we can use to dump the pks

For obvious reasons i went for the path number 2. This pushed me to understand way better how Arxan is protecting obfuscated code, which makes the client crash with colorful and random stack traces if the code is tamperd.

Watchpoints was very useful in this purpose. I tried those a lot of times ago without results (I was probably placing them in the wrong spot) but whatever. I spoke a lot of times about how to achieve watchpoints using frida without hardware support. Dwarf has a comfortable js api to do so anyway.

var what = moduleBase.add(0x1000);
Process.setExceptionHandler(function (details) {
    console.log(JSON.stringify(details));
    console.log(details.address.sub(moduleBase));
});
Memory.protect(what, 1, '---');

By placing some watchpoints in the module space I was able to spot a couple of places in the code which are reading the bytes in obfuscated functions. My theory on this is that the jumps in obfuscated code are calculated using a sum of the bytes of obfuscated code, this could also explain the colorful and random crashes if the code is tampered, aka, the code will jump in random places resulting in random crashes, but I didn’t investigate further on it as it was not in my purpose.

The general idea of my final solution is illustrated in those steps:

  • The watchpoints were hitting in different places, on all the 5 games and the code was very different
  • By doing some analysis in all of the code which read in the module space (which is obfuscated as well) I was able to identify a couple of math ops repeated on all of the games.
  • Thus math ops uses different registers but, Memory.scan frida api allows nibble level wildcards. I was then able to breakpoint inside all of the functions, which we will name crco (crc offsets), reading the module space. I’m currently searching for 5 different patterns, there could be way more but, in the moment I’m writing this, I’m able to dump the pks in a couple of seconds.

    This is one of the 5 patterns im looking at:
arxanChecks += Memory.scanSync(module.base, module.size, '0B 01 2? 8A 4? 01 28 8A')
  • My lazy a@@ solution was not to crack all those crco out. First attempt was giving the expected result so I didn’t go any further providing something that might allow statical tampering.
  • At this point, following the solution that was already designed on my mind, I was needing a similar array (arxanChecks which holds the pointers of all the crco) holding some other pointers (I must spend some lines to explain something here).

Since a couple of years, the pks is built in runtime with a simple hasher, which uses static values in a loop of 16 rounds to build the 32 bytes key.
This loop is obfuscated and split in 16 blocks of obfuscated code. Those blocks are shuffled. Randomly, each of those block may be split in more blocks, depending on the obfuscation seed provided in the compiler configurations.

  • All of those blocks ends up, obviously, with an STR instruction, mixed together with other STR instructions involved with obfuscation. Thanks god, the obfuscation is using specific registers and so, the hasher involved registers are always X7, X8 and X9.
  • Some of the games have this hasher repeated more then 10 times. This means, they are not using a single function which they call, that return the key, but rather, when the code needs the pks, the function is embedded inside a big and unique function (during the runtime, and general the initial encryption stage in which the code calculate the shared key or the nonce, the pks is used multiple times and so, recalculated every time).
    Anyway, by using the same Memory.scanSync api I was able to populate an array with all the STR instructions which stores the bytes of the final key.
    I’m using a total of 4 scans, here is one example
let scans = Memory.scanSync(module.base, module.size, '08 01 09 2A ?? 3D 40 92');
  • At this point, I’m up with an array of crco and an array holding the pointers of the key involved STR instructions.
    Giving the assumption that nothing interesting is happening between crco and the first memcpy in the same thread, I was able to hook all my stuff (all the crco and STR), cleanup everything when any of the crco hit, attach everything back at the first memcpy on same thread, repeat.
    At some point the code will hit 16 STR instructions in the right order (we don’t care at the shuffle as the code will anyway execute the code in the correct order) and we are able to get the 2 bytes which we can join together to retrieve the pks.

My videobot solution

As a developer and part of the mobile team @ Overwolf my main role is to build stuff. I’m that one guy providing a way, colorful or not, to obtain whatever we need to build our things. I’m also the Android developer, with 8 years experience including custom OS and kernel development.
One of the things I built that is still rocking nowadays it’s a solution that runs 24/7 on a dedicated device, which records replays from Clash Royale top players and push them to youtube. Here is the channel if someone want to take it a look.

I’m very proud of the thousand lines of code running to achieve all of this and I always promised my self that one day, I would post some technical details about it.

The device, first a Lenovo p2 which I had to throw away because the battery exploded after 5/6 months of non-stop usage and screen on, actually is a razr phone 2. My solution is built with (and run) 3 applications and a python backend running on a digital ocean droplet. The applications shares the same uid in order to allows each other to access the files stored in the sandbox or use broadcasts:

  • LifeGuard (Android application)
  • CaC (Android application)
  • Videbot (Android application)
  • Backend (Py running in the droplet)

The LifeGuard job:

  • Unlocking the device when for some weird reasons the screen goes off (it never happened except for device reboot) or when the LifeGuard reboot the device for unexpected errors (i.e there are no new video recorded since 45 minutes) it must unlock the first lockscreen.
  • Reboot the device if there are no new records within 45 minutes. (this could happens, especially with the new device, because my solution is injecting code into the android audio_server, in example, to allow internal audio record and this, somehow and time to time, crash the whole system)
  • It starts and makes sure that the CaC is running

The CaC job:

  • It connect to our discord server and post a summary of what the phone (which is located ~9000km from my deks) is doing.
  • It gives me a root shell through discord
  • It uses ffmpeg to compress and encode the recorded video and push it to the python backend
  • It make sure that LifeGuard and VideoBot are running fine, calculating the order of the actions performed by all of the apps.

The Videobot job:

  • It inject arbitrary code in the android audio server allowing for internal audio record
  • It start and inject arbitrary code into Clash Royale, cracking some checks and allowing the game to connect to a socket coded into the Videobot which is a replica of the game server
  • It connect to the py backend which provide a replay id to be recorded
  • It forge some packets which allows the replay we want to appear in the clan chat
  • It interact with the screen device simulating tap and swipes in order to start this replay and the recording
  • It notify the CaC when the recording is done, which will be encoded and sent to the py backend

*Note: to record the video I’m using standard Android mediacoder api with the system reserved audio source to record internal audio.

The py backend job:

  • It connect to our discord server and provide a root shell to the server and details of what the phones (because it’s built assuming more then one device can connect to it) are doing
  • It receive the videos from the devices and by using ffmpeg it joins the intro and outro
  • It uses a headless chromedriver to build the thumbnail (basically, we are generating in runtime html code that we run in the chromedriver and screenshot it)
  • It push the final videos to youtube among with tags, descriptions etc

What’s interesting in all of this, to being in-line with my blog topics, is what I’m doing to make Clash Royale able to connect to the server bound into the Videobot and achieve encryption/decryption.

First of all, I’m using the project I opensourced here to inject with ease a frida agent from an Android application to another Android application.
The first hook I need here is on getaddrinfo, where the code is retrieving the address of the server. Later – I’m replacing the host to 127.0.0.1. In the past year, Supercell, has introduced many checks after the getaddrinfo call to make sure that the address is not redirected to localhost. 0.0.0.0 was left and added to the checks later after my report.

Here is the relevant code to patch this out:

        var lop = Interceptor.attach(Module.findExportByName('libc.so', 'getaddrinfo'), function() {
        this.path = this.context.x0.readUtf8String();
        if (this.path === 'game_server_address') {
            log('[+] replacing host');
            this.context.x0.writeUtf8String('127.0.0.1');
            lop.detach();

            module = Process.findModuleByName('libg.so');
            Memory.protect(module.base, module.size, 'rwx');
            /*
            text:000000000073C284                 REV             W8, W8
            .text:000000000073C288                 AND             W9, W8, #0xFF000000
            08 09 C0 5A 09 1D 08 12
            */
            var res = Memory.scanSync(module.base, module.size, '08 09 C0 5A 09 1D 08 12');
            for (var i=0;i<res.length;i++) {
                var intl = Interceptor.attach(res[i].address, function() {
                    log('hit sock check');
                    this.context.x8 = 0x22;
                    attachSendto();
                });
            }
        }

I praise Supercell to leave me this spot open until the solution discussed with the Clash Royale team sees light and allow me to keep the Videbot running without touching the game client.

The next step is to place 2 hooks. An hook to the SVC instruction which call the syscall sendto and wait for the login message to be dispatched and an hook to the libc free function to prevent it to free memory.

The login message includes in the first chunk of bytes, the 32 byte public client key (pck) which the server uses together with it’s private key to achieve decryption and encryption.

Once i have the pck, I’m able to scan for it in any mallocated ranges from the module space, which are not be freed thanks to the previous hook, and use broadcast receiver to post to the Videobot the 64 bytes before and after any founds giving the (right) assumption that the encryption related memory is still holding some structs which holds pck and the private client key (pck2).

Once the Videobot receive the broadcast, an hexadecimal String of 128 bytes, I’m splitting this String into 32 bytes and use the Sodium encryption functions (scalarmult) to “kind of brute force”/crosscheck pck2. With the private client key and the public server key, I’m then able to inverse the encryption and decryption of messages.

      for (var k in r) {
        if (typeof r[k].file === 'undefined') {
          try {
            var res = Memory.scanSync(r[k].base, r[k].size, publicKey);

            if (res.length > 0) {
              for (var t in res) {
                // let's brute in chunk of 32 bytes
                for (var i = -32; i < 32; i += 32) {
                  if (i < 0) {
                    keys.push(res[t].address.sub(i).readByteArray(32));
                  } else if (i === 0) {
                    // it's publicKey, ignore
                    // omfg i really made an empty condition. Blame me.
                  } else {
                    keys.push(res[t].address.add(i).readByteArray(32));
                  }
                }
              }
            }
          } catch (err) {
            log(err + '');
          }
        }
      }

      var malloc = new NativeFunction(Module.findExportByName('libc.so', 'malloc'), 'pointer', ['int']);
      var free = new NativeFunction(Module.findExportByName('libc.so', 'free'), 'int', ['pointer']);
      var buf = ptr(malloc(keys.length * 32).toString());
      Memory.protect(buf, keys.length * 32, 'rw-');

      for (var _i = 0; _i < keys.length; _i++) {
        buf.add(_i * 32).writeByteArray(keys[_i]);
      }

      var what = ba2hex(buf.readByteArray(keys.length * 32));
      free(buf);
      log('send keys to back (len: ' + what.length + ')\n' + what);
      Java.performNow(function () {
        var Intent = Java.use('android.content.Intent');
        var ActivityThread = Java.use('android.app.ActivityThread');
        var Context = Java.use('android.content.Context');
        var ctx = Java.cast(ActivityThread.currentApplication().getApplicationContext(), Context);
        var intent = Intent.$new("com.igio90.crackroyale.keys");
        intent.putExtra('keys', what);
        ctx.sendBroadcast(intent);
        Intent.$dispose();
        ActivityThread.$dispose();
        Context.$dispose();
      });

Here im sharing some of the cool numbers from the youtube analytics panel, which give evidence that those solutions are… cool.. and can be adopted/adapted for other games.

Have fun, and hopefully, see y’all with the chapter 10.

2 Comments

Kosinadara May 2, 2020 Reply

Hi! How can I contact with you?

prq233 June 12, 2022 Reply

Hello, can you give me a clash of clans server and a patched APK? I am a middle school student and a game fan. I can pay you.This my email prq_dl@163.com

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.