庖丁解牛,一文搞懂安卓类加载器

admin 2024年11月27日23:10:09评论3 views字数 11058阅读36分51秒阅读模式
庖丁解牛,一文搞懂安卓类加载器

最近学习一些加固的文章,研究了一些加固的实现,但是对安卓8以下和安卓8版本以上的classloader实现过程有些模糊,网上的文章描述的有些错误,既然如此何不自己直接分析下源码对比下各个系统的不同呢,分析源码肯定不会是骗人的,说干就干,本篇文章,描述的较清晰,根据逆向的思维去观察思考代码的,如果您在阅读过程中觉得那个地方有歧义,欢迎指出。

庖丁解牛,一文搞懂安卓类加载器
 目录

⊙   一.classloader的继承关系

二.讨论各classloader的功能

三.双亲委派机制

⊙   四.classLoader的运用

一.classloader的继承关系

我们知道安卓里面有几个类加载器,但是不知道其继承关系,为了加深印象,我们去源码找下。

http://aospxref.com/android-8.0.0_r36/xref/libcore/ojluni/src/main/java/java/lang/ClassLoader.javapublic abstract class ClassLoader class BootClassLoader extends ClassLoader
http://aospxref.com/android-8.0.0_r36/xref/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.javapublic class BaseDexClassLoader extends ClassLoader 
http://aospxref.com/android-8.0.0_r36/xref/libcore/dalvik/src/main/java/dalvik/system/InMemoryDexClassLoader.java>public final class InMemoryDexClassLoader extends BaseDexClassLoader 
<http://aospxref.com/android-8.0.0_r36/xref/libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java>public class PathClassLoader extends BaseDexClassLoader
<http://aospxref.com/android-8.0.0_r36/xref/libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java>public class DexClassLoader extends BaseDexClassLoader 
<http://aospxref.com/android-8.0.0_r36/xref/libcore/ojluni/src/main/java/java/security/SecureClassLoader.java#42>public class SecureClassLoader extends ClassLoader 
<http://aospxref.com/android-8.0.0_r36/xref/libcore/ojluni/src/main/java/java/net/URLClassLoader.java>public class URLClassLoader extends SecureClassLoader implements Closeable 

根据上面的类的继承关系我们就可以画一个UML类图表示其继承关系,但是中间的属性和方法被我省略了。

庖丁解牛,一文搞懂安卓类加载器

二.讨论各classloader的功能

2.1PathClassloader和DexClassLoader 的区别(源码分析及其对比)?

安卓8.0代码如下:

public class PathClassLoader extends BaseDexClassLoader {    public PathClassLoader(String dexPath, ClassLoader parent) {        super(dexPath, null, null, parent);    }    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {        super(dexPath, null, librarySearchPath, parent);    }}public class DexClassLoader extends BaseDexClassLoader {    public DexClassLoader(String dexPath, String optimizedDirectory,            String librarySearchPath, ClassLoader parent) {        super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);    }}

安卓8.1的代码如下:

public class DexClassLoader extends BaseDexClassLoader {    public DexClassLoader(String dexPath, String optimizedDirectory,            String librarySearchPath, ClassLoader parent) {        super(dexPath, null, librarySearchPath, parent);    }}public class PathClassLoader extends BaseDexClassLoader {    public PathClassLoader(String dexPath, ClassLoader parent) {        super(dexPath, null, null, parent);    }    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {        super(dexPath, null, librarySearchPath, parent);    }}

我们观察到,安卓8.1源码Dexclassloader其实已经和Pathclassloader的构造方法一样了(完全一模一样),只是说Pathclassloader多了个重载,不用加载library的实现。

那我们再看一下8.0的源码,竟然多了一个new File(optimizedDirectory)参数,那我们就看下,这个参数是干啥用的

安卓8,0public class BaseDexClassLoader extends ClassLoader {    private final DexPathList pathList;    /** =     * @param optimizedDirectory this parameter is deprecated and has no effect     */    public BaseDexClassLoader(String dexPath, File optimizedDirectory,            String librarySearchPath, ClassLoader parent) {        super(parent);        this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);        if (reporter != null) {            reporter.report(this.pathList.getDexPaths());        }    }

在上方的英文注释中看到ram optimizedDirectory this parameter is deprecated ...,也就是说这个参数被弃用了,被弃用就相当于null了呗。

于是得出结论在安卓8.0以后确实pathdexclassloader,Dexclassloader实现上面是相同的了(传递四个参数的的时候)。

这个时候读者可能有疑虑了,那8.0以下呢?

于是我查询了安卓7.1的源代码

 //pathclassloader还是和安卓8以上的相同public class PathClassLoader extends BaseDexClassLoader {    public PathClassLoader(String dexPath, ClassLoader parent) {        super(dexPath, null, null, parent);    }    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {        super(dexPath, null, librarySearchPath, parent);    }}//DexclassLoader 其实optimizedDirectory参数已经被用到public class DexClassLoader extends BaseDexClassLoader {    public DexClassLoader(String dexPath, String optimizedDirectory,            String librarySearchPath, ClassLoader parent) {        super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);    }}//这里是Dexclassloader的父类,通过这个参数被使用可以证明上方的结论public class BaseDexClassLoader extends ClassLoader {    private final DexPathList pathList;    public BaseDexClassLoader(String dexPath, File optimizedDirectory,            String librarySearchPath, ClassLoader parent) {        super(parent);        this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);    }

通过对比源码我们知道了一个事情,从安卓8开始后,pathclassloader和dexclassloader没啥不同了(在传递librarySearchPath这个参数的时候)。

但是8.0以后得版本呢?在这里笔者去看了下安卓11,安卓12,安卓13的源码pathclassloader与Dexclassloader的实现和安卓8.1一样了。

这里有个问题,如果做加固的不想支持安卓8.0以前的版本,那么直接可以采用pathclassloader来加载解完密的dex即可。如果想做好兼容性,那么做好考虑dexclassloader,但是要注意参数的传递问题。

其实这里还没说清pathclassloader和dexclassloader是干啥用的?写这个问题前,看到了csdn上很多离谱的答案,果然是自己看看源码才知道其实现过程。

两者都能加载外部的jar/apk/dex/

在8.0之前:区别就是dexclassloader多一个optimizedDirectory参数,可以指定odex存放的位置。

在8.0及其8.0以后:没啥区别。

写这么详细,其实也是为了在我们搞逆向的使用frida的时候,知道存在多个类加载器是如何用的,正所谓知彼知己才能百战不殆。

2.2BootClassLoader 与PathClassLoader

2.2.1BootClassLoader

BootClassLoader这个是Dalvik/ART虚拟机用于加载系统核心类库的,是安卓上面所有classloader的最终parent,安卓系统启动时来预加载常用的类例如string。

安卓系统java预先加载一些系统类的时机是在Zygote开始的,我们就可以看起怎么做的。

http://aospxref.com/android-8.0.0_r36/xref/frameworks/base/core/java/com/android/internal/os/ZygoteInit.java

public class ZygoteInit { public static void main(String argv[]) {         ...        //1.孵化器首先调用preload                preload(bootTimingsTraceLog);        ...} static void preload(BootTimingsTraceLog bootTimingsTraceLog) {        Log.d(TAG, "begin preload");...        bootTimingsTraceLog.traceBegin("PreloadClasses");    //2.preload调用preoadClasses        preloadClasses();        bootTimingsTraceLog.traceEnd(); ...}//这一大堆其实为了预置需要提前加载的类。private static void preloadClasses() {        final VMRuntime runtime = VMRuntime.getRuntime();    .......        try {            BufferedReader br                = new BufferedReader(new InputStreamReader(is), 256);            int count = 0;            String line;            while ((line = br.readLine()) != null) {                // Skip comments and blank lines.                line = line.trim();                if (line.startsWith("#") || line.equals("")) {                    continue;                }           //在这地方如果BootClassloader没有被创建,则新建一个Bootclassloader实例,源码看下方                    Class.forName(line, true, null);    }    public static Class<?> forName(String name, boolean initialize,                                   ClassLoader loader)        throws ClassNotFoundException    {        if (loader == null) {      //这里创建的            loader = BootClassLoader.getInstance();        }        Class<?> result;        try {            result = classForName(name, initialize, loader);        } catch (ClassNotFoundException e) {            Throwable cause = e.getCause();            if (cause instanceof LinkageError) {                throw (LinkageError) cause;            }            throw e;        }        return result;    }从这里我们也得出个结论 如果在安卓里面直接用Class.forName() 其实就是从BootClassLoader的里面找寻的类。

2.2.2pathclassloader

上面我们已经知道了BootClassloader加载的时机,我们探讨下pathclassloader加载的时机

Zygote进程启动SyetemServer进程时会调用ZygoteInit的handleSystemServerProcess方法,

http://aospxref.com/android-8.0.0_r36/xref/frameworks/base/core/java/com/android/internal/os/ZygoteInit.java#createPathClassLoader

其中

    private static void handleSystemServerProcess(            ZygoteConnection.Arguments parsedArgs)            throws Zygote.MethodAndArgsCaller {    //我们着重追这个...                cl = createPathClassLoader(systemServerClasspath, parsedArgs.targetSdkVersion);....}

再往下

   static PathClassLoader createPathClassLoader(String classPath, int targetSdkVersion) {      String libraryPath = System.getProperty("java.library.path");  //这个就是pathclassloder生成的地方了。      return PathClassLoaderFactory.createClassLoader(classPath,                                                    true /* isNamespaceShared */);    }

我们看到下方创建了这个pathclassloder

    public static PathClassLoader createClassLoader(String dexPath,                                                    String librarySearchPath,                                                    String libraryPermittedPath,                                                    ClassLoader parent,                                                    int targetSdkVersion,                                                    boolean isNamespaceShared) {....        PathClassLoader pathClassloader = new PathClassLoader(dexPath, librarySearchPath, parent);....        return pathClassloader;    }

也就是说先创建的bootclassloader,然后又创建的pathclassloader

这里其实还有个疑问,我们在安卓逆向或者开发程序的时候知道,pathclassloader的parent是bootclassloader,那么为什么呢?

我们从上方看到了 ClassLoader.getSystemClassLoader(),然后我找到了这个systemclassloader创建的代码,也就是可以解释为啥bootlclassloader是其父节点。

从下方截图也证明了我们对于源码分析得出来的结果

庖丁解牛,一文搞懂安卓类加载器

    private static ClassLoader createSystemClassLoader() {        String classPath = System.getProperty("java.class.path", ".");        String librarySearchPath = System.getProperty("java.library.path", "");        // TODO Make this a java.net.URLClassLoader once we have those?        return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());    }

PathClassLoader的主要功能加载APP自身dex、四大组件等都是通过它来实现。

2.3 InMemoryDexClassloader

这个没有什么好说的,在安卓8.0引入的,从内存中加载dex,做加固的时候经常会用到。

 2.4URLClassLoader

从Url路径中加载文件。

三 .双亲委派

庖丁解牛,一文搞懂安卓类加载器

这张图已经揭示了,在activity中的父classloader是Bootclassloader

在上文已经知道例如像String这种的类,其实是由BootClassLoader去加载的,然后我用pathclassloader去加载string,是成功的,如下图所示。

庖丁解牛,一文搞懂安卓类加载器

用BootClassLoader也是可以成功的,这点也说明不了双亲委派

庖丁解牛,一文搞懂安卓类加载器

接下来我们再试验用BootClassLoader加载

庖丁解牛,一文搞懂安卓类加载器

这个时候可以看得出BootClassLoader加载不成的,

但是上面的结论只能证明,子classloader没有的类可以委派父classloader来load,所以我们来看源码中是如何实现的

我们首先拿PathClassLoader的loadclass来分析:

PathClassLoader内没有loadclass,我们去其父类BaseClassLoader查看,发现也没有实现,那我们去爷爷类BaseDexClassLoader中找到了其实现

    protected Class<?> loadClass(String name, boolean resolve)        throws ClassNotFoundException    {            //1.判断类是否被加载过,            Class<?> c = findLoadedClass(name);            if (c == null) {                try {                    if (parent != null) {            //2.q                        c = parent.loadClass(name, false);                    } else {                        c = findBootstrapClassOrNull(name);                    }                } catch (ClassNotFoundException e) {                    // ClassNotFoundException thrown if class not found                    // from the non-null parent class loader                }                if (c == null) {                    // If still not found, then invoke findClass in order                    // to find the class.                    c = findClass(name);                }            }            return c;    }

1.判断类是否被加载过,如果加载过直接返回这个类。

2.如果类没被加载过,那么去调用其父classloader的loadClass(这里其实就是双亲委派,优先让父classLoader加载),这个地方其实就是递归了,有几个祖先,就会让其祖先去加载,

3.如果其祖先都没有能够返回这个类,那么直接当前的类去加载。

根据上方的结论我们可以根据其关系画一个流程图:

庖丁解牛,一文搞懂安卓类加载器

其实我们现在有个疑问?BootClassLoader现在是classloader的最上层后他还往上加载吗?

//http://aospxref.com/android-8.0.0_r36/xref/libcore/ojluni/src/main/java/java/lang/ClassLoader.java    protected Class<?> loadClass(String className, boolean resolve)           throws ClassNotFoundException {        Class<?> clazz = findLoadedClass(className);        if (clazz == null) {            clazz = findClass(className);        }        return clazz;    }

BootClassLoader已经是根ClassLoader了所以我们从这里,并没有看到递归。

四.classLoader的运用

4.1 加固

因为一个app跑起来,肯定避免不了组件的声明周期,在我们上方的代码中,其实pathclassloader的parent是BootClassloader,但是在加固中我们就可以新建一个DexclassLoader类,并把BootClassloader实例类当做其parent,然后再通过反射的方式把pathClassloader的parent设置为DexclassLoader,这样的操作通过双亲委派的机制,在调用的时候出现找不到类的情况。

4.2 热更新

我们知道pathclassloader其实加载APP自身dex、四大组件等都是通过它来实现,我们看其实现的函数findclass,实现从classloader中加载并返回。

        @Override        protected Class<?> findClass(String name) throws ClassNotFoundException {            List<Throwable> suppressedExceptions = new ArrayList<Throwable>();            Class c = pathList.findClass(name, suppressedExceptions);            if (c == null) {                ClassNotFoundException cnfe = new ClassNotFoundException(                        "Didn't find class "" + name + "" on path: " + pathList);                for (Throwable t : suppressedExceptions) {                    cnfe.addSuppressed(t);                }                throw cnfe;            }            return c;        }

但是我们再上方看到返回的class是从pathList类中得到的

    public Class<?> findClass(String name, List<Throwable> suppressed) {        for (Element element : dexElements) {            Class<?> clazz = element.findClass(name, definingContext, suppressed);            if (clazz != null) {                return clazz;            }        }        if (dexElementsSuppressedExceptions != null) {            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));        }        return null;    }

我们看到,其实就是遍历dex,去找,如果找到就返回,如果没找到就返回null

那么热更新的手段我们可以改变dexElements中的元素,从而影响其类的加载,那么如果想要改变dexElements的元素,我们就需要通过反射去获取然后去修改,并且dexElements的遍历是有先后的,我们可以通过把我们想要热更新的dex放到最前面,就达到了使用的时候加载我们目的的类,达到了热更新的目的。

4.3  frida或者xposehook的时候

经常会碰到一个情况,在hook加固的app的时候,脱完壳之后可以看到在dex其加密算法的实现,但是我们使用frida去hook的时候,经常会出现classNotfound的情况。其实就是classloader搞得鬼,我们可以枚举所有的classloader实例,然后看看在那个里面存在类的加载,然后更换默认的类加载器去hook.

作者简介:专注于Android、IOS逆向、安全加固实战经验分享

作者简介:专注于Android、IOS逆向、安全加固实战经验分享。

作者简介:专注于网络爬虫、Js逆向、App逆向实战经验分享。

我是BestToYou,分享工作或日常学习中关于二进制逆向和分析的一些思路和一些自己闲暇时刻调试的一些程序,文中若有错误的地方,恳请大家联系我批评指正。

庖丁解牛,一文搞懂安卓类加载器

原文始发于微信公众号(二进制科学):庖丁解牛,一文搞懂安卓类加载器

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年11月27日23:10:09
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   庖丁解牛,一文搞懂安卓类加载器https://cn-sec.com/archives/1886479.html

发表评论

匿名网友 填写信息