This article mainly explains how to use dynamic proxy technology Hook to remove AMS services from the system to achieve the start-up process of intercepting Activity. The amount of code is not very large. In order to understand it more easily, we need to master the reflection of JAVA, dynamic proxy technology, and the start-up process of Activity.
If you have forgotten some of the above knowledge points, it is recommended to read the following three articles on demand, otherwise skip.
- Java reflection
- Analysis and Extension of Java Dynamic Agent Mechanism, Part 1
- Deep Understanding of Activity Startup Process (3) - Detailed Process of Activity Startup 1
1. The Principle of Finding Hook Points
Android mainly relies on the analysis of system source classes to do so, first we have to find the object that is Hook, which I call Hook point; what kind of object is better Hook? Naturally, it's easy to find objects. What kind of objects are easy to find? Static variables and instances; within a process, static variables and instance variables are relatively difficult to change, so they are very easy to locate, while ordinary objects are either unable to mark or easy to change. We find the so-called Hook point according to this principle.
2. Finding Hook Points
Usually when you click on a Button, you start the Activity jump. What happens in the process? How can we Hook to intercept the Activity start?
public void start(View view) {
Intent intent = new Intent(this, OtherActivity.class);
startActivity(intent);
}
Our goal is to intercept the startActivity method, track the source code, and find that the final startup Activity is done by the execStartActivity of the Instrumentation class. Actually, this class is equivalent to the middleman of launching Activity, which operates in the middle of launching Activity.
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
IApplicationThread whoThread = (IApplicationThread) contextThread;
....
try {
intent.migrateExtraStreamToClipData();
intent.prepareToLeaveProcess(who);
//Get an object through ActivityManagerNative.getDefault() and start a new Activity
int result = ActivityManagerNative.getDefault()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options);
checkStartActivityResult(result, intent);
} catch (RemoteException e) {
throw new RuntimeException("Failure from system", e);
}
return null;
}
For Activity Manager Native, you are familiar with the start-up process of Activity/Service.
public abstract class ActivityManagerNative extends Binder implements IActivityManager
It inherits Binder and implements an IActivityManager interface, which is the Stub class for remote service communication. A complete AID L has two parts, one is a Stub communicating with the server, the other is a Proxy communicating with the client. Activity Manager Native is Stub. Read the source code and find that there is also an Activity Manager Proxy in the Activity Manager Native file, so let's not talk about it here.
static public IActivityManager getDefault() {
return gDefault.get();
}
Activity Manager Native. getDefault () gets an IActivityManager object, which is started by IActivityManager. The implementation class of IActivityManager is ActivityManagerService. ActivityManagerService is in another process. All activities start is a cross-process communication process, so the real initiation of activity is through the remote service ActivityMana. GerService to start.
private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() {
protected IActivityManager create() {
IBinder b = ServiceManager.getService("activity");
if (false) {
Log.v("ActivityManager", "default service binder = " + b);
}
IActivityManager am = asInterface(b);
if (false) {
Log.v("ActivityManager", "default service = " + am);
}
return am;
}
In fact, gDefalut implements the singleton pattern with Singleton. Internally, we can see that the Binder object of AMS remote service is obtained from Service Manager, and then the cost localization object is transformed by using asInterface method. Our purpose is to intercept startActivity, so changing the IActivityManager object can achieve this point. Here gDefault is static, and according to Hook principle, this point is achieved by changing the IActivityManager object. It's a better Hook point.
3. Hook drops startActivity and outputs logs
Let's first implement a small requirement to print a log when we start Activity.
public class HookUtil {
private Class<?> proxyActivity;
private Context context;
public HookUtil(Class<?> proxyActivity, Context context) {
this.proxyActivity = proxyActivity;
this.context = context;
}
public void hookAms() {
//Reflect all the way until you get the object of IActivityManager
try {
Class<?> ActivityManagerNativeClss = Class.forName("android.app.ActivityManagerNative");
Field defaultFiled = ActivityManagerNativeClss.getDeclaredField("gDefault");
defaultFiled.setAccessible(true);
Object defaultValue = defaultFiled.get(null);
//Reflective SingleTon
Class<?> SingletonClass = Class.forName("android.util.Singleton");
Field mInstance = SingletonClass.getDeclaredField("mInstance");
mInstance.setAccessible(true);
//You've got the ActivityManager object here.
Object iActivityManagerObject = mInstance.get(defaultValue);
//Start dynamic proxy, replace the real Activity Manager with proxy objects, hide the truth and cross the sea
Class<?> IActivityManagerIntercept = Class.forName("android.app.IActivityManager");
AmsInvocationHandler handler = new AmsInvocationHandler(iActivityManagerObject);
Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class<?>[]{IActivityManagerIntercept}, handler);
//Now replace this object
mInstance.set(defaultValue, proxy);
} catch (Exception e) {
e.printStackTrace();
}
}
private class AmsInvocationHandler implements InvocationHandler {
private Object iActivityManagerObject;
private AmsInvocationHandler(Object iActivityManagerObject) {
this.iActivityManagerObject = iActivityManagerObject;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Log.i("HookUtil", method.getName());
//I'm going to do something here.
if ("startActivity".contains(method.getName())) {
Log.e("HookUtil","Activity Start-up has begun.");
Log.e("HookUtil","My little brother is here!!!");
}
return method.invoke(iActivityManagerObject, args);
}
}
}
Combining annotations should be easy to understand and configure in Application
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
HookUtil hookUtil=new HookUtil(SecondActivity.class, this);
hookUtil.hookAms();
}
}
Look at the results of the implementation:
As you can see, our successful Hook dropped startActivity and output a log. With the above foundation, now let's start with something useful. Activity can be started without registering in the manifest file. What's wrong with this?
4. Start Activity without registration
As follows, Target Activity is not registered in the manifest file. How to start Target Activity?
public void start(View view) {
Intent intent = new Intent(this, TargetActivity.class);
startActivity(intent);
}
This idea can be as follows: above has intercepted the activation activity process, in invoke we can get the activation parameter intent information, so here we can construct an intent of the false activation information ourselves, which is registered in the manifest file when the Intent starts (after the Activity Manager Service checks the manifest file). The agent's Intent is swapped over with the real Intent and then started.
First, get the intent information of the real startup parameter
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("startActivity".contains(method.getName())) {
//Change
Intent intent = null;
int index = 0;
for (int i = 0; i < args.length; i++) {
Object arg = args[i];
if (arg instanceof Intent) {
//The Intent parameter of startActivity has been found.
intent = (Intent) args[i];
//This intent cannot be started because Acitivity is not registered in the manifest file
index = i;
}
}
//Forge an agent's Intent, which starts proxy Activity
Intent proxyIntent = new Intent();
ComponentName componentName = new ComponentName(context, proxyActivity);
proxyIntent.setComponent(componentName);
proxyIntent.putExtra("oldIntent", intent);
args[index] = proxyIntent;
}
return method.invoke(iActivityManagerObject, args);
}
With the above two steps, the agent's Intent can be verified by Activity Manager Service because I registered it in the manifest file.
<activity android:name=".ProxyActivity" />
In order not to start Proxy Activity, now we need to find a suitable time to replace the real Intent and start Activeness that we really want to start. Friends who have seen Activity's start-up process know that this process is implemented by Handler sending messages. However, according to the code of Handler processing messages, the distribution and processing of messages are orderly. The following is the code of Handler processing messages:
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
When handler processes messages, it first checks whether the callback interface is implemented. If it is implemented, it will directly execute the interface method, then the handleMessage method, and finally the handleMessage method, which is rewritten most of the time. The main thread of ActivityThread uses the rewritten method. The priority of the method is the lowest. We can completely implement the interface to replace the handler process. See here for details. Android Source Parsing Handler Series Part 1 - Message Global Pool
public void hookSystemHandler() {
try {
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
//Get the main thread object
Object activityThread = currentActivityThreadMethod.invoke(null);
//Get the mH field
Field mH = activityThreadClass.getDeclaredField("mH");
mH.setAccessible(true);
//Get Handler
Handler handler = (Handler) mH.get(activityThread);
//Get the original mCallBack field
Field mCallBack = Handler.class.getDeclaredField("mCallback");
mCallBack.setAccessible(true);
//Here we set up the CallBack object that we implemented the interface ourselves.
mCallBack.set(handler, new ActivityThreadHandlerCallback(handler)) ;
} catch (Exception e) {
e.printStackTrace();
}
}
Custom Callback Class
private class ActivityThreadHandlerCallback implements Handler.Callback {
private Handler handler;
private ActivityThreadHandlerCallback(Handler handler) {
this.handler = handler;
}
@Override
public boolean handleMessage(Message msg) {
Log.i("HookAmsUtil", "handleMessage");
//Replace the previous Intent
if (msg.what ==100) {
Log.i("HookAmsUtil","lauchActivity");
handleLauchActivity(msg);
}
handler.handleMessage(msg);
return true;
}
private void handleLauchActivity(Message msg) {
Object obj = msg.obj;//ActivityClientRecord
try{
Field intentField = obj.getClass().getDeclaredField("intent");
intentField.setAccessible(true);
Intent proxyInent = (Intent) intentField.get(obj);
Intent realIntent = proxyInent.getParcelableExtra("oldIntent");
if (realIntent != null) {
proxyInent.setComponent(realIntent.getComponent());
}
}catch (Exception e){
Log.i("HookAmsUtil","lauchActivity falied");
}
}
}
Finally, it is injected into application
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
//This ProxyActivity has been registered in the manifest file, and all Activities can use ProxyActivity without declaration to bypass monitoring.
HookAmsUtil hookAmsUtil = new HookAmsUtil(ProxyActivity.class, this);
hookAmsUtil.hookSystemHandler();
hookAmsUtil.hookAms();
}
}
To execute, click the button in MainActivity and successfully jump to Target Activity. This is the first in a series of plug-in blogs.
Please accept mybest wishes for your happiness and success !