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

相关文章

使用Python实现IP地址和端口状态检测与监控

《使用Python实现IP地址和端口状态检测与监控》在网络运维和服务器管理中,IP地址和端口的可用性监控是保障业务连续性的基础需求,本文将带你用Python从零打造一个高可用IP监控系统,感兴趣的小伙... 目录概述:为什么需要IP监控系统使用步骤说明1. 环境准备2. 系统部署3. 核心功能配置系统效果展

Python实现微信自动锁定工具

《Python实现微信自动锁定工具》在数字化办公时代,微信已成为职场沟通的重要工具,但临时离开时忘记锁屏可能导致敏感信息泄露,下面我们就来看看如何使用Python打造一个微信自动锁定工具吧... 目录引言:当微信隐私遇到自动化守护效果展示核心功能全景图技术亮点深度解析1. 无操作检测引擎2. 微信路径智能获

Python中pywin32 常用窗口操作的实现

《Python中pywin32常用窗口操作的实现》本文主要介绍了Python中pywin32常用窗口操作的实现,pywin32主要的作用是供Python开发者快速调用WindowsAPI的一个... 目录获取窗口句柄获取最前端窗口句柄获取指定坐标处的窗口根据窗口的完整标题匹配获取句柄根据窗口的类别匹配获取句

在 Spring Boot 中实现异常处理最佳实践

《在SpringBoot中实现异常处理最佳实践》本文介绍如何在SpringBoot中实现异常处理,涵盖核心概念、实现方法、与先前查询的集成、性能分析、常见问题和最佳实践,感兴趣的朋友一起看看吧... 目录一、Spring Boot 异常处理的背景与核心概念1.1 为什么需要异常处理?1.2 Spring B

Python位移操作和位运算的实现示例

《Python位移操作和位运算的实现示例》本文主要介绍了Python位移操作和位运算的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录1. 位移操作1.1 左移操作 (<<)1.2 右移操作 (>>)注意事项:2. 位运算2.1

如何在 Spring Boot 中实现 FreeMarker 模板

《如何在SpringBoot中实现FreeMarker模板》FreeMarker是一种功能强大、轻量级的模板引擎,用于在Java应用中生成动态文本输出(如HTML、XML、邮件内容等),本文... 目录什么是 FreeMarker 模板?在 Spring Boot 中实现 FreeMarker 模板1. 环

Qt实现网络数据解析的方法总结

《Qt实现网络数据解析的方法总结》在Qt中解析网络数据通常涉及接收原始字节流,并将其转换为有意义的应用层数据,这篇文章为大家介绍了详细步骤和示例,感兴趣的小伙伴可以了解下... 目录1. 网络数据接收2. 缓冲区管理(处理粘包/拆包)3. 常见数据格式解析3.1 jsON解析3.2 XML解析3.3 自定义

SpringMVC 通过ajax 前后端数据交互的实现方法

《SpringMVC通过ajax前后端数据交互的实现方法》:本文主要介绍SpringMVC通过ajax前后端数据交互的实现方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价... 在前端的开发过程中,经常在html页面通过AJAX进行前后端数据的交互,SpringMVC的controll

Spring Security自定义身份认证的实现方法

《SpringSecurity自定义身份认证的实现方法》:本文主要介绍SpringSecurity自定义身份认证的实现方法,下面对SpringSecurity的这三种自定义身份认证进行详细讲解,... 目录1.内存身份认证(1)创建配置类(2)验证内存身份认证2.JDBC身份认证(1)数据准备 (2)配置依

利用python实现对excel文件进行加密

《利用python实现对excel文件进行加密》由于文件内容的私密性,需要对Excel文件进行加密,保护文件以免给第三方看到,本文将以Python语言为例,和大家讲讲如何对Excel文件进行加密,感兴... 目录前言方法一:使用pywin32库(仅限Windows)方法二:使用msoffcrypto-too