查看原文
其他

抱歉,Xposed真的可以为所欲为

CoderPig 鸿洋 2021-10-12

本文作者


作者:CoderPig

链接:

https://juejin.cn/post/6945000696441896973

本文由作者授权发布。


前言


两年前接触的Xposed,动机是:公司年会上抢不到红包,影响了吃饭的心情,想写一个自动抢红包的外挂。


在搜索引擎的帮助下,了解到了Xposed,在学习过程中,顺带写了点教程帮助后来者:

《抱歉,Xposed真的可以为所欲为——1.基础知识储备》

https://juejin.cn/post/6844903593628139528


《抱歉,Xposed真的可以为所欲为——2.改为OV机型流畅玩耍高帧率王者农药》https://juejin.cn/post/6844903593988849671


《抱歉,Xposed真的可以为所欲为——3.微信运动占领封面》https://juejin.cn/post/6844903598036353037


《抱歉,Xposed真的可以为所欲为——4.猜拳投骰子你能赢算我输》https://juejin.cn/post/6844903599076540430


《抱歉,Xposed真的可以为所欲为——5.我自己刷的Xposed凭什么不给我用》https://juejin.cn/post/6844903616877182984


《抱歉,Xposed真的可以为所欲为——6.你的表白撤不回了》https://juejin.cn/post/6844903624263335943


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源码食用。

http://androidxref.com/5.1.1_r6/xref/frameworks/


1Xposed的组成


这个庞大的工程由下面四个项目组成:


Xposed

https://github.com/rovo89/Xposed


C++部分,Xposed版的zygote,用于替换原生zygote,并为XposedBridge提供
JNI方法,需由XposedInstaller在root后放到/system/bin目录下;

XposedBridge
https://github.com/rovo89/XposedBridge

Java部分,编译后会生成一个jar包,负责在Native层与Framework层进行交互;

XposedInstaller
https://github.com/rovo89/XposedInstaller

Xposed插件管理及功能控制的APP,包括启用、下载、禁用插件等功能;

XposedTools
https://github.com/rovo89/XposedTools

用于编译Xposed及XposedBridge;


2Xposed的使用


一个简单的Hook示例如下:



几步走:


① 类实现IXposedHookLoadPackage接口。


② 重写handleLoadPackage()方法。


③ 调用XposedHelpers.findAndHookMethod(),传入完整类名、类加载器、方法名,参数类型,XC_MethodHook实例。


④ 按需重写beforeHookedMethod()(方法调用前执行代码) 和afterHookedMethod()方法(方法调用后执行代码)。


通过这样的操作,即可随意蹂躏一个Java方法的逻辑,以达到自己的目的。


3Android系统的启动过程


在开始探索实现原理前,先来了解下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。


4Zygote的启动流程


跟下 /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。


5Xposed如何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_ZYGOTEde.robv.android.xposed.XposedBridgemain() 方法,若初始化失败,则按照正常流程初始化。跟下XposedBridge → main(),部分代码如下:



到此Xposed Hook Zygote的流程很明了了:


编译生成自定义app_process→ 把原先调用ZygoteInit.main()处 改为调用XposedInit.main() → Hook资源和一些准备工作 → 调用系统原本启动Zygote的方法。


6Xposed如何替换系统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/framework/sdk23/arm64/xposed-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库等写到系统私有目录。


7APP启动创建进程的过程


在了解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, 
        nullnullnull, 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(thisdata.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进程的。


8Xposed如何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);
}

Hookcom.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);
    }
});

简单捋一捋这里逻辑:


① HookActivityThread的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如下:




总结


以上就是本节从源码层面探索Xposed基本实现原理的全部内容 ,以我目前的水平,深扒下去太难了...由衷地佩服作者rovo89大佬的Linux、Android系统功底,而且听说是业余时间写的,TQL吧!不过在探索过程中除了对Xposed的实现有所了解外,顺带过了Android系统的一些基础姿势(系统、Zygote和App进程的启动过程),受益良多!顺带总结此问题的面试八股文:
https://github.com/rovo89


  • 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,即可实现全局注入;



加餐:如何检测Xposed


内容摘取自:《Android Hook技术防范漫谈》

https://tech.meituan.com/2018/02/02/android-anti-hooking.html


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类中的fieldCachemethodCacheconstructorCache变量,读取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

https://github.com/KagurazakaHanabi/XposedHider/blob/master/app/src/main/java/com/yaerin/xposed/hider/XposedHook.java



另外,在Github上看到一个XposedChecker

https://github.com/w568w/XposedChecker

比较全的Xposed检测方案,JNI方案可以借鉴一波~



参考文献:

Xposed注入实现分析及免重启定制
https://bbs.pediy.com/thread-223713.htm

从源码解析-Android中Zygote进程是如何fork一个APP进程的
https://blog.csdn.net/qq_30993595/article/details/82747738

https://blog.csdn.net/ascii2/article/details/47974217

为所欲为的 Xposed 是怎么实现的?
https://mp.weixin.qq.com/s/zvxrLasOMOP9J_XSeonpvA

Application启动过程(最详细&最简单)
https://www.jianshu.com/p/4a8f44b6eecb

Android Hook框架Xposed原理与源代码分析
https://blog.csdn.net/wxyyxc1992/article/details/17320911




最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!


推荐阅读


玩转Android AOP  ,这3个案例你需要掌握!
Dialog 对应的 Context 必须是 Activity吗?
分享一波深入的Android 性能优化总结!


点击 关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~


┏(^0^)┛明天见!

: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存