|
在搜索引擎的帮助下,了解到了Xposed,在学习过程中,顺带写了点教程帮助后来者:
两年前接触的 Xposed,动机是:公司年会上抢不到红包,影响了吃饭的心情,想写一个自动抢红包的外挂。
在搜索引擎的帮助下,了解到了Xposed,在学习过程中,顺带写了点教程帮助后来者:
Xposed的使用不难,API也就那些,难点是: 逆向弄清楚Hook APP的方法调用流程,怎么调,参数都是干嘛的等。
经过反复练习,逆向Hook一个普通的APP(非企业级加固)写出可用的Xposed插件早已驾轻就熟(主要是磨时间),但有一个顾虑一直萦绕心间:不知道Xposed底层的具体实现原理,有些面试官(不菜那种)见到简历上写的 写过多个Xposed插件 时都会说上一句:
哦,还写过Xposed插件啊,都写过些什么插件?有了解过Xposed底层是怎么实现的吗?

啃源码,查资料,弄懂流程,写清楚,是一件费时费力且具有挑战性的事。
今年立过flag,要积极雄起,适逢在准备面试,索性把 弄懂Xposed原理 排上日程,内容较多建议收藏慢品,燥起来~

Tips:Xposed通常只能 Hook java层 及 应用资源的替换,有两个实现版本:4.4前的Dalvik虚拟机实现 和 5.0后ART虚拟机实现,本文针对后者进行分析,同时搭配 Android 5.1.1_r6 源码食用。
0x1、Xposed的组成这个庞大的工程由下面四个项目组成:
- Xposed → C++部分,Xposed版的zygote,用于替换原生zygote,并为XposedBridge提供JNI方法,需由XposedInstaller在root后放到/system/bin目录下;
- XposedBridge → Java部分,编译后会生成一个jar包,负责在Native层与Framework层进行交互;
- XposedInstaller → Xposed插件管理及功能控制的APP,包括启用、下载、禁用插件等功能;
- XposedTools → 用于编译Xposed及XposedBridge;
0x2、Xposed的使用一个简单的Hook示例如下:

几步走:
- ① 类实现 IXposedHookLoadPackage 接口
- ② 重写 handleLoadPackage() 方法
- ③ 调用 XposedHelpers.findAndHookMethod(),传入完整类名、类加载器、方法名,参数类型,XC_MethodHook实例;
- ④ 按需重写 beforeHookedMethod()(方法调用前执行代码) 和 afterHookedMethod() 方法(方法调用后执行代码);
通过这样的操作,即可随意蹂躏一个Java方法的逻辑,以达到自己的目的。
0x3、Android系统的启动过程在开始探索实现原理前,先来了解下Android系统的启动过程,简略流程图如下:

关注 Zygote进程 , 它由 init进程 启动,启动时会创建一个Davlik虚拟机实例,并把Java运行时库加载到进程中,并注册一些Android核心类的JNI到前面创建的Dalvik虚拟机实例中。
所有APP进程都是由Zygote进程孵化(fork) 而来的,fork时不仅仅会获得Zygote进程中的Dalvik虚拟机实例拷贝,还会与Zygote一起 共享Java运行时库。
Xposed就是利用这样的机制,只往Zygote中注入 XposedBridge.jar,就可以实现全局注入。
Tips:Android 5.0 开始zygote是两个进程,32位(兼容armeabi和armeabi-v7a等32 位架构的本地动态库的应用) 和 64位(arm64-v8a等64 位架构本地库动态库),init.rc 文件也做了区分,init.zygote32.rc 启动 32位的zygote,init.zygote64 启动 64位的zygote。
0x4、Zygote的启动流程跟下 /system/core/rootdir/init.zygote.rc:
service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server复制代码分段解析下这段代码:
- service → ATL语言语法,启动一个服务进程;
- zygote → 启动的程序名称,这指zygote进程;
- /system/bin/app_process → 可执行文件路径( app_main.cpp );
- -Xzygote /system/bin → 指定参数传到app_main.cpp中;
- --zygote --start-system-server → 传的具体参数值;
简单点说就是:启动了Zygote进程,传递的参数可在 /frameworks/base/cmds/app_process/app_main.cpp 中找到:

对传进来的参数做匹配,zygote、startSystem标志位设置为true,接着定位下哪里用到了zygote这个标记:

跟下:runtime.start() 定位到 frameworks/base/core/jni/AndroidRuntime.cpp,关键代码如下:
// ① 初始化jni接口JniInvocation jni_invocation;jni_invocation.Init(NULL);// ② 创建VM虚拟机JNIEnv* env;if (startVm(&mJavaVM, &env) != 0) { return;}onVmCreated(env);// ③ 注册JNI方法if (startReg(env) < 0) { ALOGE("Unable to register all android natives\n"); return;}// ④ 调用className类的static void main(String args[]) 方法slashClassName = toSlashClassName(className);jclass startClass = env->FindClass(slashClassName);// 找到main函数jmethodID startMeth = env->GetStaticMethodID(startClass, "main", "([Ljava/lang/String;)V");if (startMeth == NULL) { ALOGE("JavaVM unable to find main() in '%s'\n", className); /* keep going */} else { // 通过 JNI 调用 main 函数,从 C++ 到 Java env->CallStaticVoidMethod(startClass, startMeth, strArray); if (env->ExceptionCheck()) threadExitUncaughtException(env);}复制代码所以这里创建了一个虚拟机,注册JNI方法,然后调用 com.android.internal.os.ZygoteInit 的 main(),跟下frameworks/base/core/java/com/android/internal/os/ZygoteInit.java:
public static void main(String argv[]) { try { ... // ① 注册一个name为zygote的socket,用于和其他进程通信 registerZygoteSocket(socketName); // ② 预加载所需资源到VM中,如class、resource、OpenGL、公用Library等; // 所有fork的子进程共享这份空间而无需重新加载,减少了应用程序的启动时间, // 但也增加了系统的启动时间,Android启动最耗时的部分之一。 preload(); // ③ 初始化gc,只是通知VM进行垃圾回收,具体回收时间、怎么回收,由VM内部算法决定。 // gc()需在fork前完成,这样将来复制的子进程才能有尽可能少的垃圾内存没释放; gcAndFinalize(); // ④ 启动system_server,即fork一个Zygote子进程 if (startSystemServer) { startSystemServer(abiList, socketName); } // ⑤ 进入循环模式,获取客户端连接并处理 runSelectLoop(abiList); // ⑥ 关闭和清理zygote socket closeServerSocket(); } catch (MethodAndArgsCaller caller) { caller.run(); } catch (RuntimeException ex) { Log.e(TAG, "Zygote died with exception", ex); closeServerSocket(); throw ex; }}复制代码跟下 startSystemServer():
private static boolean startSystemServer(String abiList, String socketName) throws MethodAndArgsCaller, RuntimeException { int pid; try { ... // fork出system_server进程,返回pid,此处pid为0 pid = Zygote.forkSystemServer( parsedArgs.uid, parsedArgs.gid, parsedArgs.gids, parsedArgs.debugFlags, null, parsedArgs.permittedCapabilities, parsedArgs.effectiveCapabilities); } catch (IllegalArgumentException ex) { throw new RuntimeException(ex); } /* 进入子进程 */ if (pid == 0) { // Android 5.0上有两个Zygote进程:zygote 和 zygote64 // 对于有两个zygote进程的情况,需等待第二个zygote创建完成; if (hasSecondZygote(abiList)) { waitForSecondaryZygote(socketName); } // 完成system_server进程的剩余工作 handleSystemServerProcess(parsedArgs); } return true;}复制代码Tips:fork()方法被调用一次,返回两次,区别是:子进程的返回值是0,父进程的返回值是子进程的进程id,可以保证子进程的进程id不可能为0。
0x5、Xposed如何Hook Zygote从上面的跟踪结果,不难得出这样的调用链:
init进程 → init.rc → app_process(app_main.cpp) → 启动Zygote进程 → ZygoteInit的main() → startSystemServer() → fork出system_server子进程。
接着来看下Xposed具体是怎么注入Zygote的,打开Xposed的仓库,发现这样两个文件:

em...区分Android 5.0 前后版本,打开 Android.mk:

呕吼,定制 app_process 文件,而且可以根据sdk版本选择对应文件作为入口,打开 app_main2.app,跟到下述位置:

有点眼熟,跟下:xposed.cpp –> initialize,部分代码如下:
bool initialize(bool zygote, bool startSystemServer, const char* className, int argc, char* const argv[]) { // ① 初始化xposed相关变量 xposed->zygote = zygote; xposed->startSystemServer = startSystemServer; xposed->startClassName = className; xposed->xposedVersionInt = xposedVersionInt; ... // ② 将XposedBridge.jar加载到系统的CLASSPATH路径中。 return addJarToClasspath();}复制代码跳回前面,如果初始化成功,调用 调用 XPOSED_CLASS_DOTS_ZYGOTE 即 de.robv.android.xposed.XposedBridge 的 main() 方法,若初始化失败,则按照正常流程初始化。跟下 XposedBridge → main(),部分代码如下:

到此Xposed Hook Zygote的流程很明了了:
编译生成自定义app_process → 把原先调用 ZygoteInit.main() 处 改为调用 XposedInit.main() → Hook资源和一些准备工作 → 调用系统原本启动Zygote的方法。
0x6、Xposed如何替换系统app_process上面说到系统的app_process替换成自定义的了,那具体是怎么替换的呢?
那就得看看 XposedInstaller 了,除了介绍那里说到的管理Xposed模块外,它还有两个核心功能:替换系统app_process 和 将XposedBridge.jar文件放到私有目录中。
打开XposedInstaller项目搜下app_process:

在Code没找到,却在某次commit找到了,点开,定位到 assets/install.sh

流程简洁明了,改权限搬运文件,不过这是旧版的,在新版中却没找到这个关键字。
打开手机XposedInstaller抓下包,发现了这样的请求:
http://dl-xda.xposed.info/framework.json复制代码接着项目中搜索下域名:

跟抓包url有点不同,请求下framework.json:

可以,就是拼接zip包url的模板,包含:系统sdk版本,架构,xposed版本号,以我的魅蓝e2为例,拼接后的url:
http://dl-xda.xposed.info/framew ... v89-sdk23-arm64.zip复制代码下载解压后,开始找搬运脚本,可却只找到了二进制文件、so库和XposedBridge.jar:

尴尬,只能从源码入手了,XposedInstaller点击安装时会弹出 Install 和 Install via recovery,搜下后面这个字符串:

跟到:FrameworkZips → INSTALLER

不难看出这个函数是拿来解析framework.json的

哪里调用到了 parseZipSpec():

哪里调用到了 getOnline():

跟下:StatusInstallerFragment → addZipViews() → addZipView() → showActionDialog()

这个 flash() 就是刷入方法,可以看到第二个参数类型是不一样的:FlashDirectly 和 FlashRecoveryAuto,先跟下前者的flash()方法:

再看看 FlashRecoveryAuto 的 flash() 方法:

当系统处于recovery模式会自动检测command文件是否存在,存在其中的指令,后续刷入跟前者一致。
不过,还是没有解决我的疑问,啥时候替换的app_process,网上说update-binary其中会调用一个 flash-script.sh 文件,在zip包中可以找到此文件:

em...很明显,就是替换文件的脚本,还对32位及64位的情况作了区分,验证起来有些麻烦,后面我如果自己编译Xposed再验证下吧,暂且记住这个结论:
XposedInstaller下载补丁包 → 获取root权限 → 解压复制update-binary文件到特定目录 → 文件执行时会调用flash-script.sh脚本,将app_process、Xposedbridge.jar、so库等写到系统私有目录。
0x7、APP启动创建进程的过程在了解Xposed如何注入APP进程前,先要简单了解下 APP启动创建进程的过程。
当我们点击一个桌面图标启动应用(注:Launcher也是一个APP),层层调用最后走到:ActivityStackSupervisor.startSpecificActivityLocked,判断进程不存在就会去创建一个新的进程。整套流程大概是这样的:
APP进程 → Binder机制 → 通知system_server中的ActivityManagerService(AMS) → system_server中的LocalSocket → 通知Zygote进程 → fork一个APP子进程。
小插曲:为什么SystemServer进程与Zygote进程通讯采用Socket而不是Binder?
答:Binder通信是多线程的,可能存在一种情况,父进程binder线程有锁,fork子进程也有锁,但是父进程的子线程并没有拷贝过来,此时子进程会处于死锁。为了规避这种情况,fork不允许存在多线程转而使用socket通信。
收到Socket通知fork子进程,回到 ZygoteInit.runSelectLoop(),此时进入轮询模式,等待客户端连接并处理:
private static void runSelectLoop(String abiList) throws MethodAndArgsCaller { ArrayList<FileDescriptor> fds = new ArrayList<FileDescriptor>(); ArrayList<ZygoteConnection> peers = new ArrayList<ZygoteConnection>(); FileDescriptor[] fdArray = new FileDescriptor[4]; while (true) { int index; //... if (index < 0) { throw new RuntimeException("Error in select()"); } else if (index == 0) { // 有客户端连接请求,创建ZygoteConnection并加入到fds中 ZygoteConnection newPeer = acceptCommandPeer(abiList); peers.add(newPeer); fds.add(newPeer.getFileDescriptor()); } else { // 通过socket接收来自对端的数据,执行相应操作 boolean done; done = peers.get(index).runOnce(); if (done) { peers.remove(index); fds.remove(index); // 处理完移除该文件描述符 } } }}复制代码acceptCommandPeer() 获取连接过来的客户端,然后执行 ZygoteConnection.runOnce()处理请求。跟下:runOnce():
// fork子进程pid = Zygote.forkAndSpecialize(parsedArgs.uid, parsedArgs.gid, parsedArgs.gids, parsedArgs.debugFlags, rlimits, parsedArgs.mountExternal, parsedArgs.seInfo, parsedArgs.niceName, fdsToClose, parsedArgs.instructionSet, parsedArgs.appDataDir);if (pid == 0) { // 子进程执行 IoUtils.closeQuietly(serverPipeFd); serverPipeFd = null; // 进入子进程流程 handleChildProc(parsedArgs, descriptors, childPipeFd, newStderr); return true;} else { // 父进程执行 IoUtils.closeQuietly(childPipeFd); childPipeFd = null; // 进入父进程流程 return handleParentProc(pid, descriptors, serverPipeFd, parsedArgs);}复制代码跟下:handleChildProc(),关键代码如下:
if (parsedArgs.invokeWith != null) { WrapperInit.execApplication(parsedArgs.invokeWith, parsedArgs.niceName, parsedArgs.targetSdkVersion, pipeFd, parsedArgs.remainingArgs);} else { RuntimeInit.zygoteInit(parsedArgs.targetSdkVersion, parsedArgs.remainingArgs, null /* classLoader */);}复制代码关注:RuntimeInit.zygoteInit(),关键代码如下:
public static final void zygoteInit(int targetSdkVersion, String[] argv, ClassLoader classLoader) throws ZygoteInit.MethodAndArgsCaller { if (DEBUG) Slog.d(TAG, "RuntimeInit: Starting application from zygote"); redirectLogStreams(); // 通用初始化 commonInit(); // Zygote初始化 nativeZygoteInit(); // 应用初始化 applicationInit(targetSdkVersion, argv, classLoader);}复制代码跟下:applicationInit(),关键代码如下:
private static void applicationInit(int targetSdkVersion, String[] argv, ClassLoader classLoader) throws ZygoteInit.MethodAndArgsCaller { // true代表APP退出时不调用AppRuntime.onExit(),否则会在退出前调用 nativeSetExitWithoutCleanup(true); // 设置虚拟机的内存利用率参数为0.75 VMRuntime.getRuntime().setTargetHeapUtilization(0.75f); VMRuntime.getRuntime().setTargetSdkVersion(targetSdkVersion); // 参数解析 final Arguments args; try { args = new Arguments(argv); } catch (IllegalArgumentException ex) { Slog.e(TAG, ex.getMessage()); return; } // 此处args.startClass为android.app.ActivityThread,调用它的main方法 invokeStaticMain(args.startClass, args.startArgs, classLoader);}复制代码跟下:ActivityThread.main(),关键代码如下:
public static void main(String[] args) { // 创建主线程的Looper Looper.prepareMainLooper(); // 关联AMS ActivityThread thread = new ActivityThread(); thread.attach(false, startSeq); // 初始化主线程Handler if (sMainThreadHandler == null) { sMainThreadHandler = thread.getHandler(); } // 开启主线程消息循环 Looper.loop(); throw new RuntimeException("Main thread loop unexpectedly exited");}复制代码到此,子进程和主线程创建完毕,接着就是各种初始化操作了,此处特别关注Application的启动过程,跟下 thread.attach():
// ① 获取AMS的代理对象final IActivityManager mgr = ActivityManager.getService();try { // ② 通过代理对象调用attachApplication()获得启动application的所需信息(进程相关数据) mgr.attachApplication(mAppThread, startSeq);} catch (RemoteException ex) { throw ex.rethrowFromSystemServer();}复制代码跟下:ActivityManagerService.attachApplication():
public final void attachApplication(IApplicationThread thread, long startSeq) { synchronized (this) { int callingPid = Binder.getCallingPid(); final int callingUid = Binder.getCallingUid(); final long origId = Binder.clearCallingIdentity(); attachApplicationLocked(thread, callingPid, callingUid, startSeq);// 1 Binder.restoreCallingIdentity(origId); }}复制代码跟下:attachApplicationLocked()
private final boolean attachApplicationLocked(IApplicationThread thread, int pid, int callingUid, long startSeq) { // 根据pid获取存储在AMS中,对应进程的相关信息 ProcessRecord app; long startTime = SystemClock.uptimeMillis(); if (pid != MY_PID && pid >= 0) { synchronized (mPidsSelfLocked) { app = mPidsSelfLocked.get(pid); } } else { app = null; } // IApplicationThread是ActivityThread的内部类,负责管理与AMS的通讯, // 此处是通知ActivityThread启动Application thread.bindApplication(processName, appInfo, providers, null, profilerInfo, null, null, null, testMode, mBinderTransactionTrackingEnabled, enableTrackAllocation, isRestrictedBackupMode || !normalMode, app.persistent, new Configuration(getGlobalConfiguration()), app.compat, getCommonServicesLocked(app.isolated), mCoreSettingsObserver.getCoreSettingsLocked(), buildSerial, isAutofillCompatEnabled); // ...}复制代码跟下 bindApplication():
public final void bindApplication(String processName, ApplicationInfo appInfo, //... AppBindData data = new AppBindData(); data.processName = processName; data.appInfo = appInfo; data.providers = providers; //... sendMessage(H.BIND_APPLICATION, data);}复制代码初始化AppBindData实例完成一些初始化,然后发送一个 BIND_APPLICATION 的消息到消息队列中。跟下 handleMessage():
public void handleMessage(Message msg) { switch (msg.what) { case BIND_APPLICATION: Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "bindApplication"); AppBindData data = (AppBindData)msg.obj; handleBindApplication(data); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); break;复制代码跟下 handleBindApplication(),核心代码如下:
private void handleBindApplication(AppBindData data) { // 将UI线程注册为运行时虚拟机 VMRuntime.registerSensitiveThread(); // 创建上下文对象 final ContextImpl appContext = ContextImpl.createAppContext(this, data.info); updateLocaleListFromAppContext(appContext, mResourcesManager.getConfiguration().getLocales()); // 创建Instrumentation实例,用于创建、启动Application,并跟踪Application的生命周期。 try { final ClassLoader cl = instrContext.getClassLoader(); mInstrumentation = (Instrumentation) cl.loadClass(data.instrumentationName.getClassName()).newInstance(); } catch (Exception e) { throw new RuntimeException( "Unable to instantiate instrumentation " + data.instrumentationName + ": " + e.toString(), e); } // 创建Application对象 Application app; app = data.info.makeApplication(data.restrictedBackupMode, null); // 调用Instrumentation的onCreate(),内部是空实现 mInstrumentation.onCreate(data.instrumentationArgs); // 内部实际上调用的application的onCreate() mInstrumentation.callApplicationOnCreate(app);复制代码总结下列个流程图:

弄清楚APP启动创建进程的过程,接着来康康Xposed是怎么HOOK APP进程的。
0x8、Xposed如何Hook APP进程① XposedInit.initForZygote()回到上面Hook Zygote的 XposedInit.initForZygote(),跟下:
if (needsToCloseFilesForFork()) { XC_MethodHook callback = new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { XposedBridge.closeFilesBeforeForkNative(); } @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { XposedBridge.reopenFilesAfterForkNative(); } }; Class<?> zygote = findClass("com.android.internal.os.Zygote", null); hookAllMethods(zygote, "nativeForkAndSpecialize", callback); hookAllMethods(zygote, "nativeForkSystemServer", callback);}复制代码Hook com.android.internal.os.Zygote 类的 nativeForkAndSpecialize() 和 nativeForkSystemServer() 方法,添加钩子回调,继续往下走:
// normal process initialization (for new Activity, Service, BroadcastReceiver etc.)findAndHookMethod(ActivityThread.class, "handleBindApplication", "android.app.ActivityThread.AppBindData", new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { // 获得activityThread、ApplicationInfo、ComponentName实例 ActivityThread activityThread = (ActivityThread) param.thisObject; ApplicationInfo appInfo = (ApplicationInfo) getObjectField(param.args[0], "appInfo"); String reportedPackageName = appInfo.packageName.equals("android") ? "system" : appInfo.packageName; SELinuxHelper.initForProcess(reportedPackageName); ComponentName instrumentationName = (ComponentName) getObjectField(param.args[0], "instrumentationName"); // 判断ComponentName是否为null,null说明没有hook成功 if (instrumentationName != null) { Log.w(TAG, "Instrumentation detected, disabling framework for " + reportedPackageName); XposedBridge.disableHooks = true; return; } CompatibilityInfo compatInfo = (CompatibilityInfo) getObjectField(param.args[0], "compatInfo"); if (appInfo.sourceDir == null) return; setObjectField(activityThread, "mBoundApplication", param.args[0]); loadedPackagesInProcess.add(reportedPackageName); // 获得LoadedApk实例,设置资源目录 LoadedApk loadedApk = activityThread.getPackageInfoNoCheck(appInfo, compatInfo); XResources.setPackageNameForResDir(appInfo.packageName, loadedApk.getResDir()); // 初始化LoadPackageParam,塞packageName、processName、classloader 等 XC_LoadPackage.LoadPackageParam lpparam = new XC_LoadPackage.LoadPackageParam(XposedBridge.sLoadedPackageCallbacks); lpparam.packageName = reportedPackageName; lpparam.processName = (String) getObjectField(param.args[0], "processName"); lpparam.classLoader = loadedApk.getClassLoader(); lpparam.appInfo = appInfo; lpparam.isFirstApplication = true; // 将lpparam参数传递到所有Xposed模块 XC_LoadPackage.callAll(lpparam); }});复制代码简单捋一捋这里逻辑:
- ① Hook ActivityThread 的 handleBindApplication(),参数类型为:**android.app.ActivityThread.AppBindData**在,这里有疑惑可以看回App启动创建进程部分代码;
- ② param.args[0] 为上面的**AppBindData**参数,获取这个参数里的属性和对一些属性进行覆盖;
- ③ 通过 activityThread 实例获得 loadedApk 实例,拿到**ClassLoader**,然后传递到所有Xposed模块;
还是比较简单的,在应用调用 Application.onCreate(),把ClassLoader传递到所有的Xposed模块。
② findAndHookMethod()
跟下 findMethodExact():

跟下:XposedBridge.hookMethod():

③ hookMethodNative()此处讲解的是:libxposed_dalvik.cpp,libxposed_art.cpp 中实现较为复杂,暂且略过~

跟下:hookedMethodCallback():

跟下:methodXposedBridgeHandleHookedMethod

好吧,最终调用的是 XposedBridge 里的 handleHookedMethod()。
④ handleHookedMethod()
这里就比较好理解了,就是循环按顺序依次将:beforeHookedMethod、原方法、afterHookMethod 调用一遍,而调用原方法调用的:XposedBridge_invokeOriginalMethodNative 如下:

0x9、总结以上就是本节 从源码层面探索Xposed基本实现原理 的全部内容 ,以我目前的水平,深扒下去太难了...由衷地佩服作者 rovo89 大佬的Linux、Android系统功底,而且听说是业余时间写的,TQL吧!不过在探索过程中除了对Xposed的实现有所了解外,顺带过了Android系统的一些基础姿势(系统、Zygote和App进程的启动过程),受益良多!顺带总结此问题的面试八股文:
- Xposed需要root权限,安装XposedInstaller获得root权限后,执行update-binary将app_process、Xposedbridge.jar、so库等刷入到系统私有目录中;
- init进程解析执行init.rc,通过app_process启动Zygote进程,Zygote是所有APP进程的父进程;
- Xposed的app_process把原先 ZygoteInit.main() 改为调用 XposedInit.main(),执行完一些Hook工作再调用原本启动的Zygote的方法;
- 子进程fork时不仅会获得虚拟机实例拷贝,还会和Zygote共享Java运行时库,所以只需在Zygote注入一次XposedBridge.jar,即可实现全局注入;
0x?、加餐:如何检测Xposed内容摘取自:《Android Hook技术防范漫谈》
Java层检测① 通过PackageManager查看安装列表过滤
PackageManager packageManager = context.getPackageManager();List applicationInfoList = packageManager.getInstalledApplications(PackageManager.GET_META_DATA);for (ApplicationInfo applicationInfo: applicationInfoList) { if (applicationInfo.packageName.equals("de.robv.android.xposed.installer")) { // is Xposed TODO... } }复制代码没啥用,Hook下 PackageManager → getInstalledApplications() 不返回xposed包名即可,有些人自行编译的Xposed包名也不一定是这个。
② 自造异常读取堆栈
程序方法异常栈中会出现Xposed相关身影,自造异常try-catch,判断下日志信息是否有Xposed的调用方法:
try { throw new Exception("blah");} catch(Exception e) { for (StackTraceElement stackTraceElement: e.getStackTrace()) { // stackTraceElement.getClassName() stackTraceElement.getMethodName() 是否存 在Xposed }}复制代码③ 检查关键字Java方法变成Native JNI方法
反射调用 Modifier.isNative(method.getModifiers()) 检验普通Java方法是否变成了Native JNI方法,是的话很有可能被Xposed Hook了,当然Xposed同样可以Hook此方法返回值来规避。
④ 反射读取XposedHelper类字段
反射遍历XposedHelper类中的fieldCache、methodCache、constructorCache变量,读取HashMap缓存字段,如字段项的key中包含App中唯一或敏感方法等,即可认为有Xposed注入。

boolean methodCache = CheckHook(clsXposedHelper, "methodCache", keyWord);private static boolean CheckHook(Object cls, String filedName, String str) { boolean result = false; String interName; Set keySet; try { Field filed = cls.getClass().getDeclaredField(filedName); filed.setAccessible(true); keySet = filed.get(cls)).keySet(); if (!keySet.isEmpty()) { for (Object aKeySet: keySet) { interName = aKeySet.toString().toLowerCase(); if (interName.contains("meituan") || interName.contains("dianping") ) { result = true; break; } } } ... return result;}复制代码native层检测Java层做和何种检测都可以hook对应API来绕过检测,Xposed一般Hook不了Native层,所以可以在Native层使用C来解析/proc/self/maps文件,搜检App自身加载的库中是否存在XposedBridge.jar、相关的Dex、Jar和So库等文件。
bool is_xposed(){ bool rel = false; FILE *fp = NULL; char* filepath = "/proc/self/maps"; ... string xp_name = "XposedBridge.jar"; fp = fopen(filepath,"r")) while (!feof(fp)) { fgets(strLine,BUFFER_SIZE,fp); origin_str = strLine; str = trim(origin_str); if (contain(str,xp_name)) { rel = true; //检测到Xposed模块 break; } } ...}复制代码上述这种方法依旧可绕过,直接Hook File类,把它指向别的路径即可:XposedHider

另外,在Github上看到一个 XposedChecker 比较全的Xposed检测方案,JNI方案可以借鉴一波~

作者:coder-pig
链接:https://juejin.cn/post/6945000696441896973
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
|
|