一个 Xposed 工程中实现 Native Hook 编写与 so 注入

在使用 Xposed 注入 so 时通常需要以下几个步骤:

  1. 建立一个 Xposed 工程,实现 so 注入逻辑,指定被注入 so 的路径
  2. 建立一个 Native Hook 工程,编译生成 Hook 所需 so
  3. 第2步 生成的 so 文件拷贝到手机中 第1步 指定的目录下

这样弄来弄去感觉有些麻烦,主要是复制操作就需要不停点击各个窗口,adb 命令输来输去,于是就想个办法,一个 Android 工程搞定上面所有步骤。
测试环境如下:

  1. Android 9.0
  2. EdXposed ,用于 Android 8.0+ 的 Xposed 改版,模块更新后不需要重启,提高 debug 效率,接口还是原来的味道。
  3. Dobby,强烈推荐的 Native hook 框架,优点很多,比如代码更新频率高,跨平台,也就是 Android、iOS、桌面平台等通吃,32bit 和 64 bit 都能用,具体可以看看项目页面。

1. 原理说明

简化 Native Hook 的编写和注入分为两点:

  1. xposed 和 dobby ,一个java层,一个native层,本来就可以合并在一个 Android Studio 工程里,没啥好说的。
  2. 使用 Android 自带的 IPC 机制,把工程编译生成的 so 文件,拷贝到目标进程下,加载执行。
    这里我选择 ContentProvider 共享 /assets 目录下的 so 文件,当然可以用其他 IPC 方式,只不过 ContentProvider 是专门用于数据共享的组件,用起来更简单不容易错。

2. 代码实现

写 Xposed 模块很重要的一点就是要清楚什么代码运行在什么进程内。xposed_init 文件指明入口的代码,Xposed 会把他们注入到各个目标进程。而剩下的代码,则会在项目编译生成的 Apk 进程中执行。

1. 建立 Xposed 项目

使用 Android Studio 建立 Xposed 项目,这个没啥好废话的

2. 编写 ContentProvider 组件

不详细说明 ContentProvider 怎么用的,其他文档、博客比我讲的详细正确多了,我这里只点明一下思路。

  • 在 AndroidManifest 中声明 provider:
<provider
            android:name=".SoProvider"
            android:authorities="your.authorities" 这里需要改成你的
            android:enabled="true"
            android:exported="true"
            android:grantUriPermissions="true">
</provider>
  • 创建 SoProvider.java,并重写 openAssetFile 方法:
    openAssetFile 方法并未完整实现,需要重写
// 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;
    }
// 未实现
public @Nullable ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode)
            throws FileNotFoundException {
        throw new FileNotFoundException("No files supported by provider at "
                + uri);
    }

重写代码如下,这部分代码是在本进程中执行的,写好了可以测试一下能否使用

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 文件夹时,会压缩其中文件,这时需要在 build.gradle 添加规则,不压缩 .so 文件:
android {
    ......
    aaptOptions {
        noCompress "so"  //表示不让aapt压缩的文件后缀
    }
    ......
}

3. Xposed 模块编写

  • 获取目标 App 进程 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 注入点选择
    Android 通过 System.load()System.loadLibrary() 加载 native 库,目标 App 的 so 加载完毕后,再注入我们的 so,即可完成 Native Hook
    Android 9 中,想要加载其他 so,可以通过 Hook java.lang.Runtimeload0loadLibrary0 判断目标 so 是否已经加载完毕,再调用 XposedBridge.invokeOriginalMethod() 加载我们的 so。
    选择其他的方式很有可能导致 App 崩溃,这点可以自行测试,Hook 点的选取可以参考源码。
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);
        ......
    }
});
  • 获取远程 so 文件并加载
    代码细节,主要分为以下几步:
  1. 判断 so 名,是否为 native hook 目标
  2. 通过 ContentResolver 拷贝远程 so
  3. 加载远程 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 目标
        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 检查远程 so 文件是否存在
                // 远程 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();
                // 复制 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 目录下,这一步我也不想手动操作了,在项目的 CmakeLists.txt 增加输出路径即可:

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

我这里是直接输出到 assets 目录,想要做点什么不一样操作的,比如还要保留一部分 so,自行查阅 cmake 语法

总结

其他方法也行,但思路最重要。还可以加入 Service 等组件,使得 Xposed 模块更加智能化。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容