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
<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.

PartOneActivity
The PartOne acitivity only has a button that gives use hints when we click on it:
Deep links.
Params.
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 ofPartTwoActivity
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.
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 oflight
andswitch
with a value ofon
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
Make sure to escape the &
when passing multiple parameters !
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 🥳

Last updated
Was this helpful?