本文介绍了Android脱壳技术中的Dex加载分析。主要内容包括ART和Dalvik虚拟机的简介、Java中的类加载机制、Dex文件在Android系统中的加载过程以及在反编译与脱壳过程中如何绕过加密和防护措施。
0. ART 和 Dalvik 简介
Android 5.0 之后采用 ART 虚拟机,相比于 Dalvik 虚拟机,有以下不同点:
ART 采用 Ahead of Time,即预编译技术,将程序字节码编译为机器码,避免了解释器,好处是程序执行变快,坏处是程序占用硬盘空间变大,并且安装时间变长。
Dalvik 采用的是 JIT,即时编译。通过算法找到热点代码,然后对热点代码进行实时编译,放入缓存,下一次运行该部分代码就不必经过解释器。
1. Java 中的类加载机制
在 Java 中类加载的方式有以下三种:
- JVM 初始化时通过 libpath 加载
- ClassLoader.loadClass(“java.util.Base64”)
- Class.forName(“java.util.Base64”)
三种方法本质相同:找到类路径,然后调用 ClassLoader 加载。loadClass
方法首先通过 findLoadedClass
默认实现是首先委托父类加载器加载,如果父类加载器加载失败,则尝试使用当前类加载器加载,当前类加载器会调用 findClass
方法加载类。所以,如果要实现自定 义的类加载器,可以通过继承 ClassLoader
,重写 findClass
方法。下面是一个用 chatgpt 生成的自定义类加载器的例子:
import java.io.*;
public Class MyClassLoader extends ClassLoader {
private String ClassPath;
public MyClassLoader(String ClassPath) {
this.ClassPath = ClassPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = getClassData(name);
return defineClass(name, data, 0, data.length);
} catch (IOException e) {
throw new ClassNotFoundException("Class not found: " + name, e);
}
}
private byte[] getClassData(String ClassName) throws IOException {
String path = ClassPath + File.separatorChar + ClassName.replace('.', File.separatorChar) + ".Class";
InputStream in = new FileInputStream(path);
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = -1;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
in.close();
return out.toByteArray();
}
}
用例:
MyClassLoader myClassLoader = new MyClassLoader("/my/Class/path");
Class<?> cls = myClassLoader.loadClass("MyClass");
Object obj = cls.newInstance();
2. Android 中的类加载机制
前文说到通过重写 findClass
可以实现自定义类加载,Android 平台下的自定义类加载器也是如此,但相比于直接从父类 ClassLoader 继承,Android 的两大类加载器:DexClassLoader 和 PathClassLoader 都继承自 BaseDexClassLoader,而 BaseDexClassLoader 继承自 ClassLoader,但顶级父类 ClassLoader 的 findClass 方法直接返回了 Exception:
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
所以 findClass 的函数主体存在于 BaseDexClassLoader 中,逻辑很简单:首先判断需要加载的类是否在 sharedLibrary,如果存在直接通过 loader.LoadClass(name)
进行加载;否则通过 pathList.findClass(name, suppressedExceptions)
加载。
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// First, check whether the Class is present in our shared libraries.
if (sharedLibraryLoaders != null) {
for (ClassLoader loader : sharedLibraryLoaders) {
try {
return loader.loadClass(name);
} catch (ClassNotFoundException ignored) {
}
}
}
// Check whether the Class in question is present in the dexPath that
// this Classloader operates on.
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;
}
DexClassLoader 和 PathClassLoader 有何不同呢?
首先看 DexClassLoader 的构造函数,接收 4 个参数:dexPath、optimizedDirectory、librarySearchPath 和 parent。其中,optimizedDirectory 自从 android 8 以来就被废弃了,所以 super 函数中该参数是 null。如下代码所示:
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
而 PathClassLoader 的构造函数如下,对比 DexClassLoader 的构造函数,两者都是一致的,所以在功能上,PathClassLoader 和 DexClassLoader 是相同的。
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
@libcore.api.CorePlatformApi
public PathClassLoader(
String dexPath, String librarySearchPath, ClassLoader parent,
ClassLoader[] sharedLibraryLoaders) {
super(dexPath, librarySearchPath, parent, sharedLibraryLoaders);
}
在 android 7 时代,DexClassLoader 和 PathClassLoader,主要差距在于:他们的父类 BaseDexClassLoader 仍然将 optimizedDirectory 当做析构函数的参数。如下分别是 3 者的源码,如果 optimizedDirectory 不为 null,则通过 DexPathList 从 optimizedDirectory 加载 dex。
// 1. DexClassLoader构造函数
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
// 2. PathDexClassLoader的构造函数
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
// 3. BaseDexClassLoader的构造函数,如果optimizedDirectory不为null,则通过optimizedDirectory加载dex文件
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
}
...
//跟入DexPathList到底层,通过DexFile加载,最后调用openDexFile
private DexFile(String sourceName, String outputName, int flags, ClassLoader loader,
DexPathList.Element[] elements) throws IOException {
if (outputName != null) {
try {
String parent = new File(outputName).getParent();
if (Libcore.os.getuid() != Libcore.os.stat(parent).st_uid) {
throw new IllegalArgumentException("Optimized data directory " + parent
+ " is not owned by the current user. Shared storage cannot protect"
+ " your application from code injection attacks.");
}
} catch (ErrnoException ignored) {
// assume we'll fail with a more contextual error later
}
}
mCookie = openDexFile(sourceName, outputName, flags, loader, elements);
}
Comments NOTHING