简介
- 所有的加固代码 都需要通过Classloader加载然后才可以执行
classloader介绍
-
双亲委派机制
双亲委派模式的工作原理的是;如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行
如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器
如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式
即每个儿子都不愿意干活,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成,这个就是双亲委派。 -
Android中的classloader
ClassLoader为抽象类;
BootClassLoader预加载常用类,单例模式。
BaseDexClassLoader是PathClassLoader、DexClassLoader、InMemoryDexClassLoader的父类,类加载的主要逻辑都是在BaseDexClassLoader完成的。
SecureClassLoader继承了抽象类ClassLoader,拓展了ClassLoader类加入了权限方面的功能,加强了安全性,其子类URLClassLoader是用URL路径从jar文件中加载类和资源。
其中重点关注的是PathClassLoader和DexClassLoader。
PathClassLoader是Android默认使用的类加载器,一个apk中的Activity等类便是在其中加载。
DexClassLoader可以加载任意目录下的dex/jar/apk/zip文件,比PathClassLoader更灵活,是实现插件化、热修复以及dex加壳的重点。
Android8.0新引入InMemoryDexClassLoader,从名字便可看出是用于直接从内存中加载dex。
APP 的启动流程
APP类加载过程
- BootClassLoader加载系统核心库
- PathClassLoader加载APP自身dex
- 进入APP自身组件开始执行,调用声明Application的attachBaseContext,onCreate
- AppThread中有LoadedApk对象。
- AppThread handledBindApplication(应该是AMS 回调的ApplicationThread)中,第一次进入到App自身的代码中去。
- Application attach -> oncreate最先被执行,如果加壳的话,此时的classloader只有壳的代码 没有自身代码
- 如果不处理classloader的话,只有壳代码,是无法启动注册的Activity Service等的。
* 所以壳代码需要在这里加载真正的代码
组件生命周期的处理
DexClassLoader加载的类是没有组件生命周期的,也就是说即使DexClassLoader通过对APK的动态加载完成了对组件类的加载,当系统启动该组件时,依然会出现加载类失败的异常。
需要替换系统组件的classloader才可以。加固厂商必然饶不过去
两种解决方案:
1、替换系统组件类加载器为我们的DexClassLoader,同时设置DexClassLoader的parent为系统组件类加载器;
//classloader 已经加载了 自身代码 并且parent是原始的classloader
public void replaceClassloader(ClassLoader classloader){
try {
Class> ActivityThreadClazz=classloader.loadClass("android.app.ActivityThread");
Method currentActivityThreadMethod= ActivityThreadClazz.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
Object activityThreadObj=currentActivityThreadMethod.invoke(null);
//final ArrayMap> mPackages = new ArrayMap<>();
Field mPackagesField=ActivityThreadClazz.getDeclaredField("mPackages");
mPackagesField.setAccessible(true);
ArrayMap mPackagesObj= (ArrayMap) mPackagesField.get(activityThreadObj);
WeakReference wr= (WeakReference) mPackagesObj.get(this.getPackageName());
Object loadedApkObj=wr.get();
Class LoadedApkClazz=classloader.loadClass("android.app.LoadedApk");
//private ClassLoader mClassLoader;
Field mClassLoaderField=LoadedApkClazz.getDeclaredField("mClassLoader");
mClassLoaderField.setAccessible(true);
mClassLoaderField.set(loadedApkObj,classloader);
} catch (Exception e) {
e.printStackTrace();
}
}
2、打破原有的双亲关系,在系统组件类加载器和BootClassLoader的中间插入我们自己的DexClassLoader即可;
public void startTestActivitySecondMethod(Context context,String dexfilepath){
File optfile=context.getDir("opt_dex",0);
File libfile=context.getDir("lib_path",0);
ClassLoader pathClassloader=MainActivity.class.getClassLoader();
ClassLoader bootClassloader=MainActivity.class.getClassLoader().getParent();
DexClassLoader dexClassLoader=new DexClassLoader(dexfilepath,optfile.getAbsolutePath(),libfile.getAbsolutePath(),bootClassloader);
try {
Field parentField=ClassLoader.class.getDeclaredField("parent");
parentField.setAccessible(true);
parentField.set(pathClassloader,dexClassLoader);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
dex整体加密型加固
- 将Dex整体加密,在需要的时候解密,通过Classloader加入内存中。
关键代码
public class TestActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//com.example.myapplication.TestClass.testFunc
Context appContext=this.getApplicationContext();
//testDexClassLoader(appContext,"/sdcard/3.dex");
//startTestActivity(this,"/sdcard/4.dex");
startTestActivitySecondMethod(this,"/sdcard/6.dex");
}
//替换LoadedApk中的mClassLoader 加载自身代码
public void replaceClassloader(ClassLoader classloader){
try {
Class> ActivityThreadClazz=classloader.loadClass("android.app.ActivityThread");
Method currentActivityThreadMethod= ActivityThreadClazz.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
Object activityThreadObj=currentActivityThreadMethod.invoke(null);
//final ArrayMap> mPackages = new ArrayMap<>();
Field mPackagesField=ActivityThreadClazz.getDeclaredField("mPackages");
mPackagesField.setAccessible(true);
ArrayMap mPackagesObj= (ArrayMap) mPackagesField.get(activityThreadObj);
WeakReference wr= (WeakReference) mPackagesObj.get(this.getPackageName());
Object loadedApkObj=wr.get();
Class LoadedApkClazz=classloader.loadClass("android.app.LoadedApk");
//private ClassLoader mClassLoader;
Field mClassLoaderField=LoadedApkClazz.getDeclaredField("mClassLoader");
mClassLoaderField.setAccessible(true);
mClassLoaderField.set(loadedApkObj,classloader);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
public void startTestActivityFirstMethod(Context context,String dexfilepath){
File optfile=context.getDir("opt_dex",0);
File libfile=context.getDir("lib_path",0);
ClassLoader parentClassloader=MainActivity.class.getClassLoader();
ClassLoader tmpClassLoader=context.getClassLoader();
DexClassLoader dexClassLoader=new DexClassLoader(dexfilepath,optfile.getAbsolutePath(),libfile.getAbsolutePath(),MainActivity.class.getClassLoader());
replaceClassloader(dexClassLoader);
Class> clazz=null;
try {
clazz = dexClassLoader.loadClass("com.example.myapplication.TestActivity");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
context.startActivity(new Intent(context,clazz));
}
public void startTestActivitySecondMethod(Context context,String dexfilepath){
File optfile=context.getDir("opt_dex",0);
File libfile=context.getDir("lib_path",0);
ClassLoader pathClassloader=MainActivity.class.getClassLoader();
ClassLoader bootClassloader=MainActivity.class.getClassLoader().getParent();
//加载dex 这里直接加载了未加密的,可以解密后 再加载,然后删除。也可以替换为memoryclassloader加载 ,实现不落地加载
DexClassLoader dexClassLoader=new DexClassLoader(dexfilepath,optfile.getAbsolutePath(),libfile.getAbsolutePath(),bootClassloader);
try {
//插入我们自己的dexClassLoader 加入生命周期的处理
Field parentField=ClassLoader.class.getDeclaredField("parent");
parentField.setAccessible(true);
parentField.set(pathClassloader,dexClassLoader);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
/*
* this:dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.xyh.loaddex-8_fCxispeBuExjw1ryrRZg==/base.apk"],nativeLibraryDirectories=[/data/app/com.xyh.loaddex-8_fCxispeBuExjw1ryrRZg==/lib/arm64, /system/lib64, /vendor/lib64]]]--parent:dalvik.system.DexClassLoader[DexPathList[[dex file "/sdcard/6.dex"],nativeLibraryDirectories=[/data/user/0/com.xyh.loaddex/app_lib_path, /system/lib64, /vendor/lib64]]]
this:dalvik.system.DexClassLoader[DexPathList[[dex file "/sdcard/6.dex"],nativeLibraryDirectories=[/data/user/0/com.xyh.loaddex/app_lib_path, /system/lib64, /vendor/lib64]]]--parent:java.lang.BootClassLoader@fd4323d
root:java.lang.BootClassLoader@fd4323d*/
ClassLoader tmpClassloader=pathClassloader;
ClassLoader parentClassloader=pathClassloader.getParent();
while(parentClassloader!=null){
Log.i("xyh","this:"+tmpClassloader+"--parent:"+parentClassloader);
tmpClassloader=parentClassloader;
parentClassloader=parentClassloader.getParent();
}
Log.i("xyh","root:"+tmpClassloader);
Class> clazz=null;
try {
clazz = dexClassLoader.loadClass("com.example.myapplication.TestActivity");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
context.startActivity(new Intent(context,clazz));
}
}
加固类型-函数抽取
壳与脱壳之二代壳函数抽取/#源码分析
Dalvik指令抽取原理介绍
Android中实现「类方法指令抽取方式」加固方案原理解析
Android免Root权限通过Hook系统函数修改程序运行时内存指令逻辑
- 在函数粒度上保护代码
- 将函数指令全部替换为Nop或无效指令,在执行时还原代码。
- 主要原理是将原指令置为nop,然后hook,dexFindClass,找到原始的方法指令指针,DexCode->insns,替换insns。
Art 抽取壳介绍
Art下实现难点:dex2oat编译流程,dex2oat是可以进行脱壳,dex2oat完成了对抽取的dex进行编译生成了oat文件,后续的函数运行中,从oat中取出函数编译生成的二进制代码来执行,因此函数对dex填充后,如果时机不对,时机在dex2oat后,自然从dex2oat后那么我们动态修改的dex中的smali指令流就不会生效,因为后面app运行调用的真正的代码就会从dex2oat编译生成的oat文件,和以前的dex无关了。因此如果希望填充回去smali指令生效要么禁用dex2oat实现阻止编译,这样对加载到内存中的dex文件进行填充始终会保持生效,要么保持dex2oat编译,但是还原代码时机要早于dex2oat就ok了,保证dex2oat再次对dex编译的时候,dex已经是一个完整dex,不会影响我们填充的代码,但是肯定dex文件存在完整的时候,可以利用dex2oat编译的流程进行脱壳,一般加壳厂商都是牺牲掉app一部分的运行效率,干掉dex2oat的过程,因为google本身提倡dex2oat就是为了提升app运行效率。
ART模式下抽取壳 要么阻止dex2oat 要么在dex2oat之前保证加载的dex是一个完整的dex
如果选择第一种方案 , 在 dex2oat 之前进行恢复 , 这没有任何意义 , dex2oat 编译后 , 生成的 oat 文件是完整的 , 此时 可以 完整的将 oat 文件 dump 到 SD 卡中 , 基本等于没有加固 , 还是一个一代壳 ;
因此 , 大部分加固厂商 , 选择 禁用 dex2oat 机制 ; 这样处于安全考虑 , 牺牲了应用的运行效率 ;
- classloader加载dex的时候 会进行dex2oat的优化,将dex的解释执行,变成直接执行,提升运行效率。
- 7.0之后会根据函数使用频率进行dex2oat的优化,并不会直接全部dex2oat
https://source.android.google.cn/devices/tech/dalvik/configure
ART 使用预先 (AOT) 编译,并且从 Android 7.0(代号 Nougat,简称 N)开始结合使用 AOT、即时 (JIT) 编译和配置文件引导型编译。所有这些编译模式的组合均可配置,我们将在本部分中对此进行介绍。例如,Pixel 设备配置了以下编译流程:
7.0之后最初安装应用时不进行任何 AOT 编译。应用前几次运行时,系统会对其进行解译,并对经常执行的方法进行 JIT 编译。
当设备闲置和充电时,编译守护程序会运行,以便根据在应用前几次运行期间生成的配置文件对常用代码进行 AOT 编译。
下一次重新启动应用时将会使用配置文件引导型代码,并避免在运行时对已经过编译的方法进行 JIT 编译。在应用后续运行期间经过 JIT 编译的方法将会添加到配置文件中,然后编译守护程序将会对这些方法进行 AOT 编译。
ART 的运作方式 在 Android O 版本中,将会生成以下文件:
.vdex:其中包含 APK 的未压缩 DEX 代码,以及一些旨在加快验证速度的元数据。
使用010editer就可以找到dex
.odex:其中包含 APK 中已经过 AOT 编译的方法代码。
oatdump 可以反编译odex文件,可以dump出函数的smali指令
.art (optional):其中包含 APK 中列出的某些字符串和类的 ART 内部表示,用于加快应用启动速度。
全部进行oat优化的话,非常耗时,所以7.0之后选取了保守的优化策略
odex 优化后,oatdump还是可以dump出原始的smali指令,只是优化后的函数 在CODE段,会有优化后的二进制汇编指令,在执行的时候会直接进入quick模式,执行汇编指令,加快运行速度。具有CODE段后,将不再进入解释执行模式。
- art抽取壳必须在dex2oat之前还原,否则dex2oat之后,就无法还原。要么阻止dex2oat 要么提前还原
- 首先要干掉Dex2oat的过程。可以参考github TurboDex(快速加载dex 阻止dex2oat) dex2oat 非常耗时;不进行oat的话 会影响dex的运行效率。
- 阻止了dex2oat后 所有代码都是解释执行了。那么必然会去内存中寻找ArtMethod的code_item去执行
- 那么在art类加载的时候还原code_item就可以正常去执行了
- 剩下的和dalvik基本一致
- 1、先通过010edite将一个函数全部抽取为nop
- 2、hook art loadmethod的过程
- 3、将原始的指令还原到artmethod中
ART的类加载执行流程
强烈建议阅读此文章
ART 在 Android 安全攻防中的应用
关键代码
#include
#include
#include
#include
#include
#include
#include
#include
//import c header
extern "C" {
#include "hook/dlfcn/dlfcn_compat.h"
#include "hook/include/inlineHook.h"
}
typedef unsigned char byte;
#define TAG "SecondShell"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
struct DexFile {
// Field order required by test "ValidateFieldOrderOfJavaCppUnionClasses".
// The class we are a part of.
uint32_t declaring_class_;
// Access flags; low 16 bits are defined by spec.
void *begin;
/* Dex file fields. The defining dex file is available via declaring_class_->dex_cache_ */
// Offset to the CodeItem.
uint32_t size;
};
struct ArtMethod {
// Field order required by test "ValidateFieldOrderOfJavaCppUnionClasses".
// The class we are a part of.
uint32_t declaring_class_;
// Access flags; low 16 bits are defined by spec.
uint32_t access_flags_;
/* Dex file fields. The defining dex file is available via declaring_class_->dex_cache_ */
// Offset to the CodeItem.
uint32_t dex_code_item_offset_;
// Index into method_ids of the dex file associated with this method.
uint32_t dex_method_index_;
};
void* *(*oriexecve)(const char *__file, char *const *__argv, char *const *__envp);
void* *myexecve(const char *__file, char *const *__argv, char *const *__envp) {
LOGD("process:%d,enter execve:%s", getpid(), __file);
if (strstr(__file, "dex2oat")) {
return NULL;
} else {
return oriexecve(__file, __argv, __envp);
}
}
//void ClassLinker::LoadMethod(Thread* self, const DexFile& dex_file, const ClassDataItemIterator& it,Handle klass, ArtMethod* dst)
void *(*oriloadmethod)(void *, void *, void *, void *, void *);
void *myloadmethod(void *a, void *b, void *c, void *d, void *e) {
LOGD("process:%d,before run loadmethod:", getpid());
struct ArtMethod *artmethod = (struct ArtMethod *) e;
struct DexFile *dexfile = (struct DexFile *) b;
LOGD("process:%d,enter loadmethod:dexfilebegin:%p,size:%d", getpid(), dexfile->begin,
dexfile->size);//0,57344
char dexfilepath[100] = {0};
sprintf(dexfilepath, "/sdcard/%d_%d.dex", dexfile->size, getpid());
int fd = open(dexfilepath, O_CREAT | O_RDWR, 0666);
if (fd > 0) {
write(fd, dexfile->begin, dexfile->size);
close(fd);
}
void *result = oriloadmethod(a, b, c, d, e);
LOGD("process:%d,enter loadmethod:code_offset:%d,idx:%d", getpid(),
artmethod->dex_code_item_offset_, artmethod->dex_method_index_);
byte *code_item_addr = static_cast(dexfile->begin) + artmethod->dex_code_item_offset_;
LOGD("process:%d,enter loadmethod:dexfilebegin:%p,size:%d,beforedumpcodeitem:%p", getpid(),
dexfile->begin, dexfile->size, code_item_addr);
if (artmethod->dex_method_index_ == 15203) {//TestClass.testFunc->methodidx
LOGD("process:%d,enter loadmethod:dexfilebegin:%p,size:%d,start repire method", getpid(),
dexfile->begin, dexfile->size);
byte *code_item_addr = (byte *) dexfile->begin + artmethod->dex_code_item_offset_;
LOGD("process:%d,enter loadmethod:dexfilebegin:%p,size:%d,beforedumpcodeitem:%p", getpid(),
dexfile->begin, dexfile->size, code_item_addr);
int result = mprotect(dexfile->begin, dexfile->size, PROT_WRITE);
byte *code_item_start = static_cast(code_item_addr) + 16;
LOGD("process:%d,enter loadmethod:dexfilebegin:%p,size:%d,code_item_start:%p", getpid(),
dexfile->begin, dexfile->size, code_item_start);
byte inst[16] = {0x1a, 0x00, 0xed, 0x34, 0x1a, 0x01, 0x43, 0x32, 0x71, 0x20, 0x91, 0x05,
0x10, 0x00, 0x0e, 0x00};
for (int i = 0; i < sizeof(inst); i++) {
code_item_start[i] = inst[i];
}
//2343->i am from com.kanxue.test02.TestClass.testFunc
code_item_start[2] = 0x43;//34ed->kanxue
code_item_start[3] = 0x23;
memset(dexfilepath, 0, 100);
sprintf(dexfilepath, "/sdcard/%d_%d.dex_15203_2", dexfile->size, getpid());
fd = open(dexfilepath, O_CREAT | O_RDWR, 0666);
if (fd > 0) {
write(fd, dexfile->begin, dexfile->size);
close(fd);
}
}
LOGD("process:%d,after loadmethod:code_offset:%d,idx:%d", getpid(),
artmethod->dex_code_item_offset_, artmethod->dex_method_index_);//0,57344
return result;
}
void hooklibc() {
LOGD("go into hooklibc");
//7.0 命名空间限制
void *libc_addr = dlopen_compat("libc.so", RTLD_NOW);
void *execve_addr = dlsym_compat(libc_addr, "execve");
if (execve_addr != NULL) {
if (ELE7EN_OK == registerInlineHook((uint32_t) execve_addr, (uint32_t) myexecve,
(uint32_t **) &oriexecve)) {
if (ELE7EN_OK == inlineHook((uint32_t) execve_addr)) {
LOGD("inlineHook execve success");
} else {
LOGD("inlineHook execve failure");
}
}
}
}
void hookART() {
LOGD("go into hookART");
void *libart_addr = dlopen_compat("/system/lib/libart.so", RTLD_NOW);
if (libart_addr != NULL) {
void *loadmethod_addr = dlsym_compat(libart_addr,
"_ZN3art11ClassLinker10LoadMethodERKNS_7DexFileERKNS_21ClassDataItemIteratorENS_6HandleINS_6mirror5ClassEEEPNS_9ArtMethodE");
if (loadmethod_addr != NULL) {
if (ELE7EN_OK == registerInlineHook((uint32_t) loadmethod_addr, (uint32_t) myloadmethod,
(uint32_t **) &oriloadmethod)) {
if (ELE7EN_OK == inlineHook((uint32_t) loadmethod_addr)) {
LOGD("inlineHook loadmethod success");
} else {
LOGD("inlineHook loadmethod failure");
}
}
}
}
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_kanxue_secondshell_180_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "ART SecondShell";
return env->NewStringUTF(hello.c_str());
}
extern "C" JNIEXPORT void JNICALL
Java_com_kanxue_secondshell_180_MainActivity_SecondShell(
JNIEnv *env,
jobject /* this */) {
hooklibc();//hook execve 禁止执行dex2oat
hookART();//hook LoadMethod 修复被抽取的artmethod,并在修复前修复后dumpdex
return;
}
加固类型-VMP,Dex2C
- VMP 自定义解释器,解释执行自定义的指令。
- Dex2c 在编译时,将java代码编译成c代码 增加破解难度
- 未来加固的主要方向->VMP,Dex2C
- VMP 会把java函数native化,原理:VMP是做虚拟机解释器
- VMP的多个native函数 很可能是一个地址。多个native函数 都是调用的一个地方
- 破解需要找到解释器,找到smaili的映射关系即可恢复
- github 搜索 ADVMP 可以参考 没具体研究
- Dex2c 开源项目 dcc,原理:对java语义分析,重新生成native函数。
- python dcc.py dcc.apk -o dcc_out.apk 使用DCC增加保护,java函数会变成native函数。 只建议部分函数增加保护,否则会严重影响性能
- dcc可以将APK的java代码 转化为native代码,并且可以增加ollvm的混淆
- dcc的native注册函数 基本是一一对应
- 基本不太好破解。。
- VMP 会把java函数native化,原理:VMP是做虚拟机解释器