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框架:
- 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 函式。
- Legend
Legend 是 Android 免 Root 環境下的一個 Apk Hook 框架,該框架程式碼設計簡潔,通用性高,適合逆向工程時一些 Hook 場景。大部分的功能都放到了 Java 層,這樣的相容性就非常好。
原理是這樣的,直接構造出新舊方法對應的虛擬機器資料結構,然後替換資訊寫到記憶體中即可。
Hook 必須掌握的知識
- 反射
如果你對反射還不是很熟悉的話,建議你先複習一下 java 反射的相關知識。有興趣的,可以看一下我的這一篇部落格 Java 反射機制詳解
- 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);
}
}
Hook Activity
關於怎樣 hook activity,以及怎樣啟動沒有在 AndroidManifet 註冊的 activity,可以檢視我的這一篇部落格。
最後的最後
賣一下廣告,歡迎大家關注我的微信公眾號,掃一掃下方二維碼或搜尋微訊號 stormjun,即可關注。 目前專注於 Android 開發,主要分享 Android開發相關知識和一些相關的優秀文章,包括個人總結,職場經驗等。