Simple practice of Android Hook mechanism

brief introduction

What is Hook

Hook is also called "hook". It can intercept and monitor the transmission of events in the process of event transmission, and integrate its own code with the system method. In this way, when these methods are called, we can execute our own code, which is also the idea of aspect oriented programming (AOP).

Hook classification

1. According to the Android development mode, Native mode (C/C + +) is distinguished from Java mode (Java), which is on the Android platform

  • Hook at Java level;
  • Hook at Native level;

2. The root hook object and hook post-processing events are different. Hook is also divided into:

  • Message Hook;
  • API Hook;

3. For different processes of Hook, it can also be divided into:

  • Global Hook;
  • Single process Hook;

Common Hook framework

In Android development, there are the following common Hook frameworks:

1 . Xposed

By replacing / system / bin / APP_ The process program controls the Zygote process so that the app_process will load xposedbridge during startup Jar, which completes the hijacking of the Zygote process and its created Dalvik virtual machine. Xposed hijacks all hook functions at startup, and adds custom code before and after the execution of the original Function.

2 . Cydia Substrate

Cydia Substrate framework provides Apple users with a service framework related to prison break. Of course, it also launched the Android version. Cydia Substrate is a code modification platform, which can modify the code of any process. Whether written in Java or C/C + + (native code), Xposed only supports hook app_ Java functions in process.

3 . Legend

Legend is an Apk Hook framework in Android Root free environment. The code design of the framework is simple and has high universality. It is suitable for some Hook scenarios in reverse engineering. Most of the functions are placed in the Java layer, so the compatibility is very good. The principle is as follows: directly construct the virtual machine data structure corresponding to the old and new methods, and then write the replacement information to memory.

What Hook must master

  • reflex

If you are not familiar with reflection, I suggest you review the relevant knowledge of java reflection first. If you are interested, you can take a look at my blog to explain the java reflection mechanism in detail

  • Dynamic proxy of java

Dynamic proxy refers to the dynamic generation of proxy classes at runtime. We don't need to manually write one proxy class after another like static proxy. In java, we can use InvocationHandler to implement dynamic proxy. If you are interested, please check my blog for a detailed explanation of java proxy mode

The main content of this article is to explain the Hook of a single process and how to Hook.

Hook usage example

Hook selected keys

  • Hook's selection point: try to static variables and singletons, because once the object is created, they are not easy to change and are very easy to locate.
  • Hook process:
  • The principle of finding Hook points is to try to use static variables or singleton objects and Hook public objects and methods.
  • Choose the appropriate proxy method. If it is an interface, you can use dynamic proxy.
  • Replace the original object with a proxy object.
  • There are many API versions of Android, and the methods and classes may be different, so we should do a good job in API compatibility.

Simple case 1: use Hook to modify view Onclicklistener event

First, let's analyze View Setonclicklistener source code to find the appropriate Hook point. You can see that the OnClickListener object is saved in an internal class called ListenerInfo, where mListenerInfo is the member variable of View. Various listening events of View are saved in ListeneInfo. Therefore, we can find a way to hook ListenerInfo's mOnClickListener.

 1public void setOnClickListener(@Nullable OnClickListener l) {
 2    if (!isClickable()) {
 3        setClickable(true);
 4    }
 5    getListenerInfo().mOnClickListener = l;
 6}
 7
 8static class ListenerInfo {
 9
10     ---
11
12    ListenerInfo getListenerInfo() {
13        if (mListenerInfo != null) {
14            return mListenerInfo;
15        }
16        mListenerInfo = new ListenerInfo();
17        return mListenerInfo;
18    }
19
20    ---
21}

Next, let's take a look at how to hook view Onclicklistener event?

There are roughly three steps:

  • Step 1: get the ListenerInfo object

From the source code of View, we can know that we can get it through getListenerInfo method, so we use reflection to get the ListenerInfo object

  • Step 2: get the original OnClickListener event method

From the above analysis, we know that OnClickListener events are saved in ListenerInfo. Similarly, we use reflection to obtain

  • Step 3: replace the original OnClickListener with Hook proxy class
1public static void hookOnClickListener(View view) throws Exception {
 2    // Step 1: get the ListenerInfo object by reflection
 3    Method getListenerInfo = View.class.getDeclaredMethod("getListenerInfo");
 4    getListenerInfo.setAccessible(true);
 5    Object listenerInfo = getListenerInfo.invoke(view);
 6    // Step 2: get the original OnClickListener event method
 7    Class<?> listenerInfoClz = Class.forName("android.view.View$ListenerInfo");
 8    Field mOnClickListener = listenerInfoClz.getDeclaredField("mOnClickListener");
 9    mOnClickListener.setAccessible(true);
10    View.OnClickListener originOnClickListener = (View.OnClickListener) mOnClickListener.get(listenerInfo);
11    // Step 3: replace the original OnClickListener with the Hook proxy class
12    View.OnClickListener hookedOnClickListener = new HookedClickListenerProxy(originOnClickListener);
13    mOnClickListener.set(listenerInfo, hookedOnClickListener);
14}
1public class HookedClickListenerProxy implements View.OnClickListener {
 2
 3    private View.OnClickListener origin;
 4
 5    public HookedClickListenerProxy(View.OnClickListener origin) {
 6        this.origin = origin;
 7    }
 8
 9    @Override
10    public void onClick(View v) {
11        Toast.makeText(v.getContext(), "Hook Click Listener", Toast.LENGTH_SHORT).show();
12        if (origin != null) {
13            origin.onClick(v);
14        }
15    }
16
17}

Execute the following code to see that when we click this button, the toast "Hook Click Listener" will pop up

1mBtn1 = (Button) findViewById(R.id.btn_1);
2mBtn1.setOnClickListener(this);
3try {
4    HookHelper.hookOnClickListener(mBtn1);
5} catch (Exception e) {
6    e.printStackTrace();
7}

Simple case 2: HooK Notification

The core code of sending a message to the notification bar is as follows:

1NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
2notificationManager.notify(id, builder.build());

Trace the notify method and find that it will eventually call the notifyAsUser method

1public void notify(String tag, int id, Notification notification)
2{
3    notifyAsUser(tag, id, notification, new UserHandle(UserHandle.myUserId()));
4}

In the notifyAsUser method, we were pleasantly surprised to find that the service is a singleton. Therefore, we can think of a method to hook the service, and notifyAsUser will eventually call the enqueueNotificationWithTag method of the service. Therefore, it is enough to hook the enqueueNotificationWithTag method of the service

1public void notifyAsUser(String tag, int id, Notification notification, UserHandle user)
 2{
 3    // 
 4    INotificationManager service = getService();
 5    String pkg = mContext.getPackageName();
 6    // Fix the notification as best we can.
 7    Notification.addFieldsFromContext(mContext, notification);
 8    if (notification.sound != null) {
 9        notification.sound = notification.sound.getCanonicalUri();
10        if (StrictMode.vmFileUriExposureEnabled()) {
11            notification.sound.checkFileUriExposed("Notification.sound");
12        }
13    }
14    fixLegacySmallIcon(notification, pkg);
15    if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1) {
16        if (notification.getSmallIcon() == null) {
17            throw new IllegalArgumentException("Invalid notification (no valid small icon): "
18                    + notification);
19        }
20    }
21    if (localLOGV) Log.v(TAG, pkg + ": notify(" + id + ", " + notification + ")");
22    final Notification copy = Builder.maybeCloneStrippedForDelivery(notification);
23    try {
24        service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id,
25                copy, user.getIdentifier());
26    } catch (RemoteException e) {
27        throw e.rethrowFromSystemServer();
28    }
29}
30
31private static INotificationManager sService;
32
33static public INotificationManager getService()
34{
35    if (sService != null) {
36        return sService;
37    }
38    IBinder b = ServiceManager.getService("notification");
39    sService = INotificationManager.Stub.asInterface(b);
40    return sService;
41}

To sum up, there are three steps to Hook Notification:

  • Step 1: get the sService of NotificationManager
  • Step 2: because sService is an interface, we can use dynamic proxy to obtain dynamic proxy objects
  • Step 3: replace the system's sService with the dynamic proxy object proxyNotiMng

So we can write the following code

1public static void hookNotificationManager(final Context context) throws Exception {
 2    NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
 3
 4    Method getService = NotificationManager.class.getDeclaredMethod("getService");
 5    getService.setAccessible(true);
 6    // Step 1: get the system's sService
 7    final Object sOriginService = getService.invoke(notificationManager);
 8
 9    Class iNotiMngClz = Class.forName("android.app.INotificationManager");
10    // Step 2: get our dynamic proxy object
11    Object proxyNotiMng = Proxy.newProxyInstance(context.getClass().getClassLoader(), new
12            Class[]{iNotiMngClz}, new InvocationHandler() {
13
14        @Override
15        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
16            Log.d(TAG, "invoke(). method:" + method);
17            String name = method.getName();
18            Log.d(TAG, "invoke: name=" + name);
19            if (args != null && args.length > 0) {
20                for (Object arg : args) {
21                    Log.d(TAG, "invoke: arg=" + arg);
22                }
23            }
24            Toast.makeText(context.getApplicationContext(), "Someone's been notified", Toast.LENGTH_SHORT).show();
25            // The operation is handled by the sOriginService and the notification is not intercepted
26            return method.invoke(sOriginService, args);
27            // Intercept the notice and do nothing
28            //                    return null;
29            // Or filter according to the Tag and ID of the notification
30        }
31    });
32    // Step 3: replace the system's sService with proxyNotiMng
33    Field sServiceField = NotificationManager.class.getDeclaredField("sService");
34    sServiceField.setAccessible(true);
35    sServiceField.set(notificationManager, proxyNotiMng);
36
37}

Advanced Hook usage

Hook ClipboardManager

The first method

From the example of hook NotificationManager above, we can know that there is a static variable sService in NotificationManager, which is the remote service. Therefore, we try to find out whether the same similar static variables exist in ClipboardManager.

Looking at its source code, it is found that there is an mService variable, which is initialized in the ClipboardManager constructor, and the construction method of ClipboardManager is marked with @ hide, indicating that the method is invisible to the caller.

We know that ClipboardManager and NotificationManager are all single instances, that is, the system will only be created once. Therefore, we can also consider the mService of ClipboardManager as a singleton. Therefore, mService should be a point to consider hook.

 1public class ClipboardManager extends android.text.ClipboardManager {
 2    private final Context mContext;
 3    private final IClipboard mService;
 4
 5    /** {@hide} */
 6    public ClipboardManager(Context context, Handler handler) throws ServiceNotFoundException {
 7        mContext = context;
 8        mService = IClipboard.Stub.asInterface(
 9                ServiceManager.getServiceOrThrow(Context.CLIPBOARD_SERVICE));
10    }
11}

Next, let's take another look at the relevant methods of ClipboardManager, setPrimaryClip and getPrimaryClip

 1public void setPrimaryClip(ClipData clip) {
 2    try {
 3        if (clip != null) {
 4            clip.prepareToLeaveProcess(true);
 5        }
 6        mService.setPrimaryClip(clip, mContext.getOpPackageName());
 7    } catch (RemoteException e) {
 8        throw e.rethrowFromSystemServer();
 9    }
10}
11
12/**
13 * Returns the current primary clip on the clipboard.
14 */
15public ClipData getPrimaryClip() {
16    try {
17        return mService.getPrimaryClip(mContext.getOpPackageName());
18    } catch (RemoteException e) {
19        throw e.rethrowFromSystemServer();
20    }
21}

It can be found that these methods will eventually call the relevant methods of mService. Therefore, the mService of ClipboardManager is indeed a point where you can hook.

hook ClipboardManager. Implementation of mservice

It takes about three steps

  • Step 1: get the mService of ClipboardManager
  • Step 2: initialize the dynamic proxy object
  • Step 3: replace the mService of the system with proxyNotiMng
 1public static void hookClipboardService(final Context context) throws Exception {
 2    ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
 3    Field mServiceFiled = ClipboardManager.class.getDeclaredField("mService");
 4    mServiceFiled.setAccessible(true);
 5    // Step 1: get the mService of the system
 6    final Object mService = mServiceFiled.get(clipboardManager);
 7
 8    // Step 2: initialize the dynamic proxy object
 9    Class aClass = Class.forName("android.content.IClipboard");
10    Object proxyInstance = Proxy.newProxyInstance(context.getClass().getClassLoader(), new
11            Class[]{aClass}, new InvocationHandler() {
12
13        @Override
14        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
15            Log.d(TAG, "invoke(). method:" + method);
16            String name = method.getName();
17            if (args != null && args.length > 0) {
18                for (Object arg : args) {
19                    Log.d(TAG, "invoke: arg=" + arg);
20                }
21            }
22            if ("setPrimaryClip".equals(name)) {
23                Object arg = args[0];
24                if (arg instanceof ClipData) {
25                    ClipData clipData = (ClipData) arg;
26                    int itemCount = clipData.getItemCount();
27                    for (int i = 0; i < itemCount; i++) {
28                        ClipData.Item item = clipData.getItemAt(i);
29                        Log.i(TAG, "invoke: item=" + item);
30                    }
31                }
32                Toast.makeText(context, "Someone is detected setting the contents of the pasteboard", Toast.LENGTH_SHORT).show();
33            } else if ("getPrimaryClip".equals(name)) {
34                Toast.makeText(context, "Someone is detected trying to get the contents of the pasteboard", Toast.LENGTH_SHORT).show();
35            }
36            // The operation is handled by the sOriginService and the notification is not intercepted
37            return method.invoke(mService, args);
38
39        }
40    });
41
42    // Step 3: replace the mService of the system with proxyNotiMng
43    Field sServiceField = ClipboardManager.class.getDeclaredField("mService");
44    sServiceField.setAccessible(true);
45    sServiceField.set(clipboardManager, proxyInstance);
46
47}

The second method

Anyone who has a basic understanding of the Android source code knows that various managers in Android are obtained through ServiceManager. Therefore, we can hook all system managers through service Manager. Of course, clipboard Manager is no exception.

 1public final class ServiceManager {
 2
 3
 4    /**
 5     * Returns a reference to a service with the given name.
 6     * 
 7     * @param name the name of the service to get
 8     * @return a reference to the service, or <code>null</code> if the service doesn't exist
 9     */
10    public static IBinder getService(String name) {
11        try {
12            IBinder service = sCache.get(name);
13            if (service != null) {
14                return service;
15            } else {
16                return getIServiceManager().getService(name);
17            }
18        } catch (RemoteException e) {
19            Log.e(TAG, "error in getService", e);
20        }
21        return null;
22    }
23}

Old routine

  • Step 1: get the remote Binder object of the clipboard service through reflection. Here we can get it through the ServiceManager getService method
  • Step 2: create our dynamic proxy object and dynamically proxy the original Binder object
  • Step 3: sneak in and set our dynamic proxy object
 1public static void hookClipboardService() throws Exception {
 2
 3    //Get the remote Binder object of the clipboard service through reflection
 4    Class serviceManager = Class.forName("android.os.ServiceManager");
 5    Method getServiceMethod = serviceManager.getMethod("getService", String.class);
 6    IBinder remoteBinder = (IBinder) getServiceMethod.invoke(null, Context.CLIPBOARD_SERVICE);
 7
 8    //Create a new Binder we need to dynamically proxy the original Binder object
 9    IBinder hookBinder = (IBinder) Proxy.newProxyInstance(serviceManager.getClassLoader(),
10            new Class[]{IBinder.class}, new ClipboardHookRemoteBinderHandler(remoteBinder));
11
12    //Get the cache collection of ServiceManger storage Binder object through reflection, and put our new proxy Binder into the cache
13    Field sCacheField = serviceManager.getDeclaredField("sCache");
14    sCacheField.setAccessible(true);
15    Map<String, IBinder> sCache = (Map<String, IBinder>) sCacheField.get(null);
16    sCache.put(Context.CLIPBOARD_SERVICE, hookBinder);
17
18}
 1public class ClipboardHookRemoteBinderHandler implements InvocationHandler {
 2
 3    private IBinder remoteBinder;
 4    private Class iInterface;
 5    private Class stubClass;
 6
 7    public ClipboardHookRemoteBinderHandler(IBinder remoteBinder) {
 8        this.remoteBinder = remoteBinder;
 9        try {
10            this.iInterface = Class.forName("android.content.IClipboard");
11            this.stubClass = Class.forName("android.content.IClipboard$Stub");
12        } catch (Exception e) {
13            e.printStackTrace();
14        }
15    }
16
17    @Override
18    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
19        Log.d("RemoteBinderHandler", method.getName() + "() is invoked");
20        if ("queryLocalInterface".equals(method.getName())) {
21            //The specific service method cannot be intercepted here, because it is a remote Binder and has not been converted into a local Binder object
22            //So first intercept the queryLocalInterface method we know and return the proxy of a local Binder object
23            return Proxy.newProxyInstance(remoteBinder.getClass().getClassLoader(),
24                    new Class[]{this.iInterface},
25                    new ClipboardHookLocalBinderHandler(remoteBinder, stubClass));
26        }
27
28        return method.invoke(remoteBinder, args);
29    }
30}

Keywords: Android hook

Added by mjlively on Wed, 02 Mar 2022 12:25:05 +0200