Google CTF 2018 shall we play a game

Although we haven’t managed to submit the flag correctly, I’m still publishing this write-up. Maybe it helps someone.

The Google CTF 2018 “shall we play a game?” challenge:

This was in the reverse engineering category, only included a link to an apk file (mirror) and the short description to win the game 1’000’000 times to get the flag.

I already had the setup to run and investigate Android Apps from a very great BSides Munich workshop Fun with Frida. In the end I didn’t end up using Frida to solve the challenge, but the setup alone helped a lot already.

Looking at the game, it is a very simple tic-tac-toe game with a win counter that goes up to 1’000’000:

Starting to reverse it I’ve used unpack-apk.sh to extract the apk file and attempt to decompile it as well. Looking at the source code at app/extracted/src/main/java/com/google/ctf/shallweplayagame/GameActivity.java we find that this is the main program. Two functions there are of interest to us (comments by me):

    // Display flag and some magic we don't understand
    void m() {
        Object _ = N._(Integer.valueOf(0), N.a, Integer.valueOf(0));
        Object _2 = N._(Integer.valueOf(1), N.b, this.q, Integer.valueOf(1));
        N._(Integer.valueOf(0), N.c, _, Integer.valueOf(2), _2);
        ((TextView) findViewById(R.id.score)).setText(new String((byte[]) N._(Integer.valueOf(0), N.d, _, this.r)));
        o();
    }

    void n() {
        // Reset the board, remove X and O from the board
        for (int i = 0; i < 3; i++) {
            for (int i2 = 0; i2 < 3; i2++) {
                this.l[i2][i].a(a.EMPTY, 25);
            }
        }
        k();
        // Increase win counter
        this.o++;
        // Some magic we don't understand
        Object _ = N._(Integer.valueOf(2), N.e, Integer.valueOf(2));
        N._(Integer.valueOf(2), N.f, _, this.q);
        this.q = (byte[]) N._(Integer.valueOf(2), N.g, _);
        // Check if win counter is 1'000'000
        if (this.o == 1000000) {
            // Show the flag
            m();
            return;
        }
        ((TextView) findViewById(R.id.score)).setText(String.format("%d / %d", new Object[]{Integer.valueOf(this.o), Integer.valueOf(1000000)}));
    }

My first attempts were to start the game with a win counter of already 999’999 or decrease the 1’000’000 to 2. But neither worked, we won the game but instead of the flag we’d get binary garbage displayed. It’s clear that the magic we don’t understand needs to run 1’000’000 to produce the correct string (line 21 – 23).

I’ve started to look at the assembly file app/extracted/smali/com/google/ctf/shallweplayagame/GameActivity.smali and the method n() as that’s where we need to make changes:


.method n()V
    .locals 10

    const v9, 0xf4240

This must be the right place, 0xf4240 is 1’000’000 in hex. We can somewhat easy find the increase of the win counter:


    iget v0, p0, Lcom/google/ctf/shallweplayagame/GameActivity;->o:I

    add-int/lit8 v0, v0, 0x1

    iput v0, p0, Lcom/google/ctf/shallweplayagame/GameActivity;->o:I

And further down is the win check:


    iget v0, p0, Lcom/google/ctf/shallweplayagame/GameActivity;->o:I

    if-ne v0, v9, :cond_2

    invoke-virtual {p0}, Lcom/google/ctf/shallweplayagame/GameActivity;->m()V

Between those sections, we need to add a new loop which runs 1’000’000 times. I’ve did that with the following patch:


--- orig/GameActivity.smali	2018-06-26 10:47:29.072510136 +0200
+++ patched/GameActivity.smali	2018-06-26 11:21:00.495157390 +0200
@@ -664,7 +664,8 @@
 .end method
 
 .method n()V
-    .locals 10
+    # Increase local variable count by one
+    .locals 11
 
     const v9, 0xf4240
 
@@ -676,6 +677,9 @@
 
     const/4 v6, 0x2
 
+    # Add new variable v10 with value 0
+    const v10, 0x0
+
     move v2, v1
 
     :goto_0
@@ -716,8 +720,20 @@
 
     add-int/lit8 v0, v0, 0x1
 
+    # move 1'000'000 into the win counter, we now only need to win once
+    move v0, v9
+
     iput v0, p0, Lcom/google/ctf/shallweplayagame/GameActivity;->o:I
 
+    # add new loop, goto_3 label
+    :goto_3
+
+    # break out condition, if v10 is 1'000'000 goto cond_3
+    if-ge v10, v9, :cond_3
+
+    # increase loop counter v10 by 1
+    add-int/lit8 v10, v10, 0x1
+
     new-array v0, v7, [Ljava/lang/Object;
 
     invoke-static {v6}, Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer;
@@ -786,6 +802,12 @@
 
     iput-object v0, p0, Lcom/google/ctf/shallweplayagame/GameActivity;->q:[B
 
+    # end of the for loop, jump back up to goto_3 label until v10 is 1'000'000
+    goto :goto_3
+
+    # break out label, jump here if v10 is 1'000'000
+    :cond_3
+
     iget v0, p0, Lcom/google/ctf/shallweplayagame/GameActivity;->o:I
 
     if-ne v0, v9, :cond_2

Now we just need to build a new apk file, zipalign it, sign it, install it on our emulator and run it:


# apktool b
# zipalign -v 4 dist/app.apk app.aligned.apk
# jarsigner -verbose -storepass android -keystore ~/.android/debug.keystore app.aligned.apk signkey
# adb install app.aligned.apk

And when we run it finally this screen is displayed:

The flag is CTF{ThLssOfInncncIsThPrcOfAppls} or CTF{ThLssOfInncncIsThPrcOfAppIs} or CTF{ThLssOflnncncIsThPrcOfAppls} – I’m still not sure.

Leave a Reply

Your email address will not be published.