Actual simulation of Android Hook mechanism

Keywords: Android Java Programming

brief introduction

What is Hook

Hook, also known as "hook", can intercept and monitor the transmission of events in the process of event transmission, and integrate its own code and system methods. So 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 model, the distinction between Native mode (C/C++) and Java mode (Java) is made on the Android platform.

  • Java level Hook;

  • Hook at the Native level;

2. Root Hook object and Hook post-processing event are different, Hook is also divided into:

  • Message Hook;

  • API Hook;

3. According to the different processes of Hook, it can also be divided into:

  • Global Hook;

  • Single process Hook;

Common Hook Framework

In Android development, there are some common Hook frameworks as follows:

  • Xposed

    By replacing the / system/bin/app_process program to control the Zyget process, the app_process loads the Jar package XposedBridge.jar during startup, thus completing the hijacking of the Zyget process and the Dualvik virtual machine it created.
    Xposed completes hijacking of all Hook Function s at boot time, adding custom code before and after execution of the original Function.

  • Cydia Substrate

    The Cydia Substrate framework provides Apple users with jailbreak-related service frameworks, and of course Android version is also available. Cydia Substrate is a code modification platform that can modify the code of any process. Whether written in Java or C/C++ (native code), Xposed only supports Java functions in Hook app_process.

  • Legend

    Legend is an Apk Hook framework in Android Root-free environment. The framework code is concise and versatile. It is suitable for some Hook scenarios in reverse engineering. Most of the functions are placed in the Java layer, which is very compatible.
    The principle is that we can directly construct the data structure of the virtual machine corresponding to the old and new methods, and then write the replacement information into memory.

Hook Must Master Knowledge

  • reflex

If you are not familiar with reflection, I suggest you review the relevant knowledge of java reflection first.

Dynamic proxy refers to the generation of proxy classes dynamically at runtime, without requiring us to manually write one proxy class after another like static proxy. In java, we can use Invocation Handler to implement dynamic proxy.

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

Hook usage example

Key Points of Hook Selection

  • Hook's Choice Points: Static variables and instances as much as possible, because once objects are created, they are not easy to change, they are very easy to locate.

  • Hook process:

    • The principle of finding Hook points is to try to find static variables or singleton objects, and try to find Hook public objects and methods.

    • Choose the appropriate proxy mode, if it is an interface, you can use dynamic proxy.

    • Stealing Beams and Replacing Columns - Replacing the original object with the proxy object.

  • There are many API versions of Android, and the methods and classes may be different, so we need to do a good job of API compatibility.

Simple Case 1: Modify View.OnClickListener Event with Hook

First, we analyze the View.setOnClickListener source code to find the appropriate Hook point. You can see that the OnClickListener object is stored in an internal class called ListenerInfo, where mListenerInfo is a member variable of View. ListeneInfo stores various listening events for View. Therefore, we can find a way to hook the mOnClickListener of ListenerInfo.

public void setOnClickListener(@Nullable OnClickListener l) {    if (!isClickable()) {
        setClickable(true);
    }
    getListenerInfo().mOnClickListener = l;
}static class ListenerInfo {

     ---    ListenerInfo getListenerInfo() {        if (mListenerInfo != null) {            return mListenerInfo;
        }
        mListenerInfo = new ListenerInfo();        return mListenerInfo;
    }
    
    ---
}

Next, let's look at how the Hook View.OnClickListener event works.

It can be divided into three steps:

  • Step 1: Get the ListenerInfo object

From the source code of View, we can see that we can get it through the 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, and similarly we use reflection to obtain them.

  • Step 3: Replace the original OnClickListener with the Hook proxy class

public static void hookOnClickListener(View view) throws Exception {    //Step 1: Reflect the ListenerInfo object
    Method getListenerInfo = View.class.getDeclaredMethod("getListenerInfo");
    getListenerInfo.setAccessible(true);
    Object listenerInfo = getListenerInfo.invoke(view);    //Step 2: Get the original OnClickListener event method
    Class<?> listenerInfoClz = Class.forName("android.view.View$ListenerInfo");
    Field mOnClickListener = listenerInfoClz.getDeclaredField("mOnClickListener");
    mOnClickListener.setAccessible(true);
    View.OnClickListener originOnClickListener = (View.OnClickListener) mOnClickListener.get(listenerInfo);    //Step 3: Replace the original OnClickListener with the Hook proxy class
    View.OnClickListener hookedOnClickListener = new HookedClickListenerProxy(originOnClickListener);
    mOnClickListener.set(listenerInfo, hookedOnClickListener);
}
public class HookedClickListenerProxy implements View.OnClickListener {    private View.OnClickListener origin;    public HookedClickListenerProxy(View.OnClickListener origin) {        this.origin = origin;
    }    @Override
    public void onClick(View v) {
        Toast.makeText(v.getContext(), "Hook Click Listener", Toast.LENGTH_SHORT).show();        if (origin != null) {
            origin.onClick(v);
        }
    }
    
}

Execute the following code and you will see that when we click the button, toast "Hook Click Listener" pops up.

mBtn1 = (Button) findViewById(R.id.btn_1);
mBtn1.setOnClickListener(this);try {
    HookHelper.hookOnClickListener(mBtn1);
} catch (Exception e) {
    e.printStackTrace();
}

Simple Case 2: HooK Notification

The core code for sending messages to the notification bar is as follows:

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

Tracking the notify method finds that the notify AsUser method will eventually be called

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

In the notifyAsUser method, we are surprised to find that service is a singleton, so we can think of a way to hook the service, and notifyAsUser will eventually call the enqueueNotificationWithTag method of service. So hook the enqueueNotification WithTag method of service

public void notifyAsUser(String tag, int id, Notification notification, UserHandle user){    // 
    INotificationManager service = getService();
    String pkg = mContext.getPackageName();    // Fix the notification as best we can.
    Notification.addFieldsFromContext(mContext, notification);    if (notification.sound != null) {
        notification.sound = notification.sound.getCanonicalUri();        if (StrictMode.vmFileUriExposureEnabled()) {
            notification.sound.checkFileUriExposed("Notification.sound");
        }
    }
    fixLegacySmallIcon(notification, pkg);    if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1) {        if (notification.getSmallIcon() == null) {            throw new IllegalArgumentException("Invalid notification (no valid small icon): "
                    + notification);
        }
    }    if (localLOGV) Log.v(TAG, pkg + ": notify(" + id + ", " + notification + ")");    final Notification copy = Builder.maybeCloneStrippedForDelivery(notification);    try {
        service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id,
                copy, user.getIdentifier());
    } catch (RemoteException e) {        throw e.rethrowFromSystemServer();
    }
}private static INotificationManager sService;static public INotificationManager getService(){    if (sService != null) {        return sService;
    }
    IBinder b = ServiceManager.getService("notification");
    sService = INotificationManager.Stub.asInterface(b);    return sService;
}

To sum up, Hook Notification takes about three steps:

  • Step 1: Get the sService of Notification Manager

  • Step 2: Because sService is an interface, we can use dynamic proxy to get dynamic proxy objects.

  • Step 3: Replace the sService of the system with proxyNotiMng, a dynamic proxy object

So, we can write the following code

public static void hookNotificationManager(final Context context) throws Exception {
    NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);

    Method getService = NotificationManager.class.getDeclaredMethod("getService");
    getService.setAccessible(true);    //Step 1: Get the system's sService
    final Object sOriginService = getService.invoke(notificationManager);

    Class iNotiMngClz = Class.forName("android.app.INotificationManager");    //Step 2: Get our dynamic proxy object
    Object proxyNotiMng = Proxy.newProxyInstance(context.getClass().getClassLoader(), new
            Class[]{iNotiMngClz}, new InvocationHandler() {        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Log.d(TAG, "invoke(). method:" + method);
            String name = method.getName();
            Log.d(TAG, "invoke: name=" + name);            if (args != null && args.length > 0) {                for (Object arg : args) {
                    Log.d(TAG, "invoke: arg=" + arg);
                }
            }
            Toast.makeText(context.getApplicationContext(), "A notification was detected", Toast.LENGTH_SHORT).show();            //Operations are handled by sOriginService without intercepting notifications
            return method.invoke(sOriginService, args);            //Interception notice, do nothing
            //                    return null;
            //Or screening based on notification Tag and ID
        }
    });    //Step 3: Replace the sService with proxyNotiMng
    Field sServiceField = NotificationManager.class.getDeclaredField("sService");
    sServiceField.setAccessible(true);
    sServiceField.set(notificationManager, proxyNotiMng);

}

Hook uses Advancement

Hook ClipboardManager

The first method

From the hook Notification Manager example above, we can see that there is a static variable sService in Notification Manager, which is a remote service. Therefore, we try to find out if there are similar static variables in Clipboard Manager.

Looking at its source code, we find that it has an mService variable, which is initialized in the Clipboard Manager constructor, and the Clipboard Manager constructor uses the @hide tag to indicate that the method is invisible to the caller.

And we know that Clipboard Manager, Notification Manager are all singletons, that is, the system will only be created once. So we can also think that
Clipboard Manager's mService is a singleton. So mService should be a point where hook s can be considered.

public class ClipboardManager extends android.text.ClipboardManager {    private final Context mContext;    private final IClipboard mService;    /** {@hide} */
    public ClipboardManager(Context context, Handler handler) throws ServiceNotFoundException {
        mContext = context;
        mService = IClipboard.Stub.asInterface(
                ServiceManager.getServiceOrThrow(Context.CLIPBOARD_SERVICE));
    }
}

Next, let's take a look at the Clipboard Manager method setPrimaryClip, getPrimaryClip

public void setPrimaryClip(ClipData clip) {    try {        if (clip != null) {
            clip.prepareToLeaveProcess(true);
        }
        mService.setPrimaryClip(clip, mContext.getOpPackageName());
    } catch (RemoteException e) {        throw e.rethrowFromSystemServer();
    }
}/**
 * Returns the current primary clip on the clipboard.
 */public ClipData getPrimaryClip() {    try {        return mService.getPrimaryClip(mContext.getOpPackageName());
    } catch (RemoteException e) {        throw e.rethrowFromSystemServer();
    }
}

It can be found that these methods will eventually call the mService-related methods. Therefore, the mService of Clipboard Manager is indeed a point where hook s can be made.

Implementation of hook Clipboard Manager.mService

It takes about three steps.

  • Step 1: Get the mService of Clipboard Manager

  • Step 2: Initialize dynamic proxy objects

  • Step 3: Replace the mService of the system with proxyNotiMng

public static void hookClipboardService(final Context context) throws Exception {
    ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
    Field mServiceFiled = ClipboardManager.class.getDeclaredField("mService");
    mServiceFiled.setAccessible(true);    //Step 1: Get the system mService
    final Object mService = mServiceFiled.get(clipboardManager);    
    //Step 2: Initialize dynamic proxy objects
    Class aClass = Class.forName("android.content.IClipboard");
    Object proxyInstance = Proxy.newProxyInstance(context.getClass().getClassLoader(), new
            Class[]{aClass}, new InvocationHandler() {        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Log.d(TAG, "invoke(). method:" + method);
            String name = method.getName();            if (args != null && args.length > 0) {                for (Object arg : args) {
                    Log.d(TAG, "invoke: arg=" + arg);
                }
            }            if ("setPrimaryClip".equals(name)) {
                Object arg = args[0];                if (arg instanceof ClipData) {
                    ClipData clipData = (ClipData) arg;                    int itemCount = clipData.getItemCount();                    for (int i = 0; i < itemCount; i++) {
                        ClipData.Item item = clipData.getItemAt(i);
                        Log.i(TAG, "invoke: item=" + item);
                    }
                }
                Toast.makeText(context, "Detection of someone setting the pasteboard content", Toast.LENGTH_SHORT).show();
            } else if ("getPrimaryClip".equals(name)) {
                Toast.makeText(context, "Detection of someone trying to get the contents of the pasteboard", Toast.LENGTH_SHORT).show();
            }            //Operations are handled by sOriginService without intercepting notifications
            return method.invoke(mService, args);

        }
    });    //Step 3: Replace the mService of the system with proxyNotiMng
    Field sServiceField = ClipboardManager.class.getDeclaredField("mService");
    sServiceField.setAccessible(true);
    sServiceField.set(clipboardManager, proxyInstance);

}

image

The second method

Anyone who has a basic understanding of Android source code knows that various managers in Android are acquired through Service Manager. Therefore, we can hook all system managers through Service Manager, and Clipboard Manager is no exception.

public final class ServiceManager {    /**
     * Returns a reference to a service with the given name.
     * 
     * @param name the name of the service to get
     * @return a reference to the service, or <code>null</code> if the service doesn't exist
     */
    public static IBinder getService(String name) {        try {
            IBinder service = sCache.get(name);            if (service != null) {                return service;
            } else {                return getIServiceManager().getService(name);
            }
        } catch (RemoteException e) {
            Log.e(TAG, "error in getService", e);
        }        return null;
    }
}

The old routine

  • Step 1: Get the remote Binder object of the clipboard service by reflection, where we can get it by the Service Manager getService method

  • Step 2: Create our dynamic proxy object and dynamically proxy the original Binder object

  • Step 3: Change pillars by stealing beams, and set up our dynamic proxy objects.

public static void hookClipboardService() throws Exception {    //Remote Binder Objects for Shearboard Services by Reflection
    Class serviceManager = Class.forName("android.os.ServiceManager");
    Method getServiceMethod = serviceManager.getMethod("getService", String.class);
    IBinder remoteBinder = (IBinder) getServiceMethod.invoke(null, Context.CLIPBOARD_SERVICE);    //Create a new Binder that we need to dynamically proxy the original Binder object
    IBinder hookBinder = (IBinder) Proxy.newProxyInstance(serviceManager.getClassLoader(),            new Class[]{IBinder.class}, new ClipboardHookRemoteBinderHandler(remoteBinder));    //Retrieve the cache set of Binder objects stored by Service Manger through reflection, and put our new proxy Binder into the cache
    Field sCacheField = serviceManager.getDeclaredField("sCache");
    sCacheField.setAccessible(true);    Map<String, IBinder> sCache = (Map<String, IBinder>) sCacheField.get(null);
    sCache.put(Context.CLIPBOARD_SERVICE, hookBinder);

}
public class ClipboardHookRemoteBinderHandler implements InvocationHandler {    private IBinder remoteBinder;    private Class iInterface;    private Class stubClass;    public ClipboardHookRemoteBinderHandler(IBinder remoteBinder) {        this.remoteBinder = remoteBinder;        try {            this.iInterface = Class.forName("android.content.IClipboard");            this.stubClass = Class.forName("android.content.IClipboard$Stub");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Log.d("RemoteBinderHandler", method.getName() + "() is invoked");        if ("queryLocalInterface".equals(method.getName())) {            //There is no way to intercept specific service methods because this is a remote Binder that has not yet been converted into a local Binder object.
            //So first intercept the queryLocalInterface method we know, and return a proxy for a local Binder object
            return Proxy.newProxyInstance(remoteBinder.getClass().getClassLoader(),                    new Class[]{this.iInterface},                    new ClipboardHookLocalBinderHandler(remoteBinder, stubClass));
        }        return method.invoke(remoteBinder, args);
    }
}


Posted by returnButton on Mon, 22 Jul 2019 03:43:08 -0700