1. 程式人生 > >Android Hook 機制之簡單實戰

Android Hook 機制之簡單實戰

簡介

什麼是 Hook

Hook 又叫“鉤子”,它可以在事件傳送的過程中截獲並監控事件的傳輸,將自身的程式碼與系統方法進行融入。這樣當這些方法被呼叫時,也就可以執行我們自己的程式碼,這也是面向切面程式設計的思想(AOP)。

Hook 分類

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

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

2.根 Hook 物件與 Hook 後處理事件方式不同,Hook還分為:

  • 訊息Hook;
  • API Hook;

3.針對Hook的不同程序上來說,還可以分為:

  • 全域性Hook;
  • 單個程序Hook;

常見 Hook 框架

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

  1. Xposed

通過替換 /system/bin/app_process 程式控制 Zygote 程序,使得 app_process 在啟動過程中會載入 XposedBridge.jar 這個 Jar 包,從而完成對 Zygote 程序及其建立的 Dalvik 虛擬機器的劫持。
Xposed 在開機的時候完成對所有的 Hook Function 的劫持,在原 Function 執行的前後加上自定義程式碼。

  1. Cydia Substrate

Cydia Substrate 框架為蘋果使用者提供了越獄相關的服務框架,當然也推出了 Android 版 。Cydia Substrate 是一個程式碼修改平臺,它可以修改任何程序的程式碼。不管是用 Java 還是 C/C++(native程式碼)編寫的,而 Xposed 只支援 Hook app_process 中的 Java 函式。

  1. Legend

Legend 是 Android 免 Root 環境下的一個 Apk Hook 框架,該框架程式碼設計簡潔,通用性高,適合逆向工程時一些 Hook 場景。大部分的功能都放到了 Java 層,這樣的相容性就非常好。
原理是這樣的,直接構造出新舊方法對應的虛擬機器資料結構,然後替換資訊寫到記憶體中即可。

Hook 必須掌握的知識

  • 反射

如果你對反射還不是很熟悉的話,建議你先複習一下 java 反射的相關知識。有興趣的,可以看一下我的這一篇部落格 Java 反射機制詳解

  • java 的動態代理

動態代理是指在執行時動態生成代理類,不需要我們像靜態代理那個去手動寫一個個的代理類。在 java 中,我們可以使用 InvocationHandler 實現動態代理,有興趣的,可以檢視我的這一篇部落格

java 代理模式詳解

本文的主要內容是講解單個程序的 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);
    }
}

Hook Activity

關於怎樣 hook activity,以及怎樣啟動沒有在 AndroidManifet 註冊的 activity,可以檢視我的這一篇部落格。

最後的最後

賣一下廣告,歡迎大家關注我的微信公眾號,掃一掃下方二維碼或搜尋微訊號 stormjun,即可關注。 目前專注於 Android 開發,主要分享 Android開發相關知識和一些相關的優秀文章,包括個人總結,職場經驗等。