IV - Android Reverse Engineering

Let's start by decompiling the APK using JADX which is a Dex to Java decompiler:

jadx BountyPay.apk

In our case the interesting files are going to be in /sources/bounty:

/BountyPay/sources/bounty
└── pay
    ├── BuildConfig.java
    ├── CongratsActivity.java
    ├── MainActivity.java
    ├── PartOneActivity.java
    ├── PartThreeActivity.java
    ├── PartTwoActivity.java
    └── R.java

1 directory, 7 files

We can see that there is one MainActivity and three activities named PartOne, PartTwo and PartThree.

Looking at the Manifest we can see that there are actually 5 activities define:

  • bounty.pay.MainActivity

  • bounty.pay.PartOneActivity

  • bounty.pay.PartTwoActivity

  • bounty.pay.PartThreeActivity

  • bounty.pay.CongratsActivity

AndroidManifest.xml
<activity android:theme="@style/AppTheme.NoActionBar" android:label="@string/title_activity_congrats" android:name="bounty.pay.CongratsActivity"/>
<activity android:theme="@style/AppTheme.NoActionBar" android:label="@string/title_activity_part_three" android:name="bounty.pay.PartThreeActivity">
    <intent-filter android:label="">
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
        <data android:scheme="three" android:host="part"/>
    </intent-filter>
</activity>
<activity android:theme="@style/AppTheme.NoActionBar" android:label="@string/title_activity_part_two" android:name="bounty.pay.PartTwoActivity">
    <intent-filter android:label="">
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
        <data android:scheme="two" android:host="part"/>
    </intent-filter>
</activity>
<activity android:theme="@style/AppTheme.NoActionBar" android:label="@string/title_activity_part_one" android:name="bounty.pay.PartOneActivity">
    <intent-filter android:label="">
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
        <data android:scheme="one" android:host="part"/>
    </intent-filter>
</activity>
<activity android:name="bounty.pay.MainActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>
</activity>

Something interesting to note is that PartOne, PartTwo and PartThree activities all defined an intent filter a scheme and an host:

  • one://part

  • two://part

  • three://part

Let's install the app on our favorite emulator (here I'm using Android Studio's emulator) using adb:

adb install BountyPay.apk

The fist screen invites us to enter a username and a Twitter handle.

BountyPay

PartOneActivity

The PartOne acitivity only has a button that gives use hints when we click on it:

  • Deep links.

  • Params.

Deeplinks are a concept that help users navigate between the web and applications. They are basically URLs which navigate users directly to the specific content in applications. Optionally, some data or parameter can be passed along.

When we look at the code we can see the corresponding code:

public void onClick(View view) {
  if (PartOneActivity.this.click == 0) {
    Snackbar.make(view, (CharSequence) "Deep links.", 0)
    .setAction((CharSequence) "Action", (View.OnClickListener) null)
    .show();
    PartOneActivity.this.click++;
  } else if (PartOneActivity.this.click == 1) {
    Snackbar.make(view, (CharSequence) "Params.", 0)
    .setAction((CharSequence) "Action", (View.OnClickListener) null)
    .show();
    PartOneActivity.this.click = 0;
  }
}

Afterwards we can see that there are a few conditions to continue to the next activity:

  • The setting should contains a username (set on the first screen)

  • A query parameter named start should be present with a value of PartTwoActivity

If all the condiditions are met the PartTwo activity is started.

if (!settings.contains("USERNAME")) {
  Toast.makeText(getApplicationContext(), "Please create a CTF username :)", 0)
  .show();
  startActivity(new Intent(this, MainActivity.class));
}
if (getIntent() != null && getIntent().getData() != null 
    && (firstParam = getIntent().getData().getQueryParameter("start")) != null 
    && firstParam.equals("PartTwoActivity") 
    && settings.contains("USERNAME")) {

  String user = settings.getString("USERNAME", "");
  SharedPreferences.Editor editor = settings.edit();
  String twitterhandle = settings.getString("TWITTERHANDLE", "");
  editor.putString("PARTONE", "COMPLETE").apply();
  logFlagFound(user, twitterhandle);
  startActivity(new Intent(this, PartTwoActivity.class));
}

This can be done using adb using the following command:

adb shell am start -W -a android.intent.action.VIEW \
-d "one://part?start=PartTwoActivity" bounty.pay
Starting: Intent { act=android.intent.action.VIEW dat=one://part?start=PartTwoActivity pkg=bounty.pay }
Status: ok
Activity: bounty.pay/.PartOneActivity
ThisTime: 470
TotalTime: 470
WaitTime: 499
Complete

PartTwoActivity

Here again we are presented with a white screen with a button giving us two hints:

  • Currently invisible.

  • Visible with the right params.

This seems to imply that there is some invisible content that will be revealed if we send the right parameters. Let's look at the code.

PartTwoActivity.java
public void onCreate(Bundle savedInstanceState) {
  [...]
  editText.setVisibility(4);
  button.setVisibility(4);
  textview.setVisibility(4);
  [...]
  if (!settings.contains("USERNAME")) {
    Toast.makeText(
      getApplicationContext(), "Please create a CTF username :)",
      0
    )
    .show();
    startActivity(new Intent(this, MainActivity.class));
  }
  if (!settings.contains("PARTONE")) {
    Toast.makeText(
      getApplicationContext(),
      "Part one not completed!",
      0
    )
    .show();
    startActivity(new Intent(this, MainActivity.class));
  }
  if (getIntent() != null && getIntent().getData() != null) {
    Uri data = getIntent().getData();
    String firstParam = data.getQueryParameter("two");
    String secondParam = data.getQueryParameter("switch");
    if (firstParam != null &&
      firstParam.equals("light") &&
      secondParam != null &&
      secondParam.equals("on")) {
      editText.setVisibility(0);
      button.setVisibility(0);
      textview.setVisibility(0);
    }
  }
}

This time we can see that some conditions are required to be able to see the invisible content:

  • The username needs to be set

  • Part one needs to be complete

  • Two parameter are required, two with a value of light and switch with a value of on

Here again we can do this using adb:

adb shell am start -W -a android.intent.action.VIEW \
-d "two://part?two=light\&switch=on" bounty.pay
Starting: Intent { act=android.intent.action.VIEW dat=two://part?two=light&switch=on pkg=bounty.pay }
Status: ok
Activity: bounty.pay/.PartTwoActivity
ThisTime: 238
TotalTime: 238
WaitTime: 275
Complete

As expected we can now see an input field expecting a Header value and underneath an MD5 hash.

Clicking on the submit button will trigger the submitInfo function. We can see that the header value should start with X- and if so the correctHeader function will be called which in turns will start PartThreeActivity.

public void submitInfo(View view) {
  final String post = ((EditText) findViewById(R.id.editText)).getText().toString();
  this.childRef.addListenerForSingleValueEvent(new ValueEventListener() {
    public void onDataChange(DataSnapshot dataSnapshot) {
      SharedPreferences settings = PartTwoActivity.this.getSharedPreferences(
        PartTwoActivity.KEY_USERNAME,
        0
      );
      SharedPreferences.Editor editor = settings.edit();
      String str = post;
      if (str.equals("X-" + ((String) dataSnapshot.getValue()))) {
        PartTwoActivity.this.logFlagFound(
          settings.getString("USERNAME", ""),
          settings.getString("TWITTERHANDLE", "")
        );
        editor.putString("PARTTWO", "COMPLETE").apply();
        PartTwoActivity.this.correctHeader();
        return;
      }
      Toast.makeText(PartTwoActivity.this, "Try again! :D", 0).show();
    }

    public void onCancelled(DatabaseError databaseError) {
      Log.e(PartTwoActivity.TAG, "onCancelled", databaseError.toException());
    }
  });
}

/* access modifiers changed from: private */
public void correctHeader() {
  startActivity(new Intent(this, PartThreeActivity.class));
}

PartThreeActivity

This time the conditions that needs to be met are:

  • First param three should be equal to Base64("PartThreeActivity")

  • Second param switch should be equal to Base64("on")

  • Third param header should be equal to the previously defined header, in our case X-Token

if (getIntent() != null && getIntent().getData() != null) {
  Uri data = getIntent().getData();
  String firstParam = data.getQueryParameter("three");
  String secondParam = data.getQueryParameter("switch");
  String thirdParam = data.getQueryParameter("header");
  byte[] decodeFirstParam = Base64.decode(firstParam, 0);
  byte[] decodeSecondParam = Base64.decode(secondParam, 0);
  final String decodedFirstParam = new String(decodeFirstParam, StandardCharsets.UTF_8);
  final String decodedSecondParam = new String(decodeSecondParam, StandardCharsets.UTF_8);
  AnonymousClass5 r17 = r0;
  DatabaseReference databaseReference = this.childRefThree;
  byte[] bArr = decodeSecondParam;
  final String str = firstParam;
  byte[] bArr2 = decodeFirstParam;
  final String str2 = secondParam;
  String str3 = secondParam;
  final String secondParam2 = thirdParam;
  String str4 = firstParam;
  final EditText editText2 = editText;
  Uri uri = data;
  final Button button2 = button;
  AnonymousClass5 r0 = new ValueEventListener() {
    public void onDataChange(DataSnapshot dataSnapshot) {
      String str;
      String value = (String) dataSnapshot.getValue();
      if (str != null && decodedFirstParam.equals("PartThreeActivity") &&
        str2 != null && decodedSecondParam.equals("on") &&
        (str = secondParam2) != null) {
        if (str.equals("X-" + value)) {
          editText2.setVisibility(0);
          button2.setVisibility(0);
          PartThreeActivity.this.thread.start();
        }
      }
    }

    public void onCancelled(DatabaseError databaseError) {
      Log.e("TAG", "onCancelled", databaseError.toException());
    }
  };
  databaseReference.addListenerForSingleValueEvent(r0);
}

Now that we know all the requirement we can send the intent.

adb shell am start -W -a android.intent.action.VIEW \
-d "three://part?three=UGFydFRocmVlQWN0aXZpdHk=\&switch=b24\=\&header=X-Token" bounty.pay

We are then asked to provide a "leaked hash".

Looking into the logs using abd logcat we can quickly see our leaked hash:

adb logcat | grep IS:
05-31 20:32:30.618  5199  6360 D HOST IS: : http://api.bountypay.h1ctf.com
05-31 20:32:30.618  5199  6360 D TOKEN IS: : 8e9998ee3137ca9ade8f372739f062c1

When submitted we get the Congrats activity 🥳

CongratsActivity

Last updated

Was this helpful?