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()进行抽取

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) 来初始化真实程序的application

      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 实例为 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 是为了立即替换 ActivityThreadApplication 引用,保证当前应用主线程能够立即使用新的 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);
          }
        }
        
    • 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模块

  1. 从原app的各个dex,每个dex的method中抽取字节码存入OoooooOooo文件中,并用nop填充
  2. 修改入口
  3. 合并shell的dex(shell-files/dex/classes.dex)与填充nop后的dex
  4. 复制壳的so文件,并对.bitcode节区进行rc4加密
  5. 最终签名并构建apk

Shell模块

ProxyApplication-attachBaseContext重写

  1. jniBridge.ia初始化一些结构体和变量以便后续用
  2. 加载壳so并初始化
    1. 解密.bitcode节区
    2. 设置hook
      1. 禁用dex2oat
      2. 给dex添加写权限
      3. 调用defineClass时回填指令(关键参数是dex_file和dex_class_def)
    3. 设置反调试
  3. 将现在的apk内的classes.dex缓存到code_cache/i11111i111.zip,然后动态添加到现在的classloader内

ProxyApplication-onCreate重写

  1. 替换当前的application为原始程序的application
  2. 执行原始程序application的初始化流程,如attach、onCreate
  3. 完成原始app的运行

Refs

分享一个自己做的函数抽取壳 - 吾爱破解 - 52pojie.cn

https://github.com/luoyesiqiu/dpt-shell

0 条评论
某人
表情
可输入 255