前言
dpt-shell自22年以来进行了不少的调整,本文试对v1.11.3版的dpt-shell进行源码分析,补充作者在HowItWorks中未撰写出来的部分并作积累。
简而言之,dpt-shell可以分为两个模块,一个是Processeor模块,用于对原app进行指令抽空并构建新app;另一个是shell模块,用于在app运行时回填指令,顺利执行app的代码。以下是对这两个模块的详细分析
Processeor模块分析
入口点在src\main\java\com\luoye\dpt\Dpt.java,解析用户的运行参数后进入apk.protect()进行抽取
private static void process(Apk apk){
if(!new File("shell-files").exists()) {
LogUtils.error("Cannot find shell files!");
return;
}
File apkFile = new File(apk.getFilePath());
if(!apkFile.exists()){
LogUtils.error("Apk not exists!");
return;
}
//apk extract path
String apkMainProcessPath = apk.getWorkspaceDir().getAbsolutePath();
LogUtils.info("Apk main process path: " + apkMainProcessPath);
ZipUtils.unZip(apk.getFilePath(),apkMainProcessPath);
String packageName = ManifestUtils.getPackageName(apkMainProcessPath + File.separator + "AndroidManifest.xml");
apk.setPackageName(packageName);
// 1. 指令抽空
apk.extractDexCode(apkMainProcessPath);
// 2. AMF的处理
apk.saveApplicationName(apkMainProcessPath); // 保存原始ApplicationName到assets/app_name
apk.writeProxyAppName(apkMainProcessPath); // 写入代理ApplicationName
if(apk.isAppComponentFactory()){
apk.saveAppComponentFactory(apkMainProcessPath); // 保存原始AppComponentFactory到assets/app_acf
apk.writeProxyComponentFactoryName(apkMainProcessPath); // 写入代理AppComponentFactory
}
if(apk.isDebuggable()) {
LogUtils.info("Make apk debuggable.");
apk.setDebuggable(apkMainProcessPath, true);
}
apk.setExtractNativeLibs(apkMainProcessPath);
apk.addJunkCodeDex(apkMainProcessPath);
// 3. 压缩源dex到新的路径并删除旧的路径
apk.compressDexFiles(apkMainProcessPath); // 源dex压缩存放到assets/i11111i111.zip
apk.deleteAllDexFiles(apkMainProcessPath);
// 4. 合并壳dex和原dex
apk.combineDexZipWithShellDex(apkMainProcessPath);
// 5. 复制壳的so文件,加密so文件
apk.copyNativeLibs(apkMainProcessPath); // 复制壳的so文件
apk.encryptSoFiles(apkMainProcessPath);
// 6. 构建apk
apk.buildApk(apkFile.getAbsolutePath(),apkMainProcessPath, FileUtils.getExecutablePath());
File apkMainProcessFile = new File(apkMainProcessPath);
if (apkMainProcessFile.exists()) {
FileUtils.deleteRecurse(apkMainProcessFile);
}
LogUtils.info("All done.");
}
public void protect() {
process(this);
}
1. 指令抽空
extractDexCode:调用DexUtils.extractAllMethods获取List<instruction> ret,最后将每个方法的字节码信息写入到assets/OoooooOooo
文件</instruction>
extractAllMethods:解析Dex文件的结构体,获取directMethods,virtualMethods,调用extractMethod来进行patch
extractMethod:一边用byteCode保存原字节码,一边用outRandomAccessFile.writeShort(0)写入nop
下面主要贴一下extractDexCode的源码好了
private void extractDexCode(String apkOutDir){
List<File> dexFiles = getDexFiles(apkOutDir);
Map<Integer,List<Instruction>> instructionMap = new HashMap<>();
String appNameNew = "OoooooOooo";
String dataOutputPath = getOutAssetsDir(apkOutDir).getAbsolutePath() + File.separator + appNameNew;
CountDownLatch countDownLatch = new CountDownLatch(dexFiles.size());
for(File dexFile : dexFiles) {
ThreadPool.getInstance().execute(() -> {
final int dexNo = getDexNumber(dexFile.getName());
if(dexNo < 0){
return;
}
String extractedDexName = dexFile.getName().endsWith(".dex") ? dexFile.getName().replaceAll("\\.dex$", "_extracted.dat") : "_extracted.dat";
File extractedDexFile = new File(dexFile.getParent(), extractedDexName);
List<Instruction> ret = DexUtils.extractAllMethods(dexFile, extractedDexFile, getPackageName(), isDumpCode());
instructionMap.put(dexNo,ret);
File dexFileRightHashes = new File(dexFile.getParent(),FileUtils.getNewFileSuffix(dexFile.getName(),"dat"));
DexUtils.writeHashes(extractedDexFile,dexFileRightHashes);
dexFile.delete();
extractedDexFile.delete();
dexFileRightHashes.renameTo(dexFile);
countDownLatch.countDown();
});
}
ThreadPool.getInstance().shutdown();
try {
countDownLatch.await();
}
catch (Exception ignored){
}
MultiDexCode multiDexCode = MultiDexCodeUtils.makeMultiDexCode(instructionMap);
MultiDexCodeUtils.writeMultiDexCode(dataOutputPath,multiDexCode);
}
2. AMF的处理
主要是保存了一下源ApplicationName和AppComponentFactory,后续壳加载的时候用
此外修改了程序的AMF.xml,替换为代理ApplicationName和代理AppComponentFactory
3. 压缩源dex到新的路径并删除旧的路径
源dex压缩存放到assets/i11111i111.zip
4. 合并壳和原dex
combineDexZipWithShellDex:将壳文件添加到dex,并修复size、sha1、checksum
5. 复制壳so,加密so文件
copyNativeLibs这里就没啥好说的,直接添加(shell-files/libs → assets/vwwwwwvwww)
encryptSoFiles这块对比于初始版本是新添加的功能:
- assets/vwwwwwvwww文件夹下存放的是壳的so文件,这里遍历so文件,然后对.bitcode节区进行RC4加密
- key为
ncWK&S5wbqU%IX6j
,在com\luoye\dpt\Const.java定义
6. 签名构建APK
zipalign,对zip进行对齐
对APK进行签名,assets/dpt.jks
- signApkDebug
-
signApk,调用command来实现
private static boolean signApk(String apkPath, String keyStorePath, String signedApkPath, String keyAlias, String storePassword, String KeyPassword) { ArrayList<String> commandList = new ArrayList<>(); commandList.add("sign"); commandList.add("--ks"); commandList.add(keyStorePath); commandList.add("--ks-key-alias"); commandList.add(keyAlias); commandList.add("--ks-pass"); commandList.add("pass:" + storePassword); commandList.add("--key-pass"); commandList.add("pass:" + KeyPassword); commandList.add("--out"); commandList.add(signedApkPath); commandList.add("--v1-signing-enabled"); commandList.add("true"); commandList.add("--v2-signing-enabled"); commandList.add("true"); commandList.add("--v3-signing-enabled"); commandList.add("true"); commandList.add(apkPath); int size = commandList.size(); String[] commandArray = new String[size]; commandArray = commandList.toArray(commandArray); try { ApkSignerTool.main(commandArray); } catch (Exception e) { e.printStackTrace(); return false; } return true; }
完成!
Shell模块分析
ProxyApplication-attachBaseContext重写
这里的逻辑也会初始版本发生了一些变化,直接分析当前版本的
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base); // 先调用父类的attachBaseContext
Log.d(TAG,"dpt attachBaseContext classloader = " + base.getClassLoader());
realApplicationName = FileUtils.readAppName(this);
if(!Global.sIsReplacedClassLoader) {
ApplicationInfo applicationInfo = base.getApplicationInfo();
if(applicationInfo == null) {
throw new NullPointerException("application info is null");
}
FileUtils.unzipLibs(applicationInfo.sourceDir,applicationInfo.dataDir);
JniBridge.loadShellLibs(applicationInfo.dataDir,applicationInfo.sourceDir);
Log.d(TAG,"ProxyApplication init");
JniBridge.ia();
ClassLoader targetClassLoader = base.getClassLoader();
JniBridge.cbde(targetClassLoader);
Global.sIsReplacedClassLoader = true;
}
}
JniBridge.loadShellLibs(applicationInfo.dataDir,applicationInfo.sourceDir)
- 加载shell的so文件,后面一小节将会分析so的初始化流程
- System.load(fullLibPath)
JniBridge.ia():init_app
- 通过jni调用so文件的ia函数(动调注册init_app),路径在\main\cpp\dpt.cpp
- load_apk:这里是将apk映射到内存中
- load_zip_by_mmap,打开zip后通过mmap映射到内存
- read_zip_file_entry
- 读取刚刚加载的apk,然后在内存中找到assets/OoooooOooo的地址,保存指针codeItemFilePtr
- readCodeItem
- MultiDexCode结构体解析出来获得各个dex的指令字节, 然后添加到dexMap中,dexMap是全局变量
- extractDexesInNeeded
- writeDexAchieve:找apk中的classes.dex写入到code_cache/i11111i111.zip
- unloadapk
DPT_ENCRYPT void init_app(JNIEnv *env, jclass __unused) {
DLOGD("init_app!");
clock_t start = clock();
void *apk_addr = nullptr;
size_t apk_size = 0;
load_apk(env,&apk_addr,&apk_size);
uint64_t entry_size = 0;
if(codeItemFilePtr == nullptr) {
read_zip_file_entry(apk_addr,apk_size,CODE_ITEM_NAME_IN_ZIP,&codeItemFilePtr,&entry_size);
}
else {
DLOGD("no need read codeitem from zip");
}
readCodeItem((uint8_t *)codeItemFilePtr,entry_size);
pthread_mutex_lock(&g_write_dexes_mutex);
extractDexesInNeeded(env,apk_addr,apk_size);
pthread_mutex_unlock(&g_write_dexes_mutex);
unload_apk(apk_addr,apk_size);
printTime("read apk data took =" , start);
}
JniBridge.cbde:combineDexElements,动态合并新的dex
- 合并targetClassLoader原来的DexElements和之前释放的code_cache/i11111i111.zip(此时dex还是空的)
- 合并dexElements目的是把我们新加载的dex放到dexElements数组开头,这样ClassLoader加载类时就会优先从我们的dex中查找。
DPT_ENCRYPT void combineDexElement(JNIEnv* env, jclass __unused, jobject targetClassLoader, const char* pathChs) {
jobjectArray extraDexElements = makePathElements(env,pathChs);
dalvik_system_BaseDexClassLoader targetBaseDexClassLoader(env,targetClassLoader);
jobject originDexPathListObj = targetBaseDexClassLoader.getPathList();
dalvik_system_DexPathList targetDexPathList(env,originDexPathListObj);
jobjectArray originDexElements = targetDexPathList.getDexElements();
jsize extraSize = env->GetArrayLength(extraDexElements);
jsize originSize = env->GetArrayLength(originDexElements);
dalvik_system_DexPathList::Element element(env, nullptr);
jclass ElementClass = element.getClass();
jobjectArray newDexElements = env->NewObjectArray(originSize + extraSize,ElementClass, nullptr);
for(int i = 0;i < originSize;i++) {
jobject elementObj = env->GetObjectArrayElement(originDexElements, i);
env->SetObjectArrayElement(newDexElements,i,elementObj);
}
for(int i = originSize;i < originSize + extraSize;i++) {
jobject elementObj = env->GetObjectArrayElement(extraDexElements, i - originSize);
env->SetObjectArrayElement(newDexElements,i,elementObj);
}
targetDexPathList.setDexElements(newDexElements);
DLOGD("combineDexElement success");
}
ProxyApplication-onCreate重写
先调用父类的onCreate,后面主要看replaceApplication,同样发生在native层
private void replaceApplication() {
if (Global.sNeedCalledApplication && !TextUtils.isEmpty(realApplicationName)) {
realApplication = (Application) JniBridge.ra(realApplicationName);
Log.d(TAG, "applicationExchange: " + realApplicationName+" realApplication="+realApplication.getClass().getName());
JniBridge.craa(getApplicationContext(), realApplicationName);
JniBridge.craoc(realApplicationName);
Global.sNeedCalledApplication = false;
}
}
JniBridge.ra:replaceApplication,主要实例化了一个application,执行replaceApplicationOnLoadedApk和replaceApplicationOnActivityThread来替换这个实例
-
replaceApplicationOnLoadedApk
- 获取原始程序的Application类并进行替换,替换涉及两处地方:
-
ActivityThread
(负责管理应用程序的生命周期和组件加载)中BoundApplication的appBindData(这里还是在为LoadedApk
的makeapplication做准备,而不是替换ActivityThread
的初始实例) -
LoadedApk
(APK文件在内存中的表示)中的ApplicationInfo
-
-
最后调用
loadedApk.makeApplication(JNI_FALSE,nullptr)
来初始化真实程序的applicationDPT_ENCRYPT void replaceApplicationOnLoadedApk(JNIEnv *env, jclass __unused,jobject realApplication) { android_app_ActivityThread activityThread(env); jobject mBoundApplicationObj = activityThread.getBoundApplication(); // 获取 BoundApplication 对象 android_app_ActivityThread::AppBindData appBindData(env,mBoundApplicationObj); jobject loadedApkObj = appBindData.getInfo(); android_app_LoadedApk loadedApk(env,loadedApkObj); // LoadedApk对象是APK文件在内存中的表示 //make it null loadedApk.setApplication(nullptr); // 以便可以替换为新的 Application 对象。 jobject mAllApplicationsObj = activityThread.getAllApplication(); java_util_ArrayList arrayList(env,mAllApplicationsObj); jobject removed = (jobject)arrayList.remove(0); // 移除原来的 Application 对象。 if(removed != nullptr){ DLOGD("replaceApplicationOnLoadedApk proxy application removed"); } jobject ApplicationInfoObj = loadedApk.getApplicationInfo(); // 获取 ApplicationInfo 对象。 android_content_pm_ApplicationInfo applicationInfo(env,ApplicationInfoObj); char applicationName[128] = {0}; getClassName(env,realApplication,applicationName, ARRAY_LENGTH(applicationName)); // 获取真实 Application 对象的类名 DLOGD("applicationName = %s",applicationName); char realApplicationNameChs[128] = {0}; parseClassName(applicationName,realApplicationNameChs); // 前面获取了类名了,现在解析类 jstring realApplicationName = env->NewStringUTF(realApplicationNameChs); auto realApplicationNameGlobal = (jstring)env->NewGlobalRef(realApplicationName); android_content_pm_ApplicationInfo appInfo(env,appBindData.getAppInfo()); //replace class name 替换类名 applicationInfo.setClassName(realApplicationNameGlobal); appInfo.setClassName(realApplicationNameGlobal); DLOGD("replaceApplicationOnLoadedApk begin makeApplication!"); // call make application loadedApk.makeApplication(JNI_FALSE,nullptr); DLOGD("replaceApplicationOnLoadedApk success!"); }
- 获取原始程序的Application类并进行替换,替换涉及两处地方:
-
replaceApplicationOnActivityThread
-
替换
ActivityThread
的初始Application
实例为realApplication
DPT_ENCRYPT void replaceApplicationOnActivityThread(JNIEnv *env,jclass __unused, jobject realApplication){ android_app_ActivityThread activityThread(env); activityThread.setInitialApplication(realApplication); DLOGD("replaceApplicationOnActivityThread success"); }
-
-
这里做个区分:
- 修改
AppBindData
中的ApplicationInfo
类名,是为了保证LoadedApk
在将来某一时刻需要重新实例化Application
时,能够使用新的Application
类。 - 调用
setInitialApplication
是为了立即替换ActivityThread
的Application
引用,保证当前应用主线程能够立即使用新的Application
实例。
- 修改
JniBridge.craa:callRealApplicationAttach
- 正常走原始程序的Application的attach(反射调用)
JniBridge.craoc:callRealApplicationOnCreate
- 正常走原始程序的Application的onCreate(反射调用)
so的初始化
壳的so文件在init_array中优先调用
//dpt.h
INIT_ARRAY_SECTION void init_dpt();
// dpt.cpp
void init_dpt() {
decrypt_bitcode();
DLOGI("init_dpt call!");
dpt_hook();
createAntiRiskProcess();
}
decrypt_bitcode
- 前面提到现在so文件开启了加密保护,下述JniBridge的一些关键函数都是定义在.bitcode节区的,因此是进行了RC4加密的,而它们在so文件加载的init_array时进行RC4的解密,密钥存到了.data节区中
dpt_hook
- 函数路径位于src\main\cpp\dpt_hook.cpp
-
hook libc.so的execve
字符串子串匹配dex2oat,禁用dex2oat
DPT_ENCRYPT int fake_execve(const char *pathname, char *const argv[], char *const envp[]) { BYTEHOOK_STACK_SCOPE(); DLOGW("execve hooked: %s", pathname); if (strstr(pathname, "dex2oat") != nullptr) { DLOGW("execve blocked: %s", pathname); errno = EACCES; return -1; } return BYTEHOOK_CALL_PREV(fake_execve, pathname, argv, envp); }
-
hook libc.so的mmap
添加写权限,以便后续修改dex
DPT_ENCRYPT void* fake_mmap(void* __addr, size_t __size, int __prot, int __flags, int __fd, off_t __offset){ BYTEHOOK_STACK_SCOPE(); int prot = __prot; int hasRead = (__prot & PROT_READ) == PROT_READ; int hasWrite = (__prot & PROT_WRITE) == PROT_WRITE; char fd_path[256] = {0}; dpt_readlink(__fd,fd_path, ARRAY_LENGTH(fd_path)); if(strstr(fd_path,"webview.vdex") != nullptr) { DLOGW("fake_mmap link path: %s, no need to change prot",fd_path); goto tail; } if(hasRead && !hasWrite) { prot = prot | PROT_WRITE; DLOGD("fake_mmap call fd = %d,size = %zu, prot = %d,flag = %d",__fd,__size, prot,__flags); } if(g_sdkLevel == 30){ if(strstr(fd_path,"base.vdex") != nullptr){ DLOGE("fake_mmap want to mmap base.vdex"); __flags = 0; } } tail: void *addr = BYTEHOOK_CALL_PREV(fake_mmap,__addr, __size, prot, __flags, __fd, __offset); return addr; }
-
hook DefineClass
- 这里通过DobbyHook库来对java层的代码进行hook
-
classloader在加载类的时候会调用defineClass,根据sdk版本选择合适的hook函数,如
DPT_ENCRYPT void *DefineClassV21(void* thiz, const char* descriptor, void* class_loader, const void* dex_file, const void* dex_class_def) { if(LIKELY(g_originDefineClassV21 != nullptr)) { patchClass(descriptor,dex_file,dex_class_def); return g_originDefineClassV21( thiz,descriptor,class_loader, dex_file, dex_class_def); } return nullptr; }
-
patchClass(指令回填的核心函数)
- 解析dex,构建结构体,主要是dex::ClassDataMethod directMethods[direct_methods_size]和dex::ClassDataMethod virtualMethods[virtual_methods_size]
- 然后调用patchMethod来对每个方法进行具体的回填
-
patchMethod
- 通过传入的参数dexIndex来找到映射到内存空间的dex,然后通过mprotect增加读写权限
-
获取原始字节码的偏移,然后通过memcpy直接回填
DPT_ENCRYPT void patchMethod(uint8_t *begin,__unused const char *location,uint32_t dexSize,int dexIndex,uint32_t methodIdx,uint32_t codeOff){ if(codeOff == 0){ // 代码偏移为0,不需要patch NLOG("[*] patchMethod dex: %d methodIndex: %d no need patch!",dexIndex,methodIdx); return; } auto *dexCodeItem = (dex::CodeItem *) (begin + codeOff); // 原始的dex codeItem的偏移 uint16_t firstDvmCode = *((uint16_t*)dexCodeItem->insns_); if(firstDvmCode != 0x0012 && firstDvmCode != 0x0016 && firstDvmCode != 0x000e){ NLOG("[*] this method has code no need to patch"); return; } auto dexIt = dexMap.find(dexIndex); if (LIKELY(dexIt != dexMap.end())) { auto dexMemIt = dexMemMap.find(dexIndex); if(UNLIKELY(dexMemIt == dexMemMap.end())){ change_dex_protective(begin,dexSize,dexIndex); } auto codeItemMap = dexIt->second; auto codeItemIt = codeItemMap->find(methodIdx); if (LIKELY(codeItemIt != codeItemMap->end())) { data::CodeItem* codeItem = codeItemIt->second; auto *realCodeItemPtr = (uint8_t *)(dexCodeItem->insns_); NLOG("[*] patchMethod codeItem patch, methodIndex = %d,insnsSize = %d >>> %p(0x%x)", codeItem->getMethodIdx(), codeItem->getInsnsSize(), realCodeItemPtr, (unsigned int)(realCodeItemPtr - begin)); memcpy(realCodeItemPtr,codeItem->getInsns(),codeItem->getInsnsSize()); } else{ NLOG("[*] patchMethod cannot find methodId: %d in codeitem map, dex index: %d(%s)",methodIdx,dexIndex,location); } } else{ DLOGW("[*] patchMethod cannot find dex: '%s' in dex map",location); } }
createAntiRiskProcess
DPT_ENCRYPT void createAntiRiskProcess() {
pid_t child = fork();
if(child < 0) {
DLOGW("%s fork fail!", __FUNCTION__);
detectFrida();
}
else if(child == 0) {
DLOGD("%s in child process", __FUNCTION__);
detectFrida();
doPtrace();
}
else {
DLOGD("%s in main process, child pid: %d", __FUNCTION__, child);
protectChildProcess(child);
detectFrida();
}
}
-
这里进行了两种反调试的操作,一个是检测Frida,另一个是常规的PTRACE_TRACEME,无论是子进程还是父进程都会有反调检测
-
detectFrida
-
创建反调试线程,具体有下面几种检测方法
- 读取进程的内存映射/proc/%d/maps,直接查
frida-agent
字符串 - 读取进程的线程/proc/%d/task,查字符串
pool-frida、gmain、gdbus、gum-js-loop
,如果线程匹配超过2个就crash-
pool-frida
:管理 Frida 内部的线程池,用于处理多线程任务和通信。 -
gmain
:GLib 主循环,处理事件循环和 Frida 的核心事件。 -
gdbus
:DBus 线程,处理与系统服务或其他进程的 DBus 消息通信。 -
gum-js-loop
:JavaScript 主循环,执行 Frida 注入的 JavaScript 代码和 hook 函数。
-
[[noreturn]] void *detectFridaOnThread(__unused void *args) { while (true) { int frida_so_count = find_in_maps(1,"frida-agent"); if(frida_so_count > 0) { DLOGD("detectFridaOnThread found frida so"); crash(); } int frida_thread_count = find_in_threads_list(4 ,"pool-frida" ,"gmain" ,"gdbus" ,"gum-js-loop"); if(frida_thread_count >= 2) { DLOGD("detectFridaOnThread found frida threads"); crash(); } sleep(10); } }
- 读取进程的内存映射/proc/%d/maps,直接查
-
-
doPtrace
void doPtrace() { __unused int ret = sys_ptrace(PTRACE_TRACEME,0,0,0); DLOGD("doPtrace result: %d",ret); }
-
ProxyComponentFactory重写
在系统需要创建组件实例时按需调用,通过代理的方式控制组件的实例化,优先使用目标 AppComponentFactory
创建组件,创建失败再用默认的
其他
hook时机的调整
回看22年的帖子,当时作者使用LoadMethod作为hook和指令回填的目标,现在已经换成DefineClass,原因在HowItWorks.md也有解释
ClassDef这个结构还有一个特点,它是dex文件的结构,也就是说dex文件格式不变,它一般就不会变。还有,DefineClass函数的参数会改变吗?目前来看从Android M到现在没有变过。所以使用它不用太担心随着Android版本的升级而导致字段偏移的变化,也就是兼容性较强。这就是为什么用DefineClass作为Hook点。
dpt之前就是使用的LoadMethod函数作为Hook点,在LoadMethod函数里面做CodeItem填充操作。但是后来发现,LoadMethod函数参数不太固定,随着Android版本的升级可能要不断适配,而且每个函数都要填充,会影响一定的性能。
总结
processor模块
- 从原app的各个dex,每个dex的method中抽取字节码存入
OoooooOooo
文件中,并用nop填充 - 修改入口
- 合并shell的dex(shell-files/dex/classes.dex)与填充nop后的dex
- 复制壳的so文件,并对.bitcode节区进行rc4加密
- 最终签名并构建apk
Shell模块
ProxyApplication-attachBaseContext重写
- jniBridge.ia初始化一些结构体和变量以便后续用
- 加载壳so并初始化
- 解密.bitcode节区
- 设置hook
- 禁用dex2oat
- 给dex添加写权限
- 调用defineClass时回填指令(关键参数是dex_file和dex_class_def)
- 设置反调试
- 将现在的apk内的classes.dex缓存到code_cache/i11111i111.zip,然后动态添加到现在的classloader内
ProxyApplication-onCreate重写
- 替换当前的application为原始程序的application
- 执行原始程序application的初始化流程,如attach、onCreate
- 完成原始app的运行
Refs
-
-
-
-
-
-