BSides SF CTF 2017 – Flag Receiver - Mobile Reverse Engineering

Reading time ~6 minutes

The second mobile reversing challenge of BSides SF CTF. Slightly harder than the first but only just. Here’s the clue and APK:

Flag Receiver - 200 Here is a simple mobile application that will hand you the flag.. if you ask for it the right way.

P.S, it is meant to have a blank landing activity :)Use string starting with Flag:


Upon examining the code using Jadx-Gui (my new favourite Java decompiler since SANS Holiday Hack Challenge 2016) we get a feeling right away as to why the MainActivity is blank with no UI. It’s expecting interaction via “other” methods. See this code snippet:

public class MainActivity extends Activity {
    protected void onCreate(Bundle savedInstanceState) {
        TextView tv = new TextView(getApplicationContext());
        tv.setText("To-do: UI pending");
        IntentFilter filter = new IntentFilter();
        registerReceiver(new Send_to_Activity(), filter, permission._MSG, null);

It’s expecting to receive an “intent” called com.flagstore.ctf.INCOMING_INTENT. If it receives the intent it will handle it with the Send_to_Activity which does a check for the “extras” field msg for a “magic word”:

public class Send_to_Activity extends BroadcastReceiver {
    public void onReceive(Context context, Intent intent) {
        if (intent.getStringExtra("msg").equalsIgnoreCase("OpenSesame")) {
            Log.d("Here", "Intent");
            context.startActivity(new Intent(context, CTFReceiver.class));
        Toast.makeText(context, "Ah, ah, ah, you didn't say the magic word!", 1).show();

Once that check is passed, the CTFReceiver class is called to display a button, which, when clicked will call a JNI library function called getPhrase() with three arguments. The return value from getPhrase() then gets broadcast as an outgoing intent.

public void onClick(View v) {
    Intent intent = new Intent();
    String a = CTFReceiver.this.getResources().getString(R.string.str3) + "fpcMpwfFurWGlWu`uDlUge";
    String b = Utilities.doBoth(CTFReceiver.this.getResources().getString(R.string.passphrase));
    String name = getClass().getName().split("\\.")[4];
    intent.putExtra("msg", CTFReceiver.this.getPhrase(a, b, Utilities.doBoth(name.substring(0, name.length() - 2))));

The content of this intent is what we assume to be the flag?

So in order to get the flag it should be as simple as broadcasting an intent with a extras msg field of “OpenSesame”. Then somehow receiving that broadcast outgoing intent. We’ve done this before last year for Google CTF. So we give this a try.

The first step is to boot an AVD (Android Virtual Device) in the Android SDK. I tend to use my Santoku Linux VM for this it’s a handy place to lock all my mobile RE tools away in. If you extract the shared library files from the APK you notice they’ve kindly provided .so files for ARM and x86 so almost any AVD should work. I use an x86 one for speed.

Once booted, I validate adb can see it and install the APK:

santoku@santokuvm:~/bsides$ adb devices
List of devices attached
emulator-5554  device
santoku@santokuvm:~/bsides$ adb install flagstore.apk 
[100%] /data/local/tmp/flagstore.apk
  pkg: /data/local/tmp/flagstore.apk

Once the app is installed, I load it and, sure enough, no UI:

I use adb to broadcast the correct intent with the correct msg field:

santoku@santokuvm:~/bsides$ adb shell am broadcast -a com.flagstore.ctf.INCOMING_INTENT --es msg "OpenSesame" 
Broadcasting: Intent { act=com.flagstore.ctf.INCOMING_INTENT (has extras) }
Broadcast completed: result=0

Which we predictably get a big “Broadcast” button in-app:

Clicking the button dashes our hopes as the app crashes…

Examining the logs using adb logcat -d we see a Java stacktrace of why it failed, apparently the getPhrase() JNI library returned invalid characters. Given this is a congratulatory message, perhaps we’re close?

02-14 22:41:40.834  1089  1089 F art     : art/runtime/] JNI DETECTED ERROR IN APPLICATION: input is not valid Modified UTF-8: illegal continuation byte 0xd1
02-14 22:41:40.834  1089  1089 F art     : art/runtime/]     string: 'CongratsGoodWorkYouFoundIy^Pp<E0><8C><D1>^P<E1><A4>Ve^Q<A5>̼'
02-14 22:41:40.834  1089  1089 F art     : art/runtime/]     in call to NewStringUTF
02-14 22:41:40.835  1089  1089 F art     : art/runtime/]     from java.lang.String com.flagstore.ctf.flagstore.CTFReceiver.getPhrase(java.lang.String, java.lang.String,

Time to break out IDA Pro and examine this shared library function which is stored within the APK file in the lib/x86/ path. We can extract it using APK tool or unzip, like so:

santoku@santokuvm:~/bsides$ apktool d flagstore.apk 
I: Using Apktool 2.1.1 on flagstore.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: /home/santoku/apktool/framework/1.apk
I: Regular manifest package...
I: Decoding file-resources...
I: Decoding values */* XMLs...
I: Baksmaling classes.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...
santoku@santokuvm:~/bsides$ cd flagstore/lib/x86/
santoku@santokuvm:~/bsides/flagstore/lib/x86$ file ELF 32-bit LSB  shared object, Intel 80386, version 1 (SYSV), dynamically linked, BuildID[sha1]=3614bf743b9b0565a9109432080e2b32fd861f30, stripped

If we open it in IDA Pro, we can see the getPhrase() function becomes Java_com_flagstore_ctf_flagstore_CTFReceiver_getPhrase() and the three arguments get shifted to the right by two places due to the peculiarities of the JNI. So the arguments from the Java code are now a3, a4, and a5 in the partial psuedocode below.

int __cdecl Java_com_flagstore_ctf_flagstore_CTFReceiver_getPhrase(int a1, int a2, int a3, int a4, int a5)
  v21 = '^';
  v20 = 'v}rl';
  v19 = 'PijM';
  v18 = 'e_oB';
  v17 = 'wdAD';
  v16 = 'NEHf';
  *v15 = 'H~A@';
  strncat(v15, v6, 51u);
  strncpy(v23, v7, 76u);
  strncpy(v22, v8, 76u);
  v9 = 0;
    v10 = v15[v9] ^ v23[v9] ^ v22[v9];
    v13[v9] = v10;
    printf("%c\n", v10);
  while ( v9 != 76 );
  v14 = 0;
  printf("Here is your Reply: %s", v13);
  result = (*(*a1 + 668))(a1, v13);
  v12 = *MK_FP(__GS__, 20);
  return result;

All this is really doing is, appending a hardcoded string from the library itself to a3, and then XOR’ing a3, a4, and a5 together and returning the result. Why isn’t it working? I’m not really sure. Time for debugging Davlik with IDA Pro to capture our arguments to getPhrase() so we can try doing this XOR operation ourselves.

IDA Pro’s Davlik debugger is pretty awesome. It works by simply opening the APK file in IDA Pro and selecting the classes.dex file when prompted. You can now set breakpoints withing the Davlik bytecode and it will communicate via ADB to your running Android Virtual Device.

Since we know we want to check the arguments to getPhrase() we find the CTFReceiver class in IDA Pro by searching in the functions window:

If we double click this CTFReceive$1_onClick, we can eventually come across the code at offset 0xf46be which is an invoke-virtual call of CTFReceiver.getPhrase(). We right click and set a breakpoint here:

Next we start debugging the app. First we need to set the debugger options properly under the Debugger -> Debugger Options -> Set Specific Options menu.

  • Set the path to adb.exe
  • Populate the Package Name and Activity from the AndroidManfiest.xml

It should look something like this when ready. The APK must also already be installed on the AVD.

Once ready click Play in IDA Pro to begin the debugger. The app should run on the phone, you may need to click play a few times but you want to get to the point where IDA Pro has a “Please wait… Running” popup box with a “Suspend” button.

Send the intent to the AVD like we did before. This time I’m doing it from within Windows

C:\Program Files (x86)\Android\android-sdk\platform-tools>adb shell am broadcast -a com.flagstore.ctf.INCOMING_INTENT --es msg "OpenSesame"
Broadcasting: Intent { act=com.flagstore.ctf.INCOMING_INTENT (has extras) }
Broadcast completed: result=0

And again, click the Broadcast button in the app. This should allow us to reach our code path where our breakpoint is set. The app wont crash this time instead, the debugger should show the instruction pointer is at our breakpoint.

Open the “locals” window to view the local variables in the current context (Debugger -> Debugger Windows -> Locals). We should now see the current state of the three important variables: a, b and c which correspond to our a3, a4, and a5 respectively in the native library psuedocode we saw above. The values are:

  • a = "wgHoNi[nvVfptxF@hpsd9DhrM@sz]fpcMpwfFurWGlWuuDlUge”`

Nice. Don’t forget, to get the full length value of a we need to prepend that static string from the native library binary. We use the “Strings Window” in IDA Pro to get the correct byte order and full value of that string: “@A~HfHENDAdwBo_eMjiPlr}v^”

So now our 3 values are:

  • a = "@A~HfHENDAdwBo_eMjiPlr}v^wgHoNi[nvVfptxF@hpsd9DhrM@sz]fpcMpwfFurWGlWuuDlUge”`

Now we can apply the transformation from the shared library ourselves with a little bit of Python:

a = "@A~HfHENDAdwBo_eMjiPlr}v^"
a = a + "wgHoNi[nvVfptxF@hpsd9DhrM@sz]fpcMpwfFurWGlWu`uDlUge"
out = ""
for i in range(len(a)):
    out += chr(ord(a[i]) ^ ord(b[i]) ^ ord(c[i]))
print out

Which reveals the flag!

root@kali:~/bsides/re/flagstore# python

Interviewing in Tech: Security Engineer & Security Analyst

Landing a job as a security engineer or analyst at a tech company is a significant feat. It requires not only technical acumen but also s...… Continue reading

BSides Sydney 2023 Writeups

Published on November 24, 2023

DUCTF 2023 Writeups

Published on August 31, 2023