一個(gè) Xposed 工程中實(shí)現(xiàn) Native Hook 編寫與 so 注入

在使用 Xposed 注入 so 時(shí)通常需要以下幾個(gè)步驟:

  1. 建立一個(gè) Xposed 工程,實(shí)現(xiàn) so 注入邏輯戴陡,指定被注入 so 的路徑
  2. 建立一個(gè) Native Hook 工程传睹,編譯生成 Hook 所需 so
  3. 第2步 生成的 so 文件拷貝到手機(jī)中 第1步 指定的目錄下

這樣弄來弄去感覺有些麻煩弃锐,主要是復(fù)制操作就需要不停點(diǎn)擊各個(gè)窗口遏佣,adb 命令輸來輸去白粉,于是就想個(gè)辦法掩缓,一個(gè) Android 工程搞定上面所有步驟雪情。
測(cè)試環(huán)境如下:

  1. Android 9.0
  2. EdXposed ,用于 Android 8.0+ 的 Xposed 改版拾因,模塊更新后不需要重啟旺罢,提高 debug 效率,接口還是原來的味道绢记。
  3. Dobby扁达,強(qiáng)烈推薦的 Native hook 框架,優(yōu)點(diǎn)很多蠢熄,比如代碼更新頻率高跪解,跨平臺(tái),也就是 Android签孔、iOS叉讥、桌面平臺(tái)等通吃,32bit 和 64 bit 都能用饥追,具體可以看看項(xiàng)目頁面图仓。

1. 原理說明

簡(jiǎn)化 Native Hook 的編寫和注入分為兩點(diǎn):

  1. xposed 和 dobby ,一個(gè)java層但绕,一個(gè)native層救崔,本來就可以合并在一個(gè) Android Studio 工程里惶看,沒啥好說的。
  2. 使用 Android 自帶的 IPC 機(jī)制六孵,把工程編譯生成的 so 文件纬黎,拷貝到目標(biāo)進(jìn)程下,加載執(zhí)行劫窒。
    這里我選擇 ContentProvider 共享 /assets 目錄下的 so 文件本今,當(dāng)然可以用其他 IPC 方式,只不過 ContentProvider 是專門用于數(shù)據(jù)共享的組件主巍,用起來更簡(jiǎn)單不容易錯(cuò)冠息。

2. 代碼實(shí)現(xiàn)

寫 Xposed 模塊很重要的一點(diǎn)就是要清楚什么代碼運(yùn)行在什么進(jìn)程內(nèi)。xposed_init 文件指明入口的代碼孕索,Xposed 會(huì)把他們注入到各個(gè)目標(biāo)進(jìn)程铐达。而剩下的代碼,則會(huì)在項(xiàng)目編譯生成的 Apk 進(jìn)程中執(zhí)行檬果。

1. 建立 Xposed 項(xiàng)目

使用 Android Studio 建立 Xposed 項(xiàng)目,這個(gè)沒啥好廢話的

2. 編寫 ContentProvider 組件

不詳細(xì)說明 ContentProvider 怎么用的唐断,其他文檔选脊、博客比我講的詳細(xì)正確多了,我這里只點(diǎn)明一下思路脸甘。

  • 在 AndroidManifest 中聲明 provider:
<provider
            android:name=".SoProvider"
            android:authorities="your.authorities" 這里需要改成你的
            android:enabled="true"
            android:exported="true"
            android:grantUriPermissions="true">
</provider>
  • 創(chuàng)建 SoProvider.java恳啥,并重寫 openAssetFile 方法:
    openAssetFile 方法并未完整實(shí)現(xiàn),需要重寫
// ContentProvider.java
public @Nullable AssetFileDescriptor openAssetFile(@NonNull Uri uri, @NonNull String mode)
            throws FileNotFoundException {
        ParcelFileDescriptor fd = openFile(uri, mode);
        return fd != null ? new AssetFileDescriptor(fd, 0, -1) : null;
    }
// 未實(shí)現(xiàn)
public @Nullable ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode)
            throws FileNotFoundException {
        throw new FileNotFoundException("No files supported by provider at "
                + uri);
    }

重寫代碼如下丹诀,這部分代碼是在本進(jìn)程中執(zhí)行的钝的,寫好了可以測(cè)試一下能否使用

public class SoProvider extends ContentProvider {
    private final String TAG = "SoProvider";

    // ......省略其他無用代碼......

    @Nullable
    @Override
    public AssetFileDescriptor openAssetFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
        AssetFileDescriptor afd = null;
        try {
            Context context = getContext();
            if (context == null) {
                throw new FileNotFoundException("Context null");
            }
            AssetManager am = context.getAssets();
            /*Log.d(TAG, "Uri authority: " + uri.getAuthority());
            Log.d(TAG, "Uri path: " + uri.getPath());*/
            // uri.getPath 得到的是 /assets/your/path/...so
            // 需要切割掉路徑中的 "/assets/"
            String assetPath = Objects.requireNonNull(uri.getPath()).substring(8);
            Log.d(TAG, "Asset path: " + assetPath);
            afd = am.openFd(assetPath);
            Log.d(TAG, String.format("openAssetFile: Open asset file: %s, len: %d", assetPath, afd.getDeclaredLength()));

        } catch (IOException e) {
            Log.e(TAG, "openAssetFile failed: " + e.getMessage());
        }
        return afd;
    }
}
  • 禁止 aapt 壓縮
    aapt 在打包 assets 文件夾時(shí),會(huì)壓縮其中文件铆遭,這時(shí)需要在 build.gradle 添加規(guī)則硝桩,不壓縮 .so 文件:
android {
    ......
    aaptOptions {
        noCompress "so"  //表示不讓aapt壓縮的文件后綴
    }
    ......
}

3. Xposed 模塊編寫

  • 獲取目標(biāo) App 進(jìn)程 Context
    方法不唯一,視具體情況而定枚荣,我這里選擇通過 Application.getBaseContext 方法得到碗脊,而 Application 又是繼承 ContextWrapper ,可以這樣寫:
public class XposedEntry implements IXposedHookLoadPackage {
    private static final String TAG = "HookTag";
    private static Context appContext = null;

    @Override
    public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {

        final String currentPackageName = lpparam.packageName;
        if (!currentPackageName.equals("your.target.package")) {
            return;
        }

        XposedHelpers.findAndHookMethod("android.content.ContextWrapper", lpparam.classLoader, "attachBaseContext", Context.class, new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                appContext = (Context) param.args[0];
            }
        });
}
  • Xposed 注入點(diǎn)選擇
    Android 通過 System.load()System.loadLibrary() 加載 native 庫橄妆,目標(biāo) App 的 so 加載完畢后衙伶,再注入我們的 so,即可完成 Native Hook
    Android 9 中害碾,想要加載其他 so矢劲,可以通過 Hook java.lang.Runtimeload0loadLibrary0 判斷目標(biāo) so 是否已經(jīng)加載完畢,再調(diào)用 XposedBridge.invokeOriginalMethod() 加載我們的 so慌随。
    選擇其他的方式很有可能導(dǎo)致 App 崩潰芬沉,這點(diǎn)可以自行測(cè)試,Hook 點(diǎn)的選取可以參考源碼。
XposedHelpers.findAndHookMethod("java.lang.Runtime", lpparam.classLoader, "loadLibrary0", ClassLoader.class, String.class, new XC_MethodHook() {
    @Override
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
        super.beforeHookedMethod(param);
    }

    @Override
    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
        ......
        XposedBridge.invokeOriginalMethod(param.method, param.thisObject, newArgs);
        ......
    }
});

XposedHelpers.findAndHookMethod("java.lang.Runtime", lpparam.classLoader, "load0", Class.class, String.class, new XC_MethodHook() {
    @Override
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
        super.beforeHookedMethod(param);
    }

    @Override
    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
        ......
        XposedBridge.invokeOriginalMethod(param.method, param.thisObject, newArgs);
        ......
    }
});
  • 獲取遠(yuǎn)程 so 文件并加載
    代碼細(xì)節(jié)花嘶,主要分為以下幾步:
  1. 判斷 so 名笋籽,是否為 native hook 目標(biāo)
  2. 通過 ContentResolver 拷貝遠(yuǎn)程 so
  3. 加載遠(yuǎn)程 so
XposedHelpers.findAndHookMethod("java.lang.Runtime", lpparam.classLoader, "load0", Class.class, String.class, new XC_MethodHook() {
    @Override
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
        super.beforeHookedMethod(param);
    }

    @Override
    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
        Class<?> fromClass = (Class<?>) param.args[0];
        String libName = (String) param.args[1];
        // Log.d(TAG, "load0: " + libName);
        // 1. 判斷 so 名,是否為 native hook 目標(biāo)
        if (libName != null && libName.equals("/target/so/name")) {
            try {
                Log.d(TAG, "Found target so file\n" + libName);
                if (appContext == null) {
                    Log.d(TAG, "App Context is null");
                    return;
                }
                Log.d(TAG, "Got app context");
                // 待注入的 so 是否存在椭员,存在則刪除
                File injectedSoFile = new File(appContext.getFilesDir(), "libhook.so");
                recursiveDelete(injectedSoFile);
                Log.d(TAG, "Old so deleted");
                // ContentResolver 檢查遠(yuǎn)程 so 文件是否存在
                // 遠(yuǎn)程 so uri车海,換成你自己的
                Uri uri = Uri.parse("content://your.authorities/assets/sodir/armeabi-v7a/libhook.so");
                // Obtain remote so file
                ContentResolver resolver = appContext.getContentResolver();
                AssetFileDescriptor descriptor = resolver.openAssetFileDescriptor(uri, "r", null);
                if (descriptor == null) {
                    Log.e(TAG, "Invalid AssetFileDescriptor");
                    return;
                }
                if (descriptor.getLength() > Integer.MAX_VALUE) {
                    Log.e(TAG, "File too large");
                    return;
                }
                Log.d(TAG, "Found remote so file");
                int fileLen = (int) descriptor.getLength();
                FileInputStream fileInputStream = descriptor.createInputStream();
                // 復(fù)制 so 文件到本地
                byte[] fileContent = new byte[fileLen];
                fileInputStream.read(fileContent, 0, fileLen);
                FileOutputStream localSo = new FileOutputStream(injectedSoFile);
                localSo.write(fileContent);
                fileInputStream.close();
                localSo.close();
                descriptor.close();
                Log.d(TAG, "Copy so success, so size: " + injectedSoFile.length());
                Log.d(TAG, "Load my library");
                // 加載 so
                Object[] newArgs = new Object[]{fromClass, injectedSoFile.getAbsolutePath()};
                XposedBridge.invokeOriginalMethod(param.method, param.thisObject, newArgs);
            } catch (IOException e) {
                Log.e(TAG, "Receive so file error: " + e.getMessage());
            }
        }
    }
});

4. 指定工程 so 輸出目錄

工程的 so 編譯完成后,還需要放到 assets 目錄下隘击,這一步我也不想手動(dòng)操作了侍芝,在項(xiàng)目的 CmakeLists.txt 增加輸出路徑即可:

set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/../assets/sodir/${ANDROID_ABI})

我這里是直接輸出到 assets 目錄,想要做點(diǎn)什么不一樣操作的埋同,比如還要保留一部分 so州叠,自行查閱 cmake 語法

總結(jié)

其他方法也行,但思路最重要凶赁。還可以加入 Service 等組件咧栗,使得 Xposed 模塊更加智能化。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末虱肄,一起剝皮案震驚了整個(gè)濱河市致板,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌咏窿,老刑警劉巖斟或,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異集嵌,居然都是意外死亡萝挤,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門根欧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來怜珍,“玉大人,你說我怎么就攤上這事凤粗』婷妫” “怎么了?”我有些...
    開封第一講書人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵侈沪,是天一觀的道長(zhǎng)揭璃。 經(jīng)常有香客問我,道長(zhǎng)亭罪,這世上最難降的妖魔是什么瘦馍? 我笑而不...
    開封第一講書人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮应役,結(jié)果婚禮上情组,老公的妹妹穿的比我還像新娘燥筷。我一直安慰自己,他們只是感情好院崇,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開白布肆氓。 她就那樣靜靜地躺著,像睡著了一般底瓣。 火紅的嫁衣襯著肌膚如雪谢揪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,079評(píng)論 1 285
  • 那天捐凭,我揣著相機(jī)與錄音拨扶,去河邊找鬼。 笑死茁肠,一個(gè)胖子當(dāng)著我的面吹牛患民,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播垦梆,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼匹颤,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了托猩?” 一聲冷哼從身側(cè)響起惋嚎,我...
    開封第一講書人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎站刑,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鼻百,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡绞旅,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了温艇。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片因悲。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖勺爱,靈堂內(nèi)的尸體忽然破棺而出晃琳,到底是詐尸還是另有隱情,我是刑警寧澤琐鲁,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布卫旱,位于F島的核電站,受9級(jí)特大地震影響围段,放射性物質(zhì)發(fā)生泄漏顾翼。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一奈泪、第九天 我趴在偏房一處隱蔽的房頂上張望适贸。 院中可真熱鬧灸芳,春花似錦、人聲如沸拜姿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蕊肥。三九已至谒获,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間晴埂,已是汗流浹背究反。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留儒洛,地道東北人精耐。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像琅锻,于是被迫代替她去往敵國和親卦停。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容