Google CTF - Writeup

Food - Android Reverse Engineering

Posted by 0xacb on June 25, 2017 · 14 mins read

This was the first challenge I solved while playing Google CTF, worth 191 points. In this writeup I’ll explain how I solved it, using both static and dynamic analysis techniques.

Android is all about the desserts, but can you come up with the secret recipe to cook up a flag?

food.apk

We’re given an APK file. I pushed it into my local x86 Nexus 5 emulator and noticed that it was crashing just after starting the app.

Static Analysis

After unzipping the APK, I started by decompiling classes.dex using JADX and found the following interesting snippet of code in FoodActivity.java:

package com.google.ctf.food;
import android.app.Activity;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;

public class FoodActivity extends AppCompatActivity {
    public static Activity activity;

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView((int) C0165R.layout.activity_food);
        activity = this;
        System.loadLibrary("cook");
    }
}

Ok, I’m dealing with native stuff since it loads a “cook” library!

Let's start cooking

This shared library can be found in the lib directory. Two architectures are supported: lib/armeabi/libcook.so and lib/x86/libcook.so

I like to reverse ARM, but I’m more used to x86. Since we already know that the app is crashing somewhere while loading libcook.so, then JNI_OnLoad must be the right place to start digging. Let’s take a look on the assembly code.

JNI_OnLoad seems too complex for static analysis. However, we can take some notes:

  • sub_680 receives two arguments and decodes some obfuscated data using logical operations and returns a readable string, hopefully. Let’s rename this function to decodeData. There are 22 calls to this function in libcook.

  • On the other hand, sub_710 is called exactly one time in the whole library, near the end of JNI_OnLoad. Let’s rename this function to finalFunc. This function seems pretty interesting because it inspects “/proc/self/maps”, mprotects some segment of memory as RWX and writes in a loop the result of some_data[i] ^ 0x5A.

  • There are more external function calls, such as: dlopen, mkdir, fopen, fwrite, fclose, remove and rmdir.

I wanted to solve this challenge as fast as possible, so I just used dynamic analysis to uncover whatever was going on in the native layer.

Dynamic analysis

First of all, I took a look on the logs using adb logcat and realized that in fact, it was crashing inside JNI_OnLoad: “JNI_ERR returned from JNI_OnLoad”.

So, I decided to attach a debugger to the application process in order to understand what was going on. However, since the application crashes imediatelly, and I couldn’t find a feasible way to attach a debugger before JNI_OnLoad, I created a new app in Android Studio with the same package name (com.google.ctf.food), added a button with a listener that executes System.loadLibrary(“cook”) and copied the libaries to the directory app/src/main/jniLibs of my new project:

package com.google.ctf.food;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class FoodActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_food);
        Button btn = (Button) this.findViewById(R.id.button);
        
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                System.loadLibrary("cook");
            }
        });
    }
}

Then, I can just install the new APK, open it, attach a debugger, set breakpoints and then press the button. I use this technique sometimes while dealing with Android RE challenges involving native libs, since we can observe the result of any native function with a given input, for example.

Debugging

First, we need to setup GDB in the emulator. You can find a nice tutorial here.

generic_x86:/data/app # ps | grep food
u0_a84    3416  1259  1009820 48364 SyS_epoll_ a7c31424 S com.google.ctf.food
generic_x86:/data/app # gdbserver :1337 --attach 3416
Attached; pid = 3416
Listening on port 1337
➜  adb forward tcp:1337 tcp:1337
➜  ./gdb
(gdb) target remote:1337

Now, we just need some gdb skills to understand what JNI_OnLoad is doing. We can set a breakpoint in JNI_OnLoad, continue the execution and press the button to load libcook.

(gdb) b JNI_OnLoad
(gdb) continue

Thread 1 "google.ctf.food" hit Breakpoint 1, 0x9ab00ae0 in JNI_OnLoad ()
   from target:/data/app/com.google.ctf.food-2/lib/x86/libcook.s

So, JNI_OnLoad is located at 0x9ab00ae0. We need this address to calculate further breakpoints, since all code section addresses are randomized due to ASLR (PIE).

I didn’t care about what certain functions did in the static analysis phase, such as decodeData (sub_680). I uncovered the result of every decodeData call by setting breakpoints and stepping instructions in GDB. For instance, we want to set a breakpoint in the address 0xbb1 (just after call sub_680) and we know that JNI_OnLoad is at 0xae0 while observing assembly. Then, it’s possible to calculate the new breakpoint address:

0x9ab00ae0 + (0xbb1-0xae0) = 0x9ab00bb1

(gdb) b *0x9ab00bb1
Breakpoint 2 at 0x9ab00bb1
(gdb) c
Continuing.

Thread 1 "google.ctf.food" hit Breakpoint 2, 0x9ab00bb1 in JNI_OnLoad ()
   from target:/data/app/com.google.ctf.food-2/lib/x86/libcook.so)
(gdb) x/s $eax
0x9bd12f10: "/data/data/com.google.ctf.food/files/d.dex"

Good! I started uncovering strings in the binary just like this and therefore inspecting the program behavior. I found that it was creating the files /data/data/com.google.ctf.food/files/d.dex and /data/data/com.google.ctf.food/files/odex/d.dex. Then, new classes are loaded using DexClassLoader.

Also, I found out the reason why the app was crashing, it was trying to load libdvm.so which I don’t seem to have in my emulator. I ignored the result of dlopen("libdvm.so") by manually jumping from jz loc_1580 (0xe19) to the adjacent address (0xe1f).

Now, we just need to set a breakpoint before the remove and rmdir function calls, in order to grab the /data/data/com.google.ctf.food/files directory, since they will be removed after this state of execution.

Back To Static Analysis!

After pulling the new files from the emulator, we can use JADX again to decompile files/d.dex. We have now the following interesting files:

S.java

Basically, this is the new main activity. It loads 32 emoji buttons to a grid layout:

🍕 🍬 🍞 🍎
🍅 🍙 🍝 🍓
🍈 🍉 🌰 🍗
🍤 🍦 🍇 🍌
🍣 🍄 🍊 🍒
🍠 🍍 🍆 🍟
🍔 🍜 🍩 🍚
🍨 🌾 🌽 🍖

There is a click listener for each one, that sends a broadcast with the respective ID.

F.java


public class C0000F extends BroadcastReceiver {
    private static byte[] flag;
    private Activity f0a;
    private int f1c;
    private byte[] f2k;

    static {
        flag = new byte[]{(byte) -19, (byte) 116, (byte) 58, (byte) 108, (byte) -1, (byte) 33, 
          (byte) 9, (byte) 61, (byte) -61, (byte) -37, (byte) 108, (byte) -123, (byte) 3,
           (byte) 35, (byte) 97, (byte) -10, (byte) -15, (byte) 15, (byte) -85, (byte) -66, 
           (byte) -31, (byte) -65, (byte) 17, (byte) 79, (byte) 31, (byte) 25, (byte) -39,
            (byte) 95, (byte) 93, (byte) 1, (byte) -110, (byte) -103, (byte) -118, (byte) -38,
             (byte) -57, (byte) -58, (byte) -51, (byte) -79};
    }
        
    public C0000F(Activity activity) {
        this.f0a = activity;
        this.f2k = new byte[8];
        for (int i = 0; i < 8; i++) {
            this.f2k[i] = (byte) 0;
        }
        this.f1c = 0;
    }

    public void cc() {}

    public void onReceive(Context context, Intent intent) {
        this.f2k[this.f1c] = (byte) intent.getExtras().getInt("id");
        cc();
        this.f1c++;
        if (this.f1c == 8) {
            this.f1c = 0;
            this.f2k = new byte[8];
            for (int i = 0; i < 8; i++) {
                this.f2k[i] = (byte) 0;
            }
        }
    }
}

Basically, we need to find out the right key to decrypt the flag! Note that the cc function is does nothing, which is weird…

ℝ.java

package com.google.ctf.food;

public class ℝ
{
  public ℝ() {}
  
  public static byte[] ℂ(byte[] paramArrayOfByte1, byte[] paramArrayOfByte2)
  {
    byte[] arrayOfByte1 = new byte['Ā'];
    byte[] arrayOfByte2 = new byte['Ā'];
    int m = 0;
    int i = 0;
    int j;
    label32:
    int k;
    if (i == 256)
    {
      j = i ^ i;
      i = 0;
      if (j != 256) {
        break label87;
      }
      paramArrayOfByte2 = new byte[paramArrayOfByte1.length];
      k = j ^ j;
      j = i ^ i;
      i = m;
    }
    for (;;)
    {
      if (i == paramArrayOfByte1.length)
      {
        return paramArrayOfByte2;
        arrayOfByte1[i] = ((byte)i);
        arrayOfByte2[i] = paramArrayOfByte2[(i % paramArrayOfByte2.length)];
        i += 1;
        break;
        label87:
        i = i + arrayOfByte1[j] + arrayOfByte2[j] & 0xFF;
        arrayOfByte1[i] = ((byte)(arrayOfByte1[i] ^ arrayOfByte1[j]));
        arrayOfByte1[j] = ((byte)(arrayOfByte1[j] ^ arrayOfByte1[i]));
        arrayOfByte1[i] = ((byte)(arrayOfByte1[i] ^ arrayOfByte1[j]));
        j += 1;
        break label32;
      }
      k = k + 1 & 0xFF;
      j = j + arrayOfByte1[k] & 0xFF;
      arrayOfByte1[j] = ((byte)(arrayOfByte1[j] ^ arrayOfByte1[k]));
      arrayOfByte1[k] = ((byte)(arrayOfByte1[k] ^ arrayOfByte1[j]));
      arrayOfByte1[j] = ((byte)(arrayOfByte1[j] ^ arrayOfByte1[k]));
      paramArrayOfByte2[i] = ((byte)(paramArrayOfByte1[i] ^ arrayOfByte1[(arrayOfByte1[k] + arrayOfByte1[j] & 0xFF)]));
      i += 1;
    }
  }
}

After a brief analysis, we can say that this is a RC4 implementation. We can encrypt or decrypt a given plaintext or ciphertext, respectively, by providing the key. But… Where is the key? The key is composed by 8 bytes between 0 and 31, that match the indexes of the order of the 8 clicked emoji buttons. I even started bruteforcing the key and conspiring about the challenge description to choose the right dessert and fruit emojis. Then, I started thinking… I must be missing something!

Fixing the missing function

I used backsmali to inspect the cc function of the dex file:

                           |[2] code_item: Lcom/google/ctf/food/F;->cc
                           |()V
000710: 0000               |  registers_size = 6
000712: 0000               |  ins_size = 1
000714: 0000               |  outs_size = 3
000716: 0000               |  tries_size = 0
000718: 0000 0000          |  debug_info_off = 0x13a2
00071c: 0000 0000          |  insns_size = 0x48
                           |  instructions:
000720: 0000               |    return-void
000722: 0000               |    return-void
000724: 0000               |    return-void
...
0007ae: 0000               |    return-void

Too many return-void instructions! This confirms the reason why the decompiled code from cc was empty. Remember the weird XOR operations in the libcook.so at finalFunc (sub_710)? If you inspect the assembly code, you can see that they start at the offset 0x720 of the memory segment. We just need to get what is being XORed using GDB!

I coded a python script to replace the return-void instructions in the new patched.dex file:

def sxor(s1, s2):
    return "".join(chr(ord(a) ^ ord(b)) for a,b in zip(s1,s2))

a = "I^RZy\033{Z|[fZZZHZo\032UZ\022X[Z\016\t_Z\022YYZ\355h\327x\025X[Z\202ZZ[r\250xZEZ*z~ZJZ@[ZZ4z\177ZJZPZcZGZ\016\nXZ4J[ZZZVZx[EZ8X^Z\016\t_Z+zxZhZVX*z~Z{ZHH+jOZJXVZ4JLZZZTZZY[ZRZZZ@AD^OXH]" #Got this using GDB

result = sxor(a, "\x5a"*len(a))

f = open("d.dex")
s = f.read()
f.close()

f = open("patched.dex", "wb")
f.write(s[:0x720] + result + s[0x720+0x90:])
f.close()

I decompiled it again and found the new decompiled Java code for the cc function:

public void cc() {
    byte[] arrayOfByte = new byte[8];
    byte[] tmp6_5 = arrayOfByte;
    tmp6_5[0] = 26; //0x1a
    byte[] tmp11_6 = tmp6_5;
    tmp11_6[1] = 27; //0x1b
    byte[] tmp16_11 = tmp11_6;
    tmp16_11[2] = 30; //0x1e
    byte[] tmp21_16 = tmp16_11;
    tmp21_16[3] = 4; //0x04
    byte[] tmp26_21 = tmp21_16;
    tmp26_21[4] = 21; //0x15
    byte[] tmp31_26 = tmp26_21;
    tmp31_26[5] = 2; //0x02
    byte[] tmp36_31 = tmp31_26;
    tmp36_31[6] = 18; //0x12
    byte[] tmp42_36 = tmp36_31;
    tmp42_36[7] = 7; //0x07
    tmp42_36;
    int i = 0;
    while (i < 8) {
      arrayOfByte[i] = ((byte)(arrayOfByte[i] ^ this.k[i]));
      i += 1;
    }
    if (new String(arrayOfByte).compareTo("\u0013\u0011\u0013\u0003\u0004\u0003\u0001\u0005") == 0) {
      Toast.makeText(this.a.getApplicationContext(), new String(flag, this.k), 1).show();
    }
  }

That’s much better, now the solution is trivial and deterministic, no bruteforce needed :)

import ctypes
from Crypto.Cipher import ARC4

def sxor(s1, s2):
    return "".join(chr(ord(a) ^ ord(b)) for a,b in zip(s1,s2))

l = [-19, 116, 58, 108, -1, 33, 9, 61, -61, -37, 108, -123, 3, 35, 97, -10, -15, 15, -85, -66, -31, -65, 17, 79, 31, 25, -39, 95, 93, 1, -110, -103, -118, -38, -57, -58, -51, -79]
flag = "".join(map((lambda n: chr(ctypes.c_ubyte(n).value)), l))

key = sxor("\x1a\x1b\x1e\x04\x15\x02\x12\x07", "\x13\x11\x13\x03\x04\x03\x01\x05")
cipher = ARC4.new(key)
print("Flag: %s" % cipher.decrypt(flag))

Flag: CTF{bacon_lettuce_tomato_lobster_soul}

Final notes

  • Runtime dex modifications from the native layer are becoming a thing
  • This technique can be used to obfuscate code and make reverse tasks harder, but still possible
  • Dynamic analysis can speed up a lot the process of native Android RE :)
  • Thanks Google for this challenge!