Write Up IntechCTF — Game

Maulvi Alfansuri
12 min readOct 24, 2022

--

Continuing from the previous article (https://medium.com/@maulvialf/write-up-intechctf-android-challenge-1ec1b1822be0), I would share write-up about challenge IntechCTF named game.

Challenge Description

This challenge consist of few problems and had different flags on each problem. Challenge can be accessed on my challenge repository here. So here the write up.

Table of Content

· Problem 1
· Problem 2
· Problem 3
· Problem 4
· Conclusion

Problem 1

Problem setter only provided us with APK files with no library source files.

APK Files

Launch the application, there should flag input form.

Level 1 Form

Next, open the application on the JADX-Gui to static analysis APK files. On the main activity, there are two native methods named check and Init.

Native Function on the main activity

Function init is called on this method. Function init runs at first before the second native lib function.

Init function called

And function check is called on this method.

Check function called

There are two libraries that exist on the application. On the application, we still did not find that libgame.so is imported on this level so we can ignore this for now.

library list

After doing initial static analysis, next we move to static analysis on the library. Extract the APK files (can be done using unzip) and load libintechfest.so on the decompiler.

This is the init function of library libintechfest.so.

Init function libintechfest.so

On these functions, there are static variables g_key that process some kind of encryption/decryption. Below is the value of g_key.

g_jkey

Extract value and do the decryption process would get the first flag.

decrypt g_key
decrypt g_key

Problem 2

After we get the first flag, let's analyze the init function again. On line 43–44, memory gets xored by value of g_key after decryption. Memory that xored is addressed from .text segment.

.text is decrypted on the runtime by xored with decrypted g_key

Look up the .text segments, the byte code of the instruction is encrypted.

encrypted .text segment

According to this information, we know that the application should decrypt the .text segment on the library on the init function on the memory. From this information, we know the application should have a library that decrypted after the init function. So let's try to dump the library using Frida.

Frida is able to dump memory on the runtime. There are a few approaches to this. On this challenge, I use these tools to dump decrypted library. Clone the files and run dump_so.py with the library as a parameter.

Dump library from memory

After library file is dumped, open the library on the decompiler and we should see that .text segments are already decrypted and able to be analyzed.

After decrypted, new library would had new function

Now we should look at checkFlag function.

Function checkFlag

On the checkFlag, there are functions level1 and level2. We can check this is there any other level function from function list.

List Level on current library

There are functions level1 and level2. The first and second functions only show the pointer and execute the function pointer.

Level 2 pointer
Level 1 Pointer

However, some string decompilation showed on the third function on the list.

Level 1 function show a broken function

Decompilation shows a broken function, let's check the disassembly view.

Disassembly shown instruction code did not defined as function.

On the disassembly view, there are code on the .text segment that did not assigned as function. Lets declare the function and see what its look like. To create bytecode as function, right click on the bytecode position and choose create function.

Define function manually

After we create function from the bytecode, we should see the decompiled code like this

After defined level 1 function can be decompiled nicely

From decompiled function, we know that application do another decrypting function on the segment .level2 when this function called. This function is created below the function 1, that we can assume this code is part of function level1. Lets analyze this function.

Function level1, had two parameter, int64 a1 and int a2. First parameter used on function sub_104d0. However after we tried hooking this function with Frida application failed show anything useful. I still did not know why sometimes frida failed to hook native function, my hypothesis is there are error parse (decompiler also found error positive sp value has been detected). So we find another function that can be hook and had same parameter.

If you noticed, this function looks compromising to hook. It had first parameter same with sub_10610 and second parameter is return value of something decrypted, looks so legit.

Function sub_104D0 seems compromising to hook

The function also did not had some kind of error that maybe broken like function before.

Function did not had any broken flow

Lets hook this function with frida. Below is function hook code.

Hook function 104D0 and get the parameter

Run the hooks, and input AAAA in input form.

Hook return flag on the decrypted

Great, we know hat second parameter is another flag. Our input is compared with decrypted flag. Lets input itf{h1dd3n_bY_w3ak_3ncrypt10n} and we successfully continued to level2 or in this case problem 3.

Problem 3

After we submit the flag, we should redirected to level 2. There are information is that app’s name is weird. So in this case problem related to filename checking. Lets noted this for later.

Level 2 with only button click

Click Go To Level 3 did not show anything. Different from second problem or level1 that had input form, on this level we did not provided with form.

Click on the button only show try again

Java code did not show anything different from level 1 or level 2. There are no code that significantly change.

Form function on the java

Lets check again native function checkFlag on the library that we already decrypted.

Native function CheckFlag

On the function checkFlag, we know that parameter a1 is game level. If value a3 equal to 1, function level1 would executed. So level2 supposed to be our challenge now.

Look on the function level2 had weird function. Function level2 called another declared object.

Level2 pointer

However this function is did not like instruction bytecode. We can check this by viewing the disassembly.

Level 2 pointer
Level 2 code that encrypted

Welp, it seems level2 had instruction encrypted on the .text segment. But why this happened?. We know that this library decrypted on the first phase before we clearing level1. So it seems, after clearing level1, another library segment would (in this case level2) decrypted.

This can be confirmed by disassembly code of function level1.

Decryption of level 2 code
Dissasembly of code decryption

If we dissect the function more deeper, there are method decrypt_section executed and had parameter .level2.

So how to get new library that had .level2 segment decrypted this time. Same with first problem. We should dump library again!. Lets run another dump_so after we clearing level1.

Dump library on new level

After new library acquired, lets open new library on the decompiler. Function level2 now is created and already decrypted.

Another function generated after dump new library

Now lets analyze function level2. Below is code of level2.

Function level2 decompiled

Function level2 had complexity more than function level1 that we solved before. But lets break it down the function.

On the line 35–49 there are generated some string that decrypted and saved on parameter named pointer. Pointer used by function pthread_setspecific.

On the line 55–75 there are JNI function call executed. However method and java classname is decrypted on the runtime. But we should able to hook the function call parameter and return value. Below is hook code to retrieve return from function SSM:Decrypt.

Hook SSM::Decrypt with frida

Run the frida script would return this output below. There are few times application executed.

Dump library on new level

Lets rewrite decompiled code with the decrypted string. Below is code that we already modified with decrypted string.

Snippet code after decryption

In general this code would called method loadLabel which part of method in getApplicationInfo and return the value in CharSequence. After we research this, we found out loadLabel that used exist on the class https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/content/pm/PackageItemInfo.java.

LoadLabel on the android source code

Lets hook this function with frida to know the return value.

Hook loadlabel

Run the script would return this output.

Frida return label as application name

Load label method had value nHVZeGukN75PpvXrhtOe that similar to package name. So application supposed to be compared this string to pointer that decrypted here. However function spawned with pthread_setspicific.

Pointer decrypted on this function
Function called with pthread

Lets check another approach. If application compared string on the library, application maybe had a chance used strcmp on the comparison. We able to hook strcmp and filtered to only show parameter when one of the parameter had value nHVZeGukN75PpvXrhtOe. From this, we should able to retrieve another parameter that compare. This is frida code that implement that logic.

Hook strcmp and only shown output when match

Running the script, would return another parameter named Circle Of Trust. Great, Our guess luckily success.

Run the hook would shown string Circle of Trust

Lets hook loadLabel again to replace nHVZeGukN75PpvXrhtOe to Circle Of Trust.

Hook loadLabel and replaced with Circle of Trust

Run this frida script. And click the go to Level 3 and great, we able to continued to next level.

Run the replaced hook
We successfully continued to level3

Problem 4

After we completed the Level 2, We able to access next level. On this level there are some kind of game.

New Level 3

If we click play game, there are pop up window like this.

Preparation to access level 3

Click go, we should redirected to the game. The game need us to click 10 times to circles that generated on the screen. All 9 circle can be clicked easily.

game want player click 10 circle to get the flag

However real struggle exist on the final circle. Final circle would would spawn circle randomly that nearly impossible to click.

Final circle point spawn randomly in milisecond and impossible to click

After few trying that failed. Lets look at the code instead. After successfully pass application name check. We should go to code that decrypt .level3. This approach same with decryption from level2.

level 3 decryption code

Below is instruction code that encrypted. There are function initDex that named on the encrypted code.

Level 3 encrypted code shown function named initDex

Same with dump library from before, Lets dump binary again .

Another dump library

After new library generated, lets open the library on the decompiler to analysis. After opening the binary on the decompiler, we should get new function named initDex. On the application string decrypted with same SSM:Decrypt.

initDex function decompiled

Lets running SSM:Decrypt hook again chained with loadlabel hook to view what string that decrypted this time.

hook loadlabel and decrypt string

Run this script would show output below.

Decrypted new string on the level3

From the string that shown, application called java code that getAsset and get game.dex files. That confirmed assets also exist on the application resources. There also format string that supposed to /data/data/com.intechfest.game/cache. There also some kind of random key n1n0_k4w4ii. From these information we get know that application would read file game.dex on the assets, processed somehow with key and write on the application cache.

game.dex on the assets directory

This confirmed that file game.dex in the assets is not valid as dex that indicated this file encrypted.

File game.dex is encrypted

If we look application folder cache, there are few cache dex that are generated.

File game.dex on the application cache.

Dump this file and we know that this file is decrypted file of game.dex. Great !.

Dump cache files and we get decrypted game.dx

Open the dex files to Jadx-Gui would get class named GameView.

GameJava class

We got logic from the game !. But if we hook this on frida, frida would shown output like this.

Hook runtime dex would shown error

This can be happened because dex file run on the runtime. Default frida hook only able hook that already defined DexPathList. But this can be modified.

We found this issue on frida repo https://github.com/frida/frida-java-bridge/issues/227. From this issue we should able to load runtime dex if we overload dalvik_system_BaseDexClassLoader and reload the loader. If we reload the loader, Frida would generated the dex that load and we should be able to hook runtime dex.

Now which function that we wanna hook?. Lets check method onTouchEvent. On this method, our click would compared. If we success, parameter m_score would increased by one and submitScore() executed one time. Lets doubled it on so our score did not touch 9 and directly get to 10. Lets also hook get_flag method to know the return value.

Below is code that implemented load label override, decrypt string and hook runtime dex.

Final script hook

Run the Frida script, and every touch that we click would get 2 score and we able to get the flag. However the flag is cropped and did not show full on the screen.

Flag shown after 10 points reached. But flag string is cropped

Check on the Frida output, and we get full flag itf{0bFusC4t10n_V3rryy_AnN0y1nG_R1gHt} and completed all the level on this challenge.

Flag shown on function getFlag return value

Conclusion

I enjoyed a lot doing this challenge. I learn a lot about hooking native function, dumping library, library encryption on the android, override internal android class and hook runtime dex class. This challenge also can be solvable from different way (in example we able to decrypt the string by static analysis). Kudos for the problem setter (https://github.com/aimardcr and teams) that created this challenge.

--

--