Android Hook 機制之實戰模擬

嗨,你終於來啦 ~ 等你很久啦~ 喜歡的小夥伴歡迎關注,我會按期分享Android知識點及解析,還會不斷更新的BATJ面試專題,歡迎你們前來探討交流,若有好的文章也歡迎投稿javascript

簡介

什麼是 Hook

Hook 又叫「鉤子」,它能夠在事件傳送的過程當中截獲並監控事件的傳輸,將自身的代碼與系統方法進行融入。這樣當這些方法被調用時,也就能夠執行咱們本身的代碼,這也是面向切面編程的思想(AOP)。java

Hook 分類

1.根據Android開發模式,Native模式(C/C++)和Java模式(Java)區分,在Android平臺上android

  • Java層級的Hook;
  • Native層級的Hook;

2.根 Hook 對象與 Hook 後處理事件方式不一樣,Hook還分爲:面試

  • 消息Hook;
  • API Hook;

3.針對Hook的不一樣進程上來講,還能夠分爲:編程

  • 全局Hook;
  • 單個進程Hook;

常見 Hook 框架

在Android開發中,有如下常見的一些Hook框架:緩存

  • Xposed

經過替換 /system/bin/app_process 程序控制 Zygote 進程,使得 app_process 在啓動過程當中會加載 XposedBridge.jar 這個 Jar 包,從而完成對 Zygote 進程及其建立的 Dalvik 虛擬機的劫持。
Xposed 在開機的時候完成對全部的 Hook Function 的劫持,在原 Function 執行的先後加上自定義代碼。數據結構

  • Cydia Substrate

Cydia Substrate 框架爲蘋果用戶提供了越獄相關的服務框架,固然也推出了 Android 版 。Cydia Substrate 是一個代碼修改平臺,它能夠修改任何進程的代碼。不論是用 Java 仍是 C/C++(native代碼)編寫的,而 Xposed 只支持 Hook app_process 中的 Java 函數。app

  • Legend

Legend 是 Android 免 Root 環境下的一個 Apk Hook 框架,該框架代碼設計簡潔,通用性高,適合逆向工程時一些 Hook 場景。大部分的功能都放到了 Java 層,這樣的兼容性就很是好。
原理是這樣的,直接構造出新舊方法對應的虛擬機數據結構,而後替換信息寫到內存中便可。框架

Hook 必須掌握的知識

  • 反射

若是你對反射還不是很熟悉的話,建議你先複習一下 java 反射的相關知識。ide

  • java 的動態代理

動態代理是指在運行時動態生成代理類,不須要咱們像靜態代理那個去手動寫一個個的代理類。在 java 中,咱們可使用 InvocationHandler 實現動態代理。

本文的主要內容是講解單個進程的 Hook,以及怎樣 Hook。

Hook 使用實例

Hook 選擇的關鍵點

  • Hook 的選擇點:儘可能靜態變量和單例,由於一旦建立對象,它們不容易變化,很是容易定位。

  • Hook 過程:

    • 尋找 Hook 點,原則是儘可能靜態變量或者單例對象,儘可能 Hook public 的對象和方法。
    • 選擇合適的代理方式,若是是接口能夠用動態代理。
    • 偷樑換柱——用代理對象替換原始對象。
  • Android 的 API 版本比較多,方法和類可能不同,因此要作好 API 的兼容工做。

簡單案例一: 使用 Hook 修改 View.OnClickListener 事件

首先,咱們先分析 View.setOnClickListener 源碼,找出合適的 Hook 點。能夠看到 OnClickListener 對象被保存在了一個叫作 ListenerInfo 的內部類裏,其中 mListenerInfo 是 View 的成員變量。ListeneInfo 裏面保存了 View 的各類監聽事件。所以,咱們能夠想辦法 hook ListenerInfo 的 mOnClickListener 。 

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;
    }
    
    ---
}
複製代碼

接下來,讓咱們一塊兒來看一下怎樣 Hook View.OnClickListener 事件?

大概分爲三步:

  • 第一步:獲取 ListenerInfo 對象

從 View 的源代碼,咱們能夠知道咱們能夠經過 getListenerInfo 方法獲取,因而,咱們利用反射獲得 ListenerInfo 對象

  • 第二步:獲取原始的 OnClickListener事件方法

從上面的分析,咱們知道 OnClickListener 事件被保存在 ListenerInfo 裏面,同理咱們利用反射獲取

  • 第三步:偷樑換柱,用 Hook代理類 替換原始的 OnClickListener
public static void hookOnClickListener(View view) throws Exception {
    // 第一步:反射獲得 ListenerInfo 對象
    Method getListenerInfo = View.class.getDeclaredMethod("getListenerInfo");
    getListenerInfo.setAccessible(true);
    Object listenerInfo = getListenerInfo.invoke(view);
    // 第二步:獲得原始的 OnClickListener事件方法
    Class<?> listenerInfoClz = Class.forName("android.view.View$ListenerInfo");
    Field mOnClickListener = listenerInfoClz.getDeclaredField("mOnClickListener");
    mOnClickListener.setAccessible(true);
    View.OnClickListener originOnClickListener = (View.OnClickListener) mOnClickListener.get(listenerInfo);
    // 第三步:用 Hook代理類 替換原始的 OnClickListener
    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);
        }
    }
    
}
複製代碼

執行如下代碼,將會看到當咱們點擊該按鈕的時候,會彈出 toast 「Hook Click Listener」

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

簡單案例二: HooK Notification

發送消息到通知欄的核心代碼以下:

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

跟蹤 notify 方法發現最終會調用到 notifyAsUser 方法

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

複製代碼

而在 notifyAsUser 方法中,咱們驚喜地發現 service 是一個單例,所以,咱們能夠想方法 hook 住這個 service,而 notifyAsUser 最終會調用到 service 的 enqueueNotificationWithTag 方法。所以 hook 住 service 的 enqueueNotificationWithTag 方法便可

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;
}

複製代碼

綜上,要 Hook Notification,大概須要三步:

  • 第一步:獲得 NotificationManager 的 sService
  • 第二步:由於 sService 是接口,因此咱們可使用動態代理,獲取動態代理對象
  • 第三步:偷樑換柱,使用動態代理對象 proxyNotiMng 替換系統的 sService

因而,咱們能夠寫出以下的代碼

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);
    // 第一步:獲得系統的 sService
    final Object sOriginService = getService.invoke(notificationManager);

    Class iNotiMngClz = Class.forName("android.app.INotificationManager");
    // 第二步:獲得咱們的動態代理對象
    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(), "檢測到有人發通知了", Toast.LENGTH_SHORT).show();
            // 操做交由 sOriginService 處理,不攔截通知
            return method.invoke(sOriginService, args);
            // 攔截通知,什麼也不作
            // return null;
            // 或者是根據通知的 Tag 和 ID 進行篩選
        }
    });
    // 第三步:偷樑換柱,使用 proxyNotiMng 替換系統的 sService
    Field sServiceField = NotificationManager.class.getDeclaredField("sService");
    sServiceField.setAccessible(true);
    sServiceField.set(notificationManager, proxyNotiMng);

}


複製代碼

Hook 使用進階

Hook ClipboardManager

第一種方法

從上面的 hook NotificationManager 例子中,咱們能夠得知 NotificationManager 中有一個靜態變量 sService,這個變量是遠端的 service。所以,咱們嘗試查找 ClipboardManager 中是否是也存在相同的相似靜態變量。

查看它的源碼發現它存在 mService 變量,該變量是在 ClipboardManager 構造函數中初始化的,而 ClipboardManager 的構造方法用 @hide 標記,代表該方法對調用者不可見。

而咱們知道 ClipboardManager,NotificationManager 其實這些都是單例的,即系統只會建立一次。所以咱們也能夠認爲
ClipboardManager 的 mService 是單例的。所以 mService 應該是能夠考慮 hook 的一個點。

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));
    }
}
複製代碼

接下來,咱們再來一個看一下 ClipboardManager 的相關方法 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();
    }
}
複製代碼

能夠發現這些方法最終都會調用到 mService 的相關方法。所以,ClipboardManager 的 mService 確實是一個能夠 hook 的一個點。

hook ClipboardManager.mService 的實現

大概須要三個步驟

  • 第一步:獲得 ClipboardManager 的 mService
  • 第二步:初始化動態代理對象
  • 第三步:偷樑換柱,使用 proxyNotiMng 替換系統的 mService
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);
    // 第一步:獲得系統的 mService
    final Object mService = mServiceFiled.get(clipboardManager);
    
    // 第二步:初始化動態代理對象
    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, "檢測到有人設置粘貼板內容", Toast.LENGTH_SHORT).show();
            } else if ("getPrimaryClip".equals(name)) {
                Toast.makeText(context, "檢測到有人要獲取粘貼板的內容", Toast.LENGTH_SHORT).show();
            }
            // 操做交由 sOriginService 處理,不攔截通知
            return method.invoke(mService, args);

        }
    });

    // 第三步:偷樑換柱,使用 proxyNotiMng 替換系統的 mService
    Field sServiceField = ClipboardManager.class.getDeclaredField("mService");
    sServiceField.setAccessible(true);
    sServiceField.set(clipboardManager, proxyInstance);

}


複製代碼

第二種方法

對 Android 源碼有基本瞭解的人都知道,Android 中的各類 Manager 都是經過 ServiceManager 獲取的。所以,咱們能夠經過 ServiceManager hook 全部系統 Manager,ClipboardManager 固然也不例外。

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;
    }
}
複製代碼

老套路

  • 第一步:經過反射獲取剪切板服務的遠程Binder對象,這裏咱們能夠經過 ServiceManager getService 方法得到
  • 第二步:建立咱們的動態代理對象,動態代理原來的Binder對象
  • 第三步:偷樑換柱,把咱們的動態代理對象設置進去
public static void hookClipboardService() throws Exception {

    //經過反射獲取剪切板服務的遠程Binder對象
    Class serviceManager = Class.forName("android.os.ServiceManager");
    Method getServiceMethod = serviceManager.getMethod("getService", String.class);
    IBinder remoteBinder = (IBinder) getServiceMethod.invoke(null, Context.CLIPBOARD_SERVICE);

    //新建一個咱們須要的Binder,動態代理原來的Binder對象
    IBinder hookBinder = (IBinder) Proxy.newProxyInstance(serviceManager.getClassLoader(),
            new Class[]{IBinder.class}, new ClipboardHookRemoteBinderHandler(remoteBinder));

    //經過反射獲取ServiceManger存儲Binder對象的緩存集合,把咱們新建的代理Binder放進緩存
    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())) {
            //這裏不能攔截具體的服務的方法,由於這是一個遠程的Binder,尚未轉化爲本地Binder對象
            //因此先攔截咱們所知的queryLocalInterface方法,返回一個本地Binder對象的代理
            return Proxy.newProxyInstance(remoteBinder.getClass().getClassLoader(),
                    new Class[]{this.iInterface},
                    new ClipboardHookLocalBinderHandler(remoteBinder, stubClass));
        }

        return method.invoke(remoteBinder, args);
    }
}
複製代碼
本文轉載自公衆號Android技術人,喜歡的小夥伴能夠過去關注一波
相關文章
相關標籤/搜索