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 todecodeData
. 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 ofJNI_OnLoad
. Let’s rename this function tofinalFunc
. 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 ofsome_data[i] ^ 0x5A
. -
There are more external function calls, such as:
dlopen
,mkdir
,fopen
,fwrite
,fclose
,remove
andrmdir
.
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
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*[email protected][ZZ4z\177ZJZPZcZGZ\016\nXZ4J[ZZZVZx[EZ8X^Z\016\t_Z+zxZhZVX*z~Z{ZHH+jOZJXVZ4JLZZZTZZY[[email protected]^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!