This writeup is for the “Food” challenge found in Google CTF Quals 2017, from the reversing category.

JNI-Native Reversing

We are only given a food.apk, and from there we immediately unzip it and run dex2jar, followed by jd-gui to view the source code. Fortunately, the source is very succinct:

public class FoodActivity extends AppCompatActivity {
    public static Activity activity;
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView((int) R.layout.activity_food);
        activity = this;
        System.loadLibrary("cook");
    }
}

Although I’m not well versed in Android development, it looks like it’s loading a library libcook.so from either one of lib/{armeabi,x86}.

The bizarre disassembly in JNI_Onload seems to indicate that all useful strings have been encrypted, and are only decrypted at runtime. We called it decrypt_string and reimplemented it in python:

def decrypt_string(sz, nums):
    ret = [ None ] * (2 * sz)
    for i in range(sz):
        from ctypes import c_uint
        v5 = nums[i]
        byte_1 = c_uint( v5 & 0x000000ff         ).value
        byte_2 = c_uint((v5 & 0x0000ff00) >> 0x08).value
        byte_3 = c_uint((v5 & 0x00ff0000) >> 0x10).value
        byte_4 = c_uint((v5 & 0xff000000) >> 0x18).value
        ret[i*2+0] = ~((~byte_1 | byte_2) & (~byte_2 | v5))
        ret[i*2+1] = byte_4 ^ byte_3
    from ctypes import c_byte
    return "".join(map(lambda x: chr(c_byte(x).value), ret))

We can now resolve many important strings which are passed as arguments to fopen and fwrite, shown below, as well as to later JNI runtime functions.

FILE *fp = fopen("/data/data/com.google.ctf.food/files/d.dex", "wb");
if (fp != NULL) {
    fwrite("dex\n035...", 0x15A8, 1, fp);
    fclose(fp);
}

Unfortunately, binwalk doesn’t recognize dex files, but clearly we’re writing an embedded dex file to the location /data/data/com.google.ctf.food/files/d.dex, which we can dump from the binary.

Before we analyze this dex file, we continue reversing the JNI_Onload routine, to discover a particularly scary routine which parses /proc/self/maps and patches itself at runtime, a little later on.

FILE *fp = fopen("/proc/self/maps", "r");
while (fgets(s, 256, fp)) {
    if (strstr(s, "/d.dex")) {
        int page_size = sysconf(_SC_PAGE_SIZE);
        char *dex = strtoul(s, 0, 16);
        if (mprotect(dex, (1968 / page_size + 8) * page_sz,
                     PROT_READ|PROT_WRITE|PROT_EXEC))
            return 0;
        for (char *i = dex; i < dex + 8 * page_sz; ++i) {
            if (strcmp(i, "dex\n0") == 0) {
                for (int i = 0; i < 0x90; ++i)
                    dex[0x720 + i] = xor_me[i] ^ 0x5a;
            }
        }
        break;
    }
}

At a high level, it reads /proc/self/maps until it finds the base address of /d.dex, and looks for the magic header, dex\n0. It then proceeds to patch the JVM bytecode at runtime with a sinister binary string of length 0x90 that’s xor’d with 0x5a. This d.dex, we presume is the same d.dex file we dumped from earlier.

We go to offset 0x720 of the d.dex file, to discover what appears to be 0x90 bytes of jvm-bytecode nops. We patch in the unxor’d content into the dex file, and decompile it in much the same way we did with classes.dex.

Reversing d.dex

Unfortunately the files were a bit large, but we note the following routines in F.java:

public void cc() {
    byte[] arrayOfByte = new byte[8]
        { 26, 27, 30, 4, 21, 2, 18, 7 };
    for (int i = 0; i < 8; i++)
        arrayOfByte[i] = ((byte)(arrayOfByte[i] ^ this.k[i]));
    if (new String(arrayOfByte).compareTo(
            "\023\021\023\003\004\003\001\005") == 0)
        Toast.makeText(this.a.getApplicationContext(),
                       new String(.(flag, this.k)), 1).show();
}

public void onReceive(Context paramContext, Intent paramIntent) {
    // ...
    this.k[this.c] = ((byte)i);
    cc();
    // reset this.k if this.cc++ == 8
}

In the unpatched d.dex, cc() was originally nopped to thwart static analysis. S.java sets up a view in which one can request foods in a particular order, by pressing the corresponding buttons. onRecieve() will take the indices of each order and save it in this.k, and once all 8 have been entered cc() xors k with arrayOfBytes, and compares it to a new string.

Once we’ve figured out the right k, we will be given the flag. We can figure out this k with the following python code:

bytes = [ 26, 27, 30, 4, 21, 2, 18, 7 ]
targt = [023, 021, 023, 003, 004, 003, 001, 005, ]
k = map(lambda (i,j): i ^ j, zip(bytes, targt))

To save some time, we can construct a simple Soln.java class, and simply import com.google.ctf.food.ℝ (since we couldn’t get any of this to run in an android emulator).

import com.google.ctf.food.;

public class soln {
    private static byte[] flag = {
        -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 };
    private static byte[] k = {
        9, 10, 13, 7, 17, 1, 19, 2 };

    public static void main(String []args) {
        System.out.println(new String(.(flag, k)));
    }
}

This spits out the flag, CTF{bacon_lettuce_tomato_lobster_soul}