在使用 Xposed 注入 so 时通常需要以下几个步骤:
- 建立一个 Xposed 工程,实现 so 注入逻辑,指定被注入 so 的路径
- 建立一个 Native Hook 工程,编译生成 Hook 所需 so
- 把 第2步 生成的 so 文件拷贝到手机中 第1步 指定的目录下
这样弄来弄去感觉有些麻烦,主要是复制操作就需要不停点击各个窗口,adb 命令输来输去,于是就想个办法,一个 Android 工程搞定上面所有步骤。
测试环境如下:
- Android 9.0
- EdXposed ,用于 Android 8.0+ 的 Xposed 改版,模块更新后不需要重启,提高 debug 效率,接口还是原来的味道。
- Dobby,强烈推荐的 Native hook 框架,优点很多,比如代码更新频率高,跨平台,也就是 Android、iOS、桌面平台等通吃,32bit 和 64 bit 都能用,具体可以看看项目页面。
1. 原理说明
简化 Native Hook 的编写和注入分为两点:
- xposed 和 dobby ,一个java层,一个native层,本来就可以合并在一个 Android Studio 工程里,没啥好说的。
- 使用 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,可以通过 Hookjava.lang.Runtime
的load0
与loadLibrary0
判断目标 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 文件并加载
代码细节,主要分为以下几步:
- 判断 so 名,是否为 native hook 目标
- 通过
ContentResolver
拷贝远程 so - 加载远程 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 模块更加智能化。