在使用 Xposed 注入 so 時(shí)通常需要以下幾個(gè)步驟:
- 建立一個(gè) Xposed 工程,實(shí)現(xiàn) so 注入邏輯戴陡,指定被注入 so 的路徑
- 建立一個(gè) Native Hook 工程传睹,編譯生成 Hook 所需 so
- 把 第2步 生成的 so 文件拷貝到手機(jī)中 第1步 指定的目錄下
這樣弄來弄去感覺有些麻煩弃锐,主要是復(fù)制操作就需要不停點(diǎn)擊各個(gè)窗口遏佣,adb 命令輸來輸去白粉,于是就想個(gè)辦法掩缓,一個(gè) Android 工程搞定上面所有步驟雪情。
測(cè)試環(huán)境如下:
- Android 9.0
- EdXposed ,用于 Android 8.0+ 的 Xposed 改版拾因,模塊更新后不需要重啟旺罢,提高 debug 效率,接口還是原來的味道绢记。
- 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):
- xposed 和 dobby ,一個(gè)java層但绕,一個(gè)native層救崔,本來就可以合并在一個(gè) Android Studio 工程里惶看,沒啥好說的。
- 使用 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矢劲,可以通過 Hookjava.lang.Runtime
的load0
與loadLibrary0
判斷目標(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é)花嘶,主要分為以下幾步:
- 判斷 so 名笋籽,是否為 native hook 目標(biāo)
- 通過
ContentResolver
拷貝遠(yuǎn)程 so - 加載遠(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 模塊更加智能化。