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?


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

import android.os.Bundle;

public class FoodActivity extends AppCompatActivity {
    public static Activity activity;

    protected void onCreate(Bundle savedInstanceState) {
        setContentView((int) C0165R.layout.activity_food);
        activity = this;

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/ and lib/x86/

I like to reverse ARM, but I’m more used to x86. Since we already know that the app is crashing somewhere while loading, 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 (, 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:


import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class FoodActivity extends AppCompatActivity {

    protected void onCreate(Bundle savedInstanceState) {
        Button btn = (Button) this.findViewById(;
        btn.setOnClickListener(new View.OnClickListener() {
            public void onClick(View view) {

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.


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
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 "" hit Breakpoint 1, 0x9ab00ae0 in JNI_OnLoad ()
   from target:/data/app/

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

Thread 1 "" hit Breakpoint 2, 0x9ab00bb1 in JNI_OnLoad ()
   from target:/data/app/
(gdb) x/s $eax
0x9bd12f10: "/data/data/"

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/ and /data/data/ Then, new classes are loaded using DexClassLoader.

Also, I found out the reason why the app was crashing, it was trying to load which I don’t seem to have in my emulator. I ignored the result of dlopen("") 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/ 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:

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.

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");
        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…



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;
    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;
        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
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 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 = open("patched.dex", "wb")
f.write(s[:0x720] + result + s[0x720+0x90:])

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
    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 =
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!