Art下Xposed解析整理
Art下Xposed解析整理
Xposed主要实现两大功能,即注入和Hook,本文将主要分析整理Xposed框架从安装到使用一些关键实现
一、项目介绍
1.XposedInstaller
用于Xposed框架及模块的管理工作,最终产物即是我们看到的Xposed安装器应用
2.Xposed
本质上是Xposed版的zygote(孵化器),由XposedInstaller来将原生的zygote进行替换,从而达到注入的目的
3.XposedBridge
此项目产物即XposedBridge.jar,也就是Xposed待注入的jar包,编写Xposed模块时使用到的一些类如XposedHelpers、XposedBridge都在这里
4.android_art
此项目编译编译生成需要替换的系统so库例如libart.so
二、安装过程
详细的安装过程不是本文关注的重点,本文只探讨一些关键部分
在XposedInstall中点击Install将会下载Xposed需要的zip包,jar包内容物如下图(图片来自珍惜Any博客)
在"flash-script.sh"中,我们可以看到该脚本对于下载的zip包进行解压缩、替换系统文件、链接等操作,具体代码如下
从上述代码
我们可以看到,脚本对zip内容物对应的系统文件进行替换,对孵化器文件(app_process32)进行重新链接使其指向到Xposed的文件(app_process32_xposed),并且将libxposed_art和XposedBridge.jar等文件释放到系统目录。至此Xposed框架的安装完成。
三、源码分析
1.注入
由上述安装过程可以知道,Xposed安装完毕后将替换系统的app_process,要理解注入过程,我们先查看AOSP源码中的AndroidRuntime.cpp的start方法
根据源码可知,start方法会通过查找入参类的main方法进行反射调用,因此要想达到注入目的,需要替换原文件的main方法,对于xposed框架,替换的main方法在app_main2.cpp中。我们来比较一下源码和xposed的代码
源码:
Xposed代码:
可以看到,在执行runtime的start方法之前,Xposed先进行了初始化,然后将原本传入start的类名修改成了Xposed的类名。另外Xposed也重写了AndroidRuntime.cpp的 onVmCreated
方法,在其中加入了加载xposed 的so库文件的操作,如下图
而在 initialize
方法中,Xposed会将XposedBridge.jar加入到环境变量中,以实现注入,具体代码在 addJarToClasspath
方法代码如下
至此Xposed 的注入工作完成,由于替换了孵化器,所有被孵化器fork出的进程都将具备XposedBridge.jar的代码。
2.Hook
从findAndHookMethod方法向上寻找,最终到达HookMethodNative方法
其中 method
即待hook的方法,declaringClass
为方法所在的类,slot
为method在类中的偏移,additionalInfo
是一个 AdditionalHookInfo
对象,包含了hook的回调即入参返回值类型等信息,定义如下
接下来到native层,HookMethodNative在libxposed_common.cpp进行注册,具体实现在libxposed_art.cpp的 XposedBridge_hookMethodNative
方法,代码如下
此方法先是通过 ArtMethod::FromReflectedMethod
拿到待Hook方法的ArtMethod对象,最后调用ArtMethod的 EnableXposedHook
方法,此方法位于修改后的art_method.cc文件中,在此方法中,首先会将原方法进行备份,然后保存到 XposedHookInfo
结构体中,代码如下
接下来就是修改ArtMethod实现Hook,在分析前先看看ArtMethod类的简化版结构,不同安卓的变化比较大,以 5.0为例
class ArtMethod {
…………
protect:
HeapReference<Class> declaring_class_;
HeapReference<ObjectArray<ArtMethod>> dex_cache_resolved_methods_;
HeapReference<ObjectArray<Class>> dex_cache_resolved_types_;
//常见的Hook方案都需要修改此标志位将方法变为native方法
uint32_t access_flags_;
//codeitem在ArtMethod对象种的偏移
uint32_t dex_code_item_offset_;
uint32_t dex_method_index_;
uint32_t method_index_;
struct PACKED(4) PtrSizedFields {
//通过解释执行方式的入口点
void* entry_point_from_interpreter_;
//JNI方法入口点 非jni方法此字段无效 安卓7以前通常将原方法保存在此字段
void* entry_point_from_jni_;
//替换后方法的入口点
void* entry_point_from_quick_compiled_code_;
#if defined(ART_USE_PORTABLE_COMPILER)
void* entry_point_from_portable_compiled_code_;
#endif
} ptr_sized_fields_;
static GcRoot<Class> java_lang_reflect_ArtMethod_;
}
……
总结一下安卓7之前的hook流程:
获取函数的入口,得到函数结构体,替换accessflags将方法native化,保存原函数信息在entry_point_fromjni,替换entry_point_from_quick_compiled_code。调用被hook的方法时,首先会跳转替换的方法,然后跳转到entry_point_fromjni所指向的原方法。
下面放一下安卓5-9种PtrSizedFields结构体的变化
5.0:
struct PACKED(4) PtrSizedFields {
void* entry_point_from_interpreter_;
void* entry_point_from_jni_;
void* entry_point_from_quick_compiled_code_;
#if defined(ART_USE_PORTABLE_COMPILER)
void* entry_point_from_portable_compiled_code_;
#endif
} ptr_sized_fields_;
6.0:
struct PtrSizedFields {
void* entry_point_from_interpreter_;
void* entry_point_from_jni_;
void* entry_point_from_quick_compiled_code_;
} ptr_sized_fields_;
7.0:
struct PtrSizedFields {
ArtMethod** dex_cache_resolved_methods_;
void* dex_cache_resolved_types_;
void* entry_point_from_jni_;
void* entry_point_from_quick_compiled_code_;
} ptr_sized_fields_;
8.0:
struct PtrSizedFields {
ArtMethod** dex_cache_resolved_methods_;
void* data_;
void* entry_point_from_quick_compiled_code_;
} ptr_sized_fields_;
9.0:
struct PtrSizedFields {
void* data_;
void* entry_point_from_quick_compiled_code_;
} ptr_sized_fields_;
对于不同安卓版本的适配,主要是对原函数存放位置的适配,下面我以Xposed原生支持的安卓7为例,关键代码如下
由上述代码可知,EnableXposedHook
会先阻止JIT编译,然后调用 SetEntryPointFromJniPtrSize
方法将前面设置好的XposedHookInfo对象保存到ArtMethod的 entry_point_from_jni_
字段,如下图
接着调用 SetEntryPointFromQuickCompiledCode
方法将ArtMethod机器码执行时的入口点改为 GetQuickProxyInvokeHandler
方法的执行结果,即 art_quick_proxy_invoke_handler
如下图
最后调用 SetCodeItemOffset
将目标方法的codeitem置空,因为函数已经是native方法了
到这里hook需要的修改已经完成,接下来分析hook的调用流程。
3.调用
ArtMethod通过调用Invoke方法执行,当执行的是本地机器码时会调用 art_quick_invoke_stub
,静态方法是 art_quick_invoke_static_stub
通过查看汇编代码,可以看到 art_quick_invoke_stub
进入了INVOKE_STUB_CALL_AND_RETURN然后调用了宏
ART_METHOD_QUICK_CODE_OFFSET_64
查看宏定义如下图
可以看到这个就是我们前面修改过的hook入口点,此时实际返回的是 art_quick_proxy_invoke_handler
查看 art_quick_proxy_invoke_handler
的汇编代码可以看到直接调用了artQuickProxyInvokeHandler方法,直接看关键代码,判断当前方法是否被hook,如果被hook则调用 InvokeXposedHandleHookedMethod
方法,InvokeXposedHandleHookedMethod
关键代码如下:
根据注释可以知道,本质上是调用了java层的 XposedBridge.handleHookedMethod方法
其中 xposed_callback_class
、xposed_callback_method
两个参数在 onVmCreated
中完成赋值,如下
至此hook调用重新回到java层,在 handleHookedMethod
方法中,会先判断hook是否禁用,然后依次执行
beforeHookedMethod
、原方法、afterHookedMethod
当然,你也可以跳过原方法执行,只需要设置 returnEarly
即可,代码如下图
至此全部调用流程完成
四、检测和反制
1.Xposed Install检测
即检测包名 de.robv.android.xposed.installer
的应用是否安装
反制方式:
- 禁用app读取应用列表权限
- 修改Installer包名
2.特征文件检测
Xposed在安装时的特征文件释放到system目录下,应用启动时会将其载入到内存中,例如XposedBridge.jar,检测的原理是读取 proc/self/maps
文件,然后拿到加载的so和jar判断是否有xposed相关文件
反制方式
- 重新编译抹去Xposed特征
- 如果使用VA可以直接进行IO重定向
- hook
BufferedReader
类返回错误结果,代码如下
XposedHelpers.findAndHookMethod(BufferedReader.class, "readLine", new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
String result = (String) param.getResult();
if(result != null) {
if (result.contains("/data/data/de.robv.android.xposed.installer/bin/XposedBridge.jar")) {
param.setResult("");new File("").lastModified();
}
}
super.afterHookedMethod(param);
}
});
3.特征类检测
由于Xposed替换了系统的app_process导致所有进程的入口点变为 de.robv.android.xposed.XposedBridge.Main
方法,所以可以通过自造异常来查看堆栈中是否有特征类来达到检测目的。此外,也可以直接用loadClass或者反射方式确定特征类或方法是否存在。
反制方式:
- 重新编译Xposed,修改特征
- hook检测方法,代码如下
//通过堆栈检测
XposedHelpers.findAndHookMethod(StackTraceElement.class, "getClassName", new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
String result = (String) param.getResult();
if (result != null){
if (result.contains("de.robv.android.xposed.")) {
param.setResult("");
// Log.i(tag, "替换了,字符串名称 " + result);
}else if(result.contains("com.android.internal.os.ZygoteInit")){
param.setResult("");
}
}
super.afterHookedMethod(param);
}
});
//通过loadClass检测
XposedHelpers.findAndHookMethod(ClassLoader.class, "loadClass", String.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
if(param.args != null && param.args[0] != null && param.args[0].toString().startsWith("de.robv.android.xposed.")){
// 改成一个不存在的类
param.args[0] = "de.robv.android.xposed.ThTest";
}
super.beforeHookedMethod(param);
}
});
4.指定方法Native化
从前面的Hook过程我们可以知道,被hook的函数会变成native函数,我们可以通过这一特征进行检测
反制方式
- hook
isNative
方法
// 定义全局变量 modify
XposedHelpers.findAndHookMethod(Method.class, "getModifiers", new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
Method method = (Method)param.thisObject;
String[] array = new String[] { "getDeviceId" };
String method_name = method.getName();
if(Arrays.asList(array).contains(method_name)){
modify = 0;
}else{
modify = (int)param.getResult();
}
super.afterHookedMethod(param);
}
});
XposedHelpers.findAndHookMethod(Modifier.class, "isNative", int.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
param.args[0] = modify;
super.beforeHookedMethod(param);
}
});
五、免重启方案
在给出免重启方案前,我们先来了解一下为什么在更新模块之后需要重启手机,参考葫芦娃的文章
简单梳理一下,上文说到Xposed会替换原系统的孵化器并在启动时反射调用XposedBridge
类的main
方法,关键代码如下
我们可以看到main方法中调用了XposedInit.loadModules
方法将所有xposed模块一次性加入到内存中,因此后续修改插件也不会重新读取,必须重启手机才能实现。下面给出两种方案
1.修改插件加载逻辑
也就是葫芦娃文章中提到的方案,即修改XposedBridge
的源代码,在hookhandleBindApplication
的代码中重新加载一次插件,即每次fork新进程都会加载一次插件,这样就可以解决问题。这种方式需要重新编译XposedBridge.jar并替换手机中的jar包文件,也有现成的轮子可用
2.动态加载Dex方式
也是网上比较流行的方式,由于安卓中所有安装的app都在/data/app/包名 目录下存放apk文件,并且每次更新app都会更新这个apk文件,那么可以通过动态加载这个apk来反射调用目标xposed模块的handleLoadPackage
方法,关键代码如下:
/**
* 重定向handleLoadPackage函数前会执行initZygote
*
* @param loadPackageParam
* @throws Throwable
*/
@Override
public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {
// 排除系统应用
if (loadPackageParam.appInfo == null ||
(loadPackageParam.appInfo.flags & (ApplicationInfo.FLAG_SYSTEM | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)) == 1) {
return;
}
//将loadPackageParam的classloader替换为宿主程序Application的classloader,解决宿主程序存在多个.dex文件时,有时候ClassNotFound的问题
XposedHelpers.findAndHookMethod(Application.class, "attach", Context.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
Context context = (Context) param.args[0];
loadPackageParam.classLoader = context.getClassLoader();
Class<?> cls = getApkClass(context, modulePackageName, handleHookClass);
Object instance = cls.newInstance();
try {
cls.getDeclaredMethod(initMethod, startupparam.getClass()).invoke(instance, startupparam);
}catch (NoSuchMethodException e){
// 找不到initZygote方法
}
cls.getDeclaredMethod(handleHookMethod, loadPackageParam.getClass()).invoke(instance, loadPackageParam);
}
});
}
private Class<?> getApkClass(Context context, String modulePackageName, String handleHookClass) throws Throwable {
File apkFile = findApkFile(context, modulePackageName);
if (apkFile == null) {
throw new RuntimeException("寻找模块apk失败");
}
//加载指定的hook逻辑处理类,并调用它的handleHook方法
PathClassLoader pathClassLoader = new PathClassLoader(apkFile.getAbsolutePath(), ClassLoader.getSystemClassLoader());
Class<?> cls = Class.forName(handleHookClass, true, pathClassLoader);
return cls;
}
标题:Art下Xposed解析整理
作者:Cubeeeee
地址:http://blog.nps.fuguicun.com/articles/2021/11/24/1637742668826.html