0x00 前言
早期的一种Android加固手段是通过动态地加载dex文件来达到保护目的,当然这种办法早就已经过时,那么要了解这种加固手段就必须了解Android的动态加载机制
0x01 DexClassLoader和PathClassLoader
Android中有许多类加载器,当然本文讨论范围主要是DexClassLoader
和PathClassLoader
这两个,这两个类都继承自BaseDexCloader
这个父类,区别在于PathClassLoader这个类默认的优化路径参数为null,所以只能是内部存储路径,而DexClassLoader则可以选择内部存储路径或者外部存储路径
PathClassLoader:
1 | public class PathClassLoader extends BaseDexClassLoader { |
DexClassLoader:
1 | public class DexClassLoader extends BaseDexClassLoader { |
发现他们均是调用父类的构造函数,跟进查看:
1 | public BaseDexClassLoader(String dexPath, File optimizedDirectory, |
第二个参数optimizedDirectory
就是dex文件优化后的输出路径,可以看到DexClassLoader
中使用了new File()
函数来指定相应的路径,而PathClassLoader
则是传入了null
值,那么只能是默认的内部存储
这两个类差距就在这,其他的区别不大,都是调用父类的构造函数
0x02 Davlik加载Dex文件
应用程序在第一次启动app的时候,会在/dalvik/dalvik-cache目录下生成odex文件结构,其实就是在原app的dex文件结构基础上,增加odex文件头和并在原dex文件末尾增加依赖库信息和辅助信息,依赖库信息知名该dex文件所需要的本地函数库。
辅助信息记录了dex中类的索引表,dalvik为每一个dex文件创建一个对应DexClassLookup对象用于记dex文件所有的类索引信息,DexClassLookup对象为的每个类都生成了table结构体对象,该对象记录了类的描述符的哈希值、类描述符在dex文件中的偏移和类在dex文件中的偏移。其table结构体对象定义如下。也就是说通过该table结构体对象就可以对dex文件中某一个类快速定位。
Davlik加载解析dex的过程中有几个重要的数据结构DexFile
,RawDexFile
,DvmDex
和ClassObject
,加载的dex解析之后就是以ClassObject的形式存储在内存中,然后通过解析具体的指令集去执行。存储指令集的数据结构就是Method结构体中的insns数组,这个了解过Dex文件结构的应该清楚。ClassObject结构体对象几乎包含类目标在运行时所需的所有资源,包括当前类的超类、当前类使用的类加载器、Method类型指针等。
app在启动过程中创建了PathClassLoader去加载dex文件:
源码位置:http://androidxref.com/4.4_r1/xref/frameworks/base/core/java/android/app/ApplicationLoaders.java
1 | class ApplicationLoaders |
跟进PathClassLoader个构造函数:
1 | public PathClassLoader(String dexPath, ClassLoader parent) { |
跟进父类BaseDexClassLoader:
1 | /** |
这里有四个参数:
- dexPath————————–dex的路径
- optimizedDirectory ————优化后的存储路径
- libraryPath———————–包含native方法的库文件路径
- parent—————————-父加载器
调用了父类ClassLoader构造函数,这里重点关注这个DexPathList函数,跟进查看其构造函数:
1 | public DexPathList(ClassLoader definingContext, String dexPath, |
前面巴拉巴拉的一大堆验证,先不管,跟进此处这个重要的函数makeDexElements(splitDexPath(dexPath), optimizedDirectory,suppressedExceptions);
这个splitDexPath函数作用是将传进来的dexPath路径进行字符串分割
1 | /** |
将分割后的字符串传递给makeDexElements
函数
发现无论是dex结尾的还是jar,apk结尾的都会去调用loadDexFile函数,然后返回一个Dexfile类型
dex = loadDexFile(file, optimizedDirectory);
那么跟进查看
1 | /** |
这里会先判断传递进来的optimizedDirectory是否为空,那么因为之前是由PathClassLoader传递进来的,那么这个变量肯定就是null了,那么我们就跟进DexFile的构造函数:
1 | /** |
注释写的很清楚,继续跟进:
1 | /** |
这里调用了一个openDexFile(),其实不管你传进来的optimizedDirectory是否为空,最后都会调用到这个函数
调用完后会返回一个Cookie记录,那么我们跟进这个函数
1 | /* |
发现最终会调用一个native函数,继续跟进
http://androidxref.com/4.4_r1/xref/dalvik/vm/native/dalvik_system_DexFile.cpp
下面有几个判断,一个一个来:
1 | if (hasDexExtension(sourceName) |
hasDexExtension
判断是否以dex结尾,如果是,继续dvmRawDexFileOpen()
操作
1 | else if (dvmJarFileOpen(sourceName, outputName, &pJarFile, false) == 0) { |
apk或者jar结尾的就进行dvmJarFileOpen
操作
那么这个native函数主要也就是进行这两个操作,其实两个函数操作差不多,只是第二个多了一个提取,将dex文件提取出来。
跟进第一个函数,有点长,一段一段来:
然后往下跟进:
1 | /* |
这里cachedName是就是新建的一个缓存,那么这里只要先在这个缓存操作,然后再把缓存拷贝到目标文件即可,注意这个strdup函数,这是个拷贝函数内部会调用malloc创建内存,后面有free函数跟他相匹配出现
继续跟进
1 | optFd = dvmOpenCachedDexFile(fileName, cachedName, modTime, |
这个函数的作用就是给dex文件写入一个opt头,如果写成功那么newFile的值变为true
具体就不跟进,主要操作如下:
如果写入成功:
再往下:
1 | if (dvmDexFileOpenFromFd(optFd, &pDvmDex) != 0) { |
dvmDexFileOpenFromFd
这个函数最主要就是,将优化后得dex文件(也就是odex文件)通过mmap
映射到内存中,并通过mprotect
修改它的映射内存为只读权限,然后将映射为只读的这块dex数据中的内容全部提取到DexFile
这个数据结构中去
然后最后成功的话就返回给RawDexFile
然后回到这:
那么dvmJarFileOpen函数的操作大同小异,就是多了一个提取的操作
最后将这个返回给之前的Cookie
1 | RETURN_PTR(pDexOrJar); |
看一下宏定义:
1 | #define RETURN_PTR(_val)do { pResult->l = (Object*)(_val); return; } while(0) |
那么所谓cookie其实是个DexOrJar类型结构体的指针
那么整个加载的流程差不多就是这样了。。。。。。
0x03 运行时解析
类加载的目的其实就是为所需的类生成一个ClassObject的实例,然后需要的时候调用这个实例对象
看一下ClassObject的定义,里面有一些描述变量以及一些辅助的描述信息
1 | /* |
大致分析一下流程
回到BaseDexClassLoader.java
这里的findclass方法就是对指定的类名开始进行搜索加载,跟进查看,这里调用了DexPathList类的findclass方法
/**
* Finds the named class in one of the dex files pointed at by
* this instance. This will find the one in the earliest listed
* path element. If the class is found but has not yet been
* defined, then this method will define it in the defining
* context that this instance was constructed with.
*
* @param name of class to find
* @param suppressed exceptions encountered whilst finding the class
* @return the named class or {@code null} if the class is not
* found in any of the dex files
*/
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
这里的dexElements,来看下定义 ,那么这里就是遍历一下之前所有的DexFile实例,其实也就是遍历了所有加载过的dex文件
/**
* List of dex/resource (class path) elements.
* Should be called pathElements, but the Facebook app uses reflection
* to modify 'dexElements' (http://b/7726934).
*/
private final Element[] dexElements;
然后调用了loadClassBinaryName方法,跟过去看下
/**
* See {@link #loadClass(String, ClassLoader)}.
*
* This takes a "binary" class name to better match ClassLoader semantics.
*
* @hide
*/
public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
return defineClass(name, loader, mCookie, suppressed);
}
调用了defineClass,跟进
private static Class defineClass(String name, ClassLoader loader, int cookie, List<Throwable> suppressed) {
Class result = null;
try {
result = defineClassNative(name, loader, cookie);
} catch (NoClassDefFoundError e) {
if (suppressed != null) {
suppressed.add(e);
}
} catch (ClassNotFoundException e) {
if (suppressed != null) {
suppressed.add(e);
}
}
return result;
}
此处调用了一个native层的方法defineClassNative
1 | private static native Class defineClassNative(String name, ClassLoader loader, int cookie) |
有点长,一段段来分析
1 | static void Dalvik_dalvik_system_DexFile_defineClassNative(const u4* args, |
跟进一下dvmDefineClass
1 | ClassObject* dvmDefineClass(DvmDex* pDvmDex, const char* descriptor, |
这里返回调用了findClassNoInit方法,跟过去看一下(此处参考文章http://pwn4.fun/2016/04/11/Dalvik%E7%B1%BB%E5%8A%A0%E8%BD%BD%E6%A8%A1%E5%9D%97/)
这函数有点长0.0,就取关键部分分析
1 | static ClassObject* findClassNoInit(const char* descriptor, Object* loader, |
调用dvmLookupClass函数判断本类是否已经被加载,
如果已经加载:直接返回,
如果没加载未加载:判断能否找到Dex文件,能找到调用dexFindClass
在指定Dex文件中根据类的描述符查找相关类(用户类);找不到调用searchBootPathForClass
从系统启动基本路径中查找并加载目标类(系统类)。调用loadClassFromDex函数实现加载类达到可运行状态。调用dvmAddClassToHash实现将新加载的类添加到哈希表中方便在此查找。
loadClassFromDex 函数将调用loadClassFromDex0
函数完成对该类的加载工作,返回值为一个ClassObject
结构体对象。
1 | static ClassObject* loadClassFromDex0(DvmDex* pDvmDex, |
依次完成:
1.在内存中为类对象申请存储空间;
2.设置字段信息;
3.为超类建立索引;
4.加载类接口;
5.加载类字段;
6.加载类方法,并将以上数据封装成一个ClassObject结构体对象并返回。
0x04 最后
每次分析源码都能看到一些新的东西,常看常新,上面的知识点对于脱早期壳非常重要
比如熟悉Dalvik加载dex的机制,那么就能知道可以在何处下断,然后在内存中dump下dex文件
熟悉类的运行时解析机制,就可以对付某些讲类指令抽空的壳