[Android][Security] Android 逆向之 xposed

  • 2020 年 1 月 20 日
  • 筆記

Xposed

網上關於Xposed的介紹很多,但都是點到為止,比如:

在Android系統中,應用程式進程以及系統服務進程SystemServer都是由Zygote進程孵化出來的,而Zygote進程是由Init進程啟動的,Zygote進程在啟動時會創建一個Dalvik虛擬機實例,每當它孵化一個新的應用程式進程時,都會將這個Dalvik虛擬機實例複製到新的應用程式進程裡面去,從而使得每一個應用程式進程都有一個獨立的Dalvik虛擬機實例,這也是Xposed選擇替換app_process的原因。 Zygote進程在啟動的過程中,除了會創建一個Dalvik虛擬機實例之外,還會註冊一些Android核心類的JNI方法到Dalvik虛擬機實例中去,以及將Java運行時庫載入到進程中來。而一個應用程式進程被Zygote進程孵化出來的時候,不僅會獲得Zygote進程中的Dalvik虛擬機實例拷貝,還會與Zygote一起共享Java運行時庫,這也就是可以將XposedBridge這個jar包載入到每一個Android應用程式中的原因,

我當然不會滿足於這麼一點淺薄的介紹,既然用這個框架了,那就得把這個框架搞清楚對不?

一句話原理:

Xposed框架的原理是通過替換/system/bin/app_process程式控制zygote進程,使得app_process在啟動過程中會載入XposedBridge.jar這個jar包,從而完成對Zygote進程及其創建的Dalvik虛擬機的劫持。

為什麼是app_process

Android系統是基於Linux內核的,而在Linux系統中,所有的進程都是init進程的子孫進程,也就是說,所有的進程都是直接或者間接地由init進程fork出來的。Zygote進程也不例外,它是在系統啟動的過程,由init進程創建的。在系統啟動腳本system/core/rootdir/init.rc文件中,我們可以看到啟動Zygote進程的腳本命令:

service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server      socket zygote stream 666      onrestart write /sys/android_power/request_state wake      onrestart write /sys/power/state on      onrestart restart media      onrestart restart netd

系統啟動之後就可以在/dev/socket目錄下看到有一個名為zygote的文件,就是zygote佔用的socket埠。

所以,zygote是由app_process啟動的,替換app_process後,啟動的就是Xposed之後的zygote了。

為什麼XposedBridge可以生效

Xposed版zygote進程在啟動時會創建一個Dalvik虛擬機實例,以及註冊一些Android核心類的JNI方法到Dalvik虛擬機實例中去。同時Xposed版zygote把XposedBridge.jar添加到CLASSPATH環境變數,並將Java運行時庫載入到進程中。一個應用程式進程被Zygote進程孵化出來的時候,不僅會獲得Zygote進程中的Dalvik虛擬機實例拷貝,還會與Zygote一起共享Java運行時庫,所以XposedBridge.jar可以被載入到每一個Android應用程式中。

zygote進程載入XposedBridge將所有需要替換的Method通過JNI方法hookMethodNative指向Native方法 xposedCallHandler,xposedCallHandler在轉入handleHookedMethod這個Java方法執行用戶規定的Hook Func。

Xposed版zygote在啟動時還會獲得一個JNIEnv實例,該實例描述的是zygote進程的主執行緒的JNI環境,Xposed版zygote進程通過JNIEnv實例的成員函數CallStaticVoidMethod()調用de.robv.android.xposed.XposedBridge的main函數作為java程式碼的入口點。

de.robv.android.xposed.XposedBridge.main函數做了以下幾件事:

(1) 初始化xposed框架。

(2) 調用initForZygote()方法hook應用進程創建時調用的一些關鍵函數,比如通過掛鉤LoadedApk的構造函數獲得應用進程的相關資訊並保存至XC_LoadPackage.LoadPackageParam的實例中,該實例在後續hook應用程式中的函數時可用於獲取應用程式相關資訊。通過掛鉤handleBindApplication方法,可以在應用程式啟動時調用所有IXposedHookLoadPackage類型的鉤子(其實最終調用的是IXposedHookLoadPackage的handleLoadPackage方法)。該類型的鉤子用於對應用程式進行掛鉤,假如要hook應用程式中的函數,我們編寫的xposed插件中的鉤子類必須實現IXposedHookLoadPackag介面,重寫它的handleLoadPackage方法並在方法體中調用xposed框架提供的掛鉤函數(比如findAndHookMethod)hook想要掛鉤的應用程式函數。

(3) 調用loadModules()載入所有的xposed插件,將這些插件中不同鉤子類型的鉤子分別保存起來。有三種類型的鉤子,IXposedHookLoadPackage類型的鉤子對應用程式掛鉤,IXposedHookZygoteInit類型鉤子對Zygote的初始化進行掛鉤,IXposedHookInitPackageResources類型鉤子對資源進行掛鉤。

(4) 最後再調用原始的ZygoteInit.main函數,完成zygote的全部初始化工作。

http://4hou.win/wordpress/?p=7516

https://blog.csdn.net/u014385722/article/details/82013306

使用Java反射實現API Hook

通過對 Android 平台的虛擬機注入與 Java 反射的方式,來改變 Android 虛擬機調用函數的方式(ClassLoader),從而達到 Java 函數重定向的目的,這裡我們將此類操作稱為 Java API Hook。

先從簡單的開始,比如嘗試Hook按鈕的點擊事件。

首先先看一下點擊事件:

/**   * Interface definition for a callback to be invoked when a view is clicked.   */  public interface OnClickListener {      /**       * Called when a view has been clicked.       *       * @param v The view that was clicked.       */      void onClick(View v);  }

我們對Button綁定點擊事件:

mBtnHijack = findViewById(R.id.btn_hijack);  mBtnHijack.setOnClickListener(v -> {    Toast.makeText(MainActivity.this, "Click button", Toast.LENGTH_LONG).show();  });

所以下一步是看setOnClickListener方法是怎麼保存OnClickListener介面的:

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

看到OnClickListener被保存到ListenerInfo的成員變數中:

ListenerInfo getListenerInfo() {    if (mListenerInfo != null) {      return mListenerInfo;    }    mListenerInfo = new ListenerInfo();    return mListenerInfo;  }    static class ListenerInfo {  	...    public OnClickListener mOnClickListener;      protected OnLongClickListener mOnLongClickListener;      protected OnContextClickListener mOnContextClickListener;    ...  }

而ListenerInfo是View的一個內部類。

既然知道OnClickListener的保存位置,那麼我們要Hook點擊事件,就是創建一個自己的點擊事件,然後替換掉原來的事件即可。

先創建一個實現自己功能的點擊事件

class HookedOnClickListener implements View.OnClickListener {    private View.OnClickListener origin; // 原始的點擊事件      HookedOnClickListener(View.OnClickListener origin) {      this.origin = origin;    }      @Override    public void onClick(View v) {      Toast.makeText(MainActivity.this, "hook click", Toast.LENGTH_SHORT).show();      Log.i("WOW", "Before click, do what you want to to.");      if (origin != null) {        origin.onClick(v); // 執行原始的點擊邏輯      }      Log.i("WOW", "After click, do what you want to to.");    }  }

然後就是使用反射,用我們的OnClickListener替換原來註冊的點擊回調:

private void hookOnClickListener(View view) {    try {      // 得到 View 的 ListenerInfo 對象      Method getListenerInfo = View.class.getDeclaredMethod("getListenerInfo");      // 強制訪問      getListenerInfo.setAccessible(true);      // 執行getListenerInfo拿到對象      Object listenerInfo = getListenerInfo.invoke(view);      // 得到 原始的 ListenerInfo 類      Class<?> listenerInfoClz = Class.forName("android.view.View$ListenerInfo");      // 從 ListenerInfo找到onClickListener屬性      Field mOnClickListener = listenerInfoClz.getDeclaredField("mOnClickListener");      mOnClickListener.setAccessible(true);      // 用前面的listenerInfo對象獲取原始的listener      View.OnClickListener originOnClickListener = (View.OnClickListener) mOnClickListener.get(listenerInfo);      // 用自定義的 OnClickListener 替換原始的 OnClickListener      View.OnClickListener hookedOnClickListener = new HookedOnClickListener(originOnClickListener);      mOnClickListener.set(listenerInfo, hookedOnClickListener);    } catch (Exception e) {      Log.w("hook clickListener failed!", e);    }  }

把這段程式碼放到按鈕設置OnClickListener之後:

mBtnHijack.setOnClickListener(v -> {  	Toast.makeText(MainActivity.this, "Click button", Toast.LENGTH_LONG).show();  });  hookOnClickListener(mBtnHijack);

這樣就完成了對按鈕點擊事件的Hook。

但是這隻能編碼Hook自己的應用,這樣做的意義是什麼呢?

當應用內接入了眾多的 SDK,SDK 內部會使用系統服務 NotificationManager 發送通知,這就導致通知難以管理和控制。現在我們就用 Hook 技術攔截部分通知,限制應用內的通知發送操作。

發送通知是由NotificationManager的notify方法實現,通過查看源碼,定位到:

public void notifyAsUser(String tag, int id, Notification notification, UserHandle user)      {          INotificationManager service = getService();          ...          try {              service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id, copy, user.getIdentifier());          } catch (RemoteException e) {              throw e.rethrowFromSystemServer();          }      }    private static INotificationManager sService;    /** @hide */  static public INotificationManager getService()  {    if (sService != null) {      return sService;    }    IBinder b = ServiceManager.getService("notification");    sService = INotificationManager.Stub.asInterface(b);    return sService;  }

INotificationManager 是跨進程通訊的 Binder 類,sService 是 NMS(NotificationManagerService) 在客戶端的代理,發送通知要委託給 sService,由它傳遞給 NMS。我們發現 sService 是個靜態成員變數,而且只會初始化一次。只要把 sService 替換成自定義的不就行了么,確實如此。

private void hookNotificationManager(Context context) {    try {      NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);      // 得到系統的 sService      Method getService = NotificationManager.class.getDeclaredMethod("getService");      getService.setAccessible(true);      final Object sService = getService.invoke(notificationManager);        Class iNotiMngClz = Class.forName("android.app.INotificationManager");      // 動態代理 INotificationManager      Object proxyNotiMng = Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{iNotiMngClz}, new InvocationHandler() {          @Override        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable           		  {          log.debug("invoke(). method:{}", method);          if (args != null && args.length > 0) {            for (Object arg : args) {              log.debug("type:{}, arg:{}", arg != null ? arg.getClass() : null, arg);            }          }          // 操作交由 sService 處理,不攔截通知          // return method.invoke(sService, args);          // 攔截通知,什麼也不做          return null;          // 或者是根據通知的 Tag 和 ID 進行篩選        }      });      // 替換 sService      Field sServiceField = NotificationManager.class.getDeclaredField("sService");      sServiceField.setAccessible(true);      sServiceField.set(notificationManager, proxyNotiMng);    } catch (Exception e) {      log.warn("Hook NotificationManager failed!", e);    }  }

Hook 的時機還是盡量要早,我們在 attachBaseContext 裡面操作。

@Override  protected void attachBaseContext(Context newBase) {      super.attachBaseContext(newBase);      hookNotificationManager(newBase);  }

這樣我們就完成了對通知的攔截,可見 Hook 技術真的是非常強大,好多插件化的原理都是建立在 Hook 之上的。

總結一下:

  1. Hook 的選擇點:靜態變數和單例,因為一旦創建對象,它們不容易變化,非常容易定位。
  2. Hook 過程:
    • 尋找 Hook 點,原則是靜態變數或者單例對象,盡量 Hook public 的對象和方法。
    • 選擇合適的代理方式,如果是介面可以用動態代理。
    • 偷梁換柱——用代理對象替換原始對象。
  3. Android 的 API 版本比較多,方法和類可能不一樣,所以要做好 API 的兼容工作。

Xposed Hook微信運動

首先在AndroidManifest.xml Application下添加xposed模組

<!--xposed描述-->  <meta-data      android:name="xposeddescription"      android:value="這是一個Xposed常式" />    <!--xposed最低版本53-->  <meta-data      android:name="xposedminversion"      android:value="53" />    <!--這是一個xposed模組-->  <meta-data      android:name="xposedmodule"      android:value="true" />

Gradle添加依賴

compileOnly 'de.robv.android.xposed:api:82'  compileOnly 'de.robv.android.xposed:api:82:sources'

然後再assets目錄添加一個xposed_init文件供Xposed框架訪問,內容為包名:

com.softard.xposedemo.HookTest

然後創建我們的HookTest

public class HookTest implements IXposedHookLoadPackage {    // 實現Hook篡改程式      @SuppressLint("PrivateApi")      @Override      public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {  			if (lpparam.packageName.equals("com.tencent.mm")) { // 搞一搞微信              XposedBridge.log("hoooook wechat");              Class<?> clazz1 = Class.forName(                      "android.hardware.SystemSensorManager$SensorEventQueue", true, lpparam.classLoader);                XposedBridge.hookAllMethods(clazz1, "dispatchSensorEvent", new XC_MethodHook() {                  @Override                  protected void beforeHookedMethod(MethodHookParam param) throws Throwable {                      int times = XSharedPreferencesUtil.getPref().getInt("step", 500);                        XposedBridge.log("~~~~~~~Multi times: " + times);                      XposedBridge.log("Wechat2222 Sensor param " + ((float[]) param.args[1])[0]);                      ((float[]) param.args[1])[0] = ((float[]) param.args[1])[0] * times;                      XposedBridge.log("final Sensor param " + ((float[]) param.args[1])[0]);                      super.beforeHookedMethod(param);                  }                    @Override                  protected void afterHookedMethod(MethodHookParam param) throws Throwable {                      super.afterHookedMethod(param);                  }              });            }*/      }  }

To be continued