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