美文网首页
一个 Xposed 工程中实现 Native Hook 编写与

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

作者: 大发明家达文西 | 来源:发表于2021-01-13 17:42 被阅读0次

在使用 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 模块更加智能化。

相关文章

网友评论

      本文标题:一个 Xposed 工程中实现 Native Hook 编写与

      本文链接:https://www.haomeiwen.com/subject/vpwuaktx.html