xposed实现插件代码更新同时避免重启系统方案

2024-06-12 16:48

本文主要是介绍xposed实现插件代码更新同时避免重启系统方案,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

话说,刚开始接触xposed,就对这个频繁重启不太爽。我很少使用动态调试的方式去跟踪apk,因为处理反调试,开启调试环境(重编、或者开启系统debug开关等)特别麻烦。但是纯粹使用xposed拦截的方式去跟踪业务逻辑呢,修改一个日志打印都需要重启Android系统。所以我就想,能不能做到更新插件代码,不重启Android系统,也让系统生效呢?然后我去研究了xposed源码,和Android相关源码,算是得到了一个比较好的方案。

 

目前热加载在我的工具包里面使用非常频繁了,我们大量的破解工作都是基于这个热加载框架实现。https://gitee.com/virjar/xposedhooktool

 

下面讲述我如何找到热加载方案,以及如何实现的。

1. xposed加载插件的逻辑分析。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

/**

     * Load a module from an APK by calling the init(String) method for all classes defined

     * in <code>assets/xposed_init</code>.摘抄自xposed源码,其中apk为插件apk路径,如 /data/app/com.virjar.xposedhooktool-1/base.apk

     */

    private static void loadModule(String apk, ClassLoader topClassLoader) {

        Log.i(TAG, "Loading modules from " + apk);

        DexFile dexFile;

        try {

            dexFile = new DexFile(apk);

        catch (IOException e) {

            Log.e(TAG, "  Cannot load module", e);

            return;

        }

 

    //....省略代码

        ClassLoader mcl = new PathClassLoader(apk, XposedBridge.BOOTCLASSLOADER);

//....省略代码

                        if (moduleInstance instanceof IXposedHookLoadPackage)

                            XposedBridge.hookLoadPackage(new IXposedHookLoadPackage.Wrapper((IXposedHookLoadPackage) moduleInstance));

//....省略代码

    }

xposed在系统启动的时候,就分析了各个xposed模块。然后将其注入到了受精卵进程里面,使用的是PathClassloader。PathClassLoader和我们写的xposed模块对应的apk文件关联。这样任何一个apk执行,其vm环境都存在xposed模块的代码

2. 当宿主app启动的时候发生了什么

当我们注入的宿主apk启动的时候,由于进程是重受精卵fork而来,所以vm的环境中,已经存在了模块代码,代码和我们的插件apk地址关联。但是这个关联有不确定性,由于系统启动的时候,已经创建了classLoader。系统启动后,我们刷新xposed模块的apk,宿主apk里面的classLoader,会加载最新的xposed模块的apk么?答案是不会的,他只会使用第一次关联的那个xposed模块apk。这就是为啥我们更新xposed模块代码之后,必须重启才能生效的根本原因。

3. 为啥pathclassloader不会加载最新的apk。

上图是Android源码中,关于DexFile加载的描述。当一个apk对应的dex文件被打开时,将会使用dexopt,将dex进行优化,变成odex文件,并且存放到/data/dalvik-cache中。然后真正打开的文件,永远是dalvik-cache里面的odex文件。

4. 如果一个apk被占用(其他进程打开了),这个时候重装apk。将会发生么?

1

2

3

/data/app/com.virjar.xposedhooktool-1/base.apk

/data/app/com.virjar.xposedhooktool-2/base.apk

/data/app/com.virjar.xposedhooktool/base.apk

覆盖安装一个apk,如果原来的apk正在使用中。那么将会使用新的apk路径存放,也就是说,安装路径将会和第一次的安装路径不一样。上面的案例是在小米note上面的实验。

5. 如何修复这个关联关系,让xposed框架加载最新的插件代码

这里我们看到,可能的方案,就是删除dalvik-cache里面的缓存,这样Android系统就会重新生成这个缓存文件。重启宿主,就会使用最新的apk代码?这个思路其实有问题。

1. xposed模块代码在受精卵的时候注入,这个时候,文件已经打开。我们知道,如果一个进程在删除文件之前打开那个文件,那么进程仍然持有被删除的文件的fd,只是删除后,其他进程再次打开文件,将会找不到删除掉的文件。

2. 覆盖安装模式,apk不是安装在第一次安装的路径。在上一点我们提到,apk安装路径可能由/data/app/com.virjar.xposedhooktool-1/base.apk变成了/data/app/com.virjar.xposedhooktool-2/base.apk,这个时候xposed变量里面存储的还是老的地址。所以也是打开不了新的apk的。

 

所以,如果我们能够计算出最新的apk安装地址,然后使用一个新的classLoader关联新的apk,然后通过这个classLoader里面的class去注入代码。那么是不是就可以永远使用最新的apk,避免重启Android系统了呢?

6.实现代码

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

package com.virjar.xposedhooktool.hotload;

 

import android.annotation.SuppressLint;

import android.app.Application;

import android.content.Context;

import android.content.pm.PackageInfo;

import android.content.pm.PackageManager;

import android.util.Log;

 

import com.google.common.collect.Maps;

 

import org.apache.commons.io.IOUtils;

import org.apache.commons.lang3.StringUtils;

import org.xmlpull.v1.XmlPullParser;

 

import java.io.File;

import java.io.InputStream;

import java.util.concurrent.ConcurrentMap;

import java.util.zip.ZipFile;

 

import brut.androlib.res.decoder.AXmlResourceParser;

import dalvik.system.PathClassLoader;

import de.robv.android.xposed.IXposedHookLoadPackage;

import de.robv.android.xposed.XC_MethodHook;

import de.robv.android.xposed.XposedBridge;

import de.robv.android.xposed.XposedHelpers;

import de.robv.android.xposed.callbacks.XC_LoadPackage;

 

/**

 * XposedInit

 * <br/>

 * 请注意,该类是热加载入口,不允许直接访问工程其他代码,只要访问过的类,都不能实现热加载

 *

 * @author virjar@virjar.com

 */

public class XposedInit implements IXposedHookLoadPackage {

 

    @Override

    public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam lpparam) {

        XposedHelpers.findAndHookMethod(Application.class"attach", Context.classnew XC_MethodHook() {

 

            //由于集成了脱壳功能,所以必须选择before了

            @Override

            protected void beforeHookedMethod(MethodHookParam param) throws Throwable {

                hotLoadPlugin(lpparam.classLoader, (Context) param.args[0], lpparam);

            }

        });

    }

 

    public static String packageName(ClassLoader classLoader) {

        Object element = bindApkLocation(classLoader);

        if (element == null) {

            return null;

        }

        //原文件可能已被删除,直接打开文件无法得到句柄,所以只能去获取持有删除文件句柄对象

        ZipFile zipFile = (ZipFile) XposedHelpers.getObjectField(element, "zipFile");

        return findPackageName(zipFile);

    }

 

    private static ClassLoader replaceClassloader(Context context, XC_LoadPackage.LoadPackageParam lpparam) {

        ClassLoader classLoader = XposedInit.class.getClassLoader();

        if (!(classLoader instanceof PathClassLoader)) {

            XposedBridge.log("classloader is not PathClassLoader: " + classLoader.toString());

            return classLoader;

        }

 

        //find the apk location installed in android system,this file maybe a dex cache mapping(do not the real installed apk)

        Object element = bindApkLocation(classLoader);

        if (element == null) {

            return classLoader;

        }

        File apkLocation = (File) XposedHelpers.getObjectField(element, "zip");

        //原文件可能已被删除,直接打开文件无法得到句柄,所以只能去获取持有删除文件句柄对象

        ZipFile zipFile = (ZipFile) XposedHelpers.getObjectField(element, "zipFile");

        if (zipFile == null && apkLocation.exists()) {

            try {

                zipFile = new ZipFile(apkLocation);

            catch (Exception e) {

                //ignore

            }

        }

//        if (zipFile == null) {

//            return classLoader;

//        }

        String packageName = findPackageName(zipFile);

        if (StringUtils.isBlank(packageName)) {

//            XposedBridge.log("can not find package name  for this apk ");

//            return classLoader;

            //先暂时这么写,为啥有问题后面排查

            packageName = "com.virjar.xposedhooktool";

        }

 

        //find real apk location by package name

        PackageManager packageManager = context.getPackageManager();

        if (packageManager == null) {

            XposedBridge.log("can not find packageManager");

            return classLoader;

        }

 

        PackageInfo packageInfo = null;

        try {

            packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_META_DATA);

        catch (PackageManager.NameNotFoundException e) {

            //ignore

        }

        if (packageInfo == null) {

            XposedBridge.log("can not find plugin install location for plugin: " + packageName);

            return classLoader;

        }

 

        //check if apk file has relocated,apk location maybe change if xposed plugin is reinstalled(system did not reboot)

        //xposed 插件安装后不能立即生效(需要重启Android系统)的本质原因是这两个文件不equal

 

        //hotClassLoader can load apk class && classLoader.getParent() can load xposed framework and android framework

        //使用parent是为了绕过缓存,也就是不走系统启动的时候链接的插件apk,但是xposed框架在这个classloader里面持有,所以集成

 

        return createClassLoader(classLoader.getParent(), packageInfo);

    }

 

    @SuppressLint("PrivateApi")

    private void hotLoadPlugin(ClassLoader ownerClassLoader, Context context, XC_LoadPackage.LoadPackageParam lpparam) {

        boolean hasInstantRun = true;

        try {

            XposedInit.class.getClassLoader().loadClass(INSTANT_RUN_CLASS);

        catch (ClassNotFoundException e) {

            //正常情况应该报错才对

            hasInstantRun = false;

        }

        if (hasInstantRun) {

            Log.e("weijia""  Cannot load module, please disable \"Instant Run\" in Android Studio.");

            return;

        }

 

        ClassLoader hotClassLoader = replaceClassloader(context, lpparam);

//        if (hotClassLoader == XposedInit.class.getClassLoader()) {

//            //这证明不需要实现代码替换,或者热加载框架作用失效

//            //XposedBridge.log("热加载未生效");

//        }

        // check  Instant Run, 热加载启动后,需要重新检查Instant Run

        hasInstantRun = true;

        try {

            hotClassLoader.loadClass(INSTANT_RUN_CLASS);

        catch (ClassNotFoundException e) {

            //正常情况应该报错才对

            hasInstantRun = false;

        }

        if (hasInstantRun) {

            Log.e("weijia""  Cannot load module, please disable \"Instant Run\" in Android Studio.");

            return;

        }

 

        try {

            Class<?> aClass = hotClassLoader.loadClass("com.virjar.xposedhooktool.hotload.HotLoadPackageEntry");

            Log.i("weijia""invoke hot load entry");

            aClass

                    .getMethod("entry", ClassLoader.class, ClassLoader.class, Context.class, XC_LoadPackage.LoadPackageParam.class)

                    .invoke(null, ownerClassLoader, hotClassLoader, context, lpparam);

        catch (Exception e) {

            if (e instanceof ClassNotFoundException) {

                InputStream inputStream = hotClassLoader.getResourceAsStream("assets/hotload_entry.txt");

                if (inputStream == null) {

                    XposedBridge.log("do you not disable Instant Runt for Android studio?");

                else {

                    IOUtils.closeQuietly(inputStream);

                }

            }

            XposedBridge.log(e);

        }

    }

 

    private static final String INSTANT_RUN_CLASS = "com.android.tools.fd.runtime.BootstrapApplication";

    private static ConcurrentMap<String, PathClassLoader> classLoaderCache = Maps.newConcurrentMap();

 

    /**

     * 这样做的目的是保证classloader单例,因为宿主存在多个dex的时候,或者有壳的宿主在解密代码之后,存在多次context的创建,当然xposed本身也存在多次IXposedHookLoadPackage的回调

     *

     * @param parent      父classloader

     * @param packageInfo 插件自己的包信息

     * @return 根据插件apk创建的classloader

     */

    private static PathClassLoader createClassLoader(ClassLoader parent, PackageInfo packageInfo) {

        if (classLoaderCache.containsKey(packageInfo.applicationInfo.sourceDir)) {

            return classLoaderCache.get(packageInfo.applicationInfo.sourceDir);

        }

        synchronized (XposedInit.class) {

            if (classLoaderCache.containsKey(packageInfo.applicationInfo.sourceDir)) {

                return classLoaderCache.get(packageInfo.applicationInfo.sourceDir);

            }

            XposedBridge.log("create a new classloader for plugin with new apk path: " + packageInfo.applicationInfo.sourceDir);

            PathClassLoader hotClassLoader = new PathClassLoader(packageInfo.applicationInfo.sourceDir, parent);

            classLoaderCache.putIfAbsent(packageInfo.applicationInfo.sourceDir, hotClassLoader);

            return hotClassLoader;

        }

    }

 

 

    /**

     * File name in an APK for the Android manifest.

     */

    private static final String ANDROID_MANIFEST_FILENAME = "AndroidManifest.xml";

 

    private static Object bindApkLocation(ClassLoader pathClassLoader) {

        // 不能使用getResourceAsStream,这是因为classloader双亲委派的影响

//        InputStream stream = pathClassLoader.getResourceAsStream(ANDROID_MANIFEST_FILENAME);

//        if (stream == null) {

//            XposedBridge.log("can not find AndroidManifest.xml in classloader");

//            return null;

//        }

 

        // we can`t call package parser in android inner api,parse logic implemented with native code

        //this object is dalvik.system.DexPathList,android inner api

        Object pathList = XposedHelpers.getObjectField(pathClassLoader, "pathList");

        if (pathList == null) {

            XposedBridge.log("can not find pathList in pathClassLoader");

            return null;

        }

 

        //this object is  dalvik.system.DexPathList.Element[]

        Object[] dexElements = (Object[]) XposedHelpers.getObjectField(pathList, "dexElements");

        if (dexElements == null || dexElements.length == 0) {

            XposedBridge.log("can not find dexElements in pathList");

            return null;

        }

 

        return dexElements[0];

        // Object dexElement = dexElements[0];

 

        // /data/app/com.virjar.xposedhooktool/base.apk

        // /data/app/com.virjar.xposedhooktool-1/base.apk

        // /data/app/com.virjar.xposedhooktool-2/base.apk

        // return (File) XposedHelpers.getObjectField(dexElement, "zip");

    }

 

    private static String findPackageName(ZipFile zipFile) {

        if (zipFile == null) {

            return null;

        }

        InputStream stream = null;

        try {

            stream = zipFile.getInputStream(zipFile.getEntry(ANDROID_MANIFEST_FILENAME));

            AXmlResourceParser xpp = new AXmlResourceParser(stream);

            int eventType;

            //migrated form ApkTool

            while ((eventType = xpp.next()) > -1) {

                if (XmlPullParser.END_DOCUMENT == eventType) {

                    return null;

                else if (XmlPullParser.START_TAG == eventType && "manifest".equalsIgnoreCase(xpp.getName())) {

                    // read <manifest> for package:

                    for (int i = 0; i < xpp.getAttributeCount(); i++) {

                        if (StringUtils.equalsIgnoreCase(xpp.getAttributeName(i), "package")) {

                            return xpp.getAttributeValue(i);

                        }

                    }

                }

            }

            return null;

        catch (Exception e) {

            XposedBridge.log(e);

            return null;

        finally {

            //不能关闭zipFile

            IOUtils.closeQuietly(stream);

        }

    }

}

7.原理描述

我使用一个永远不变的class,作为加载器。这个Class的功能就是寻找最新的apk安装路径,然后构造新的classLoader,然后调用hook入口。

其中apk安装路径计算,使用PackageManager,这个也就是xposed-installer里面的逻辑。

需要注意的是,classLoader有双亲委派机制,如果我们的classLoader使用xposed创建的classLoader作为parent的话,加载的class都会以super为主。我们的新代码将不会生效(因为虚拟机里面,父classLoader能够加载的类,不允许子classLoader来加载)

而且,热加载入口,不能直接调用业务入口,因为classLoader有一个隐式加载Class的过程,会使用当然Class的classLoader加载将要访问的class。当前classLoader加载的class,依然是老的apk里面的代码。

所以通过新的classLoader显示加载业务入口,然后使用反射调用他。

8.实例代码地址:

https://gitee.com/virjar/xposedhooktool/blob/master/app/src/main/java/com/virjar/xposedhooktool/hotload/XposedInit.java

这篇关于xposed实现插件代码更新同时避免重启系统方案的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1054777

相关文章

Oracle查询优化之高效实现仅查询前10条记录的方法与实践

《Oracle查询优化之高效实现仅查询前10条记录的方法与实践》:本文主要介绍Oracle查询优化之高效实现仅查询前10条记录的相关资料,包括使用ROWNUM、ROW_NUMBER()函数、FET... 目录1. 使用 ROWNUM 查询2. 使用 ROW_NUMBER() 函数3. 使用 FETCH FI

Python脚本实现自动删除C盘临时文件夹

《Python脚本实现自动删除C盘临时文件夹》在日常使用电脑的过程中,临时文件夹往往会积累大量的无用数据,占用宝贵的磁盘空间,下面我们就来看看Python如何通过脚本实现自动删除C盘临时文件夹吧... 目录一、准备工作二、python脚本编写三、脚本解析四、运行脚本五、案例演示六、注意事项七、总结在日常使用

Java实现Excel与HTML互转

《Java实现Excel与HTML互转》Excel是一种电子表格格式,而HTM则是一种用于创建网页的标记语言,虽然两者在用途上存在差异,但有时我们需要将数据从一种格式转换为另一种格式,下面我们就来看看... Excel是一种电子表格格式,广泛用于数据处理和分析,而HTM则是一种用于创建网页的标记语言。虽然两

Java中Springboot集成Kafka实现消息发送和接收功能

《Java中Springboot集成Kafka实现消息发送和接收功能》Kafka是一个高吞吐量的分布式发布-订阅消息系统,主要用于处理大规模数据流,它由生产者、消费者、主题、分区和代理等组件构成,Ka... 目录一、Kafka 简介二、Kafka 功能三、POM依赖四、配置文件五、生产者六、消费者一、Kaf

在C#中获取端口号与系统信息的高效实践

《在C#中获取端口号与系统信息的高效实践》在现代软件开发中,尤其是系统管理、运维、监控和性能优化等场景中,了解计算机硬件和网络的状态至关重要,C#作为一种广泛应用的编程语言,提供了丰富的API来帮助开... 目录引言1. 获取端口号信息1.1 获取活动的 TCP 和 UDP 连接说明:应用场景:2. 获取硬

使用Python实现在Word中添加或删除超链接

《使用Python实现在Word中添加或删除超链接》在Word文档中,超链接是一种将文本或图像连接到其他文档、网页或同一文档中不同部分的功能,本文将为大家介绍一下Python如何实现在Word中添加或... 在Word文档中,超链接是一种将文本或图像连接到其他文档、网页或同一文档中不同部分的功能。通过添加超

windos server2022里的DFS配置的实现

《windosserver2022里的DFS配置的实现》DFS是WindowsServer操作系统提供的一种功能,用于在多台服务器上集中管理共享文件夹和文件的分布式存储解决方案,本文就来介绍一下wi... 目录什么是DFS?优势:应用场景:DFS配置步骤什么是DFS?DFS指的是分布式文件系统(Distr

NFS实现多服务器文件的共享的方法步骤

《NFS实现多服务器文件的共享的方法步骤》NFS允许网络中的计算机之间共享资源,客户端可以透明地读写远端NFS服务器上的文件,本文就来介绍一下NFS实现多服务器文件的共享的方法步骤,感兴趣的可以了解一... 目录一、简介二、部署1、准备1、服务端和客户端:安装nfs-utils2、服务端:创建共享目录3、服

JAVA系统中Spring Boot应用程序的配置文件application.yml使用详解

《JAVA系统中SpringBoot应用程序的配置文件application.yml使用详解》:本文主要介绍JAVA系统中SpringBoot应用程序的配置文件application.yml的... 目录文件路径文件内容解释1. Server 配置2. Spring 配置3. Logging 配置4. Ma

2.1/5.1和7.1声道系统有什么区别? 音频声道的专业知识科普

《2.1/5.1和7.1声道系统有什么区别?音频声道的专业知识科普》当设置环绕声系统时,会遇到2.1、5.1、7.1、7.1.2、9.1等数字,当一遍又一遍地看到它们时,可能想知道它们是什... 想要把智能电视自带的音响升级成专业级的家庭影院系统吗?那么你将面临一个重要的选择——使用 2.1、5.1 还是