[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 之上的。
總結一下:
- Hook 的選擇點:靜態變量和單例,因為一旦創建對象,它們不容易變化,非常容易定位。
- Hook 過程:
- 尋找 Hook 點,原則是靜態變量或者單例對象,盡量 Hook public 的對象和方法。
- 選擇合適的代理方式,如果是接口可以用動態代理。
- 偷梁換柱——用代理對象替換原始對象。
- 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