dpt-shell源码详细解析(v1.11.3)
前言
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()进行抽取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | 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文件
extractAllMethods:解析Dex文件的结构体,获取directMethods,virtualMethods,调用extractMethod来进行patch
extractMethod:一边用byteCode保存原字节码,一边用outRandomAccessFile.writeShort(0)写入nop
下面主要贴一下extractDexCode的源码好了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | 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来实现
12345678910111213141516171819202122232425262728293031323334353637privatestaticbooleansignApk(String apkPath, String keyStorePath, String signedApkPath,String keyAlias,String storePassword,String KeyPassword) {ArrayList<String> commandList =newArrayList<>();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);intsize = commandList.size();String[] commandArray =newString[size];commandArray = commandList.toArray(commandArray);try{ApkSignerTool.main(commandArray);}catch(Exception e) {e.printStackTrace();returnfalse;}returntrue;}
完成!
Shell模块分析
ProxyApplication-attachBaseContext重写
这里的逻辑也会初始版本发生了一些变化,直接分析当前版本的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | @Overrideprotected 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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中查找。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | 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层
1 2 3 4 5 6 7 8 9 10 | 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)来初始化真实程序的application
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748DPT_ENCRYPTvoidreplaceApplicationOnLoadedApk(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 nullloadedApk.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);charapplicationName[128] = {0};getClassName(env,realApplication,applicationName, ARRAY_LENGTH(applicationName));// 获取真实 Application 对象的类名DLOGD("applicationName = %s",applicationName);charrealApplicationNameChs[128] = {0};parseClassName(applicationName,realApplicationNameChs);// 前面获取了类名了,现在解析类jstring realApplicationName = env->NewStringUTF(realApplicationNameChs);autorealApplicationNameGlobal = (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 applicationloadedApk.makeApplication(JNI_FALSE,nullptr);DLOGD("replaceApplicationOnLoadedApk success!");}- 获取原始程序的Application类并进行替换,替换涉及两处地方:
replaceApplicationOnActivityThread
- 替换
ActivityThread的初始Application实例为realApplication
12345DPT_ENCRYPTvoidreplaceApplicationOnActivityThread(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中优先调用
1 2 3 4 5 6 7 8 9 10 11 12 | //dpt.h INIT_ARRAY_SECTION void init_dpt();// dpt.cppvoid 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
12345678910DPT_ENCRYPTintfake_execve(constchar*pathname,char*constargv[],char*constenvp[]) {BYTEHOOK_STACK_SCOPE();DLOGW("execve hooked: %s", pathname);if(strstr(pathname,"dex2oat") != nullptr) {DLOGW("execve blocked: %s", pathname);errno= EACCES;return-1;}returnBYTEHOOK_CALL_PREV(fake_execve, pathname, argv, envp);}hook libc.so的mmap
添加写权限,以便后续修改dex
123456789101112131415161718192021222324252627282930DPT_ENCRYPTvoid* fake_mmap(void* __addr,size_t__size,int__prot,int__flags,int__fd, off_t __offset){BYTEHOOK_STACK_SCOPE();intprot = __prot;inthasRead = (__prot & PROT_READ) == PROT_READ;inthasWrite = (__prot & PROT_WRITE) == PROT_WRITE;charfd_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);gototail;}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);returnaddr;}hook DefineClass
这里通过DobbyHook库来对java层的代码进行hook
classloader在加载类的时候会调用defineClass,根据sdk版本选择合适的hook函数,如
12345678910111213DPT_ENCRYPTvoid*DefineClassV21(void* thiz,constchar* descriptor,void* class_loader,constvoid* dex_file,constvoid* dex_class_def) {if(LIKELY(g_originDefineClassV21 != nullptr)) {patchClass(descriptor,dex_file,dex_class_def);returng_originDefineClassV21( thiz,descriptor,class_loader, dex_file, dex_class_def);}returnnullptr;}patchClass**(指令回填的核心函数)**
- 解析dex,构建结构体,主要是dex::ClassDataMethod directMethods[direct_methods_size]和dex::ClassDataMethod virtualMethods[virtual_methods_size]
- 然后调用patchMethod来对每个方法进行具体的回填
patchMethod
- 通过传入的参数dexIndex来找到映射到内存空间的dex,然后通过mprotect增加读写权限
- 获取原始字节码的偏移,然后通过memcpy直接回填
12345678910111213141516171819202122232425262728293031323334353637383940414243DPT_ENCRYPTvoidpatchMethod(uint8_t *begin,__unusedconstchar*location,uint32_t dexSize,intdexIndex,uint32_t methodIdx,uint32_t codeOff){if(codeOff == 0){// 代码偏移为0,不需要patchNLOG("[*] 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;}autodexIt = dexMap.find(dexIndex);if(LIKELY(dexIt != dexMap.end())) {autodexMemIt = dexMemMap.find(dexIndex);if(UNLIKELY(dexMemIt == dexMemMap.end())){change_dex_protective(begin,dexSize,dexIndex);}autocodeItemMap = dexIt->second;autocodeItemIt = 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,(unsignedint)(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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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个就crashpool-frida:管理 Frida 内部的线程池,用于处理多线程任务和通信。gmain:GLib 主循环,处理事件循环和 Frida 的核心事件。gdbus:DBus 线程,处理与系统服务或其他进程的 DBus 消息通信。gum-js-loop:JavaScript 主循环,执行 Frida 注入的 JavaScript 代码和 hook 函数。
- 读取进程的内存映射/proc/%d/maps,直接查
1234567891011121314151617181920[[noreturn]]void*detectFridaOnThread(__unusedvoid*args) {while(true) {intfrida_so_count = find_in_maps(1,"frida-agent");if(frida_so_count > 0) {DLOGD("detectFridaOnThread found frida so");crash();}intfrida_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);}}- 创建反调试线程,具体有下面几种检测方法
doPtrace
1234voiddoPtrace() {__unusedintret = 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的运行