抱歉,Xposed真的可以为所欲为——终 · 庖丁解码(下)

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: Xposed的使用不难,API也就那些,难点是: 逆向弄清楚Hook APP的方法调用流程,怎么调,参数都是干嘛的等。 经过反复练习,逆向Hook一个普通的APP(非企业级加固)写出可用的Xposed插件早已驾轻就熟(主要是磨时间),但有一个顾虑一直萦绕心间:不知道Xposed底层的具体实现原理。Tips:Xposed通常只能 Hook java层 及 应用资源的替换,有两个实现版本:4.4前的Dalvik虚拟机实现 和 5.0后ART虚拟机实现,本文针对后者进行分析,同时搭配 Android 5.1.1_r6 源码食用。

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 ActivityThreadhandleBindApplication(),参数类型为:**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方案可以借鉴一波~


网络异常,图片无法展示
|


参考文献:



相关文章
|
4月前
|
Python
惊呆了!学会这一招,你的Python上下文管理器也能玩出花样来文管理器也能玩出花样来
【7月更文挑战第6天】Python的上下文管理器是资源优雅管理的关键,与with语句结合,确保资源获取和释放。通过实现`__enter__`和`__exit__`,不仅能做资源分配和释放,还能扩展实现如计时、自动重试、事务处理等功能。例如,TimerContextManager类记录代码执行时间,展示了上下文管理器的灵活性。学习和利用这一机制能提升代码质量,增强功能,是Python编程的必备技巧。
33 0
|
数据库连接 数据库
红皮书——错误点
红皮书——错误点
红皮书——错误点
|
小程序 Java 机器人
使用Java实现发送微信消息(附源码)_此程序在手再也不怕对象跟你闹了
此程序在手再也不怕女朋友跟你闹了!!!!自从有了女朋友比如:早安、晚安之类的问候语可不能断,但是也难免有时候会忘记那么该咋么办呢?很简单写一个程序么,近日闲来无趣想着用Java写一个自动发送微信的小程序,实现定时给指定的好友发送指定的消息,这不就很Nice了?本文主要包括实现的思路、代码的实现、打包为jar快捷方式!
155 0
|
JSON 应用服务中间件 数据格式
杨洋撒撒一大片,Controller接收中文不再“不正经”,乱码问题这样解决,你信或不信
杨洋撒撒一大片,Controller接收中文不再“不正经”,乱码问题这样解决,你信或不信
124 0
杨洋撒撒一大片,Controller接收中文不再“不正经”,乱码问题这样解决,你信或不信
|
安全 Java API
抱歉,Xposed真的可以为所欲为——终 · 庖丁解码(中)
Xposed的使用不难,API也就那些,难点是: 逆向弄清楚Hook APP的方法调用流程,怎么调,参数都是干嘛的等。 经过反复练习,逆向Hook一个普通的APP(非企业级加固)写出可用的Xposed插件早已驾轻就熟(主要是磨时间),但有一个顾虑一直萦绕心间:不知道Xposed底层的具体实现原理。Tips:Xposed通常只能 Hook java层 及 应用资源的替换,有两个实现版本:4.4前的Dalvik虚拟机实现 和 5.0后ART虚拟机实现,本文针对后者进行分析,同时搭配 Android 5.1.1_r6 源码食用。
602 0
|
安全 Java API
抱歉,Xposed真的可以为所欲为——终 · 庖丁解码(上)
Xposed的使用不难,API也就那些,难点是: 逆向弄清楚Hook APP的方法调用流程,怎么调,参数都是干嘛的等。 经过反复练习,逆向Hook一个普通的APP(非企业级加固)写出可用的Xposed插件早已驾轻就熟(主要是磨时间),但有一个顾虑一直萦绕心间:不知道Xposed底层的具体实现原理。Tips:Xposed通常只能 Hook java层 及 应用资源的替换,有两个实现版本:4.4前的Dalvik虚拟机实现 和 5.0后ART虚拟机实现,本文针对后者进行分析,同时搭配 Android 5.1.1_r6 源码食用。
369 0
|
API
抱歉,Xposed真的可以为所欲为——1.基础知识储备(下)
本节简单介绍了什么是Xposed,基本原理,如何创建一个Xposed项目以及Xposed常用的类与方法。
748 0
|
Java Linux API
抱歉,Xposed真的可以为所欲为——1.基础知识储备(上)
本节简单介绍了什么是Xposed,基本原理,如何创建一个Xposed项目以及Xposed常用的类与方法。
440 0
|
存储 网络协议 数据安全/隐私保护
邮件传输的过程都看不懂。那我走(狗头)
给学习网络的新手一个我自己总结的建议: 我觉得学网络先要先把最基础最常用的协议的原理搞的明明白白,然后再学习难的协议或者是拓展的协议就好理解容易上手了。
231 0
邮件传输的过程都看不懂。那我走(狗头)
|
SQL
开胃菜解析
开胃菜解析
153 0