类加载与脱壳机
自制脱壳机,将公开了源码的Fart6移植到Fart10,同时去除一些Fart特征。
loadClass的过程
loadClass的过程可以总结成:1.判断是否是当前类加载器加载了此类;2.若不是,让父加载器加载此类;3.若父加载器加载失败,由当前加载器加载。
一般实现类加载,同时使用ClassLoader的实例调用loadClass。

可以看到,ClassLoader.loadClass(String className)间接调用重载函数,resolve在这里不起作用,在JVM中,如果resolve为true,表示加载类之后是否需要立即进行链接操作中的解析步骤。(链接包括:验证、准备(分配空间)、解析),而在安卓虚拟机中,这里忽略resolve。
1 findLoadedClass(ClassLoader)——查询当前加载器是否加载了目标类
findLoadedClass是用来检查缓存的,这个方法会检查当前ClassLoader实例是否已经加载过名为className的类,每个ClassLoader实例都有一个缓存区,用来存放已经加载过的类。
如果当前实例是启动类加载器,则将局部变量 loader 设为 null。这是因为查询启动类加载器加载的类通常需要通过 VM 的特殊 native 接口,并使用 null 来代表它。
如果当前实例不是启动类加载器(比如是 AppClassLoader 或 PathClassLoader 等),则将 loader 设置为 this,即当前 ClassLoader 实例本身。

findLoadedClass(VMClassaLoader、JNI函数)
这里的VMClassLoader.findLoadedClass是一个static的JNI函数,具体实现在native层,目录为:/art/runtime/native/java_lang_VMClassLoader.cc。
这个 C++ 函数 VMClassLoader_findLoadedClass 的核心目的是:接收一个 Java 层的 ClassLoader 对象和一个类名字符串,然后在 ART 虚拟机内部的数据结构中查找,判断这个特定的 ClassLoader 是否已经加载并解析了具有该名称的类。如果找到了并且类是可用状态(已解析),就返回对应的 Java Class 对象 (jclass);否则返回 nullptr。
现在来简单解释一下下图的函数VMClassLoader_findLoadedClass。
第31行,env是标准的JNI的接口,提供java与native之间的交互。soa可以理解为ART对JNI交互的封装与简化,它的底层还是使用env来工作,但自动管理线程状态,更安全、方便。
第32行,将Java层的jobject javaLoader解码成ART内部表示的mirror::ClassaLoader*指针loader,这里的mirror是ART中表示Java堆对象的类的命名空间。
第33行,将javaName从java的jstring转换成c/c++使用的UTF-8编码字符串。
第37行,获取ClassLinker实例,ClassLinker实例要负责类的加载、链接(验证、准备、解析)、初始化。所有类查找、类定义、解析相关的操作,都要经过ClassLinker。
第38行,将点分格式的类名转换成虚拟机内部使用的描述符,即斜杠格式。
第39行,将类描述符转换成唯一的哈希值。
第40行,根据哈希值、loader、类描述符、当前的线程(soa.Self()返回线程Thread指针),来去找*唯一的类。
这里重点关注LookupClass。
LookupClass
在LookupClass中。
第一阶段,查主表。(缓存查找)
使用 ReaderMutexLock 获取对 ClassLinker 内部类表的读取锁,保证线程安全。
调用 LookupClassFromTableLocked 在主要的类表(包括 Zygote 预加载表 pre_zygote_class_table_ 和运行时表 class_table_)中查找。
如果在表中找到了匹配的类 (result != nullptr),就立即返回结果。锁会自动释放。
第二阶段,查启动镜像(启动类加载器)。
仅当第一阶段失败、查找的是启动类加载器 (class_loader == nullptr)、且需要查找镜像时,才执行这里。
调用 LookupClassFromImage(descriptor) 尝试在 dex_caches_ 中查找。
如果找到了 (result != nullptr): 调用 InsertClass 将其加入主类表(可能是 class_table_),起到缓存作用,然后返回找到的 result。
如果还没找到 (result == nullptr): 增加查找镜像失败的计数器。如果失败次数超过阈值 (kMaxFailedDexCacheLookups),则触发一个优化操作 MoveImageClassesToClassTable(),将所有启动镜像中的类都添加到主类表中,以加速后续查找。最后返回 nullptr。

LookupClassFromTableLocked
在LookupClassFromTableLocked中,pre_zygote_class_table_记录着预加载的类(Zygote进程在创建时,初始化了虚拟机并加载了一系列预加载的动态链接库,这些动态链接库里的类记录在这个预加载类表里)。而class_table_则是动态类表,它存储了 Zygote 之后由各种 ClassLoader 加载的类,以及从启动镜像(image)查找到并缓存起来的类。

LookupClassFromImage
函数LookupClassFromImage是从启动类加载器的dex_cache中找目标类,DexCache类用于关联DexFile对象。

2 parent.loadClass——委托父加载器加载
上述流程已经把如何寻找一个类讲得很详细了,接下来讲委派双亲机制。
下图中,在findLoadedClass找不到className后,就会尝试执行parent.loadClass,让父加载器去加载,父加载器的执行流程也是:findLoadedClass、parent.loadClass、findClass。
所以,这里不细讲委派机制(双亲委派机制不难),直接讲如何加载一个未加载的类。
3 findClass——自己加载
这里分析的是BaseDexClassLoader的findClass,一般整体壳都绕不开BaseDexClassLoader,BaseDexClassLoader的父类是ClassLoader,我们之前分析的findLoadedClass和parent.loadClass都是ClassLoader类中,loadClass里出现的函数。BaseDexClassLoader并没有实现自己的loadClass,所以loadClass都是从ClassLoader继承过来的。
但ClassLoader的findClass已经被BaseDexClassLoader覆写了,我们这里就需要分析findClass。
findClass 函数的核心职责就是:在当前 ClassLoader 管理的一系列 Dex 文件中,按顺序查找指定的类,如果找到,就加载并定义它。
下图中,pathList是个老熟人了。

pathList——属于类DexPathList
pathList是BaseDexClassLoader声明的一个DexPathList类型的私有成员变量。

关于DexPathList,从字面上来看,它是存储Dex路径列表的一个变量,从功能上来说,它存储的路径包括:apk的路径、dex的路径、jar的路径、class的路径等。然而,类DexPathList存储路径的列表(成员变量)叫dexElements,这一度让我以为——“一个Element实例代表一个Dex文件路径”,这个概念其实是错的。
通过注释,其实可以知道,这个成员变量应该叫pathElements,结果因为Facebook app改了名字,What can I say。
纠正一下上面的概念,这里的dexElements是一个Element数组,每个Element实例存储的是一个路径,这个路径要是指向apk,可能包含不止一个Dex,所以一个Element实例代表一个Dex文件路径的概念是错误的。
接着看看DexPathList的findClass。
可以看到,这里取出了每个Element实例,并取出了element.dexFile赋给dex,然后调用了dex.loadClassBinaryName去找目标类是否在当前的文件(apk/dex/jar/class)中。

dexElements——属于类Element
类Element是定义在DexPathList的静态内部类,从成员变量中可以看出,它存储的类型有apk/zip/dex等,还有一个很关键的成员变量dexFile。
mCookie——属于类DexFile
DexFile的mCookie很重要,在native层中,通过相关函数的调用,可以利用mCookie去获取DexFile(可以理解为PathFile)里存放的所有Dex文件。
阅读DexFile.java中的代码,会发现mCookie的值由jni函数openDexFileNative获得。

java层openDexFileNative对应着native层的DexFile_openDexFileNative。
第154-161行,将输入的jstring转换成c/c++的const char*来使用。
第163行,获取ART运行时的ClassLinker实例,打开Dex文件和处理OAT文件是ClassLinker的职责之一。
第164行,创建一个std::vector,用于存放成功打开的DexFile对象。
第165行,创建error_msgs用于收集打开过程中可能出现的错误信息。
第167行,可以理解为OpenDexFilesFromOat将已打开的Dex文件(们)的内部句柄打包成一个long[]数组,返回给dex_files。
第170行,将这些句柄从native类型转换成JNI类型,赋值给array,array会通过return赋给Java层的mCookie。
于是我们知道了,mCookie是已打开的Dex文件们的句柄数组指针。
loadClassBinaryName——属于类DexFile
回到这张图,讲DexFile的loadClassBinaryName。

直接跳转到defineClass。

再跳转到defineClassNative。
defineClassNative是一个静态JNI函数。
第220行,眼熟吧,将cookie转换回了dex_files句柄数组。
第234行,遍历每一个打开的Dex文件句柄。
第235行,通过函数FindClassDef,去每个Dex文件里寻找目标Class是否存在。
第236行,如果存在,则通过ClassLinker的实例class_linker,对这个Dex文件进行注册,加入dex_caches。

FindClassDef——找到类定义
这个函数的核心目的是:在当前的 DexFile 对象所代表的 Dex 文件内部,根据给定的类描述符 (descriptor) 字符串和其哈希值,查找并返回对应的 DexFile::ClassDef 结构体指针。
其中,第481-485行,在解析一个Dex文件,获得Class的数量。
第486-499行,遍历类定义表,根据类定义结构的class_idx_是指向type索引表,这里通过描述符descriptor获取到了type_idx,然后和每个类的class_idx_进行比较,以此找出类。
下图是ClassDef的结构。
RegisterDexFile——将DexFile加入缓存
dex_cache会被加入ClassLinker中的dex_caches_中(通过dex_caches_.push_back),ClassLinker会通过这个列表跟踪所有已注册的Dex缓存,建立了dex_cache和dex_file之间的连接(dex_cache->SetDexFile(&dex_file))。
这个过程确保了每个被 ART 管理的 Dex 文件都有一个对应的 DexCache,并且 ClassLinker 能够找到并管理这些缓存。
DefineClass——类的加载、链接、初始化
这个函数是实际定义一个新类的核心逻辑。它接收一个在 dex_file 中找到的类定义 dex_class_def,以及要使用的 class_loader,然后负责创建对应的运行时 mirror::Class 对象,并执行加载和链接的关键步骤。
第1816行-1829行,处理特殊的类。
第1831-1841行,分配内存创建Class对象。
第1842行,将DexFile对应的DexCache设置到Class对象中。
第1844行,用ClassDef和ClassLoader信息填充Class对象。
第1847-1850行,为String类专门进设置标志。——没深入分析。
第1853-1854行,获得这个类的锁,防止并发初始化,记录当前线程为类的初始化线程。

第1857行,根据描述符、Class对象.Get()、类描述符的hash值插入一个新加载的类到类表class_table_中。
第1858-1862行,插入失败,说明被其它线程插入了,使用已存在的Class对象并确保它已经被解析。
第1868-1876行,加载类的成员(字段和方法信息),如果失败,标志Class状态为Error。
第1878-1879行,此时类状态应该为Loaded(成员已经加载,但父类/接口未连接)。
第1880行,加载并链接父类和接口。
第1894行,执行链接(验证、准备、解析字段/方法、设置vtable/iftable等)。

最后返回的是一个定义并链接好的Class对象。
整个逻辑如下图所示。
(注:说明一下,Java的DexFile类和C/C++的DexFile类不一样,Java的DexFile通常指向一个APK文件或JAR文件或Dex文件等等,所以一个DexFile类可能会存储着许多个Dex文件;而C/C++的DexFile类指向一个Dex文件)
一个 Java DexFile 对象虽然是基于一个单一的源文件路径 (APK/JAR) 创建的,但它内部通过 mCookie (一个 long[] 数组) 可以管理从该源文件或其对应的 OAT 文件中加载出来的一个或多个底层的 C++ art::DexFile 实例。这对于支持 Android 的 MultiDex 机制至关重要。
Fart的逻辑
Fart脱壳的步骤主要分为3步:
1.找到合适的点,通过DexFile结构脱下完整的dex;
2.主动调用类中的每一个方法,并实现对CodeItem的dump;
3.修复Dex。
整体壳
下图是Fart的第1点的逻辑。
一个类的初始化函数是不会被直接编译成OAT代码的,而一个Java的Method,除了走OAT代码模式,就必须走解释器模式,解释器模式必须经过函数Execute,因此,可以在Execute里针对

抽取壳
下图是Fart的第2点逻辑。

之后,又有人写了一个FartExt,逻辑如下。