Rating: 5.0
DISCLAIMER: Solved after competition ended.
Android is all about the desserts, but can you come up with the secret recipe to cook up a flag?<br><br>food.apk
If the link above doesn't work to download the apk, I uploaded the apk to this folder as food.apk.
The first step with our apk (Android mobile app) is to decompile it. I normally use this site: http://www.javadecompilers.com/apk. You can view the files from the decompiled apk in the food_source_from_JADX folder.
For those of you who aren't familiar with Android apps, when you start an application, the MainActivity
is normally the one that's launched first, but we can verify which Activity is launched by looking at the AndroidManifest.xml
, which can be found here.
Pay close attention to this:
<activity android:name="com.google.ctf.food.FoodActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
The android.intent.action.MAIN
and android.intent.category.LAUNCHER
tell us that com.google.ctf.food.FoodActivity
is the main activity of the application and the one that's launched first when you first open the app.
So now let's take a look at the com.google.ctf.food.FoodActivity.java file
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) C0174R.layout.activity_food);
activity = this;
System.loadLibrary("cook");
}
}
So for this activity, C0174R.layout.activity_food
is the layout that's used, let's look at the layout resource file and see if there's anything interesting in it. It will probably be located in /res/layout/activity_food.xml. There's only a Relative Layout and nothing else interesting, so let's take a look at this line: System.loadLibrary("cook");
.
According to the Android Docs:
Loads the native library specified by the libname argument. The libname argument must not contain any platform specific prefix, file extension or path. If a native library called libname is statically linked with the VM, then the JNI_OnLoad_libname function exported by the library is invoked. See the JNI Specification for more details. Otherwise, the libname argument is loaded from a system library location and mapped to a native library image in an implementation- dependent manner.<br><br>The call
System.loadLibrary(name)
is effectively equivalent to the call<br><br>Runtime.getRuntime().loadLibrary(name)
So essentially what the method is doing is loading a native library, and looking at this StackOverflow question: https://stackoverflow.com/questions/27421134/system-loadlibrary-couldnt-find-native-library-in-my-case, we see that if our argument is "cook"
, then there's probably a libcook.so
in the /lib/
folder, which there is!
Note: There's a x86
and armeabi
folder, I use the libcook.so
in the x86
folder, because when I decompile the file using IDA Pro, it's easier to view the pseudocode and assembly code.
So the .so
tells us that it's a shared library file, and according to what the Android Docs said above, we're looking for a JNI_OnLoad_libname function cause that is what invoked, so the flag will probably be somewhere in that function.
So now we decompile the libcook.so
file using IDA Pro. The psuedocode that IDA gives us is in the file libcook.c. On the side we can view the assembly code for the JNI_OnLoad
function, so let's look at that.
Initially, we see a lot of mov
statements with large hex numbers, so it looks like those values are being put onto the stack. After these values are put on the stack, then the function sub_680
is called. This assembly code corresponding to this statement in the pseudocode: filename = (char *)sub_680(21, 32);
.
Now let's look at sub_680
. The following is the psuedocode for the function:
_BYTE *__cdecl sub_680(int a1, char a2)
{
_BYTE *v2; // ebp@1
int v3; // ecx@1
unsigned int v4; // edx@2
v2 = malloc(2 * a1 + 1);
v3 = 0;
if ( a1 > 0 )
{
do
{
v4 = *((_DWORD *)&a2 + v3);
v2[2 * v3] = ~((BYTE1(v4) | ~*(&a2 + 4 * v3)) & (v4 | ~BYTE1(v4)));
v2[2 * v3++ + 1] = (v4 >> 16) ^ BYTE3(v4);
}
while ( v3 != a1 );
}
v2[2 * a1] = 0;
return v2;
}
So analyzing the function we see a couple of things, first it frees up memory using malloc
. Then it does a do-while loop in which we take the address of a2
(one of the inputs), cast it to a _DWORD
pointer, add v3
to this pointer, and then dereference the pointer. This is typical psuedocode IDA will give you when looping through some kind of array, we don't see any array. Remember those mov
statements from earlier? All of those values were added to the stack, and v4
is basically just looping through all of those values. The loop runs a1
times, and if we see how many values are added to the stack through the mov
statements, it's 21, and we also see 21 as a parameter to the sub_680
function, so that confirms that the loop is probably looping through the values we added to the stack
With the line filename = (char *)sub_680(21, 32);
, 32
is what's passed to a2
, so if we look at the stack and the assembly code, that's the mov [esp+0ECh+mode], 25410F20h
line (32 in hex is 0x20). Because the char
type in c is only 1 byte, we can only pass one byte as the value of a2
, that's the reason why 32 is used as opposed to 0x25410F20, so the last byte (0x20) is being passed as the parameter. But, as we see in the psuedocode, it casts the addess as a _DWORD
pointer, so when we derefence it, v4
will have the value of 0x25410F20, because that value can be stored in a _DWORD
.
So v4
is looping through the values we added to the stack, and then it's doing some computations and storing them where we freed memory using malloc
and then the function returns v2
. Let's try and rewrite the computations the function is doing in python along with those values we added to the stack just to see what numbers we get and attempt to convert those numbers to printable ASCII chars.
def byte3(val):
return (val >> 24) & 0xff
def byte2(val):
return (val >> 16) & 0xff
def byte1(val):
return (val >> 8) & 0xff
def byte0(val):
return val & 0xff
def dec(vals):
string = '';
for s in vals:
news = '';
news += chr(~((byte1(s) | ~byte0(s)) & (s | ~byte1(s))));
news += chr(byte2(s) ^ byte3(s));
string += news;
return string;
vals = [0x25410F20, 0x10640564, 0x5B744120, 0x4650C68, 0x6675420, 0x0C6F5D72, 0x36E1A75, 0x47204A64, 0x0A650D62, 0x0A660265, 0x4F614520, 0x10640D6E, 0x4D634620, 0x6F096F, 0x4420046B, 0x86E5A75, 0x5691D74, 0x5320096C, 0x16724D62, 0x1377416F, 0x1D650B6E]
filename = dec(vals);
print filename
So to explain the code, the reason why *(&a2 + 4 * v3))
turned into byte0(s)
is because a2
is a char, so it's only 1 byte, and when you add 4*v3
, which will take values of 0,4,8,..
, you're basically going up the stack 0 bytes, 4 bytes, 8 bytes, etc, which corresponds to the lowest byte of each value we added on the stack. Also this expression: (v4 >> 16)
was changed to just byte2(s)
because v2
in the pseudocode is of type _BYTE
, so that means all of the computations will only get an answer that's a byte, which will probably be the lower byte of the result. When we run the python script we get the following: /data/data/com.google.ctf.food/files/d.dex
. So this look like a file that might be hidden somewhere in this apk.
We look further in the pseudocode and see this line: fwrite("dex\n035", 0x15A8u, 1u, v5)
, and v5 = fopen(filename, v4)
, so we open the file /data/data/com.google.ctf.food/files/d.dex
and we're writing data to it. We can double click on the "dex\n035"
in IDA and see that it's at address 0x00001640
, so we copy the data from 0x00001640
to 0x00001640 + 0x15A8
and save that file as d.dex.
Searching online, a .dex
file is nothing but a compiled Android program. All dex files are then zipped into one apk, and that's how an Android app is made. So we can decompile the d.dex
file we have using an online tool again. This site: http://www.javadecompilers.com/apk works pretty well, and you can view the files from the decompiled d.dex file in the d.dex_source_from_JADX folder.
We have 4 java files with weird names, but the C0000F.java is interesting.
public class C0000F extends BroadcastReceiver {
private static byte[] 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};
private Activity f0a;
private int f1c;
private byte[] f2k = new byte[8];
public void cc() {
/* JADX: method processing error */
throw new UnsupportedOperationException("Method not decompiled: com.google.ctf.food.F.cc():void");
}
public C0000F(Activity activity) {
this.f0a = activity;
for (int i = 0; i < 8; i++) {
this.f2k[i] = (byte) 0;
}
this.f1c = 0;
}
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;
}
}
}
}
We see a byte[]
array called "flag", but none of the bytes there are printable ascii chars, so we probably have to apply some kind of transformation to it. Also it seems one of the methods: cc()
wasn't able to decompile properly. If we look at the other 3 java files, we don't see any reference to the byte[] flag
from this java file. So what do we do now?
Let's look a little further in the pseudocode. We see these 3 lines:
remove(filename),
remove(v37),
rmdir(path),
So it looks like it removes the d.dex
file, and then close to the end of JNI_OnLoad
there's a function call to sub_710()
. Let's take a look at that function.
signed int sub_710()
{
const char *v0; // esi@1
const char *v1; // eax@1
const char *v2; // eax@3
signed int result; // eax@5
__int32 v4; // esi@8
_BYTE *v5; // eax@8
void *v6; // edi@8
int v7; // eax@8
void *v8; // edx@9
char *v9; // edi@17
int v10; // esi@17
char v11; // al@18
int v12; // edx@18
FILE *v13; // [sp+28h] [bp-1C4h]@1
_BYTE *v14; // [sp+2Ch] [bp-1C0h]@8
char nptr[4]; // [sp+33h] [bp-1B9h]@8
int v16; // [sp+37h] [bp-1B5h]@8
char v17; // [sp+3Bh] [bp-1B1h]@8
char v18; // [sp+3Ch] [bp-1B0h]@17
char haystack; // [sp+CCh] [bp-120h]@3
int v20; // [sp+D0h] [bp-11Ch]@8
int v21; // [sp+1CCh] [bp-20h]@1
v21 = _stack_chk_guard;
v0 = sub_680(1, 114);
v1 = sub_680(8, 72);
v13 = fopen(v1, v0);
if ( v13 )
{
do
{
if ( !fgets(&haystack, 256, v13) )
goto LABEL_5;
v2 = sub_680(3, 101);
}
while ( !strstr(&haystack, v2) );
v17 = 0;
*(_DWORD *)nptr = *(_DWORD *)&haystack;
v16 = v20;
v4 = sysconf(40);
v5 = (_BYTE *)strtoul(nptr, 0, 16);
v14 = v5;
v6 = v5;
v7 = mprotect(v5, v4 * (1968 / v4 + 8), 7);
if ( !v7 )
{
v8 = v6;
if ( 8 * v4 > 0 )
{
while ( *(_BYTE *)v8 != 100
|| *((_BYTE *)v8 + 1) != 101
|| *((_BYTE *)v8 + 2) != 120
|| *((_BYTE *)v8 + 3) != 10
|| *((_BYTE *)v8 + 4) != 48 )
{
++v7;
v8 = (char *)v8 + 1;
if ( v7 == 8 * v4 )
goto LABEL_5;
}
qmemcpy(&v18, &unk_15A0, 0x90u);
v9 = &v18;
v10 = v7 - (_DWORD)&v18;
do
{
v11 = *v9;
v12 = (int)&(v9++)[v10];
v14[v12 + 1824] = v11 ^ 0x5A;
}
while ( v9 != &haystack );
}
LABEL_5:
fclose(v13);
result = 1;
goto LABEL_6;
}
}
result = 0;
LABEL_6:
if ( v21 != _stack_chk_guard )
sub_650();
return result;
}
We first notice two calls to the sub_680
function again, and then it calls fopen
, so what file are we opening? If we use our script again, the value of v1
is /proc/self/maps
. So we're reading the process memory of our apk (Linux and Android are actually very similar in how they work and their architecture).
This line: fgets(&haystack, 256, v13)
is reading 256 chars from /proc/self/maps
and storing those chars in haystack
. There's another call to sub_680
: v2 = sub_680(3, 101);
, and v2
has a value of /d.dex
, so looking at that first do-while loop, we're reading the process memory of our apk and we're looking for /d.dex
. The way /proc/self/maps
works is that it lists each process in a table-like format and then displays information about each process such as the address range each process take up, the permissions (read/write/execute) associated with the memory, and other things. So we're looking for the /d.dex
process specifically. Let's look at these 2 lines:
*(_DWORD *)nptr = *(_DWORD *)&haystack;
v5 = (_BYTE *)strtoul(nptr, 0, 16);
So we are taking 4 bytes from where where haystack
is stored and then we're storing that value in nptr
(which has type char[4]
, so it's a string. Then we take the number from that string, convert it from base 16, and then store that value in v5
. So whenever you look at /proc/self/maps
, the beginning of each line for a process starts with the starting address of where the memory is stored, so what these 2 lines are doing is it's storing the starting address for where /d.dex
stores its memory and it's putting that value in v5
. From what we did previously, we can assume that this starting value is 0x00001640
, because that was the starting address we used when writing to the d.dex
file.
If we look at the rest of the code we see this line: qmemcpy(&v18, &unk_15A0, + 0x90u);
, which is basically copying the data from address &unk_15A0 + 0x90u
to &v18
, and also this line: v14[v12 + 1824] = v11 ^ 0x5A;
. The way the pseudocode is written is a little confusing, but because we're assuming v5
is 0x00001640
then v7
will still have a value of 0
because the first 5 bytes (0x1640-0x1644) are 100, 101, 120, 10, 48
exactly so the while loop doesn't run at all. Now looking at the assembly code it seems v12 = (int)&(v9++)[v10];
is just taking the pointers v9
and v10
, adding their values, and v12
is that value, so it's a pointer from the sum of 2 other pointers. Because v7
is 0
, however, v12
will also have a value of 0
. v14 = v5
, so v14
starts are the address 0x00001640
and we have to add 1824 to that. Then we replace 0x00001640 + 1824
to 0x00001640 + 1824 + 90
with the 90 bytes from the qmemcpy
call xored with 0x5A
, which is done in the following python code:
a = '49 5E 52 5A 79 1B 7B 5A 7C 5B 66 5A 5A 5A 48 5A 6F 1A 55 5A 12 58 5B 5A 0E 09 5F 5A 12 59 59 5A ED 68 D7 78 15 58 5B 5A 82 5A 5A 5B 72 A8 78 5A 45 5A 2A 7A 7E 5A 4A 5A 40 5B 5A 5A 34 7A 7F 5A 4A 5A 50 5A 63 5A 47 5A 0E 0A 58 5A 34 4A 5B 5A 5A 5A 56 5A 78 5B 45 5A 38 58 5E 5A 0E 09 5F 5A 2B 7A 78 5A 68 5A 56 58 2A 7A 7E 5A 7B 5A 48 48 2B 6A 4F 5A 4A 58 56 5A 34 4A 4C 5A 5A 5A 54 5A 5A 59 5B 5A 52 5A 5A 5A 40 41 44 5E 4F 58 48 5D';
li = a.split(' ');
newli = [];
for char in li:
newli.append(chr(int(char, 16) ^ 0x5A).encode('hex'))
print ''.join(newli);
This returns 130408002341210026013c000000120035400f00480201005453050048030300b7328d224f020100d800000128f222001f007020240010001a0100006e20250010000a0039001d00545002006e10010000000c0022011f0062020400545305007120220032000c0270202400210012127130150010020c006e10160000000e0000030100080000001a1b1e0415021207
Now using a hex editor, you can replace those 90 bytes with the new 90 bytes we just computed and that file is now d_new.dex. Once again we decompile it again and you can view the decompiled files in the d_new.dex_source_from_JADX folder. Going back to our C0000F.java, we now see that the cc()
function has decompiled successfully!
package com.google.ctf.food;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;
public class C0000F extends BroadcastReceiver {
private static byte[] 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};
private Activity f0a;
private int f1c;
private byte[] f2k = new byte[8];
public C0000F(Activity activity) {
this.f0a = activity;
for (int i = 0; i < 8; i++) {
this.f2k[i] = (byte) 0;
}
this.f1c = 0;
}
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;
}
}
}
public void cc() {
byte[] bArr = new byte[]{(byte) 26, (byte) 27, (byte) 30, (byte) 4, (byte) 21, (byte) 2, (byte) 18, (byte) 7};
for (int i = 0; i < 8; i++) {
bArr[i] = (byte) (bArr[i] ^ this.f2k[i]);
}
if (new String(bArr).compareTo("\u0013\u0011\u0013\u0003\u0004\u0003\u0001\u0005") == 0) {
Toast.makeText(this.f0a.getApplicationContext(), new String(C0004.m0(flag, this.f2k)), 1).show();
}
}
}
So in order to get our flag, we have to run C0004.m0(flag, this.f2k)
, unfortunately we don't know what this.f2k
is because of this line this.f2k[this.f1c] = (byte) intent.getExtras().getInt("id");
. If we look in the C0003S.java file, we see the following lines:
Intent intent = new Intent(C0003S.f3I);
intent.putExtra("id", i);
activity.sendBroadcast(intent);
And i
can take on any random value from 0 to 32. However, we can figure out this.f2k
by doing some simple xor: we know each value of bArr[]
will be xored with each value from this.f2k
to get a new bArr[]
, but we know that new String(bArr)
has to equal "\u0013\u0011\u0013\u0003\u0004\u0003\u0001\u0005"
, so we know that bArr[]
has to have values of [0x13,0x11,0x13,0x3,0x4,0x3,0x1,0x5]
and then we can xor that with new byte[]{(byte) 26, (byte) 27, (byte) 30, (byte) 4, (byte) 21, (byte) 2, (byte) 18, (byte) 7};
in order to get this.f2k
and then we can get our flag. This is done in FoodSolve.java.
When you run FoodSolve.java you will get the flag: CTF{bacon_lettuce_tomato_lobster_soul}
.
Our flag is CTF{bacon_lettuce_tomato_lobster_soul}
Very fun and interesting problem, learned a lot about reversing apks!