Tags: jni apk android 

Rating: 5.0

## General problem description

```
Win the game 1,000,000 times to get the flag.
```

For this challenge we got an .apk file, which we should apperently run and win 1,000,000 times. We let the online Java-decompiler at `http://www.javadecompilers.com/apk`. Running the apk on an Android-Phone or emulator shows us the game: Tic Tac Toe. We also get a counter `0/1000000` on the bottom of the screen. Each win increases the counter by one.

## Naive approach by recompiling the app

We used the decompiled code from `http://www.javadecompilers.com/apk` and analyzed the generated code. The code consists of 4 Java classes and one native library. Let us first look at the Java-classes:

* C0644N.java: The class is just a wrapper that loads the native library and holds a few float-arrays
```java
class C0644N {
static final int[] f2334a = new int[]{0, 1, 0};
static final int[] f2335b = new int[]{1, 0, 2};
static final int[] f2336c = new int[]{2, 0, 1};
static final int[] f2337d = new int[]{3, 0, 0};
static final int[] f2338e = new int[]{4, 1, 0};
static final int[] f2339f = new int[]{5, 0, 1};
static final int[] f2340g = new int[]{6, 0, 0};
static final int[] f2341h = new int[]{7, 0, 2};
static final int[] f2342i = new int[]{8, 0, 1};

static {
System.loadLibrary("rary");
}

static native Object m3217_(Object... objArr);
}
```
* C0649a.java: This class is responsible for displaying the cells in the Tic Tac Toe field
```java
public class C0649a extends RelativeLayout {
...
}
```
* C0652b.java: This class is responisble for the fade in and out of the symbols in the Tic Tac Toe cells
```java
public class C0652b {
...
}
```
* GameActivity.java: This is the main activity of the App and holds most of the App logic. Here are important passages of the class file together with comments as to what we assumed they do:
```java
public class GameActivity extends C0433c implements OnClickListener {
C0649a[][] f2327l = ((C0649a[][]) Array.newInstance(C0649a.class, new int[]{3, 3}));// initialises the Cells
...
Object f2329n = C0644N.m3217_(3, C0644N.f2341h, Long.valueOf((((((((1416127776 + 1869507705) + 544696686) + 1852403303) + 544042870) + 1696622963) + 544108404) + 544501536) + 1886151033));
...
byte[] f2332q = new byte[32];// empty byte array
byte[] f2333r = new byte[]{(byte) -61, (byte) 15, (byte) 25, (byte) -115, (byte) -46, (byte) -11, (byte) 65, (byte) -3, (byte) 34, (byte) 93, (byte) -39, (byte) 98, (byte) 123, (byte) 17, (byte) 42, (byte) -121, (byte) 60, (byte) 40, (byte) -60, (byte) -112, (byte) 77, (byte) 111, (byte) 34, (byte) 14, (byte) -31, (byte) -4, (byte) -7, (byte) 66, (byte) 116, (byte) 108, (byte) 114, (byte) -122};// pre-filled byte array
public GameActivity() {
C0644N.m3217_(3, C0644N.f2342i, this.f2329n, this.f2332q);// calls the native function
...
}
C0649a m3210a(List<C0649a> list) {
return (C0649a) list.get(((Random) this.f2329n).nextInt(list.size()));// casts the object got from the native funktion to random and uses it to choose the next cell for the app
}
...
//this function is called after 1,000,000 wins
void m3214m() {
Object _ = C0644N.m3217_(0, C0644N.f2334a, 0);//native call
Object _2 = C0644N.m3217_(1, C0644N.f2335b, this.f2332q, 1);//native call
C0644N.m3217_(0, C0644N.f2336c, _, 2, _2);//native call
((TextView) findViewById(R.id.score)).setText(new String((byte[]) C0644N.m3217_(0, C0644N.f2337d, _, this.f2333r)));//print result of native call
...
}
//this function is called if the player wins
void m3215n() {
...
this.f2330o++;//increase counter
Object _ = C0644N.m3217_(2, C0644N.f2338e, 2);//native call
C0644N.m3217_(2, C0644N.f2339f, _, this.f2332q);//native call
this.f2332q = (byte[]) C0644N.m3217_(2, C0644N.f2340g, _);//native call
if (this.f2330o == 1000000) {// wuhuuu 1,000,000 wins
m3214m();
return;
}
((TextView) findViewById(R.id.score)).setText(String.format("%d / %d", new Object[]{Integer.valueOf(this.f2330o), Integer.valueOf(1000000)}));
}
...
}
```
We took this class and simply called the win-method 1,000,000 times and then the win method. To get the result we changed `((TextView) findViewById(R.id.score)).setText(...)` with `System.out.println(...)` calls, so we get the flag in the console. Finally we wrapped all that into a simple Aktivity and tried it out.
```java
GameActivity gameActivity = new GameActivity();
for(int i = 0; i < 1000000; i++)
gameActivity.m3215n();
```
We tried it on 2 different Android devices(ARM based) and the x86_64 emulator of Android Studios. Each run took some time(~20 minutes). All of those resulted in garbage being printed.

## Hey don't forget `Random`

After our first tries failed we looked over the GameActivity-class and noticed we forgot the `nextInt`-calls. The `Random`-object is fetched from the native-library so it is theoretically possible, that either the state of the `Random`-object is important for the native library or the `nextInt` is overloaded inside of the native-library and does something special.

Looking through the code we found that the `nextInt`-method has to be called twice before we can theoretically win, so we add the 2 calls at the start of every "win"-method.

The result was exactly the same as before. There was no change in the output.

## Diving deep into the native library

We started by loading the native library into IDA and decompiling it.
It is important to note, that we created the first argument to a `JNI**`-type, which we modeled after the `jni.h`. We did not model the whole struct, as there are multiple hundred of function-pointers defined in there and we only defined the ones actually needed.
```c
void* __fastcall Java_com_google_ctf_shallweplayagame_N__1(JNI **env, void* arg1, void* arg2){
...
if ( !initialized ) {
init_data();
initialized = 1;
}
v4 = 0LL;
LODWORD(third_arg_array_ele0) = ((int (__fastcall *)(JNI **, void*, _QWORD))(*env)->GetObjectArrayElement)(env, arg2_1, 0LL);
third_arg_array_ele0_1 = third_arg_array_ele0;
LODWORD(Integer_class) = ((int (__fastcall *)(JNI **, const char *))(*env)->FindClass)(env, "java/lang/Integer");
if ( Integer_class ) {
LODWORD(Integer_intValue) = ((int (__fastcall *)(JNI **, void*, const char *, const char *))(*env)->GetMethodID)(env, Integer_class, "intValue", "()I");
if ( Integer_intValue )
v4 = call_method(env, third_arg_array_ele0_1, Integer_intValue, Integer_intValue, v9, v10, v36);
else
v4 = 0LL;
}
LODWORD(class_def) = ((int (__fastcall *)(JNI **, void*))(*env)->FindClass)(env, class_table[v4]);
LODWORD(parameter2) = ((int (__fastcall *)(JNI **, void*, signed __int64))(*env)->GetObjectArrayElement)(env, arg2_1, 1LL);
v14 = 0;
LODWORD(parameter2_int_arr) = ((int (__fastcall *)(JNI **, void*, _QWORD))(*env)->GetIntArrayElements)( env, parameter2, 0LL);
array2_0 = *parameter2_int_arr;
LODWORD(parameter2_int_arr_2) = ((int (__fastcall *)(JNI **, void*, _QWORD))(*env)->GetIntArrayElements)(env, parameter2, 0LL);
array2_1 = *(_DWORD *)(parameter2_int_arr_2 + 4);
LODWORD(parameter2_int_arr_3) = ((int (__fastcall *)(_QWORD, _QWORD, _QWORD))(*env)->GetIntArrayElements)( env, parameter2, 0LL);
array2_2 = *(_DWORD *)(parameter2_int_arr_3 + 8);
LODWORD(object_class_) = ((int (__fastcall *)(JNI **, const char *))(*env)->FindClass)(env, "java/lang/Object");
arg2_1_length = ((int (__fastcall *)(JNI **, void*))(*env)->GetArrayLength)(env, arg2_1);
v24 = 3 - (array2_1 != 0 || array2_2 == 2);
LODWORD(object_array) = ((int (__fastcall *)(JNI **, _QWORD, void*, _QWORD))(*env)->NewObjectArray)( env, (unsigned int)(arg2_1_length - v24), object_class_, 0LL);
v26 = object_array;
v27 = __OFSUB__(arg2_1_length, v24);
v28 = arg2_1_length - v24;
v40 = object_array;
if ( !((unsigned __int8)((v28 < 0) ^ v27) | (v28 == 0)) ) {
do {
LODWORD(v29) = ((int (__fastcall *)(JNI **, void*, _QWORD))(*env)->GetObjectArrayElement)(env, arg2_1, v24 + v14);
((void (__fastcall *)(JNI **, void*, _QWORD, void*))(*env)->SetObjectArrayElement)(env, v26, v14++, v29);
} while ( v28 != v14 );
}
if ( array2_1 ) {
if ( array2_1 == 1 ) {
LODWORD(v30) = ((int (__fastcall *)(JNI **, void*, void*, void*))(*env)->CallStaticObjectMethod)( env, class_def, method_table[array2_0], string_table[array2_0 + 2]);
result = sub_1CA0(class_def, &v40, v30, array2_0, array2_2);
} else {
result = 0LL;
}
} else {
LODWORD(method) = ((int (__fastcall *)(JNI **, void*, void*, void*))(*env)->GetMethodID)(env, class_def, method_table[array2_0], string_table[array2_0 + 2]);
LODWORD(arg2_1_2) = ((int (__fastcall *)(JNI **, void*, signed void*))(*env)->GetObjectArrayElement)( env, arg2_1, 2LL);
result = sub_1480(&v40, class_def, arg2_1_2, method, array2_0, array2_2);
}
v35 = *MK_FP(__FS__, 40LL);
return result;
}
```

The `init_data`-method looks like this:
```
void* init_data(){
...
v2 = 1;
do
{
byte_4010[v2] += byte_4010[v2 - 1];
++v2;
}
while ( v2 < 20 );
class_table[0] = (__int64)byte_4010;
v3 = 1;
do
{
byte_4030[v3] += byte_4030[v3 - 1];
++v3;
}
while ( v3 < 32 );
class_table[1] = (__int64)byte_4030;
...
```

For the init-data, we can see, that for a certain block every byte is summed up with the previous byte. We duplicated this with a simple script and got the following strings:
```
Random
MessageDigest
Cipher
SecretKeySpec
<init>
getInstance
update
digest
getInstance
<init>
init
doFinal
SHA-256
AES/ECB/NoPadding
AES
```
From the `Java_com_google_ctf_shallweplayagame_N__1`we could figure out that the first parameter is used as a decider what class to take. The second parameter is an array where the first element is what method to call. The rest of the parameters are the parameters for the method. We actually do not really care about the second or third array-elements of the second parameter, because we deduced their meaning later on through common sense.

The indices of the classes start at the decoded strings at index 0. The methods at index 4 and some other strings at index 12. Looking back at the Java code we can simplify the code to:
```java
byte[] arr = new byte[32];
byte[] f2333r = new byte[]{(byte) -61, (byte) 15, (byte) 25, (byte) -115, (byte) -46, (byte) -11, (byte) 65, (byte) -3, (byte) 34, (byte) 93, (byte) -39, (byte) 98, (byte) 123, (byte) 17, (byte) 42, (byte) -121, (byte) 60, (byte) 40, (byte) -60, (byte) -112, (byte) 77, (byte) 111, (byte) 34, (byte) 14, (byte) -31, (byte) -4, (byte) -7, (byte) 66, (byte) 116, (byte) 108, (byte) 114, (byte) -122};
new Random((((((((1416127776L + 1869507705L) + 544696686L) + 1852403303L) + 544042870L) + 1696622963L) + 544108404L) + 544501536L) + 1886151033L).nextBytes(arr);

for(int i = 0; i < 1000000; i++){
System.out.println(i);
try {
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
messageDigest.update(arr);
arr = messageDigest.digest();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
try {
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
SecretKeySpec spec = new SecretKeySpec(arr, "AES");
cipher.init(2, spec);
System.out.println(new String(cipher.doFinal(f2333r)));
} catch (NoSuchAlgorithmException | BadPaddingException | NoSuchPaddingException | IllegalBlockSizeException | InvalidKeyException e) {
e.printStackTrace();
}
```
The `SHA-256`, `AES/ECB/NoPadding` and `AES` parameters we guessed as they are common parameters for these functions.

We started this on a normal computer and got the output `CTF{ThLssOfInncncIsThPrcOfAppls}` within a few seconds.

## Rant: Non-Portability of `Random`

This challenge should normally have ended after we let the program run on our Android devices or the emulator, but due to different behaviours of `java.util.Random` the algorithm behaved differently on every single device we tried. The bytes that were saved in the initial byte-array always were different values even though the java-api states differently:
```
If two instances of Random are created with the same seed, and the same sequence of method calls is made for each, they will generate and return identical sequences of numbers. In order to guarantee this property, particular algorithms are specified for the class Random. Java implementations must use all the algorithms shown here for the class Random, for the sake of absolute portability of Java code. However, subclasses of class Random are permitted to use other algorithms, so long as they adhere to the general contracts for all the methods.
```
The class that was created was the `Random`-class and not something different. We checked with the debugger. The results were still different, which was infuriating. Note, that Random always behaved the same for each run on the same hardware/emulator, but on different devices it produced different values. This should not happen, but apparently it does.

Original writeup (https://w0y.at/writeup/2018/07/02/google-ctf-quals-2018-shall-we-play-a-game.html).