簡介
- 所有的加固代碼 都需要通過Classloader加載然后才可以執(zhí)行
classloader介紹
-
雙親委派機制
雙親委派模式的工作原理的是;如果一個類加載器收到了類加載請求,它并不會自己先去加載,而是把這個請求委托給父類的加載器去執(zhí)行
如果父類加載器還存在其父類加載器,則進一步向上委托,依次遞歸,請求最終將到達頂層的啟動類加載器
如果父類加載器可以完成類加載任務(wù),就成功返回,倘若父類加載器無法完成此加載任務(wù),子加載器才會嘗試自己去加載,這就是雙親委派模式
即每個兒子都不愿意干活,每次有活就丟給父親去干,直到父親說這件事我也干不了時,兒子自己想辦法去完成,這個就是雙親委派。 -
Android中的classloader
ClassLoader為抽象類;
BootClassLoader預(yù)加載常用類,單例模式。
BaseDexClassLoader是PathClassLoader、DexClassLoader、InMemoryDexClassLoader的父類,類加載的主要邏輯都是在BaseDexClassLoader完成的。
SecureClassLoader繼承了抽象類ClassLoader,拓展了ClassLoader類加入了權(quán)限方面的功能,加強了安全性,其子類URLClassLoader是用URL路徑從jar文件中加載類和資源。
其中重點關(guān)注的是PathClassLoader和DexClassLoader。
PathClassLoader是Android默認使用的類加載器,一個apk中的Activity等類便是在其中加載。
DexClassLoader可以加載任意目錄下的dex/jar/apk/zip文件,比PathClassLoader更靈活,是實現(xiàn)插件化、熱修復(fù)以及dex加殼的重點。
Android8.0新引入InMemoryDexClassLoader,從名字便可看出是用于直接從內(nèi)存中加載dex。

APP 的啟動流程

APP類加載過程
- BootClassLoader加載系統(tǒng)核心庫
- PathClassLoader加載APP自身dex
- 進入APP自身組件開始執(zhí)行,調(diào)用聲明Application的attachBaseContext,onCreate
- AppThread中有LoadedApk對象。
- AppThread handledBindApplication(應(yīng)該是AMS 回調(diào)的ApplicationThread)中,第一次進入到App自身的代碼中去。
- Application attach -> oncreate最先被執(zhí)行,如果加殼的話,此時的classloader只有殼的代碼 沒有自身代碼
- 如果不處理classloader的話,只有殼代碼,是無法啟動注冊的Activity Service等的。
*<mark> 所以殼代碼需要在這里加載真正的代碼</mark>
組件生命周期的處理
DexClassLoader加載的類是沒有組件生命周期的,也就是說即使DexClassLoader通過對APK的動態(tài)加載完成了對組件類的加載,當系統(tǒng)啟動該組件時,依然會出現(xiàn)加載類失敗的異常。
需要替換系統(tǒng)組件的classloader才可以。加固廠商必然饒不過去
兩種解決方案:
1、替換系統(tǒng)組件類加載器為我們的DexClassLoader,同時設(shè)置DexClassLoader的parent為系統(tǒng)組件類加載器;
//classloader 已經(jīng)加載了 自身代碼 并且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<String, WeakReference<LoadedApk>> 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、打破原有的雙親關(guān)系,在系統(tǒng)組件類加載器和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加入內(nèi)存中。
關(guān)鍵代碼
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<String, WeakReference<LoadedApk>> 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加載 ,實現(xiàn)不落地加載
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));
}
}
加固類型-函數(shù)抽取
Dalvik指令抽取原理介紹
Android中實現(xiàn)「類方法指令抽取方式」加固方案原理解析
Android免Root權(quán)限通過Hook系統(tǒng)函數(shù)修改程序運行時內(nèi)存指令邏輯
- 在函數(shù)粒度上保護代碼
- 將函數(shù)指令全部替換為Nop或無效指令,在執(zhí)行時還原代碼。
- 主要原理是將原指令置為nop,然后hook,dexFindClass,找到原始的方法指令指針,DexCode->insns,替換insns。

Art 抽取殼介紹
Art下實現(xiàn)難點:dex2oat編譯流程,dex2oat是可以進行脫殼,dex2oat完成了對抽取的dex進行編譯生成了oat文件,后續(xù)的函數(shù)運行中,從oat中取出函數(shù)編譯生成的二進制代碼來執(zhí)行,因此函數(shù)對dex填充后,如果時機不對,時機在dex2oat后,自然從dex2oat后那么我們動態(tài)修改的dex中的smali指令流就不會生效,因為后面app運行調(diào)用的真正的代碼就會從dex2oat編譯生成的oat文件,和以前的dex無關(guān)了。因此如果希望填充回去smali指令生效要么禁用dex2oat實現(xiàn)阻止編譯,這樣對加載到內(nèi)存中的dex文件進行填充始終會保持生效,要么保持dex2oat編譯,但是還原代碼時機要早于dex2oat就ok了,保證dex2oat再次對dex編譯的時候,dex已經(jīng)是一個完整dex,不會影響我們填充的代碼,但是肯定dex文件存在完整的時候,可以利用dex2oat編譯的流程進行脫殼,一般加殼廠商都是犧牲掉app一部分的運行效率,干掉dex2oat的過程,因為google本身提倡dex2oat就是為了提升app運行效率。
<mark>ART模式下抽取殼 要么阻止dex2oat 要么在dex2oat之前保證加載的dex是一個完整的dex</mark>
如果選擇第一種方案 , 在 dex2oat 之前進行恢復(fù) , 這沒有任何意義 , dex2oat 編譯后 , 生成的 oat 文件是完整的 , 此時 可以 完整的將 oat 文件 dump 到 SD 卡中 , 基本等于沒有加固 , 還是一個一代殼 ;
因此 , 大部分加固廠商 , 選擇 禁用 dex2oat 機制 ; 這樣處于安全考慮 , 犧牲了應(yīng)用的運行效率 ;

- classloader加載dex的時候 會進行dex2oat的優(yōu)化,將dex的解釋執(zhí)行,變成直接執(zhí)行,提升運行效率。
- 7.0之后會根據(jù)函數(shù)使用頻率進行dex2oat的優(yōu)化,并不會直接全部dex2oat
https://source.android.google.cn/devices/tech/dalvik/configure
ART 使用預(yù)先 (AOT) 編譯,并且從 Android 7.0(代號 Nougat,簡稱 N)開始結(jié)合使用 AOT、即時 (JIT) 編譯和配置文件引導(dǎo)型編譯。所有這些編譯模式的組合均可配置,我們將在本部分中對此進行介紹。例如,Pixel 設(shè)備配置了以下編譯流程:
7.0之后最初安裝應(yīng)用時不進行任何 AOT 編譯。應(yīng)用前幾次運行時,系統(tǒng)會對其進行解譯,并對經(jīng)常執(zhí)行的方法進行 JIT 編譯。
當設(shè)備閑置和充電時,編譯守護程序會運行,以便根據(jù)在應(yīng)用前幾次運行期間生成的配置文件對常用代碼進行 AOT 編譯。
下一次重新啟動應(yīng)用時將會使用配置文件引導(dǎo)型代碼,并避免在運行時對已經(jīng)過編譯的方法進行 JIT 編譯。在應(yīng)用后續(xù)運行期間經(jīng)過 JIT 編譯的方法將會添加到配置文件中,然后編譯守護程序?qū)@些方法進行 AOT 編譯。
ART 的運作方式 在 Android O 版本中,將會生成以下文件:
.vdex:其中包含 APK 的未壓縮 DEX 代碼,以及一些旨在加快驗證速度的元數(shù)據(jù)。
使用010editer就可以找到dex
.odex:其中包含 APK 中已經(jīng)過 AOT 編譯的方法代碼。
oatdump 可以反編譯odex文件,可以dump出函數(shù)的smali指令
.art (optional):其中包含 APK 中列出的某些字符串和類的 ART 內(nèi)部表示,用于加快應(yīng)用啟動速度。
全部進行oat優(yōu)化的話,非常耗時,所以7.0之后選取了保守的優(yōu)化策略
odex 優(yōu)化后,oatdump還是可以dump出原始的smali指令,只是優(yōu)化后的函數(shù) 在CODE段,會有優(yōu)化后的二進制匯編指令,在執(zhí)行的時候會直接進入quick模式,執(zhí)行匯編指令,加快運行速度。具有CODE段后,將不再進入解釋執(zhí)行模式。
- art抽取殼必須在dex2oat之前還原,否則dex2oat之后,就無法還原。要么阻止dex2oat 要么提前還原
- 首先要干掉Dex2oat的過程??梢詤⒖糶ithub TurboDex(快速加載dex 阻止dex2oat) dex2oat 非常耗時;不進行oat的話 會影響dex的運行效率。
- 阻止了dex2oat后 所有代碼都是解釋執(zhí)行了。那么必然會去內(nèi)存中尋找ArtMethod的code_item去執(zhí)行
- 那么在art類加載的時候還原code_item就可以正常去執(zhí)行了
- 剩下的和dalvik基本一致
- 1、先通過010edite將一個函數(shù)全部抽取為nop
- 2、hook art loadmethod的過程
- 3、將原始的指令還原到artmethod中
ART的類加載執(zhí)行流程
強烈建議閱讀此文章
ART 在 Android 安全攻防中的應(yīng)用
關(guān)鍵代碼
#include <jni.h>
#include <string>
#include <unistd.h>
#include <android/log.h>
#include <fcntl.h>
#include <asm/fcntl.h>
#include <sys/mman.h>
#include <dlfcn.h>
//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<mirror::Class> 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<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);
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<byte *>(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 禁止執(zhí)行dex2oat
hookART();//hook LoadMethod 修復(fù)被抽取的artmethod,并在修復(fù)前修復(fù)后dumpdex
return;
}
加固類型-VMP,Dex2C
- VMP 自定義解釋器,解釋執(zhí)行自定義的指令。
- Dex2c 在編譯時,將java代碼編譯成c代碼 增加破解難度
- 未來加固的主要方向->VMP,Dex2C
- VMP 會把java函數(shù)native化,原理:VMP是做虛擬機解釋器
- VMP的多個native函數(shù) 很可能是一個地址。多個native函數(shù) 都是調(diào)用的一個地方
- 破解需要找到解釋器,找到smaili的映射關(guān)系即可恢復(fù)
- github 搜索 ADVMP 可以參考 沒具體研究
- Dex2c 開源項目 dcc,原理:對java語義分析,重新生成native函數(shù)。
- python dcc.py dcc.apk -o dcc_out.apk 使用DCC增加保護,java函數(shù)會變成native函數(shù)。 只建議部分函數(shù)增加保護,否則會嚴重影響性能
- dcc可以將APK的java代碼 轉(zhuǎn)化為native代碼,并且可以增加ollvm的混淆
- dcc的native注冊函數(shù) 基本是一一對應(yīng)
- 基本不太好破解。。
- VMP 會把java函數(shù)native化,原理:VMP是做虛擬機解釋器