Java反射
在代码已经运行后,将需要动态加载的类,直接传给java封装实现的反射类,从而获取其属性甚至是直接调用
但是反射会大幅度降低性能
Java的ClassLoader
双亲委派机制
就是找类的时候先由父加载器直到Bootstrap ClassLoader寻找并更新,如果找不到,再由自己更新
Java的ClassLoader分为两种:
- 系统类加载器
BootstrapClassLoader, ExtensionsClassLoader, ApplicationClassLoader
- 自定义类加载器
Custom ClassLoader, 通过继承java.lang.ClassLoader实现
ClassLoader的继承关系如图所示:
- ClassLoader 是一个抽象类,定义了ClassLoader的主要功能
- SecureClassLoader 继承自ClassLoader,但并不是ClassLoader的实现类,而是拓展并加入了权限管理方面的功能,增强了安全性
- URLClassLoader 继承自SecureClassLoader 可通过URL路径从jar文件和文件夹中加载类和资源
- ExtClassLoader和AppClassLoader都继承自URLClassLoader
Android中的ClassLoader
Java中的ClassLoader可以加载jar和class文件(本质都是加载class文件)
而在Android中,无论DVM还是ART加载的文件都是dex文件,所以需要重新设计ClassLoader的相关类.
Android中的ClassLoader分为系统类加载器和自定义加载器:
- 系统类加载器
包括 BootClassLoader, PathClassLoader, DexClassLoader等
- 自定义加载器
通过继承BaseDexClassLoader实现,它们的继承关系如图所示:

boot secure url等类继承关系与之前的java差不多
但是多了一个base作为classloader的实现类
而下面的三种具体loader方式则继承了base类
而下面的三种具体load则是实现Android加固的主要方法
- DexClassLoader 可以加载未安装apk的dex文件
它是一代加固——整体加固(落地加载)的核心之一
- InMemoryDexClassLoader 可以加载内存中的dex文件
它是二代加固——整体加固(不落地加载)的核心之一
实际上DexClassLoader,PathClassLoader以及InMemoryDexClassLoader加载类时,均通过委托BaseDexClassLoader实现
具体流程
1、加载dex文件
Java层
PathClassLoader和DexClassLoader委托BaseDexClassLoader最终执行JNI方法DexFile.openDexFileNative进入Native层.
而InMemoryDexClassLoader委托BaseDexClassLoader后则执行DexFile.openInMemoryDexFiles进入Native层.
Native层
PathClassLoader和DexClassLoader这条委托链会根据不同情况,调用ArtDexFileLoader::Open的不同重载,或者调用OpenOneDexFileFromZip.
InMemoryDexClassLoader调用ArtDexFileLoader::Open的第3种重载.
无论是调用哪个函数,最终都会调用ArtDexFileLoader::OpenCommon. 创建DexFile对象后,Class对应的文件便被加载到ART虚拟机中
2、将dex文件转化为可供虚拟机运行的class
那三种classloader在进行load_class时会根据双亲委派机制寻找到父类classloader进行加载,最终进入DexFile
进入DexFile后,主要执行以下操作:
- DexFile
通过JNI函数defineClassNative进入Native层.
- DexFile_defineClassNative
通过FindClassDef枚举DexFile的所有DexClassDef结构并使用ClassLinker::DefineClass创建对应的Class对象.
之后调用ClassLinker::InsertDexFileInToClassLoader将对应的DexFile对象添加到ClassLoader的ClassTable中.
- ClassLinker::DefineClass
调用LoadField加载类的相关字段,之后调用LoadMethod加载方法,再调用LinkCode执行方法代码的链接.
Android应用程序启动流程
一代加固
简单总结:
加密整个文件并将加密后的apk和壳dex一起合并变成一个新的dex


通过在真实oncreate前添加
// 原来系统启动流程:System → GameApplication.attachBaseContext() → GameApplication.onCreate()
// 加固后启动流程:System → ProxyApplication.attachBaseContext() → [解密提取原APK] → [替换ClassLoader] → ProxyApplication.onCreate() → GameApplication.onCreate()
举点例子
com.example.game.apk(原始)
├── AndroidManifest.xml
│ └── android:name="com.example.game.GameApplication" 真实Application
├── classes.dex
│ ├── com.example.game.GameApplication
│ ├── com.example.game.MainActivity
│ ├── com.example.game.Player
│ └── com.example.game.WeaponSystem
├── lib/
│ ├── armeabi-v7a/libgame.so
│ └── arm64-v8a/libgame.so
└── res/...
com.example.game.apk(加壳后)
├── AndroidManifest.xml
│ └── android:name="com.example.shell.ProxyApplication" 换成壳的入口
│ <meta-data
│ android:name="APPLICATION_CLASS_NAME"
│ android:value="com.example.game.GameApplication"/> 真实类名
├── classes.dex 壳dex
│ ├── [正常dex文件头]
│ ├── com.example.shell.ProxyApplication ← 壳的代码
│ ├── com.example.shell.Reflection ← 壳的工具类
│ ├── ─────────────────────────────────────
│ ├── [加密后的原始APK二进制数据] ← 原始APK加密后附在这里
│ └── [4字节:原始APK大小,小端序] ← 最后4字节
├── lib/ ← 壳自己的so
└── res/...← 壳的资源
二代壳
区别
内部调用readAndCombineDexs读取并合并源apk的多个dex为一个文件
在android8.0以上使用InMemoryDexClassLoader进行内存时的加载
另外需要针对第一代加固不支持Multidex进行优化:
源apk每个dex文件前添加4字节标识其大小,之后全部合并成一个文件再合并到壳dex,最后添加4字节标识文件总大小

举点例子

三代加固-抽取加固
基于整体加固遇到的部分问题,引申出了三代加固: 代码抽取加固
思路: 将关键方法的指令抽空,替换为nop指令,运行时动态回填指令执行, 回填的核心是对Android系统核心函数进行hook
前面解密dex部分与一二代壳并无区别,主要是解密后的dex的方法字节码仍是null,我们该如何回填
- 解析.codes文件
解析code文件结构
[4字节: codeOff][4字节: insnSize][insnSize*2 字节: 指令数据]
[4字节: codeOff][4字节: insnSize][insnSize*2 字节: 指令数据]
codeOff:这段字节码在原始dex文件中的偏移位置
insnSize:指令数量(注意每条指令是u2=2字节,所以实际字节数是insnSize*2)
指令数据:真实的字节码2.hook_LoadMethod —— 拦截方法加载
void hook_LoadMethod() {
// 第一步:在libart.so的ELF文件里找到 LoadMethod 的符号名
// 因为C++有名称修饰,真实符号名类似:
// _ZN3art11ClassLinker10LoadMethodERKNS_7DexFileE...
const char* symbol = getClassLinkerLoadMethodSymbol();
// 第二步:用 Dobby 框架,通过符号名找到函数在内存中的地址
void* loadMethodAddress = DobbySymbolResolver(
getClassLinkerLoadMethodLibPath(), // libart.so路径
symbol
);
// 第三步:用 Dobby 替换这个函数
DobbyHook(
loadMethodAddress, // 要hook的原函数地址
(void*)newLoadMethod, // 替换成我们的函数
(void**)&g_originLoadMethod // 保存原函数地址,以便后续调用
);
}3、这里要求so中需要有
1、禁用dex2oat优化
dex2oat是Android的AOT编译器把dex字节码预编译成机器码,存到oat文件里
如果仍旧执行oat,那么就会不走LoadMethod ,就会导致方法一直都是null,所以
我们得动态(强制让ART用解释执行模式)
即:每个方法执行前必须走LoadMethod → hook生效 → 回填成功
2、让dex内存可写
ART加载dex文件时,用mmap把dex文件映射到内存
映射时权限是 PROT_READ(只读)
所以hook mmap,把所有只读映射偷偷改成可读可写
4、核心回填代码
void innerLoadMethod(void* thiz, const DexFile* dexFile, ClassAccessor::Method* method, void* klass, void* dest){
// dex文件路径
std::string location = dexFile->location_;
//logd("Load Dex File Location: %s",location.c_str())
// 判断是否为解密释放的dex文件,位于私有目录内
if(location.find("app_tmp_dex") == std::string::npos){
return;
}
// 如果未解析过dexCodes文件则进行解析,每个dex文件只解析一次,创建对应的map<CodeOff,CodeItem>映射
if(codeMapList.find(location)==codeMapList.end()){
logd("Parse dex file %s codes",location.c_str());
codeMapList[location]=std::map<uint32_t,CodeItem>(); //创建新的codeMap
parseExtractedCodeFiles(location);
}
// 不存在DexCode 直接跳过
if(method->code_off_==0){
return;
}
// 指令地址
uint8_t* codeAddr = (uint8_t*)(dexFile->begin_ + method->code_off_ + 16); //insn结构前面有16字节
//logd("MethodCodeOff: %d",method->code_off_);
// 回填指令
std::map<uint32_t,CodeItem> codeMap=codeMapList[location];
// 似乎没有走到回填指令处 (注意c++浅拷贝问题,不能随意delete)
if(codeMap.find(method->code_off_) != codeMap.end()){
CodeItem codeItem = codeMap[method->code_off_];
memcpy(codeAddr,codeItem.getInsns(),codeItem.getInsnsSize()*2); //注意指令为u2类型,长度需要*2
}
}总结
一代流程:
attachBaseContext
├── readDexFromAp读壳dex字节
├── extractSrcApkFromShellDex()
│ ├── 从末尾读4字节 → APK大小
│ ├── 截取加密APK → 解密
│ ├── 写入磁盘 Source.apk ← 落地
│ └── 解压so到 app_tmp_lib
└── replaceClassLoader()
└── DexClassLoader(磁盘路径)
二代流程:
attachBaseContext
├── readDexFromApk() 读壳dex字节(相同)
├── extractDexFilesFromShellDex()
│ ├── 从末尾读4字节 → sourceDexs总大小
│ ├── 截取加密sourceDexs → 解密
│ └── 循环拆分多个dex → ByteBuffer[]内存
└── replaceClassLoader(ByteBuffer[])
└── InMemoryDexClassLoader(内存数据)
三代流程:
Java层(ThirdProxyApplication)
├── 和二代基本相同
│ 读壳dex → 解密 → 多个ByteBuffer → 写出空壳dex
│ 复制 .codes 文件到私有目录
│ DexClassLoader 加载空壳dex
│ 替换 ClassLoader
│ 替换 Application
└── 多了一行:System.loadLibrary("androidshell") ← 触发Native层
Native层(libandroidshell.so)
├── hookMmap:让dex内存可写(为回填做准备)
├── hookExecve:禁dex2oat(保证走解释执行,LoadMethod必经)
└── hook_LoadMethod:拦截方法加载,在这里执行回填
数据层(.codes文件)
├── 存储被抽取的真实字节码
├── 格式:[codeOff(4)][insnSize(4)][指令数据(insnSize*2)]
└── 运行时由Native层读取解析