【Android安全-dpt-shell源码详细解析(v1.11.3)】此文章归类为:Android安全。
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()进行抽取
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);} |
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);} |
主要是保存了一下源ApplicationName和AppComponentFactory,后续壳加载的时候用
此外修改了程序的AMF.xml,替换为代理ApplicationName和代理AppComponentFactory
源dex压缩存放到assets/i11111i111.zip
combineDexZipWithShellDex:将壳文件添加到dex,并修复size、sha1、checksum
copyNativeLibs这里就没啥好说的,直接添加(shell-files/libs → assets/vwwwwwvwww)
encryptSoFiles这块对比于初始版本是新添加的功能:
ncWK&S5wbqU%IX6j,在com\luoye\dpt\Const.java定义zipalign,对zip进行对齐
对APK进行签名,assets/dpt.jks
signApkDebug
signApk,调用command来实现
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 | 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;} |
完成!
这里的逻辑也会初始版本发生了一些变化,直接分析当前版本的
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)
JniBridge.ia():init_app
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
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");} |
先调用父类的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
ActivityThread(负责管理应用程序的生命周期和组件加载)中BoundApplication的appBindData(这里还是在为LoadedApk的makeapplication做准备,而不是替换ActivityThread的初始实例)LoadedApk(APK文件在内存中的表示)中的ApplicationInfoloadedApk.makeApplication(JNI_FALSE,nullptr) 来初始化真实程序的application1 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 | 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!");} |
replaceApplicationOnActivityThread
ActivityThread 的初始 Application 实例为 realApplication1 2 3 4 5 | 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
JniBridge.craoc:callRealApplicationOnCreate
壳的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
dpt_hook
函数路径位于src\main\cpp\dpt_hook.cpp
hook libc.so的execve
字符串子串匹配dex2oat,禁用dex2oat
1 2 3 4 5 6 7 8 9 10 | 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
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 | 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函数,如
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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**(指令回填的核心函数)**
patchMethod
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 | 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
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(); }} |
detectFrida
frida-agent字符串pool-frida、gmain、gdbus、gum-js-loop,如果线程匹配超过2个就crashpool-frida:管理 Frida 内部的线程池,用于处理多线程任务和通信。gmain:GLib 主循环,处理事件循环和 Frida 的核心事件。gdbus:DBus 线程,处理与系统服务或其他进程的 DBus 消息通信。gum-js-loop:JavaScript 主循环,执行 Frida 注入的 JavaScript 代码和 hook 函数。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | [[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); }} |
doPtrace
1 2 3 4 | void doPtrace() { __unused int ret = sys_ptrace(PTRACE_TRACEME,0,0,0); DLOGD("doPtrace result: %d",ret);} |
在系统需要创建组件实例时按需调用,通过代理的方式控制组件的实例化,优先使用目标 AppComponentFactory 创建组件,创建失败再用默认的
回看22年的帖子,当时作者使用LoadMethod作为hook和指令回填的目标,现在已经换成DefineClass,原因在HowItWorks.md也有解释
ClassDef这个结构还有一个特点,它是dex文件的结构,也就是说dex文件格式不变,它一般就不会变。还有,DefineClass函数的参数会改变吗?目前来看从Android M到现在没有变过。所以使用它不用太担心随着Android版本的升级而导致字段偏移的变化,也就是兼容性较强。这就是为什么用DefineClass作为Hook点。
dpt之前就是使用的LoadMethod函数作为Hook点,在LoadMethod函数里面做CodeItem填充操作。但是后来发现,LoadMethod函数参数不太固定,随着Android版本的升级可能要不断适配,而且每个函数都要填充,会影响一定的性能。
OoooooOooo文件中,并用nop填充分享一个自己做的函数抽取壳 - 吾爱破解 - 52pojie.cn
https://github.com/luoyesiqiu/dpt-shell
更多【Android安全-dpt-shell源码详细解析(v1.11.3)】相关视频教程:www.yxfzedu.com