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方案可以借鉴一波~
参考文献:
- Xposed注入实现分析及免重启定制
- 从源码解析-Android中Zygote进程是如何fork一个APP进程的
- blog.csdn.net/ascii2/arti…
- 为所欲为的 Xposed 是怎么实现的?
- Application启动过程(最详细&最简单)
- Android Hook框架Xposed原理与源代码分析