2015年6月22日 星期一

Xposed Module 開發教學 - 以 MoPTT 為例

Part 1. Xposed Module 開發教學 - 以 MoPTT 為例
Part 2. Xposed Module 開發教學技巧篇 - 以 MoPTT 為例
Part 3. Xposed Module 技巧教學 - 偵測模組啟用

此文章是針對 Xposed Module 的開發進行教學,關於 Xposed 本身的功能請參考網路文章,這邊就不再贅述。

PS : 底下將 Xposed Module 簡稱為 Mod (懶得打字打太多...)

開發環境: (反正就是要開發 Android App , 如何設定這邊就不再贅述)
  1. Java Development Kit ( JDK )
  2. Android Studio (或者 Eclipse + Android SDK Tool)
  3. Xposed Bridge API 的 jar 檔案 ( 在這裡下載 : XDA )

 建議工具: 反編譯用,這些都可以 Google 找到載點
  1. JD-GUI
  2. dex2jar
  3. apktool

XDA 的 XposedBridgeAPI 載點文章可能會更新,請根據手機的版本取得對應版
本的 XposedBridgeAPI.jar 檔案,如:XposedBridgeApi-54.jar 就是 54
(在 Xposed Installer 中可以看到你手機使用的版本)。不過還是建議更新到最新
版比較好。


在開始之前,首先簡單介紹一下 Xposed 的運作原理:

簡單來說,Android App 在執行時都是透過一隻程序 "Zygote" 來啟動所有的 App,
並透過 app_process 進行執行階段的資源的讀取、載入。

而 Xposed 就是植入一個擴充自 app_process 的程序,讓每個 App 在啟動時都能
夠經過 Xposed Framework 的掌握,並藉此來進行 Method 的監聽甚至資源的取代。
( 有興趣可參考原文 Here )

因此撰寫一個 Mod 可以讓你輕鬆的 Hook (這邊翻作「監聽」似乎比較恰當) 每
個 Method 的呼叫,甚至有辦法能夠修改其內容。以 hook method 為例,會執行
底下流程:


因此可以在不修改 App 或者 framework 檔案的情況下,進行系統或者針對 App 的修改。

有了初步的了解後,接著就直接按步驟一步一步進行


[ Step 1 - 要想到一個點子 ]
當然這是最重要的,在這邊我以這一兩天吵得最兇的 MoPTT 擅自過濾字串的
事件為出發點,製作一個「禁止 MoPTT 過濾該字串」的功能。

而在另一篇文章會再加入:自訂 MoPTT 發文簽名檔 功能;

將兩個功能合併再一起來製作一個完整的 Xposed Module。



[ Step 2 - 建立 Android App 專案 ]

基本上需要注意的是, Xposed 目前支援的裝置為 Android 4.0.3+,
因此 Minimum Required SDK 可以選擇 4.0.3 就好了,當然這部分還
是要看自己打算製作在那些版本上面。


比較需要注意的是,大多數 Xposed Module 提供使用者設定介面,但其實
也可以沒有,因此不一定要建立 Activity,在這邊的例子(下一篇),我會再建立一
個 Activity 作為使用者設定的介面。


[ Step 3 - 加入 XposedBridge Library ]

由於撰寫時需要用到 Xposed Framework 的 API , 因此這邊要將 XposedBridgeApi.jar
加入參考路徑 ( 在 Compile 和製作 APK 的時候,不能將這個 jar 包進去 ),
步驟如下:
  1. 在專案上點右鍵,選擇 Build Path -> Configure Build Path...
  2. 按下 Add Extrenal JARs , 並找到 XposedBridgeApi.jar 檔案
  3. 按下確認後,他應該會出現在 Referenced Libraries 中
GIF 圖解:( 或點這裡 )


[ Step 4 - 加入 Xposed Module 描述 ]

接著要為這個 Xposed Module 加入敘述以及要求 XposedBridge 最小版本。
開啟 AndroidManifest.xml 在 <Application>這裡</Application> 間加入底下三個元素:

<meta-data android:name="xposedmodule" android:value="true" />
<meta-data android:name="xposeddescription"
        android:value="Mod 敘述"/>
<meta-data android:name="xposedminversion" android:value="54" />
  • xposeddescription 的 value 寫關於這個 Module 的敘述,會呈現在 Xposed Installer 的模組列表中,可以使用 @string/xxxx 的方式來進行 localization
  • xposedminversion 則是限制此 Module 需要用到的最低 XposedBridge 版本為何

[ Step 5 - 新增一個 Class 實作 IXposedMod 介面 ]

這是最重要的部分, Xposed Module 會以這個 Class 為基礎進行事件的處理

IXposedMod 只是最基本的 interface , 可以根據 Mod 的需要,選用不同的 sub interface:
  • IXposedHookLoadPackage:當 App 被載入時
  • IXposedHookInitPackageResources:當 App 資源載入時
  • IXposedHookZygoteInit:系統很早的初始化階段時
  • IXposedHookCmdInit:不太清楚,不過已經 deprecated 了
在此我們會用到的是 IXposedHookLoadPackage 底下會再說明原因。

因此先建立一個 Class 假設稱為 MoTweaker , 實作這一個 interface:
public class MoTweaker implements IXposedHookLoadPackage {
@Override
public void handleLoadPackage(LoadPackageParam lpkg) throws Throwable {}
}


[ Step 6 - 建立 xposed_init 檔案 ]

這也是最重要的步驟之一,Xposed framework 會根據這個內容載入對應的 Class。
請在 assets 目錄底下建立一個文字檔案:xposed_init (沒有副檔名)

文字檔內容是我們製作 Module 中實作 IXposedMod 的完整 Class 名稱,
在這邊的例子就是 tw.darkk6.xposed.motweaker.MoTweaker:


================================================
到此完成了每一個 Xposed Module 所有相同的前置作業,
接著才是撰寫 Xposed Module 正式的步驟。
================================================


[ Step 7 - 分析 App Code ]

上面提過了 Xposed的運作原理,是在呼叫某個 Method 前後進行 Hook,
並有辦法可以阻止 App 呼叫該 Method。

因此,我們必須要想,要如何 hook method 才能達成我們的目的。

由於我們這裡是針對 MoPTT 這個軟體做 hook,因此需要用到 apktool
和 dex2jar + JD-GUI 來進行 app 的 decompile,來分析程式碼,而這些
工具的使用方式就不再這邊贅述了。

未來如果想要針對系統 framework 層進行 hook,可能就需要看 Andoird 的
Source Code 了,這邊提供一個網站給大家參考:GrepCode

首先根據網路上提到,MoPTT 會將以 "Sent from JPTT on" 開頭的一行吃掉,
因此我們在 dex2jar 轉出來的 jar 檔案上透過 JD-GUI 搜尋字串:

 可以看到在 mong.moptt.ptt.a.d 和 mong.moptt.ptt.h 這兩個 class 中有找到:

稍微 trace 一下,大概看的出來,他會把每一行前面的空白 trim 掉後判斷是否
是以 "Sent from JPTT on" 開頭,如果是,就往下做一些動作 (應該就是吃掉這一行啦)

但由於 Xposed 無法直接對 Method 中的內容作修改 ( 可以完全取代,但不能在中途做修改 )

因此這邊稍微做一點 tricky 的方式:
  • 從兩段 Code 都可以看到是判斷某  string.startsWith() 作為判斷方式
  • 傳入參數都是 "Sent from JPTT on"
  • Xposed 可以在 Method 呼叫前取得、竄改參數
因此我們直接監聽 java.lang.String 的 startsWith(String prefix) 這個 method,
並判斷參數如果是 "Sent from JPTT on" 就直接 return false

不過這邊注意,由於要 hook 的是 java.lang.String 的 startsWith , 所以整個程式中
所有有用到 String.startsWith 的部分都會被我們監聽到,因此要盡量減少 hook 的
內容,避免讓整個 app 的反應變慢 (雖然我不確定到底影響有多大)


[Step 8 - 判斷並執行 hook ]

了解我們需要製作的部分後,接著就是撰寫 Code 的重點了,Xposed 在每一個
App 載入時,都會呼叫所有有實作 IXposedHookLoadPackage 模組的
handleLoadPackage 方法一次 (每個 app 都會呼叫一次)。
(所以一個 Xposde Module 是可以一次對多個 App 進行 hook 的)

而因為我們這邊只要 hook MoPTT , 因此我們撰寫底下的 Code:
@Override
public void handleLoadPackage(LoadPackageParam lpkg) throws Throwable {
    if(!"mong.moptt".equals(lpkg.packageName)) return;
    // Do hooking method
}
我們可以用 LoadPackageParam 的 packageName 來判斷現在載入的是哪一個 App,
這邊的程式碼就是:只有載入 MoPTT 時,才繼續往下做。

要注意,如果沒有加上這一段,那麼每一個 app載入時,我們的 module 都會執行到
Do hooking method 這一段,如此一來,每一個 App 都會被我們 hook。

接著就是要進行 Method 的 Hook 了,這邊有很多種方式可以 Hook,最簡單的方式是使用
 XposedHelpers.findAndHookMethod 進行。

findAndHookMethod(String,ClassLoader,String,Object...) 參數介紹:
  1. String : 要 hook 的 class 完整名稱
  2. ClassLoader : 從哪一個 classLoader 載入,這邊傳給 lpkg.classLoader 即可
  3. String : 要 hook 的 method 名稱
  4. Objects : 該 Method 的參數類型,以及最後一個必須是 XC_MethodHook 物件
假設我們要 hook String 的 startsWith(String):
findAndHookMethod(
                 "java.lang.String",
                 lpkg.classLoader,
                 "startsWith",
                 String.class,
                 callback );

而如果要 hook String 的 startsWith(String , int ):
findAndHookMethod(
                 "java.lang.String",
                 lpkg.classLoader,
                 "startsWith",
                 String.class,
                 int.class,
                 callback );
紫色的部分都是該 method 的參數型態,這邊可以使用字串描述,如:
findAndHookMethod(
                 "java.lang.String",
                 lpkg.classLoader,
                 "startsWith",
                 "java.lang.String",
                 int.class,
                 callback );
要注意的是,如果我們只監聽 String.startsWith( String , int )  的話,那麼
當程式呼叫 startsWith( String ) 時,是不會被我們所監聽的。

===================================================
補充說明:

不過,要是 String.startsWith(String) 是這樣寫的話,那還是會監聽的到:

    public boolean startsWith(String prefix){
        return  this.startsWith( prefix , 0 );
    } 
    public boolean startsWith(String prefix , int toffset){
        // bla bla bla
    }
因為呼叫 startsWith(String) 時,也是會呼叫到 startsWith(String,int),
因此還是可以監聽到。

另外還有一個方式是可以一次監聽所有相同名稱的 method:
XposedBrigde.hookAllMethods() ,但要注意,這是所有相同名稱的
Method 都會被 hook,如果像上面這種例子,程式呼叫 startsWith(String)
時,監聽的 callBack 會被觸發兩次。( 不詳細說明,體會一下箇中的滋味吧 )
===================================================

在 findAndHookMethod 最後一個參數要給予的  XC_MethodHook 物件就類似
onClickListener 這類的 class ,可以寫成 Anonymous,如:

XC_MethodHook callback = new XC_MethodHook() {
   @Override
   protected void beforeHookedMethod(MethodHookParam param) throws Throwable {}
   @Override
   protected void afterHookedMethod(MethodHookParam param) throws Throwable {}
};
當被 hook 的 method 被執行前會先執行 beforeHookedMethod,而當 method 執
行完畢後,會再執行  afterHookedMethod

在這兩個方法中,都可以透過 MethodHookParam 取得呼叫時的參數以及結果。

注意:如果在 beforeHookedMethod 中,呼叫了 param.setResult() 或者
   param.setThrowable(),程式不會執行原本的 method ,會直接結束。


到此,我要要做的事情是,監聽 String 的 startsWith(String),而若傳入的參數是
"Sent from JPTT on" 時,就直接傳回 fasle。

這部分在 before 做判斷即可,所以不需要寫到 after,礙於 Blogger 排版,將
程式碼寫在 Pastie:http://pastie.org/private/07rx1g8zoj6hyzzrdlneg


[ Step 9 - 沒啦,還在期待什麼嗎 XD ?]


是的,這樣就完成一個最簡單的 Xposed Module,完整 Code:

http://pastie.org/private/sgicqpegefvwyadzpqjg

5 則留言: