-
-
[原创]dpt-shell源码详细解析(v1.11.3)
-
发表于: 2024-12-10 22:35 34511
-
dpt-shell自22年以来进行了不少的调整,本文试对v1.11.3版的dpt-shell进行源码分析,补充作者在HowItWorks中未撰写出来的部分并作积累。
简而言之,dpt-shell可以分为两个模块,一个是Processeor模块,用于对原app进行指令抽空并构建新app;另一个是shell模块,用于在app运行时回填指令,顺利执行app的代码。以下是对这两个模块的详细分析
入口点在src\main\java\com\luoye\dpt\Dpt.java,解析用户的运行参数后进入apk.protect()进行抽取
extractDexCode:调用DexUtils.extractAllMethods获取List<Instruction> ret,最后将每个方法的字节码信息写入到assets/OoooooOooo文件
extractAllMethods:解析Dex文件的结构体,获取directMethods,virtualMethods,调用extractMethod来进行patch
extractMethod:一边用byteCode保存原字节码,一边用outRandomAccessFile.writeShort(0)写入nop
下面主要贴一下extractDexCode的源码好了
主要是保存了一下源ApplicationName和AppComponentFactory,后续壳加载的时候用
此外修改了程序的AMF.xml,替换为代理ApplicationName和代理AppComponentFactory
源dex压缩存放到assets/i11111i111.zip
combineDexZipWithShellDex:将壳文件添加到dex,并修复size、sha1、checksum
copyNativeLibs这里就没啥好说的,直接添加(shell-files/libs → assets/vwwwwwvwww)
encryptSoFiles这块对比于初始版本是新添加的功能:
zipalign,对zip进行对齐
对APK进行签名,assets/dpt.jks
signApkDebug
signApk,调用command来实现
完成!
这里的逻辑也会初始版本发生了一些变化,直接分析当前版本的
JniBridge.loadShellLibs(applicationInfo.dataDir,applicationInfo.sourceDir)
JniBridge.ia():init_app
JniBridge.cbde:combineDexElements,动态合并新的dex
先调用父类的onCreate,后面主要看replaceApplication,同样发生在native层
JniBridge.ra:replaceApplication,主要实例化了一个application,执行replaceApplicationOnLoadedApk和replaceApplicationOnActivityThread来替换这个实例
replaceApplicationOnLoadedApk
replaceApplicationOnActivityThread
这里做个区分:
JniBridge.craa:callRealApplicationAttach
JniBridge.craoc:callRealApplicationOnCreate
壳的so文件在init_array中优先调用
decrypt_bitcode
dpt_hook
函数路径位于src\main\cpp\dpt_hook.cpp
hook libc.so的execve
字符串子串匹配dex2oat,禁用dex2oat
hook libc.so的mmap
添加写权限,以便后续修改dex
hook DefineClass
这里通过DobbyHook库来对java层的代码进行hook
classloader在加载类的时候会调用defineClass,根据sdk版本选择合适的hook函数,如
patchClass**(指令回填的核心函数)**
patchMethod
createAntiRiskProcess
detectFrida
doPtrace
在系统需要创建组件实例时按需调用,通过代理的方式控制组件的实例化,优先使用目标 AppComponentFactory 创建组件,创建失败再用默认的
回看22年的帖子,当时作者使用LoadMethod作为hook和指令回填的目标,现在已经换成DefineClass,原因在HowItWorks.md也有解释
ClassDef这个结构还有一个特点,它是dex文件的结构,也就是说dex文件格式不变,它一般就不会变。还有,DefineClass函数的参数会改变吗?目前来看从Android M到现在没有变过。所以使用它不用太担心随着Android版本的升级而导致字段偏移的变化,也就是兼容性较强。这就是为什么用DefineClass作为Hook点。
dpt之前就是使用的LoadMethod函数作为Hook点,在LoadMethod函数里面做CodeItem填充操作。但是后来发现,LoadMethod函数参数不太固定,随着Android版本的升级可能要不断适配,而且每个函数都要填充,会影响一定的性能。
分享一个自己做的函数抽取壳 - 吾爱破解 - 52pojie.cn
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);}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);}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);}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);}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;}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;}@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; }}@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; }}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);}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);}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");}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");}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; }}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; }}DPT_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!");}DPT_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);